codebyplan 1.13.14 → 1.13.15
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/cli.js +319 -63
- package/package.json +1 -1
- package/templates/agents/cbp-e2e-maestro.md +26 -3
- package/templates/agents/cbp-e2e-playwright.md +24 -3
- package/templates/agents/cbp-e2e-tauri.md +25 -2
- package/templates/agents/cbp-e2e-vscode.md +28 -3
- package/templates/agents/cbp-e2e-xcuitest.md +40 -4
- package/templates/agents/cbp-task-check.md +2 -0
- package/templates/context/testing/e2e.md +57 -9
- package/templates/hooks/validate-structure-patterns.sh +1 -1
- package/templates/rules/e2e-mandatory.md +19 -2
- package/templates/settings.project.base.json +8 -1
- package/templates/skills/cbp-checkpoint-end/SKILL.md +18 -1
- package/templates/skills/cbp-frontend-ui/SKILL.md +9 -7
- package/templates/skills/cbp-round-execute/SKILL.md +31 -7
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ var VERSION, PACKAGE_NAME;
|
|
|
14
14
|
var init_version = __esm({
|
|
15
15
|
"src/lib/version.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
VERSION = "1.13.
|
|
17
|
+
VERSION = "1.13.15";
|
|
18
18
|
PACKAGE_NAME = "codebyplan";
|
|
19
19
|
}
|
|
20
20
|
});
|
|
@@ -697,7 +697,7 @@ function isRetryable(err) {
|
|
|
697
697
|
return false;
|
|
698
698
|
}
|
|
699
699
|
function delay(ms) {
|
|
700
|
-
return new Promise((
|
|
700
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
701
701
|
}
|
|
702
702
|
async function request(method, path8, options) {
|
|
703
703
|
const url = buildUrl(path8, options?.params);
|
|
@@ -1055,7 +1055,7 @@ var init_device_flow = __esm({
|
|
|
1055
1055
|
this.name = "OAuthInvalidClientError";
|
|
1056
1056
|
}
|
|
1057
1057
|
};
|
|
1058
|
-
defaultSleep = (ms) => new Promise((
|
|
1058
|
+
defaultSleep = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
1059
1059
|
}
|
|
1060
1060
|
});
|
|
1061
1061
|
|
|
@@ -1880,9 +1880,9 @@ async function writeMcpConfig(scope) {
|
|
|
1880
1880
|
return configPath;
|
|
1881
1881
|
}
|
|
1882
1882
|
async function fetchRepos(auth) {
|
|
1883
|
-
const
|
|
1883
|
+
const baseUrl3 = (process.env.CODEBYPLAN_API_URL ?? "https://www.codebyplan.com").replace(/\/$/, "");
|
|
1884
1884
|
const headers = auth.kind === "oauth" ? { Authorization: `Bearer ${await getAccessToken()}` } : { "x-api-key": auth.apiKey };
|
|
1885
|
-
const res = await fetch(`${
|
|
1885
|
+
const res = await fetch(`${baseUrl3}/api/repos`, {
|
|
1886
1886
|
headers,
|
|
1887
1887
|
signal: AbortSignal.timeout(1e4)
|
|
1888
1888
|
});
|
|
@@ -2081,8 +2081,8 @@ async function runSetup() {
|
|
|
2081
2081
|
const deviceId = await getOrCreateDeviceId(projectPath);
|
|
2082
2082
|
let branch = "main";
|
|
2083
2083
|
try {
|
|
2084
|
-
const { execSync:
|
|
2085
|
-
branch =
|
|
2084
|
+
const { execSync: execSync9 } = await import("node:child_process");
|
|
2085
|
+
branch = execSync9("git symbolic-ref --short HEAD", {
|
|
2086
2086
|
cwd: projectPath,
|
|
2087
2087
|
encoding: "utf-8"
|
|
2088
2088
|
}).trim();
|
|
@@ -3720,9 +3720,9 @@ async function eslintInit(repoId, projectPath) {
|
|
|
3720
3720
|
Install ${missingPkgs.length} missing packages? [Y/n] `
|
|
3721
3721
|
);
|
|
3722
3722
|
if (confirmed) {
|
|
3723
|
-
const { execSync:
|
|
3723
|
+
const { execSync: execSync9 } = await import("node:child_process");
|
|
3724
3724
|
try {
|
|
3725
|
-
|
|
3725
|
+
execSync9(installCmd, { cwd: projectPath, stdio: "inherit" });
|
|
3726
3726
|
console.log(" Packages installed.\n");
|
|
3727
3727
|
} catch (err) {
|
|
3728
3728
|
console.error(
|
|
@@ -4157,7 +4157,7 @@ function setRetryDelayMs(ms) {
|
|
|
4157
4157
|
RETRY_DELAY_MS = ms;
|
|
4158
4158
|
}
|
|
4159
4159
|
function sleep(ms) {
|
|
4160
|
-
return new Promise((
|
|
4160
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
4161
4161
|
}
|
|
4162
4162
|
function isTransientMcpError(err) {
|
|
4163
4163
|
if (!(err instanceof McpError)) return false;
|
|
@@ -6374,13 +6374,262 @@ var init_version_status = __esm({
|
|
|
6374
6374
|
}
|
|
6375
6375
|
});
|
|
6376
6376
|
|
|
6377
|
+
// src/cli/upload-e2e-images.ts
|
|
6378
|
+
var upload_e2e_images_exports = {};
|
|
6379
|
+
__export(upload_e2e_images_exports, {
|
|
6380
|
+
runUploadE2eImagesCommand: () => runUploadE2eImagesCommand
|
|
6381
|
+
});
|
|
6382
|
+
import { readFile as readFile15 } from "node:fs/promises";
|
|
6383
|
+
import { join as join21, basename, resolve as resolve5 } from "node:path";
|
|
6384
|
+
import { execSync as execSync7 } from "node:child_process";
|
|
6385
|
+
function baseUrl2() {
|
|
6386
|
+
return (process.env.CODEBYPLAN_API_URL ?? "https://www.codebyplan.com").replace(/\/$/, "");
|
|
6387
|
+
}
|
|
6388
|
+
function parseArgs(args) {
|
|
6389
|
+
const flags = {};
|
|
6390
|
+
const booleans = /* @__PURE__ */ new Set();
|
|
6391
|
+
const positionals = [];
|
|
6392
|
+
for (let i = 0; i < args.length; i++) {
|
|
6393
|
+
const arg = args[i];
|
|
6394
|
+
if (arg.startsWith("--")) {
|
|
6395
|
+
const key = arg.slice(2);
|
|
6396
|
+
const next = args[i + 1];
|
|
6397
|
+
if (next !== void 0 && !next.startsWith("--")) {
|
|
6398
|
+
flags[key] = next;
|
|
6399
|
+
i++;
|
|
6400
|
+
} else {
|
|
6401
|
+
booleans.add(key);
|
|
6402
|
+
}
|
|
6403
|
+
} else {
|
|
6404
|
+
positionals.push(arg);
|
|
6405
|
+
}
|
|
6406
|
+
}
|
|
6407
|
+
return {
|
|
6408
|
+
checkpointId: positionals[0],
|
|
6409
|
+
repoId: flags["repo-id"],
|
|
6410
|
+
baseBranch: flags["base-branch"] ?? "main",
|
|
6411
|
+
json: booleans.has("json"),
|
|
6412
|
+
dryRun: booleans.has("dry-run")
|
|
6413
|
+
};
|
|
6414
|
+
}
|
|
6415
|
+
async function readE2eConfig(projectPath) {
|
|
6416
|
+
try {
|
|
6417
|
+
const raw = await readFile15(
|
|
6418
|
+
join21(projectPath, ".codebyplan", "e2e.json"),
|
|
6419
|
+
"utf-8"
|
|
6420
|
+
);
|
|
6421
|
+
return JSON.parse(raw);
|
|
6422
|
+
} catch {
|
|
6423
|
+
return {};
|
|
6424
|
+
}
|
|
6425
|
+
}
|
|
6426
|
+
function collectPngsFromGitDiff(projectPath, frameworkName, frameworkConfig, baseBranch) {
|
|
6427
|
+
const pathspec = frameworkConfig.test_dir ?? frameworkConfig.app;
|
|
6428
|
+
if (!pathspec) {
|
|
6429
|
+
return [];
|
|
6430
|
+
}
|
|
6431
|
+
let stdout7;
|
|
6432
|
+
try {
|
|
6433
|
+
stdout7 = execSync7(
|
|
6434
|
+
`git diff --name-status --diff-filter=AM "${baseBranch}...HEAD" -- "${pathspec}"`,
|
|
6435
|
+
{ cwd: projectPath, encoding: "utf-8" }
|
|
6436
|
+
);
|
|
6437
|
+
} catch (err) {
|
|
6438
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6439
|
+
if (msg.includes("unknown revision") || msg.includes("ambiguous argument") || msg.includes("not a git repository")) {
|
|
6440
|
+
return [];
|
|
6441
|
+
}
|
|
6442
|
+
process.stderr.write(`upload-e2e-images: git diff failed: ${msg}
|
|
6443
|
+
`);
|
|
6444
|
+
return [];
|
|
6445
|
+
}
|
|
6446
|
+
const results = [];
|
|
6447
|
+
for (const line of stdout7.split("\n")) {
|
|
6448
|
+
const trimmed = line.trim();
|
|
6449
|
+
if (!trimmed) continue;
|
|
6450
|
+
const tab = trimmed.indexOf(" ");
|
|
6451
|
+
if (tab === -1) continue;
|
|
6452
|
+
const status = trimmed.slice(0, tab).trim();
|
|
6453
|
+
const filePath = trimmed.slice(tab + 1).trim();
|
|
6454
|
+
if (!filePath.endsWith(".png")) continue;
|
|
6455
|
+
if (frameworkName === "playwright" && !filePath.includes(".spec.ts-snapshots/"))
|
|
6456
|
+
continue;
|
|
6457
|
+
const isNew = status === "A";
|
|
6458
|
+
results.push({
|
|
6459
|
+
absolutePath: join21(projectPath, filePath),
|
|
6460
|
+
filename: basename(filePath),
|
|
6461
|
+
framework: frameworkName,
|
|
6462
|
+
is_new: isNew
|
|
6463
|
+
});
|
|
6464
|
+
}
|
|
6465
|
+
return results;
|
|
6466
|
+
}
|
|
6467
|
+
function deriveTestName(absolutePath) {
|
|
6468
|
+
const segments = absolutePath.replace(/\\/g, "/").split("/");
|
|
6469
|
+
for (let i = segments.length - 2; i >= 0; i--) {
|
|
6470
|
+
const seg = segments[i];
|
|
6471
|
+
if (seg && seg.endsWith(".spec.ts-snapshots")) {
|
|
6472
|
+
return seg.replace(".spec.ts-snapshots", "");
|
|
6473
|
+
}
|
|
6474
|
+
}
|
|
6475
|
+
return basename(absolutePath, ".png");
|
|
6476
|
+
}
|
|
6477
|
+
function buildManifestItem(png) {
|
|
6478
|
+
const testName = deriveTestName(png.absolutePath);
|
|
6479
|
+
const pageOrScreen = basename(png.absolutePath, ".png");
|
|
6480
|
+
return {
|
|
6481
|
+
filename: png.filename,
|
|
6482
|
+
test_name: testName,
|
|
6483
|
+
page_or_screen: pageOrScreen,
|
|
6484
|
+
framework: png.framework,
|
|
6485
|
+
is_new: png.is_new,
|
|
6486
|
+
baseline_diff_pct: null
|
|
6487
|
+
};
|
|
6488
|
+
}
|
|
6489
|
+
async function runUploadE2eImagesCommand(args) {
|
|
6490
|
+
const parsed = parseArgs(args);
|
|
6491
|
+
if (!parsed.checkpointId) {
|
|
6492
|
+
process.stderr.write(
|
|
6493
|
+
"upload-e2e-images: missing required argument <checkpointId>\n\nUsage: codebyplan upload-e2e-images <checkpointId> [--repo-id <uuid>]\n [--base-branch <name>] [--json] [--dry-run]\n\nExample: codebyplan upload-e2e-images chk-abc-123 --base-branch main\n"
|
|
6494
|
+
);
|
|
6495
|
+
process.exit(1);
|
|
6496
|
+
}
|
|
6497
|
+
const checkpointId = parsed.checkpointId;
|
|
6498
|
+
const projectPath = resolve5(process.cwd());
|
|
6499
|
+
let repoId = parsed.repoId;
|
|
6500
|
+
if (!repoId) {
|
|
6501
|
+
const found = await findCodebyplanConfig(projectPath);
|
|
6502
|
+
repoId = found?.contents.repo_id;
|
|
6503
|
+
}
|
|
6504
|
+
if (!repoId) {
|
|
6505
|
+
process.stderr.write(
|
|
6506
|
+
"upload-e2e-images: could not determine repo_id.\nPass --repo-id <uuid> or ensure .codebyplan/repo.json exists.\n"
|
|
6507
|
+
);
|
|
6508
|
+
process.exit(1);
|
|
6509
|
+
}
|
|
6510
|
+
const e2eConfig = await readE2eConfig(projectPath);
|
|
6511
|
+
const frameworks = e2eConfig.frameworks ?? {};
|
|
6512
|
+
const allPngs = [];
|
|
6513
|
+
for (const [name, cfg] of Object.entries(frameworks)) {
|
|
6514
|
+
if (!cfg.enabled || !cfg.auto_run) continue;
|
|
6515
|
+
const pngs = collectPngsFromGitDiff(
|
|
6516
|
+
projectPath,
|
|
6517
|
+
name,
|
|
6518
|
+
cfg,
|
|
6519
|
+
parsed.baseBranch
|
|
6520
|
+
);
|
|
6521
|
+
allPngs.push(...pngs);
|
|
6522
|
+
}
|
|
6523
|
+
if (allPngs.length === 0) {
|
|
6524
|
+
process.stdout.write(
|
|
6525
|
+
`No new/changed e2e screenshots found for ${checkpointId}
|
|
6526
|
+
`
|
|
6527
|
+
);
|
|
6528
|
+
process.exit(0);
|
|
6529
|
+
}
|
|
6530
|
+
const manifest = allPngs.map(buildManifestItem);
|
|
6531
|
+
if (parsed.dryRun) {
|
|
6532
|
+
if (parsed.json) {
|
|
6533
|
+
process.stdout.write(JSON.stringify(manifest, null, 2) + "\n");
|
|
6534
|
+
} else {
|
|
6535
|
+
process.stdout.write(
|
|
6536
|
+
`[dry-run] Would upload ${manifest.length} screenshot(s) for checkpoint ${checkpointId}:
|
|
6537
|
+
`
|
|
6538
|
+
);
|
|
6539
|
+
for (const item of manifest) {
|
|
6540
|
+
const label = item.is_new ? "NEW" : "CHANGED";
|
|
6541
|
+
process.stdout.write(
|
|
6542
|
+
` [${label}] ${item.filename} (${item.framework}, test: ${item.test_name})
|
|
6543
|
+
`
|
|
6544
|
+
);
|
|
6545
|
+
}
|
|
6546
|
+
}
|
|
6547
|
+
process.exit(0);
|
|
6548
|
+
}
|
|
6549
|
+
const formData = new FormData();
|
|
6550
|
+
formData.append("checkpointId", checkpointId);
|
|
6551
|
+
formData.append("repoId", repoId);
|
|
6552
|
+
formData.append("manifest", JSON.stringify(manifest));
|
|
6553
|
+
for (const png of allPngs) {
|
|
6554
|
+
let bytes;
|
|
6555
|
+
try {
|
|
6556
|
+
bytes = await readFile15(png.absolutePath);
|
|
6557
|
+
} catch {
|
|
6558
|
+
process.stderr.write(
|
|
6559
|
+
`upload-e2e-images: could not read file: ${png.absolutePath}
|
|
6560
|
+
`
|
|
6561
|
+
);
|
|
6562
|
+
process.exit(1);
|
|
6563
|
+
}
|
|
6564
|
+
const blob = new Blob([bytes], { type: "image/png" });
|
|
6565
|
+
formData.append("files", blob, png.filename);
|
|
6566
|
+
}
|
|
6567
|
+
const auth = await getAuthHeaders();
|
|
6568
|
+
const url = `${baseUrl2()}/api/checkpoint-images`;
|
|
6569
|
+
let res;
|
|
6570
|
+
try {
|
|
6571
|
+
res = await fetch(url, {
|
|
6572
|
+
method: "POST",
|
|
6573
|
+
headers: auth.headers,
|
|
6574
|
+
body: formData
|
|
6575
|
+
});
|
|
6576
|
+
} catch (err) {
|
|
6577
|
+
process.stderr.write(
|
|
6578
|
+
`upload-e2e-images: network error: ${err instanceof Error ? err.message : String(err)}
|
|
6579
|
+
`
|
|
6580
|
+
);
|
|
6581
|
+
process.exit(1);
|
|
6582
|
+
}
|
|
6583
|
+
if (!res.ok) {
|
|
6584
|
+
let bodyText = "";
|
|
6585
|
+
try {
|
|
6586
|
+
bodyText = await res.text();
|
|
6587
|
+
} catch {
|
|
6588
|
+
}
|
|
6589
|
+
process.stderr.write(
|
|
6590
|
+
`upload-e2e-images: API returned ${res.status}: ${bodyText}
|
|
6591
|
+
`
|
|
6592
|
+
);
|
|
6593
|
+
process.exit(1);
|
|
6594
|
+
}
|
|
6595
|
+
let result;
|
|
6596
|
+
try {
|
|
6597
|
+
result = await res.json();
|
|
6598
|
+
} catch {
|
|
6599
|
+
process.stderr.write(
|
|
6600
|
+
"upload-e2e-images: failed to parse API response as JSON\n"
|
|
6601
|
+
);
|
|
6602
|
+
process.exit(1);
|
|
6603
|
+
}
|
|
6604
|
+
if (parsed.json) {
|
|
6605
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
6606
|
+
return;
|
|
6607
|
+
}
|
|
6608
|
+
process.stdout.write(
|
|
6609
|
+
`Uploaded ${manifest.length} screenshot(s) for checkpoint ${checkpointId}
|
|
6610
|
+
`
|
|
6611
|
+
);
|
|
6612
|
+
const paths = result.data.stored_paths ?? [];
|
|
6613
|
+
for (const p of paths) {
|
|
6614
|
+
process.stdout.write(` ${p}
|
|
6615
|
+
`);
|
|
6616
|
+
}
|
|
6617
|
+
}
|
|
6618
|
+
var init_upload_e2e_images = __esm({
|
|
6619
|
+
"src/cli/upload-e2e-images.ts"() {
|
|
6620
|
+
"use strict";
|
|
6621
|
+
init_api();
|
|
6622
|
+
init_flags();
|
|
6623
|
+
}
|
|
6624
|
+
});
|
|
6625
|
+
|
|
6377
6626
|
// src/cli/cmux-sync.ts
|
|
6378
6627
|
var cmux_sync_exports = {};
|
|
6379
6628
|
__export(cmux_sync_exports, {
|
|
6380
6629
|
runCmuxSync: () => runCmuxSync
|
|
6381
6630
|
});
|
|
6382
|
-
import { execSync as
|
|
6383
|
-
import { basename } from "node:path";
|
|
6631
|
+
import { execSync as execSync8, execFileSync as execFileSync2 } from "node:child_process";
|
|
6632
|
+
import { basename as basename2 } from "node:path";
|
|
6384
6633
|
async function runCmuxSync() {
|
|
6385
6634
|
try {
|
|
6386
6635
|
if (!process.env.CMUX_WORKSPACE_ID) {
|
|
@@ -6389,17 +6638,17 @@ async function runCmuxSync() {
|
|
|
6389
6638
|
const bin = process.env.CMUX_BUNDLED_CLI_PATH || process.env.CMUX_CLAUDE_HOOK_CMUX_BIN || "cmux";
|
|
6390
6639
|
let branch = "";
|
|
6391
6640
|
try {
|
|
6392
|
-
branch =
|
|
6641
|
+
branch = execSync8("git rev-parse --abbrev-ref HEAD", {
|
|
6393
6642
|
encoding: "utf8"
|
|
6394
6643
|
}).trim();
|
|
6395
6644
|
} catch {
|
|
6396
6645
|
}
|
|
6397
6646
|
let folder = "";
|
|
6398
6647
|
try {
|
|
6399
|
-
const toplevel =
|
|
6648
|
+
const toplevel = execSync8("git rev-parse --show-toplevel", {
|
|
6400
6649
|
encoding: "utf8"
|
|
6401
6650
|
}).trim();
|
|
6402
|
-
folder =
|
|
6651
|
+
folder = basename2(toplevel);
|
|
6403
6652
|
} catch {
|
|
6404
6653
|
}
|
|
6405
6654
|
if (branch) {
|
|
@@ -6440,19 +6689,19 @@ var init_cmux_sync = __esm({
|
|
|
6440
6689
|
});
|
|
6441
6690
|
|
|
6442
6691
|
// src/lib/migrate-local-config.ts
|
|
6443
|
-
import { mkdir as mkdir6, readFile as
|
|
6444
|
-
import { join as
|
|
6692
|
+
import { mkdir as mkdir6, readFile as readFile16, unlink as unlink2, writeFile as writeFile12 } from "node:fs/promises";
|
|
6693
|
+
import { join as join22 } from "node:path";
|
|
6445
6694
|
function legacySharedPath(projectPath) {
|
|
6446
|
-
return
|
|
6695
|
+
return join22(projectPath, ".codebyplan.json");
|
|
6447
6696
|
}
|
|
6448
6697
|
function legacyLocalPath(projectPath) {
|
|
6449
|
-
return
|
|
6698
|
+
return join22(projectPath, ".codebyplan.local.json");
|
|
6450
6699
|
}
|
|
6451
6700
|
function newDirPath(projectPath) {
|
|
6452
|
-
return
|
|
6701
|
+
return join22(projectPath, ".codebyplan");
|
|
6453
6702
|
}
|
|
6454
6703
|
function sentinelPath(projectPath) {
|
|
6455
|
-
return
|
|
6704
|
+
return join22(projectPath, ".codebyplan", "repo.json");
|
|
6456
6705
|
}
|
|
6457
6706
|
async function statSafe(p) {
|
|
6458
6707
|
const { stat: stat2 } = await import("node:fs/promises");
|
|
@@ -6491,7 +6740,7 @@ async function runLocalMigration(projectPath) {
|
|
|
6491
6740
|
}
|
|
6492
6741
|
let legacyRaw;
|
|
6493
6742
|
try {
|
|
6494
|
-
legacyRaw = await
|
|
6743
|
+
legacyRaw = await readFile16(legacySharedPath(projectPath), "utf-8");
|
|
6495
6744
|
} catch {
|
|
6496
6745
|
return {
|
|
6497
6746
|
migrated: true,
|
|
@@ -6518,7 +6767,7 @@ async function runLocalMigration(projectPath) {
|
|
|
6518
6767
|
let deviceId;
|
|
6519
6768
|
let deviceWrittenByHelper = false;
|
|
6520
6769
|
try {
|
|
6521
|
-
const localRaw = await
|
|
6770
|
+
const localRaw = await readFile16(legacyLocalPath(projectPath), "utf-8");
|
|
6522
6771
|
const localParsed = JSON.parse(localRaw);
|
|
6523
6772
|
if (typeof localParsed.device_id === "string") {
|
|
6524
6773
|
deviceId = localParsed.device_id;
|
|
@@ -6546,7 +6795,7 @@ async function runLocalMigration(projectPath) {
|
|
|
6546
6795
|
if ("organization_id" in cfg) repoJson.organization_id = cfg.organization_id;
|
|
6547
6796
|
if ("project_id" in cfg) repoJson.project_id = cfg.project_id;
|
|
6548
6797
|
await writeFile12(
|
|
6549
|
-
|
|
6798
|
+
join22(projectPath, ".codebyplan", "repo.json"),
|
|
6550
6799
|
JSON.stringify(repoJson, null, 2) + "\n",
|
|
6551
6800
|
"utf-8"
|
|
6552
6801
|
);
|
|
@@ -6559,7 +6808,7 @@ async function runLocalMigration(projectPath) {
|
|
|
6559
6808
|
if ("port_allocations" in cfg)
|
|
6560
6809
|
serverJson.port_allocations = cfg.port_allocations;
|
|
6561
6810
|
await writeFile12(
|
|
6562
|
-
|
|
6811
|
+
join22(projectPath, ".codebyplan", "server.json"),
|
|
6563
6812
|
JSON.stringify(serverJson, null, 2) + "\n",
|
|
6564
6813
|
"utf-8"
|
|
6565
6814
|
);
|
|
@@ -6568,7 +6817,7 @@ async function runLocalMigration(projectPath) {
|
|
|
6568
6817
|
if ("git_branch" in cfg) gitJson.git_branch = cfg.git_branch;
|
|
6569
6818
|
if ("branch_config" in cfg) gitJson.branch_config = cfg.branch_config;
|
|
6570
6819
|
await writeFile12(
|
|
6571
|
-
|
|
6820
|
+
join22(projectPath, ".codebyplan", "git.json"),
|
|
6572
6821
|
JSON.stringify(gitJson, null, 2) + "\n",
|
|
6573
6822
|
"utf-8"
|
|
6574
6823
|
);
|
|
@@ -6576,35 +6825,35 @@ async function runLocalMigration(projectPath) {
|
|
|
6576
6825
|
const shipmentJson = {};
|
|
6577
6826
|
if ("shipment" in cfg) shipmentJson.shipment = cfg.shipment;
|
|
6578
6827
|
await writeFile12(
|
|
6579
|
-
|
|
6828
|
+
join22(projectPath, ".codebyplan", "shipment.json"),
|
|
6580
6829
|
JSON.stringify(shipmentJson, null, 2) + "\n",
|
|
6581
6830
|
"utf-8"
|
|
6582
6831
|
);
|
|
6583
6832
|
filesChanged.push(".codebyplan/shipment.json");
|
|
6584
6833
|
const vendorJson = {};
|
|
6585
6834
|
await writeFile12(
|
|
6586
|
-
|
|
6835
|
+
join22(projectPath, ".codebyplan", "vendor.json"),
|
|
6587
6836
|
JSON.stringify(vendorJson, null, 2) + "\n",
|
|
6588
6837
|
"utf-8"
|
|
6589
6838
|
);
|
|
6590
6839
|
filesChanged.push(".codebyplan/vendor.json");
|
|
6591
6840
|
const e2eJson = {};
|
|
6592
6841
|
await writeFile12(
|
|
6593
|
-
|
|
6842
|
+
join22(projectPath, ".codebyplan", "e2e.json"),
|
|
6594
6843
|
JSON.stringify(e2eJson, null, 2) + "\n",
|
|
6595
6844
|
"utf-8"
|
|
6596
6845
|
);
|
|
6597
6846
|
filesChanged.push(".codebyplan/e2e.json");
|
|
6598
6847
|
const eslintJson = {};
|
|
6599
6848
|
await writeFile12(
|
|
6600
|
-
|
|
6849
|
+
join22(projectPath, ".codebyplan", "eslint.json"),
|
|
6601
6850
|
JSON.stringify(eslintJson, null, 2) + "\n",
|
|
6602
6851
|
"utf-8"
|
|
6603
6852
|
);
|
|
6604
6853
|
filesChanged.push(".codebyplan/eslint.json");
|
|
6605
6854
|
if (!deviceWrittenByHelper) {
|
|
6606
6855
|
await writeFile12(
|
|
6607
|
-
|
|
6856
|
+
join22(projectPath, ".codebyplan", "device.local.json"),
|
|
6608
6857
|
JSON.stringify({ device_id: deviceId }, null, 2) + "\n",
|
|
6609
6858
|
"utf-8"
|
|
6610
6859
|
);
|
|
@@ -6616,9 +6865,9 @@ async function runLocalMigration(projectPath) {
|
|
|
6616
6865
|
"Migration write incomplete: .codebyplan/repo.json was not persisted. Re-run migration to retry from a clean state."
|
|
6617
6866
|
);
|
|
6618
6867
|
}
|
|
6619
|
-
const gitignorePath =
|
|
6868
|
+
const gitignorePath = join22(projectPath, ".gitignore");
|
|
6620
6869
|
try {
|
|
6621
|
-
const gitignoreContent = await
|
|
6870
|
+
const gitignoreContent = await readFile16(gitignorePath, "utf-8");
|
|
6622
6871
|
const legacyLine = ".codebyplan.local.json";
|
|
6623
6872
|
const newLine = ".codebyplan/device.local.json";
|
|
6624
6873
|
const hasLegacy = gitignoreContent.split("\n").some((l) => l.trimEnd() === legacyLine);
|
|
@@ -6669,7 +6918,7 @@ var init_migrate_local_config = __esm({
|
|
|
6669
6918
|
// src/cli/config.ts
|
|
6670
6919
|
var config_exports = {};
|
|
6671
6920
|
__export(config_exports, {
|
|
6672
|
-
readE2eConfig: () =>
|
|
6921
|
+
readE2eConfig: () => readE2eConfig2,
|
|
6673
6922
|
readGitConfig: () => readGitConfig,
|
|
6674
6923
|
readRepoConfig: () => readRepoConfig,
|
|
6675
6924
|
readServerConfig: () => readServerConfig,
|
|
@@ -6677,8 +6926,8 @@ __export(config_exports, {
|
|
|
6677
6926
|
readVendorConfig: () => readVendorConfig,
|
|
6678
6927
|
runConfig: () => runConfig
|
|
6679
6928
|
});
|
|
6680
|
-
import { mkdir as mkdir7, readFile as
|
|
6681
|
-
import { join as
|
|
6929
|
+
import { mkdir as mkdir7, readFile as readFile17, writeFile as writeFile13 } from "node:fs/promises";
|
|
6930
|
+
import { join as join23 } from "node:path";
|
|
6682
6931
|
async function runConfig() {
|
|
6683
6932
|
const flags = parseFlags(3);
|
|
6684
6933
|
const dryRun = hasFlag("dry-run", 3);
|
|
@@ -6711,14 +6960,14 @@ async function runConfig() {
|
|
|
6711
6960
|
console.log("\n Config complete.\n");
|
|
6712
6961
|
}
|
|
6713
6962
|
async function syncConfigToFile(repoId, projectPath, dryRun) {
|
|
6714
|
-
const codebyplanDir =
|
|
6963
|
+
const codebyplanDir = join23(projectPath, ".codebyplan");
|
|
6715
6964
|
let resolvedWorktreeId;
|
|
6716
6965
|
try {
|
|
6717
6966
|
const deviceId = await getOrCreateDeviceId(projectPath);
|
|
6718
6967
|
let branch = "main";
|
|
6719
6968
|
try {
|
|
6720
|
-
const { execSync:
|
|
6721
|
-
branch =
|
|
6969
|
+
const { execSync: execSync9 } = await import("node:child_process");
|
|
6970
|
+
branch = execSync9("git symbolic-ref --short HEAD", {
|
|
6722
6971
|
cwd: projectPath,
|
|
6723
6972
|
encoding: "utf-8"
|
|
6724
6973
|
}).trim();
|
|
@@ -6854,11 +7103,11 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
|
|
|
6854
7103
|
];
|
|
6855
7104
|
let anyUpdated = false;
|
|
6856
7105
|
for (const { name, payload, createOnly } of files) {
|
|
6857
|
-
const filePath =
|
|
7106
|
+
const filePath = join23(codebyplanDir, name);
|
|
6858
7107
|
const newJson = JSON.stringify(payload, null, 2) + "\n";
|
|
6859
7108
|
let currentJson = "";
|
|
6860
7109
|
try {
|
|
6861
|
-
currentJson = await
|
|
7110
|
+
currentJson = await readFile17(filePath, "utf-8");
|
|
6862
7111
|
} catch {
|
|
6863
7112
|
}
|
|
6864
7113
|
if (createOnly && currentJson !== "") continue;
|
|
@@ -6873,8 +7122,8 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
|
|
|
6873
7122
|
}
|
|
6874
7123
|
async function readRepoConfig(projectPath) {
|
|
6875
7124
|
try {
|
|
6876
|
-
const raw = await
|
|
6877
|
-
|
|
7125
|
+
const raw = await readFile17(
|
|
7126
|
+
join23(projectPath, ".codebyplan", "repo.json"),
|
|
6878
7127
|
"utf-8"
|
|
6879
7128
|
);
|
|
6880
7129
|
return JSON.parse(raw);
|
|
@@ -6884,8 +7133,8 @@ async function readRepoConfig(projectPath) {
|
|
|
6884
7133
|
}
|
|
6885
7134
|
async function readServerConfig(projectPath) {
|
|
6886
7135
|
try {
|
|
6887
|
-
const raw = await
|
|
6888
|
-
|
|
7136
|
+
const raw = await readFile17(
|
|
7137
|
+
join23(projectPath, ".codebyplan", "server.json"),
|
|
6889
7138
|
"utf-8"
|
|
6890
7139
|
);
|
|
6891
7140
|
return JSON.parse(raw);
|
|
@@ -6895,8 +7144,8 @@ async function readServerConfig(projectPath) {
|
|
|
6895
7144
|
}
|
|
6896
7145
|
async function readGitConfig(projectPath) {
|
|
6897
7146
|
try {
|
|
6898
|
-
const raw = await
|
|
6899
|
-
|
|
7147
|
+
const raw = await readFile17(
|
|
7148
|
+
join23(projectPath, ".codebyplan", "git.json"),
|
|
6900
7149
|
"utf-8"
|
|
6901
7150
|
);
|
|
6902
7151
|
return JSON.parse(raw);
|
|
@@ -6906,8 +7155,8 @@ async function readGitConfig(projectPath) {
|
|
|
6906
7155
|
}
|
|
6907
7156
|
async function readShipmentConfig(projectPath) {
|
|
6908
7157
|
try {
|
|
6909
|
-
const raw = await
|
|
6910
|
-
|
|
7158
|
+
const raw = await readFile17(
|
|
7159
|
+
join23(projectPath, ".codebyplan", "shipment.json"),
|
|
6911
7160
|
"utf-8"
|
|
6912
7161
|
);
|
|
6913
7162
|
return JSON.parse(raw);
|
|
@@ -6917,8 +7166,8 @@ async function readShipmentConfig(projectPath) {
|
|
|
6917
7166
|
}
|
|
6918
7167
|
async function readVendorConfig(projectPath) {
|
|
6919
7168
|
try {
|
|
6920
|
-
const raw = await
|
|
6921
|
-
|
|
7169
|
+
const raw = await readFile17(
|
|
7170
|
+
join23(projectPath, ".codebyplan", "vendor.json"),
|
|
6922
7171
|
"utf-8"
|
|
6923
7172
|
);
|
|
6924
7173
|
return JSON.parse(raw);
|
|
@@ -6926,10 +7175,10 @@ async function readVendorConfig(projectPath) {
|
|
|
6926
7175
|
return null;
|
|
6927
7176
|
}
|
|
6928
7177
|
}
|
|
6929
|
-
async function
|
|
7178
|
+
async function readE2eConfig2(projectPath) {
|
|
6930
7179
|
try {
|
|
6931
|
-
const raw = await
|
|
6932
|
-
|
|
7180
|
+
const raw = await readFile17(
|
|
7181
|
+
join23(projectPath, ".codebyplan", "e2e.json"),
|
|
6933
7182
|
"utf-8"
|
|
6934
7183
|
);
|
|
6935
7184
|
return JSON.parse(raw);
|
|
@@ -6985,14 +7234,14 @@ var init_server_detect = __esm({
|
|
|
6985
7234
|
});
|
|
6986
7235
|
|
|
6987
7236
|
// src/lib/port-verify.ts
|
|
6988
|
-
import { readFile as
|
|
7237
|
+
import { readFile as readFile18 } from "node:fs/promises";
|
|
6989
7238
|
async function verifyPorts(projectPath, portAllocations) {
|
|
6990
7239
|
const mismatches = [];
|
|
6991
7240
|
const allocatedPorts = new Set(portAllocations.map((a) => a.port));
|
|
6992
7241
|
const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
|
|
6993
7242
|
for (const pkgPath of packageJsonPaths) {
|
|
6994
7243
|
try {
|
|
6995
|
-
const raw = await
|
|
7244
|
+
const raw = await readFile18(pkgPath, "utf-8");
|
|
6996
7245
|
const pkg = JSON.parse(raw);
|
|
6997
7246
|
const scriptPort = detectPortFromScripts(pkg);
|
|
6998
7247
|
if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
|
|
@@ -7055,7 +7304,7 @@ async function findUnallocatedApps(projectPath, portAllocations) {
|
|
|
7055
7304
|
}
|
|
7056
7305
|
let pkg;
|
|
7057
7306
|
try {
|
|
7058
|
-
const raw = await
|
|
7307
|
+
const raw = await readFile18(`${app.absPath}/package.json`, "utf-8");
|
|
7059
7308
|
pkg = JSON.parse(raw);
|
|
7060
7309
|
} catch {
|
|
7061
7310
|
continue;
|
|
@@ -7263,10 +7512,10 @@ async function runTechStack() {
|
|
|
7263
7512
|
);
|
|
7264
7513
|
}
|
|
7265
7514
|
try {
|
|
7266
|
-
const { execSync:
|
|
7515
|
+
const { execSync: execSync9 } = await import("node:child_process");
|
|
7267
7516
|
let branch = "main";
|
|
7268
7517
|
try {
|
|
7269
|
-
branch =
|
|
7518
|
+
branch = execSync9("git symbolic-ref --short HEAD", {
|
|
7270
7519
|
cwd: projectPath,
|
|
7271
7520
|
encoding: "utf-8"
|
|
7272
7521
|
}).trim();
|
|
@@ -7429,11 +7678,11 @@ async function ask(q, opts) {
|
|
|
7429
7678
|
try {
|
|
7430
7679
|
while (true) {
|
|
7431
7680
|
const choices = q.choices.map((c) => `[${c.key}] ${c.label}`).join(" ");
|
|
7432
|
-
const answer = await new Promise((
|
|
7681
|
+
const answer = await new Promise((resolve8) => {
|
|
7433
7682
|
rl.question(`${q.message}
|
|
7434
7683
|
${choices}
|
|
7435
7684
|
> `, (input) => {
|
|
7436
|
-
|
|
7685
|
+
resolve8(input.trim().toLowerCase());
|
|
7437
7686
|
});
|
|
7438
7687
|
});
|
|
7439
7688
|
const match = q.choices.find(
|
|
@@ -8066,11 +8315,11 @@ var init_uninstall = __esm({
|
|
|
8066
8315
|
// src/index.ts
|
|
8067
8316
|
init_version();
|
|
8068
8317
|
import { readFileSync as readFileSync8 } from "node:fs";
|
|
8069
|
-
import { resolve as
|
|
8318
|
+
import { resolve as resolve7 } from "node:path";
|
|
8070
8319
|
void (async () => {
|
|
8071
8320
|
if (!process.env.CODEBYPLAN_API_KEY) {
|
|
8072
8321
|
try {
|
|
8073
|
-
const envPath =
|
|
8322
|
+
const envPath = resolve7(process.cwd(), ".env.local");
|
|
8074
8323
|
const content = readFileSync8(envPath, "utf-8");
|
|
8075
8324
|
for (const line of content.split("\n")) {
|
|
8076
8325
|
const trimmed = line.trim();
|
|
@@ -8203,6 +8452,12 @@ void (async () => {
|
|
|
8203
8452
|
await runVersionStatus2();
|
|
8204
8453
|
process.exit(0);
|
|
8205
8454
|
}
|
|
8455
|
+
if (arg === "upload-e2e-images") {
|
|
8456
|
+
const { runUploadE2eImagesCommand: runUploadE2eImagesCommand2 } = await Promise.resolve().then(() => (init_upload_e2e_images(), upload_e2e_images_exports));
|
|
8457
|
+
const rest = process.argv.slice(3);
|
|
8458
|
+
await runUploadE2eImagesCommand2(rest);
|
|
8459
|
+
process.exit(0);
|
|
8460
|
+
}
|
|
8206
8461
|
if (arg === "cmux-sync") {
|
|
8207
8462
|
const { runCmuxSync: runCmuxSync2 } = await Promise.resolve().then(() => (init_cmux_sync(), cmux_sync_exports));
|
|
8208
8463
|
await runCmuxSync2();
|
|
@@ -8304,6 +8559,7 @@ void (async () => {
|
|
|
8304
8559
|
codebyplan round sync-approvals Sync git diff and approvals with round/task state
|
|
8305
8560
|
codebyplan bump Detect changed packages and patch-bump versions
|
|
8306
8561
|
codebyplan ship Ship current feat branch to production via PR
|
|
8562
|
+
codebyplan upload-e2e-images Upload new/changed committed e2e PNGs for a checkpoint
|
|
8307
8563
|
codebyplan scaffold-publish-workflow Write the publish-on-main GitHub workflow into ./.github/workflows/
|
|
8308
8564
|
codebyplan branch migrate Rewrite branch_config from 3-branch to 2-tier model
|
|
8309
8565
|
codebyplan claude Claude asset management (install/update/uninstall)
|
package/package.json
CHANGED
|
@@ -38,7 +38,7 @@ env:
|
|
|
38
38
|
TEST_EMAIL: ${TEST_EMAIL}
|
|
39
39
|
TEST_PASSWORD: ${TEST_PASSWORD}
|
|
40
40
|
APP_ID: com.yourorg.yourapp
|
|
41
|
-
screenshotsDir:
|
|
41
|
+
screenshotsDir: e2e/screenshots/maestro
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
## Shared Login Flow
|
|
@@ -158,8 +158,31 @@ delete + confirm + verify removed.
|
|
|
158
158
|
- takeScreenshot: "flow-name-after-state"
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
-
Screenshots written to `
|
|
162
|
-
|
|
161
|
+
Screenshots written to `e2e/screenshots/maestro/` (via `screenshotsDir` in `config.yaml`).
|
|
162
|
+
Committed path convention: `e2e/screenshots/maestro/{flow}-{state}.png` (repo root).
|
|
163
|
+
This path is intentionally outside `apps/web/e2e/screenshots/` (which is gitignored).
|
|
164
|
+
|
|
165
|
+
After the flow completes, `git add e2e/screenshots/maestro/` to track new PNGs.
|
|
166
|
+
|
|
167
|
+
**`is_new` detection**: `git ls-files --error-unmatch <path>` exits non-zero → `is_new: true`.
|
|
168
|
+
|
|
169
|
+
Enumerate committed PNGs: `e2e/screenshots/maestro/*.png`.
|
|
170
|
+
|
|
171
|
+
## e2e_gallery Population
|
|
172
|
+
|
|
173
|
+
After the run, for each committed PNG in `e2e/screenshots/maestro/*.png`, emit one
|
|
174
|
+
`e2e_gallery[]` entry:
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
- test_name: string # Maestro flow filename (e.g. "dashboard")
|
|
178
|
+
page_or_screen: string # screen / flow name
|
|
179
|
+
framework: maestro
|
|
180
|
+
committed_path: string # repo-relative: e2e/screenshots/maestro/{flow}-{state}.png
|
|
181
|
+
is_new: boolean # detected via git ls-files
|
|
182
|
+
baseline_diff_pct: null # Maestro does not produce pixel diffs
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Include this in the specialist output alongside `screenshots[]`.
|
|
163
186
|
|
|
164
187
|
## Run Command
|
|
165
188
|
|
|
@@ -189,7 +189,8 @@ test.describe("Home page", () => {
|
|
|
189
189
|
```ts
|
|
190
190
|
await expect(page).toHaveScreenshot("state-name.png", { maxDiffPixelRatio: 0.001 });
|
|
191
191
|
```
|
|
192
|
-
Baselines live beside spec under `{spec}.spec.ts-snapshots/`. Committed
|
|
192
|
+
Baselines live beside spec under `{spec}.spec.ts-snapshots/`. Committed path:
|
|
193
|
+
`apps/{app}/e2e/{spec}.spec.ts-snapshots/{name}-{browser}.png`.
|
|
193
194
|
|
|
194
195
|
**Diagnostic** (intermediate states):
|
|
195
196
|
```ts
|
|
@@ -199,9 +200,29 @@ await page.screenshot({
|
|
|
199
200
|
});
|
|
200
201
|
```
|
|
201
202
|
|
|
202
|
-
Enumerate PNGs: `
|
|
203
|
+
Enumerate committed PNGs: `{spec}.spec.ts-snapshots/**/*.png` (NOT `test-results/` — those are transient).
|
|
203
204
|
|
|
204
|
-
|
|
205
|
+
**`is_new` detection**: `git ls-files --error-unmatch <committed_path>` exits non-zero →
|
|
206
|
+
`is_new: true` (auto-committed first-run baseline; `git add` the file). Exit zero → `is_new: false`.
|
|
207
|
+
|
|
208
|
+
**Never run `--update-snapshots` automatically.** A diff on an existing baseline is a `visual_regression` failure.
|
|
209
|
+
|
|
210
|
+
## e2e_gallery Population
|
|
211
|
+
|
|
212
|
+
After the run, for each committed PNG in `{spec}.spec.ts-snapshots/**/*.png`, emit one
|
|
213
|
+
`e2e_gallery[]` entry. For `screenshots[].viewport`: default to `'desktop'`; set `'mobile'`
|
|
214
|
+
when the playwright.config project/device emulation indicates a mobile viewport (e.g. `devices['iPhone 14']`).
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
- test_name: string # test title from test.info().title
|
|
218
|
+
page_or_screen: string # route / screen name
|
|
219
|
+
framework: playwright
|
|
220
|
+
committed_path: string # repo-relative path to the .spec.ts-snapshots PNG
|
|
221
|
+
is_new: boolean # detected via git ls-files (see above)
|
|
222
|
+
baseline_diff_pct: number | null # from Playwright diff output; null when is_new
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Include this in the specialist output alongside `screenshots[]`.
|
|
205
226
|
|
|
206
227
|
## Run Command
|
|
207
228
|
|
|
@@ -148,10 +148,33 @@ For CRUD: create + verify visible; edit + verify; delete + confirm + verify remo
|
|
|
148
148
|
## Screenshot Capture
|
|
149
149
|
|
|
150
150
|
```ts
|
|
151
|
-
await browser.saveScreenshot(
|
|
151
|
+
await browser.saveScreenshot(
|
|
152
|
+
`./e2e/screenshots/webdriverio/${testName}-${state}.png`
|
|
153
|
+
);
|
|
152
154
|
```
|
|
153
155
|
|
|
154
|
-
|
|
156
|
+
Committed path convention: `{app-dir}/e2e/screenshots/webdriverio/{spec}-{state}.png`.
|
|
157
|
+
After the run, `git add {app-dir}/e2e/screenshots/webdriverio/` to track new PNGs.
|
|
158
|
+
|
|
159
|
+
**`is_new` detection**: `git ls-files --error-unmatch <path>` exits non-zero → `is_new: true`.
|
|
160
|
+
|
|
161
|
+
Enumerate committed PNGs: `{app-dir}/e2e/screenshots/webdriverio/**/*.png`.
|
|
162
|
+
|
|
163
|
+
## e2e_gallery Population
|
|
164
|
+
|
|
165
|
+
After the run, for each committed PNG under `{app-dir}/e2e/screenshots/webdriverio/`, emit one
|
|
166
|
+
`e2e_gallery[]` entry:
|
|
167
|
+
|
|
168
|
+
```yaml
|
|
169
|
+
- test_name: string # spec describe/it label
|
|
170
|
+
page_or_screen: string # window / view name
|
|
171
|
+
framework: webdriverio
|
|
172
|
+
committed_path: string # repo-relative: {app-dir}/e2e/screenshots/webdriverio/{spec}-{state}.png
|
|
173
|
+
is_new: boolean # detected via git ls-files
|
|
174
|
+
baseline_diff_pct: null # WebDriverIO does not produce pixel diffs
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Include this in the specialist output alongside `screenshots[]`.
|
|
155
178
|
|
|
156
179
|
## Run Command
|
|
157
180
|
|
|
@@ -162,10 +162,35 @@ snapshots to `test-fixtures/`.
|
|
|
162
162
|
## Screenshot Capture
|
|
163
163
|
|
|
164
164
|
VS Code extension tests do not have browser-style screenshot capture. For visual review,
|
|
165
|
-
write fixture output
|
|
166
|
-
with `viewport: 'device'`. `baseline_diff_pct: null` for all entries.
|
|
165
|
+
write fixture output PNGs to the committed dir:
|
|
167
166
|
|
|
168
|
-
|
|
167
|
+
Committed path convention: `{app-dir}/e2e/screenshots/vscode/{suite}-{test}.png`.
|
|
168
|
+
|
|
169
|
+
This dir **may be empty** for behavior-only tests that produce no visual output (SD-3).
|
|
170
|
+
When capturing PNGs is possible, write them there and `git add` them.
|
|
171
|
+
|
|
172
|
+
Enumerate committed PNGs: `{app-dir}/e2e/screenshots/vscode/**/*.png` (may be empty).
|
|
173
|
+
|
|
174
|
+
## e2e_gallery Population
|
|
175
|
+
|
|
176
|
+
Always emit `e2e_gallery[]` in the specialist output — even when empty (never omit the field):
|
|
177
|
+
|
|
178
|
+
```yaml
|
|
179
|
+
e2e_gallery: [] # empty for behavior-only extensions with no PNG output
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
When committed PNGs do exist, emit one entry per PNG:
|
|
183
|
+
|
|
184
|
+
```yaml
|
|
185
|
+
- test_name: string # suite/test name
|
|
186
|
+
page_or_screen: string # VS Code view / panel name
|
|
187
|
+
framework: vscode-test
|
|
188
|
+
committed_path: string # repo-relative: {app-dir}/e2e/screenshots/vscode/{suite}-{test}.png
|
|
189
|
+
is_new: boolean # detected via git ls-files
|
|
190
|
+
baseline_diff_pct: null # vscode-test does not produce pixel diffs
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Include this in the specialist output alongside `screenshots[]`.
|
|
169
194
|
|
|
170
195
|
## Run Command
|
|
171
196
|
|
|
@@ -168,11 +168,40 @@ screenshot.lifetime = .keepAlways
|
|
|
168
168
|
add(screenshot)
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
Attachments are written to the test results bundle under `DerivedData`.
|
|
172
|
-
|
|
171
|
+
Attachments are written to the test results bundle under `DerivedData`. After the run,
|
|
172
|
+
export them to the committed path using `xcrun xcresulttool`:
|
|
173
173
|
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
```bash
|
|
175
|
+
# Export all attachments from the result bundle to committed dir
|
|
176
|
+
xcrun xcresulttool export \
|
|
177
|
+
--path ./build/results.xcresult \
|
|
178
|
+
--output-path {app-dir}/e2e/screenshots/xcuitest \
|
|
179
|
+
--type directory
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Committed path convention: `{app-dir}/e2e/screenshots/xcuitest/{TestClass}/{testMethod}-{n}.png`.
|
|
183
|
+
After export, `git add {app-dir}/e2e/screenshots/xcuitest/` to track new PNGs.
|
|
184
|
+
|
|
185
|
+
**`is_new` detection**: `git ls-files --error-unmatch <path>` exits non-zero → `is_new: true`.
|
|
186
|
+
|
|
187
|
+
Enumerate committed PNGs: `{app-dir}/e2e/screenshots/xcuitest/**/*.png`.
|
|
188
|
+
|
|
189
|
+
## e2e_gallery Population
|
|
190
|
+
|
|
191
|
+
After export, for each committed PNG under `{app-dir}/e2e/screenshots/xcuitest/`, emit one
|
|
192
|
+
`e2e_gallery[]` entry:
|
|
193
|
+
|
|
194
|
+
```yaml
|
|
195
|
+
- test_name: string # TestClass/testMethod
|
|
196
|
+
page_or_screen: string # screen name inferred from the attachment name
|
|
197
|
+
framework: xcuitest
|
|
198
|
+
committed_path: string # repo-relative: {app-dir}/e2e/screenshots/xcuitest/{TestClass}/{testMethod}-{n}.png
|
|
199
|
+
is_new: boolean # detected via git ls-files
|
|
200
|
+
baseline_diff_pct: null # XCUITest does not produce pixel diffs
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Include this in the specialist output alongside `screenshots[]` (which retains the
|
|
204
|
+
`DerivedData` transient path for diagnostic reference).
|
|
176
205
|
|
|
177
206
|
## Run Command
|
|
178
207
|
|
|
@@ -181,9 +210,16 @@ xcodebuild test \
|
|
|
181
210
|
-workspace ios/YourApp.xcworkspace \
|
|
182
211
|
-scheme YourApp \
|
|
183
212
|
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
|
|
213
|
+
-resultBundlePath ./build/results.xcresult \
|
|
184
214
|
TEST_EMAIL="$TEST_EMAIL" \
|
|
185
215
|
TEST_PASSWORD="$TEST_PASSWORD" \
|
|
186
216
|
| xcbeautify
|
|
217
|
+
# Then export attachments to committed dir:
|
|
218
|
+
xcrun xcresulttool export \
|
|
219
|
+
--path ./build/results.xcresult \
|
|
220
|
+
--output-path {app-dir}/e2e/screenshots/xcuitest \
|
|
221
|
+
--type directory
|
|
222
|
+
git add {app-dir}/e2e/screenshots/xcuitest/
|
|
187
223
|
```
|
|
188
224
|
|
|
189
225
|
## pnpm Script
|
|
@@ -84,6 +84,8 @@ Review all QA items across all rounds:
|
|
|
84
84
|
|
|
85
85
|
**E2E pass vs skipped distinction**: When reading `auto_qa.items[]` for `check: 'e2e'`, do NOT conflate `status: 'pass'` with `status: 'skipped'`. A spec that ran with `passed === 0 && skipped > 0` for any path touching `files_changed` is a hard fail, not a pass — verdict text MUST explicitly call this out: "E2E spec authored but assertions did not execute (skip-gated)." Do NOT issue a READY verdict on a zero-assertion e2e run; route to a fix round per `rules/e2e-mandatory.md`.
|
|
86
86
|
|
|
87
|
+
**Committed-screenshot check**: For any round where `round.context.e2e_eligible[]` is non-empty, verify `round.context.e2e_gallery[]` is non-empty. Refuse a READY verdict when it is empty — verdict text: "E2E ran but produced zero committed screenshots — open a fix round per `rules/e2e-mandatory.md` § Committed-Screenshot Enforcement." Sole exception: when `vscode-test` is the ONLY eligible framework, an empty `e2e_gallery[]` is allowed (SD-3, behavior-only extensions).
|
|
88
|
+
|
|
87
89
|
List any pending or failed items. Determine if they are blockers.
|
|
88
90
|
|
|
89
91
|
### Phase 5: File Approval Check
|
|
@@ -70,6 +70,13 @@ output:
|
|
|
70
70
|
viewport: 'desktop' | 'mobile' | 'tablet' | 'device'
|
|
71
71
|
is_new: bool
|
|
72
72
|
baseline_diff_pct: number | null
|
|
73
|
+
e2e_gallery: # ADDITIVE alongside screenshots[]; consumed by TASK-3 / checkpoint-end
|
|
74
|
+
- test_name: string
|
|
75
|
+
page_or_screen: string
|
|
76
|
+
framework: string # playwright | maestro | xcuitest | webdriverio | vscode-test
|
|
77
|
+
committed_path: string # repo-relative; MUST be git-tracked after the run
|
|
78
|
+
is_new: boolean # true => no prior baseline; auto-captured+committed this run
|
|
79
|
+
baseline_diff_pct: number | null # null for non-playwright frameworks
|
|
73
80
|
user_interactions: [{question, answer}]
|
|
74
81
|
tech_stack_reconciliation:
|
|
75
82
|
db_framework: string | null
|
|
@@ -174,18 +181,57 @@ For each failed test, assign exactly one category:
|
|
|
174
181
|
`env`, `auth`, `access` failures MUST NOT count toward `test_results.failed` until
|
|
175
182
|
preflight passes — they block the run instead.
|
|
176
183
|
|
|
184
|
+
## Committed-Screenshot Mandate
|
|
185
|
+
|
|
186
|
+
Every eligible e2e run MUST persist relevant screenshots to the framework's committed
|
|
187
|
+
directory (tracked in git). Transient dirs (e.g. `test-results/`, `DerivedData`) are for
|
|
188
|
+
diagnostics only — they are NOT the committed path.
|
|
189
|
+
|
|
190
|
+
| Framework | Committed path |
|
|
191
|
+
|---|---|
|
|
192
|
+
| playwright | `apps/{app}/e2e/{spec}.spec.ts-snapshots/{name}-{browser}.png` |
|
|
193
|
+
| maestro | `e2e/screenshots/maestro/{flow}-{state}.png` (repo root) |
|
|
194
|
+
| xcuitest | `{app-dir}/e2e/screenshots/xcuitest/{TestClass}/{testMethod}-{n}.png` |
|
|
195
|
+
| webdriverio | `{app-dir}/e2e/screenshots/webdriverio/{spec}-{state}.png` |
|
|
196
|
+
| vscode-test | `{app-dir}/e2e/screenshots/vscode/{suite}-{test}.png` (SD-3: may be empty for behavior-only extensions) |
|
|
197
|
+
|
|
198
|
+
SD-3: the vscode-test committed dir may be empty for behavior-only extensions (no visual surface); the agent must still emit `e2e_gallery: []` explicitly. `cbp-task-check` Phase 4 treats an empty `e2e_gallery[]` as allowed when `vscode-test` is the ONLY eligible framework.
|
|
199
|
+
|
|
200
|
+
**Gitignore caution**: root `.gitignore` ignores `apps/web/e2e/screenshots/`. For the `{app-dir}`-relative frameworks (xcuitest, webdriverio, vscode-test), `{app-dir}` MUST NOT resolve to `apps/web` — committed PNGs there would be silently dropped from git. Remedy: use a non-ignored subdir (e.g. `apps/web/e2e/baselines/<framework>/`). A `.gitignore` negation (`!apps/web/e2e/screenshots/<framework>/`) does NOT work — git does not recurse into an ignored parent directory, so PNGs in that subdir would be silently dropped on a fresh checkout. Maestro (repo-root `e2e/screenshots/maestro/`) is already safe.
|
|
201
|
+
|
|
202
|
+
`is_new` detection: `git ls-files --error-unmatch <path>` exits non-zero → `is_new: true`
|
|
203
|
+
(no committed baseline exists yet; auto-capture and `git add`). Exit zero → `is_new: false`.
|
|
204
|
+
|
|
205
|
+
## Auto-New / Gated-Changed Update Model
|
|
206
|
+
|
|
207
|
+
**NEW screens** (`is_new === true`): the specialist auto-captures the PNG and runs
|
|
208
|
+
`git add <committed_path>`. The test passes; `cbp-frontend-ui` Step 5b reviews semantically.
|
|
209
|
+
No user gate required for first-run capture.
|
|
210
|
+
|
|
211
|
+
**EXISTING baselines that visually diff** (`is_new === false`, `baseline_diff_pct > threshold`):
|
|
212
|
+
classify as `visual_regression`. Do NOT auto-update. Surface as a blocking accept-or-fix gate
|
|
213
|
+
at `/cbp-round-end` Step 7. The user must explicitly approve (`--update-snapshots`) or open a
|
|
214
|
+
fix task. This relaxes the prior always-manual contract ONLY for new screens.
|
|
215
|
+
|
|
177
216
|
## Screenshot Collection Rule
|
|
178
217
|
|
|
179
|
-
After every run, enumerate all PNGs
|
|
180
|
-
specific paths are in each agent's body. Every
|
|
181
|
-
`{test_name, path, page_or_screen, viewport, is_new, baseline_diff_pct}`.
|
|
218
|
+
After every run, enumerate all committed PNGs and populate BOTH `screenshots[]` and
|
|
219
|
+
`e2e_gallery[]`. Framework-specific paths are in each agent's body. Every `screenshots[]`
|
|
220
|
+
entry requires: `{test_name, path, page_or_screen, viewport, is_new, baseline_diff_pct}`.
|
|
221
|
+
Every `e2e_gallery[]` entry requires: `{test_name, page_or_screen, framework, committed_path,
|
|
222
|
+
is_new, baseline_diff_pct}`. `committed_path` MUST be a git-tracked path after the run.
|
|
223
|
+
|
|
224
|
+
`/cbp-round-execute` Step 5b aggregates `e2e_gallery[]` across all specialists and stores it
|
|
225
|
+
in `round.context.e2e_gallery`. TASK-3 / checkpoint-end consumes this aggregated gallery to
|
|
226
|
+
upload images to the DB.
|
|
182
227
|
|
|
183
228
|
Screenshots flow to `cbp-frontend-ui` invoked by `/cbp-round-execute` Step 5b with
|
|
184
229
|
`phase: 'screenshot_review'` — NOT inline by `round-executor` Step 3.8 (which runs
|
|
185
230
|
`phase: 'style_only'` without e2e output).
|
|
186
231
|
|
|
187
|
-
**
|
|
188
|
-
the user decides via QA whether to update baselines.
|
|
232
|
+
**Changed baselines are never auto-accepted.** A `toHaveScreenshot` diff on an existing
|
|
233
|
+
baseline is `visual_regression`; the user decides via QA whether to update baselines.
|
|
234
|
+
New-screen auto-capture (above) is the only exception to the always-manual contract.
|
|
189
235
|
|
|
190
236
|
## Completion Rule
|
|
191
237
|
|
|
@@ -237,7 +283,9 @@ An agent is NOT spawned when ANY of the following hold:
|
|
|
237
283
|
spawn multiple specialists in the same round (one per eligible framework). Agents run in
|
|
238
284
|
parallel with `cbp-testing-qa-agent`. Each specialist's output is stored under
|
|
239
285
|
`round.context.e2e_outputs[framework]` (a framework-keyed map); `/cbp-round-execute` Step 5b
|
|
240
|
-
aggregates `screenshots[]` across all entries before the
|
|
286
|
+
aggregates `screenshots[]` and `e2e_gallery[]` across all entries before the
|
|
287
|
+
`cbp-frontend-ui` review. The aggregated `e2e_gallery[]` is persisted separately to
|
|
288
|
+
`round.context.e2e_gallery` for consumption by TASK-3 / checkpoint-end.
|
|
241
289
|
|
|
242
290
|
**whole_checkpoint_mode dispatch** (`/cbp-checkpoint-check` Step 5b and `/cbp-checkpoint-plan`
|
|
243
291
|
Step 4): pass `round_number: 0`, `whole_checkpoint_mode: true`, and the aggregated
|
|
@@ -298,6 +346,6 @@ a loop, snapshot text/href BEFORE navigation rather than holding stale `Locator`
|
|
|
298
346
|
|
|
299
347
|
| Situation | What happens |
|
|
300
348
|
|---|---|
|
|
301
|
-
| No baseline (new screen) | Playwright creates on first run;
|
|
302
|
-
| Baseline exists, diff ≤ threshold | Test passes. |
|
|
303
|
-
| Baseline exists, diff > threshold | `visual_regression` failure
|
|
349
|
+
| No baseline (new screen, `is_new: true`) | Playwright creates on first run; auto-committed; `git add` runs; `e2e_gallery[].is_new: true`; `cbp-frontend-ui` Step 5b reviews semantically. No user gate. |
|
|
350
|
+
| Baseline exists, diff ≤ threshold | Test passes; `is_new: false`; `baseline_diff_pct` recorded. |
|
|
351
|
+
| Baseline exists, diff > threshold | `visual_regression` failure; `is_new: false`. Agent does NOT retry. `cbp-frontend-ui` Step 5b flags it; `/cbp-round-end` Step 3b constructs user QA item. User decides: fix-task or `--update-snapshots`. |
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
_SUB='(templates|examples|reference|scripts)/[a-z0-9.-]+\.(md|sh|json|ya?ml)'
|
|
13
13
|
enforce_path_pattern '^/\.claude/skills/' "^/\.claude/skills/[a-z0-9-]+/(SKILL\.md|[a-z0-9-]+\.md|${_SUB})$" 'Invalid skill path' "Pattern: /.claude/skills/{name}/SKILL.md | /.claude/skills/{name}/{file}.md | /.claude/skills/{name}/(templates|examples|reference|scripts)/{file}.{md,sh,json,yaml}"
|
|
14
14
|
enforce_path_pattern '^/\.claude/agents/' "^/\.claude/agents/([a-z0-9-]+\.md|[a-z0-9-]+/(AGENT\.md|[a-z0-9-]+\.md|${_SUB}))$" 'Invalid agent path' "Pattern: /.claude/agents/{name}.md | /.claude/agents/{name}/AGENT.md | /.claude/agents/{name}/{file}.md | /.claude/agents/{name}/(templates|examples|reference|scripts)/{file}.{md,sh,json,yaml}"
|
|
15
|
-
enforce_path_pattern '^/\.claude/rules/' '^/\.claude/rules/[a-
|
|
15
|
+
enforce_path_pattern '^/\.claude/rules/' '^/\.claude/rules/[a-z0-9-]+\.md$' 'Invalid native rule path' 'Pattern: /.claude/rules/{name}.md'
|
|
16
16
|
if match_path '^/\.claude/hooks/' && ! match_path '^/\.claude/hooks/__test-fixtures__/'; then
|
|
17
17
|
if ! match_path '^/\.claude/hooks/[a-z-]+\.sh$'; then
|
|
18
18
|
block 'Invalid hook path' 'Pattern: /.claude/hooks/{name}.sh'
|
|
@@ -61,10 +61,27 @@ A spec that ran with `passed === 0 && skipped > 0` for any path touching `files_
|
|
|
61
61
|
**hard fail**, not a pass — `cbp-task-check` (`agents/cbp-task-check.md`) refuses a READY
|
|
62
62
|
verdict on a zero-assertion e2e run and routes to a fix round per this rule.
|
|
63
63
|
|
|
64
|
+
## Committed-Screenshot Enforcement
|
|
65
|
+
|
|
66
|
+
An eligible e2e run that produces **zero committed screenshots** for any `pages_affected`
|
|
67
|
+
path it touched is a defect — not a valid pass. Every framework must write at least one
|
|
68
|
+
PNG to its committed dir (per the table in `context/testing/e2e.md` § Committed-Screenshot
|
|
69
|
+
Mandate) and `git add` it before reporting `status: 'completed'`.
|
|
70
|
+
|
|
71
|
+
`cbp-task-check` refuses a READY verdict when `e2e_gallery[]` is empty AND the round
|
|
72
|
+
touched UI source paths for an eligible framework — sole exception: `vscode-test`-only
|
|
73
|
+
rounds (SD-3, behavior-only extensions; see below). The fix path is the same as for a
|
|
74
|
+
zero-assertion run: open a fix round that captures the missing committed screenshots.
|
|
75
|
+
|
|
76
|
+
The sole exception is `vscode-test`: the committed dir may be empty when the extension
|
|
77
|
+
has no visual output (behavior-only tests). Agents must still define the dir and report
|
|
78
|
+
`e2e_gallery: []` explicitly — not omit the field.
|
|
79
|
+
|
|
64
80
|
## Cross-References
|
|
65
81
|
|
|
66
82
|
- `context/testing/e2e.md` — Input/Output contract, pre-flight loop, failure classification,
|
|
67
|
-
and
|
|
68
|
-
- `agents/cbp-task-check.md` — enforces the zero-assertion hard-fail
|
|
83
|
+
committed-screenshot mandate, auto-new/gated-changed model, and dispatch routing table.
|
|
84
|
+
- `agents/cbp-task-check.md` — enforces the zero-assertion hard-fail and the empty
|
|
85
|
+
`e2e_gallery[]` hard-fail at verdict time.
|
|
69
86
|
- `skills/cbp-round-execute/SKILL.md` Step 5/6, `skills/cbp-checkpoint-check/SKILL.md` Step 5b
|
|
70
87
|
— the config-driven dispatch and `e2e_eligible_skipped` gate implementations.
|
|
@@ -88,7 +88,9 @@
|
|
|
88
88
|
"Bash(codebyplan ship:*)",
|
|
89
89
|
"Bash(npx codebyplan ship:*)",
|
|
90
90
|
"Bash(codebyplan claude:*)",
|
|
91
|
-
"Bash(npx codebyplan claude:*)"
|
|
91
|
+
"Bash(npx codebyplan claude:*)",
|
|
92
|
+
"Bash(codebyplan upload-e2e-images:*)",
|
|
93
|
+
"Bash(npx codebyplan upload-e2e-images:*)"
|
|
92
94
|
],
|
|
93
95
|
"allow": [
|
|
94
96
|
"Skill(cbp-build-cc-agent)",
|
|
@@ -125,6 +127,11 @@
|
|
|
125
127
|
"Skill(cbp-setup-e2e)",
|
|
126
128
|
"Skill(cbp-setup-eslint)",
|
|
127
129
|
"Skill(cbp-ship-configure)",
|
|
130
|
+
"Skill(cbp-standalone-task-check)",
|
|
131
|
+
"Skill(cbp-standalone-task-complete)",
|
|
132
|
+
"Skill(cbp-standalone-task-create)",
|
|
133
|
+
"Skill(cbp-standalone-task-start)",
|
|
134
|
+
"Skill(cbp-standalone-task-testing)",
|
|
128
135
|
"Skill(cbp-supabase-branch-check)",
|
|
129
136
|
"Skill(cbp-supabase-migrate)",
|
|
130
137
|
"Skill(cbp-supabase-setup)",
|
|
@@ -113,6 +113,22 @@ If `/cbp-ship` reports `aborted_at` (user aborted) or any surface failed verific
|
|
|
113
113
|
|
|
114
114
|
If the repo has zero configured surfaces (very early-stage), `/cbp-ship` exits with `## No deployable surfaces configured` — that's a success state, continue to cleanup.
|
|
115
115
|
|
|
116
|
+
### Step 7.5: Upload E2E Screenshots to DB (best-effort)
|
|
117
|
+
|
|
118
|
+
After `/cbp-ship` completes successfully, upload the checkpoint's new/changed committed E2E screenshots to the CodeByPlan DB so they can be reviewed per-checkpoint in the web UI (CHK-171). This step is **best-effort and non-blocking** — a failure here MUST NOT halt shipment or cleanup.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npx codebyplan upload-e2e-images "$CHECKPOINT_ID" --repo-id "$REPO_ID" --base-branch "$BASE" --json
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The command collects only the PNGs added/changed on the feat branch vs `$BASE` (this checkpoint's own e2e work — not the whole baseline set), POSTs them to `POST /api/checkpoint-images`, and the endpoint stores each file + patches `checkpoints.e2e_screenshots`. Capture the outcome into `E2E_IMAGES_UPLOADED` for Step 10:
|
|
125
|
+
|
|
126
|
+
- Exit 0 with uploaded paths → `{ count: <n>, stored_paths: [...], skipped: false }`.
|
|
127
|
+
- Exit 0 with `No new/changed e2e screenshots found` → `{ count: 0, stored_paths: [], skipped: true }`.
|
|
128
|
+
- Non-zero exit → `{ count: 0, stored_paths: [], skipped: true, error: "<stderr summary>" }`; emit a non-blocking warning and continue to Step 8.
|
|
129
|
+
|
|
130
|
+
`--repo-id` defaults to `repo_id` from `.codebyplan/repo.json` when omitted.
|
|
131
|
+
|
|
116
132
|
### Step 8: Stale Feat Branch Cleanup
|
|
117
133
|
|
|
118
134
|
After successful shipment, identify stale remote feat branches:
|
|
@@ -202,7 +218,8 @@ context.shipment: {
|
|
|
202
218
|
skipped: [...], // populated by /cbp-ship — surfaces explicitly skipped
|
|
203
219
|
stale_branches_cleaned: [list of deleted git branches],
|
|
204
220
|
feat_branch_deleted: true/false,
|
|
205
|
-
supabase_branches_deleted: [list of Supabase preview branch names removed in Steps 8–9]
|
|
221
|
+
supabase_branches_deleted: [list of Supabase preview branch names removed in Steps 8–9],
|
|
222
|
+
e2e_images_uploaded: E2E_IMAGES_UPLOADED // from Step 7.5 — { count, stored_paths, skipped, error? } (CHK-171)
|
|
206
223
|
}
|
|
207
224
|
```
|
|
208
225
|
|
|
@@ -41,8 +41,8 @@ input:
|
|
|
41
41
|
path: string # Repo-relative or absolute path to PNG
|
|
42
42
|
page_or_screen: string
|
|
43
43
|
viewport: 'desktop' | 'mobile' | 'tablet' | 'device'
|
|
44
|
-
is_new: bool #
|
|
45
|
-
baseline_diff_pct: number | null # Pixel-diff % vs
|
|
44
|
+
is_new: bool # true = no prior committed baseline; auto-captured+committed this run
|
|
45
|
+
baseline_diff_pct: number | null # Pixel-diff % vs committed baseline (null for non-playwright frameworks)
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
## Output Contract
|
|
@@ -167,9 +167,10 @@ If no design source PNGs exist for the changed pages, skip this phase.
|
|
|
167
167
|
For each screenshot in `e2e_screenshots[]`:
|
|
168
168
|
|
|
169
169
|
1. **Read the PNG via the Read tool** (Claude multimodal — the PNG is shown to the model directly). Do not use Bash to inspect bytes.
|
|
170
|
-
2. **Check
|
|
171
|
-
-
|
|
172
|
-
-
|
|
170
|
+
2. **Check new vs changed baseline**:
|
|
171
|
+
- `is_new === true`: screenshot was auto-captured and committed this run (no prior baseline). Review semantically only — no regression to flag. Populate `screenshot_review.new_screens_reviewed`.
|
|
172
|
+
- `is_new === false` AND `baseline_diff_pct > 0.1%`: emit finding `{category: 'baseline_regression', severity: 'critical', file: {path}, screenshot: {path}, issue: 'Pixel diff vs committed baseline: {diff_pct}%', suggestion: 'Inspect the diff PNG (same folder, -diff suffix). Either fix the regression or, if intentional, run `playwright test --update-snapshots` and commit the new baseline.'}`
|
|
173
|
+
- Do NOT auto-update changed baselines. The user must explicitly approve via QA.
|
|
173
174
|
3. **Semantic review of rendered output** (both new screens and existing):
|
|
174
175
|
- **Text overflow / truncation** — text clipped, ellipsis in unintended places, buttons cut off
|
|
175
176
|
- **Unstyled elements** — unbranded default fonts, missing styles (flash of unstyled content captured), default blue links
|
|
@@ -237,7 +238,8 @@ Go beyond fixing violations — actively improve visual quality. If spacing coul
|
|
|
237
238
|
- Token compliance checked
|
|
238
239
|
- Spacing consistency verified
|
|
239
240
|
- **All `e2e_screenshots` reviewed** (when provided) — rendered output checked for overflow, unstyled elements, missing imagery, contrast, layout breaks, loading/error artifacts, design-source fidelity
|
|
240
|
-
-
|
|
241
|
+
- New-screen baselines reviewed semantically (`is_new === true` — auto-committed, no user gate)
|
|
242
|
+
- Changed-baseline regressions surfaced (never auto-accepted; `is_new === false` AND diff > threshold)
|
|
241
243
|
- Critical/warning issues auto-fixed where possible (styling only, in-scope only)
|
|
242
244
|
- Findings categorized by severity
|
|
243
245
|
|
|
@@ -258,5 +260,5 @@ Go beyond fixing violations — actively improve visual quality. If spacing coul
|
|
|
258
260
|
- **Also invoked by**: `/cbp-checkpoint-check` with screenshots aggregated from a whole-checkpoint e2e run
|
|
259
261
|
- **Consumes**: `e2e_screenshots[]` aggregated from `round.context.e2e_outputs[*].screenshots` (populated by the `cbp-e2e-*` specialists at `/cbp-round-execute` Step 5)
|
|
260
262
|
- **Output written to**: `round.context.frontend_ui_review` — when invoked twice per round, the second invocation merges with the first
|
|
261
|
-
- **Downstream gate**: this skill emits `findings[]` only.
|
|
263
|
+
- **Downstream gate**: this skill emits `findings[]` only. Changed-baseline-regression findings (`is_new === false`) surface as a BLOCKING gate at `/cbp-round-end` Step 7 (never auto-accepted); new-screen baselines (`is_new === true`) are auto-committed and reviewed semantically only; rendered-visual critical findings are surfaced in the Step 7 findings presentation.
|
|
262
264
|
- **Paired with**: `frontend-design` (pre-implementation aesthetic decision), `frontend-ux` (interaction-quality self-review, also Step 3.8)
|
|
@@ -184,11 +184,35 @@ Input contracts: `cbp-testing-qa-agent` receives `executor_output`, `testing_pro
|
|
|
184
184
|
|
|
185
185
|
### Step 5b: Post-E2E Screenshot Review (cbp-frontend-ui Phase 6.5)
|
|
186
186
|
|
|
187
|
-
Aggregate
|
|
187
|
+
Aggregate across ALL specialists that ran:
|
|
188
188
|
|
|
189
|
-
|
|
189
|
+
```js
|
|
190
|
+
screenshots = Object.values(round.context.e2e_outputs ?? {}).flatMap(o => o.screenshots ?? []);
|
|
191
|
+
e2e_gallery = Object.values(round.context.e2e_outputs ?? {}).flatMap(o => o.e2e_gallery ?? []);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Auto-new baseline handling**: for each entry in `e2e_gallery` where `is_new === true`, the
|
|
195
|
+
specialist has already run `git add <committed_path>`. No additional user gate is needed.
|
|
196
|
+
**Changed-baseline handling**: entries where `is_new === false` AND `baseline_diff_pct > threshold`
|
|
197
|
+
are `visual_regression` — do NOT auto-accept; surface as blocking gate at Step 7.
|
|
198
|
+
|
|
199
|
+
Persist `e2e_gallery` to `round.context.e2e_gallery` (additive alongside existing
|
|
200
|
+
`round.context.e2e_outputs`). This field is consumed by TASK-3 / checkpoint-end for DB upload.
|
|
201
|
+
Note: `e2e_gallery[]` is aggregated and persisted regardless of whether `cbp-frontend-ui` runs — the empty-gallery enforcement lives in `cbp-task-check` Phase 4, while the `screenshots[]` visual review (frontend-ui Phase 6.5) is a separate concern gated on `screenshots[]` being non-empty.
|
|
202
|
+
|
|
203
|
+
When the aggregated `screenshots` list is non-empty, invoke the `cbp-frontend-ui` skill with
|
|
204
|
+
`phase: 'screenshot_review'` (input: `files_changed`, `e2e_screenshots: <aggregated screenshots>`,
|
|
205
|
+
`context: { checkpoint_goal, round_requirements }`). Under this phase the skill runs only
|
|
206
|
+
Phase 6.5 (Rendered-Output Visual Review) + 7 + 8 — Phases 1-6 (style) already ran at Step 3.8.
|
|
207
|
+
|
|
208
|
+
Persist findings to `round.context.frontend_ui_review` (merge with Step 3.8's style-only output
|
|
209
|
+
if present). Baseline-regression findings surface as a BLOCKING gate at `/cbp-round-end` Step 7
|
|
210
|
+
(an explicit accept-or-fix user decision; changed baselines are NEVER auto-accepted);
|
|
211
|
+
rendered_visual critical findings are surfaced in the Step 7 findings presentation. Neither
|
|
212
|
+
auto-fails the round. cbp-testing-qa-agent does NOT read these findings (full independence).
|
|
190
213
|
|
|
191
|
-
**Skip** when `round.context.e2e_outputs` is absent/empty, the aggregated `screenshots` list
|
|
214
|
+
**Skip** when `round.context.e2e_outputs` is absent/empty, the aggregated `screenshots` list
|
|
215
|
+
is empty, or `testing_profile === 'claude_only'`.
|
|
192
216
|
|
|
193
217
|
### Step 6: Hard-Fail Routing
|
|
194
218
|
|
|
@@ -215,9 +239,9 @@ When `cbp-testing-qa-agent` spawn fails OR the resolved `testing_profile` is `cl
|
|
|
215
239
|
|
|
216
240
|
Update round context via MCP `update_round` / `update_standalone_round` per KIND:
|
|
217
241
|
|
|
218
|
-
- `context`: { ...existing, executor_output, testing_qa_output, e2e_eligible, e2e_outputs, frontend_ui_review }
|
|
242
|
+
- `context`: { ...existing, executor_output, testing_qa_output, e2e_eligible, e2e_outputs, e2e_gallery, frontend_ui_review }
|
|
219
243
|
|
|
220
|
-
`e2e_outputs` (a framework-keyed map of specialist outputs, e.g. `{ playwright: {...}, maestro: {...} }`) and `frontend_ui_review` are present only when the gates above admitted them (≥1 eligible framework ran AND Step 5b ran). `e2e_eligible[]` records which frameworks were eligible this round and drives the Step 6 `e2e_eligible_skipped` check.
|
|
244
|
+
`e2e_outputs` (a framework-keyed map of specialist outputs, e.g. `{ playwright: {...}, maestro: {...} }`), `e2e_gallery` (aggregated flat array of committed-PNG entries across all specialists — consumed by TASK-3 / checkpoint-end for DB upload), and `frontend_ui_review` are present only when the gates above admitted them (≥1 eligible framework ran AND Step 5b ran). `e2e_eligible[]` records which frameworks were eligible this round and drives the Step 6 `e2e_eligible_skipped` check.
|
|
221
245
|
|
|
222
246
|
### Step 8: Auto-trigger Round End
|
|
223
247
|
|
|
@@ -234,13 +258,13 @@ Trigger `/cbp-round-end`.
|
|
|
234
258
|
- `testing_profile` from `task.context` governs which checks run — read it once in Step 2; pass to every testing-qa + e2e specialist spawn
|
|
235
259
|
- `claude_only` profile skips all agent spawns (testing-qa AND `cbp-e2e-*`); runs hook syntax and skill structure checks inline
|
|
236
260
|
- E2E dispatch is **config-driven and opt-out** (`.codebyplan/e2e.json`), not gated on `has_ui_work`/`testing_profile` — an eligible framework that silently does not run is an `e2e_eligible_skipped` hard-fail (`rules/e2e-mandatory.md`)
|
|
237
|
-
- Step 5b (cbp-frontend-ui Phase 6.5) runs only when e2e produced screenshots — gated on the aggregated `e2e_outputs[*].screenshots[]` being non-empty
|
|
261
|
+
- Step 5b (cbp-frontend-ui Phase 6.5) runs only when e2e produced screenshots — gated on the aggregated `e2e_outputs[*].screenshots[]` being non-empty; `e2e_gallery[]` is always aggregated and persisted when any specialist ran
|
|
238
262
|
- Claude NEVER git adds files in round commands
|
|
239
263
|
|
|
240
264
|
## Integration
|
|
241
265
|
|
|
242
266
|
- **Reads**: MCP `get_current_task` / `get_current_standalone_task`, `get_rounds` / `get_standalone_rounds` (per KIND)
|
|
243
|
-
- **Writes**: MCP `update_round` / `update_standalone_round` (context with executor_output + testing_qa_output + e2e_eligible + e2e_outputs + frontend_ui_review) — per KIND
|
|
267
|
+
- **Writes**: MCP `update_round` / `update_standalone_round` (context with executor_output + testing_qa_output + e2e_eligible + e2e_outputs + e2e_gallery + frontend_ui_review) — per KIND
|
|
244
268
|
- **Spawns**: `cbp-round-executor` (per wave or single), `cbp-testing-qa-agent` (per wave, parallel sibling of the `cbp-e2e-*` specialists), the `cbp-e2e-*` specialists (config-driven dispatch per `context/testing/e2e.md`, one per eligible framework in `.codebyplan/e2e.json`), `cbp-database-agent` (if DB work), `cbp-security-agent` (if security review needed)
|
|
245
269
|
- **Skill invocations**: `cbp-frontend-ui` at Step 5b with `phase: 'screenshot_review'` (post-e2e)
|
|
246
270
|
- **Triggers**: `/cbp-round-end` (auto)
|