ccqa 1.10.0 → 1.11.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 +190 -81
- package/dist/package.json +1 -1
- package/package.json +1 -1
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
|
|
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);
|
|
@@ -1307,6 +1327,61 @@ function missingNativeBinaryMessage(pkg) {
|
|
|
1307
1327
|
return `${pkg} is not installed. The Claude Agent SDK needs it to start Claude on this platform, so every Claude-backed command (run in live mode, drift, diagnose) will fail. It ships as an optional dependency of the SDK, which a lockfile can drop silently: reinstall without omitting optional dependencies, or add it to your project as a direct dependency pinned to the same version as @anthropic-ai/claude-agent-sdk.`;
|
|
1308
1328
|
}
|
|
1309
1329
|
//#endregion
|
|
1330
|
+
//#region src/claude/cost-tally.ts
|
|
1331
|
+
/**
|
|
1332
|
+
* Sum every Claude invocation made inside a scope.
|
|
1333
|
+
*
|
|
1334
|
+
* A command like `record` calls Claude several times — the browser trace, the
|
|
1335
|
+
* codegen cleanup, one diagnosis per auto-fix retry — and the caller wants one
|
|
1336
|
+
* number for the whole command. Threading a cost out of each of those return
|
|
1337
|
+
* types would touch every layer in between, so the tally is scoped instead:
|
|
1338
|
+
* `invokeClaudeStreaming` adds to whichever scope is active, and nothing
|
|
1339
|
+
* between the two has to know.
|
|
1340
|
+
*
|
|
1341
|
+
* Scoped rather than module-global because commands run specs concurrently
|
|
1342
|
+
* (`drift` uses a pool). Two scopes must not fold into each other.
|
|
1343
|
+
*/
|
|
1344
|
+
const tallyStore = new AsyncLocalStorage();
|
|
1345
|
+
/** Record one invocation against the active scope. No-op outside one. */
|
|
1346
|
+
function tallyInvocation(cost) {
|
|
1347
|
+
tallyStore.getStore()?.push(cost);
|
|
1348
|
+
}
|
|
1349
|
+
/** Run `fn` with a fresh tally. Read the total from inside with `readCostTally`. */
|
|
1350
|
+
async function withCostTally(fn) {
|
|
1351
|
+
return tallyStore.run([], fn);
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* The active scope's total so far, or null outside one.
|
|
1355
|
+
*
|
|
1356
|
+
* Read rather than pushed at the caller because commands end in
|
|
1357
|
+
* `process.exit`, which never reaches a `finally`. Each one reports the
|
|
1358
|
+
* total itself, at the point it knows it is done.
|
|
1359
|
+
*
|
|
1360
|
+
* Fields stay `null` when no invocation reported them, so a caller can tell
|
|
1361
|
+
* "nothing was billed" from "the SDK didn't say" (mock runs, SDK errors).
|
|
1362
|
+
*/
|
|
1363
|
+
function readCostTally() {
|
|
1364
|
+
const collected = tallyStore.getStore();
|
|
1365
|
+
return collected === void 0 ? null : sum(collected);
|
|
1366
|
+
}
|
|
1367
|
+
function sum(costs) {
|
|
1368
|
+
const add = (pick) => {
|
|
1369
|
+
const present = costs.map(pick).filter((v) => v !== null);
|
|
1370
|
+
return present.length === 0 ? null : present.reduce((a, b) => a + b, 0);
|
|
1371
|
+
};
|
|
1372
|
+
return {
|
|
1373
|
+
totalCostUsd: add((c) => c.totalCostUsd),
|
|
1374
|
+
durationMs: add((c) => c.durationMs),
|
|
1375
|
+
durationApiMs: add((c) => c.durationApiMs),
|
|
1376
|
+
numTurns: add((c) => c.numTurns),
|
|
1377
|
+
inputTokens: add((c) => c.inputTokens),
|
|
1378
|
+
cacheCreationInputTokens: add((c) => c.cacheCreationInputTokens),
|
|
1379
|
+
cacheReadInputTokens: add((c) => c.cacheReadInputTokens),
|
|
1380
|
+
outputTokens: add((c) => c.outputTokens),
|
|
1381
|
+
models: [...new Set(costs.flatMap((c) => c.models))]
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
//#endregion
|
|
1310
1385
|
//#region src/claude/invoke.ts
|
|
1311
1386
|
function resolveModel(explicit) {
|
|
1312
1387
|
if (explicit) return explicit;
|
|
@@ -1478,6 +1553,7 @@ async function invokeClaudeStreaming(options, onEvent) {
|
|
|
1478
1553
|
errorDetail = err instanceof Error ? err.message : String(err);
|
|
1479
1554
|
if (!result) result = errorDetail;
|
|
1480
1555
|
}
|
|
1556
|
+
tallyInvocation(cost);
|
|
1481
1557
|
return {
|
|
1482
1558
|
result,
|
|
1483
1559
|
isError,
|
|
@@ -4669,6 +4745,34 @@ function isWithin(rootAbs, abs) {
|
|
|
4669
4745
|
return abs === rootAbs || abs.startsWith(rootAbs + sep);
|
|
4670
4746
|
}
|
|
4671
4747
|
//#endregion
|
|
4748
|
+
//#region src/cli/usage-errors.ts
|
|
4749
|
+
/**
|
|
4750
|
+
* Turn a `RunUsageError` into `[error] <message>` plus its exit code, for a
|
|
4751
|
+
* commander action.
|
|
4752
|
+
*
|
|
4753
|
+
* The helpers shared across commands — `resolveAnalysisBase`,
|
|
4754
|
+
* `collectChangedSpecs` — signal a bad invocation this way, so every command
|
|
4755
|
+
* that calls one needs the same boundary. Without it the process dies on an
|
|
4756
|
+
* unhandled rejection and prints a stack trace: `bin/ccqa.ts` installs no
|
|
4757
|
+
* global handler, so there is nowhere else for it to land.
|
|
4758
|
+
*
|
|
4759
|
+
* Lives here rather than beside `RunUsageError` because `src/run/errors.ts` is
|
|
4760
|
+
* deliberately dependency-free, and this needs the logger.
|
|
4761
|
+
*/
|
|
4762
|
+
function withUsageErrors(fn) {
|
|
4763
|
+
return async (...args) => {
|
|
4764
|
+
try {
|
|
4765
|
+
await fn(...args);
|
|
4766
|
+
} catch (err) {
|
|
4767
|
+
if (err instanceof RunUsageError) {
|
|
4768
|
+
error(err.message);
|
|
4769
|
+
process.exit(err.exitCode);
|
|
4770
|
+
}
|
|
4771
|
+
throw err;
|
|
4772
|
+
}
|
|
4773
|
+
};
|
|
4774
|
+
}
|
|
4775
|
+
//#endregion
|
|
4672
4776
|
//#region src/runtime/profile-env.ts
|
|
4673
4777
|
/**
|
|
4674
4778
|
* Profile env vars are hub-sourced (pulled via `ccqa hub`) and merged into
|
|
@@ -5121,7 +5225,7 @@ const DraftNamingSchema = z.object({
|
|
|
5121
5225
|
//#endregion
|
|
5122
5226
|
//#region src/cli/draft.ts
|
|
5123
5227
|
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) => {
|
|
5228
|
+
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
5229
|
await ensureCcqaDir();
|
|
5126
5230
|
let featureName;
|
|
5127
5231
|
let specName;
|
|
@@ -5134,7 +5238,7 @@ const draftCommand = addLanguageOption(new Command("draft").argument("[feature/s
|
|
|
5134
5238
|
prefilledIntent = intent;
|
|
5135
5239
|
}
|
|
5136
5240
|
await runDraft(featureName, specName, opts, prefilledIntent);
|
|
5137
|
-
});
|
|
5241
|
+
}));
|
|
5138
5242
|
async function runDraft(featureName, specName, opts, prefilledIntent) {
|
|
5139
5243
|
header("draft", `${featureName}/${specName}`);
|
|
5140
5244
|
const ja = useJapanesePrompts(opts.language);
|
|
@@ -6426,26 +6530,6 @@ function createDiffProvider(args) {
|
|
|
6426
6530
|
} };
|
|
6427
6531
|
}
|
|
6428
6532
|
//#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
6533
|
//#region src/run/git-context.ts
|
|
6450
6534
|
/** The `--failure-analysis` value that selects per-spec hub-ledger baselines. */
|
|
6451
6535
|
const LAST_GREEN = "last-green";
|
|
@@ -8214,6 +8298,14 @@ function isCcqaPath(path) {
|
|
|
8214
8298
|
return /(?:^|\/)\.ccqa\//.test(path);
|
|
8215
8299
|
}
|
|
8216
8300
|
/**
|
|
8301
|
+
* A malformed reply costs the whole selection, so it is worth one more call
|
|
8302
|
+
* before giving up. `ccqa drift` retries per spec for the same reason; this
|
|
8303
|
+
* call carries every undecided spec at once, so the blast radius is larger,
|
|
8304
|
+
* not smaller. Observed in practice: three runs over one commit produced a
|
|
8305
|
+
* parse failure, a clean answer, and a different clean answer.
|
|
8306
|
+
*/
|
|
8307
|
+
const MAX_ATTEMPTS = 2;
|
|
8308
|
+
/**
|
|
8217
8309
|
* One model call for the whole undecided set, not one per spec: the specs are
|
|
8218
8310
|
* judged against the same diff, and seeing them together is what lets the
|
|
8219
8311
|
* model tell them apart.
|
|
@@ -8224,32 +8316,44 @@ function isCcqaPath(path) {
|
|
|
8224
8316
|
*/
|
|
8225
8317
|
async function judgeWithModel(input) {
|
|
8226
8318
|
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
8319
|
let parsed;
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
|
|
8320
|
+
let lastError = "";
|
|
8321
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
8322
|
+
const { result, isError } = await invokeClaudeStreaming({
|
|
8323
|
+
prompt: buildSelectPrompt({
|
|
8324
|
+
changed: productChanges,
|
|
8325
|
+
specs: undecided,
|
|
8326
|
+
base,
|
|
8327
|
+
head
|
|
8328
|
+
}),
|
|
8329
|
+
systemPrompt: buildSelectSystemPrompt(),
|
|
8330
|
+
allowedTools: [
|
|
8331
|
+
"Read",
|
|
8332
|
+
"Grep",
|
|
8333
|
+
"Glob"
|
|
8334
|
+
],
|
|
8335
|
+
silenceBashLog: true,
|
|
8336
|
+
cwd,
|
|
8337
|
+
...model ? { model } : {}
|
|
8338
|
+
}, (_msg) => {});
|
|
8339
|
+
if (isError) {
|
|
8340
|
+
lastError = "the selection model returned an error";
|
|
8341
|
+
continue;
|
|
8342
|
+
}
|
|
8343
|
+
const json = extractJsonBlock(result);
|
|
8344
|
+
if (!json) {
|
|
8345
|
+
lastError = "the selection model returned no JSON block";
|
|
8346
|
+
continue;
|
|
8347
|
+
}
|
|
8348
|
+
try {
|
|
8349
|
+
parsed = JSON.parse(json);
|
|
8350
|
+
lastError = "";
|
|
8351
|
+
break;
|
|
8352
|
+
} catch (e) {
|
|
8353
|
+
lastError = `the selection model's JSON did not parse: ${e.message}`;
|
|
8354
|
+
}
|
|
8252
8355
|
}
|
|
8356
|
+
if (lastError) return abandonSelection(undecided, `${lastError} (${MAX_ATTEMPTS} attempts)`);
|
|
8253
8357
|
const changedPaths = new Set(productChanges.map((f) => f.path));
|
|
8254
8358
|
const byUndecidedKey = new Map(undecided.map((s) => [specKey(s), s]));
|
|
8255
8359
|
const answers = /* @__PURE__ */ new Map();
|
|
@@ -14954,7 +15058,7 @@ async function confirmOverwrite(path) {
|
|
|
14954
15058
|
rl.close();
|
|
14955
15059
|
}
|
|
14956
15060
|
}
|
|
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) => {
|
|
15061
|
+
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
15062
|
const { featureName, specName } = parseSpecPath(specPath);
|
|
14959
15063
|
const language = opts.language ?? "auto";
|
|
14960
15064
|
const cwd = resolveCwd(opts.cwd);
|
|
@@ -15009,14 +15113,38 @@ const generateCommand = addHubOptions(addProfileOption(addLanguageOption(new Com
|
|
|
15009
15113
|
...language ? { language } : {},
|
|
15010
15114
|
...opts.model ? { model: opts.model } : {}
|
|
15011
15115
|
});
|
|
15012
|
-
});
|
|
15116
|
+
}));
|
|
15117
|
+
//#endregion
|
|
15118
|
+
//#region src/cli/cost-line.ts
|
|
15119
|
+
/**
|
|
15120
|
+
* Write what this command spent on Claude to stderr.
|
|
15121
|
+
*
|
|
15122
|
+
* stderr rather than stdout because `drift --format json` and `select-specs`
|
|
15123
|
+
* put machine-readable output on stdout, and a cost line mixed into it breaks
|
|
15124
|
+
* the consumer. Cost is diagnostics about the command, not its output.
|
|
15125
|
+
*/
|
|
15126
|
+
function reportCost() {
|
|
15127
|
+
const cost = readCostTally();
|
|
15128
|
+
if (cost === null) return;
|
|
15129
|
+
const summary = formatLiveCost(cost, { compact: false });
|
|
15130
|
+
if (summary) process.stderr.write(`[cost] ${summary}\n`);
|
|
15131
|
+
}
|
|
15013
15132
|
//#endregion
|
|
15014
15133
|
//#region src/cli/record.ts
|
|
15015
15134
|
const VALIDATION_MODES = ["lenient", "strict"];
|
|
15016
15135
|
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
15136
|
if (VALIDATION_MODES.includes(raw)) return raw;
|
|
15018
15137
|
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) => {
|
|
15138
|
+
}, "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) => {
|
|
15139
|
+
await withCostTally(async () => {
|
|
15140
|
+
try {
|
|
15141
|
+
await runRecord(specPath, opts);
|
|
15142
|
+
} finally {
|
|
15143
|
+
reportCost();
|
|
15144
|
+
}
|
|
15145
|
+
});
|
|
15146
|
+
}));
|
|
15147
|
+
async function runRecord(specPath, opts) {
|
|
15020
15148
|
const { featureName, specName } = parseSpecPath(specPath);
|
|
15021
15149
|
const language = opts.language ?? "auto";
|
|
15022
15150
|
if (opts.skipTrace && opts.skipCodegen) {
|
|
@@ -15103,7 +15231,7 @@ const recordCommand = addHubOptions(addProfileOption(addLanguageOption(new Comma
|
|
|
15103
15231
|
...language ? { language } : {},
|
|
15104
15232
|
...opts.model ? { model: opts.model } : {}
|
|
15105
15233
|
});
|
|
15106
|
-
}
|
|
15234
|
+
}
|
|
15107
15235
|
/**
|
|
15108
15236
|
* Compact summary of the trace pass for the record agent-prompt refresh.
|
|
15109
15237
|
* Steps are reconstructed from the trace's status-line protocol (STEP_START
|
|
@@ -15416,37 +15544,12 @@ function driftResultsToReport(results, meta) {
|
|
|
15416
15544
|
};
|
|
15417
15545
|
}
|
|
15418
15546
|
//#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
15547
|
//#region src/cli/drift.ts
|
|
15448
15548
|
const DEFAULT_CONCURRENCY = 3;
|
|
15449
15549
|
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) => {
|
|
15550
|
+
await withCostTally(() => runDrift(specPath, opts));
|
|
15551
|
+
}));
|
|
15552
|
+
async function runDrift(specPath, opts) {
|
|
15450
15553
|
const format = parseFormat$1(opts.format);
|
|
15451
15554
|
const threshold = parseSeverity(opts.severity);
|
|
15452
15555
|
const concurrency = parseConcurrency(opts.concurrency);
|
|
@@ -15498,8 +15601,9 @@ const driftCommand = addLanguageOption(new Command("drift").argument("[feature/s
|
|
|
15498
15601
|
format,
|
|
15499
15602
|
baseRef
|
|
15500
15603
|
});
|
|
15604
|
+
reportCost();
|
|
15501
15605
|
process.exit(determineExitCode(results, threshold));
|
|
15502
|
-
}
|
|
15606
|
+
}
|
|
15503
15607
|
/**
|
|
15504
15608
|
* Push a finished drift audit to a ccqa hub as a `kind: "drift"` run, so it
|
|
15505
15609
|
* shows up alongside `ccqa run` runs in the hub UI. Best-effort: a missing
|
|
@@ -15554,6 +15658,7 @@ async function pushDriftResults(args, resolveHub = resolveHubClient) {
|
|
|
15554
15658
|
}
|
|
15555
15659
|
}
|
|
15556
15660
|
function exitWithNoSpecs(format, message) {
|
|
15661
|
+
reportCost();
|
|
15557
15662
|
if (format === "json") process.stdout.write(`${JSON.stringify({ specs: [] }, null, 2)}\n`);
|
|
15558
15663
|
else if (format === "text") info(message);
|
|
15559
15664
|
process.exit(0);
|
|
@@ -15619,6 +15724,9 @@ const initCommand = new Command("init").description("Create the .ccqa/ spec skel
|
|
|
15619
15724
|
//#region src/cli/select-specs.ts
|
|
15620
15725
|
const DEFAULT_HEAD = "HEAD";
|
|
15621
15726
|
const selectSpecsCommand = new Command("select-specs").description("Decide which specs a range of commits reaches. Reads the diff and the spec inventory and returns one verdict per spec: needed | notNeeded | unknown.").requiredOption("--base <ref>", "Commit the range starts at — typically what is currently deployed, or the previous commit on the branch.").option("--head <ref>", `Commit the range ends at (default: ${DEFAULT_HEAD})`).option("--cwd <path>", "Working directory used as both the .ccqa root and the codebase Claude reads. Changes outside it are reported but never attributed to a spec. Defaults to process.cwd().").option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").option("--format <fmt>", "Output format: text | json", "text").action(async (opts) => {
|
|
15727
|
+
await withCostTally(() => runSelectSpecs(opts));
|
|
15728
|
+
});
|
|
15729
|
+
async function runSelectSpecs(opts) {
|
|
15622
15730
|
const format = parseFormat(opts.format);
|
|
15623
15731
|
const cwd = resolveCwd(opts.cwd);
|
|
15624
15732
|
const head = opts.head ?? DEFAULT_HEAD;
|
|
@@ -15664,8 +15772,9 @@ const selectSpecsCommand = new Command("select-specs").description("Decide which
|
|
|
15664
15772
|
...opts.model ? { model: opts.model } : {}
|
|
15665
15773
|
});
|
|
15666
15774
|
process.stdout.write(format === "json" ? `${JSON.stringify(report, null, 2)}\n` : renderText(report));
|
|
15775
|
+
reportCost();
|
|
15667
15776
|
process.exit(0);
|
|
15668
|
-
}
|
|
15777
|
+
}
|
|
15669
15778
|
const VERDICT_ORDER = [
|
|
15670
15779
|
"needed",
|
|
15671
15780
|
"unknown",
|
package/dist/package.json
CHANGED