ccqa 1.10.0 → 1.10.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
@@ -39,6 +39,26 @@ const EVIDENCE_SUBDIR = "evidence";
39
39
  /** Per-spec run artifacts for external (runCommand) targets: `artifacts/<feature>__<spec>/`. */
40
40
  const ARTIFACTS_SUBDIR = "artifacts";
41
41
  //#endregion
42
+ //#region src/run/errors.ts
43
+ /**
44
+ * Usage error (bad flag combination, broken profile, failed `git diff`, …)
45
+ * thrown by the `run` pipeline and the helpers it calls, e.g.
46
+ * `collectChangedSpecs`. `executeRun` never calls `process.exit`, so each
47
+ * host maps this itself: the CLI action catches it and exits with
48
+ * `exitCode`; the hub runner records it as a run-level error.
49
+ */
50
+ var RunUsageError = class extends Error {
51
+ exitCode = 2;
52
+ constructor(message) {
53
+ super(message);
54
+ this.name = "RunUsageError";
55
+ }
56
+ };
57
+ /** An error's message, whatever was thrown. Shared so the run modules report failures alike. */
58
+ function errMessage(err) {
59
+ return err instanceof Error ? err.message : String(err);
60
+ }
61
+ //#endregion
42
62
  //#region src/runtime/env-vars.ts
43
63
  const ENV_VAR_RE = /\$\{([A-Z_][A-Z0-9_]*)\}|\$([A-Z_][A-Z0-9_]*)/g;
44
64
  const ANY_VAR_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g;
@@ -399,7 +419,7 @@ function parseSpecPath(specPath) {
399
419
  featureName: parts[0],
400
420
  specName: parts[1]
401
421
  };
402
- throw new Error(`Invalid spec path: "${specPath}". Expected "<feature>/<spec>" or "features/<feature>/test-cases/<spec>".`);
422
+ throw new RunUsageError(`Invalid spec path: "${specPath}". Expected "<feature>/<spec>" or "features/<feature>/test-cases/<spec>".`);
403
423
  }
