11---
22name : plan
33description : Batch plan issue resolution using issue-plan-agent (explore + plan closed-loop)
4- argument-hint : " <issue-id>[,<issue-id>,...] [--batch-size 3]"
4+ argument-hint : " <issue-id>[,<issue-id>,...] [--batch-size 3] --all-pending "
55allowed-tools : TodoWrite(*), Task(*), SlashCommand(*), AskUserQuestion(*), Bash(*), Read(*), Write(*)
66---
77
@@ -78,24 +78,49 @@ Phase 4: Summary
7878### Phase 1: Issue Loading
7979
8080``` javascript
81- // Parse input
82- const issueIds = userInput .includes (' ,' )
83- ? userInput .split (' ,' ).map (s => s .trim ())
84- : [userInput .trim ()];
85-
86- // Read issues.jsonl
81+ // Parse input and flags
8782const issuesPath = ' .workflow/issues/issues.jsonl' ;
88- const allIssues = Bash (` cat "${ issuesPath} " 2>/dev/null || echo ''` )
89- .split (' \n ' )
90- .filter (line => line .trim ())
91- .map (line => JSON .parse (line));
83+ const batchSize = flags .batchSize || 3 ;
84+
85+ // Key fields for planning (avoid loading full issue data)
86+ const PLAN_FIELDS = ' id,title,status,context,affected_components,lifecycle_requirements,priority,bound_solution_id' ;
87+
88+ let issueIds = [];
89+
90+ if (flags .allPending ) {
91+ // Use jq to filter pending/registered issues - extract only IDs
92+ const pendingIds = Bash (`
93+ cat "${ issuesPath} " 2>/dev/null | \\
94+ jq -r 'select(.status == "pending" or .status == "registered") | .id' 2>/dev/null || echo ''
95+ ` ).trim ();
96+
97+ issueIds = pendingIds ? pendingIds .split (' \n ' ).filter (Boolean ) : [];
98+
99+ if (issueIds .length === 0 ) {
100+ console .log (' No pending issues found.' );
101+ return ;
102+ }
103+ console .log (` Found ${ issueIds .length } pending issues` );
104+ } else {
105+ // Parse comma-separated issue IDs
106+ issueIds = userInput .includes (' ,' )
107+ ? userInput .split (' ,' ).map (s => s .trim ())
108+ : [userInput .trim ()];
109+ }
92110
93- // Load and validate issues
111+ // Load issues using jq to extract only key fields
94112const issues = [];
95113for (const id of issueIds) {
96- let issue = allIssues .find (i => i .id === id);
97-
98- if (! issue) {
114+ // Use jq to find issue by ID and extract only needed fields
115+ const issueJson = Bash (`
116+ cat "${ issuesPath} " 2>/dev/null | \\
117+ jq -c 'select(.id == "${ id} ") | {${ PLAN_FIELDS } }' 2>/dev/null | head -1
118+ ` ).trim ();
119+
120+ let issue;
121+ if (issueJson) {
122+ issue = JSON .parse (issueJson);
123+ } else {
99124 console .log (` Issue ${ id} not found. Creating...` );
100125 issue = {
101126 id,
@@ -114,12 +139,13 @@ for (const id of issueIds) {
114139}
115140
116141// Group into batches
117- const batchSize = flags .batchSize || 3 ;
118142const batches = [];
119143for (let i = 0 ; i < issues .length ; i += batchSize) {
120144 batches .push (issues .slice (i, i + batchSize));
121145}
122146
147+ console .log (` Processing ${ issues .length } issues in ${ batches .length } batch(es)` );
148+
123149TodoWrite ({
124150 todos: batches .flatMap ((batch , i ) => [
125151 { content: ` Plan batch ${ i+ 1 } ` , status: ' pending' , activeForm: ` Planning batch ${ i+ 1 } ` }
@@ -130,10 +156,13 @@ TodoWrite({
130156### Phase 2: Unified Explore + Plan (issue-plan-agent)
131157
132158``` javascript
159+ // Ensure solutions directory exists
160+ Bash (` mkdir -p .workflow/issues/solutions` );
161+
133162for (const [batchIndex , batch ] of batches .entries ()) {
134163 updateTodo (` Plan batch ${ batchIndex + 1 } ` , ' in_progress' );
135164
136- // Build issue prompt for agent with lifecycle requirements
165+ // Build issue prompt for agent - agent writes solutions directly
137166 const issuePrompt = `
138167## Issues to Plan (Closed-Loop Tasks Required)
139168
@@ -152,7 +181,29 @@ ${batch.map((issue, i) => `
152181## Project Root
153182${ process .cwd ()}
154183
155- ## Requirements - CLOSED-LOOP TASKS
184+ ## Output Requirements
185+
186+ **IMPORTANT**: Write solutions DIRECTLY to files, do NOT return full solution content.
187+
188+ ### 1. Write Solution Files
189+ For each issue, write solution to: \` .workflow/issues/solutions/{issue-id}.jsonl\`
190+ - Append one JSON line per solution
191+ - Solution must include all closed-loop task fields (see Solution Format below)
192+
193+ ### 2. Return Summary Only
194+ After writing solutions, return ONLY a brief JSON summary:
195+ \`\`\` json
196+ {
197+ "planned": [
198+ { "issue_id": "XXX", "solution_id": "SOL-xxx", "task_count": 3, "description": "Brief description" }
199+ ],
200+ "conflicts": [
201+ { "file": "path/to/file", "issues": ["ID1", "ID2"], "suggested_order": ["ID1", "ID2"] }
202+ ]
203+ }
204+ \`\`\`
205+
206+ ## Closed-Loop Task Requirements
156207
157208Each task MUST include ALL lifecycle phases:
158209
@@ -168,12 +219,10 @@ Each task MUST include ALL lifecycle phases:
168219
169220### 3. Regression
170221- regression: string[] (commands to run for regression check)
171- - Based on issue's regression_scope setting
172222
173223### 4. Acceptance
174224- acceptance.criteria: string[] (testable acceptance criteria)
175225- acceptance.verification: string[] (how to verify each criterion)
176- - acceptance.manual_checks: string[] (manual checks if needed)
177226
178227### 5. Commit
179228- commit.type: feat|fix|refactor|test|docs|chore
@@ -188,32 +237,26 @@ Each task MUST include ALL lifecycle phases:
1882374. Infer commit scope from affected files
189238` ;
190239
191- // Launch issue-plan-agent (combines explore + plan)
240+ // Launch issue-plan-agent - agent writes solutions directly
192241 const result = Task (
193242 subagent_type= " issue-plan-agent" ,
194243 run_in_background= false ,
195244 description= ` Explore & plan ${ batch .length } issues` ,
196245 prompt= issuePrompt
197246 );
198247
199- // Parse agent output
200- const agentOutput = JSON .parse (result);
201-
202- // Register solutions for each issue (append to solutions/{issue-id}.jsonl)
203- for (const item of agentOutput .solutions ) {
204- const solutionPath = ` .workflow/issues/solutions/${ item .issue_id } .jsonl` ;
248+ // Parse brief summary from agent
249+ const summary = JSON .parse (result);
205250
206- // Ensure solutions directory exists
207- Bash (` mkdir -p .workflow/issues/solutions` );
208-
209- // Append solution as new line
210- Bash (` echo '${ JSON .stringify (item .solution )} ' >> "${ solutionPath} "` );
251+ // Display planning results
252+ for (const item of summary .planned || []) {
253+ console .log (` ✓ ${ item .issue_id } : ${ item .solution_id } (${ item .task_count } tasks) - ${ item .description } ` );
211254 }
212255
213256 // Handle conflicts if any
214- if (agentOutput .conflicts ? .length > 0 ) {
257+ if (summary .conflicts ? .length > 0 ) {
215258 console .log (` \n ⚠ File conflicts detected:` );
216- agentOutput .conflicts .forEach (c => {
259+ summary .conflicts .forEach (c => {
217260 console .log (` ${ c .file } : ${ c .issues .join (' , ' )} → suggested: ${ c .suggested_order .join (' → ' )} ` );
218261 });
219262 }
@@ -225,90 +268,86 @@ Each task MUST include ALL lifecycle phases:
225268### Phase 3: Solution Binding
226269
227270` ` ` javascript
228- // Re-read issues.jsonl
229- let allIssuesUpdated = Bash (` cat "${ issuesPath} "` )
230- .split (' \n ' )
231- .filter (line => line .trim ())
232- .map (line => JSON .parse (line));
271+ // Collect issues needing user selection (multiple solutions)
272+ const needSelection = [];
233273
234274for (const issue of issues) {
235275 const solPath = ` .workflow/issues/solutions/${ issue .id } .jsonl` ;
236- const solutions = Bash (` cat "${ solPath} " 2>/dev/null || echo ''` )
237- .split (' \n ' )
238- .filter (line => line .trim ())
239- .map (line => JSON .parse (line));
240-
241- if (solutions .length === 0 ) {
242- console .log (` ⚠ No solutions for ${ issue .id } ` );
243- continue ;
244- }
245276
246- let selectedSolId;
277+ // Use jq to count solutions
278+ const count = parseInt (Bash (` cat "${ solPath} " 2>/dev/null | jq -s 'length' 2>/dev/null || echo '0'` ).trim ()) || 0 ;
247279
248- if (solutions .length === 1 ) {
280+ if (count === 0 ) continue ; // No solutions - skip silently (agent already reported)
281+
282+ if (count === 1 ) {
249283 // Auto-bind single solution
250- selectedSolId = solutions[ 0 ] .id ;
251- console . log ( ` ✓ Auto-bound ${ selectedSolId } to ${ issue .id } ( ${ solutions[ 0 ]. tasks ? . length || 0 } tasks) ` );
284+ const solId = Bash ( ` cat " ${ solPath } " | jq -r ' .id' | head -1 ` ). trim () ;
285+ bindSolution ( issue .id , solId );
252286 } else {
253- // Multiple solutions - ask user
254- const answer = AskUserQuestion({
255- questions: [{
256- question: ` Select solution for ${issue .id }: ` ,
257- header: issue.id,
258- multiSelect: false,
259- options: solutions.map(s => ({
260- label: ` ${s .id }: ${s .description || ' Solution' }` ,
261- description: ` ${s .tasks ? .length || 0 } tasks`
262- }))
263- }]
264- });
265-
266- selectedSolId = extractSelectedSolutionId(answer);
267- console.log(` ✓ Bound ${selectedSolId} to ${issue .id }` );
287+ // Multiple solutions - collect for batch selection
288+ const options = Bash (` cat "${ solPath} " | jq -c '{id, description, task_count: (.tasks | length)}'` ).trim ();
289+ needSelection .push ({ issue, options: options .split (' \n ' ).map (s => JSON .parse (s)) });
268290 }
291+ }
269292
270- // Update issue in allIssuesUpdated
271- const issueIndex = allIssuesUpdated.findIndex(i => i.id === issue.id);
272- if (issueIndex !== -1) {
273- allIssuesUpdated[issueIndex].bound_solution_id = selectedSolId;
274- allIssuesUpdated[issueIndex].status = 'planned';
275- allIssuesUpdated[issueIndex].planned_at = new Date().toISOString();
276- allIssuesUpdated[issueIndex].updated_at = new Date().toISOString();
293+ // Batch ask user for multiple-solution issues
294+ if (needSelection .length > 0 ) {
295+ const answer = AskUserQuestion ({
296+ questions: needSelection .map (({ issue, options }) => ({
297+ question: ` Select solution for ${ issue .id } :` ,
298+ header: issue .id ,
299+ multiSelect: false ,
300+ options: options .map (s => ({
301+ label: ` ${ s .id } (${ s .task_count } tasks)` ,
302+ description: s .description || ' Solution'
303+ }))
304+ }))
305+ });
306+
307+ // Bind selected solutions
308+ for (const { issue } of needSelection) {
309+ const selectedSolId = extractSelectedSolutionId (answer, issue .id );
310+ if (selectedSolId) bindSolution (issue .id , selectedSolId);
277311 }
278-
279- // Mark solution as bound in solutions file
280- const updatedSolutions = solutions.map(s => ({
281- ...s,
282- is_bound: s.id === selectedSolId,
283- bound_at: s.id === selectedSolId ? new Date().toISOString() : s.bound_at
284- }));
285- Write(solPath, updatedSolutions.map(s => JSON.stringify(s)).join('\n '));
286312}
287313
288- // Write updated issues.jsonl
289- Write(issuesPath, allIssuesUpdated.map(i => JSON.stringify(i)).join('\n '));
314+ // Helper: bind solution to issue
315+ function bindSolution (issueId , solutionId ) {
316+ const now = new Date ().toISOString ();
317+ const solPath = ` .workflow/issues/solutions/${ issueId} .jsonl` ;
318+
319+ // Update issue status
320+ Bash (`
321+ tmpfile=$(mktemp) && \\
322+ cat "${ issuesPath} " | jq -c 'if .id == "${ issueId} " then . + {
323+ bound_solution_id: "${ solutionId} ", status: "planned",
324+ planned_at: "${ now} ", updated_at: "${ now} "
325+ } else . end' > "$tmpfile" && mv "$tmpfile" "${ issuesPath} "
326+ ` );
327+
328+ // Mark solution as bound
329+ Bash (`
330+ tmpfile=$(mktemp) && \\
331+ cat "${ solPath} " | jq -c 'if .id == "${ solutionId} " then . + {
332+ is_bound: true, bound_at: "${ now} "
333+ } else . + {is_bound: false} end' > "$tmpfile" && mv "$tmpfile" "${ solPath} "
334+ ` );
335+ }
290336` ` `
291337
292338### Phase 4: Summary
293339
294340` ` ` javascript
341+ // Brief summary using jq
342+ const stats = Bash (`
343+ cat "${ issuesPath} " 2>/dev/null | \\
344+ jq -s '[.[] | select(.status == "planned")] | length' 2>/dev/null || echo '0'
345+ ` ).trim ();
346+
295347console .log (`
296- ## Planning Complete
297-
298- ** Issues Planned** : ${issues .length }
299-
300- ### Bound Solutions
301- ${issues .map (i => {
302- const issue = allIssuesUpdated .find (a => a .id === i .id );
303- return issue? .bound_solution_id
304- ? ` ✓ ${ i .id } : ${ issue .bound_solution_id } `
305- : ` ○ ${ i .id } : No solution bound` ;
306- }).join (' \n ' )}
307-
308- ### Next Steps
309- 1. Review: \` ccw issue status <issue-id>\`
310- 2. Form queue: \` /issue:queue\`
311- 3. Execute: \` /issue:execute\`
348+ ## Done: ${ issues .length } issues → ${ stats} planned
349+
350+ Next: \` /issue:queue\` → \` /issue:execute\`
312351` );
313352` ` `
314353
0 commit comments