@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.
- package/README.md +29 -5
- package/docs/developer-usage-guide.md +3 -4
- package/docs/plans/completed/2026-04-09-workflow-next-autocomplete-design.md +185 -0
- package/docs/plans/completed/2026-04-09-workflow-next-autocomplete-implementation.md +334 -0
- package/docs/plans/completed/2026-04-09-workflow-next-handoff-state-design.md +251 -0
- package/docs/plans/completed/2026-04-09-workflow-next-handoff-state-implementation.md +253 -0
- package/extensions/subagent/index.ts +73 -8
- package/extensions/workflow-monitor/workflow-next-completions.ts +68 -0
- package/extensions/workflow-monitor/workflow-next-state.ts +112 -0
- package/extensions/workflow-monitor.ts +57 -6
- package/package.json +1 -1
|
@@ -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
|
-
|
|
292
|
-
|
|
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:
|
|
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: [
|
|
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"}
|
|
870
|
+
content: [{ type: "text", text: buildFailureMessage(`Agent ${result.stopReason || "failed"}`, result) }],
|
|
806
871
|
details: stableDetails,
|
|
807
872
|
isError: true,
|
|
808
873
|
};
|