ccqa 1.7.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/ccqa.mjs +241 -62
- package/dist/hub-client/index.d.mts +8 -0
- package/dist/hub-client/index.mjs +2 -0
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -789,10 +789,26 @@ async function runPool(items, concurrency, fn) {
|
|
|
789
789
|
//#endregion
|
|
790
790
|
//#region src/drift/auth.ts
|
|
791
791
|
/**
|
|
792
|
+
* Claude Code can also run against AWS Bedrock / Google Vertex AI, selected by
|
|
793
|
+
* these env toggles. Credentials then come from the cloud SDK's own chain
|
|
794
|
+
* (instance/task roles, gcloud auth, …), so none of the Anthropic-side probes
|
|
795
|
+
* below apply — a set toggle counts as auth being available. ccqa forwards the
|
|
796
|
+
* toggle verbatim; only "0"/"false" (any case) is treated as explicitly off.
|
|
797
|
+
*/
|
|
798
|
+
const CLOUD_PROVIDER_ENV_KEYS = ["CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX"];
|
|
799
|
+
function cloudProviderEnabled() {
|
|
800
|
+
return CLOUD_PROVIDER_ENV_KEYS.some((key) => {
|
|
801
|
+
const value = process.env[key]?.trim().toLowerCase();
|
|
802
|
+
return value !== void 0 && value !== "" && value !== "0" && value !== "false";
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
792
806
|
* Probe whether the host has any credential the Anthropic SDK can pick up:
|
|
793
807
|
* 1. ANTHROPIC_API_KEY env var (CI / scripted use)
|
|
794
|
-
* 2.
|
|
795
|
-
*
|
|
808
|
+
* 2. CLAUDE_CODE_USE_BEDROCK / CLAUDE_CODE_USE_VERTEX (cloud-provider
|
|
809
|
+
* endpoints authenticated by the cloud SDK's credential chain)
|
|
810
|
+
* 3. ~/.claude/.credentials.json (Claude Code login, file-based platforms)
|
|
811
|
+
* 4. macOS Keychain item "Claude Code-credentials" (Claude Code login on
|
|
796
812
|
* darwin stores the OAuth credentials in the Keychain, not on disk)
|
|
797
813
|
*
|
|
798
814
|
* Claude-driven hooks are opt-in, so the caller only consults this after the
|
|
@@ -802,11 +818,12 @@ async function runPool(items, concurrency, fn) {
|
|
|
802
818
|
function driftAuthAvailable() {
|
|
803
819
|
const key = process.env["ANTHROPIC_API_KEY"];
|
|
804
820
|
if (typeof key === "string" && key.length > 0) return { ok: true };
|
|
821
|
+
if (cloudProviderEnabled()) return { ok: true };
|
|
805
822
|
if (existsSync(join(homedir(), ".claude", ".credentials.json"))) return { ok: true };
|
|
806
823
|
if (process.platform === "darwin" && keychainHasClaudeCredentials()) return { ok: true };
|
|
807
824
|
return {
|
|
808
825
|
ok: false,
|
|
809
|
-
reason: "no ANTHROPIC_API_KEY / claude login"
|
|
826
|
+
reason: "no ANTHROPIC_API_KEY / Bedrock or Vertex env / claude login"
|
|
810
827
|
};
|
|
811
828
|
}
|
|
812
829
|
/**
|
|
@@ -2425,22 +2442,22 @@ function toAgentBrowserArgs(action) {
|
|
|
2425
2442
|
if (!action.locator) return null;
|
|
2426
2443
|
return [
|
|
2427
2444
|
lit("select"),
|
|
2428
|
-
|
|
2445
|
+
val(locatorToSelector(action.locator)),
|
|
2429
2446
|
val(action.value ?? "")
|
|
2430
2447
|
];
|
|
2431
2448
|
case "drag":
|
|
2432
2449
|
if (!action.locator || !action.target) return null;
|
|
2433
2450
|
return [
|
|
2434
2451
|
lit("drag"),
|
|
2435
|
-
|
|
2436
|
-
|
|
2452
|
+
val(locatorToSelector(action.locator)),
|
|
2453
|
+
val(locatorToSelector(action.target))
|
|
2437
2454
|
];
|
|
2438
2455
|
case "upload": {
|
|
2439
2456
|
const files = action.files ?? [];
|
|
2440
2457
|
if (!action.locator || files.length === 0) return null;
|
|
2441
2458
|
return [
|
|
2442
2459
|
lit("upload"),
|
|
2443
|
-
|
|
2460
|
+
val(locatorToSelector(action.locator)),
|
|
2444
2461
|
...files.map(val)
|
|
2445
2462
|
];
|
|
2446
2463
|
}
|
|
@@ -2480,7 +2497,7 @@ function interactionToArgs(action) {
|
|
|
2480
2497
|
const abAction = action.action === "type" ? "fill" : action.action;
|
|
2481
2498
|
const takesInput = action.action === "fill" || action.action === "type";
|
|
2482
2499
|
if (!(loc.by !== "css" || action.index !== void 0 || action.action === "focus")) {
|
|
2483
|
-
const args = [lit(abAction),
|
|
2500
|
+
const args = [lit(abAction), val(loc.value)];
|
|
2484
2501
|
if (takesInput) args.push(val(action.value ?? ""));
|
|
2485
2502
|
return args;
|
|
2486
2503
|
}
|
|
@@ -2490,7 +2507,7 @@ function interactionToArgs(action) {
|
|
|
2490
2507
|
if (loc.by !== "css") return null;
|
|
2491
2508
|
if (action.index === "first" || action.index === "last") out.push(lit(action.index));
|
|
2492
2509
|
else out.push(lit("nth"), lit(String(action.index)));
|
|
2493
|
-
out.push(
|
|
2510
|
+
out.push(val(loc.value));
|
|
2494
2511
|
} else {
|
|
2495
2512
|
if (loc.by === "css") return null;
|
|
2496
2513
|
out.push(lit(loc.by), val(loc.value));
|
|
@@ -3016,6 +3033,7 @@ const ReportSpecResultSchema = z.object({
|
|
|
3016
3033
|
assertions: z.array(ReportAssertionSchema).nullable(),
|
|
3017
3034
|
analysis: FailureAnalysisSchema.nullable(),
|
|
3018
3035
|
analysisSkipped: z.string().nullable(),
|
|
3036
|
+
customPromptVersion: z.string().optional(),
|
|
3019
3037
|
analysisBase: z.object({
|
|
3020
3038
|
ref: z.string(),
|
|
3021
3039
|
sha: z.string()
|
|
@@ -3055,6 +3073,7 @@ const RunReportDataSchema = z.object({
|
|
|
3055
3073
|
kind: z.enum(["run", "drift"]).default("run"),
|
|
3056
3074
|
createdAt: z.string(),
|
|
3057
3075
|
runId: z.string().nullable(),
|
|
3076
|
+
runUrl: z.string().nullable().optional(),
|
|
3058
3077
|
git: GitEnvelopeSchema,
|
|
3059
3078
|
model: z.string().nullable(),
|
|
3060
3079
|
language: z.string().nullable().default(null),
|
|
@@ -3097,14 +3116,54 @@ const LabelsExportSchema = z.object({
|
|
|
3097
3116
|
* names and failure signals) lives here and on the hub — never hard-coded
|
|
3098
3117
|
* into ccqa itself.
|
|
3099
3118
|
*/
|
|
3119
|
+
/**
|
|
3120
|
+
* One per-target overlay: the same learned-note fields as the top-level, minus
|
|
3121
|
+
* `basePromptVersion` (shared across the whole document — the base analysis
|
|
3122
|
+
* prompt is target-agnostic).
|
|
3123
|
+
*/
|
|
3124
|
+
const AnalysisCustomPromptOverlaySchema = z.object({
|
|
3125
|
+
customPromptVersion: z.string(),
|
|
3126
|
+
generatedAt: z.string(),
|
|
3127
|
+
guidance: z.string()
|
|
3128
|
+
});
|
|
3100
3129
|
const AnalysisCustomPromptSchema = z.object({
|
|
3101
3130
|
schemaVersion: z.literal(1),
|
|
3102
3131
|
basePromptVersion: z.string(),
|
|
3103
3132
|
customPromptVersion: z.string(),
|
|
3104
3133
|
generatedAt: z.string(),
|
|
3105
|
-
guidance: z.string()
|
|
3134
|
+
guidance: z.string(),
|
|
3135
|
+
byTarget: z.record(z.string(), AnalysisCustomPromptOverlaySchema).optional()
|
|
3106
3136
|
});
|
|
3107
3137
|
/**
|
|
3138
|
+
* Lift one overlay into a standalone single-target `AnalysisCustomPrompt`: the
|
|
3139
|
+
* overlay's own note fields plus the document-wide `schemaVersion` /
|
|
3140
|
+
* `basePromptVersion`, and never a `byTarget` map. Passing the document itself
|
|
3141
|
+
* as the overlay yields the un-scoped top-level note as a clean single prompt.
|
|
3142
|
+
*/
|
|
3143
|
+
function overlayAsPrompt(base, overlay) {
|
|
3144
|
+
return {
|
|
3145
|
+
schemaVersion: base.schemaVersion,
|
|
3146
|
+
basePromptVersion: base.basePromptVersion,
|
|
3147
|
+
customPromptVersion: overlay.customPromptVersion,
|
|
3148
|
+
generatedAt: overlay.generatedAt,
|
|
3149
|
+
guidance: overlay.guidance
|
|
3150
|
+
};
|
|
3151
|
+
}
|
|
3152
|
+
/**
|
|
3153
|
+
* The effective single overlay for one target: its `byTarget` entry when it has
|
|
3154
|
+
* usable guidance, else the un-scoped top-level note when THAT has guidance,
|
|
3155
|
+
* else null. The returned value is a plain single-target `AnalysisCustomPrompt`
|
|
3156
|
+
* (no `byTarget`), so every downstream consumer — the prompt block and the
|
|
3157
|
+
* recorded `customPromptVersion` — sees exactly what was injected for the row.
|
|
3158
|
+
*/
|
|
3159
|
+
function resolveCustomPromptForTarget(cp, target) {
|
|
3160
|
+
if (!cp) return null;
|
|
3161
|
+
const scoped = cp.byTarget?.[target];
|
|
3162
|
+
if (scoped && scoped.guidance.trim()) return overlayAsPrompt(cp, scoped);
|
|
3163
|
+
if (cp.guidance.trim()) return overlayAsPrompt(cp, cp);
|
|
3164
|
+
return null;
|
|
3165
|
+
}
|
|
3166
|
+
/**
|
|
3108
3167
|
* Render the custom prompt as a prompt section, or "" when there's nothing to add.
|
|
3109
3168
|
* Returning "" for the empty/absent case is what keeps the base prompt
|
|
3110
3169
|
* byte-for-byte identical when no custom prompt is supplied (backward compatibility).
|
|
@@ -5694,6 +5753,7 @@ function createFailureAnalysisPass(deps) {
|
|
|
5694
5753
|
warnedDiffUnavailable = true;
|
|
5695
5754
|
info(`failure analysis: source diff unavailable (${specDiff.error}) — analyzing without diff context`);
|
|
5696
5755
|
}
|
|
5756
|
+
const customPrompt = resolveCustomPromptForTarget(deps.customPrompt, input.target);
|
|
5697
5757
|
const fields = {
|
|
5698
5758
|
analysis: null,
|
|
5699
5759
|
analysisSkipped: null,
|
|
@@ -5733,7 +5793,7 @@ function createFailureAnalysisPass(deps) {
|
|
|
5733
5793
|
...input.artifactsDir ? { artifactsDir: input.artifactsDir } : {},
|
|
5734
5794
|
...deps.language ? { outputLanguage: deps.language } : {},
|
|
5735
5795
|
...deps.triageUserPrompt ? { triageUserPrompt: deps.triageUserPrompt } : {},
|
|
5736
|
-
...
|
|
5796
|
+
...customPrompt ? { customPrompt } : {}
|
|
5737
5797
|
}, {
|
|
5738
5798
|
...deps.model ? { model: deps.model } : {},
|
|
5739
5799
|
cwd: deps.cwd,
|
|
@@ -5746,7 +5806,8 @@ function createFailureAnalysisPass(deps) {
|
|
|
5746
5806
|
printAnalysis(featureName, specName, outcome.analysis);
|
|
5747
5807
|
return {
|
|
5748
5808
|
...fields,
|
|
5749
|
-
analysis: outcome.analysis
|
|
5809
|
+
analysis: outcome.analysis,
|
|
5810
|
+
...customPrompt ? { customPromptVersion: customPrompt.customPromptVersion } : {}
|
|
5750
5811
|
};
|
|
5751
5812
|
} };
|
|
5752
5813
|
}
|
|
@@ -5849,6 +5910,7 @@ async function analyzeExternalRows(rows, run) {
|
|
|
5849
5910
|
readScript: () => readGeneratedTestSources(ref, deps.cwd),
|
|
5850
5911
|
failureLog: row.failureLogExcerpt ?? "",
|
|
5851
5912
|
specYaml: row.specYaml,
|
|
5913
|
+
target: row.target ?? "agent-browser",
|
|
5852
5914
|
driftIssues,
|
|
5853
5915
|
artifactsDir: readableArtifactsDir(ref, deps)
|
|
5854
5916
|
});
|
|
@@ -7967,7 +8029,8 @@ function analysisFieldsFor(a, status) {
|
|
|
7967
8029
|
analysisSkipped: a.analysisSkipped,
|
|
7968
8030
|
failureLogExcerpt: a.failureLogExcerpt,
|
|
7969
8031
|
diffExcerpt: a.diffExcerpt,
|
|
7970
|
-
...a.analysisBase ? { analysisBase: a.analysisBase } : {}
|
|
8032
|
+
...a.analysisBase ? { analysisBase: a.analysisBase } : {},
|
|
8033
|
+
...a.customPromptVersion ? { customPromptVersion: a.customPromptVersion } : {}
|
|
7971
8034
|
};
|
|
7972
8035
|
if (status === "failed") return { analysisSkipped: ANALYSIS_DISABLED };
|
|
7973
8036
|
return {};
|
|
@@ -8192,6 +8255,7 @@ async function analyzeOneLiveFailure(r, diffProvider, driftForSpec, auth, opts,
|
|
|
8192
8255
|
diffExcerpt: null
|
|
8193
8256
|
};
|
|
8194
8257
|
if (specDiff.error) info(`failure analysis: source diff unavailable (${specDiff.error}) — analyzing without diff context`);
|
|
8258
|
+
const customPrompt = resolveCustomPromptForTarget(opts.customPrompt, AGENT_BROWSER_TARGET);
|
|
8195
8259
|
const outcome = await analyzeFailure({
|
|
8196
8260
|
liveTranscriptExcerpt: excerpt,
|
|
8197
8261
|
specYaml: r.specYaml,
|
|
@@ -8203,7 +8267,7 @@ async function analyzeOneLiveFailure(r, diffProvider, driftForSpec, auth, opts,
|
|
|
8203
8267
|
driftIssues: driftForSpec,
|
|
8204
8268
|
...opts.language ? { outputLanguage: opts.language } : {},
|
|
8205
8269
|
...opts.triageUserPrompt ? { triageUserPrompt: opts.triageUserPrompt } : {},
|
|
8206
|
-
...
|
|
8270
|
+
...customPrompt ? { customPrompt } : {}
|
|
8207
8271
|
}, {
|
|
8208
8272
|
...opts.model ? { model: opts.model } : {},
|
|
8209
8273
|
cwd,
|
|
@@ -8220,7 +8284,8 @@ async function analyzeOneLiveFailure(r, diffProvider, driftForSpec, auth, opts,
|
|
|
8220
8284
|
analysisBase: {
|
|
8221
8285
|
ref: specDiff.base.ref,
|
|
8222
8286
|
sha: specDiff.base.sha
|
|
8223
|
-
}
|
|
8287
|
+
},
|
|
8288
|
+
...customPrompt ? { customPromptVersion: customPrompt.customPromptVersion } : {}
|
|
8224
8289
|
};
|
|
8225
8290
|
}
|
|
8226
8291
|
function count(steps, target) {
|
|
@@ -8536,27 +8601,27 @@ function actionToLine$1(action) {
|
|
|
8536
8601
|
if (val) assertLine = `abAssertNotVisible(${jExpr$1("text=" + val)}, 180_000);`;
|
|
8537
8602
|
break;
|
|
8538
8603
|
case "element_visible":
|
|
8539
|
-
if (sel) assertLine = `abAssertVisible(${
|
|
8604
|
+
if (sel) assertLine = `abAssertVisible(${jExpr$1(sel)});`;
|
|
8540
8605
|
break;
|
|
8541
8606
|
case "element_not_visible":
|
|
8542
|
-
if (sel) assertLine = `abAssertNotVisible(${
|
|
8607
|
+
if (sel) assertLine = `abAssertNotVisible(${jExpr$1(sel)});`;
|
|
8543
8608
|
break;
|
|
8544
8609
|
case "url_contains":
|
|
8545
8610
|
if (val) assertLine = `abAssertUrl(${jExpr$1(val)});`;
|
|
8546
8611
|
break;
|
|
8547
8612
|
case "element_enabled":
|
|
8548
8613
|
if (isStateSelector(sel)) return tautologicalStateAssertMarker(action, sel);
|
|
8549
|
-
if (sel && !sel.startsWith("text=") && !sel.startsWith("[aria-label=")) assertLine = `abAssertEnabled(${
|
|
8614
|
+
if (sel && !sel.startsWith("text=") && !sel.startsWith("[aria-label=")) assertLine = `abAssertEnabled(${jExpr$1(sel)});`;
|
|
8550
8615
|
break;
|
|
8551
8616
|
case "element_disabled":
|
|
8552
8617
|
if (isStateSelector(sel)) return tautologicalStateAssertMarker(action, sel);
|
|
8553
|
-
if (sel && !sel.startsWith("text=") && !sel.startsWith("[aria-label=")) assertLine = `abAssertDisabled(${
|
|
8618
|
+
if (sel && !sel.startsWith("text=") && !sel.startsWith("[aria-label=")) assertLine = `abAssertDisabled(${jExpr$1(sel)});`;
|
|
8554
8619
|
break;
|
|
8555
8620
|
case "element_checked":
|
|
8556
|
-
if (sel) assertLine = `abAssertChecked(${
|
|
8621
|
+
if (sel) assertLine = `abAssertChecked(${jExpr$1(sel)});`;
|
|
8557
8622
|
break;
|
|
8558
8623
|
case "element_unchecked":
|
|
8559
|
-
if (sel) assertLine = `abAssertUnchecked(${
|
|
8624
|
+
if (sel) assertLine = `abAssertUnchecked(${jExpr$1(sel)});`;
|
|
8560
8625
|
break;
|
|
8561
8626
|
}
|
|
8562
8627
|
if (comment && assertLine) return `${comment}\n ${assertLine}`;
|
|
@@ -8571,8 +8636,9 @@ function actionToLine$1(action) {
|
|
|
8571
8636
|
}
|
|
8572
8637
|
/**
|
|
8573
8638
|
* Render one argv token into TS source: env-expandable tokens (fill values,
|
|
8574
|
-
* URLs, find texts) become template literals via
|
|
8575
|
-
*
|
|
8639
|
+
* URLs, find texts, CSS/selector-engine strings) become template literals via
|
|
8640
|
+
* `jExpr` when they carry a `${VAR}` / `$VAR` ref; command words and flags are
|
|
8641
|
+
* plain string literals.
|
|
8576
8642
|
*/
|
|
8577
8643
|
function renderToken(token) {
|
|
8578
8644
|
return token.expandsEnv ? jExpr$1(token.text) : j$1(token.text);
|
|
@@ -8605,7 +8671,7 @@ const j$1 = (s) => JSON.stringify(s);
|
|
|
8605
8671
|
* emits them as `${process.env.VAR ?? ""}` template-literal substitutions
|
|
8606
8672
|
* instead of baking the literal `$VAR` string into the script. Used for
|
|
8607
8673
|
* values that came from a spec or block param: form fills, opened URLs,
|
|
8608
|
-
* assertion texts/URLs.
|
|
8674
|
+
* assertion texts/URLs, and selector strings carrying a `${RUN_ID}`-style ref.
|
|
8609
8675
|
*/
|
|
8610
8676
|
const jExpr$1 = (s) => envRefsToJsExpression(s);
|
|
8611
8677
|
//#endregion
|
|
@@ -9487,6 +9553,9 @@ function emitPlaywrightDraft(input) {
|
|
|
9487
9553
|
* Render a locator (plus positional pick) as a Playwright locator expression.
|
|
9488
9554
|
* Semantic strategies map 1:1 onto the getBy* family; `by: "css"` keeps its
|
|
9489
9555
|
* raw selector-engine string (locator() accepts `text=...` forms verbatim).
|
|
9556
|
+
* Every locator value — css included — goes through `jExpr`, so a `${VAR}` /
|
|
9557
|
+
* `$VAR` ref in a recorded selector expands to a `process.env` template
|
|
9558
|
+
* literal instead of baking the literal ref text into the selector.
|
|
9490
9559
|
*/
|
|
9491
9560
|
function locatorToPlaywright(locator, index) {
|
|
9492
9561
|
let expr;
|
|
@@ -9518,7 +9587,7 @@ function locatorToPlaywright(locator, index) {
|
|
|
9518
9587
|
expr = `page.getByTestId(${jExpr(locator.value)})`;
|
|
9519
9588
|
break;
|
|
9520
9589
|
case "css":
|
|
9521
|
-
expr = `page.locator(${
|
|
9590
|
+
expr = `page.locator(${jExpr(locator.value)})`;
|
|
9522
9591
|
break;
|
|
9523
9592
|
}
|
|
9524
9593
|
if (index === "first") return `${expr}.first()`;
|
|
@@ -10061,6 +10130,25 @@ function createIncrementalReport(reportDir, envelope, sink) {
|
|
|
10061
10130
|
};
|
|
10062
10131
|
}
|
|
10063
10132
|
//#endregion
|
|
10133
|
+
//#region src/run/github-run.ts
|
|
10134
|
+
/**
|
|
10135
|
+
* The GitHub Actions run URL for the current job, built from the standard
|
|
10136
|
+
* Actions environment variables. Returns null unless all three are present,
|
|
10137
|
+
* so nothing is ever invented for a local run — the same "only when in CI"
|
|
10138
|
+
* contract the report envelope's `runId` (GITHUB_RUN_ID) already follows.
|
|
10139
|
+
*/
|
|
10140
|
+
function githubRunUrl(env = process.env) {
|
|
10141
|
+
const server = env["GITHUB_SERVER_URL"];
|
|
10142
|
+
const repo = env["GITHUB_REPOSITORY"];
|
|
10143
|
+
const runId = githubRunId(env);
|
|
10144
|
+
if (!server || !repo || !runId) return null;
|
|
10145
|
+
return `${server}/${repo}/actions/runs/${runId}`;
|
|
10146
|
+
}
|
|
10147
|
+
/** The current GitHub Actions run id (GITHUB_RUN_ID); null outside Actions. */
|
|
10148
|
+
function githubRunId(env = process.env) {
|
|
10149
|
+
return env["GITHUB_RUN_ID"] ?? null;
|
|
10150
|
+
}
|
|
10151
|
+
//#endregion
|
|
10064
10152
|
//#region src/prompts/agent-update.ts
|
|
10065
10153
|
/**
|
|
10066
10154
|
* Build the prompts used by `--update-agent-prompt` to refresh
|
|
@@ -10582,11 +10670,15 @@ async function executeRun(targets, opts) {
|
|
|
10582
10670
|
let hubSink;
|
|
10583
10671
|
if (hubCtx != null && opts.pushReport) try {
|
|
10584
10672
|
const branch = await detectBranch(cwd);
|
|
10673
|
+
const ciRunId = githubRunId();
|
|
10674
|
+
const runUrl = githubRunUrl();
|
|
10585
10675
|
const opened = await hubCtx.hub.openRun({
|
|
10586
10676
|
project: hubCtx.project,
|
|
10587
10677
|
...branch ? { branch } : {},
|
|
10588
10678
|
...opts.profile ? { profile: opts.profile } : {},
|
|
10589
10679
|
...git.head ? { gitHead: git.head } : {},
|
|
10680
|
+
...ciRunId ? { ciRunId } : {},
|
|
10681
|
+
...runUrl ? { runUrl } : {},
|
|
10590
10682
|
kind: "run"
|
|
10591
10683
|
});
|
|
10592
10684
|
hubRunId = opened.id;
|
|
@@ -10932,6 +11024,7 @@ async function analyzeDeterministicSummaries(summaries, cwd, reportDir, { pass,
|
|
|
10932
11024
|
readScript: () => readScriptSafe(s.scriptFile),
|
|
10933
11025
|
failureLog,
|
|
10934
11026
|
specYaml,
|
|
11027
|
+
target: AGENT_BROWSER_TARGET,
|
|
10935
11028
|
driftIssues
|
|
10936
11029
|
});
|
|
10937
11030
|
results.push({
|
|
@@ -10940,6 +11033,7 @@ async function analyzeDeterministicSummaries(summaries, cwd, reportDir, { pass,
|
|
|
10940
11033
|
analysis: fields.analysis,
|
|
10941
11034
|
analysisSkipped: fields.analysisSkipped,
|
|
10942
11035
|
...fields.analysisBase ? { analysisBase: fields.analysisBase } : {},
|
|
11036
|
+
...fields.customPromptVersion ? { customPromptVersion: fields.customPromptVersion } : {},
|
|
10943
11037
|
driftIssues,
|
|
10944
11038
|
failureLogExcerpt: failureLog.length > 0 ? failureLog : null,
|
|
10945
11039
|
diffExcerpt: fields.diffExcerpt,
|
|
@@ -10958,11 +11052,13 @@ async function analyzeDeterministicSummaries(summaries, cwd, reportDir, { pass,
|
|
|
10958
11052
|
*/
|
|
10959
11053
|
function buildReportEnvelope(args) {
|
|
10960
11054
|
const { git, customPromptVersion, triageUserPromptHash, opts } = args;
|
|
11055
|
+
const runUrl = githubRunUrl();
|
|
10961
11056
|
return {
|
|
10962
11057
|
schemaVersion: 1,
|
|
10963
11058
|
kind: "run",
|
|
10964
11059
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10965
|
-
runId:
|
|
11060
|
+
runId: githubRunId(),
|
|
11061
|
+
...runUrl !== null ? { runUrl } : {},
|
|
10966
11062
|
git: {
|
|
10967
11063
|
head: git.head,
|
|
10968
11064
|
base: git.base?.ref ?? null,
|
|
@@ -14427,6 +14523,7 @@ z.object({
|
|
|
14427
14523
|
gitHead: z.string().nullable(),
|
|
14428
14524
|
promptVersion: z.string(),
|
|
14429
14525
|
ciRunId: z.string().nullable(),
|
|
14526
|
+
runUrl: z.string().nullable().optional(),
|
|
14430
14527
|
reportCreatedAt: z.string(),
|
|
14431
14528
|
createdAt: z.string()
|
|
14432
14529
|
});
|
|
@@ -14592,6 +14689,7 @@ function createPushRunHandler(config) {
|
|
|
14592
14689
|
gitHead: report.git.head,
|
|
14593
14690
|
promptVersion: report.promptVersion,
|
|
14594
14691
|
ciRunId: report.runId,
|
|
14692
|
+
runUrl: report.runUrl ?? null,
|
|
14595
14693
|
reportCreatedAt: report.createdAt,
|
|
14596
14694
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14597
14695
|
};
|
|
@@ -14619,6 +14717,8 @@ function createOpenRunHandler(config) {
|
|
|
14619
14717
|
return async (ctx) => {
|
|
14620
14718
|
const { project, branch, profile, kind } = parseRunScope(ctx);
|
|
14621
14719
|
const gitHead = ctx.url.searchParams.get("gitHead");
|
|
14720
|
+
const ciRunId = ctx.url.searchParams.get("ciRunId");
|
|
14721
|
+
const runUrl = ctx.url.searchParams.get("runUrl");
|
|
14622
14722
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14623
14723
|
const run = {
|
|
14624
14724
|
id: randomUUID(),
|
|
@@ -14635,7 +14735,8 @@ function createOpenRunHandler(config) {
|
|
|
14635
14735
|
},
|
|
14636
14736
|
gitHead: gitHead || null,
|
|
14637
14737
|
promptVersion: "",
|
|
14638
|
-
ciRunId: null,
|
|
14738
|
+
ciRunId: ciRunId || null,
|
|
14739
|
+
runUrl: runUrl || null,
|
|
14639
14740
|
reportCreatedAt: now,
|
|
14640
14741
|
createdAt: now
|
|
14641
14742
|
};
|
|
@@ -14654,6 +14755,7 @@ const PatchRunRequestSchema = z.object({
|
|
|
14654
14755
|
language: z.string().nullable().optional(),
|
|
14655
14756
|
promptVersion: z.string().optional(),
|
|
14656
14757
|
customPromptVersion: z.string().nullable().optional(),
|
|
14758
|
+
runUrl: z.string().nullable().optional(),
|
|
14657
14759
|
triageUserPromptHash: z.string().optional()
|
|
14658
14760
|
}).partial().optional()
|
|
14659
14761
|
});
|
|
@@ -14754,6 +14856,7 @@ function createPatchRunHandler(config) {
|
|
|
14754
14856
|
...reportMeta?.language !== void 0 ? { language: reportMeta.language } : {},
|
|
14755
14857
|
...reportMeta?.promptVersion !== void 0 ? { promptVersion: reportMeta.promptVersion } : {},
|
|
14756
14858
|
...reportMeta?.customPromptVersion !== void 0 ? { customPromptVersion: reportMeta.customPromptVersion } : {},
|
|
14859
|
+
...reportMeta?.runUrl !== void 0 ? { runUrl: reportMeta.runUrl } : {},
|
|
14757
14860
|
...reportMeta?.triageUserPromptHash !== void 0 ? { triageUserPromptHash: reportMeta.triageUserPromptHash } : {}
|
|
14758
14861
|
};
|
|
14759
14862
|
const merged = mergeResults(current?.results ?? [], rows);
|
|
@@ -15684,6 +15787,9 @@ ${HTML_BODY}
|
|
|
15684
15787
|
</body>
|
|
15685
15788
|
</html>`;
|
|
15686
15789
|
}
|
|
15790
|
+
function refreshButton(id) {
|
|
15791
|
+
return `<button class="btn ghost sm" id="${id}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><path d="M21 3v5h-5"/></svg> <span data-i18n="common.refresh">Refresh</span></button>`;
|
|
15792
|
+
}
|
|
15687
15793
|
const HTML_BODY = `
|
|
15688
15794
|
<div id="login" class="login" hidden>
|
|
15689
15795
|
<div class="login-card">
|
|
@@ -15743,9 +15849,7 @@ const HTML_BODY = `
|
|
|
15743
15849
|
<div class="page-bar">
|
|
15744
15850
|
<h1 data-i18n="projects.title">Projects</h1>
|
|
15745
15851
|
<div class="spacer"></div>
|
|
15746
|
-
|
|
15747
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><path d="M21 3v5h-5"/></svg> Refresh
|
|
15748
|
-
</button>
|
|
15852
|
+
${refreshButton("projects-refresh")}
|
|
15749
15853
|
<button class="btn primary sm" id="projects-new">
|
|
15750
15854
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg> <span data-i18n="projects.new">New project</span>
|
|
15751
15855
|
</button>
|
|
@@ -15761,9 +15865,7 @@ const HTML_BODY = `
|
|
|
15761
15865
|
<div class="page-bar">
|
|
15762
15866
|
<h1 data-i18n="runs.title">Runs</h1>
|
|
15763
15867
|
<div class="spacer"></div>
|
|
15764
|
-
|
|
15765
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><path d="M21 3v5h-5"/></svg> Refresh
|
|
15766
|
-
</button>
|
|
15868
|
+
${refreshButton("runs-refresh")}
|
|
15767
15869
|
</div>
|
|
15768
15870
|
<div class="content">
|
|
15769
15871
|
<div class="card" id="runs-card">
|
|
@@ -15784,7 +15886,7 @@ const HTML_BODY = `
|
|
|
15784
15886
|
<h1 data-i18n="perspectives.title">Perspectives</h1>
|
|
15785
15887
|
<span class="updated" id="persp-updated"></span>
|
|
15786
15888
|
<div class="spacer"></div>
|
|
15787
|
-
|
|
15889
|
+
${refreshButton("persp-refresh")}
|
|
15788
15890
|
</div>
|
|
15789
15891
|
<div class="content">
|
|
15790
15892
|
<p id="persp-status" class="empty-note" hidden></p>
|
|
@@ -15854,7 +15956,7 @@ const HTML_BODY = `
|
|
|
15854
15956
|
<div class="page-bar">
|
|
15855
15957
|
<h1 data-i18n="learning.title">Learning</h1>
|
|
15856
15958
|
<div class="spacer"></div>
|
|
15857
|
-
|
|
15959
|
+
${refreshButton("jobs-refresh")}
|
|
15858
15960
|
</div>
|
|
15859
15961
|
<div class="content">
|
|
15860
15962
|
<p id="jobs-status" class="empty-note" hidden></p>
|
|
@@ -15877,7 +15979,7 @@ const HTML_BODY = `
|
|
|
15877
15979
|
<div class="proj-menu" id="sec-profile-menu" role="menu" hidden></div>
|
|
15878
15980
|
</div>
|
|
15879
15981
|
<div class="spacer"></div>
|
|
15880
|
-
|
|
15982
|
+
${refreshButton("sec-load")}
|
|
15881
15983
|
</div>
|
|
15882
15984
|
<div class="content">
|
|
15883
15985
|
<div class="scope-note">
|
|
@@ -15902,7 +16004,7 @@ const HTML_BODY = `
|
|
|
15902
16004
|
<div class="page-bar">
|
|
15903
16005
|
<h1 data-i18n="prompts.title">Prompts</h1>
|
|
15904
16006
|
<div class="spacer"></div>
|
|
15905
|
-
|
|
16007
|
+
${refreshButton("pr-load")}
|
|
15906
16008
|
</div>
|
|
15907
16009
|
<div class="content">
|
|
15908
16010
|
<p id="prompts-status" class="empty-note" hidden></p>
|
|
@@ -16143,6 +16245,8 @@ const CSS = `
|
|
|
16143
16245
|
.subline { margin-top: 3px; }
|
|
16144
16246
|
.ci-badge { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; font-family: var(--mono); color: var(--muted); background: var(--surface-3); border: 1px solid var(--border); border-radius: 5px; padding: 1px 6px; }
|
|
16145
16247
|
.ci-badge.local { color: var(--muted-2); }
|
|
16248
|
+
a.ci-badge { text-decoration: none; }
|
|
16249
|
+
a.ci-badge:hover { color: var(--fg); border-color: var(--fg-dim); }
|
|
16146
16250
|
|
|
16147
16251
|
.badge { display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px 2px 7px; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500; border: 1px solid transparent; }
|
|
16148
16252
|
.badge .d { width: 6px; height: 6px; border-radius: 50%; }
|
|
@@ -16606,6 +16710,7 @@ const CLIENT_JS = `
|
|
|
16606
16710
|
"prompt.runnAgent.hint": "Notes ccqa keeps for itself while generating runn runbooks, refined automatically. Read-only — ccqa regenerates it.",
|
|
16607
16711
|
"prompt.triageUser.hint": "Rules you write for how failure causes are classified — e.g. which kinds of changes count as a spec change on this project. Applied on every failure analysis.",
|
|
16608
16712
|
"prompt.customPrompt.hint": "Learned from your triage grades to make ccqa classify failure causes the way you do. Read-only — a learning job creates it.",
|
|
16713
|
+
"prompt.customPrompt.fallback": "Un-scoped (fallback)",
|
|
16609
16714
|
"prompt.readonly": "read-only",
|
|
16610
16715
|
"prompt.notSet": "Not set. Type guidance and Save to store it on the hub.",
|
|
16611
16716
|
"prompt.notSetRo": "Not set yet — ccqa fills this in as it runs.",
|
|
@@ -16623,7 +16728,7 @@ const CLIENT_JS = `
|
|
|
16623
16728
|
"jobs.failed": "The learning job failed.", "jobs.newCustomPrompt": "New custom prompt:", "jobs.empty": "No learning jobs yet. Grade failing specs on a run, then Learn."
|
|
16624
16729
|
},
|
|
16625
16730
|
ja: {
|
|
16626
|
-
"nav.projects": "プロジェクト", "nav.runs": "実行", "nav.perspectives": "
|
|
16731
|
+
"nav.projects": "プロジェクト", "nav.runs": "実行", "nav.perspectives": "テスト観点", "nav.secrets": "シークレット",
|
|
16627
16732
|
"nav.prompts": "プロンプト", "nav.learning": "学習",
|
|
16628
16733
|
"app.project": "プロジェクト", "app.profile": "プロファイル", "app.disconnect": "切断", "app.noProject": "プロジェクト未選択",
|
|
16629
16734
|
"app.newProfile": "新規プロファイル",
|
|
@@ -16666,15 +16771,15 @@ const CLIENT_JS = `
|
|
|
16666
16771
|
"learn.cta.desc": "採点した内容をもとに、ccqaが次回から同じように失敗の原因を分類できるよう学習します。",
|
|
16667
16772
|
"learn.cta.run": "学習",
|
|
16668
16773
|
"secrets.title": "シークレット", "prompts.title": "プロンプト", "learning.title": "学習",
|
|
16669
|
-
"perspectives.title": "
|
|
16774
|
+
"perspectives.title": "テスト観点",
|
|
16670
16775
|
"perspectives.search": "ケースを検索…",
|
|
16671
16776
|
"perspectives.filter.all": "すべて", "perspectives.filter.deterministic": "決定的",
|
|
16672
16777
|
"perspectives.filter.live": "ライブ", "perspectives.filter.norec": "未recordのみ",
|
|
16673
16778
|
"perspectives.col.case": "ケース", "perspectives.col.mode": "モード", "perspectives.col.status": "状態",
|
|
16674
16779
|
"perspectives.noHit": "該当するケースがありません。",
|
|
16675
16780
|
"perspectives.updated": "最終更新:",
|
|
16676
|
-
"perspectives.empty": "
|
|
16677
|
-
"perspectives.loadFailed": "
|
|
16781
|
+
"perspectives.empty": "まだテスト観点がありません。ccqa perspectives を実行するか、recordすると自動作成されます。",
|
|
16782
|
+
"perspectives.loadFailed": "テスト観点の読み込みに失敗しました",
|
|
16678
16783
|
"perspectives.mode.deterministic": "決定的", "perspectives.mode.live": "ライブ",
|
|
16679
16784
|
"perspectives.status.runnable": "実行可能", "perspectives.status.notRecorded": "未record",
|
|
16680
16785
|
"perspectives.metric.features": "機能", "perspectives.metric.cases": "テストケース",
|
|
@@ -16703,6 +16808,7 @@ const CLIENT_JS = `
|
|
|
16703
16808
|
"prompt.runnAgent.hint": "runnランブック生成中にccqaが自分用に書き留め、自動で洗練していくメモです。読み取り専用 — ccqaが再生成します。",
|
|
16704
16809
|
"prompt.triageUser.hint": "失敗原因を分類するときのルールを自分で書きます(例: どの変更をこのプロジェクトで仕様変更として扱うか)。失敗分析のたびに適用されます。",
|
|
16705
16810
|
"prompt.customPrompt.hint": "あなたの採点から学習し、ccqaがあなたと同じように失敗の原因を分類できるようにします。読み取り専用 — 学習ジョブが生成します。",
|
|
16811
|
+
"prompt.customPrompt.fallback": "共通(フォールバック)",
|
|
16706
16812
|
"prompt.readonly": "読み取り専用",
|
|
16707
16813
|
"prompt.notSet": "未設定。指示を入力して保存するとハブに保存されます。",
|
|
16708
16814
|
"prompt.notSetRo": "未設定 — ccqaが実行しながら自動で書き込みます。",
|
|
@@ -16910,9 +17016,21 @@ const CLIENT_JS = `
|
|
|
16910
17016
|
}
|
|
16911
17017
|
|
|
16912
17018
|
function ciBadge(run) {
|
|
16913
|
-
|
|
16914
|
-
|
|
16915
|
-
|
|
17019
|
+
if (!run.ciRunId) return el("span", "ci-badge local", "local run");
|
|
17020
|
+
var text = "Actions #" + run.ciRunId;
|
|
17021
|
+
// A link to the GitHub Actions run when the URL was recorded; same chip
|
|
17022
|
+
// style otherwise (plain text).
|
|
17023
|
+
if (run.runUrl) {
|
|
17024
|
+
var a = el("a", "ci-badge", text);
|
|
17025
|
+
a.href = run.runUrl;
|
|
17026
|
+
a.target = "_blank";
|
|
17027
|
+
a.rel = "noopener";
|
|
17028
|
+
// The runs-list row is itself clickable (opens the run); opening the CI
|
|
17029
|
+
// link must not also navigate the row.
|
|
17030
|
+
a.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
17031
|
+
return a;
|
|
17032
|
+
}
|
|
17033
|
+
return el("span", "ci-badge", text);
|
|
16916
17034
|
}
|
|
16917
17035
|
|
|
16918
17036
|
function labelChip(label) {
|
|
@@ -18636,13 +18754,29 @@ const CLIENT_JS = `
|
|
|
18636
18754
|
}
|
|
18637
18755
|
|
|
18638
18756
|
// The custom prompt body is JSON (schemaVersion/basePromptVersion/customPromptVersion/
|
|
18639
|
-
// generatedAt/guidance
|
|
18640
|
-
//
|
|
18641
|
-
//
|
|
18757
|
+
// generatedAt/guidance, plus an optional per-target byTarget map); the textarea
|
|
18758
|
+
// only ever shows the learned guidance text, never the raw JSON. When byTarget
|
|
18759
|
+
// is present, the slot shows the un-scoped fallback (when it has guidance) plus
|
|
18760
|
+
// each per-target overlay under a short header, so it reflects the whole learned
|
|
18761
|
+
// set. A parse failure falls back to the raw text so a malformed custom prompt
|
|
18762
|
+
// still shows something instead of leaving the UI stuck.
|
|
18642
18763
|
function customPromptDisplayText(text) {
|
|
18643
18764
|
if (text == null) return "";
|
|
18765
|
+
var NL = "\\n";
|
|
18644
18766
|
try {
|
|
18645
18767
|
var parsed = JSON.parse(text);
|
|
18768
|
+
var byTarget = parsed && parsed.byTarget;
|
|
18769
|
+
if (byTarget && typeof byTarget === "object") {
|
|
18770
|
+
var parts = [];
|
|
18771
|
+
var top = typeof parsed.guidance === "string" ? parsed.guidance.trim() : "";
|
|
18772
|
+
if (top) parts.push("[" + t("prompt.customPrompt.fallback") + "]" + NL + top);
|
|
18773
|
+
Object.keys(byTarget).sort().forEach(function (tg) {
|
|
18774
|
+
var entry = byTarget[tg];
|
|
18775
|
+
var g = entry && typeof entry.guidance === "string" ? entry.guidance.trim() : "";
|
|
18776
|
+
if (g) parts.push("[" + tg + "]" + NL + g);
|
|
18777
|
+
});
|
|
18778
|
+
if (parts.length) return parts.join(NL + NL);
|
|
18779
|
+
}
|
|
18646
18780
|
return typeof parsed.guidance === "string" ? parsed.guidance : text;
|
|
18647
18781
|
} catch (e) {
|
|
18648
18782
|
// A malformed custom prompt shouldn't blank the panel; show the raw body but
|
|
@@ -19268,6 +19402,22 @@ function evidenceSignalFor(headline, note) {
|
|
|
19268
19402
|
}
|
|
19269
19403
|
function createLearningWorker(deps) {
|
|
19270
19404
|
const { storage, invoke = invokeClaudeStreaming, authCheck = driftAuthAvailable } = deps;
|
|
19405
|
+
/**
|
|
19406
|
+
* One Claude call turning a batch of graded cases into a calibration note.
|
|
19407
|
+
* Returns null when nothing usable came back so the caller can drop that
|
|
19408
|
+
* group's overlay without failing the whole job.
|
|
19409
|
+
*/
|
|
19410
|
+
const learnGuidance = async (cases) => {
|
|
19411
|
+
const { result, isError } = await invoke({
|
|
19412
|
+
prompt: buildLearningUserPrompt(cases.slice(0, LEARNING_MAX_CASES)),
|
|
19413
|
+
systemPrompt: LEARNING_SYSTEM_PROMPT,
|
|
19414
|
+
allowedTools: [],
|
|
19415
|
+
disableBuiltinTools: true,
|
|
19416
|
+
maxTurns: 1
|
|
19417
|
+
}, () => {});
|
|
19418
|
+
const guidance = result?.trim();
|
|
19419
|
+
return isError || !guidance ? null : guidance;
|
|
19420
|
+
};
|
|
19271
19421
|
return async function runLearningJob(job) {
|
|
19272
19422
|
const auth = authCheck();
|
|
19273
19423
|
if (!auth.ok) throw new Error(`triage learning needs Claude auth on the hub: ${auth.reason}`);
|
|
@@ -19285,7 +19435,8 @@ function createLearningWorker(deps) {
|
|
|
19285
19435
|
predicted: r.predicted.label,
|
|
19286
19436
|
actualCause: actual,
|
|
19287
19437
|
evidenceSignal: evidenceSignalFor(r.predicted.headline, r.note),
|
|
19288
|
-
matches: r.predicted.label === actual
|
|
19438
|
+
matches: r.predicted.label === actual,
|
|
19439
|
+
...r.target ? { target: r.target } : {}
|
|
19289
19440
|
});
|
|
19290
19441
|
}
|
|
19291
19442
|
}
|
|
@@ -19294,32 +19445,47 @@ function createLearningWorker(deps) {
|
|
|
19294
19445
|
runLimit,
|
|
19295
19446
|
casesConsidered: cases.length
|
|
19296
19447
|
} });
|
|
19297
|
-
const
|
|
19298
|
-
|
|
19299
|
-
|
|
19300
|
-
|
|
19301
|
-
|
|
19302
|
-
|
|
19303
|
-
|
|
19304
|
-
|
|
19305
|
-
|
|
19306
|
-
|
|
19448
|
+
const fallbackCases = [];
|
|
19449
|
+
const targetCases = /* @__PURE__ */ new Map();
|
|
19450
|
+
for (const c of cases) {
|
|
19451
|
+
if (!c.target) {
|
|
19452
|
+
fallbackCases.push(c);
|
|
19453
|
+
continue;
|
|
19454
|
+
}
|
|
19455
|
+
const list = targetCases.get(c.target) ?? [];
|
|
19456
|
+
list.push(c);
|
|
19457
|
+
targetCases.set(c.target, list);
|
|
19458
|
+
}
|
|
19307
19459
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
19460
|
+
const sortedTargets = [...targetCases.keys()].sort((a, b) => a.localeCompare(b));
|
|
19461
|
+
const [fallbackGuidance, ...targetGuidances] = await Promise.all([fallbackCases.length > 0 ? learnGuidance(fallbackCases) : Promise.resolve(null), ...sortedTargets.map((target) => learnGuidance(targetCases.get(target)))]);
|
|
19462
|
+
const byTarget = {};
|
|
19463
|
+
sortedTargets.forEach((target, i) => {
|
|
19464
|
+
const guidance = targetGuidances[i];
|
|
19465
|
+
if (guidance) byTarget[target] = {
|
|
19466
|
+
customPromptVersion: `${generatedAt}-${target}-c${targetCases.get(target).length}`,
|
|
19467
|
+
generatedAt,
|
|
19468
|
+
guidance
|
|
19469
|
+
};
|
|
19470
|
+
});
|
|
19471
|
+
if (!fallbackGuidance && Object.keys(byTarget).length === 0) throw new Error("triage learning: Claude returned no usable calibration note");
|
|
19472
|
+
const prevCustomPrompt = await loadStoredCustomPrompt(storage, job.project);
|
|
19308
19473
|
const customPrompt = {
|
|
19309
19474
|
schemaVersion: 1,
|
|
19310
19475
|
basePromptVersion: "7",
|
|
19311
|
-
customPromptVersion: `${generatedAt}-c${
|
|
19476
|
+
customPromptVersion: `${generatedAt}-c${fallbackCases.length}`,
|
|
19312
19477
|
generatedAt,
|
|
19313
|
-
guidance
|
|
19478
|
+
guidance: fallbackGuidance ?? "",
|
|
19479
|
+
...Object.keys(byTarget).length > 0 ? { byTarget } : {}
|
|
19314
19480
|
};
|
|
19315
19481
|
AnalysisCustomPromptSchema.parse(customPrompt);
|
|
19316
19482
|
const beforePrompt = buildFailureAnalysisPrompt({
|
|
19317
19483
|
...PROMPT_PREVIEW_FIXTURE,
|
|
19318
|
-
customPrompt: prevCustomPrompt
|
|
19484
|
+
customPrompt: representativeOverlay(prevCustomPrompt)
|
|
19319
19485
|
});
|
|
19320
19486
|
const afterPrompt = buildFailureAnalysisPrompt({
|
|
19321
19487
|
...PROMPT_PREVIEW_FIXTURE,
|
|
19322
|
-
customPrompt
|
|
19488
|
+
customPrompt: representativeOverlay(customPrompt)
|
|
19323
19489
|
});
|
|
19324
19490
|
await storage.prompts.put(job.project, "analysis-custom-prompt", new TextEncoder().encode(JSON.stringify(customPrompt)), {
|
|
19325
19491
|
customPromptVersion: customPrompt.customPromptVersion,
|
|
@@ -19336,6 +19502,19 @@ function createLearningWorker(deps) {
|
|
|
19336
19502
|
});
|
|
19337
19503
|
};
|
|
19338
19504
|
}
|
|
19505
|
+
/**
|
|
19506
|
+
* A single representative overlay for the before/after prompt preview: the
|
|
19507
|
+
* un-scoped fallback when it has guidance, else the first target overlay by
|
|
19508
|
+
* name, else null. Only the preview uses this — the stored blob keeps every
|
|
19509
|
+
* overlay; run-time injection picks per target (resolveCustomPromptForTarget).
|
|
19510
|
+
*/
|
|
19511
|
+
function representativeOverlay(cp) {
|
|
19512
|
+
if (!cp) return null;
|
|
19513
|
+
if (cp.guidance.trim()) return overlayAsPrompt(cp, cp);
|
|
19514
|
+
const firstTarget = cp.byTarget ? Object.keys(cp.byTarget).sort()[0] : void 0;
|
|
19515
|
+
const overlay = firstTarget ? cp.byTarget?.[firstTarget] : void 0;
|
|
19516
|
+
return overlay ? overlayAsPrompt(cp, overlay) : null;
|
|
19517
|
+
}
|
|
19339
19518
|
/** Read the currently-stored custom prompt, or null when there is none / it's unreadable. */
|
|
19340
19519
|
async function loadStoredCustomPrompt(storage, project) {
|
|
19341
19520
|
const entry = await storage.prompts.get(project, "analysis-custom-prompt");
|
|
@@ -52,6 +52,7 @@ declare const RunSchema: z.ZodObject<{
|
|
|
52
52
|
gitHead: z.ZodNullable<z.ZodString>;
|
|
53
53
|
promptVersion: z.ZodString;
|
|
54
54
|
ciRunId: z.ZodNullable<z.ZodString>;
|
|
55
|
+
runUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
55
56
|
reportCreatedAt: z.ZodString;
|
|
56
57
|
createdAt: z.ZodString;
|
|
57
58
|
}, z.core.$strip>;
|
|
@@ -217,6 +218,7 @@ declare const ReportSpecResultSchema: z.ZodObject<{
|
|
|
217
218
|
reasoning: z.ZodString;
|
|
218
219
|
}, z.core.$strip>>;
|
|
219
220
|
analysisSkipped: z.ZodNullable<z.ZodString>;
|
|
221
|
+
customPromptVersion: z.ZodOptional<z.ZodString>;
|
|
220
222
|
analysisBase: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
221
223
|
ref: z.ZodString;
|
|
222
224
|
sha: z.ZodString;
|
|
@@ -319,6 +321,7 @@ declare const RunReportDataSchema: z.ZodObject<{
|
|
|
319
321
|
}>>;
|
|
320
322
|
createdAt: z.ZodString;
|
|
321
323
|
runId: z.ZodNullable<z.ZodString>;
|
|
324
|
+
runUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
322
325
|
git: z.ZodObject<{
|
|
323
326
|
head: z.ZodNullable<z.ZodString>;
|
|
324
327
|
base: z.ZodNullable<z.ZodString>;
|
|
@@ -384,6 +387,7 @@ declare const RunReportDataSchema: z.ZodObject<{
|
|
|
384
387
|
reasoning: z.ZodString;
|
|
385
388
|
}, z.core.$strip>>;
|
|
386
389
|
analysisSkipped: z.ZodNullable<z.ZodString>;
|
|
390
|
+
customPromptVersion: z.ZodOptional<z.ZodString>;
|
|
387
391
|
analysisBase: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
388
392
|
ref: z.ZodString;
|
|
389
393
|
sha: z.ZodString;
|
|
@@ -584,6 +588,10 @@ interface HubClient {
|
|
|
584
588
|
profile?: string;
|
|
585
589
|
kind?: "run" | "drift";
|
|
586
590
|
gitHead?: string;
|
|
591
|
+
/** CI run id (GITHUB_RUN_ID) and its run URL, stamped at open time so an
|
|
592
|
+
* interrupted incremental run still links back to its CI run. */
|
|
593
|
+
ciRunId?: string;
|
|
594
|
+
runUrl?: string;
|
|
587
595
|
}): Promise<Run>;
|
|
588
596
|
/** Add finished spec rows (+ evidence) to a running run; `done` closes it. */
|
|
589
597
|
patchRun(id: string, body: PatchRunRequest): Promise<Run>;
|
|
@@ -110,6 +110,8 @@ function createHubClient(opts) {
|
|
|
110
110
|
if (meta.profile) params.set("profile", meta.profile);
|
|
111
111
|
if (meta.kind) params.set("kind", meta.kind);
|
|
112
112
|
if (meta.gitHead) params.set("gitHead", meta.gitHead);
|
|
113
|
+
if (meta.ciRunId) params.set("ciRunId", meta.ciRunId);
|
|
114
|
+
if (meta.runUrl) params.set("runUrl", meta.runUrl);
|
|
113
115
|
return json(`/api/v1/runs/open?${params}`, { method: "POST" });
|
|
114
116
|
},
|
|
115
117
|
patchRun(id, body) {
|
package/dist/package.json
CHANGED