mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
Compare commits
2 Commits
fix/2130-e
...
fix/2134-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb03ba3dd8 | ||
|
|
637daa831b |
@@ -42,11 +42,9 @@ function splitInlineArray(body) {
|
||||
|
||||
function extractFrontmatter(content) {
|
||||
const frontmatter = {};
|
||||
// Find ALL frontmatter blocks at the start of the file.
|
||||
// If multiple blocks exist (corruption from CRLF mismatch), use the LAST one
|
||||
// since it represents the most recent state sync.
|
||||
const allBlocks = [...content.matchAll(/(?:^|\n)\s*---\r?\n([\s\S]+?)\r?\n---/g)];
|
||||
const match = allBlocks.length > 0 ? allBlocks[allBlocks.length - 1] : null;
|
||||
// Match frontmatter only at byte 0 — a `---` block later in the document
|
||||
// body (YAML examples, horizontal rules) must never be treated as frontmatter.
|
||||
const match = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
|
||||
if (!match) return frontmatter;
|
||||
|
||||
const yaml = match[1];
|
||||
|
||||
@@ -1104,7 +1104,9 @@ function cmdInitManager(cwd, raw) {
|
||||
return true;
|
||||
});
|
||||
|
||||
const completedCount = phases.filter(p => p.disk_status === 'complete').length;
|
||||
// Exclude backlog phases (999.x) from completion accounting (#2129)
|
||||
const nonBacklogPhases = phases.filter(p => !/^999(?:\.|$)/.test(p.number));
|
||||
const completedCount = nonBacklogPhases.filter(p => p.disk_status === 'complete').length;
|
||||
|
||||
// Read manager flags from config (passthrough flags for each step)
|
||||
// Validate: flags must be CLI-safe (only --flags, alphanumeric, hyphens, spaces)
|
||||
@@ -1135,7 +1137,7 @@ function cmdInitManager(cwd, raw) {
|
||||
in_progress_count: phases.filter(p => ['partial', 'planned', 'discussed', 'researched'].includes(p.disk_status)).length,
|
||||
recommended_actions: filteredActions,
|
||||
waiting_signal: waitingSignal,
|
||||
all_complete: completedCount === phases.length && phases.length > 0,
|
||||
all_complete: completedCount === nonBacklogPhases.length && nonBacklogPhases.length > 0,
|
||||
project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
|
||||
roadmap_exists: true,
|
||||
state_exists: true,
|
||||
|
||||
@@ -838,9 +838,11 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
||||
.sort((a, b) => comparePhaseNum(a, b));
|
||||
|
||||
// Find the next phase directory after current
|
||||
// Skip backlog phases (999.x) — they are parked ideas, not sequential work (#2129)
|
||||
for (const dir of dirs) {
|
||||
const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
|
||||
if (dm) {
|
||||
if (/^999(?:\.|$)/.test(dm[1])) continue;
|
||||
if (comparePhaseNum(dm[1], phaseNum) > 0) {
|
||||
nextPhaseNum = dm[1];
|
||||
nextPhaseName = dm[2] || null;
|
||||
|
||||
@@ -113,6 +113,104 @@ describe('extractFrontmatter', () => {
|
||||
assert.strictEqual(result.second, 'two');
|
||||
assert.strictEqual(result.third, 'three');
|
||||
});
|
||||
|
||||
// ─── Bug #2130: body --- sequence mis-parse ──────────────────────────────
|
||||
|
||||
test('#2130: frontmatter at top with YAML example block in body — returns top frontmatter', () => {
|
||||
const content = [
|
||||
'---',
|
||||
'name: my-agent',
|
||||
'type: execute',
|
||||
'---',
|
||||
'',
|
||||
'# Documentation',
|
||||
'',
|
||||
'Here is a YAML example:',
|
||||
'',
|
||||
'```yaml',
|
||||
'---',
|
||||
'key: value',
|
||||
'other: stuff',
|
||||
'---',
|
||||
'```',
|
||||
'',
|
||||
'End of doc.',
|
||||
].join('\n');
|
||||
const result = extractFrontmatter(content);
|
||||
assert.strictEqual(result.name, 'my-agent', 'should extract name from TOP frontmatter');
|
||||
assert.strictEqual(result.type, 'execute', 'should extract type from TOP frontmatter');
|
||||
assert.strictEqual(result.key, undefined, 'should NOT extract key from body YAML block');
|
||||
assert.strictEqual(result.other, undefined, 'should NOT extract other from body YAML block');
|
||||
});
|
||||
|
||||
test('#2130: frontmatter at top with horizontal rules in body — returns top frontmatter', () => {
|
||||
const content = [
|
||||
'---',
|
||||
'title: My Doc',
|
||||
'status: active',
|
||||
'---',
|
||||
'',
|
||||
'# Section One',
|
||||
'',
|
||||
'Some text.',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'# Section Two',
|
||||
'',
|
||||
'More text.',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'# Section Three',
|
||||
].join('\n');
|
||||
const result = extractFrontmatter(content);
|
||||
assert.strictEqual(result.title, 'My Doc', 'should extract title from TOP frontmatter');
|
||||
assert.strictEqual(result.status, 'active', 'should extract status from TOP frontmatter');
|
||||
});
|
||||
|
||||
test('#2130: body-only --- block with no frontmatter at byte 0 — returns empty', () => {
|
||||
const content = [
|
||||
'# My Document',
|
||||
'',
|
||||
'Some intro text.',
|
||||
'',
|
||||
'---',
|
||||
'key: value',
|
||||
'other: stuff',
|
||||
'---',
|
||||
'',
|
||||
'End of doc.',
|
||||
].join('\n');
|
||||
const result = extractFrontmatter(content);
|
||||
assert.deepStrictEqual(result, {}, 'should return empty object when --- block is not at byte 0');
|
||||
});
|
||||
|
||||
test('#2130: valid frontmatter at byte 0 still works (regression guard)', () => {
|
||||
const content = [
|
||||
'---',
|
||||
'phase: 01',
|
||||
'plan: 03',
|
||||
'type: execute',
|
||||
'wave: 1',
|
||||
'depends_on: ["01-01", "01-02"]',
|
||||
'files_modified:',
|
||||
' - src/auth.ts',
|
||||
' - src/middleware.ts',
|
||||
'autonomous: true',
|
||||
'---',
|
||||
'',
|
||||
'# Plan body here',
|
||||
].join('\n');
|
||||
const result = extractFrontmatter(content);
|
||||
assert.strictEqual(result.phase, '01');
|
||||
assert.strictEqual(result.plan, '03');
|
||||
assert.strictEqual(result.type, 'execute');
|
||||
assert.strictEqual(result.wave, '1');
|
||||
assert.deepStrictEqual(result.depends_on, ['01-01', '01-02']);
|
||||
assert.deepStrictEqual(result.files_modified, ['src/auth.ts', 'src/middleware.ts']);
|
||||
assert.strictEqual(result.autonomous, 'true');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── reconstructFrontmatter ─────────────────────────────────────────────────
|
||||
|
||||
@@ -531,4 +531,46 @@ describe('init manager', () => {
|
||||
|
||||
assert.strictEqual(output.response_language, undefined);
|
||||
});
|
||||
|
||||
test('all_complete is true when non-backlog phases are complete and 999.x exists (#2129)', () => {
|
||||
writeState(tmpDir);
|
||||
writeRoadmap(tmpDir, [
|
||||
{ number: '1', name: 'Setup', complete: true },
|
||||
{ number: '2', name: 'Core', complete: true },
|
||||
{ number: '3', name: 'Polish', complete: true },
|
||||
{ number: '999.1', name: 'Backlog idea' },
|
||||
]);
|
||||
|
||||
// Scaffold completed phases on disk
|
||||
scaffoldPhase(tmpDir, 1, { slug: 'setup', plans: 2, summaries: 2 });
|
||||
scaffoldPhase(tmpDir, 2, { slug: 'core', plans: 1, summaries: 1 });
|
||||
scaffoldPhase(tmpDir, 3, { slug: 'polish', plans: 1, summaries: 1 });
|
||||
|
||||
const result = runGsdTools('init manager', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.all_complete, true, 'all_complete should be true when only 999.x phases remain incomplete');
|
||||
});
|
||||
|
||||
test('all_complete false with incomplete non-backlog phase still produces recommended_actions (#2129)', () => {
|
||||
writeState(tmpDir);
|
||||
writeRoadmap(tmpDir, [
|
||||
{ number: '1', name: 'Setup', complete: true },
|
||||
{ number: '2', name: 'Core', complete: true },
|
||||
{ number: '3', name: 'Polish' },
|
||||
{ number: '999.1', name: 'Backlog idea' },
|
||||
]);
|
||||
|
||||
scaffoldPhase(tmpDir, 1, { slug: 'setup', plans: 1, summaries: 1 });
|
||||
scaffoldPhase(tmpDir, 2, { slug: 'core', plans: 1, summaries: 1 });
|
||||
// Phase 3 has no directory — should trigger discuss recommendation
|
||||
|
||||
const result = runGsdTools('init manager', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.all_complete, false, 'all_complete should be false with phase 3 incomplete');
|
||||
assert.ok(output.recommended_actions.length > 0, 'recommended_actions should not be empty when non-backlog phases remain');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2330,6 +2330,83 @@ describe('phase complete updates Performance Metrics', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// phase complete — backlog phase (999.x) exclusion (#2129)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('phase complete excludes 999.x backlog from next-phase (#2129)', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('next phase skips 999.x backlog dirs and falls back to roadmap', () => {
|
||||
// ROADMAP defines phases 1, 2, 3 and a backlog 999.1
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
[
|
||||
'# Roadmap',
|
||||
'',
|
||||
'- [ ] Phase 1: Setup',
|
||||
'- [ ] Phase 2: Core',
|
||||
'- [ ] Phase 3: Polish',
|
||||
'- [ ] Phase 999.1: Backlog idea',
|
||||
'',
|
||||
'### Phase 1: Setup',
|
||||
'**Goal:** Initial setup',
|
||||
'',
|
||||
'### Phase 2: Core',
|
||||
'**Goal:** Build core',
|
||||
'',
|
||||
'### Phase 3: Polish',
|
||||
'**Goal:** Polish everything',
|
||||
'',
|
||||
'### Phase 999.1: Backlog idea',
|
||||
'**Goal:** Parked idea',
|
||||
].join('\n')
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'STATE.md'),
|
||||
[
|
||||
'# State',
|
||||
'',
|
||||
'**Current Phase:** 02',
|
||||
'**Status:** In progress',
|
||||
'**Current Plan:** 02-01',
|
||||
'**Last Activity:** 2025-01-01',
|
||||
'**Last Activity Description:** Working',
|
||||
].join('\n')
|
||||
);
|
||||
|
||||
// Phase 1 and 2 exist on disk, phase 3 does NOT exist yet, 999.1 DOES exist
|
||||
const p1 = path.join(tmpDir, '.planning', 'phases', '01-setup');
|
||||
fs.mkdirSync(p1, { recursive: true });
|
||||
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
||||
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
|
||||
|
||||
const p2 = path.join(tmpDir, '.planning', 'phases', '02-core');
|
||||
fs.mkdirSync(p2, { recursive: true });
|
||||
fs.writeFileSync(path.join(p2, '02-01-PLAN.md'), '# Plan');
|
||||
fs.writeFileSync(path.join(p2, '02-01-SUMMARY.md'), '# Summary');
|
||||
|
||||
// Backlog stub on disk — this is what triggers the bug
|
||||
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '999.1-backlog-idea'), { recursive: true });
|
||||
|
||||
const result = runGsdTools('phase complete 2', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
// Should find phase 3 from roadmap, NOT 999.1 from filesystem
|
||||
assert.strictEqual(output.next_phase, '3', 'next_phase should be 3, not 999.1');
|
||||
assert.strictEqual(output.is_last_phase, false, 'should not be last phase');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// milestone complete command
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user