@tianhai/pi-workflow-kit 0.4.1 → 0.5.1
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-cleanup-legacy-state-and-enforce-think-phases-design.md +56 -0
- package/docs/plans/completed/2026-04-09-cleanup-legacy-state-and-enforce-think-phases-implementation.md +196 -0
- 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/constants.ts +6 -0
- package/extensions/plan-tracker.ts +7 -1
- 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/workflow-tracker.ts +28 -6
- package/extensions/workflow-monitor.ts +64 -77
- package/package.json +1 -1
|
@@ -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
|
+
```
|
package/extensions/constants.ts
CHANGED
|
@@ -7,3 +7,9 @@
|
|
|
7
7
|
* This tool id intentionally remains unchanged across the rebrand.
|
|
8
8
|
*/
|
|
9
9
|
export const PLAN_TRACKER_TOOL_NAME = "plan_tracker";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Custom entry type written by workflow-monitor's /workflow-reset so that
|
|
13
|
+
* plan-tracker's reconstructState picks up an empty task list.
|
|
14
|
+
*/
|
|
15
|
+
export const PLAN_TRACKER_CLEARED_TYPE = "plan_tracker_cleared";
|
|
@@ -10,7 +10,7 @@ import { StringEnum } from "@mariozechner/pi-ai";
|
|
|
10
10
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
|
|
11
11
|
import { Text } from "@mariozechner/pi-tui";
|
|
12
12
|
import { type Static, Type } from "@sinclair/typebox";
|
|
13
|
-
import { PLAN_TRACKER_TOOL_NAME } from "./constants.js";
|
|
13
|
+
import { PLAN_TRACKER_CLEARED_TYPE, PLAN_TRACKER_TOOL_NAME } from "./constants.js";
|
|
14
14
|
|
|
15
15
|
export type TaskStatus = "pending" | "in_progress" | "complete" | "blocked";
|
|
16
16
|
export type TaskPhase =
|
|
@@ -208,6 +208,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
208
208
|
const entries = ctx.sessionManager.getBranch();
|
|
209
209
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
210
210
|
const entry = entries[i];
|
|
211
|
+
// Check for explicit clear signal (written by /workflow-reset)
|
|
212
|
+
// biome-ignore lint/suspicious/noExplicitAny: pi SDK session entry type
|
|
213
|
+
if (entry.type === "custom" && (entry as any).customType === PLAN_TRACKER_CLEARED_TYPE) {
|
|
214
|
+
tasks = [];
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
211
217
|
if (entry.type !== "message") continue;
|
|
212
218
|
const msg = entry.message;
|
|
213
219
|
if (msg.role !== "toolResult" || msg.toolName !== PLAN_TRACKER_TOOL_NAME) continue;
|
|
@@ -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
|
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { AutocompleteItem } from "@mariozechner/pi-tui";
|
|
4
|
+
|
|
5
|
+
const WORKFLOW_NEXT_PHASES = ["brainstorm", "plan", "execute", "finalize"] as const;
|
|
6
|
+
const ARTIFACT_SUFFIX_BY_PHASE = {
|
|
7
|
+
brainstorm: null,
|
|
8
|
+
plan: "-design.md",
|
|
9
|
+
execute: "-implementation.md",
|
|
10
|
+
finalize: "-implementation.md",
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
type WorkflowNextPhase = (typeof WORKFLOW_NEXT_PHASES)[number];
|
|
14
|
+
|
|
15
|
+
function getPhaseCompletions(prefix: string): AutocompleteItem[] | null {
|
|
16
|
+
const normalized = prefix.replace(/^\s+/, "");
|
|
17
|
+
const firstToken = normalized.split(/\s+/, 1)[0] ?? "";
|
|
18
|
+
const completingFirstArg = normalized.length === 0 || !/\s/.test(normalized);
|
|
19
|
+
|
|
20
|
+
if (completingFirstArg || !WORKFLOW_NEXT_PHASES.includes(firstToken as WorkflowNextPhase)) {
|
|
21
|
+
const phasePrefix = completingFirstArg ? normalized : firstToken;
|
|
22
|
+
const items = WORKFLOW_NEXT_PHASES.filter((phase) => phase.startsWith(phasePrefix)).map((phase) => ({
|
|
23
|
+
value: phase,
|
|
24
|
+
label: phase,
|
|
25
|
+
}));
|
|
26
|
+
return items.length > 0 ? items : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function listArtifactsForPhase(phase: WorkflowNextPhase, typedPrefix: string): AutocompleteItem[] | null {
|
|
33
|
+
const suffix = ARTIFACT_SUFFIX_BY_PHASE[phase];
|
|
34
|
+
if (!suffix) return null;
|
|
35
|
+
|
|
36
|
+
const plansDir = path.join(process.cwd(), "docs", "plans");
|
|
37
|
+
if (!fs.existsSync(plansDir)) return null;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const items = fs
|
|
41
|
+
.readdirSync(plansDir)
|
|
42
|
+
.filter((name) => name.endsWith(suffix))
|
|
43
|
+
.map((name) => path.join("docs", "plans", name))
|
|
44
|
+
.filter((relPath) => relPath.startsWith(typedPrefix))
|
|
45
|
+
.map((relPath) => ({ value: `${phase} ${relPath}`, label: relPath }));
|
|
46
|
+
|
|
47
|
+
return items.length > 0 ? items : null;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function getWorkflowNextCompletions(prefix: string): Promise<AutocompleteItem[] | null> {
|
|
54
|
+
const phaseCompletions = getPhaseCompletions(prefix);
|
|
55
|
+
if (phaseCompletions) return phaseCompletions;
|
|
56
|
+
|
|
57
|
+
const normalized = prefix.replace(/^\s+/, "");
|
|
58
|
+
const match = normalized.match(/^(\S+)(?:\s+(.*))?$/);
|
|
59
|
+
const phase = match?.[1] as WorkflowNextPhase | undefined;
|
|
60
|
+
const artifactPrefix = match?.[2] ?? "";
|
|
61
|
+
const startingSecondArg = /\s$/.test(prefix) || artifactPrefix.length > 0;
|
|
62
|
+
|
|
63
|
+
if (phase && WORKFLOW_NEXT_PHASES.includes(phase) && startingSecondArg) {
|
|
64
|
+
return listArtifactsForPhase(phase, artifactPrefix);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helper functions for /workflow-next handoff validation and derived state.
|
|
3
|
+
*
|
|
4
|
+
* These functions have no side effects and no dependencies on the extension runtime,
|
|
5
|
+
* making them straightforward to test and reason about.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type Phase, type PhaseStatus, WORKFLOW_PHASES, type WorkflowTrackerState } from "./workflow-tracker";
|
|
9
|
+
|
|
10
|
+
/** Map of each phase to its immediate next phase (null for finalize). */
|
|
11
|
+
const NEXT_PHASE: Record<Phase, Phase | null> = {
|
|
12
|
+
brainstorm: "plan",
|
|
13
|
+
plan: "execute",
|
|
14
|
+
execute: "finalize",
|
|
15
|
+
finalize: null,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validate whether a `/workflow-next` request is allowed.
|
|
20
|
+
*
|
|
21
|
+
* Rules:
|
|
22
|
+
* - A current phase must exist in the workflow state.
|
|
23
|
+
* - The current phase must have status exactly "complete".
|
|
24
|
+
* - The requested phase must be the immediate next phase.
|
|
25
|
+
*
|
|
26
|
+
* Returns `null` if the handoff is valid, or an error message string.
|
|
27
|
+
*/
|
|
28
|
+
export function validateNextWorkflowPhase(currentState: WorkflowTrackerState, requestedPhase: Phase): string | null {
|
|
29
|
+
const current = currentState.currentPhase;
|
|
30
|
+
|
|
31
|
+
if (!current) {
|
|
32
|
+
return "No workflow phase is active. Start a workflow first or use /workflow-reset.";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const next = NEXT_PHASE[current];
|
|
36
|
+
if (next === null) {
|
|
37
|
+
return `Cannot hand off: ${current} is the final phase. Use /workflow-reset for a new task.`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const currentStatus = currentState.phases[current];
|
|
41
|
+
|
|
42
|
+
// Same-phase handoff
|
|
43
|
+
if (requestedPhase === current) {
|
|
44
|
+
return `Cannot hand off to ${requestedPhase} from ${current}. Use /workflow-reset for a new task or continue in this session.`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Backward handoff
|
|
48
|
+
const currentIdx = WORKFLOW_PHASES.indexOf(current);
|
|
49
|
+
const requestedIdx = WORKFLOW_PHASES.indexOf(requestedPhase);
|
|
50
|
+
if (requestedIdx < currentIdx) {
|
|
51
|
+
return `Cannot hand off to ${requestedPhase} from ${current}: backward transitions are not allowed.`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Current phase not complete
|
|
55
|
+
if (currentStatus !== "complete") {
|
|
56
|
+
return `Cannot hand off to ${requestedPhase} because ${current} is not complete (status: ${currentStatus}).`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Direct jump (skipping intermediate phases)
|
|
60
|
+
if (requestedPhase !== next) {
|
|
61
|
+
return `Cannot hand off to ${requestedPhase} from ${current}. /workflow-next only supports the immediate next phase: ${next}.`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Derive the workflow state snapshot for a new session created by `/workflow-next`.
|
|
69
|
+
*
|
|
70
|
+
* Rules:
|
|
71
|
+
* - All phases before the requested phase are marked "complete".
|
|
72
|
+
* - The requested phase is marked "active".
|
|
73
|
+
* - All phases after the requested phase are marked "pending".
|
|
74
|
+
* - currentPhase is set to the requested phase.
|
|
75
|
+
* - Artifacts and prompted flags are preserved for earlier phases.
|
|
76
|
+
*/
|
|
77
|
+
export function deriveWorkflowHandoffState(
|
|
78
|
+
currentState: WorkflowTrackerState,
|
|
79
|
+
requestedPhase: Phase,
|
|
80
|
+
): WorkflowTrackerState {
|
|
81
|
+
const requestedIdx = WORKFLOW_PHASES.indexOf(requestedPhase);
|
|
82
|
+
|
|
83
|
+
const newPhases = { ...currentState.phases };
|
|
84
|
+
const newArtifacts = { ...currentState.artifacts };
|
|
85
|
+
const newPrompted = { ...currentState.prompted };
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < WORKFLOW_PHASES.length; i++) {
|
|
88
|
+
const phase = WORKFLOW_PHASES[i]!;
|
|
89
|
+
|
|
90
|
+
if (i < requestedIdx) {
|
|
91
|
+
// Earlier phases: mark complete, preserve artifacts/prompted
|
|
92
|
+
newPhases[phase] = "complete";
|
|
93
|
+
} else if (i === requestedIdx) {
|
|
94
|
+
// Target phase: active
|
|
95
|
+
newPhases[phase] = "active";
|
|
96
|
+
newArtifacts[phase] = currentState.artifacts[phase] ?? null;
|
|
97
|
+
newPrompted[phase] = false;
|
|
98
|
+
} else {
|
|
99
|
+
// Later phases: pending, clear artifacts/prompted
|
|
100
|
+
newPhases[phase] = "pending";
|
|
101
|
+
newArtifacts[phase] = null;
|
|
102
|
+
newPrompted[phase] = false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
phases: newPhases as Record<Phase, PhaseStatus>,
|
|
108
|
+
currentPhase: requestedPhase,
|
|
109
|
+
artifacts: newArtifacts as Record<Phase, string | null>,
|
|
110
|
+
prompted: newPrompted as Record<Phase, boolean>,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -194,15 +194,37 @@ export class WorkflowTracker {
|
|
|
194
194
|
if (!PLANS_DIR_RE.test(path)) return false;
|
|
195
195
|
|
|
196
196
|
if (DESIGN_RE.test(path)) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
// Only advance if we haven't already passed the brainstorm phase.
|
|
198
|
+
// Writing a design doc during plan/execute/finalize (e.g., updating
|
|
199
|
+
// the plan) must NOT reset workflow state.
|
|
200
|
+
const curIdx = this.state.currentPhase ? WORKFLOW_PHASES.indexOf(this.state.currentPhase) : -1;
|
|
201
|
+
if (curIdx > WORKFLOW_PHASES.indexOf("brainstorm")) {
|
|
202
|
+
return this.recordArtifact("brainstorm", path);
|
|
203
|
+
}
|
|
204
|
+
let changed = false;
|
|
205
|
+
changed = this.recordArtifact("brainstorm", path) || changed;
|
|
206
|
+
// Activating and immediately completing: the design doc is the
|
|
207
|
+
// deliverable that signals brainstorm is done. Do NOT mark prompted
|
|
208
|
+
// so the agent_end boundary prompt still fires to offer session handoff.
|
|
209
|
+
changed = this.advanceTo("brainstorm") || changed;
|
|
210
|
+
changed = this.completeCurrent() || changed;
|
|
211
|
+
return changed;
|
|
200
212
|
}
|
|
201
213
|
|
|
202
214
|
if (IMPLEMENTATION_RE.test(path)) {
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
215
|
+
// Only advance if we haven't already passed the plan phase.
|
|
216
|
+
const curIdx = this.state.currentPhase ? WORKFLOW_PHASES.indexOf(this.state.currentPhase) : -1;
|
|
217
|
+
if (curIdx > WORKFLOW_PHASES.indexOf("plan")) {
|
|
218
|
+
return this.recordArtifact("plan", path);
|
|
219
|
+
}
|
|
220
|
+
let changed = false;
|
|
221
|
+
changed = this.recordArtifact("plan", path) || changed;
|
|
222
|
+
// Activating and immediately completing: the implementation plan
|
|
223
|
+
// is the deliverable that signals plan phase is done. Do NOT mark
|
|
224
|
+
// prompted so the agent_end boundary prompt still fires.
|
|
225
|
+
changed = this.advanceTo("plan") || changed;
|
|
226
|
+
changed = this.completeCurrent() || changed;
|
|
227
|
+
return changed;
|
|
206
228
|
}
|
|
207
229
|
|
|
208
230
|
return false;
|