404
424
  function getFeatureDir(featureName, cwd) {
405
425
  return join(getCcqaDir(cwd), "features", featureName);
@@ -4669,6 +4689,34 @@ function isWithin(rootAbs, abs) {
4669
4689
  return abs === rootAbs || abs.startsWith(rootAbs + sep);
4670
4690
  }
4671
4691
  //#endregion
4692
+ //#region src/cli/usage-errors.ts
4693
+ /**
4694
+ * Turn a `RunUsageError` into `[error] <message>` plus its exit code, for a
4695
+ * commander action.
4696
+ *
4697
+ * The helpers shared across commands — `resolveAnalysisBase`,
4698
+ * `collectChangedSpecs` — signal a bad invocation this way, so every command
4699
+ * that calls one needs the same boundary. Without it the process dies on an
4700
+ * unhandled rejection and prints a stack trace: `bin/ccqa.ts` installs no
4701
+ * global handler, so there is nowhere else for it to land.
4702
+ *
4703
+ * Lives here rather than beside `RunUsageError` because `src/run/errors.ts` is
4704
+ * deliberately dependency-free, and this needs the logger.
4705
+ */
4706
+ function withUsageErrors(fn) {
4707
+ return async (...args) => {
4708
+ try {
4709
+ await fn(...args);
4710
+ } catch (err) {
4711
+ if (err instanceof RunUsageError) {
4712
+ error(err.message);
4713
+ process.exit(err.exitCode);
4714
+ }
4715
+ throw err;
4716
+ }
4717
+ };
4718
+ }
4719
+ //#endregion
4672
4720
  //#region src/runtime/profile-env.ts
4673
4721
  /**
4674
4722
  * Profile env vars are hub-sourced (pulled via `ccqa hub`) and merged into
@@ -5121,7 +5169,7 @@ const DraftNamingSchema = z.object({
5121
5169
  //#endregion
5122
5170
  //#region src/cli/draft.ts
5123
5171
  const CATEGORY_LABEL = DRAFT_CATEGORY_LABEL;
5124
- const draftCommand = addLanguageOption(new Command("draft").argument("[feature/spec]", "Optional spec path (e.g. tasks/create-and-complete). If omitted, Claude proposes one from your intent.").description("Interactively draft and refine a spec.yaml with Claude Code").option("--instruction <text>", "Non-interactive single-shot instruction (skips the interactive loop)").option("--apply", "Auto-apply each generated patch without [y/N] confirmation", false)).action(async (specPath, opts) => {
5172
+ const draftCommand = addLanguageOption(new Command("draft").argument("[feature/spec]", "Optional spec path (e.g. tasks/create-and-complete). If omitted, Claude proposes one from your intent.").description("Interactively draft and refine a spec.yaml with Claude Code").option("--instruction <text>", "Non-interactive single-shot instruction (skips the interactive loop)").option("--apply", "Auto-apply each generated patch without [y/N] confirmation", false)).action(withUsageErrors(async (specPath, opts) => {
5125
5173
  await ensureCcqaDir();
5126
5174
  let featureName;
5127
5175
  let specName;
@@ -5134,7 +5182,7 @@ const draftCommand = addLanguageOption(new Command("draft").argument("[feature/s
5134
5182
  prefilledIntent = intent;
5135
5183
  }
5136
5184
  await runDraft(featureName, specName, opts, prefilledIntent);
5137
- });
5185
+ }));
5138
5186
  async function runDraft(featureName, specName, opts, prefilledIntent) {
5139
5187
  header("draft", `${featureName}/${specName}`);
5140
5188
  const ja = useJapanesePrompts(opts.language);
@@ -6426,26 +6474,6 @@ function createDiffProvider(args) {
6426
6474
  } };
6427
6475
  }
6428
6476
  //#endregion
6429
- //#region src/run/errors.ts
6430
- /**
6431
- * Usage error (bad flag combination, broken profile, failed `git diff`, …)
6432
- * thrown by the `run` pipeline and the helpers it calls, e.g.
6433
- * `collectChangedSpecs`. `executeRun` never calls `process.exit`, so each
6434
- * host maps this itself: the CLI action catches it and exits with
6435
- * `exitCode`; the hub runner records it as a run-level error.
6436
- */
6437
- var RunUsageError = class extends Error {
6438
- exitCode = 2;
6439
- constructor(message) {
6440
- super(message);
6441
- this.name = "RunUsageError";
6442
- }
6443
- };
6444
- /** An error's message, whatever was thrown. Shared so the run modules report failures alike. */
6445
- function errMessage(err) {
6446
- return err instanceof Error ? err.message : String(err);
6447
- }
6448
- //#endregion
6449
6477
  //#region src/run/git-context.ts
6450
6478
  /** The `--failure-analysis` value that selects per-spec hub-ledger baselines. */
6451
6479
  const LAST_GREEN = "last-green";
@@ -8214,6 +8242,14 @@ function isCcqaPath(path) {
8214
8242
  return /(?:^|\/)\.ccqa\//.test(path);
8215
8243
  }
8216
8244
  /**
8245
+ * A malformed reply costs the whole selection, so it is worth one more call
8246
+ * before giving up. `ccqa drift` retries per spec for the same reason; this
8247
+ * call carries every undecided spec at once, so the blast radius is larger,
8248
+ * not smaller. Observed in practice: three runs over one commit produced a
8249
+ * parse failure, a clean answer, and a different clean answer.
8250
+ */
8251
+ const MAX_ATTEMPTS = 2;
8252
+ /**
8217
8253
  * One model call for the whole undecided set, not one per spec: the specs are
8218
8254
  * judged against the same diff, and seeing them together is what lets the
8219
8255
  * model tell them apart.
@@ -8224,32 +8260,44 @@ function isCcqaPath(path) {
8224
8260
  */
8225
8261
  async function judgeWithModel(input) {
8226
8262
  const { productChanges, undecided, cwd, base, head, model } = input;
8227
- const { result, isError } = await invokeClaudeStreaming({
8228
- prompt: buildSelectPrompt({
8229
- changed: productChanges,
8230
- specs: undecided,
8231
- base,
8232
- head
8233
- }),
8234
- systemPrompt: buildSelectSystemPrompt(),
8235
- allowedTools: [
8236
- "Read",
8237
- "Grep",
8238
- "Glob"
8239
- ],
8240
- silenceBashLog: true,
8241
- cwd,
8242
- ...model ? { model } : {}
8243
- }, (_msg) => {});
8244
- if (isError) return abandonSelection(undecided, "the selection model returned an error");
8245
- const json = extractJsonBlock(result);
8246
- if (!json) return abandonSelection(undecided, "the selection model returned no JSON block");
8247
8263
  let parsed;
8248
- try {
8249
- parsed = JSON.parse(json);
8250
- } catch (e) {
8251
- return abandonSelection(undecided, `the selection model's JSON did not parse: ${e.message}`);
8264
+ let lastError = "";
8265
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
8266
+ const { result, isError } = await invokeClaudeStreaming({
8267
+ prompt: buildSelectPrompt({
8268
+ changed: productChanges,
8269
+ specs: undecided,
8270
+ base,
8271
+ head
8272
+ }),
8273
+ systemPrompt: buildSelectSystemPrompt(),
8274
+ allowedTools: [
8275
+ "Read",
8276
+ "Grep",
8277
+ "Glob"
8278
+ ],
8279
+ silenceBashLog: true,
8280
+ cwd,
8281
+ ...model ? { model } : {}
8282
+ }, (_msg) => {});
8283
+ if (isError) {
8284
+ lastError = "the selection model returned an error";
8285
+ continue;
8286
+ }
8287
+ const json = extractJsonBlock(result);
8288
+ if (!json) {
8289
+ lastError = "the selection model returned no JSON block";
8290
+ continue;
8291
+ }
8292
+ try {
8293
+ parsed = JSON.parse(json);
8294
+ lastError = "";
8295
+ break;
8296
+ } catch (e) {
8297
+ lastError = `the selection model's JSON did not parse: ${e.message}`;
8298
+ }
8252
8299
  }
8300
+ if (lastError) return abandonSelection(undecided, `${lastError} (${MAX_ATTEMPTS} attempts)`);
8253
8301
  const changedPaths = new Set(productChanges.map((f) => f.path));
8254
8302
  const byUndecidedKey = new Map(undecided.map((s) => [specKey(s), s]));
8255
8303
  const answers = /* @__PURE__ */ new Map();
@@ -14954,7 +15002,7 @@ async function confirmOverwrite(path) {
14954
15002
  rl.close();
14955
15003
  }
14956
15004
  }
14957
- const generateCommand = addHubOptions(addProfileOption(addLanguageOption(new Command("generate").argument("<feature/spec>", "Spec id in '<feature>/<spec>' form (resolves to .ccqa/features/<feature>/test-cases/<spec>/)").description("Generate test code from a spec via its target plugin. Recording-backed targets compile the existing ir.json (run `ccqa record` first); spec-input targets generate directly from the spec.").option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").option("--target <id>", "Generate through this target instead of the spec's own — e.g. emit a Playwright spec from an agent-browser recording. The spec's `target:` stays the default for `ccqa run`.").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("--max-retries <n>", "Maximum number of auto-fix retries", "3").option("--force", "Overwrite previously generated test code without warning").option("--no-snapshot", "Don't pin AGENT_BROWSER_SESSION / capture page snapshots after a failure (debug toggle)").option("--update-agent-prompt", "After generation, ask Claude to refresh the target's \"<target>.agent\" learning prompt on the hub from a summary of the run. LLM-generating targets (playwright, runn) only; requires a hub 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(async (specPath, opts) => {
15005
+ const generateCommand = addHubOptions(addProfileOption(addLanguageOption(new Command("generate").argument("<feature/spec>", "Spec id in '<feature>/<spec>' form (resolves to .ccqa/features/<feature>/test-cases/<spec>/)").description("Generate test code from a spec via its target plugin. Recording-backed targets compile the existing ir.json (run `ccqa record` first); spec-input targets generate directly from the spec.").option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").option("--target <id>", "Generate through this target instead of the spec's own — e.g. emit a Playwright spec from an agent-browser recording. The spec's `target:` stays the default for `ccqa run`.").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("--max-retries <n>", "Maximum number of auto-fix retries", "3").option("--force", "Overwrite previously generated test code without warning").option("--no-snapshot", "Don't pin AGENT_BROWSER_SESSION / capture page snapshots after a failure (debug toggle)").option("--update-agent-prompt", "After generation, ask Claude to refresh the target's \"<target>.agent\" learning prompt on the hub from a summary of the run. LLM-generating targets (playwright, runn) only; requires a hub 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) => {
14958
15006
  const { featureName, specName } = parseSpecPath(specPath);
14959
15007
  const language = opts.language ?? "auto";
14960
15008
  const cwd = resolveCwd(opts.cwd);
@@ -15009,14 +15057,14 @@ const generateCommand = addHubOptions(addProfileOption(addLanguageOption(new Com
15009
15057
  ...language ? { language } : {},
15010
15058
  ...opts.model ? { model: opts.model } : {}
15011
15059
  });
15012
- });
15060
+ }));
15013
15061
  //#endregion
15014
15062
  //#region src/cli/record.ts
15015
15063
  const VALIDATION_MODES = ["lenient", "strict"];
15016
15064
  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.").option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").option("--validation-mode <mode>", "Post-trace validation behaviour: 'lenient' (default) tags failing actions; 'strict' drops them.", (raw) => {
15017
15065
  if (VALIDATION_MODES.includes(raw)) return raw;
15018
15066
  throw new Error(`--validation-mode must be one of ${VALIDATION_MODES.join(" | ")}`);
15019
- }, "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("--max-retries <n>", "Maximum number of auto-fix retries", "3").option("--force", "Overwrite an existing test.spec.ts without warning").option("--no-snapshot", "Don't pin AGENT_BROWSER_SESSION / capture page snapshots after a failure (debug toggle)").option("--skip-trace", "Skip the trace step and run codegen against an existing ir.json").option("--skip-codegen", "Run only the trace step (do not generate test.spec.ts)").option("--update-agent-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.").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(async (specPath, opts) => {
15067
+ }, "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("--max-retries <n>", "Maximum number of auto-fix retries", "3").option("--force", "Overwrite an existing test.spec.ts without warning").option("--no-snapshot", "Don't pin AGENT_BROWSER_SESSION / capture page snapshots after a failure (debug toggle)").option("--skip-trace", "Skip the trace step and run codegen against an existing ir.json").option("--skip-codegen", "Run only the trace step (do not generate test.spec.ts)").option("--update-agent-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.").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) => {
15020
15068
  const { featureName, specName } = parseSpecPath(specPath);
15021
15069
  const language = opts.language ?? "auto";
15022
15070
  if (opts.skipTrace && opts.skipCodegen) {
@@ -15103,7 +15151,7 @@ const recordCommand = addHubOptions(addProfileOption(addLanguageOption(new Comma
15103
15151
  ...language ? { language } : {},
15104
15152
  ...opts.model ? { model: opts.model } : {}
15105
15153
  });
15106
- });
15154
+ }));
15107
15155
  /**
15108
15156
  * Compact summary of the trace pass for the record agent-prompt refresh.
15109
15157
  * Steps are reconstructed from the trace's status-line protocol (STEP_START
@@ -15416,34 +15464,6 @@ function driftResultsToReport(results, meta) {
15416
15464
  };
15417
15465
  }
15418
15466
  //#endregion
15419
- //#region src/cli/usage-errors.ts
15420
- /**
15421
- * Turn a `RunUsageError` into `[error] <message>` plus its exit code, for a
15422
- * commander action.
15423
- *
15424
- * The helpers shared across commands — `resolveAnalysisBase`,
15425
- * `collectChangedSpecs` — signal a bad invocation this way, so every command
15426
- * that calls one needs the same boundary. Without it the process dies on an
15427
- * unhandled rejection and prints a stack trace: `bin/ccqa.ts` installs no
15428
- * global handler, so there is nowhere else for it to land.
15429
- *
15430
- * Lives here rather than beside `RunUsageError` because `src/run/errors.ts` is
15431
- * deliberately dependency-free, and this needs the logger.
15432
- */
15433
- function withUsageErrors(fn) {
15434
- return async (...args) => {
15435
- try {
15436
- await fn(...args);
15437
- } catch (err) {
15438
- if (err instanceof RunUsageError) {
15439
- error(err.message);
15440
- process.exit(err.exitCode);
15441
- }
15442
- throw err;
15443
- }
15444
- };
15445
- }
15446
- //#endregion
15447
15467
  //#region src/cli/drift.ts
