mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-02 15:04:37 +01:00
* fix(preamble): completeness means complete coverage with minimal code (GSTA-96) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(preamble): tier 2+ skills load the shared preamble once per session (GSTA-96) Every tier 2+ SKILL.md inlined the same ~23 KB preamble. It now renders once per host into sections/preamble.md beside each tier 2+ SKILL.md; the SKILL.md keeps a pointer that asks for the Read only when the file's id is not already in context. Only sections byte-identical to the shared render move, so every skill keeps exactly its pre-hoist paragraphs (verified across all 41 skills). Parity caps for ten skills raised to their measured unions (owner-approved, Garry 2026-10-25: "do it"); 63 size pins lowered and 26 deleted. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(paid-select): a generated sections/preamble.md takes its skill template's identity in the PR planner (GSTA-96) screen-summary failed on #3434 with 'PR profile requires full validation' for nine skills whose only paid coverage is periodic: their new sections/preamble.md had no dependency identity, while the SKILL.md beside it inherits its template's. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(section-manifest): the shared sections/preamble.md renders from the hoist resolver, not a section template (GSTA-96) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (CHANGELOG says 3 characters shorter; model-overlays guards the coverage half) The completeness section shrank by 3 characters (5 bytes), matching the 89 pins lowered by 3. The default-copy test pinned only the minimal-code half; an expectMentions check now fails if the full-coverage or never-excuses-a-gap clause is dropped. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (model-overlays binds smaller diff to equal coverage) The default-copy check now also fails if 'on equal coverage' is dropped from the smaller-diff rule (which would tell models to prefer the smaller diff unconditionally) or if the no-impossible-state-checks clause is removed. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: CHANGELOG lists the model-overlays coverage checks The [Unreleased] Tests bullet now says the default-copy test checks both halves of the rule (full coverage of the requirement; smaller diff only at equal coverage; no impossible-state checks; never excuses a gap), matching88e3cc0and44eacaf. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: plan-ceo-review and ETHOS say completeness means coverage, not more code Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (CEO bullet bounds code by accepted scope; tests bind each completeness rule) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: cover the shared-preamble hoist (ship Step 7) Adds test/preamble-hoist.test.ts (tier 1 unchanged; tier 2+ SKILL.md plus sections/preamble.md reproduces the pre-hoist preamble; pointer position and id) and two gen-skill-docs-checks cases (one shared preamble per host, beside every pointer and named by its id; preamble artifact kind). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (ship Step 9.4) - setup: follow a symlinked sections/ dir in the carved-pointer status check - touchfiles: select preamble-hoist.ts alongside every preamble.ts entry - section-manifest: exempt sections/preamble.md only for skills whose SKILL.md carries the shared-preamble pointer - skill-size-budget: growth ceiling measures SKILL.md plus its shared preamble - host-conformance: tolerate a missing sections/ dir, as setup does - skill-validation: read bin/gstack-skill-start directly - skill-fixture: error messages and docblocks name the actual boundary (harness-version non-behavioral note updated) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: describe the size-budget growth check's startup measure (ship Step 9.4, pass 2) The growth ceiling now measures SKILL.md plus the shared preamble, the same measure as the shrink floor; the docblock said SKILL.md only. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: CHANGELOG says simplification.md quotes the old rule and CLAUDE.md restates the new one Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: CHANGELOG lists the hoist tests and the growth-ceiling measure (ship Step 9.4) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: cover the symlinked sections/ status check and the ETHOS house-rule quote (ship Step 7) host-conformance: --status still finds a broken bare sections/ pointer when a carved Claude skill's installed sections/ is a directory symlink (fails at the pre-repair0e5d11fd9). gen-skill-docs: the house rule review/specialists/ simplification.md quotes from ETHOS.md still appears in ETHOS.md. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: describe the shared preamble file (ship Step 14.5 docs audit) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: the shared-preamble skip needs the skill's own Read; digest and first-run intro say complete coverage, minimal code (GSTA-96) The pointer skipped the shared file when any text carrying its id was in context, so a page or file with that heading could stand in for the anti-spoofing rules (security, red team and adversarial review, ship run 2). It now requires the skill's own earlier Read. The digest's Boil the Ocean bullet and the first-run intro still said 'AI makes completeness cheap, so do the complete thing' (docs audit blocker); both now carry Garry's 'complete coverage, minimal code' (2026-10-25: '1/ yes fix this'). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: pin the shared-preamble provenance skip and the new Boil the Ocean wording (ship Step 7) preamble-hoist: on every host, tier and render option the pointer allows the skip only when the skill's own Read is in context. agents-digest and gstack-skill-start: the digest bullet and the lake intro say complete coverage, minimal code, never the retired phrasing. Each fails at9af36d010. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: describe the provenance skip where the docs still described the id-only one (ship Step 9.4) The preamble-hoist.ts docblock and ARCHITECTURE.md still said the shared preamble is re-read only when its id is absent; since2acf1a44athe pointer requires the skill's own earlier Read of the file. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: CHANGELOG lists the provenance-pointer and Boil the Ocean wording tests (ship Step 13) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * revert(digest): keep the agents digest at main's bytes; an unstamped head cannot change a release output (GSTA-96) CI 'Check VERSION is not stale vs queue' classified #3434 as stamped because it touched agents-digest/gstack-AGENTS.md (a release_outputs path), while regenerating only the generator fails the digest freshness test. The first-run intro rewording stays. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: voice judge reads the hoisted preamble; register ETHOS.md on its reader cases Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: refresh the claude ship golden the main merge took from main (ship Step 5 preflight) The1a4dcaf07merge resolved test/fixtures/golden/claude-ship-SKILL.md to main's pre-hoist bytes. The golden is a copy of ship/SKILL.md; regenerated with bun scripts/gen-ship-goldens.ts (pregate regen: REGEN_STALE). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: pr-dependencies' unplaceable-file example uses SECURITY.md now that ETHOS.md is mapped6183ca8aeregistered ETHOS.md on its reader cases in touchfiles-data.ts, so a PR touching only ETHOS.md plans in pr mode and no longer restores the full gate. The fail-closed test and the real-unknown label check now use SECURITY.md, which is tracked and unmapped; the assertions are unchanged. CHANGELOG names the third test file. (CI free shard 7 at 7c21720ac; remote pregate shard 6.) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: host path-leak checks read the moved shared preamble too; name SHARED_PREAMBLE_FILE (ship review pass 2) The external-host path-leak assertions in gen-skill-docs.test.ts (Codex, Factory) and host-config.test.ts (Copilot, Antigravity) read SKILL.md alone, so a Claude path that moved into sections/preamble.md with the hoist went unchecked. They now read the skill's startup text through readSkillStartup. workflow-judge-input and host-conformance use the SHARED_PREAMBLE_FILE constant instead of a bare 'preamble.md' literal, and preamble.ts's header describes the hoisted composition. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: CHANGELOG token savings name the per-skill skip and the autoplan follow-up (ship review pass 2) The pointer's skip is bound to the skill's own copy of sections/preamble.md, so "every later one saves about 5,800" holds per skill, not per session, and /autoplan's skip list and the inline /office-hours loader do not yet skip the pointer section. The entry now says so instead of claiming the full /autoplan saving. Adding the skip entry grows size-pinned skills, so it is a follow-up. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: corpus budget measures startup bytes; carve-safety counts shared preamble as uncarved (ship review pass 2) skill-size-budget's growth ceiling and shrink floor already measured SKILL.md plus sections/preamble.md, but the total-corpus check still summed hoisted SKILL.md bytes against the pre-hoist baseline, hiding about 0.9 MB of headroom. It now sums the same startup bytes (3,114,885 -> 2,590,484, x0.832); the unreachable SKILL.md-missing fallback is gone. auq-format-always-loaded's carve-safety guard treated any sections/ dir as carved, so every skill with only the shared preamble ran it and its sanity check passed on preamble.md. It now uses setup --status's rule: a section other than the shared preamble. Those skills keep the per-skill checks. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: remaining SKILL.md-only negatives read the shared preamble too (ship review pass 3) The parameterized external-host path-leak smoke test, the two Codex .claude path checks and both vestigial-handshake negatives read SKILL.md alone, so text that moved into sections/preamble.md went unchecked. They now read readSkillStartup. preamble.ts's header no longer says tier 1 renders through generatePreamble(). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: ratchet skeleton caps and eager context ceilings down after the hoist (ship review pass 3) The hoist moved about 22 KB out of every tier 2+ SKILL.md, which left each hoisted maxSkeletonBytes cap and each eagerPerInvocation ceiling roughly 22 KB / 5-6K tokens slack. The 20 hoisted skeleton caps now sit at the post-hoist skeleton plus their pre-hoist headroom, and 42 context-budget ceilings take the recaptured (lower) values. Nothing was raised: the four ceilings the recapture would have raised keep their old values. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: preamble-hoist's section splitter closes a fence only on a bare delimiter (ship Step 11) Inside an open fence, a line such as "```bash" is content in CommonMark, but splitSections treated it as the closing fence, so a later "## " line in the same block could split a section mid-code-block and hoist half of it. A closing delimiter now needs nothing but whitespace after it. Every host's render is unchanged (gen-skill-docs --dry-run FRESH on all twelve hosts). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: skill-startup.ts is a global, pinned harness file like skill-fixture.ts (ship review pass 3) skill-fixture.ts is a global touchfile pinned in scripts/harness-version.json because it reshapes the skill content most E2E suites read, and it now builds that content through test/helpers/skill-startup.ts. That helper was in neither list, so a later edit to it would change fixture inputs without the harness-version guard and select only 27 keys. It is now global and pinned, recorded non-behavioral (its fixtures are byte-identical to the pre-hoist output). ARCHITECTURE.md and the CHANGELOG stop calling the pointer id a content id: it hashes the shared text before host rewrites. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: CHANGELOG matches the merged skip lists and the baseline diff (ship review, run 4) The headline still called the autoplan and inline /office-hours skip entries a follow-up, but the main merge added them; it now says the loaded skills skip their shared Read. The #3451 sentence is dropped, because that fix shipped in v1.91.237.0 and is recorded there. The typecheck baseline line says the TS2769 entry was re-keyed for the 'preamble' artifact kind with its count unchanged, not fixed and lowered. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: pin the pointer entry in the loader skip lists; drop a carve-safety tautology (ship review, run 4) A new INVOKE_SKILL test checks that plan-ceo-review's inline /office-hours skip list and autoplan's section skip list both name the shared-preamble pointer heading, and that office-hours and the four plan reviews carry it. The carve-safety "sanity" assertion repeated the predicate discoverSkills() now uses to set sectionsDir, so it could never fail; it is removed. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: gstack-doctor finds the Codex model patch in the hoisted shared preamble (ship Step 11, run 4) The doctor's patch row grepped only the Codex review SKILL.md for the "Model-Specific Behavioral Patch (" heading. The hoist moves that section into sections/preamble.md, so the required row (for ship, review, the plan reviews, office-hours, investigate and more) would fail on every correct post-hoist Codex install and block install.sh's "gstack: ok". It now checks the SKILL.md and then its shared preamble and names the file it found. A new doctor test covers a hoisted render. preamble-hoist's variantSubsections also returns null when no subsection differs, instead of emitting a replacing heading with no subsections. Every host's render is unchanged. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(docsync): fixtureDocs installs the generated sections/preamble.md (hunk taken from #3472) A generated skill that loads the shared preamble names sections/preamble.md as a required asset before skill start, and a spawned document-release child blocks on its absence, so docsync-spawned stayed red on every #3434 screen. fixtureDocs now copies that file when the generated skill has it. This single hunk is taken unchanged from gstack#3472 (head2e3f7ba1, GSTA-107), which trains ahead of #3434, so the train merge sees identical lines. Nothing else from #3472 is included. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: CHANGELOG notes the docsync fixture hunk taken from #3472 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: CHANGELOG names the post-merge context-budget ratchet (captain hoisted) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(auq): planted skill fixtures resolve the shared preamble like an install (GSTA-96) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(plan-format): mode menu delivers its question without output-harvesting wording (GSTA-96) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): record the v1.91.270.0 merge values (GSTA-96) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(auq): parallel-capture contract expects the planted skill with its sections path mapped (GSTA-96) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): record the v1.91.273.0 merge re-measurements (GSTA-96) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): restore the released entries the previous commit truncated (GSTA-96) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): end two sentences the prose gate flags (GSTA-96) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
802 lines
41 KiB
TypeScript
802 lines
41 KiB
TypeScript
/**
|
|
* LLM-as-a-Judge evals for generated SKILL.md quality.
|
|
*
|
|
* Uses the Anthropic API directly (not Agent SDK) to evaluate whether
|
|
* generated command docs are clear, complete, and actionable for an AI agent.
|
|
*
|
|
* Requires: ANTHROPIC_API_KEY env var (or EVALS=1 with key already set)
|
|
* Run: EVALS=1 bun run test:eval
|
|
*
|
|
* Cost: ~$0.05-0.15 per run (sonnet)
|
|
*/
|
|
|
|
import { afterAll, expect } from 'bun:test';
|
|
import { JUDGE_MS } from './helpers/eval-budgets';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { callJudge, judge, JudgeRefusalError, JUDGE_SCORE_SCHEMA, QA_ANTI_REFUSAL_JUDGE_SCHEMA, VOICE_DIRECTIVE_JUDGE_SCHEMA, CROSS_SKILL_CONSISTENCY_JUDGE_SCHEMA, buildQaWorkflowJudgePrompt, buildQaHealthRubricJudgePrompt, buildQaAntiRefusalJudgePrompt, buildCrossSkillConsistencyJudgePrompt, buildVoiceDirectiveJudgePrompt, DEFAULT_JUDGE_MAX_TOKENS, judgePanel, judgePanelMean, judgePanelMedian, judgePanelSummary, judgePanelMajority, judgePanelReasoning, JUDGE_SCORE_DIMENSIONS } from './helpers/llm-judge';
|
|
import { ASK_QUESTIONS_HEADING, ENG_REVIEW_EXCERPT } from './helpers/workflow-excerpt';
|
|
import type { JudgeScore } from './helpers/llm-judge';
|
|
import { readWorkflowJudgeInput, buildWorkflowJudgePrompt, QA_DISCOVERY_REFERENCES, WORKFLOW_JUDGE_RESPONSE_SCHEMA, type WorkflowJudgeInput } from './helpers/workflow-judge-input';
|
|
import { BROWSE_JUDGE_FLOORS, browseJudgeFloorsMet, prepareWorkflowJudgeCache, validWorkflowJudgeScore } from './helpers/workflow-judge-cache';
|
|
import { buildCookieWorkflowJudgeInput, COOKIE_WORKFLOW_JUDGE } from './helpers/cookie-workflow-judge-input';
|
|
import { getCookieWorkflowManualReview, type ManualJudgeReview } from './helpers/cookie-workflow-manual-review';
|
|
import { resolveEvalModel } from '../lib/eval-model';
|
|
import type { EvalCacheValue } from '../scripts/eval-input-cache';
|
|
import { LLM_JUDGE_TOUCHFILES } from './helpers/touchfiles';
|
|
import { readSkillEffective } from './helpers/skill-startup';
|
|
// Runs when EVALS=1 is set (requires ANTHROPIC_API_KEY in env) — the EVALS
|
|
// gate lives in the shared describeIfSelected. Selection machinery is shared
|
|
// with the E2E suite; only the touchfiles table (LLM_JUDGE_TOUCHFILES, passed
|
|
// explicitly below) differs. No EVALS_TIER filter applies here — LLM-judge
|
|
// tests have no E2E_TIERS entries and run in both tier lanes.
|
|
import {
|
|
ROOT,
|
|
computeDiffSelection,
|
|
resolveModuleSelection,
|
|
createEvalCollector,
|
|
finalizeEvalCollector,
|
|
describeIfSelected as describeIfSelectedShared,
|
|
testConcurrentIfSelected,
|
|
} from './helpers/e2e-helpers';
|
|
|
|
// Eval result collector
|
|
const evalCollector = createEvalCollector('llm-judge');
|
|
|
|
/**
|
|
* Browse carve (token-reduction Phase 4): the '## Snapshot Flags' and
|
|
* '## Full Command List' reference blocks moved from browse/SKILL.md into the
|
|
* generated on-demand section browse/sections/command-list.md ('## Snapshot
|
|
* Flags' first, then '## Full Command List'). '## SETUP', '## Core QA
|
|
* Patterns', and '## CSS Inspector' stay in the skeleton. Non-empty guard:
|
|
* judging an empty slice would silently pass garbage to the judge.
|
|
*/
|
|
function readBrowseCommandSection(): string {
|
|
const p = path.join(ROOT, 'browse', 'sections', 'command-list.md');
|
|
const content = fs.readFileSync(p, 'utf-8');
|
|
if (!content.includes('## Snapshot Flags') || !content.includes('## Full Command List')) {
|
|
throw new Error(
|
|
`${p} is missing the expected headers — regenerate with: bun run gen:skill-docs`,
|
|
);
|
|
}
|
|
return content;
|
|
}
|
|
|
|
/** Slice a section out of the command-list section file, guarded non-empty. */
|
|
function sliceBrowseSection(startHeader: string): string {
|
|
const content = readBrowseCommandSection();
|
|
const start = content.indexOf(startHeader);
|
|
if (start < 0) throw new Error(`browse/sections/command-list.md: "${startHeader}" not found`);
|
|
const section = content.slice(start);
|
|
if (section.trim().length < 200) {
|
|
throw new Error(`browse/sections/command-list.md slice at "${startHeader}" is empty/stub — regenerate with: bun run gen:skill-docs`);
|
|
}
|
|
return section;
|
|
}
|
|
|
|
// --- Diff-based test selection (LLM_JUDGE_TOUCHFILES, not the E2E table) ---
|
|
const selectedTests = resolveModuleSelection(
|
|
process.env.EVALS ? process.env.EVALS_JUDGE_SELECTION_JSON : undefined,
|
|
() => computeDiffSelection(LLM_JUDGE_TOUCHFILES, 'LLM-judge'),
|
|
);
|
|
|
|
/** Wrap a describe block to skip if none of THIS FILE's tests are selected. */
|
|
function describeIfSelected(name: string, testNames: string[], fn: () => void) {
|
|
describeIfSelectedShared(name, testNames, fn, selectedTests);
|
|
}
|
|
|
|
/** Per-test gate against this file's selection (concurrent, as before). */
|
|
function testIfSelected(testName: string, fn: () => Promise<void>, timeout: number) {
|
|
testConcurrentIfSelected(testName, fn, timeout, selectedTests);
|
|
}
|
|
|
|
describeIfSelected('LLM-as-judge quality evals', [
|
|
'browse/SKILL.md reference', 'setup block',
|
|
], () => {
|
|
testIfSelected('browse/SKILL.md reference', async () => {
|
|
const t0 = Date.now();
|
|
// Browse carve: snapshot flags + the full command list are the whole
|
|
// generated section file; one judge grades the union. Scores are also
|
|
// compared with test/fixtures/eval-baselines.json and the difference is
|
|
// recorded (UPDATE_BASELINES=1 rewrites it); only the floors gate.
|
|
const section = sliceBrowseSection('## Snapshot Flags');
|
|
|
|
const samples = await judgePanel(() => judge('browse skill reference (flags + commands)', section));
|
|
const { median: scores, mean } = judgePanelSummary(samples, JUDGE_SCORE_DIMENSIONS);
|
|
console.log('Browse SKILL.md panel:', JSON.stringify({ gate: 'median', median: scores, mean, samples }, null, 2));
|
|
|
|
const baselinesPath = path.join(ROOT, 'test', 'fixtures', 'eval-baselines.json');
|
|
const baselines = JSON.parse(fs.readFileSync(baselinesPath, 'utf-8'));
|
|
const regressions = (['clarity', 'completeness', 'actionability'] as const)
|
|
.filter(dim => scores[dim] < baselines.browse_skill[dim])
|
|
.map(dim => `browse_skill.${dim}: ${scores[dim]} < baseline ${baselines.browse_skill[dim]}`);
|
|
if (process.env.UPDATE_BASELINES) {
|
|
baselines.browse_skill = { clarity: scores.clarity, completeness: scores.completeness, actionability: scores.actionability };
|
|
fs.writeFileSync(baselinesPath, JSON.stringify(baselines, null, 2) + '\n');
|
|
console.log('Updated eval baselines');
|
|
}
|
|
|
|
evalCollector?.addTest({
|
|
name: 'browse/SKILL.md reference',
|
|
suite: 'LLM-as-judge quality evals',
|
|
tier: 'llm-judge',
|
|
passed: browseJudgeFloorsMet(scores),
|
|
duration_ms: Date.now() - t0,
|
|
cost_usd: 0.02 * samples.length,
|
|
judge_scores: { clarity: scores.clarity, completeness: scores.completeness, actionability: scores.actionability },
|
|
judge_reasoning: regressions.length ? `${judgePanelReasoning(samples)} | ${regressions.join('; ')}` : judgePanelReasoning(samples),
|
|
});
|
|
|
|
expect(scores.clarity).toBeGreaterThanOrEqual(BROWSE_JUDGE_FLOORS.clarity);
|
|
expect(scores.completeness).toBeGreaterThanOrEqual(BROWSE_JUDGE_FLOORS.completeness);
|
|
expect(scores.actionability).toBeGreaterThanOrEqual(BROWSE_JUDGE_FLOORS.actionability);
|
|
}, JUDGE_MS);
|
|
|
|
testIfSelected('setup block', async () => {
|
|
const t0 = Date.now();
|
|
// P2 (v1.2.0): the browse setup block moved from the root router to browse/SKILL.md.
|
|
const content = fs.readFileSync(path.join(ROOT, 'browse', 'SKILL.md'), 'utf-8');
|
|
// The setup block is the Aside contract ('## BROWSER SETUP (Aside ...') with
|
|
// the browse binary as fallback; older renders headed it '## SETUP'. Slice
|
|
// from whichever heading is present to the next H2.
|
|
let setupStart = content.indexOf('## BROWSER SETUP');
|
|
if (setupStart < 0) setupStart = content.indexOf('## SETUP');
|
|
const setupEnd = content.indexOf('\n## ', setupStart + 3);
|
|
if (setupStart < 0 || setupEnd < 0) throw new Error('browse/SKILL.md: setup block not found — regenerate with: bun run gen:skill-docs');
|
|
const section = content.slice(setupStart, setupEnd);
|
|
|
|
const samples = await judgePanel(() => judge('setup/binary discovery instructions', section));
|
|
const { median: scores, mean } = judgePanelSummary(samples, JUDGE_SCORE_DIMENSIONS);
|
|
console.log('Setup block panel:', JSON.stringify({ gate: 'median', median: scores, mean, samples }, null, 2));
|
|
|
|
evalCollector?.addTest({
|
|
name: 'setup block',
|
|
suite: 'LLM-as-judge quality evals',
|
|
tier: 'llm-judge',
|
|
passed: scores.actionability >= 3 && scores.clarity >= 3,
|
|
duration_ms: Date.now() - t0,
|
|
cost_usd: 0.02 * samples.length,
|
|
judge_scores: { clarity: scores.clarity, completeness: scores.completeness, actionability: scores.actionability },
|
|
judge_reasoning: judgePanelReasoning(samples),
|
|
});
|
|
|
|
// Setup block is intentionally minimal (binary discovery only).
|
|
// SKILL_DIR is inferred from context, so judge sometimes scores 3.
|
|
expect(scores.actionability).toBeGreaterThanOrEqual(3);
|
|
expect(scores.clarity).toBeGreaterThanOrEqual(3);
|
|
}, JUDGE_MS);
|
|
|
|
});
|
|
|
|
// --- Part 7: QA skill quality evals (C6) ---
|
|
|
|
/**
|
|
* QA carve (token-reduction Phase 4): the '## Modes', '## Workflow',
|
|
* '## Health Score Rubric', '## Framework-Specific Guidance', and
|
|
* '## Important Rules' blocks moved from qa/SKILL.md into the generated
|
|
* on-demand section qa/sections/qa-patterns.md. Monolith-tolerant: falls back
|
|
* to the skeleton when the section file doesn't exist (pre-carve checkout).
|
|
*/
|
|
function readQaPatterns(): string {
|
|
const sectionPath = path.join(ROOT, 'qa', 'sections', 'qa-patterns.md');
|
|
return fs.existsSync(sectionPath)
|
|
? fs.readFileSync(sectionPath, 'utf-8')
|
|
: fs.readFileSync(path.join(ROOT, 'qa', 'SKILL.md'), 'utf-8');
|
|
}
|
|
|
|
/** Slice out of the qa-patterns section, guarded non-empty: judging an empty
|
|
* slice would silently pass garbage to the judge. */
|
|
function sliceQaPatterns(startHeader: string, endHeader?: string): string {
|
|
const content = readQaPatterns();
|
|
const start = content.indexOf(startHeader);
|
|
if (start < 0) throw new Error(`qa/sections/qa-patterns.md: "${startHeader}" not found — regenerate with: bun run gen:skill-docs`);
|
|
const end = endHeader ? content.indexOf(endHeader, start) : -1;
|
|
const section = end > start ? content.slice(start, end) : content.slice(start);
|
|
if (section.trim().length < 200) {
|
|
throw new Error(`qa/sections/qa-patterns.md slice at "${startHeader}" is empty/stub — regenerate with: bun run gen:skill-docs`);
|
|
}
|
|
return section;
|
|
}
|
|
|
|
describeIfSelected('QA skill quality evals', ['qa/SKILL.md workflow', 'qa/SKILL.md health rubric', 'qa/SKILL.md anti-refusal'], () => {
|
|
testIfSelected('qa/SKILL.md workflow', async () => {
|
|
const t0 = Date.now();
|
|
const section = readWorkflowJudgeInput({ root: ROOT, skillPath: 'qa/SKILL.md',
|
|
startMarker: '# /qa: Test', endMarker: null,
|
|
// qa-patterns.md loads both browser assets; judges penalized their absence.
|
|
references: ['qa/templates/functional-report-template.md', 'qa/templates/qa-report-template.md', 'qa/references/issue-taxonomy.md'] }).text;
|
|
|
|
const samples = await judgePanel(() => callJudge<JudgeScore>(buildQaWorkflowJudgePrompt(section)));
|
|
const { median: scores, mean } = judgePanelSummary(samples, JUDGE_SCORE_DIMENSIONS);
|
|
console.log('QA workflow panel:', JSON.stringify({ gate: 'median', median: scores, mean, samples }, null, 2));
|
|
|
|
evalCollector?.addTest({
|
|
name: 'qa/SKILL.md workflow',
|
|
suite: 'QA skill quality evals',
|
|
tier: 'llm-judge',
|
|
passed: scores.clarity >= 3 && scores.completeness >= 3 && scores.actionability >= 4,
|
|
duration_ms: Date.now() - t0,
|
|
cost_usd: 0.02 * samples.length,
|
|
judge_scores: { clarity: scores.clarity, completeness: scores.completeness, actionability: scores.actionability },
|
|
judge_reasoning: judgePanelReasoning(samples),
|
|
});
|
|
|
|
expect(scores.clarity).toBeGreaterThanOrEqual(3);
|
|
// Completeness scores 3 when judge notes the health rubric is in a separate
|
|
// section (the eval only passes the Workflow section, not the full document).
|
|
expect(scores.completeness).toBeGreaterThanOrEqual(3);
|
|
expect(scores.actionability).toBeGreaterThanOrEqual(4);
|
|
}, JUDGE_MS);
|
|
|
|
testIfSelected('qa/SKILL.md health rubric', async () => {
|
|
const t0 = Date.now();
|
|
const section = sliceQaPatterns('## Health Score Rubric');
|
|
|
|
const samples = await judgePanel(() => callJudge<JudgeScore>(buildQaHealthRubricJudgePrompt(section), undefined, { jsonSchema: JUDGE_SCORE_SCHEMA }));
|
|
const { median: scores, mean } = judgePanelSummary(samples, JUDGE_SCORE_DIMENSIONS);
|
|
console.log('QA health rubric panel:', JSON.stringify({ gate: 'median', median: scores, mean, samples }, null, 2));
|
|
|
|
evalCollector?.addTest({
|
|
name: 'qa/SKILL.md health rubric',
|
|
suite: 'QA skill quality evals',
|
|
tier: 'llm-judge',
|
|
passed: scores.clarity >= 3 && scores.completeness >= 3 && scores.actionability >= 4,
|
|
duration_ms: Date.now() - t0,
|
|
cost_usd: 0.02 * samples.length,
|
|
judge_scores: { clarity: scores.clarity, completeness: scores.completeness, actionability: scores.actionability },
|
|
judge_reasoning: judgePanelReasoning(samples),
|
|
});
|
|
|
|
expect(scores.clarity).toBeGreaterThanOrEqual(3);
|
|
expect(scores.completeness).toBeGreaterThanOrEqual(3);
|
|
expect(scores.actionability).toBeGreaterThanOrEqual(4);
|
|
}, JUDGE_MS);
|
|
|
|
testIfSelected('qa/SKILL.md anti-refusal', async () => {
|
|
const t0 = Date.now();
|
|
// Extract both the diff-aware mode section and Important Rules section.
|
|
// (Pre-carve this sliced '## Important Rules' → '## Framework-Specific',
|
|
// which was EMPTY — Framework-Specific precedes Important Rules — so the
|
|
// judge only ever saw excerpt 1. The section-file slice fixes that: rules
|
|
// run to the end of qa-patterns.md, so rule 12 "Never refuse to use the
|
|
// browser" now actually reaches the judge.)
|
|
const diffAwareSection = sliceQaPatterns('### Diff-aware', '### Full');
|
|
const rulesSection = sliceQaPatterns('## Important Rules');
|
|
|
|
const samples = await judgePanel(() => callJudge<{ would_browse: boolean; fallback_behavior: string; confidence: number; reasoning: string }>(buildQaAntiRefusalJudgePrompt(diffAwareSection, rulesSection), undefined, { jsonSchema: QA_ANTI_REFUSAL_JUDGE_SCHEMA }));
|
|
const result = { would_browse: judgePanelMajority(samples, 'would_browse'), ...judgePanelMedian(samples, ['confidence'] as const) };
|
|
|
|
console.log('QA anti-refusal panel:', JSON.stringify({ gate: 'median', result, mean: judgePanelMean(samples, ['confidence'] as const), samples }, null, 2));
|
|
|
|
evalCollector?.addTest({
|
|
name: 'qa/SKILL.md anti-refusal',
|
|
suite: 'QA skill quality evals',
|
|
tier: 'llm-judge',
|
|
passed: result.would_browse === true && result.confidence >= 4,
|
|
duration_ms: Date.now() - t0,
|
|
cost_usd: 0.02 * samples.length,
|
|
judge_scores: { would_browse: result.would_browse ? 1 : 0, confidence: result.confidence },
|
|
judge_reasoning: judgePanelReasoning(samples),
|
|
});
|
|
|
|
expect(result.would_browse).toBe(true);
|
|
expect(result.confidence).toBeGreaterThanOrEqual(4);
|
|
}, JUDGE_MS);
|
|
});
|
|
|
|
// --- Part 7: Cross-skill consistency judge (C7) ---
|
|
|
|
describeIfSelected('Cross-skill consistency evals', ['cross-skill greptile consistency'], () => {
|
|
testIfSelected('cross-skill greptile consistency', async () => {
|
|
const t0 = Date.now();
|
|
const reviewContent = fs.readFileSync(path.join(ROOT, 'review', 'SKILL.md'), 'utf-8');
|
|
const shipContent = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
|
const triageContent = fs.readFileSync(path.join(ROOT, 'review', 'greptile-triage.md'), 'utf-8');
|
|
const retroContent = fs.readFileSync(path.join(ROOT, 'retro', 'SKILL.md'), 'utf-8');
|
|
|
|
const extractGrepLines = (content: string, filename: string) => {
|
|
const lines = content.split('\n')
|
|
.filter(l => /greptile|history\.md|REMOTE_SLUG/i.test(l))
|
|
.map(l => l.trim());
|
|
return `--- ${filename} ---\n${lines.join('\n')}`;
|
|
};
|
|
|
|
const collected = [
|
|
extractGrepLines(reviewContent, 'review/SKILL.md'),
|
|
extractGrepLines(shipContent, 'ship/SKILL.md'),
|
|
extractGrepLines(triageContent, 'review/greptile-triage.md'),
|
|
extractGrepLines(retroContent, 'retro/SKILL.md'),
|
|
].join('\n\n');
|
|
|
|
const samples = await judgePanel(() => callJudge<{ consistent: boolean; issues: string[]; score: number; reasoning: string }>(buildCrossSkillConsistencyJudgePrompt(collected), undefined, { jsonSchema: CROSS_SKILL_CONSISTENCY_JUDGE_SCHEMA }));
|
|
const result = { consistent: judgePanelMajority(samples, 'consistent'), ...judgePanelMedian(samples, ['score'] as const) };
|
|
|
|
console.log('Cross-skill consistency panel:', JSON.stringify({ gate: 'median', result, mean: judgePanelMean(samples, ['score'] as const), samples }, null, 2));
|
|
|
|
evalCollector?.addTest({
|
|
name: 'cross-skill greptile consistency',
|
|
suite: 'Cross-skill consistency evals',
|
|
tier: 'llm-judge',
|
|
passed: result.consistent && result.score >= 4,
|
|
duration_ms: Date.now() - t0,
|
|
cost_usd: 0.02 * samples.length,
|
|
judge_scores: { consistency_score: result.score },
|
|
judge_reasoning: judgePanelReasoning(samples),
|
|
});
|
|
|
|
expect(result.consistent).toBe(true);
|
|
expect(result.score).toBeGreaterThanOrEqual(4);
|
|
}, JUDGE_MS);
|
|
});
|
|
|
|
// --- Workflow SKILL.md quality evals (10 new tests for 100% coverage) ---
|
|
|
|
/**
|
|
* DRY helper for workflow SKILL.md judge tests.
|
|
* Extracts a section from a SKILL.md file and judges its quality as an agent workflow.
|
|
*/
|
|
// Keep model work at JUDGE_MS. Only terminal recording/cache I/O gets grace.
|
|
const WORKFLOW_JUDGE_RECORD_MS = 5_000;
|
|
const WORKFLOW_JUDGE_TEST_MS = JUDGE_MS + 10_000;
|
|
const workflowJudgeAttempts = new Map<string, { attempt: number; cancel(): void }>();
|
|
|
|
async function runWorkflowJudge(opts: {
|
|
testName: string;
|
|
suite: string;
|
|
skillPath: string;
|
|
startMarker: string;
|
|
endMarker: string | RegExp | null;
|
|
references?: readonly string[];
|
|
judgeContext: string;
|
|
judgeGoal: string;
|
|
agentCapability?: 'frontier';
|
|
schemaTransport?: boolean;
|
|
compactReasoning?: boolean;
|
|
maxTokens?: number;
|
|
stream?: boolean;
|
|
effort?: 'medium';
|
|
model?: string;
|
|
thresholds?: { clarity: number; completeness: number; actionability: number };
|
|
readInput?: () => WorkflowJudgeInput;
|
|
}) {
|
|
const started = performance.now();
|
|
const previous = workflowJudgeAttempts.get(opts.testName);
|
|
previous?.cancel();
|
|
const attempt = (previous?.attempt ?? 0) + 1;
|
|
const controller = new AbortController();
|
|
const workDeadline = started + JUDGE_MS;
|
|
let stage: 'input' | 'judge' | 'validation' | 'recording' = 'input';
|
|
let finalized = false;
|
|
let samples: JudgeScore[] | undefined;
|
|
let scores: Record<typeof JUDGE_SCORE_DIMENSIONS[number], number> | undefined;
|
|
let manualReview: ManualJudgeReview | undefined;
|
|
let customInputMetadata: { prompt: string; model: string } | undefined;
|
|
let reused: ReturnType<ReturnType<typeof prepareWorkflowJudgeCache>['lookup']> = null;
|
|
let timer: ReturnType<typeof setTimeout>;
|
|
let rejectStopped: (error: Error) => void;
|
|
const stopped = new Promise<never>((_, reject) => { rejectStopped = reject; });
|
|
const deadlineError = () => Object.assign(new Error(`${opts.testName} exceeded its workflow judge ${stage === 'recording' ? 'recording' : 'work'} deadline`), { name: 'WorkflowJudgeDeadline' });
|
|
const deadline = () => workDeadline + (stage === 'recording' ? WORKFLOW_JUDGE_RECORD_MS : 0);
|
|
const active = () => !finalized && workflowJudgeAttempts.get(opts.testName) === state
|
|
&& performance.now() < deadline();
|
|
const finish = (passed: boolean, error?: unknown) => {
|
|
if (finalized) return;
|
|
finalized = true;
|
|
clearTimeout(timer);
|
|
if (!passed) controller.abort(error);
|
|
evalCollector?.addTest({
|
|
name: opts.testName, suite: opts.suite, tier: 'llm-judge', passed, attempt,
|
|
duration_ms: Math.max(0, performance.now() - started),
|
|
cost_usd: reused || !samples ? 0 : 0.02 * samples.length,
|
|
execution: reused ? 'reused' : 'executed',
|
|
...customInputMetadata,
|
|
...(manualReview ? { manual_review: manualReview } : {}),
|
|
...(reused ? { reused_from: { input_key: reused.reuse.key, run_id: reused.reuse.source.runId,
|
|
revision: reused.reuse.source.revision, completed_at: new Date(reused.reuse.source.completedAt).toISOString() } } : {}),
|
|
...(scores ? { judge_scores: { clarity: scores.clarity, completeness: scores.completeness, actionability: scores.actionability } } : {}),
|
|
...(samples ? { judge_reasoning: judgePanelReasoning(samples) } : {}),
|
|
...(passed ? {} : { exit_reason: error instanceof JudgeRefusalError ? 'provider_refusal'
|
|
: error instanceof Error && error.name === 'WorkflowJudgeDeadline' ? 'timeout'
|
|
: error instanceof Error && error.name === 'WorkflowJudgeSuperseded' ? 'cancelled'
|
|
: stage === 'validation' ? 'validation_failed' : 'harness_error',
|
|
error: `${error instanceof Error ? error.message : String(error)}${samples ? '' : error instanceof JudgeRefusalError
|
|
? '\nNo automated score; provider refusal usage retained when manually accepted; cost unavailable.'
|
|
: '\nNo completed model response; cost and usage unavailable.'}` }),
|
|
});
|
|
};
|
|
const stop = (error: Error) => {
|
|
if (finalized) return;
|
|
try { finish(false, error); } finally { rejectStopped(error); }
|
|
};
|
|
const state = { attempt, cancel: () => stop(performance.now() >= deadline() ? deadlineError()
|
|
: Object.assign(new Error(`${opts.testName} attempt superseded by retry`), { name: 'WorkflowJudgeSuperseded' })) };
|
|
// Register before any input read: a failed fixture read is still attempt one.
|
|
workflowJudgeAttempts.set(opts.testName, state);
|
|
const checkActive = () => {
|
|
if (!active()) {
|
|
if (!finalized) stop(deadlineError());
|
|
throw controller.signal.reason ?? deadlineError();
|
|
}
|
|
};
|
|
const arm = () => { clearTimeout(timer); timer = setTimeout(() => stop(deadlineError()), Math.max(0, deadline() - performance.now())); };
|
|
arm();
|
|
|
|
// registered attempt -> bounded request -> unchanged assertions -> terminal record
|
|
// Timeout/retry finalizes once; late provider continuations cannot publish evidence.
|
|
const work = async () => {
|
|
checkActive();
|
|
const thresholds = { clarity: 3, completeness: 3, actionability: 4, ...opts.thresholds };
|
|
const input = opts.readInput ? opts.readInput() : readWorkflowJudgeInput({ root: ROOT, skillPath: opts.skillPath,
|
|
startMarker: opts.startMarker, endMarker: opts.endMarker, references: opts.references });
|
|
checkActive();
|
|
const prompt = buildWorkflowJudgePrompt(opts, input);
|
|
if (opts.readInput) customInputMetadata = { prompt, model: resolveEvalModel('judge', opts.model) };
|
|
const cache = prepareWorkflowJudgeCache({ ...opts, root: ROOT, thresholds, prompt, attempt });
|
|
checkActive();
|
|
reused = cache.lookup();
|
|
checkActive();
|
|
stage = 'judge';
|
|
const maxTokens = opts.maxTokens ?? DEFAULT_JUDGE_MAX_TOKENS;
|
|
let result: JudgeScore[];
|
|
try {
|
|
result = reused?.samples ?? await judgePanel(() => callJudge<JudgeScore>(prompt, opts.model, { signal: controller.signal, max_tokens: maxTokens,
|
|
...(opts.stream ? { stream: true } : {}),
|
|
...(opts.schemaTransport ? { jsonSchema: WORKFLOW_JUDGE_RESPONSE_SCHEMA } : {}),
|
|
...(opts.effort ? { effort: opts.effort } : {}) }));
|
|
} catch (error) {
|
|
checkActive();
|
|
if (error instanceof JudgeRefusalError && customInputMetadata) {
|
|
const approved = getCookieWorkflowManualReview(ROOT, { testName: opts.testName, prompt,
|
|
model: customInputMetadata.model, maxTokens, thresholds, attempt }, error.refusal);
|
|
checkActive();
|
|
if (approved) {
|
|
manualReview = approved;
|
|
console.log(`[workflow-judge] ${opts.testName}: MANUAL ACCEPTANCE, no automated score; ${approved.approval.approval_url}`);
|
|
finish(false, error);
|
|
return;
|
|
}
|
|
}
|
|
throw error;
|
|
}
|
|
checkActive();
|
|
samples = result;
|
|
console.log(`[workflow-judge] ${opts.testName}: ${reused ? `reused ${reused.reuse.source.runId} @ ${reused.reuse.source.revision} (${new Date(reused.reuse.source.completedAt).toISOString()})` : 'executed'}`);
|
|
stage = 'validation';
|
|
if (opts.compactReasoning && !samples.every(sample => validWorkflowJudgeScore(sample as unknown as EvalCacheValue, { clarity: 1, completeness: 1, actionability: 1 }, true))) {
|
|
throw new Error('Workflow judge violated the compact response contract');
|
|
}
|
|
scores = judgePanelMedian(samples, JUDGE_SCORE_DIMENSIONS);
|
|
console.log(`${opts.testName} panel:`, JSON.stringify({ gate: 'median', median: scores, mean: judgePanelMean(samples, JUDGE_SCORE_DIMENSIONS), samples }, null, 2));
|
|
expect(scores.clarity).toBeGreaterThanOrEqual(thresholds.clarity);
|
|
expect(scores.completeness).toBeGreaterThanOrEqual(thresholds.completeness);
|
|
expect(scores.actionability).toBeGreaterThanOrEqual(thresholds.actionability);
|
|
checkActive();
|
|
stage = 'recording';
|
|
arm();
|
|
const discardReceipt = reused ? undefined : cache.publish(samples, active);
|
|
try { checkActive(); finish(true); }
|
|
catch (error) { discardReceipt?.(); throw error; }
|
|
};
|
|
try { await Promise.race([work(), stopped]); }
|
|
catch (error) {
|
|
const failure = finalized ? controller.signal.reason ?? error
|
|
: performance.now() >= deadline() ? deadlineError() : error;
|
|
if (!finalized) finish(false, failure);
|
|
throw failure;
|
|
}
|
|
}
|
|
|
|
// Block 1: Ship & Release skills
|
|
describeIfSelected('Ship & Release skill evals', ['ship/SKILL.md workflow', 'document-release/SKILL.md workflow'], () => {
|
|
testIfSelected('ship/SKILL.md workflow', async () => {
|
|
await runWorkflowJudge({
|
|
testName: 'ship/SKILL.md workflow',
|
|
schemaTransport: true,
|
|
compactReasoning: true,
|
|
maxTokens: 65_536,
|
|
stream: true,
|
|
// Default effort thought past JUDGE_MS in 3 of 18 measured samples; medium kept all 18 under 80 s.
|
|
effort: 'medium',
|
|
suite: 'Ship & Release skill evals',
|
|
agentCapability: 'frontier',
|
|
// The contract now precedes platform detection; keep the complete workflow.
|
|
skillPath: 'ship/SKILL.md',
|
|
startMarker: '# Ship:',
|
|
endMarker: '## Important Rules',
|
|
references: QA_DISCOVERY_REFERENCES,
|
|
judgeContext: 'a ship/release workflow document',
|
|
judgeGoal: 'how to create a PR: merge base, test, review and explore changed behavior, handle required blocked checks, bump metadata, finish and vet every-ship documentation, then verify stable inputs, push and create/update the PR with visible QA and docs outcomes',
|
|
});
|
|
}, WORKFLOW_JUDGE_TEST_MS);
|
|
|
|
testIfSelected('document-release/SKILL.md workflow', async () => {
|
|
await runWorkflowJudge({
|
|
testName: 'document-release/SKILL.md workflow',
|
|
schemaTransport: true,
|
|
suite: 'Ship & Release skill evals',
|
|
skillPath: 'document-release/SKILL.md',
|
|
startMarker: '# Document Release:',
|
|
endMarker: '## Important Rules',
|
|
judgeContext: 'a release documentation audit workflow',
|
|
judgeGoal: 'how to audit relevant nested docs and authored sources; in ship-owned mode complete a bounded docs-only audit and return a typed result without Git/metadata authority, while standalone mode retains its approval and publication protections',
|
|
});
|
|
}, WORKFLOW_JUDGE_TEST_MS);
|
|
});
|
|
|
|
// Block 2: Plan Review skills
|
|
describeIfSelected('Plan Review skill evals', [
|
|
'plan-ceo-review/SKILL.md modes', 'plan-eng-review/SKILL.md sections', 'plan-design-review/SKILL.md passes',
|
|
], () => {
|
|
testIfSelected('plan-ceo-review/SKILL.md modes', async () => {
|
|
await runWorkflowJudge({
|
|
testName: 'plan-ceo-review/SKILL.md modes',
|
|
schemaTransport: true,
|
|
suite: 'Plan Review skill evals',
|
|
skillPath: 'plan-ceo-review/SKILL.md',
|
|
startMarker: '## Step 0: Nuclear Scope Challenge',
|
|
endMarker: '## Review Sections',
|
|
judgeContext: 'a CEO/founder plan review framework with 4 scope modes',
|
|
judgeGoal: 'how to conduct a CEO-perspective plan review: challenge scope, select a mode (Expansion, Selective Expansion, Hold Scope, Reduction), then review sections interactively',
|
|
});
|
|
}, WORKFLOW_JUDGE_TEST_MS);
|
|
|
|
testIfSelected('plan-eng-review/SKILL.md sections', async () => {
|
|
await runWorkflowJudge({
|
|
testName: 'plan-eng-review/SKILL.md sections',
|
|
schemaTransport: true,
|
|
suite: 'Plan Review skill evals',
|
|
skillPath: ENG_REVIEW_EXCERPT.skillPath,
|
|
startMarker: '# Plan Review Mode',
|
|
endMarker: null,
|
|
judgeContext: 'an engineering plan review framework with 4 review sections',
|
|
judgeGoal: 'how to review a plan for architecture quality, code quality, test coverage, and performance — walking through each section interactively with AskUserQuestion',
|
|
});
|
|
}, WORKFLOW_JUDGE_TEST_MS);
|
|
|
|
testIfSelected('plan-design-review/SKILL.md passes', async () => {
|
|
await runWorkflowJudge({
|
|
testName: 'plan-design-review/SKILL.md passes',
|
|
schemaTransport: true,
|
|
suite: 'Plan Review skill evals',
|
|
skillPath: 'plan-design-review/SKILL.md',
|
|
startMarker: '## Review Sections',
|
|
endMarker: ASK_QUESTIONS_HEADING,
|
|
judgeContext: 'a design plan review framework with 7 review passes',
|
|
judgeGoal: 'how to review a plan for design quality using a 0-10 rating method: rate each dimension, explain what a 10 looks like, edit the plan to fix gaps, then re-rate',
|
|
});
|
|
}, WORKFLOW_JUDGE_TEST_MS);
|
|
});
|
|
|
|
// Block 3: Design skills
|
|
describeIfSelected('Design skill evals', ['design-review/SKILL.md fix loop', 'design-consultation/SKILL.md research'], () => {
|
|
testIfSelected('design-review/SKILL.md fix loop', async () => {
|
|
await runWorkflowJudge({
|
|
testName: 'design-review/SKILL.md fix loop',
|
|
schemaTransport: true,
|
|
suite: 'Design skill evals',
|
|
skillPath: 'design-review/SKILL.md',
|
|
startMarker: '## Phase 7:',
|
|
endMarker: '## Additional Rules',
|
|
judgeContext: 'a design audit triage and fix loop workflow',
|
|
judgeGoal: 'how to triage design issues by severity, fix them atomically in source code, commit each fix, and re-verify with before/after screenshots',
|
|
});
|
|
}, WORKFLOW_JUDGE_TEST_MS);
|
|
|
|
testIfSelected('design-consultation/SKILL.md research', async () => {
|
|
await runWorkflowJudge({
|
|
testName: 'design-consultation/SKILL.md research',
|
|
schemaTransport: true,
|
|
suite: 'Design skill evals',
|
|
skillPath: 'design-consultation/SKILL.md',
|
|
startMarker: '## Phase 0:',
|
|
endMarker: '## Phase 4:',
|
|
judgeContext: 'a design consultation research and proposal workflow',
|
|
judgeGoal: 'how to gather product context, research the competitive landscape, and produce a complete design system proposal with typography, color, spacing, and motion specifications',
|
|
});
|
|
}, WORKFLOW_JUDGE_TEST_MS);
|
|
});
|
|
|
|
// Block 4: Deploy skills
|
|
describeIfSelected('Deploy skill evals', [
|
|
'land-and-deploy/SKILL.md workflow', 'canary/SKILL.md monitoring loop',
|
|
'benchmark/SKILL.md perf collection', 'setup-deploy/SKILL.md platform setup',
|
|
], () => {
|
|
testIfSelected('land-and-deploy/SKILL.md workflow', async () => {
|
|
await runWorkflowJudge({
|
|
testName: 'land-and-deploy/SKILL.md workflow',
|
|
schemaTransport: true,
|
|
suite: 'Deploy skill evals',
|
|
skillPath: 'land-and-deploy/SKILL.md',
|
|
startMarker: '## Step 1: Pre-flight',
|
|
endMarker: '## Important Rules',
|
|
judgeContext: 'a merge-deploy-verify workflow for landing PRs to production',
|
|
judgeGoal: 'how to merge a PR via GitHub CLI, wait for CI and deploy workflows (with platform-specific strategies for Fly.io/Render/Vercel/Netlify), run canary health checks on production, and offer revert if something breaks — with timing data logged for retrospectives',
|
|
});
|
|
}, WORKFLOW_JUDGE_TEST_MS);
|
|
|
|
testIfSelected('canary/SKILL.md monitoring loop', async () => {
|
|
await runWorkflowJudge({
|
|
testName: 'canary/SKILL.md monitoring loop',
|
|
schemaTransport: true,
|
|
suite: 'Deploy skill evals',
|
|
skillPath: 'canary/SKILL.md',
|
|
startMarker: '### Phase 2: Baseline Capture',
|
|
endMarker: '## Important Rules',
|
|
judgeContext: 'a post-deploy canary monitoring workflow driving a real browser (Aside first, the gstack headless browser as fallback)',
|
|
judgeGoal: 'how to capture baseline screenshots and metrics before deploy, run a continuous monitoring loop checking each page every 60 seconds for console errors and performance regressions, fire alerts with evidence (screenshots), and produce a health report with per-page status and verdict',
|
|
});
|
|
}, WORKFLOW_JUDGE_TEST_MS);
|
|
|
|
testIfSelected('benchmark/SKILL.md perf collection', async () => {
|
|
await runWorkflowJudge({
|
|
testName: 'benchmark/SKILL.md perf collection',
|
|
schemaTransport: true,
|
|
suite: 'Deploy skill evals',
|
|
skillPath: 'benchmark/SKILL.md',
|
|
startMarker: '### Phase 3: Performance Data Collection',
|
|
endMarker: '## Important Rules',
|
|
judgeContext: 'a performance regression detection workflow using browser-based Web Vitals measurement (Aside first, the gstack headless browser as fallback)',
|
|
judgeGoal: 'how to collect real performance metrics (TTFB, FCP, LCP, bundle sizes, request counts) via performance.getEntries(), compare against baselines with regression thresholds, produce a performance report with delta analysis, and track trends over time',
|
|
});
|
|
}, WORKFLOW_JUDGE_TEST_MS);
|
|
|
|
testIfSelected('setup-deploy/SKILL.md platform setup', async () => {
|
|
await runWorkflowJudge({
|
|
testName: 'setup-deploy/SKILL.md platform setup',
|
|
schemaTransport: true,
|
|
suite: 'Deploy skill evals',
|
|
skillPath: 'setup-deploy/SKILL.md',
|
|
startMarker: '### Step 2: Detect platform',
|
|
endMarker: '## Important Rules',
|
|
judgeContext: 'a deployment configuration setup workflow that detects deploy platforms and writes config to CLAUDE.md',
|
|
judgeGoal: 'how to detect deploy platforms (Fly.io, Render, Vercel, Netlify, Heroku, GitHub Actions, custom), gather platform-specific configuration (URLs, status commands, health checks, custom hooks), and persist everything to CLAUDE.md for future automated use',
|
|
});
|
|
}, WORKFLOW_JUDGE_TEST_MS);
|
|
});
|
|
|
|
// Block 5: Other skills
|
|
describeIfSelected('Other skill evals', [
|
|
'retro/SKILL.md instructions', 'qa-only/SKILL.md workflow', 'review/SKILL.md workflow', 'gstack-upgrade/SKILL.md upgrade flow',
|
|
'sync-gbrain/SKILL.md read-only readiness',
|
|
], () => {
|
|
testIfSelected('review/SKILL.md workflow', async () => {
|
|
await runWorkflowJudge({
|
|
testName: 'review/SKILL.md workflow',
|
|
suite: 'Other skill evals',
|
|
skillPath: 'review/SKILL.md',
|
|
startMarker: '## Step 0: Detect platform and base branch',
|
|
endMarker: null,
|
|
references: [...QA_DISCOVERY_REFERENCES, 'review/checklist.md', 'review/specialists/testing.md'],
|
|
judgeContext: 'a pre-landing review with bounded exploratory QA',
|
|
judgeGoal: 'how to review and explore changed behavior even for small diffs without a plan or server, preserve report-only discovery and the test_stub ASK gate, handle incomplete probes honestly, and rerun affected evidence after approved repairs',
|
|
agentCapability: 'frontier',
|
|
// This ~64k-token bundle is the largest workflow input. Thinking and the answer share max_tokens:
|
|
// at default effort, 24 measured samples used up to 6.9k output tokens and a CI sample spent all
|
|
// 8192 without an answer. The ship-sized streaming cap removes truncation; effort stays default.
|
|
maxTokens: 65_536,
|
|
stream: true,
|
|
});
|
|
}, WORKFLOW_JUDGE_TEST_MS);
|
|
|
|
testIfSelected('sync-gbrain/SKILL.md read-only readiness', async () => {
|
|
await runWorkflowJudge({
|
|
testName: 'sync-gbrain/SKILL.md read-only readiness',
|
|
schemaTransport: true,
|
|
suite: 'Other skill evals',
|
|
skillPath: 'sync-gbrain/SKILL.md',
|
|
startMarker: '## Step 4: Refresh',
|
|
endMarker: '## Concurrency note',
|
|
judgeContext: 'a source-scoped gbrain readiness and guidance workflow',
|
|
judgeGoal: 'how to verify the pinned worktree source using only bounded reads, preserve guidance when the read is unknown, and report the verdict without creating or deleting pages',
|
|
});
|
|
}, WORKFLOW_JUDGE_TEST_MS);
|
|
|
|
testIfSelected('retro/SKILL.md instructions', async () => {
|
|
await runWorkflowJudge({
|
|
testName: 'retro/SKILL.md instructions',
|
|
schemaTransport: true,
|
|
suite: 'Other skill evals',
|
|
skillPath: 'retro/SKILL.md',
|
|
startMarker: '## Instructions',
|
|
endMarker: '## Tone',
|
|
judgeContext: 'an engineering retrospective data gathering and analysis workflow',
|
|
judgeGoal: 'how to gather git metrics (commit history, test counts, work patterns), analyze them, produce a structured retro report with praise, growth areas, and trend tracking',
|
|
});
|
|
}, WORKFLOW_JUDGE_TEST_MS);
|
|
|
|
testIfSelected('qa-only/SKILL.md workflow', async () => {
|
|
await runWorkflowJudge({
|
|
testName: 'qa-only/SKILL.md workflow',
|
|
schemaTransport: true,
|
|
suite: 'Other skill evals',
|
|
skillPath: 'qa-only/SKILL.md',
|
|
startMarker: '# /qa-only:',
|
|
endMarker: null,
|
|
references: QA_DISCOVERY_REFERENCES.filter(file => file !== 'qa/sections/exploratory.md'),
|
|
judgeContext: 'a report-only QA testing workflow',
|
|
judgeGoal: 'how to select browser/native functional/mixed targets, explore safely with repository tools, report exact contract evidence and coverage limits, conditionally load browser/DX instructions and never mutate product/tests/Git through any tool',
|
|
});
|
|
}, WORKFLOW_JUDGE_TEST_MS);
|
|
|
|
testIfSelected('gstack-upgrade/SKILL.md upgrade flow', async () => {
|
|
await runWorkflowJudge({
|
|
testName: 'gstack-upgrade/SKILL.md upgrade flow',
|
|
schemaTransport: true,
|
|
suite: 'Other skill evals',
|
|
skillPath: 'gstack-upgrade/SKILL.md',
|
|
startMarker: '## Inline upgrade flow',
|
|
endMarker: '## Standalone usage',
|
|
judgeContext: 'a version upgrade detection and execution workflow',
|
|
judgeGoal: 'how to detect install type, compare versions, back up current install, upgrade via git or fresh clone, run setup, and show what changed',
|
|
});
|
|
}, WORKFLOW_JUDGE_TEST_MS);
|
|
});
|
|
|
|
// Voice directive eval — tests that the voice section produces the right tone
|
|
describeIfSelected('Voice directive eval', ['voice directive tone'], () => {
|
|
testIfSelected('voice directive tone', async () => {
|
|
const t0 = Date.now();
|
|
// Read a tier 2+ skill as loaded (SKILL.md plus its shared preamble) to get the full voice directive in context
|
|
const content = readSkillEffective(path.join(ROOT, 'review', 'SKILL.md'));
|
|
const voiceStart = content.indexOf('## Voice');
|
|
if (voiceStart === -1) {
|
|
throw new Error('Voice section not found in review/SKILL.md or its shared preamble. Was preamble.ts regenerated?');
|
|
}
|
|
const voiceEnd = content.indexOf('\n## ', voiceStart + 1);
|
|
const voiceSection = content.slice(voiceStart, voiceEnd > 0 ? voiceEnd : voiceStart + 3000);
|
|
|
|
const samples = await judgePanel(() => callJudge<{
|
|
directness: number;
|
|
concreteness: number;
|
|
avoids_corporate: number;
|
|
avoids_ai_vocabulary: number;
|
|
connects_user_outcomes: number;
|
|
reasoning: string;
|
|
}>(buildVoiceDirectiveJudgePrompt(voiceSection), undefined, { jsonSchema: VOICE_DIRECTIVE_JUDGE_SCHEMA }));
|
|
const { median: result, mean } = judgePanelSummary(samples, ['directness', 'concreteness', 'avoids_corporate', 'avoids_ai_vocabulary', 'connects_user_outcomes'] as const);
|
|
|
|
console.log('Voice directive panel:', JSON.stringify({ gate: 'median', median: result, mean, samples }, null, 2));
|
|
|
|
evalCollector?.addTest({
|
|
name: 'voice directive tone',
|
|
suite: 'Voice directive eval',
|
|
tier: 'llm-judge',
|
|
passed: result.directness >= 4 && result.concreteness >= 4 && result.avoids_corporate >= 4
|
|
&& result.avoids_ai_vocabulary >= 4 && result.connects_user_outcomes >= 4,
|
|
duration_ms: Date.now() - t0,
|
|
cost_usd: 0.02 * samples.length,
|
|
judge_scores: {
|
|
directness: result.directness,
|
|
concreteness: result.concreteness,
|
|
avoids_corporate: result.avoids_corporate,
|
|
avoids_ai_vocabulary: result.avoids_ai_vocabulary,
|
|
connects_user_outcomes: result.connects_user_outcomes,
|
|
},
|
|
judge_reasoning: judgePanelReasoning(samples),
|
|
});
|
|
|
|
expect(result.directness).toBeGreaterThanOrEqual(4);
|
|
expect(result.concreteness).toBeGreaterThanOrEqual(4);
|
|
expect(result.avoids_corporate).toBeGreaterThanOrEqual(4);
|
|
expect(result.avoids_ai_vocabulary).toBeGreaterThanOrEqual(4);
|
|
expect(result.connects_user_outcomes).toBeGreaterThanOrEqual(4);
|
|
}, JUDGE_MS);
|
|
});
|
|
|
|
describeIfSelected('Cookie setup workflow quality', ['setup-browser-cookies/SKILL.md workflow'], () => {
|
|
testIfSelected('setup-browser-cookies/SKILL.md workflow', async () => {
|
|
await runWorkflowJudge({
|
|
testName: 'setup-browser-cookies/SKILL.md workflow',
|
|
suite: 'Cookie setup workflow quality',
|
|
skillPath: 'setup-browser-cookies/SKILL.md',
|
|
startMarker: '# Setup Browser Cookies',
|
|
endMarker: null,
|
|
...COOKIE_WORKFLOW_JUDGE,
|
|
readInput: () => buildCookieWorkflowJudgeInput(ROOT),
|
|
});
|
|
}, WORKFLOW_JUDGE_TEST_MS);
|
|
});
|
|
|
|
// Module-level afterAll — finalize eval collector after all tests complete
|
|
afterAll(() => finalizeEvalCollector(evalCollector));
|