From 104a39e573b35469b4b13f9290f01114c4e65df9 Mon Sep 17 00:00:00 2001 From: j2h4u <39818683+j2h4u@users.noreply.github.com> Date: Wed, 4 Mar 2026 03:10:49 +0500 Subject: [PATCH] fix(lifecycle): extract one-liner from summary body when not in frontmatter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The summary template puts the one-liner as a `**bold**` line after the `# Phase N` heading, but `cmdSummaryExtract` and `cmdMilestoneComplete` only checked frontmatter `one-liner` field — which is often empty. Adds `extractOneLinerFromBody()` to core.cjs as a fallback that parses the first `**...**` line after the heading. Co-Authored-By: Claude Opus 4.6 --- get-shit-done/bin/lib/commands.cjs | 4 ++-- get-shit-done/bin/lib/core.cjs | 18 +++++++++++++++++ get-shit-done/bin/lib/milestone.cjs | 7 ++++--- tests/commands.test.cjs | 31 +++++++++++++++++++++++++++++ tests/milestone.test.cjs | 28 ++++++++++++++++++++++++++ 5 files changed, 83 insertions(+), 5 deletions(-) diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index 7227ea5c8..10696d0b1 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.cjs @@ -4,7 +4,7 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); -const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, extractCurrentMilestone, planningPaths, toPosixPath, output, error, findPhaseInternal, getRoadmapPhaseInternal } = require('./core.cjs'); +const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, extractCurrentMilestone, planningPaths, toPosixPath, output, error, findPhaseInternal, extractOneLinerFromBody, getRoadmapPhaseInternal } = require('./core.cjs'); const { extractFrontmatter } = require('./frontmatter.cjs'); const { MODEL_PROFILES } = require('./model-profiles.cjs'); @@ -296,7 +296,7 @@ function cmdSummaryExtract(cwd, summaryPath, fields, raw) { // Build full result const fullResult = { path: summaryPath, - one_liner: fm['one-liner'] || null, + one_liner: fm['one-liner'] || extractOneLinerFromBody(content) || null, key_files: fm['key-files'] || [], tech_added: (fm['tech-stack'] && fm['tech-stack'].added) || [], patterns: fm['patterns-established'] || [], diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index f39ac5bd2..807f883d7 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -642,6 +642,23 @@ function resolveModelInternal(cwd, agentType) { return alias; } +// ─── Summary body helpers ───────────────────────────────────────────────── + +/** + * Extract a one-liner from the summary body when it's not in frontmatter. + * The summary template defines one-liner as a bold markdown line after the heading: + * # Phase X: Name Summary + * **[substantive one-liner text]** + */ +function extractOneLinerFromBody(content) { + if (!content) return null; + // Strip frontmatter first + const body = content.replace(/^---\n[\s\S]*?\n---\n*/, ''); + // Find the first **...** line after a # heading + const match = body.match(/^#[^\n]*\n+\*\*([^*]+)\*\*/m); + return match ? match[1].trim() : null; +} + // ─── Misc utilities ─────────────────────────────────────────────────────────── function pathExistsInternal(cwd, targetPath) { @@ -760,6 +777,7 @@ module.exports = { extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, + extractOneLinerFromBody, MODEL_ALIAS_MAP, planningDir, planningPaths, diff --git a/get-shit-done/bin/lib/milestone.cjs b/get-shit-done/bin/lib/milestone.cjs index af473e59e..b105b9f2d 100644 --- a/get-shit-done/bin/lib/milestone.cjs +++ b/get-shit-done/bin/lib/milestone.cjs @@ -4,7 +4,7 @@ const fs = require('fs'); const path = require('path'); -const { escapeRegex, getMilestonePhaseFilter, normalizeMd, planningPaths, output, error } = require('./core.cjs'); +const { escapeRegex, getMilestonePhaseFilter, extractOneLinerFromBody, normalizeMd, planningPaths, output, error } = require('./core.cjs'); const { extractFrontmatter } = require('./frontmatter.cjs'); const { writeStateMd } = require('./state.cjs'); @@ -131,8 +131,9 @@ function cmdMilestoneComplete(cwd, version, options, raw) { try { const content = fs.readFileSync(path.join(phasesDir, dir, s), 'utf-8'); const fm = extractFrontmatter(content); - if (fm['one-liner']) { - accomplishments.push(fm['one-liner']); + const oneLiner = fm['one-liner'] || extractOneLinerFromBody(content); + if (oneLiner) { + accomplishments.push(oneLiner); } // Count tasks: prefer **Tasks:** N from Performance section, // then { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync( + path.join(phaseDir, '01-01-SUMMARY.md'), + `--- +phase: "01" +key-files: + - src/lib/db.ts +--- + +# Phase 1: Foundation Summary + +**JWT auth with refresh rotation using jose library** + +## Performance + +- **Duration:** 28 min +- **Tasks:** 5 +` + ); + + const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.one_liner, 'JWT auth with refresh rotation using jose library', + 'one-liner should be extracted from body **bold** line'); + }); + test('handles missing frontmatter fields gracefully', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); fs.mkdirSync(phaseDir, { recursive: true }); diff --git a/tests/milestone.test.cjs b/tests/milestone.test.cjs index 7dcaee60d..77b3c8376 100644 --- a/tests/milestone.test.cjs +++ b/tests/milestone.test.cjs @@ -448,6 +448,34 @@ describe('milestone complete command', () => { assert.strictEqual(output.tasks, 7, 'should count tasks from **Tasks:** N field'); }); + test('extracts one-liner from body when not in frontmatter', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap v1.0\n\n### Phase 1: Foundation\n**Goal:** Setup\n` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n` + ); + + const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(p1, { recursive: true }); + // No one-liner in frontmatter, but present in body as bold line + fs.writeFileSync( + path.join(p1, '01-01-SUMMARY.md'), + `---\nphase: "01"\n---\n\n# Phase 1: Foundation Summary\n\n**JWT auth with refresh rotation using jose library**\n\n## Performance\n` + ); + + const result = runGsdTools('milestone complete v1.0 --name MVP', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok( + output.accomplishments.includes('JWT auth with refresh rotation using jose library'), + 'should extract one-liner from body bold line' + ); + }); + test('handles empty phases directory', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'),