* fix(#950): emit status: complete in quick-task SUMMARY frontmatter Add `status: complete` to all four SUMMARY templates (summary.md, summary-minimal.md, summary-standard.md, summary-complex.md), to the executor agent's documented frontmatter field list, and to the quick.md executor constraints block. The audit-open milestone-close scanner (scanQuickTasks) reads this field to decide whether a quick task is done; without it the scanner falls back to `[unknown]` and false-flags finished tasks as open. Writer-side fix; the scanner is correct and unchanged. Blast-radius: no other scanner reads `status:` from phase-plan SUMMARY files. Phase disk_status is derived from file-count heuristics only. Adding the field to the shared template is therefore safe and the value `complete` is semantically accurate for a finished plan. Regression test: tests/bug-950-quick-summary-status-complete.test.cjs - RED: 4 template-contract tests fail before fix, behavioral tests pass - GREEN: all 8 tests pass after fix Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: add changeset for fix/950-quick-summary-status-complete (#951) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#950): assert writer-path contract + scope template checks to YAML frontmatter (adversarial review) - Add `// allow-test-rule: source-text-is-the-product` at file top (before block comment) - Add `extractFrontmatter()` helper that handles both leading-frontmatter files (summary-minimal/standard/complex.md) and fenced-frontmatter files (summary.md, whose frontmatter is embedded inside a ```markdown fence) — assertions now target the actual YAML block, not the whole file - Scope all four [TEMPLATE CONTRACT] tests through extractFrontmatter() so a stray `status: complete` in prose/examples cannot produce a false green; error messages now print the extracted block to aid diagnosis - Add [WRITER-PATH] quick.md test: asserts the <constraints> block instructs the executor to write `status: complete` in SUMMARY frontmatter - Add [WRITER-PATH] gsd-executor.md test: asserts the Frontmatter spec documents `status: complete` as a required field - Sanity-checked: guards fail when `status: complete` is removed from a template or from quick.md, and pass once restored Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
5
.changeset/silly-pandas-munch.md
Normal file
5
.changeset/silly-pandas-munch.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 951
|
||||
---
|
||||
**`audit-open` no longer false-flags completed quick tasks** — quick-task SUMMARYs now carry `status: complete` in frontmatter by construction, so the milestone-close auditor stops reporting finished quick tasks as `[unknown]`.
|
||||
@@ -604,7 +604,7 @@ This file is the canonical output of this step. The orchestrator reads `.plannin
|
||||
|
||||
**Use template:** @~/.claude/gsd-core/templates/summary.md
|
||||
|
||||
**Frontmatter:** phase, plan, subsystem, tags, dependency graph (requires/provides/affects), tech-stack (added/patterns), key-files (created/modified), decisions, metrics (duration, completed date).
|
||||
**Frontmatter:** phase, plan, subsystem, tags, dependency graph (requires/provides/affects), tech-stack (added/patterns), key-files (created/modified), decisions, metrics (duration, completed date), status (`status: complete` — required so the audit-open scanner recognises the summary as done).
|
||||
|
||||
**Title:** `# Phase [X] Plan [Y]: [Name] Summary`
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ patterns-established:
|
||||
- "Pattern 1: description"
|
||||
duration: Xmin
|
||||
completed: YYYY-MM-DD
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Phase [X]: [Name] Summary (Complex)
|
||||
|
||||
@@ -15,6 +15,7 @@ key-files:
|
||||
key-decisions: []
|
||||
duration: Xmin
|
||||
completed: YYYY-MM-DD
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Phase [X]: [Name] Summary (Minimal)
|
||||
|
||||
@@ -16,6 +16,7 @@ key-decisions:
|
||||
- "Decision 1"
|
||||
duration: Xmin
|
||||
completed: YYYY-MM-DD
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Phase [X]: [Name] Summary
|
||||
|
||||
@@ -43,6 +43,7 @@ requirements-completed: [] # REQUIRED — Copy ALL requirement IDs from this pl
|
||||
# Metrics
|
||||
duration: Xmin
|
||||
completed: YYYY-MM-DD
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Phase [X]: [Name] Summary
|
||||
|
||||
@@ -728,7 +728,7 @@ SUMMARY.md and stop — the user must rerun with worktrees disabled.
|
||||
- Execute all tasks in the plan
|
||||
- Commit each task atomically (code changes only)
|
||||
- Run the <submodule_commit_guard> bash block before every \`git commit\` if SUBMODULE_PATHS is non-empty
|
||||
- Create summary at: ${QUICK_DIR}/${quick_id}-SUMMARY.md
|
||||
- Create summary at: ${QUICK_DIR}/${quick_id}-SUMMARY.md with `status: complete` in SUMMARY frontmatter (required so the audit-open milestone-close scanner recognises the task as done, not [unknown])
|
||||
- Do NOT commit docs artifacts (SUMMARY.md, STATE.md, PLAN.md) — the orchestrator handles the docs commit in Step 8
|
||||
- Do NOT update ROADMAP.md (quick tasks are separate from planned phases)
|
||||
</constraints>
|
||||
|
||||
301
tests/bug-950-quick-summary-status-complete.test.cjs
Normal file
301
tests/bug-950-quick-summary-status-complete.test.cjs
Normal file
@@ -0,0 +1,301 @@
|
||||
// allow-test-rule: source-text-is-the-product
|
||||
/**
|
||||
* Regression tests for bug #950
|
||||
*
|
||||
* audit-open chronically flagged genuinely-complete quick tasks as [unknown]
|
||||
* because NO shipped summary template carried a `status:` frontmatter field —
|
||||
* so status was only emitted when the writing agent improvised it.
|
||||
*
|
||||
* The fix: add `status: complete` to all four summary templates and enforce it
|
||||
* in the executor agent + quick.md workflow. Tests here exercise the scanner
|
||||
* directly via auditOpenArtifacts() and also guard template text as a secondary
|
||||
* contract check.
|
||||
*
|
||||
* Primary guard: behavioral audit-scanner tests (tasks read by scanQuickTasks)
|
||||
* Secondary guard: template-contract text checks (template text IS the runtime contract)
|
||||
* Writer-path guard: contract assertions on quick.md + gsd-executor.md (source-text-is-the-product)
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { describe, test, before, after } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
|
||||
const auditModule = require('../gsd-core/bin/lib/audit.cjs');
|
||||
const { auditOpenArtifacts } = auditModule;
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const TEMPLATES_DIR = path.resolve(__dirname, '..', 'gsd-core', 'templates');
|
||||
const QUICK_MD = path.resolve(__dirname, '..', 'gsd-core', 'workflows', 'quick.md');
|
||||
const EXECUTOR_MD = path.resolve(__dirname, '..', 'agents', 'gsd-executor.md');
|
||||
|
||||
function mkTmp() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'bug-950-'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the first YAML frontmatter block from a file's content.
|
||||
*
|
||||
* Two layouts are handled:
|
||||
* - Leading frontmatter: file starts with `---\n…\n---` (summary-minimal/standard/complex.md)
|
||||
* - Fenced frontmatter: frontmatter lives inside a ```markdown … ``` fence (summary.md,
|
||||
* whose content IS a markdown example showing the template). In that case we extract
|
||||
* the `---\n…\n---` block that sits immediately after the opening fence line.
|
||||
*
|
||||
* Returns the raw text of the YAML block (between the two `---` delimiters, exclusive),
|
||||
* or null if no frontmatter could be found.
|
||||
*/
|
||||
function extractFrontmatter(content) {
|
||||
// Case 1: file begins with --- (leading frontmatter)
|
||||
if (/^---\r?\n/.test(content)) {
|
||||
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
// Case 2: frontmatter is embedded inside a fenced block (```markdown\n---\n…\n---\n)
|
||||
const fenceMatch = content.match(/```(?:markdown|md)?\r?\n(---\r?\n[\s\S]*?\r?\n---)\r?\n/);
|
||||
if (fenceMatch) {
|
||||
// Strip the outer --- delimiters to get just the YAML body
|
||||
const block = fenceMatch[1];
|
||||
const inner = block.match(/^---\r?\n([\s\S]*?)\r?\n---$/);
|
||||
return inner ? inner[1] : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
describe('bug #950: quick-task SUMMARY must carry status: complete', () => {
|
||||
// Ensure GSD env vars do not redirect planningDir() away from our fixture.
|
||||
let prevProject, prevWorkstream;
|
||||
before(() => {
|
||||
prevProject = process.env.GSD_PROJECT;
|
||||
prevWorkstream = process.env.GSD_WORKSTREAM;
|
||||
delete process.env.GSD_PROJECT;
|
||||
delete process.env.GSD_WORKSTREAM;
|
||||
});
|
||||
after(() => {
|
||||
if (prevProject !== undefined) process.env.GSD_PROJECT = prevProject;
|
||||
if (prevWorkstream !== undefined) process.env.GSD_WORKSTREAM = prevWorkstream;
|
||||
});
|
||||
|
||||
// ── Behavioral: scanner recognizes complete quick tasks ───────────────────
|
||||
|
||||
test('[PRIMARY] quick task SUMMARY with status: complete is NOT flagged open', () => {
|
||||
// Simulates an executor that correctly wrote the SUMMARY with status: complete
|
||||
// (as required after the fix). The scanner must report 0 open quick tasks.
|
||||
const cwd = mkTmp();
|
||||
try {
|
||||
const quickId = '260609-test-status-complete';
|
||||
const taskDir = path.join(cwd, '.planning', 'quick', quickId);
|
||||
fs.mkdirSync(taskDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, `${quickId}-SUMMARY.md`),
|
||||
[
|
||||
'---',
|
||||
'status: complete',
|
||||
'date: 2026-06-09',
|
||||
'slug: test-status-complete',
|
||||
'---',
|
||||
'',
|
||||
'# Quick Task Summary',
|
||||
'',
|
||||
'Task completed successfully.',
|
||||
].join('\n'),
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
const result = auditOpenArtifacts(cwd);
|
||||
const realQuickTasks = result.items.quick_tasks.filter(
|
||||
i => !i.scan_error && !i._remainder_count
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
realQuickTasks.length,
|
||||
0,
|
||||
`quick task SUMMARY with status: complete must NOT appear as open; ` +
|
||||
`got: ${JSON.stringify(realQuickTasks)}`
|
||||
);
|
||||
assert.equal(result.counts.quick_tasks, 0);
|
||||
} finally {
|
||||
cleanup(cwd);
|
||||
}
|
||||
});
|
||||
|
||||
test('[PRIMARY] quick task SUMMARY without status: field is still flagged [unknown]', () => {
|
||||
// Negative case: a SUMMARY that lacks status: still surfaces as [unknown].
|
||||
// This proves the scanner still catches real gaps — the fix must be on the writer side.
|
||||
const cwd = mkTmp();
|
||||
try {
|
||||
const quickId = '260609-test-no-status';
|
||||
const taskDir = path.join(cwd, '.planning', 'quick', quickId);
|
||||
fs.mkdirSync(taskDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, `${quickId}-SUMMARY.md`),
|
||||
[
|
||||
'---',
|
||||
'date: 2026-06-09',
|
||||
'slug: test-no-status',
|
||||
'---',
|
||||
'',
|
||||
'# Quick Task Summary',
|
||||
'',
|
||||
'Task done, but no status field.',
|
||||
].join('\n'),
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
const result = auditOpenArtifacts(cwd);
|
||||
const realQuickTasks = result.items.quick_tasks.filter(
|
||||
i => !i.scan_error && !i._remainder_count
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
realQuickTasks.length,
|
||||
1,
|
||||
`quick task SUMMARY without status: must appear as open (unknown); ` +
|
||||
`got: ${JSON.stringify(realQuickTasks)}`
|
||||
);
|
||||
assert.equal(realQuickTasks[0].status, 'unknown', 'expected status to be unknown');
|
||||
} finally {
|
||||
cleanup(cwd);
|
||||
}
|
||||
});
|
||||
|
||||
test('[PRIMARY] quick task without any SUMMARY is still flagged [missing]', () => {
|
||||
// Proves the missing-SUMMARY case still surfaces.
|
||||
const cwd = mkTmp();
|
||||
try {
|
||||
const quickId = '260609-test-missing-summary';
|
||||
const taskDir = path.join(cwd, '.planning', 'quick', quickId);
|
||||
fs.mkdirSync(taskDir, { recursive: true });
|
||||
// No SUMMARY file at all.
|
||||
|
||||
const result = auditOpenArtifacts(cwd);
|
||||
const realQuickTasks = result.items.quick_tasks.filter(
|
||||
i => !i.scan_error && !i._remainder_count
|
||||
);
|
||||
|
||||
assert.equal(realQuickTasks.length, 1, 'missing SUMMARY must still be flagged');
|
||||
assert.equal(realQuickTasks[0].status, 'missing');
|
||||
} finally {
|
||||
cleanup(cwd);
|
||||
}
|
||||
});
|
||||
|
||||
test('[PRIMARY] SUMMARY with status: COMPLETE (uppercase) is also recognized', () => {
|
||||
// Scanner lowercases before comparing — verify case-insensitivity holds.
|
||||
const cwd = mkTmp();
|
||||
try {
|
||||
const quickId = '260609-test-uppercase-complete';
|
||||
const taskDir = path.join(cwd, '.planning', 'quick', quickId);
|
||||
fs.mkdirSync(taskDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, `${quickId}-SUMMARY.md`),
|
||||
'---\nstatus: COMPLETE\n---\n# Summary\nDone.\n',
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
const result = auditOpenArtifacts(cwd);
|
||||
const realQuickTasks = result.items.quick_tasks.filter(
|
||||
i => !i.scan_error && !i._remainder_count
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
realQuickTasks.length,
|
||||
0,
|
||||
`quick task SUMMARY with status: COMPLETE (uppercase) must not appear as open; ` +
|
||||
`got: ${JSON.stringify(realQuickTasks)}`
|
||||
);
|
||||
} finally {
|
||||
cleanup(cwd);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Secondary: template-contract checks ──────────────────────────────────
|
||||
// Assertions are scoped to the actual YAML frontmatter block, not the whole file,
|
||||
// so a stray `status: complete` in prose or examples cannot produce a false green.
|
||||
|
||||
test('[TEMPLATE CONTRACT] summary.md contains status: complete in frontmatter', () => {
|
||||
// summary.md is a documentation template — its frontmatter lives inside a
|
||||
// ```markdown fence. extractFrontmatter() finds and returns that block.
|
||||
const content = fs.readFileSync(path.join(TEMPLATES_DIR, 'summary.md'), 'utf-8');
|
||||
const fm = extractFrontmatter(content);
|
||||
assert.ok(
|
||||
fm !== null,
|
||||
'gsd-core/templates/summary.md: could not locate a YAML frontmatter block (leading --- or fenced ```markdown --- block)'
|
||||
);
|
||||
assert.ok(
|
||||
/^status:\s*complete\s*$/m.test(fm),
|
||||
`gsd-core/templates/summary.md: \`status: complete\` not found in the frontmatter block.\n` +
|
||||
`Frontmatter extracted:\n${fm}`
|
||||
);
|
||||
});
|
||||
|
||||
test('[TEMPLATE CONTRACT] summary-minimal.md contains status: complete in frontmatter', () => {
|
||||
const content = fs.readFileSync(path.join(TEMPLATES_DIR, 'summary-minimal.md'), 'utf-8');
|
||||
const fm = extractFrontmatter(content);
|
||||
assert.ok(
|
||||
fm !== null,
|
||||
'gsd-core/templates/summary-minimal.md: could not locate a leading YAML frontmatter block'
|
||||
);
|
||||
assert.ok(
|
||||
/^status:\s*complete\s*$/m.test(fm),
|
||||
`gsd-core/templates/summary-minimal.md: \`status: complete\` not found in the frontmatter block.\n` +
|
||||
`Frontmatter extracted:\n${fm}`
|
||||
);
|
||||
});
|
||||
|
||||
test('[TEMPLATE CONTRACT] summary-standard.md contains status: complete in frontmatter', () => {
|
||||
const content = fs.readFileSync(path.join(TEMPLATES_DIR, 'summary-standard.md'), 'utf-8');
|
||||
const fm = extractFrontmatter(content);
|
||||
assert.ok(
|
||||
fm !== null,
|
||||
'gsd-core/templates/summary-standard.md: could not locate a leading YAML frontmatter block'
|
||||
);
|
||||
assert.ok(
|
||||
/^status:\s*complete\s*$/m.test(fm),
|
||||
`gsd-core/templates/summary-standard.md: \`status: complete\` not found in the frontmatter block.\n` +
|
||||
`Frontmatter extracted:\n${fm}`
|
||||
);
|
||||
});
|
||||
|
||||
test('[TEMPLATE CONTRACT] summary-complex.md contains status: complete in frontmatter', () => {
|
||||
const content = fs.readFileSync(path.join(TEMPLATES_DIR, 'summary-complex.md'), 'utf-8');
|
||||
const fm = extractFrontmatter(content);
|
||||
assert.ok(
|
||||
fm !== null,
|
||||
'gsd-core/templates/summary-complex.md: could not locate a leading YAML frontmatter block'
|
||||
);
|
||||
assert.ok(
|
||||
/^status:\s*complete\s*$/m.test(fm),
|
||||
`gsd-core/templates/summary-complex.md: \`status: complete\` not found in the frontmatter block.\n` +
|
||||
`Frontmatter extracted:\n${fm}`
|
||||
);
|
||||
});
|
||||
|
||||
// ── Writer-path contract checks ───────────────────────────────────────────
|
||||
// Guards quick.md and gsd-executor.md so a future edit removing `status: complete`
|
||||
// from the SUMMARY-creation instructions would fail the suite before the bug recurs.
|
||||
// (source-text-is-the-product: the deployed .md text IS the runtime contract for agents)
|
||||
|
||||
test('[WRITER-PATH] quick.md constraints require status: complete in SUMMARY frontmatter', () => {
|
||||
const content = fs.readFileSync(QUICK_MD, 'utf-8');
|
||||
assert.ok(
|
||||
/status:\s*complete/.test(content),
|
||||
'gsd-core/workflows/quick.md must instruct the executor to write `status: complete` in the SUMMARY frontmatter. ' +
|
||||
'The <constraints> block must contain the `status: complete` requirement so a future edit cannot silently drop it.'
|
||||
);
|
||||
});
|
||||
|
||||
test('[WRITER-PATH] gsd-executor.md frontmatter spec requires status: complete', () => {
|
||||
const content = fs.readFileSync(EXECUTOR_MD, 'utf-8');
|
||||
assert.ok(
|
||||
/status[\s\S]{0,40}complete/.test(content),
|
||||
'agents/gsd-executor.md must document `status: complete` as a required SUMMARY frontmatter field. ' +
|
||||
'The Frontmatter section must include `status: complete` so the executor always emits it.'
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user