fix(#1526): delegate auto-chain post-completion to transition workflow (#3419)

* fix(#1526): delegate auto-chain post-completion to transition workflow

execute-phase's auto-chain completion called phase.complete then a light inline
set (partial PROJECT.md update + offer-next) and never invoked the transition
workflow, silently skipping graduation scan, session-continuity, project-reference,
accumulated-context, and current-position updates — so a phase completed via
auto-chain left different project state than a normal transition.

Fix (delegate, user decision 2026-08-13): replace execute-phase's update_project_md
+ offer_next with a delegation step that @-includes transition.md in post-completion
mode. Add a post_completion_mode step to transition.md that skips verify_completion
+ update_roadmap_and_state (phase.complete already ran; avoids double-write) and
begins at evolve_project. Standalone transition (mode 1) is unchanged.

Regression: tests/auto-chain-transition-delegation.test.cjs (source-text-is-the-
product) asserts the delegation, the skip-set, the removed inline step, and the mode.
Ack fragment 1526 covers execute-phase.md + transition.md growth (spent 2930 fragment
removed — same-path owner conflict, like #3025/#3024).

* docs(#1526): backfill changeset PR number (#3419)

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-13 20:31:55 -04:00
committed by GitHub
parent 0624c5da6f
commit b77b7f8e56
9 changed files with 159 additions and 60 deletions

View File

@@ -359,54 +359,56 @@ const workflowPath = path.resolve(
__dirname, '..', 'gsd-core', 'workflows', 'execute-phase.md'
);
describe('bug #2002: offer_next checks CONTEXT.md before suggesting next step', () => {
// offer_next body extracted to references/offer-next.md (#2537); content tests
// read the reference file. The <step name="offer_next"> tag + @-reference remain
// in execute-phase.md.
const offerNextRefPath = path.join(__dirname, '..', 'gsd-core', 'references', 'offer-next.md');
let content;
describe('bug #2002: next-step suggestion checks CONTEXT.md (now via transition offer_next_phase, reached by execute-phase post-completion delegation — #1526)', () => {
// #1526: execute-phase no longer carries an inline offer_next step — it delegates
// post-completion to the transition workflow, whose offer_next_phase step performs
// the #2002 CONTEXT.md-gated next-step suggestion. These tests track that behavior
// in its new home (transition.md) and assert the delegation reaches it.
const transPath = path.resolve(__dirname, '..', 'gsd-core', 'workflows', 'transition.md');
let offerNextPhase;
test('setup: offer-next reference file is readable', () => {
content = fs.readFileSync(offerNextRefPath, 'utf-8');
assert.ok(content.length > 0, 'offer-next.md must not be empty');
test('setup: transition.md offer_next_phase section is readable', () => {
const trans = fs.readFileSync(transPath, 'utf-8');
const start = trans.indexOf('<step name="offer_next_phase">');
const end = trans.indexOf('</step>', start);
assert.notEqual(start, -1, 'transition.md must have an offer_next_phase step');
offerNextPhase = trans.slice(start, end);
assert.ok(offerNextPhase.length > 0, 'offer_next_phase section must be non-empty');
});
test('execute-phase.md still carries the offer_next step + @-reference', () => {
test('#1526: execute-phase delegates post-completion to transition (no inline offer_next step)', () => {
const wf = fs.readFileSync(workflowPath, 'utf-8');
assert.ok(wf.includes('<step name="offer_next">'), 'offer_next step tag must remain in execute-phase.md');
assert.ok(wf.includes('references/offer-next.md'), 'execute-phase.md must @-reference the extracted offer-next.md');
assert.ok(wf.includes('delegate_post_completion_to_transition'), 'execute-phase must delegate post-completion to transition');
assert.ok(wf.includes('@~/.claude/gsd-core/workflows/transition.md'), 'execute-phase must @-include transition.md');
assert.equal(wf.includes('<step name="offer_next">'), false, 'inline offer_next step is intentionally removed (delegated to transition.offer_next_phase)');
});
test('offer_next section checks for CONTEXT.md existence', () => {
content = content || fs.readFileSync(offerNextRefPath, 'utf-8');
test('offer_next_phase checks for CONTEXT.md existence (#2002 preserved)', () => {
assert.ok(
content.includes('CONTEXT.md'),
'offer_next must reference CONTEXT.md to determine primary next step'
offerNextPhase.includes('CONTEXT.md'),
'offer_next_phase must reference CONTEXT.md to determine primary next step'
);
});
test('offer_next presents /gsd-discuss-phase when CONTEXT.md does not exist', () => {
content = content || fs.readFileSync(offerNextRefPath, 'utf-8');
test('offer_next_phase presents /gsd-discuss-phase when CONTEXT.md does not exist', () => {
assert.ok(
/CONTEXT\.md.*does not exist|CONTEXT\.md.*not.*exist|If CONTEXT\.md does/i.test(content) ||
/gsd-discuss-phase.*recommended|recommended.*gsd-discuss-phase/i.test(content),
'offer_next must present /gsd-discuss-phase as primary when CONTEXT.md does not exist'
/CONTEXT\.md.*does not exist|CONTEXT\.md.*not.*exist|If CONTEXT\.md does/i.test(offerNextPhase) ||
/discuss-phase/i.test(offerNextPhase),
'offer_next_phase must present /gsd-discuss-phase as primary when CONTEXT.md does not exist'
);
});
test('offer_next presents /gsd-plan-phase when CONTEXT.md exists', () => {
content = content || fs.readFileSync(offerNextRefPath, 'utf-8');
test('offer_next_phase presents /gsd-plan-phase when CONTEXT.md exists', () => {
assert.ok(
/CONTEXT\.md.*exists|exists.*CONTEXT\.md|If CONTEXT\.md/i.test(content),
'offer_next must present /gsd-plan-phase as primary when CONTEXT.md exists'
/CONTEXT\.md.*exists|exists.*CONTEXT\.md|If CONTEXT\.md/i.test(offerNextPhase),
'offer_next_phase must present /gsd-plan-phase as primary when CONTEXT.md exists'
);
});
test('offer_next section contains at least one conditional guard before listing commands', () => {
content = content || fs.readFileSync(offerNextRefPath, 'utf-8');
test('offer_next_phase contains at least one conditional guard before listing commands', () => {
assert.ok(
/If CONTEXT\.md/i.test(content),
'offer_next must contain at least one "If CONTEXT.md" conditional guard'
/If CONTEXT\.md/i.test(offerNextPhase),
'offer_next_phase must contain at least one "If CONTEXT.md" conditional guard'
);
});
});