@tianhai/pi-workflow-kit 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,14 +8,12 @@
8
8
  * - Register workflow_reference tool for on-demand reference content
9
9
  */
10
10
 
11
- import * as fs from "node:fs";
12
11
  import * as path from "node:path";
13
12
  import { StringEnum } from "@mariozechner/pi-ai";
14
13
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
15
14
  import { Text } from "@mariozechner/pi-tui";
16
15
  import { Type } from "@sinclair/typebox";
17
- import { PLAN_TRACKER_TOOL_NAME } from "./constants.js";
18
- import { log } from "./lib/logging.js";
16
+ import { PLAN_TRACKER_CLEARED_TYPE, PLAN_TRACKER_TOOL_NAME } from "./constants.js";
19
17
  import type { PlanTrackerDetails } from "./plan-tracker.js";
20
18
  import { getCurrentGitRef } from "./workflow-monitor/git";
21
19
  import { loadReference, REFERENCE_TOPICS } from "./workflow-monitor/reference-tool";
@@ -27,7 +25,16 @@ import {
27
25
  getTddViolationWarning,
28
26
  getVerificationViolationWarning,
29
27
  } from "./workflow-monitor/warnings";
30
- import { createWorkflowHandler, type Violation, type WorkflowHandler } from "./workflow-monitor/workflow-handler";
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";
31
38
  import {
32
39
  computeBoundaryToPrompt,
33
40
  type Phase,
@@ -55,64 +62,32 @@ async function selectValue<T extends string>(
55
62
 
56
63
  const SUPERPOWERS_STATE_ENTRY_TYPE = "superpowers_state";
57
64
 
58
- export function getStateFilePath(): string {
59
- return path.join(process.cwd(), ".pi", "superpowers-state.json");
60
- }
61
-
62
- export function reconstructState(ctx: ExtensionContext, handler: WorkflowHandler, stateFilePath?: string | false) {
65
+ export function reconstructState(ctx: ExtensionContext, handler: WorkflowHandler) {
63
66
  handler.resetState();
64
67
 
65
- // Read both file-based and session-based state, then pick the newer one.
66
- let fileData: (Record<string, unknown> & { savedAt?: number }) | null = null;
67
- let sessionData: (Record<string, unknown> & { savedAt?: number }) | null = null;
68
-
69
- if (stateFilePath !== false) {
70
- try {
71
- const statePath = stateFilePath ?? getStateFilePath();
72
- if (fs.existsSync(statePath)) {
73
- const raw = fs.readFileSync(statePath, "utf-8");
74
- fileData = JSON.parse(raw);
75
- }
76
- } catch (err) {
77
- log.warn(
78
- `Failed to read state file, falling back to session entries: ${err instanceof Error ? err.message : err}`,
79
- );
80
- }
81
- }
82
-
83
- // Scan session branch for most recent superpowers state entry
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.
84
71
  const entries = ctx.sessionManager.getBranch();
85
72
  for (let i = entries.length - 1; i >= 0; i--) {
86
73
  const entry = entries[i];
87
74
  // biome-ignore lint/suspicious/noExplicitAny: pi SDK session entry type
88
75
  if (entry.type === "custom" && (entry as any).customType === SUPERPOWERS_STATE_ENTRY_TYPE) {
89
76
  // biome-ignore lint/suspicious/noExplicitAny: pi SDK session entry type
90
- sessionData = (entry as any).data;
91
- break;
77
+ handler.setFullState((entry as any).data);
78
+ return;
92
79
  }
93
80
  // Migration fallback: old-format workflow-only entries
94
81
  // biome-ignore lint/suspicious/noExplicitAny: pi SDK session entry type
95
82
  if (entry.type === "custom" && (entry as any).customType === WORKFLOW_TRACKER_ENTRY_TYPE) {
96
83
  // biome-ignore lint/suspicious/noExplicitAny: pi SDK session entry type
97
- sessionData = { workflow: (entry as any).data };
98
- break;
84
+ handler.setFullState({ workflow: (entry as any).data });
85
+ return;
99
86
  }
100
87
  }
101
88
 
102
- // Pick the newer source when both are available; otherwise use whichever exists.
103
- if (fileData && sessionData) {
104
- const fileSavedAt = fileData.savedAt ?? 0;
105
- const sessionSavedAt = sessionData.savedAt ?? 0;
106
- const winner = fileSavedAt >= sessionSavedAt ? fileData : sessionData;
107
- handler.setFullState(winner);
108
- } else if (fileData) {
109
- handler.setFullState(fileData);
110
- } else if (sessionData) {
111
- handler.setFullState(sessionData);
112
- } else {
113
- // No entries found — reset to fresh defaults
114
- handler.setFullState({});
115
- }
89
+ // No entries found reset to fresh defaults
90
+ handler.setFullState({});
116
91
  }
117
92
 
118
93
  export default function (pi: ExtensionAPI) {
@@ -123,10 +98,9 @@ export default function (pi: ExtensionAPI) {
123
98
  const pendingViolations = new Map<string, Violation>();
124
99
  const pendingVerificationViolations = new Map<string, VerificationViolation>();
125
100
  const pendingBranchGates = new Map<string, string>();
126
- const pendingProcessWarnings = new Map<string, string>();
127
101
 
128
- type ViolationBucket = "process" | "practice";
129
- const strikes: Record<ViolationBucket, number> = { process: 0, practice: 0 };
102
+ type ViolationBucket = "practice";
103
+ const strikes: Record<ViolationBucket, number> = { practice: 0 };
130
104
  const sessionAllowed: Partial<Record<ViolationBucket, boolean>> = {};
131
105
 
132
106
  async function maybeEscalate(bucket: ViolationBucket, ctx: ExtensionContext): Promise<"allow" | "block"> {
@@ -160,14 +134,6 @@ export default function (pi: ExtensionAPI) {
160
134
  const persistState = () => {
161
135
  const stateWithTimestamp = { ...handler.getFullState(), savedAt: Date.now() };
162
136
  pi.appendEntry(SUPERPOWERS_STATE_ENTRY_TYPE, stateWithTimestamp);
163
- // Also persist to file for cross-session survival
164
- try {
165
- const statePath = getStateFilePath();
166
- fs.mkdirSync(path.dirname(statePath), { recursive: true });
167
- fs.writeFileSync(statePath, JSON.stringify(stateWithTimestamp, null, 2));
168
- } catch (err) {
169
- log.warn(`Failed to persist state file: ${err instanceof Error ? err.message : err}`);
170
- }
171
137
  };
172
138
 
173
139
  const phaseToSkill: Record<string, string> = {
@@ -210,10 +176,7 @@ export default function (pi: ExtensionAPI) {
210
176
  pendingViolations.clear();
211
177
  pendingVerificationViolations.clear();
212
178
  pendingBranchGates.clear();
213
- pendingProcessWarnings.clear();
214
- strikes.process = 0;
215
179
  strikes.practice = 0;
216
- delete sessionAllowed.process;
217
180
  delete sessionAllowed.practice;
218
181
  branchNoticeShown = false;
219
182
  branchConfirmed = false;
@@ -494,16 +457,13 @@ export default function (pi: ExtensionAPI) {
494
457
  const isPlansWrite = resolved.startsWith(plansRoot);
495
458
 
496
459
  if (isThinkingPhase && !isPlansWrite) {
497
- const escalation = await maybeEscalate("process", ctx);
498
- if (escalation === "block") {
499
- return { blocked: true };
500
- }
501
-
502
- pendingProcessWarnings.set(
503
- toolCallId,
504
- `⚠️ PROCESS VIOLATION: Wrote ${filePath} during ${phase} phase.\n` +
505
- "During brainstorming/planning you may only write to docs/plans/. Stop and return to docs/plans/ or advance workflow phases intentionally.",
506
- );
460
+ return {
461
+ blocked: true,
462
+ reason:
463
+ `⚠️ PROCESS VIOLATION: Wrote ${filePath} during ${phase} phase.\n` +
464
+ "During brainstorming/planning you may only write to docs/plans/. " +
465
+ "Read code and docs to understand the problem, then discuss the design before implementing.",
466
+ };
507
467
  }
508
468
 
509
469
  changed = handler.handleFileWritten(filePath) || changed;
@@ -595,12 +555,6 @@ export default function (pi: ExtensionAPI) {
595
555
  }
596
556
  }
597
557
  pendingViolations.delete(toolCallId);
598
-
599
- const processWarning = pendingProcessWarnings.get(toolCallId);
600
- if (processWarning) {
601
- injected.push(processWarning);
602
- }
603
- pendingProcessWarnings.delete(toolCallId);
604
558
  }
605
559
 
606
560
  // Handle bash results (test runs, commits, investigation)
@@ -802,6 +756,8 @@ export default function (pi: ExtensionAPI) {
802
756
  description: "Reset workflow tracker to fresh state for a new task",
803
757
  async handler(_args, ctx) {
804
758
  handler.resetState();
759
+ // Emit a clear signal so plan-tracker also reconstructs to empty.
760
+ pi.appendEntry(PLAN_TRACKER_CLEARED_TYPE, { clearedAt: Date.now() });
805
761
  persistState();
806
762
  updateWidget(ctx);
807
763
  if (ctx.hasUI) {
@@ -812,6 +768,7 @@ export default function (pi: ExtensionAPI) {
812
768
 
813
769
  pi.registerCommand("workflow-next", {
814
770
  description: "Start a fresh session for the next workflow phase (optionally referencing an artifact path)",
771
+ getArgumentCompletions: getWorkflowNextCompletions,
815
772
  async handler(args, ctx) {
816
773
  if (!ctx.hasUI) {
817
774
  ctx.ui.notify("workflow-next requires interactive mode", "error");
@@ -828,8 +785,38 @@ export default function (pi: ExtensionAPI) {
828
785
  return;
829
786
  }
830
787
 
788
+ // Validate handoff against current workflow state
789
+ const currentWorkflowState = handler.getWorkflowState();
790
+ if (currentWorkflowState?.currentPhase) {
791
+ const validationError = validateNextWorkflowPhase(currentWorkflowState, phase as Phase);
792
+ if (validationError) {
793
+ ctx.ui.notify(validationError, "error");
794
+ return;
795
+ }
796
+ }
797
+
798
+ // Derive handoff state for session seeding
799
+ const derivedWorkflow = currentWorkflowState
800
+ ? deriveWorkflowHandoffState(currentWorkflowState, phase as Phase)
801
+ : undefined;
802
+
831
803
  const parentSession = ctx.sessionManager.getSessionFile();
832
- const res = await ctx.newSession({ parentSession });
804
+ const res = await ctx.newSession({
805
+ parentSession,
806
+ setup: derivedWorkflow
807
+ ? async (sm) => {
808
+ const fullState = handler.getFullState();
809
+ sm.appendCustomEntry(SUPERPOWERS_STATE_ENTRY_TYPE, {
810
+ ...fullState,
811
+ workflow: derivedWorkflow,
812
+ tdd: { ...TDD_DEFAULTS, testFiles: [], sourceFiles: [] },
813
+ debug: { ...DEBUG_DEFAULTS },
814
+ verification: { ...VERIFICATION_DEFAULTS },
815
+ savedAt: Date.now(),
816
+ });
817
+ }
818
+ : undefined,
819
+ });
833
820
  if (res.cancelled) return;
834
821
 
835
822
  const lines: string[] = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tianhai/pi-workflow-kit",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "Workflow skills and enforcement extensions for pi",
5
5
  "keywords": [
6
6
  "pi-package"