* 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:
@@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user