feat(discuss-phase): cross-reference pending todos against phase scope

Add a cross_reference_todos step to discuss-phase that surfaces relevant
backlog items before scope-setting decisions are made.

Implementation:
- New 'todo match-phase <N>' CLI command (commands.cjs) that scores
  pending todos against a phase's ROADMAP goal using three heuristics:
  keyword overlap, area match, and file path overlap
- New cross_reference_todos step in discuss-phase.md between
  load_prior_context and scout_codebase
- CONTEXT.md template gains 'Folded Todos' subsection in <decisions>
  and 'Reviewed Todos (not folded)' subsection in <deferred>

Design:
- No AI call for matching — pure keyword/area/file heuristics for speed
- Silent skip when todo_count is 0 or no matches (no workflow slowdown)
- Auto mode folds all todos with score >= 0.4 automatically
- Scoring: keywords (up to 0.6), area match (0.3), file overlap (0.4)

Tests: 5 new tests covering empty state, keyword matching, unrelated
todo exclusion, area matching, and score sorting.

Closes #1111
This commit is contained in:
Tom Boucher
2026-03-16 16:51:24 -04:00
parent fc468adb42
commit e84999663c
4 changed files with 273 additions and 2 deletions

View File

@@ -567,6 +567,98 @@ describe('todo complete command', () => {
});
});
// ─────────────────────────────────────────────────────────────────────────────
// todo match-phase command
// ─────────────────────────────────────────────────────────────────────────────
describe('todo match-phase command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => cleanup(tmpDir));
test('returns empty matches when no todos exist', () => {
const result = runGsdTools('todo match-phase 01', tmpDir);
assert.ok(result.success, 'should succeed');
const output = JSON.parse(result.output);
assert.strictEqual(output.todo_count, 0);
assert.deepStrictEqual(output.matches, []);
});
test('matches todo by keyword overlap with phase name', () => {
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
fs.mkdirSync(pendingDir, { recursive: true });
fs.writeFileSync(path.join(pendingDir, 'auth-todo.md'),
'title: Add OAuth token refresh\narea: auth\ncreated: 2026-03-01\n\nNeed to handle token expiry for OAuth flows.');
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'),
'# Roadmap\n\n### Phase 01: Authentication and Session Management\n\n**Goal:** Implement OAuth login and session handling\n');
const result = runGsdTools('todo match-phase 01', tmpDir);
assert.ok(result.success, 'should succeed');
const output = JSON.parse(result.output);
assert.strictEqual(output.todo_count, 1, 'should find 1 todo');
assert.ok(output.matches.length > 0, 'should have matches');
assert.strictEqual(output.matches[0].title, 'Add OAuth token refresh');
assert.ok(output.matches[0].score > 0, 'score should be positive');
assert.ok(output.matches[0].reasons.length > 0, 'should have reasons');
});
test('does not match unrelated todo', () => {
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
fs.mkdirSync(pendingDir, { recursive: true });
fs.writeFileSync(path.join(pendingDir, 'auth-todo.md'),
'title: Add OAuth token refresh\narea: auth\ncreated: 2026-03-01\n\nOAuth token expiry.');
fs.writeFileSync(path.join(pendingDir, 'unrelated-todo.md'),
'title: Fix CSS grid layout in dashboard\narea: ui\ncreated: 2026-03-01\n\nGrid columns break on mobile.');
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'),
'# Roadmap\n\n### Phase 01: Authentication and Session Management\n\n**Goal:** Implement OAuth login and session handling\n');
const result = runGsdTools('todo match-phase 01', tmpDir);
assert.ok(result.success, 'should succeed');
const output = JSON.parse(result.output);
const matchTitles = output.matches.map(m => m.title);
assert.ok(matchTitles.includes('Add OAuth token refresh'), 'auth todo should match');
assert.ok(!matchTitles.includes('Fix CSS grid layout in dashboard'), 'unrelated todo should not match');
});
test('matches todo by area overlap', () => {
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
fs.mkdirSync(pendingDir, { recursive: true });
fs.writeFileSync(path.join(pendingDir, 'auth-todo.md'),
'title: Add OAuth token refresh\narea: auth\ncreated: 2026-03-01\n\nOAuth token handling.');
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'),
'# Roadmap\n\n### Phase 01: Auth System\n\n**Goal:** Build auth module\n');
const result = runGsdTools('todo match-phase 01', tmpDir);
const output = JSON.parse(result.output);
const authMatch = output.matches.find(m => m.title === 'Add OAuth token refresh');
assert.ok(authMatch, 'should find auth todo');
const hasAreaReason = authMatch.reasons.some(r => r.startsWith('area:'));
assert.ok(hasAreaReason, 'should match on area');
});
test('sorts matches by score descending', () => {
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
fs.mkdirSync(pendingDir, { recursive: true });
fs.writeFileSync(path.join(pendingDir, 'weak-match.md'),
'title: Check token format\narea: general\ncreated: 2026-03-01\n\nToken format validation.');
fs.writeFileSync(path.join(pendingDir, 'strong-match.md'),
'title: Session management authentication OAuth token handling\narea: auth\ncreated: 2026-03-01\n\nSession auth OAuth tokens.');
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'),
'# Roadmap\n\n### Phase 01: Authentication and Session Management\n\n**Goal:** Implement OAuth login, session handling, and token management\n');
const result = runGsdTools('todo match-phase 01', tmpDir);
const output = JSON.parse(result.output);
assert.ok(output.matches.length >= 2, 'should have multiple matches');
for (let i = 1; i < output.matches.length; i++) {
assert.ok(output.matches[i - 1].score >= output.matches[i].score,
`match ${i-1} score (${output.matches[i-1].score}) should be >= match ${i} score (${output.matches[i].score})`);
}
});
});
// ─────────────────────────────────────────────────────────────────────────────
// scaffold command
// ─────────────────────────────────────────────────────────────────────────────