forgedock 1.0.17 → 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -1
- package/bin/cross-publish.mjs +344 -0
- package/bin/forgedock.mjs +229 -17
- package/commands/autopilot.md +77 -0
- package/commands/design-bench.md +176 -0
- package/commands/design-render-critique-loop.md +103 -0
- package/commands/design.md +259 -0
- package/commands/issue.md +7 -3
- package/commands/orchestrate.md +121 -0
- package/commands/{resume.md → pipeline-resume.md} +4 -4
- package/commands/{status.md → pipeline-status.md} +1 -1
- package/commands/qa-sweep.md +37 -2
- package/commands/quality-gate.md +57 -0
- package/commands/review-pr-staging.md +158 -2
- package/commands/test-gate.md +929 -0
- package/commands/work-on/build/implement.md +5 -3
- package/commands/work-on.md +53 -3
- package/docs/CI.md +177 -0
- package/docs/FORGE-PROTOCOL.md +324 -0
- package/docs/articles/b-vs-c-baseline.md +210 -0
- package/docs/articles/full-abc-benchmark.md +205 -0
- package/docs/articles/selective-spec-loading-token-savings.md +140 -0
- package/docs/design/abc-benchmark.md +195 -0
- package/docs/design/design-architect-rationale.md +123 -0
- package/docs/design/design-memory.md +61 -0
- package/docs/design/design-spec-schema.md +248 -0
- package/docs/design/design-system-lint.md +150 -0
- package/docs/design/divergent-generation.md +106 -0
- package/docs/design/effects-appropriateness.md +133 -0
- package/docs/design/fixtures/briefs/README.md +47 -0
- package/docs/design/fixtures/briefs/cadence.md +39 -0
- package/docs/design/fixtures/briefs/plume.md +39 -0
- package/docs/design/fixtures/briefs/slipstream.md +39 -0
- package/docs/design/fixtures/briefs/tender.md +39 -0
- package/docs/design/fixtures/briefs/voltage.md +39 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-1.html +364 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-2.html +420 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-3.html +506 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-1.html +611 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-2.html +594 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-3.html +817 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/runs.json +175 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/scorecard.json +138 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-1.html +373 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-2.html +398 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-3.html +595 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-1.html +531 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-2.html +565 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-3.html +674 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-1.html +388 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-2.html +463 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-3.html +568 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-1.html +562 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-2.html +618 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-3.html +618 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-1.html +364 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-2.html +420 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-3.html +506 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-1.html +527 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-2.html +445 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-3.html +483 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-1.html +611 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-2.html +594 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-3.html +817 -0
- package/docs/design/fixtures/runs/full-abc/runs.json +190 -0
- package/docs/design/fixtures/runs/full-abc/scorecard.json +144 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-1.html +496 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-2.html +447 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-3.html +564 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-1.html +373 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-2.html +398 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-3.html +595 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-1.html +622 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-2.html +475 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-3.html +660 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-1.html +531 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-2.html +565 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-3.html +674 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-1.html +525 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-2.html +446 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-3.html +463 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-1.html +388 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-2.html +463 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-3.html +568 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-1.html +1068 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-2.html +1423 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-3.html +1245 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-1.html +305 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-2.html +327 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-3.html +428 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-1.html +1270 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-2.html +1172 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-3.html +1532 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-1.html +315 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-2.html +342 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-3.html +353 -0
- package/docs/design/fixtures/runs/full-abc-opus/runs.json +190 -0
- package/docs/design/fixtures/runs/full-abc-opus/scorecard.json +144 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-1.html +345 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-2.html +396 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-3.html +502 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-1.html +408 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-2.html +446 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-3.html +496 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-1.html +813 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-2.html +711 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-3.html +721 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-1.html +280 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-2.html +291 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-3.html +366 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-1.html +1010 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-2.html +836 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-3.html +930 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-1.html +407 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-2.html +396 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-3.html +529 -0
- package/docs/design/foundations/brutalist.css +294 -0
- package/docs/design/foundations/editorial.css +300 -0
- package/docs/design/foundations/minimal-luxury.css +295 -0
- package/docs/design/foundations/technical.css +293 -0
- package/docs/design/foundations/warm-photo.css +299 -0
- package/docs/design/reference-corpus.md +1726 -0
- package/docs/design/render-critique-loop.md +57 -0
- package/docs/spec-graph-schema.md +397 -0
- package/package.json +1 -1
- package/scripts/bench-scorecard.mjs +239 -0
- package/scripts/build-spec-graph.mjs +509 -0
- package/scripts/check-native-conflicts.sh +200 -0
- package/scripts/classify-lane.sh +0 -7
- package/scripts/design-system-lint.mjs +993 -0
- package/scripts/graph-query.sh +486 -0
- package/scripts/transition-label.sh +10 -2
- package/scripts/validate-spec-graph.sh +423 -0
- package/templates/workflows/forgedock-review.yml +96 -0
package/bin/forgedock.mjs
CHANGED
|
@@ -204,7 +204,7 @@ function stripJsonc(raw) {
|
|
|
204
204
|
if (ch === "/" && i + 1 < len && raw[i + 1] === "*") {
|
|
205
205
|
i += 2;
|
|
206
206
|
while (i + 1 < len && !(raw[i] === "*" && raw[i + 1] === "/")) i++;
|
|
207
|
-
i += 2;
|
|
207
|
+
if (i + 1 < len) i += 2; // only advance past */ if terminator was found
|
|
208
208
|
continue;
|
|
209
209
|
}
|
|
210
210
|
|
|
@@ -240,7 +240,7 @@ function stripJsonc(raw) {
|
|
|
240
240
|
if (j + 1 < len && raw[j] === "/" && raw[j + 1] === "*") {
|
|
241
241
|
j += 2;
|
|
242
242
|
while (j + 1 < len && !(raw[j] === "*" && raw[j + 1] === "/")) j++;
|
|
243
|
-
j += 2;
|
|
243
|
+
if (j + 1 < len) j += 2; // only advance past */ if terminator was found
|
|
244
244
|
advanced = true;
|
|
245
245
|
}
|
|
246
246
|
}
|
|
@@ -321,6 +321,51 @@ function atomicWriteFile(filePath, content) {
|
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Idempotently write a `.symlink-source` sentinel file to TARGET_DIR
|
|
326
|
+
* (~/.claude/commands/) so other installers can detect that ForgeDock
|
|
327
|
+
* owns this namespace.
|
|
328
|
+
*
|
|
329
|
+
* The sentinel is a plain UTF-8 text file (not a symlink) containing
|
|
330
|
+
* the source path, a reinstall hint, and the install timestamp. It is
|
|
331
|
+
* updated on every install so the timestamp stays fresh. Write failures
|
|
332
|
+
* are non-fatal — the function returns 'failed' and the caller logs a
|
|
333
|
+
* warning; install continues regardless.
|
|
334
|
+
*
|
|
335
|
+
* Calling convention matches the other state-write helpers:
|
|
336
|
+
* 'written' — sentinel was newly created or refreshed
|
|
337
|
+
* 'failed' — an error occurred (non-fatal; install continues)
|
|
338
|
+
*
|
|
339
|
+
* <!-- Added: forge#1038 -->
|
|
340
|
+
*
|
|
341
|
+
* @returns {'written' | 'failed'}
|
|
342
|
+
*/
|
|
343
|
+
function writeSymlinkSentinel() {
|
|
344
|
+
const sentinelPath = join(TARGET_DIR, ".symlink-source");
|
|
345
|
+
const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
346
|
+
const content =
|
|
347
|
+
`# ForgeDock command symlinks — DO NOT REPOINT\n` +
|
|
348
|
+
`# Source: ${COMMANDS_DIR}\n` +
|
|
349
|
+
`#\n` +
|
|
350
|
+
`# These symlinks are managed by ForgeDock (https://forgedock.com).\n` +
|
|
351
|
+
`# ForgeDock owns the global ~/.claude/commands/ namespace.\n` +
|
|
352
|
+
`# Project-specific commands should install to the project's\n` +
|
|
353
|
+
`# .claude/commands/ directory instead (Claude Code merges both;\n` +
|
|
354
|
+
`# project-local commands win on name collisions).\n` +
|
|
355
|
+
`#\n` +
|
|
356
|
+
`# Running another installer here will silently repoint these\n` +
|
|
357
|
+
`# symlinks, breaking all ForgeDock commands globally.\n` +
|
|
358
|
+
`#\n` +
|
|
359
|
+
`# To reinstall ForgeDock: npx forgedock install\n` +
|
|
360
|
+
`# Last installed: ${timestamp}\n`;
|
|
361
|
+
try {
|
|
362
|
+
atomicWriteFile(sentinelPath, content);
|
|
363
|
+
return "written";
|
|
364
|
+
} catch {
|
|
365
|
+
return "failed";
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
324
369
|
/**
|
|
325
370
|
* Install the ForgeDock SessionStart hook into ~/.claude/settings.json
|
|
326
371
|
* idempotently. Does not modify any existing hooks unrelated to ForgeDock.
|
|
@@ -566,9 +611,18 @@ function injectManagedBlock(filePath) {
|
|
|
566
611
|
"[\\s\\S]*?" +
|
|
567
612
|
escapeRegExp(CLAUDE_BLOCK_END),
|
|
568
613
|
);
|
|
614
|
+
if (!blockRegex.test(existing)) {
|
|
615
|
+
// Should not happen: both markers were confirmed above via includes() and
|
|
616
|
+
// indexOf() ordering. Guard against silent misclassification — if the regex
|
|
617
|
+
// somehow fails to match, replaced === existing would incorrectly return
|
|
618
|
+
// 'unchanged' instead of surfacing the broken state.
|
|
619
|
+
throw new Error(
|
|
620
|
+
`injectManagedBlock: regex failed to match markers in ${filePath}`,
|
|
621
|
+
);
|
|
622
|
+
}
|
|
569
623
|
const replaced = existing.replace(blockRegex, managed);
|
|
570
624
|
if (replaced === existing) {
|
|
571
|
-
//
|
|
625
|
+
// Content between markers was already identical to the managed block
|
|
572
626
|
return "unchanged";
|
|
573
627
|
}
|
|
574
628
|
atomicWriteFile(filePath, replaced);
|
|
@@ -754,8 +808,12 @@ function removeForgeHomeFromProfile(profilePath) {
|
|
|
754
808
|
// Step 1: Remove the 2-line ForgeDock block (comment + export), preceded
|
|
755
809
|
// by an optional leading blank line that install() inserts.
|
|
756
810
|
// The \r? handles profiles with CRLF line endings.
|
|
811
|
+
// The value pattern (?:[^"\\]|\\.)* matches a double-quoted shell string
|
|
812
|
+
// that may contain backslash-escape sequences (e.g. \" or \\) written by
|
|
813
|
+
// shellEscapeDoubleQuotedPath(). The simpler [^"]* would stop prematurely
|
|
814
|
+
// at the escaped-quote character inside \", failing to match the full line.
|
|
757
815
|
let cleaned = content.replace(
|
|
758
|
-
/\r?\n[ \t]*# ForgeDock — autonomous development pipeline\r?\nexport FORGE_HOME="[^"]*"\r?\n/g,
|
|
816
|
+
/\r?\n[ \t]*# ForgeDock — autonomous development pipeline\r?\nexport FORGE_HOME="(?:[^"\\]|\\.)*"\r?\n/g,
|
|
759
817
|
"\n",
|
|
760
818
|
);
|
|
761
819
|
|
|
@@ -820,6 +878,16 @@ async function linkCommands(step) {
|
|
|
820
878
|
if (current === file) {
|
|
821
879
|
skipped++;
|
|
822
880
|
} else {
|
|
881
|
+
// Symlink points to a different source — warn if it appears to belong
|
|
882
|
+
// to another Forge/ForgeDock installation (i.e. not a broken link that
|
|
883
|
+
// already pointed here under a different absolute path).
|
|
884
|
+
// Only emit the warning once per install to avoid log spam.
|
|
885
|
+
// <!-- Added: forge#1038 -->
|
|
886
|
+
if (!current.startsWith(COMMANDS_DIR)) {
|
|
887
|
+
step.note(
|
|
888
|
+
yellow(`collision: ${rel} was → ${current} — repointing to ForgeDock`),
|
|
889
|
+
);
|
|
890
|
+
}
|
|
823
891
|
try {
|
|
824
892
|
await symlink(file, target + ".tmp");
|
|
825
893
|
const { rename } = await import("fs/promises");
|
|
@@ -986,6 +1054,29 @@ async function install() {
|
|
|
986
1054
|
{
|
|
987
1055
|
label: "Checking environment",
|
|
988
1056
|
async run(step) {
|
|
1057
|
+
// Worktree guard — refuse to install when FORGE_HOME is a git worktree.
|
|
1058
|
+
// In a worktree, FORGE_HOME/.git is a regular file (not a directory).
|
|
1059
|
+
// Installing from a worktree would bake an ephemeral path into the
|
|
1060
|
+
// symlinks and SessionStart hook; the path breaks when the worktree
|
|
1061
|
+
// is cleaned up, taking all Forge commands offline globally.
|
|
1062
|
+
// <!-- Added: forge#1037 -->
|
|
1063
|
+
const gitPath = join(FORGE_HOME, ".git");
|
|
1064
|
+
let gitStat = null;
|
|
1065
|
+
try {
|
|
1066
|
+
gitStat = await lstat(gitPath);
|
|
1067
|
+
} catch (err) {
|
|
1068
|
+
if (err.code !== "ENOENT") throw err;
|
|
1069
|
+
// .git absent — npm install or detached checkout; not a worktree.
|
|
1070
|
+
}
|
|
1071
|
+
if (gitStat !== null && gitStat.isFile()) {
|
|
1072
|
+
throw new Error(
|
|
1073
|
+
`install() is running from a git worktree (${FORGE_HOME}).\n` +
|
|
1074
|
+
` Installing from a worktree would repoint ~/.claude/commands/ symlinks\n` +
|
|
1075
|
+
` to an ephemeral path that breaks when the worktree is deleted.\n` +
|
|
1076
|
+
` Run \`npx forgedock install\` from the main repository clone instead.`,
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
989
1080
|
await mkdir(TARGET_DIR, { recursive: true });
|
|
990
1081
|
step.note(cyan(TARGET_DIR));
|
|
991
1082
|
},
|
|
@@ -1000,6 +1091,20 @@ async function install() {
|
|
|
1000
1091
|
);
|
|
1001
1092
|
},
|
|
1002
1093
|
},
|
|
1094
|
+
{
|
|
1095
|
+
label: "Writing namespace sentinel",
|
|
1096
|
+
async run(step) {
|
|
1097
|
+
// Write .symlink-source to ~/.claude/commands/ so other installers
|
|
1098
|
+
// can detect that ForgeDock owns this namespace. Non-fatal on failure.
|
|
1099
|
+
// <!-- Added: forge#1038 -->
|
|
1100
|
+
const result = writeSymlinkSentinel();
|
|
1101
|
+
if (result === "failed") {
|
|
1102
|
+
step.note("could not write .symlink-source — run npx forgedock install again to retry");
|
|
1103
|
+
} else {
|
|
1104
|
+
step.note(cyan(join(TARGET_DIR, ".symlink-source")));
|
|
1105
|
+
}
|
|
1106
|
+
},
|
|
1107
|
+
},
|
|
1003
1108
|
{
|
|
1004
1109
|
label: "Linking scripts",
|
|
1005
1110
|
async run(step) {
|
|
@@ -1036,7 +1141,12 @@ async function install() {
|
|
|
1036
1141
|
// Either not present, or set to a stale path — refresh it.
|
|
1037
1142
|
if (content.includes("FORGE_HOME")) {
|
|
1038
1143
|
// Strip the old block before appending the updated one.
|
|
1039
|
-
removeForgeHomeFromProfile(profile);
|
|
1144
|
+
const removeResult = removeForgeHomeFromProfile(profile);
|
|
1145
|
+
if (removeResult === "failed") {
|
|
1146
|
+
// Removal failed — old export still present. Skip append to
|
|
1147
|
+
// avoid writing a duplicate FORGE_HOME export. (ref: forge#846)
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1040
1150
|
}
|
|
1041
1151
|
appendFileSync(
|
|
1042
1152
|
profile,
|
|
@@ -1158,8 +1268,17 @@ async function uninstall() {
|
|
|
1158
1268
|
console.log(` ${RED}Removed${RESET}: ${rel}`);
|
|
1159
1269
|
removed++;
|
|
1160
1270
|
}
|
|
1271
|
+
} else if (
|
|
1272
|
+
readFileSync(file, "utf-8") === readFileSync(target, "utf-8")
|
|
1273
|
+
) {
|
|
1274
|
+
// Regular file installed by ForgeDock in copy mode — content matches.
|
|
1275
|
+
const { unlink } = await import("fs/promises");
|
|
1276
|
+
await unlink(target);
|
|
1277
|
+
console.log(` ${RED}Removed${RESET}: ${rel}`);
|
|
1278
|
+
removed++;
|
|
1161
1279
|
}
|
|
1162
|
-
} catch {
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
if (err.code !== "ENOENT") throw err;
|
|
1163
1282
|
// Doesn't exist — nothing to do
|
|
1164
1283
|
}
|
|
1165
1284
|
}
|
|
@@ -1580,6 +1699,14 @@ async function init(fromInstall = false) {
|
|
|
1580
1699
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1581
1700
|
.join(" ");
|
|
1582
1701
|
|
|
1702
|
+
// Pre-escape values for embedding in YAML double-quoted strings.
|
|
1703
|
+
// Backslash must be escaped first to avoid double-escaping.
|
|
1704
|
+
const safeOwner = owner.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
1705
|
+
const safeRepo = repo.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
1706
|
+
const safeProjectName = projectName.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
1707
|
+
const safeDefaultBranch = defaultBranch.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
1708
|
+
const safeStagingBranch = stagingBranch.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
1709
|
+
|
|
1583
1710
|
const content = `# forge.yaml — ForgeDock Configuration
|
|
1584
1711
|
#
|
|
1585
1712
|
# Auto-generated by: npx forgedock init
|
|
@@ -1595,9 +1722,9 @@ async function init(fromInstall = false) {
|
|
|
1595
1722
|
# =============================================================================
|
|
1596
1723
|
|
|
1597
1724
|
project:
|
|
1598
|
-
name: "${
|
|
1599
|
-
owner: "${
|
|
1600
|
-
repo: "${
|
|
1725
|
+
name: "${safeProjectName}"
|
|
1726
|
+
owner: "${safeOwner}"
|
|
1727
|
+
repo: "${safeRepo}"
|
|
1601
1728
|
description: "${description.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"
|
|
1602
1729
|
|
|
1603
1730
|
# =============================================================================
|
|
@@ -1613,8 +1740,8 @@ paths:
|
|
|
1613
1740
|
# =============================================================================
|
|
1614
1741
|
|
|
1615
1742
|
branches:
|
|
1616
|
-
default: "${
|
|
1617
|
-
staging: "${
|
|
1743
|
+
default: "${safeDefaultBranch}"
|
|
1744
|
+
staging: "${safeStagingBranch}"
|
|
1618
1745
|
feature_pattern: "milestone/{slug}"
|
|
1619
1746
|
|
|
1620
1747
|
# =============================================================================
|
|
@@ -1624,22 +1751,22 @@ branches:
|
|
|
1624
1751
|
|
|
1625
1752
|
# repos:
|
|
1626
1753
|
# default:
|
|
1627
|
-
# repo: "${
|
|
1628
|
-
# staging_branch: "${
|
|
1754
|
+
# repo: "${safeOwner}/${safeRepo}"
|
|
1755
|
+
# staging_branch: "${safeStagingBranch}"
|
|
1629
1756
|
# satellites:
|
|
1630
1757
|
# - prefix: "mcp"
|
|
1631
|
-
# repo: "${
|
|
1758
|
+
# repo: "${safeOwner}/your-satellite-repo"
|
|
1632
1759
|
# staging_branch: "main"
|
|
1633
1760
|
# local_path: "${join(cwd, "..", "your-satellite-repo").replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"
|
|
1634
1761
|
|
|
1635
1762
|
# =============================================================================
|
|
1636
1763
|
# PROJECT BOARD (OPTIONAL)
|
|
1637
1764
|
# GitHub Projects v2 integration.
|
|
1638
|
-
# To find IDs: gh project list --owner ${
|
|
1765
|
+
# To find IDs: gh project list --owner ${safeOwner}
|
|
1639
1766
|
# =============================================================================
|
|
1640
1767
|
|
|
1641
1768
|
# project_board:
|
|
1642
|
-
# owner: "${
|
|
1769
|
+
# owner: "${safeOwner}"
|
|
1643
1770
|
# project_number: 1
|
|
1644
1771
|
# project_id: "PVT_kwHOxxxxxxxxxxxxxxxx"
|
|
1645
1772
|
# field_ids:
|
|
@@ -1665,9 +1792,37 @@ branches:
|
|
|
1665
1792
|
# =============================================================================
|
|
1666
1793
|
|
|
1667
1794
|
# verification:
|
|
1668
|
-
# health_endpoint: "https://api.${
|
|
1795
|
+
# health_endpoint: "https://api.${safeRepo}.io/health"
|
|
1669
1796
|
# health_patterns:
|
|
1670
1797
|
# - '"status": "ok"'
|
|
1798
|
+
#
|
|
1799
|
+
# # Integration test suites run by /test-gate against the provisioned cluster.
|
|
1800
|
+
# # Each entry specifies a logical cluster (matched to test_services), the shell
|
|
1801
|
+
# # command to execute, and the working directory (relative to project root).
|
|
1802
|
+
# # Used by: test-gate (Phase 3 Provision + Phase 4 Fan out)
|
|
1803
|
+
# #
|
|
1804
|
+
# # integration_tests:
|
|
1805
|
+
# # - cluster: "api"
|
|
1806
|
+
# # command: "pytest tests/integration/ -q --tb=short"
|
|
1807
|
+
# # working_dir: "."
|
|
1808
|
+
#
|
|
1809
|
+
# # Maps logical cluster names (from integration_tests) to running container
|
|
1810
|
+
# # names. /test-gate checks these containers are live before running tests.
|
|
1811
|
+
# # Omit entries for clusters that do not use Docker.
|
|
1812
|
+
# # Used by: test-gate (Phase 3 Provision)
|
|
1813
|
+
# #
|
|
1814
|
+
# # test_services:
|
|
1815
|
+
# # api: "${safeRepo}-api-blue"
|
|
1816
|
+
# # worker: "${safeRepo}-worker-blue"
|
|
1817
|
+
#
|
|
1818
|
+
# # Controls /test-gate posture at the staging-to-main deploy boundary.
|
|
1819
|
+
# # posture: blocking (default) or advisory.
|
|
1820
|
+
# # override_phrase: exact comment text an operator posts to bypass a BLOCK.
|
|
1821
|
+
# # Used by: test-gate (Phase 7 Verdict), review-pr-staging (Phase 6.5)
|
|
1822
|
+
# #
|
|
1823
|
+
# # test_gate:
|
|
1824
|
+
# # posture: "blocking"
|
|
1825
|
+
# # override_phrase: "OVERRIDE: shipping with test failures \u2014"
|
|
1671
1826
|
`;
|
|
1672
1827
|
|
|
1673
1828
|
atomicWriteFile(outputPath, content);
|
|
@@ -1866,6 +2021,7 @@ async function status(dir) {
|
|
|
1866
2021
|
* 6. CLAUDE.md has the ForgeDock behavioral block (cwd)
|
|
1867
2022
|
* 7. Required workflow labels exist on the GitHub repo (needs forge.yaml + gh auth)
|
|
1868
2023
|
* 8. FORGE_HOME environment variable is set
|
|
2024
|
+
* 9. Playwright MCP registered in ~/.claude/mcp_servers.json (advisory warn — required for /qa-sweep)
|
|
1869
2025
|
*
|
|
1870
2026
|
* Exits with code 0 if all checks pass, code 1 if any fail.
|
|
1871
2027
|
*/
|
|
@@ -2229,6 +2385,62 @@ async function doctor() {
|
|
|
2229
2385
|
}
|
|
2230
2386
|
}
|
|
2231
2387
|
|
|
2388
|
+
// ── Check 9: Playwright MCP registered ────────────────────────────────────
|
|
2389
|
+
// Playwright MCP is a guaranteed ForgeDock dependency for browser automation
|
|
2390
|
+
// commands (/qa-sweep). This check is advisory (warn, not fail) because MCP
|
|
2391
|
+
// server presence is orthogonal to the core ForgeDock install — but missing
|
|
2392
|
+
// it causes /qa-sweep to silently fail mid-sweep.
|
|
2393
|
+
{
|
|
2394
|
+
const mcpServersPath = join(HOME, ".claude", "mcp_servers.json");
|
|
2395
|
+
let playwrightFound = false;
|
|
2396
|
+
let fileReadable = false;
|
|
2397
|
+
let hasMcpServers = false;
|
|
2398
|
+
|
|
2399
|
+
try {
|
|
2400
|
+
const mcpRaw = readFileSync(mcpServersPath, "utf-8");
|
|
2401
|
+
const mcpConfig = JSON.parse(mcpRaw);
|
|
2402
|
+
// Read + parse succeeded — the file exists and contains valid JSON.
|
|
2403
|
+
fileReadable = true;
|
|
2404
|
+
const servers = mcpConfig?.mcpServers;
|
|
2405
|
+
if (servers && typeof servers === "object") {
|
|
2406
|
+
hasMcpServers = true;
|
|
2407
|
+
// Search for a registered server whose name or command references playwright
|
|
2408
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
2409
|
+
const nameLower = name.toLowerCase();
|
|
2410
|
+
const cmdStr = [
|
|
2411
|
+
entry?.command ?? "",
|
|
2412
|
+
...(Array.isArray(entry?.args) ? entry.args : []),
|
|
2413
|
+
].join(" ").toLowerCase();
|
|
2414
|
+
if (nameLower.includes("playwright") || cmdStr.includes("playwright")) {
|
|
2415
|
+
playwrightFound = true;
|
|
2416
|
+
break;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
} catch {
|
|
2421
|
+
// File absent or unreadable — treat as no MCP servers configured
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
if (!fileReadable) {
|
|
2425
|
+
warn(
|
|
2426
|
+
"Playwright MCP",
|
|
2427
|
+
"No MCP servers file found (~/.claude/mcp_servers.json). Register Playwright MCP to enable /qa-sweep: claude mcp add playwright npx @playwright/mcp@latest",
|
|
2428
|
+
);
|
|
2429
|
+
} else if (!hasMcpServers) {
|
|
2430
|
+
warn(
|
|
2431
|
+
"Playwright MCP",
|
|
2432
|
+
"MCP servers file found (~/.claude/mcp_servers.json) but it has no 'mcpServers' key. Register Playwright MCP to enable /qa-sweep: claude mcp add playwright npx @playwright/mcp@latest",
|
|
2433
|
+
);
|
|
2434
|
+
} else if (playwrightFound) {
|
|
2435
|
+
pass("Playwright MCP", "registered in Claude Code MCP servers");
|
|
2436
|
+
} else {
|
|
2437
|
+
warn(
|
|
2438
|
+
"Playwright MCP",
|
|
2439
|
+
"Not registered — /qa-sweep and browser automation commands will fail. Run: claude mcp add playwright npx @playwright/mcp@latest",
|
|
2440
|
+
);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2232
2444
|
// ── Summary ────────────────────────────────────────────────────────────────
|
|
2233
2445
|
console.log("");
|
|
2234
2446
|
if (failures === 0 && warnings === 0) {
|
package/commands/autopilot.md
CHANGED
|
@@ -312,6 +312,7 @@ Print a structured report:
|
|
|
312
312
|
- Issues commented: {count}
|
|
313
313
|
- Orphans closed: {count}
|
|
314
314
|
- Stale labels fixed: {count}
|
|
315
|
+
- Spec-edit impact analyses logged: {count} (FORGE:AUTOPILOT_IMPACT — blast radius surfaced before spec fixes)
|
|
315
316
|
|
|
316
317
|
### Recommended Next Steps
|
|
317
318
|
{Prioritized list of what the user should look at or deploy}
|
|
@@ -372,6 +373,81 @@ Nothing merges to `main` — you deploy when ready.
|
|
|
372
373
|
|
|
373
374
|
Wait for user response. If they adjust, re-select. If they skip, end the cycle.
|
|
374
375
|
|
|
376
|
+
### 4B.5: Spec-Edit Impact Analysis (MANDATORY before any spec-touching fix) <!-- Added: forge#870 -->
|
|
377
|
+
|
|
378
|
+
**Goal**: Before autopilot dispatches a fix that edits ForgeDock's own command specs (`commands/*.md`), surface the blast radius of the change so the edit doesn't silently break downstream consumers (every spec that reads a changed FORGE annotation, every command in a changed sub-phase chain, every script/label a touched node feeds).
|
|
379
|
+
|
|
380
|
+
This step **consumes the read-only query interface** shipped by the Spec Knowledge Graph (`scripts/graph-query.sh`, subcommand `impact`). It does **not** reimplement graph traversal — the blast radius is computed once, in `graph-query.sh`, and autopilot only reads its output.
|
|
381
|
+
|
|
382
|
+
**When it runs**: for each approved fix target whose affected files (from the issue body's `## Affected Files` section, or the recon finding that created it) include any path matching `commands/*.md`. Non-spec fixes skip this step entirely.
|
|
383
|
+
|
|
384
|
+
**Graceful degradation**: `graph-query.sh` ships only with the Spec Knowledge Graph. If it is absent (fast-lane repos, or a checkout predating the milestone), **skip with a noted reason — never block the cycle**.
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
GRAPH_QUERY="$(git rev-parse --show-toplevel)/scripts/graph-query.sh"
|
|
388
|
+
|
|
389
|
+
# For each approved fix target that edits commands/*.md:
|
|
390
|
+
for issue in $SPEC_FIX_TARGETS; do
|
|
391
|
+
if [ ! -x "$GRAPH_QUERY" ]; then
|
|
392
|
+
echo "skip impact analysis for #$issue — graph-query.sh not present (Spec Knowledge Graph not installed)"
|
|
393
|
+
continue
|
|
394
|
+
fi
|
|
395
|
+
|
|
396
|
+
# Derive the spec node(s) the fix changes. Prefer the FORGE annotation or
|
|
397
|
+
# command/sub-phase named in the issue's Affected Files / recon finding.
|
|
398
|
+
# graph-query.sh normalizes all of these forms:
|
|
399
|
+
# FORGE:CONTRACT == CONTRACT == ann:FORGE:CONTRACT
|
|
400
|
+
# work-on:build:implement == cmd:work-on:build:implement
|
|
401
|
+
# classify-lane.sh == script:classify-lane.sh
|
|
402
|
+
# workflow:merged == label:workflow:merged
|
|
403
|
+
for node in $CHANGED_SPEC_NODES; do
|
|
404
|
+
echo "### Impact of changing $node (fix #$issue)"
|
|
405
|
+
bash "$GRAPH_QUERY" impact "$node" --human
|
|
406
|
+
done
|
|
407
|
+
done
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
**Log the result in a FORGE annotation.** Post one `FORGE:AUTOPILOT_IMPACT` comment per spec-touching fix target on its issue, BEFORE invoking `/work-on` in 4C:
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
gh issue comment $issue --body "<!-- FORGE:AUTOPILOT_IMPACT -->
|
|
414
|
+
## Spec-Edit Impact Analysis
|
|
415
|
+
|
|
416
|
+
**Changed spec node(s)**: \`$CHANGED_SPEC_NODES\`
|
|
417
|
+
**Query**: \`graph-query.sh impact <node> --human\` (Spec Knowledge Graph, read-only)
|
|
418
|
+
|
|
419
|
+
### Blast Radius (downstream consumers of this change)
|
|
420
|
+
\`\`\`
|
|
421
|
+
$IMPACT_OUTPUT
|
|
422
|
+
\`\`\`
|
|
423
|
+
|
|
424
|
+
Every command/spec listed above reads the changed annotation or sits in the changed sub-phase chain. The \`/work-on\` build for this fix MUST keep these consumers working — verify the changed field/marker is still produced in the shape they expect.
|
|
425
|
+
|
|
426
|
+
<!-- FORGE:AUTOPILOT_IMPACT:COMPLETE -->"
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
If `graph-query.sh` was absent, post the same annotation with the blast-radius block set to `(skipped — Spec Knowledge Graph not installed)` so the skip is auditable.
|
|
430
|
+
|
|
431
|
+
**Worked example** — autopilot picks up a fix that renames a field inside the `FORGE:CONTRACT` annotation. Before dispatching `/work-on`, it runs:
|
|
432
|
+
|
|
433
|
+
```bash
|
|
434
|
+
$ graph-query.sh impact FORGE:CONTRACT --human
|
|
435
|
+
Impact (blast radius) of changing ann:FORGE:CONTRACT:
|
|
436
|
+
cmd:milestone
|
|
437
|
+
cmd:orchestrate
|
|
438
|
+
cmd:resume
|
|
439
|
+
cmd:review-pr
|
|
440
|
+
cmd:work-on
|
|
441
|
+
cmd:work-on-monolithic
|
|
442
|
+
cmd:work-on:build
|
|
443
|
+
cmd:work-on:build:implement
|
|
444
|
+
cmd:work-on:review
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
The summary makes the nine downstream consumers explicit before the edit lands, so the contract-field rename is reviewed against every reader — not discovered as a broken consumer later.
|
|
448
|
+
|
|
449
|
+
**`FORGE:AUTOPILOT_IMPACT`** is a leaf annotation: autopilot is its only writer and no downstream pipeline phase consumes it. It exists to record the blast radius on the issue thread for human and reviewer visibility.
|
|
450
|
+
|
|
375
451
|
### 4C: Execute fixes
|
|
376
452
|
|
|
377
453
|
For each approved fix target, invoke `/work-on` via the Skill tool:
|
|
@@ -403,6 +479,7 @@ FIX_RESULTS.push({
|
|
|
403
479
|
|
|
404
480
|
**Merged to staging**: {count}
|
|
405
481
|
**Needs attention**: {list}
|
|
482
|
+
**Spec-edit impact analyses**: {count} blast-radius summaries logged (FORGE:AUTOPILOT_IMPACT) before spec fixes
|
|
406
483
|
|
|
407
484
|
When ready to deploy, merge `staging` → `main` via GitHub.
|
|
408
485
|
```
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Run the ABC benchmark — reference URL → design-blind brief → render arms A/B/C → blind judge → scorecard with win-rates over n runs
|
|
3
|
+
argument-hint: [<brief-name>|all] [--n <runs>] [--corpus-version <ver>]
|
|
4
|
+
---
|
|
5
|
+
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
6
|
+
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
|
7
|
+
|
|
8
|
+
# /design-bench — The ABC Benchmark Rig
|
|
9
|
+
|
|
10
|
+
**Input**: $ARGUMENTS — a seed brief name (`cadence` | `tender` | `slipstream` | `voltage` | `plume`) or `all`, with optional `--n <runs>` (default 3, min 3) and `--corpus-version <ver>`.
|
|
11
|
+
|
|
12
|
+
This is the **fitness function** for the UI Taste Harness (milestone #13, issue #878). It is built *before*
|
|
13
|
+
the harness and is the only thing that tells us whether each lever the harness adds actually moves design
|
|
14
|
+
quality. Running this command answers one question: **did arm A (the harness) score higher against the real
|
|
15
|
+
reference (arm C) than it did last time?**
|
|
16
|
+
|
|
17
|
+
Read [`docs/design/abc-benchmark.md`](../docs/design/abc-benchmark.md) (methodology) before running. The three
|
|
18
|
+
arms, the same-model rule, the C-as-calibration check, the three-layer judging protocol, and the n>=3 rule all
|
|
19
|
+
live there — this command is the executable workflow over that methodology.
|
|
20
|
+
|
|
21
|
+
**No application runtime.** ForgeDock is a set of command specs. This command is a step-by-step procedure an
|
|
22
|
+
agent executes; the only executable helper is the deterministic aggregator `scripts/bench-scorecard.mjs`.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## The three arms (one variable)
|
|
27
|
+
|
|
28
|
+
| Arm | What it is | How this command produces it |
|
|
29
|
+
|-----|------------|------------------------------|
|
|
30
|
+
| **A** | ForgeDock harness output | Run the brief through `/design` (#888). **Pluggable** — see [Arm A is pluggable](#arm-a-is-pluggable). |
|
|
31
|
+
| **B** | Raw frontier model, one shot | Hand the brief to the model once, no tools, no iteration. |
|
|
32
|
+
| **C** | The real reference page | Capture the brief's real reference URL through the same render pipeline. |
|
|
33
|
+
|
|
34
|
+
**Hard rule — same generation model for A and B.** The harness is the ONLY difference between A and B; that is
|
|
35
|
+
what makes the A-vs-B delta attributable to the harness. Different models benchmark models, not the harness.
|
|
36
|
+
|
|
37
|
+
**Validated generation model: `claude-opus-4-6`.** Benchmark results (#878) show a tier-level quality difference: 90% A-vs-B pairwise win-rate with Opus vs 54% with Sonnet, rubric mean 4.11 vs 3.40, slop 1.2 vs 1.8. The canonical baseline going forward is the Opus run. Raw data: `docs/design/fixtures/runs/full-abc-opus/`. Use Opus for both arm A and arm B unless explicitly testing a different model.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Phase 0: Resolve inputs
|
|
42
|
+
|
|
43
|
+
1. Parse `$ARGUMENTS`: the brief name(s) and `--n` (default 3; **reject n<3** — n=1 is a lie). `all` = every
|
|
44
|
+
brief under `docs/design/fixtures/briefs/` (excluding `README.md`).
|
|
45
|
+
2. For each brief, load `docs/design/fixtures/briefs/<name>.md`. This is the design-blind input handed to arms
|
|
46
|
+
A and B.
|
|
47
|
+
3. Load the brief→reference(C) mapping from `docs/design/fixtures/briefs/README.md`. **The mapping is for the
|
|
48
|
+
judge/calibration record only — it is NEVER shown to the generating arms.** A real brand name leaking into a
|
|
49
|
+
brief measures recall, not taste, and inflates arm B.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Phase 1: Brief (reference URL → design-blind brief)
|
|
54
|
+
|
|
55
|
+
The seed briefs are pre-stripped fixtures, so for the seed set this phase is a load, not a derivation.
|
|
56
|
+
|
|
57
|
+
**To add a new reference URL** (outside the seed set): fetch the page, strip every visual cue (color, type,
|
|
58
|
+
layout, "make it like X"), keep only what the product *does* / who it's for / what the page must communicate,
|
|
59
|
+
and assign an **anonymized** product name. Save as a new fixture and add its mapping row. Follow
|
|
60
|
+
`docs/design/fixtures/briefs/README.md`.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Phase 2: Render arms (kill confounds)
|
|
65
|
+
|
|
66
|
+
Render **all three arms through the identical pipeline** so the judge compares design, not rendering artifacts.
|
|
67
|
+
|
|
68
|
+
For each of the `--n` runs per brief:
|
|
69
|
+
|
|
70
|
+
1. **Arm A** — run the brief through `/design` (#888) → self-contained HTML.
|
|
71
|
+
2. **Arm B** — one-shot the SAME model with the brief → self-contained HTML. No tools, no iteration.
|
|
72
|
+
3. **Arm C** — the real reference URL.
|
|
73
|
+
|
|
74
|
+
Render every arm with **Playwright** (milestone render dependency) at:
|
|
75
|
+
|
|
76
|
+
- **1440px** desktop and **390px** mobile,
|
|
77
|
+
- `networkidle` + fonts loaded before capture,
|
|
78
|
+
- **full-page** screenshot.
|
|
79
|
+
|
|
80
|
+
For arm C, trim cookie banners / hero video and **record any trim applied** in the run record (reproducibility).
|
|
81
|
+
|
|
82
|
+
> **Arm A is pluggable.** Until `/design` (#888) lands, arm A is produced by a clearly-labeled manual hook:
|
|
83
|
+
> generate arm A by hand using the harness method-of-the-day and drop its HTML into the run record marked
|
|
84
|
+
> `arm_a_source: manual`. The rig — brief loading, render protocol, judging, scorecard — is fully operational
|
|
85
|
+
> without #888. When #888 lands, replace the manual hook with the `/design` invocation; nothing else changes.
|
|
86
|
+
> **Do NOT block the benchmark on #888.**
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Phase 3: Judge (three layers)
|
|
91
|
+
|
|
92
|
+
The judge is a **named, independent stage**. See [Anti-Goodhart](#anti-goodhart) — it MUST NOT be the harness's
|
|
93
|
+
own critique loop (#882).
|
|
94
|
+
|
|
95
|
+
For each run, produce judge output in the [runs-file schema](../docs/design/abc-benchmark.md#scorecard-schema):
|
|
96
|
+
|
|
97
|
+
### Layer 1 — Blind randomized pairwise (primary)
|
|
98
|
+
Show the judge two **anonymized** screenshots in **shuffled** order and ask: *"Which reads more like a
|
|
99
|
+
professionally designed, well-funded SaaS landing page?"* Run **A-vs-C, B-vs-C, A-vs-B**. Record the winner
|
|
100
|
+
(`"A" | "B" | "C" | "tie"`) per comparison. Screenshots carry no arm labels.
|
|
101
|
+
|
|
102
|
+
### Layer 2 — Rubric (1–5 per arm)
|
|
103
|
+
Score each arm on: hierarchy, typography, color discipline, whitespace rhythm, originality-vs-slop, mobile.
|
|
104
|
+
|
|
105
|
+
### Layer 3 — Slop detector (binary)
|
|
106
|
+
Run the anti-slop negatives checklist (corpus #880) over each arm; record the count of negatives present
|
|
107
|
+
(lower is better).
|
|
108
|
+
|
|
109
|
+
Append each run to the brief's `runs[]` array in the runs file.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Phase 4: Scorecard
|
|
114
|
+
|
|
115
|
+
Aggregate deterministically:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
node scripts/bench-scorecard.mjs runs.json > scorecard.json
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The aggregator (see `scripts/bench-scorecard.mjs`):
|
|
122
|
+
- **rejects any product with n<3 runs** (exit 1) — enforces the no-point-estimate rule,
|
|
123
|
+
- emits **win-rate vs C** per arm (overall + per-product), the **A-vs-B harness delta**, **rubric mean+stdev**
|
|
124
|
+
per arm/dimension, **mean slop** per arm,
|
|
125
|
+
- runs the **judge-calibration check**: C must beat A and B in the pairwise; any run where C loses is listed in
|
|
126
|
+
`judge_calibration.miscalibrated_runs` and the command exits 2 with a warning. A real reference losing to a
|
|
127
|
+
generated arm is a judge defect, not a harness win — treat those runs as suspect.
|
|
128
|
+
|
|
129
|
+
Post the result as a `FORGE:BENCH_SCORECARD` annotation (registered in
|
|
130
|
+
[`docs/FORGE-PROTOCOL.md`](../docs/FORGE-PROTOCOL.md)) on the tracking issue:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
gh issue comment <ISSUE> --body "<!-- FORGE:BENCH_SCORECARD -->
|
|
134
|
+
## ABC Benchmark Scorecard
|
|
135
|
+
|
|
136
|
+
**Corpus version**: <ver> · **Generation model (A & B)**: <model> (canonical: claude-opus-4-6) · **Judge model**: <independent judge>
|
|
137
|
+
**Products**: <n_products> · **Runs/product**: <n>
|
|
138
|
+
|
|
139
|
+
| Arm | Win-rate vs C | A-vs-B | Mean slop |
|
|
140
|
+
|-----|---------------|--------|-----------|
|
|
141
|
+
| A | <wr> | <a_vs_b> | <slop_A> |
|
|
142
|
+
| B | <wr> | — | <slop_B> |
|
|
143
|
+
|
|
144
|
+
**Judge calibration**: <ok | MISCALIBRATED — N runs where C lost (suspect)>
|
|
145
|
+
|
|
146
|
+
\`\`\`json
|
|
147
|
+
<scorecard.json>
|
|
148
|
+
\`\`\`
|
|
149
|
+
|
|
150
|
+
<!-- FORGE:BENCH_SCORECARD:COMPLETE -->"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Two non-negotiable rules
|
|
156
|
+
|
|
157
|
+
### n=1 is a lie
|
|
158
|
+
Taste output is high-variance. Run **3–5 generations per arm across 3–5 products** and report **distributions**.
|
|
159
|
+
The aggregator hard-fails on n<3.
|
|
160
|
+
|
|
161
|
+
### Anti-Goodhart
|
|
162
|
+
The benchmark judge **MUST be independent from the harness's own critique loop (#882)**. Optimizing against the
|
|
163
|
+
same judge that scores the benchmark is training to the test — the number rises while real quality does not. The
|
|
164
|
+
benchmark judge uses its own prompt and is a distinct stage; it does not reuse the #882 critique judge.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Acceptance (issue #878)
|
|
169
|
+
|
|
170
|
+
- [x] Reproducible command/workflow: reference URL → brief → render arms → judge → scorecard.
|
|
171
|
+
- [x] Arms A/B/C defined; same generation model for A and B; C = gold standard + judge calibration.
|
|
172
|
+
- [x] Three judging layers: blind randomized pairwise, 1–5 rubric, binary slop detector.
|
|
173
|
+
- [x] n=3–5 across 3–5 products; distributions reported; n=1 rejected by the aggregator.
|
|
174
|
+
- [x] Benchmark judge independent from the #882 critique loop (anti-Goodhart).
|
|
175
|
+
- [x] Five seed briefs as fixtures (Cadence/Tender/Slipstream/Voltage/Plume), anonymized + mapped to C.
|
|
176
|
+
- [x] Arm A pluggable — rig runs now, plugs into `/design` (#888) when it lands.
|