@damper/cli 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/start.js +135 -19
- package/dist/services/claude.d.ts +8 -0
- package/dist/services/claude.js +28 -0
- package/dist/templates/CLAUDE_APPEND.md.js +5 -4
- package/dist/templates/TASK_CONTEXT.md.js +2 -1
- package/dist/ui/task-picker.d.ts +1 -0
- package/dist/ui/task-picker.js +53 -38
- package/package.json +1 -1
package/dist/commands/start.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
import { createDamperApi } from '../services/damper-api.js';
|
|
4
|
-
import { createWorktree, getMainProjectRoot } from '../services/worktree.js';
|
|
4
|
+
import { createWorktree, getMainProjectRoot, removeWorktreeDir } from '../services/worktree.js';
|
|
5
5
|
import { bootstrapContext, refreshContext } from '../services/context-bootstrap.js';
|
|
6
6
|
import { pickTask } from '../ui/task-picker.js';
|
|
7
|
-
import { launchClaude, postTaskFlow, isClaudeInstalled, isDamperMcpConfigured, configureDamperMcp } from '../services/claude.js';
|
|
7
|
+
import { launchClaude, launchClaudeForReview, postTaskFlow, isClaudeInstalled, isDamperMcpConfigured, configureDamperMcp } from '../services/claude.js';
|
|
8
8
|
import { getWorktreesForProject, cleanupStaleWorktrees } from '../services/state.js';
|
|
9
9
|
import { getApiKey, isProjectConfigured, getProjectConfigPath } from '../services/config.js';
|
|
10
10
|
import { shortIdRaw } from '../ui/format.js';
|
|
@@ -77,23 +77,39 @@ export async function startCommand(options) {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
else {
|
|
80
|
-
// Interactive task picker
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
80
|
+
// Interactive task picker with review loop
|
|
81
|
+
while (true) {
|
|
82
|
+
// Re-fetch worktrees each iteration (handles cleanup between reviews)
|
|
83
|
+
const currentWorktrees = getWorktreesForProject(projectRoot);
|
|
84
|
+
const result = await pickTask({
|
|
85
|
+
api,
|
|
86
|
+
worktrees: currentWorktrees,
|
|
87
|
+
typeFilter: options.type,
|
|
88
|
+
statusFilter: options.status,
|
|
89
|
+
});
|
|
90
|
+
if (!result) {
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
if (result.action === 'review') {
|
|
94
|
+
await handleReviewAndComplete({
|
|
95
|
+
api,
|
|
96
|
+
apiKey,
|
|
97
|
+
task: result.task,
|
|
98
|
+
worktree: result.worktree,
|
|
99
|
+
projectRoot,
|
|
100
|
+
});
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
// action is 'start' or 'resume' — break into normal flow
|
|
104
|
+
taskId = result.task.id;
|
|
105
|
+
taskTitle = result.task.title;
|
|
106
|
+
isResume = result.isResume;
|
|
107
|
+
isNewTask = result.isNewTask || false;
|
|
108
|
+
forceTakeover = result.forceTakeover || options.force || false;
|
|
109
|
+
if (result.worktree) {
|
|
110
|
+
worktreePath = result.worktree.path;
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
97
113
|
}
|
|
98
114
|
}
|
|
99
115
|
if (isResume && worktreePath) {
|
|
@@ -194,3 +210,103 @@ export async function startCommand(options) {
|
|
|
194
210
|
isNewTask,
|
|
195
211
|
});
|
|
196
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Review a task and attempt to complete it via Claude, then return to the picker.
|
|
215
|
+
*/
|
|
216
|
+
async function handleReviewAndComplete(options) {
|
|
217
|
+
const { api, apiKey, task, worktree, projectRoot } = options;
|
|
218
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
219
|
+
const { execa } = await import('execa');
|
|
220
|
+
const taskId = task.id;
|
|
221
|
+
const cwd = worktree?.path || projectRoot;
|
|
222
|
+
const hasWorktree = !!worktree;
|
|
223
|
+
console.log(pc.cyan(`\nReviewing task #${shortIdRaw(taskId)}: ${task.title}`));
|
|
224
|
+
console.log(pc.dim(`Directory: ${cwd}`));
|
|
225
|
+
// Lock the task
|
|
226
|
+
console.log(pc.dim('\nLocking task in Damper...'));
|
|
227
|
+
try {
|
|
228
|
+
await api.startTask(taskId, true);
|
|
229
|
+
console.log(pc.green('✓ Task locked'));
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
const error = err;
|
|
233
|
+
console.log(pc.red(`Failed to lock task: ${error.message}`));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
// Launch Claude for review
|
|
237
|
+
console.log(pc.dim('\nLaunching Claude for review...\n'));
|
|
238
|
+
await launchClaudeForReview({ cwd, apiKey, taskId });
|
|
239
|
+
console.log(pc.dim('\n─────────────────────────────────────────\n'));
|
|
240
|
+
// Check task status after Claude exits
|
|
241
|
+
let taskStatus;
|
|
242
|
+
try {
|
|
243
|
+
const updatedTask = await api.getTask(taskId);
|
|
244
|
+
taskStatus = updatedTask.status;
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
console.log(pc.yellow('Could not fetch task status from Damper.'));
|
|
248
|
+
}
|
|
249
|
+
if (taskStatus === 'done') {
|
|
250
|
+
console.log(pc.green(`✓ Task #${shortIdRaw(taskId)} completed!`));
|
|
251
|
+
if (hasWorktree) {
|
|
252
|
+
// Check for unpushed commits and offer to push + cleanup
|
|
253
|
+
let hasUnpushed = false;
|
|
254
|
+
try {
|
|
255
|
+
const { stdout } = await execa('git', ['log', '@{u}..HEAD', '--oneline'], { cwd, stdio: 'pipe' });
|
|
256
|
+
hasUnpushed = stdout.trim().length > 0;
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
try {
|
|
260
|
+
const { stdout } = await execa('git', ['log', 'origin/main..HEAD', '--oneline'], { cwd, stdio: 'pipe' });
|
|
261
|
+
hasUnpushed = stdout.trim().length > 0;
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// Assume there might be commits
|
|
265
|
+
hasUnpushed = true;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (hasUnpushed) {
|
|
269
|
+
const shouldPush = await confirm({
|
|
270
|
+
message: 'Push unpushed commits?',
|
|
271
|
+
default: true,
|
|
272
|
+
});
|
|
273
|
+
if (shouldPush) {
|
|
274
|
+
try {
|
|
275
|
+
const { stdout: branch } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd, stdio: 'pipe' });
|
|
276
|
+
await execa('git', ['push', '-u', 'origin', branch.trim()], { cwd, stdio: 'inherit' });
|
|
277
|
+
console.log(pc.green('✓ Pushed to remote'));
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
console.log(pc.red('Failed to push. You can push manually.'));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const shouldCleanup = await confirm({
|
|
285
|
+
message: 'Remove worktree and branch?',
|
|
286
|
+
default: true,
|
|
287
|
+
});
|
|
288
|
+
if (shouldCleanup) {
|
|
289
|
+
try {
|
|
290
|
+
await removeWorktreeDir(cwd, projectRoot);
|
|
291
|
+
console.log(pc.green('✓ Worktree and branch removed'));
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
const error = err;
|
|
295
|
+
console.log(pc.red(`Failed to remove worktree: ${error.message}`));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
// Not completed — release the lock
|
|
302
|
+
console.log(pc.yellow(`Task #${shortIdRaw(taskId)} not completed (status: ${taskStatus || 'unknown'})`));
|
|
303
|
+
try {
|
|
304
|
+
await api.abandonTask(taskId, 'Review session ended without completion');
|
|
305
|
+
console.log(pc.dim('Task lock released.'));
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
// Ignore — task may already be unlocked
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
console.log();
|
|
312
|
+
}
|
|
@@ -47,6 +47,14 @@ export declare function postTaskFlow(options: {
|
|
|
47
47
|
projectRoot: string;
|
|
48
48
|
isNewTask?: boolean;
|
|
49
49
|
}): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Launch Claude to review and complete a task
|
|
52
|
+
*/
|
|
53
|
+
export declare function launchClaudeForReview(options: {
|
|
54
|
+
cwd: string;
|
|
55
|
+
apiKey: string;
|
|
56
|
+
taskId: string;
|
|
57
|
+
}): Promise<void>;
|
|
50
58
|
/**
|
|
51
59
|
* Check if Claude Code CLI is installed
|
|
52
60
|
*/
|
package/dist/services/claude.js
CHANGED
|
@@ -441,6 +441,34 @@ export async function postTaskFlow(options) {
|
|
|
441
441
|
}
|
|
442
442
|
console.log();
|
|
443
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* Launch Claude to review and complete a task
|
|
446
|
+
*/
|
|
447
|
+
export async function launchClaudeForReview(options) {
|
|
448
|
+
const { cwd, apiKey, taskId } = options;
|
|
449
|
+
const prompt = [
|
|
450
|
+
`Review task #${taskId} and determine if it is ready to be marked as complete.`,
|
|
451
|
+
'',
|
|
452
|
+
'Steps:',
|
|
453
|
+
'1. Read TASK_CONTEXT.md (if it exists) to understand the requirements',
|
|
454
|
+
'2. Check git log and git diff main...HEAD to review what was done',
|
|
455
|
+
'3. Run the project tests (use `bun run test` from the monorepo root, NEVER `bun test`)',
|
|
456
|
+
'4. Check the completion checklist via get_project_settings MCP tool',
|
|
457
|
+
'5. If everything looks good, call complete_task via MCP with a summary and confirmations',
|
|
458
|
+
'6. If NOT ready, explain what is missing or broken — do NOT call complete_task',
|
|
459
|
+
'',
|
|
460
|
+
'IMPORTANT: Do NOT make any code changes. This is a review-only session.',
|
|
461
|
+
].join('\n');
|
|
462
|
+
await new Promise((resolve) => {
|
|
463
|
+
const child = spawn('claude', [prompt], {
|
|
464
|
+
cwd,
|
|
465
|
+
stdio: 'inherit',
|
|
466
|
+
env: { ...process.env, DAMPER_API_KEY: apiKey },
|
|
467
|
+
});
|
|
468
|
+
child.on('error', () => resolve());
|
|
469
|
+
child.on('close', () => resolve());
|
|
470
|
+
});
|
|
471
|
+
}
|
|
444
472
|
/**
|
|
445
473
|
* Launch Claude to resolve merge conflicts
|
|
446
474
|
*/
|
|
@@ -24,10 +24,11 @@ ${planSection}
|
|
|
24
24
|
- \`.claude/settings.local.json\`
|
|
25
25
|
|
|
26
26
|
**Your responsibilities (via Damper MCP):**
|
|
27
|
-
1.
|
|
28
|
-
2. Use \`
|
|
29
|
-
3.
|
|
30
|
-
4.
|
|
27
|
+
1. **Do NOT commit or complete tasks without explicit user confirmation** - Always ask the user before running \`git commit\` or calling \`complete_task\`
|
|
28
|
+
2. Use \`add_commit\` after each git commit
|
|
29
|
+
3. Use \`add_note\` ONLY for non-obvious approach decisions (e.g. "Decision: chose X because Y")
|
|
30
|
+
4. When user confirms: call \`complete_task\` with a one-line summary
|
|
31
|
+
5. If stopping early: call \`abandon_task\` with what remains and blockers
|
|
31
32
|
|
|
32
33
|
The CLI just bootstrapped this environment - YOU handle the task lifecycle.
|
|
33
34
|
`.trim();
|
|
@@ -44,7 +44,8 @@ export function generateTaskContext(options) {
|
|
|
44
44
|
lines.push('- Generated this TASK_CONTEXT.md from Damper API');
|
|
45
45
|
lines.push('- Configured Damper MCP tools for task lifecycle');
|
|
46
46
|
lines.push('');
|
|
47
|
-
lines.push('**Cleanup:**
|
|
47
|
+
lines.push('**Cleanup:** Ask the user for confirmation before committing or completing the task.');
|
|
48
|
+
lines.push('Once confirmed, call `complete_task` or `abandon_task`. The user will run');
|
|
48
49
|
lines.push('`npx @damper/cli cleanup` to remove the worktree and branch - you don\'t need to');
|
|
49
50
|
lines.push('provide manual cleanup instructions.');
|
|
50
51
|
lines.push('');
|
package/dist/ui/task-picker.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ interface TaskPickerResult {
|
|
|
12
12
|
isResume: boolean;
|
|
13
13
|
forceTakeover?: boolean;
|
|
14
14
|
isNewTask?: boolean;
|
|
15
|
+
action: 'start' | 'resume' | 'review';
|
|
15
16
|
}
|
|
16
17
|
export declare function pickTask(options: TaskPickerOptions): Promise<TaskPickerResult | null>;
|
|
17
18
|
export {};
|
package/dist/ui/task-picker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { select, confirm, input, Separator } from '@inquirer/prompts';
|
|
1
|
+
import { search, select, confirm, input, Separator } from '@inquirer/prompts';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
import { shortId, shortIdRaw, getTypeIcon, getPriorityIcon, formatEffort, formatProgressCompact, formatDueDate, sectionHeader, relativeTime, getTerminalWidth, padEnd, padStart, } from './format.js';
|
|
4
4
|
// Layout constants (terminal column widths)
|
|
@@ -168,40 +168,6 @@ export async function pickTask(options) {
|
|
|
168
168
|
];
|
|
169
169
|
const layout = getMetaLayout(allDisplayTasks, getTerminalWidth());
|
|
170
170
|
const titleWidth = getTitleWidth(layout);
|
|
171
|
-
const choices = [];
|
|
172
|
-
if (inProgressChoices.length > 0) {
|
|
173
|
-
choices.push(new Separator(`\n${sectionHeader(`In Progress (${inProgressChoices.length})`)}`));
|
|
174
|
-
for (const choice of inProgressChoices) {
|
|
175
|
-
choices.push({
|
|
176
|
-
name: formatTaskChoice(choice, titleWidth, layout),
|
|
177
|
-
value: choice,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
if (availableChoices.length > 0) {
|
|
182
|
-
choices.push(new Separator(`\n${sectionHeader(`Available (${availableChoices.length})`)}`));
|
|
183
|
-
for (const choice of availableChoices) {
|
|
184
|
-
choices.push({
|
|
185
|
-
name: formatTaskChoice(choice, titleWidth, layout),
|
|
186
|
-
value: choice,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
if (lockedChoices.length > 0) {
|
|
191
|
-
choices.push(new Separator(`\n${sectionHeader(`Locked (${lockedChoices.length})`)}`));
|
|
192
|
-
for (const choice of lockedChoices) {
|
|
193
|
-
choices.push({
|
|
194
|
-
name: formatTaskChoice(choice, titleWidth, layout),
|
|
195
|
-
value: choice,
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
// Always show "Create new task" option
|
|
200
|
-
choices.push(new Separator(''));
|
|
201
|
-
choices.push({
|
|
202
|
-
name: pc.green('+ Create new task'),
|
|
203
|
-
value: { type: 'create_new' },
|
|
204
|
-
});
|
|
205
171
|
if (inProgressChoices.length === 0 && availableChoices.length === 0 && lockedChoices.length === 0) {
|
|
206
172
|
console.log(pc.yellow('\nNo existing tasks found.'));
|
|
207
173
|
if (typeFilter) {
|
|
@@ -209,19 +175,58 @@ export async function pickTask(options) {
|
|
|
209
175
|
}
|
|
210
176
|
}
|
|
211
177
|
console.log(pc.bold(`\nProject: ${project}`));
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
178
|
+
// Build choices with search filtering support
|
|
179
|
+
const buildChoices = (term) => {
|
|
180
|
+
const lowerTerm = term?.toLowerCase().trim() || '';
|
|
181
|
+
const matches = (task) => !lowerTerm || task.title.toLowerCase().includes(lowerTerm) || task.id.includes(lowerTerm);
|
|
182
|
+
const filtered = [];
|
|
183
|
+
const filteredInProgress = inProgressChoices.filter(c => matches(c.task));
|
|
184
|
+
if (filteredInProgress.length > 0) {
|
|
185
|
+
filtered.push(new Separator(`\n${sectionHeader(`In Progress (${filteredInProgress.length})`)}`));
|
|
186
|
+
for (const choice of filteredInProgress) {
|
|
187
|
+
filtered.push({ name: formatTaskChoice(choice, titleWidth, layout), value: choice });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const filteredAvailable = availableChoices.filter(c => matches(c.task));
|
|
191
|
+
if (filteredAvailable.length > 0) {
|
|
192
|
+
filtered.push(new Separator(`\n${sectionHeader(`Available (${filteredAvailable.length})`)}`));
|
|
193
|
+
for (const choice of filteredAvailable) {
|
|
194
|
+
filtered.push({ name: formatTaskChoice(choice, titleWidth, layout), value: choice });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const filteredLocked = lockedChoices.filter(c => matches(c.task));
|
|
198
|
+
if (filteredLocked.length > 0) {
|
|
199
|
+
filtered.push(new Separator(`\n${sectionHeader(`Locked (${filteredLocked.length})`)}`));
|
|
200
|
+
for (const choice of filteredLocked) {
|
|
201
|
+
filtered.push({ name: formatTaskChoice(choice, titleWidth, layout), value: choice });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Always show "Create new task"
|
|
205
|
+
filtered.push(new Separator(''));
|
|
206
|
+
filtered.push({ name: pc.green('+ Create new task'), value: { type: 'create_new' } });
|
|
207
|
+
return filtered;
|
|
208
|
+
};
|
|
209
|
+
const selected = await search({
|
|
210
|
+
message: 'Select a task (type to filter):',
|
|
211
|
+
source: (term) => buildChoices(term),
|
|
215
212
|
pageSize: 20,
|
|
216
213
|
});
|
|
217
214
|
if (selected.type === 'create_new') {
|
|
218
215
|
return handleCreateNewTask(api);
|
|
219
216
|
}
|
|
220
217
|
if (selected.type === 'in_progress') {
|
|
218
|
+
const action = await select({
|
|
219
|
+
message: `#${shortIdRaw(selected.task.id)} ${selected.task.title}`,
|
|
220
|
+
choices: [
|
|
221
|
+
{ name: 'Resume working', value: 'resume' },
|
|
222
|
+
{ name: 'Review & complete', value: 'review' },
|
|
223
|
+
],
|
|
224
|
+
});
|
|
221
225
|
return {
|
|
222
226
|
task: selected.task,
|
|
223
227
|
worktree: selected.worktree,
|
|
224
228
|
isResume: true,
|
|
229
|
+
action,
|
|
225
230
|
};
|
|
226
231
|
}
|
|
227
232
|
if (selected.type === 'locked') {
|
|
@@ -240,11 +245,20 @@ export async function pickTask(options) {
|
|
|
240
245
|
task: selected.task,
|
|
241
246
|
isResume: false,
|
|
242
247
|
forceTakeover: true,
|
|
248
|
+
action: 'start',
|
|
243
249
|
};
|
|
244
250
|
}
|
|
251
|
+
const action = await select({
|
|
252
|
+
message: `#${shortIdRaw(selected.task.id)} ${selected.task.title}`,
|
|
253
|
+
choices: [
|
|
254
|
+
{ name: 'Start working', value: 'start' },
|
|
255
|
+
{ name: 'Quick complete', value: 'review' },
|
|
256
|
+
],
|
|
257
|
+
});
|
|
245
258
|
return {
|
|
246
259
|
task: selected.task,
|
|
247
260
|
isResume: false,
|
|
261
|
+
action,
|
|
248
262
|
};
|
|
249
263
|
}
|
|
250
264
|
async function handleCreateNewTask(api) {
|
|
@@ -275,5 +289,6 @@ async function handleCreateNewTask(api) {
|
|
|
275
289
|
task,
|
|
276
290
|
isResume: false,
|
|
277
291
|
isNewTask: true,
|
|
292
|
+
action: 'start',
|
|
278
293
|
};
|
|
279
294
|
}
|