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>
383 lines
24 KiB
TypeScript
383 lines
24 KiB
TypeScript
/** Free regression coverage for the file bundle sent to workflow judges. */
|
||
import { afterEach, describe, expect, test } from 'bun:test';
|
||
import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
||
import { tmpdir } from 'node:os';
|
||
import { dirname, join, resolve } from 'node:path';
|
||
import { createHash } from 'node:crypto';
|
||
import { readWorkflowJudgeInput, buildWorkflowJudgePrompt, QA_DISCOVERY_REFERENCES, WORKFLOW_JUDGE_RESPONSE_SCHEMA, WORKFLOW_JUDGE_REASONING_WORD_LIMIT } from './helpers/workflow-judge-input';
|
||
import { ASK_QUESTIONS_HEADING, ENG_REVIEW_EXCERPT } from './helpers/workflow-excerpt';
|
||
import { readSkillStartup, SHARED_PREAMBLE_FILE } from './helpers/skill-startup';
|
||
|
||
const ROOT = resolve(import.meta.dir, '..');
|
||
const scratchRoots: string[] = [];
|
||
|
||
test('cache extraction preserves every byte of the original workflow request and rubric', () => {
|
||
// Captured from runWorkflowJudge's pre-cache template literal, not from the
|
||
// new builder: moving request construction must not change the paid metric.
|
||
const prompt = buildWorkflowJudgePrompt({ judgeContext: 'test workflow', judgeGoal: 'test goal' },
|
||
{ files: [], text: 'full prompt\nincluding lines' });
|
||
expect(createHash('sha256').update(prompt).digest('hex')).toBe('71cc9c777bf28ff0efd610259b411e3539852f83a0888fa0e92469331f8b9a43');
|
||
});
|
||
|
||
test.each(['ship', 'review'])('%s clarity targets frontier readers without excusing missing decisions or authority', skill => {
|
||
const source = readFileSync(join(ROOT, 'test/skill-llm-eval.test.ts'), 'utf8');
|
||
const registration = source.match(new RegExp(`testIfSelected\\('${skill}/SKILL\\.md workflow',[\\s\\S]*?await runWorkflowJudge\\(\\{([\\s\\S]*?)\\n \\}\\);`));
|
||
expect(registration).not.toBeNull();
|
||
const options = new Function('QA_DISCOVERY_REFERENCES', `return ({${registration![1]}});`)(QA_DISCOVERY_REFERENCES);
|
||
expect(options.agentCapability).toBe('frontier');
|
||
expect(options.thresholds).toBeUndefined();
|
||
const input = readWorkflowJudgeInput({ root: ROOT, ...options });
|
||
const prompt = buildWorkflowJudgePrompt(options, input);
|
||
expect(prompt).toContain('GPT-5.6 Sol-level capability or stronger');
|
||
expect(prompt).toContain('Length, technical vocabulary and multiple explicit recovery paths alone are not clarity defects');
|
||
expect(prompt).toContain('Do not invent missing policies, permissions or evidence');
|
||
expect(prompt).toContain('Clarity 4 means the target agent can determine the next permitted action on each applicable path');
|
||
expect(prompt).toContain('Score clarity 3 or lower when execution still requires guessing');
|
||
expect(prompt).toContain('conflicting order, undefined decisions, unclear authority or missing input/output handling');
|
||
expect(prompt).toContain('cite the specific file/step and explain the competing actions or missing decision');
|
||
expect(prompt.endsWith(input.text)).toBe(true);
|
||
expect(prompt).toContain('"clarity": N, "completeness": N, "actionability": N, "reasoning": "brief explanation"');
|
||
});
|
||
|
||
test('frontier calibration bounds reporting without reducing the evaluated source bundle', () => {
|
||
const input = { files: [], text: 'Entire source bundle remains present.' };
|
||
const prompt = buildWorkflowJudgePrompt({ judgeContext: 'a workflow', judgeGoal: 'how to finish', agentCapability: 'frontier' }, input);
|
||
expect(prompt).toContain('Evaluate the whole workflow, but keep the JSON reasoning under 120 words with at most two decisive examples');
|
||
expect(prompt).toContain('For a clarity defect, cite the specific file/step and explain the competing actions or missing decision');
|
||
expect(prompt).not.toContain('For each clarity defect');
|
||
expect(prompt.endsWith(input.text)).toBe(true);
|
||
});
|
||
|
||
test('judges are asked for 120 words while the enforced reasoning limit stays below 150', () => {
|
||
const prompt = buildWorkflowJudgePrompt({ judgeContext: 'a workflow', judgeGoal: 'how to finish', agentCapability: 'frontier' }, { files: [], text: '' });
|
||
expect(prompt).toContain('keep the JSON reasoning under 120 words');
|
||
expect(prompt).not.toContain('150 words');
|
||
expect(WORKFLOW_JUDGE_RESPONSE_SCHEMA.properties.reasoning.description).toStartWith('Under 120 words');
|
||
expect(WORKFLOW_JUDGE_REASONING_WORD_LIMIT).toBe(150);
|
||
});
|
||
|
||
afterEach(() => {
|
||
for (const root of scratchRoots.splice(0)) rmSync(root, { recursive: true, force: true });
|
||
});
|
||
|
||
function fixture(files: Record<string, string>): string {
|
||
const root = mkdtempSync(join(tmpdir(), 'gstack-workflow-judge-'));
|
||
scratchRoots.push(root);
|
||
for (const [path, content] of Object.entries(files)) {
|
||
mkdirSync(dirname(join(root, path)), { recursive: true });
|
||
writeFileSync(join(root, path), content);
|
||
}
|
||
return root;
|
||
}
|
||
|
||
function occurrences(text: string, needle: string): number {
|
||
return text.split(needle).length - 1;
|
||
}
|
||
|
||
/** The judge's bundle: every generated section except runner-only profiles (manifest `runner_only`, plan B4). */
|
||
function sectionPaths(skill: string): string[] {
|
||
const runnerOnly = new Set((JSON.parse(readFileSync(join(ROOT, skill, 'sections', 'manifest.json'), 'utf8')).sections as Array<{ file: string; runner_only?: boolean }>)
|
||
.filter(section => section.runner_only).map(section => section.file));
|
||
return readdirSync(join(ROOT, skill, 'sections'))
|
||
.filter(name => name.endsWith('.md') && !runnerOnly.has(name) && name !== SHARED_PREAMBLE_FILE)
|
||
.sort()
|
||
.map(name => `${skill}/sections/${name}`);
|
||
}
|
||
|
||
describe('workflow judge file bundle', () => {
|
||
test('the actual Eng judge registration includes authorization and the referenced final gate', () => {
|
||
const source = readFileSync(join(ROOT, 'test/skill-llm-eval.test.ts'), 'utf8');
|
||
const registration = source.match(/testIfSelected\('plan-eng-review\/SKILL\.md sections',[\s\S]*?await runWorkflowJudge\(\{([\s\S]*?)\n \}\);/);
|
||
expect(registration).not.toBeNull();
|
||
const options = new Function('ENG_REVIEW_EXCERPT', `return ({${registration![1]}});`)(ENG_REVIEW_EXCERPT);
|
||
const input = readWorkflowJudgeInput({ root: ROOT, ...options });
|
||
const entry = input.files.find(file => file.kind === 'entrypoint')!;
|
||
const complete = readSkillStartup(join(ROOT, options.skillPath));
|
||
expect(entry.content).toBe(complete.slice(complete.indexOf('# Plan Review Mode')));
|
||
expect(entry.content).toContain('Do not build features, acceptance suites or benchmarks unless explicitly authorized');
|
||
expect(entry.content).toContain('## Scope gate');
|
||
expect(entry.content).toContain('## Section self-check');
|
||
expect(entry.content).toContain('## EXIT PLAN MODE GATE (BLOCKING)');
|
||
expect(entry.content.trimEnd()).toMatch(/After success telemetry and cache dispatch, call ExitPlanMode[^\n]*\.$/);
|
||
});
|
||
|
||
test('preserves the exact entrypoint excerpt and each complete section in sorted named files', () => {
|
||
const entrypoint = 'excluded preamble\n## Begin\nRead sections/z-last.md when directed.\n## End\nexcluded epilogue';
|
||
const sections = {
|
||
'example/sections/z-last.md': 'Z section prefix\nZ section suffix',
|
||
'example/sections/a-first.md': 'A section prefix\nA section suffix',
|
||
};
|
||
const root = fixture({
|
||
'example/SKILL.md': entrypoint,
|
||
...sections,
|
||
'example/sections/ignored.md.tmpl': 'DO NOT EVALUATE TEMPLATE',
|
||
'example/sections/manifest.json': '{"ignored": true}',
|
||
});
|
||
const input = readWorkflowJudgeInput({ root, skillPath: 'example/SKILL.md', startMarker: '## Begin', endMarker: '## End' });
|
||
|
||
expect(input.files).toEqual([
|
||
{ path: 'example/SKILL.md', kind: 'entrypoint', content: '## Begin\nRead sections/z-last.md when directed.\n', startLine: 2, endLine: 3 },
|
||
{ path: 'example/sections/a-first.md', kind: 'section', content: sections['example/sections/a-first.md'], startLine: 1, endLine: 2 },
|
||
{ path: 'example/sections/z-last.md', kind: 'section', content: sections['example/sections/z-last.md'], startLine: 1, endLine: 2 },
|
||
]);
|
||
expect(input.text).not.toContain('excluded preamble');
|
||
expect(input.text).not.toContain('excluded epilogue');
|
||
expect(input.text).not.toContain('DO NOT EVALUATE TEMPLATE');
|
||
expect(input.text).not.toContain('manifest.json');
|
||
for (const file of input.files) {
|
||
expect(occurrences(input.text, file.content)).toBe(1);
|
||
const begin = input.text.split('\n').filter(line => line.includes('BEGIN FILE') && line.includes(file.path));
|
||
const end = input.text.split('\n').filter(line => line.includes('END FILE') && line.includes(file.path));
|
||
expect(begin).toHaveLength(1);
|
||
expect(end).toHaveLength(1);
|
||
expect(begin[0]).toContain(`${file.startLine}-${file.endLine}`);
|
||
}
|
||
});
|
||
|
||
test('describes lazy file loading without presenting bundle order as execution order', () => {
|
||
const root = fixture({
|
||
'example/SKILL.md': '## Begin\nSTOP and read sections/step.md.\n## End',
|
||
'example/sections/step.md': 'Run the separately loaded step.',
|
||
});
|
||
const { text } = readWorkflowJudgeInput({ root, skillPath: 'example/SKILL.md', startMarker: '## Begin', endMarker: '## End' });
|
||
const header = text.slice(0, text.indexOf('BEGIN FILE'));
|
||
expect(header).toMatch(/bundle/i);
|
||
expect(header).toMatch(/file/i);
|
||
expect(header).toMatch(/separate|separately/i);
|
||
expect(header).toMatch(/lazy|on-demand/i);
|
||
expect(header).toMatch(/directives|instructions/i);
|
||
expect(header).toMatch(/(?:not|isn't|does not)[^.\n]*execution order/i);
|
||
expect(text).toContain('STOP and read sections/step.md.');
|
||
});
|
||
|
||
test('cross-skill references retain complete bytes once and fail on missing assets', () => {
|
||
const root = fixture({
|
||
'example/SKILL.md': '## Begin\nRead the shared resource.\n## End',
|
||
'example/sections/local.md': 'Complete local section.',
|
||
'shared/sections/method.md': 'Shared prefix\n## End\nShared suffix',
|
||
});
|
||
const options = { root, skillPath: 'example/SKILL.md', startMarker: '## Begin', endMarker: '## End',
|
||
references: ['example/sections/local.md', 'shared/sections/method.md', 'shared/sections/method.md'] };
|
||
const input = readWorkflowJudgeInput(options);
|
||
expect(input.files.filter(file => file.kind === 'reference')).toEqual([
|
||
{ path: 'shared/sections/method.md', kind: 'reference', content: 'Shared prefix\n## End\nShared suffix', startLine: 1, endLine: 3 },
|
||
]);
|
||
expect(input.files.filter(file => file.path === 'example/sections/local.md')).toHaveLength(1);
|
||
expect(occurrences(input.text, 'Shared prefix')).toBe(1);
|
||
expect(() => readWorkflowJudgeInput({ ...options, references: ['shared/missing.md'] })).toThrow();
|
||
expect(() => readWorkflowJudgeInput({ ...options, references: ['../outside.md'] })).toThrow('Reference outside root');
|
||
});
|
||
|
||
for (const skill of ['ship', 'qa-only', 'document-release', 'review']) {
|
||
test(`actual ${skill} judge includes all referenced QA or documentation sections`, () => {
|
||
const caller = readFileSync(join(ROOT, 'test/skill-llm-eval.test.ts'), 'utf8');
|
||
const name = `${skill}/SKILL.md workflow`;
|
||
const start = caller.indexOf(`testIfSelected('${name}'`);
|
||
expect(start).toBeGreaterThanOrEqual(0);
|
||
const registration = caller.slice(start).match(/await runWorkflowJudge\(\{([\s\S]*?)\n \}\);/);
|
||
expect(registration).not.toBeNull();
|
||
const options = new Function('QA_DISCOVERY_REFERENCES', `return ({${registration![1]}});`)(QA_DISCOVERY_REFERENCES);
|
||
const input = readWorkflowJudgeInput({ root: ROOT, ...options });
|
||
for (const file of options.references ?? []) {
|
||
expect(input.files.find(item => item.path === file)?.content).toBe(readFileSync(join(ROOT, file), 'utf8'));
|
||
}
|
||
if (skill === 'ship') {
|
||
expect(input.files.map(file => file.path)).toContain('ship/sections/documentation.md');
|
||
expect(input.files.map(file => file.path)).toContain('qa/sections/exploratory.md');
|
||
} else if (skill === 'qa-only') {
|
||
expect(input.files.map(file => file.path)).toContain('qa/sections/system-functional.md');
|
||
expect(input.files.filter(file => file.path.endsWith('/exploratory.md'))).toHaveLength(1);
|
||
expect(input.files.find(file => file.kind === 'entrypoint')?.content).toContain('Never fix bugs or write product tests');
|
||
} else if (skill === 'document-release') {
|
||
expect(input.files.map(file => file.path)).toContain('document-release/sections/audit-scope.md');
|
||
expect(input.text).toContain('Ship-owned documentation mode');
|
||
} else {
|
||
expect(input.files.map(file => file.path)).toContain('qa/sections/exploratory.md');
|
||
expect(input.files.map(file => file.path)).toContain('review/checklist.md');
|
||
expect(input.text).toContain('test_stub');
|
||
expect(input.text).toContain('## Step 5: Fix-First Review');
|
||
}
|
||
});
|
||
}
|
||
|
||
test('retains section prelude and suffix exactly once when both markers are inside a section', () => {
|
||
// Generated comments made the old 120-character prefix heuristic append
|
||
// this whole file after its partial slice, duplicating every review pass.
|
||
const section = [
|
||
'<!-- AUTO-GENERATED from review-sections.md.tmpl — do not edit directly -->',
|
||
'<!-- Regenerate: bun run gen:skill-docs -->',
|
||
'section context before the selected marker',
|
||
'## Review Sections',
|
||
'### Pass 1: Information Architecture',
|
||
'Evaluate the first pass.',
|
||
'## CRITICAL RULE',
|
||
'section context after the selected marker',
|
||
].join('\n');
|
||
const root = fixture({
|
||
'example/SKILL.md': 'Entrypoint preamble.\nSTOP and read sections/review-sections.md.',
|
||
'example/sections/review-sections.md': section,
|
||
});
|
||
const input = readWorkflowJudgeInput({ root, skillPath: 'example/SKILL.md', startMarker: '## Review Sections', endMarker: '## CRITICAL RULE' });
|
||
|
||
expect(input.files).toEqual([
|
||
{ path: 'example/sections/review-sections.md', kind: 'section', content: section, startLine: 1, endLine: 8 },
|
||
]);
|
||
expect(occurrences(input.text, '### Pass 1: Information Architecture')).toBe(1);
|
||
expect(occurrences(input.text, 'section context before the selected marker')).toBe(1);
|
||
expect(occurrences(input.text, 'section context after the selected marker')).toBe(1);
|
||
expect(occurrences(input.text, section)).toBe(1);
|
||
expect(input.text).not.toContain('Entrypoint preamble.');
|
||
});
|
||
|
||
test('handles marker windows spanning files without losing section prefixes or suffixes', () => {
|
||
const root = fixture({
|
||
'example/SKILL.md': 'omitted\n## Begin\nEntrypoint tail',
|
||
'example/sections/a.md': 'First section prefix\nFirst section body\nFirst section suffix',
|
||
'example/sections/b.md': 'Second section prefix\n## End\nSecond section suffix',
|
||
});
|
||
const input = readWorkflowJudgeInput({ root, skillPath: 'example/SKILL.md', startMarker: '## Begin', endMarker: '## End' });
|
||
|
||
expect(input.files[0]).toEqual({
|
||
path: 'example/SKILL.md', kind: 'entrypoint', content: '## Begin\nEntrypoint tail', startLine: 2, endLine: 3,
|
||
});
|
||
expect(input.files.slice(1).map(file => file.content)).toEqual([
|
||
'First section prefix\nFirst section body\nFirst section suffix',
|
||
'Second section prefix\n## End\nSecond section suffix',
|
||
]);
|
||
expect(occurrences(input.text, 'Second section prefix')).toBe(1);
|
||
expect(occurrences(input.text, 'Second section suffix')).toBe(1);
|
||
});
|
||
|
||
test('keeps separate files even when their generated preludes and contents match', () => {
|
||
const body = '<!-- AUTO-GENERATED -->\nShared instruction';
|
||
const root = fixture({
|
||
'example/SKILL.md': '## Begin\nRead both sections.\n## End',
|
||
'example/sections/a.md': body,
|
||
'example/sections/b.md': body,
|
||
});
|
||
const input = readWorkflowJudgeInput({ root, skillPath: 'example/SKILL.md', startMarker: '## Begin', endMarker: '## End' });
|
||
expect(input.files.filter(file => file.kind === 'section').map(file => file.path)).toEqual([
|
||
'example/sections/a.md', 'example/sections/b.md',
|
||
]);
|
||
expect(occurrences(input.text, body)).toBe(2);
|
||
});
|
||
|
||
test('supports an uncarved workflow and an open-ended slice', () => {
|
||
const root = fixture({ 'example/SKILL.md': 'omitted\n## Begin\nRetain this final line' });
|
||
const input = readWorkflowJudgeInput({ root, skillPath: 'example/SKILL.md', startMarker: '## Begin', endMarker: null });
|
||
expect(input.files).toEqual([
|
||
{ path: 'example/SKILL.md', kind: 'entrypoint', content: '## Begin\nRetain this final line', startLine: 2, endLine: 3 },
|
||
]);
|
||
});
|
||
|
||
test('rejects missing start markers, missing end markers, and end markers preceding the start', () => {
|
||
const root = fixture({ 'example/SKILL.md': '## Earlier\n## Begin\nBody' });
|
||
expect(() => readWorkflowJudgeInput({ root, skillPath: 'example/SKILL.md', startMarker: '## Missing', endMarker: null })).toThrow(/Start marker not found/);
|
||
expect(() => readWorkflowJudgeInput({ root, skillPath: 'example/SKILL.md', startMarker: '## Begin', endMarker: '## Missing' })).toThrow(/End marker not found/);
|
||
expect(() => readWorkflowJudgeInput({ root, skillPath: 'example/SKILL.md', startMarker: '## Begin', endMarker: '## Earlier' })).toThrow(/End marker not found/);
|
||
});
|
||
|
||
test('generated ship includes base-branch initialization and every lazy section once', () => {
|
||
const skillPath = 'ship/SKILL.md';
|
||
const source = readFileSync(join(ROOT, skillPath), 'utf8');
|
||
// Bind to the paid caller's actual slice and retain both the opening contract
|
||
// and initialization, regardless of their ordering in the authored workflow.
|
||
const caller = readFileSync(join(ROOT, 'test/skill-llm-eval.test.ts'), 'utf8');
|
||
const markers = caller.match(/skillPath: 'ship\/SKILL\.md',\s+startMarker: '([^']+)',\s+endMarker: '([^']+)'/);
|
||
expect(markers).not.toBeNull();
|
||
const [, startMarker, endMarker] = markers!;
|
||
expect(startMarker).toBe('# Ship:');
|
||
const input = readWorkflowJudgeInput({ root: ROOT, skillPath, startMarker, endMarker });
|
||
const entrypoint = input.files.find(file => file.kind === 'entrypoint');
|
||
expect(entrypoint?.content).toBe(source.slice(source.indexOf(startMarker), source.indexOf(endMarker, source.indexOf(startMarker))));
|
||
expect(entrypoint?.content).toContain('git remote get-url origin');
|
||
expect(entrypoint?.content).toContain('STOP blocks advancement until the stated repair/resume route clears; without one, end this attempt');
|
||
const flow = entrypoint!.content.replace(/\s+/g, ' ');
|
||
expect(flow).toContain('Every new invocation repeats Steps 1–16, including both reviews and the docs audit');
|
||
expect(flow).toContain('children return evidence, not permission to proceed');
|
||
expect(flow).toContain('Follow the saved work list');
|
||
expect(flow).not.toContain('| At step | Outcome |');
|
||
expect(flow).toContain('`gstack-wtree` prints a Git tree hash');
|
||
expect(flow).toContain('Offline output without that fallback, failure, malformed output or an empty version is unusable');
|
||
expect(entrypoint?.content).toContain('Answer each AskUserQuestion before continuing');
|
||
expect(entrypoint?.content).toContain('Routine authorization never waives those gates or their required user decisions');
|
||
expect(entrypoint?.content).toContain('## Step 0: Detect platform and base branch');
|
||
expect(entrypoint?.content).toContain('gh pr view --json baseRefName');
|
||
expect(entrypoint?.content).toContain('Print the detected base branch name.');
|
||
expect(occurrences(input.text, startMarker)).toBe(1);
|
||
const sections = input.files.filter(file => file.kind === 'section');
|
||
expect(sections.map(file => file.path)).toEqual(sectionPaths('ship'));
|
||
for (const file of sections) {
|
||
const source = readFileSync(join(ROOT, file.path), 'utf8');
|
||
expect(file.content).toBe(source);
|
||
expect(occurrences(input.text, source)).toBe(1);
|
||
expect(file.startLine).toBe(1);
|
||
}
|
||
});
|
||
|
||
test('generated engineering review includes the scope choices and readiness probe its steps reference', () => {
|
||
const skillPath = 'plan-eng-review/SKILL.md';
|
||
const caller = readFileSync(join(ROOT, 'test/skill-llm-eval.test.ts'), 'utf8');
|
||
const registration = caller.match(/testIfSelected\('plan-eng-review\/SKILL\.md sections',[\s\S]*?await runWorkflowJudge\(\{([\s\S]*?)\n \}\);/);
|
||
expect(registration).not.toBeNull();
|
||
const options = new Function('ENG_REVIEW_EXCERPT', `return ({${registration![1]}});`)(ENG_REVIEW_EXCERPT);
|
||
expect(ENG_REVIEW_EXCERPT.skillPath).toBe(skillPath);
|
||
const input = readWorkflowJudgeInput({ root: ROOT, ...options });
|
||
const entrypoint = input.files.find(file => file.kind === 'entrypoint')!;
|
||
expect(entrypoint.content).toContain('B) A plan or design doc');
|
||
expect(entrypoint.content).toContain('## Scope gate');
|
||
expect(entrypoint.content.indexOf('## Scope gate')).toBeLessThan(entrypoint.content.indexOf('### Step 0: Scope Challenge'));
|
||
expect(entrypoint.content).toContain('## Web research runs in Aside');
|
||
// E7: the readiness probe prints the resolved Aside path (READY: $_A).
|
||
expect(entrypoint.content).toContain('echo "READY: $_A');
|
||
expect(input.text.indexOf('echo "READY: $_A')).toBeLessThan(input.text.indexOf('- **Search check:**'));
|
||
expect(entrypoint.content).not.toContain('- **Search check:**');
|
||
expect(occurrences(input.text, '- **Search check:**')).toBe(1);
|
||
expect(occurrences(input.text, '## Scope gate')).toBe(1);
|
||
expect(occurrences(input.text, '### 1. Architecture review')).toBe(1);
|
||
const sections = input.files.filter(file => file.kind === 'section');
|
||
expect(sections.map(file => file.path)).toEqual(sectionPaths('plan-eng-review'));
|
||
for (const file of sections) {
|
||
const source = readFileSync(join(ROOT, file.path), 'utf8');
|
||
expect(file.content).toBe(source);
|
||
expect(occurrences(input.text, source)).toBe(1);
|
||
}
|
||
});
|
||
|
||
test('generated design consultation includes the prechecks its proposal and preview reference', () => {
|
||
const skillPath = 'design-consultation/SKILL.md';
|
||
const caller = readFileSync(join(ROOT, 'test/skill-llm-eval.test.ts'), 'utf8');
|
||
const markers = caller.match(/skillPath: 'design-consultation\/SKILL\.md',\s+startMarker: '([^']+)',\s+endMarker: '([^']+)'/);
|
||
expect(markers).not.toBeNull();
|
||
const [, startMarker, endMarker] = markers!;
|
||
const input = readWorkflowJudgeInput({ root: ROOT, skillPath, startMarker, endMarker });
|
||
const entrypoint = input.files.find(file => file.kind === 'entrypoint')!;
|
||
for (const prerequisite of ['## Phase 0: Pre-checks', 'DESIGN_MD_FORMAT:', 'DESIGN_READY', 'DESIGN_NOT_AVAILABLE']) {
|
||
expect(entrypoint.content).toContain(prerequisite);
|
||
}
|
||
expect(entrypoint.content.indexOf('## Phase 0:')).toBeLessThan(entrypoint.content.indexOf('## Phase 1:'));
|
||
expect(occurrences(input.text, '## Phase 0: Pre-checks')).toBe(1);
|
||
const sections = input.files.filter(file => file.kind === 'section');
|
||
expect(sections.map(file => file.path)).toEqual(sectionPaths('design-consultation'));
|
||
for (const file of sections) {
|
||
const source = readFileSync(join(ROOT, file.path), 'utf8');
|
||
expect(file.content).toBe(source);
|
||
expect(occurrences(input.text, source)).toBe(1);
|
||
}
|
||
});
|
||
|
||
test('generated plan-design passes retain their full section without duplicating Pass 1', () => {
|
||
const input = readWorkflowJudgeInput({
|
||
root: ROOT, skillPath: 'plan-design-review/SKILL.md', startMarker: '## Review Sections', endMarker: ASK_QUESTIONS_HEADING,
|
||
});
|
||
expect(input.files.filter(file => file.kind === 'entrypoint')).toHaveLength(0);
|
||
expect(input.files.map(file => file.path)).toEqual(sectionPaths('plan-design-review'));
|
||
const section = input.files.find(file => file.path === 'plan-design-review/sections/review-sections.md');
|
||
expect(section?.content).toBe(readFileSync(join(ROOT, 'plan-design-review/sections/review-sections.md'), 'utf8'));
|
||
expect(section?.content).toStartWith('<!-- AUTO-GENERATED');
|
||
expect(section?.content).toMatch(ASK_QUESTIONS_HEADING);
|
||
expect(section?.content).toContain('## Formatting Rules');
|
||
expect(occurrences(input.text, '### Pass 1: Information Architecture')).toBe(1);
|
||
});
|
||
});
|