@tianhai/pi-workflow-kit 0.5.3 → 0.7.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.
Files changed (70) hide show
  1. package/README.md +50 -490
  2. package/docs/developer-usage-guide.md +41 -401
  3. package/docs/oversight-model.md +13 -34
  4. package/docs/plans/2026-04-11-finalizing-merge-options-design.md +33 -0
  5. package/docs/plans/completed/2026-04-11-checkpoint-review-gates-design.md +50 -0
  6. package/docs/plans/completed/2026-04-11-checkpoint-review-gates-implementation.md +98 -0
  7. package/docs/plans/completed/2026-04-11-finalizing-merge-options-design.md +33 -0
  8. package/docs/plans/completed/2026-04-11-finalizing-merge-options-implementation.md +75 -0
  9. package/docs/plans/completed/2026-04-11-workspace-setup-design.md +28 -0
  10. package/docs/plans/completed/2026-04-11-workspace-setup-implementation.md +57 -0
  11. package/docs/workflow-phases.md +32 -46
  12. package/extensions/workflow-guard.ts +67 -0
  13. package/package.json +3 -7
  14. package/skills/brainstorming/SKILL.md +20 -67
  15. package/skills/executing-tasks/SKILL.md +49 -214
  16. package/skills/finalizing/SKILL.md +67 -0
  17. package/skills/writing-plans/SKILL.md +29 -129
  18. package/ROADMAP.md +0 -16
  19. package/agents/code-reviewer.md +0 -18
  20. package/agents/config.ts +0 -5
  21. package/agents/implementer.md +0 -26
  22. package/agents/spec-reviewer.md +0 -13
  23. package/agents/worker.md +0 -17
  24. package/docs/plans/2026-04-10-brainstorming-boundary-enforcement-design.md +0 -60
  25. package/docs/plans/completed/2026-04-09-cleanup-legacy-state-and-enforce-think-phases-design.md +0 -56
  26. package/docs/plans/completed/2026-04-09-cleanup-legacy-state-and-enforce-think-phases-implementation.md +0 -196
  27. package/docs/plans/completed/2026-04-09-workflow-next-autocomplete-design.md +0 -185
  28. package/docs/plans/completed/2026-04-09-workflow-next-autocomplete-implementation.md +0 -334
  29. package/docs/plans/completed/2026-04-09-workflow-next-handoff-state-design.md +0 -251
  30. package/docs/plans/completed/2026-04-09-workflow-next-handoff-state-implementation.md +0 -253
  31. package/extensions/constants.ts +0 -15
  32. package/extensions/lib/logging.ts +0 -138
  33. package/extensions/plan-tracker.ts +0 -508
  34. package/extensions/subagent/agents.ts +0 -144
  35. package/extensions/subagent/concurrency.ts +0 -52
  36. package/extensions/subagent/env.ts +0 -47
  37. package/extensions/subagent/index.ts +0 -1181
  38. package/extensions/subagent/lifecycle.ts +0 -25
  39. package/extensions/subagent/timeout.ts +0 -13
  40. package/extensions/workflow-monitor/debug-monitor.ts +0 -98
  41. package/extensions/workflow-monitor/git.ts +0 -31
  42. package/extensions/workflow-monitor/heuristics.ts +0 -58
  43. package/extensions/workflow-monitor/investigation.ts +0 -52
  44. package/extensions/workflow-monitor/reference-tool.ts +0 -42
  45. package/extensions/workflow-monitor/skip-confirmation.ts +0 -19
  46. package/extensions/workflow-monitor/tdd-monitor.ts +0 -137
  47. package/extensions/workflow-monitor/test-runner.ts +0 -37
  48. package/extensions/workflow-monitor/verification-monitor.ts +0 -61
  49. package/extensions/workflow-monitor/warnings.ts +0 -81
  50. package/extensions/workflow-monitor/workflow-handler.ts +0 -363
  51. package/extensions/workflow-monitor/workflow-next-completions.ts +0 -68
  52. package/extensions/workflow-monitor/workflow-next-state.ts +0 -112
  53. package/extensions/workflow-monitor/workflow-tracker.ts +0 -286
  54. package/extensions/workflow-monitor/workflow-transitions.ts +0 -88
  55. package/extensions/workflow-monitor.ts +0 -909
  56. package/skills/dispatching-parallel-agents/SKILL.md +0 -194
  57. package/skills/receiving-code-review/SKILL.md +0 -196
  58. package/skills/systematic-debugging/SKILL.md +0 -170
  59. package/skills/systematic-debugging/condition-based-waiting-example.ts +0 -158
  60. package/skills/systematic-debugging/condition-based-waiting.md +0 -115
  61. package/skills/systematic-debugging/defense-in-depth.md +0 -122
  62. package/skills/systematic-debugging/find-polluter.sh +0 -63
  63. package/skills/systematic-debugging/reference/rationalizations.md +0 -61
  64. package/skills/systematic-debugging/root-cause-tracing.md +0 -169
  65. package/skills/test-driven-development/SKILL.md +0 -266
  66. package/skills/test-driven-development/reference/examples.md +0 -101
  67. package/skills/test-driven-development/reference/rationalizations.md +0 -67
  68. package/skills/test-driven-development/reference/when-stuck.md +0 -33
  69. package/skills/test-driven-development/testing-anti-patterns.md +0 -299
  70. package/skills/using-git-worktrees/SKILL.md +0 -231
@@ -1,363 +0,0 @@
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
- completeWorkflowPhase(phase: Phase): boolean;
92
- advanceWorkflowTo(phase: Phase): boolean;
93
- skipWorkflowPhases(phases: Phase[]): boolean;
94
- handleSkillFileRead(path: string): boolean;
95
- resetState(): void;
96
- }
97
-
98
- function deriveNonCodeMode(tasks: PlanTrackerTask[]): boolean {
99
- const activeTask = tasks.find((task) => task.status === "in_progress");
100
- return activeTask?.type === "non-code";
101
- }
102
-
103
- function areAllTasksTerminal(tasks: PlanTrackerTask[]): boolean {
104
- return tasks.length > 0 && tasks.every((task) => task.status === "complete" || task.status === "blocked");
105
- }
106
-
107
- export function createWorkflowHandler(): WorkflowHandler {
108
- const tdd = new TddMonitor();
109
- const debug = new DebugMonitor();
110
- const verification = new VerificationMonitor();
111
- const tracker = new WorkflowTracker();
112
- let debugFailStreak = 0;
113
-
114
- return {
115
- handleToolCall(toolName: string, input: Record<string, unknown>): ToolCallResult {
116
- // Track investigation from tool calls (LSP, kota, web search)
117
- if (isInvestigationToolCall(toolName, input)) {
118
- debug.onInvestigation();
119
- }
120
-
121
- if (toolName === "write" || toolName === "edit") {
122
- const path = input.path as string | undefined;
123
- if (path) {
124
- if (isSourceFile(path)) {
125
- verification.onSourceWritten();
126
- }
127
-
128
- // Debug violations take precedence, and when debug is active we don't
129
- // additionally enforce TDD write-order violations.
130
- if (debug.isActive() && isSourceFile(path)) {
131
- const debugViolation = debug.onSourceWritten(path);
132
- return { violation: debugViolation };
133
- }
134
-
135
- const tddViolation = tdd.onFileWritten(path);
136
- return { violation: tddViolation };
137
- }
138
- }
139
- return { violation: null };
140
- },
141
-
142
- handleReadOrInvestigation(toolName: string, _path: string): void {
143
- if (toolName === "read") {
144
- debug.onInvestigation();
145
- }
146
- },
147
-
148
- handleBashResult(command: string, output: string, exitCode: number | undefined): void {
149
- if (isInvestigationCommand(command)) {
150
- debug.onInvestigation();
151
- }
152
-
153
- if (/\bgit\s+commit\b/.test(command)) {
154
- debugFailStreak = 0;
155
- tdd.onCommit();
156
- debug.onCommit();
157
- return;
158
- }
159
-
160
- if (parseTestCommand(command)) {
161
- const passed = parseTestResult(output, exitCode);
162
- if (passed !== null) {
163
- if (passed) {
164
- verification.recordVerification();
165
- } else {
166
- verification.reset();
167
- }
168
-
169
- const excludeFromDebug = !passed && tdd.getPhase() === "red-pending";
170
-
171
- tdd.onTestResult(passed);
172
-
173
- if (passed) {
174
- debugFailStreak = 0;
175
- debug.onTestPassed();
176
- } else if (!excludeFromDebug) {
177
- debugFailStreak += 1;
178
- const tddPhase = tdd.getPhase();
179
- if (debugFailStreak >= 1 && tddPhase === "idle") {
180
- debug.onTestFailed();
181
- }
182
- }
183
- }
184
- }
185
- },
186
-
187
- handleBashInvestigation(command: string): void {
188
- if (isInvestigationCommand(command)) {
189
- debug.onInvestigation();
190
- }
191
- },
192
-
193
- isDebugActive(): boolean {
194
- return debug.isActive();
195
- },
196
-
197
- getDebugFixAttempts(): number {
198
- return debug.getFixAttempts();
199
- },
200
-
201
- getTddPhase(): string {
202
- return tdd.getPhase();
203
- },
204
-
205
- getWidgetText(): string {
206
- const parts: string[] = [];
207
-
208
- const phase = tdd.getPhase();
209
- if (phase !== "idle") {
210
- parts.push(`TDD: ${phase.toUpperCase()}`);
211
- }
212
-
213
- if (debug.isActive()) {
214
- parts.push("Debug: ACTIVE");
215
- }
216
-
217
- return parts.join(" | ");
218
- },
219
-
220
- getTddState() {
221
- return tdd.getState();
222
- },
223
-
224
- checkCommitGate(command: string) {
225
- return verification.checkCommitGate(command);
226
- },
227
-
228
- recordVerificationWaiver() {
229
- verification.recordVerificationWaiver();
230
- },
231
-
232
- restoreTddState(phase: TddPhase, testFiles: string[], sourceFiles: string[], redVerificationPending = false) {
233
- tdd.setState(phase, testFiles, sourceFiles, redVerificationPending);
234
- },
235
-
236
- handleInputText(text: string) {
237
- return tracker.onInputText(text);
238
- },
239
-
240
- handleFileWritten(path: string) {
241
- return tracker.onFileWritten(path);
242
- },
243
-
244
- handlePlanTrackerToolCall(input: Record<string, unknown>) {
245
- if (input.action === "init") {
246
- tdd.setNonCodeMode(false);
247
- return tracker.onPlanTrackerInit();
248
- }
249
- return false;
250
- },
251
-
252
- handlePlanTrackerToolResult(details: PlanTrackerDetails | undefined) {
253
- if (!details) return false;
254
-
255
- let changed = false;
256
- const nextNonCodeMode = deriveNonCodeMode(details.tasks);
257
- if (tdd.getState().nonCodeMode !== nextNonCodeMode) {
258
- tdd.setNonCodeMode(nextNonCodeMode);
259
- changed = true;
260
- }
261
-
262
- if (areAllTasksTerminal(details.tasks) && tracker.getState().currentPhase === "execute") {
263
- changed = tracker.completeCurrent() || changed;
264
- }
265
-
266
- return changed;
267
- },
268
-
269
- getWorkflowState() {
270
- return tracker.getState();
271
- },
272
-
273
- getFullState() {
274
- return {
275
- workflow: tracker.getState(),
276
- tdd: tdd.getState(),
277
- debug: debug.getState(),
278
- verification: verification.getState(),
279
- };
280
- },
281
-
282
- setFullState(snapshot: SuperpowersStatePatch) {
283
- if (snapshot.workflow) {
284
- const defaultWorkflow = new WorkflowTracker().getState();
285
- tracker.setState({
286
- ...defaultWorkflow,
287
- ...snapshot.workflow,
288
- phases: { ...defaultWorkflow.phases, ...snapshot.workflow.phases },
289
- artifacts: { ...defaultWorkflow.artifacts, ...snapshot.workflow.artifacts },
290
- prompted: { ...defaultWorkflow.prompted, ...snapshot.workflow.prompted },
291
- });
292
- }
293
- if (snapshot.tdd) {
294
- const tddState = { ...TDD_DEFAULTS, ...snapshot.tdd };
295
- tdd.setState(
296
- tddState.phase,
297
- tddState.testFiles,
298
- tddState.sourceFiles,
299
- tddState.redVerificationPending,
300
- tddState.nonCodeMode,
301
- );
302
- }
303
- if (snapshot.debug) {
304
- debug.setState({ ...DEBUG_DEFAULTS, ...snapshot.debug });
305
- }
306
- if (snapshot.verification) {
307
- verification.setState({ ...VERIFICATION_DEFAULTS, ...snapshot.verification });
308
- }
309
- debugFailStreak = 0;
310
- },
311
-
312
- restoreWorkflowStateFromBranch(branch: SessionEntry[]) {
313
- const state = WorkflowTracker.reconstructFromBranch(branch);
314
- if (state) {
315
- tracker.setState(state);
316
- }
317
- },
318
-
319
- markWorkflowPrompted(phase: Phase) {
320
- return tracker.markPrompted(phase);
321
- },
322
-
323
- completeCurrentWorkflowPhase() {
324
- return tracker.completeCurrent();
325
- },
326
-
327
- completeWorkflowPhase(phase: Phase) {
328
- return tracker.completePhase(phase);
329
- },
330
-
331
- advanceWorkflowTo(phase) {
332
- return tracker.advanceTo(phase);
333
- },
334
-
335
- skipWorkflowPhases(phases) {
336
- return tracker.skipPhases(phases);
337
- },
338
-
339
- handleSkillFileRead(path: string) {
340
- return tracker.onSkillFileRead(path);
341
- },
342
-
343
- resetState() {
344
- const freshState: SuperpowersStateSnapshot = {
345
- workflow: new WorkflowTracker().getState(),
346
- tdd: { ...TDD_DEFAULTS, testFiles: [], sourceFiles: [] },
347
- debug: { ...DEBUG_DEFAULTS },
348
- verification: { ...VERIFICATION_DEFAULTS },
349
- };
350
-
351
- tracker.setState(freshState.workflow);
352
- tdd.setState(
353
- freshState.tdd.phase,
354
- freshState.tdd.testFiles,
355
- freshState.tdd.sourceFiles,
356
- freshState.tdd.redVerificationPending,
357
- );
358
- debug.setState(freshState.debug);
359
- verification.setState(freshState.verification);
360
- debugFailStreak = 0;
361
- },
362
- };
363
- }
@@ -1,68 +0,0 @@
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
- }
@@ -1,112 +0,0 @@
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
- }