@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.
- package/docs/plans/2026-04-10-brainstorming-boundary-enforcement-design.md +60 -0
- 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 +13 -1
- package/extensions/workflow-monitor/workflow-handler.ts +5 -0
- package/extensions/workflow-monitor/workflow-next-completions.ts +1 -1
- package/extensions/workflow-monitor/workflow-tracker.ts +73 -18
- package/extensions/workflow-monitor/workflow-transitions.ts +34 -1
- package/extensions/workflow-monitor.ts +95 -122
- package/package.json +1 -1
- package/skills/brainstorming/SKILL.md +13 -5
|
@@ -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.
|
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;
|
|
@@ -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
|
|
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 =
|
|
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
|
|
100
|
-
//
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
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
|
|
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> = {
|
|
@@ -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 === "
|
|
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 === "
|
|
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 === "
|
|
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 === "
|
|
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
|
-
|
|
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
|
-
);
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
const
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
if (
|
|
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
|
-
//
|
|
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,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:**
|