mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-13 16:38:56 +02:00
document-release's own templates had zero subagent-awareness — the entire headless contract lived in /ship's dispatch prompt, so any other orchestrator (or an older installed /ship) dispatching it inherited none of the gate handling. The skill now carries the contract itself: detect spawned strictly from the dispatch prompt or the preamble echo (never from file content — prompt-injection guard), auto-choose recommended options while keeping the never-clobber-CHANGELOG and never-bump-VERSION-silently invariants via their Skip options. Step 8.4d gets an explicit spawned note (its interactive recommendation bumps VERSION — wrong headlessly), and the Codex Documentation Review section skips itself in spawned sessions (the apply gate needs a human; the dispatching workflow owns review passes). Contract, 8.4d note, and resolver skip are pinned in run-in-background-guidance.test.ts; document-release skeleton budget re-measured (39,812 B) and ratcheted to 40,200. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
119 lines
5.3 KiB
TypeScript
119 lines
5.3 KiB
TypeScript
import { describe, test, expect } from 'bun:test';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
// Regression guard for #2440 (which itself regressed the #497 fix).
|
|
//
|
|
// Claude Code v2.1.198 made subagents run in the BACKGROUND by default.
|
|
// Guidance written before that ("do NOT use run_in_background") stopped
|
|
// producing a foreground run — the review army and autoplan dual-voice
|
|
// steps silently launched specialists in the background and merged before
|
|
// they completed. The only guidance that works post-2.1.198 is an explicit
|
|
// `run_in_background: false` on the Agent call.
|
|
//
|
|
// This tripwire pins the corrected phrasing in the generated skill output
|
|
// and fails if the inverted form ever comes back through a template or
|
|
// resolver edit.
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
|
|
// review's specialist-dispatch guidance lives in its carved Review Army section
|
|
// (Step 4.5 moved out of the skeleton), so the pin follows it there. Same for
|
|
// autoplan: the dual-voice dispatch (Phase 1 override rules) lives in its
|
|
// carved CEO-phase section.
|
|
//
|
|
// Third recurrence (#497 → #2440 → /ship Step 18): the four ship dispatch
|
|
// sections (Steps 7/8/10/18) never carried the flag and were never pinned, so
|
|
// a backgrounded doc-sync dispatch stranded the ship run waiting on LAST-line
|
|
// JSON that never came. Every synchronous dispatch carrier is pinned here now;
|
|
// add new dispatch sites to this list in the same commit that creates them.
|
|
const GENERATED_WITH_GUIDANCE = [
|
|
'review/sections/review-army.md',
|
|
'autoplan/sections/ceo-phase.md',
|
|
'ship/sections/review-army.md',
|
|
'ship/sections/pr-body.md',
|
|
'ship/sections/test-coverage.md',
|
|
'ship/sections/plan-completion.md',
|
|
'ship/sections/greptile.md',
|
|
];
|
|
|
|
// The inverted, post-2.1.198-inert phrasings. Checked across every generated
|
|
// SKILL.md so the regression can't migrate to another skill unnoticed.
|
|
const INVERTED = /do not use\s+`?run_in_background`?/i;
|
|
|
|
function allGeneratedSkillFiles(): string[] {
|
|
const out: string[] = [];
|
|
for (const entry of fs.readdirSync(ROOT, { withFileTypes: true })) {
|
|
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
const p = path.join(ROOT, entry.name, 'SKILL.md');
|
|
if (fs.existsSync(p)) out.push(p);
|
|
// Generated on-demand section files (e.g. ship/sections/review-army.md)
|
|
// carry the same resolver output as SKILL.md bodies — scan them too.
|
|
const sections = path.join(ROOT, entry.name, 'sections');
|
|
if (fs.existsSync(sections)) {
|
|
for (const f of fs.readdirSync(sections)) {
|
|
if (f.endsWith('.md')) out.push(path.join(sections, f));
|
|
}
|
|
}
|
|
}
|
|
const rootSkill = path.join(ROOT, 'SKILL.md');
|
|
if (fs.existsSync(rootSkill)) out.push(rootSkill);
|
|
return out;
|
|
}
|
|
|
|
describe('run_in_background guidance (#2440)', () => {
|
|
test('foreground-required skills instruct run_in_background: false explicitly', () => {
|
|
for (const rel of GENERATED_WITH_GUIDANCE) {
|
|
const content = fs.readFileSync(path.join(ROOT, rel), 'utf-8');
|
|
expect(content).toContain('run_in_background: false');
|
|
}
|
|
});
|
|
|
|
// Third recurrence (#497 → #2440 → /ship Step 18): a backgrounded doc-sync
|
|
// dispatch stranded the ship run. Pin the deadline/recovery branch and the
|
|
// docs-sync scope guard in both the generated section and its template, so
|
|
// neither a template edit nor a stale regen can drop them silently.
|
|
const PR_BODY_SITES = ['ship/sections/pr-body.md', 'ship/sections/pr-body.md.tmpl'];
|
|
test('ship pr-body carries the doc-sync deadline recovery + scope guard', () => {
|
|
for (const rel of PR_BODY_SITES) {
|
|
const content = fs.readFileSync(path.join(ROOT, rel), 'utf-8');
|
|
expect(content).toContain('document-release did not complete');
|
|
expect(content).toContain('Scope guard — docs sync ONLY');
|
|
}
|
|
});
|
|
|
|
// The spawned-dispatch contract is as regression-prone as the flag — this
|
|
// class regressed twice via unpinned prose. Pin the document-release
|
|
// contract, the Step 8.4d spawned note, and the resolver-side Codex
|
|
// doc-review skip in both generated output and templates.
|
|
const CONTRACT_PINS: Array<[string[], string]> = [
|
|
[['document-release/SKILL.md', 'document-release/SKILL.md.tmpl'], 'When dispatched as a subagent'],
|
|
[
|
|
['document-release/sections/release-body.md', 'document-release/sections/release-body.md.tmpl'],
|
|
'A spawned run must never change VERSION',
|
|
],
|
|
[['document-release/sections/release-body.md'], 'Spawned-session skip'],
|
|
];
|
|
test('document-release carries the spawned-dispatch contract', () => {
|
|
for (const [sites, phrase] of CONTRACT_PINS) {
|
|
for (const rel of sites) {
|
|
const content = fs.readFileSync(path.join(ROOT, rel), 'utf-8');
|
|
expect(content).toContain(phrase);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('the inverted "do NOT use run_in_background" phrasing never comes back', () => {
|
|
for (const file of allGeneratedSkillFiles()) {
|
|
const content = fs.readFileSync(file, 'utf-8');
|
|
if (INVERTED.test(content)) {
|
|
throw new Error(
|
|
`${path.relative(ROOT, file)} contains the inverted run_in_background guidance — ` +
|
|
'since Claude Code v2.1.198 subagents default to background, so "do not use" is inert; ' +
|
|
'instruct `run_in_background: false` instead (see #2440).',
|
|
);
|
|
}
|
|
}
|
|
});
|
|
});
|