Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Fixed
- `phase complete` and `milestone complete` now update STATE.md fields in both `**Bold:**` and plain `Field:` formats using `stateReplaceField`

## [1.22.4] - 2026-03-03

### Added
Expand Down
32 changes: 18 additions & 14 deletions get-shit-done/bin/lib/milestone.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const fs = require('fs');
const path = require('path');
const { escapeRegex, getMilestonePhaseFilter, output, error } = require('./core.cjs');
const { extractFrontmatter } = require('./frontmatter.cjs');
const { writeStateMd } = require('./state.cjs');
const { writeStateMd, stateReplaceField } = require('./state.cjs');

function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
if (!reqIdsRaw || reqIdsRaw.length === 0) {
Expand Down Expand Up @@ -177,21 +177,25 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
fs.writeFileSync(milestonesPath, `# Milestones\n\n${milestoneEntry}`, 'utf-8');
}

// Update STATE.md
// Update STATE.md — use stateReplaceField which handles both **bold:** and plain Field: formats
if (fs.existsSync(statePath)) {
let stateContent = fs.readFileSync(statePath, 'utf-8');
stateContent = stateContent.replace(
/(\*\*Status:\*\*\s*).*/,
`$1${version} milestone complete`
);
stateContent = stateContent.replace(
/(\*\*Last Activity:\*\*\s*).*/,
`$1${today}`
);
stateContent = stateContent.replace(
/(\*\*Last Activity Description:\*\*\s*).*/,
`$1${version} milestone completed and archived`
);

const replaceWithFallback = (content, primary, fallback, value) => {
let result = stateReplaceField(content, primary, value);
if (result) return result;
if (fallback) {
result = stateReplaceField(content, fallback, value);
if (result) return result;
}
return content;
};

stateContent = replaceWithFallback(stateContent, 'Status', null, `${version} milestone complete`);
stateContent = replaceWithFallback(stateContent, 'Last Activity', 'Last activity', today);
stateContent = replaceWithFallback(stateContent, 'Last Activity Description', null,
`${version} milestone completed and archived`);

writeStateMd(statePath, stateContent, cwd);
}

Expand Down
62 changes: 35 additions & 27 deletions get-shit-done/bin/lib/phase.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const fs = require('fs');
const path = require('path');
const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, toPosixPath, output, error } = require('./core.cjs');
const { extractFrontmatter } = require('./frontmatter.cjs');
const { writeStateMd } = require('./state.cjs');
const { writeStateMd, stateReplaceField, stateExtractField } = require('./state.cjs');

function cmdPhasesList(cwd, options, raw) {
const phasesDir = path.join(cwd, '.planning', 'phases');
Expand Down Expand Up @@ -829,47 +829,55 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
} catch {}
}

// Update STATE.md
// Update STATE.md — use stateReplaceField which handles both **bold:** and plain Field: formats
if (fs.existsSync(statePath)) {
let stateContent = fs.readFileSync(statePath, 'utf-8');

// Update Current Phase
stateContent = stateContent.replace(
/(\*\*Current Phase:\*\*\s*).*/,
`$1${nextPhaseNum || phaseNum}`
);
// Helper: try primary field name, then fallback
const replaceWithFallback = (content, primary, fallback, value) => {
let result = stateReplaceField(content, primary, value);
if (result) return result;
if (fallback) {
result = stateReplaceField(content, fallback, value);
if (result) return result;
}
return content;
};

// Update Current Phase — preserve "X of Y (Name)" compound format
const phaseValue = nextPhaseNum || phaseNum;
const existingPhaseField = stateExtractField(stateContent, 'Current Phase')
|| stateExtractField(stateContent, 'Phase');
let newPhaseValue = phaseValue;
if (existingPhaseField) {
const totalMatch = existingPhaseField.match(/of\s+(\d+)/);
const nameMatch = existingPhaseField.match(/\(([^)]+)\)/);
if (totalMatch) {
const total = totalMatch[1];
const nameStr = nextPhaseName ? ` (${nextPhaseName.replace(/-/g, ' ')})` : (nameMatch ? ` (${nameMatch[1]})` : '');
newPhaseValue = `${phaseValue} of ${total}${nameStr}`;
}
}
stateContent = replaceWithFallback(stateContent, 'Current Phase', 'Phase', newPhaseValue);

// Update Current Phase Name
if (nextPhaseName) {
stateContent = stateContent.replace(
/(\*\*Current Phase Name:\*\*\s*).*/,
`$1${nextPhaseName.replace(/-/g, ' ')}`
);
stateContent = replaceWithFallback(stateContent, 'Current Phase Name', null, nextPhaseName.replace(/-/g, ' '));
}

// Update Status
stateContent = stateContent.replace(
/(\*\*Status:\*\*\s*).*/,
`$1${isLastPhase ? 'Milestone complete' : 'Ready to plan'}`
);
stateContent = replaceWithFallback(stateContent, 'Status', null,
isLastPhase ? 'Milestone complete' : 'Ready to plan');

// Update Current Plan
stateContent = stateContent.replace(
/(\*\*Current Plan:\*\*\s*).*/,
`$1Not started`
);
stateContent = replaceWithFallback(stateContent, 'Current Plan', 'Plan', 'Not started');

// Update Last Activity
stateContent = stateContent.replace(
/(\*\*Last Activity:\*\*\s*).*/,
`$1${today}`
);
stateContent = replaceWithFallback(stateContent, 'Last Activity', 'Last activity', today);

// Update Last Activity Description
stateContent = stateContent.replace(
/(\*\*Last Activity Description:\*\*\s*).*/,
`$1Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`
);
stateContent = replaceWithFallback(stateContent, 'Last Activity Description', null,
`Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`);

writeStateMd(statePath, stateContent, cwd);
}
Expand Down
17 changes: 17 additions & 0 deletions tests/milestone.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,23 @@ describe('milestone complete command', () => {
assert.strictEqual(output.phases, 2, 'should count only phases 456 and 457');
});

test('updates STATE.md with plain format fields', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap v1.0\n`
);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'STATE.md'),
`# State\n\nStatus: In progress\nLast Activity: 2025-01-01\nLast Activity Description: Working\n`
);

const result = runGsdTools('milestone complete v1.0 --name Test', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);

const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
assert.ok(state.includes('v1.0 milestone complete'), 'plain Status field should be updated');
});

test('handles empty phases directory', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
Expand Down
25 changes: 25 additions & 0 deletions tests/phase.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1323,6 +1323,31 @@ describe('phase complete command', () => {
const req = fs.readFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), 'utf-8');
assert.ok(req.includes('- [ ] **AMT-01**'), 'AMT-01 should remain unchanged');
});

test('updates STATE.md with plain format fields (no bold)', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap\n\n### Phase 1: Only\n**Goal:** Test\n`
);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'STATE.md'),
`# State\n\nPhase: 1 of 1 (Only)\nStatus: In progress\nPlan: 01-01\nLast Activity: 2025-01-01\nLast Activity Description: Working\n`
);

const p1 = path.join(tmpDir, '.planning', 'phases', '01-only');
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 result = runGsdTools('phase complete 1', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);

const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
assert.ok(state.includes('Milestone complete'), 'plain Status field should be updated');
assert.ok(state.includes('Not started'), 'plain Plan field should be updated');
// Verify compound format preserved
assert.ok(state.match(/Phase:.*of\s+1/), 'should preserve "of N" in compound Phase format');
});
});

// ─────────────────────────────────────────────────────────────────────────────
Expand Down