mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-27 23:21:53 +02:00
v1.89.1.0 fix: remove continuous checkpoint commits (#2970)
* v1.89.1.0 fix: remove continuous checkpoint commits and repair validation blockers * fix: clarify shipping and engineering review recovery * fix: interpret native no-change review descriptions * test: separate descendant readiness from timeout delivery
This commit is contained in:
@@ -20,6 +20,7 @@ import { generateTestBootstrap } from '../scripts/resolvers/testing';
|
||||
import { generateBrowseFallback, generateBrowseSetup } from '../scripts/resolvers/browse';
|
||||
import { RESOLVERS } from '../scripts/resolvers/index';
|
||||
import { HOST_PATHS } from '../scripts/resolvers/types';
|
||||
import { extractDesignResearchContract } from './helpers/skill-fixture';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const ctx = { skillName: 'qa', tmplPath: '', host: 'claude' as const, paths: HOST_PATHS['claude'] };
|
||||
@@ -304,15 +305,16 @@ describe('web research ({{ASIDE_RESEARCH}})', () => {
|
||||
const md = fs.readFileSync(path.join(ROOT, skill, 'SKILL.md'), 'utf-8');
|
||||
expect({ skill, count: md.split('## Web research runs in Aside').length - 1 }).toEqual({ skill, count: 1 });
|
||||
expect({ skill, hasFallbackLine: md.includes('Search unavailable — proceeding with in-distribution knowledge only.') }).toEqual({ skill, hasFallbackLine: true });
|
||||
// The rendered RESOLVER output (heading through its closing sentence) carries the receipted
|
||||
// prelude and no bare send. Skill-authored blocks after the placeholder are the template's own.
|
||||
const start = md.indexOf('## Web research runs in Aside');
|
||||
const closing = "not the user's data.";
|
||||
const end = md.indexOf(closing, start);
|
||||
expect({ skill, hasClosing: end > start }).toEqual({ skill, hasClosing: true });
|
||||
const rendered = md.slice(start, end + closing.length);
|
||||
const routing = generateAsideResearch({ ...ctx, skillName: skill });
|
||||
expect({ skill, count: md.split(routing).length - 1 }).toEqual({ skill, count: 1 });
|
||||
const rendered = skill === 'design-consultation' ? extractDesignResearchContract(md) : routing;
|
||||
expect({ skill, hasPrelude: rendered.includes('_aside_exec() {'), sameProbe: rendered.includes(setupProbe.trimEnd()) }).toEqual({ skill, hasPrelude: true, sameProbe: true });
|
||||
expect({ skill, bareAsideExec: BARE_ASIDE_EXEC.test(rendered) }).toEqual({ skill, bareAsideExec: false });
|
||||
if (skill === 'design-consultation') {
|
||||
expect(routing).toContain('Reuse the Phase 0 BROWSER SETUP result; do not repeat the probe here');
|
||||
expect(rendered.split(setupProbe.trimEnd())).toHaveLength(2);
|
||||
expect(rendered.split('_aside_exec() {')).toHaveLength(2);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ import selectorCapture from './fixtures/auto-decide-mode-selector-749df.json';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const TARGET = 'plan-ceo-review-mode';
|
||||
const UNRELATED = 'feature-continuous-checkpoint';
|
||||
const UNRELATED = 'telemetry-consent';
|
||||
|
||||
function withFixture(check: (fixture: {
|
||||
state: string;
|
||||
@@ -149,17 +149,17 @@ await import(path.join(root, 'test/skill-e2e-auto-decide-preserved.test.ts'));
|
||||
expect(output).not.toContain('GSTACK_INSTRUCTION_BEGIN:');
|
||||
expect(run('gstack-config', ['get', 'cross_project_learnings'])).toBe('false');
|
||||
expect(run('gstack-question-preference', ['--check', TARGET, '--summary-stdin'], 'Choose the CEO review mode')).toBe('AUTO_DECIDE\n');
|
||||
expect(run('gstack-question-preference', ['--check', UNRELATED, '--summary-stdin'], 'Enable continuous checkpoint auto-commits?')).toBe('ASK_NORMALLY\n');
|
||||
expect(run('gstack-question-preference', ['--check', UNRELATED, '--summary-stdin'], 'Share anonymous usage data?')).toBe('ASK_NORMALLY\n');
|
||||
expect(JSON.parse(fs.readFileSync(preferenceFile, 'utf8'))).toEqual({ [TARGET]: 'never-ask' });
|
||||
});
|
||||
});
|
||||
|
||||
test('the missing checkpoint marker reproduces the unrelated question from both paid failures', () => {
|
||||
test('the missing checkpoint marker no longer introduces an unrelated question', () => {
|
||||
withFixture(({ state, run }) => {
|
||||
fs.unlinkSync(path.join(state, '.feature-prompted-continuous-checkpoint'));
|
||||
expect(fs.existsSync(path.join(state, '.feature-prompted-continuous-checkpoint'))).toBe(false);
|
||||
const output = run('gstack-skill-start', ['--skill', 'plan-ceo-review']);
|
||||
expect(output).toContain('GSTACK_INSTRUCTION_BEGIN: feature-checkpoint ');
|
||||
expect(output).toContain('Feature discovery: AskUserQuestion for Continuous checkpoint auto-commits.');
|
||||
expect(output).not.toContain('GSTACK_INSTRUCTION_BEGIN:');
|
||||
expect(output).not.toMatch(/checkpoint/i);
|
||||
expect(run('gstack-question-preference', ['--check', TARGET])).toBe('AUTO_DECIDE\n');
|
||||
expect(run('gstack-question-preference', ['--check', UNRELATED])).toBe('ASK_NORMALLY\n');
|
||||
});
|
||||
|
||||
@@ -45,7 +45,8 @@ describe('content-binding template drift', () => {
|
||||
test('ship Step 16 carries the evidence check (mechanized IRON LAW)', () => {
|
||||
const ship = rendered('ship/SKILL.md');
|
||||
expect(ship).toMatch(/gstack-evidence check --label tests --expect-cmd '[^']+' --label vitest --expect-cmd '[^']+' --max-age 24 --allow-paths CHANGELOG\.md,VERSION,package\.json/);
|
||||
expect(ship).toContain('a failed CHECK never blocks');
|
||||
expect(ship).toContain('A failed CHECK identifies evidence to repair; it is not a test failure');
|
||||
expect(ship).toContain('required live RUN must pass');
|
||||
});
|
||||
|
||||
test('ship Step 5 lanes run wrapped with per-lane labels', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterAll, describe, expect, test } from 'bun:test';
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { afterAll, describe, expect, spyOn, test } from 'bun:test';
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
@@ -16,6 +16,7 @@ const PID = path.join(DIR, 'descendant.pid');
|
||||
// inherited pipes; a PID returned by spawn alone does not establish that state.
|
||||
writeFileSync(DESCENDANT, `
|
||||
import { writeFileSync } from 'node:fs';
|
||||
await Bun.sleep(Number(process.env.DESCENDANT_DELAY_MS || 0));
|
||||
setInterval(() => {}, 1000);
|
||||
await new Promise(resolve => process.stdout.write(' ', resolve));
|
||||
await new Promise(resolve => process.stderr.write(' ', resolve));
|
||||
@@ -28,6 +29,10 @@ import { existsSync, rmSync, writeFileSync } from 'node:fs';
|
||||
const prompt = await Bun.stdin.text();
|
||||
writeFileSync(process.env.CAPTURE!, JSON.stringify({args:process.argv.slice(2),prompt,cwd:process.cwd(),model:process.env.ANTHROPIC_MODEL,auth:process.env.ANTHROPIC_API_KEY}));
|
||||
const mode = process.env.FAKE_MODE;
|
||||
if (mode === 'startup-timeout') {
|
||||
setInterval(() => {}, 1000);
|
||||
await new Promise(() => {});
|
||||
}
|
||||
if (mode === 'timeout' || mode === 'descendant' || mode === 'escaped') {
|
||||
rmSync(process.env.PID_FILE!, { force: true });
|
||||
// libuv on Windows kills non-detached children when this fake exits. The
|
||||
@@ -207,15 +212,49 @@ describe('Claude Code restricted execution', () => {
|
||||
test('timeout kills its descendants and clears process signal listeners', async () => {
|
||||
rmSync(PID, { force: true });
|
||||
const before = ['SIGINT','SIGTERM','exit'].map(name => process.listenerCount(name));
|
||||
const start = Date.now();
|
||||
const schedule = globalThis.setTimeout;
|
||||
let fireTimeout: (() => void) | undefined;
|
||||
const timer = spyOn(globalThis, 'setTimeout').mockImplementation((callback, delay, ...args) => {
|
||||
if (delay !== 500) return schedule(callback, delay, ...args);
|
||||
fireTimeout = () => callback(...args);
|
||||
return schedule(() => {}, 0);
|
||||
});
|
||||
let invocation: ReturnType<typeof run>;
|
||||
try {
|
||||
const result = await run('timeout', {timeoutMs:500});
|
||||
invocation = run('timeout', {timeoutMs:500, env:{...env('timeout'), DESCENDANT_DELAY_MS:'750'}});
|
||||
} finally { timer.mockRestore(); }
|
||||
try {
|
||||
expect(fireTimeout).toBeDefined();
|
||||
const readyBy = Date.now() + 2000;
|
||||
while (!existsSync(PID) && Date.now() < readyBy) await Bun.sleep(5);
|
||||
expect(running(Number(readFileSync(PID, 'utf8')))).toBe(true);
|
||||
const start = Date.now();
|
||||
const expire = fireTimeout!;
|
||||
fireTimeout = undefined;
|
||||
expire();
|
||||
const result = await invocation;
|
||||
expect(result.status).toBe('unavailable');
|
||||
expect(result.error?.code).toBe('timeout');
|
||||
expect(Date.now() - start).toBeLessThan(2000);
|
||||
expect(['SIGINT','SIGTERM','exit'].map(name => process.listenerCount(name))).toEqual(before);
|
||||
await expectDescendantDead();
|
||||
} finally { cleanupDescendant(); }
|
||||
} finally {
|
||||
fireTimeout?.();
|
||||
await invocation;
|
||||
cleanupDescendant();
|
||||
}
|
||||
});
|
||||
|
||||
test('the real deadline bounds startup before descendant readiness', async () => {
|
||||
rmSync(PID, { force: true });
|
||||
const before = ['SIGINT','SIGTERM','exit'].map(name => process.listenerCount(name));
|
||||
const start = Date.now();
|
||||
const result = await run('startup-timeout', {timeoutMs:500});
|
||||
expect(result.status).toBe('unavailable');
|
||||
expect(result.error?.code).toBe('timeout');
|
||||
expect(Date.now() - start).toBeLessThan(2000);
|
||||
expect(['SIGINT','SIGTERM','exit'].map(name => process.listenerCount(name))).toEqual(before);
|
||||
expect(() => readFileSync(PID)).toThrow();
|
||||
});
|
||||
|
||||
test('a child exiting with inherited pipes is unavailable within the drain deadline', async () => {
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { runInNewContext } from 'node:vm';
|
||||
import { generateCompareHtml } from '../design/src/compare';
|
||||
|
||||
const root = path.resolve(import.meta.dir, '..');
|
||||
const quote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
|
||||
|
||||
function fixture() {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), 'design-consult-contract-'));
|
||||
const repo = path.join(dir, 'repo');
|
||||
const bin = path.join(dir, 'bin');
|
||||
const scratch = path.join(dir, 'scratch');
|
||||
for (const sub of [repo, bin, scratch]) mkdirSync(sub);
|
||||
const calls = path.join(dir, 'calls');
|
||||
const preload = path.join(dir, 'offline.ts');
|
||||
writeFileSync(preload, `import { appendFileSync } from 'node:fs';
|
||||
globalThis.fetch = async (url, init) => {
|
||||
const pathname = new URL(String(url)).pathname;
|
||||
if (!['/v1/responses', '/v1/chat/completions'].includes(pathname)) throw new Error('Unexpected request');
|
||||
appendFileSync(process.env.CALLS!, pathname + '\\n');
|
||||
if (process.env.MODE === 'unavailable') return new Response('unavailable', { status: 503 });
|
||||
if (pathname === '/v1/responses') return Response.json({ id: 'fixture-response', output: [{ type: 'image_generation_call', result: 'aW1hZ2U=' }] });
|
||||
const content = process.env.MODE === 'check' ? 'FAIL: illegible title' : JSON.stringify({ colors: [{ name: 'accent', hex: '#123456', usage: 'action' }], typography: [], spacing: ['8px'], layout: ['columns'], mood: 'calm' });
|
||||
return Response.json({ choices: [{ message: { content } }] });
|
||||
};
|
||||
`);
|
||||
const image = path.join(repo, 'approved.png');
|
||||
writeFileSync(image, 'fixture image');
|
||||
const design = path.join(bin, 'design');
|
||||
writeFileSync(design, `#!/bin/sh\nexec ${quote(process.execPath)} --no-env-file --preload ${quote(preload)} ${quote(path.join(root, 'design/src/cli.ts'))} "$@"\n`, { mode: 0o700 });
|
||||
writeFileSync(path.join(bin, 'git'), `#!/bin/sh
|
||||
if [ "$GIT_CLAIM_ALL" = 1 ] || [ "$PWD" = "$FIXTURE_REPO" ]; then printf '%s\\n' "$FIXTURE_REPO"; else exit 128; fi
|
||||
`, { mode: 0o700 });
|
||||
writeFileSync(path.join(bin, 'mktemp'), '#!/bin/sh\nprintf "%s\\n" "$FIXTURE_SCRATCH"\n', { mode: 0o700 });
|
||||
const env = {
|
||||
PATH: `${bin}${path.delimiter}${process.env.PATH}`, HOME: dir, GSTACK_HOME: path.join(dir, 'state'),
|
||||
OPENAI_API_KEY: 'fixture-not-a-real-key', CALLS: calls, FIXTURE_REPO: repo, FIXTURE_SCRATCH: scratch,
|
||||
D: design, APPROVED_IMAGE: image,
|
||||
};
|
||||
const sessions: string[] = [];
|
||||
const run = (args: string[], extra: Record<string, string> = {}) => spawnSync(design, args, {
|
||||
cwd: repo, env: { ...env, ...extra }, encoding: 'utf8', timeout: 15_000,
|
||||
});
|
||||
return { dir, repo, env, image, calls, run, sessions, cleanup: () => {
|
||||
for (const session of sessions) rmSync(session, { force: true });
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
} };
|
||||
}
|
||||
|
||||
test('actual CLI variants have no session; generation supplies the session required by iteration', () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const variants = f.run(['variants', '--brief', 'Readable civic dashboard', '--count', '1', '--output-dir', f.dir]);
|
||||
expect(variants.status, variants.stderr).toBe(0);
|
||||
const variantResult = JSON.parse(variants.stdout);
|
||||
expect(variantResult.paths).toEqual([path.join(f.dir, 'variant-A.png')]);
|
||||
expect(variantResult).not.toHaveProperty('sessionFile');
|
||||
const callCount = readFileSync(f.calls, 'utf8').split('\n').length;
|
||||
const missing = f.run(['iterate', '--feedback', 'Larger title', '--output', path.join(f.dir, 'missing.png')]);
|
||||
expect(missing.status).not.toBe(0);
|
||||
expect(readFileSync(f.calls, 'utf8').split('\n')).toHaveLength(callCount);
|
||||
const generated = f.run(['generate', '--brief', 'Readable civic dashboard', '--output', f.image]);
|
||||
expect(generated.status, generated.stderr).toBe(0);
|
||||
const { sessionFile } = JSON.parse(generated.stdout);
|
||||
f.sessions.push(sessionFile);
|
||||
expect(existsSync(sessionFile)).toBe(true);
|
||||
const iterated = f.run(['iterate', '--session', sessionFile, '--feedback', 'Larger title', '--output', path.join(f.dir, 'refined.png')]);
|
||||
expect(iterated.status, iterated.stderr).toBe(0);
|
||||
expect(JSON.parse(iterated.stdout).sessionFile).toBe(sessionFile);
|
||||
expect(JSON.parse(readFileSync(sessionFile, 'utf8')).feedbackHistory).toEqual(['Larger title']);
|
||||
} finally { f.cleanup(); }
|
||||
});
|
||||
|
||||
test('actual CLI quality check distinguishes failure from skipped coverage despite exit zero', () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const failed = f.run(['check', '--image', f.image, '--brief', 'Readable title'], { MODE: 'check' });
|
||||
expect(failed.status, failed.stderr).toBe(0);
|
||||
expect(JSON.parse(failed.stdout)).toEqual({ pass: false, issues: 'illegible title' });
|
||||
const unavailable = f.run(['check', '--image', f.image, '--brief', 'Readable title'], { MODE: 'unavailable' });
|
||||
expect(unavailable.status, unavailable.stderr).toBe(0);
|
||||
expect(JSON.parse(unavailable.stdout)).toEqual({ pass: true, issues: 'Vision check unavailable — skipped' });
|
||||
} finally { f.cleanup(); }
|
||||
});
|
||||
|
||||
test('the extraction recipe prevents the actual CLI automatic DESIGN.md write and refuses a Git-bound scratch directory', () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const projectDesign = path.join(f.repo, 'DESIGN.md');
|
||||
const original = '# Existing design\n\nKeep this decision.\n';
|
||||
writeFileSync(projectDesign, original);
|
||||
const direct = f.run(['extract', '--image', f.image]);
|
||||
expect(direct.status, direct.stderr).toBe(0);
|
||||
expect(readFileSync(projectDesign, 'utf8')).toContain('## Extracted Design Language');
|
||||
writeFileSync(projectDesign, original);
|
||||
const section = readFileSync(path.join(root, 'design-consultation/sections/proposal-and-preview.md.tmpl'), 'utf8');
|
||||
const recipe = [...section.matchAll(/```bash\n([\s\S]*?)```/g)].find(match => match[1].includes('_EXTRACT_DIR='))?.[1];
|
||||
expect(recipe).toBeDefined();
|
||||
const isolated = spawnSync('bash', ['-c', recipe!], { cwd: f.repo, env: f.env, encoding: 'utf8', timeout: 15_000 });
|
||||
expect(isolated.status, isolated.stderr).toBe(0);
|
||||
expect(JSON.parse(isolated.stdout).colors[0].hex).toBe('#123456');
|
||||
expect(readFileSync(projectDesign, 'utf8')).toBe(original);
|
||||
const calls = readFileSync(f.calls, 'utf8');
|
||||
const refused = spawnSync('bash', ['-c', recipe!], {
|
||||
cwd: f.repo, env: { ...f.env, GIT_CLAIM_ALL: '1' }, encoding: 'utf8', timeout: 15_000,
|
||||
});
|
||||
expect(refused.status).not.toBe(0);
|
||||
expect(refused.stderr).toContain('Extraction refused');
|
||||
expect(readFileSync(f.calls, 'utf8')).toBe(calls);
|
||||
expect(readFileSync(projectDesign, 'utf8')).toBe(original);
|
||||
} finally { f.cleanup(); }
|
||||
});
|
||||
|
||||
test('the actual comparison board callback emits regenerateAction without requiring remixSpec', () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const html = generateCompareHtml([f.image]);
|
||||
const callback = html.match(/ function submitRegenerate\(detail\) \{[\s\S]*?\n \}/)?.[0];
|
||||
expect(callback).toBeDefined();
|
||||
const sent: unknown[] = [];
|
||||
const elements = { 'feedback-result': { textContent: '' }, status: { textContent: '' } };
|
||||
const invoke = runInNewContext(`${callback}\nsubmitRegenerate`, {
|
||||
document: { getElementById: (id: keyof typeof elements) => elements[id] },
|
||||
collectFeedback: () => ({ preferred: 'A', ratings: { A: 4 }, comments: {}, overall: null }),
|
||||
postFeedback: (value: unknown) => { sent.push(value); return { then: () => {} }; },
|
||||
});
|
||||
for (const action of ['different', 'match', 'more_like_A', "A's layout with B's colors"]) {
|
||||
invoke(action);
|
||||
const result = JSON.parse(elements['feedback-result'].textContent);
|
||||
expect(result).toEqual({ preferred: 'A', ratings: { A: 4 }, comments: {}, overall: null, regenerated: true, regenerateAction: action });
|
||||
expect(sent.at(-1)).toEqual(result);
|
||||
expect(result).not.toHaveProperty('remixSpec');
|
||||
expect(elements.status.textContent).toBe('regenerate');
|
||||
}
|
||||
} finally { f.cleanup(); }
|
||||
});
|
||||
@@ -2,7 +2,9 @@ import { expect, test } from 'bun:test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { ALL_HOST_CONFIGS } from '../hosts';
|
||||
import { HOST_PATHS, type TemplateContext } from '../scripts/resolvers/types';
|
||||
import { generateDesignOutsideVoices, generateOverusedFonts, generateDesignShotgunLoop, generateTasteProfile } from '../scripts/resolvers/design';
|
||||
import { generateDesignOutsideVoices, generateOverusedFonts, generateDesignShotgunLoop, generateTasteProfile, generateDesignMdCheck, generateDesignSetup } from '../scripts/resolvers/design';
|
||||
import { generateBrowseFallback } from '../scripts/resolvers/browse';
|
||||
import { generateAsideSetup, generateAsideResearch } from '../scripts/resolvers/aside';
|
||||
import { outsideVoiceInvocation } from '../scripts/resolvers/outside-voice';
|
||||
import { validateOutsideReview } from '../lib/outside-review-result';
|
||||
|
||||
@@ -61,15 +63,97 @@ test('preview paths retain verified fonts and select their own token source', ()
|
||||
test('consultation drafts before independent dispatch and compares completed input at Q2', () => {
|
||||
const root = readFileSync(new URL('../design-consultation/SKILL.md.tmpl', import.meta.url), 'utf8');
|
||||
const section = readFileSync(new URL('../design-consultation/sections/proposal-and-preview.md.tmpl', import.meta.url), 'utf8');
|
||||
expect(root.indexOf('Draft your own direction')).toBeLessThan(root.indexOf('{{DESIGN_OUTSIDE_VOICES}}'));
|
||||
expect(root.indexOf('{{DESIGN_OUTSIDE_VOICES}}')).toBeLessThan(root.indexOf('{{SECTION:proposal-and-preview}}'));
|
||||
expect(root).toContain("Keep that draft out of both reviewers' prompts");
|
||||
expect(root).not.toContain('{{DESIGN_OUTSIDE_VOICES}}');
|
||||
expect(root).not.toContain('Draft your own direction');
|
||||
expect(root.indexOf('{{SECTION:proposal-and-preview}}')).toBeGreaterThan(root.indexOf('## Phase 2: Research'));
|
||||
const ordered = ['### Your Design Knowledge', '**Choosing faces:', '{{OVERUSED_FONTS}}', '{{DESIGN_SLOP_BULLETS}}', 'Draft your own direction', '{{DESIGN_OUTSIDE_VOICES}}', '**AskUserQuestion Q2'];
|
||||
for (let i = 0; i < ordered.length; i++) {
|
||||
expect(section.indexOf(ordered[i])).toBeGreaterThan(i === 0 ? -1 : section.indexOf(ordered[i - 1]));
|
||||
}
|
||||
expect(section).toContain("Keep that draft out of both reviewers' prompts");
|
||||
expect(root).toContain('The optional outside-voices choice below still applies');
|
||||
const question = section.slice(section.indexOf('**AskUserQuestion Q2'), section.indexOf('### Your Design Knowledge'));
|
||||
const question = section.slice(section.indexOf('**AskUserQuestion Q2'), section.indexOf('## Phase 4'));
|
||||
expect(question).toContain('completed/unavailable/skipped voices');
|
||||
expect(question).toContain('agreements, differences, ideas adopted and product-specific reasons');
|
||||
expect(question).toContain('omit comparisons if none completed');
|
||||
expect(section).toContain('Do not count agreement as a vote or invent a missing proposal');
|
||||
expect(section).toContain('Verify any newly suggested fonts before adopting them');
|
||||
expect(section).toContain('label old proposals stale');
|
||||
});
|
||||
|
||||
test('optional browser research has one unavailable branch and reuses its readiness probe', () => {
|
||||
const ctx = context('claude');
|
||||
const fallback = generateBrowseFallback(ctx);
|
||||
expect(fallback).toContain('Do not offer or run a build');
|
||||
expect(fallback).toContain('skip Phase 2 Step 2; Step 1 still uses WebSearch');
|
||||
expect(fallback).not.toContain('OK to proceed?');
|
||||
expect(generateBrowseFallback(context('claude', 'qa'))).toContain('OK to proceed?');
|
||||
const research = generateAsideResearch(ctx);
|
||||
expect(research).toContain('Reuse the Phase 0 BROWSER SETUP result');
|
||||
expect((generateAsideSetup(ctx) + research).match(/console\.log\("ASIDE_READY /g)).toHaveLength(1);
|
||||
expect(research.toLowerCase()).toContain('read-only: do not sign in, submit, or change anything');
|
||||
expect(research).toContain('Sanitize every query before it leaves the machine');
|
||||
});
|
||||
|
||||
test('existing-system choices reach their matching final format without early writes', () => {
|
||||
const root = readFileSync(new URL('../design-consultation/SKILL.md.tmpl', import.meta.url), 'utf8');
|
||||
const section = readFileSync(new URL('../design-consultation/sections/proposal-and-preview.md.tmpl', import.meta.url), 'utf8');
|
||||
expect(root).toContain('**Cancel:** STOP the skill now, with no file changes or further probes');
|
||||
expect(root).toContain('**Update:** carry the existing decisions into Q1 as constraints');
|
||||
expect(root).toContain('**Start fresh:** set aside prior visual choices');
|
||||
expect(root).toContain('All conversion, marker and design writes wait for Q-final');
|
||||
const format = generateDesignMdCheck(context('claude'));
|
||||
expect(format).toContain('convert`, without `--write`');
|
||||
expect(format).toContain('After Q-final approval outside plan mode');
|
||||
expect(format).toContain('In plan mode, record the chosen format in Proposed DESIGN.md instead');
|
||||
expect(section).toContain('Never convert a kept file just to make validation say spec');
|
||||
expect(section).toContain('Any subsequent token, font or direction change invalidates that approval');
|
||||
expect(section).toContain('E) Skip the preview — proceed to Phase 6\'s Q-final, not straight to writing');
|
||||
});
|
||||
|
||||
test.each(ALL_HOST_CONFIGS.map(({ name }) => name))('%s: only Update with DESIGN.md enters the entire format-check block', host => {
|
||||
const root = readFileSync(new URL('../design-consultation/SKILL.md.tmpl', import.meta.url), 'utf8');
|
||||
const format = generateDesignMdCheck(context(host));
|
||||
const gate = format.indexOf('**Update-only gate:**');
|
||||
const command = format.indexOf('```bash');
|
||||
const end = format.indexOf('**End of Update-only format check.**');
|
||||
expect(gate).toBeGreaterThan(-1);
|
||||
expect(command).toBeGreaterThan(gate);
|
||||
expect(end).toBeGreaterThan(format.indexOf('**A) Convert**'));
|
||||
expect(format.slice(gate, command)).toContain('Only **Update** with DESIGN.md enters this block (command and all result branches)');
|
||||
expect(format.slice(gate, command)).toContain('**Start fresh**, **No existing file**, or a lone design-system.md: skip to **Gather product context from the codebase**');
|
||||
expect(format.slice(gate, command)).toContain('**Cancel** has already stopped the skill');
|
||||
expect(root.indexOf('**Gather product context from the codebase:**')).toBeGreaterThan(root.indexOf('{{DESIGN_MD_CHECK}}'));
|
||||
expect(root).toContain('**Cancel:** STOP the skill now, with no file changes or further probes');
|
||||
expect(generateDesignMdCheck(context(host, 'design-review'), ['calibrate'])).not.toContain('Update-only');
|
||||
});
|
||||
|
||||
test('design command guidance carries session, extraction and quality-check side effects', () => {
|
||||
const setup = generateDesignSetup(context('claude'));
|
||||
const section = readFileSync(new URL('../design-consultation/sections/proposal-and-preview.md.tmpl', import.meta.url), 'utf8');
|
||||
expect(setup).toContain('$D extract --image /absolute/path.png');
|
||||
expect(setup).toContain('automatically update DESIGN.md');
|
||||
expect(setup).toContain('`variants` returns `paths` but creates no session');
|
||||
expect(section).toContain('`pass: false` means regenerate');
|
||||
expect(section).toContain('`pass: true` with an unavailable/skipped warning is missing automated coverage');
|
||||
expect(section).toContain('run it only in a fresh non-repository scratch directory');
|
||||
expect(section).toContain('Empty arrays, an "Unable to extract" mood or command failure');
|
||||
for (const command of (section + generateDesignShotgunLoop(context('claude'))).matchAll(/\$D iterate[^`\n]+/g)) {
|
||||
expect(command[0]).toContain('--session');
|
||||
}
|
||||
});
|
||||
|
||||
test('board feedback distinguishes sessionless regeneration, final choice and missing input', () => {
|
||||
const loop = generateDesignShotgunLoop(context('claude'));
|
||||
const examples = [...loop.matchAll(/```json\n([\s\S]*?)```/g)].map(match => JSON.parse(match[1]));
|
||||
expect(examples.find(value => value.regenerated === false)).toMatchObject({ preferred: 'A' });
|
||||
expect(examples.find(value => value.regenerated === true)).toMatchObject({ regenerateAction: 'more_like_B' });
|
||||
expect(loop).toContain('it does not emit a required `remixSpec`');
|
||||
expect(loop).toContain('Archive this round\'s feedback files');
|
||||
expect(loop).toContain('revisions regenerate; a final choice needs summary confirmation');
|
||||
expect(loop).toContain('never infer approval from a missing file');
|
||||
expect(loop).toContain('publishes to a persistent daemon, opens the board and exits');
|
||||
expect(loop).toContain('Re-run the quality check and visual self-gate on every new image');
|
||||
});
|
||||
|
||||
test('taste context has defined count and bounded legacy and malformed-profile fallbacks', () => {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { extractDesignResearchContract } from './helpers/skill-fixture';
|
||||
import { E2E_TOUCHFILES, selectTests } from './helpers/touchfiles';
|
||||
|
||||
const source = readFileSync(new URL('../design-consultation/SKILL.md', import.meta.url), 'utf8');
|
||||
|
||||
test('research-only fixture supplies actual readiness and egress dependencies without expanding scope', () => {
|
||||
const contract = extractDesignResearchContract(source);
|
||||
expect(contract.match(/console\.log\("ASIDE_READY /g)).toHaveLength(1);
|
||||
expect(contract).toContain('GSTACK_SKIP_ASIDE');
|
||||
expect(contract).toContain('Reuse the Phase 0 BROWSER SETUP result');
|
||||
expect(contract).toContain('_gstack_egress_run open aside-agent');
|
||||
expect(contract.indexOf('ASIDE_READY')).toBeLessThan(contract.indexOf('## Web research runs in Aside'));
|
||||
expect(contract).not.toContain('## Phase 2: Research');
|
||||
expect(contract).not.toContain('**Step 2: Visual research');
|
||||
expect(contract).not.toContain('best websites {current year}');
|
||||
expect(contract).not.toContain('DESIGN.md');
|
||||
expect(extractDesignResearchContract(source.replace('ASIDE_READY', 'PROBE_CHANGED'))).toContain('PROBE_CHANGED');
|
||||
});
|
||||
|
||||
test.each(['## BROWSER SETUP', '### Rules for driving a real browser', '## Web research runs in Aside',
|
||||
'## Phase 2: Research', '**Step 1: Identify', '**Step 2: Visual research', '_aside_exec()'])('missing %s fails closed before a paid run', marker => {
|
||||
expect(() => extractDesignResearchContract(source.replace(marker, 'REMOVED'))).toThrow();
|
||||
});
|
||||
|
||||
test('research fixture changes select their actual live consumer', () => {
|
||||
for (const file of ['test/helpers/skill-fixture.ts', 'test/design-research-fixture.test.ts']) {
|
||||
expect(selectTests([file], E2E_TOUCHFILES, []).selected).toContain('design-consultation-research');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,211 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { ALL_HOST_CONFIGS } from '../hosts';
|
||||
import { generateBrainWriteBack } from '../scripts/resolvers/gbrain';
|
||||
import { generateAskUserFormat } from '../scripts/resolvers/preamble/generate-ask-user-format';
|
||||
import { generateTestCoverageAuditPlan, generateTestCoverageAuditShip } from '../scripts/resolvers/testing';
|
||||
import { HOST_PATHS, type TemplateContext } from '../scripts/resolvers/types';
|
||||
import { ENG_REVIEW_EXCERPT } from './helpers/workflow-excerpt';
|
||||
|
||||
const entry = readFileSync('plan-eng-review/SKILL.md.tmpl', 'utf8');
|
||||
const section = readFileSync('plan-eng-review/sections/review-sections.md.tmpl', 'utf8');
|
||||
const compact = (text: string) => text.replace(/\s+/g, ' ');
|
||||
|
||||
function between(text: string, start: string, end: string): string {
|
||||
const from = text.indexOf(start);
|
||||
const to = text.indexOf(end, from + start.length);
|
||||
expect(from).toBeGreaterThanOrEqual(0);
|
||||
expect(to).toBeGreaterThan(from);
|
||||
return text.slice(from, to);
|
||||
}
|
||||
|
||||
function ordered(text: string, stages: string[]) {
|
||||
const positions = stages.map(stage => text.indexOf(stage));
|
||||
expect(positions.every(position => position >= 0)).toBe(true);
|
||||
expect(positions).toEqual([...positions].sort((a, b) => a - b));
|
||||
}
|
||||
|
||||
const scope = section.split('## Scope Challenge\n')[1]?.split('## Review Sections')[0] ?? '';
|
||||
const assessment = scope.split('### A. Assess the target')[1]?.split('### B. Resolve complexity selectors')[0] ?? '';
|
||||
const complexity = scope.split('### B. Resolve complexity selectors')[1]?.split('### C. Resolve findings')[0] ?? '';
|
||||
const findings = scope.split('### C. Resolve findings')[1] ?? '';
|
||||
const recovery = entry.split('## Recovery routing')[1]?.split('{{EXIT_PLAN_MODE_GATE}}')[0] ?? '';
|
||||
|
||||
describe('engineering review routing contracts', () => {
|
||||
test('the bounded engineering excerpt includes the recovery rules it references', () => {
|
||||
const excerpt = between(entry, ENG_REVIEW_EXCERPT.startMarker, ENG_REVIEW_EXCERPT.endMarker);
|
||||
ordered(excerpt, ['{{SECTION:review-sections}}', '## Recovery routing', '**Paused question:**',
|
||||
'**Repairable write/read failure:**', '**Late change or missing work:**', '**Blocked outcome:**']);
|
||||
expect(compact(entry.slice(entry.indexOf(ENG_REVIEW_EXCERPT.endMarker)))).toContain('use Recovery routing above');
|
||||
});
|
||||
|
||||
test('preparation establishes permission and evidence before applying review rules', () => {
|
||||
const preparation = between(section, '## Review preparation', '## Review record and write policy');
|
||||
ordered(preparation, ['1. Select the report file and permissions under **Review record and write policy**', '2. Run **Prior Learnings**',
|
||||
'3. Run **Retrospective learning**', '4. Read **Confidence Calibration**', '**Decision procedure**',
|
||||
'**Scope Challenge A → B → C**', 'Sections 1–4 in order']);
|
||||
expect(compact(preparation)).toContain('Run **Prior Learnings** and resolve its configuration question');
|
||||
expect(compact(preparation)).toContain('as rules, not review passes');
|
||||
expect(entry).toContain('Keep the reviewed target fixed');
|
||||
});
|
||||
|
||||
test('compression cannot remove mandatory review stages or decision content', () => {
|
||||
const priority = between(entry, '## Priority hierarchy', '## My engineering preferences');
|
||||
expect(compact(priority)).toContain('Complete every required stage, decision gate and output');
|
||||
expect(compact(priority)).toContain('Shorten only optional commentary');
|
||||
expect(compact(priority)).toContain('never Scope Challenge, Sections 1–4, the test diagram or required decision/report content');
|
||||
expect(priority).not.toContain('Everything else');
|
||||
expect(compact(section)).toContain('Never condense, abbreviate or skip a section');
|
||||
});
|
||||
|
||||
test('Scope Challenge has one named route and completes all seven assessments first', () => {
|
||||
expect([...scope.matchAll(/^### (.+)$/gm)].map(match => match[1])).toEqual([
|
||||
'A. Assess the target', 'B. Resolve complexity selectors', 'C. Resolve findings',
|
||||
]);
|
||||
expect([...assessment.matchAll(/^- \*\*([^*]+)\*\*/gm)].map(match => match[1])).toEqual([
|
||||
'What already solves each sub-problem?', 'What minimum changes achieve the goal?',
|
||||
'Complexity check:', 'Search check:', 'TODOS cross-reference:', 'Completeness check:', 'Distribution check:',
|
||||
]);
|
||||
expect(compact(assessment)).toContain('Complete these checks before the complexity decision in B');
|
||||
expect(compact(assessment)).toContain('Do not apply scope changes or write findings into the plan yet');
|
||||
expect(scope).not.toContain('Below the threshold, start at step 1');
|
||||
});
|
||||
|
||||
test('below-threshold route skips selectors, never findings or remedy approvals', () => {
|
||||
expect(compact(complexity)).toContain("Below both thresholds, skip B's questions and go directly to **C. Resolve findings**");
|
||||
expect(findings).toContain('Run C whether B was completed or skipped');
|
||||
ordered(compact(findings), ['1. Present numbered Scope Challenge findings',
|
||||
'2. Resolve each remedy through Decision procedure',
|
||||
'3. Report accepted/rejected/deferred/pending dispositions',
|
||||
'Continue to Section 1 only when no answer is pending']);
|
||||
expect(findings).toContain('Findings and scope answers approve no remedies');
|
||||
expect(findings).toContain('"No issues found" for an empty list');
|
||||
});
|
||||
|
||||
test('high complexity retains separate cuts and mandatory arrangement choice', () => {
|
||||
expect(compact(complexity)).toContain('At 8+ files or 2+ new classes/services, STOP before Section 1');
|
||||
ordered(compact(complexity), ['Initial scope selectors need no grid or **pre-answer** ledger write',
|
||||
'1. Explain the complexity', 'Ask each proposed feature cut/deferral separately',
|
||||
'2. Always ask the structure question when this gate trips, even with no cuts',
|
||||
'3. Save the actual feature and structure answers as one scope record']);
|
||||
expect(compact(complexity)).toContain('With no proposed cuts, keep the feature list and go directly to the structure question');
|
||||
expect(compact(complexity)).toContain('Both retain the same approved feature list, contracts and approved security/error/test/performance fixes');
|
||||
expect(compact(complexity)).toContain('Pending remedies not decided here: <ids>');
|
||||
});
|
||||
|
||||
test('no safe smaller arrangement does not authorize scope cuts or bypass the pause', () => {
|
||||
ordered(compact(complexity), ['If no smaller arrangement preserves these commitments',
|
||||
'offer confirmation of the original arrangement or a pause to investigate a smaller one',
|
||||
'A pause leaves the arrangement undecided', 'then return to this structure selector',
|
||||
'Do not continue to C until it is settled']);
|
||||
expect(compact(complexity)).toContain('investigate only the agreed question');
|
||||
});
|
||||
|
||||
test('selector answers are verified after answering without invented pending records', () => {
|
||||
const summary = compact(complexity.slice(complexity.indexOf('3. Save the actual')));
|
||||
ordered(summary, ['feature answers: <refs>; structure: <A/B + ref>; accepted scope: <exact scope>; pending remedies: <ids or none>',
|
||||
'post-answer scope summary, not a remedy\'s pending ledger record', 'Read it back against the actual answers',
|
||||
'After verification, apply only accepted scope changes', 'Continue to **C. Resolve findings**']);
|
||||
expect(summary).toContain('Do not invent a pre-answer record afterward');
|
||||
expect(summary).toContain('A failed save or Read blocks advancement');
|
||||
expect(summary).toContain('on the permitted read-only route, present and verify it as **not persisted**');
|
||||
expect(compact(section)).toContain('Scope Challenge B saves actual selector answers afterward; it does not use this remedy loop');
|
||||
});
|
||||
|
||||
test('engineering remedies still require full save Read ask answer apply Read ordering', () => {
|
||||
const procedure = between(section, '## Decision procedure', '## Scope Challenge');
|
||||
ordered(procedure, ['### 3. Compare one choice', '### 4. Save the pending record',
|
||||
'use Read to fetch the entire saved record', '### 5. Ask and wait',
|
||||
'AskUserQuestion({ questions: [currentDecision] })', '**STOP until the actual answer arrives.**',
|
||||
'### 6. Apply and refresh', 'Read the entire resolution block, including State',
|
||||
'Return to step 1 with the updated working plan and answer']);
|
||||
expect(compact(procedure)).toContain('An Investigate/Defer option must bound the investigation');
|
||||
expect(compact(procedure)).toContain('It approves no implementation, including a conditional fix');
|
||||
expect(compact(procedure)).toContain('Do not apply a remedy, make another call, start the next section or call ExitPlanMode while the choice awaits an answer');
|
||||
expect(compact(procedure)).toContain("Apply the preamble's Completeness scores or kind-note accordingly");
|
||||
const questions = generateAskUserFormat({ skillName: 'plan-eng-review', host: 'claude', paths: HOST_PATHS.claude } as TemplateContext);
|
||||
expect(questions).toContain('10 = complete, 7 = happy path, 3 = shortcut');
|
||||
expect(questions).toContain('Note: options differ in kind, not coverage — no completeness score.');
|
||||
});
|
||||
|
||||
test('unavailable research preserves an explicit coverage limit and continues review', () => {
|
||||
expect(compact(assessment)).toContain('If Aside is unavailable, use host WebSearch for these queries');
|
||||
expect(compact(assessment)).toContain('With neither, skip and note: "Search unavailable — proceeding with in-distribution knowledge only."');
|
||||
const outside = between(section, '### Continue after Outside Voice', '### TODOS.md updates');
|
||||
expect(compact(outside)).toContain('Only completed reviews enter Cross-model tension');
|
||||
expect(compact(outside)).toContain('Record the actual coverage, including disabled or unavailable outcomes');
|
||||
expect(section).toContain('Outside voice: recorded provider, completed / unavailable / disabled / skipped (reason)');
|
||||
expect(compact(outside)).toContain('Resolve the TODO choices, then check Approval readiness before Required outputs');
|
||||
});
|
||||
|
||||
test('paused transport and failed persistence have distinct non-success outcomes', () => {
|
||||
const pause = between(recovery, '**Paused question:**', '**Repairable write/read failure:**');
|
||||
expect(pause).toContain('without completion telemetry or ExitPlanMode');
|
||||
expect(compact(pause)).toContain('may have surfaced is still pending; do not duplicate it');
|
||||
const failure = between(recovery, '**Repairable write/read failure:**', '**Late change or missing work:**');
|
||||
expect(compact(failure)).toContain('Stop before the dependent question or output');
|
||||
expect(compact(failure)).toContain('If no recovery is specified or it fails, follow **Blocked outcome**');
|
||||
expect(compact(failure)).toContain('Never turn a failed permitted save into a chat-only success');
|
||||
const policy = between(section, '## Review record and write policy', '{{LEARNINGS_SEARCH}}');
|
||||
expect(compact(policy)).toContain('not the forbidden-write branches above');
|
||||
expect(compact(policy)).toContain('Best-effort logs retain their stated non-blocking behavior');
|
||||
expect(recovery).toContain('`OUTCOME=error`');
|
||||
expect(recovery).not.toContain('`OUTCOME=success`');
|
||||
});
|
||||
|
||||
test('late changes rerun affected approvals and outputs before another navigation answer', () => {
|
||||
const late = compact(between(recovery, '**Late change or missing work:**', '**Blocked outcome:**'));
|
||||
ordered(late, ['Return to the affected review stage', 'new or reopened choices use Decision procedure',
|
||||
'Repeat Approval readiness', 'Required outputs steps 1–4', 'before choosing navigation again']);
|
||||
expect(late).toContain('Refresh affected tests, tasks, dependencies and parallelization');
|
||||
expect(late).toContain('Unchanged saved outputs may reuse their successful Review Log');
|
||||
expect(late).toContain('If a final gate discovers stale evidence, follow **Blocked outcome** first');
|
||||
const finish = between(section, '## Required outputs', '### Output reference');
|
||||
expect(compact(finish)).toContain('A substantive change follows **Recovery routing → Late change or missing work** before navigation resumes');
|
||||
expect(compact(finish)).toContain('Navigation grants no implementation authority');
|
||||
ordered(finish, ['1. **Prepare the review body.**', '2. **Save and Read back.**',
|
||||
'3. **Log the saved review.**', '4. **Publish.**', '5. **Choose navigation.**', '6. **Finish.**']);
|
||||
});
|
||||
|
||||
test('plan test diagrams cover proposed paths without inventing existing implementation', () => {
|
||||
for (const host of ALL_HOST_CONFIGS) {
|
||||
const ctx = { skillName: 'plan-eng-review', host: host.name, paths: HOST_PATHS[host.name] } as TemplateContext;
|
||||
const audit = generateTestCoverageAuditPlan(ctx);
|
||||
expect(audit).toContain('For each existing or proposed component in the selected target');
|
||||
expect(audit).toContain('Every existing or proposed function/method in scope');
|
||||
expect(audit).toContain('the selected target. For each existing or proposed feature');
|
||||
expect(audit).toContain('Future paths remain proposals, not runnable code');
|
||||
for (const obligation of ['Every conditional branch', 'Every error path', 'Every call to another function',
|
||||
'Every edge:', 'dedicated tool call before drawing the diagram', 'No skipping regression coverage']) {
|
||||
expect(audit).toContain(obligation);
|
||||
}
|
||||
const ship = generateTestCoverageAuditShip({ ...ctx, skillName: 'ship' });
|
||||
expect(ship).toContain('For each changed file, draw an ASCII diagram showing:');
|
||||
expect(ship).toContain('Every function/method that was added or modified');
|
||||
expect(ship).toContain('the changed code. For each changed feature');
|
||||
expect(ship).not.toContain('existing or proposed');
|
||||
}
|
||||
});
|
||||
|
||||
test('reserved calibration gate is explicitly skipped without enabling a write path', () => {
|
||||
for (const host of ALL_HOST_CONFIGS) {
|
||||
const ctx = { skillName: 'plan-eng-review', host: host.name, paths: HOST_PATHS[host.name] } as TemplateContext;
|
||||
const output = generateBrainWriteBack(ctx);
|
||||
ordered(output, ['reserved default-off gate', 'this runtime does not set it',
|
||||
'Skip this section and continue the finish sequence', 'Do not enable it or infer permission from brain availability',
|
||||
'not an instruction to write now', 'Skip unless']);
|
||||
expect(output).toContain('brain_trust_policy@<endpoint-hash>=personal');
|
||||
expect(output).toContain('If unknown, skip');
|
||||
expect(output).toContain('mcp__gbrain__takes_add');
|
||||
expect(output).toContain('mcp__gbrain__put_page');
|
||||
expect(output).toContain('source_skill: plan-eng-review');
|
||||
expect(output).not.toContain('${BRAIN_CALIBRATION_WRITEBACK');
|
||||
for (const skillName of ['office-hours', 'plan-ceo-review', 'plan-design-review', 'plan-devex-review']) {
|
||||
const other = generateBrainWriteBack({ ...ctx, skillName });
|
||||
expect(other).toStartWith('## Brain Calibration Write-Back (gated)\n\nSkip unless');
|
||||
expect(other).not.toContain('reserved default-off gate');
|
||||
expect(other).not.toContain('Skip this section and continue the finish sequence');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
+19
-98
@@ -295,31 +295,6 @@ For high-stakes ambiguity (architecture, data model, destructive scope, missing
|
||||
|
||||
A claimed limitation or requirement ("the API can't do this", "X requires a credential", "that's impossible on this platform") is a material claim. State one only with the verbatim error, the documented statement, or a live probe in hand — pattern-matching a failure to a familiar story is not evidence. When a cheap probe settles the question, run it BEFORE asking the user anything or declaring a step blocked.
|
||||
|
||||
## Continuous Checkpoint Mode
|
||||
|
||||
If `CHECKPOINT_MODE` is `"continuous"`: auto-commit completed logical units with `WIP:` prefix.
|
||||
|
||||
Commit after new intentional files, completed functions/modules, verified bug fixes, and before long-running install/build/test commands.
|
||||
|
||||
Commit format:
|
||||
|
||||
```
|
||||
WIP: <concise description of what changed>
|
||||
|
||||
[gstack-context]
|
||||
Decisions: <key choices made this step>
|
||||
Remaining: <what's left in the logical unit>
|
||||
Tried: <failed approaches worth recording> (omit if none)
|
||||
Skill: </skill-name-if-running>
|
||||
[/gstack-context]
|
||||
```
|
||||
|
||||
Rules: stage only intentional files, NEVER `git add -A`, do not commit broken tests or mid-edit state, and push only if `CHECKPOINT_PUSH` is `"true"`. Do not announce each WIP commit.
|
||||
|
||||
`/context-restore` reads `[gstack-context]`; `/ship` squashes WIP commits into clean commits.
|
||||
|
||||
If `CHECKPOINT_MODE` is `"explicit"`: ignore this section unless a skill or user asks to commit.
|
||||
|
||||
## Context Health (soft directive)
|
||||
|
||||
During long-running skill sessions, periodically write a brief `[PROGRESS]` summary: done, next, surprises.
|
||||
@@ -670,14 +645,15 @@ service with existing deployment — verify that a distribution pipeline exists.
|
||||
- B) Defer — add a P1 distribution TODO in Step 14
|
||||
- C) Not needed — this is internal/web-only, existing deployment covers it
|
||||
|
||||
4. **If release pipeline exists:** Continue silently.
|
||||
5. **If no new artifact detected:** Skip silently.
|
||||
4. **If the user chooses A:** Add packaging and publish configuration using this repository's CI conventions. Ask for the intended distribution target if it is unknown; do not invent a registry or credentials. Include the new workflow in the tests and review below. Do not publish a release during `/ship`.
|
||||
5. **If release pipeline exists:** Continue silently.
|
||||
6. **If no new artifact detected:** Skip silently.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Merge the base branch (BEFORE tests)
|
||||
|
||||
Merge the base ref fetched in Step 1 so tests cover the same state used by Step 2:
|
||||
Merge the base ref fetched in Step 1 so tests and reviews cover the integrated code:
|
||||
|
||||
```bash
|
||||
git merge origin/<base> --no-edit
|
||||
@@ -718,7 +694,7 @@ for slot selection. Bump level and queue collisions remain agent decisions.
|
||||
```
|
||||
Save the JSON `baseVersion` as `BASE_VERSION`, then read `state` and dispatch:
|
||||
- **FRESH** → do the bump (steps 2-4).
|
||||
- **ALREADY_BUMPED** → keep `NEW_VERSION` at `currentVersion`; recover the prior `BUMP_LEVEL` from the release decision (or base/current version difference), then run step 3's queue check. Do not bump again without approval.
|
||||
- **ALREADY_BUMPED** → keep `NEW_VERSION` at `currentVersion`. Use the recorded level for this release; if absent, compare `baseVersion` and `currentVersion` left to right: the first changed major/minor/patch/micro component supplies `BUMP_LEVEL` (a missing fourth component is zero). Then run step 3's queue check. This recovers the level, not permission to bump again.
|
||||
- **DRIFT_STALE_PKG** → run `gstack-version-bump repair`, then reclassify. On success, follow **ALREADY_BUMPED**, including its queue check; on failure, STOP. Repair alone never re-bumps.
|
||||
- **DRIFT_UNEXPECTED** → **STOP**. package.json disagrees with VERSION while VERSION matches base — a manual edit bypassed /ship. Reconcile manually, then re-run.
|
||||
|
||||
@@ -776,25 +752,7 @@ Never turn dropped scope into TODOs or invent unapproved follow-ups. Reuse match
|
||||
|
||||
## Step 15: Commit (bisectable chunks)
|
||||
|
||||
### Step 15.0: Preserve checkpoint context
|
||||
|
||||
Run `~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode`. `continuous` means automatic `WIP:`
|
||||
checkpoint commits; any other value skips WIP consolidation. In continuous mode,
|
||||
count `WIP:` commits in `origin/<base>..HEAD`. If none exist, skip Step 15.2.
|
||||
Otherwise preserve their context before committing or rewriting history:
|
||||
|
||||
```bash
|
||||
mkdir -p "$(git rev-parse --show-toplevel)/.gstack"
|
||||
git log origin/<base>..HEAD --grep="^WIP:" --format="%H%n%B%n---END---" > \
|
||||
"$(git rev-parse --show-toplevel)/.gstack/wip-context-before-squash.md"
|
||||
```
|
||||
|
||||
If export fails, do not rewrite history. Step 13 already read these bodies for
|
||||
CHANGELOG; retain this PR context locally, outside commits.
|
||||
|
||||
### Step 15.1: Bisectable Commits
|
||||
|
||||
Create small, logical commits for `git bisect`. If all changes are already committed, continue to Step 15.2; never create an empty commit.
|
||||
Create small, logical commits for `git bisect`. If all changes are already committed, continue to Step 16; never create an empty commit.
|
||||
|
||||
1. Group by coherent change. Keep each model/service/controller with its tests;
|
||||
keep controller views together. Migrations may stand alone or accompany their
|
||||
@@ -815,48 +773,6 @@ EOF
|
||||
)"
|
||||
```
|
||||
|
||||
### Step 15.2: Consolidate WIP commits when safe
|
||||
|
||||
After Step 15.1, run only for continuous-mode WIP commits. Require a clean working
|
||||
tree except the context export. Run `git fetch origin`; failure means STOP.
|
||||
Inspect `WIP_BASE..HEAD`, where `WIP_BASE` is `git merge-base HEAD origin/<base>`:
|
||||
|
||||
- **merge commits:** do not replay or flatten Step 3's integration merge.
|
||||
- **published commits** (`git branch -r --contains <sha>` returns a ref): never rewrite.
|
||||
- For either, ask to preserve WIP history and continue to Step 16 (recommended),
|
||||
or stop for manual consolidation. Never rebase or force-push these paths.
|
||||
|
||||
For a linear, unpublished range, prepare and inspect an oldest-first todo.
|
||||
Keep non-WIP commits as `pick` in relative order; put each WIP after its verified
|
||||
logical target as `fixup`. Include every commit exactly once. An ambiguous or
|
||||
out-of-range target needs a preserve-history/stop decision. First entry stays
|
||||
`pick` or `reword`; all-WIP ranges retain a logical `reword` anchor. Rewording
|
||||
requires a noninteractive `WIP_EDITOR` script that writes descriptive messages;
|
||||
picks/fixups alone use `true`. Set the reviewed todo's absolute path below:
|
||||
|
||||
```bash
|
||||
export WIP_TODO="<absolute path to prepared todo>"
|
||||
test -s "$WIP_TODO" || exit 1
|
||||
WIP_BASE=$(git merge-base HEAD origin/<base>) || exit 1
|
||||
test -z "$(git status --porcelain -- . ':(exclude).gstack/wip-context-before-squash.md')" || exit 1
|
||||
test -z "$(git rev-list --merges "$WIP_BASE"..HEAD)" || exit 1
|
||||
for sha in $(git rev-list "$WIP_BASE"..HEAD); do
|
||||
test -z "$(git branch -r --contains "$sha")" || exit 1
|
||||
done
|
||||
ORIGINAL_TREE=$(git rev-parse 'HEAD^{tree}')
|
||||
GIT_EDITOR="${WIP_EDITOR:-true}" GIT_SEQUENCE_EDITOR='cp "$WIP_TODO"' git rebase -i "$WIP_BASE" || {
|
||||
git rebase --abort
|
||||
echo "STATUS: BLOCKED — WIP consolidation conflicted; original history restored"
|
||||
exit 1
|
||||
}
|
||||
test "$ORIGINAL_TREE" = "$(git rev-parse 'HEAD^{tree}')" || {
|
||||
echo "STATUS: BLOCKED — consolidation changed contents; inspect before continuing"
|
||||
exit 1
|
||||
}
|
||||
```
|
||||
|
||||
Only an unchanged tree after successful consolidation may proceed to Step 16.
|
||||
|
||||
---
|
||||
|
||||
## Step 16: Verification Gate
|
||||
@@ -885,14 +801,19 @@ Step 7 tests, review fixes, and Step 14 TODO edits intentionally make evidence S
|
||||
|
||||
- **Every line FRESH (exit 0):** recorded runs passed on identical content except
|
||||
the listed release files. Cite label, exit, timestamp, and log path; continue.
|
||||
- **Any STALE/MISSING (exit non-zero):** rerun the stale/missing lanes on final
|
||||
content, wrapped as `~/.claude/skills/gstack/bin/gstack-evidence run --label <lane> -- '<command>'`.
|
||||
Read results and recheck once. A content, command, or age mismatch requires
|
||||
relevant fresh verification. If the ledger alone cannot record or verify a
|
||||
successful live run, confirm unchanged final content and cite the exact command,
|
||||
exit, and log; report ledger unavailable and continue, but never label the ledger FRESH.
|
||||
If unchanged content cannot be confirmed, STOP. Do not rerun green suites solely for bookkeeping.
|
||||
A failed CHECK selects live verification: a failed CHECK never blocks; a failed RUN does, except for the explicit triage waiver below.
|
||||
- **Any STALE/MISSING (exit non-zero):** inspect the reason before choosing recovery:
|
||||
- **Content, command or age mismatch, or no passing live evidence:** rerun the
|
||||
affected lanes on final content, wrapped as `~/.claude/skills/gstack/bin/gstack-evidence run --label <lane> -- '<command>'`.
|
||||
Read results and recheck once. TODO edits and generated tests are content
|
||||
changes, not ledger-only bookkeeping.
|
||||
- **Ledger read/write failure only:** if a successful live run already covers
|
||||
the unchanged final content, exact command and permitted age, cite its exit,
|
||||
timestamp and log directly. Report ledger unavailable and continue, never
|
||||
ledger FRESH. Do not rerun green suites solely because the ledger cannot save
|
||||
or read its record. If unchanged content cannot be confirmed, STOP.
|
||||
|
||||
A failed CHECK identifies evidence to repair; it is not a test failure. The
|
||||
required live RUN must pass, except for the explicit triage waiver below.
|
||||
|
||||
Paste build and rerun results. Later code, test, or build-input changes return
|
||||
through this gate before pushing. Step 18 owns validation of its post-push
|
||||
|
||||
+36
-116
@@ -303,31 +303,6 @@ For high-stakes ambiguity (architecture, data model, destructive scope, missing
|
||||
|
||||
A claimed limitation or requirement ("the API can't do this", "X requires a credential", "that's impossible on this platform") is a material claim. State one only with the verbatim error, the documented statement, or a live probe in hand — pattern-matching a failure to a familiar story is not evidence. When a cheap probe settles the question, run it BEFORE asking the user anything or declaring a step blocked.
|
||||
|
||||
## Continuous Checkpoint Mode
|
||||
|
||||
If `CHECKPOINT_MODE` is `"continuous"`: auto-commit completed logical units with `WIP:` prefix.
|
||||
|
||||
Commit after new intentional files, completed functions/modules, verified bug fixes, and before long-running install/build/test commands.
|
||||
|
||||
Commit format:
|
||||
|
||||
```
|
||||
WIP: <concise description of what changed>
|
||||
|
||||
[gstack-context]
|
||||
Decisions: <key choices made this step>
|
||||
Remaining: <what's left in the logical unit>
|
||||
Tried: <failed approaches worth recording> (omit if none)
|
||||
Skill: </skill-name-if-running>
|
||||
[/gstack-context]
|
||||
```
|
||||
|
||||
Rules: stage only intentional files, NEVER `git add -A`, do not commit broken tests or mid-edit state, and push only if `CHECKPOINT_PUSH` is `"true"`. Do not announce each WIP commit.
|
||||
|
||||
`/context-restore` reads `[gstack-context]`; `/ship` squashes WIP commits into clean commits.
|
||||
|
||||
If `CHECKPOINT_MODE` is `"explicit"`: ignore this section unless a skill or user asks to commit.
|
||||
|
||||
## Context Health (soft directive)
|
||||
|
||||
During long-running skill sessions, periodically write a brief `[PROGRESS]` summary: done, next, surprises.
|
||||
@@ -663,14 +638,15 @@ service with existing deployment — verify that a distribution pipeline exists.
|
||||
- B) Defer — add a P1 distribution TODO in Step 14
|
||||
- C) Not needed — this is internal/web-only, existing deployment covers it
|
||||
|
||||
4. **If release pipeline exists:** Continue silently.
|
||||
5. **If no new artifact detected:** Skip silently.
|
||||
4. **If the user chooses A:** Add packaging and publish configuration using this repository's CI conventions. Ask for the intended distribution target if it is unknown; do not invent a registry or credentials. Include the new workflow in the tests and review below. Do not publish a release during `/ship`.
|
||||
5. **If release pipeline exists:** Continue silently.
|
||||
6. **If no new artifact detected:** Skip silently.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Merge the base branch (BEFORE tests)
|
||||
|
||||
Merge the base ref fetched in Step 1 so tests cover the same state used by Step 2:
|
||||
Merge the base ref fetched in Step 1 so tests and reviews cover the integrated code:
|
||||
|
||||
```bash
|
||||
git merge origin/<base> --no-edit
|
||||
@@ -2031,20 +2007,22 @@ or missing-reviewer rules.
|
||||
- Overall RECOMMENDATION
|
||||
- If 3 or fewer ASK items, you may use individual AskUserQuestion calls instead
|
||||
|
||||
4. **After all fixes (auto + user-approved):**
|
||||
- If fixes were applied, commit named fixed files (`git add <fixed-files> && git commit -m "fix: pre-landing review fixes"`), then **stay in this invocation and loop**: re-run the test suite (Step 5), then re-run the whole Step 9 cycle from a new pass's start-token capture, including design, specialists, Red Team, and dedup. Repeat until a complete pass applies ZERO fixes with tests green or the same explicit Step 5 waiver. NEVER tell the user to run `/ship` again just for this cycle.
|
||||
4. **After all fixes (auto + user-approved), take the first matching branch:**
|
||||
- If a dispatched specialist or Red Team failed, emit items 5–6 with `status:"unavailable"`, `completed:false` and `converged:false`. Then **STOP before Step 10**, naming the missing reviewer and retaining applied fixes. When coverage is available, rerun Step 5 and affected Steps 6–8 if code changed, then resume with a new Step 9 pass. Intentionally gated or host-unsupported reviewers were not dispatched and do not trigger this stop.
|
||||
- If fixes were applied, commit named fixed files (`git add <fixed-files> && git commit -m "fix: pre-landing review fixes"`), then **stay in this invocation and loop**: re-run the test suite (Step 5) and affected Steps 6–8, then re-run the whole Step 9 cycle from a new pass's start-token capture, including design, specialists, Red Team, and dedup. Repeat until a complete pass applies ZERO fixes with tests green or the same explicit Step 5 waiver. NEVER tell the user to run `/ship` again just for this cycle.
|
||||
- **Bound: 3 fix cycles.** If cycle 3 still fixes code, persist item 6 below with `converged:false` and that pass's original REVIEW_START, then STOP and report which findings keep reappearing.
|
||||
- A zero-fix pass (including explicit skips) proceeds to summary and persistence below; missing dispatched coverage still prevents completion.
|
||||
- A zero-fix pass (including explicit skips) proceeds to summary and persistence below.
|
||||
|
||||
5. Output summary: `Pre-Landing Review: N issues — M auto-fixed, K asked (J fixed, L skipped)`
|
||||
|
||||
If no issues found: `Pre-Landing Review: No issues found.`
|
||||
If coverage is incomplete: `Pre-Landing Review: INCOMPLETE — <missing reviewers>`.
|
||||
Otherwise, if no issues found: `Pre-Landing Review: No issues found.`
|
||||
|
||||
6. Persist the review result to the review log:
|
||||
```bash
|
||||
$GSTACK_ROOT/bin/gstack-review-log '{"skill":"review","timestamp":"TIMESTAMP","status":"STATUS","issues_found":N,"critical":N,"informational":N,"quality_score":SCORE,"specialists":SPECIALISTS_JSON,"findings":FINDINGS_JSON,"commit":"'"$(git rev-parse --short HEAD)"'","via":"ship","completed":COMPLETED,"converged":CONVERGED,"cycles":CYCLES}' --finish REVIEW_START
|
||||
```
|
||||
Substitute TIMESTAMP (ISO 8601), STATUS ("clean" if no issues, "issues_found" otherwise),
|
||||
Substitute TIMESTAMP (ISO 8601), STATUS ("unavailable" for missing dispatched coverage, otherwise "issues_found" for unresolved defects or "clean" for none),
|
||||
and N values from the remaining unresolved findings, not the original pre-fix totals. The `via:"ship"` distinguishes from standalone `/review` runs.
|
||||
- `REVIEW_START` = the token captured at the start of Step 9 before this pass read the diff. `COMPLETED` = true only if the checklist and dispatched specialists completed; failed or missing dispatched coverage is false, never clean. A host-unsupported or intentionally gated specialist was not dispatched and does not block completion; retain the skip/unavailable label. `CONVERGED` = true only for a completed pass that applied zero fixes. `CYCLES` = fix cycles performed (0 for a first-pass completion). Never recapture at persistence to certify fixes that have not been reviewed.
|
||||
- `quality_score` = the PR Quality Score computed in Step 9.2 (e.g., 7.5). If specialists were skipped or unsupported by this host, use `10.0`
|
||||
@@ -2105,7 +2083,7 @@ For each comment in `comments`:
|
||||
|
||||
**SUPPRESSED:** Skip silently — these are known false positives from previous triage.
|
||||
|
||||
**After all comments are resolved:** If any fixes were applied, the tests from Step 5 are now stale. **Re-run tests** (Step 5) before continuing to Step 11. If no fixes were applied, continue to Step 11.
|
||||
**After all comments are resolved:** If fixes were applied, run Step 5 and any affected checks from Steps 6–8, then repeat Step 9 on the changed tree before continuing to Step 11. Keep the replies already sent; do not repeat unchanged comment decisions. If no fixes were applied, continue to Step 11.
|
||||
|
||||
---
|
||||
|
||||
@@ -2183,7 +2161,7 @@ Read the diff for this branch. First list changed files: `DIFF_BASE=$(git merge-
|
||||
|
||||
Think like an attacker and a chaos engineer. Your job is to find ways this code will fail in production. Look for: edge cases, race conditions, security holes, resource leaks, failure modes, silent data corruption, logic errors that produce wrong results silently, error handling that swallows failures, and trust boundary violations. Be adversarial. Be thorough. No compliments — just the problems. For each finding, classify as FIXABLE (you know how to fix it) or INVESTIGATE (needs human judgment). After listing findings, end your output with ONE line in the canonical format `Recommendation: <action> because <one-line reason naming the most exploitable finding>` — examples: `Recommendation: Fix the unbounded retry at queue.ts:78 because it'll DoS the worker pool under sustained 429s` or `Recommendation: Ship as-is because the strongest finding is a theoretical race that requires conditions we can't trigger in production`. The reason must point to a specific finding (or no-fix rationale). Generic reasons like 'because it's safer' do not qualify."
|
||||
|
||||
Present findings under an `ADVERSARIAL REVIEW (Codex (in-host) subagent):` header. **FIXABLE findings** flow into the same Fix-First pipeline as the structured review. **INVESTIGATE findings** are presented as informational.
|
||||
Present findings under an `ADVERSARIAL REVIEW (Codex (in-host) subagent):` header. **FIXABLE findings:** collect them for the Step 11 completion procedure below; it uses Step 9.4's classification and approval rules. **INVESTIGATE findings** are presented as informational.
|
||||
|
||||
If the subagent fails or times out: "Codex (in-host) adversarial subagent unavailable. Continuing."
|
||||
|
||||
@@ -2339,7 +2317,7 @@ A) Investigate and fix now (recommended)
|
||||
B) Continue — review will still complete
|
||||
```
|
||||
|
||||
If A: address the findings. After fixing, re-run tests (Step 5) since code has changed. Re-run the same shared structured invocation and diff scope to verify.
|
||||
If A: record approval to fix these findings in the Step 11 completion procedure below. If B: retain the acknowledged findings and failed gate; do not report a clean review.
|
||||
|
||||
Read stderr for errors (same error handling as Claude Code adversarial above).
|
||||
|
||||
@@ -2379,6 +2357,13 @@ ADVERSARIAL REVIEW SYNTHESIS (always-on, N lines):
|
||||
|
||||
High-confidence findings (agreed on by multiple sources) should be prioritized for fixes.
|
||||
|
||||
### Step 11 completion and late-fix loop
|
||||
|
||||
1. Finish all available passes and persist each source/phase's actual result above. Missing or failed passes remain unavailable, never clean.
|
||||
2. Triage the collected FIXABLE findings using Step 9.4 items 1–3: AUTO-FIX or ASK, apply automatic and approved fixes, and retain explicit skips. Do not ask again for a Step 11 P1 fix already approved.
|
||||
3. If anything changed, commit only the fixed files. Run Step 5 and affected Steps 6–8, then repeat Step 9 from a fresh start token. After Step 9 converges, return directly to Step 11 and repeat its passes on the changed tree. Prior responses do not certify the fixes; do not repeat unchanged Step 10 comment decisions.
|
||||
4. Bound this late-fix loop to three fix cycles. If the third cycle still changes code, record non-convergence and STOP with the recurring findings. A zero-fix cycle continues to Step 12 with actual coverage and any explicit acknowledgments; unavailable or waived coverage is never reported as a clean completed pass.
|
||||
|
||||
---
|
||||
|
||||
## Capture Learnings
|
||||
@@ -2433,7 +2418,7 @@ for slot selection. Bump level and queue collisions remain agent decisions.
|
||||
```
|
||||
Save the JSON `baseVersion` as `BASE_VERSION`, then read `state` and dispatch:
|
||||
- **FRESH** → do the bump (steps 2-4).
|
||||
- **ALREADY_BUMPED** → keep `NEW_VERSION` at `currentVersion`; recover the prior `BUMP_LEVEL` from the release decision (or base/current version difference), then run step 3's queue check. Do not bump again without approval.
|
||||
- **ALREADY_BUMPED** → keep `NEW_VERSION` at `currentVersion`. Use the recorded level for this release; if absent, compare `baseVersion` and `currentVersion` left to right: the first changed major/minor/patch/micro component supplies `BUMP_LEVEL` (a missing fourth component is zero). Then run step 3's queue check. This recovers the level, not permission to bump again.
|
||||
- **DRIFT_STALE_PKG** → run `gstack-version-bump repair`, then reclassify. On success, follow **ALREADY_BUMPED**, including its queue check; on failure, STOP. Repair alone never re-bumps.
|
||||
- **DRIFT_UNEXPECTED** → **STOP**. package.json disagrees with VERSION while VERSION matches base — a manual edit bypassed /ship. Reconcile manually, then re-run.
|
||||
|
||||
@@ -2464,16 +2449,6 @@ for slot selection. Bump level and queue collisions remain agent decisions.
|
||||
```
|
||||
Substitute `NEW_VERSION`, `BUMP_LEVEL`, and one-line `WHY` (scope or breaking-change signal). Best-effort, non-interactive, non-blocking.
|
||||
|
||||
**Before drafting:** In continuous checkpoint mode, read the WIP commit bodies
|
||||
while they still exist (no WIP commits means no extra context):
|
||||
|
||||
```bash
|
||||
git log origin/<base>..HEAD --grep="^WIP:" --format="%H%n%B"
|
||||
```
|
||||
|
||||
Use their `[gstack-context]` notes only where supported by the diff. Step 15.0
|
||||
later preserves these bodies for PR context before squashing them.
|
||||
|
||||
## Step 13: CHANGELOG (auto-generate)
|
||||
|
||||
1. Read `CHANGELOG.md` header to know the format.
|
||||
@@ -2542,25 +2517,7 @@ Never turn dropped scope into TODOs or invent unapproved follow-ups. Reuse match
|
||||
|
||||
## Step 15: Commit (bisectable chunks)
|
||||
|
||||
### Step 15.0: Preserve checkpoint context
|
||||
|
||||
Run `$GSTACK_ROOT/bin/gstack-config get checkpoint_mode`. `continuous` means automatic `WIP:`
|
||||
checkpoint commits; any other value skips WIP consolidation. In continuous mode,
|
||||
count `WIP:` commits in `origin/<base>..HEAD`. If none exist, skip Step 15.2.
|
||||
Otherwise preserve their context before committing or rewriting history:
|
||||
|
||||
```bash
|
||||
mkdir -p "$(git rev-parse --show-toplevel)/.gstack"
|
||||
git log origin/<base>..HEAD --grep="^WIP:" --format="%H%n%B%n---END---" > \
|
||||
"$(git rev-parse --show-toplevel)/.gstack/wip-context-before-squash.md"
|
||||
```
|
||||
|
||||
If export fails, do not rewrite history. Step 13 already read these bodies for
|
||||
CHANGELOG; retain this PR context locally, outside commits.
|
||||
|
||||
### Step 15.1: Bisectable Commits
|
||||
|
||||
Create small, logical commits for `git bisect`. If all changes are already committed, continue to Step 15.2; never create an empty commit.
|
||||
Create small, logical commits for `git bisect`. If all changes are already committed, continue to Step 16; never create an empty commit.
|
||||
|
||||
1. Group by coherent change. Keep each model/service/controller with its tests;
|
||||
keep controller views together. Migrations may stand alone or accompany their
|
||||
@@ -2581,48 +2538,6 @@ EOF
|
||||
)"
|
||||
```
|
||||
|
||||
### Step 15.2: Consolidate WIP commits when safe
|
||||
|
||||
After Step 15.1, run only for continuous-mode WIP commits. Require a clean working
|
||||
tree except the context export. Run `git fetch origin`; failure means STOP.
|
||||
Inspect `WIP_BASE..HEAD`, where `WIP_BASE` is `git merge-base HEAD origin/<base>`:
|
||||
|
||||
- **merge commits:** do not replay or flatten Step 3's integration merge.
|
||||
- **published commits** (`git branch -r --contains <sha>` returns a ref): never rewrite.
|
||||
- For either, ask to preserve WIP history and continue to Step 16 (recommended),
|
||||
or stop for manual consolidation. Never rebase or force-push these paths.
|
||||
|
||||
For a linear, unpublished range, prepare and inspect an oldest-first todo.
|
||||
Keep non-WIP commits as `pick` in relative order; put each WIP after its verified
|
||||
logical target as `fixup`. Include every commit exactly once. An ambiguous or
|
||||
out-of-range target needs a preserve-history/stop decision. First entry stays
|
||||
`pick` or `reword`; all-WIP ranges retain a logical `reword` anchor. Rewording
|
||||
requires a noninteractive `WIP_EDITOR` script that writes descriptive messages;
|
||||
picks/fixups alone use `true`. Set the reviewed todo's absolute path below:
|
||||
|
||||
```bash
|
||||
export WIP_TODO="<absolute path to prepared todo>"
|
||||
test -s "$WIP_TODO" || exit 1
|
||||
WIP_BASE=$(git merge-base HEAD origin/<base>) || exit 1
|
||||
test -z "$(git status --porcelain -- . ':(exclude).gstack/wip-context-before-squash.md')" || exit 1
|
||||
test -z "$(git rev-list --merges "$WIP_BASE"..HEAD)" || exit 1
|
||||
for sha in $(git rev-list "$WIP_BASE"..HEAD); do
|
||||
test -z "$(git branch -r --contains "$sha")" || exit 1
|
||||
done
|
||||
ORIGINAL_TREE=$(git rev-parse 'HEAD^{tree}')
|
||||
GIT_EDITOR="${WIP_EDITOR:-true}" GIT_SEQUENCE_EDITOR='cp "$WIP_TODO"' git rebase -i "$WIP_BASE" || {
|
||||
git rebase --abort
|
||||
echo "STATUS: BLOCKED — WIP consolidation conflicted; original history restored"
|
||||
exit 1
|
||||
}
|
||||
test "$ORIGINAL_TREE" = "$(git rev-parse 'HEAD^{tree}')" || {
|
||||
echo "STATUS: BLOCKED — consolidation changed contents; inspect before continuing"
|
||||
exit 1
|
||||
}
|
||||
```
|
||||
|
||||
Only an unchanged tree after successful consolidation may proceed to Step 16.
|
||||
|
||||
---
|
||||
|
||||
## Step 16: Verification Gate
|
||||
@@ -2651,14 +2566,19 @@ Step 7 tests, review fixes, and Step 14 TODO edits intentionally make evidence S
|
||||
|
||||
- **Every line FRESH (exit 0):** recorded runs passed on identical content except
|
||||
the listed release files. Cite label, exit, timestamp, and log path; continue.
|
||||
- **Any STALE/MISSING (exit non-zero):** rerun the stale/missing lanes on final
|
||||
content, wrapped as `$GSTACK_ROOT/bin/gstack-evidence run --label <lane> -- '<command>'`.
|
||||
Read results and recheck once. A content, command, or age mismatch requires
|
||||
relevant fresh verification. If the ledger alone cannot record or verify a
|
||||
successful live run, confirm unchanged final content and cite the exact command,
|
||||
exit, and log; report ledger unavailable and continue, but never label the ledger FRESH.
|
||||
If unchanged content cannot be confirmed, STOP. Do not rerun green suites solely for bookkeeping.
|
||||
A failed CHECK selects live verification: a failed CHECK never blocks; a failed RUN does, except for the explicit triage waiver below.
|
||||
- **Any STALE/MISSING (exit non-zero):** inspect the reason before choosing recovery:
|
||||
- **Content, command or age mismatch, or no passing live evidence:** rerun the
|
||||
affected lanes on final content, wrapped as `$GSTACK_ROOT/bin/gstack-evidence run --label <lane> -- '<command>'`.
|
||||
Read results and recheck once. TODO edits and generated tests are content
|
||||
changes, not ledger-only bookkeeping.
|
||||
- **Ledger read/write failure only:** if a successful live run already covers
|
||||
the unchanged final content, exact command and permitted age, cite its exit,
|
||||
timestamp and log directly. Report ledger unavailable and continue, never
|
||||
ledger FRESH. Do not rerun green suites solely because the ledger cannot save
|
||||
or read its record. If unchanged content cannot be confirmed, STOP.
|
||||
|
||||
A failed CHECK identifies evidence to repair; it is not a test failure. The
|
||||
required live RUN must pass, except for the explicit triage waiver below.
|
||||
|
||||
Paste build and rerun results. Later code, test, or build-input changes return
|
||||
through this gate before pushing. Step 18 owns validation of its post-push
|
||||
|
||||
+36
-116
@@ -283,31 +283,6 @@ For high-stakes ambiguity (architecture, data model, destructive scope, missing
|
||||
|
||||
A claimed limitation or requirement ("the API can't do this", "X requires a credential", "that's impossible on this platform") is a material claim. State one only with the verbatim error, the documented statement, or a live probe in hand — pattern-matching a failure to a familiar story is not evidence. When a cheap probe settles the question, run it BEFORE asking the user anything or declaring a step blocked.
|
||||
|
||||
## Continuous Checkpoint Mode
|
||||
|
||||
If `CHECKPOINT_MODE` is `"continuous"`: auto-commit completed logical units with `WIP:` prefix.
|
||||
|
||||
Commit after new intentional files, completed functions/modules, verified bug fixes, and before long-running install/build/test commands.
|
||||
|
||||
Commit format:
|
||||
|
||||
```
|
||||
WIP: <concise description of what changed>
|
||||
|
||||
[gstack-context]
|
||||
Decisions: <key choices made this step>
|
||||
Remaining: <what's left in the logical unit>
|
||||
Tried: <failed approaches worth recording> (omit if none)
|
||||
Skill: </skill-name-if-running>
|
||||
[/gstack-context]
|
||||
```
|
||||
|
||||
Rules: stage only intentional files, NEVER `git add -A`, do not commit broken tests or mid-edit state, and push only if `CHECKPOINT_PUSH` is `"true"`. Do not announce each WIP commit.
|
||||
|
||||
`/context-restore` reads `[gstack-context]`; `/ship` squashes WIP commits into clean commits.
|
||||
|
||||
If `CHECKPOINT_MODE` is `"explicit"`: ignore this section unless a skill or user asks to commit.
|
||||
|
||||
## Context Health (soft directive)
|
||||
|
||||
During long-running skill sessions, periodically write a brief `[PROGRESS]` summary: done, next, surprises.
|
||||
@@ -643,14 +618,15 @@ service with existing deployment — verify that a distribution pipeline exists.
|
||||
- B) Defer — add a P1 distribution TODO in Step 14
|
||||
- C) Not needed — this is internal/web-only, existing deployment covers it
|
||||
|
||||
4. **If release pipeline exists:** Continue silently.
|
||||
5. **If no new artifact detected:** Skip silently.
|
||||
4. **If the user chooses A:** Add packaging and publish configuration using this repository's CI conventions. Ask for the intended distribution target if it is unknown; do not invent a registry or credentials. Include the new workflow in the tests and review below. Do not publish a release during `/ship`.
|
||||
5. **If release pipeline exists:** Continue silently.
|
||||
6. **If no new artifact detected:** Skip silently.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Merge the base branch (BEFORE tests)
|
||||
|
||||
Merge the base ref fetched in Step 1 so tests cover the same state used by Step 2:
|
||||
Merge the base ref fetched in Step 1 so tests and reviews cover the integrated code:
|
||||
|
||||
```bash
|
||||
git merge origin/<base> --no-edit
|
||||
@@ -2270,20 +2246,22 @@ or missing-reviewer rules.
|
||||
- Overall RECOMMENDATION
|
||||
- If 3 or fewer ASK items, you may use individual AskUserQuestion calls instead
|
||||
|
||||
4. **After all fixes (auto + user-approved):**
|
||||
- If fixes were applied, commit named fixed files (`git add <fixed-files> && git commit -m "fix: pre-landing review fixes"`), then **stay in this invocation and loop**: re-run the test suite (Step 5), then re-run the whole Step 9 cycle from a new pass's start-token capture, including design, specialists, Red Team, and dedup. Repeat until a complete pass applies ZERO fixes with tests green or the same explicit Step 5 waiver. NEVER tell the user to run `/ship` again just for this cycle.
|
||||
4. **After all fixes (auto + user-approved), take the first matching branch:**
|
||||
- If a dispatched specialist or Red Team failed, emit items 5–6 with `status:"unavailable"`, `completed:false` and `converged:false`. Then **STOP before Step 10**, naming the missing reviewer and retaining applied fixes. When coverage is available, rerun Step 5 and affected Steps 6–8 if code changed, then resume with a new Step 9 pass. Intentionally gated or host-unsupported reviewers were not dispatched and do not trigger this stop.
|
||||
- If fixes were applied, commit named fixed files (`git add <fixed-files> && git commit -m "fix: pre-landing review fixes"`), then **stay in this invocation and loop**: re-run the test suite (Step 5) and affected Steps 6–8, then re-run the whole Step 9 cycle from a new pass's start-token capture, including design, specialists, Red Team, and dedup. Repeat until a complete pass applies ZERO fixes with tests green or the same explicit Step 5 waiver. NEVER tell the user to run `/ship` again just for this cycle.
|
||||
- **Bound: 3 fix cycles.** If cycle 3 still fixes code, persist item 6 below with `converged:false` and that pass's original REVIEW_START, then STOP and report which findings keep reappearing.
|
||||
- A zero-fix pass (including explicit skips) proceeds to summary and persistence below; missing dispatched coverage still prevents completion.
|
||||
- A zero-fix pass (including explicit skips) proceeds to summary and persistence below.
|
||||
|
||||
5. Output summary: `Pre-Landing Review: N issues — M auto-fixed, K asked (J fixed, L skipped)`
|
||||
|
||||
If no issues found: `Pre-Landing Review: No issues found.`
|
||||
If coverage is incomplete: `Pre-Landing Review: INCOMPLETE — <missing reviewers>`.
|
||||
Otherwise, if no issues found: `Pre-Landing Review: No issues found.`
|
||||
|
||||
6. Persist the review result to the review log:
|
||||
```bash
|
||||
$GSTACK_ROOT/bin/gstack-review-log '{"skill":"review","timestamp":"TIMESTAMP","status":"STATUS","issues_found":N,"critical":N,"informational":N,"quality_score":SCORE,"specialists":SPECIALISTS_JSON,"findings":FINDINGS_JSON,"commit":"'"$(git rev-parse --short HEAD)"'","via":"ship","completed":COMPLETED,"converged":CONVERGED,"cycles":CYCLES}' --finish REVIEW_START
|
||||
```
|
||||
Substitute TIMESTAMP (ISO 8601), STATUS ("clean" if no issues, "issues_found" otherwise),
|
||||
Substitute TIMESTAMP (ISO 8601), STATUS ("unavailable" for missing dispatched coverage, otherwise "issues_found" for unresolved defects or "clean" for none),
|
||||
and N values from the remaining unresolved findings, not the original pre-fix totals. The `via:"ship"` distinguishes from standalone `/review` runs.
|
||||
- `REVIEW_START` = the token captured at the start of Step 9 before this pass read the diff. `COMPLETED` = true only if the checklist and dispatched specialists completed; failed or missing dispatched coverage is false, never clean. A host-unsupported or intentionally gated specialist was not dispatched and does not block completion; retain the skip/unavailable label. `CONVERGED` = true only for a completed pass that applied zero fixes. `CYCLES` = fix cycles performed (0 for a first-pass completion). Never recapture at persistence to certify fixes that have not been reviewed.
|
||||
- `quality_score` = the PR Quality Score computed in Step 9.2 (e.g., 7.5). If specialists were skipped or unsupported by this host, use `10.0`
|
||||
@@ -2344,7 +2322,7 @@ For each comment in `comments`:
|
||||
|
||||
**SUPPRESSED:** Skip silently — these are known false positives from previous triage.
|
||||
|
||||
**After all comments are resolved:** If any fixes were applied, the tests from Step 5 are now stale. **Re-run tests** (Step 5) before continuing to Step 11. If no fixes were applied, continue to Step 11.
|
||||
**After all comments are resolved:** If fixes were applied, run Step 5 and any affected checks from Steps 6–8, then repeat Step 9 on the changed tree before continuing to Step 11. Keep the replies already sent; do not repeat unchanged comment decisions. If no fixes were applied, continue to Step 11.
|
||||
|
||||
---
|
||||
|
||||
@@ -2441,7 +2419,7 @@ Read the diff for this branch. First list changed files: `DIFF_BASE=$(git merge-
|
||||
|
||||
Think like an attacker and a chaos engineer. Your job is to find ways this code will fail in production. Look for: edge cases, race conditions, security holes, resource leaks, failure modes, silent data corruption, logic errors that produce wrong results silently, error handling that swallows failures, and trust boundary violations. Be adversarial. Be thorough. No compliments — just the problems. For each finding, classify as FIXABLE (you know how to fix it) or INVESTIGATE (needs human judgment). After listing findings, end your output with ONE line in the canonical format `Recommendation: <action> because <one-line reason naming the most exploitable finding>` — examples: `Recommendation: Fix the unbounded retry at queue.ts:78 because it'll DoS the worker pool under sustained 429s` or `Recommendation: Ship as-is because the strongest finding is a theoretical race that requires conditions we can't trigger in production`. The reason must point to a specific finding (or no-fix rationale). Generic reasons like 'because it's safer' do not qualify."
|
||||
|
||||
Present findings under an `ADVERSARIAL REVIEW (factory (in-host) subagent):` header. **FIXABLE findings** flow into the same Fix-First pipeline as the structured review. **INVESTIGATE findings** are presented as informational.
|
||||
Present findings under an `ADVERSARIAL REVIEW (factory (in-host) subagent):` header. **FIXABLE findings:** collect them for the Step 11 completion procedure below; it uses Step 9.4's classification and approval rules. **INVESTIGATE findings** are presented as informational.
|
||||
|
||||
If the subagent fails or times out: "factory (in-host) adversarial subagent unavailable. Continuing."
|
||||
|
||||
@@ -2590,7 +2568,7 @@ A) Investigate and fix now (recommended)
|
||||
B) Continue — review will still complete
|
||||
```
|
||||
|
||||
If A: address the findings. After fixing, re-run tests (Step 5) since code has changed. Re-run the same shared structured invocation and diff scope to verify.
|
||||
If A: record approval to fix these findings in the Step 11 completion procedure below. If B: retain the acknowledged findings and failed gate; do not report a clean review.
|
||||
|
||||
Read stderr for errors (same error handling as Codex adversarial above).
|
||||
|
||||
@@ -2630,6 +2608,13 @@ ADVERSARIAL REVIEW SYNTHESIS (always-on, N lines):
|
||||
|
||||
High-confidence findings (agreed on by multiple sources) should be prioritized for fixes.
|
||||
|
||||
### Step 11 completion and late-fix loop
|
||||
|
||||
1. Finish all available passes and persist each source/phase's actual result above. Missing or failed passes remain unavailable, never clean.
|
||||
2. Triage the collected FIXABLE findings using Step 9.4 items 1–3: AUTO-FIX or ASK, apply automatic and approved fixes, and retain explicit skips. Do not ask again for a Step 11 P1 fix already approved.
|
||||
3. If anything changed, commit only the fixed files. Run Step 5 and affected Steps 6–8, then repeat Step 9 from a fresh start token. After Step 9 converges, return directly to Step 11 and repeat its passes on the changed tree. Prior responses do not certify the fixes; do not repeat unchanged Step 10 comment decisions.
|
||||
4. Bound this late-fix loop to three fix cycles. If the third cycle still changes code, record non-convergence and STOP with the recurring findings. A zero-fix cycle continues to Step 12 with actual coverage and any explicit acknowledgments; unavailable or waived coverage is never reported as a clean completed pass.
|
||||
|
||||
---
|
||||
|
||||
## Capture Learnings
|
||||
@@ -2684,7 +2669,7 @@ for slot selection. Bump level and queue collisions remain agent decisions.
|
||||
```
|
||||
Save the JSON `baseVersion` as `BASE_VERSION`, then read `state` and dispatch:
|
||||
- **FRESH** → do the bump (steps 2-4).
|
||||
- **ALREADY_BUMPED** → keep `NEW_VERSION` at `currentVersion`; recover the prior `BUMP_LEVEL` from the release decision (or base/current version difference), then run step 3's queue check. Do not bump again without approval.
|
||||
- **ALREADY_BUMPED** → keep `NEW_VERSION` at `currentVersion`. Use the recorded level for this release; if absent, compare `baseVersion` and `currentVersion` left to right: the first changed major/minor/patch/micro component supplies `BUMP_LEVEL` (a missing fourth component is zero). Then run step 3's queue check. This recovers the level, not permission to bump again.
|
||||
- **DRIFT_STALE_PKG** → run `gstack-version-bump repair`, then reclassify. On success, follow **ALREADY_BUMPED**, including its queue check; on failure, STOP. Repair alone never re-bumps.
|
||||
- **DRIFT_UNEXPECTED** → **STOP**. package.json disagrees with VERSION while VERSION matches base — a manual edit bypassed /ship. Reconcile manually, then re-run.
|
||||
|
||||
@@ -2715,16 +2700,6 @@ for slot selection. Bump level and queue collisions remain agent decisions.
|
||||
```
|
||||
Substitute `NEW_VERSION`, `BUMP_LEVEL`, and one-line `WHY` (scope or breaking-change signal). Best-effort, non-interactive, non-blocking.
|
||||
|
||||
**Before drafting:** In continuous checkpoint mode, read the WIP commit bodies
|
||||
while they still exist (no WIP commits means no extra context):
|
||||
|
||||
```bash
|
||||
git log origin/<base>..HEAD --grep="^WIP:" --format="%H%n%B"
|
||||
```
|
||||
|
||||
Use their `[gstack-context]` notes only where supported by the diff. Step 15.0
|
||||
later preserves these bodies for PR context before squashing them.
|
||||
|
||||
## Step 13: CHANGELOG (auto-generate)
|
||||
|
||||
1. Read `CHANGELOG.md` header to know the format.
|
||||
@@ -2793,25 +2768,7 @@ Never turn dropped scope into TODOs or invent unapproved follow-ups. Reuse match
|
||||
|
||||
## Step 15: Commit (bisectable chunks)
|
||||
|
||||
### Step 15.0: Preserve checkpoint context
|
||||
|
||||
Run `$GSTACK_ROOT/bin/gstack-config get checkpoint_mode`. `continuous` means automatic `WIP:`
|
||||
checkpoint commits; any other value skips WIP consolidation. In continuous mode,
|
||||
count `WIP:` commits in `origin/<base>..HEAD`. If none exist, skip Step 15.2.
|
||||
Otherwise preserve their context before committing or rewriting history:
|
||||
|
||||
```bash
|
||||
mkdir -p "$(git rev-parse --show-toplevel)/.gstack"
|
||||
git log origin/<base>..HEAD --grep="^WIP:" --format="%H%n%B%n---END---" > \
|
||||
"$(git rev-parse --show-toplevel)/.gstack/wip-context-before-squash.md"
|
||||
```
|
||||
|
||||
If export fails, do not rewrite history. Step 13 already read these bodies for
|
||||
CHANGELOG; retain this PR context locally, outside commits.
|
||||
|
||||
### Step 15.1: Bisectable Commits
|
||||
|
||||
Create small, logical commits for `git bisect`. If all changes are already committed, continue to Step 15.2; never create an empty commit.
|
||||
Create small, logical commits for `git bisect`. If all changes are already committed, continue to Step 16; never create an empty commit.
|
||||
|
||||
1. Group by coherent change. Keep each model/service/controller with its tests;
|
||||
keep controller views together. Migrations may stand alone or accompany their
|
||||
@@ -2832,48 +2789,6 @@ EOF
|
||||
)"
|
||||
```
|
||||
|
||||
### Step 15.2: Consolidate WIP commits when safe
|
||||
|
||||
After Step 15.1, run only for continuous-mode WIP commits. Require a clean working
|
||||
tree except the context export. Run `git fetch origin`; failure means STOP.
|
||||
Inspect `WIP_BASE..HEAD`, where `WIP_BASE` is `git merge-base HEAD origin/<base>`:
|
||||
|
||||
- **merge commits:** do not replay or flatten Step 3's integration merge.
|
||||
- **published commits** (`git branch -r --contains <sha>` returns a ref): never rewrite.
|
||||
- For either, ask to preserve WIP history and continue to Step 16 (recommended),
|
||||
or stop for manual consolidation. Never rebase or force-push these paths.
|
||||
|
||||
For a linear, unpublished range, prepare and inspect an oldest-first todo.
|
||||
Keep non-WIP commits as `pick` in relative order; put each WIP after its verified
|
||||
logical target as `fixup`. Include every commit exactly once. An ambiguous or
|
||||
out-of-range target needs a preserve-history/stop decision. First entry stays
|
||||
`pick` or `reword`; all-WIP ranges retain a logical `reword` anchor. Rewording
|
||||
requires a noninteractive `WIP_EDITOR` script that writes descriptive messages;
|
||||
picks/fixups alone use `true`. Set the reviewed todo's absolute path below:
|
||||
|
||||
```bash
|
||||
export WIP_TODO="<absolute path to prepared todo>"
|
||||
test -s "$WIP_TODO" || exit 1
|
||||
WIP_BASE=$(git merge-base HEAD origin/<base>) || exit 1
|
||||
test -z "$(git status --porcelain -- . ':(exclude).gstack/wip-context-before-squash.md')" || exit 1
|
||||
test -z "$(git rev-list --merges "$WIP_BASE"..HEAD)" || exit 1
|
||||
for sha in $(git rev-list "$WIP_BASE"..HEAD); do
|
||||
test -z "$(git branch -r --contains "$sha")" || exit 1
|
||||
done
|
||||
ORIGINAL_TREE=$(git rev-parse 'HEAD^{tree}')
|
||||
GIT_EDITOR="${WIP_EDITOR:-true}" GIT_SEQUENCE_EDITOR='cp "$WIP_TODO"' git rebase -i "$WIP_BASE" || {
|
||||
git rebase --abort
|
||||
echo "STATUS: BLOCKED — WIP consolidation conflicted; original history restored"
|
||||
exit 1
|
||||
}
|
||||
test "$ORIGINAL_TREE" = "$(git rev-parse 'HEAD^{tree}')" || {
|
||||
echo "STATUS: BLOCKED — consolidation changed contents; inspect before continuing"
|
||||
exit 1
|
||||
}
|
||||
```
|
||||
|
||||
Only an unchanged tree after successful consolidation may proceed to Step 16.
|
||||
|
||||
---
|
||||
|
||||
## Step 16: Verification Gate
|
||||
@@ -2902,14 +2817,19 @@ Step 7 tests, review fixes, and Step 14 TODO edits intentionally make evidence S
|
||||
|
||||
- **Every line FRESH (exit 0):** recorded runs passed on identical content except
|
||||
the listed release files. Cite label, exit, timestamp, and log path; continue.
|
||||
- **Any STALE/MISSING (exit non-zero):** rerun the stale/missing lanes on final
|
||||
content, wrapped as `$GSTACK_ROOT/bin/gstack-evidence run --label <lane> -- '<command>'`.
|
||||
Read results and recheck once. A content, command, or age mismatch requires
|
||||
relevant fresh verification. If the ledger alone cannot record or verify a
|
||||
successful live run, confirm unchanged final content and cite the exact command,
|
||||
exit, and log; report ledger unavailable and continue, but never label the ledger FRESH.
|
||||
If unchanged content cannot be confirmed, STOP. Do not rerun green suites solely for bookkeeping.
|
||||
A failed CHECK selects live verification: a failed CHECK never blocks; a failed RUN does, except for the explicit triage waiver below.
|
||||
- **Any STALE/MISSING (exit non-zero):** inspect the reason before choosing recovery:
|
||||
- **Content, command or age mismatch, or no passing live evidence:** rerun the
|
||||
affected lanes on final content, wrapped as `$GSTACK_ROOT/bin/gstack-evidence run --label <lane> -- '<command>'`.
|
||||
Read results and recheck once. TODO edits and generated tests are content
|
||||
changes, not ledger-only bookkeeping.
|
||||
- **Ledger read/write failure only:** if a successful live run already covers
|
||||
the unchanged final content, exact command and permitted age, cite its exit,
|
||||
timestamp and log directly. Report ledger unavailable and continue, never
|
||||
ledger FRESH. Do not rerun green suites solely because the ledger cannot save
|
||||
or read its record. If unchanged content cannot be confirmed, STOP.
|
||||
|
||||
A failed CHECK identifies evidence to repair; it is not a test failure. The
|
||||
required live RUN must pass, except for the explicit triage waiver below.
|
||||
|
||||
Paste build and rerun results. Later code, test, or build-input changes return
|
||||
through this gate before pushing. Step 18 owns validation of its post-push
|
||||
|
||||
+73
File diff suppressed because one or more lines are too long
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"source": "CI run 36048177576, shared-libs-review-index-flags, merge ref 1215f799 (head b1dc44e7)",
|
||||
"cases": [
|
||||
{
|
||||
"attempt": 1,
|
||||
"input": {
|
||||
"questions": [
|
||||
{
|
||||
"question": "1 advisory needs your input (no defects were found).\n\n1. [ADVISORY] src/retry-worker.ts:2 — The diff replaces `export { retrySeconds } from '../lib/retry-after'` with a byte-identical inline copy of the helper. src/retry-route.ts:2 (raw file; hidden from `git diff` by its assume-unchanged flag) carries the same copy. lib/retry-after.ts:2 is the proven helper, already used by src/scheduler.ts:1 and covered by test/retry-after.test.ts (`retry parser contract`).\n Fix: restore the one-line re-export in the worker and replace the route's copy with the same re-export. Est. implementation: −32 / +2 lines (≈30 saved); no new tests needed; shared-failure blast radius = scheduler + worker + route, all same TS package, no runtime/deployment boundary crossed.\n\nRECOMMENDATION: Fix both callers — the helper already exists and is tested, and the copies preserve no differences.\n\nHow do you want to handle this?",
|
||||
"header": "Advisory",
|
||||
"options": [
|
||||
{
|
||||
"label": "Fix both callers (Recommended)",
|
||||
"description": "Restore `export { retrySeconds } from '../lib/retry-after'` in src/retry-worker.ts and src/retry-route.ts."
|
||||
},
|
||||
{
|
||||
"label": "Fix worker only",
|
||||
"description": "Only revert src/retry-worker.ts (the file in the diff); leave src/retry-route.ts as-is."
|
||||
},
|
||||
{
|
||||
"label": "Skip",
|
||||
"description": "Keep the duplicated copies. Recorded as an explicit skip for this snapshot."
|
||||
}
|
||||
],
|
||||
"multiSelect": false
|
||||
},
|
||||
{
|
||||
"question": "src/retry-route.ts has the Git assume-unchanged index bit set (`git ls-files -v` shows `h`). This hides real edits to that file from `git diff`, `git status`, and the review fingerprint — which is exactly why the prior skipped decision could not be trusted. Should I clear it?",
|
||||
"header": "Index flag",
|
||||
"options": [
|
||||
{
|
||||
"label": "Clear the flag (Recommended)",
|
||||
"description": "Run `git update-index --no-assume-unchanged src/retry-route.ts` so the file's changes become visible to Git and future reviews."
|
||||
},
|
||||
{
|
||||
"label": "Leave it",
|
||||
"description": "Do not touch the index. I will note it in the summary; the path stays excluded from reusable coverage."
|
||||
}
|
||||
],
|
||||
"multiSelect": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"attempt": 2,
|
||||
"input": {
|
||||
"questions": [
|
||||
{
|
||||
"question": "Pre-Landing Review: 0 issues (0 critical, 0 informational). 1 advisory needs your input:\n\n1. [ADVISORY] src/retry-worker.ts:2 — This diff replaces the worker's re-export of lib/retry-after.ts with a byte-identical inline copy of `retrySeconds`; src/retry-route.ts:2 carries a third identical copy. lib/retry-after.ts is already used by src/scheduler.ts and tested by test/retry-after.test.ts ('retry parser contract').\n Fix: restore `export { retrySeconds } from '../lib/retry-after';` in retry-worker.ts and apply the same one-line re-export in retry-route.ts. Impl: −30/+2 = 28 lines saved; no new tests needed (existing contract test covers the helper); shared-failure blast radius = scheduler + worker + route (scheduler already depends on it).\n\nRECOMMENDATION: Fix both — the worker change is a regression of existing reuse, and the route copy is the same contract. (Prior skip on this branch could not be reused: src/retry-route.ts is assume-unchanged and its raw bytes differ from the snapshot.)\n\nHow should I handle the advisory?",
|
||||
"header": "Advisory",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "Fix both files (Recommended)",
|
||||
"description": "Re-export retrySeconds from lib/retry-after.ts in both src/retry-worker.ts and src/retry-route.ts (−28 lines)."
|
||||
},
|
||||
{
|
||||
"label": "Fix worker only",
|
||||
"description": "Restore the re-export in src/retry-worker.ts (revert this diff); leave src/retry-route.ts's copy in place."
|
||||
},
|
||||
{
|
||||
"label": "Skip",
|
||||
"description": "Keep the inline copies as-is; record the advisory as skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "src/retry-route.ts is flagged assume-unchanged in the Git index (`git ls-files -v` shows `h`), so its on-disk edit (an appended comment) is invisible to `git status`/`git diff` and to the review snapshot. Should I clear that flag?",
|
||||
"header": "Index flag",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "Clear the flag (Recommended)",
|
||||
"description": "Run `git update-index --no-assume-unchanged src/retry-route.ts` so Git and future reviews see the file's real contents."
|
||||
},
|
||||
{
|
||||
"label": "Leave it",
|
||||
"description": "Don't touch the index flag; I'll note it in the review summary and exclude the path from reusable coverage."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -95,7 +95,7 @@ function keysReadInTree(root = ROOT): string[] {
|
||||
continue;
|
||||
}
|
||||
// Skip this file: its own prose cites example keys.
|
||||
if (ent.name === SELF) continue;
|
||||
if (ent.name === SELF || ent.name === 'CHANGELOG.md') continue;
|
||||
if (!/\.(md|ts|sh)$|^gstack-[a-z-]+$/.test(ent.name)) continue;
|
||||
let text: string;
|
||||
try {
|
||||
@@ -110,12 +110,29 @@ function keysReadInTree(root = ROOT): string[] {
|
||||
}
|
||||
|
||||
describe('gstack-config defaults (gate, free)', () => {
|
||||
test('retired checkpoint keys have no defaults or advertised configuration', () => {
|
||||
expect(fs.readFileSync(CONFIG_BIN, 'utf8')).not.toMatch(/checkpoint/i);
|
||||
for (const key of ['checkpoint_mode', 'checkpoint_push']) {
|
||||
expect(defaultArms()).not.toContain(key);
|
||||
expect(get(key)).toEqual({ out: '', code: 1 });
|
||||
}
|
||||
for (const command of ['list', 'defaults']) {
|
||||
const result = spawnSync('bash', [CONFIG_BIN, command], {
|
||||
encoding: 'utf8', timeout: 30_000,
|
||||
env: { PATH: process.env.PATH, HOME: STATE, GSTACK_STATE_ROOT: STATE },
|
||||
});
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).not.toMatch(/checkpoint_mode|checkpoint_push/);
|
||||
}
|
||||
});
|
||||
|
||||
test('workspace history does not add call sites to the source census', () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-config-census-'));
|
||||
try {
|
||||
fs.mkdirSync(path.join(root, '.context', 'old-checkout'), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, 'active.md'), 'gstack-config get question_tuning\n');
|
||||
fs.writeFileSync(path.join(root, '.context', 'old-checkout', 'old.md'), 'gstack-config get retired_workspace_key\n');
|
||||
fs.writeFileSync(path.join(root, 'CHANGELOG.md'), 'Previously used gstack-config get retired_release_key\n');
|
||||
expect(keysReadInTree(root)).toEqual(['question_tuning']);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
|
||||
@@ -62,7 +62,7 @@ afterAll(() => {
|
||||
* The STATUS-key contract. Post-Phase-2 these split into two consumers:
|
||||
* keys the rendered prose still interprets directly (SESSION_KIND,
|
||||
* CONDUCTOR_SESSION, SESSION_ID/TEL_START, EXPLAIN_LEVEL, QUESTION_TUNING,
|
||||
* PROACTIVE, SKILL_PREFIX, REPO_MODE, CHECKPOINT_*, GSTACK_PLAN_MODE,
|
||||
* PROACTIVE, SKILL_PREFIX, REPO_MODE, GSTACK_PLAN_MODE,
|
||||
* ARTIFACTS_SYNC, ...) and keys the script's OWN emission gates consume
|
||||
* (ACTIVATED, FIRST_TASK, LAKE_INTRO, TEL_PROMPTED, PROACTIVE_PROMPTED,
|
||||
* HAS_ROUTING, ROUTING_DECLINED, VENDORED_GSTACK, ...). Both classes stay in
|
||||
@@ -93,8 +93,6 @@ const PROSE_REFERENCED_KEYS = [
|
||||
'ROUTING_DECLINED',
|
||||
'VENDORED_GSTACK',
|
||||
'MODEL_OVERLAY',
|
||||
'CHECKPOINT_MODE',
|
||||
'CHECKPOINT_PUSH',
|
||||
'GSTACK_PLAN_MODE',
|
||||
'ARTIFACTS_SYNC',
|
||||
];
|
||||
@@ -135,6 +133,26 @@ describe('gstack-skill-start contract', () => {
|
||||
});
|
||||
|
||||
describe('gstack-skill-start behavior', () => {
|
||||
for (const legacy of [false, true]) {
|
||||
test(`checkpoint commits stay retired with ${legacy ? 'legacy opt-in' : 'fresh'} state`, () => {
|
||||
const state = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-ss-retired-'));
|
||||
const config = 'update_check: false\n' + (legacy ? 'checkpoint_mode: continuous\ncheckpoint_push: true\n' : '');
|
||||
fs.writeFileSync(path.join(state, 'config.yaml'), config);
|
||||
if (legacy) fs.writeFileSync(path.join(state, '.feature-prompted-continuous-checkpoint'), '');
|
||||
try {
|
||||
const output = runStart([], {
|
||||
GSTACK_HOME: state,
|
||||
GSTACK_CHECKPOINT_MODE: 'continuous',
|
||||
GSTACK_CHECKPOINT_PUSH: 'true',
|
||||
});
|
||||
expect(output).not.toMatch(/checkpoint|auto-commit|WIP:/i);
|
||||
expect(output).toContain('GSTACK_INSTRUCTION_BEGIN: feature-overlay');
|
||||
expect(fs.readFileSync(path.join(state, 'config.yaml'), 'utf8')).toBe(config);
|
||||
expect(fs.existsSync(path.join(state, '.feature-prompted-continuous-checkpoint'))).toBe(legacy);
|
||||
} finally { fs.rmSync(state, { recursive: true, force: true }); }
|
||||
});
|
||||
}
|
||||
|
||||
test('sanitizes GSTACK_INSTRUCTION markers out of passthrough output (OV4)', () => {
|
||||
// Poison the learnings passthrough: >5 entries triggers learnings-search
|
||||
// passthrough; simplest deterministic injection point is FIRST_TASK via a
|
||||
@@ -313,21 +331,6 @@ describe('gstack-skill-start behavior', () => {
|
||||
const localStart = path.join(projectSkillRoot, 'bin', 'gstack-skill-start');
|
||||
const env = { PATH: process.env.PATH!, HOME: tmpHome, GSTACK_HOME: freshGh };
|
||||
try {
|
||||
const checkpoint = execFileSync(localStart, ['--skill', 'testskill'], {
|
||||
timeout: 30_000,
|
||||
encoding: 'utf-8',
|
||||
cwd: projectRoot,
|
||||
env,
|
||||
});
|
||||
expect(checkpoint).toContain(
|
||||
`touch "${path.join(freshGh, '.feature-prompted-continuous-checkpoint')}"`,
|
||||
);
|
||||
expect(checkpoint).not.toContain('GSTACK_INSTRUCTION_BEGIN: feature-overlay');
|
||||
expect(checkpoint).not.toContain(
|
||||
path.join(projectSkillRoot, '.feature-prompted-continuous-checkpoint'),
|
||||
);
|
||||
|
||||
fs.writeFileSync(path.join(freshGh, '.feature-prompted-continuous-checkpoint'), '');
|
||||
const overlay = execFileSync(localStart, ['--skill', 'testskill'], {
|
||||
timeout: 30_000,
|
||||
encoding: 'utf-8',
|
||||
|
||||
@@ -5541,7 +5541,9 @@ export function planFloorDXReplyInput(visible: string, call: NativePlanQuestionC
|
||||
const first = lines.findIndex(line => /^ 1\. /.test(line));
|
||||
if (first < 0) return null;
|
||||
lines[first] = lines[first]!.replace(/^ 1\./, '❯ 1.');
|
||||
const pane = planFloorDXPane(lines.join('\n'), call);
|
||||
const pane = planFloorDXPane(lines.map(line => line.replace(
|
||||
/^(Enter to select · ↑\/↓ to navigate · (?:n to add notes · )?)ctrl\+g to edit in [^\x00-\x1f\x7f·]+ · (Esc to cancel)$/,
|
||||
'$1$2')).join('\n'), call);
|
||||
if (!pane || compact(pane) !== compact(state.pane)) return null;
|
||||
return state.stage === 'paste'
|
||||
? { input: '\x1b[200~' + state.reply + '\x1b[201~', stage: 'submit' }
|
||||
|
||||
@@ -339,7 +339,6 @@ if (evalsEnabled) {
|
||||
'.telemetry-prompted',
|
||||
'.proactive-prompted',
|
||||
'.first-loop-tip-shown',
|
||||
'.feature-prompted-continuous-checkpoint',
|
||||
'.feature-prompted-model-overlay',
|
||||
]) {
|
||||
const p = path.join(gstackDir, f);
|
||||
|
||||
@@ -213,7 +213,6 @@ export function seedHermeticGstackHome(gstackHome: string): void {
|
||||
'.telemetry-prompted',
|
||||
'.proactive-prompted',
|
||||
'.first-loop-tip-shown',
|
||||
'.feature-prompted-continuous-checkpoint',
|
||||
'.feature-prompted-model-overlay',
|
||||
]) {
|
||||
fs.writeFileSync(path.join(gstackHome, f), '');
|
||||
|
||||
@@ -835,9 +835,12 @@ function skippedReviewOption(question: any): any {
|
||||
option[field] !== undefined && typeof option[field] !== 'string')) return [];
|
||||
const label = option.label.replace(/[‘’]/g, "'").replace(/^\s*(?:[A-Z]|\d+)[.)]\s*/i, '')
|
||||
.replace(/\s*\(recommended\)\s*$/i, '').trim();
|
||||
const description = (option.description ?? '').replace(/[‘’]/g, "'").trim();
|
||||
const declinesChange = /^(?:do not|don't)\s+(?:apply|change|edit|fix|refactor|extract|modify|touch|clear|remove|update|replace|add|migrate|implement|reuse|import)\b/i;
|
||||
const rank = /^(?:skip|decline)(?=$|\s|[,.!])/i.test(label) ? 3
|
||||
: /^(?:do not|don't)\s+(?:apply|change|edit|fix|refactor|extract|modify|touch|clear|remove|update|replace|add|migrate|implement|reuse|import)\b/i.test(label) ? 2
|
||||
: /^(?:keep|leave)\b.*\b(?:current|existing|unchanged|untouched|as[- ]is|alone|set|copies|copy|implementation|code|source)\b/i.test(label) ? 1 : 0;
|
||||
: declinesChange.test(label) ? 2
|
||||
: /^(?:keep|leave)\b.*\b(?:current|existing|unchanged|untouched|as[- ]is|alone|set|copies|copy|implementation|code|source)\b/i.test(label)
|
||||
|| (/^(?:keep|leave)\b/i.test(label) && declinesChange.test(description)) ? 1 : 0;
|
||||
if (!rank) return [];
|
||||
// A leading decline names rejected work. Classify later commitments rather
|
||||
// than action words inside recorded metadata or hypothetical consequences.
|
||||
|
||||
@@ -287,3 +287,16 @@ export function sliceBetween(text: string, start: string, end: string): string {
|
||||
if (j < 0) throw new Error(`skill fixture: end marker not found after start: ${end}`);
|
||||
return text.slice(i, j);
|
||||
}
|
||||
|
||||
export function extractDesignResearchContract(skill: string): string {
|
||||
const setup = sliceBetween(skill, '## BROWSER SETUP', '### Rules for driving a real browser');
|
||||
const probe = setup.match(/```bash\n[\s\S]*?\n```/)?.[0];
|
||||
if (!probe) throw new Error('skill fixture: design research readiness probe missing');
|
||||
const routing = sliceBetween(skill, '## Web research runs in Aside', '## Phase 2: Research');
|
||||
const search = sliceBetween(skill, '**Step 1: Identify', '**Step 2: Visual research');
|
||||
const prelude = search.match(/^_EG=.*_aside_exec\(\).*$/m)?.[0];
|
||||
if (!prelude) throw new Error('skill fixture: design research egress prelude missing');
|
||||
return ['Run this readiness probe once before research:', probe, routing,
|
||||
'For each Aside research call, include this prelude before invoking `_aside_exec` with the requested query:',
|
||||
'```bash', prelude, '```'].join('\n\n');
|
||||
}
|
||||
|
||||
@@ -21,15 +21,15 @@
|
||||
* Each test lists the file patterns that, if changed, require the test to run.
|
||||
*/
|
||||
export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
'shared-libs-review-path-eligibility': ['review/**', 'scripts/resolvers/shared-libs.ts', 'scripts/resolvers/review.ts', 'scripts/resolvers/review-army.ts', 'lib/review-evidence.ts', 'bin/gstack-review-log', 'bin/gstack-review-read', 'bin/gstack-wtree', 'test/helpers/shared-libs-eval-fixture.ts', 'test/skill-e2e-shared-libs-paths.test.ts', 'test/helpers/shared-libs-path-fixture.ts', 'test/shared-libs-fixture.test.ts', 'test/helpers/e2e-gate.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/agent-sdk-runner.ts', 'lib/claude-bin.ts', 'lib/eval-model.ts', 'test/fixtures/shared-libs-index-flags-skip-question.json', 'test/shared-libs-revalidation-prompt.test.ts'],
|
||||
'shared-libs-review-index-flags': ['review/**', 'scripts/resolvers/shared-libs.ts', 'scripts/resolvers/review.ts', 'scripts/resolvers/review-army.ts', 'lib/review-evidence.ts', 'bin/gstack-review-log', 'bin/gstack-review-read', 'bin/gstack-wtree', 'test/helpers/shared-libs-eval-fixture.ts', 'test/skill-e2e-shared-libs-paths.test.ts', 'test/helpers/shared-libs-path-fixture.ts', 'test/shared-libs-fixture.test.ts', 'test/helpers/e2e-gate.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/agent-sdk-runner.ts', 'lib/claude-bin.ts', 'lib/eval-model.ts', 'test/fixtures/shared-libs-index-flags-skip-question.json', 'test/shared-libs-revalidation-prompt.test.ts', 'test/fixtures/shared-libs-paths-max-turns-public.json'],
|
||||
'shared-libs-review-prior-coverage': ['review/**', 'scripts/resolvers/shared-libs.ts', 'scripts/resolvers/review.ts', 'scripts/resolvers/review-army.ts', 'lib/review-evidence.ts', 'bin/gstack-review-log', 'bin/gstack-review-read', 'bin/gstack-wtree', 'test/helpers/shared-libs-eval-fixture.ts', 'test/skill-e2e-shared-libs-paths.test.ts', 'test/helpers/shared-libs-path-fixture.ts', 'test/shared-libs-fixture.test.ts', 'test/helpers/e2e-gate.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/agent-sdk-runner.ts', 'lib/claude-bin.ts', 'lib/eval-model.ts', 'test/fixtures/shared-libs-index-flags-skip-question.json', 'test/shared-libs-revalidation-prompt.test.ts'],
|
||||
'shared-libs-review-path-eligibility': ['review/**', 'scripts/resolvers/shared-libs.ts', 'scripts/resolvers/review.ts', 'scripts/resolvers/review-army.ts', 'lib/review-evidence.ts', 'bin/gstack-review-log', 'bin/gstack-review-read', 'bin/gstack-wtree', 'test/helpers/shared-libs-eval-fixture.ts', 'test/skill-e2e-shared-libs-paths.test.ts', 'test/helpers/shared-libs-path-fixture.ts', 'test/shared-libs-fixture.test.ts', 'test/helpers/e2e-gate.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/agent-sdk-runner.ts', 'lib/claude-bin.ts', 'lib/eval-model.ts', 'test/fixtures/shared-libs-index-flags-*.json', 'test/shared-libs-revalidation-prompt.test.ts'],
|
||||
'shared-libs-review-index-flags': ['review/**', 'scripts/resolvers/shared-libs.ts', 'scripts/resolvers/review.ts', 'scripts/resolvers/review-army.ts', 'lib/review-evidence.ts', 'bin/gstack-review-log', 'bin/gstack-review-read', 'bin/gstack-wtree', 'test/helpers/shared-libs-eval-fixture.ts', 'test/skill-e2e-shared-libs-paths.test.ts', 'test/helpers/shared-libs-path-fixture.ts', 'test/shared-libs-fixture.test.ts', 'test/helpers/e2e-gate.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/agent-sdk-runner.ts', 'lib/claude-bin.ts', 'lib/eval-model.ts', 'test/fixtures/shared-libs-index-flags-*.json', 'test/shared-libs-revalidation-prompt.test.ts', 'test/fixtures/shared-libs-paths-max-turns-public.json'],
|
||||
'shared-libs-review-prior-coverage': ['review/**', 'scripts/resolvers/shared-libs.ts', 'scripts/resolvers/review.ts', 'scripts/resolvers/review-army.ts', 'lib/review-evidence.ts', 'bin/gstack-review-log', 'bin/gstack-review-read', 'bin/gstack-wtree', 'test/helpers/shared-libs-eval-fixture.ts', 'test/skill-e2e-shared-libs-paths.test.ts', 'test/helpers/shared-libs-path-fixture.ts', 'test/shared-libs-fixture.test.ts', 'test/helpers/e2e-gate.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/agent-sdk-runner.ts', 'lib/claude-bin.ts', 'lib/eval-model.ts', 'test/fixtures/shared-libs-index-flags-*.json', 'test/shared-libs-revalidation-prompt.test.ts'],
|
||||
'shared-libs-codex-read-only': ['deslop-shared-libs/**', 'scripts/resolvers/shared-libs.ts', 'scripts/resolvers/index.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/shared-libs-eval-fixture.ts', 'test/helpers/codex-session-runner.ts', 'test/helpers/skill-fixture.ts', 'test/helpers/hermetic-env.ts', 'test/helpers/eval-budgets.ts', 'test/codex-e2e-shared-libs.test.ts', 'test/shared-libs-fixture.test.ts', 'test/helpers/e2e-gate.ts', 'hosts/codex.ts', 'hosts/define-host.ts', 'scripts/resolvers/constants.ts', 'test/fixtures/shared-libs-readonly-substitution-ci16358.json'],
|
||||
// Shared-code audit and scoped review lifecycle
|
||||
'shared-libs-read-only': ['deslop-shared-libs/**', 'scripts/resolvers/shared-libs.ts', 'scripts/resolvers/index.ts', 'test/helpers/shared-libs-eval-fixture.ts', 'test/skill-e2e-shared-libs.test.ts', 'test/shared-libs-fixture.test.ts', 'test/helpers/e2e-gate.ts', 'scripts/gen-skill-docs.ts', 'lib/claude-bin.ts', 'lib/eval-model.ts', 'test/fixtures/shared-libs-readonly-substitution-ci16358.json'],
|
||||
'shared-libs-unsupported-git': ['deslop-shared-libs/**', 'scripts/resolvers/shared-libs.ts', 'scripts/resolvers/index.ts', 'test/helpers/shared-libs-eval-fixture.ts', 'test/skill-e2e-shared-libs.test.ts', 'test/shared-libs-fixture.test.ts', 'test/helpers/e2e-gate.ts', 'scripts/gen-skill-docs.ts', 'lib/claude-bin.ts', 'lib/eval-model.ts', 'test/fixtures/shared-libs-readonly-substitution-ci16358.json'],
|
||||
'shared-libs-review-lifecycle': ['deslop-shared-libs/**', 'scripts/resolvers/shared-libs.ts', 'scripts/resolvers/index.ts', 'test/helpers/shared-libs-eval-fixture.ts', 'review/**', 'scripts/resolvers/review.ts', 'scripts/resolvers/review-army.ts', 'lib/review-evidence.ts', 'bin/gstack-review-log', 'bin/gstack-review-read', 'bin/gstack-wtree', 'test/skill-e2e-shared-libs.test.ts', 'test/shared-libs-fixture.test.ts', 'test/helpers/e2e-gate.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/agent-sdk-runner.ts', 'lib/claude-bin.ts', 'lib/eval-model.ts', 'test/fixtures/shared-libs-index-flags-skip-question.json'],
|
||||
'shared-libs-review-revalidation': ['deslop-shared-libs/**', 'scripts/resolvers/shared-libs.ts', 'scripts/resolvers/index.ts', 'test/helpers/shared-libs-eval-fixture.ts', 'review/**', 'scripts/resolvers/review.ts', 'scripts/resolvers/review-army.ts', 'lib/review-evidence.ts', 'bin/gstack-review-log', 'bin/gstack-review-read', 'bin/gstack-wtree', 'test/skill-e2e-shared-libs.test.ts', 'test/shared-libs-fixture.test.ts', 'test/helpers/e2e-gate.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/agent-sdk-runner.ts', 'lib/claude-bin.ts', 'lib/eval-model.ts', 'test/helpers/shared-libs-review-start-evidence.ts', 'test/shared-libs-review-start-evidence.test.ts', 'test/fixtures/shared-libs-review-start-public.json', 'test/shared-libs-revalidation-prompt.test.ts', 'test/fixtures/shared-libs-revalidation-max-turns-public.json', 'test/fixtures/shared-libs-index-flags-skip-question.json'],
|
||||
'shared-libs-review-lifecycle': ['deslop-shared-libs/**', 'scripts/resolvers/shared-libs.ts', 'scripts/resolvers/index.ts', 'test/helpers/shared-libs-eval-fixture.ts', 'review/**', 'scripts/resolvers/review.ts', 'scripts/resolvers/review-army.ts', 'lib/review-evidence.ts', 'bin/gstack-review-log', 'bin/gstack-review-read', 'bin/gstack-wtree', 'test/skill-e2e-shared-libs.test.ts', 'test/shared-libs-fixture.test.ts', 'test/helpers/e2e-gate.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/agent-sdk-runner.ts', 'lib/claude-bin.ts', 'lib/eval-model.ts', 'test/fixtures/shared-libs-index-flags-*.json'],
|
||||
'shared-libs-review-revalidation': ['deslop-shared-libs/**', 'scripts/resolvers/shared-libs.ts', 'scripts/resolvers/index.ts', 'test/helpers/shared-libs-eval-fixture.ts', 'review/**', 'scripts/resolvers/review.ts', 'scripts/resolvers/review-army.ts', 'lib/review-evidence.ts', 'bin/gstack-review-log', 'bin/gstack-review-read', 'bin/gstack-wtree', 'test/skill-e2e-shared-libs.test.ts', 'test/shared-libs-fixture.test.ts', 'test/helpers/e2e-gate.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/agent-sdk-runner.ts', 'lib/claude-bin.ts', 'lib/eval-model.ts', 'test/helpers/shared-libs-review-start-evidence.ts', 'test/shared-libs-review-start-evidence.test.ts', 'test/fixtures/shared-libs-review-start-public.json', 'test/shared-libs-revalidation-prompt.test.ts', 'test/fixtures/shared-libs-revalidation-max-turns-public.json', 'test/fixtures/shared-libs-index-flags-*.json'],
|
||||
'shared-libs-opportunity-judgment': ['deslop-shared-libs/**', 'scripts/resolvers/shared-libs.ts', 'scripts/resolvers/index.ts', 'test/helpers/shared-libs-eval-fixture.ts', 'test/skill-e2e-shared-libs-periodic.test.ts', 'test/shared-libs-fixture.test.ts', 'test/helpers/e2e-gate.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/llm-judge.ts', 'lib/claude-bin.ts', 'lib/eval-model.ts', 'test/fixtures/shared-libs-readonly-substitution-ci16358.json'],
|
||||
'shared-libs-pr-coverage': ['deslop-shared-libs/**', 'scripts/resolvers/shared-libs.ts', 'scripts/resolvers/index.ts', 'test/helpers/shared-libs-eval-fixture.ts', 'test/skill-e2e-shared-libs-periodic.test.ts', 'test/shared-libs-fixture.test.ts', 'test/helpers/e2e-gate.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/llm-judge.ts', 'lib/claude-bin.ts', 'lib/eval-model.ts', 'test/fixtures/shared-libs-readonly-substitution-ci16358.json'],
|
||||
'shared-libs-plan-callers': ['test/helpers/shared-libs-plan-actor.ts', 'test/shared-libs-plan-actor.test.ts', 'scripts/resolvers/confidence.ts', 'test/helpers/shared-libs-plan-excerpt.ts', 'test/shared-libs-rendering.test.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'deslop-shared-libs/**', 'scripts/resolvers/shared-libs.ts', 'scripts/resolvers/index.ts', 'test/helpers/shared-libs-eval-fixture.ts', 'plan-eng-review/**', 'test/skill-e2e-shared-libs-periodic.test.ts', 'test/eng-scope-entry-ap.test.ts', 'test/plan-scope-recovery-av.test.ts', 'test/fixtures/plan-scope-recovery-av.json', 'test/review-entry-and-design-clarity-au.test.ts', 'scripts/resolvers/preamble/generate-preamble-bash.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'test/shared-libs-fixture.test.ts', 'test/helpers/e2e-gate.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/agent-sdk-runner.ts', 'test/helpers/llm-judge.ts', 'lib/claude-bin.ts', 'lib/eval-model.ts'],
|
||||
@@ -140,6 +140,7 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/tasks-section.ts'
|
||||
],
|
||||
'plan-eng-review': ['test/session-runner-stream-lifecycle.test.ts', 'test/paid-retry-supervision.test.ts',
|
||||
'test/eng-review-routing.test.ts',
|
||||
'scripts/resolvers/learnings.ts',
|
||||
"test/plan-scope-recovery-av.test.ts",
|
||||
"test/fixtures/plan-scope-recovery-av.json", 'test/eng-scope-entry-ap.test.ts', 'plan-eng-review/**', 'test/skill-e2e-plan.test.ts',
|
||||
@@ -147,6 +148,7 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
'scripts/resolvers/testing.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/review.ts', 'test/plan-review-cases.test.ts'
|
||||
],
|
||||
'plan-eng-review-artifact': ['test/session-runner-stream-lifecycle.test.ts', 'test/paid-retry-supervision.test.ts',
|
||||
'test/eng-review-routing.test.ts',
|
||||
'scripts/resolvers/learnings.ts',
|
||||
"test/plan-scope-recovery-av.test.ts",
|
||||
"test/fixtures/plan-scope-recovery-av.json", 'test/eng-scope-entry-ap.test.ts', 'plan-eng-review/**', 'test/skill-e2e-plan.test.ts',
|
||||
@@ -154,6 +156,7 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
'scripts/resolvers/testing.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/review.ts', 'test/plan-review-cases.test.ts'
|
||||
],
|
||||
'plan-review-report': ['test/session-runner-stream-lifecycle.test.ts', 'test/paid-retry-supervision.test.ts',
|
||||
'test/eng-review-routing.test.ts',
|
||||
'test/helpers/office-hours-attempt.ts', 'test/office-hours-attempt.test.ts',
|
||||
'test/plan-review-report-recording.test.ts', 'test/fixtures/plan-review-report-public.json',
|
||||
'scripts/resolvers/learnings.ts',
|
||||
@@ -745,7 +748,7 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
'test/pty-workspace-trust.test.ts', 'test/fixtures/pty-companion-cli.ts', 'lib/fs-atomic.ts', 'test/helpers/pty-current-screen.ts', 'test/pty-current-screen.test.ts', 'test/fixtures/native-viewport.ts', 'test/helpers/plan-skill-questions.ts', 'test/fixtures/eng-auq-validation-error.json', 'test/fixtures/bash-directory-permission.json', 'test/fixtures/design-tasks-bash-permission.json', 'test/plan-skill-read-permission.test.ts', 'test/fixtures/read-permission.json', 'test/pty-numbered-option-indent-native.test.ts', 'test/fixtures/ceo-split-e5-numbered-description-491.json', 'test/plan-skill-questions.test.ts', 'test/helpers/plan-skill-question-events.ts', 'test/plan-skill-question-events.test.ts', 'test/helpers/plan-skill-question-hook-scope.ts', 'test/helpers/skill-census.ts', 'test/plan-skill-question-hook-scope.test.ts', 'test/helpers/ceo-finding-fixture.ts', 'test/ceo-finding-fixture.test.ts', 'test/plan-design-floor-fixture.test.ts'
|
||||
],
|
||||
'plan-devex-finding-floor': [
|
||||
'test/plan-floor-dx-actor.test.ts', 'test/fixtures/plan-floor-dx-custom-491.json',
|
||||
'test/plan-floor-dx-actor.test.ts', 'test/fixtures/plan-floor-dx-custom-491.json', 'test/fixtures/plan-floor-dx-editor-hint.json',
|
||||
'test/paid-retry-supervision.test.ts',
|
||||
'lib/claude-public-transcript.ts', 'test/plan-create-prepublication.test.ts', 'test/fixtures/plan-create-prepublication-491.json', 'test/plan-create-combined-permission.test.ts', 'test/fixtures/plan-create-combined-permission-70b.json', 'test/fixtures/plan-floor-quote-70b.json', 'test/fixtures/plan-floor-product-type-70b.json',
|
||||
'test/plan-create-permission.test.ts',
|
||||
@@ -1111,7 +1114,7 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
// Design
|
||||
'design-consultation-core': ['test/session-runner-stream-lifecycle.test.ts', 'design-consultation/**', 'lib/design-catalog.ts', 'lib/design-md.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/llm-judge.ts', 'test/skill-e2e-design.test.ts', 'scripts/resolvers/design.ts', 'scripts/resolvers/outside-voice.ts', 'design-consultation/sections/**', 'test/design-consultation-contract.test.ts'],
|
||||
'design-consultation-existing': ['test/session-runner-stream-lifecycle.test.ts', 'design-consultation/**', 'lib/design-md.ts', 'bin/gstack-design-md.ts', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-design.test.ts'],
|
||||
'design-consultation-research': ['test/session-runner-stream-lifecycle.test.ts', 'design-consultation/**', 'scripts/resolvers/aside.ts', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-design.test.ts', 'scripts/resolvers/design.ts', 'scripts/resolvers/outside-voice.ts', 'design-consultation/sections/**', 'test/design-consultation-contract.test.ts'],
|
||||
'design-consultation-research': ['test/session-runner-stream-lifecycle.test.ts', 'design-consultation/**', 'scripts/resolvers/aside.ts', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-design.test.ts', 'test/helpers/skill-fixture.ts', 'test/design-research-fixture.test.ts', 'scripts/resolvers/design.ts', 'scripts/resolvers/outside-voice.ts', 'design-consultation/sections/**', 'test/design-consultation-contract.test.ts'],
|
||||
'design-consultation-preview': ['test/session-runner-stream-lifecycle.test.ts', 'design-consultation/**', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-design.test.ts', 'test/design-board-reload.test.ts'],
|
||||
'plan-design-review-no-ui-scope': ['test/session-runner-stream-lifecycle.test.ts',
|
||||
"test/plan-scope-recovery-av.test.ts",
|
||||
@@ -1782,6 +1785,7 @@ export const LLM_JUDGE_TOUCHFILES: Record<string, string[]> = {
|
||||
|
||||
// Ship & Release
|
||||
'ship/SKILL.md workflow': ['ship/SKILL.md', 'ship/SKILL.md.tmpl', 'test/skill-llm-eval.test.ts', 'test/helpers/workflow-judge-input.ts', 'test/helpers/workflow-judge-cache.ts', 'test/workflow-judge-cache.test.ts', 'scripts/eval-input-cache.ts', 'test/eval-input-cache.test.ts', 'test/workflow-judge-input.test.ts', 'test/helpers/workflow-excerpt.ts',
|
||||
'test/ship-workflow-clarity.test.ts', 'scripts/resolvers/review.ts',
|
||||
'scripts/resolvers/testing.ts', 'ship/sections/**'
|
||||
],
|
||||
'document-release/SKILL.md workflow': ['document-release/SKILL.md', 'document-release/SKILL.md.tmpl', 'test/skill-llm-eval.test.ts', 'test/helpers/workflow-judge-input.ts', 'test/helpers/workflow-judge-cache.ts', 'test/workflow-judge-cache.test.ts', 'scripts/eval-input-cache.ts', 'test/eval-input-cache.test.ts', 'test/workflow-judge-input.test.ts', 'test/helpers/workflow-excerpt.ts'],
|
||||
@@ -1791,6 +1795,7 @@ export const LLM_JUDGE_TOUCHFILES: Record<string, string[]> = {
|
||||
'scripts/resolvers/preamble/generate-ask-user-format.ts'
|
||||
],
|
||||
'plan-eng-review/SKILL.md sections': [
|
||||
'test/eng-review-routing.test.ts', 'scripts/resolvers/gbrain.ts',
|
||||
'scripts/resolvers/learnings.ts',
|
||||
"test/plan-scope-recovery-av.test.ts",
|
||||
"test/fixtures/plan-scope-recovery-av.json", 'test/eng-scope-entry-ap.test.ts', 'plan-eng-review/SKILL.md', 'plan-eng-review/SKILL.md.tmpl', 'test/skill-llm-eval.test.ts', 'test/helpers/workflow-judge-input.ts', 'test/helpers/workflow-judge-cache.ts', 'test/workflow-judge-cache.test.ts', 'scripts/eval-input-cache.ts', 'test/eval-input-cache.test.ts', 'test/workflow-judge-input.test.ts', 'test/helpers/workflow-excerpt.ts',
|
||||
|
||||
@@ -62,7 +62,6 @@ describe('hermetic wiring tripwire', () => {
|
||||
|
||||
test('feature prompt acknowledgements are seeded in GSTACK_HOME everywhere', () => {
|
||||
const markers = [
|
||||
'.feature-prompted-continuous-checkpoint',
|
||||
'.feature-prompted-model-overlay',
|
||||
];
|
||||
// CI seeding lives in the composite action (v1.77 moved it out of the
|
||||
|
||||
@@ -31,7 +31,6 @@ const MOVED: Record<string, string> = {
|
||||
'spawned-session': 'spawned by an AI orchestrator',
|
||||
'privacy-stop-gate': 'How much should sync?',
|
||||
'upgrade-flow': 'Inline upgrade flow',
|
||||
'feature-discovery': 'Continuous checkpoint auto-commits',
|
||||
};
|
||||
|
||||
function generatedSkillFiles(): string[] {
|
||||
@@ -49,6 +48,14 @@ function generatedSkillFiles(): string[] {
|
||||
}
|
||||
|
||||
describe('onboarding moved-literals tombstone (F5)', () => {
|
||||
test('checkpoint prompts and automatic WIP instructions are absent from runtime and skills', () => {
|
||||
expect(SCRIPT).not.toMatch(/checkpoint|auto-commit|WIP:/i);
|
||||
for (const file of generatedSkillFiles()) {
|
||||
expect(fs.readFileSync(file, 'utf8'), path.relative(ROOT, file))
|
||||
.not.toMatch(/Continuous Checkpoint Mode|CHECKPOINT_MODE|CHECKPOINT_PUSH|\[gstack-context\]|wip-context-before-squash/);
|
||||
}
|
||||
});
|
||||
|
||||
test('every moved flow lives in bin/gstack-skill-start', () => {
|
||||
const missing = Object.entries(MOVED).filter(([, lit]) => !SCRIPT.includes(lit));
|
||||
expect(
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import {expect,test} from 'bun:test';
|
||||
import {planFloorDXPane,planFloorDXReplyInput,matchesNativePlanQuestion,type PlanFloorDXReply} from './helpers/claude-pty-runner';
|
||||
import captured from './fixtures/plan-floor-dx-custom-491.json';
|
||||
import editorHints from './fixtures/plan-floor-dx-editor-hint.json';
|
||||
import {E2E_TOUCHFILES,selectTests} from './helpers/touchfiles';
|
||||
const call=captured.call;
|
||||
const state=(stage:PlanFloorDXReply['stage']='focus'):PlanFloorDXReply=>({call:structuredClone(call),
|
||||
pane:planFloorDXPane(captured.questionViewport,call)!,reply:captured.reply,stage});
|
||||
|
||||
test('editor-hint capture changes select the live DX finding-floor probe',()=>{
|
||||
expect(selectTests(['test/fixtures/plan-floor-dx-editor-hint.json'],E2E_TOUCHFILES,[]).selected)
|
||||
.toContain('plan-devex-finding-floor');
|
||||
});
|
||||
|
||||
test('generic matcher authenticates the crop while DX custom replies still require the complete pane',()=>{
|
||||
expect(matchesNativePlanQuestion(captured.originalViewport,call)).toBe(true);
|
||||
expect(planFloorDXPane(captured.originalViewport,call)).toBeNull();
|
||||
@@ -72,3 +79,27 @@ test('a short complete native setup still binds while an arbitrary shorter prefi
|
||||
expect(planFloorDXPane('☐ Empathy\n'+short.questions[0]!.question+'\n'+menu,short)).not.toBeNull();
|
||||
expect(planFloorDXPane('☐ Empathy\n'+call.questions[0]!.question.slice(0,300)+'…\n'+menu,call)).toBeNull();
|
||||
});
|
||||
|
||||
test.each(editorHints.cases)('captured attempt $attempt accepts the native editor hint only after custom-field focus',capture=>{
|
||||
const reply:PlanFloorDXReply={call:capture.call,pane:capture.questionViewport,reply:capture.reply,stage:'focus'};
|
||||
expect(planFloorDXReplyInput(capture.questionViewport,capture.call,reply)).toEqual({input:'4',stage:'paste'});
|
||||
expect(planFloorDXReplyInput(capture.focusedViewport,capture.call,{...reply,stage:'paste'}))
|
||||
.toEqual({input:'\x1b[200~'+capture.reply+'\x1b[201~',stage:'submit'});
|
||||
expect(planFloorDXReplyInput(capture.focusedViewport,capture.call,reply)).toBeNull();
|
||||
for(const changed of [
|
||||
capture.focusedViewport.replace('ctrl+g to edit in Vim','ctrl+g to approve'),
|
||||
capture.focusedViewport.replace('ctrl+g to edit in Vim','ctrl+g to edit in Vim · ctrl+g to edit in Vim'),
|
||||
capture.focusedViewport.replace('Type something.','Approve everything'),
|
||||
capture.focusedViewport+'\nForeign question is now active.',
|
||||
]) expect(planFloorDXReplyInput(changed,capture.call,{...reply,stage:'paste'})).toBeNull();
|
||||
});
|
||||
|
||||
test.each(['Vim','Nano','Visual Studio Code'])('custom input with %s hint still verifies the exact reply before submission',editor=>{
|
||||
const hint=(text:string)=>text.replace(' · Esc to cancel',` · ctrl+g to edit in ${editor} · Esc to cancel`);
|
||||
expect(planFloorDXReplyInput(hint(captured.focusedViewport),call,state('paste')))
|
||||
.toEqual({input:'\x1b[200~'+captured.reply+'\x1b[201~',stage:'submit'});
|
||||
expect(planFloorDXReplyInput(hint(captured.filledViewport),call,state('submit'))).toEqual({input:'\r',stage:'done'});
|
||||
expect(planFloorDXReplyInput(hint(captured.focusedViewport),call,state('submit'))).toBeNull();
|
||||
expect(planFloorDXReplyInput(hint(captured.filledViewport).replace('Confirmed review context:','Unapproved context:'),call,state('submit'))).toBeNull();
|
||||
expect(planFloorDXReplyInput(hint(captured.filledViewport),{...call,answered:true},state('submit'))).toBeNull();
|
||||
});
|
||||
|
||||
@@ -297,8 +297,9 @@ test('Eng loads its one remedy procedure before Scope Challenge findings and ret
|
||||
expect(complexityRule).toBeLessThan(scope.indexOf('1. Explain the complexity'));
|
||||
expect(compactProse(scope.slice(complexityRule))).toContain('Ask and wait before changes');
|
||||
expect(compactProse(scope.slice(complexityRule))).toContain('Save the actual feature and structure answers as one scope record');
|
||||
expect(compactProse(scope.slice(complexityRule))).toContain('Save this record under the write policy');
|
||||
expect(compactProse(scope.slice(complexityRule))).toContain('no retroactive pending record');
|
||||
expect(compactProse(scope.slice(complexityRule))).toContain('post-answer scope summary, not a remedy\'s pending ledger record');
|
||||
expect(compactProse(scope.slice(complexityRule))).toContain('Save it under the write policy and Read it back against the actual answers');
|
||||
expect(compactProse(scope.slice(complexityRule))).toContain('Do not invent a pre-answer record afterward');
|
||||
expect(scope).not.toContain('proceed as-is');
|
||||
const stop = skeleton.indexOf('**STOP while a Scope Challenge complexity question');
|
||||
const sectionRead = skeleton.indexOf(suffix ? '{{SECTION:review-sections}}' : '> **STOP.** Before starting the Scope Challenge');
|
||||
@@ -307,8 +308,9 @@ test('Eng loads its one remedy procedure before Scope Challenge findings and ret
|
||||
expect(skeleton).toContain('Scope Challenge is mandatory before Section 1');
|
||||
expect(skeleton.split(suffix ? '{{SECTION:review-sections}}' : '> **STOP.** Before starting the Scope Challenge')).toHaveLength(2);
|
||||
expect(compactProse(scope)).toContain('apply only accepted scope changes');
|
||||
expect(compactProse(sections)).toContain("follow the preparation sections below through Confidence Calibration");
|
||||
expect(compactProse(sections)).toContain("Read Decision procedure as the rule for later choices. Start the review at Scope Challenge, then complete Sections 1–4 in order");
|
||||
expect(compactProse(sections)).toContain('After startup, prepare in this order:');
|
||||
expect(compactProse(sections)).toContain('Read **Confidence Calibration** and **Decision procedure** as rules, not review passes');
|
||||
expect(compactProse(sections)).toContain('Then run **Scope Challenge A → B → C**, followed by Sections 1–4 in order');
|
||||
const preparationOrder = ['## Review record and write policy',
|
||||
suffix ? '{{LEARNINGS_SEARCH}}' : '## Prior Learnings', '## Retrospective learning',
|
||||
suffix ? '{{CONFIDENCE_CALIBRATION}}' : '## Confidence Calibration',
|
||||
@@ -328,14 +330,14 @@ test('Eng loads its one remedy procedure before Scope Challenge findings and ret
|
||||
expect(compactProse(scope)).toContain('accepted/rejected/deferred/pending');
|
||||
expect(compactProse(scope)).toContain('"No issues found" for an empty list');
|
||||
expect(compactProse(scope)).toContain('Findings and scope answers approve no remedies');
|
||||
const scopeFinish = ['Below the threshold, start at step 1', '1. Present numbered Scope Challenge findings',
|
||||
const scopeFinish = ["Below both thresholds, skip B's questions and go directly to **C. Resolve findings**", '### C. Resolve findings', '1. Present numbered Scope Challenge findings',
|
||||
'2. Resolve each remedy through Decision procedure', '3. Report accepted/rejected/deferred/pending dispositions from those answers',
|
||||
'Continue to Section 1 only when no answer is pending'].map(step => scope.indexOf(step));
|
||||
expect(scopeFinish.every(position => position >= 0)).toBe(true);
|
||||
expect(scopeFinish).toEqual([...scopeFinish].sort((a, b) => a - b));
|
||||
const selfCheck = compactProse(skeleton.slice(skeleton.indexOf('## Section self-check'), skeleton.indexOf('**Paused question:**')));
|
||||
const selfCheck = compactProse(skeleton.slice(skeleton.indexOf('## Section self-check'), skeleton.indexOf(suffix ? '{{EXIT_PLAN_MODE_GATE}}' : '## EXIT PLAN MODE GATE')));
|
||||
expect(selfCheck).toContain('Confirm you read the section and completed Scope Challenge, Sections 1–4, Outside Voice and outputs');
|
||||
expect(selfCheck).toContain('If evidence is missing, Read `sections/review-sections.md` and repair only gaps through its decision/output recovery steps');
|
||||
expect(selfCheck).toContain('If evidence is missing, Read `sections/review-sections.md` and use Recovery routing above');
|
||||
expect(selfCheck).toContain('Preserve verified work');
|
||||
expect(selfCheck).not.toContain('Redo memory-only work');
|
||||
const stages = skeleton.indexOf('After target selection, every question uses');
|
||||
@@ -441,7 +443,11 @@ describe('Eng approved-work decision gate', () => {
|
||||
const outputs = template.split('## Required outputs')[1]!.split('### "NOT in scope"')[0]!;
|
||||
expect(compactProse(outputs)).toContain("Derive unresolved choices from each record's current State, actual answer and accepted scope");
|
||||
expect(compactProse(outputs)).toContain("Run this finish sequence after Approval readiness passes");
|
||||
expect(compactProse(outputs)).toContain("On recovery, resume at the failed step. Reuse a successful Review Log for unchanged saved outputs; changed outputs must pass steps 1–4 again");
|
||||
expect(compactProse(outputs)).toContain("For recovery or changed outputs, use the entrypoint's **Recovery routing**");
|
||||
const recovery = compactProse(readFileSync('plan-eng-review/SKILL.md.tmpl', 'utf8'));
|
||||
expect(recovery).toContain('Resume at the failed step using Recovery routing');
|
||||
expect(recovery).toContain('Required outputs steps 1–4 for changed outputs before choosing navigation again');
|
||||
expect(recovery).toContain('Unchanged saved outputs may reuse their successful Review Log');
|
||||
});
|
||||
|
||||
test('identifies commitments before comparing values, then saves before asking', () => {
|
||||
@@ -572,7 +578,10 @@ describe('Eng approved-work decision gate', () => {
|
||||
expect(compactProse(save)).toContain("When revising, replace the whole current payload");
|
||||
expect(compactProse(save)).toContain("Do not leave duplicate Question, Header or Options fields");
|
||||
expect(compactProse(save)).toContain("present the complete record and grid as **not persisted**");
|
||||
expect(compactProse(save)).toContain("an unreadable or unverifiable record follows the write policy's recovery and then **Blocked outcome**");
|
||||
expect(compactProse(save)).toContain('unreadable or unverifiable records use **Recovery routing**');
|
||||
const recovery = compactProse(readFileSync('plan-eng-review/SKILL.md.tmpl', 'utf8'));
|
||||
expect(recovery).toContain('Use that step\'s stated recovery, then repeat its full Read-back verification');
|
||||
expect(recovery).toContain('If no recovery is specified or it fails, follow **Blocked outcome**');
|
||||
expect(compactProse(save)).toContain("If any payload field changes, including a shortened label or formatting edit, repeat step 3, replace the whole saved payload and Read it again");
|
||||
expect(compactProse(send)).toContain("Copy the verified question, header, labels and descriptions literally");
|
||||
expect(compactProse(send)).toContain("Do not add or strip brief paragraphs or rebuild options");
|
||||
@@ -717,7 +726,7 @@ describe('Eng approved-work decision gate', () => {
|
||||
expect(compactProse(policy)).toContain("Present each completely as **not persisted** and continue");
|
||||
expect(compactProse(policy)).toContain("Ask for a permitted destination if the user can supply one; wait without completion telemetry");
|
||||
expect(compactProse(policy)).toContain("If none is permitted, complete the review in chat as **not persisted**, then use **Blocked outcome**");
|
||||
expect(compactProse(policy)).toContain("Use the failed step's stated recovery; if saving or read-back still fails, take **Blocked outcome**");
|
||||
expect(compactProse(policy)).toContain('A failed permitted save uses **Recovery routing → Repairable write/read failure**, not the forbidden-write branches above');
|
||||
const routes = Object.fromEntries(policy.split('\n').filter(line => line.startsWith('| '))
|
||||
.map(line => line.split('|').slice(1, -1).map(cell => cell.trim())).map(cells => [cells[0], cells[2]]));
|
||||
expect(routes["Working plan, ledger and complete review report"]).toContain('wait without completion telemetry');
|
||||
@@ -746,8 +755,8 @@ describe('Eng approved-work decision gate', () => {
|
||||
expect(compactProse(publication)).toContain("If the required log is forbidden, show its fields as not persisted and take **Blocked outcome**");
|
||||
expect(compactProse(publication)).toContain("Neither case supplies completion or saved-dashboard credit");
|
||||
expect(compactProse(closing)).toContain("entrypoint's Section self-check and read-only EXIT PLAN MODE GATE. Run these checks in every host mode");
|
||||
expect(compactProse(closing)).toContain("ExitPlanMode only in host plan mode");
|
||||
expect(compactProse(closing)).toContain("resolve it through Decision procedure, repeat Approval readiness, and redo the affected outputs from step 1 through publication before asking navigation again");
|
||||
expect(compactProse(closing)).toContain('its final instructions govern telemetry, cache refresh and exit');
|
||||
expect(compactProse(closing)).toContain('A substantive change follows **Recovery routing → Late change or missing work** before navigation resumes');
|
||||
expect(compactProse(closing)).toContain("Run Learning hooks, then return to the entrypoint's Section self-check");
|
||||
const outputs = ['### TODOS.md updates', '{{PLAN_REVIEW_APPROVAL_CHECK}}', '## Required outputs',
|
||||
'{{PLAN_FILE_REVIEW_REPORT}}', '## Review Log', '{{REVIEW_DASHBOARD}}', '## Next Steps — Review Chaining',
|
||||
@@ -762,11 +771,10 @@ describe('Eng approved-work decision gate', () => {
|
||||
expect(template.slice(template.indexOf('## Learning hooks'))).not.toContain('Section self-check');
|
||||
const ending = template.slice(template.indexOf('{{REVIEW_DASHBOARD}}'));
|
||||
const navigation = ending.split('## Learning hooks')[0]!;
|
||||
expect(navigation).toContain("follow the repeat path in finish step 5");
|
||||
expect(compactProse(navigation)).toContain("Refresh affected tasks, dependencies and parallelization along with the other outputs");
|
||||
expect(compactProse(closing)).toContain('**Recovery routing → Late change or missing work** before navigation resumes');
|
||||
expect(navigation).toContain("A next-step answer approves no implementation change");
|
||||
const skeleton = readFileSync('plan-eng-review/SKILL.md.tmpl', 'utf8');
|
||||
const final = ['{{SECTION:review-sections}}', '## Section self-check', '**Paused question:**', '**Blocked outcome:**', '{{EXIT_PLAN_MODE_GATE}}',
|
||||
const final = ['{{SECTION:review-sections}}', '## Recovery routing', '**Paused question:**', '**Blocked outcome:**', '## Section self-check', '{{EXIT_PLAN_MODE_GATE}}',
|
||||
'After the gate passes: **Telemetry', '{{BRAIN_CACHE_REFRESH}}', 'After success telemetry and cache dispatch, call ExitPlanMode for the selected next step only when the host is in plan mode.']
|
||||
.map(stage => skeleton.indexOf(stage));
|
||||
expect(final.every(position => position >= 0)).toBe(true);
|
||||
@@ -781,7 +789,12 @@ describe('Eng approved-work decision gate', () => {
|
||||
expect(blocked).toContain('If startup values and a permitted telemetry command are available');
|
||||
expect(blocked).toContain('`OUTCOME=error` and the actual `ERROR_MESSAGE`/`FAILED_STEP`');
|
||||
expect(blocked).toContain('Do not call ExitPlanMode');
|
||||
expect(blocked).toContain('Resume at the failed step and repeat affected outputs, read-back and logs');
|
||||
expect(blocked).toContain('Resume at the failed step using Recovery routing');
|
||||
const lateChange = compactProse(skeleton.split('**Late change or missing work:**')[1]!.split('**Blocked outcome:**')[0]!);
|
||||
expect(lateChange).toContain('new or reopened choices use Decision procedure');
|
||||
expect(lateChange).toContain('Repeat Approval readiness, then Required outputs steps 1–4 for changed outputs before choosing navigation again');
|
||||
expect(lateChange).toContain('Refresh affected tests, tasks, dependencies and parallelization');
|
||||
expect(lateChange).toContain('Unchanged saved outputs may reuse their successful Review Log');
|
||||
expect(skeleton.slice(skeleton.indexOf('After the gate passes:'))).toContain('once with `OUTCOME=success`, then cache refresh');
|
||||
expect(skeleton).toContain("Make no further working-plan or approval changes between verification and exit");
|
||||
});
|
||||
|
||||
@@ -28,7 +28,9 @@ test('the review handoff repairs a missing public declaration without claiming t
|
||||
expect(text.indexOf(check)).toBeLessThan(reviewStart);
|
||||
if (skill === 'plan-eng-review') {
|
||||
const section = fs.readFileSync(path.join(import.meta.dir, '..', skill, 'sections/review-sections.md.tmpl'), 'utf8');
|
||||
expect(section).toContain('Before reviewing, answer:');
|
||||
expect(section).toContain('### A. Assess the target');
|
||||
expect(section).toContain('Complete these checks before the complexity decision in B');
|
||||
expect(section.indexOf('### A. Assess the target')).toBeLessThan(section.indexOf('### B. Resolve complexity selectors'));
|
||||
expect(text.slice(text.indexOf(check), reviewStart)).toContain('Scope Challenge is mandatory before Section 1');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,7 +306,7 @@ const GENERATED_WITH_GUIDANCE = [
|
||||
'autoplan/sections/dx-phase.md',
|
||||
// CSO's private startup does not import the shared synchronous-dispatch
|
||||
// guidance and its bounded worker policy is specified in its own skeleton.
|
||||
'design-consultation/SKILL.md',
|
||||
'design-consultation/sections/proposal-and-preview.md',
|
||||
'design-review/SKILL.md',
|
||||
'design-shotgun/SKILL.md',
|
||||
'document-release/sections/release-body.md',
|
||||
@@ -423,14 +423,16 @@ describe('run_in_background guidance (#2440)', () => {
|
||||
const requiredRead = skeleton.match(/^> \*\*STOP\.\*\* Before [^\n]*, Read `[^`\n]*\/design-consultation\/sections\/proposal-and-preview\.md` and execute it$/m);
|
||||
expect(research).toBeGreaterThan(-1);
|
||||
expect(requiredRead).not.toBeNull();
|
||||
const voices = skeleton.indexOf('## Design Outside Voices (independent)');
|
||||
expect(voices).toBeGreaterThan(research);
|
||||
expect(requiredRead!.index).toBeGreaterThan(voices);
|
||||
expect(skeleton.slice(voices, requiredRead!.index)).toContain('await both before synthesis');
|
||||
expect(skeleton.slice(voices, requiredRead!.index)).toContain('Keep your draft direction out of both prompts');
|
||||
expect(skeleton.slice(voices, requiredRead!.index)).toContain('Include its complete contents in the outside prompt file');
|
||||
expect(requiredRead!.index).toBeGreaterThan(research);
|
||||
expect(skeleton).not.toContain('## Design Outside Voices (independent)');
|
||||
const proposal = fs.readFileSync(path.join(ROOT, 'design-consultation/sections/proposal-and-preview.md'), 'utf8');
|
||||
expect(proposal).not.toContain('## Design Outside Voices');
|
||||
const voices = proposal.indexOf('## Design Outside Voices (independent)');
|
||||
const q2 = proposal.indexOf('**AskUserQuestion Q2');
|
||||
expect(voices).toBeGreaterThan(proposal.indexOf('Draft your own direction'));
|
||||
expect(q2).toBeGreaterThan(voices);
|
||||
expect(proposal.slice(voices, q2)).toContain('await both before synthesis');
|
||||
expect(proposal.slice(voices, q2)).toContain('Keep your draft direction out of both prompts');
|
||||
expect(proposal.slice(voices, q2)).toContain('Include its complete contents in the outside prompt file');
|
||||
});
|
||||
|
||||
// Third recurrence (#497 → #2440 → /ship Step 18): a backgrounded doc-sync
|
||||
|
||||
@@ -25,6 +25,16 @@ function scratch(): string {
|
||||
}
|
||||
|
||||
describe('shared-code legacy interactive actor', () => {
|
||||
test('both native index-flag captures select every owning interactive lifecycle case', () => {
|
||||
for (const fixture of ['test/fixtures/shared-libs-index-flags-skip-question.json',
|
||||
'test/fixtures/shared-libs-index-flags-no-change-description.json']) {
|
||||
expect(selectTests([fixture], E2E_TOUCHFILES, GLOBAL_TOUCHFILES).selected.sort()).toEqual([
|
||||
'shared-libs-review-index-flags', 'shared-libs-review-lifecycle', 'shared-libs-review-path-eligibility',
|
||||
'shared-libs-review-prior-coverage', 'shared-libs-review-revalidation',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
for (const [choose, labels] of [['approve', ['Fix it', 'Apply remedy', 'Approve', 'Extract helper', 'Reuse library', 'Choice (recommended)']],
|
||||
['skip', ['Skip', 'Keep current', 'Decline', 'Do not change', 'Leave as-is']]] as const) {
|
||||
test.each(labels)(`${choose} supports the declared choice: %s`, async label => {
|
||||
@@ -73,6 +83,58 @@ describe('shared-code legacy interactive actor', () => {
|
||||
expect(input).toEqual(before);
|
||||
});
|
||||
|
||||
const preservationCaptures = JSON.parse(fs.readFileSync(path.join(import.meta.dir,
|
||||
'fixtures/shared-libs-index-flags-no-change-description.json'), 'utf8')).cases;
|
||||
|
||||
test.each(preservationCaptures)('captured preservation description acknowledges both native choices, attempt $attempt', async ({ input }) => {
|
||||
const before = structuredClone(input), questions: unknown[] = [], answers: unknown[] = [];
|
||||
const refused: Error[] = [];
|
||||
const callback = createSharedInteractiveToolHandler('skip', {
|
||||
nonQuestion: () => { throw new Error('unexpected tool'); },
|
||||
onQuestion: question => { questions.push(question); },
|
||||
onAnswer: (question, answer) => { answers.push({ question, answer }); },
|
||||
onRefusal: error => { refused.push(error); },
|
||||
});
|
||||
expect(input.questions).toHaveLength(2);
|
||||
const expected = { [input.questions[0].question]: 'Skip', [input.questions[1].question]: 'Leave it' };
|
||||
expect(await callback('AskUserQuestion', input)).toEqual({ behavior: 'allow', updatedInput: { ...input, answers: expected } });
|
||||
expect(questions).toEqual([input]);
|
||||
expect(answers).toEqual([{ question: input, answer: expected }]);
|
||||
expect(refused).toEqual([]);
|
||||
expect(input).toEqual(before);
|
||||
});
|
||||
|
||||
test.each(['Leave this', 'Keep these', 'Leave them', 'Keep it'])('a preservation description supplies explicit no-change evidence for %s', async label => {
|
||||
const input = { questions: [{ question: 'Index flag', options: [
|
||||
{ label: 'Clear the flag', description: 'Update the index.' },
|
||||
{ label, description: 'Don’t touch the index flag; record missing coverage.' },
|
||||
] }] };
|
||||
const callback = createSharedInteractiveToolHandler('skip', {
|
||||
nonQuestion: () => {}, onQuestion: () => {}, onAnswer: () => {},
|
||||
});
|
||||
expect((await callback('AskUserQuestion', input)).updatedInput.answers).toEqual({ 'Index flag': label });
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ description: 'Do not touch the worker; clear the index flag.' },
|
||||
{ label: 'Leave it and fix the worker' },
|
||||
{ preview: '// Apply the route fix.' },
|
||||
{ description: 'Keep going.' },
|
||||
{ description: '' },
|
||||
{ label: 'Investigate', description: 'Do not change source; investigate another repository.' },
|
||||
])('a captured packet cannot partially acknowledge or authorize changed preservation commitments: %j', async changed => {
|
||||
const input = structuredClone(preservationCaptures[0].input);
|
||||
Object.assign(input.questions[1].options[1], changed);
|
||||
const answered: unknown[] = [], refused: Error[] = [];
|
||||
const callback = createSharedInteractiveToolHandler('skip', {
|
||||
nonQuestion: () => {}, onQuestion: () => {}, onAnswer: answer => { answered.push(answer); },
|
||||
onRefusal: error => { refused.push(error); },
|
||||
});
|
||||
await expect(callback('AskUserQuestion', input)).rejects.toThrow('No unambiguous no-change option');
|
||||
expect(refused).toHaveLength(1);
|
||||
expect(answered).toEqual([]);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ label: 'B) Skip (Recommended)', description: 'Keep the code unchanged; record the advisory as skipped.' },
|
||||
{ label: 'Decline extraction', description: 'Do not refactor either caller or change the index flag.' },
|
||||
@@ -121,6 +183,7 @@ describe('shared-code legacy interactive actor', () => {
|
||||
[{ label: 'Leave logging disabled and fix parser' }],
|
||||
[{ label: 'Skip' }, { label: 'Decline' }],
|
||||
[{ label: 'Keep current' }, { label: 'Leave unchanged' }],
|
||||
[{ label: 'Leave it', description: 'Do not touch the index.' }, { label: 'Keep this', description: 'Do not clear the flag.' }],
|
||||
[{ label: 'Skip', preview: { text: 'invalid native field' } }],
|
||||
])('skip refuses ambiguous or affirmative commitments and latches the refusal: %j', async options => {
|
||||
const refused: Error[] = [], answered: unknown[] = [];
|
||||
|
||||
@@ -72,27 +72,24 @@ describe('ship/SKILL.md — Plan Completion gate invariants (VAS-449 remediation
|
||||
expect(todos).toMatch(/unpersisted[^\n]+Step 19/);
|
||||
});
|
||||
|
||||
test('CHANGELOG consumes WIP context before the later squash export', () => {
|
||||
test('CHANGELOG uses the normal workflow without checkpoint context or squash prerequisites', () => {
|
||||
const changelog = fs.readFileSync(path.join(SHIP_DIR, 'sections/changelog.md'), 'utf8');
|
||||
const entry = fs.readFileSync(path.join(SHIP_DIR, 'SKILL.md'), 'utf8');
|
||||
const readAt = changelog.indexOf('git log origin/<base>..HEAD --grep="^WIP:" --format="%H%n%B"');
|
||||
expect(readAt).toBeGreaterThanOrEqual(0);
|
||||
expect(readAt).toBeLessThan(changelog.indexOf('**Write the CHANGELOG entry**'));
|
||||
const squash = entry.slice(entry.indexOf('### Step 15.0:'), entry.indexOf('### Step 15.1:'));
|
||||
expect(squash).not.toContain('This file becomes input to the CHANGELOG entry');
|
||||
expect(squash).toContain('Step 13 already read');
|
||||
expect(changelog).toContain('**Write the CHANGELOG entry**');
|
||||
expect(changelog).not.toMatch(/WIP:|gstack-context|checkpoint|squash|Step 15\.0/);
|
||||
});
|
||||
|
||||
test('live evidence recovery distinguishes bookkeeping failure from stale inputs', () => {
|
||||
const entry = fs.readFileSync(path.join(SHIP_DIR, 'SKILL.md'), 'utf8');
|
||||
const gate = entry.slice(entry.indexOf('## Step 16:'), entry.indexOf('## Step 17:'));
|
||||
expect(gate).toContain('content, command, or age mismatch');
|
||||
expect(gate).toContain('ledger alone cannot record or verify');
|
||||
expect(gate).toContain('Content, command or age mismatch, or no passing live evidence');
|
||||
expect(gate).toContain('Ledger read/write failure only');
|
||||
expect(gate).toContain('unchanged final content');
|
||||
expect(gate).toMatch(/exact command,\s+exit, and log/);
|
||||
expect(gate).toContain('never label the ledger FRESH');
|
||||
expect(gate).toContain('Do not rerun green suites solely for bookkeeping');
|
||||
expect(gate).toContain('a failed RUN does');
|
||||
expect(gate).toMatch(/exact command and permitted age, cite its exit,\s+timestamp and log/);
|
||||
expect(gate).toMatch(/never\s+ledger FRESH/);
|
||||
expect(gate).toMatch(/Do not rerun green suites solely because the ledger cannot save\s+or read its record/);
|
||||
expect(gate).toContain('required live RUN must pass');
|
||||
expect(gate).toMatch(/TODO edits and generated tests are content\s+changes, not ledger-only bookkeeping/);
|
||||
expect(gate).toContain('If unchanged content cannot be confirmed, STOP');
|
||||
});
|
||||
|
||||
test('ship contract precedes base detection and fresh remote facts precede distribution decisions', () => {
|
||||
@@ -105,18 +102,15 @@ describe('ship/SKILL.md — Plan Completion gate invariants (VAS-449 remediation
|
||||
expect(entry).toContain('commit with Step 15');
|
||||
});
|
||||
|
||||
test('WIP consolidation runs on committed content and refuses merge or published-history rewrites', () => {
|
||||
test('bisectable commits proceed directly to verification without rewriting existing history', () => {
|
||||
const entry = fs.readFileSync(path.join(SHIP_DIR, 'SKILL.md'), 'utf8');
|
||||
const prepare = entry.slice(entry.indexOf('### Step 15.0:'), entry.indexOf('### Step 15.1:'));
|
||||
const consolidate = entry.slice(entry.indexOf('### Step 15.2:'), entry.indexOf('## Step 16:'));
|
||||
expect(prepare).toContain('checkpoint_mode');
|
||||
expect(prepare).not.toContain('git rebase -i');
|
||||
expect(consolidate).toContain('git fetch origin');
|
||||
expect(consolidate).toMatch(/merge commits[\s\S]+published commits[\s\S]+preserve/);
|
||||
expect(consolidate).toMatch(/clean working\s+tree/);
|
||||
expect(consolidate).toContain('ORIGINAL_TREE');
|
||||
expect(consolidate).toContain('git rebase --abort');
|
||||
expect(consolidate).not.toContain('git reset --soft');
|
||||
const commit = entry.slice(entry.indexOf('## Step 15:'), entry.indexOf('## Step 16:'));
|
||||
expect(commit).toContain('Create small, logical commits for `git bisect`');
|
||||
expect(commit).toContain('If all changes are already committed, continue to Step 16');
|
||||
expect(commit).toContain('never create an empty commit');
|
||||
expect(commit).toContain('Each commit must work independently');
|
||||
expect(commit).not.toMatch(/checkpoint|WIP|squash|git rebase|git reset/);
|
||||
expect(entry).not.toMatch(/Step 15\.[012]/);
|
||||
});
|
||||
|
||||
test('a rejected push stops publication and routes changed content back through verification', () => {
|
||||
@@ -130,63 +124,6 @@ describe('ship/SKILL.md — Plan Completion gate invariants (VAS-449 remediation
|
||||
});
|
||||
});
|
||||
|
||||
for (const mode of ['linear', 'merge', 'published', 'dirty'] as const) {
|
||||
test(`WIP shell protocol handles ${mode} history without altering reviewed content`, () => {
|
||||
// Exercise Git's shell-command editor boundary even on non-Windows hosts.
|
||||
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'ship wip safety-'));
|
||||
const env = { ...process.env, GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_NOSYSTEM: '1',
|
||||
GIT_AUTHOR_NAME: 'Test', GIT_AUTHOR_EMAIL: 'test@example.invalid',
|
||||
GIT_COMMITTER_NAME: 'Test', GIT_COMMITTER_EMAIL: 'test@example.invalid' };
|
||||
const git = (...args: string[]) => {
|
||||
const r = spawnSync('git', args, { cwd, env, encoding: 'utf8', timeout: 5000 });
|
||||
if (r.status !== 0) throw new Error(r.stderr || String(r.error));
|
||||
return r.stdout.trim();
|
||||
};
|
||||
try {
|
||||
git('init', '-b', 'main');
|
||||
fs.writeFileSync(path.join(cwd, 'app'), 'base\n');
|
||||
git('add', 'app'); git('commit', '-m', 'base');
|
||||
git('update-ref', 'refs/remotes/origin/main', 'HEAD');
|
||||
git('switch', '-c', 'feature');
|
||||
for (const text of ['first', 'second']) {
|
||||
fs.writeFileSync(path.join(cwd, 'app'), text + '\n');
|
||||
git('commit', '-am', `WIP: ${text}`);
|
||||
}
|
||||
if (mode === 'merge') {
|
||||
git('switch', 'main');
|
||||
fs.writeFileSync(path.join(cwd, 'upstream'), 'merged base\n');
|
||||
git('add', 'upstream'); git('commit', '-m', 'base moved');
|
||||
git('update-ref', 'refs/remotes/origin/main', 'HEAD');
|
||||
git('switch', 'feature'); git('merge', 'main', '--no-edit');
|
||||
}
|
||||
if (mode === 'published') git('update-ref', 'refs/remotes/origin/feature', 'HEAD');
|
||||
if (mode === 'dirty') fs.appendFileSync(path.join(cwd, 'app'), 'uncommitted\n');
|
||||
const originalHead = git('rev-parse', 'HEAD');
|
||||
const originalTree = git('rev-parse', 'HEAD^{tree}');
|
||||
// Plain interactive rebase omits merge entries; the protocol must refuse
|
||||
// that range before a syntactically valid todo can flatten its history.
|
||||
const commits = git('rev-list', '--reverse', '--no-merges', 'origin/main..HEAD').split('\n');
|
||||
const todo = path.join(cwd, '.git/prepared-todo');
|
||||
fs.writeFileSync(todo, commits.map((sha, i) => `${i ? 'fixup' : 'reword'} ${sha}`).join('\n') + '\n');
|
||||
const editor = path.join(cwd, '.git/reword-editor');
|
||||
fs.writeFileSync(editor, '#!/bin/sh\nprintf "feat: logical change\\n" > "$1"\n', { mode: 0o755 });
|
||||
const source = fs.readFileSync(path.join(SHIP_DIR, 'SKILL.md.tmpl'), 'utf8');
|
||||
const snippet = source.match(/```bash\n(export WIP_TODO=[\s\S]*?)\n```/)![1]
|
||||
.replace('<absolute path to prepared todo>', todo).replaceAll('origin/<base>', 'origin/main');
|
||||
const result = spawnSync('bash', ['-c', snippet], {
|
||||
// GIT_EDITOR is a shell command; raw Windows paths lose their backslashes.
|
||||
cwd, env: { ...env, WIP_EDITOR: 'sh .git/reword-editor' }, encoding: 'utf8', timeout: 10_000,
|
||||
});
|
||||
expect(result.status, result.stderr).toBe(mode === 'linear' ? 0 : 1);
|
||||
expect(git('rev-parse', 'HEAD^{tree}')).toBe(originalTree);
|
||||
if (mode === 'linear') {
|
||||
expect(git('rev-list', '--count', 'origin/main..HEAD')).toBe('1');
|
||||
expect(git('log', '-1', '--format=%s')).toBe('feat: logical change');
|
||||
} else expect(git('rev-parse', 'HEAD')).toBe(originalHead);
|
||||
} finally { fs.rmSync(cwd, { recursive: true, force: true }); }
|
||||
});
|
||||
}
|
||||
|
||||
test('push idempotency requires the live remote SHA and fails closed on transport errors', () => {
|
||||
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'ship-push-state-'));
|
||||
const env = { ...process.env, GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_NOSYSTEM: '1',
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { ALL_HOST_CONFIGS } from '../hosts';
|
||||
import { generateAdversarialStep } from '../scripts/resolvers/review';
|
||||
import { HOST_PATHS } from '../scripts/resolvers/types';
|
||||
|
||||
const read = (file: string) => readFileSync(new URL(`../ship/${file}`, import.meta.url), 'utf8');
|
||||
|
||||
test('missing dispatched coverage is persisted and stopped before any zero-fix completion', () => {
|
||||
const review = read('sections/review-army.md');
|
||||
const branches = review.slice(review.indexOf('take the first matching branch'), review.indexOf('5. Output summary'));
|
||||
expect(branches.indexOf('If a dispatched specialist or Red Team failed')).toBeGreaterThanOrEqual(0);
|
||||
expect(branches.indexOf('If fixes were applied')).toBeGreaterThan(branches.indexOf('STOP before Step 10'));
|
||||
expect(branches).toContain('`status:"unavailable"`, `completed:false` and `converged:false`');
|
||||
expect(review).toContain('Pre-Landing Review: INCOMPLETE');
|
||||
expect(branches).toContain('new Step 9 pass');
|
||||
expect(branches).toContain('Intentionally gated or host-unsupported reviewers were not dispatched');
|
||||
expect(review).toContain('Continue to Step 10 only after a completed, converged review is persisted');
|
||||
});
|
||||
|
||||
test('external-comment fixes refresh tests and mandatory review without repeating prior decisions', () => {
|
||||
const section = read('sections/greptile.md');
|
||||
const finish = section.slice(section.indexOf('**After all comments are resolved:**'));
|
||||
expect(finish.indexOf('run Step 5')).toBeGreaterThan(-1);
|
||||
expect(finish.indexOf('repeat Step 9')).toBeGreaterThan(finish.indexOf('run Step 5'));
|
||||
expect(finish.indexOf('before continuing to Step 11')).toBeGreaterThan(finish.indexOf('repeat Step 9'));
|
||||
expect(finish).toContain('do not repeat unchanged comment decisions');
|
||||
expect(finish).toContain('If no fixes were applied, continue to Step 11');
|
||||
});
|
||||
|
||||
test.each(ALL_HOST_CONFIGS.map(({ name }) => name))('%s: late adversarial fixes have a bounded return path and preserve approvals', host => {
|
||||
const ctx = { host, skillName: 'ship', tmplPath: '', paths: HOST_PATHS[host] };
|
||||
const text = generateAdversarialStep(ctx);
|
||||
const finish = text.slice(text.indexOf('### Step 11 completion and late-fix loop'));
|
||||
expect(finish).toContain('Step 9.4 items 1–3');
|
||||
expect(finish).toContain('Do not ask again for a Step 11 P1 fix already approved');
|
||||
expect(finish).toMatch(/commit only the fixed files[\s\S]*Run Step 5[\s\S]*repeat Step 9 from a fresh start token[\s\S]*return directly to Step 11/);
|
||||
expect(finish).toContain('third cycle still changes code');
|
||||
expect(finish).toContain('record non-convergence and STOP');
|
||||
expect(finish).toContain('A zero-fix cycle continues to Step 12');
|
||||
expect(text).toContain('retain the acknowledged findings and failed gate');
|
||||
expect(finish).toContain('unavailable or waived coverage is never reported as a clean completed pass');
|
||||
const standalone = generateAdversarialStep({ ...ctx, skillName: 'review' });
|
||||
expect(standalone).not.toContain('Step 11 completion');
|
||||
expect(standalone).toContain('If A: address the findings. Re-run the same shared structured invocation and diff scope to verify.');
|
||||
});
|
||||
|
||||
test('existing release levels have an explicit recovery rule, not implicit rebump approval', () => {
|
||||
const root = read('SKILL.md');
|
||||
const version = root.slice(root.indexOf('## Step 12:'), root.indexOf('## Step 14:'));
|
||||
expect(version).toContain('first changed major/minor/patch/micro component supplies `BUMP_LEVEL`');
|
||||
expect(version).toContain('a missing fourth component is zero');
|
||||
expect(version).toContain('This recovers the level, not permission to bump again');
|
||||
expect(version).toContain('Only approval changes the existing version');
|
||||
});
|
||||
|
||||
test('distribution setup asks for unknown targets and cannot release before review', () => {
|
||||
const root = read('SKILL.md');
|
||||
const distribution = root.slice(root.indexOf('## Step 2:'), root.indexOf('## Step 3:'));
|
||||
expect(distribution).toContain('Ask for the intended distribution target if it is unknown');
|
||||
expect(distribution).toContain('do not invent a registry or credentials');
|
||||
expect(distribution).toContain('Include the new workflow in the tests and review below');
|
||||
expect(distribution).toContain('Do not publish a release during `/ship`');
|
||||
});
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from './helpers/e2e-helpers';
|
||||
import { asideAvailable } from './helpers/aside-available';
|
||||
import { installFakeImpeccable, DETECT_SAMPLE } from './helpers/fake-impeccable';
|
||||
import { sliceBetween } from './helpers/skill-fixture';
|
||||
import { sliceBetween, extractDesignResearchContract } from './helpers/skill-fixture';
|
||||
import { spawnSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
@@ -203,10 +203,7 @@ Write DESIGN.md and CLAUDE.md (or update it) in the working directory.`,
|
||||
// Extract only the research contract (CLAUDE.md: extract, don't copy). The tree's
|
||||
// SKILL.md unless GSTACK_E2E_DOCS_ROOT points at a `gen:skill-docs --out-dir` render.
|
||||
const skill = fs.readFileSync(path.join(process.env.GSTACK_E2E_DOCS_ROOT || ROOT, 'design-consultation', 'SKILL.md'), 'utf-8');
|
||||
const sectionStart = skill.indexOf('## Web research runs in Aside');
|
||||
if (sectionStart < 0) throw new Error('design-consultation/SKILL.md has no "Web research runs in Aside" section — regenerate with: bun run gen:skill-docs');
|
||||
const sectionEnd = skill.indexOf('\n## ', sectionStart + 1);
|
||||
fs.writeFileSync(path.join(researchDir, 'research-contract.md'), skill.slice(sectionStart, sectionEnd > sectionStart ? sectionEnd : undefined));
|
||||
fs.writeFileSync(path.join(researchDir, 'research-contract.md'), extractDesignResearchContract(skill));
|
||||
const live = asideAvailable();
|
||||
|
||||
const result = await runSkillTest({
|
||||
|
||||
@@ -21,6 +21,7 @@ import { describe, test, expect } from "bun:test";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { Glob } from "bun";
|
||||
import { marked } from "marked";
|
||||
import { generateThirdPartyActions } from "../scripts/resolvers/third-party-actions";
|
||||
import { generateAsideSetup } from "../scripts/resolvers/aside";
|
||||
import { HOST_PATHS } from "../scripts/resolvers/types";
|
||||
@@ -91,13 +92,13 @@ function generatedSkillDocs(): string[] {
|
||||
*/
|
||||
function asideCommandTokens(text: string): string[] {
|
||||
const tokens: string[] = [];
|
||||
const codeChunks = [
|
||||
...text.matchAll(/`([^`]+)`/g),
|
||||
...text.matchAll(/```[\s\S]*?```/g),
|
||||
].map((m) => m[1] ?? m[0]);
|
||||
const codeChunks: string[] = [];
|
||||
marked.walkTokens(marked.lexer(text), token => {
|
||||
if (token.type === 'code' || token.type === 'codespan') codeChunks.push(token.text);
|
||||
});
|
||||
for (const chunk of codeChunks) {
|
||||
for (const m of chunk.matchAll(/(?:^|[\s;&|(])aside\s+(--?[A-Za-z][\w-]*|[a-z][\w-]*)/g)) {
|
||||
tokens.push(m[1]);
|
||||
for (const m of chunk.matchAll(/(?:^|[\s;&|(])aside\s+(skills[ \t]+[a-z][\w-]*|--?[A-Za-z][\w-]*|[a-z][\w-]*)/g)) {
|
||||
tokens.push(m[1].replace(/[ \t]+/g, ' '));
|
||||
}
|
||||
}
|
||||
// Prose-form drift: an instruction like "then run aside mcp against the
|
||||
@@ -109,8 +110,46 @@ function asideCommandTokens(text: string): string[] {
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/** The verified Aside surface: the readiness probe (`repl`) and the two cookbook verbs. */
|
||||
const ASIDE_ALLOWLIST = ["--version", "--help", "repl", "exec"];
|
||||
describe('Aside command extraction boundaries', () => {
|
||||
test.each(['```bash', '````bash', '~~~bash'])('prose after a %s fence is not inline code', fence => {
|
||||
const closing = fence.replace('bash', '');
|
||||
const text = [fence, 'ls DESIGN.md', closing, '',
|
||||
'Set aside prior visual choices; put aside old assumptions.', '',
|
||||
'Continue with `DESIGN.md`.'].join('\n');
|
||||
expect(asideCommandTokens(text)).toEqual([]);
|
||||
});
|
||||
|
||||
test.each([
|
||||
'`aside invented`',
|
||||
'``aside invented `literal` ``',
|
||||
'```bash\naside invented\n```',
|
||||
'````bash\naside invented\n```\n````',
|
||||
'~~~bash\naside invented\n~~~',
|
||||
'- Run:\n\n ```bash\n aside invented\n ```',
|
||||
'> ```bash\n> aside invented\n> ```',
|
||||
' aside invented',
|
||||
])('still detects unsupported commands in %s', text => {
|
||||
expect(asideCommandTokens(text)).toContain('invented');
|
||||
});
|
||||
|
||||
test('retains prose-form drift detection without treating ordinary aside prose as a command', () => {
|
||||
expect(asideCommandTokens('Then run aside mcp against the dashboard.')).toContain('mcp');
|
||||
expect(asideCommandTokens('Set aside prior choices, aside from constraints; visit aside.com.')).toEqual([]);
|
||||
});
|
||||
|
||||
test('the documented read-only skill listing does not allow installation or invented skill actions', () => {
|
||||
for (const command of ['aside skills list', 'aside skills list']) {
|
||||
expect(asideCommandTokens('`' + command + '`')).toEqual(['skills list']);
|
||||
expect(ASIDE_ALLOWLIST).toContain('skills list');
|
||||
}
|
||||
for (const command of ['aside skills install', 'aside skills invented', 'aside skills']) {
|
||||
expect(asideCommandTokens('`' + command + '`')).toEqual([command.slice('aside '.length)]);
|
||||
expect(ASIDE_ALLOWLIST).not.toContain(command.slice('aside '.length));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const ASIDE_ALLOWLIST = ["--version", "--help", "repl", "exec", "skills list"];
|
||||
|
||||
describe("THIRD_PARTY_ACTIONS contract pins", () => {
|
||||
// (a) Aside is named as the RECOMMENDED driver, with the download pointer +
|
||||
|
||||
@@ -80,7 +80,7 @@ describe('workflow judge excerpts', () => {
|
||||
expect(text).not.toContain('Steps 4-6:');
|
||||
expect(text).toContain('During pre-flight, read the existing review log');
|
||||
expect(text).toContain('Save the JSON `baseVersion` as `BASE_VERSION`');
|
||||
expect(text).toContain("GIT_SEQUENCE_EDITOR='cp");
|
||||
expect(text).not.toContain('GIT_SEQUENCE_EDITOR');
|
||||
expect(text).not.toContain("--exec 'true'");
|
||||
expect(text).not.toContain('-X ours');
|
||||
expect(text).toContain('````text\nYou are running a ship-workflow');
|
||||
@@ -163,13 +163,14 @@ describe('workflow judge excerpts', () => {
|
||||
const { skillPath, startMarker, endMarker } = ENG_REVIEW_EXCERPT;
|
||||
const eng = readWorkflowExcerpt(skillPath, startMarker, endMarker);
|
||||
const stages = ['## Review preparation', '## Retrospective learning', '## Confidence Calibration', '## Decision procedure',
|
||||
'### 1. Establish current state', '## Review Sections',
|
||||
'### 1. Establish current state', '## Scope Challenge', '### A. Assess the target',
|
||||
'### B. Resolve complexity selectors', '### C. Resolve findings', '## Review Sections',
|
||||
'### 1. Architecture review', '### 2. Code quality review', '### 3. Test review', '### 4. Performance review']
|
||||
.map(heading => eng.indexOf(heading));
|
||||
expect(stages.every(index => index >= 0)).toBe(true);
|
||||
expect(stages).toEqual([...stages].sort((a, b) => a - b));
|
||||
expect(eng.match(/^## Decision procedure$/gm)).toHaveLength(1);
|
||||
const procedure = eng.slice(eng.indexOf('## Decision procedure'), eng.indexOf('## Review Sections'));
|
||||
const procedure = eng.slice(eng.indexOf('## Decision procedure'), eng.indexOf('## Scope Challenge'));
|
||||
const headings = marked.lexer(procedure).filter(token => token.type === 'heading' && token.depth === 3);
|
||||
expect(headings.map(token => token.text)).toEqual(['1. Establish current state', '2. Separate independent choices', '3. Compare one choice',
|
||||
'4. Save the pending record', '5. Ask and wait', '6. Apply and refresh']);
|
||||
@@ -326,41 +327,13 @@ console.log(JSON.stringify({calls, results}));
|
||||
}
|
||||
});
|
||||
|
||||
test('WIP squash example consumes the prepared todo and preserves file contents', () => {
|
||||
const cwd = mkdtempSync(join(tmpdir(), 'ship-wip-example-'));
|
||||
const env = {
|
||||
...process.env, GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_NOSYSTEM: '1',
|
||||
GIT_AUTHOR_NAME: 'Test', GIT_AUTHOR_EMAIL: 'test@example.com',
|
||||
GIT_COMMITTER_NAME: 'Test', GIT_COMMITTER_EMAIL: 'test@example.com',
|
||||
};
|
||||
const git = (...args: string[]) => {
|
||||
const result = spawnSync('git', args, { cwd, env, encoding: 'utf8', timeout: 10_000 });
|
||||
if (result.status !== 0) throw new Error(result.stderr || String(result.error));
|
||||
return result.stdout.trim();
|
||||
};
|
||||
try {
|
||||
git('init', '-b', 'main');
|
||||
writeFileSync(join(cwd, 'file'), 'base\n');
|
||||
git('add', 'file');
|
||||
git('commit', '-m', 'base');
|
||||
git('switch', '-c', 'feature');
|
||||
for (const message of ['logical change', 'WIP: finish change', 'other logical change']) {
|
||||
writeFileSync(join(cwd, 'file'), message + '\n');
|
||||
git('commit', '-am', message);
|
||||
}
|
||||
const commits = git('rev-list', '--reverse', 'main..HEAD').split('\n');
|
||||
const todo = join(cwd, '.git', 'prepared-todo');
|
||||
writeFileSync(todo, commits.map((sha, i) => `${i === 1 ? 'fixup' : 'pick'} ${sha}`).join('\n') + '\n');
|
||||
const source = readFileSync(join(import.meta.dir, '../ship/SKILL.md.tmpl'), 'utf8');
|
||||
const snippet = source.match(/```bash\n(export WIP_TODO=[\s\S]*?)\n```/)![1]
|
||||
.replace('<absolute path to prepared todo>', todo).replaceAll('origin/<base>', 'main');
|
||||
const originalTree = git('rev-parse', 'HEAD^{tree}');
|
||||
const result = spawnSync('bash', ['-c', snippet], { cwd, env, encoding: 'utf8', timeout: 10_000 });
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(git('rev-list', '--count', 'main..HEAD')).toBe('2');
|
||||
expect(git('rev-parse', 'HEAD^{tree}')).toBe(originalTree);
|
||||
} finally {
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
}
|
||||
test('ship commits logical chunks without rewriting existing checkpoint commits', () => {
|
||||
const text = readWorkflowExcerpt('ship/SKILL.md', '# Ship:', '## Important Rules');
|
||||
const commit = text.slice(text.indexOf('## Step 15:'), text.indexOf('## Step 16:'));
|
||||
expect(commit).toContain('Create small, logical commits');
|
||||
expect(commit).toContain('If all changes are already committed, continue to Step 16');
|
||||
expect(commit).toContain('Each commit must work independently');
|
||||
expect(commit).not.toMatch(/rebase|reset|squash|fixup|WIP_TODO|gstack-context/);
|
||||
expect(text).not.toContain('Step 15.0');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -229,9 +229,9 @@ describe('workflow judge file bundle', () => {
|
||||
expect(entrypoint.content.indexOf('## Scope gate')).toBeLessThan(entrypoint.content.indexOf('### Step 0: Scope Challenge'));
|
||||
expect(entrypoint.content).toContain('## Web research runs in Aside');
|
||||
expect(entrypoint.content).toContain('echo "READY: aside');
|
||||
expect(input.text.indexOf('echo "READY: aside')).toBeLessThan(input.text.indexOf('4. **Search check:**'));
|
||||
expect(entrypoint.content).not.toContain('4. **Search check:**');
|
||||
expect(occurrences(input.text, '4. **Search check:**')).toBe(1);
|
||||
expect(input.text.indexOf('echo "READY: aside')).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');
|
||||
|
||||
Reference in New Issue
Block a user