@tianhai/pi-workflow-kit 0.5.0 → 0.5.3

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.
@@ -0,0 +1,60 @@
1
+ # Brainstorming Skill Boundary Enforcement
2
+
3
+ **Date:** 2026-04-10
4
+ **Status:** approved
5
+
6
+ ## Problem
7
+
8
+ The brainstorming skill's boundaries are advisory only — a quiet `## Boundaries` bullet list mid-file that the model reads then ignores once a task "feels" straightforward. There is no structural enforcement (pi doesn't implement `allowed-tools`, no tool-blocking extension API).
9
+
10
+ In practice, `/skill:brainstorming` was invoked, the skill was loaded, and the agent immediately jumped to reading code, diagnosing the bug, and editing source files — violating every boundary.
11
+
12
+ ## Design
13
+
14
+ Two changes to `skills/brainstorming/SKILL.md`:
15
+
16
+ ### 1. Add `allowed-tools` frontmatter
17
+
18
+ ```yaml
19
+ allowed-tools: read bash
20
+ ```
21
+
22
+ Pi doesn't parse this field yet, but:
23
+ - It's part of the Agent Skills spec (experimental)
24
+ - Future pi versions may inject it into the system prompt
25
+ - Serves as machine-readable declaration of intent
26
+ - Zero cost today
27
+
28
+ ### 2. Replace quiet Boundaries section with prominent top-of-file block
29
+
30
+ Move from a mid-file `## Boundaries` section to a visually distinct blockquote immediately after the `# Heading`, before the Overview:
31
+
32
+ ```markdown
33
+ > ⚠️ **BOUNDARY — DO NOT VIOLATE**
34
+ >
35
+ > This skill is **read-only exploration**. You MUST NOT use `edit` or `write` tools.
36
+ > The only tools allowed are `read` and `bash` (for investigation only).
37
+ >
38
+ > - ✅ Read code and docs: yes
39
+ > - ✅ Write to `docs/plans/`: yes (design documents only)
40
+ > - ❌ Edit or create any other files: **absolutely no**
41
+ >
42
+ > If you find yourself reaching for `edit` or `write`, **stop**. Present what
43
+ > you found as a design section and ask the user to approve it first.
44
+ ```
45
+
46
+ Key improvements over current form:
47
+ - **Blockquote + warning emoji** — visually distinct from normal content
48
+ - **"DO NOT VIOLATE"** — strong language models respond to
49
+ - **Names forbidden tools explicitly** (`edit`, `write`) — no ambiguity
50
+ - **Recovery instruction** — "stop, present what you found" — constructive next step
51
+ - **Positioned at the top** — seen before the Overview, not buried mid-file
52
+
53
+ ## Out of scope
54
+
55
+ - Extension-level enforcement (requires pi core changes to support tool call interception)
56
+ - Changing other skills' boundaries (this is a pattern, but brainstorming is the most frequently violated)
57
+
58
+ ## Testing
59
+
60
+ Manual verification: invoke `/skill:brainstorming`, confirm the model does not reach for `edit`/`write` during exploration.
@@ -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
+ ```
@@ -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;
@@ -230,6 +236,12 @@ export default function (pi: ExtensionAPI) {
230
236
  }
231
237
  };
232
238
 
239
+ // Listen for clear signal from workflow-monitor's /workflow-reset command.
240
+ // This allows cross-extension communication without a callTool API.
241
+ pi.events.on("plan_tracker:clear", () => {
242
+ tasks = [];
243
+ });
244
+
233
245
  // Reconstruct state + widget on session events
234
246
  // session_start covers startup, reload, new, resume, fork (pi v0.65.0+)
235
247
  pi.on("session_start", async (_event, ctx) => {
@@ -88,6 +88,7 @@ export interface WorkflowHandler {
88
88
  restoreWorkflowStateFromBranch(branch: SessionEntry[]): void;
89
89
  markWorkflowPrompted(phase: Phase): boolean;
90
90
  completeCurrentWorkflowPhase(): boolean;
91
+ completeWorkflowPhase(phase: Phase): boolean;
91
92
  advanceWorkflowTo(phase: Phase): boolean;
92
93
  skipWorkflowPhases(phases: Phase[]): boolean;
93
94
  handleSkillFileRead(path: string): boolean;
@@ -323,6 +324,10 @@ export function createWorkflowHandler(): WorkflowHandler {
323
324
  return tracker.completeCurrent();
324
325
  },
325
326
 
327
+ completeWorkflowPhase(phase: Phase) {
328
+ return tracker.completePhase(phase);
329
+ },
330
+
326
331
  advanceWorkflowTo(phase) {
327
332
  return tracker.advanceTo(phase);
328
333
  },
@@ -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, label: relPath }));
45
+ .map((relPath) => ({ value: `${phase} ${relPath}`, label: relPath }));
46
46
 
47
47
  return items.length > 0 ? items : null;
48
48
  } catch {
@@ -12,9 +12,31 @@ export interface WorkflowTrackerState {
12
12
  prompted: Record<Phase, boolean>;
13
13
  }
14
14
 
15
- export type TransitionBoundary = "design_committed" | "plan_ready" | "execution_complete";
15
+ export type TransitionBoundary =
16
+ | "design_reviewable"
17
+ | "plan_reviewable"
18
+ | "design_committed"
19
+ | "plan_ready"
20
+ | "execution_complete";
16
21
 
17
22
  export function computeBoundaryToPrompt(state: WorkflowTrackerState): TransitionBoundary | null {
23
+ // Reviewable: current phase has its deliverable artifact but hasn't been
24
+ // user-confirmed as complete. Prompt the user to review before moving on.
25
+ if (
26
+ state.currentPhase === "brainstorm" &&
27
+ state.artifacts.brainstorm &&
28
+ state.phases.brainstorm === "active" &&
29
+ !state.prompted.brainstorm
30
+ ) {
31
+ return "design_reviewable";
32
+ }
33
+ if (state.currentPhase === "plan" && state.artifacts.plan && state.phases.plan === "active" && !state.prompted.plan) {
34
+ return "plan_reviewable";
35
+ }
36
+
37
+ // Committed: phase is complete but user hasn't been prompted for
38
+ // transition options yet (e.g. phases completed via skip-confirmation
39
+ // "mark complete" or execute phase auto-completing on all tasks terminal).
18
40
  if (state.phases.brainstorm === "complete" && !state.prompted.brainstorm) {
19
41
  return "design_committed";
20
42
  }
@@ -96,24 +118,32 @@ export class WorkflowTracker {
96
118
 
97
119
  if (current) {
98
120
  const curIdx = WORKFLOW_PHASES.indexOf(current);
99
- if (nextIdx <= curIdx) {
100
- // Backward or same-phase navigation = new task. Reset everything.
121
+ if (nextIdx === curIdx) {
122
+ // Same-phase navigation is a no-op. This prevents accidental resets
123
+ // when plan_tracker init is called while already in execute, or when
124
+ // a skill is re-invoked during its own phase.
125
+ return false;
126
+ }
127
+ if (nextIdx < curIdx) {
128
+ // Backward navigation = intentional new task. Reset everything.
101
129
  this.reset();
102
130
  // Fall through to activate the target phase below.
103
131
  } else {
104
- // Forward advance: auto-complete the current phase.
105
- if (this.state.phases[current] === "active") {
106
- this.state.phases[current] = "complete";
132
+ // Forward advance: do NOT auto-complete the current phase.
133
+ // Phase completion requires explicit user confirmation via
134
+ // boundary prompts or skip-confirmation "mark complete".
135
+ // However, refuse to jump over unresolved intermediate phases.
136
+ for (let i = curIdx + 1; i < nextIdx; i++) {
137
+ const intermediate = WORKFLOW_PHASES[i]!;
138
+ const status = this.state.phases[intermediate];
139
+ if (status !== "complete" && status !== "skipped") {
140
+ // Can't advance past an unresolved intermediate phase.
141
+ return false;
142
+ }
107
143
  }
108
144
  }
109
145
  }
110
146
 
111
- for (const p of WORKFLOW_PHASES) {
112
- if (p !== phase && this.state.phases[p] === "active") {
113
- this.state.phases[p] = "complete";
114
- }
115
- }
116
-
117
147
  this.state.currentPhase = phase;
118
148
  if (this.state.phases[phase] === "pending") {
119
149
  this.state.phases[phase] = "active";
@@ -138,6 +168,10 @@ export class WorkflowTracker {
138
168
  completeCurrent(): boolean {
139
169
  const phase = this.state.currentPhase;
140
170
  if (!phase) return false;
171
+ return this.completePhase(phase);
172
+ }
173
+
174
+ completePhase(phase: Phase): boolean {
141
175
  if (this.state.phases[phase] === "complete") return false;
142
176
  this.state.phases[phase] = "complete";
143
177
  return true;
@@ -194,21 +228,42 @@ export class WorkflowTracker {
194
228
  if (!PLANS_DIR_RE.test(path)) return false;
195
229
 
196
230
  if (DESIGN_RE.test(path)) {
197
- const changedArtifact = this.recordArtifact("brainstorm", path);
198
- const changedPhase = this.advanceTo("brainstorm");
199
- return changedArtifact || changedPhase;
231
+ // Only advance if we haven't already passed the brainstorm phase.
232
+ // Writing a design doc during plan/execute/finalize (e.g., updating
233
+ // the plan) must NOT reset workflow state.
234
+ const curIdx = this.state.currentPhase ? WORKFLOW_PHASES.indexOf(this.state.currentPhase) : -1;
235
+ if (curIdx > WORKFLOW_PHASES.indexOf("brainstorm")) {
236
+ return this.recordArtifact("brainstorm", path);
237
+ }
238
+ let changed = false;
239
+ changed = this.recordArtifact("brainstorm", path) || changed;
240
+ // Activate brainstorm phase but do NOT auto-complete.
241
+ // User confirms completion via the reviewable boundary prompt at agent_end.
242
+ changed = this.advanceTo("brainstorm") || changed;
243
+ return changed;
200
244
  }
201
245
 
202
246
  if (IMPLEMENTATION_RE.test(path)) {
203
- const changedArtifact = this.recordArtifact("plan", path);
204
- const changedPhase = this.advanceTo("plan");
205
- return changedArtifact || changedPhase;
247
+ // Only advance if we haven't already passed the plan phase.
248
+ const curIdx = this.state.currentPhase ? WORKFLOW_PHASES.indexOf(this.state.currentPhase) : -1;
249
+ if (curIdx > WORKFLOW_PHASES.indexOf("plan")) {
250
+ return this.recordArtifact("plan", path);
251
+ }
252
+ let changed = false;
253
+ changed = this.recordArtifact("plan", path) || changed;
254
+ // Activate plan phase but do NOT auto-complete.
255
+ // User confirms completion via the reviewable boundary prompt at agent_end.
256
+ changed = this.advanceTo("plan") || changed;
257
+ return changed;
206
258
  }
207
259
 
208
260
  return false;
209
261
  }
210
262
 
211
263
  onPlanTrackerInit(): boolean {
264
+ // Guard: don't advance if already in execute (prevents accidental resets).
265
+ // Also refuse to jump over unresolved phases (e.g., plan still active).
266
+ if (this.state.currentPhase === "execute") return false;
212
267
  return this.advanceTo("execute");
213
268
  }
214
269
 
@@ -1,6 +1,6 @@
1
1
  import type { Phase, TransitionBoundary } from "./workflow-tracker";
2
2
 
3
- export type TransitionChoice = "next" | "fresh" | "skip" | "discuss";
3
+ export type TransitionChoice = "next" | "fresh" | "skip" | "revise" | "discuss";
4
4
 
5
5
  export interface TransitionPrompt {
6
6
  boundary: TransitionBoundary;
@@ -17,8 +17,36 @@ const BASE_OPTIONS: TransitionPrompt["options"] = [
17
17
  { choice: "discuss", label: "Discuss" },
18
18
  ];
19
19
 
20
+ // Reviewable options: shown when a phase has its artifact but hasn't
21
+ // been user-confirmed as complete. Includes explicit approval + revision.
22
+ const REVIEWABLE_OPTIONS: TransitionPrompt["options"] = [
23
+ { choice: "next", label: "✓ Looks good, next step (this session)" },
24
+ { choice: "fresh", label: "✓ Looks good, fresh session → next step" },
25
+ { choice: "skip", label: "Skip phase" },
26
+ { choice: "revise", label: "✗ Needs more work" },
27
+ { choice: "discuss", label: "Discuss" },
28
+ ];
29
+
20
30
  export function getTransitionPrompt(boundary: TransitionBoundary, artifactPath: string | null): TransitionPrompt {
21
31
  switch (boundary) {
32
+ // Reviewable: phase has artifact but user hasn't confirmed completion
33
+ case "design_reviewable":
34
+ return {
35
+ boundary,
36
+ title: `Design written${artifactPath ? `: ${artifactPath}` : ""}. Ready to proceed?`,
37
+ nextPhase: "plan",
38
+ artifactPath,
39
+ options: REVIEWABLE_OPTIONS,
40
+ };
41
+ case "plan_reviewable":
42
+ return {
43
+ boundary,
44
+ title: `Plan written${artifactPath ? `: ${artifactPath}` : ""}. Ready to proceed?`,
45
+ nextPhase: "execute",
46
+ artifactPath,
47
+ options: REVIEWABLE_OPTIONS,
48
+ };
49
+ // Committed: phase already complete, user chooses how to proceed
22
50
  case "design_committed":
23
51
  return {
24
52
  boundary,
@@ -53,3 +81,8 @@ export function getTransitionPrompt(boundary: TransitionBoundary, artifactPath:
53
81
  };
54
82
  }
55
83
  }
84
+
85
+ /** Whether a boundary represents a phase that still needs user confirmation. */
86
+ export function isReviewableBoundary(boundary: TransitionBoundary): boolean {
87
+ return boundary === "design_reviewable" || boundary === "plan_reviewable";
88
+ }
@@ -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";
@@ -47,7 +45,7 @@ import {
47
45
  WORKFLOW_TRACKER_ENTRY_TYPE,
48
46
  type WorkflowTrackerState,
49
47
  } from "./workflow-monitor/workflow-tracker";
50
- import { getTransitionPrompt } from "./workflow-monitor/workflow-transitions";
48
+ import { getTransitionPrompt, isReviewableBoundary } from "./workflow-monitor/workflow-transitions";
51
49
 
52
50
  type SelectOption<T extends string> = { label: string; value: T };
53
51
 
@@ -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 getLegacyStateFilePath(): string {
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
- // Read both file-based and session-based state, then pick the newer one.
79
- let fileData: (Record<string, unknown> & { savedAt?: number }) | null = null;
80
- let sessionData: (Record<string, unknown> & { savedAt?: number }) | null = null;
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
- sessionData = (entry as any).data;
111
- break;
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
- sessionData = { workflow: (entry as any).data };
118
- break;
84
+ handler.setFullState({ workflow: (entry as any).data });
85
+ return;
119
86
  }
120
87
  }
121
88
 
122
- // Pick the newer source when both are available; otherwise use whichever exists.
123
- if (fileData && sessionData) {
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 = "process" | "practice";
149
- const strikes: Record<ViolationBucket, number> = { process: 0, practice: 0 };
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> = {
@@ -219,6 +165,8 @@ export default function (pi: ExtensionAPI) {
219
165
  }
220
166
 
221
167
  const boundaryToPhase: Record<TransitionBoundary, keyof typeof phaseToSkill> = {
168
+ design_reviewable: "brainstorm",
169
+ plan_reviewable: "plan",
222
170
  design_committed: "brainstorm",
223
171
  plan_ready: "plan",
224
172
  execution_complete: "execute",
@@ -230,10 +178,7 @@ export default function (pi: ExtensionAPI) {
230
178
  pendingViolations.clear();
231
179
  pendingVerificationViolations.clear();
232
180
  pendingBranchGates.clear();
233
- pendingProcessWarnings.clear();
234
- strikes.process = 0;
235
181
  strikes.practice = 0;
236
- delete sessionAllowed.process;
237
182
  delete sessionAllowed.practice;
238
183
  branchNoticeShown = false;
239
184
  branchConfirmed = false;
@@ -290,12 +235,19 @@ export default function (pi: ExtensionAPI) {
290
235
  const missingSkill = phaseToSkill[missing] ?? missing;
291
236
  const options = [
292
237
  { label: `Do ${missing} now`, value: "do_now" as const },
238
+ { label: `Mark ${missing} as complete`, value: "mark_complete" as const },
293
239
  { label: `Skip ${missing}`, value: "skip" as const },
294
240
  { label: "Cancel", value: "cancel" as const },
295
241
  ];
296
242
  const choice = await selectValue(ctx, `Phase "${missing}" is unresolved. What would you like to do?`, options);
297
243
 
298
- if (choice === "skip") {
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") {
299
251
  handler.skipWorkflowPhases([missing]);
300
252
  handler.handleInputText(text);
301
253
  persistState();
@@ -337,12 +289,17 @@ export default function (pi: ExtensionAPI) {
337
289
  const skill = phaseToSkill[phase] ?? phase;
338
290
  const options = [
339
291
  { label: `Do ${phase} now`, value: "do_now" as const },
292
+ { label: `Mark ${phase} as complete`, value: "mark_complete" as const },
340
293
  { label: `Skip ${phase}`, value: "skip" as const },
341
294
  { label: "Cancel", value: "cancel" as const },
342
295
  ];
343
296
  const choice = await selectValue(ctx, `Phase "${phase}" is unresolved. What would you like to do?`, options);
344
297
 
345
- if (choice === "skip") {
298
+ if (choice === "mark_complete") {
299
+ handler.completeWorkflowPhase(phase as Phase);
300
+ persistState();
301
+ updateWidget(ctx);
302
+ } else if (choice === "skip") {
346
303
  handler.skipWorkflowPhases([phase]);
347
304
  persistState();
348
305
  updateWidget(ctx);
@@ -369,12 +326,18 @@ export default function (pi: ExtensionAPI) {
369
326
  const missingSkill = phaseToSkill[missing] ?? missing;
370
327
  const options = [
371
328
  { label: `Do ${missing} now`, value: "do_now" as const },
329
+ { label: `Mark ${missing} as complete`, value: "mark_complete" as const },
372
330
  { label: `Skip ${missing}`, value: "skip" as const },
373
331
  { label: "Cancel", value: "cancel" as const },
374
332
  ];
375
333
  const choice = await selectValue(ctx, `Phase "${missing}" is unresolved. What would you like to do?`, options);
376
334
 
377
- if (choice === "skip") {
335
+ if (choice === "mark_complete") {
336
+ handler.completeWorkflowPhase(missing as Phase);
337
+ persistState();
338
+ updateWidget(ctx);
339
+ return "allowed";
340
+ } else if (choice === "skip") {
378
341
  handler.skipWorkflowPhases([missing]);
379
342
  persistState();
380
343
  updateWidget(ctx);
@@ -413,12 +376,17 @@ export default function (pi: ExtensionAPI) {
413
376
  const skill = phaseToSkill[phase] ?? phase;
414
377
  const options = [
415
378
  { label: `Do ${phase} now`, value: "do_now" as const },
379
+ { label: `Mark ${phase} as complete`, value: "mark_complete" as const },
416
380
  { label: `Skip ${phase}`, value: "skip" as const },
417
381
  { label: "Cancel", value: "cancel" as const },
418
382
  ];
419
383
  const choice = await selectValue(ctx, `Phase "${phase}" is unresolved. What would you like to do?`, options);
420
384
 
421
- if (choice === "skip") {
385
+ if (choice === "mark_complete") {
386
+ handler.completeWorkflowPhase(phase as Phase);
387
+ persistState();
388
+ updateWidget(ctx);
389
+ } else if (choice === "skip") {
422
390
  handler.skipWorkflowPhases([phase]);
423
391
  persistState();
424
392
  updateWidget(ctx);
@@ -514,16 +482,13 @@ export default function (pi: ExtensionAPI) {
514
482
  const isPlansWrite = resolved.startsWith(plansRoot);
515
483
 
516
484
  if (isThinkingPhase && !isPlansWrite) {
517
- const escalation = await maybeEscalate("process", ctx);
518
- if (escalation === "block") {
519
- return { blocked: true };
520
- }
521
-
522
- pendingProcessWarnings.set(
523
- toolCallId,
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
- );
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
+ };
527
492
  }
528
493
 
529
494
  changed = handler.handleFileWritten(filePath) || changed;
@@ -615,12 +580,6 @@ export default function (pi: ExtensionAPI) {
615
580
  }
616
581
  }
617
582
  pendingViolations.delete(toolCallId);
618
-
619
- const processWarning = pendingProcessWarnings.get(toolCallId);
620
- if (processWarning) {
621
- injected.push(processWarning);
622
- }
623
- pendingProcessWarnings.delete(toolCallId);
624
583
  }
625
584
 
626
585
  // Handle bash results (test runs, commits, investigation)
@@ -674,18 +633,13 @@ export default function (pi: ExtensionAPI) {
674
633
 
675
634
  const boundaryPhase = boundaryToPhase[boundary];
676
635
  const prompt = getTransitionPrompt(boundary, latestState.artifacts[boundaryPhase]);
636
+ const reviewable = isReviewableBoundary(boundary);
677
637
 
678
638
  const options = prompt.options.map((o) => o.label);
679
639
  const pickedLabel = await ctx.ui.select(prompt.title, options);
680
640
 
681
641
  const selected = prompt.options.find((o) => o.label === pickedLabel)?.choice ?? null;
682
642
 
683
- const marked = handler.markWorkflowPrompted(boundaryPhase);
684
- if (marked) {
685
- persistState();
686
- updateWidget(ctx);
687
- }
688
-
689
643
  const nextSkill = phaseToSkill[prompt.nextPhase] ?? "writing-plans";
690
644
  const nextInSession = `/skill:${nextSkill}`;
691
645
  const fresh = `/workflow-next ${prompt.nextPhase}${prompt.artifactPath ? ` ${prompt.artifactPath}` : ""}`;
@@ -694,42 +648,56 @@ export default function (pi: ExtensionAPI) {
694
648
  "- Does this work require documentation updates? (README, CHANGELOG, API docs, inline docs)\n" +
695
649
  "- What was learned during this implementation? (surprises, codebase knowledge, things to do differently)\n\n";
696
650
 
697
- if (selected === "next") {
698
- const advanced = prompt.nextPhase === "finalize" ? handler.advanceWorkflowTo("finalize") : false;
699
- if (advanced) {
700
- persistState();
701
- updateWidget(ctx);
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();
702
656
  }
703
- ctx.ui.setEditorText(prompt.nextPhase === "finalize" ? finishReminder + nextInSession : nextInSession);
704
- } else if (selected === "fresh") {
705
- const advanced = prompt.nextPhase === "finalize" ? handler.advanceWorkflowTo("finalize") : false;
706
- if (advanced) {
707
- persistState();
708
- updateWidget(ctx);
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);
709
668
  }
710
- ctx.ui.setEditorText(prompt.nextPhase === "finalize" ? finishReminder + fresh : fresh);
711
669
  } else if (selected === "skip") {
712
- // Explicit user-confirmed skip: mark the next phase as skipped, then move on.
713
- handler.skipWorkflowPhases([prompt.nextPhase]);
714
-
715
- const nextIdx = WORKFLOW_PHASES.indexOf(prompt.nextPhase);
716
- const phaseAfterSkip = WORKFLOW_PHASES[nextIdx + 1] ?? null;
717
-
718
- if (phaseAfterSkip) {
719
- const currentState = handler.getWorkflowState();
720
- const currentIdx = currentState?.currentPhase ? WORKFLOW_PHASES.indexOf(currentState.currentPhase) : -1;
721
- const afterSkipIdx = WORKFLOW_PHASES.indexOf(phaseAfterSkip);
722
- if (afterSkipIdx > currentIdx) {
723
- handler.advanceWorkflowTo(phaseAfterSkip);
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)) {
724
681
  const skipSkill = phaseToSkill[phaseAfterSkip] ?? "writing-plans";
725
682
  ctx.ui.setEditorText(`/skill:${skipSkill}`);
726
683
  }
727
684
  }
728
685
 
686
+ handler.markWorkflowPrompted(boundaryPhase);
729
687
  persistState();
730
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.
731
693
  } else if (selected === "discuss") {
732
- // Don't advance phase. Set editor text to prompt discussion.
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
+ }
733
701
  ctx.ui.setEditorText(
734
702
  `Let's discuss before moving to the next step.\n` +
