@tianhai/pi-workflow-kit 0.4.1 → 0.5.0

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.
@@ -0,0 +1,251 @@
1
+ # /workflow-next Handoff State Design
2
+
3
+ Date: 2026-04-09
4
+ Status: approved
5
+
6
+ ## Summary
7
+
8
+ Fix `/workflow-next` so that, for the same feature, a fresh handoff session preserves prior completed workflow history instead of showing earlier phases as `pending` again.
9
+
10
+ The command should become a strict forward-only handoff for the immediate next phase. It must not allow same-phase handoff, backward handoff, or direct jumps across multiple phases.
11
+
12
+ ## Problem
13
+
14
+ Today `/workflow-next` creates a new session and pre-fills the editor with the next skill, but it does not explicitly seed the new session with a derived workflow state for the same feature.
15
+
16
+ As a result, the new session may start from an empty workflow tracker state:
17
+
18
+ - `brainstorm: pending`
19
+ - `plan: pending`
20
+ - `execute: pending`
21
+ - `finalize: pending`
22
+
23
+ When the prefilled skill is then detected, the tracker advances only to the requested phase. Earlier phases remain `pending`, even when they were already completed in the previous session.
24
+
25
+ ## Goals
26
+
27
+ - Preserve prior completed workflow phases across `/workflow-next` handoff for the same feature.
28
+ - Preserve earlier-phase artifact paths and prompted flags.
29
+ - Keep TDD, debug, and verification state fresh in the new session.
30
+ - Make `/workflow-next` a strict immediate-next handoff command.
31
+ - Reject invalid handoffs before creating a new session.
32
+ - Rename the persisted state file to reflect current naming.
33
+
34
+ ## Non-goals
35
+
36
+ - Allow arbitrary phase switching.
37
+ - Allow skipping phases through `/workflow-next`.
38
+ - Carry over TDD/debug/verification runtime state into the new session.
39
+ - Change the existing slash-command UX beyond stricter validation and correct state seeding.
40
+
41
+ ## Decisions
42
+
43
+ ### 1. Preserve derived workflow-only state
44
+
45
+ When `/workflow-next <phase>` is used for the same feature, the new session will receive a derived workflow snapshot.
46
+
47
+ Rules:
48
+
49
+ - all phases before the requested phase are `complete`
50
+ - the requested phase is `active`
51
+ - all phases after the requested phase are `pending`
52
+ - `currentPhase` is the requested phase
53
+ - earlier-phase artifacts are preserved
54
+ - earlier-phase prompted flags are preserved
55
+
56
+ This snapshot is derived from the current workflow state, not reconstructed from filenames alone.
57
+
58
+ ### 2. Do not preserve execution-local monitor state
59
+
60
+ The new session must start with fresh:
61
+
62
+ - TDD state
63
+ - debug state
64
+ - verification state
65
+
66
+ Only workflow lineage is preserved.
67
+
68
+ ### 3. `/workflow-next` is immediate-next only
69
+
70
+ Allowed transitions:
71
+
72
+ - `brainstorm -> plan`
73
+ - `plan -> execute`
74
+ - `execute -> finalize`
75
+
76
+ Only when the current phase status is exactly `complete`.
77
+
78
+ Disallowed transitions:
79
+
80
+ - same-phase handoff
81
+ - backward handoff
82
+ - direct jumps such as `brainstorm -> execute` or `plan -> finalize`
83
+ - moving forward when the current phase is `pending`, `active`, or `skipped`
84
+
85
+ Skipped phases do not qualify for `/workflow-next`.
86
+
87
+ ### 4. Hard-fail invalid handoffs
88
+
89
+ Invalid requests must show an error and stop before opening a new session.
90
+
91
+ Examples:
92
+
93
+ - `Cannot hand off to execute from brainstorm. /workflow-next only supports the immediate next phase.`
94
+ - `Cannot hand off to plan because brainstorm is not complete.`
95
+ - `Cannot hand off to plan from plan. Use /workflow-reset for a new task or continue in this session.`
96
+
97
+ ### 5. Rename persisted state file
98
+
99
+ Rename the local state file from:
100
+
101
+ - `.pi/superpowers-state.json`
102
+
103
+ To:
104
+
105
+ - `.pi/workflow-kit-state.json`
106
+
107
+ Migration behavior:
108
+
109
+ - reconstruction first checks `.pi/workflow-kit-state.json`
110
+ - if absent, it falls back to `.pi/superpowers-state.json`
111
+ - persistence writes only `.pi/workflow-kit-state.json`
112
+
113
+ This preserves compatibility for existing users while moving to clearer naming.
114
+
115
+ ## Proposed implementation
116
+
117
+ ## Helper module
118
+
119
+ Add a small helper module under `extensions/workflow-monitor/`, for example:
120
+
121
+ - `workflow-next-state.ts`
122
+
123
+ Responsibilities:
124
+
125
+ ### `validateNextWorkflowPhase(currentState, requestedPhase)`
126
+
127
+ Input:
128
+
129
+ - current workflow state
130
+ - requested target phase
131
+
132
+ Behavior:
133
+
134
+ - require a current phase to exist
135
+ - require the requested phase to be the immediate next phase
136
+ - require `currentState.phases[currentState.currentPhase] === "complete"`
137
+ - return either success or a precise error message
138
+
139
+ ### `deriveWorkflowHandoffState(currentState, requestedPhase)`
140
+
141
+ Input:
142
+
143
+ - current workflow state
144
+ - requested target phase
145
+
146
+ Behavior:
147
+
148
+ - produce a new workflow snapshot for the handoff session
149
+ - mark prior phases `complete`
150
+ - mark the requested phase `active`
151
+ - leave later phases `pending`
152
+ - preserve artifacts and prompted flags for earlier phases
153
+ - set `currentPhase` to the requested phase
154
+
155
+ ## `/workflow-next` handler changes
156
+
157
+ Update `extensions/workflow-monitor.ts` so the handler:
158
+
159
+ 1. parses `phase` and optional artifact path
160
+ 2. validates the phase value against the known set
161
+ 3. reads `handler.getWorkflowState()`
162
+ 4. calls `validateNextWorkflowPhase(...)`
163
+ 5. if invalid, notifies with an error and returns
164
+ 6. creates the new session
165
+ 7. seeds the new session with a fresh snapshot containing:
166
+ - derived `workflow`
167
+ - default `tdd`
168
+ - default `debug`
169
+ - default `verification`
170
+ 8. pre-fills the editor text as today
171
+
172
+ The prefilled text remains useful, but the session no longer depends on skill detection to reconstruct earlier history.
173
+
174
+ ## Persistence flow
175
+
176
+ ### Current
177
+
178
+ State is persisted in two places:
179
+
180
+ - session custom entry (`superpowers_state`)
181
+ - local JSON file under `.pi/`
182
+
183
+ ### New behavior
184
+
185
+ Keep the same general persistence model, but:
186
+
187
+ - continue using the full snapshot shape for session persistence
188
+ - write the renamed local file
189
+ - allow reconstruction from either the new or legacy filename
190
+
191
+ ## Testing
192
+
193
+ Add or update tests for:
194
+
195
+ ### Workflow-next validation
196
+
197
+ - allows `brainstorm -> plan` only when `brainstorm` is `complete`
198
+ - allows `plan -> execute` only when `plan` is `complete`
199
+ - allows `execute -> finalize` only when `execute` is `complete`
200
+ - rejects same-phase handoff
201
+ - rejects backward handoff
202
+ - rejects direct jumps
203
+ - rejects handoff when current phase is `active`
204
+ - rejects handoff when current phase is `pending`
205
+ - rejects handoff when current phase is `skipped`
206
+
207
+ ### Derived state
208
+
209
+ - preserves prior completed phases in the new session
210
+ - preserves artifacts for earlier phases
211
+ - preserves prompted flags for earlier phases
212
+ - marks requested phase `active`
213
+ - leaves later phases `pending`
214
+ - resets TDD/debug/verification state in the new session
215
+
216
+ ### State-file migration
217
+
218
+ - `getStateFilePath()` returns `.pi/workflow-kit-state.json`
219
+ - reconstruction reads the new file when present
220
+ - reconstruction falls back to `.pi/superpowers-state.json` when the new file is absent
221
+ - persistence writes only the new filename
222
+
223
+ ## Risks and mitigations
224
+
225
+ ### Risk: session seeding API constraints
226
+
227
+ Depending on the pi session API, the new session may not directly expose a way to append a custom state entry before user submission.
228
+
229
+ Mitigation:
230
+
231
+ - if direct session seeding is supported, use it
232
+ - otherwise encode the derived workflow state into the handoff path using the existing persistence/reconstruction mechanism with minimal, well-scoped changes
233
+ - verify behavior with an integration test around `/workflow-next`
234
+
235
+ ### Risk: invalidating existing expectations
236
+
237
+ Tests and current behavior explicitly allow advancing to a later phase from empty state without backfilling earlier phases.
238
+
239
+ Mitigation:
240
+
241
+ - limit the stricter semantics to `/workflow-next`
242
+ - keep generic workflow tracker behavior unchanged unless a separate design chooses otherwise
243
+
244
+ ## Acceptance criteria
245
+
246
+ - Using `/workflow-next` for the immediate next phase of the same feature preserves prior completed phases in the fresh session.
247
+ - Earlier completed phases do not regress to `pending` in the new session.
248
+ - Artifacts and prompted flags for earlier phases are preserved.
249
+ - TDD/debug/verification state is fresh in the new session.
250
+ - Same-phase, backward, and direct-jump handoffs are rejected.
251
+ - The local state file is renamed to `.pi/workflow-kit-state.json` with fallback support for the legacy filename.
@@ -0,0 +1,253 @@
1
+ # /workflow-next Handoff State Implementation Plan
2
+
3
+ > **REQUIRED SUB-SKILL:** Use the executing-tasks skill to implement this plan task-by-task.
4
+
5
+ **Goal:** Make `/workflow-next` preserve prior completed workflow history for same-feature handoffs, enforce immediate-next-only transitions, and rename the persisted local state file with legacy fallback.
6
+
7
+ **Architecture:** Add a small workflow-next state helper that validates allowed handoffs and derives the workflow snapshot for the new session. Update the workflow monitor to seed the new session through `ctx.newSession({ setup })` with derived workflow state plus fresh monitor state, and add focused tests for validation, state derivation, and file migration behavior.
8
+
9
+ **Tech Stack:** TypeScript, Vitest, pi extension API (`ctx.newSession({ setup })`, `SessionManager.appendCustomEntry`)
10
+
11
+ ---
12
+
13
+ ## Verification
14
+
15
+ All tasks completed. Final test results:
16
+ - `tests/extension/workflow-monitor/workflow-next-command.test.ts` — 17/17 pass
17
+ - `tests/extension/workflow-monitor/state-persistence.test.ts` — 25/25 pass
18
+ - `tests/extension/workflow-monitor/` (full suite) — 360/360 pass
19
+
20
+ No regressions. All acceptance criteria met.
21
+
22
+
23
+ ---
24
+
25
+ ### Task 1: Add failing tests for workflow-next handoff validation and state seeding
26
+
27
+ **Type:** code
28
+ **TDD scenario:** Modifying tested code — run existing tests first
29
+
30
+ **Files:**
31
+ - Modify: `tests/extension/workflow-monitor/workflow-next-command.test.ts`
32
+ - Test: `tests/extension/workflow-monitor/workflow-next-command.test.ts`
33
+
34
+ **Step 1: Write the failing tests**
35
+
36
+ Add tests covering:
37
+ - allows `plan -> execute` only when `plan` is complete
38
+ - rejects same-phase handoff
39
+ - rejects backward handoff
40
+ - rejects direct jump handoff
41
+ - rejects handoff when current phase is active
42
+ - seeds new session setup with derived workflow state preserving earlier completed phases, artifacts, and prompted flags
43
+ - resets TDD/debug/verification state in the seeded session snapshot
44
+
45
+ **Step 2: Run test to verify it fails**
46
+
47
+ Run: `npx vitest run tests/extension/workflow-monitor/workflow-next-command.test.ts`
48
+ Expected: FAIL with missing validation and missing setup-state assertions
49
+
50
+ **Step 3: Write minimal implementation support in test scaffolding only if needed**
51
+
52
+ If needed, extend the fake `ctx.newSession` stub in the test so it records the `setup` callback and lets the test invoke it with a fake session manager that captures appended custom entries.
53
+
54
+ **Step 4: Run test to verify it still fails for the intended production behavior gap**
55
+
56
+ Run: `npx vitest run tests/extension/workflow-monitor/workflow-next-command.test.ts`
57
+ Expected: FAIL only on the new assertions tied to unimplemented production code
58
+
59
+ **Step 5: Commit**
60
+
61
+ ```bash
62
+ git add tests/extension/workflow-monitor/workflow-next-command.test.ts
63
+ git commit -m "test: cover workflow-next handoff validation"
64
+ ```
65
+
66
+ ### Task 2: Add failing tests for state-file rename and legacy fallback
67
+
68
+ **Type:** code
69
+ **TDD scenario:** Modifying tested code — run existing tests first
70
+
71
+ **Files:**
72
+ - Modify: `tests/extension/workflow-monitor/state-persistence.test.ts`
73
+ - Test: `tests/extension/workflow-monitor/state-persistence.test.ts`
74
+
75
+ **Step 1: Write the failing tests**
76
+
77
+ Add tests covering:
78
+ - `getStateFilePath()` returns `.pi/workflow-kit-state.json`
79
+ - `reconstructState()` prefers `.pi/workflow-kit-state.json` when present
80
+ - `reconstructState()` falls back to `.pi/superpowers-state.json` when the new file is absent
81
+ - extension persistence writes the new filename only
82
+
83
+ **Step 2: Run test to verify it fails**
84
+
85
+ Run: `npx vitest run tests/extension/workflow-monitor/state-persistence.test.ts`
86
+ Expected: FAIL because current code still uses `.pi/superpowers-state.json`
87
+
88
+ **Step 3: Keep test fixtures minimal**
89
+
90
+ Reuse existing `withTempCwd()` and fake pi helpers. When testing persistence wiring, assert against files under `.pi/` in the temp directory rather than broad repo state.
91
+
92
+ **Step 4: Run test to verify it still fails for the intended production behavior gap**
93
+
94
+ Run: `npx vitest run tests/extension/workflow-monitor/state-persistence.test.ts`
95
+ Expected: FAIL only on filename/migration assertions
96
+
97
+ **Step 5: Commit**
98
+
99
+ ```bash
100
+ git add tests/extension/workflow-monitor/state-persistence.test.ts
101
+ git commit -m "test: cover workflow state file migration"
102
+ ```
103
+
104
+ ### Task 3: Implement workflow-next handoff validation and derived state helper
105
+
106
+ **Type:** code
107
+ **TDD scenario:** New feature — full TDD cycle
108
+
109
+ **Files:**
110
+ - Create: `extensions/workflow-monitor/workflow-next-state.ts`
111
+ - Modify: `extensions/workflow-monitor.ts`
112
+ - Test: `tests/extension/workflow-monitor/workflow-next-command.test.ts`
113
+
114
+ **Step 1: Write the helper module with pure functions**
115
+
116
+ Implement functions such as:
117
+ - `getImmediateNextPhase(currentPhase)`
118
+ - `validateWorkflowNextRequest(currentState, requestedPhase)`
119
+ - `deriveWorkflowHandoffState(currentState, requestedPhase)`
120
+
121
+ Behavior:
122
+ - require an existing current phase
123
+ - require current phase status to be exactly `complete`
124
+ - allow only the immediate next phase
125
+ - reject same/backward/direct-jump handoffs with precise messages
126
+ - derive workflow state with earlier phases `complete`, target `active`, later `pending`
127
+ - preserve earlier-phase artifacts and prompted flags
128
+
129
+ **Step 2: Update `/workflow-next` to use the helper and seed session state**
130
+
131
+ In `extensions/workflow-monitor.ts`:
132
+ - import the helper functions
133
+ - validate before calling `ctx.newSession(...)`
134
+ - use `ctx.newSession({ parentSession, setup })`
135
+ - inside `setup`, append a `superpowers_state` custom entry containing:
136
+ - derived `workflow`
137
+ - fresh `tdd` from `TDD_DEFAULTS`
138
+ - fresh `debug` from `DEBUG_DEFAULTS`
139
+ - fresh `verification` from `VERIFICATION_DEFAULTS`
140
+ - `savedAt: Date.now()`
141
+ - keep the editor prefill behavior unchanged
142
+
143
+ **Step 3: Run targeted tests**
144
+
145
+ Run: `npx vitest run tests/extension/workflow-monitor/workflow-next-command.test.ts`
146
+ Expected: PASS
147
+
148
+ **Step 4: Review for YAGNI and edge cases**
149
+
150
+ Verify:
151
+ - helper stays pure and focused
152
+ - no generic tracker semantics are changed outside `/workflow-next`
153
+ - invalid requests exit before session creation
154
+
155
+ **Step 5: Commit**
156
+
157
+ ```bash
158
+ git add extensions/workflow-monitor/workflow-next-state.ts extensions/workflow-monitor.ts tests/extension/workflow-monitor/workflow-next-command.test.ts
159
+ git commit -m "feat: preserve workflow state across workflow-next"
160
+ ```
161
+
162
+ ### Task 4: Implement state-file rename with legacy fallback
163
+
164
+ **Type:** code
165
+ **TDD scenario:** Modifying tested code — run existing tests first
166
+
167
+ **Files:**
168
+ - Modify: `extensions/workflow-monitor.ts`
169
+ - Test: `tests/extension/workflow-monitor/state-persistence.test.ts`
170
+
171
+ **Step 1: Update state file path helpers**
172
+
173
+ In `extensions/workflow-monitor.ts`:
174
+ - change `getStateFilePath()` to return `.pi/workflow-kit-state.json`
175
+ - add a legacy-path helper for `.pi/superpowers-state.json` if needed
176
+ - update `reconstructState()` to check new path first, then legacy path
177
+
178
+ **Step 2: Keep persistence write path singular**
179
+
180
+ Ensure `persistState()` writes only the new path and does not continue writing the legacy file.
181
+
182
+ **Step 3: Run targeted tests**
183
+
184
+ Run: `npx vitest run tests/extension/workflow-monitor/state-persistence.test.ts`
185
+ Expected: PASS
186
+
187
+ **Step 4: Verify no unintended regressions in reconstruction logic**
188
+
189
+ Confirm the existing session-entry reconstruction behavior still works when no file exists.
190
+
191
+ **Step 5: Commit**
192
+
193
+ ```bash
194
+ git add extensions/workflow-monitor.ts tests/extension/workflow-monitor/state-persistence.test.ts
195
+ git commit -m "refactor: rename workflow state file"
196
+ ```
197
+
198
+ ### Task 5: Update user-facing docs for the new workflow-next contract
199
+
200
+ **Type:** non-code
201
+
202
+ **Files:**
203
+ - Modify: `README.md`
204
+ - Modify: `docs/developer-usage-guide.md`
205
+ - Modify: `docs/workflow-phases.md`
206
+
207
+ **Acceptance criteria:**
208
+ - Criterion 1: `/workflow-next` docs describe immediate-next-only handoff semantics.
209
+ - Criterion 2: docs mention that the command preserves prior completed workflow history for the same feature.
210
+ - Criterion 3: docs do not claim arbitrary phase jumps are supported.
211
+
212
+ **Implementation notes:**
213
+ - Keep examples aligned with allowed transitions only.
214
+ - Mention the stricter behavior near existing `/workflow-next` examples rather than adding a long new section.
215
+ - If the local state file is mentioned anywhere, rename it to `.pi/workflow-kit-state.json`.
216
+
217
+ **Verification:**
218
+ - Review each acceptance criterion one-by-one.
219
+ - Confirm wording matches the implemented behavior and test coverage.
220
+
221
+ ### Task 6: Run focused verification and capture final status
222
+
223
+ **Type:** code
224
+ **TDD scenario:** Trivial change — use judgment
225
+
226
+ **Files:**
227
+ - Modify: `docs/plans/2026-04-09-workflow-next-handoff-state-implementation.md`
228
+ - Test: `tests/extension/workflow-monitor/workflow-next-command.test.ts`
229
+ - Test: `tests/extension/workflow-monitor/state-persistence.test.ts`
230
+
231
+ **Step 1: Run focused verification**
232
+
233
+ Run:
234
+ - `npx vitest run tests/extension/workflow-monitor/workflow-next-command.test.ts`
235
+ - `npx vitest run tests/extension/workflow-monitor/state-persistence.test.ts`
236
+
237
+ Expected: PASS
238
+
239
+ **Step 2: Run a broader confidence check**
240
+
241
+ Run: `npx vitest run tests/extension/workflow-monitor`
242
+ Expected: PASS
243
+
244
+ **Step 3: Update the implementation plan artifact with verification notes if useful**
245
+
246
+ Add a short note under the plan or in a small completion section summarizing which test commands passed.
247
+
248
+ **Step 4: Commit**
249
+
250
+ ```bash
251
+ git add docs/plans/2026-04-09-workflow-next-handoff-state-implementation.md
252
+ git commit -m "test: verify workflow-next handoff changes"
253
+ ```
@@ -153,6 +153,8 @@ interface SingleResult {
153
153
  stderr: string;
154
154
  usage: UsageStats;
155
155
  model?: string;
156
+ modelProvider?: string;
157
+ modelSource?: "agent" | "parent" | "default";
156
158
  stopReason?: string;
157
159
  errorMessage?: string;
158
160
  step?: number;
@@ -165,6 +167,32 @@ interface SubagentDetails {
165
167
  results: SingleResult[];
166
168
  }
167
169
 
170
+ interface ParentModelInfo {
171
+ id: string;
172
+ provider: string;
173
+ }
174
+
175
+ interface ResolvedModelSelection {
176
+ model: string;
177
+ provider?: string;
178
+ source: "agent" | "parent" | "default";
179
+ }
180
+
181
+ function resolveModelSelection(
182
+ agentModel: string | undefined,
183
+ parentModel: ParentModelInfo | undefined,
184
+ ): ResolvedModelSelection {
185
+ if (agentModel) {
186
+ return { model: agentModel, provider: undefined, source: "agent" };
187
+ }
188
+
189
+ if (parentModel?.id) {
190
+ return { model: parentModel.id, provider: parentModel.provider, source: "parent" };
191
+ }
192
+
193
+ return { model: DEFAULT_MODEL, provider: undefined, source: "default" };
194
+ }
195
+
168
196
  function getFinalOutput(messages: Message[]): string {
169
197
  for (let i = messages.length - 1; i >= 0; i--) {
170
198
  const msg = messages[i];
@@ -177,6 +205,36 @@ function getFinalOutput(messages: Message[]): string {
177
205
  return "";
178
206
  }
179
207
 
208
+ function buildModelArgs(selection: ResolvedModelSelection): string[] {
209
+ const args: string[] = [];
210
+ if (selection.provider) args.push("--provider", selection.provider);
211
+ args.push("--model", selection.model);
212
+ return args;
213
+ }
214
+
215
+ function formatModelSelection(
216
+ result: Pick<SingleResult, "model" | "modelProvider" | "modelSource">,
217
+ ): string | undefined {
218
+ if (!result.model) return undefined;
219
+ const modelLabel = result.modelProvider ? `${result.modelProvider}/${result.model}` : result.model;
220
+ switch (result.modelSource) {
221
+ case "parent":
222
+ return `${modelLabel} (inherited from parent session)`;
223
+ case "agent":
224
+ return `${modelLabel} (pinned by agent config)`;
225
+ case "default":
226
+ return `${modelLabel} (default fallback)`;
227
+ default:
228
+ return modelLabel;
229
+ }
230
+ }
231
+
232
+ function buildFailureMessage(prefix: string, result: SingleResult): string {
233
+ const errorMsg = result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
234
+ const modelSelection = formatModelSelection(result);
235
+ return modelSelection ? `${prefix}: ${errorMsg}\nModel: ${modelSelection}` : `${prefix}: ${errorMsg}`;
236
+ }
237
+
180
238
  // biome-ignore lint/suspicious/noExplicitAny: pi SDK message content type
181
239
  type DisplayItem = { type: "text"; text: string } | { type: "toolCall"; name: string; args: Record<string, any> };
182
240
 
@@ -227,7 +285,7 @@ function collectSummary(messages: Message[]): { filesChanged: string[]; testsRan
227
285
  return { filesChanged: Array.from(files), testsRan };
228
286
  }
229
287
 
230
- export const __internal = { collectSummary };
288
+ export const __internal = { collectSummary, resolveModelSelection };
231
289
 
232
290
  async function mapWithConcurrencyLimit<TIn, TOut>(
233
291
  items: TIn[],
@@ -265,6 +323,7 @@ async function runSingleAgent(
265
323
  task: string,
266
324
  cwd: string | undefined,
267
325
  step: number | undefined,
326
+ parentModel: ParentModelInfo | undefined,
268
327
  signal: AbortSignal | undefined,
269
328
  onUpdate: OnUpdateCallback | undefined,
270
329
  makeDetails: (results: SingleResult[]) => SubagentDetails,
@@ -288,8 +347,8 @@ async function runSingleAgent(
288
347
  }
289
348
 
290
349
  const args: string[] = ["--mode", "json", "-p", "--no-session"];
291
- if (agent.model) args.push("--model", agent.model);
292
- else args.push("--model", DEFAULT_MODEL);
350
+ const selectedModel = resolveModelSelection(agent.model, parentModel);
351
+ args.push(...buildModelArgs(selectedModel));
293
352
  if (agent.tools && agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
294
353
  if (agent.extensions) {
295
354
  for (const ext of agent.extensions) {
@@ -307,7 +366,9 @@ async function runSingleAgent(
307
366
  messages: [],
308
367
  stderr: "",
309
368
  usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
310
- model: agent.model,
369
+ model: selectedModel.model,
370
+ modelProvider: selectedModel.provider,
371
+ modelSource: selectedModel.source,
311
372
  step,
312
373
  };
313
374
 
@@ -596,6 +657,7 @@ export default function (pi: ExtensionAPI) {
596
657
  projectAgentsDir: discovery.projectAgentsDir,
597
658
  results,
598
659
  });
660
+ const parentModel = ctx.model ? { id: ctx.model.id, provider: ctx.model.provider } : undefined;
599
661
 
600
662
  if (modeCount !== 1) {
601
663
  const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
@@ -665,6 +727,7 @@ export default function (pi: ExtensionAPI) {
665
727
  taskWithContext,
666
728
  step.cwd,
667
729
  i + 1,
730
+ parentModel,
668
731
  signal,
669
732
  chainUpdate,
670
733
  makeDetails("chain"),
@@ -675,9 +738,10 @@ export default function (pi: ExtensionAPI) {
675
738
 
676
739
  const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
677
740
  if (isError) {
678
- const errorMsg = result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
679
741
  return {
680
- content: [{ type: "text", text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }],
742
+ content: [
743
+ { type: "text", text: buildFailureMessage(`Chain stopped at step ${i + 1} (${step.agent})`, result) },
744
+ ],
681
745
  details: makeDetails("chain")(results),
682
746
  isError: true,
683
747
  };
@@ -737,6 +801,7 @@ export default function (pi: ExtensionAPI) {
737
801
  t.task,
738
802
  t.cwd,
739
803
  undefined,
804
+ parentModel,
740
805
  signal,
741
806
  // Per-task update callback
742
807
  (partial) => {
@@ -779,6 +844,7 @@ export default function (pi: ExtensionAPI) {
779
844
  params.task,
780
845
  params.cwd,
781
846
  undefined,
847
+ parentModel,
782
848
  signal,
783
849
  onUpdate,
784
850
  makeDetails("single"),
@@ -800,9 +866,8 @@ export default function (pi: ExtensionAPI) {
800
866
  };
801
867
  const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
802
868
  if (isError) {
803
- const errorMsg = result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
804
869
  return {
805
- content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }],
870
+ content: [{ type: "text", text: buildFailureMessage(`Agent ${result.stopReason || "failed"}`, result) }],
806
871
  details: stableDetails,
807
872
  isError: true,
808
873
  };