ccqa 1.29.0 → 1.31.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 +83 -27
- package/dist/hub-client/index.d.mts +7 -2
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -871,10 +871,12 @@ function cloudProviderEnabled() {
|
|
|
871
871
|
/**
|
|
872
872
|
* Probe whether the host has any credential the Anthropic SDK can pick up:
|
|
873
873
|
* 1. ANTHROPIC_API_KEY env var (CI / scripted use)
|
|
874
|
-
* 2.
|
|
874
|
+
* 2. CLAUDE_CODE_OAUTH_TOKEN env var (a long-lived subscription token from
|
|
875
|
+
* `claude setup-token`, the headless-CI counterpart of a login)
|
|
876
|
+
* 3. CLAUDE_CODE_USE_BEDROCK / CLAUDE_CODE_USE_VERTEX (cloud-provider
|
|
875
877
|
* endpoints authenticated by the cloud SDK's credential chain)
|
|
876
|
-
*
|
|
877
|
-
*
|
|
878
|
+
* 4. ~/.claude/.credentials.json (Claude Code login, file-based platforms)
|
|
879
|
+
* 5. macOS Keychain item "Claude Code-credentials" (Claude Code login on
|
|
878
880
|
* darwin stores the OAuth credentials in the Keychain, not on disk)
|
|
879
881
|
*
|
|
880
882
|
* Claude-driven hooks are opt-in, so the caller only consults this after the
|
|
@@ -882,14 +884,16 @@ function cloudProviderEnabled() {
|
|
|
882
884
|
* that surfaces as "analysis skipped".
|
|
883
885
|
*/
|
|
884
886
|
function driftAuthAvailable() {
|
|
885
|
-
const key
|
|
886
|
-
|
|
887
|
+
for (const key of ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"]) {
|
|
888
|
+
const value = process.env[key];
|
|
889
|
+
if (typeof value === "string" && value.length > 0) return { ok: true };
|
|
890
|
+
}
|
|
887
891
|
if (cloudProviderEnabled()) return { ok: true };
|
|
888
892
|
if (existsSync(join(homedir(), ".claude", ".credentials.json"))) return { ok: true };
|
|
889
893
|
if (process.platform === "darwin" && keychainHasClaudeCredentials()) return { ok: true };
|
|
890
894
|
return {
|
|
891
895
|
ok: false,
|
|
892
|
-
reason: "no ANTHROPIC_API_KEY / Bedrock or Vertex env / claude login"
|
|
896
|
+
reason: "no ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN / Bedrock or Vertex env / claude login"
|
|
893
897
|
};
|
|
894
898
|
}
|
|
895
899
|
/**
|
|
@@ -1509,18 +1513,27 @@ function resolveModel(explicit) {
|
|
|
1509
1513
|
* - `ANTHROPIC_AUTH_TOKEN` — sent as `Authorization: Bearer <token>`.
|
|
1510
1514
|
* - `ANTHROPIC_API_KEY` — API key, when used instead of a token.
|
|
1511
1515
|
* - `ANTHROPIC_CUSTOM_HEADERS` — extra request headers.
|
|
1516
|
+
* - `CLAUDE_CODE_OAUTH_TOKEN` — long-lived subscription token from
|
|
1517
|
+
* `claude setup-token`, the headless-CI counterpart of a login.
|
|
1512
1518
|
*/
|
|
1513
1519
|
const ENDPOINT_ENV_KEYS = [
|
|
1514
1520
|
"ANTHROPIC_BASE_URL",
|
|
1515
1521
|
"ANTHROPIC_AUTH_TOKEN",
|
|
1516
1522
|
"ANTHROPIC_API_KEY",
|
|
1517
|
-
"ANTHROPIC_CUSTOM_HEADERS"
|
|
1523
|
+
"ANTHROPIC_CUSTOM_HEADERS",
|
|
1524
|
+
"CLAUDE_CODE_OAUTH_TOKEN"
|
|
1518
1525
|
];
|
|
1519
1526
|
/**
|
|
1520
1527
|
* Collects the endpoint/auth variables set in the current process environment
|
|
1521
1528
|
* so they can be forwarded, verbatim, to every Claude Code invocation. Returns
|
|
1522
1529
|
* only the keys that are actually set (non-empty), so unset variables never
|
|
1523
1530
|
* override the SDK's own defaults.
|
|
1531
|
+
*
|
|
1532
|
+
* When both credentials are present the OAuth token wins and the API key is
|
|
1533
|
+
* not forwarded. Left to the CLI the API key would win, which makes "switch a
|
|
1534
|
+
* CI job to the subscription token" require unwiring the key everywhere; with
|
|
1535
|
+
* the precedence here, adding the one variable is the whole switch, and
|
|
1536
|
+
* removing it is the whole rollback.
|
|
1524
1537
|
*/
|
|
1525
1538
|
function resolveEndpointEnv() {
|
|
1526
1539
|
const endpointEnv = {};
|
|
@@ -1528,6 +1541,7 @@ function resolveEndpointEnv() {
|
|
|
1528
1541
|
const value = process.env[key];
|
|
1529
1542
|
if (value && value.length > 0) endpointEnv[key] = value;
|
|
1530
1543
|
}
|
|
1544
|
+
if (endpointEnv["CLAUDE_CODE_OAUTH_TOKEN"]) delete endpointEnv["ANTHROPIC_API_KEY"];
|
|
1531
1545
|
return endpointEnv;
|
|
1532
1546
|
}
|
|
1533
1547
|
/**
|
|
@@ -7523,6 +7537,7 @@ z.object({
|
|
|
7523
7537
|
const SpecDriftEntrySchema = z.object({
|
|
7524
7538
|
label: DriftLabelSchema.nullable(),
|
|
7525
7539
|
surface: DriftSurfaceSchema.optional(),
|
|
7540
|
+
subDiagnosis: DriftSubDiagnosisSchema.optional(),
|
|
7526
7541
|
specChangeKind: SpecChangeKindSchema.optional(),
|
|
7527
7542
|
confidence: z.number().optional(),
|
|
7528
7543
|
headline: z.string().optional(),
|
|
@@ -13830,8 +13845,12 @@ function createRunTeardown() {
|
|
|
13830
13845
|
* A command must install at most one: a second handler that also exits would
|
|
13831
13846
|
* race this one and could terminate mid-finalizer. A second signal while
|
|
13832
13847
|
* tearing down hard-exits immediately rather than waiting.
|
|
13848
|
+
*
|
|
13849
|
+
* `onSignal` runs synchronously before the teardown starts, so a finalizer
|
|
13850
|
+
* can already see which signal is ending the process (e.g. `ccqa record`
|
|
13851
|
+
* seals its hub run with a "terminated by signal" note).
|
|
13833
13852
|
*/
|
|
13834
|
-
function installTeardownSignalHandlers(teardown) {
|
|
13853
|
+
function installTeardownSignalHandlers(teardown, onSignal) {
|
|
13835
13854
|
let handling = false;
|
|
13836
13855
|
const handler = (sig) => {
|
|
13837
13856
|
const code = sig === "SIGINT" ? 130 : 143;
|
|
@@ -13840,6 +13859,7 @@ function installTeardownSignalHandlers(teardown) {
|
|
|
13840
13859
|
return;
|
|
13841
13860
|
}
|
|
13842
13861
|
handling = true;
|
|
13862
|
+
onSignal?.(sig);
|
|
13843
13863
|
teardown.run().finally(() => process.exit(code));
|
|
13844
13864
|
};
|
|
13845
13865
|
const onInt = () => handler("SIGINT");
|
|
@@ -14004,6 +14024,16 @@ function generateSessionName() {
|
|
|
14004
14024
|
*/
|
|
14005
14025
|
function buildTraceSystemPrompt(input) {
|
|
14006
14026
|
const sessionName = input.sessionName ?? generateSessionName();
|
|
14027
|
+
const callerGuidance = input.instruction ? `## Caller Guidance
|
|
14028
|
+
|
|
14029
|
+
The caller provided extra guidance for this recording — for example, a drift
|
|
14030
|
+
audit's finding about what a previous recording of this spec got wrong. Treat
|
|
14031
|
+
it as advice on what to avoid or verify while recording; the spec's steps
|
|
14032
|
+
above remain the contract for what to do.
|
|
14033
|
+
|
|
14034
|
+
${input.instruction}
|
|
14035
|
+
|
|
14036
|
+
` : "";
|
|
14007
14037
|
const stepsText = input.steps.map((step) => `### ${step.id} [${step.source}]
|
|
14008
14038
|
- **Instruction**: ${step.instruction}
|
|
14009
14039
|
- **Expected**: ${step.expected}`).join("\n\n");
|
|
@@ -14391,7 +14421,7 @@ RUN_COMPLETED|passed|<summary>
|
|
|
14391
14421
|
RUN_COMPLETED|failed|<summary>
|
|
14392
14422
|
\`\`\`
|
|
14393
14423
|
|
|
14394
|
-
## Start
|
|
14424
|
+
${callerGuidance}## Start
|
|
14395
14425
|
|
|
14396
14426
|
Begin by clearing cookies, then proceed straight to the first step's instruction.
|
|
14397
14427
|
|
|
@@ -14867,7 +14897,8 @@ async function runTrace(featureName, specName, model, validationMode = "lenient"
|
|
|
14867
14897
|
const baseSystemPrompt = buildTraceSystemPrompt({
|
|
14868
14898
|
title: spec.title,
|
|
14869
14899
|
steps: expanded,
|
|
14870
|
-
sessionName
|
|
14900
|
+
sessionName,
|
|
14901
|
+
...opts.instruction ? { instruction: opts.instruction } : {}
|
|
14871
14902
|
});
|
|
14872
14903
|
const promptBundle = await loadPromptBundleFromHub(opts.hubContext ?? null, "record");
|
|
14873
14904
|
if (promptBundle !== null) meta("prompt", promptBundle.loaded.join(" + "));
|
|
@@ -14878,7 +14909,7 @@ async function runTrace(featureName, specName, model, validationMode = "lenient"
|
|
|
14878
14909
|
const statusLines = [];
|
|
14879
14910
|
let overallStatus = "passed";
|
|
14880
14911
|
const traceActions = [];
|
|
14881
|
-
const stepTracker = createStepTracker();
|
|
14912
|
+
const stepTracker = createStepTracker(opts.onStep);
|
|
14882
14913
|
const withStepId = (action, stepId) => {
|
|
14883
14914
|
if (!action) return null;
|
|
14884
14915
|
return stepId ? {
|
|
@@ -15220,15 +15251,18 @@ function groupCountByStep(actions) {
|
|
|
15220
15251
|
}
|
|
15221
15252
|
return counts;
|
|
15222
15253
|
}
|
|
15223
|
-
function createStepTracker() {
|
|
15254
|
+
function createStepTracker(onChange) {
|
|
15224
15255
|
let currentStepId;
|
|
15256
|
+
const advance = (stepId) => {
|
|
15257
|
+
if (stepId === currentStepId) return;
|
|
15258
|
+
currentStepId = stepId;
|
|
15259
|
+
onChange?.(stepId);
|
|
15260
|
+
};
|
|
15225
15261
|
return {
|
|
15226
15262
|
current: () => currentStepId,
|
|
15227
|
-
fromStepStartLine:
|
|
15228
|
-
currentStepId = stepId;
|
|
15229
|
-
},
|
|
15263
|
+
fromStepStartLine: advance,
|
|
15230
15264
|
fromCommand: (stepId) => {
|
|
15231
|
-
if (stepId)
|
|
15265
|
+
if (stepId) advance(stepId);
|
|
15232
15266
|
return currentStepId;
|
|
15233
15267
|
}
|
|
15234
15268
|
};
|
|
@@ -15486,7 +15520,7 @@ async function runGenerateCli(specPath, opts) {
|
|
|
15486
15520
|
//#endregion
|
|
15487
15521
|
//#region src/cli/record.ts
|
|
15488
15522
|
const VALIDATION_MODES = ["lenient", "strict"];
|
|
15489
|
-
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("--trace-validation <mode>", "What to do with actions that fail post-trace validation: 'lenient' (default) tags them; 'strict' drops them.", (raw) => {
|
|
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) => {
|
|
15490
15524
|
if (VALIDATION_MODES.includes(raw)) return raw;
|
|
15491
15525
|
throw new Error(`--trace-validation must be one of ${VALIDATION_MODES.join(" | ")}`);
|
|
15492
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) => {
|
|
@@ -15543,19 +15577,30 @@ async function runRecord(specPath, opts) {
|
|
|
15543
15577
|
if (push) info(`hub: record run opened (${push.runId})`);
|
|
15544
15578
|
let recorded = false;
|
|
15545
15579
|
let sealed = true;
|
|
15580
|
+
let tracingStep;
|
|
15581
|
+
let killedBy;
|
|
15546
15582
|
const teardown = createRunTeardown();
|
|
15547
15583
|
teardown.onFinalize(async () => {
|
|
15548
|
-
if (push)
|
|
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);
|
|
15587
|
+
});
|
|
15588
|
+
const disposeSignalHandlers = installTeardownSignalHandlers(teardown, (sig) => {
|
|
15589
|
+
killedBy = sig;
|
|
15549
15590
|
});
|
|
15550
|
-
const disposeSignalHandlers = installTeardownSignalHandlers(teardown);
|
|
15551
15591
|
try {
|
|
15552
15592
|
let traceResult = null;
|
|
15553
15593
|
let generated = true;
|
|
15554
15594
|
try {
|
|
15555
15595
|
traceResult = await runTrace(featureName, specName, opts.model, opts.traceValidation ?? "lenient", language, {
|
|
15556
15596
|
cwd: cwdForProfile,
|
|
15557
|
-
hubContext
|
|
15597
|
+
hubContext,
|
|
15598
|
+
...opts.instruction ? { instruction: opts.instruction } : {},
|
|
15599
|
+
onStep: (stepId) => {
|
|
15600
|
+
tracingStep = stepId;
|
|
15601
|
+
}
|
|
15558
15602
|
});
|
|
15603
|
+
tracingStep = void 0;
|
|
15559
15604
|
blank();
|
|
15560
15605
|
if (!opts.traceOnly) generated = (await runGenerate(featureName, specName, {
|
|
15561
15606
|
maxRetries: parseInt(opts.autoFixMaxRetries ?? "3", 10),
|
|
@@ -15594,15 +15639,23 @@ async function runRecord(specPath, opts) {
|
|
|
15594
15639
|
* Close the record run with the one row this command produced, answering
|
|
15595
15640
|
* whether it closed. One spec is recorded per invocation, so one row is the
|
|
15596
15641
|
* whole run — enough for the runs list to say what the money bought.
|
|
15642
|
+
*
|
|
15643
|
+
* `failureNote` (e.g. "terminated by signal (SIGTERM) during step-03") rides
|
|
15644
|
+
* in the row's `failureLogExcerpt` — the field a failed `ccqa run` row already
|
|
15645
|
+
* uses for its failure text — so the hub says why a recording died instead of
|
|
15646
|
+
* a bare status:"failed". Ignored on a successful recording.
|
|
15597
15647
|
*/
|
|
15598
|
-
async function sealRecordPush(push, featureName, specName, recorded) {
|
|
15648
|
+
async function sealRecordPush(push, featureName, specName, recorded, failureNote) {
|
|
15599
15649
|
return sealHubRun(push, {
|
|
15600
|
-
rows: [
|
|
15601
|
-
|
|
15602
|
-
|
|
15603
|
-
|
|
15604
|
-
|
|
15605
|
-
|
|
15650
|
+
rows: [{
|
|
15651
|
+
...emptySpecRow({
|
|
15652
|
+
feature: featureName,
|
|
15653
|
+
spec: specName,
|
|
15654
|
+
title: null,
|
|
15655
|
+
status: recorded ? "passed" : "failed"
|
|
15656
|
+
}),
|
|
15657
|
+
...!recorded && failureNote ? { failureLogExcerpt: failureNote } : {}
|
|
15658
|
+
}],
|
|
15606
15659
|
reportMeta: {
|
|
15607
15660
|
git: {
|
|
15608
15661
|
head: push.gitHead,
|
|
@@ -17443,6 +17496,7 @@ function gradedDriftEntry(ledger, key, runId, label) {
|
|
|
17443
17496
|
};
|
|
17444
17497
|
if (label === null) {
|
|
17445
17498
|
delete graded.surface;
|
|
17499
|
+
delete graded.subDiagnosis;
|
|
17446
17500
|
delete graded.headline;
|
|
17447
17501
|
delete graded.confidence;
|
|
17448
17502
|
}
|
|
@@ -17807,9 +17861,11 @@ async function updateDriftLedger(storage, run, results) {
|
|
|
17807
17861
|
if (row.status === "skipped") continue;
|
|
17808
17862
|
const key = `${row.feature}/${row.spec}`;
|
|
17809
17863
|
const diagnosis = row.analysis ? normalizeDiagnosis(row.analysis) : null;
|
|
17864
|
+
const subDiagnosis = DriftSubDiagnosisSchema.safeParse(diagnosis?.subDiagnosis);
|
|
17810
17865
|
ledger.specs[key] = {
|
|
17811
17866
|
label: diagnosis ? diagnosis.label : null,
|
|
17812
17867
|
surface: diagnosis?.surface,
|
|
17868
|
+
subDiagnosis: subDiagnosis.success ? subDiagnosis.data : void 0,
|
|
17813
17869
|
specChangeKind: diagnosis?.specChangeKind,
|
|
17814
17870
|
confidence: diagnosis?.confidence,
|
|
17815
17871
|
headline: diagnosis?.headline,
|
|
@@ -35,9 +35,9 @@ declare const RunSchema: z.ZodObject<{
|
|
|
35
35
|
running: "running";
|
|
36
36
|
}>;
|
|
37
37
|
kind: z.ZodDefault<z.ZodEnum<{
|
|
38
|
-
record: "record";
|
|
39
38
|
run: "run";
|
|
40
39
|
drift: "drift";
|
|
40
|
+
record: "record";
|
|
41
41
|
}>>;
|
|
42
42
|
drift: z.ZodDefault<z.ZodNullable<z.ZodObject<{
|
|
43
43
|
specs: z.ZodNumber;
|
|
@@ -383,6 +383,11 @@ declare const DriftLedgerResponseSchema: z.ZodObject<{
|
|
|
383
383
|
spec: "spec";
|
|
384
384
|
generated: "generated";
|
|
385
385
|
}>>;
|
|
386
|
+
subDiagnosis: z.ZodOptional<z.ZodEnum<{
|
|
387
|
+
OVER_ASSERTION: "OVER_ASSERTION";
|
|
388
|
+
SELECTOR_DRIFT: "SELECTOR_DRIFT";
|
|
389
|
+
NONE: "NONE";
|
|
390
|
+
}>>;
|
|
386
391
|
specChangeKind: z.ZodOptional<z.ZodEnum<{
|
|
387
392
|
FEATURE_REMOVED: "FEATURE_REMOVED";
|
|
388
393
|
BEHAVIOUR_CHANGED: "BEHAVIOUR_CHANGED";
|
|
@@ -621,9 +626,9 @@ type ReportSpecResult = z.infer<typeof ReportSpecResultSchema>;
|
|
|
621
626
|
declare const RunReportDataSchema: z.ZodObject<{
|
|
622
627
|
schemaVersion: z.ZodLiteral<1>;
|
|
623
628
|
kind: z.ZodDefault<z.ZodEnum<{
|
|
624
|
-
record: "record";
|
|
625
629
|
run: "run";
|
|
626
630
|
drift: "drift";
|
|
631
|
+
record: "record";
|
|
627
632
|
}>>;
|
|
628
633
|
createdAt: z.ZodString;
|
|
629
634
|
runId: z.ZodNullable<z.ZodString>;
|
package/dist/package.json
CHANGED