diff --git a/get-shit-done/bin/gsd-tools.js b/get-shit-done/bin/gsd-tools.js index 39ba9b32b..840905b70 100755 --- a/get-shit-done/bin/gsd-tools.js +++ b/get-shit-done/bin/gsd-tools.js @@ -51,7 +51,8 @@ * progress [json|table|bar] Render progress in various formats * * Todos: - * todo complete Move todo from pending to completed + * todo start Move todo from pending to in-progress + * todo complete Move todo from in-progress (or pending) to done * * Scaffolding: * scaffold context --phase Create CONTEXT.md template @@ -517,9 +518,12 @@ function cmdCurrentTimestamp(format, raw) { function cmdListTodos(cwd, area, raw) { const pendingDir = path.join(cwd, '.planning', 'todos', 'pending'); + const inProgressDir = path.join(cwd, '.planning', 'todos', 'in-progress'); let count = 0; const todos = []; + let inProgressCount = 0; + const inProgressTodos = []; try { const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md')); @@ -548,7 +552,35 @@ function cmdListTodos(cwd, area, raw) { } } catch {} - const result = { count, todos }; + try { + const files = fs.readdirSync(inProgressDir).filter(f => f.endsWith('.md')); + + for (const file of files) { + try { + const content = fs.readFileSync(path.join(inProgressDir, file), 'utf-8'); + const createdMatch = content.match(/^created:\s*(.+)$/m); + const titleMatch = content.match(/^title:\s*(.+)$/m); + const areaMatch = content.match(/^area:\s*(.+)$/m); + const startedMatch = content.match(/^started:\s*(.+)$/m); + + const todoArea = areaMatch ? areaMatch[1].trim() : 'general'; + + if (area && todoArea !== area) continue; + + inProgressCount++; + inProgressTodos.push({ + file, + created: createdMatch ? createdMatch[1].trim() : 'unknown', + started: startedMatch ? startedMatch[1].trim() : 'unknown', + title: titleMatch ? titleMatch[1].trim() : 'Untitled', + area: todoArea, + path: path.join('.planning', 'todos', 'in-progress', file), + }); + } catch {} + } + } catch {} + + const result = { count, todos, in_progress_count: inProgressCount, in_progress_todos: inProgressTodos }; output(result, raw, count.toString()); } @@ -3386,6 +3418,35 @@ function cmdProgressRender(cwd, format, raw) { } } +// ─── Todo Start ────────────────────────────────────────────────────────────── + +function cmdTodoStart(cwd, filename, raw) { + if (!filename) { + error('filename required for todo start'); + } + + const pendingDir = path.join(cwd, '.planning', 'todos', 'pending'); + const inProgressDir = path.join(cwd, '.planning', 'todos', 'in-progress'); + const sourcePath = path.join(pendingDir, filename); + + if (!fs.existsSync(sourcePath)) { + error(`Todo not found: ${filename}`); + } + + // Ensure in-progress directory exists + fs.mkdirSync(inProgressDir, { recursive: true }); + + // Read, add started timestamp, move + let content = fs.readFileSync(sourcePath, 'utf-8'); + const today = new Date().toISOString().split('T')[0]; + content = `started: ${today}\n` + content; + + fs.writeFileSync(path.join(inProgressDir, filename), content, 'utf-8'); + fs.unlinkSync(sourcePath); + + output({ started: true, file: filename, date: today }, raw, 'started'); +} + // ─── Todo Complete ──────────────────────────────────────────────────────────── function cmdTodoComplete(cwd, filename, raw) { @@ -3393,23 +3454,28 @@ function cmdTodoComplete(cwd, filename, raw) { error('filename required for todo complete'); } + const inProgressDir = path.join(cwd, '.planning', 'todos', 'in-progress'); const pendingDir = path.join(cwd, '.planning', 'todos', 'pending'); - const completedDir = path.join(cwd, '.planning', 'todos', 'completed'); - const sourcePath = path.join(pendingDir, filename); + const doneDir = path.join(cwd, '.planning', 'todos', 'done'); + // Check in-progress first, then fall back to pending + let sourcePath = path.join(inProgressDir, filename); if (!fs.existsSync(sourcePath)) { - error(`Todo not found: ${filename}`); + sourcePath = path.join(pendingDir, filename); + if (!fs.existsSync(sourcePath)) { + error(`Todo not found: ${filename}`); + } } - // Ensure completed directory exists - fs.mkdirSync(completedDir, { recursive: true }); + // Ensure done directory exists + fs.mkdirSync(doneDir, { recursive: true }); // Read, add completion timestamp, move let content = fs.readFileSync(sourcePath, 'utf-8'); const today = new Date().toISOString().split('T')[0]; content = `completed: ${today}\n` + content; - fs.writeFileSync(path.join(completedDir, filename), content, 'utf-8'); + fs.writeFileSync(path.join(doneDir, filename), content, 'utf-8'); fs.unlinkSync(sourcePath); output({ completed: true, file: filename, date: today }, raw, 'completed'); @@ -3980,7 +4046,7 @@ function cmdInitTodos(cwd, area, raw) { const config = loadConfig(cwd); const now = new Date(); - // List todos (reuse existing logic) + // List pending todos const pendingDir = path.join(cwd, '.planning', 'todos', 'pending'); let count = 0; const todos = []; @@ -4009,6 +4075,37 @@ function cmdInitTodos(cwd, area, raw) { } } catch {} + // List in-progress todos + const inProgressDir = path.join(cwd, '.planning', 'todos', 'in-progress'); + let inProgressCount = 0; + const inProgressTodos = []; + + try { + const files = fs.readdirSync(inProgressDir).filter(f => f.endsWith('.md')); + for (const file of files) { + try { + const content = fs.readFileSync(path.join(inProgressDir, file), 'utf-8'); + const createdMatch = content.match(/^created:\s*(.+)$/m); + const titleMatch = content.match(/^title:\s*(.+)$/m); + const areaMatch = content.match(/^area:\s*(.+)$/m); + const startedMatch = content.match(/^started:\s*(.+)$/m); + const todoArea = areaMatch ? areaMatch[1].trim() : 'general'; + + if (area && todoArea !== area) continue; + + inProgressCount++; + inProgressTodos.push({ + file, + created: createdMatch ? createdMatch[1].trim() : 'unknown', + started: startedMatch ? startedMatch[1].trim() : 'unknown', + title: titleMatch ? titleMatch[1].trim() : 'Untitled', + area: todoArea, + path: path.join('.planning', 'todos', 'in-progress', file), + }); + } catch {} + } + } catch {} + const result = { // Config commit_docs: config.commit_docs, @@ -4020,16 +4117,20 @@ function cmdInitTodos(cwd, area, raw) { // Todo inventory todo_count: count, todos, + in_progress_count: inProgressCount, + in_progress_todos: inProgressTodos, area_filter: area || null, // Paths pending_dir: '.planning/todos/pending', - completed_dir: '.planning/todos/completed', + in_progress_dir: '.planning/todos/in-progress', + done_dir: '.planning/todos/done', // File existence planning_exists: pathExistsInternal(cwd, '.planning'), todos_dir_exists: pathExistsInternal(cwd, '.planning/todos'), pending_dir_exists: pathExistsInternal(cwd, '.planning/todos/pending'), + in_progress_dir_exists: pathExistsInternal(cwd, '.planning/todos/in-progress'), }; output(result, raw); @@ -4522,10 +4623,12 @@ async function main() { case 'todo': { const subcommand = args[1]; - if (subcommand === 'complete') { + if (subcommand === 'start') { + cmdTodoStart(cwd, args[2], raw); + } else if (subcommand === 'complete') { cmdTodoComplete(cwd, args[2], raw); } else { - error('Unknown todo subcommand. Available: complete'); + error('Unknown todo subcommand. Available: start, complete'); } break; } diff --git a/get-shit-done/bin/gsd-tools.test.js b/get-shit-done/bin/gsd-tools.test.js index c79786433..42c344dc8 100644 --- a/get-shit-done/bin/gsd-tools.test.js +++ b/get-shit-done/bin/gsd-tools.test.js @@ -1885,6 +1885,60 @@ describe('progress command', () => { }); }); +// ───────────────────────────────────────────────────────────────────────────── +// todo start command +// ───────────────────────────────────────────────────────────────────────────── + +describe('todo start command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('moves todo from pending to in-progress', () => { + const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending'); + fs.mkdirSync(pendingDir, { recursive: true }); + fs.writeFileSync( + path.join(pendingDir, 'add-dark-mode.md'), + `title: Add dark mode\narea: ui\ncreated: 2025-01-01\n` + ); + + const result = runGsdTools('todo start add-dark-mode.md', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.started, true); + + // Verify moved + assert.ok( + !fs.existsSync(path.join(tmpDir, '.planning', 'todos', 'pending', 'add-dark-mode.md')), + 'should be removed from pending' + ); + assert.ok( + fs.existsSync(path.join(tmpDir, '.planning', 'todos', 'in-progress', 'add-dark-mode.md')), + 'should be in in-progress' + ); + + // Verify started timestamp added + const content = fs.readFileSync( + path.join(tmpDir, '.planning', 'todos', 'in-progress', 'add-dark-mode.md'), + 'utf-8' + ); + assert.ok(content.startsWith('started:'), 'should have started timestamp'); + }); + + test('fails for nonexistent todo', () => { + const result = runGsdTools('todo start nonexistent.md', tmpDir); + assert.ok(!result.success, 'should fail'); + assert.ok(result.error.includes('not found'), 'error mentions not found'); + }); +}); + // ───────────────────────────────────────────────────────────────────────────── // todo complete command // ───────────────────────────────────────────────────────────────────────────── @@ -1900,12 +1954,12 @@ describe('todo complete command', () => { cleanup(tmpDir); }); - test('moves todo from pending to completed', () => { - const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending'); - fs.mkdirSync(pendingDir, { recursive: true }); + test('completes todo from in-progress', () => { + const inProgressDir = path.join(tmpDir, '.planning', 'todos', 'in-progress'); + fs.mkdirSync(inProgressDir, { recursive: true }); fs.writeFileSync( - path.join(pendingDir, 'add-dark-mode.md'), - `title: Add dark mode\narea: ui\ncreated: 2025-01-01\n` + path.join(inProgressDir, 'add-dark-mode.md'), + `started: 2025-01-01\ntitle: Add dark mode\narea: ui\ncreated: 2025-01-01\n` ); const result = runGsdTools('todo complete add-dark-mode.md', tmpDir); @@ -1916,22 +1970,47 @@ describe('todo complete command', () => { // Verify moved assert.ok( - !fs.existsSync(path.join(tmpDir, '.planning', 'todos', 'pending', 'add-dark-mode.md')), - 'should be removed from pending' + !fs.existsSync(path.join(tmpDir, '.planning', 'todos', 'in-progress', 'add-dark-mode.md')), + 'should be removed from in-progress' ); assert.ok( - fs.existsSync(path.join(tmpDir, '.planning', 'todos', 'completed', 'add-dark-mode.md')), - 'should be in completed' + fs.existsSync(path.join(tmpDir, '.planning', 'todos', 'done', 'add-dark-mode.md')), + 'should be in done' ); // Verify completion timestamp added const content = fs.readFileSync( - path.join(tmpDir, '.planning', 'todos', 'completed', 'add-dark-mode.md'), + path.join(tmpDir, '.planning', 'todos', 'done', 'add-dark-mode.md'), 'utf-8' ); assert.ok(content.startsWith('completed:'), 'should have completed timestamp'); }); + test('completes todo from pending directly', () => { + const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending'); + fs.mkdirSync(pendingDir, { recursive: true }); + fs.writeFileSync( + path.join(pendingDir, 'fix-bug.md'), + `title: Fix bug\narea: api\ncreated: 2025-01-01\n` + ); + + const result = runGsdTools('todo complete fix-bug.md', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.completed, true); + + // Verify moved to done (not completed) + assert.ok( + !fs.existsSync(path.join(tmpDir, '.planning', 'todos', 'pending', 'fix-bug.md')), + 'should be removed from pending' + ); + assert.ok( + fs.existsSync(path.join(tmpDir, '.planning', 'todos', 'done', 'fix-bug.md')), + 'should be in done' + ); + }); + test('fails for nonexistent todo', () => { const result = runGsdTools('todo complete nonexistent.md', tmpDir); assert.ok(!result.success, 'should fail'); diff --git a/get-shit-done/workflows/add-todo.md b/get-shit-done/workflows/add-todo.md index 39c3737fd..ca69d8da0 100644 --- a/get-shit-done/workflows/add-todo.md +++ b/get-shit-done/workflows/add-todo.md @@ -19,7 +19,7 @@ Extract from init JSON: `commit_docs`, `date`, `timestamp`, `todo_count`, `todos Ensure directories exist: ```bash -mkdir -p .planning/todos/pending .planning/todos/done +mkdir -p .planning/todos/pending .planning/todos/in-progress .planning/todos/done ``` Note existing areas from the todos array for consistency in infer_area step. diff --git a/get-shit-done/workflows/check-todos.md b/get-shit-done/workflows/check-todos.md index e16d1bed5..370a7b5e4 100644 --- a/get-shit-done/workflows/check-todos.md +++ b/get-shit-done/workflows/check-todos.md @@ -105,7 +105,7 @@ Use AskUserQuestion: - header: "Action" - question: "This todo relates to Phase [N]: [name]. What would you like to do?" - options: - - "Work on it now" — move to done, start working + - "Work on it now" — move to in-progress, start working - "Add to phase plan" — include when planning Phase [N] - "Brainstorm approach" — think through before deciding - "Put it back" — return to list @@ -116,7 +116,7 @@ Use AskUserQuestion: - header: "Action" - question: "What would you like to do with this todo?" - options: - - "Work on it now" — move to done, start working + - "Work on it now" — move to in-progress, start working - "Create a phase" — /gsd:add-phase with this scope - "Brainstorm approach" — think through before deciding - "Put it back" — return to list @@ -125,7 +125,7 @@ Use AskUserQuestion: **Work on it now:** ```bash -mv ".planning/todos/pending/[filename]" ".planning/todos/done/" +mv ".planning/todos/pending/[filename]" ".planning/todos/in-progress/" ``` Update STATE.md todo count. Present problem/solution context. Begin work or ask how to proceed. @@ -150,11 +150,11 @@ Re-run `init todos` to get updated count, then update STATE.md "### Pending Todo -If todo was moved to done/, commit the change: +If todo was moved to in-progress/, commit the change: ```bash git rm --cached .planning/todos/pending/[filename] 2>/dev/null || true -node ~/.claude/get-shit-done/bin/gsd-tools.js commit "docs: start work on todo - [title]" --files .planning/todos/done/[filename] .planning/STATE.md +node ~/.claude/get-shit-done/bin/gsd-tools.js commit "docs: start work on todo - [title]" --files .planning/todos/in-progress/[filename] .planning/STATE.md ``` Tool respects `commit_docs` config and gitignore automatically. @@ -172,5 +172,5 @@ Confirm: "Committed: docs: start work on todo - [title]" - [ ] Appropriate actions offered - [ ] Selected action executed - [ ] STATE.md updated if todo count changed -- [ ] Changes committed to git (if todo moved to done/) +- [ ] Changes committed to git (if todo moved to in-progress/)