Skip to content

Commit 80bfa62

Browse files
committed
feat: add idea selection and planning prompts
- Add generateIdeaSelectionPrompt for multi-idea selection - Add generateIdeaPlanPrompt for idea-based planning - Expand plan tests for new prompt generation Signed-off-by: leocavalcante <[email protected]>
1 parent 88e4db6 commit 80bfa62

File tree

2 files changed

+266
-25
lines changed

2 files changed

+266
-25
lines changed

src/plan.ts

Lines changed: 141 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,130 @@
44

55
import type { Task } from "./types.ts"
66

7-
/** Task checkbox patterns */
8-
const UNCOMPLETED_TASK_PATTERN = /^- \[ \] (.+)$/
9-
const COMPLETED_TASK_PATTERN = /^- \[[xX]\] (.+)$/
7+
/** Task patterns - supports multiple formats */
8+
const TASK_PATTERNS = {
9+
/** Checkbox uncompleted: - [ ] task */
10+
checkboxUncompleted: /^- \[ \] (.+)$/,
11+
/** Checkbox completed: - [x] task or - [X] task */
12+
checkboxCompleted: /^- \[[xX]\] (.+)$/,
13+
/** Numbered list: 1. task, 2. task, etc. */
14+
numbered: /^\d+\.\s+(.+)$/,
15+
/** Step/Task header: ### Step 1: task, ## Task 2: task, etc. */
16+
stepHeader: /^#{1,4}\s*(?:Step|Task)\s*\d*[:.]\s*(.+)$/i,
17+
/** Plain bullet: - task or * task */
18+
bullet: /^[-*]\s+(.+)$/,
19+
/** Done marker for non-checkbox formats */
20+
doneMarker: /^\[DONE\]\s*/,
21+
}
1022