735
703
  `We're at: ${prompt.title}\n` +
@@ -822,8 +790,13 @@ export default function (pi: ExtensionAPI) {
822
790
  description: "Reset workflow tracker to fresh state for a new task",
823
791
  async handler(_args, ctx) {
824
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() });
825
797
  persistState();
826
798
  updateWidget(ctx);
799
+ pi.events.emit("plan_tracker:clear");
827
800
  if (ctx.hasUI) {
828
801
  ctx.ui.notify("Workflow reset. Ready for a new task.", "info");
829
802
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tianhai/pi-workflow-kit",
3
- "version": "0.5.0",
3
+ "version": "0.5.3",
4
4
  "description": "Workflow skills and enforcement extensions for pi",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -1,23 +1,31 @@
1
1
  ---
2
2
  name: brainstorming
3
3
  description: "You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation."
4
+ allowed-tools: read bash
4
5
  ---
5
6
 
6
7
  > **Related skills:** Consider `/skill:using-git-worktrees` to set up an isolated workspace, then `/skill:writing-plans` for implementation planning.
7
8
 
8
9
  # Brainstorming Ideas Into Designs
9
10
 
11
+ > ⚠️ **BOUNDARY — DO NOT VIOLATE**
12
+ >
13
+ > This skill is **read-only exploration**. You MUST NOT use `edit` or `write` tools.
14
+ > The only tools allowed are `read` and `bash` (for investigation only).
15
+ >
16
+ > - ✅ Read code and docs: yes
17
+ > - ✅ Write to `docs/plans/`: yes (design documents only)
18
+ > - ❌ Edit or create any other files: **absolutely no**
19
+ >
20
+ > If you find yourself reaching for `edit` or `write`, **stop**. Present what
21
+ > you found as a design section and ask the user to approve it first.
22
+
10
23
  ## Overview
11
24
 
12
25
  Help turn ideas into fully formed designs and specs through natural collaborative dialogue.
13
26
 
14
27
  Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design in small sections (200-300 words), checking after each section whether it looks right so far.
15
28
 
16
- ## Boundaries
17
- - Read code and docs: yes
18
- - Write to docs/plans/: yes
19
- - Edit or create any other files: no
20
-
21
29
  ## The Process
22
30
 
23
31
  **Before anything else — check git state:**