@tianhai/pi-workflow-kit 0.5.3 → 0.6.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 +44 -494
- package/docs/developer-usage-guide.md +41 -401
- package/docs/oversight-model.md +13 -34
- package/docs/workflow-phases.md +32 -46
- package/extensions/workflow-guard.ts +67 -0
- package/package.json +3 -7
- package/skills/brainstorming/SKILL.md +16 -67
- package/skills/executing-tasks/SKILL.md +26 -227
- package/skills/finalizing/SKILL.md +33 -0
- package/skills/writing-plans/SKILL.md +23 -132
- package/ROADMAP.md +0 -16
- package/agents/code-reviewer.md +0 -18
- package/agents/config.ts +0 -5
- package/agents/implementer.md +0 -26
- package/agents/spec-reviewer.md +0 -13
- package/agents/worker.md +0 -17
- package/docs/plans/2026-04-10-brainstorming-boundary-enforcement-design.md +0 -60
- package/docs/plans/completed/2026-04-09-cleanup-legacy-state-and-enforce-think-phases-design.md +0 -56
- package/docs/plans/completed/2026-04-09-cleanup-legacy-state-and-enforce-think-phases-implementation.md +0 -196
- package/docs/plans/completed/2026-04-09-workflow-next-autocomplete-design.md +0 -185
- package/docs/plans/completed/2026-04-09-workflow-next-autocomplete-implementation.md +0 -334
- package/docs/plans/completed/2026-04-09-workflow-next-handoff-state-design.md +0 -251
- package/docs/plans/completed/2026-04-09-workflow-next-handoff-state-implementation.md +0 -253
- package/extensions/constants.ts +0 -15
- package/extensions/lib/logging.ts +0 -138
- package/extensions/plan-tracker.ts +0 -508
- package/extensions/subagent/agents.ts +0 -144
- package/extensions/subagent/concurrency.ts +0 -52
- package/extensions/subagent/env.ts +0 -47
- package/extensions/subagent/index.ts +0 -1181
- package/extensions/subagent/lifecycle.ts +0 -25
- package/extensions/subagent/timeout.ts +0 -13
- package/extensions/workflow-monitor/debug-monitor.ts +0 -98
- package/extensions/workflow-monitor/git.ts +0 -31
- package/extensions/workflow-monitor/heuristics.ts +0 -58
- package/extensions/workflow-monitor/investigation.ts +0 -52
- package/extensions/workflow-monitor/reference-tool.ts +0 -42
- package/extensions/workflow-monitor/skip-confirmation.ts +0 -19
- package/extensions/workflow-monitor/tdd-monitor.ts +0 -137
- package/extensions/workflow-monitor/test-runner.ts +0 -37
- package/extensions/workflow-monitor/verification-monitor.ts +0 -61
- package/extensions/workflow-monitor/warnings.ts +0 -81
- package/extensions/workflow-monitor/workflow-handler.ts +0 -363
- package/extensions/workflow-monitor/workflow-next-completions.ts +0 -68
- package/extensions/workflow-monitor/workflow-next-state.ts +0 -112
- package/extensions/workflow-monitor/workflow-tracker.ts +0 -286
- package/extensions/workflow-monitor/workflow-transitions.ts +0 -88
- package/extensions/workflow-monitor.ts +0 -909
- package/skills/dispatching-parallel-agents/SKILL.md +0 -194
- package/skills/receiving-code-review/SKILL.md +0 -196
- package/skills/systematic-debugging/SKILL.md +0 -170
- package/skills/systematic-debugging/condition-based-waiting-example.ts +0 -158
- package/skills/systematic-debugging/condition-based-waiting.md +0 -115
- package/skills/systematic-debugging/defense-in-depth.md +0 -122
- package/skills/systematic-debugging/find-polluter.sh +0 -63
- package/skills/systematic-debugging/reference/rationalizations.md +0 -61
- package/skills/systematic-debugging/root-cause-tracing.md +0 -169
- package/skills/test-driven-development/SKILL.md +0 -266
- package/skills/test-driven-development/reference/examples.md +0 -101
- package/skills/test-driven-development/reference/rationalizations.md +0 -67
- package/skills/test-driven-development/reference/when-stuck.md +0 -33
- package/skills/test-driven-development/testing-anti-patterns.md +0 -299
- package/skills/using-git-worktrees/SKILL.md +0 -231
|
@@ -1,909 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workflow Kit monitor extension.
|
|
3
|
-
*
|
|
4
|
-
* Observes tool_call and tool_result events to:
|
|
5
|
-
* - Track TDD phase (RED→GREEN→REFACTOR) and inject warnings on violations
|
|
6
|
-
* - Track debug fix-fail cycles and inject warnings on investigation skips / thrashing
|
|
7
|
-
* - Show workflow state in TUI widget
|
|
8
|
-
* - Register workflow_reference tool for on-demand reference content
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import * as path from "node:path";
|
|
12
|
-
import { StringEnum } from "@mariozechner/pi-ai";
|
|
13
|
-
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
14
|
-
import { Text } from "@mariozechner/pi-tui";
|
|
15
|
-
import { Type } from "@sinclair/typebox";
|
|
16
|
-
import { PLAN_TRACKER_CLEARED_TYPE, PLAN_TRACKER_TOOL_NAME } from "./constants.js";
|
|
17
|
-
import type { PlanTrackerDetails } from "./plan-tracker.js";
|
|
18
|
-
import { getCurrentGitRef } from "./workflow-monitor/git";
|
|
19
|
-
import { loadReference, REFERENCE_TOPICS } from "./workflow-monitor/reference-tool";
|
|
20
|
-
import { getUnresolvedPhases, getUnresolvedPhasesBefore } from "./workflow-monitor/skip-confirmation";
|
|
21
|
-
import type { VerificationViolation } from "./workflow-monitor/verification-monitor";
|
|
22
|
-
import {
|
|
23
|
-
type DebugViolationType,
|
|
24
|
-
getDebugViolationWarning,
|
|
25
|
-
getTddViolationWarning,
|
|
26
|
-
getVerificationViolationWarning,
|
|
27
|
-
} from "./workflow-monitor/warnings";
|
|
28
|
-
import {
|
|
29
|
-
createWorkflowHandler,
|
|
30
|
-
DEBUG_DEFAULTS,
|
|
31
|
-
TDD_DEFAULTS,
|
|
32
|
-
VERIFICATION_DEFAULTS,
|
|
33
|
-
type Violation,
|
|
34
|
-
type WorkflowHandler,
|
|
35
|
-
} from "./workflow-monitor/workflow-handler";
|
|
36
|
-
import { getWorkflowNextCompletions } from "./workflow-monitor/workflow-next-completions";
|
|
37
|
-
import { deriveWorkflowHandoffState, validateNextWorkflowPhase } from "./workflow-monitor/workflow-next-state";
|
|
38
|
-
import {
|
|
39
|
-
computeBoundaryToPrompt,
|
|
40
|
-
type Phase,
|
|
41
|
-
parseSkillName,
|
|
42
|
-
resolveSkillPhase,
|
|
43
|
-
type TransitionBoundary,
|
|
44
|
-
WORKFLOW_PHASES,
|
|
45
|
-
WORKFLOW_TRACKER_ENTRY_TYPE,
|
|
46
|
-
type WorkflowTrackerState,
|
|
47
|
-
} from "./workflow-monitor/workflow-tracker";
|
|
48
|
-
import { getTransitionPrompt, isReviewableBoundary } from "./workflow-monitor/workflow-transitions";
|
|
49
|
-
|
|
50
|
-
type SelectOption<T extends string> = { label: string; value: T };
|
|
51
|
-
|
|
52
|
-
async function selectValue<T extends string>(
|
|
53
|
-
ctx: ExtensionContext,
|
|
54
|
-
title: string,
|
|
55
|
-
options: SelectOption<T>[],
|
|
56
|
-
): Promise<T> {
|
|
57
|
-
const labels = options.map((o) => o.label);
|
|
58
|
-
const pickedLabel = await ctx.ui.select(title, labels);
|
|
59
|
-
const picked = options.find((o) => o.label === pickedLabel);
|
|
60
|
-
return (picked?.value ?? "cancel") as T;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const SUPERPOWERS_STATE_ENTRY_TYPE = "superpowers_state";
|
|
64
|
-
|
|
65
|
-
export function reconstructState(ctx: ExtensionContext, handler: WorkflowHandler) {
|
|
66
|
-
handler.resetState();
|
|
67
|
-
|
|
68
|
-
// Scan session branch for most recent superpowers state entry.
|
|
69
|
-
// The session branch IS the single source of truth — no file-based
|
|
70
|
-
// persistence needed since pi's journal survives restarts and reloads.
|
|
71
|
-
const entries = ctx.sessionManager.getBranch();
|
|
72
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
73
|
-
const entry = entries[i];
|
|
74
|
-
// biome-ignore lint/suspicious/noExplicitAny: pi SDK session entry type
|
|
75
|
-
if (entry.type === "custom" && (entry as any).customType === SUPERPOWERS_STATE_ENTRY_TYPE) {
|
|
76
|
-
// biome-ignore lint/suspicious/noExplicitAny: pi SDK session entry type
|
|
77
|
-
handler.setFullState((entry as any).data);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
// Migration fallback: old-format workflow-only entries
|
|
81
|
-
// biome-ignore lint/suspicious/noExplicitAny: pi SDK session entry type
|
|
82
|
-
if (entry.type === "custom" && (entry as any).customType === WORKFLOW_TRACKER_ENTRY_TYPE) {
|
|
83
|
-
// biome-ignore lint/suspicious/noExplicitAny: pi SDK session entry type
|
|
84
|
-
handler.setFullState({ workflow: (entry as any).data });
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// No entries found — reset to fresh defaults
|
|
90
|
-
handler.setFullState({});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export default function (pi: ExtensionAPI) {
|
|
94
|
-
const handler = createWorkflowHandler();
|
|
95
|
-
|
|
96
|
-
// Pending warnings are keyed by toolCallId to avoid cross-call leakage when
|
|
97
|
-
// tool results are interleaved.
|
|
98
|
-
const pendingViolations = new Map<string, Violation>();
|
|
99
|
-
const pendingVerificationViolations = new Map<string, VerificationViolation>();
|
|
100
|
-
const pendingBranchGates = new Map<string, string>();
|
|
101
|
-
|
|
102
|
-
type ViolationBucket = "practice";
|
|
103
|
-
const strikes: Record<ViolationBucket, number> = { practice: 0 };
|
|
104
|
-
const sessionAllowed: Partial<Record<ViolationBucket, boolean>> = {};
|
|
105
|
-
|
|
106
|
-
async function maybeEscalate(bucket: ViolationBucket, ctx: ExtensionContext): Promise<"allow" | "block"> {
|
|
107
|
-
if (!ctx.hasUI) return "allow";
|
|
108
|
-
if (sessionAllowed[bucket]) return "allow";
|
|
109
|
-
|
|
110
|
-
strikes[bucket] += 1;
|
|
111
|
-
if (strikes[bucket] < 2) return "allow";
|
|
112
|
-
|
|
113
|
-
const choice = await ctx.ui.select(
|
|
114
|
-
`The agent has repeatedly violated ${bucket} guardrails. Allow it to continue?`,
|
|
115
|
-
["Yes, continue", "Yes, allow all for this session", "No, stop"],
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
if (choice === "Yes, continue") {
|
|
119
|
-
strikes[bucket] = 0;
|
|
120
|
-
return "allow";
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (choice === "Yes, allow all for this session") {
|
|
124
|
-
sessionAllowed[bucket] = true;
|
|
125
|
-
return "allow";
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return "block";
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
let branchNoticeShown = false;
|
|
132
|
-
let branchConfirmed = false;
|
|
133
|
-
|
|
134
|
-
const persistState = () => {
|
|
135
|
-
const stateWithTimestamp = { ...handler.getFullState(), savedAt: Date.now() };
|
|
136
|
-
pi.appendEntry(SUPERPOWERS_STATE_ENTRY_TYPE, stateWithTimestamp);
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const phaseToSkill: Record<string, string> = {
|
|
140
|
-
brainstorm: "brainstorming",
|
|
141
|
-
plan: "writing-plans",
|
|
142
|
-
execute: "executing-tasks",
|
|
143
|
-
finalize: "executing-tasks",
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
function parseTargetPhase(text: string): Phase | null {
|
|
147
|
-
const lines = text.split(/\r?\n/);
|
|
148
|
-
let furthest: Phase | null = null;
|
|
149
|
-
let furthestIdx = -1;
|
|
150
|
-
const workflowState = handler.getWorkflowState();
|
|
151
|
-
|
|
152
|
-
for (const line of lines) {
|
|
153
|
-
const skill = parseSkillName(line);
|
|
154
|
-
if (!skill) continue;
|
|
155
|
-
const phase = resolveSkillPhase(skill, workflowState);
|
|
156
|
-
if (!phase) continue;
|
|
157
|
-
const idx = WORKFLOW_PHASES.indexOf(phase);
|
|
158
|
-
if (idx > furthestIdx) {
|
|
159
|
-
furthest = phase;
|
|
160
|
-
furthestIdx = idx;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return furthest;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const boundaryToPhase: Record<TransitionBoundary, keyof typeof phaseToSkill> = {
|
|
168
|
-
design_reviewable: "brainstorm",
|
|
169
|
-
plan_reviewable: "plan",
|
|
170
|
-
design_committed: "brainstorm",
|
|
171
|
-
plan_ready: "plan",
|
|
172
|
-
execution_complete: "execute",
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
// --- State reconstruction on session events ---
|
|
176
|
-
function resetSessionState(ctx: ExtensionContext) {
|
|
177
|
-
reconstructState(ctx, handler);
|
|
178
|
-
pendingViolations.clear();
|
|
179
|
-
pendingVerificationViolations.clear();
|
|
180
|
-
pendingBranchGates.clear();
|
|
181
|
-
strikes.practice = 0;
|
|
182
|
-
delete sessionAllowed.practice;
|
|
183
|
-
branchNoticeShown = false;
|
|
184
|
-
branchConfirmed = false;
|
|
185
|
-
updateWidget(ctx);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// session_start covers startup, reload, new, resume, fork (pi v0.65.0+)
|
|
189
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
190
|
-
resetSessionState(ctx);
|
|
191
|
-
});
|
|
192
|
-
// session_tree for /tree navigation where a different session branch is loaded
|
|
193
|
-
pi.on("session_tree", async (_event, ctx) => {
|
|
194
|
-
resetSessionState(ctx);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
// --- Input observation (skill detection + skip-confirmation gate) ---
|
|
198
|
-
pi.on("input", async (event, ctx) => {
|
|
199
|
-
if (event.source === "extension") return;
|
|
200
|
-
const text = (event.text as string | undefined) ?? (event.input as string | undefined) ?? "";
|
|
201
|
-
|
|
202
|
-
const targetPhase = parseTargetPhase(text);
|
|
203
|
-
|
|
204
|
-
// If no UI or no target phase, just track and proceed
|
|
205
|
-
if (!ctx.hasUI || !targetPhase) {
|
|
206
|
-
if (handler.handleInputText(text)) {
|
|
207
|
-
persistState();
|
|
208
|
-
updateWidget(ctx);
|
|
209
|
-
}
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const currentState = handler.getWorkflowState();
|
|
214
|
-
if (!currentState) {
|
|
215
|
-
if (handler.handleInputText(text)) {
|
|
216
|
-
persistState();
|
|
217
|
-
updateWidget(ctx);
|
|
218
|
-
}
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const unresolved = getUnresolvedPhasesBefore(targetPhase, currentState);
|
|
223
|
-
|
|
224
|
-
if (unresolved.length === 0) {
|
|
225
|
-
if (handler.handleInputText(text)) {
|
|
226
|
-
persistState();
|
|
227
|
-
updateWidget(ctx);
|
|
228
|
-
}
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// --- Single unresolved phase ---
|
|
233
|
-
if (unresolved.length === 1) {
|
|
234
|
-
const missing = unresolved[0];
|
|
235
|
-
const missingSkill = phaseToSkill[missing] ?? missing;
|
|
236
|
-
const options = [
|
|
237
|
-
{ label: `Do ${missing} now`, value: "do_now" as const },
|
|
238
|
-
{ label: `Mark ${missing} as complete`, value: "mark_complete" as const },
|
|
239
|
-
{ label: `Skip ${missing}`, value: "skip" as const },
|
|
240
|
-
{ label: "Cancel", value: "cancel" as const },
|
|
241
|
-
];
|
|
242
|
-
const choice = await selectValue(ctx, `Phase "${missing}" is unresolved. What would you like to do?`, options);
|
|
243
|
-
|
|
244
|
-
if (choice === "mark_complete") {
|
|
245
|
-
handler.completeWorkflowPhase(missing as Phase);
|
|
246
|
-
handler.handleInputText(text);
|
|
247
|
-
persistState();
|
|
248
|
-
updateWidget(ctx);
|
|
249
|
-
return;
|
|
250
|
-
} else if (choice === "skip") {
|
|
251
|
-
handler.skipWorkflowPhases([missing]);
|
|
252
|
-
handler.handleInputText(text);
|
|
253
|
-
persistState();
|
|
254
|
-
updateWidget(ctx);
|
|
255
|
-
return;
|
|
256
|
-
} else if (choice === "do_now") {
|
|
257
|
-
ctx.ui.setEditorText(`/skill:${missingSkill}`);
|
|
258
|
-
return { blocked: true };
|
|
259
|
-
} else {
|
|
260
|
-
// cancel
|
|
261
|
-
return { blocked: true };
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// --- Multiple unresolved phases ---
|
|
266
|
-
const summaryOptions = [
|
|
267
|
-
{ label: "Review one-by-one", value: "review_individually" as const },
|
|
268
|
-
{ label: "Skip all and continue", value: "skip_all" as const },
|
|
269
|
-
{ label: "Cancel", value: "cancel" as const },
|
|
270
|
-
];
|
|
271
|
-
const summaryChoice = await selectValue(
|
|
272
|
-
ctx,
|
|
273
|
-
`${unresolved.length} phases are unresolved: ${unresolved.join(", ")}. What would you like to do?`,
|
|
274
|
-
summaryOptions,
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
if (summaryChoice === "skip_all") {
|
|
278
|
-
handler.skipWorkflowPhases(unresolved);
|
|
279
|
-
handler.handleInputText(text);
|
|
280
|
-
persistState();
|
|
281
|
-
updateWidget(ctx);
|
|
282
|
-
return;
|
|
283
|
-
} else if (summaryChoice === "cancel") {
|
|
284
|
-
return { blocked: true };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// review_individually: prompt for each
|
|
288
|
-
for (const phase of unresolved) {
|
|
289
|
-
const skill = phaseToSkill[phase] ?? phase;
|
|
290
|
-
const options = [
|
|
291
|
-
{ label: `Do ${phase} now`, value: "do_now" as const },
|
|
292
|
-
{ label: `Mark ${phase} as complete`, value: "mark_complete" as const },
|
|
293
|
-
{ label: `Skip ${phase}`, value: "skip" as const },
|
|
294
|
-
{ label: "Cancel", value: "cancel" as const },
|
|
295
|
-
];
|
|
296
|
-
const choice = await selectValue(ctx, `Phase "${phase}" is unresolved. What would you like to do?`, options);
|
|
297
|
-
|
|
298
|
-
if (choice === "mark_complete") {
|
|
299
|
-
handler.completeWorkflowPhase(phase as Phase);
|
|
300
|
-
persistState();
|
|
301
|
-
updateWidget(ctx);
|
|
302
|
-
} else if (choice === "skip") {
|
|
303
|
-
handler.skipWorkflowPhases([phase]);
|
|
304
|
-
persistState();
|
|
305
|
-
updateWidget(ctx);
|
|
306
|
-
} else if (choice === "do_now") {
|
|
307
|
-
ctx.ui.setEditorText(`/skill:${skill}`);
|
|
308
|
-
return { blocked: true };
|
|
309
|
-
} else {
|
|
310
|
-
// cancel
|
|
311
|
-
return { blocked: true };
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// All individually reviewed (all skipped) - allow transition
|
|
316
|
-
handler.handleInputText(text);
|
|
317
|
-
persistState();
|
|
318
|
-
updateWidget(ctx);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
// --- Completion action gate prompt ---
|
|
322
|
-
// biome-ignore lint/suspicious/noExplicitAny: pi SDK context type
|
|
323
|
-
async function promptCompletionGate(unresolved: Phase[], ctx: any): Promise<"allowed" | "blocked"> {
|
|
324
|
-
if (unresolved.length === 1) {
|
|
325
|
-
const missing = unresolved[0];
|
|
326
|
-
const missingSkill = phaseToSkill[missing] ?? missing;
|
|
327
|
-
const options = [
|
|
328
|
-
{ label: `Do ${missing} now`, value: "do_now" as const },
|
|
329
|
-
{ label: `Mark ${missing} as complete`, value: "mark_complete" as const },
|
|
330
|
-
{ label: `Skip ${missing}`, value: "skip" as const },
|
|
331
|
-
{ label: "Cancel", value: "cancel" as const },
|
|
332
|
-
];
|
|
333
|
-
const choice = await selectValue(ctx, `Phase "${missing}" is unresolved. What would you like to do?`, options);
|
|
334
|
-
|
|
335
|
-
if (choice === "mark_complete") {
|
|
336
|
-
handler.completeWorkflowPhase(missing as Phase);
|
|
337
|
-
persistState();
|
|
338
|
-
updateWidget(ctx);
|
|
339
|
-
return "allowed";
|
|
340
|
-
} else if (choice === "skip") {
|
|
341
|
-
handler.skipWorkflowPhases([missing]);
|
|
342
|
-
persistState();
|
|
343
|
-
updateWidget(ctx);
|
|
344
|
-
return "allowed";
|
|
345
|
-
} else if (choice === "do_now") {
|
|
346
|
-
ctx.ui.setEditorText(`/skill:${missingSkill}`);
|
|
347
|
-
return "blocked";
|
|
348
|
-
} else {
|
|
349
|
-
return "blocked";
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Multiple unresolved
|
|
354
|
-
const summaryOptions = [
|
|
355
|
-
{ label: "Review one-by-one", value: "review_individually" as const },
|
|
356
|
-
{ label: "Skip all and continue", value: "skip_all" as const },
|
|
357
|
-
{ label: "Cancel", value: "cancel" as const },
|
|
358
|
-
];
|
|
359
|
-
const summaryChoice = await selectValue(
|
|
360
|
-
ctx,
|
|
361
|
-
`${unresolved.length} phases are unresolved: ${unresolved.join(", ")}. What would you like to do?`,
|
|
362
|
-
summaryOptions,
|
|
363
|
-
);
|
|
364
|
-
|
|
365
|
-
if (summaryChoice === "skip_all") {
|
|
366
|
-
handler.skipWorkflowPhases(unresolved);
|
|
367
|
-
persistState();
|
|
368
|
-
updateWidget(ctx);
|
|
369
|
-
return "allowed";
|
|
370
|
-
} else if (summaryChoice === "cancel") {
|
|
371
|
-
return "blocked";
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// review_individually
|
|
375
|
-
for (const phase of unresolved) {
|
|
376
|
-
const skill = phaseToSkill[phase] ?? phase;
|
|
377
|
-
const options = [
|
|
378
|
-
{ label: `Do ${phase} now`, value: "do_now" as const },
|
|
379
|
-
{ label: `Mark ${phase} as complete`, value: "mark_complete" as const },
|
|
380
|
-
{ label: `Skip ${phase}`, value: "skip" as const },
|
|
381
|
-
{ label: "Cancel", value: "cancel" as const },
|
|
382
|
-
];
|
|
383
|
-
const choice = await selectValue(ctx, `Phase "${phase}" is unresolved. What would you like to do?`, options);
|
|
384
|
-
|
|
385
|
-
if (choice === "mark_complete") {
|
|
386
|
-
handler.completeWorkflowPhase(phase as Phase);
|
|
387
|
-
persistState();
|
|
388
|
-
updateWidget(ctx);
|
|
389
|
-
} else if (choice === "skip") {
|
|
390
|
-
handler.skipWorkflowPhases([phase]);
|
|
391
|
-
persistState();
|
|
392
|
-
updateWidget(ctx);
|
|
393
|
-
} else if (choice === "do_now") {
|
|
394
|
-
ctx.ui.setEditorText(`/skill:${skill}`);
|
|
395
|
-
return "blocked";
|
|
396
|
-
} else {
|
|
397
|
-
return "blocked";
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return "allowed";
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// --- Completion action detection helpers ---
|
|
405
|
-
const COMMIT_RE = /\bgit\s+commit\b/;
|
|
406
|
-
const PUSH_RE = /\bgit\s+push\b/;
|
|
407
|
-
const PR_RE = /\bgh\s+pr\s+create\b/;
|
|
408
|
-
|
|
409
|
-
function getCompletionActionTarget(command: string): Phase | null {
|
|
410
|
-
if (COMMIT_RE.test(command)) return "finalize";
|
|
411
|
-
if (PUSH_RE.test(command)) return "finalize";
|
|
412
|
-
if (PR_RE.test(command)) return "finalize";
|
|
413
|
-
return null;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function getUnresolvedPhasesForAction(_target: Phase, state: WorkflowTrackerState): Phase[] {
|
|
417
|
-
// For all completion actions, check that finalize is complete
|
|
418
|
-
return getUnresolvedPhases(["finalize"], state);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// --- Tool call observation (detect file writes + verification gate) ---
|
|
422
|
-
pi.on("tool_call", async (event, ctx) => {
|
|
423
|
-
const toolCallId = event.toolCallId;
|
|
424
|
-
|
|
425
|
-
if (event.toolName === "bash") {
|
|
426
|
-
// biome-ignore lint/suspicious/noExplicitAny: pi SDK event input type
|
|
427
|
-
const command = ((event.input as Record<string, any>).command as string | undefined) ?? "";
|
|
428
|
-
|
|
429
|
-
const state = handler.getWorkflowState();
|
|
430
|
-
const phaseIdx = state?.currentPhase ? WORKFLOW_PHASES.indexOf(state.currentPhase) : -1;
|
|
431
|
-
const finalizeIdx = WORKFLOW_PHASES.indexOf("finalize");
|
|
432
|
-
|
|
433
|
-
// Completion action gating (interactive only, finalize phase)
|
|
434
|
-
// Suppress during active plan execution — prompts only fire after execution completes
|
|
435
|
-
const isExecuting = state?.currentPhase === "execute" && state.phases.execute === "active";
|
|
436
|
-
if (ctx.hasUI && state && phaseIdx >= finalizeIdx && !isExecuting) {
|
|
437
|
-
const actionTarget = getCompletionActionTarget(command);
|
|
438
|
-
if (actionTarget) {
|
|
439
|
-
const unresolved = getUnresolvedPhasesForAction(actionTarget, state);
|
|
440
|
-
if (unresolved.length > 0) {
|
|
441
|
-
const gateResult = await promptCompletionGate(unresolved, ctx);
|
|
442
|
-
if (gateResult === "blocked") {
|
|
443
|
-
return { blocked: true };
|
|
444
|
-
}
|
|
445
|
-
if (unresolved.length > 0) {
|
|
446
|
-
handler.recordVerificationWaiver();
|
|
447
|
-
persistState();
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
const executeIdx = WORKFLOW_PHASES.indexOf("execute");
|
|
454
|
-
if (phaseIdx >= executeIdx) {
|
|
455
|
-
const verificationViolation = handler.checkCommitGate(command);
|
|
456
|
-
if (verificationViolation) {
|
|
457
|
-
pendingVerificationViolations.set(toolCallId, verificationViolation);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// biome-ignore lint/suspicious/noExplicitAny: pi SDK event input type
|
|
463
|
-
const input = event.input as Record<string, any>;
|
|
464
|
-
const result = handler.handleToolCall(event.toolName, input);
|
|
465
|
-
if (result.violation) {
|
|
466
|
-
pendingViolations.set(toolCallId, result.violation);
|
|
467
|
-
persistState();
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
let changed = false;
|
|
471
|
-
|
|
472
|
-
if (event.toolName === "write" || event.toolName === "edit") {
|
|
473
|
-
const filePath = input.path as string | undefined;
|
|
474
|
-
if (filePath) {
|
|
475
|
-
const state = handler.getWorkflowState();
|
|
476
|
-
const phase = state?.currentPhase;
|
|
477
|
-
const isThinkingPhase = phase === "brainstorm" || phase === "plan";
|
|
478
|
-
let normalizedForCheck = filePath;
|
|
479
|
-
if (normalizedForCheck.startsWith("./")) normalizedForCheck = normalizedForCheck.slice(2);
|
|
480
|
-
const resolved = path.resolve(process.cwd(), normalizedForCheck);
|
|
481
|
-
const plansRoot = path.join(process.cwd(), "docs", "plans") + path.sep;
|
|
482
|
-
const isPlansWrite = resolved.startsWith(plansRoot);
|
|
483
|
-
|
|
484
|
-
if (isThinkingPhase && !isPlansWrite) {
|
|
485
|
-
return {
|
|
486
|
-
blocked: true,
|
|
487
|
-
reason:
|
|
488
|
-
`⚠️ PROCESS VIOLATION: Wrote ${filePath} during ${phase} phase.\n` +
|
|
489
|
-
"During brainstorming/planning you may only write to docs/plans/. " +
|
|
490
|
-
"Read code and docs to understand the problem, then discuss the design before implementing.",
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
changed = handler.handleFileWritten(filePath) || changed;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
if (!branchConfirmed) {
|
|
498
|
-
const ref = getCurrentGitRef();
|
|
499
|
-
branchConfirmed = true;
|
|
500
|
-
|
|
501
|
-
if (ref) {
|
|
502
|
-
pendingBranchGates.set(
|
|
503
|
-
toolCallId,
|
|
504
|
-
`⚠️ First write of this session. You're on branch \`${ref}\`.\n` +
|
|
505
|
-
"Confirm with the user this is the correct branch before continuing, or create a new branch/worktree.",
|
|
506
|
-
);
|
|
507
|
-
} else {
|
|
508
|
-
// Not a git repo: disable branch messages silently.
|
|
509
|
-
branchNoticeShown = true;
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// plan-tracker init advances workflow phase to execute — intentional integration contract
|
|
515
|
-
if (event.toolName === PLAN_TRACKER_TOOL_NAME) {
|
|
516
|
-
changed = handler.handlePlanTrackerToolCall(input) || changed;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
if (changed) {
|
|
520
|
-
persistState();
|
|
521
|
-
updateWidget(ctx);
|
|
522
|
-
}
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
// --- Tool result modification (inject warnings + track investigation) ---
|
|
526
|
-
pi.on("tool_result", async (event, ctx) => {
|
|
527
|
-
const toolCallId = event.toolCallId;
|
|
528
|
-
|
|
529
|
-
if (event.toolName === "read") {
|
|
530
|
-
// biome-ignore lint/suspicious/noExplicitAny: pi SDK event input type
|
|
531
|
-
const path = ((event.input as Record<string, any>).path as string) ?? "";
|
|
532
|
-
if (handler.handleSkillFileRead(path)) {
|
|
533
|
-
persistState();
|
|
534
|
-
}
|
|
535
|
-
handler.handleReadOrInvestigation("read", path);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
if (
|
|
539
|
-
event.toolName === PLAN_TRACKER_TOOL_NAME &&
|
|
540
|
-
handler.handlePlanTrackerToolResult(event.details as PlanTrackerDetails | undefined)
|
|
541
|
-
) {
|
|
542
|
-
persistState();
|
|
543
|
-
updateWidget(ctx);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
const injected: string[] = [];
|
|
547
|
-
|
|
548
|
-
// Layer 1: announce current branch on first tool result in session.
|
|
549
|
-
if (!branchNoticeShown) {
|
|
550
|
-
const ref = getCurrentGitRef();
|
|
551
|
-
if (ref) {
|
|
552
|
-
injected.push(`📌 Current branch: \`${ref}\``);
|
|
553
|
-
} else {
|
|
554
|
-
branchConfirmed = true;
|
|
555
|
-
}
|
|
556
|
-
branchNoticeShown = true;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// Inject violation warning on write/edit for the matching tool call.
|
|
560
|
-
if (event.toolName === "write" || event.toolName === "edit") {
|
|
561
|
-
const violation = pendingViolations.get(toolCallId);
|
|
562
|
-
if (violation) {
|
|
563
|
-
const warningText = formatViolationWarning(violation);
|
|
564
|
-
injected.push(warningText);
|
|
565
|
-
|
|
566
|
-
// Wire practice escalation for TDD violations (post-write, warns but never blocks current call)
|
|
567
|
-
const isTddViolation =
|
|
568
|
-
violation.type === "source-before-test" ||
|
|
569
|
-
violation.type === "source-during-red" ||
|
|
570
|
-
violation.type === "existing-tests-not-run-before-change";
|
|
571
|
-
if (isTddViolation) {
|
|
572
|
-
const escalation = await maybeEscalate("practice", ctx);
|
|
573
|
-
if (escalation === "block") {
|
|
574
|
-
injected.push(
|
|
575
|
-
"🛑 STOP: The agent has repeatedly violated TDD practice guardrails. " +
|
|
576
|
-
"Do not write any more source code until you have addressed the TDD violations above. " +
|
|
577
|
-
"Review the test-driven-development skill before proceeding.",
|
|
578
|
-
);
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
pendingViolations.delete(toolCallId);
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// Handle bash results (test runs, commits, investigation)
|
|
586
|
-
if (event.toolName === "bash") {
|
|
587
|
-
// biome-ignore lint/suspicious/noExplicitAny: pi SDK event input type
|
|
588
|
-
const command = ((event.input as Record<string, any>).command as string) ?? "";
|
|
589
|
-
const output = event.content
|
|
590
|
-
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
591
|
-
.map((c) => c.text)
|
|
592
|
-
.join("\n");
|
|
593
|
-
// biome-ignore lint/suspicious/noExplicitAny: pi SDK event details type
|
|
594
|
-
const exitCode = (event.details as any)?.exitCode as number | undefined;
|
|
595
|
-
handler.handleBashResult(command, output, exitCode);
|
|
596
|
-
persistState();
|
|
597
|
-
|
|
598
|
-
const verificationViolation = pendingVerificationViolations.get(toolCallId);
|
|
599
|
-
if (verificationViolation) {
|
|
600
|
-
injected.push(getVerificationViolationWarning(verificationViolation.type, verificationViolation.command));
|
|
601
|
-
}
|
|
602
|
-
pendingVerificationViolations.delete(toolCallId);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
if (event.toolName === "write" || event.toolName === "edit") {
|
|
606
|
-
const branchGate = pendingBranchGates.get(toolCallId);
|
|
607
|
-
if (branchGate) {
|
|
608
|
-
injected.push(branchGate);
|
|
609
|
-
}
|
|
610
|
-
pendingBranchGates.delete(toolCallId);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
if (injected.length > 0) {
|
|
614
|
-
updateWidget(ctx);
|
|
615
|
-
return {
|
|
616
|
-
content: [{ type: "text", text: injected.join("\n\n") }, ...event.content],
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
updateWidget(ctx);
|
|
621
|
-
return undefined;
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
// --- Boundary prompting at natural handoff points ---
|
|
625
|
-
pi.on("agent_end", async (_event, ctx) => {
|
|
626
|
-
if (!ctx.hasUI) return;
|
|
627
|
-
|
|
628
|
-
const latestState = handler.getWorkflowState();
|
|
629
|
-
if (!latestState) return;
|
|
630
|
-
|
|
631
|
-
const boundary = computeBoundaryToPrompt(latestState);
|
|
632
|
-
if (!boundary) return;
|
|
633
|
-
|
|
634
|
-
const boundaryPhase = boundaryToPhase[boundary];
|
|
635
|
-
const prompt = getTransitionPrompt(boundary, latestState.artifacts[boundaryPhase]);
|
|
636
|
-
const reviewable = isReviewableBoundary(boundary);
|
|
637
|
-
|
|
638
|
-
const options = prompt.options.map((o) => o.label);
|
|
639
|
-
const pickedLabel = await ctx.ui.select(prompt.title, options);
|
|
640
|
-
|
|
641
|
-
const selected = prompt.options.find((o) => o.label === pickedLabel)?.choice ?? null;
|
|
642
|
-
|
|
643
|
-
const nextSkill = phaseToSkill[prompt.nextPhase] ?? "writing-plans";
|
|
644
|
-
const nextInSession = `/skill:${nextSkill}`;
|
|
645
|
-
const fresh = `/workflow-next ${prompt.nextPhase}${prompt.artifactPath ? ` ${prompt.artifactPath}` : ""}`;
|
|
646
|
-
const finishReminder =
|
|
647
|
-
"Before finishing:\n" +
|
|
648
|
-
"- Does this work require documentation updates? (README, CHANGELOG, API docs, inline docs)\n" +
|
|
649
|
-
"- What was learned during this implementation? (surprises, codebase knowledge, things to do differently)\n\n";
|
|
650
|
-
|
|
651
|
-
if (selected === "next" || selected === "fresh") {
|
|
652
|
-
// For reviewable boundaries: mark current phase complete first.
|
|
653
|
-
// For committed boundaries: phase is already complete.
|
|
654
|
-
if (reviewable) {
|
|
655
|
-
handler.completeCurrentWorkflowPhase();
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// Advance to the next phase
|
|
659
|
-
handler.advanceWorkflowTo(prompt.nextPhase);
|
|
660
|
-
handler.markWorkflowPrompted(boundaryPhase);
|
|
661
|
-
persistState();
|
|
662
|
-
updateWidget(ctx);
|
|
663
|
-
|
|
664
|
-
if (selected === "next") {
|
|
665
|
-
ctx.ui.setEditorText(prompt.nextPhase === "finalize" ? finishReminder + nextInSession : nextInSession);
|
|
666
|
-
} else {
|
|
667
|
-
ctx.ui.setEditorText(prompt.nextPhase === "finalize" ? finishReminder + fresh : fresh);
|
|
668
|
-
}
|
|
669
|
-
} else if (selected === "skip") {
|
|
670
|
-
if (reviewable) {
|
|
671
|
-
// Skip the current phase (the one with the artifact) and advance to next.
|
|
672
|
-
handler.skipWorkflowPhases([boundaryPhase]);
|
|
673
|
-
handler.advanceWorkflowTo(prompt.nextPhase);
|
|
674
|
-
} else {
|
|
675
|
-
// Committed boundary: skip the NEXT phase and advance past it.
|
|
676
|
-
handler.skipWorkflowPhases([prompt.nextPhase]);
|
|
677
|
-
const nextIdx = WORKFLOW_PHASES.indexOf(prompt.nextPhase);
|
|
678
|
-
const phaseAfterSkip = WORKFLOW_PHASES[nextIdx + 1] ?? null;
|
|
679
|
-
|
|
680
|
-
if (phaseAfterSkip && handler.advanceWorkflowTo(phaseAfterSkip)) {
|
|
681
|
-
const skipSkill = phaseToSkill[phaseAfterSkip] ?? "writing-plans";
|
|
682
|
-
ctx.ui.setEditorText(`/skill:${skipSkill}`);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
handler.markWorkflowPrompted(boundaryPhase);
|
|
687
|
-
persistState();
|
|
688
|
-
updateWidget(ctx);
|
|
689
|
-
} else if (selected === "revise") {
|
|
690
|
-
// Reviewable only: user wants to keep working. Don't set prompted
|
|
691
|
-
// so the review prompt fires again at the next agent_end.
|
|
692
|
-
// Don't advance, don't modify phase state.
|
|
693
|
-
} else if (selected === "discuss") {
|
|
694
|
-
// For reviewable: don't set prompted (prompt fires again after discussion).
|
|
695
|
-
// For committed: set prompted (phase is already done, user just wants to chat).
|
|
696
|
-
if (!reviewable) {
|
|
697
|
-
handler.markWorkflowPrompted(boundaryPhase);
|
|
698
|
-
persistState();
|
|
699
|
-
updateWidget(ctx);
|
|
700
|
-
}
|
|
701
|
-
ctx.ui.setEditorText(
|
|
702
|
-
`Let's discuss before moving to the next step.\n` +
|
|
703
|
-
`We're at: ${prompt.title}\n` +
|
|
704
|
-
`What questions or concerns do you want to work through?`,
|
|
705
|
-
);
|
|
706
|
-
}
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
// --- Format violation warning based on type ---
|
|
710
|
-
function formatViolationWarning(violation: Violation): string {
|
|
711
|
-
if (
|
|
712
|
-
violation.type === "source-before-test" ||
|
|
713
|
-
violation.type === "source-during-red" ||
|
|
714
|
-
violation.type === "existing-tests-not-run-before-change"
|
|
715
|
-
) {
|
|
716
|
-
const phase = handler.getWorkflowState()?.currentPhase;
|
|
717
|
-
return getTddViolationWarning(violation.type, violation.file, phase ?? undefined);
|
|
718
|
-
}
|
|
719
|
-
return getDebugViolationWarning(
|
|
720
|
-
violation.type as DebugViolationType,
|
|
721
|
-
violation.file,
|
|
722
|
-
"fixAttempts" in violation ? violation.fixAttempts : 0,
|
|
723
|
-
);
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// biome-ignore lint/suspicious/noExplicitAny: pi SDK theme type
|
|
727
|
-
function formatPhaseStrip(state: WorkflowTrackerState | null, theme: any): string {
|
|
728
|
-
if (!state?.currentPhase) return "";
|
|
729
|
-
|
|
730
|
-
const arrow = theme.fg("dim", " → ");
|
|
731
|
-
return WORKFLOW_PHASES.map((phase) => {
|
|
732
|
-
const status = state.phases[phase];
|
|
733
|
-
if (state.currentPhase === phase) return theme.fg("accent", `[${phase}]`);
|
|
734
|
-
if (status === "complete") return theme.fg("success", `✓${phase}`);
|
|
735
|
-
if (status === "skipped") return theme.fg("dim", `–${phase}`);
|
|
736
|
-
return theme.fg("dim", phase);
|
|
737
|
-
}).join(arrow);
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
// --- TUI Widget ---
|
|
741
|
-
function updateWidget(ctx: ExtensionContext) {
|
|
742
|
-
if (!ctx.hasUI) return;
|
|
743
|
-
|
|
744
|
-
const tddPhase = handler.getTddPhase().toUpperCase();
|
|
745
|
-
const hasDebug = handler.isDebugActive();
|
|
746
|
-
const workflow = handler.getWorkflowState();
|
|
747
|
-
const hasWorkflow = !!workflow?.currentPhase;
|
|
748
|
-
|
|
749
|
-
if (!hasWorkflow && tddPhase === "IDLE" && !hasDebug) {
|
|
750
|
-
ctx.ui.setWidget("workflow_monitor", undefined);
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
ctx.ui.setWidget("workflow_monitor", (_tui, theme) => {
|
|
755
|
-
const parts: string[] = [];
|
|
756
|
-
|
|
757
|
-
const phaseStrip = formatPhaseStrip(workflow, theme);
|
|
758
|
-
if (phaseStrip) {
|
|
759
|
-
parts.push(phaseStrip);
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
// TDD phase
|
|
763
|
-
if (tddPhase !== "IDLE") {
|
|
764
|
-
const colorMap: Record<string, string> = {
|
|
765
|
-
"RED-PENDING": "error",
|
|
766
|
-
RED: "error",
|
|
767
|
-
GREEN: "success",
|
|
768
|
-
REFACTOR: "accent",
|
|
769
|
-
};
|
|
770
|
-
parts.push(theme.fg(colorMap[tddPhase] ?? "muted", `TDD: ${tddPhase}`));
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// Debug state
|
|
774
|
-
if (hasDebug) {
|
|
775
|
-
const attempts = handler.getDebugFixAttempts();
|
|
776
|
-
if (attempts >= 3) {
|
|
777
|
-
parts.push(theme.fg("error", `Debug: ${attempts} fix attempts ⚠️`));
|
|
778
|
-
} else if (attempts > 0) {
|
|
779
|
-
parts.push(theme.fg("warning", `Debug: ${attempts} fix attempt${attempts !== 1 ? "s" : ""}`));
|
|
780
|
-
} else {
|
|
781
|
-
parts.push(theme.fg("accent", "Debug: investigating"));
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
return parts.length > 0 ? new Text(parts.join(theme.fg("dim", " | ")), 0, 0) : undefined;
|
|
786
|
-
});
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
pi.registerCommand("workflow-reset", {
|
|
790
|
-
description: "Reset workflow tracker to fresh state for a new task",
|
|
791
|
-
async handler(_args, ctx) {
|
|
792
|
-
handler.resetState();
|
|
793
|
-
// Emit a clear signal so plan-tracker also reconstructs to empty on next
|
|
794
|
-
// session reload. Also notify plan-tracker in real time via the shared
|
|
795
|
-
// event bus so its in-memory state and widget update immediately.
|
|
796
|
-
pi.appendEntry(PLAN_TRACKER_CLEARED_TYPE, { clearedAt: Date.now() });
|
|
797
|
-
persistState();
|
|
798
|
-
updateWidget(ctx);
|
|
799
|
-
pi.events.emit("plan_tracker:clear");
|
|
800
|
-
if (ctx.hasUI) {
|
|
801
|
-
ctx.ui.notify("Workflow reset. Ready for a new task.", "info");
|
|
802
|
-
}
|
|
803
|
-
},
|
|
804
|
-
});
|
|
805
|
-
|
|
806
|
-
pi.registerCommand("workflow-next", {
|
|
807
|
-
description: "Start a fresh session for the next workflow phase (optionally referencing an artifact path)",
|
|
808
|
-
getArgumentCompletions: getWorkflowNextCompletions,
|
|
809
|
-
async handler(args, ctx) {
|
|
810
|
-
if (!ctx.hasUI) {
|
|
811
|
-
ctx.ui.notify("workflow-next requires interactive mode", "error");
|
|
812
|
-
return;
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
const [phase, artifact] = args.trim().split(/\s+/, 2);
|
|
816
|
-
const validPhases = new Set(["brainstorm", "plan", "execute", "finalize"]);
|
|
817
|
-
if (!phase || !validPhases.has(phase)) {
|
|
818
|
-
ctx.ui.notify(
|
|
819
|
-
"Usage: /workflow-next <phase> [artifact-path] (phase: brainstorm|plan|execute|finalize)",
|
|
820
|
-
"error",
|
|
821
|
-
);
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// Validate handoff against current workflow state
|
|
826
|
-
const currentWorkflowState = handler.getWorkflowState();
|
|
827
|
-
if (currentWorkflowState?.currentPhase) {
|
|
828
|
-
const validationError = validateNextWorkflowPhase(currentWorkflowState, phase as Phase);
|
|
829
|
-
if (validationError) {
|
|
830
|
-
ctx.ui.notify(validationError, "error");
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
// Derive handoff state for session seeding
|
|
836
|
-
const derivedWorkflow = currentWorkflowState
|
|
837
|
-
? deriveWorkflowHandoffState(currentWorkflowState, phase as Phase)
|
|
838
|
-
: undefined;
|
|
839
|
-
|
|
840
|
-
const parentSession = ctx.sessionManager.getSessionFile();
|
|
841
|
-
const res = await ctx.newSession({
|
|
842
|
-
parentSession,
|
|
843
|
-
setup: derivedWorkflow
|
|
844
|
-
? async (sm) => {
|
|
845
|
-
const fullState = handler.getFullState();
|
|
846
|
-
sm.appendCustomEntry(SUPERPOWERS_STATE_ENTRY_TYPE, {
|
|
847
|
-
...fullState,
|
|
848
|
-
workflow: derivedWorkflow,
|
|
849
|
-
tdd: { ...TDD_DEFAULTS, testFiles: [], sourceFiles: [] },
|
|
850
|
-
debug: { ...DEBUG_DEFAULTS },
|
|
851
|
-
verification: { ...VERIFICATION_DEFAULTS },
|
|
852
|
-
savedAt: Date.now(),
|
|
853
|
-
});
|
|
854
|
-
}
|
|
855
|
-
: undefined,
|
|
856
|
-
});
|
|
857
|
-
if (res.cancelled) return;
|
|
858
|
-
|
|
859
|
-
const lines: string[] = [];
|
|
860
|
-
if (artifact) lines.push(`Continue from artifact: ${artifact}`);
|
|
861
|
-
|
|
862
|
-
if (phase === "brainstorm") {
|
|
863
|
-
lines.push("/skill:brainstorming");
|
|
864
|
-
} else if (phase === "plan") {
|
|
865
|
-
lines.push("/skill:writing-plans");
|
|
866
|
-
} else if (phase === "execute") {
|
|
867
|
-
lines.push("/skill:executing-tasks");
|
|
868
|
-
lines.push("Execute the approved plan task-by-task.");
|
|
869
|
-
} else if (phase === "finalize") {
|
|
870
|
-
lines.push("/skill:executing-tasks");
|
|
871
|
-
lines.push("Finalize the completed work (review, PR, docs, archive, cleanup).");
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
ctx.ui.setEditorText(lines.join("\n"));
|
|
875
|
-
ctx.ui.notify("New session ready. Submit when ready.", "info");
|
|
876
|
-
},
|
|
877
|
-
});
|
|
878
|
-
|
|
879
|
-
// --- Reference Tool ---
|
|
880
|
-
pi.registerTool({
|
|
881
|
-
name: "workflow_reference",
|
|
882
|
-
label: "Workflow Guide",
|
|
883
|
-
description: `Detailed guidance for workflow skills. Topics: ${REFERENCE_TOPICS.join(", ")}`,
|
|
884
|
-
parameters: Type.Object({
|
|
885
|
-
topic: StringEnum(REFERENCE_TOPICS as unknown as readonly [string, ...string[]], {
|
|
886
|
-
description: "Reference topic to load",
|
|
887
|
-
}),
|
|
888
|
-
}),
|
|
889
|
-
async execute(_toolCallId, params) {
|
|
890
|
-
const content = await loadReference(params.topic);
|
|
891
|
-
return {
|
|
892
|
-
content: [{ type: "text", text: content }],
|
|
893
|
-
details: { topic: params.topic },
|
|
894
|
-
};
|
|
895
|
-
},
|
|
896
|
-
renderCall(args, theme) {
|
|
897
|
-
let text = theme.fg("toolTitle", theme.bold("workflow_reference "));
|
|
898
|
-
text += theme.fg("accent", args.topic);
|
|
899
|
-
return new Text(text, 0, 0);
|
|
900
|
-
},
|
|
901
|
-
renderResult(result, _options, theme) {
|
|
902
|
-
// biome-ignore lint/suspicious/noExplicitAny: pi SDK event details type
|
|
903
|
-
const topic = (result.details as any)?.topic ?? "unknown";
|
|
904
|
-
const content = result.content[0];
|
|
905
|
-
const len = content?.type === "text" ? content.text.length : 0;
|
|
906
|
-
return new Text(theme.fg("success", "✓ ") + theme.fg("muted", `${topic} (${len} chars)`), 0, 0);
|
|
907
|
-
},
|
|
908
|
-
});
|
|
909
|
-
}
|