mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
The sliding-window pattern serialized discuss to one phase at a time even when phases had no dependency relationship. Replaced it with a simple predicate: every undiscussed phase whose dependencies are satisfied is marked is_next_to_discuss, letting the user pick any of them from the manager's recommended_actions list. Closes #2268 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1080,15 +1080,10 @@ function cmdInitManager(cwd, raw) {
|
||||
: '—';
|
||||
}
|
||||
|
||||
// Sliding window: discuss is sequential — only the first undiscussed phase is available
|
||||
let foundNextToDiscuss = false;
|
||||
for (const phase of phases) {
|
||||
if (!foundNextToDiscuss && (phase.disk_status === 'empty' || phase.disk_status === 'no_directory')) {
|
||||
phase.is_next_to_discuss = true;
|
||||
foundNextToDiscuss = true;
|
||||
} else {
|
||||
phase.is_next_to_discuss = false;
|
||||
}
|
||||
phase.is_next_to_discuss =
|
||||
(phase.disk_status === 'empty' || phase.disk_status === 'no_directory') &&
|
||||
phase.deps_satisfied;
|
||||
}
|
||||
|
||||
// Check for WAITING.json signal
|
||||
|
||||
112
tests/bug-2268-parallel-discuss.test.cjs
Normal file
112
tests/bug-2268-parallel-discuss.test.cjs
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Regression test for bug #2268
|
||||
*
|
||||
* cmdInitProgress used a sliding-window pattern that set is_next_to_discuss
|
||||
* only on the FIRST undiscussed phase. Multiple independent undiscussed phases
|
||||
* could not be discussed in parallel — the manager only ever recommended one
|
||||
* discuss action at a time.
|
||||
*
|
||||
* Fix: mark ALL undiscussed phases as is_next_to_discuss = true so the user
|
||||
* can pick any of them.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { test, describe, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
|
||||
|
||||
function writeRoadmap(tmpDir, phases) {
|
||||
const sections = phases.map(p => {
|
||||
let section = `### Phase ${p.number}: ${p.name}\n\n**Goal:** Do the thing\n`;
|
||||
return section;
|
||||
}).join('\n');
|
||||
const checklist = phases.map(p => {
|
||||
const mark = p.complete ? 'x' : ' ';
|
||||
return `- [${mark}] **Phase ${p.number}: ${p.name}**`;
|
||||
}).join('\n');
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
`# Roadmap\n\n## Progress\n\n${checklist}\n\n${sections}`
|
||||
);
|
||||
}
|
||||
|
||||
function writeState(tmpDir) {
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '---\nstatus: active\n---\n# State\n');
|
||||
}
|
||||
|
||||
let tmpDir;
|
||||
|
||||
describe('bug #2268: parallel discuss — all undiscussed phases marked is_next_to_discuss', () => {
|
||||
beforeEach(() => { tmpDir = createTempProject(); });
|
||||
afterEach(() => { cleanup(tmpDir); });
|
||||
|
||||
test('two undiscussed phases: both marked is_next_to_discuss', () => {
|
||||
writeState(tmpDir);
|
||||
writeRoadmap(tmpDir, [
|
||||
{ number: '1', name: 'Foundation' },
|
||||
{ number: '2', name: 'Cloud Deployment' },
|
||||
]);
|
||||
|
||||
const result = runGsdTools('init manager', tmpDir);
|
||||
const output = JSON.parse(result.output);
|
||||
|
||||
assert.strictEqual(output.phases[0].is_next_to_discuss, true, 'phase 1 should be discussable');
|
||||
assert.strictEqual(output.phases[1].is_next_to_discuss, true, 'phase 2 should also be discussable');
|
||||
});
|
||||
|
||||
test('two undiscussed phases: both get discuss recommendations', () => {
|
||||
writeState(tmpDir);
|
||||
writeRoadmap(tmpDir, [
|
||||
{ number: '1', name: 'Foundation' },
|
||||
{ number: '2', name: 'Cloud Deployment' },
|
||||
]);
|
||||
|
||||
const result = runGsdTools('init manager', tmpDir);
|
||||
const output = JSON.parse(result.output);
|
||||
|
||||
const discussActions = output.recommended_actions.filter(a => a.action === 'discuss');
|
||||
assert.strictEqual(discussActions.length, 2, 'should recommend discuss for both undiscussed phases');
|
||||
|
||||
const phases = discussActions.map(a => a.phase).sort();
|
||||
assert.deepStrictEqual(phases, ['1', '2']);
|
||||
});
|
||||
|
||||
test('five undiscussed phases: all five marked is_next_to_discuss', () => {
|
||||
writeState(tmpDir);
|
||||
writeRoadmap(tmpDir, [
|
||||
{ number: '1', name: 'Alpha' },
|
||||
{ number: '2', name: 'Beta' },
|
||||
{ number: '3', name: 'Gamma' },
|
||||
{ number: '4', name: 'Delta' },
|
||||
{ number: '5', name: 'Epsilon' },
|
||||
]);
|
||||
|
||||
const result = runGsdTools('init manager', tmpDir);
|
||||
const output = JSON.parse(result.output);
|
||||
|
||||
for (const phase of output.phases) {
|
||||
assert.strictEqual(phase.is_next_to_discuss, true, `phase ${phase.number} should be discussable`);
|
||||
}
|
||||
});
|
||||
|
||||
test('discussed phase stays false; undiscussed sibling is true', () => {
|
||||
writeState(tmpDir);
|
||||
writeRoadmap(tmpDir, [
|
||||
{ number: '1', name: 'Foundation' },
|
||||
{ number: '2', name: 'API Layer' },
|
||||
]);
|
||||
// scaffold CONTEXT.md to mark phase 1 as discussed
|
||||
const dir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, '01-CONTEXT.md'), '# Context');
|
||||
|
||||
const result = runGsdTools('init manager', tmpDir);
|
||||
const output = JSON.parse(result.output);
|
||||
|
||||
assert.strictEqual(output.phases[0].is_next_to_discuss, false, 'discussed phase must not be is_next_to_discuss');
|
||||
assert.strictEqual(output.phases[1].is_next_to_discuss, true, 'undiscussed sibling must be is_next_to_discuss');
|
||||
});
|
||||
});
|
||||
@@ -160,7 +160,7 @@ describe('init manager', () => {
|
||||
assert.strictEqual(output.phases[1].deps_satisfied, false); // phase 1 not complete
|
||||
});
|
||||
|
||||
test('sliding window: only first undiscussed phase is next to discuss', () => {
|
||||
test('parallel discuss: all undiscussed phases are marked is_next_to_discuss', () => {
|
||||
writeState(tmpDir);
|
||||
writeRoadmap(tmpDir, [
|
||||
{ number: '1', name: 'Foundation' },
|
||||
@@ -171,18 +171,17 @@ describe('init manager', () => {
|
||||
const result = runGsdTools('init manager', tmpDir);
|
||||
const output = JSON.parse(result.output);
|
||||
|
||||
// Only phase 1 should be discussable
|
||||
// All three phases are undiscussed — all should be discussable
|
||||
assert.strictEqual(output.phases[0].is_next_to_discuss, true);
|
||||
assert.strictEqual(output.phases[1].is_next_to_discuss, false);
|
||||
assert.strictEqual(output.phases[2].is_next_to_discuss, false);
|
||||
assert.strictEqual(output.phases[1].is_next_to_discuss, true);
|
||||
assert.strictEqual(output.phases[2].is_next_to_discuss, true);
|
||||
|
||||
// Only recommendation should be discuss phase 1
|
||||
assert.strictEqual(output.recommended_actions.length, 1);
|
||||
assert.strictEqual(output.recommended_actions[0].action, 'discuss');
|
||||
assert.strictEqual(output.recommended_actions[0].phase, '1');
|
||||
// All three should have discuss recommendations
|
||||
const discussActions = output.recommended_actions.filter(a => a.action === 'discuss');
|
||||
assert.strictEqual(discussActions.length, 3);
|
||||
});
|
||||
|
||||
test('sliding window: after discussing N, plan N + discuss N+1', () => {
|
||||
test('discussed phase is not discussable; undiscussed siblings are', () => {
|
||||
writeState(tmpDir);
|
||||
writeRoadmap(tmpDir, [
|
||||
{ number: '1', name: 'Foundation' },
|
||||
@@ -196,16 +195,18 @@ describe('init manager', () => {
|
||||
const result = runGsdTools('init manager', tmpDir);
|
||||
const output = JSON.parse(result.output);
|
||||
|
||||
// Phase 1 is discussed, phase 2 is next to discuss
|
||||
// Phase 1 is discussed; phases 2 and 3 are both undiscussed and discussable
|
||||
assert.strictEqual(output.phases[0].is_next_to_discuss, false);
|
||||
assert.strictEqual(output.phases[1].is_next_to_discuss, true);
|
||||
assert.strictEqual(output.phases[2].is_next_to_discuss, false);
|
||||
assert.strictEqual(output.phases[2].is_next_to_discuss, true);
|
||||
|
||||
// Should recommend plan phase 1 AND discuss phase 2
|
||||
// Should recommend plan phase 1 AND discuss phases 2 and 3
|
||||
const phase1Rec = output.recommended_actions.find(r => r.phase === '1');
|
||||
const phase2Rec = output.recommended_actions.find(r => r.phase === '2');
|
||||
const phase3Rec = output.recommended_actions.find(r => r.phase === '3');
|
||||
assert.strictEqual(phase1Rec.action, 'plan');
|
||||
assert.strictEqual(phase2Rec.action, 'discuss');
|
||||
assert.strictEqual(phase3Rec.action, 'discuss');
|
||||
});
|
||||
|
||||
test('sliding window: full pipeline — execute N, plan N+1, discuss N+2', () => {
|
||||
@@ -225,17 +226,17 @@ describe('init manager', () => {
|
||||
const result = runGsdTools('init manager', tmpDir);
|
||||
const output = JSON.parse(result.output);
|
||||
|
||||
// Phase 4 is first undiscussed
|
||||
// Phases 4 and 5 are both undiscussed — both discussable
|
||||
assert.strictEqual(output.phases[3].is_next_to_discuss, true);
|
||||
assert.strictEqual(output.phases[4].is_next_to_discuss, false);
|
||||
assert.strictEqual(output.phases[4].is_next_to_discuss, true);
|
||||
|
||||
// Recommendations: execute 2, plan 3, discuss 4
|
||||
// Recommendations: execute 2, plan 3, discuss 4, discuss 5
|
||||
assert.strictEqual(output.recommended_actions[0].action, 'execute');
|
||||
assert.strictEqual(output.recommended_actions[0].phase, '2');
|
||||
assert.strictEqual(output.recommended_actions[1].action, 'plan');
|
||||
assert.strictEqual(output.recommended_actions[1].phase, '3');
|
||||
assert.strictEqual(output.recommended_actions[2].action, 'discuss');
|
||||
assert.strictEqual(output.recommended_actions[2].phase, '4');
|
||||
const discussRecs = output.recommended_actions.filter(a => a.action === 'discuss').map(a => a.phase).sort();
|
||||
assert.deepStrictEqual(discussRecs, ['4', '5']);
|
||||
});
|
||||
|
||||
test('recommendation ordering: execute > plan > discuss', () => {
|
||||
|
||||
Reference in New Issue
Block a user