@tianhai/pi-workflow-kit 0.4.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/LICENSE +22 -0
- package/README.md +509 -0
- package/ROADMAP.md +16 -0
- package/agents/code-reviewer.md +18 -0
- package/agents/config.ts +5 -0
- package/agents/implementer.md +26 -0
- package/agents/spec-reviewer.md +13 -0
- package/agents/worker.md +17 -0
- package/banner.jpg +0 -0
- package/docs/developer-usage-guide.md +463 -0
- package/docs/oversight-model.md +49 -0
- package/docs/workflow-phases.md +71 -0
- package/extensions/constants.ts +9 -0
- package/extensions/lib/logging.ts +138 -0
- package/extensions/plan-tracker.ts +496 -0
- package/extensions/subagent/agents.ts +144 -0
- package/extensions/subagent/concurrency.ts +52 -0
- package/extensions/subagent/env.ts +47 -0
- package/extensions/subagent/index.ts +1116 -0
- package/extensions/subagent/lifecycle.ts +25 -0
- package/extensions/subagent/timeout.ts +13 -0
- package/extensions/workflow-monitor/debug-monitor.ts +98 -0
- package/extensions/workflow-monitor/git.ts +31 -0
- package/extensions/workflow-monitor/heuristics.ts +58 -0
- package/extensions/workflow-monitor/investigation.ts +52 -0
- package/extensions/workflow-monitor/reference-tool.ts +42 -0
- package/extensions/workflow-monitor/skip-confirmation.ts +19 -0
- package/extensions/workflow-monitor/tdd-monitor.ts +137 -0
- package/extensions/workflow-monitor/test-runner.ts +37 -0
- package/extensions/workflow-monitor/verification-monitor.ts +61 -0
- package/extensions/workflow-monitor/warnings.ts +81 -0
- package/extensions/workflow-monitor/workflow-handler.ts +358 -0
- package/extensions/workflow-monitor/workflow-tracker.ts +231 -0
- package/extensions/workflow-monitor/workflow-transitions.ts +55 -0
- package/extensions/workflow-monitor.ts +885 -0
- package/package.json +49 -0
- package/skills/brainstorming/SKILL.md +70 -0
- package/skills/dispatching-parallel-agents/SKILL.md +194 -0
- package/skills/executing-tasks/SKILL.md +247 -0
- package/skills/receiving-code-review/SKILL.md +196 -0
- package/skills/systematic-debugging/SKILL.md +170 -0
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/skills/systematic-debugging/find-polluter.sh +63 -0
- package/skills/systematic-debugging/reference/rationalizations.md +61 -0
- package/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/skills/test-driven-development/SKILL.md +266 -0
- package/skills/test-driven-development/reference/examples.md +101 -0
- package/skills/test-driven-development/reference/rationalizations.md +67 -0
- package/skills/test-driven-development/reference/when-stuck.md +33 -0
- package/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/skills/using-git-worktrees/SKILL.md +231 -0
- package/skills/writing-plans/SKILL.md +149 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import type { SessionEntry } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { PlanTrackerDetails, PlanTrackerTask } from "../plan-tracker.js";
|
|
3
|
+
import { DebugMonitor, type DebugViolation } from "./debug-monitor";
|
|
4
|
+
import { isSourceFile } from "./heuristics";
|
|
5
|
+
import { isInvestigationCommand, isInvestigationToolCall } from "./investigation";
|
|
6
|
+
import { TddMonitor, type TddPhase, type TddViolation } from "./tdd-monitor";
|
|
7
|
+
import { parseTestCommand, parseTestResult } from "./test-runner";
|
|
8
|
+
import { VerificationMonitor, type VerificationViolation } from "./verification-monitor";
|
|
9
|
+
import { type Phase, type PhaseStatus, WorkflowTracker, type WorkflowTrackerState } from "./workflow-tracker";
|
|
10
|
+
|
|
11
|
+
export type Violation = TddViolation | DebugViolation;
|
|
12
|
+
|
|
13
|
+
export interface ToolCallResult {
|
|
14
|
+
violation: Violation | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SuperpowersStateSnapshot {
|
|
18
|
+
workflow: WorkflowTrackerState;
|
|
19
|
+
tdd: {
|
|
20
|
+
phase: TddPhase;
|
|
21
|
+
testFiles: string[];
|
|
22
|
+
sourceFiles: string[];
|
|
23
|
+
redVerificationPending: boolean;
|
|
24
|
+
nonCodeMode: boolean;
|
|
25
|
+
};
|
|
26
|
+
debug: {
|
|
27
|
+
active: boolean;
|
|
28
|
+
investigated: boolean;
|
|
29
|
+
fixAttempts: number;
|
|
30
|
+
};
|
|
31
|
+
verification: {
|
|
32
|
+
verified: boolean;
|
|
33
|
+
verificationWaived: boolean;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type SuperpowersStatePatch = {
|
|
38
|
+
workflow?: Partial<WorkflowTrackerState> & {
|
|
39
|
+
phases?: Partial<Record<Phase, PhaseStatus>>;
|
|
40
|
+
artifacts?: Partial<Record<Phase, string | null>>;
|
|
41
|
+
prompted?: Partial<Record<Phase, boolean>>;
|
|
42
|
+
};
|
|
43
|
+
tdd?: Partial<SuperpowersStateSnapshot["tdd"]>;
|
|
44
|
+
debug?: Partial<SuperpowersStateSnapshot["debug"]>;
|
|
45
|
+
verification?: Partial<SuperpowersStateSnapshot["verification"]>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const TDD_DEFAULTS = {
|
|
49
|
+
phase: "idle" as TddPhase,
|
|
50
|
+
testFiles: [] as string[],
|
|
51
|
+
sourceFiles: [] as string[],
|
|
52
|
+
redVerificationPending: false,
|
|
53
|
+
nonCodeMode: false,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const DEBUG_DEFAULTS = {
|
|
57
|
+
active: false,
|
|
58
|
+
investigated: false,
|
|
59
|
+
fixAttempts: 0,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const VERIFICATION_DEFAULTS = {
|
|
63
|
+
verified: false,
|
|
64
|
+
verificationWaived: false,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export interface WorkflowHandler {
|
|
68
|
+
handleToolCall(toolName: string, input: Record<string, unknown>): ToolCallResult;
|
|
69
|
+
handleReadOrInvestigation(toolName: string, path: string): void;
|
|
70
|
+
handleBashResult(command: string, output: string, exitCode: number | undefined): void;
|
|
71
|
+
/** @internal Used in tests; will be wired to bash events in future */
|
|
72
|
+
handleBashInvestigation(command: string): void;
|
|
73
|
+
isDebugActive(): boolean;
|
|
74
|
+
getDebugFixAttempts(): number;
|
|
75
|
+
getTddPhase(): string;
|
|
76
|
+
getWidgetText(): string;
|
|
77
|
+
getTddState(): ReturnType<TddMonitor["getState"]>;
|
|
78
|
+
checkCommitGate(command: string): VerificationViolation | null;
|
|
79
|
+
recordVerificationWaiver(): void;
|
|
80
|
+
restoreTddState(phase: TddPhase, testFiles: string[], sourceFiles: string[], redVerificationPending?: boolean): void;
|
|
81
|
+
handleInputText(text: string): boolean;
|
|
82
|
+
handleFileWritten(path: string): boolean;
|
|
83
|
+
handlePlanTrackerToolCall(input: Record<string, unknown>): boolean;
|
|
84
|
+
handlePlanTrackerToolResult(details: PlanTrackerDetails | undefined): boolean;
|
|
85
|
+
getWorkflowState(): WorkflowTrackerState | null;
|
|
86
|
+
getFullState(): SuperpowersStateSnapshot;
|
|
87
|
+
setFullState(snapshot: SuperpowersStatePatch): void;
|
|
88
|
+
restoreWorkflowStateFromBranch(branch: SessionEntry[]): void;
|
|
89
|
+
markWorkflowPrompted(phase: Phase): boolean;
|
|
90
|
+
completeCurrentWorkflowPhase(): boolean;
|
|
91
|
+
advanceWorkflowTo(phase: Phase): boolean;
|
|
92
|
+
skipWorkflowPhases(phases: Phase[]): boolean;
|
|
93
|
+
handleSkillFileRead(path: string): boolean;
|
|
94
|
+
resetState(): void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function deriveNonCodeMode(tasks: PlanTrackerTask[]): boolean {
|
|
98
|
+
const activeTask = tasks.find((task) => task.status === "in_progress");
|
|
99
|
+
return activeTask?.type === "non-code";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function areAllTasksTerminal(tasks: PlanTrackerTask[]): boolean {
|
|
103
|
+
return tasks.length > 0 && tasks.every((task) => task.status === "complete" || task.status === "blocked");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function createWorkflowHandler(): WorkflowHandler {
|
|
107
|
+
const tdd = new TddMonitor();
|
|
108
|
+
const debug = new DebugMonitor();
|
|
109
|
+
const verification = new VerificationMonitor();
|
|
110
|
+
const tracker = new WorkflowTracker();
|
|
111
|
+
let debugFailStreak = 0;
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
handleToolCall(toolName: string, input: Record<string, unknown>): ToolCallResult {
|
|
115
|
+
// Track investigation from tool calls (LSP, kota, web search)
|
|
116
|
+
if (isInvestigationToolCall(toolName, input)) {
|
|
117
|
+
debug.onInvestigation();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (toolName === "write" || toolName === "edit") {
|
|
121
|
+
const path = input.path as string | undefined;
|
|
122
|
+
if (path) {
|
|
123
|
+
if (isSourceFile(path)) {
|
|
124
|
+
verification.onSourceWritten();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Debug violations take precedence, and when debug is active we don't
|
|
128
|
+
// additionally enforce TDD write-order violations.
|
|
129
|
+
if (debug.isActive() && isSourceFile(path)) {
|
|
130
|
+
const debugViolation = debug.onSourceWritten(path);
|
|
131
|
+
return { violation: debugViolation };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const tddViolation = tdd.onFileWritten(path);
|
|
135
|
+
return { violation: tddViolation };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { violation: null };
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
handleReadOrInvestigation(toolName: string, _path: string): void {
|
|
142
|
+
if (toolName === "read") {
|
|
143
|
+
debug.onInvestigation();
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
handleBashResult(command: string, output: string, exitCode: number | undefined): void {
|
|
148
|
+
if (isInvestigationCommand(command)) {
|
|
149
|
+
debug.onInvestigation();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (/\bgit\s+commit\b/.test(command)) {
|
|
153
|
+
debugFailStreak = 0;
|
|
154
|
+
tdd.onCommit();
|
|
155
|
+
debug.onCommit();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (parseTestCommand(command)) {
|
|
160
|
+
const passed = parseTestResult(output, exitCode);
|
|
161
|
+
if (passed !== null) {
|
|
162
|
+
if (passed) {
|
|
163
|
+
verification.recordVerification();
|
|
164
|
+
} else {
|
|
165
|
+
verification.reset();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const excludeFromDebug = !passed && tdd.getPhase() === "red-pending";
|
|
169
|
+
|
|
170
|
+
tdd.onTestResult(passed);
|
|
171
|
+
|
|
172
|
+
if (passed) {
|
|
173
|
+
debugFailStreak = 0;
|
|
174
|
+
debug.onTestPassed();
|
|
175
|
+
} else if (!excludeFromDebug) {
|
|
176
|
+
debugFailStreak += 1;
|
|
177
|
+
const tddPhase = tdd.getPhase();
|
|
178
|
+
if (debugFailStreak >= 1 && tddPhase === "idle") {
|
|
179
|
+
debug.onTestFailed();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
handleBashInvestigation(command: string): void {
|
|
187
|
+
if (isInvestigationCommand(command)) {
|
|
188
|
+
debug.onInvestigation();
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
isDebugActive(): boolean {
|
|
193
|
+
return debug.isActive();
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
getDebugFixAttempts(): number {
|
|
197
|
+
return debug.getFixAttempts();
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
getTddPhase(): string {
|
|
201
|
+
return tdd.getPhase();
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
getWidgetText(): string {
|
|
205
|
+
const parts: string[] = [];
|
|
206
|
+
|
|
207
|
+
const phase = tdd.getPhase();
|
|
208
|
+
if (phase !== "idle") {
|
|
209
|
+
parts.push(`TDD: ${phase.toUpperCase()}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (debug.isActive()) {
|
|
213
|
+
parts.push("Debug: ACTIVE");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return parts.join(" | ");
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
getTddState() {
|
|
220
|
+
return tdd.getState();
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
checkCommitGate(command: string) {
|
|
224
|
+
return verification.checkCommitGate(command);
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
recordVerificationWaiver() {
|
|
228
|
+
verification.recordVerificationWaiver();
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
restoreTddState(phase: TddPhase, testFiles: string[], sourceFiles: string[], redVerificationPending = false) {
|
|
232
|
+
tdd.setState(phase, testFiles, sourceFiles, redVerificationPending);
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
handleInputText(text: string) {
|
|
236
|
+
return tracker.onInputText(text);
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
handleFileWritten(path: string) {
|
|
240
|
+
return tracker.onFileWritten(path);
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
handlePlanTrackerToolCall(input: Record<string, unknown>) {
|
|
244
|
+
if (input.action === "init") {
|
|
245
|
+
tdd.setNonCodeMode(false);
|
|
246
|
+
return tracker.onPlanTrackerInit();
|
|
247
|
+
}
|
|
248
|
+
return false;
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
handlePlanTrackerToolResult(details: PlanTrackerDetails | undefined) {
|
|
252
|
+
if (!details) return false;
|
|
253
|
+
|
|
254
|
+
let changed = false;
|
|
255
|
+
const nextNonCodeMode = deriveNonCodeMode(details.tasks);
|
|
256
|
+
if (tdd.getState().nonCodeMode !== nextNonCodeMode) {
|
|
257
|
+
tdd.setNonCodeMode(nextNonCodeMode);
|
|
258
|
+
changed = true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (areAllTasksTerminal(details.tasks) && tracker.getState().currentPhase === "execute") {
|
|
262
|
+
changed = tracker.completeCurrent() || changed;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return changed;
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
getWorkflowState() {
|
|
269
|
+
return tracker.getState();
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
getFullState() {
|
|
273
|
+
return {
|
|
274
|
+
workflow: tracker.getState(),
|
|
275
|
+
tdd: tdd.getState(),
|
|
276
|
+
debug: debug.getState(),
|
|
277
|
+
verification: verification.getState(),
|
|
278
|
+
};
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
setFullState(snapshot: SuperpowersStatePatch) {
|
|
282
|
+
if (snapshot.workflow) {
|
|
283
|
+
const defaultWorkflow = new WorkflowTracker().getState();
|
|
284
|
+
tracker.setState({
|
|
285
|
+
...defaultWorkflow,
|
|
286
|
+
...snapshot.workflow,
|
|
287
|
+
phases: { ...defaultWorkflow.phases, ...snapshot.workflow.phases },
|
|
288
|
+
artifacts: { ...defaultWorkflow.artifacts, ...snapshot.workflow.artifacts },
|
|
289
|
+
prompted: { ...defaultWorkflow.prompted, ...snapshot.workflow.prompted },
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
if (snapshot.tdd) {
|
|
293
|
+
const tddState = { ...TDD_DEFAULTS, ...snapshot.tdd };
|
|
294
|
+
tdd.setState(
|
|
295
|
+
tddState.phase,
|
|
296
|
+
tddState.testFiles,
|
|
297
|
+
tddState.sourceFiles,
|
|
298
|
+
tddState.redVerificationPending,
|
|
299
|
+
tddState.nonCodeMode,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
if (snapshot.debug) {
|
|
303
|
+
debug.setState({ ...DEBUG_DEFAULTS, ...snapshot.debug });
|
|
304
|
+
}
|
|
305
|
+
if (snapshot.verification) {
|
|
306
|
+
verification.setState({ ...VERIFICATION_DEFAULTS, ...snapshot.verification });
|
|
307
|
+
}
|
|
308
|
+
debugFailStreak = 0;
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
restoreWorkflowStateFromBranch(branch: SessionEntry[]) {
|
|
312
|
+
const state = WorkflowTracker.reconstructFromBranch(branch);
|
|
313
|
+
if (state) {
|
|
314
|
+
tracker.setState(state);
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
markWorkflowPrompted(phase: Phase) {
|
|
319
|
+
return tracker.markPrompted(phase);
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
completeCurrentWorkflowPhase() {
|
|
323
|
+
return tracker.completeCurrent();
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
advanceWorkflowTo(phase) {
|
|
327
|
+
return tracker.advanceTo(phase);
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
skipWorkflowPhases(phases) {
|
|
331
|
+
return tracker.skipPhases(phases);
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
handleSkillFileRead(path: string) {
|
|
335
|
+
return tracker.onSkillFileRead(path);
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
resetState() {
|
|
339
|
+
const freshState: SuperpowersStateSnapshot = {
|
|
340
|
+
workflow: new WorkflowTracker().getState(),
|
|
341
|
+
tdd: { ...TDD_DEFAULTS, testFiles: [], sourceFiles: [] },
|
|
342
|
+
debug: { ...DEBUG_DEFAULTS },
|
|
343
|
+
verification: { ...VERIFICATION_DEFAULTS },
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
tracker.setState(freshState.workflow);
|
|
347
|
+
tdd.setState(
|
|
348
|
+
freshState.tdd.phase,
|
|
349
|
+
freshState.tdd.testFiles,
|
|
350
|
+
freshState.tdd.sourceFiles,
|
|
351
|
+
freshState.tdd.redVerificationPending,
|
|
352
|
+
);
|
|
353
|
+
debug.setState(freshState.debug);
|
|
354
|
+
verification.setState(freshState.verification);
|
|
355
|
+
debugFailStreak = 0;
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { SessionEntry } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export const WORKFLOW_PHASES = ["brainstorm", "plan", "execute", "finalize"] as const;
|
|
4
|
+
|
|
5
|
+
export type Phase = (typeof WORKFLOW_PHASES)[number];
|
|
6
|
+
export type PhaseStatus = "pending" | "active" | "complete" | "skipped";
|
|
7
|
+
|
|
8
|
+
export interface WorkflowTrackerState {
|
|
9
|
+
phases: Record<Phase, PhaseStatus>;
|
|
10
|
+
currentPhase: Phase | null;
|
|
11
|
+
artifacts: Record<Phase, string | null>;
|
|
12
|
+
prompted: Record<Phase, boolean>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type TransitionBoundary = "design_committed" | "plan_ready" | "execution_complete";
|
|
16
|
+
|
|
17
|
+
export function computeBoundaryToPrompt(state: WorkflowTrackerState): TransitionBoundary | null {
|
|
18
|
+
if (state.phases.brainstorm === "complete" && !state.prompted.brainstorm) {
|
|
19
|
+
return "design_committed";
|
|
20
|
+
}
|
|
21
|
+
if (state.phases.plan === "complete" && !state.prompted.plan) {
|
|
22
|
+
return "plan_ready";
|
|
23
|
+
}
|
|
24
|
+
if (state.phases.execute === "complete" && !state.prompted.execute) {
|
|
25
|
+
return "execution_complete";
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cloneState(state: WorkflowTrackerState): WorkflowTrackerState {
|
|
31
|
+
return JSON.parse(JSON.stringify(state)) as WorkflowTrackerState;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function emptyState(): WorkflowTrackerState {
|
|
35
|
+
const phases = Object.fromEntries(WORKFLOW_PHASES.map((p) => [p, "pending"])) as Record<Phase, PhaseStatus>;
|
|
36
|
+
|
|
37
|
+
const artifacts = Object.fromEntries(WORKFLOW_PHASES.map((p) => [p, null])) as Record<Phase, string | null>;
|
|
38
|
+
|
|
39
|
+
const prompted = Object.fromEntries(WORKFLOW_PHASES.map((p) => [p, false])) as Record<Phase, boolean>;
|
|
40
|
+
|
|
41
|
+
return { phases, currentPhase: null, artifacts, prompted };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const WORKFLOW_TRACKER_ENTRY_TYPE = "workflow_tracker_state";
|
|
45
|
+
|
|
46
|
+
export function parseSkillName(line: string): string | null {
|
|
47
|
+
const slashMatch = line.match(/^\s*\/skill:([^\s]+)/);
|
|
48
|
+
const xmlMatch = line.match(/<skill\s+name="([^"]+)"/);
|
|
49
|
+
return slashMatch?.[1] ?? xmlMatch?.[1] ?? null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const SKILL_TO_PHASE: Record<string, Phase> = {
|
|
53
|
+
brainstorming: "brainstorm",
|
|
54
|
+
"writing-plans": "plan",
|
|
55
|
+
"using-git-worktrees": "plan", // pre-execute worktree setup belongs to plan
|
|
56
|
+
"executing-tasks": "execute",
|
|
57
|
+
"systematic-debugging": "execute", // used within execute phase
|
|
58
|
+
"dispatching-parallel-agents": "execute", // used within execute phase
|
|
59
|
+
"test-driven-development": "execute", // makes TDD skill phase-aware
|
|
60
|
+
"receiving-code-review": "finalize", // post-PR external review
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export function resolveSkillPhase(skill: string, state: WorkflowTrackerState | null | undefined): Phase | null {
|
|
64
|
+
if (skill === "executing-tasks") {
|
|
65
|
+
if (state?.currentPhase === "finalize" || state?.phases.execute === "complete") {
|
|
66
|
+
return "finalize";
|
|
67
|
+
}
|
|
68
|
+
return "execute";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return SKILL_TO_PHASE[skill] ?? null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const PLANS_DIR_RE = /^docs\/plans\//;
|
|
75
|
+
const DESIGN_RE = /-design\.md$/;
|
|
76
|
+
const IMPLEMENTATION_RE = /-implementation\.md$/;
|
|
77
|
+
|
|
78
|
+
export class WorkflowTracker {
|
|
79
|
+
private state: WorkflowTrackerState = emptyState();
|
|
80
|
+
|
|
81
|
+
getState(): WorkflowTrackerState {
|
|
82
|
+
return cloneState(this.state);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
setState(state: WorkflowTrackerState): void {
|
|
86
|
+
this.state = cloneState(state);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
reset(): void {
|
|
90
|
+
this.state = emptyState();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
advanceTo(phase: Phase): boolean {
|
|
94
|
+
const current = this.state.currentPhase;
|
|
95
|
+
const nextIdx = WORKFLOW_PHASES.indexOf(phase);
|
|
96
|
+
|
|
97
|
+
if (current) {
|
|
98
|
+
const curIdx = WORKFLOW_PHASES.indexOf(current);
|
|
99
|
+
if (nextIdx <= curIdx) {
|
|
100
|
+
// Backward or same-phase navigation = new task. Reset everything.
|
|
101
|
+
this.reset();
|
|
102
|
+
// Fall through to activate the target phase below.
|
|
103
|
+
} else {
|
|
104
|
+
// Forward advance: auto-complete the current phase.
|
|
105
|
+
if (this.state.phases[current] === "active") {
|
|
106
|
+
this.state.phases[current] = "complete";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const p of WORKFLOW_PHASES) {
|
|
112
|
+
if (p !== phase && this.state.phases[p] === "active") {
|
|
113
|
+
this.state.phases[p] = "complete";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.state.currentPhase = phase;
|
|
118
|
+
if (this.state.phases[phase] === "pending") {
|
|
119
|
+
this.state.phases[phase] = "active";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
skipPhase(phase: Phase): boolean {
|
|
126
|
+
const status = this.state.phases[phase];
|
|
127
|
+
if (status !== "pending" && status !== "active") return false;
|
|
128
|
+
this.state.phases[phase] = "skipped";
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
skipPhases(phases: Phase[]): boolean {
|
|
133
|
+
let changed = false;
|
|
134
|
+
for (const p of phases) changed = this.skipPhase(p) || changed;
|
|
135
|
+
return changed;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
completeCurrent(): boolean {
|
|
139
|
+
const phase = this.state.currentPhase;
|
|
140
|
+
if (!phase) return false;
|
|
141
|
+
if (this.state.phases[phase] === "complete") return false;
|
|
142
|
+
this.state.phases[phase] = "complete";
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
recordArtifact(phase: Phase, path: string): boolean {
|
|
147
|
+
if (this.state.artifacts[phase] === path) return false;
|
|
148
|
+
this.state.artifacts[phase] = path;
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
markPrompted(phase: Phase): boolean {
|
|
153
|
+
if (this.state.prompted[phase]) return false;
|
|
154
|
+
this.state.prompted[phase] = true;
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
onInputText(text: string): boolean {
|
|
159
|
+
const lines = text.split(/\r?\n/);
|
|
160
|
+
let changed = false;
|
|
161
|
+
|
|
162
|
+
for (const line of lines) {
|
|
163
|
+
const skill = parseSkillName(line);
|
|
164
|
+
if (!skill) continue;
|
|
165
|
+
const phase = resolveSkillPhase(skill, this.state);
|
|
166
|
+
if (!phase) continue;
|
|
167
|
+
// Guard against backward navigation: skills shared across phases (e.g. executing-tasks
|
|
168
|
+
// covers both execute and finalize) must not reset state when re-invoked in a later phase.
|
|
169
|
+
const currentIdx = this.state.currentPhase ? WORKFLOW_PHASES.indexOf(this.state.currentPhase) : -1;
|
|
170
|
+
const targetIdx = WORKFLOW_PHASES.indexOf(phase);
|
|
171
|
+
if (targetIdx <= currentIdx) continue;
|
|
172
|
+
if (this.advanceTo(phase)) changed = true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return changed;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
onSkillFileRead(path: string): boolean {
|
|
179
|
+
const match = path.match(/\/skills\/([^/]+)\/SKILL\.md$/);
|
|
180
|
+
if (!match) return false;
|
|
181
|
+
const phase = resolveSkillPhase(match[1], this.state);
|
|
182
|
+
if (!phase) return false;
|
|
183
|
+
// Guard against backward navigation: some skills (e.g. executing-tasks) serve
|
|
184
|
+
// multiple phases. Re-reading their SKILL.md during a later phase (e.g. finalize)
|
|
185
|
+
// must not reset workflow state. Rely on plan_tracker init or explicit /workflow-reset
|
|
186
|
+
// to restart from scratch.
|
|
187
|
+
const currentIdx = this.state.currentPhase ? WORKFLOW_PHASES.indexOf(this.state.currentPhase) : -1;
|
|
188
|
+
const targetIdx = WORKFLOW_PHASES.indexOf(phase);
|
|
189
|
+
if (targetIdx <= currentIdx) return false;
|
|
190
|
+
return this.advanceTo(phase);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
onFileWritten(path: string): boolean {
|
|
194
|
+
if (!PLANS_DIR_RE.test(path)) return false;
|
|
195
|
+
|
|
196
|
+
if (DESIGN_RE.test(path)) {
|
|
197
|
+
const changedArtifact = this.recordArtifact("brainstorm", path);
|
|
198
|
+
const changedPhase = this.advanceTo("brainstorm");
|
|
199
|
+
return changedArtifact || changedPhase;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (IMPLEMENTATION_RE.test(path)) {
|
|
203
|
+
const changedArtifact = this.recordArtifact("plan", path);
|
|
204
|
+
const changedPhase = this.advanceTo("plan");
|
|
205
|
+
return changedArtifact || changedPhase;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
onPlanTrackerInit(): boolean {
|
|
212
|
+
return this.advanceTo("execute");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
static reconstructFromBranch(branch: SessionEntry[]): WorkflowTrackerState | null {
|
|
216
|
+
let last: WorkflowTrackerState | null = null;
|
|
217
|
+
|
|
218
|
+
for (const entry of branch) {
|
|
219
|
+
if (entry.type !== "custom") continue;
|
|
220
|
+
// biome-ignore lint/suspicious/noExplicitAny: pi SDK session entry type
|
|
221
|
+
if ((entry as any).customType !== WORKFLOW_TRACKER_ENTRY_TYPE) continue;
|
|
222
|
+
// biome-ignore lint/suspicious/noExplicitAny: pi SDK session entry type
|
|
223
|
+
const data = (entry as any).data as WorkflowTrackerState | undefined;
|
|
224
|
+
if (data && typeof data === "object") {
|
|
225
|
+
last = cloneState(data);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return last;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Phase, TransitionBoundary } from "./workflow-tracker";
|
|
2
|
+
|
|
3
|
+
export type TransitionChoice = "next" | "fresh" | "skip" | "discuss";
|
|
4
|
+
|
|
5
|
+
export interface TransitionPrompt {
|
|
6
|
+
boundary: TransitionBoundary;
|
|
7
|
+
title: string;
|
|
8
|
+
nextPhase: Phase;
|
|
9
|
+
artifactPath: string | null;
|
|
10
|
+
options: { choice: TransitionChoice; label: string }[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const BASE_OPTIONS: TransitionPrompt["options"] = [
|
|
14
|
+
{ choice: "next", label: "Next step (this session)" },
|
|
15
|
+
{ choice: "fresh", label: "Fresh session → next step" },
|
|
16
|
+
{ choice: "skip", label: "Skip" },
|
|
17
|
+
{ choice: "discuss", label: "Discuss" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export function getTransitionPrompt(boundary: TransitionBoundary, artifactPath: string | null): TransitionPrompt {
|
|
21
|
+
switch (boundary) {
|
|
22
|
+
case "design_committed":
|
|
23
|
+
return {
|
|
24
|
+
boundary,
|
|
25
|
+
title: "Design committed. What next?",
|
|
26
|
+
nextPhase: "plan",
|
|
27
|
+
artifactPath,
|
|
28
|
+
options: BASE_OPTIONS,
|
|
29
|
+
};
|
|
30
|
+
case "plan_ready":
|
|
31
|
+
return {
|
|
32
|
+
boundary,
|
|
33
|
+
title: "Plan ready. What next?",
|
|
34
|
+
nextPhase: "execute",
|
|
35
|
+
artifactPath,
|
|
36
|
+
options: BASE_OPTIONS,
|
|
37
|
+
};
|
|
38
|
+
case "execution_complete":
|
|
39
|
+
return {
|
|
40
|
+
boundary,
|
|
41
|
+
title: "All tasks complete. What next?",
|
|
42
|
+
nextPhase: "finalize",
|
|
43
|
+
artifactPath,
|
|
44
|
+
options: BASE_OPTIONS,
|
|
45
|
+
};
|
|
46
|
+
default:
|
|
47
|
+
return {
|
|
48
|
+
boundary,
|
|
49
|
+
title: "What next?",
|
|
50
|
+
nextPhase: "plan",
|
|
51
|
+
artifactPath,
|
|
52
|
+
options: BASE_OPTIONS,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|