mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-27 07:01:54 +02:00
* feat: bind shared-code review advice to source and branch * feat: add shared-code extraction audit and scoped review checks * test: recognize complete source reads and explicit coverage legends * chore: bump version and changelog (v1.88.0.0) Co-Authored-By: OpenAI Codex <noreply@openai.com> * test: capture native review questions and retain public evidence Capture the actual first public native question with strict ownership and display matching. Preserve terminal failures and raw evidence, and retain SDK completion checks. * test: recognize verified review evidence and complete fixtures Recognize complete source and diagram evidence, concrete design and developer-experience decisions, and the complete planted scenario contracts. Preserve negative controls and grading thresholds. * fix: preserve decision brief structure in native questions Keep the required pros-and-cons heading and final Net field in native question text. Regenerate host outputs and document the release and evaluation repairs. Co-Authored-By: OpenAI Codex <noreply@openai.com> * docs: update project documentation for v1.88.0.0 Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix: correct eval retry accounting and ship workflow gates * fix: capture native eval evidence and stabilize CI fixtures * fix: keep shared-code eval skips read-only Choose explicit no-change answers instead of mixed fix/preservation options. Reuse the bounded revalidation prompt for path fixtures so required review metadata is available without repeated discovery. Preserve source checks, retry limits, and failed native terminal outcomes. Add captured-question and callback regressions, plus evaluation selection coverage for the affected fixtures. --------- Co-authored-by: OpenAI Codex <noreply@openai.com>
155 lines
7.6 KiB
TypeScript
155 lines
7.6 KiB
TypeScript
/**
|
|
* /ship → /document-release Step 18 dispatch stays wired AND visible.
|
|
*
|
|
* The v1.54.0.0 carve (46c1fae7) moved Step 18 (documentation sync) out of
|
|
* the ship skeleton into sections/pr-body.md. The wiring survived, but on
|
|
* the Claude host the skeleton stopped saying "document-release" anywhere
|
|
* in the workflow body — the dispatch became invisible at the decision
|
|
* points and "the idea of running document-release as part of ship got
|
|
* lost". This tripwire pins both halves so neither can silently regress:
|
|
*
|
|
* - the carved section keeps the full dispatch contract (imperative,
|
|
* subagent_type, JSON return keys, non-blocking failure), and
|
|
* - the Claude skeleton names /document-release at its
|
|
* three touchpoints (manifest trigger via section-index + STOP pointer,
|
|
* Step 17 handoff, hoisted doc-sync invariant) while the imperative
|
|
* itself stays carved.
|
|
*
|
|
* Structural backstop lives in test/helpers/carve-guards.ts (ship entry);
|
|
* this named test documents the regression story. Behavioral proof is the
|
|
* ship-docsync E2E (test/skill-e2e-ship-docsync.test.ts).
|
|
*/
|
|
import { describe, test, expect } from 'bun:test';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
const ROOT = path.join(import.meta.dir, '..');
|
|
|
|
const SECTION_SITES = [
|
|
path.join(ROOT, 'ship', 'sections', 'pr-body.md'),
|
|
path.join(ROOT, 'ship', 'sections', 'pr-body.md.tmpl'),
|
|
];
|
|
|
|
// ship/SKILL.md only — NOT the claude golden: test/host-config.test.ts
|
|
// already enforces golden == generated byte-for-byte, so substring asserts
|
|
// on the golden would be pure duplication. The codex/factory goldens ARE
|
|
// asserted below because for them the check is content (the inlined
|
|
// section survived generation), which byte-equality alone doesn't prove.
|
|
const CLAUDE_SKELETON = path.join(ROOT, 'ship', 'SKILL.md');
|
|
const INLINED_GOLDENS = [
|
|
path.join(ROOT, 'test', 'fixtures', 'golden', 'codex-ship-SKILL.md'),
|
|
path.join(ROOT, 'test', 'fixtures', 'golden', 'factory-ship-SKILL.md'),
|
|
];
|
|
|
|
const CARVED_IMPERATIVE = 'Dispatch /document-release as a subagent';
|
|
|
|
describe('/ship Step 18 dispatches /document-release (carve visibility)', () => {
|
|
test('carved section carries the full dispatch contract', () => {
|
|
for (const file of SECTION_SITES) {
|
|
const content = fs.readFileSync(file, 'utf-8');
|
|
expect(content).toContain(
|
|
'## Step 18: Documentation sync (via subagent, before PR creation)'
|
|
);
|
|
expect(content).toContain(CARVED_IMPERATIVE);
|
|
expect(content).toContain('subagent_type: "general-purpose"');
|
|
// The JSON return contract Step 19 bakes into the PR body.
|
|
expect(content).toContain('"files_updated"');
|
|
expect(content).toContain('"commit_sha"');
|
|
expect(content).toContain('"pushed"');
|
|
expect(content).toContain('"documentation_section"');
|
|
// Deliberate design: docs sync never holds a ship hostage.
|
|
expect(content).toContain('Do not block /ship on subagent failure');
|
|
// These four strings are the ship-docsync E2E's dispatch-matcher markers
|
|
// (test/skill-e2e-ship-docsync.test.ts): the first two are INCLUSION
|
|
// markers (verbatim from the dictated Step 18 subagent prompt); the last
|
|
// two are EXCLUSION markers (section scaffolding that disqualifies a
|
|
// whole-section paste). Rewording any of them in pr-body.md.tmpl silently
|
|
// deadens the paid matcher; update all four in lockstep.
|
|
expect(content).toContain('You are executing the /document-release workflow');
|
|
expect(content).toContain('.claude/skills/gstack/document-release/SKILL.md');
|
|
expect(content).toContain('## Step 19: Create PR/MR');
|
|
expect(content).toContain('Parent processing:');
|
|
// #2733: the dispatch marks the subagent spawned so document-release's
|
|
// AUQ gates auto-choose instead of prose-stopping (which breaks the
|
|
// parent's LAST-line JSON parse). Three layers pinned: the env marker
|
|
// prefix, the behavioral instruction, and the framing sentence.
|
|
expect(content).toContain('GSTACK_SESSION_KIND=spawned');
|
|
expect(content).toContain('auto-choose the RECOMMENDED option');
|
|
expect(content).toContain('as a SPAWNED subagent');
|
|
// Auto-chosen gate decisions ride the JSON contract (console-printed by
|
|
// the parent), never the public PR body.
|
|
expect(content).toContain('"decisions"');
|
|
const docHeading = content.indexOf('\n## Documentation\n');
|
|
expect(docHeading, 'PR-body template must carry the ## Documentation heading').toBeGreaterThan(0);
|
|
// End bound searched FROM docHeading and asserted found — otherwise a
|
|
// removed/reordered '## Test plan' heading degrades this guard to a
|
|
// vacuous empty-slice check instead of failing loudly (#2733 review).
|
|
const docEnd = content.indexOf('\n## Test plan\n', docHeading);
|
|
expect(docEnd, '## Test plan heading must follow ## Documentation').toBeGreaterThan(docHeading);
|
|
const docSection = content.slice(docHeading, docEnd);
|
|
expect(docSection, 'decisions must never leak into the PR-body Documentation embed').not.toContain('decisions');
|
|
}
|
|
});
|
|
|
|
test('claude skeleton names document-release at all three touchpoints', () => {
|
|
const content = fs.readFileSync(CLAUDE_SKELETON, 'utf-8');
|
|
// Manifest trigger — renders into the section-index row AND the STOP
|
|
// pointer, so it must appear at least twice.
|
|
const trigger =
|
|
'dispatching the /document-release subagent to sync docs (Step 18) and then creating or updating the PR/MR (Step 19)';
|
|
expect(content.split(trigger).length - 1).toBeGreaterThanOrEqual(2);
|
|
// Step 17 handoff.
|
|
expect(content).toContain(
|
|
'Continue to mandatory Step 18 (dispatch /document-release)'
|
|
);
|
|
// Hoisted doc-sync invariant (beside the PR-title invariant).
|
|
expect(content).toContain('**Doc-sync invariant');
|
|
expect(content).toContain('dispatches the /document-release subagent');
|
|
// The STOP-Read pointer still routes to the carved section.
|
|
expect(content).toMatch(
|
|
/> \*\*STOP\.\*\*[^\n]*Read `[^`]*ship\/sections\/pr-body\.md`/
|
|
);
|
|
// Ordering pin: the hoisted invariant must sit ABOVE the pr-body STOP
|
|
// pointer (mustStayInSkeleton asserts presence only — a future edit could
|
|
// drift the paragraph below the STOP with every registry check green).
|
|
const invariantIdx = content.indexOf('**Doc-sync invariant');
|
|
const stopIdx = content.indexOf(
|
|
'> **STOP.** Before dispatching the /document-release subagent'
|
|
);
|
|
expect(invariantIdx).toBeGreaterThan(-1);
|
|
expect(stopIdx).toBeGreaterThan(invariantIdx);
|
|
});
|
|
|
|
test('the dispatch imperative stays carved out of the claude skeleton', () => {
|
|
const content = fs.readFileSync(CLAUDE_SKELETON, 'utf-8');
|
|
expect(content).not.toContain(CARVED_IMPERATIVE);
|
|
});
|
|
|
|
test('manifest trigger names the subagent', () => {
|
|
const manifest = JSON.parse(
|
|
fs.readFileSync(
|
|
path.join(ROOT, 'ship', 'sections', 'manifest.json'),
|
|
'utf-8'
|
|
)
|
|
);
|
|
const prBody = manifest.sections.find(
|
|
(s: { id: string }) => s.id === 'pr-body'
|
|
);
|
|
expect(prBody).toBeDefined();
|
|
expect(prBody.trigger).toContain('the /document-release subagent');
|
|
});
|
|
|
|
test('codex + factory goldens inline Step 18 before Step 19', () => {
|
|
for (const file of INLINED_GOLDENS) {
|
|
const content = fs.readFileSync(file, 'utf-8');
|
|
const step18 = content.indexOf(
|
|
'## Step 18: Documentation sync (via subagent, before PR creation)'
|
|
);
|
|
const step19 = content.indexOf('## Step 19: Create PR/MR');
|
|
expect(step18).toBeGreaterThan(-1);
|
|
expect(step19).toBeGreaterThan(step18);
|
|
expect(content).toContain(CARVED_IMPERATIVE);
|
|
}
|
|
});
|
|
});
|