ccqa 1.11.0 → 1.13.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/README.md +22 -22
- package/dist/bin/ccqa.mjs +297 -272
- package/dist/hub-client/index.d.mts +1 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -3326,7 +3326,7 @@ const DEFAULT_CONCURRENCY$1 = 3;
|
|
|
3326
3326
|
/**
|
|
3327
3327
|
* Run drift checks against a list of pre-collected targets. Pure library
|
|
3328
3328
|
* function: no commander, no process.exit, no stdout writes. Callers handle
|
|
3329
|
-
* presentation. `cli/
|
|
3329
|
+
* presentation. `cli/audit` does the full sweep with `--only-affected-by` scoping;
|
|
3330
3330
|
* `cli/run` calls this with just the failing specs after vitest.
|
|
3331
3331
|
*/
|
|
3332
3332
|
async function analyzeDrift(input) {
|
|
@@ -4426,16 +4426,6 @@ Write the runbook to \`${suggestedPath}\` unless the conventions/examples clearl
|
|
|
4426
4426
|
//#region src/drift/affected.ts
|
|
4427
4427
|
const execFileP = promisify(execFile);
|
|
4428
4428
|
/**
|
|
4429
|
-
* GITHUB_BASE_REF holds a bare branch name (e.g. "main"); the local checkout
|
|
4430
|
-
* only has it as a remote-tracking ref, so prefix `origin/` unless already
|
|
4431
|
-
* qualified. Used by `ccqa run`'s resolveAnalysisBase (`src/run/git-context.ts`),
|
|
4432
|
-
* which both `ccqa run --changed` and `ccqa drift --changed` resolve their
|
|
4433
|
-
* base through, so the rule can't drift between them.
|
|
4434
|
-
*/
|
|
4435
|
-
function normalizeGithubBaseRef(ref) {
|
|
4436
|
-
return ref.startsWith("origin/") ? ref : `origin/${ref}`;
|
|
4437
|
-
}
|
|
4438
|
-
/**
|
|
4439
4429
|
* Paths that differ between `base` and `head` (two-dot: `git diff base..head`),
|
|
4440
4430
|
* from `cwd`. Renames are reported under their NEW path with status
|
|
4441
4431
|
* "renamed" — the OLD path is dropped since only the current layout matters.
|
|
@@ -5225,7 +5215,7 @@ const DraftNamingSchema = z.object({
|
|
|
5225
5215
|
//#endregion
|
|
5226
5216
|
//#region src/cli/draft.ts
|
|
5227
5217
|
const CATEGORY_LABEL = DRAFT_CATEGORY_LABEL;
|
|
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("--
|
|
5218
|
+
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("-y, --yes", "Apply each generated patch without asking [y/N]", false)).action(withUsageErrors(async (specPath, opts) => {
|
|
5229
5219
|
await ensureCcqaDir();
|
|
5230
5220
|
let featureName;
|
|
5231
5221
|
let specName;
|
|
@@ -5262,7 +5252,7 @@ async function runDraft(featureName, specName, opts, prefilledIntent) {
|
|
|
5262
5252
|
specName,
|
|
5263
5253
|
existing,
|
|
5264
5254
|
userInput: userInput.trim(),
|
|
5265
|
-
autoApply: opts.
|
|
5255
|
+
autoApply: opts.yes === true,
|
|
5266
5256
|
language: opts.language
|
|
5267
5257
|
});
|
|
5268
5258
|
if (oneShot) process.exit(turnResult.hasError && !turnResult.applied ? 1 : 0);
|
|
@@ -5482,7 +5472,7 @@ async function proposeNaming(opts) {
|
|
|
5482
5472
|
const final = ensureUnique(tree, sanitized.featureName, sanitized.specName);
|
|
5483
5473
|
meta("proposed", `${final.featureName}/${final.specName}`);
|
|
5484
5474
|
if (proposed.reason) meta("reason", proposed.reason);
|
|
5485
|
-
if (oneShot || opts.
|
|
5475
|
+
if (oneShot || opts.yes === true) return {
|
|
5486
5476
|
naming: final,
|
|
5487
5477
|
intent: intent.trim()
|
|
5488
5478
|
};
|
|
@@ -6531,15 +6521,13 @@ function createDiffProvider(args) {
|
|
|
6531
6521
|
}
|
|
6532
6522
|
//#endregion
|
|
6533
6523
|
//#region src/run/git-context.ts
|
|
6534
|
-
/** The `--failure-analysis` value that selects per-spec hub-ledger baselines. */
|
|
6535
|
-
const LAST_GREEN = "last-green";
|
|
6536
6524
|
/**
|
|
6537
|
-
*
|
|
6538
|
-
*
|
|
6539
|
-
*
|
|
6540
|
-
*
|
|
6525
|
+
* Marks a baseline as "each spec's own last green commit" rather than one
|
|
6526
|
+
* shared ref. Not a flag value — `--on-fail-explain` uses per-spec baselines
|
|
6527
|
+
* unless `--on-fail-explain-base` names a ref — but the report and the log
|
|
6528
|
+
* lines need a word for it.
|
|
6541
6529
|
*/
|
|
6542
|
-
const
|
|
6530
|
+
const LAST_GREEN = "last-green";
|
|
6543
6531
|
/** Resolve `ref` to a full commit sha, or null when it does not exist locally. */
|
|
6544
6532
|
async function resolveCommitSha(ref, cwd) {
|
|
6545
6533
|
try {
|
|
@@ -6555,35 +6543,17 @@ async function resolveCommitSha(ref, cwd) {
|
|
|
6555
6543
|
}
|
|
6556
6544
|
}
|
|
6557
6545
|
/**
|
|
6558
|
-
* Resolve a
|
|
6559
|
-
*
|
|
6560
|
-
*
|
|
6561
|
-
*
|
|
6562
|
-
* - a string value is an explicit ref;
|
|
6563
|
-
* - bare `true` derives the ref from GITHUB_BASE_REF (pull_request events)
|
|
6564
|
-
* and errors outside that context;
|
|
6565
|
-
* - the ref must resolve to a local commit, so a shallow CI checkout that
|
|
6566
|
-
* never fetched the base surfaces here as an actionable error instead of
|
|
6567
|
-
* an empty diff downstream.
|
|
6546
|
+
* Resolve a base ref to a verified baseline, failing fast — before any spec
|
|
6547
|
+
* runs — when it cannot be resolved. The ref must resolve to a local commit,
|
|
6548
|
+
* so a shallow CI checkout that never fetched the base surfaces here as an
|
|
6549
|
+
* actionable error instead of an empty diff downstream.
|
|
6568
6550
|
*
|
|
6569
6551
|
* `flagName` only shapes the error messages.
|
|
6570
6552
|
*/
|
|
6571
|
-
async function resolveAnalysisBase(
|
|
6572
|
-
|
|
6573
|
-
let source;
|
|
6574
|
-
if (flagValue === "last-green") throw new RunUsageError(`${flagName}=${LAST_GREEN} is not supported — last-green baselines are per-spec and only apply to --failure-analysis`);
|
|
6575
|
-
if (flagValue === "last-run") throw new RunUsageError(`${flagName}=${LAST_RUN} is not supported — last-run selects which specs to run and only applies to --changed`);
|
|
6576
|
-
if (typeof flagValue === "string") {
|
|
6577
|
-
ref = flagValue;
|
|
6578
|
-
source = "explicit";
|
|
6579
|
-
} else {
|
|
6580
|
-
const ghBase = process.env["GITHUB_BASE_REF"];
|
|
6581
|
-
if (!ghBase) throw new RunUsageError(`${flagName} without a base needs GITHUB_BASE_REF (a pull_request workflow); outside that context pass the base explicitly, e.g. ${baseExample ?? `${flagName}=origin/main`}`);
|
|
6582
|
-
ref = normalizeGithubBaseRef(ghBase);
|
|
6583
|
-
source = "github-base-ref";
|
|
6584
|
-
}
|
|
6553
|
+
async function resolveAnalysisBase(ref, flagName, cwd) {
|
|
6554
|
+
const source = "explicit";
|
|
6585
6555
|
const sha = await resolveCommitSha(ref, cwd);
|
|
6586
|
-
if (sha === null) throw new RunUsageError(`${flagName}: '${ref}' is not a resolvable git ref in this checkout. If this is CI, the base may not be fetched (try fetch-depth: 0). If '${ref}' was meant as a spec target, put spec targets before flags
|
|
6556
|
+
if (sha === null) throw new RunUsageError(`${flagName}: '${ref}' is not a resolvable git ref in this checkout. If this is CI, the base may not be fetched (try fetch-depth: 0). If '${ref}' was meant as a spec target, put spec targets before flags.`);
|
|
6587
6557
|
return {
|
|
6588
6558
|
ref,
|
|
6589
6559
|
sha,
|
|
@@ -6655,7 +6625,7 @@ async function fetchLastGreenLedger(hubCtx, profile, cwd) {
|
|
|
6655
6625
|
...profile ? { profile } : {}
|
|
6656
6626
|
});
|
|
6657
6627
|
} catch (err) {
|
|
6658
|
-
throw new RunUsageError(`--
|
|
6628
|
+
throw new RunUsageError(`--on-fail-explain: could not fetch the last-green ledger from the hub: ${err instanceof Error ? err.message : String(err)}`);
|
|
6659
6629
|
}
|
|
6660
6630
|
const n = Object.keys(entries).length;
|
|
6661
6631
|
const scope = branch === fallbackBranch ? branch : `${branch} → ${fallbackBranch}`;
|
|
@@ -6774,6 +6744,50 @@ function formatDryRunLines(agentBrowser, routed) {
|
|
|
6774
6744
|
return tagged.map((t) => ` ${t.key.padEnd(width)} ${t.tag}`);
|
|
6775
6745
|
}
|
|
6776
6746
|
//#endregion
|
|
6747
|
+
//#region src/run/audited-clean.ts
|
|
6748
|
+
/**
|
|
6749
|
+
* Fetch the drift ledger and reduce it to the specs that are safe to run.
|
|
6750
|
+
*
|
|
6751
|
+
* A spec qualifies only when the ledger holds an entry for it *and* that entry
|
|
6752
|
+
* found no drift. A spec that has never been audited does not qualify: the
|
|
6753
|
+
* point of the flag is to spend a run only where a cheap audit already said
|
|
6754
|
+
* the spec still describes the code, and "never looked" is not that.
|
|
6755
|
+
*/
|
|
6756
|
+
async function fetchAuditedLedger(hubCtx) {
|
|
6757
|
+
let ledger;
|
|
6758
|
+
try {
|
|
6759
|
+
ledger = await hubCtx.hub.getDriftLedger(hubCtx.project);
|
|
6760
|
+
} catch (err) {
|
|
6761
|
+
throw new RunUsageError(`--only-audited-clean: could not fetch the drift ledger from the hub: ${errMessage(err)}`);
|
|
6762
|
+
}
|
|
6763
|
+
const clean = /* @__PURE__ */ new Set();
|
|
6764
|
+
const audited = /* @__PURE__ */ new Set();
|
|
6765
|
+
for (const [key, entry] of Object.entries(ledger.specs)) {
|
|
6766
|
+
audited.add(key);
|
|
6767
|
+
if (entry.label === null) clean.add(key);
|
|
6768
|
+
}
|
|
6769
|
+
return {
|
|
6770
|
+
clean,
|
|
6771
|
+
audited
|
|
6772
|
+
};
|
|
6773
|
+
}
|
|
6774
|
+
function selectAuditedClean(specs, ledger) {
|
|
6775
|
+
const selected = [];
|
|
6776
|
+
let unaudited = 0;
|
|
6777
|
+
let drifted = 0;
|
|
6778
|
+
for (const spec of specs) {
|
|
6779
|
+
const key = specKey(spec);
|
|
6780
|
+
if (ledger.clean.has(key)) selected.push(spec);
|
|
6781
|
+
else if (ledger.audited.has(key)) drifted++;
|
|
6782
|
+
else unaudited++;
|
|
6783
|
+
}
|
|
6784
|
+
return {
|
|
6785
|
+
selected,
|
|
6786
|
+
unaudited,
|
|
6787
|
+
drifted
|
|
6788
|
+
};
|
|
6789
|
+
}
|
|
6790
|
+
//#endregion
|
|
6777
6791
|
//#region src/run/rerun-selection.ts
|
|
6778
6792
|
/**
|
|
6779
6793
|
* `ccqa run --changed=last-run`: select specs from the hub's re-run verdicts
|
|
@@ -6793,7 +6807,7 @@ const RERUN_MIN_HUB_VERSION = "1.9";
|
|
|
6793
6807
|
* has no profile-free answer.
|
|
6794
6808
|
*/
|
|
6795
6809
|
function requireRerunProfile(profile) {
|
|
6796
|
-
if (profile === void 0) throw new RunUsageError(
|
|
6810
|
+
if (profile === void 0) throw new RunUsageError("--only-stale requires --profile <name>: the deploy log it reads is per-profile, so which specs need a re-run has no answer without one");
|
|
6797
6811
|
return profile;
|
|
6798
6812
|
}
|
|
6799
6813
|
/**
|
|
@@ -6806,9 +6820,9 @@ async function fetchRerunReport(hubCtx, profile) {
|
|
|
6806
6820
|
report = await hubCtx.hub.getRerun(hubCtx.project, { profile });
|
|
6807
6821
|
} catch (err) {
|
|
6808
6822
|
if (err instanceof HubApiError && err.status === 404) throw new RunUsageError(explainNotFound(hubCtx, err));
|
|
6809
|
-
throw new RunUsageError(`--
|
|
6823
|
+
throw new RunUsageError(`--only-stale: could not ask the hub which specs need a re-run: ${errMessage(err)}`);
|
|
6810
6824
|
}
|
|
6811
|
-
if (report.deployHead === null) throw new RunUsageError(`--
|
|
6825
|
+
if (report.deployHead === null) throw new RunUsageError(`--only-stale: no deploy has been recorded for profile "${profile}" of project "${hubCtx.project}", so nothing can be compared against. Wire \`ccqa hub deploy record\` into the deploy job, or pass an explicit baseline (--changed=<ref>).`);
|
|
6812
6826
|
return {
|
|
6813
6827
|
...report,
|
|
6814
6828
|
deployHead: report.deployHead
|
|
@@ -6820,8 +6834,8 @@ async function fetchRerunReport(hubCtx, profile) {
|
|
|
6820
6834
|
* means the hub does not serve this route at all.
|
|
6821
6835
|
*/
|
|
6822
6836
|
function explainNotFound(hubCtx, err) {
|
|
6823
|
-
if (err.code === "no_perspectives") return `--
|
|
6824
|
-
return `--
|
|
6837
|
+
if (err.code === "no_perspectives") return `--only-stale: project "${hubCtx.project}" has no perspectives document on the hub, so no spec is registered to compare against a deploy. Run \`ccqa perspectives\` first.`;
|
|
6838
|
+
return `--only-stale: this hub does not serve re-run verdicts — it needs ccqa ${RERUN_MIN_HUB_VERSION} or newer. Upgrade the hub, or pass an explicit baseline (--changed=<ref>).`;
|
|
6825
6839
|
}
|
|
6826
6840
|
/** States the summary line reports, worst-known-first. */
|
|
6827
6841
|
const SUMMARY_ORDER = [
|
|
@@ -8463,6 +8477,101 @@ function oneLine$1(text) {
|
|
|
8463
8477
|
return text.trim().replace(/\s+/g, " ");
|
|
8464
8478
|
}
|
|
8465
8479
|
//#endregion
|
|
8480
|
+
//#region src/cli/session.ts
|
|
8481
|
+
const AB = resolveAgentBrowserBin$1();
|
|
8482
|
+
/**
|
|
8483
|
+
* Run agent-browser attached to the user's terminal (no timeout, inherited
|
|
8484
|
+
* stdio) so a human can complete an interactive login during `bootstrap`.
|
|
8485
|
+
* Distinct from runtime/spawn-ab.ts, which pipes stdio and hard-times-out for
|
|
8486
|
+
* non-interactive automation.
|
|
8487
|
+
*/
|
|
8488
|
+
function runAbInteractive(args) {
|
|
8489
|
+
return spawnSync(AB, args, { stdio: "inherit" }).status ?? 1;
|
|
8490
|
+
}
|
|
8491
|
+
function validateName(name) {
|
|
8492
|
+
const parsed = SessionNameSchema.safeParse(name);
|
|
8493
|
+
if (!parsed.success) {
|
|
8494
|
+
error(`invalid session name "${name}": ${parsed.error.issues[0]?.message ?? "bad name"}`);
|
|
8495
|
+
process.exit(2);
|
|
8496
|
+
}
|
|
8497
|
+
return parsed.data;
|
|
8498
|
+
}
|
|
8499
|
+
const profileOption$1 = ["--profile <name>", "Sessions bucket to read/write on the hub. Defaults to 'default'."];
|
|
8500
|
+
const projectOption$1 = ["--project <name>", "Project the session belongs to on the hub. Defaults to the current directory's name."];
|
|
8501
|
+
const sessionCaptureCommand = new Command("capture").description("Open a headed browser so you can log in by hand, then upload the resulting session (cookies + localStorage) to the hub for `session:` specs to restore.").argument("<name>", "Session name to save").option("--url <url>", "URL to open first (e.g. the login page). Omit to start with a blank tab.").option(...profileOption$1).option(...hubUrlOption).option(...hubTokenOption).option(...projectOption$1).option("--cwd <path>", "Directory the default --project name is derived from (defaults to the current directory).").action(async (rawName, opts) => {
|
|
8502
|
+
const name = validateName(rawName);
|
|
8503
|
+
resolveCwd(opts.cwd);
|
|
8504
|
+
const project = resolveProject(opts);
|
|
8505
|
+
let hub;
|
|
8506
|
+
try {
|
|
8507
|
+
hub = requireHubClient(opts);
|
|
8508
|
+
} catch (err) {
|
|
8509
|
+
if (!(err instanceof HubConnectionError)) throw err;
|
|
8510
|
+
error(err.message);
|
|
8511
|
+
process.exit(2);
|
|
8512
|
+
}
|
|
8513
|
+
header("session bootstrap", name);
|
|
8514
|
+
meta("project", project);
|
|
8515
|
+
meta("profile", opts.profile ?? "default");
|
|
8516
|
+
blank();
|
|
8517
|
+
const openArgs = [
|
|
8518
|
+
"--headed",
|
|
8519
|
+
"open",
|
|
8520
|
+
...opts.url ? [opts.url] : ["about:blank"]
|
|
8521
|
+
];
|
|
8522
|
+
info("opening a browser — log in by hand, then return here.");
|
|
8523
|
+
const openStatus = runAbInteractive(openArgs);
|
|
8524
|
+
if (openStatus !== 0) {
|
|
8525
|
+
error(`agent-browser open exited ${openStatus}`);
|
|
8526
|
+
process.exit(1);
|
|
8527
|
+
}
|
|
8528
|
+
const rl = createInterface({
|
|
8529
|
+
input: process.stdin,
|
|
8530
|
+
output: process.stdout
|
|
8531
|
+
});
|
|
8532
|
+
await rl.question("\nPress Enter once you are fully logged in to save the session… ");
|
|
8533
|
+
rl.close();
|
|
8534
|
+
const tmpDir = await mkdtemp(join(tmpdir(), "ccqa-session-bootstrap-"));
|
|
8535
|
+
try {
|
|
8536
|
+
const tmpPath = join(tmpDir, "state.json");
|
|
8537
|
+
const saveStatus = runAbInteractive([
|
|
8538
|
+
"state",
|
|
8539
|
+
"save",
|
|
8540
|
+
tmpPath
|
|
8541
|
+
]);
|
|
8542
|
+
runAbInteractive(["close"]);
|
|
8543
|
+
if (saveStatus !== 0) {
|
|
8544
|
+
error(`agent-browser state save exited ${saveStatus}`);
|
|
8545
|
+
process.exit(1);
|
|
8546
|
+
}
|
|
8547
|
+
const state = await loadStorageState(tmpPath);
|
|
8548
|
+
let payload = state;
|
|
8549
|
+
if (opts.url) {
|
|
8550
|
+
info("verifying the saved session restores to a signed-in page…");
|
|
8551
|
+
const check = verifySessionRestores(tmpPath, opts.url);
|
|
8552
|
+
if (!check.restored) {
|
|
8553
|
+
error(`session did not restore cleanly: ${check.reason}`);
|
|
8554
|
+
hint("fully load the application (sign in, open the target workspace/page, wait for it to settle) before pressing Enter, then run bootstrap again. Nothing was uploaded.");
|
|
8555
|
+
process.exit(1);
|
|
8556
|
+
}
|
|
8557
|
+
info("restore verified — the session starts signed in.");
|
|
8558
|
+
payload = {
|
|
8559
|
+
...state,
|
|
8560
|
+
[SESSION_VERIFY_URL_KEY]: opts.url
|
|
8561
|
+
};
|
|
8562
|
+
} else warn("no --url given — the session can't be verified now, and runs can't health-check it before executing steps; strongly consider re-running with --url <a signed-in page URL>.");
|
|
8563
|
+
await hub.putSession(project, opts.profile ?? "default", name, payload);
|
|
8564
|
+
} finally {
|
|
8565
|
+
await rm(tmpDir, {
|
|
8566
|
+
recursive: true,
|
|
8567
|
+
force: true
|
|
8568
|
+
});
|
|
8569
|
+
}
|
|
8570
|
+
blank();
|
|
8571
|
+
info(`uploaded session "${name}" to the hub (encrypted at rest)`);
|
|
8572
|
+
hint("reference it from a spec with: session: " + name);
|
|
8573
|
+
});
|
|
8574
|
+
//#endregion
|
|
8466
8575
|
//#region src/cli/hub.ts
|
|
8467
8576
|
/**
|
|
8468
8577
|
* `ccqa hub` — the client side of the ccqa hub (a results/secret control
|
|
@@ -8473,8 +8582,8 @@ function oneLine$1(text) {
|
|
|
8473
8582
|
* talk to the hub over the same public REST API (docs/hub-api.md) via
|
|
8474
8583
|
* `ccqa/hub-client`.
|
|
8475
8584
|
*/
|
|
8476
|
-
const profileOption
|
|
8477
|
-
const projectOption
|
|
8585
|
+
const profileOption = ["--profile <name>", "Profile bucket the session/variable belongs to. Defaults to 'default'."];
|
|
8586
|
+
const projectOption = ["--project <name>", "Project the session/variable belongs to on the hub. Defaults to the current directory's name."];
|
|
8478
8587
|
const cwdOption = ["--cwd <path>", "Directory the default --project name is derived from (defaults to the current directory)."];
|
|
8479
8588
|
/**
|
|
8480
8589
|
* The hub base URL from flags / env (trailing slashes trimmed), or exit 2.
|
|
@@ -8511,7 +8620,7 @@ async function readStdin() {
|
|
|
8511
8620
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
8512
8621
|
return Buffer.concat(chunks).toString("utf8");
|
|
8513
8622
|
}
|
|
8514
|
-
const sessionPush = new Command("push").description("Upload a locally-saved browser session (.ccqa/sessions/<profile>/<name>.json) to the hub, so it's available for `ccqa run` to fetch at run time. Encrypted at rest on the hub.").argument("<name>", "Session name to upload (resolves to .ccqa/sessions/<profile>/<name>.json)").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption
|
|
8623
|
+
const sessionPush = new Command("push").description("Upload a locally-saved browser session (.ccqa/sessions/<profile>/<name>.json) to the hub, so it's available for `ccqa run` to fetch at run time. Encrypted at rest on the hub.").argument("<name>", "Session name to upload (resolves to .ccqa/sessions/<profile>/<name>.json)").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption).option(...profileOption).option("--cwd <path>", "Project root containing .ccqa/ (defaults to the current directory).").action(withHubErrors(async (rawName, opts) => {
|
|
8515
8624
|
const name = validateSessionName(rawName);
|
|
8516
8625
|
const cwd = resolveCwd(opts.cwd);
|
|
8517
8626
|
const project = resolveProject(opts);
|
|
@@ -8531,7 +8640,7 @@ const sessionPush = new Command("push").description("Upload a locally-saved brow
|
|
|
8531
8640
|
meta("profile", profile);
|
|
8532
8641
|
info(`uploaded session "${name}" to the hub (encrypted at rest)`);
|
|
8533
8642
|
}));
|
|
8534
|
-
const sessionLs = new Command("ls").description("List sessions stored on the hub for a project/profile (names + last-updated times). `ls` shows metadata only.").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption
|
|
8643
|
+
const sessionLs = new Command("ls").description("List sessions stored on the hub for a project/profile (names + last-updated times). `ls` shows metadata only.").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption).option(...profileOption).option(...cwdOption).action(withHubErrors(async (opts) => {
|
|
8535
8644
|
const project = resolveProject(opts);
|
|
8536
8645
|
const profile = opts.profile ?? "default";
|
|
8537
8646
|
const sessions = await connect(opts).listSessions(project, profile);
|
|
@@ -8542,7 +8651,7 @@ const sessionLs = new Command("ls").description("List sessions stored on the hub
|
|
|
8542
8651
|
}
|
|
8543
8652
|
for (const s of sessions) meta(s.name, `updated ${s.updatedAt}`);
|
|
8544
8653
|
}));
|
|
8545
|
-
const sessionRm = new Command("rm").description("Delete a session from the hub.").argument("<name>", "Session name to delete").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption
|
|
8654
|
+
const sessionRm = new Command("rm").description("Delete a session from the hub.").argument("<name>", "Session name to delete").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption).option(...profileOption).option(...cwdOption).action(withHubErrors(async (rawName, opts) => {
|
|
8546
8655
|
const name = validateSessionName(rawName);
|
|
8547
8656
|
const project = resolveProject(opts);
|
|
8548
8657
|
const profile = opts.profile ?? "default";
|
|
@@ -8550,8 +8659,8 @@ const sessionRm = new Command("rm").description("Delete a session from the hub."
|
|
|
8550
8659
|
header("hub session rm", name);
|
|
8551
8660
|
info(`deleted session "${name}" from the hub`);
|
|
8552
8661
|
}));
|
|
8553
|
-
const sessionCommand
|
|
8554
|
-
const varSet = new Command("set").description("Store an environment variable on the hub, fetched at run time by `ccqa run` / `ccqa record`. Use --sensitive to hide the value from `ls` output (it is still returned in full to the run).").argument("<name>", "Variable name (e.g. BASE_URL)").option("--value <value>", "Variable value. Omit to read the value from stdin (better for secrets).").option("--sensitive", "Hide the value in `ls` output. Any token holder can still read it via the run-time fetch.").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption
|
|
8662
|
+
const sessionCommand = new Command("session").description("Manage browser sessions stored on the hub (fetched automatically by `ccqa run` / `ccqa record` at run time).").addCommand(sessionCaptureCommand).addCommand(sessionPush).addCommand(sessionLs).addCommand(sessionRm);
|
|
8663
|
+
const varSet = new Command("set").description("Store an environment variable on the hub, fetched at run time by `ccqa run` / `ccqa record`. Use --sensitive to hide the value from `ls` output (it is still returned in full to the run).").argument("<name>", "Variable name (e.g. BASE_URL)").option("--value <value>", "Variable value. Omit to read the value from stdin (better for secrets).").option("--sensitive", "Hide the value in `ls` output. Any token holder can still read it via the run-time fetch.").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption).option(...profileOption).option(...cwdOption).action(withHubErrors(async (name, opts) => {
|
|
8555
8664
|
const project = resolveProject(opts);
|
|
8556
8665
|
const profile = opts.profile ?? "default";
|
|
8557
8666
|
const value = opts.value ?? (await readStdin()).trim();
|
|
@@ -8569,7 +8678,7 @@ const varSet = new Command("set").description("Store an environment variable on
|
|
|
8569
8678
|
meta("sensitive", String(opts.sensitive ?? false));
|
|
8570
8679
|
info(`stored variable "${name}" on the hub`);
|
|
8571
8680
|
}));
|
|
8572
|
-
const varLs = new Command("ls").description("List variables stored on the hub for a project/profile. Non-sensitive values are shown inline; sensitive ones are hidden here but still fetched at run time by `ccqa run` / `ccqa record`.").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption
|
|
8681
|
+
const varLs = new Command("ls").description("List variables stored on the hub for a project/profile. Non-sensitive values are shown inline; sensitive ones are hidden here but still fetched at run time by `ccqa run` / `ccqa record`.").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption).option(...profileOption).option(...cwdOption).action(withHubErrors(async (opts) => {
|
|
8573
8682
|
const project = resolveProject(opts);
|
|
8574
8683
|
const profile = opts.profile ?? "default";
|
|
8575
8684
|
const variables = await connect(opts).listVariables(project, profile);
|
|
@@ -8583,7 +8692,7 @@ const varLs = new Command("ls").description("List variables stored on the hub fo
|
|
|
8583
8692
|
meta(v.name, shown);
|
|
8584
8693
|
}
|
|
8585
8694
|
}));
|
|
8586
|
-
const varRm = new Command("rm").description("Delete a variable from the hub.").argument("<name>", "Variable name to delete").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption
|
|
8695
|
+
const varRm = new Command("rm").description("Delete a variable from the hub.").argument("<name>", "Variable name to delete").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption).option(...profileOption).option(...cwdOption).action(withHubErrors(async (name, opts) => {
|
|
8587
8696
|
const project = resolveProject(opts);
|
|
8588
8697
|
const profile = opts.profile ?? "default";
|
|
8589
8698
|
await connect(opts).deleteVariable(project, profile, name);
|
|
@@ -8599,7 +8708,7 @@ function validatePromptName(rawName) {
|
|
|
8599
8708
|
}
|
|
8600
8709
|
return rawName;
|
|
8601
8710
|
}
|
|
8602
|
-
const promptPush = new Command("push").description("Upload a locally-generated prompt asset to the hub, so it's available to other environments running against this project.").argument("<name>", `Prompt name (${PROMPT_NAMES.join(", ")})`).option(...hubUrlOption).option(...hubTokenOption).option(...projectOption
|
|
8711
|
+
const promptPush = new Command("push").description("Upload a locally-generated prompt asset to the hub, so it's available to other environments running against this project.").argument("<name>", `Prompt name (${PROMPT_NAMES.join(", ")})`).option(...hubUrlOption).option(...hubTokenOption).option(...projectOption).option(...cwdOption).action(withHubErrors(async (rawName, opts) => {
|
|
8603
8712
|
const name = validatePromptName(rawName);
|
|
8604
8713
|
const cwd = resolveCwd(opts.cwd);
|
|
8605
8714
|
const project = resolveProject(opts);
|
|
@@ -8622,7 +8731,7 @@ const promptPush = new Command("push").description("Upload a locally-generated p
|
|
|
8622
8731
|
meta("project", project);
|
|
8623
8732
|
info(`uploaded prompt "${name}" to the hub`);
|
|
8624
8733
|
}));
|
|
8625
|
-
const promptLs = new Command("ls").description("List prompts stored on the hub for a project (name, kind, last-updated). Prompts are project-wide (not per-profile). `ls` shows metadata only.").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption
|
|
8734
|
+
const promptLs = new Command("ls").description("List prompts stored on the hub for a project (name, kind, last-updated). Prompts are project-wide (not per-profile). `ls` shows metadata only.").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption).option(...cwdOption).action(withHubErrors(async (opts) => {
|
|
8626
8735
|
const project = resolveProject(opts);
|
|
8627
8736
|
const prompts = await connect(opts).listPrompts(project);
|
|
8628
8737
|
header("hub prompts", project);
|
|
@@ -8632,7 +8741,7 @@ const promptLs = new Command("ls").description("List prompts stored on the hub f
|
|
|
8632
8741
|
}
|
|
8633
8742
|
for (const p of prompts) meta(p.name, `${p.kind}, updated ${p.updatedAt}`);
|
|
8634
8743
|
}));
|
|
8635
|
-
const promptRm = new Command("rm").description("Delete a prompt from the hub.").argument("<name>", "Prompt name to delete").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption
|
|
8744
|
+
const promptRm = new Command("rm").description("Delete a prompt from the hub.").argument("<name>", "Prompt name to delete").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption).option(...cwdOption).action(withHubErrors(async (rawName, opts) => {
|
|
8636
8745
|
const name = validatePromptName(rawName);
|
|
8637
8746
|
const project = resolveProject(opts);
|
|
8638
8747
|
await connect(opts).deletePrompt(project, name);
|
|
@@ -8720,9 +8829,9 @@ function describeSelection(selection, diffAvailable) {
|
|
|
8720
8829
|
return `${values.filter((s) => s.verdict === "needed").length} needed / ${values.filter((s) => s.verdict === "unknown").length} unknown / ${values.length} specs`;
|
|
8721
8830
|
}
|
|
8722
8831
|
const deployCommand = new Command("deploy").description("Report deploys to the hub, the input behind `ccqa run --changed=last-run`.").addCommand(deployRecord);
|
|
8723
|
-
const pushCommand = new Command("push").description("Upload the report directory of a finished `ccqa run --report` to the hub as a run. Run this after `ccqa run` (use `if: always()` in CI so failing runs are pushed too).").option("--report <dir>", `Report directory to push. Default: ${DEFAULT_REPORT_DIR}/`).option("--project <name>", "Logical project name for the run. Defaults to the current directory's name.").option("--branch <name>", "Branch label. Defaults to $GITHUB_HEAD_REF / $GITHUB_REF_NAME / current git branch.").option("--profile <name>", "Profile (environment) the run executed against. Recorded for display; runs are not scoped by profile.").option(...hubUrlOption).option(...hubTokenOption).option("--cwd <path>", "Directory the report dir is resolved against (defaults to the current directory).").action(withHubErrors(async (opts) => {
|
|
8832
|
+
const pushCommand = new Command("push").description("Upload the report directory of a finished `ccqa run --report` to the hub as a run. Run this after `ccqa run` (use `if: always()` in CI so failing runs are pushed too).").option("--report-dir <dir>", `Report directory to push. Default: ${DEFAULT_REPORT_DIR}/`).option("--project <name>", "Logical project name for the run. Defaults to the current directory's name.").option("--branch <name>", "Branch label. Defaults to $GITHUB_HEAD_REF / $GITHUB_REF_NAME / current git branch.").option("--profile <name>", "Profile (environment) the run executed against. Recorded for display; runs are not scoped by profile.").option(...hubUrlOption).option(...hubTokenOption).option("--cwd <path>", "Directory the report dir is resolved against (defaults to the current directory).").action(withHubErrors(async (opts) => {
|
|
8724
8833
|
const cwd = resolveCwd(opts.cwd);
|
|
8725
|
-
const reportDir = join(cwd, opts.
|
|
8834
|
+
const reportDir = join(cwd, opts.reportDir ?? "ccqa-report");
|
|
8726
8835
|
const project = resolveProject(opts);
|
|
8727
8836
|
let report;
|
|
8728
8837
|
try {
|
|
@@ -8754,7 +8863,7 @@ const pushCommand = new Command("push").description("Upload the report directory
|
|
|
8754
8863
|
meta("specs", `${run.specs.passed}/${run.specs.total} passed`);
|
|
8755
8864
|
info(`${resolveBaseUrl(opts)}/#/runs/${run.id}`);
|
|
8756
8865
|
}));
|
|
8757
|
-
const hubCommand = new Command("hub").description("Client for a ccqa hub: push run results and manage sessions/variables/prompts used by `ccqa run`. See docs/hub.md.").addCommand(pushCommand).addCommand(deployCommand).addCommand(sessionCommand
|
|
8866
|
+
const hubCommand = new Command("hub").description("Client for a ccqa hub: push run results and manage sessions/variables/prompts used by `ccqa run`. See docs/hub.md.").addCommand(pushCommand).addCommand(deployCommand).addCommand(sessionCommand).addCommand(varCommand).addCommand(promptCommand);
|
|
8758
8867
|
/** Loose check that a fetched session is agent-browser storage-state, mirroring loadStorageState. */
|
|
8759
8868
|
function isStorageStateShape(state) {
|
|
8760
8869
|
return typeof state === "object" && state !== null && Array.isArray(state.cookies) && Array.isArray(state.origins);
|
|
@@ -11929,14 +12038,14 @@ Write the new contents of \`${agentMdLabel}\`. Output ONLY the file contents —
|
|
|
11929
12038
|
* so the run exit code is unaffected by this opt-in side step.
|
|
11930
12039
|
*/
|
|
11931
12040
|
async function updateAgentPrompt(args) {
|
|
11932
|
-
const { kind, runSummary, hubContext, model, language } = args;
|
|
12041
|
+
const { kind, flag, runSummary, hubContext, model, language } = args;
|
|
11933
12042
|
const auth = driftAuthAvailable();
|
|
11934
12043
|
if (!auth.ok) {
|
|
11935
|
-
warn(
|
|
12044
|
+
warn(`${flag} skipped (${auth.reason})`);
|
|
11936
12045
|
return;
|
|
11937
12046
|
}
|
|
11938
12047
|
if (!hubContext) {
|
|
11939
|
-
warn(
|
|
12048
|
+
warn(`${flag} skipped (hub connection required; pass --hub-url/--hub-token or set CCQA_HUB_URL/CCQA_HUB_TOKEN)`);
|
|
11940
12049
|
return;
|
|
11941
12050
|
}
|
|
11942
12051
|
const { hub, project } = hubContext;
|
|
@@ -11950,7 +12059,7 @@ async function updateAgentPrompt(args) {
|
|
|
11950
12059
|
};
|
|
11951
12060
|
const systemPrompt = buildAgentUpdateSystemPrompt(promptInput);
|
|
11952
12061
|
const userPrompt = buildAgentUpdateUserPrompt(promptInput);
|
|
11953
|
-
info(
|
|
12062
|
+
info(`${flag}: refreshing prompt "${promptName}" on the hub (project ${project})`);
|
|
11954
12063
|
const { result, isError } = await invokeClaudeStreaming({
|
|
11955
12064
|
prompt: userPrompt,
|
|
11956
12065
|
systemPrompt,
|
|
@@ -11960,20 +12069,20 @@ async function updateAgentPrompt(args) {
|
|
|
11960
12069
|
...model ? { model } : {}
|
|
11961
12070
|
}, () => {});
|
|
11962
12071
|
if (isError || !result || result.trim().length === 0) {
|
|
11963
|
-
warn(
|
|
12072
|
+
warn(`${flag}: Claude returned no usable output${isError ? " (SDK error)" : ""}; leaving prompt "${promptName}" unchanged`);
|
|
11964
12073
|
return;
|
|
11965
12074
|
}
|
|
11966
12075
|
if (result.trim() === "NO_UPDATE") {
|
|
11967
|
-
info(
|
|
12076
|
+
info(`${flag}: no new learnings from this run; prompt "${promptName}" left unchanged`);
|
|
11968
12077
|
return;
|
|
11969
12078
|
}
|
|
11970
12079
|
const newText = stripCodeFences(result.trim()) + "\n";
|
|
11971
12080
|
await hub.putPrompt(project, promptName, newText);
|
|
11972
|
-
info(
|
|
11973
|
-
info(
|
|
12081
|
+
info(`${flag}: updated prompt "${promptName}" on the hub`);
|
|
12082
|
+
info(`${flag}: review it in the hub UI's Prompts tab`);
|
|
11974
12083
|
} catch (err) {
|
|
11975
12084
|
if (err instanceof HubApiError) {
|
|
11976
|
-
warn(
|
|
12085
|
+
warn(`${flag} skipped (hub request failed: ${err.status} ${err.code}: ${err.message})`);
|
|
11977
12086
|
return;
|
|
11978
12087
|
}
|
|
11979
12088
|
throw err;
|
|
@@ -11992,7 +12101,7 @@ function stripCodeFences(text) {
|
|
|
11992
12101
|
//#region src/cli/changed-specs.ts
|
|
11993
12102
|
/**
|
|
11994
12103
|
* Filter specs to those a range of commits reaches. Powers `ccqa run
|
|
11995
|
-
* --
|
|
12104
|
+
* --only-affected-by <ref>`; `ccqa audit` uses the same call.
|
|
11996
12105
|
*
|
|
11997
12106
|
* The decision is made by `ccqa select-specs`, which reads the diff against
|
|
11998
12107
|
* what each spec actually does. That costs one model call, against saving the
|
|
@@ -12003,8 +12112,8 @@ function stripCodeFences(text) {
|
|
|
12003
12112
|
* the safe reading of that is to run the spec.
|
|
12004
12113
|
*/
|
|
12005
12114
|
async function collectChangedSpecs(specs, opts) {
|
|
12006
|
-
const { cwd, base, model, quiet,
|
|
12007
|
-
const resolved = await resolveAnalysisBase(base, "--
|
|
12115
|
+
const { cwd, base, model, quiet, flagName } = opts;
|
|
12116
|
+
const resolved = await resolveAnalysisBase(base, flagName ?? "--only-affected-by", cwd);
|
|
12008
12117
|
const meta$2 = (key, value) => {
|
|
12009
12118
|
if (!quiet) meta(key, value);
|
|
12010
12119
|
};
|
|
@@ -12055,12 +12164,10 @@ async function resolveVitestConfig(cwd) {
|
|
|
12055
12164
|
}
|
|
12056
12165
|
/**
|
|
12057
12166
|
* Resolve the report directory. A report (report.json + evidence) is always
|
|
12058
|
-
* written
|
|
12059
|
-
* it lands, defaulting to `DEFAULT_REPORT_DIR`. `--report` with no value (a
|
|
12060
|
-
* bare boolean flag) also means "default location".
|
|
12167
|
+
* written, so `--report-dir` only picks *where* it lands.
|
|
12061
12168
|
*/
|
|
12062
|
-
function resolveReportDir(
|
|
12063
|
-
return resolve(cwd,
|
|
12169
|
+
function resolveReportDir(reportDir, cwd) {
|
|
12170
|
+
return resolve(cwd, reportDir ?? "ccqa-report");
|
|
12064
12171
|
}
|
|
12065
12172
|
/** De-dupe by `featureName/specName`, keeping first-seen order. */
|
|
12066
12173
|
function dedupeSpecs(specs) {
|
|
@@ -12082,13 +12189,14 @@ function dedupeSpecs(specs) {
|
|
|
12082
12189
|
* maps it to `process.exit(2)`).
|
|
12083
12190
|
*/
|
|
12084
12191
|
async function executeRun(targets, opts) {
|
|
12085
|
-
|
|
12086
|
-
|
|
12087
|
-
|
|
12192
|
+
const filtering = Boolean(opts.onlyAffectedBy || opts.onlyStale || opts.onlyAuditedClean);
|
|
12193
|
+
if (filtering && targets.length > 0) throw new RunUsageError("a --only-* filter and an explicit spec target cannot be combined");
|
|
12194
|
+
const rerunProfile = opts.onlyStale === true ? requireRerunProfile(opts.profile) : null;
|
|
12195
|
+
if (opts.onlyStaleWithUnknown && rerunProfile === null) warn("--only-stale-with-unknown is ignored: it only applies to --only-stale");
|
|
12088
12196
|
const forExecution = opts.dryRun !== true;
|
|
12089
12197
|
const cwd = opts.cwd ?? process.cwd();
|
|
12090
|
-
const wantsLastGreen = opts.
|
|
12091
|
-
const [head, fixedBase] = await Promise.all([getGitHead(cwd), forExecution && opts.
|
|
12198
|
+
const wantsLastGreen = opts.onFailExplain === true && opts.onFailExplainBase === void 0;
|
|
12199
|
+
const [head, fixedBase] = await Promise.all([getGitHead(cwd), forExecution && opts.onFailExplain && opts.onFailExplainBase !== void 0 ? resolveAnalysisBase(opts.onFailExplainBase, "--on-fail-explain-base", cwd) : null]);
|
|
12092
12200
|
const git = {
|
|
12093
12201
|
head,
|
|
12094
12202
|
base: wantsLastGreen ? {
|
|
@@ -12140,15 +12248,17 @@ async function executeRun(targets, opts) {
|
|
|
12140
12248
|
} catch {
|
|
12141
12249
|
hubCtx = null;
|
|
12142
12250
|
}
|
|
12143
|
-
if (wantsLastGreen && hubCtx == null) throw new RunUsageError(
|
|
12251
|
+
if (wantsLastGreen && hubCtx == null) throw new RunUsageError("--on-fail-explain needs a hub connection for the per-spec last-green baselines (--hub-url/--hub-token or CCQA_HUB_URL/CCQA_HUB_TOKEN), or an explicit --on-fail-explain-base <ref>");
|
|
12144
12252
|
const ledgerHub = wantsLastGreen ? hubCtx : null;
|
|
12145
|
-
if (rerunProfile !== null && hubCtx == null) throw new RunUsageError(
|
|
12146
|
-
|
|
12253
|
+
if (rerunProfile !== null && hubCtx == null) throw new RunUsageError("--only-stale requires a hub connection (--hub-url/--hub-token or CCQA_HUB_URL/CCQA_HUB_TOKEN)");
|
|
12254
|
+
if (opts.onlyAuditedClean && hubCtx == null) throw new RunUsageError("--only-audited-clean requires a hub connection (--hub-url/--hub-token or CCQA_HUB_URL/CCQA_HUB_TOKEN)");
|
|
12255
|
+
const [customPrompt, triageUserPrompt, ledgerEntries, rerunReport, fetchedDeployHead, auditedLedger] = await Promise.all([
|
|
12147
12256
|
forExecution ? fetchCustomPrompt(hubCtx) : null,
|
|
12148
12257
|
forExecution ? fetchTriageUserPrompt(hubCtx) : null,
|
|
12149
12258
|
forExecution && ledgerHub ? fetchLastGreenLedger(ledgerHub, opts.profile, cwd) : null,
|
|
12150
12259
|
rerunProfile !== null && hubCtx ? fetchRerunReport(hubCtx, rerunProfile) : null,
|
|
12151
|
-
forExecution && hubCtx && opts.profile && rerunProfile === null ? tryDeployHeadSha(hubCtx, opts.profile) : null
|
|
12260
|
+
forExecution && hubCtx && opts.profile && rerunProfile === null ? tryDeployHeadSha(hubCtx, opts.profile) : null,
|
|
12261
|
+
opts.onlyAuditedClean && hubCtx ? fetchAuditedLedger(hubCtx) : null
|
|
12152
12262
|
]);
|
|
12153
12263
|
const deployedSha = rerunReport?.deployHead.sha ?? fetchedDeployHead;
|
|
12154
12264
|
if (ledgerEntries) diffProvider = createDiffProvider({
|
|
@@ -12156,7 +12266,7 @@ async function executeRun(targets, opts) {
|
|
|
12156
12266
|
cwd
|
|
12157
12267
|
});
|
|
12158
12268
|
const triageUserPromptHash = triageUserPrompt ? hashTriageUserPrompt(triageUserPrompt) : null;
|
|
12159
|
-
const reportDir = resolveReportDir(opts.
|
|
12269
|
+
const reportDir = resolveReportDir(opts.reportDir, cwd);
|
|
12160
12270
|
const analysisDeps = {
|
|
12161
12271
|
diffProvider,
|
|
12162
12272
|
auth: diffProvider ? driftAuthAvailable() : {
|
|
@@ -12172,22 +12282,28 @@ async function executeRun(targets, opts) {
|
|
|
12172
12282
|
};
|
|
12173
12283
|
const enumerateAll = () => listAllSpecsWithSpecFile(cwd);
|
|
12174
12284
|
let specs = dedupeSpecs((await Promise.all((targets.length ? targets : [void 0]).map((t) => resolveSpecTargets(t, enumerateAll, cwd)))).flat());
|
|
12175
|
-
if (
|
|
12285
|
+
if (filtering) {
|
|
12176
12286
|
const before = specs.length;
|
|
12177
12287
|
let unanswerable = 0;
|
|
12178
12288
|
if (rerunReport) {
|
|
12179
|
-
const selection = selectSpecsNeedingRerun(specs, rerunReport, { includeUnknown: opts.
|
|
12289
|
+
const selection = selectSpecsNeedingRerun(specs, rerunReport, { includeUnknown: opts.onlyStaleWithUnknown === true });
|
|
12180
12290
|
specs = selection.selected;
|
|
12181
12291
|
unanswerable = selection.excludedUnanswerable;
|
|
12182
|
-
meta("
|
|
12183
|
-
meta("
|
|
12184
|
-
}
|
|
12292
|
+
meta("stale-base", `deploy ${rerunReport.deployHead.sha.slice(0, 12)} (profile ${rerunReport.profile})`);
|
|
12293
|
+
meta("stale-states", selection.summary);
|
|
12294
|
+
}
|
|
12295
|
+
if (auditedLedger) {
|
|
12296
|
+
const picked = selectAuditedClean(specs, auditedLedger);
|
|
12297
|
+
specs = picked.selected;
|
|
12298
|
+
meta("audit-states", `${picked.selected.length} clean, ${picked.drifted} drifted, ${picked.unaudited} never audited`);
|
|
12299
|
+
}
|
|
12300
|
+
if (opts.onlyAffectedBy) specs = (await collectChangedSpecs(specs, {
|
|
12185
12301
|
cwd,
|
|
12186
|
-
base: opts.
|
|
12302
|
+
base: opts.onlyAffectedBy,
|
|
12187
12303
|
...opts.model ? { model: opts.model } : {}
|
|
12188
12304
|
})).specs;
|
|
12189
|
-
meta("
|
|
12190
|
-
if (specs.length === 0 && unanswerable > 0) hint(`${unanswerable} spec(s) were excluded because the hub could not tell whether they need a re-run; pass --
|
|
12305
|
+
meta("selected", `${specs.length} of ${before} spec${before === 1 ? "" : "s"}`);
|
|
12306
|
+
if (specs.length === 0 && unanswerable > 0) hint(`${unanswerable} spec(s) were excluded because the hub could not tell whether they need a re-run; pass --only-stale-with-unknown to run them anyway`);
|
|
12191
12307
|
}
|
|
12192
12308
|
if (specs.length === 0) {
|
|
12193
12309
|
warn("no specs to run");
|
|
@@ -12210,11 +12326,11 @@ async function executeRun(targets, opts) {
|
|
|
12210
12326
|
if (dispatch.external.length > 0) meta("external", dispatch.external.map((g) => `${g.targetId} ${g.specs.length}`).join(" / "));
|
|
12211
12327
|
if (liveSpecs.length === 0) {
|
|
12212
12328
|
const why = "it only applies to agent-browser 'mode: live' specs, and this run has none";
|
|
12213
|
-
if (typeof opts.
|
|
12214
|
-
if (opts.
|
|
12215
|
-
if (opts.
|
|
12216
|
-
} else if (opts.
|
|
12217
|
-
if (detSpecs.length === 0 && opts.
|
|
12329
|
+
if (typeof opts.liveStepRetry === "number" && opts.liveStepRetry > 0) warn(`--live-step-retry is ignored: ${why}`);
|
|
12330
|
+
if (opts.liveArtifactsDir) warn(`--live-artifacts-dir is ignored: ${why}`);
|
|
12331
|
+
if (opts.learnLivePrompt) warn(`--learn-live-prompt is ignored: ${why}`);
|
|
12332
|
+
} else if (opts.liveArtifactsDir && liveSpecs.length > 1) warn("--out is ignored when running multiple live specs");
|
|
12333
|
+
if (detSpecs.length === 0 && opts.replaySkipEvidence === true) warn("--no-evidence is ignored: it only applies to agent-browser 'mode: deterministic' specs, and this run has none");
|
|
12218
12334
|
blank();
|
|
12219
12335
|
if (opts.dryRun) {
|
|
12220
12336
|
for (const line of formatDryRunLines(withMode, dispatch)) emitRaw(line + "\n");
|
|
@@ -12227,10 +12343,10 @@ async function executeRun(targets, opts) {
|
|
|
12227
12343
|
};
|
|
12228
12344
|
}
|
|
12229
12345
|
const det = await runDeterministicSpecs(detSpecs, opts, cwd, reportDir);
|
|
12230
|
-
if (opts.
|
|
12346
|
+
if (opts.reportToHub && hubCtx == null) warn("--push-report requires --hub-url/--hub-token (or CCQA_HUB_URL/CCQA_HUB_TOKEN); skipping push");
|
|
12231
12347
|
let hubRunId = null;
|
|
12232
12348
|
let hubSink;
|
|
12233
|
-
if (hubCtx != null && opts.
|
|
12349
|
+
if (hubCtx != null && opts.reportToHub) try {
|
|
12234
12350
|
const branch = await detectBranch(cwd);
|
|
12235
12351
|
const ciRunId = githubRunId();
|
|
12236
12352
|
const runUrl = githubRunUrl();
|
|
@@ -12293,10 +12409,10 @@ async function executeRun(targets, opts) {
|
|
|
12293
12409
|
const live = await runLiveSpecs(liveSpecs, {
|
|
12294
12410
|
...opts.model ? { model: opts.model } : {},
|
|
12295
12411
|
...opts.language ? { language: opts.language } : {},
|
|
12296
|
-
...opts.
|
|
12412
|
+
...opts.liveArtifactsDir && liveSpecs.length === 1 ? { out: opts.liveArtifactsDir } : {},
|
|
12297
12413
|
cwd,
|
|
12298
12414
|
reportDir,
|
|
12299
|
-
...typeof opts.
|
|
12415
|
+
...typeof opts.liveStepRetry === "number" ? { retry: opts.liveStepRetry } : {},
|
|
12300
12416
|
concurrency: opts.concurrency ?? 1,
|
|
12301
12417
|
...opts.profile ? { profile: opts.profile } : {},
|
|
12302
12418
|
diffProvider,
|
|
@@ -12360,10 +12476,11 @@ async function executeRun(targets, opts) {
|
|
|
12360
12476
|
}
|
|
12361
12477
|
}
|
|
12362
12478
|
}
|
|
12363
|
-
if (opts.
|
|
12479
|
+
if (opts.learnLivePrompt && liveSpecs.length > 0) {
|
|
12364
12480
|
blank();
|
|
12365
12481
|
await updateAgentPrompt({
|
|
12366
12482
|
kind: "live",
|
|
12483
|
+
flag: "--learn-live-prompt",
|
|
12367
12484
|
runSummary: buildLiveRunSummary(live.reportResults),
|
|
12368
12485
|
hubContext: hubCtx,
|
|
12369
12486
|
...opts.model ? { model: opts.model } : {},
|
|
@@ -12449,8 +12566,8 @@ async function runDeterministicSpecs(specs, opts, cwd, reportDirAbs) {
|
|
|
12449
12566
|
};
|
|
12450
12567
|
const tmpDir = await mkdtemp(join(tmpdir(), "ccqa-run-"));
|
|
12451
12568
|
const vitestConfig = await resolveVitestConfig(cwd);
|
|
12452
|
-
const captureOutput =
|
|
12453
|
-
const captureEvidence = opts.
|
|
12569
|
+
const captureOutput = true;
|
|
12570
|
+
const captureEvidence = opts.replaySkipEvidence !== true;
|
|
12454
12571
|
const concurrency = Math.max(1, opts.concurrency ?? 1);
|
|
12455
12572
|
const ctx = {
|
|
12456
12573
|
cwd,
|
|
@@ -12658,7 +12775,7 @@ async function writeUnifiedReport(args) {
|
|
|
12658
12775
|
const jsonPath = join(reportDir, "report.json");
|
|
12659
12776
|
await writeFile(jsonPath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
12660
12777
|
info(`run report (json) written to ${jsonPath}`);
|
|
12661
|
-
if (opts.
|
|
12778
|
+
if (opts.reportFormat === "github") for (const line of emitGithubAnnotations(data)) emitRaw(line + "\n");
|
|
12662
12779
|
return data;
|
|
12663
12780
|
}
|
|
12664
12781
|
/**
|
|
@@ -12901,14 +13018,14 @@ function installTeardownSignalHandlers(teardown) {
|
|
|
12901
13018
|
}
|
|
12902
13019
|
//#endregion
|
|
12903
13020
|
//#region src/cli/run.ts
|
|
12904
|
-
const runCommand = addHubOptions(addProfileOption(addLanguageOption(new Command("run").argument("[targets...]", "Specs to run, space-separated: each '<feature>/<spec>', '<feature>', or omit for all. Duplicates are de-duped.").description("Run specs, on any target. Agent-browser specs replay the recorded test.spec.ts under vitest (default), or, with spec.yaml `mode: live`, have Claude drive agent-browser live per step. External-target specs (playwright, runn) run through the target's configured `runCommand`. A structured report (report.json + evidence) is always written; use --
|
|
12905
|
-
if (REPORT_FORMATS.includes(raw)) return raw;
|
|
12906
|
-
throw new Error(`--format must be one of ${REPORT_FORMATS.join(" | ")}`);
|
|
12907
|
-
}, "text").option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").option("--no-evidence", `(deterministic only) Skip step-boundary evidence capture (PNG + meta JSON written to ${DEFAULT_REPORT_DIR}/${EVIDENCE_SUBDIR}/ by default).`).option("--retry <n>", "(live only) Retry each failed step up to N more times before recording failure. Default 0.", (raw) => {
|
|
13021
|
+
const runCommand = addHubOptions(addProfileOption(addLanguageOption(new Command("run").argument("[targets...]", "Specs to run, space-separated: each '<feature>/<spec>', '<feature>', or omit for all. Duplicates are de-duped.").description("Run specs, on any target. Agent-browser specs replay the recorded test.spec.ts under vitest (default), or, with spec.yaml `mode: live`, have Claude drive agent-browser live per step. External-target specs (playwright, runn) run through the target's configured `runCommand`. A structured report (report.json + evidence) is always written; use --report-to-hub to also stream it to a hub.").optionsGroup("Which specs to run:").option("--only-affected-by <ref>", "Only specs `ccqa select-specs` judges reached by the diff against <ref> (e.g. origin/main). In pull_request CI, pass $GITHUB_BASE_REF. Cannot be combined with an explicit spec id.").option("--only-stale", "Only specs whose last result no longer holds — each spec's own last run compared against the hub's deploy log. No git diff involved. Requires a hub connection and --profile.").option("--only-stale-with-unknown", "With --only-stale: also take specs whose re-run need the hub cannot answer ('unknown') and specs that never ran ('neverRun'). Off by default: an unanswerable question is reported, not guessed.").option("--only-audited-clean", "Only specs `ccqa audit` last found no drift in. A spec that has never been audited is not taken: this flag spends a run where a cheap audit already cleared the spec, and \"never looked\" is not that. Requires a hub connection.").option("--dry-run", "Print the specs this invocation would run, then exit 0 without executing anything and without writing a report. Works with every selection flag.").optionsGroup("How to run them:").option("--concurrency <n>", "Run up to N specs in parallel within each phase (deterministic / external-target / live), never across phases. Default 1 (sequential). Live specs each get an isolated agent-browser session; high values spawn many headed Chrome instances.", parseConcurrency$1, 1).option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").option("--live-step-retry <n>", "(live only) Retry each failed step up to N more times before recording failure. This retries a step, not the whole spec — see --on-fail-explain-rerun for that.", (raw) => {
|
|
12908
13022
|
const n = Number(raw);
|
|
12909
|
-
if (!Number.isFinite(n) || n < 0 || Math.floor(n) !== n) throw new Error(`--retry must be a non-negative integer, got "${raw}"`);
|
|
13023
|
+
if (!Number.isFinite(n) || n < 0 || Math.floor(n) !== n) throw new Error(`--live-step-retry must be a non-negative integer, got "${raw}"`);
|
|
12910
13024
|
return n;
|
|
12911
|
-
}, 0).option("--
|
|
13025
|
+
}, 0).option("--live-artifacts-dir <dir>", "(live only) Override the per-spec artifact directory. Default: <specDir>/runs/<runId>. Ignored when running multiple specs.").option("--replay-skip-evidence", `(deterministic replay only) Skip step-boundary evidence capture (PNG + meta JSON written to ${DEFAULT_REPORT_DIR}/${EVIDENCE_SUBDIR}/ by default).`).optionsGroup("What to do about failures:").option("--on-fail-explain", "Classify each failure against the source diff since the commit where that spec last passed (per-spec baselines from the hub). Off by default — no Claude calls without it.").option("--on-fail-explain-base <ref>", "With --on-fail-explain: diff against <ref> instead of each spec's last green. Use when there is no hub to hold the baselines.").optionsGroup("What to do with the results:").option("--report-dir <dir>", `Directory for the structured run results (report.json + evidence PNGs), which are always written. Default: ${DEFAULT_REPORT_DIR}/.`).option("--report-format <fmt>", "Additional output format alongside HTML: 'text' (default), 'json' (writes report.json), 'github' (GitHub Actions annotations on stdout).", (raw) => {
|
|
13026
|
+
if (REPORT_FORMATS.includes(raw)) return raw;
|
|
13027
|
+
throw new Error(`--report-format must be one of ${REPORT_FORMATS.join(" | ")}`);
|
|
13028
|
+
}, "text").option("--report-to-hub", "Incrementally push the run report to the hub as the run progresses (open → patch per spec → finalize). Requires --hub-url/--hub-token (or CCQA_HUB_URL/CCQA_HUB_TOKEN). Without it, hub credentials are used only to fetch variables/sessions/prompts, not to push.").optionsGroup("Learning:").option("--learn-live-prompt", "(live only) After the run finishes, ask Claude to refresh the \"live.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(async (targets, opts) => {
|
|
12912
13029
|
await runCliAction(targets, opts);
|
|
12913
13030
|
});
|
|
12914
13031
|
/** Parse --concurrency: a positive integer. Rejects 0, negatives, non-integers. */
|
|
@@ -12920,12 +13037,16 @@ function parseConcurrency$1(raw) {
|
|
|
12920
13037
|
}
|
|
12921
13038
|
return n;
|
|
12922
13039
|
}
|
|
12923
|
-
/** Header label shown after `ccqa run`: the lone target, a count, or
|
|
13040
|
+
/** Header label shown after `ccqa run`: the lone target, a count, or how they were selected. */
|
|
12924
13041
|
function headerTarget(targets, opts) {
|
|
12925
13042
|
if (targets.length === 1) return targets[0];
|
|
12926
13043
|
if (targets.length > 1) return `${targets.length} targets`;
|
|
12927
|
-
|
|
12928
|
-
|
|
13044
|
+
const filters = [
|
|
13045
|
+
opts.onlyAffectedBy ? "affected" : null,
|
|
13046
|
+
opts.onlyStale ? "stale" : null,
|
|
13047
|
+
opts.onlyAuditedClean ? "audited clean" : null
|
|
13048
|
+
].filter((s) => s !== null);
|
|
13049
|
+
return filters.length === 0 ? "(all specs)" : `(${filters.join(" + ")})`;
|
|
12929
13050
|
}
|
|
12930
13051
|
/**
|
|
12931
13052
|
* CLI entry point: calls the library pipeline and maps its result back to a
|
|
@@ -13183,8 +13304,8 @@ For each step:
|
|
|
13183
13304
|
3. Execute the action using an ALLOWED selector (see Selector Rules), prefixing the command with \`CCQA_STEP=<step-id>\` like every agent-browser command in the step.
|
|
13184
13305
|
4. Emit \`AB_ACTION|...\` for every browser action (see AB_ACTION Protocol).
|
|
13185
13306
|
5. Run \`snapshot\` again to verify the outcome.
|
|
13186
|
-
6. Confirm at least **two independent signals** (URL change, element appearance, text change, ...).
|
|
13187
|
-
7. Record
|
|
13307
|
+
6. Confirm at least **two independent signals** (URL change, element appearance, text change, ...). This is how *you* decide the step worked and it is safe to continue. It is not what gets recorded.
|
|
13308
|
+
7. Record as assertions only the signals the step's own \`expected\` asks about, by putting a \`CCQA_ASSERT=<marker>\` prefix on the verification command itself (see Assertion Protocol). Only the assert types markers cannot express fall back to \`AB_ACTION|assert|...\` text lines.
|
|
13188
13309
|
8. Emit \`STEP_DONE\`, \`ASSERTION_FAILED\`, or \`STEP_SKIPPED\`.
|
|
13189
13310
|
|
|
13190
13311
|
**Protocol lines are recorded ONLY from your plain assistant text.** Every
|
|
@@ -13315,7 +13436,9 @@ CCQA_STEP=<step-id> CCQA_ASSERT=url_contains:/dashboard agent-browser --session
|
|
|
13315
13436
|
- A marked command that exits non-zero records nothing — fix the check and re-run it.
|
|
13316
13437
|
- A marker that doesn't match its command (e.g. \`CCQA_ASSERT=1\` on a \`click\`) is ignored with a warning — never do that.
|
|
13317
13438
|
- \`get count\` and \`get url\` exit 0 regardless of what they print. **Read the output**: if the printed count / URL contradicts the marker, the signal did NOT verify — treat it as a failed verification (emit \`ASSERTION_FAILED\` if the step cannot be confirmed another way).
|
|
13318
|
-
-
|
|
13439
|
+
- **Assert what the step asks about, nothing else.** The \`expected\` is the contract; anything else you happened to see on the way is not. A nav item, a heading or a greeting that the step never mentions adds no coverage, differs between recordings of the same spec, and is the first thing to break on replay — so the next recording quietly drops it and the test gets weaker without anyone deciding that.
|
|
13440
|
+
|
|
13441
|
+
- **\`url_contains\` is opt-in, not a habit.** The same rule, in the form that gets broken most. Emit it ONLY when a step's own \`expected\` explicitly asks about the URL or path. Do NOT add a \`url_contains\` to "prove" a login succeeded, a page loaded, or a navigation happened — confirm those with \`text_visible\` / \`element_visible\` on something the destination page renders. An unrequested URL assertion adds no coverage the visible-content assert doesn't already give, and is the single most common way an environment gets baked into a test.
|
|
13319
13442
|
- **When you do assert a URL, the substring may come from ONE place only:** a \`\${VAR}\` URL that *this step's own instruction* opened, written as that \`\${VAR}\` followed by the literal tail after it. If the step opens \`\${APP_URL}/policies\`, assert \`\${APP_URL}/policies\` (or the tail \`/policies\`); the recorder resolves \`\${APP_URL}\` per environment. Never assert on a URL you merely *observed* — login redirects, identity-provider pages, and OAuth callbacks all live on a **different, environment-named origin** than the app, so any substring of them (host, origin, OR path) names the environment.
|
|
13320
13443
|
- **A leading slash does NOT make a substring safe.** \`/auth-staging\`, \`/env-qa\`, \`/tenant-acme\` look like paths but are environment labels — the first segment of an identity-provider or tenant URL, not an application route. If a substring contains an environment name, a stage token (\`dev\`, \`stg\`, \`prod\`), a tenant/org name, or any fragment of a hostname, it is forbidden even with a leading slash. The only safe path substrings are stable *application* routes off the app's own origin (\`/dashboard\`, \`/policies/new\`), taken from a \`\${VAR}\` you opened — not from a redirect you watched.
|
|
13321
13444
|
|
|
@@ -14423,8 +14546,8 @@ For each test case above, write a 1–2 sentence factual \`summary\` of what it
|
|
|
14423
14546
|
}
|
|
14424
14547
|
//#endregion
|
|
14425
14548
|
//#region src/cli/perspectives.ts
|
|
14426
|
-
const perspectivesCommand = addHubOptions(addLanguageOption(new Command("perspectives").description("Generate/update the project's perspectives document on the hub — a factual inventory of existing test coverage (no severity, no gap analysis)").option("--instruction <text>", "Hint to steer how summaries are written").option("--
|
|
14427
|
-
if (opts.
|
|
14549
|
+
const perspectivesCommand = addHubOptions(addLanguageOption(new Command("perspectives").description("Generate/update the project's perspectives document on the hub — a factual inventory of existing test coverage (no severity, no gap analysis)").option("--instruction <text>", "Hint to steer how summaries are written").option("-y, --yes", "Apply without asking [y/N]", false).option("--verify", "Check the hub document against the local specs (mechanical fields only) and exit 1 when it is stale. No Claude calls — cheap enough for CI.", false).option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID").option("--project <name>", "Hub project to store the document under (default: cwd directory name)"))).action(withHubErrors(async (opts) => {
|
|
14550
|
+
if (opts.verify) await runPerspectivesCheck(opts);
|
|
14428
14551
|
else await runPerspectives(opts);
|
|
14429
14552
|
}));
|
|
14430
14553
|
/**
|
|
@@ -14547,7 +14670,7 @@ async function runPerspectives(opts) {
|
|
|
14547
14670
|
info("--- proposed changes (YAML view of the hub document) ---");
|
|
14548
14671
|
printUnifiedDiff(existingYaml, next);
|
|
14549
14672
|
blank();
|
|
14550
|
-
if (!(opts.
|
|
14673
|
+
if (!(opts.yes === true || /^y/i.test(await prompt(useJapanesePrompts(opts.language) ? "hub に perspectives を保存しますか? [y/N] " : "Push perspectives to the hub? [y/N] ")))) {
|
|
14551
14674
|
info("aborted — no changes written.");
|
|
14552
14675
|
return;
|
|
14553
14676
|
}
|
|
@@ -14978,7 +15101,7 @@ async function runGenerateLocked(featureName, specName, opts, cwd) {
|
|
|
14978
15101
|
}, cwd) ?? null;
|
|
14979
15102
|
if (existingOutput && !opts.force) {
|
|
14980
15103
|
if (!await confirmOverwrite(existingOutput)) {
|
|
14981
|
-
info("aborted; pass --
|
|
15104
|
+
info("aborted; pass --overwrite to replace it without prompting");
|
|
14982
15105
|
return;
|
|
14983
15106
|
}
|
|
14984
15107
|
}
|
|
@@ -15033,6 +15156,7 @@ async function runGenerateAgentPromptUpdate(target, featureName, specName, resul
|
|
|
15033
15156
|
blank();
|
|
15034
15157
|
await updateAgentPrompt({
|
|
15035
15158
|
kind: target.guidanceKind,
|
|
15159
|
+
flag: "--learn-codegen-prompt",
|
|
15036
15160
|
runSummary: buildGenerateRunSummary(target.id, featureName, specName, result, cwd),
|
|
15037
15161
|
hubContext: opts.hubContext ?? null,
|
|
15038
15162
|
...opts.model ? { model: opts.model } : {},
|
|
@@ -15058,7 +15182,7 @@ async function confirmOverwrite(path) {
|
|
|
15058
15182
|
rl.close();
|
|
15059
15183
|
}
|
|
15060
15184
|
}
|
|
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("--
|
|
15185
|
+
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.").optionsGroup("How to generate:").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("--auto-fix-max-retries <n>", "Maximum number of auto-fix retries", "3").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 previously generated test code without warning").optionsGroup("Learning:").option("--learn-codegen-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.").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) => {
|
|
15062
15186
|
const { featureName, specName } = parseSpecPath(specPath);
|
|
15063
15187
|
const language = opts.language ?? "auto";
|
|
15064
15188
|
const cwd = resolveCwd(opts.cwd);
|
|
@@ -15087,16 +15211,16 @@ const generateCommand = addHubOptions(addProfileOption(addLanguageOption(new Com
|
|
|
15087
15211
|
} : null;
|
|
15088
15212
|
try {
|
|
15089
15213
|
await runGenerate(featureName, specName, {
|
|
15090
|
-
maxRetries: parseInt(opts.
|
|
15214
|
+
maxRetries: parseInt(opts.autoFixMaxRetries ?? "3", 10),
|
|
15091
15215
|
fixMode: toFixMode(opts.autoFix ?? "interactive"),
|
|
15092
|
-
force: opts.
|
|
15093
|
-
useSnapshot: opts.
|
|
15216
|
+
force: opts.overwrite ?? false,
|
|
15217
|
+
useSnapshot: opts.sessionPin !== false,
|
|
15094
15218
|
language,
|
|
15095
15219
|
model: opts.model,
|
|
15096
15220
|
targetOverride: opts.target,
|
|
15097
15221
|
cwd,
|
|
15098
15222
|
hubContext,
|
|
15099
|
-
updateAgentPrompt: opts.
|
|
15223
|
+
updateAgentPrompt: opts.learnCodegenPrompt ?? false
|
|
15100
15224
|
});
|
|
15101
15225
|
} catch (e) {
|
|
15102
15226
|
if (e instanceof SpecLockedError) {
|
|
@@ -15132,10 +15256,10 @@ function reportCost() {
|
|
|
15132
15256
|
//#endregion
|
|
15133
15257
|
//#region src/cli/record.ts
|
|
15134
15258
|
const VALIDATION_MODES = ["lenient", "strict"];
|
|
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
|
|
15259
|
+
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) => {
|
|
15136
15260
|
if (VALIDATION_MODES.includes(raw)) return raw;
|
|
15137
|
-
throw new Error(`--validation
|
|
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("--
|
|
15261
|
+
throw new Error(`--trace-validation must be one of ${VALIDATION_MODES.join(" | ")}`);
|
|
15262
|
+
}, "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").optionsGroup("Learning:").option("--learn-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) => {
|
|
15139
15263
|
await withCostTally(async () => {
|
|
15140
15264
|
try {
|
|
15141
15265
|
await runRecord(specPath, opts);
|
|
@@ -15147,10 +15271,6 @@ const recordCommand = addHubOptions(addProfileOption(addLanguageOption(new Comma
|
|
|
15147
15271
|
async function runRecord(specPath, opts) {
|
|
15148
15272
|
const { featureName, specName } = parseSpecPath(specPath);
|
|
15149
15273
|
const language = opts.language ?? "auto";
|
|
15150
|
-
if (opts.skipTrace && opts.skipCodegen) {
|
|
15151
|
-
error("--skip-trace and --skip-codegen cannot be combined; nothing would run");
|
|
15152
|
-
process.exit(2);
|
|
15153
|
-
}
|
|
15154
15274
|
const cwdForProfile = resolveCwd(opts.cwd);
|
|
15155
15275
|
const spec = parseTestSpec(await readSpecFile(featureName, specName, cwdForProfile));
|
|
15156
15276
|
const config = await loadProjectConfig(cwdForProfile);
|
|
@@ -15192,18 +15312,16 @@ async function runRecord(specPath, opts) {
|
|
|
15192
15312
|
});
|
|
15193
15313
|
let traceResult = null;
|
|
15194
15314
|
try {
|
|
15195
|
-
|
|
15196
|
-
|
|
15197
|
-
|
|
15198
|
-
|
|
15199
|
-
|
|
15200
|
-
|
|
15201
|
-
|
|
15202
|
-
if (!opts.skipCodegen) await runGenerate(featureName, specName, {
|
|
15203
|
-
maxRetries: parseInt(opts.maxRetries ?? "3", 10),
|
|
15315
|
+
traceResult = await runTrace(featureName, specName, opts.model, opts.traceValidation ?? "lenient", language, {
|
|
15316
|
+
cwd: cwdForProfile,
|
|
15317
|
+
hubContext
|
|
15318
|
+
});
|
|
15319
|
+
blank();
|
|
15320
|
+
if (!opts.traceOnly) await runGenerate(featureName, specName, {
|
|
15321
|
+
maxRetries: parseInt(opts.autoFixMaxRetries ?? "3", 10),
|
|
15204
15322
|
fixMode: toFixMode(opts.autoFix ?? "interactive"),
|
|
15205
|
-
force: opts.
|
|
15206
|
-
useSnapshot: opts.
|
|
15323
|
+
force: opts.overwrite ?? false,
|
|
15324
|
+
useSnapshot: opts.sessionPin !== false,
|
|
15207
15325
|
language,
|
|
15208
15326
|
model: opts.model,
|
|
15209
15327
|
cwd: cwdForProfile,
|
|
@@ -15212,11 +15330,11 @@ async function runRecord(specPath, opts) {
|
|
|
15212
15330
|
} finally {
|
|
15213
15331
|
await releaseLock();
|
|
15214
15332
|
}
|
|
15215
|
-
if (opts.
|
|
15216
|
-
else {
|
|
15333
|
+
if (opts.learnTracePrompt && traceResult !== null) {
|
|
15217
15334
|
blank();
|
|
15218
15335
|
await updateAgentPrompt({
|
|
15219
15336
|
kind: "record",
|
|
15337
|
+
flag: "--learn-trace-prompt",
|
|
15220
15338
|
runSummary: buildRecordRunSummary(featureName, specName, traceResult),
|
|
15221
15339
|
hubContext,
|
|
15222
15340
|
...opts.model ? { model: opts.model } : {},
|
|
@@ -15498,7 +15616,7 @@ function specStatus(result, threshold) {
|
|
|
15498
15616
|
}
|
|
15499
15617
|
/**
|
|
15500
15618
|
* Adapts `ccqa drift` results into the shared RunReportData shape so they can
|
|
15501
|
-
* be pushed to the hub (`ccqa
|
|
15619
|
+
* be pushed to the hub (`ccqa audit --report-to-hub`) and rendered by the same report
|
|
15502
15620
|
* UI as `ccqa run`/`ccqa live`. Browser-execution fields (testCounts,
|
|
15503
15621
|
* evidence, liveRun, ...) don't apply to a drift audit and are always null —
|
|
15504
15622
|
* which is why `mode` is carried separately: nothing ran, but which surfaces
|
|
@@ -15544,35 +15662,34 @@ function driftResultsToReport(results, meta) {
|
|
|
15544
15662
|
};
|
|
15545
15663
|
}
|
|
15546
15664
|
//#endregion
|
|
15547
|
-
//#region src/cli/
|
|
15665
|
+
//#region src/cli/audit.ts
|
|
15548
15666
|
const DEFAULT_CONCURRENCY = 3;
|
|
15549
|
-
const
|
|
15550
|
-
await withCostTally(() =>
|
|
15667
|
+
const auditCommand = addLanguageOption(new Command("audit").argument("[feature/spec]", "Optional spec id. If omitted, every spec under .ccqa/features/ is checked.").description("Read each spec against the code it describes and report where the two have drifted. Static: no browser is run, so this is the cheap check to put in front of `ccqa run`.").optionsGroup("Which specs to audit:").option("--only-affected-by <ref>", "Only specs `ccqa select-specs` judges reached by the diff against <ref> (e.g. origin/main). In pull_request CI, pass $GITHUB_BASE_REF. Costs one model call; specs it cannot decide are audited rather than skipped.").optionsGroup("How to run it:").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.").optionsGroup("What to do with the results:").option("--report-format <fmt>", "Output format: text | json | github", "text").option("--report-to-hub", "Push the result to a ccqa hub as a run (kind: drift), which is what updates the drift ledger `ccqa run --only-audited-clean` reads.").option("--exit-on <level>", "Exit non-zero on this severity or higher: warn | error", "error").optionsGroup("Environment and connection:").option("--cwd <path>", "Working directory used as both the .ccqa root and the codebase Claude reads. Useful for monorepos. Defaults to process.cwd().").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) => {
|
|
15668
|
+
await withCostTally(() => runAudit(specPath, opts));
|
|
15551
15669
|
}));
|
|
15552
|
-
async function
|
|
15553
|
-
const format = parseFormat$1(opts.
|
|
15554
|
-
const threshold = parseSeverity(opts.
|
|
15670
|
+
async function runAudit(specPath, opts) {
|
|
15671
|
+
const format = parseFormat$1(opts.reportFormat);
|
|
15672
|
+
const threshold = parseSeverity(opts.exitOn);
|
|
15555
15673
|
const concurrency = parseConcurrency(opts.concurrency);
|
|
15556
15674
|
const cwd = resolveCwd(opts.cwd);
|
|
15557
15675
|
await ensureCcqaDir(cwd);
|
|
15558
|
-
if (opts.
|
|
15559
|
-
error("--
|
|
15676
|
+
if (opts.onlyAffectedBy && specPath) {
|
|
15677
|
+
error("--only-affected-by and an explicit spec id cannot be combined; it only applies to a full sweep");
|
|
15560
15678
|
process.exit(2);
|
|
15561
15679
|
}
|
|
15562
15680
|
let targets = await collectTargets(specPath, cwd);
|
|
15563
15681
|
if (targets.length === 0) exitWithNoSpecs(format, "no test specs found under .ccqa/features/");
|
|
15564
15682
|
if (format === "text") {
|
|
15565
|
-
header("
|
|
15683
|
+
header("audit", specPath ?? `${targets.length} spec${targets.length > 1 ? "s" : ""}`);
|
|
15566
15684
|
if (opts.cwd) meta("cwd", cwd);
|
|
15567
15685
|
}
|
|
15568
15686
|
let baseRef = null;
|
|
15569
|
-
if (opts.
|
|
15687
|
+
if (opts.onlyAffectedBy) {
|
|
15570
15688
|
const total = targets.length;
|
|
15571
15689
|
const selection = await collectChangedSpecs(targets, {
|
|
15572
15690
|
cwd,
|
|
15573
|
-
base: opts.
|
|
15691
|
+
base: opts.onlyAffectedBy,
|
|
15574
15692
|
quiet: format !== "text",
|
|
15575
|
-
baseExample: "--base origin/main",
|
|
15576
15693
|
...opts.model ? { model: opts.model } : {}
|
|
15577
15694
|
});
|
|
15578
15695
|
targets = selection.specs;
|
|
@@ -15593,7 +15710,7 @@ async function runDrift(specPath, opts) {
|
|
|
15593
15710
|
}
|
|
15594
15711
|
});
|
|
15595
15712
|
process.stdout.write(renderDrift(results, format, cwd));
|
|
15596
|
-
if (opts.
|
|
15713
|
+
if (opts.reportToHub) await pushDriftResults({
|
|
15597
15714
|
results,
|
|
15598
15715
|
threshold,
|
|
15599
15716
|
cwd,
|
|
@@ -15607,7 +15724,7 @@ async function runDrift(specPath, opts) {
|
|
|
15607
15724
|
/**
|
|
15608
15725
|
* Push a finished drift audit to a ccqa hub as a `kind: "drift"` run, so it
|
|
15609
15726
|
* shows up alongside `ccqa run` runs in the hub UI. Best-effort: a missing
|
|
15610
|
-
* hub connection warns and returns rather than failing the command (`--
|
|
15727
|
+
* hub connection warns and returns rather than failing the command (`--report-to-hub`
|
|
15611
15728
|
* never changes drift's own exit code).
|
|
15612
15729
|
*
|
|
15613
15730
|
* `resolveHub` is injectable so tests can supply a fake `HubClient` without
|
|
@@ -15617,7 +15734,7 @@ async function pushDriftResults(args, resolveHub = resolveHubClient) {
|
|
|
15617
15734
|
const { results, threshold, cwd, opts, format, baseRef } = args;
|
|
15618
15735
|
const hub = resolveHub(opts);
|
|
15619
15736
|
if (!hub) {
|
|
15620
|
-
warn("--
|
|
15737
|
+
warn("--report-to-hub requires a hub connection (--hub-url/--hub-token or CCQA_HUB_URL/CCQA_HUB_TOKEN) — skipping push");
|
|
15621
15738
|
return;
|
|
15622
15739
|
}
|
|
15623
15740
|
try {
|
|
@@ -15803,102 +15920,6 @@ function parseFormat(raw) {
|
|
|
15803
15920
|
process.exit(2);
|
|
15804
15921
|
}
|
|
15805
15922
|
//#endregion
|
|
15806
|
-
//#region src/cli/session.ts
|
|
15807
|
-
const AB = resolveAgentBrowserBin$1();
|
|
15808
|
-
/**
|
|
15809
|
-
* Run agent-browser attached to the user's terminal (no timeout, inherited
|
|
15810
|
-
* stdio) so a human can complete an interactive login during `bootstrap`.
|
|
15811
|
-
* Distinct from runtime/spawn-ab.ts, which pipes stdio and hard-times-out for
|
|
15812
|
-
* non-interactive automation.
|
|
15813
|
-
*/
|
|
15814
|
-
function runAbInteractive(args) {
|
|
15815
|
-
return spawnSync(AB, args, { stdio: "inherit" }).status ?? 1;
|
|
15816
|
-
}
|
|
15817
|
-
function validateName(name) {
|
|
15818
|
-
const parsed = SessionNameSchema.safeParse(name);
|
|
15819
|
-
if (!parsed.success) {
|
|
15820
|
-
error(`invalid session name "${name}": ${parsed.error.issues[0]?.message ?? "bad name"}`);
|
|
15821
|
-
process.exit(2);
|
|
15822
|
-
}
|
|
15823
|
-
return parsed.data;
|
|
15824
|
-
}
|
|
15825
|
-
const profileOption = ["--profile <name>", "Sessions bucket to read/write on the hub. Defaults to 'default'."];
|
|
15826
|
-
const projectOption = ["--project <name>", "Project the session belongs to on the hub. Defaults to the current directory's name."];
|
|
15827
|
-
const bootstrapCommand = new Command("bootstrap").description("Open a headed browser so you can log in by hand, then upload the resulting session (cookies + localStorage) to the hub for `session:` specs to restore.").argument("<name>", "Session name to save").option("--url <url>", "URL to open first (e.g. the login page). Omit to start with a blank tab.").option(...profileOption).option(...hubUrlOption).option(...hubTokenOption).option(...projectOption).option("--cwd <path>", "Directory the default --project name is derived from (defaults to the current directory).").action(async (rawName, opts) => {
|
|
15828
|
-
const name = validateName(rawName);
|
|
15829
|
-
resolveCwd(opts.cwd);
|
|
15830
|
-
const project = resolveProject(opts);
|
|
15831
|
-
let hub;
|
|
15832
|
-
try {
|
|
15833
|
-
hub = requireHubClient(opts);
|
|
15834
|
-
} catch (err) {
|
|
15835
|
-
if (!(err instanceof HubConnectionError)) throw err;
|
|
15836
|
-
error(err.message);
|
|
15837
|
-
process.exit(2);
|
|
15838
|
-
}
|
|
15839
|
-
header("session bootstrap", name);
|
|
15840
|
-
meta("project", project);
|
|
15841
|
-
meta("profile", opts.profile ?? "default");
|
|
15842
|
-
blank();
|
|
15843
|
-
const openArgs = [
|
|
15844
|
-
"--headed",
|
|
15845
|
-
"open",
|
|
15846
|
-
...opts.url ? [opts.url] : ["about:blank"]
|
|
15847
|
-
];
|
|
15848
|
-
info("opening a browser — log in by hand, then return here.");
|
|
15849
|
-
const openStatus = runAbInteractive(openArgs);
|
|
15850
|
-
if (openStatus !== 0) {
|
|
15851
|
-
error(`agent-browser open exited ${openStatus}`);
|
|
15852
|
-
process.exit(1);
|
|
15853
|
-
}
|
|
15854
|
-
const rl = createInterface({
|
|
15855
|
-
input: process.stdin,
|
|
15856
|
-
output: process.stdout
|
|
15857
|
-
});
|
|
15858
|
-
await rl.question("\nPress Enter once you are fully logged in to save the session… ");
|
|
15859
|
-
rl.close();
|
|
15860
|
-
const tmpDir = await mkdtemp(join(tmpdir(), "ccqa-session-bootstrap-"));
|
|
15861
|
-
try {
|
|
15862
|
-
const tmpPath = join(tmpDir, "state.json");
|
|
15863
|
-
const saveStatus = runAbInteractive([
|
|
15864
|
-
"state",
|
|
15865
|
-
"save",
|
|
15866
|
-
tmpPath
|
|
15867
|
-
]);
|
|
15868
|
-
runAbInteractive(["close"]);
|
|
15869
|
-
if (saveStatus !== 0) {
|
|
15870
|
-
error(`agent-browser state save exited ${saveStatus}`);
|
|
15871
|
-
process.exit(1);
|
|
15872
|
-
}
|
|
15873
|
-
const state = await loadStorageState(tmpPath);
|
|
15874
|
-
let payload = state;
|
|
15875
|
-
if (opts.url) {
|
|
15876
|
-
info("verifying the saved session restores to a signed-in page…");
|
|
15877
|
-
const check = verifySessionRestores(tmpPath, opts.url);
|
|
15878
|
-
if (!check.restored) {
|
|
15879
|
-
error(`session did not restore cleanly: ${check.reason}`);
|
|
15880
|
-
hint("fully load the application (sign in, open the target workspace/page, wait for it to settle) before pressing Enter, then run bootstrap again. Nothing was uploaded.");
|
|
15881
|
-
process.exit(1);
|
|
15882
|
-
}
|
|
15883
|
-
info("restore verified — the session starts signed in.");
|
|
15884
|
-
payload = {
|
|
15885
|
-
...state,
|
|
15886
|
-
[SESSION_VERIFY_URL_KEY]: opts.url
|
|
15887
|
-
};
|
|
15888
|
-
} else warn("no --url given — the session can't be verified now, and runs can't health-check it before executing steps; strongly consider re-running with --url <a signed-in page URL>.");
|
|
15889
|
-
await hub.putSession(project, opts.profile ?? "default", name, payload);
|
|
15890
|
-
} finally {
|
|
15891
|
-
await rm(tmpDir, {
|
|
15892
|
-
recursive: true,
|
|
15893
|
-
force: true
|
|
15894
|
-
});
|
|
15895
|
-
}
|
|
15896
|
-
blank();
|
|
15897
|
-
info(`uploaded session "${name}" to the hub (encrypted at rest)`);
|
|
15898
|
-
hint("reference it from a spec with: session: " + name);
|
|
15899
|
-
});
|
|
15900
|
-
const sessionCommand = new Command("session").description("Manage saved browser sessions (cookies + localStorage) for `session:` specs. Use `ccqa hub session ls` to list sessions stored on the hub.").addCommand(bootstrapCommand);
|
|
15901
|
-
//#endregion
|
|
15902
15923
|
//#region src/hub/api/respond.ts
|
|
15903
15924
|
/** The message of an unknown throwable, for a log line or an error body. */
|
|
15904
15925
|
function errMsg(err) {
|
|
@@ -23364,17 +23385,21 @@ function resolvePackageJson() {
|
|
|
23364
23385
|
const { version } = JSON.parse(readFileSync(resolvePackageJson(), "utf8"));
|
|
23365
23386
|
const program = new Command();
|
|
23366
23387
|
program.name("ccqa").description("E2E test CLI powered by Claude Code — agent-browser by default, or Playwright / runn targets").version(version);
|
|
23388
|
+
program.commandsGroup("Write specs:");
|
|
23367
23389
|
program.addCommand(initCommand);
|
|
23368
23390
|
program.addCommand(draftCommand);
|
|
23369
23391
|
program.addCommand(perspectivesCommand);
|
|
23392
|
+
program.commandsGroup("Build tests from them:");
|
|
23370
23393
|
program.addCommand(recordCommand);
|
|
23371
23394
|
program.addCommand(generateCommand);
|
|
23395
|
+
program.commandsGroup("Check them:");
|
|
23372
23396
|
program.addCommand(runCommand);
|
|
23373
|
-
program.addCommand(
|
|
23374
|
-
program.
|
|
23375
|
-
program.addCommand(sessionCommand);
|
|
23376
|
-
program.addCommand(serveCommand);
|
|
23397
|
+
program.addCommand(auditCommand);
|
|
23398
|
+
program.commandsGroup("Hub:");
|
|
23377
23399
|
program.addCommand(hubCommand);
|
|
23400
|
+
program.addCommand(serveCommand);
|
|
23401
|
+
program.commandsGroup("Building blocks:");
|
|
23402
|
+
program.addCommand(selectSpecsCommand);
|
|
23378
23403
|
program.parse();
|
|
23379
23404
|
//#endregion
|
|
23380
23405
|
export {};
|