fix(lifecycle): extract one-liner from summary body when not in frontmatter

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 <noreply@anthropic.com>
This commit is contained in:
j2h4u
2026-03-04 03:10:49 +05:00
parent efc398c554
commit 104a39e573
5 changed files with 83 additions and 5 deletions

View File

@@ -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'] || [],

View File

@@ -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,

View File

@@ -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 <task XML tags, then ## Task N markdown headers

View File

@@ -363,6 +363,37 @@ requirements-completed:
assert.strictEqual(output.decisions, undefined, 'decisions excluded');
});
test('extracts one-liner from body when not in frontmatter', () => {
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 });

View File

@@ -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'),