ccqa 1.10.1 → 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 +92 -3
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -1327,6 +1327,61 @@ function missingNativeBinaryMessage(pkg) {
|
|
|
1327
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.`;
|
|
1328
1328
|
}
|
|
1329
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
|
|
1330
1385
|
//#region src/claude/invoke.ts
|
|
1331
1386
|
function resolveModel(explicit) {
|
|
1332
1387
|
if (explicit) return explicit;
|
|
@@ -1498,6 +1553,7 @@ async function invokeClaudeStreaming(options, onEvent) {
|
|
|
1498
1553
|
errorDetail = err instanceof Error ? err.message : String(err);
|
|
1499
1554
|
if (!result) result = errorDetail;
|
|
1500
1555
|
}
|
|
1556
|
+
tallyInvocation(cost);
|
|
1501
1557
|
return {
|
|
1502
1558
|
result,
|
|
1503
1559
|
isError,
|
|
@@ -15059,12 +15115,36 @@ const generateCommand = addHubOptions(addProfileOption(addLanguageOption(new Com
|
|
|
15059
15115
|
});
|
|
15060
15116
|
}));
|
|
15061
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
|
+
}
|
|
15132
|
+
//#endregion
|
|
15062
15133
|
//#region src/cli/record.ts
|
|
15063
15134
|
const VALIDATION_MODES = ["lenient", "strict"];
|
|
15064
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) => {
|
|
15065
15136
|
if (VALIDATION_MODES.includes(raw)) return raw;
|
|
15066
15137
|
throw new Error(`--validation-mode must be one of ${VALIDATION_MODES.join(" | ")}`);
|
|
15067
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) {
|
|
15068
15148
|
const { featureName, specName } = parseSpecPath(specPath);
|
|
15069
15149
|
const language = opts.language ?? "auto";
|
|
15070
15150
|
if (opts.skipTrace && opts.skipCodegen) {
|
|
@@ -15151,7 +15231,7 @@ const recordCommand = addHubOptions(addProfileOption(addLanguageOption(new Comma
|
|
|
15151
15231
|
...language ? { language } : {},
|
|
15152
15232
|
...opts.model ? { model: opts.model } : {}
|
|
15153
15233
|
});
|
|
15154
|
-
}
|
|
15234
|
+
}
|
|
15155
15235
|
/**
|
|
15156
15236
|
* Compact summary of the trace pass for the record agent-prompt refresh.
|
|
15157
15237
|
* Steps are reconstructed from the trace's status-line protocol (STEP_START
|
|
@@ -15467,6 +15547,9 @@ function driftResultsToReport(results, meta) {
|
|
|
15467
15547
|
//#region src/cli/drift.ts
|
|
15468
15548
|
const DEFAULT_CONCURRENCY = 3;
|
|
15469
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) {
|
|
15470
15553
|
const format = parseFormat$1(opts.format);
|
|
15471
15554
|
const threshold = parseSeverity(opts.severity);
|
|
15472
15555
|
const concurrency = parseConcurrency(opts.concurrency);
|
|
@@ -15518,8 +15601,9 @@ const driftCommand = addLanguageOption(new Command("drift").argument("[feature/s
|
|
|
15518
15601
|
format,
|
|
15519
15602
|
baseRef
|
|
15520
15603
|
});
|
|
15604
|
+
reportCost();
|
|
15521
15605
|
process.exit(determineExitCode(results, threshold));
|
|
15522
|
-
}
|
|
15606
|
+
}
|
|
15523
15607
|
/**
|
|
15524
15608
|
* Push a finished drift audit to a ccqa hub as a `kind: "drift"` run, so it
|
|
15525
15609
|
* shows up alongside `ccqa run` runs in the hub UI. Best-effort: a missing
|
|
@@ -15574,6 +15658,7 @@ async function pushDriftResults(args, resolveHub = resolveHubClient) {
|
|
|
15574
15658
|
}
|
|
15575
15659
|
}
|
|
15576
15660
|
function exitWithNoSpecs(format, message) {
|
|
15661
|
+
reportCost();
|
|
15577
15662
|
if (format === "json") process.stdout.write(`${JSON.stringify({ specs: [] }, null, 2)}\n`);
|
|
15578
15663
|
else if (format === "text") info(message);
|
|
15579
15664
|
process.exit(0);
|
|
@@ -15639,6 +15724,9 @@ const initCommand = new Command("init").description("Create the .ccqa/ spec skel
|
|
|
15639
15724
|
//#region src/cli/select-specs.ts
|
|
15640
15725
|
const DEFAULT_HEAD = "HEAD";
|
|
15641
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) {
|
|
15642
15730
|
const format = parseFormat(opts.format);
|
|
15643
15731
|
const cwd = resolveCwd(opts.cwd);
|
|
15644
15732
|
const head = opts.head ?? DEFAULT_HEAD;
|
|
@@ -15684,8 +15772,9 @@ const selectSpecsCommand = new Command("select-specs").description("Decide which
|
|
|
15684
15772
|
...opts.model ? { model: opts.model } : {}
|
|
15685
15773
|
});
|
|
15686
15774
|
process.stdout.write(format === "json" ? `${JSON.stringify(report, null, 2)}\n` : renderText(report));
|
|
15775
|
+
reportCost();
|
|
15687
15776
|
process.exit(0);
|
|
15688
|
-
}
|
|
15777
|
+
}
|
|
15689
15778
|
const VERDICT_ORDER = [
|
|
15690
15779
|
"needed",
|
|
15691
15780
|
"unknown",
|
package/dist/package.json
CHANGED