@tianhai/pi-workflow-kit 0.5.0 → 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/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/extensions/constants.ts +6 -0
- package/extensions/plan-tracker.ts +7 -1
- package/extensions/workflow-monitor/workflow-next-completions.ts +1 -1
- package/extensions/workflow-monitor/workflow-tracker.ts +28 -6
- package/extensions/workflow-monitor.ts +22 -86
- package/package.json +1 -1
package/docs/plans/completed/2026-04-09-cleanup-legacy-state-and-enforce-think-phases-design.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Cleanup legacy state file and enforce thinking-phase boundaries
|
|
2
|
+
|
|
3
|
+
Date: 2026-04-09
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
1. **Legacy state file still read:** `workflow-monitor.ts` still has `getLegacyStateFilePath()` and a fallback read path for `.pi/superpowers-state.json`. The new filename (`workflow-kit-state.json`) is correct for writes, but the old one is still checked on read.
|
|
8
|
+
|
|
9
|
+
2. **Brainstorm/plan phases not enforced:** When the agent writes code during brainstorm or plan phase, the extension only warns (strike-based escalation allows first offense). The agent can ignore the warning and proceed with implementation work.
|
|
10
|
+
|
|
11
|
+
3. **`/workflow-next` tab completions lose the phase word:** When selecting a file artifact completion (e.g. `docs/plans/2026-04-09-foo-design.md`), pi replaces the entire argument prefix including the phase word. `/workflow-next plan des` → select file → `/workflow-next docs/plans/...` ("plan" gone).
|
|
12
|
+
|
|
13
|
+
Note: Tab key does not trigger slash command argument completions at all (pi-side behavior — falls through to file path completion when `force=true`). Argument suggestions only appear on typing. This cannot be fixed on our side.
|
|
14
|
+
|
|
15
|
+
## Scope
|
|
16
|
+
|
|
17
|
+
Three changes:
|
|
18
|
+
|
|
19
|
+
### 1. Remove `superpowers-state.json` legacy fallback
|
|
20
|
+
|
|
21
|
+
**`extensions/workflow-monitor.ts`:**
|
|
22
|
+
- Remove `getLegacyStateFilePath()` function
|
|
23
|
+
- In `reconstructState()`, remove the `else if (stateFilePath === undefined)` branch that reads the legacy filename
|
|
24
|
+
|
|
25
|
+
**`tests/extension/workflow-monitor/state-persistence.test.ts`:**
|
|
26
|
+
- Update two tests in `"file-based state persistence"` describe that write/read `superpowers-state.json` to use `workflow-kit-state.json`
|
|
27
|
+
- Fix test name `"getStateFilePath returns .pi/superpowers-state.json in cwd"`
|
|
28
|
+
- Remove the `"state file rename to .pi/workflow-kit-state.json with legacy fallback"` describe block (4 tests) — this migration behavior no longer exists
|
|
29
|
+
|
|
30
|
+
### 2. Enforce brainstorm/plan phase boundaries
|
|
31
|
+
|
|
32
|
+
**`extensions/workflow-monitor.ts`:**
|
|
33
|
+
- Replace the `maybeEscalate("process", ctx)` call + `pendingProcessWarnings.set(...)` with an immediate `{ blocked: true, reason: ... }` return that includes a reminder about what the agent should be doing instead
|
|
34
|
+
- Remove `"process"` from `ViolationBucket` type and `strikes` record (only `"practice"` remains, still used by TDD)
|
|
35
|
+
- Remove `pendingProcessWarnings` map (no longer needed)
|
|
36
|
+
|
|
37
|
+
### 3. Fix phase word lost on artifact completion selection
|
|
38
|
+
|
|
39
|
+
**`extensions/workflow-monitor/workflow-next-completions.ts`:**
|
|
40
|
+
- In `listArtifactsForPhase`, prepend the phase to `item.value` so pi's prefix replacement preserves it:
|
|
41
|
+
- `value: "plan docs/plans/2026-04-09-foo-design.md"` (replaces "plan des" → keeps "plan")
|
|
42
|
+
- `label: "docs/plans/2026-04-09-foo-design.md"` (display stays clean)
|
|
43
|
+
- Applies to all phases with artifacts: plan, execute, finalize
|
|
44
|
+
|
|
45
|
+
### Tests
|
|
46
|
+
|
|
47
|
+
**`tests/extension/workflow-monitor/workflow-next-completions.test.ts`:**
|
|
48
|
+
- Update artifact completion tests to expect `value` with phase prefix (e.g. `"plan docs/plans/..."`)
|
|
49
|
+
|
|
50
|
+
## What stays the same
|
|
51
|
+
|
|
52
|
+
- `persistState()` already writes only to `.pi/workflow-kit-state.json` — no changes
|
|
53
|
+
- `maybeEscalate()` stays (still used for `"practice"` / TDD violations)
|
|
54
|
+
- The `isThinkingPhase` check (`phase === "brainstorm" || phase === "plan"`) already covers both phases — same treatment
|
|
55
|
+
- `docs/plans/` writes remain allowed during brainstorm/plan phases
|
|
56
|
+
- Tab not triggering argument completions is a pi-side issue — not in scope
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Cleanup legacy state, enforce thinking phases, fix autocomplete
|
|
2
|
+
|
|
3
|
+
> **REQUIRED SUB-SKILL:** Use the executing-tasks skill to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Remove legacy `superpowers-state.json` fallback, block non-plans writes during brainstorm/plan phases, and fix artifact completions losing the phase word.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Three independent changes in `workflow-monitor.ts` and `workflow-next-completions.ts` with corresponding test updates. Each change is self-contained.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, Node.js, vitest, pi extension API
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
### Task 1: Remove `superpowers-state.json` legacy fallback
|
|
14
|
+
|
|
15
|
+
**Type:** code
|
|
16
|
+
**TDD scenario:** Modifying tested code — run existing tests first
|
|
17
|
+
|
|
18
|
+
**Files:**
|
|
19
|
+
- Modify: `extensions/workflow-monitor.ts:67-100`
|
|
20
|
+
- Modify: `tests/extension/workflow-monitor/state-persistence.test.ts:274-500`
|
|
21
|
+
|
|
22
|
+
**Step 1: Run existing state persistence tests**
|
|
23
|
+
|
|
24
|
+
Run: `npx vitest run tests/extension/workflow-monitor/state-persistence.test.ts`
|
|
25
|
+
Expected: All tests pass
|
|
26
|
+
|
|
27
|
+
**Step 2: Remove `getLegacyStateFilePath` and legacy fallback in source**
|
|
28
|
+
|
|
29
|
+
In `extensions/workflow-monitor.ts`:
|
|
30
|
+
|
|
31
|
+
1. Delete the `getLegacyStateFilePath` function (3 lines).
|
|
32
|
+
2. In `reconstructState`, simplify the file-read block — remove the `else if (stateFilePath === undefined)` branch that tries the legacy filename. The remaining code becomes:
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
if (stateFilePath !== false) {
|
|
36
|
+
try {
|
|
37
|
+
const statePath = stateFilePath ?? getStateFilePath();
|
|
38
|
+
if (fs.existsSync(statePath)) {
|
|
39
|
+
const raw = fs.readFileSync(statePath, "utf-8");
|
|
40
|
+
fileData = JSON.parse(raw);
|
|
41
|
+
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
log.warn(
|
|
44
|
+
`Failed to read state file, falling back to session entries: ${err instanceof Error ? err.message : err}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Step 3: Update tests**
|
|
51
|
+
|
|
52
|
+
In `tests/extension/workflow-monitor/state-persistence.test.ts`:
|
|
53
|
+
|
|
54
|
+
1. Fix test name on line 274: `"getStateFilePath returns .pi/superpowers-state.json in cwd"` → `"getStateFilePath returns .pi/workflow-kit-state.json in cwd"`
|
|
55
|
+
2. On line 318: change `"superpowers-state.json"` → `"workflow-kit-state.json"`
|
|
56
|
+
3. On line 337: change `"superpowers-state.json"` → `"workflow-kit-state.json"`
|
|
57
|
+
4. Delete the entire `describe("state file rename to .pi/workflow-kit-state.json with legacy fallback", () => { ... })` block (4 tests, from line ~404 to ~530)
|
|
58
|
+
|
|
59
|
+
**Step 4: Run tests**
|
|
60
|
+
|
|
61
|
+
Run: `npx vitest run tests/extension/workflow-monitor/state-persistence.test.ts`
|
|
62
|
+
Expected: All tests pass
|
|
63
|
+
|
|
64
|
+
**Step 5: Run full test suite**
|
|
65
|
+
|
|
66
|
+
Run: `npx vitest run`
|
|
67
|
+
Expected: All tests pass
|
|
68
|
+
|
|
69
|
+
**Step 6: Commit**
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
git add extensions/workflow-monitor.ts tests/extension/workflow-monitor/state-persistence.test.ts
|
|
73
|
+
git commit -m "refactor: remove superpowers-state.json legacy fallback"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
### Task 2: Enforce brainstorm/plan phase boundaries (block immediately)
|
|
79
|
+
|
|
80
|
+
**Type:** code
|
|
81
|
+
**TDD scenario:** Modifying tested code — update existing tests first
|
|
82
|
+
|
|
83
|
+
**Files:**
|
|
84
|
+
- Modify: `extensions/workflow-monitor.ts:135-520`
|
|
85
|
+
- Modify: `tests/extension/workflow-monitor/phase-aware-write-enforcement.test.ts`
|
|
86
|
+
|
|
87
|
+
**Step 1: Run existing enforcement tests**
|
|
88
|
+
|
|
89
|
+
Run: `npx vitest run tests/extension/workflow-monitor/phase-aware-write-enforcement.test.ts`
|
|
90
|
+
Expected: All tests pass
|
|
91
|
+
|
|
92
|
+
**Step 2: Update source — block immediately instead of escalating**
|
|
93
|
+
|
|
94
|
+
In `extensions/workflow-monitor.ts`:
|
|
95
|
+
|
|
96
|
+
1. Remove `pendingProcessWarnings` map declaration (line ~135).
|
|
97
|
+
2. Change `ViolationBucket` type from `"process" | "practice"` to `"practice"`.
|
|
98
|
+
3. Change `strikes` from `{ process: 0, practice: 0 }` to `{ practice: 0 }`.
|
|
99
|
+
4. Change `sessionAllowed` type to `Partial<Record<"practice", boolean>>`.
|
|
100
|
+
5. In `maybeEscalate`, change parameter type from `ViolationBucket` to `"practice"`.
|
|
101
|
+
6. In `tool_result` handler, remove the `pendingProcessWarnings.get(toolCallId)` / `.delete(toolCallId)` block (3 lines).
|
|
102
|
+
7. In `tool_call` handler (~line 506-516), replace the `maybeEscalate("process", ctx)` + `pendingProcessWarnings.set(...)` block with:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
return {
|
|
106
|
+
blocked: true,
|
|
107
|
+
reason:
|
|
108
|
+
`⚠️ PROCESS VIOLATION: Wrote ${filePath} during ${phase} phase.\n` +
|
|
109
|
+
"During brainstorming/planning you may only write to docs/plans/. " +
|
|
110
|
+
"Read code and docs to understand the problem, then discuss the design before implementing.",
|
|
111
|
+
};
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Step 3: Update tests**
|
|
115
|
+
|
|
116
|
+
In `tests/extension/workflow-monitor/phase-aware-write-enforcement.test.ts`:
|
|
117
|
+
|
|
118
|
+
1. The first test (`"warns when writing outside docs/plans during brainstorm"`) currently checks for an injected warning in the tool result. After the change, the write is blocked in `on_tool_call` before it executes, so `on_tool_result` is never reached. Replace the test to check that `onToolCall` returns `{ blocked: true }` with a `reason` containing `"PROCESS VIOLATION"` and `"brainstorm"`. Remove the `onToolResult` call and its assertion.
|
|
119
|
+
|
|
120
|
+
2. Add a new test: `"blocks immediately on first violation during plan phase"` — same pattern as brainstorm but with `currentPhase: "plan"`, verify `{ blocked: true, reason: expect.stringContaining("PROCESS VIOLATION") }`.
|
|
121
|
+
|
|
122
|
+
3. The test `"second process violation hard-blocks (interactive)"` is no longer relevant — blocking is immediate now. Delete it.
|
|
123
|
+
|
|
124
|
+
**Step 4: Run enforcement tests**
|
|
125
|
+
|
|
126
|
+
Run: `npx vitest run tests/extension/workflow-monitor/phase-aware-write-enforcement.test.ts`
|
|
127
|
+
Expected: All tests pass
|
|
128
|
+
|
|
129
|
+
**Step 5: Run full test suite**
|
|
130
|
+
|
|
131
|
+
Run: `npx vitest run`
|
|
132
|
+
Expected: All tests pass
|
|
133
|
+
|
|
134
|
+
**Step 6: Commit**
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
git add extensions/workflow-monitor.ts tests/extension/workflow-monitor/phase-aware-write-enforcement.test.ts
|
|
138
|
+
git commit -m "feat: block writes outside docs/plans immediately during brainstorm/plan phases"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### Task 3: Fix artifact completions losing the phase word
|
|
144
|
+
|
|
145
|
+
**Type:** code
|
|
146
|
+
**TDD scenario:** Modifying tested code — update existing tests first
|
|
147
|
+
|
|
148
|
+
**Files:**
|
|
149
|
+
- Modify: `extensions/workflow-monitor/workflow-next-completions.ts:50-60`
|
|
150
|
+
- Modify: `tests/extension/workflow-monitor/workflow-next-command.test.ts`
|
|
151
|
+
|
|
152
|
+
**Step 1: Run existing completion tests**
|
|
153
|
+
|
|
154
|
+
Run: `npx vitest run tests/extension/workflow-monitor/workflow-next-command.test.ts`
|
|
155
|
+
Expected: All tests pass
|
|
156
|
+
|
|
157
|
+
**Step 2: Update source — prepend phase to artifact values**
|
|
158
|
+
|
|
159
|
+
In `extensions/workflow-monitor/workflow-next-completions.ts`, in `listArtifactsForPhase`, the function needs the `phase` parameter included in `item.value`. Change the final `.map()`:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// Before:
|
|
163
|
+
.map((relPath) => ({ value: relPath, label: relPath }));
|
|
164
|
+
|
|
165
|
+
// After:
|
|
166
|
+
.map((relPath) => ({ value: `${phase} ${relPath}`, label: relPath }));
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
This ensures pi's `applyCompletion` replaces the full prefix (e.g. `"plan des"`) with `"plan docs/plans/..."` — preserving the phase word.
|
|
170
|
+
|
|
171
|
+
**Step 3: Update tests**
|
|
172
|
+
|
|
173
|
+
In `tests/extension/workflow-monitor/workflow-next-command.test.ts`, update these tests to expect `value` with the phase prefix:
|
|
174
|
+
|
|
175
|
+
1. `"suggests only design artifacts for plan phase"` — change `value: "docs/plans/..."` to `value: "plan docs/plans/..."`, keep `label` unchanged.
|
|
176
|
+
|
|
177
|
+
2. `"filters plan artifact suggestions by typed prefix"` — same value change.
|
|
178
|
+
|
|
179
|
+
3. `"suggests only implementation artifacts for execute and finalize"` — change execute value to `"execute docs/plans/..."` and finalize value to `"finalize docs/plans/..."`.
|
|
180
|
+
|
|
181
|
+
**Step 4: Run completion tests**
|
|
182
|
+
|
|
183
|
+
Run: `npx vitest run tests/extension/workflow-monitor/workflow-next-command.test.ts`
|
|
184
|
+
Expected: All tests pass
|
|
185
|
+
|
|
186
|
+
**Step 5: Run full test suite**
|
|
187
|
+
|
|
188
|
+
Run: `npx vitest run`
|
|
189
|
+
Expected: All tests pass
|
|
190
|
+
|
|
191
|
+
**Step 6: Commit**
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
git add extensions/workflow-monitor/workflow-next-completions.ts tests/extension/workflow-monitor/workflow-next-command.test.ts
|
|
195
|
+
git commit -m "fix: preserve phase word in /workflow-next artifact completions"
|
|
196
|
+
```
|
package/extensions/constants.ts
CHANGED
|
@@ -7,3 +7,9 @@
|
|
|
7
7
|
* This tool id intentionally remains unchanged across the rebrand.
|
|
8
8
|
*/
|
|
9
9
|
export const PLAN_TRACKER_TOOL_NAME = "plan_tracker";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Custom entry type written by workflow-monitor's /workflow-reset so that
|
|
13
|
+
* plan-tracker's reconstructState picks up an empty task list.
|
|
14
|
+
*/
|
|
15
|
+
export const PLAN_TRACKER_CLEARED_TYPE = "plan_tracker_cleared";
|
|
@@ -10,7 +10,7 @@ import { StringEnum } from "@mariozechner/pi-ai";
|
|
|
10
10
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
|
|
11
11
|
import { Text } from "@mariozechner/pi-tui";
|
|
12
12
|
import { type Static, Type } from "@sinclair/typebox";
|
|
13
|
-
import { PLAN_TRACKER_TOOL_NAME } from "./constants.js";
|
|
13
|
+
import { PLAN_TRACKER_CLEARED_TYPE, PLAN_TRACKER_TOOL_NAME } from "./constants.js";
|
|
14
14
|
|
|
15
15
|
export type TaskStatus = "pending" | "in_progress" | "complete" | "blocked";
|
|
16
16
|
export type TaskPhase =
|
|
@@ -208,6 +208,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
208
208
|
const entries = ctx.sessionManager.getBranch();
|
|
209
209
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
210
210
|
const entry = entries[i];
|
|
211
|
+
// Check for explicit clear signal (written by /workflow-reset)
|
|
212
|
+
// biome-ignore lint/suspicious/noExplicitAny: pi SDK session entry type
|
|
213
|
+
if (entry.type === "custom" && (entry as any).customType === PLAN_TRACKER_CLEARED_TYPE) {
|
|
214
|
+
tasks = [];
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
211
217
|
if (entry.type !== "message") continue;
|
|
212
218
|
const msg = entry.message;
|
|
213
219
|
if (msg.role !== "toolResult" || msg.toolName !== PLAN_TRACKER_TOOL_NAME) continue;
|
|
@@ -42,7 +42,7 @@ function listArtifactsForPhase(phase: WorkflowNextPhase, typedPrefix: string): A
|
|
|
42
42
|
.filter((name) => name.endsWith(suffix))
|
|
43
43
|
.map((name) => path.join("docs", "plans", name))
|
|
44
44
|
.filter((relPath) => relPath.startsWith(typedPrefix))
|
|
45
|
-
.map((relPath) => ({ value: relPath
|
|
45
|
+
.map((relPath) => ({ value: `${phase} ${relPath}`, label: relPath }));
|
|
46
46
|
|
|
47
47
|
return items.length > 0 ? items : null;
|
|
48
48
|
} catch {
|
|
@@ -194,15 +194,37 @@ export class WorkflowTracker {
|
|
|
194
194
|
if (!PLANS_DIR_RE.test(path)) return false;
|
|
195
195
|
|
|
196
196
|
if (DESIGN_RE.test(path)) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
// Only advance if we haven't already passed the brainstorm phase.
|
|
198
|
+
// Writing a design doc during plan/execute/finalize (e.g., updating
|
|
199
|
+
// the plan) must NOT reset workflow state.
|
|
200
|
+
const curIdx = this.state.currentPhase ? WORKFLOW_PHASES.indexOf(this.state.currentPhase) : -1;
|
|
201
|
+
if (curIdx > WORKFLOW_PHASES.indexOf("brainstorm")) {
|
|
202
|
+
return this.recordArtifact("brainstorm", path);
|
|
203
|
+
}
|
|
204
|
+
let changed = false;
|
|
205
|
+
changed = this.recordArtifact("brainstorm", path) || changed;
|
|
206
|
+
// Activating and immediately completing: the design doc is the
|
|
207
|
+
// deliverable that signals brainstorm is done. Do NOT mark prompted
|
|
208
|
+
// so the agent_end boundary prompt still fires to offer session handoff.
|
|
209
|
+
changed = this.advanceTo("brainstorm") || changed;
|
|
210
|
+
changed = this.completeCurrent() || changed;
|
|
211
|
+
return changed;
|
|
200
212
|
}
|
|
201
213
|
|
|
202
214
|
if (IMPLEMENTATION_RE.test(path)) {
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
215
|
+
// Only advance if we haven't already passed the plan phase.
|
|
216
|
+
const curIdx = this.state.currentPhase ? WORKFLOW_PHASES.indexOf(this.state.currentPhase) : -1;
|
|
217
|
+
if (curIdx > WORKFLOW_PHASES.indexOf("plan")) {
|
|
218
|
+
return this.recordArtifact("plan", path);
|
|
219
|
+
}
|
|
220
|
+
let changed = false;
|
|
221
|
+
changed = this.recordArtifact("plan", path) || changed;
|
|
222
|
+
// Activating and immediately completing: the implementation plan
|
|
223
|
+
// is the deliverable that signals plan phase is done. Do NOT mark
|
|
224
|
+
// prompted so the agent_end boundary prompt still fires.
|
|
225
|
+
changed = this.advanceTo("plan") || changed;
|
|
226
|
+
changed = this.completeCurrent() || changed;
|
|
227
|
+
return changed;
|
|
206
228
|
}
|
|
207
229
|
|
|
208
230
|
return false;
|
|
@@ -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";
|
|
@@ -64,75 +62,32 @@ async function selectValue<T extends string>(
|
|
|
64
62
|
|
|
65
63
|
const SUPERPOWERS_STATE_ENTRY_TYPE = "superpowers_state";
|
|
66
64
|
|
|
67
|
-
function
|
|
68
|
-
return path.join(process.cwd(), ".pi", "superpowers-state.json");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function getStateFilePath(): string {
|
|
72
|
-
return path.join(process.cwd(), ".pi", "workflow-kit-state.json");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function reconstructState(ctx: ExtensionContext, handler: WorkflowHandler, stateFilePath?: string | false) {
|
|
65
|
+
export function reconstructState(ctx: ExtensionContext, handler: WorkflowHandler) {
|
|
76
66
|
handler.resetState();
|
|
77
67
|
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (stateFilePath !== false) {
|
|
83
|
-
try {
|
|
84
|
-
const newPath = stateFilePath ?? getStateFilePath();
|
|
85
|
-
if (fs.existsSync(newPath)) {
|
|
86
|
-
const raw = fs.readFileSync(newPath, "utf-8");
|
|
87
|
-
fileData = JSON.parse(raw);
|
|
88
|
-
} else if (stateFilePath === undefined) {
|
|
89
|
-
// Legacy fallback: try the old filename only when no explicit path is given.
|
|
90
|
-
const legacyPath = getLegacyStateFilePath();
|
|
91
|
-
if (fs.existsSync(legacyPath)) {
|
|
92
|
-
const raw = fs.readFileSync(legacyPath, "utf-8");
|
|
93
|
-
fileData = JSON.parse(raw);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
} catch (err) {
|
|
97
|
-
log.warn(
|
|
98
|
-
`Failed to read state file, falling back to session entries: ${err instanceof Error ? err.message : err}`,
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 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.
|
|
104
71
|
const entries = ctx.sessionManager.getBranch();
|
|
105
72
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
106
73
|
const entry = entries[i];
|
|
107
74
|
// biome-ignore lint/suspicious/noExplicitAny: pi SDK session entry type
|
|
108
75
|
if (entry.type === "custom" && (entry as any).customType === SUPERPOWERS_STATE_ENTRY_TYPE) {
|
|
109
76
|
// biome-ignore lint/suspicious/noExplicitAny: pi SDK session entry type
|
|
110
|
-
|
|
111
|
-
|
|
77
|
+
handler.setFullState((entry as any).data);
|
|
78
|
+
return;
|
|
112
79
|
}
|
|
113
80
|
// Migration fallback: old-format workflow-only entries
|
|
114
81
|
// biome-ignore lint/suspicious/noExplicitAny: pi SDK session entry type
|
|
115
82
|
if (entry.type === "custom" && (entry as any).customType === WORKFLOW_TRACKER_ENTRY_TYPE) {
|
|
116
83
|
// biome-ignore lint/suspicious/noExplicitAny: pi SDK session entry type
|
|
117
|
-
|
|
118
|
-
|
|
84
|
+
handler.setFullState({ workflow: (entry as any).data });
|
|
85
|
+
return;
|
|
119
86
|
}
|
|
120
87
|
}
|
|
121
88
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
const fileSavedAt = fileData.savedAt ?? 0;
|
|
125
|
-
const sessionSavedAt = sessionData.savedAt ?? 0;
|
|
126
|
-
const winner = fileSavedAt >= sessionSavedAt ? fileData : sessionData;
|
|
127
|
-
handler.setFullState(winner);
|
|
128
|
-
} else if (fileData) {
|
|
129
|
-
handler.setFullState(fileData);
|
|
130
|
-
} else if (sessionData) {
|
|
131
|
-
handler.setFullState(sessionData);
|
|
132
|
-
} else {
|
|
133
|
-
// No entries found — reset to fresh defaults
|
|
134
|
-
handler.setFullState({});
|
|
135
|
-
}
|
|
89
|
+
// No entries found — reset to fresh defaults
|
|
90
|
+
handler.setFullState({});
|
|
136
91
|
}
|
|
137
92
|
|
|
138
93
|
export default function (pi: ExtensionAPI) {
|
|
@@ -143,10 +98,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
143
98
|
const pendingViolations = new Map<string, Violation>();
|
|
144
99
|
const pendingVerificationViolations = new Map<string, VerificationViolation>();
|
|
145
100
|
const pendingBranchGates = new Map<string, string>();
|
|
146
|
-
const pendingProcessWarnings = new Map<string, string>();
|
|
147
101
|
|
|
148
|
-
type ViolationBucket = "
|
|
149
|
-
const strikes: Record<ViolationBucket, number> = {
|
|
102
|
+
type ViolationBucket = "practice";
|
|
103
|
+
const strikes: Record<ViolationBucket, number> = { practice: 0 };
|
|
150
104
|
const sessionAllowed: Partial<Record<ViolationBucket, boolean>> = {};
|
|
151
105
|
|
|
152
106
|
async function maybeEscalate(bucket: ViolationBucket, ctx: ExtensionContext): Promise<"allow" | "block"> {
|
|
@@ -180,14 +134,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
180
134
|
const persistState = () => {
|
|
181
135
|
const stateWithTimestamp = { ...handler.getFullState(), savedAt: Date.now() };
|
|
182
136
|
pi.appendEntry(SUPERPOWERS_STATE_ENTRY_TYPE, stateWithTimestamp);
|
|
183
|
-
// Also persist to file for cross-session survival
|
|
184
|
-
try {
|
|
185
|
-
const statePath = getStateFilePath();
|
|
186
|
-
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
187
|
-
fs.writeFileSync(statePath, JSON.stringify(stateWithTimestamp, null, 2));
|
|
188
|
-
} catch (err) {
|
|
189
|
-
log.warn(`Failed to persist state file: ${err instanceof Error ? err.message : err}`);
|
|
190
|
-
}
|
|
191
137
|
};
|
|
192
138
|
|
|
193
139
|
const phaseToSkill: Record<string, string> = {
|
|
@@ -230,10 +176,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
230
176
|
pendingViolations.clear();
|
|
231
177
|
pendingVerificationViolations.clear();
|
|
232
178
|
pendingBranchGates.clear();
|
|
233
|
-
pendingProcessWarnings.clear();
|
|
234
|
-
strikes.process = 0;
|
|
235
179
|
strikes.practice = 0;
|
|
236
|
-
delete sessionAllowed.process;
|
|
237
180
|
delete sessionAllowed.practice;
|
|
238
181
|
branchNoticeShown = false;
|
|
239
182
|
branchConfirmed = false;
|
|
@@ -514,16 +457,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
514
457
|
const isPlansWrite = resolved.startsWith(plansRoot);
|
|
515
458
|
|
|
516
459
|
if (isThinkingPhase && !isPlansWrite) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
`⚠️ PROCESS VIOLATION: Wrote ${filePath} during ${phase} phase.\n` +
|
|
525
|
-
"During brainstorming/planning you may only write to docs/plans/. Stop and return to docs/plans/ or advance workflow phases intentionally.",
|
|
526
|
-
);
|
|
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
|
+
};
|
|
527
467
|
}
|
|
528
468
|
|
|
529
469
|
changed = handler.handleFileWritten(filePath) || changed;
|
|
@@ -615,12 +555,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
615
555
|
}
|
|
616
556
|
}
|
|
617
557
|
pendingViolations.delete(toolCallId);
|
|
618
|
-
|
|
619
|
-
const processWarning = pendingProcessWarnings.get(toolCallId);
|
|
620
|
-
if (processWarning) {
|
|
621
|
-
injected.push(processWarning);
|
|
622
|
-
}
|
|
623
|
-
pendingProcessWarnings.delete(toolCallId);
|
|
624
558
|
}
|
|
625
559
|
|
|
626
560
|
// Handle bash results (test runs, commits, investigation)
|
|
@@ -822,6 +756,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
822
756
|
description: "Reset workflow tracker to fresh state for a new task",
|
|
823
757
|
async handler(_args, ctx) {
|
|
824
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() });
|
|
825
761
|
persistState();
|
|
826
762
|
updateWidget(ctx);
|
|
827
763
|
if (ctx.hasUI) {
|