@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.
- package/README.md +29 -5
- package/docs/developer-usage-guide.md +3 -4
- package/docs/plans/completed/2026-04-09-cleanup-legacy-state-and-enforce-think-phases-design.md +56 -0
- package/docs/plans/completed/2026-04-09-cleanup-legacy-state-and-enforce-think-phases-implementation.md +196 -0
- package/docs/plans/completed/2026-04-09-workflow-next-autocomplete-design.md +185 -0
- package/docs/plans/completed/2026-04-09-workflow-next-autocomplete-implementation.md +334 -0
- package/docs/plans/completed/2026-04-09-workflow-next-handoff-state-design.md +251 -0
- package/docs/plans/completed/2026-04-09-workflow-next-handoff-state-implementation.md +253 -0
- package/extensions/constants.ts +6 -0
- package/extensions/plan-tracker.ts +7 -1
- package/extensions/subagent/index.ts +73 -8
- package/extensions/workflow-monitor/workflow-next-completions.ts +68 -0
- package/extensions/workflow-monitor/workflow-next-state.ts +112 -0
- package/extensions/workflow-monitor/workflow-tracker.ts +28 -6
- package/extensions/workflow-monitor.ts +64 -77
- package/package.json +1 -1
|
@@ -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 {
|
|
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
|
|
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
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
98
|
-
|
|
84
|
+
handler.setFullState({ workflow: (entry as any).data });
|
|
85
|
+
return;
|
|
99
86
|
}
|
|
100
87
|
}
|
|
101
88
|
|
|
102
|
-
//
|
|
103
|
-
|
|
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 = "
|
|
129
|
-
const strikes: Record<ViolationBucket, number> = {
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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({
|
|
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[] = [];
|