44
55import type { Task } from "./types.ts"
66
7- /** Task checkbox patterns */
8- const UNCOMPLETED_TASK_PATTERN = / ^ - \[ \] ( .+ ) $ /
9- const COMPLETED_TASK_PATTERN = / ^ - \[ [ x X ] \] ( .+ ) $ /
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 : / ^ - \[ [ x X ] \] ( .+ ) $ / ,
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 * (?: S t e p | T a s k ) \s * \d * [: .] \s * ( .+ ) $ / i,
17+ /** Plain bullet: - task or * task */
18+ bullet : / ^ [ - * ] \s + ( .+ ) $ / ,
19+ /** Done marker for non-checkbox formats */
20+ doneMarker : / ^ \[ D O N E \] \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 */
1428export 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 = / ^ - \[ [ x X ] \] / . 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 */
57148export 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 * (?: S t e p | T a s k ) \s * \d * [: .] \s * / i. test ( line . trim ( ) ) &&
177+ ! line . includes ( "[DONE]" )
178+ ) {
179+ lines [ index ] = line . replace ( / ^ ( \s * # { 1 , 4 } \s * (?: S t e p | T a s k ) \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 */
75195export 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