detached 00396bf

feat(plan): allow several in_progress steps for parallel work

commit
00396bf90ef684a9dfe931cd6bf979a70cb2c756
author
brice · · 2026-10-10 01:44 UTC
parent
deaab59
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
refs
tag: v0.49.0
6 files · +43 -41
M README.md +1 -1
@@ -274,7 +274,7 @@ Print-mode stats contain `provider`, `model`, `prompt_tokens`, `reasoning_tokens
274274 - `lsp`: query configured language servers and linters for diagnostics, definitions, references, hover information, symbols, renames, code actions, capabilities, and raw protocol requests. LSP servers use stdio JSON-RPC; project `lsp.json` files can add or override servers and CLI linters. Diagnostics are bounded, sorted, deduplicated by path/severity/code/start position, and repeated issues are grouped before they reach the model. Successful native edits, writes, renames, and applied code actions automatically attach bounded diagnostics when available. See [docs/lsp.md](docs/lsp.md).
275275 - `web_search`: search DuckDuckGo HTML and return bounded source titles, URLs, snippets, and opaque references. `web_open` opens either a complete public URL or a referenced page, while `web_find` and `web_click` respectively find literal text in an opened page and follow a displayed link. The four tools are enabled together by default for normal CLI sessions and unavailable to bot, default SDK, and packaged-agent runs. See [docs/web-search.md](docs/web-search.md).
276276 - `update_goal`: let the main agent start its own mission when none is active, set the next bounded goal within an active mission, pause work while a decision is pending, resume it once settled, mark the active goal `done` or `blocked`, or atomically supersede an invalid goal with a necessary in-mission replacement. Users can also pause and resume through `/goal`; clearing remains user-controlled.
277-- `plan`: maintain a session-scoped checklist of `pending`, `in_progress`, and `completed` steps through `set` (replace every step), `add`, `update` one step, `remove` one step, `clear`, and `show`. At most one step may be `in_progress`. `set` requires at least one step; `clear` is how every step is removed. There is no step-count limit, but a plan carried across compaction shows only its first 12 steps. The plan is persisted in every mode through the tool-result commit path, inherited by forks and imports, and cleared by `/clear`. Successful updates appear inline as a checklist and emit `plan_update` events in JSON and RPC modes.
277+- `plan`: maintain a session-scoped checklist of `pending`, `in_progress`, and `completed` steps through `set` (replace every step), `add`, `update` one step, `remove` one step, `clear`, and `show`. Several steps may be `in_progress` at once to track parallel work such as concurrent sub-agents. `set` requires at least one step; `clear` is how every step is removed. There is no step-count limit, but a plan carried across compaction shows only its first 12 steps. The plan is persisted in every mode through the tool-result commit path, inherited by forks and imports, and cleared by `/clear`. Successful updates appear inline as a checklist and emit `plan_update` events in JSON and RPC modes.
278278 - `schedule`: add, list, or cancel recurring five-field cron prompts for the current interactive session. A task captures the machine's current timezone when it is created. While this zut process remains open, a due task waits for an active target turn to finish, or resumes an inactive target session in the background; its output is recorded in that target transcript. Resumed background tasks also show a completion notification in the current TUI. Scheduling is in-memory only: quitting zut cancels pending tasks, and schedules do not survive restart or reboot. Print, stream, JSON, and RPC modes do not expose this tool.
279279
280280 Tool results carry resource provenance in the durable session. Before each provider request, Zut replaces an older read only when an equivalent newer view supersedes it, and replaces a multi-file search only after every discovered file has been consumed or mutated. This provider-only projection removes known-stale context without rewriting session history. Because an older projected result changes, prompt caching restarts from that point; compact deterministic markers limit the cost.
@@ -274,7 +274,7 @@ Print-mode stats contain `provider`, `model`, `prompt_tokens`, `reasoning_tokens
274 - `lsp`: query configured language servers and linters for diagnostics, definitions, references, hover information, symbols, renames, code actions, capabilities, and raw protocol requests. LSP servers use stdio JSON-RPC; project `lsp.json` files can add or override servers and CLI linters. Diagnostics are bounded, sorted, deduplicated by path/severity/code/start position, and repeated issues are grouped before they reach the model. Successful native edits, writes, renames, and applied code actions automatically attach bounded diagnostics when available. See [docs/lsp.md](docs/lsp.md).274 - `lsp`: query configured language servers and linters for diagnostics, definitions, references, hover information, symbols, renames, code actions, capabilities, and raw protocol requests. LSP servers use stdio JSON-RPC; project `lsp.json` files can add or override servers and CLI linters. Diagnostics are bounded, sorted, deduplicated by path/severity/code/start position, and repeated issues are grouped before they reach the model. Successful native edits, writes, renames, and applied code actions automatically attach bounded diagnostics when available. See [docs/lsp.md](docs/lsp.md).
275 - `web_search`: search DuckDuckGo HTML and return bounded source titles, URLs, snippets, and opaque references. `web_open` opens either a complete public URL or a referenced page, while `web_find` and `web_click` respectively find literal text in an opened page and follow a displayed link. The four tools are enabled together by default for normal CLI sessions and unavailable to bot, default SDK, and packaged-agent runs. See [docs/web-search.md](docs/web-search.md).275 - `web_search`: search DuckDuckGo HTML and return bounded source titles, URLs, snippets, and opaque references. `web_open` opens either a complete public URL or a referenced page, while `web_find` and `web_click` respectively find literal text in an opened page and follow a displayed link. The four tools are enabled together by default for normal CLI sessions and unavailable to bot, default SDK, and packaged-agent runs. See [docs/web-search.md](docs/web-search.md).
276 - `update_goal`: let the main agent start its own mission when none is active, set the next bounded goal within an active mission, pause work while a decision is pending, resume it once settled, mark the active goal `done` or `blocked`, or atomically supersede an invalid goal with a necessary in-mission replacement. Users can also pause and resume through `/goal`; clearing remains user-controlled.276 - `update_goal`: let the main agent start its own mission when none is active, set the next bounded goal within an active mission, pause work while a decision is pending, resume it once settled, mark the active goal `done` or `blocked`, or atomically supersede an invalid goal with a necessary in-mission replacement. Users can also pause and resume through `/goal`; clearing remains user-controlled.
277-- `plan`: maintain a session-scoped checklist of `pending`, `in_progress`, and `completed` steps through `set` (replace every step), `add`, `update` one step, `remove` one step, `clear`, and `show`. At most one step may be `in_progress`. `set` requires at least one step; `clear` is how every step is removed. There is no step-count limit, but a plan carried across compaction shows only its first 12 steps. The plan is persisted in every mode through the tool-result commit path, inherited by forks and imports, and cleared by `/clear`. Successful updates appear inline as a checklist and emit `plan_update` events in JSON and RPC modes.277+- `plan`: maintain a session-scoped checklist of `pending`, `in_progress`, and `completed` steps through `set` (replace every step), `add`, `update` one step, `remove` one step, `clear`, and `show`. Several steps may be `in_progress` at once to track parallel work such as concurrent sub-agents. `set` requires at least one step; `clear` is how every step is removed. There is no step-count limit, but a plan carried across compaction shows only its first 12 steps. The plan is persisted in every mode through the tool-result commit path, inherited by forks and imports, and cleared by `/clear`. Successful updates appear inline as a checklist and emit `plan_update` events in JSON and RPC modes.
278 - `schedule`: add, list, or cancel recurring five-field cron prompts for the current interactive session. A task captures the machine's current timezone when it is created. While this zut process remains open, a due task waits for an active target turn to finish, or resumes an inactive target session in the background; its output is recorded in that target transcript. Resumed background tasks also show a completion notification in the current TUI. Scheduling is in-memory only: quitting zut cancels pending tasks, and schedules do not survive restart or reboot. Print, stream, JSON, and RPC modes do not expose this tool.278 - `schedule`: add, list, or cancel recurring five-field cron prompts for the current interactive session. A task captures the machine's current timezone when it is created. While this zut process remains open, a due task waits for an active target turn to finish, or resumes an inactive target session in the background; its output is recorded in that target transcript. Resumed background tasks also show a completion notification in the current TUI. Scheduling is in-memory only: quitting zut cancels pending tasks, and schedules do not survive restart or reboot. Print, stream, JSON, and RPC modes do not expose this tool.
279 279
280 Tool results carry resource provenance in the durable session. Before each provider request, Zut replaces an older read only when an equivalent newer view supersedes it, and replaces a multi-file search only after every discovered file has been consumed or mutated. This provider-only projection removes known-stale context without rewriting session history. Because an older projected result changes, prompt caching restarts from that point; compact deterministic markers limit the cost.280 Tool results carry resource provenance in the durable session. Before each provider request, Zut replaces an older read only when an equivalent newer view supersedes it, and replaces a multi-file search only after every discovered file has been consumed or mutated. This provider-only projection removes known-stale context without rewriting session history. Because an older projected result changes, prompt caching restarts from that point; compact deterministic markers limit the cost.
M packages/agent/systemprompt.go +1 -1
@@ -111,7 +111,7 @@ const compactedSummaryHandoffInstruction = `When you see a "## Context Summary (
111111
112112 const taskExecutionGuidance = `Treat requests such as "can you" as instructions to do the requested work within the user's intended scope; do not stop at a plan or an offer to continue. Proceed with authorized, non-destructive work and reasonable assumptions until the task is complete or genuinely blocked. Follow each progress statement with the action it describes: pair every intermediate update with tool calls or a concrete next step in the same turn. Final answers contain the result of the work or a concrete blocker, never a bare promise to continue. Respect requests for explanation, planning, or review without making unsolicited changes. Ask only when a necessary decision blocks safe, correct progress; complete independent authorized work first. Verify changes with checks appropriate to their scope. Report which checks ran and what remains unverified. Preserve tool permissions, required confirmations, and explicit approval requirements; do not infer permission for destructive or irreversible actions.`
113113
114-const planGuidance = `For multi-phase or dependency-ordered work, and whenever the user asks for a plan, maintain the plan checklist instead of tracking steps in prose. Skip it for single-step or informational requests. Keep at most one step in_progress and update the plan at phase boundaries, not after every command. Group related steps instead of appending endlessly: a plan is phases you can verify, not a task log. Never mark a step completed without doing the work, and do not batch-complete steps after the fact. Prefer add, update, and remove over re-sending the whole list with set; use set only to replace the checklist and clear to remove it. After a plan call, do not restate the plan in prose because the interface already shows it. After compaction, or whenever step identity is unclear, call plan with action:"show" instead of guessing an index. A plan carried over from earlier work stays visible until it is cleared or replaced, so clear it once the work it describes is no longer in flight.`
114+const planGuidance = `For multi-phase or dependency-ordered work, and whenever the user asks for a plan, maintain the plan checklist instead of tracking steps in prose. Skip it for single-step or informational requests. Mark a step in_progress while it is actively worked on; several steps may be in_progress only when they truly run in parallel, such as concurrent sub-agents. Update the plan at phase boundaries, not after every command. Group related steps instead of appending endlessly: a plan is phases you can verify, not a task log. Never mark a step completed without doing the work, and do not batch-complete steps after the fact. Prefer add, update, and remove over re-sending the whole list with set; use set only to replace the checklist and clear to remove it. After a plan call, do not restate the plan in prose because the interface already shows it. After compaction, or whenever step identity is unclear, call plan with action:"show" instead of guessing an index. A plan carried over from earlier work stays visible until it is cleared or replaced, so clear it once the work it describes is no longer in flight.`
115115
116116 const skillPriorityGuidance = `Explicit user instructions take precedence over skill guidelines. Skills do not grant permissions or override system or developer constraints. If a skill would block requested work or change its scope, identify its source and the relevant instruction, explain the conflict, and follow the applicable higher-priority instruction.`
117117
@@ -111,7 +111,7 @@ const compactedSummaryHandoffInstruction = `When you see a "## Context Summary (
111 111
112 const taskExecutionGuidance = `Treat requests such as "can you" as instructions to do the requested work within the user's intended scope; do not stop at a plan or an offer to continue. Proceed with authorized, non-destructive work and reasonable assumptions until the task is complete or genuinely blocked. Follow each progress statement with the action it describes: pair every intermediate update with tool calls or a concrete next step in the same turn. Final answers contain the result of the work or a concrete blocker, never a bare promise to continue. Respect requests for explanation, planning, or review without making unsolicited changes. Ask only when a necessary decision blocks safe, correct progress; complete independent authorized work first. Verify changes with checks appropriate to their scope. Report which checks ran and what remains unverified. Preserve tool permissions, required confirmations, and explicit approval requirements; do not infer permission for destructive or irreversible actions.`112 const taskExecutionGuidance = `Treat requests such as "can you" as instructions to do the requested work within the user's intended scope; do not stop at a plan or an offer to continue. Proceed with authorized, non-destructive work and reasonable assumptions until the task is complete or genuinely blocked. Follow each progress statement with the action it describes: pair every intermediate update with tool calls or a concrete next step in the same turn. Final answers contain the result of the work or a concrete blocker, never a bare promise to continue. Respect requests for explanation, planning, or review without making unsolicited changes. Ask only when a necessary decision blocks safe, correct progress; complete independent authorized work first. Verify changes with checks appropriate to their scope. Report which checks ran and what remains unverified. Preserve tool permissions, required confirmations, and explicit approval requirements; do not infer permission for destructive or irreversible actions.`
113 113
114-const planGuidance = `For multi-phase or dependency-ordered work, and whenever the user asks for a plan, maintain the plan checklist instead of tracking steps in prose. Skip it for single-step or informational requests. Keep at most one step in_progress and update the plan at phase boundaries, not after every command. Group related steps instead of appending endlessly: a plan is phases you can verify, not a task log. Never mark a step completed without doing the work, and do not batch-complete steps after the fact. Prefer add, update, and remove over re-sending the whole list with set; use set only to replace the checklist and clear to remove it. After a plan call, do not restate the plan in prose because the interface already shows it. After compaction, or whenever step identity is unclear, call plan with action:"show" instead of guessing an index. A plan carried over from earlier work stays visible until it is cleared or replaced, so clear it once the work it describes is no longer in flight.`114+const planGuidance = `For multi-phase or dependency-ordered work, and whenever the user asks for a plan, maintain the plan checklist instead of tracking steps in prose. Skip it for single-step or informational requests. Mark a step in_progress while it is actively worked on; several steps may be in_progress only when they truly run in parallel, such as concurrent sub-agents. Update the plan at phase boundaries, not after every command. Group related steps instead of appending endlessly: a plan is phases you can verify, not a task log. Never mark a step completed without doing the work, and do not batch-complete steps after the fact. Prefer add, update, and remove over re-sending the whole list with set; use set only to replace the checklist and clear to remove it. After a plan call, do not restate the plan in prose because the interface already shows it. After compaction, or whenever step identity is unclear, call plan with action:"show" instead of guessing an index. A plan carried over from earlier work stays visible until it is cleared or replaced, so clear it once the work it describes is no longer in flight.`
115 115
116 const skillPriorityGuidance = `Explicit user instructions take precedence over skill guidelines. Skills do not grant permissions or override system or developer constraints. If a skill would block requested work or change its scope, identify its source and the relevant instruction, explain the conflict, and follow the applicable higher-priority instruction.`116 const skillPriorityGuidance = `Explicit user instructions take precedence over skill guidelines. Skills do not grant permissions or override system or developer constraints. If a skill would block requested work or change its scope, identify its source and the relevant instruction, explain the conflict, and follow the applicable higher-priority instruction.`
117 117
M packages/agent/tools/plan.go +1 -1
@@ -56,7 +56,7 @@ func (t *PlanTool) Name() string { return PlanToolName }
5656 func (t *PlanTool) AllowArgumentRewrite() bool { return false }
5757
5858 func (t *PlanTool) Description() string {
59- return "Maintain a persisted task checklist. Actions: set (replace every step), add, update one step, remove one step, clear, show the current checklist. At most one step may be in_progress at a time; prefer add/update/remove deltas over re-sending the whole list with set."
59+ return "Maintain a persisted task checklist. Actions: set (replace every step), add, update one step, remove one step, clear, show the current checklist. Several steps may be in_progress at once when work runs in parallel (for example concurrent sub-agents); prefer add/update/remove deltas over re-sending the whole list with set."
6060 }
6161
6262 func (t *PlanTool) Schema() json.RawMessage { return json.RawMessage(planSchema) }
@@ -56,7 +56,7 @@ func (t *PlanTool) Name() string { return PlanToolName }
56 func (t *PlanTool) AllowArgumentRewrite() bool { return false }56 func (t *PlanTool) AllowArgumentRewrite() bool { return false }
57 57
58 func (t *PlanTool) Description() string {58 func (t *PlanTool) Description() string {
59- return "Maintain a persisted task checklist. Actions: set (replace every step), add, update one step, remove one step, clear, show the current checklist. At most one step may be in_progress at a time; prefer add/update/remove deltas over re-sending the whole list with set."59+ return "Maintain a persisted task checklist. Actions: set (replace every step), add, update one step, remove one step, clear, show the current checklist. Several steps may be in_progress at once when work runs in parallel (for example concurrent sub-agents); prefer add/update/remove deltas over re-sending the whole list with set."
60 }60 }
61 61
62 func (t *PlanTool) Schema() json.RawMessage { return json.RawMessage(planSchema) }62 func (t *PlanTool) Schema() json.RawMessage { return json.RawMessage(planSchema) }
M packages/core/plan.go +12 -12
@@ -2,6 +2,7 @@ package core
22
33 import (
44 "fmt"
5+ "strconv"
56 "strings"
67 "time"
78 )
@@ -140,25 +141,20 @@ func (a *Agent) previewPlanOperationLocked(op PlanOperation) (PlanUpdate, error)
140141 }
141142
142143 // validatePlanSteps rejects steps that cannot be rendered or persisted: empty
143-// text, an unknown status, or more than one in_progress step. There is
144+// text or an unknown status. Several steps may be in_progress at once so
145+// parallel work (for example concurrent sub-agents) can be tracked. There is
144146 // deliberately no length cap.
145147 func validatePlanSteps(plan []PlanStep) error {
146- inProgress := 0
147148 for idx := range plan {
148149 if strings.TrimSpace(plan[idx].Step) == "" {
149150 return fmt.Errorf("plan: step text is required")
150151 }
151152 switch plan[idx].Status {
152- case PlanPending, PlanCompleted:
153- case PlanInProgress:
154- inProgress++
153+ case PlanPending, PlanCompleted, PlanInProgress:
155154 default:
156155 return fmt.Errorf("plan: unknown status %q", plan[idx].Status)
157156 }
158157 }
159- if inProgress > 1 {
160- return fmt.Errorf("plan: at most one step can be in_progress")
161- }
162158 return nil
163159 }
164160
@@ -191,18 +187,22 @@ func planMutationSummary(plan []PlanStep) string {
191187 return "Plan cleared"
192188 }
193189 completed := 0
194- inProgressIndex := 0
190+ var inProgress []string
195191 for idx, step := range plan {
196192 switch step.Status {
197193 case PlanCompleted:
198194 completed++
199195 case PlanInProgress:
200- inProgressIndex = idx + 1
196+ inProgress = append(inProgress, strconv.Itoa(idx+1))
201197 }
202198 }
203199 summary := fmt.Sprintf("Plan updated (%d/%d completed", completed, len(plan))
204- if inProgressIndex > 0 {
205- summary += fmt.Sprintf("; in progress: step %d", inProgressIndex)
200+ switch len(inProgress) {
201+ case 0:
202+ case 1:
203+ summary += "; in progress: step " + inProgress[0]
204+ default:
205+ summary += "; in progress: steps " + strings.Join(inProgress, ", ")
206206 }
207207 return summary + ")"
208208 }
@@ -2,6 +2,7 @@ package core
2 2
3 import (3 import (
4 "fmt"4 "fmt"
5+ "strconv"
5 "strings"6 "strings"
6 "time"7 "time"
7 )8 )
@@ -140,25 +141,20 @@ func (a *Agent) previewPlanOperationLocked(op PlanOperation) (PlanUpdate, error)
140 }141 }
141 142
142 // validatePlanSteps rejects steps that cannot be rendered or persisted: empty143 // validatePlanSteps rejects steps that cannot be rendered or persisted: empty
143-// text, an unknown status, or more than one in_progress step. There is144+// text or an unknown status. Several steps may be in_progress at once so
145+// parallel work (for example concurrent sub-agents) can be tracked. There is
144 // deliberately no length cap.146 // deliberately no length cap.
145 func validatePlanSteps(plan []PlanStep) error {147 func validatePlanSteps(plan []PlanStep) error {
146- inProgress := 0
147 for idx := range plan {148 for idx := range plan {
148 if strings.TrimSpace(plan[idx].Step) == "" {149 if strings.TrimSpace(plan[idx].Step) == "" {
149 return fmt.Errorf("plan: step text is required")150 return fmt.Errorf("plan: step text is required")
150 }151 }
151 switch plan[idx].Status {152 switch plan[idx].Status {
152- case PlanPending, PlanCompleted:153+ case PlanPending, PlanCompleted, PlanInProgress:
153- case PlanInProgress:
154- inProgress++
155 default:154 default:
156 return fmt.Errorf("plan: unknown status %q", plan[idx].Status)155 return fmt.Errorf("plan: unknown status %q", plan[idx].Status)
157 }156 }
158 }157 }
159- if inProgress > 1 {
160- return fmt.Errorf("plan: at most one step can be in_progress")
161- }
162 return nil158 return nil
163 }159 }
164 160
@@ -191,18 +187,22 @@ func planMutationSummary(plan []PlanStep) string {
191 return "Plan cleared"187 return "Plan cleared"
192 }188 }
193 completed := 0189 completed := 0
194- inProgressIndex := 0190+ var inProgress []string
195 for idx, step := range plan {191 for idx, step := range plan {
196 switch step.Status {192 switch step.Status {
197 case PlanCompleted:193 case PlanCompleted:
198 completed++194 completed++
199 case PlanInProgress:195 case PlanInProgress:
200- inProgressIndex = idx + 1196+ inProgress = append(inProgress, strconv.Itoa(idx+1))
201 }197 }
202 }198 }
203 summary := fmt.Sprintf("Plan updated (%d/%d completed", completed, len(plan))199 summary := fmt.Sprintf("Plan updated (%d/%d completed", completed, len(plan))
204- if inProgressIndex > 0 {200+ switch len(inProgress) {
205- summary += fmt.Sprintf("; in progress: step %d", inProgressIndex)201+ case 0:
202+ case 1:
203+ summary += "; in progress: step " + inProgress[0]
204+ default:
205+ summary += "; in progress: steps " + strings.Join(inProgress, ", ")
206 }206 }
207 return summary + ")"207 return summary + ")"
208 }208 }
M packages/core/plan_test.go +25 -17
@@ -68,6 +68,28 @@ func TestPreviewPlanOperationDoesNotMutate(t *testing.T) {
6868 }
6969 }
7070
71+func TestPreviewPlanOperationAllowsParallelInProgress(t *testing.T) {
72+ agent := &Agent{}
73+ agent.SetPlan([]PlanStep{
74+ planStep("first", PlanInProgress),
75+ planStep("second", PlanPending),
76+ planStep("third", PlanPending),
77+ })
78+
79+ update, err := agent.previewPlanOperation(PlanOperation{Action: "update", Index: 3, Status: PlanInProgress})
80+ if err != nil {
81+ t.Fatalf("update: %v", err)
82+ }
83+ if got := planMutationSummary(update.Plan); got != "Plan updated (0/3 completed; in progress: steps 1, 3)" {
84+ t.Fatalf("summary = %q", got)
85+ }
86+
87+ parallel := []PlanStep{planStep("a", PlanInProgress), planStep("b", PlanInProgress)}
88+ if _, err := agent.previewPlanOperation(PlanOperation{Action: "set", Steps: parallel}); err != nil {
89+ t.Fatalf("set: %v", err)
90+ }
91+}
92+
7193 func TestPreviewPlanOperationErrors(t *testing.T) {
7294 base := []PlanStep{
7395 planStep("first", PlanInProgress),
@@ -135,21 +157,6 @@ func TestPreviewPlanOperationErrors(t *testing.T) {
135157 op: PlanOperation{Action: "set"},
136158 want: "plan: steps are required for set (use clear to remove every step)",
137159 },
138- {
139- name: "set two in progress",
140- op: PlanOperation{Action: "set", Steps: []PlanStep{planStep("a", PlanInProgress), planStep("b", PlanInProgress)}},
141- want: "plan: at most one step can be in_progress",
142- },
143- {
144- name: "add second in progress",
145- op: PlanOperation{Action: "add", Steps: []PlanStep{planStep("fourth", PlanInProgress)}},
146- want: "plan: at most one step can be in_progress",
147- },
148- {
149- name: "update second in progress",
150- op: PlanOperation{Action: "update", Index: 2, Status: PlanInProgress},
151- want: "plan: at most one step can be in_progress",
152- },
153160 {
154161 name: "set unknown status",
155162 op: PlanOperation{Action: "set", Steps: []PlanStep{planStep("a", PlanStepStatus("done"))}},
@@ -463,6 +470,7 @@ func TestSessionPlanNormalizesOnLoad(t *testing.T) {
463470 defer reopened.Close()
464471 want := []PlanStep{
465472 planStep("first", PlanInProgress),
473+ planStep("second", PlanInProgress),
466474 planStep("third", PlanCompleted),
467475 }
468476 if reopened.Meta.Plan == nil || !slices.Equal(reopened.Meta.Plan.Steps, want) {
@@ -801,13 +809,13 @@ func TestNormalizeSessionPlan(t *testing.T) {
801809 want: []PlanStep{planStep("kept", PlanCompleted)},
802810 },
803811 {
804- name: "keeps only first in progress",
812+ name: "keeps parallel in progress steps",
805813 in: &SessionPlan{Steps: []PlanStep{
806814 planStep("first", PlanInProgress),
807815 planStep("second", PlanInProgress),
808816 planStep("third", PlanPending),
809817 }},
810- want: []PlanStep{planStep("first", PlanInProgress), planStep("third", PlanPending)},
818+ want: []PlanStep{planStep("first", PlanInProgress), planStep("second", PlanInProgress), planStep("third", PlanPending)},
811819 },
812820 }
813821 for _, tt := range tests {
@@ -68,6 +68,28 @@ func TestPreviewPlanOperationDoesNotMutate(t *testing.T) {
68 }68 }
69 }69 }
70 70
71+func TestPreviewPlanOperationAllowsParallelInProgress(t *testing.T) {
72+ agent := &Agent{}
73+ agent.SetPlan([]PlanStep{
74+ planStep("first", PlanInProgress),
75+ planStep("second", PlanPending),
76+ planStep("third", PlanPending),
77+ })
78+
79+ update, err := agent.previewPlanOperation(PlanOperation{Action: "update", Index: 3, Status: PlanInProgress})
80+ if err != nil {
81+ t.Fatalf("update: %v", err)
82+ }
83+ if got := planMutationSummary(update.Plan); got != "Plan updated (0/3 completed; in progress: steps 1, 3)" {
84+ t.Fatalf("summary = %q", got)
85+ }
86+
87+ parallel := []PlanStep{planStep("a", PlanInProgress), planStep("b", PlanInProgress)}
88+ if _, err := agent.previewPlanOperation(PlanOperation{Action: "set", Steps: parallel}); err != nil {
89+ t.Fatalf("set: %v", err)
90+ }
91+}
92+
71 func TestPreviewPlanOperationErrors(t *testing.T) {93 func TestPreviewPlanOperationErrors(t *testing.T) {
72 base := []PlanStep{94 base := []PlanStep{
73 planStep("first", PlanInProgress),95 planStep("first", PlanInProgress),
@@ -135,21 +157,6 @@ func TestPreviewPlanOperationErrors(t *testing.T) {
135 op: PlanOperation{Action: "set"},157 op: PlanOperation{Action: "set"},
136 want: "plan: steps are required for set (use clear to remove every step)",158 want: "plan: steps are required for set (use clear to remove every step)",
137 },159 },
138- {
139- name: "set two in progress",
140- op: PlanOperation{Action: "set", Steps: []PlanStep{planStep("a", PlanInProgress), planStep("b", PlanInProgress)}},
141- want: "plan: at most one step can be in_progress",
142- },
143- {
144- name: "add second in progress",
145- op: PlanOperation{Action: "add", Steps: []PlanStep{planStep("fourth", PlanInProgress)}},
146- want: "plan: at most one step can be in_progress",
147- },
148- {
149- name: "update second in progress",
150- op: PlanOperation{Action: "update", Index: 2, Status: PlanInProgress},
151- want: "plan: at most one step can be in_progress",
152- },
153 {160 {
154 name: "set unknown status",161 name: "set unknown status",
155 op: PlanOperation{Action: "set", Steps: []PlanStep{planStep("a", PlanStepStatus("done"))}},162 op: PlanOperation{Action: "set", Steps: []PlanStep{planStep("a", PlanStepStatus("done"))}},
@@ -463,6 +470,7 @@ func TestSessionPlanNormalizesOnLoad(t *testing.T) {
463 defer reopened.Close()470 defer reopened.Close()
464 want := []PlanStep{471 want := []PlanStep{
465 planStep("first", PlanInProgress),472 planStep("first", PlanInProgress),
473+ planStep("second", PlanInProgress),
466 planStep("third", PlanCompleted),474 planStep("third", PlanCompleted),
467 }475 }
468 if reopened.Meta.Plan == nil || !slices.Equal(reopened.Meta.Plan.Steps, want) {476 if reopened.Meta.Plan == nil || !slices.Equal(reopened.Meta.Plan.Steps, want) {
@@ -801,13 +809,13 @@ func TestNormalizeSessionPlan(t *testing.T) {
801 want: []PlanStep{planStep("kept", PlanCompleted)},809 want: []PlanStep{planStep("kept", PlanCompleted)},
802 },810 },
803 {811 {
804- name: "keeps only first in progress",812+ name: "keeps parallel in progress steps",
805 in: &SessionPlan{Steps: []PlanStep{813 in: &SessionPlan{Steps: []PlanStep{
806 planStep("first", PlanInProgress),814 planStep("first", PlanInProgress),
807 planStep("second", PlanInProgress),815 planStep("second", PlanInProgress),
808 planStep("third", PlanPending),816 planStep("third", PlanPending),
809 }},817 }},
810- want: []PlanStep{planStep("first", PlanInProgress), planStep("third", PlanPending)},818+ want: []PlanStep{planStep("first", PlanInProgress), planStep("second", PlanInProgress), planStep("third", PlanPending)},
811 },819 },
812 }820 }
813 for _, tt := range tests {821 for _, tt := range tests {
M packages/core/session.go +3 -9
@@ -1464,15 +1464,14 @@ func (s *Session) assignGoalToMission(goal, previous *SessionGoal) error {
14641464 return nil
14651465 }
14661466
1467-// normalizeSessionPlan drops steps that carry no text or an unknown status and
1468-// keeps only the first in_progress step. It returns nil when nothing remains.
1467+// normalizeSessionPlan drops steps that carry no text or an unknown status. It
1468+// returns nil when nothing remains.
14691469 // It does not cap the plan length.
14701470 func normalizeSessionPlan(plan *SessionPlan) *SessionPlan {
14711471 if plan == nil {
14721472 return nil
14731473 }
14741474 steps := make([]PlanStep, 0, len(plan.Steps))
1475- seenInProgress := false
14761475 for _, step := range plan.Steps {
14771476 if strings.TrimSpace(step.Step) == "" {
14781477 continue
@@ -1485,12 +1484,7 @@ func normalizeSessionPlan(plan *SessionPlan) *SessionPlan {
14851484 status = PlanPending
14861485 }
14871486 switch status {
1488- case PlanPending, PlanCompleted:
1489- case PlanInProgress:
1490- if seenInProgress {
1491- continue
1492- }
1493- seenInProgress = true
1487+ case PlanPending, PlanCompleted, PlanInProgress:
14941488 default:
14951489 continue
14961490 }
@@ -1464,15 +1464,14 @@ func (s *Session) assignGoalToMission(goal, previous *SessionGoal) error {
1464 return nil1464 return nil
1465 }1465 }
1466 1466
1467-// normalizeSessionPlan drops steps that carry no text or an unknown status and1467+// normalizeSessionPlan drops steps that carry no text or an unknown status. It
1468-// keeps only the first in_progress step. It returns nil when nothing remains.1468+// returns nil when nothing remains.
1469 // It does not cap the plan length.1469 // It does not cap the plan length.
1470 func normalizeSessionPlan(plan *SessionPlan) *SessionPlan {1470 func normalizeSessionPlan(plan *SessionPlan) *SessionPlan {
1471 if plan == nil {1471 if plan == nil {
1472 return nil1472 return nil
1473 }1473 }
1474 steps := make([]PlanStep, 0, len(plan.Steps))1474 steps := make([]PlanStep, 0, len(plan.Steps))
1475- seenInProgress := false
1476 for _, step := range plan.Steps {1475 for _, step := range plan.Steps {
1477 if strings.TrimSpace(step.Step) == "" {1476 if strings.TrimSpace(step.Step) == "" {
1478 continue1477 continue
@@ -1485,12 +1484,7 @@ func normalizeSessionPlan(plan *SessionPlan) *SessionPlan {
1485 status = PlanPending1484 status = PlanPending
1486 }1485 }
1487 switch status {1486 switch status {
1488- case PlanPending, PlanCompleted:1487+ case PlanPending, PlanCompleted, PlanInProgress:
1489- case PlanInProgress:
1490- if seenInProgress {
1491- continue
1492- }
1493- seenInProgress = true
1494 default:1488 default:
1495 continue1489 continue
1496 }1490 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close