1123
/**
12-
* Parse tasks from a plan content
24+
* Parse tasks from a plan content.
25+
* Supports multiple formats: checkboxes, numbered lists, bullets, step headers.
26+
* Falls back to treating the entire plan as a single task if no structured tasks found.
1327
*/
1428
export function getTasks(planContent: string): Task[] {
1529
const lines = planContent.split("\n")
1630
const tasks: Task[] = []
1731

32+
// Check if entire plan is marked as completed (fallback task was completed)
33+
const isPlanCompleted = planContent.trimStart().startsWith("[COMPLETED]")
34+
1835
for (let i = 0; i < lines.length; i++) {
1936
const line = lines[i]?.trim()
2037
if (!line) continue
2138

22-
// Check for uncompleted task
23-
const uncompletedMatch = line.match(UNCOMPLETED_TASK_PATTERN)
24-
if (uncompletedMatch?.[1]) {
39+
// Skip the [COMPLETED] marker line
40+
if (line === "[COMPLETED]") continue
41+
42+
// Check for completed checkbox first: - [x] task
43+
const checkboxDoneMatch = line.match(TASK_PATTERNS.checkboxCompleted)
44+
if (checkboxDoneMatch?.[1]) {
2545
tasks.push({
26-
lineNumber: i + 1, // 1-indexed
27-
description: uncompletedMatch[1].trim(),
46+
lineNumber: i + 1,
47+
description: checkboxDoneMatch[1].trim(),
48+
completed: true,
49+
})
50+
continue
51+
}
52+
53+
// Check for uncompleted checkbox: - [ ] task
54+
const checkboxMatch = line.match(TASK_PATTERNS.checkboxUncompleted)
55+
if (checkboxMatch?.[1]) {
56+
tasks.push({
57+
lineNumber: i + 1,
58+
description: checkboxMatch[1].trim(),
2859
completed: false,
2960
})
3061
continue
3162
}
3263

33-
// Check for completed task
34-
const completedMatch = line.match(COMPLETED_TASK_PATTERN)
35-
if (completedMatch?.[1]) {
64+
// Check for numbered list: 1. task
65+
const numberedMatch = line.match(TASK_PATTERNS.numbered)
66+
if (numberedMatch?.[1]) {
67+
const desc = numberedMatch[1].trim()
68+
const isDone = TASK_PATTERNS.doneMarker.test(desc)
3669
tasks.push({
3770
lineNumber: i + 1,
38-
description: completedMatch[1].trim(),
39-
completed: true,
71+
description: isDone ? desc.replace(TASK_PATTERNS.doneMarker, "") : desc,
72+
completed: isDone,
73+
})
74+
continue
75+
}
76+
77+
// Check for step header: ### Step 1: task
78+
const stepMatch = line.match(TASK_PATTERNS.stepHeader)
79+
if (stepMatch?.[1]) {
80+
const desc = stepMatch[1].trim()
81+
const isDone = TASK_PATTERNS.doneMarker.test(desc)
82+
tasks.push({
83+
lineNumber: i + 1,
84+
description: isDone ? desc.replace(TASK_PATTERNS.doneMarker, "") : desc,
85+
completed: isDone,
4086
})
87+
continue
4188
}
89+
90+
// Check for plain bullet: - task or * task (but not checkboxes or markdown headers)
91+
// Skip lines starting with # (markdown headers like "## Tasks")
92+
// Skip checkbox patterns (- [ ] or - [x])
93+
const isCheckbox = /^- \[[ xX]\]/.test(line)
94+
if (!isCheckbox && !line.startsWith("#")) {
95+
const bulletMatch = line.match(TASK_PATTERNS.bullet)
96+
if (bulletMatch?.[1]) {
97+
const desc = bulletMatch[1].trim()
98+
const isDone = TASK_PATTERNS.doneMarker.test(desc)
99+
tasks.push({
100+
lineNumber: i + 1,
101+
description: isDone ? desc.replace(TASK_PATTERNS.doneMarker, "") : desc,
102+
completed: isDone,
103+
})
104+
}
105+
}
106+
}
107+
108+
// Fallback: if no structured tasks found, treat entire plan as a single task
109+
if (tasks.length === 0 && planContent.trim() && !isPlanCompleted) {
110+
// Extract a summary from the first meaningful line (skip empty lines and markdown headers)
111+
const firstMeaningfulLine = lines.find((l) => {
112+
const trimmed = l.trim()
113+
return trimmed && !trimmed.startsWith("#") && trimmed.length > 10
114+
})
115+
const summary = firstMeaningfulLine?.trim().slice(0, 100) || "Execute plan"
116+
117+
tasks.push({
118+
lineNumber: 1,
119+
description: `[FULL PLAN] ${summary}${summary.length >= 100 ? "..." : ""}`,
120+
completed: false,
121+
})
122+
}
123+
124+
// If plan was marked [COMPLETED] but no structured tasks, return completed fallback task
125+
if (tasks.length === 0 && isPlanCompleted) {
126+
tasks.push({
127+
lineNumber: 1,
128+
description: "[FULL PLAN] Completed",
129+
completed: true,
130+
})
42131
}
43132

44133
return tasks
@@ -52,25 +141,56 @@ export function getUncompletedTasks(planContent: string): Task[] {
52141
}
53142

54143
/**
55-
* Mark a task as complete in the plan content
144+
* Mark a task as complete in the plan content.
145+
* Handles checkboxes (converts to [x]) and other formats (prepends [DONE]).
146+
* For fallback tasks (line 1 with no structure), prepends [COMPLETED] to the plan.
56147
*/
57148
export function markTaskComplete(planContent: string, lineNumber: number): string {
58149
const lines = planContent.split("\n")
59150
const index = lineNumber - 1 // Convert to 0-indexed
60151

152+
// Special handling for fallback task (entire plan as single task at line 1)
153+
// Check if this is the fallback case by seeing if getTasks would return a [FULL PLAN] task
154+
const tasks = getTasks(planContent)
155+
const isFallbackTask =
156+
tasks.length === 1 && tasks[0]?.description.startsWith("[FULL PLAN]") && lineNumber === 1
157+
158+
if (isFallbackTask) {
159+
// Mark the entire plan as completed by prepending a marker
160+
return `[COMPLETED]\n${planContent}`
161+
}
162+
61163
if (index >= 0 && index < lines.length) {
62164
const line = lines[index]
63165
if (line) {
64-
// Replace "- [ ]" with "- [x]"
65-
lines[index] = line.replace(/^(\s*)- \[ \]/, "$1- [x]")
166+
// If it's a checkbox, check it
167+
if (/^(\s*)- \[ \]/.test(line)) {
168+
lines[index] = line.replace(/^(\s*)- \[ \]/, "$1- [x]")
169+
}
170+
// For numbered lists: 1. task -> 1. [DONE] task
171+
else if (/^\d+\.\s+/.test(line.trim()) && !line.includes("[DONE]")) {
172+
lines[index] = line.replace(/^(\s*)(\d+\.\s+)/, "$1$2[DONE] ")
173+
}
174+
// For step headers: ### Step 1: task -> ### Step 1: [DONE] task
175+
else if (
176+
/^#{1,4}\s*(?:Step|Task)\s*\d*[:.]\s*/i.test(line.trim()) &&
177+
!line.includes("[DONE]")
178+
) {
179+
lines[index] = line.replace(/^(\s*#{1,4}\s*(?:Step|Task)\s*\d*[:.]\s*)/i, "$1[DONE] ")
180+
}
181+
// For plain bullets: - task -> - [DONE] task
182+
else if (/^(\s*)[-*]\s+/.test(line) && !line.includes("[DONE]") && !line.includes("- [")) {
183+
lines[index] = line.replace(/^(\s*[-*]\s+)/, "$1[DONE] ")
184+
}
66185
}
67186
}
68187

69188
return lines.join("\n")
70189
}
71190

72191
/**
73-
* Validate that a plan has actionable tasks
192+
* Validate that a plan has actionable content.
193+
* Always accepts non-empty plans (fallback to single task if no structured tasks).
74194
*/
75195
export function validatePlan(planContent: string): { valid: boolean; error?: string } {
76196
if (!planContent.trim()) {
@@ -79,8 +199,10 @@ export function validatePlan(planContent: string): { valid: boolean; error?: str
79199

80200
const tasks = getTasks(planContent)
81201

202+
// getTasks now always returns at least one task for non-empty content (fallback)
203+
// So this check is just a safety net
82204
if (tasks.length === 0) {
83-
return { valid: false, error: "Plan has no actionable tasks" }
205+
return { valid: false, error: "Plan is empty" }
84206
}
85207

86208
const uncompletedTasks = tasks.filter((t) => !t.completed)

0 commit comments

Comments
 (0)