15448
15468
  const DEFAULT_CONCURRENCY = 3;
15449
15469
  const driftCommand = addLanguageOption(new Command("drift").argument("[feature/spec]", "Optional spec id. If omitted, every spec under .ccqa/features/ is checked.").description("Standalone spec ↔ codebase static audit. Use for PR checks where the browser isn't run. For run-time audit with a structured report, see `ccqa run --report`.").option("--format <fmt>", "Output format: text | json | github", "text").option("--severity <level>", "Exit non-zero on this severity or higher: warn | error", "error").option("--concurrency <n>", `Parallel spec checks (default: ${DEFAULT_CONCURRENCY})`).option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").option("--cwd <path>", "Working directory used as both the .ccqa root and the codebase Claude reads. Useful for monorepos. Defaults to process.cwd().").option("--changed", "Restrict drift checks to the specs a change reaches, decided by `ccqa select-specs` against --base (or, in CI, $GITHUB_BASE_REF). Costs one model call; specs it cannot decide are checked rather than skipped.").option("--base <ref>", "Base ref to diff against when --changed is set. Defaults to $GITHUB_BASE_REF (CI pull_request runs); required otherwise.").option("--push", "Push the drift result to a ccqa hub as a run (kind: drift).").option("--project <name>", "Logical project name for the pushed run. Defaults to the current directory's name.").option(...hubUrlOption).option(...hubTokenOption).option(...hubHeaderOption)).action(withUsageErrors(async (specPath, opts) => {
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.10.0",
3
+ "version": "1.10.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.10.0",
3
+ "version": "1.10.1",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {