@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.
Files changed (63) hide show
  1. package/README.md +44 -494
  2. package/docs/developer-usage-guide.md +41 -401
  3. package/docs/oversight-model.md +13 -34
  4. package/docs/workflow-phases.md +32 -46
  5. package/extensions/workflow-guard.ts +67 -0
  6. package/package.json +3 -7
  7. package/skills/brainstorming/SKILL.md +16 -67
  8. package/skills/executing-tasks/SKILL.md +26 -227
  9. package/skills/finalizing/SKILL.md +33 -0
  10. package/skills/writing-plans/SKILL.md +23 -132
  11. package/ROADMAP.md +0 -16
  12. package/agents/code-reviewer.md +0 -18
  13. package/agents/config.ts +0 -5
  14. package/agents/implementer.md +0 -26
  15. package/agents/spec-reviewer.md +0 -13
  16. package/agents/worker.md +0 -17
  17. package/docs/plans/2026-04-10-brainstorming-boundary-enforcement-design.md +0 -60
  18. package/docs/plans/completed/2026-04-09-cleanup-legacy-state-and-enforce-think-phases-design.md +0 -56
  19. package/docs/plans/completed/2026-04-09-cleanup-legacy-state-and-enforce-think-phases-implementation.md +0 -196
  20. package/docs/plans/completed/2026-04-09-workflow-next-autocomplete-design.md +0 -185
  21. package/docs/plans/completed/2026-04-09-workflow-next-autocomplete-implementation.md +0 -334
  22. package/docs/plans/completed/2026-04-09-workflow-next-handoff-state-design.md +0 -251
  23. package/docs/plans/completed/2026-04-09-workflow-next-handoff-state-implementation.md +0 -253
  24. package/extensions/constants.ts +0 -15
  25. package/extensions/lib/logging.ts +0 -138
  26. package/extensions/plan-tracker.ts +0 -508
  27. package/extensions/subagent/agents.ts +0 -144
  28. package/extensions/subagent/concurrency.ts +0 -52
  29. package/extensions/subagent/env.ts +0 -47
  30. package/extensions/subagent/index.ts +0 -1181
  31. package/extensions/subagent/lifecycle.ts +0 -25
  32. package/extensions/subagent/timeout.ts +0 -13
  33. package/extensions/workflow-monitor/debug-monitor.ts +0 -98
  34. package/extensions/workflow-monitor/git.ts +0 -31
  35. package/extensions/workflow-monitor/heuristics.ts +0 -58
  36. package/extensions/workflow-monitor/investigation.ts +0 -52
  37. package/extensions/workflow-monitor/reference-tool.ts +0 -42
  38. package/extensions/workflow-monitor/skip-confirmation.ts +0 -19
  39. package/extensions/workflow-monitor/tdd-monitor.ts +0 -137
  40. package/extensions/workflow-monitor/test-runner.ts +0 -37
  41. package/extensions/workflow-monitor/verification-monitor.ts +0 -61
  42. package/extensions/workflow-monitor/warnings.ts +0 -81
  43. package/extensions/workflow-monitor/workflow-handler.ts +0 -363
  44. package/extensions/workflow-monitor/workflow-next-completions.ts +0 -68
  45. package/extensions/workflow-monitor/workflow-next-state.ts +0 -112
  46. package/extensions/workflow-monitor/workflow-tracker.ts +0 -286
  47. package/extensions/workflow-monitor/workflow-transitions.ts +0 -88
  48. package/extensions/workflow-monitor.ts +0 -909
  49. package/skills/dispatching-parallel-agents/SKILL.md +0 -194
  50. package/skills/receiving-code-review/SKILL.md +0 -196
  51. package/skills/systematic-debugging/SKILL.md +0 -170
  52. package/skills/systematic-debugging/condition-based-waiting-example.ts +0 -158
  53. package/skills/systematic-debugging/condition-based-waiting.md +0 -115
  54. package/skills/systematic-debugging/defense-in-depth.md +0 -122
  55. package/skills/systematic-debugging/find-polluter.sh +0 -63
  56. package/skills/systematic-debugging/reference/rationalizations.md +0 -61
  57. package/skills/systematic-debugging/root-cause-tracing.md +0 -169
  58. package/skills/test-driven-development/SKILL.md +0 -266
  59. package/skills/test-driven-development/reference/examples.md +0 -101
  60. package/skills/test-driven-development/reference/rationalizations.md +0 -67
  61. package/skills/test-driven-development/reference/when-stuck.md +0 -33
  62. package/skills/test-driven-development/testing-anti-patterns.md +0 -299
  63. 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
- }