diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs
index cdee42566..558185ac5 100755
--- a/get-shit-done/bin/gsd-tools.cjs
+++ b/get-shit-done/bin/gsd-tools.cjs
@@ -534,8 +534,10 @@ async function main() {
const subcommand = args[1];
if (subcommand === 'complete') {
commands.cmdTodoComplete(cwd, args[2], raw);
+ } else if (subcommand === 'match-phase') {
+ commands.cmdTodoMatchPhase(cwd, args[2], raw);
} else {
- error('Unknown todo subcommand. Available: complete');
+ error('Unknown todo subcommand. Available: complete, match-phase');
}
break;
}
diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs
index d7109df19..ff13ad6cb 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, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs');
+const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, extractCurrentMilestone, toPosixPath, output, error, findPhaseInternal, getRoadmapPhaseInternal } = require('./core.cjs');
const { extractFrontmatter } = require('./frontmatter.cjs');
const { MODEL_PROFILES } = require('./model-profiles.cjs');
@@ -448,6 +448,130 @@ function cmdProgressRender(cwd, format, raw) {
}
}
+/**
+ * Match pending todos against a phase's goal/name/requirements.
+ * Returns todos with relevance scores based on keyword, area, and file overlap.
+ * Used by discuss-phase to surface relevant todos before scope-setting.
+ */
+function cmdTodoMatchPhase(cwd, phase, raw) {
+ if (!phase) { error('phase required for todo match-phase'); }
+
+ const pendingDir = path.join(cwd, '.planning', 'todos', 'pending');
+ const todos = [];
+
+ // Load pending todos
+ try {
+ const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
+ for (const file of files) {
+ try {
+ const content = fs.readFileSync(path.join(pendingDir, file), 'utf-8');
+ const titleMatch = content.match(/^title:\s*(.+)$/m);
+ const areaMatch = content.match(/^area:\s*(.+)$/m);
+ const filesMatch = content.match(/^files:\s*(.+)$/m);
+ const body = content.replace(/^(title|area|files|created|priority):.*$/gm, '').trim();
+
+ todos.push({
+ file,
+ title: titleMatch ? titleMatch[1].trim() : 'Untitled',
+ area: areaMatch ? areaMatch[1].trim() : 'general',
+ files: filesMatch ? filesMatch[1].trim().split(/[,\s]+/).filter(Boolean) : [],
+ body: body.slice(0, 200), // first 200 chars for context
+ });
+ } catch {}
+ }
+ } catch {}
+
+ if (todos.length === 0) {
+ output({ phase, matches: [], todo_count: 0 }, raw);
+ return;
+ }
+
+ // Load phase goal/name from ROADMAP
+ const phaseInfo = getRoadmapPhaseInternal(cwd, phase);
+ const phaseName = phaseInfo ? (phaseInfo.phase_name || '') : '';
+ const phaseGoal = phaseInfo ? (phaseInfo.goal || '') : '';
+ const phaseSection = phaseInfo ? (phaseInfo.section || '') : '';
+
+ // Build keyword set from phase name + goal + section text
+ const phaseText = `${phaseName} ${phaseGoal} ${phaseSection}`.toLowerCase();
+ const stopWords = new Set(['the', 'and', 'for', 'with', 'from', 'that', 'this', 'will', 'are', 'was', 'has', 'have', 'been', 'not', 'but', 'all', 'can', 'into', 'each', 'when', 'any', 'use', 'new']);
+ const phaseKeywords = new Set(
+ phaseText.split(/[\s\-_/.,;:()\[\]{}|]+/)
+ .map(w => w.replace(/[^a-z0-9]/g, ''))
+ .filter(w => w.length > 2 && !stopWords.has(w))
+ );
+
+ // Find phase directory to get expected file paths
+ const phaseInfoDisk = findPhaseInternal(cwd, phase);
+ const phasePlans = [];
+ if (phaseInfoDisk && phaseInfoDisk.found) {
+ try {
+ const phaseDir = path.join(cwd, phaseInfoDisk.directory);
+ const planFiles = fs.readdirSync(phaseDir).filter(f => f.endsWith('-PLAN.md'));
+ for (const pf of planFiles) {
+ try {
+ const planContent = fs.readFileSync(path.join(phaseDir, pf), 'utf-8');
+ const fmFiles = planContent.match(/files_modified:\s*\[([^\]]*)\]/);
+ if (fmFiles) {
+ phasePlans.push(...fmFiles[1].split(',').map(s => s.trim().replace(/['"]/g, '')).filter(Boolean));
+ }
+ } catch {}
+ }
+ } catch {}
+ }
+
+ // Score each todo for relevance
+ const matches = [];
+ for (const todo of todos) {
+ let score = 0;
+ const reasons = [];
+
+ // Keyword match: todo title/body terms in phase text
+ const todoWords = `${todo.title} ${todo.body}`.toLowerCase()
+ .split(/[\s\-_/.,;:()\[\]{}|]+/)
+ .map(w => w.replace(/[^a-z0-9]/g, ''))
+ .filter(w => w.length > 2 && !stopWords.has(w));
+
+ const matchedKeywords = todoWords.filter(w => phaseKeywords.has(w));
+ if (matchedKeywords.length > 0) {
+ score += Math.min(matchedKeywords.length * 0.2, 0.6);
+ reasons.push(`keywords: ${[...new Set(matchedKeywords)].slice(0, 5).join(', ')}`);
+ }
+
+ // Area match: todo area appears in phase text
+ if (todo.area !== 'general' && phaseText.includes(todo.area.toLowerCase())) {
+ score += 0.3;
+ reasons.push(`area: ${todo.area}`);
+ }
+
+ // File match: todo files overlap with phase plan files
+ if (todo.files.length > 0 && phasePlans.length > 0) {
+ const fileOverlap = todo.files.filter(f =>
+ phasePlans.some(pf => pf.includes(f) || f.includes(pf))
+ );
+ if (fileOverlap.length > 0) {
+ score += 0.4;
+ reasons.push(`files: ${fileOverlap.slice(0, 3).join(', ')}`);
+ }
+ }
+
+ if (score > 0) {
+ matches.push({
+ file: todo.file,
+ title: todo.title,
+ area: todo.area,
+ score: Math.round(score * 100) / 100,
+ reasons,
+ });
+ }
+ }
+
+ // Sort by score descending
+ matches.sort((a, b) => b.score - a.score);
+
+ output({ phase, matches, todo_count: todos.length }, raw);
+}
+
function cmdTodoComplete(cwd, filename, raw) {
if (!filename) {
error('filename required for todo complete');
@@ -704,6 +828,7 @@ module.exports = {
cmdWebsearch,
cmdProgressRender,
cmdTodoComplete,
+ cmdTodoMatchPhase,
cmdScaffold,
cmdStats,
};
diff --git a/get-shit-done/workflows/discuss-phase.md b/get-shit-done/workflows/discuss-phase.md
index d8f7a6f40..d21c1fe4a 100644
--- a/get-shit-done/workflows/discuss-phase.md
+++ b/get-shit-done/workflows/discuss-phase.md
@@ -242,6 +242,47 @@ Structure the extracted information:
**If no prior context exists:** Continue without — this is expected for early phases.
+
+Check if any pending todos are relevant to this phase's scope. Surfaces backlog items that might otherwise be missed.
+
+**Load and match todos:**
+```bash
+TODO_MATCHES=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" todo match-phase "${PHASE_NUMBER}")
+```
+
+Parse JSON for: `todo_count`, `matches[]` (each with `file`, `title`, `area`, `score`, `reasons`).
+
+**If `todo_count` is 0 or `matches` is empty:** Skip silently — no workflow slowdown.
+
+**If matches found:**
+
+Present matched todos to the user. Show each match with its title, area, and why it matched:
+
+```
+📋 Found {N} pending todo(s) that may be relevant to Phase {X}:
+
+{For each match:}
+- **{title}** (area: {area}, relevance: {score}) — matched on {reasons}
+```
+
+Use AskUserQuestion (multiSelect) asking which todos to fold into this phase's scope:
+
+```
+Which of these todos should be folded into Phase {X} scope?
+(Select any that apply, or none to skip)
+```
+
+**For selected (folded) todos:**
+- Store internally as `` for inclusion in CONTEXT.md `` section
+- These become additional scope items that downstream agents (researcher, planner) will see
+
+**For unselected (reviewed but not folded) todos:**
+- Store internally as `` for inclusion in CONTEXT.md `` section
+- This prevents future phases from re-surfacing the same todos as "missed"
+
+**Auto mode (`--auto`):** Fold all todos with score >= 0.4 automatically. Log the selection.
+
+
Lightweight scan of existing code to inform gray area identification and discussion. Uses ~10% context — acceptable for an interactive session.
@@ -544,6 +585,11 @@ mkdir -p ".planning/phases/${padded_phase}-${phase_slug}"
### Claude's Discretion
[Areas where user said "you decide" — note that Claude has flexibility here]
+### Folded Todos
+[If any todos were folded into scope from the cross_reference_todos step, list them here.
+Each entry should include the todo title, original problem, and how it fits this phase's scope.
+If no todos were folded: omit this subsection entirely.]
+
@@ -595,6 +641,12 @@ Every entry needs a full relative path — not just a name.]
[Ideas that came up but belong in other phases. Don't lose them.]
+### Reviewed Todos (not folded)
+[If any todos were reviewed in cross_reference_todos but not folded into scope,
+list them here so future phases know they were considered.
+Each entry: todo title + reason it was deferred (out of scope, belongs in Phase Y, etc.)
+If no reviewed-but-deferred todos: omit this subsection entirely.]
+
[If none: "None — discussion stayed within phase scope"]
diff --git a/tests/commands.test.cjs b/tests/commands.test.cjs
index b43d2827a..f84624e47 100644
--- a/tests/commands.test.cjs
+++ b/tests/commands.test.cjs
@@ -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
// ─────────────────────────────────────────────────────────────────────────────