prizmkit 1.0.144 → 1.0.147
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/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +20 -8
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +20 -8
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +20 -8
- package/bundled/skills/_metadata.json +8 -1
- package/bundled/skills/recovery-workflow/SKILL.md +428 -0
- package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +483 -0
- package/package.json +1 -1
package/bundled/VERSION.json
CHANGED
|
@@ -158,25 +158,37 @@ $TEST_CMD 2>&1 | tee /tmp/test-baseline.txt | tail -20
|
|
|
158
158
|
**CP-2**: All acceptance criteria met, all tests pass.
|
|
159
159
|
|
|
160
160
|
{{IF_BROWSER_INTERACTION}}
|
|
161
|
-
### Phase 3.5: Browser Verification (playwright-cli)
|
|
161
|
+
### Phase 3.5: Browser Verification (playwright-cli) — MANDATORY
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
You MUST execute this phase. Do NOT skip it. Do NOT mark it as completed without actually running playwright-cli.
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
**Startup**:
|
|
166
|
+
1. Check if port is already in use: `lsof -ti:3001 | xargs kill -9 2>/dev/null || true`
|
|
167
|
+
2. Start dev server: `{{BROWSER_SETUP_COMMAND}}`
|
|
168
|
+
3. Wait for server to be ready: poll `{{BROWSER_URL}}` with `curl -s -o /dev/null -w "%{http_code}"` until it returns 200 or 302 (max 30 seconds, 2s interval)
|
|
169
|
+
4. If the page requires authentication, use playwright-cli to register a test user and log in first
|
|
170
|
+
|
|
171
|
+
**Verification**:
|
|
172
|
+
5. Use `playwright-cli` to open `{{BROWSER_URL}}`, then verify:
|
|
167
173
|
{{BROWSER_VERIFY_STEPS}}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
174
|
+
6. Take a final screenshot for evidence
|
|
175
|
+
|
|
176
|
+
**Cleanup (REQUIRED — you started it, you stop it)**:
|
|
177
|
+
7. Stop the dev server process you started in step 2 (kill the process)
|
|
178
|
+
8. Verify port is released: `lsof -ti:3001 | xargs kill -9 2>/dev/null || true`
|
|
179
|
+
|
|
180
|
+
**Reporting**:
|
|
181
|
+
9. Append results to `context-snapshot.md`:
|
|
171
182
|
```
|
|
172
183
|
## Browser Verification
|
|
173
184
|
URL: {{BROWSER_URL}}
|
|
174
185
|
Steps executed: [list]
|
|
175
186
|
Screenshot: [path]
|
|
176
187
|
Result: PASS / FAIL (reason)
|
|
188
|
+
Server cleanup: confirmed
|
|
177
189
|
```
|
|
178
190
|
|
|
179
|
-
If
|
|
191
|
+
If verification fails, log the failure details but continue to commit. Failures do NOT block the commit, but you MUST attempt verification and MUST clean up the dev server.
|
|
180
192
|
{{END_IF_BROWSER_INTERACTION}}
|
|
181
193
|
|
|
182
194
|
### Phase 4: Architecture Sync & Commit (SINGLE COMMIT)
|
|
@@ -269,25 +269,37 @@ If GATE:MISSING — send message to Reviewer (re-spawn if needed): "Write the '#
|
|
|
269
269
|
**CP-3**: Tests pass, verdict is not NEEDS_FIXES.
|
|
270
270
|
|
|
271
271
|
{{IF_BROWSER_INTERACTION}}
|
|
272
|
-
### Phase 5.5: Browser Verification (playwright-cli)
|
|
272
|
+
### Phase 5.5: Browser Verification (playwright-cli) — MANDATORY
|
|
273
273
|
|
|
274
|
-
|
|
274
|
+
You MUST execute this phase. Do NOT skip it. Do NOT mark it as completed without actually running playwright-cli.
|
|
275
275
|
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
**Startup**:
|
|
277
|
+
1. Check if port is already in use: `lsof -ti:3001 | xargs kill -9 2>/dev/null || true`
|
|
278
|
+
2. Start dev server: `{{BROWSER_SETUP_COMMAND}}`
|
|
279
|
+
3. Wait for server to be ready: poll `{{BROWSER_URL}}` with `curl -s -o /dev/null -w "%{http_code}"` until it returns 200 or 302 (max 30 seconds, 2s interval)
|
|
280
|
+
4. If the page requires authentication, use playwright-cli to register a test user and log in first
|
|
281
|
+
|
|
282
|
+
**Verification**:
|
|
283
|
+
5. Use `playwright-cli` to open `{{BROWSER_URL}}`, then verify:
|
|
278
284
|
{{BROWSER_VERIFY_STEPS}}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
285
|
+
6. Take a final screenshot for evidence
|
|
286
|
+
|
|
287
|
+
**Cleanup (REQUIRED — you started it, you stop it)**:
|
|
288
|
+
7. Stop the dev server process you started in step 2 (kill the process)
|
|
289
|
+
8. Verify port is released: `lsof -ti:3001 | xargs kill -9 2>/dev/null || true`
|
|
290
|
+
|
|
291
|
+
**Reporting**:
|
|
292
|
+
9. Append results to `context-snapshot.md`:
|
|
282
293
|
```
|
|
283
294
|
## Browser Verification
|
|
284
295
|
URL: {{BROWSER_URL}}
|
|
285
296
|
Steps executed: [list]
|
|
286
297
|
Screenshot: [path]
|
|
287
298
|
Result: PASS / FAIL (reason)
|
|
299
|
+
Server cleanup: confirmed
|
|
288
300
|
```
|
|
289
301
|
|
|
290
|
-
If
|
|
302
|
+
If verification fails, log the failure details but continue to commit. Failures do NOT block the commit, but you MUST attempt verification and MUST clean up the dev server.
|
|
291
303
|
{{END_IF_BROWSER_INTERACTION}}
|
|
292
304
|
|
|
293
305
|
### Phase 6: Architecture Sync & Commit (SINGLE COMMIT)
|
|
@@ -367,25 +367,37 @@ If GATE:MISSING — send message to Reviewer (re-spawn if needed): "Write the '#
|
|
|
367
367
|
**CP-3**: Integration tests pass, verdict is not NEEDS_FIXES.
|
|
368
368
|
|
|
369
369
|
{{IF_BROWSER_INTERACTION}}
|
|
370
|
-
### Phase 5.5: Browser Verification (playwright-cli)
|
|
370
|
+
### Phase 5.5: Browser Verification (playwright-cli) — MANDATORY
|
|
371
371
|
|
|
372
|
-
|
|
372
|
+
You MUST execute this phase. Do NOT skip it. Do NOT mark it as completed without actually running playwright-cli.
|
|
373
373
|
|
|
374
|
-
|
|
375
|
-
|
|
374
|
+
**Startup**:
|
|
375
|
+
1. Check if port is already in use: `lsof -ti:3001 | xargs kill -9 2>/dev/null || true`
|
|
376
|
+
2. Start dev server: `{{BROWSER_SETUP_COMMAND}}`
|
|
377
|
+
3. Wait for server to be ready: poll `{{BROWSER_URL}}` with `curl -s -o /dev/null -w "%{http_code}"` until it returns 200 or 302 (max 30 seconds, 2s interval)
|
|
378
|
+
4. If the page requires authentication, use playwright-cli to register a test user and log in first
|
|
379
|
+
|
|
380
|
+
**Verification**:
|
|
381
|
+
5. Use `playwright-cli` to open `{{BROWSER_URL}}`, then verify:
|
|
376
382
|
{{BROWSER_VERIFY_STEPS}}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
383
|
+
6. Take a final screenshot for evidence
|
|
384
|
+
|
|
385
|
+
**Cleanup (REQUIRED — you started it, you stop it)**:
|
|
386
|
+
7. Stop the dev server process you started in step 2 (kill the process)
|
|
387
|
+
8. Verify port is released: `lsof -ti:3001 | xargs kill -9 2>/dev/null || true`
|
|
388
|
+
|
|
389
|
+
**Reporting**:
|
|
390
|
+
9. Append results to `context-snapshot.md`:
|
|
380
391
|
```
|
|
381
392
|
## Browser Verification
|
|
382
393
|
URL: {{BROWSER_URL}}
|
|
383
394
|
Steps executed: [list]
|
|
384
395
|
Screenshot: [path]
|
|
385
396
|
Result: PASS / FAIL (reason)
|
|
397
|
+
Server cleanup: confirmed
|
|
386
398
|
```
|
|
387
399
|
|
|
388
|
-
If
|
|
400
|
+
If verification fails, log the failure details but continue to commit. Failures do NOT block the commit, but you MUST attempt verification and MUST clean up the dev server.
|
|
389
401
|
{{END_IF_BROWSER_INTERACTION}}
|
|
390
402
|
|
|
391
403
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.0.
|
|
2
|
+
"version": "1.0.147",
|
|
3
3
|
"skills": {
|
|
4
4
|
"prizm-kit": {
|
|
5
5
|
"description": "Full-lifecycle dev toolkit. Covers spec-driven development, Prizm context docs, code quality, debugging, deployment, and knowledge management.",
|
|
@@ -224,6 +224,13 @@
|
|
|
224
224
|
"category": "Custom-skill",
|
|
225
225
|
"hasAssets": false,
|
|
226
226
|
"hasScripts": false
|
|
227
|
+
},
|
|
228
|
+
"recovery-workflow": {
|
|
229
|
+
"description": "Recover and resume interrupted feature pipeline sessions. Detects partial work from failed or interrupted sessions and resumes from the correct phase.",
|
|
230
|
+
"tier": "companion",
|
|
231
|
+
"category": "Custom-skill",
|
|
232
|
+
"hasAssets": false,
|
|
233
|
+
"hasScripts": false
|
|
227
234
|
}
|
|
228
235
|
},
|
|
229
236
|
"suites": {
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "recovery-workflow"
|
|
3
|
+
description: "Recover and resume interrupted feature pipeline sessions. Detects partial work (code changes, spec/plan artifacts, branch state, test results) from failed or interrupted sessions and intelligently resumes from the correct phase — instead of discarding everything and restarting from scratch. Use this skill whenever a pipeline session fails mid-execution, times out, gets interrupted, or when the user wants to salvage partial work from a failed feature. Trigger on: 'recover feature', 'resume session', 'continue from where it stopped', 'salvage partial work', 'fix failed feature', 'session interrupted', 'recover F-XXX', 'resume F-XXX', 'continue F-XXX', 'feature stuck', 'pick up where it left off', 'session timed out', 'don't want to restart from scratch', 'token limit exceeded'. (project)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Recovery Workflow
|
|
7
|
+
|
|
8
|
+
Intelligently recover and resume interrupted feature pipeline sessions. Instead of discarding partial work and restarting from scratch (which wastes tokens and time), this skill inspects the actual filesystem state — code changes, artifacts, git branches, test results — and continues from the right point.
|
|
9
|
+
|
|
10
|
+
## When to Use
|
|
11
|
+
|
|
12
|
+
User says:
|
|
13
|
+
- "Recover F-007", "Resume F-007", "Continue F-007 from where it stopped"
|
|
14
|
+
- "Feature session failed/timed out, salvage the work"
|
|
15
|
+
- "Pipeline interrupted, don't want to restart from scratch"
|
|
16
|
+
- "F-003 is stuck, pick up where it left off"
|
|
17
|
+
- "Session timed out but there's code already written"
|
|
18
|
+
|
|
19
|
+
**Do NOT use when:**
|
|
20
|
+
- User wants a clean retry from scratch → `retry-feature.sh` or `reset-feature.sh --clean --run`
|
|
21
|
+
- Pipeline is still running normally → `dev-pipeline-launcher` (Intent B: Check Status)
|
|
22
|
+
- User wants to fix a specific bug → `bug-fix-workflow`
|
|
23
|
+
- Feature never started (no state at all) → `dev-pipeline-launcher` (Intent E: Retry)
|
|
24
|
+
|
|
25
|
+
## Why This Skill Exists
|
|
26
|
+
|
|
27
|
+
Current retry tools have a fundamental limitation:
|
|
28
|
+
|
|
29
|
+
| Tool | What it does | What's lost |
|
|
30
|
+
|------|-------------|-------------|
|
|
31
|
+
| `retry-feature.sh` | Cleans all artifacts, resets status, spawns fresh session | All code changes, spec, plan — everything |
|
|
32
|
+
| `reset-feature.sh --clean --run` | Even more thorough clean + retry | Same, plus session history |
|
|
33
|
+
| `run.sh --resume-phase N` | Tells new session "start from phase N" via prompt | Better, but doesn't inspect actual worktree state — the new AI session has no memory of what was done |
|
|
34
|
+
|
|
35
|
+
The gap: when a session is interrupted mid-implementation, the **worktree already has real code changes** — possibly a spec, plan, partial or complete implementation, even passing tests. Discarding all of this wastes significant tokens and time. This skill bridges that gap by inspecting the actual filesystem state and resuming intelligently within the current interactive session.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Overview
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
recovery-workflow <feature-id>
|
|
43
|
+
│
|
|
44
|
+
├── Phase 0: Identify feature and gather context
|
|
45
|
+
│
|
|
46
|
+
├── Phase 1: Detect state (artifacts, code, tests, branch)
|
|
47
|
+
│
|
|
48
|
+
├── Phase 2: Diagnose and present recovery options
|
|
49
|
+
│
|
|
50
|
+
└── Phase 3: Execute recovery (chain remaining prizmkit skills)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Input
|
|
54
|
+
|
|
55
|
+
| Input | Required | Example |
|
|
56
|
+
|-------|----------|---------|
|
|
57
|
+
| Feature ID | Yes | `F-007` |
|
|
58
|
+
| Feature list path | No (default: `feature-list.json`) | `my-features.json` |
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Phase 0: Feature Identification
|
|
63
|
+
|
|
64
|
+
**Goal**: Locate the feature and its context.
|
|
65
|
+
|
|
66
|
+
1. **Parse feature ID** from user input (e.g., "recover F-007", "resume F-007")
|
|
67
|
+
2. **Read feature-list.json** to get feature metadata:
|
|
68
|
+
```bash
|
|
69
|
+
python3 -c "
|
|
70
|
+
import json, re
|
|
71
|
+
with open('feature-list.json') as f:
|
|
72
|
+
data = json.load(f)
|
|
73
|
+
for feat in data.get('features', []):
|
|
74
|
+
if feat.get('id') == 'F-007':
|
|
75
|
+
print(json.dumps(feat, indent=2))
|
|
76
|
+
"
|
|
77
|
+
```
|
|
78
|
+
3. **Compute feature slug** (same algorithm the pipeline uses):
|
|
79
|
+
```python
|
|
80
|
+
# F-007 + "User Authentication" → "007-user-authentication"
|
|
81
|
+
numeric = feature_id.replace('F-', '').zfill(3)
|
|
82
|
+
slug = re.sub(r'[^a-z0-9\s-]', '', title.lower())
|
|
83
|
+
slug = re.sub(r'[\s]+', '-', slug.strip()).strip('-')
|
|
84
|
+
slug = f"{numeric}-{slug}"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
If the feature is not found, ask the user for the correct ID or file path.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Phase 1: State Detection
|
|
92
|
+
|
|
93
|
+
**Goal**: Build a complete picture of what exists from the interrupted session.
|
|
94
|
+
|
|
95
|
+
Run the detection script:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
python3 ${SKILL_DIR}/scripts/detect-recovery-state.py \
|
|
99
|
+
--feature-id <FEATURE_ID> \
|
|
100
|
+
--feature-list feature-list.json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The script scans four categories of state and outputs structured JSON:
|
|
104
|
+
|
|
105
|
+
### 1.1 Pipeline State
|
|
106
|
+
- `dev-pipeline/state/features/{FEATURE_ID}/status.json` — pipeline tracking status, retry count, session history
|
|
107
|
+
- Last session directory — path for log inspection
|
|
108
|
+
|
|
109
|
+
### 1.2 PrizmKit Artifacts
|
|
110
|
+
- `.prizmkit/specs/{slug}/spec.md` — specification generated?
|
|
111
|
+
- `.prizmkit/specs/{slug}/plan.md` — plan generated?
|
|
112
|
+
- If plan.md exists: count completed vs total tasks (look for `[x]` vs `[ ]` checkboxes)
|
|
113
|
+
|
|
114
|
+
### 1.3 Git State
|
|
115
|
+
- **Feature branch**: does `feat/{slug}` exist?
|
|
116
|
+
- **Current branch**: are we already on the feature branch?
|
|
117
|
+
- **Uncommitted changes**: `git diff --stat` (working tree changes)
|
|
118
|
+
- **Staged changes**: `git diff --cached --stat`
|
|
119
|
+
- **Commits ahead of main**: `git log main..HEAD --oneline`
|
|
120
|
+
|
|
121
|
+
### 1.4 Code Changes
|
|
122
|
+
- Number of modified/added/deleted files
|
|
123
|
+
- Which directories were touched
|
|
124
|
+
- Whether test files were created/modified
|
|
125
|
+
|
|
126
|
+
### Detection Output
|
|
127
|
+
|
|
128
|
+
The script produces a structured JSON report:
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"feature_id": "F-007",
|
|
133
|
+
"feature_title": "User Authentication",
|
|
134
|
+
"feature_slug": "007-user-authentication",
|
|
135
|
+
"pipeline": {
|
|
136
|
+
"status": "failed",
|
|
137
|
+
"retry_count": 1,
|
|
138
|
+
"last_session_id": "F-007-20260329101500",
|
|
139
|
+
"last_session_dir": "dev-pipeline/state/features/F-007/sessions/F-007-20260329101500"
|
|
140
|
+
},
|
|
141
|
+
"artifacts": {
|
|
142
|
+
"spec_exists": true,
|
|
143
|
+
"plan_exists": true,
|
|
144
|
+
"spec_path": ".prizmkit/specs/007-user-authentication/spec.md",
|
|
145
|
+
"plan_path": ".prizmkit/specs/007-user-authentication/plan.md",
|
|
146
|
+
"plan_tasks_total": 5,
|
|
147
|
+
"plan_tasks_completed": 3
|
|
148
|
+
},
|
|
149
|
+
"git": {
|
|
150
|
+
"feature_branch": "feat/007-user-authentication",
|
|
151
|
+
"branch_exists": true,
|
|
152
|
+
"on_feature_branch": true,
|
|
153
|
+
"uncommitted_files": 8,
|
|
154
|
+
"staged_files": 0,
|
|
155
|
+
"commits_ahead_of_main": 0
|
|
156
|
+
},
|
|
157
|
+
"code": {
|
|
158
|
+
"files_modified": 6,
|
|
159
|
+
"files_added": 4,
|
|
160
|
+
"test_files_touched": 2,
|
|
161
|
+
"directories_touched": ["src/auth/", "src/middleware/", "tests/"]
|
|
162
|
+
},
|
|
163
|
+
"recovery": {
|
|
164
|
+
"recommended_action": "continue_implementation",
|
|
165
|
+
"recommended_phase": "implement",
|
|
166
|
+
"reason": "spec and plan exist, 3/5 plan tasks completed, code changes present",
|
|
167
|
+
"remaining_work": "2 tasks + review + commit"
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**CHECKPOINT CP-RW-0**: State detection complete, structured report available.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Phase 2: Diagnosis & Recovery Options
|
|
177
|
+
|
|
178
|
+
**Goal**: Present findings to user and agree on a recovery strategy.
|
|
179
|
+
|
|
180
|
+
### 2.1 Present Discovery Summary
|
|
181
|
+
|
|
182
|
+
Read the detection script output and show the user a clear summary:
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
═══════════════════════════════════════════════════
|
|
186
|
+
Recovery Report: F-007 — User Authentication
|
|
187
|
+
═══════════════════════════════════════════════════
|
|
188
|
+
|
|
189
|
+
Pipeline Status: FAILED (retry 1)
|
|
190
|
+
Feature Branch: feat/007-user-authentication
|
|
191
|
+
|
|
192
|
+
Artifacts Found:
|
|
193
|
+
✓ spec.md (specification)
|
|
194
|
+
✓ plan.md (5 tasks, 3 completed)
|
|
195
|
+
✓ Code changes (10 files — 6 modified, 4 new)
|
|
196
|
+
✓ Test files (2 found)
|
|
197
|
+
|
|
198
|
+
Recommended: Continue implementation
|
|
199
|
+
Remaining: 2 tasks + review + commit
|
|
200
|
+
═══════════════════════════════════════════════════
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### 2.2 Run Tests (if code changes exist)
|
|
204
|
+
|
|
205
|
+
Before presenting options, run the project's test suite to understand the health of existing code:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
# detect test command from .prizmkit/config.json or package.json
|
|
209
|
+
npm test # or the appropriate test command
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Include test results in the summary:
|
|
213
|
+
- How many tests pass/fail
|
|
214
|
+
- If there are failures — which tests and why
|
|
215
|
+
|
|
216
|
+
### 2.3 Present Recovery Options
|
|
217
|
+
|
|
218
|
+
Based on the detected state, offer the appropriate options. The most common scenarios:
|
|
219
|
+
|
|
220
|
+
**Scenario A: Implementation in progress** (spec + plan + code changes)
|
|
221
|
+
Most common case — session died mid-implementation.
|
|
222
|
+
|
|
223
|
+
| Option | Description |
|
|
224
|
+
|--------|-------------|
|
|
225
|
+
| **(a) Smart Resume** (recommended) | Fix test failures if any, complete remaining plan tasks, then review → commit |
|
|
226
|
+
| **(b) Review & Commit Only** | If implementation looks complete (all tasks done, tests passing), skip to code review |
|
|
227
|
+
| **(c) Re-implement from Plan** | Keep spec and plan, discard code changes, re-implement. Useful if partial code is low quality |
|
|
228
|
+
| **(d) Clean Restart** | Discard everything, start from scratch (equivalent to `reset-feature.sh --clean --run`) |
|
|
229
|
+
|
|
230
|
+
**Scenario B: Only planning artifacts** (spec/plan exist, no code changes)
|
|
231
|
+
Session died during or after planning, before implementation started.
|
|
232
|
+
|
|
233
|
+
| Option | Description |
|
|
234
|
+
|--------|-------------|
|
|
235
|
+
| **(a) Start Implementation** (recommended) | Plan is ready, begin implementing |
|
|
236
|
+
| **(b) Re-plan** | Keep spec, regenerate plan (useful if plan was incomplete or poor) |
|
|
237
|
+
| **(c) Clean Restart** | Discard everything and start over |
|
|
238
|
+
|
|
239
|
+
**Scenario C: Code changes but no artifacts** (git changes, no .prizmkit artifacts)
|
|
240
|
+
Unusual — might be manual work or artifacts were cleaned.
|
|
241
|
+
|
|
242
|
+
| Option | Description |
|
|
243
|
+
|--------|-------------|
|
|
244
|
+
| **(a) Adopt & Continue** | Read existing code, proceed to review + commit |
|
|
245
|
+
| **(b) Clean Restart** | Discard and restart |
|
|
246
|
+
|
|
247
|
+
**Scenario D: Already committed** (feature branch has commits ahead of main)
|
|
248
|
+
Session completed implementation but didn't finish post-commit steps.
|
|
249
|
+
|
|
250
|
+
| Option | Description |
|
|
251
|
+
|--------|-------------|
|
|
252
|
+
| **(a) Complete Post-commit** (recommended) | Run code review, retrospective, handle merge |
|
|
253
|
+
| **(b) Clean Restart** | Discard and restart |
|
|
254
|
+
|
|
255
|
+
**Scenario E: No state found** (no branch, no artifacts, no code)
|
|
256
|
+
Feature was never executed or was fully cleaned.
|
|
257
|
+
|
|
258
|
+
| Option | Description |
|
|
259
|
+
|--------|-------------|
|
|
260
|
+
| **(a) Run from Scratch** | Suggest using `dev-pipeline-launcher` (Intent E) or `retry-feature.sh` instead |
|
|
261
|
+
|
|
262
|
+
Ask the user to choose. Default to the recommended option.
|
|
263
|
+
|
|
264
|
+
**CHECKPOINT CP-RW-1**: User confirmed recovery strategy.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Phase 3: Execute Recovery
|
|
269
|
+
|
|
270
|
+
**Goal**: Execute the remaining phases based on the user's chosen strategy.
|
|
271
|
+
|
|
272
|
+
### 3.0 Branch Setup
|
|
273
|
+
|
|
274
|
+
Ensure we're on the right branch:
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
# If feature branch exists but we're not on it:
|
|
278
|
+
git checkout feat/{slug}
|
|
279
|
+
|
|
280
|
+
# If no feature branch but code changes are on current branch:
|
|
281
|
+
git checkout -b feat/{slug}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### 3.1 Read Existing Artifacts
|
|
285
|
+
|
|
286
|
+
Load the artifacts that were preserved, so we have full context:
|
|
287
|
+
|
|
288
|
+
1. **Read spec.md** (if exists) — understand what this feature should do
|
|
289
|
+
2. **Read plan.md** (if exists) — understand the implementation plan and task breakdown
|
|
290
|
+
3. **Scan code changes** — `git diff main --stat` to understand what was already done
|
|
291
|
+
4. **Read relevant `.prizm-docs/`** — load project context (L0 root, relevant L1)
|
|
292
|
+
|
|
293
|
+
This step replaces the Phase 0-2 that the pipeline would have done (project bootstrap, context building, planning). The artifacts serve as memory of the interrupted session.
|
|
294
|
+
|
|
295
|
+
### 3.2 Fix Existing Test Failures (if any)
|
|
296
|
+
|
|
297
|
+
If tests are failing from the interrupted session, fix them first. Continuing implementation on top of a broken state compounds errors.
|
|
298
|
+
|
|
299
|
+
1. Read the test failure output
|
|
300
|
+
2. Read the relevant source files
|
|
301
|
+
3. Apply minimal fixes to make tests pass
|
|
302
|
+
4. Verify: run test suite → all green
|
|
303
|
+
|
|
304
|
+
If failures cannot be fixed after 3 attempts, ask the user whether to:
|
|
305
|
+
- Continue anyway (if failures are unrelated to this feature)
|
|
306
|
+
- Switch to "Re-implement from Plan" strategy
|
|
307
|
+
- Clean restart
|
|
308
|
+
|
|
309
|
+
### 3.3 Continue Implementation
|
|
310
|
+
|
|
311
|
+
If plan.md exists with incomplete tasks:
|
|
312
|
+
|
|
313
|
+
1. **Read plan.md** and identify remaining tasks — look for unchecked `[ ]` items in the Tasks section
|
|
314
|
+
2. **For each remaining task**:
|
|
315
|
+
- Read the task description from plan.md
|
|
316
|
+
- Implement the change following the same approach as `/prizmkit-implement`
|
|
317
|
+
- Run full test suite after each task
|
|
318
|
+
- Mark task as complete `[x]` in plan.md
|
|
319
|
+
3. After all tasks complete → full test suite must pass
|
|
320
|
+
|
|
321
|
+
If no plan.md exists but code changes are present (Scenario C), skip this step — go straight to review.
|
|
322
|
+
|
|
323
|
+
### 3.4 Code Review
|
|
324
|
+
|
|
325
|
+
Invoke `/prizmkit-code-review` on all changes:
|
|
326
|
+
- Review scope: all files modified/added relative to main branch (`git diff main --stat`)
|
|
327
|
+
- If review returns NEEDS_FIXES → apply fixes, re-review (max 2 rounds)
|
|
328
|
+
- If review returns PASS or PASS_WITH_WARNINGS → proceed
|
|
329
|
+
|
|
330
|
+
### 3.5 User Verification
|
|
331
|
+
|
|
332
|
+
Ask user: "Implementation recovered and reviewed. Verify before committing?"
|
|
333
|
+
- **(a) Run the app** → suggest and start the dev server
|
|
334
|
+
- **(b) Run a specific command** → execute what the user specifies
|
|
335
|
+
- **(c) Skip** → proceed directly to commit
|
|
336
|
+
|
|
337
|
+
If user reports issues: return to 3.3 (max 2 additional rounds).
|
|
338
|
+
|
|
339
|
+
### 3.6 Retrospective & Commit
|
|
340
|
+
|
|
341
|
+
1. **Invoke `/prizmkit-retrospective`** — update `.prizm-docs/` for structural changes caused by this feature
|
|
342
|
+
2. **Invoke `/prizmkit-committer`** — commit with `feat(<scope>): <description>` prefix
|
|
343
|
+
3. **Update pipeline state** — mark feature as completed:
|
|
344
|
+
```bash
|
|
345
|
+
python3 dev-pipeline/scripts/update-feature-status.py \
|
|
346
|
+
--feature-list feature-list.json \
|
|
347
|
+
--state-dir dev-pipeline/state \
|
|
348
|
+
--feature-id <FEATURE_ID> \
|
|
349
|
+
--action complete
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### 3.7 Post-Recovery
|
|
353
|
+
|
|
354
|
+
1. **Ask merge preference**:
|
|
355
|
+
- (a) Merge to main and delete feature branch
|
|
356
|
+
- (b) Keep feature branch for PR review
|
|
357
|
+
- (c) Decide later
|
|
358
|
+
|
|
359
|
+
2. **Report completion**:
|
|
360
|
+
```
|
|
361
|
+
Recovery complete: F-007 — User Authentication
|
|
362
|
+
|
|
363
|
+
Recovered from: implementation phase (3/5 tasks done)
|
|
364
|
+
Completed: 2 remaining tasks + test fixes + review + commit
|
|
365
|
+
Work preserved: spec.md, plan.md, 3 completed tasks
|
|
366
|
+
|
|
367
|
+
Next: Check pipeline status for remaining features,
|
|
368
|
+
or invoke /recovery-workflow for another failed feature.
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
3. **Suggest next steps**:
|
|
372
|
+
- If other features are pending/failed → offer to check pipeline status or recover another
|
|
373
|
+
- If this was the last feature → suggest integration verification
|
|
374
|
+
|
|
375
|
+
**CHECKPOINT CP-RW-2**: Feature recovered, committed, and pipeline state updated.
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## Error Handling
|
|
380
|
+
|
|
381
|
+
| Scenario | Action |
|
|
382
|
+
|----------|--------|
|
|
383
|
+
| Feature ID not found in feature-list.json | Ask user for correct ID or file path |
|
|
384
|
+
| No state to recover (never executed) | Inform user, suggest `dev-pipeline-launcher` instead |
|
|
385
|
+
| Feature branch was deleted | Check if changes exist elsewhere; if not, offer clean start |
|
|
386
|
+
| Merge conflicts on feature branch | Show conflicts, help resolve before continuing |
|
|
387
|
+
| Test failures unfixable after 3 attempts | Escalate to user, offer alternative strategies |
|
|
388
|
+
| Code quality too low to salvage | Recommend re-implement from plan |
|
|
389
|
+
| Pipeline state file inconsistent with git | Trust git and filesystem over status.json — git is the ground truth |
|
|
390
|
+
| detection script fails | Fall back to manual detection (Phase 1 checks can be done individually via bash) |
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## Relationship to Other Skills
|
|
395
|
+
|
|
396
|
+
| Skill | Relationship |
|
|
397
|
+
|-------|-------------|
|
|
398
|
+
| `dev-pipeline-launcher` | **Upstream** — launcher can suggest this skill when features fail; this skill updates pipeline state after recovery |
|
|
399
|
+
| `retry-feature.sh` | **Alternative** — full clean retry; this skill is the smart alternative |
|
|
400
|
+
| `reset-feature.sh` | **Used by option (d)** — clean restart delegates to reset-feature |
|
|
401
|
+
| `/prizmkit-implement` | **Approach shared in Phase 3.3** — same implementation methodology |
|
|
402
|
+
| `/prizmkit-code-review` | **Called in Phase 3.4** — reviews recovered code |
|
|
403
|
+
| `/prizmkit-committer` | **Called in Phase 3.6** — commits the result |
|
|
404
|
+
| `/prizmkit-retrospective` | **Called in Phase 3.6** — updates .prizm-docs/ |
|
|
405
|
+
| `feature-workflow` | **Sibling** — feature-workflow builds new features; this skill salvages interrupted ones |
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## Comparison with Existing Recovery Tools
|
|
410
|
+
|
|
411
|
+
| Dimension | recovery-workflow | retry-feature.sh | reset + run | run.sh --resume-phase |
|
|
412
|
+
|-----------|------------------|-------------------|-------------|----------------------|
|
|
413
|
+
| State inspection | Deep (git + artifacts + tests) | None | None | None |
|
|
414
|
+
| Preserves code | Yes | No (cleans) | No (cleans) | Prompt-level only |
|
|
415
|
+
| Preserves artifacts | Yes (selective) | No | No | Prompt-level |
|
|
416
|
+
| Runs tests first | Yes | No | No | No |
|
|
417
|
+
| User choice | Multiple strategies | Always clean | Always clean | Fixed phase number |
|
|
418
|
+
| Execution mode | Interactive (in-session) | Spawns new session | Spawns new session | Spawns new session |
|
|
419
|
+
| Token efficiency | High (skips done work) | Low (redo all) | Low (redo all) | Medium |
|
|
420
|
+
| Context continuity | Full (reads existing artifacts) | None (fresh AI session) | None | Partial |
|
|
421
|
+
|
|
422
|
+
## Output
|
|
423
|
+
|
|
424
|
+
- Recovered and completed feature implementation
|
|
425
|
+
- Updated pipeline state (feature marked complete)
|
|
426
|
+
- Git commit with `feat(<scope>):` prefix
|
|
427
|
+
- Updated `.prizm-docs/` (via retrospective)
|
|
428
|
+
- Recovery summary showing what was salvaged vs what was re-done
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
detect-recovery-state.py — Scan filesystem for partial work from an interrupted
|
|
4
|
+
feature pipeline session and output a structured recovery report.
|
|
5
|
+
|
|
6
|
+
Checks four state categories:
|
|
7
|
+
1. Pipeline state (dev-pipeline/state/)
|
|
8
|
+
2. PrizmKit artifacts (.prizmkit/specs/{slug}/)
|
|
9
|
+
3. Git state (branches, uncommitted changes, commits ahead)
|
|
10
|
+
4. Code changes (file counts, directories touched)
|
|
11
|
+
|
|
12
|
+
Does NOT run tests — that's left to the skill so the user sees output in real time.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
python3 detect-recovery-state.py --feature-id F-007 --feature-list feature-list.json
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def run_git(args, cwd=None):
|
|
27
|
+
"""Run a git command and return stdout, or empty string on failure."""
|
|
28
|
+
try:
|
|
29
|
+
result = subprocess.run(
|
|
30
|
+
["git"] + args,
|
|
31
|
+
capture_output=True,
|
|
32
|
+
text=True,
|
|
33
|
+
cwd=cwd,
|
|
34
|
+
timeout=10,
|
|
35
|
+
)
|
|
36
|
+
return result.stdout.strip()
|
|
37
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
38
|
+
return ""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def compute_slug(feature_id, title):
|
|
42
|
+
"""Compute feature slug using the same algorithm as the pipeline."""
|
|
43
|
+
numeric = feature_id.replace("F-", "").replace("f-", "").zfill(3)
|
|
44
|
+
slug = title.lower()
|
|
45
|
+
slug = re.sub(r"[^a-z0-9\s-]", "", slug)
|
|
46
|
+
slug = re.sub(r"[\s]+", "-", slug.strip())
|
|
47
|
+
slug = re.sub(r"-+", "-", slug).strip("-") or "feature"
|
|
48
|
+
return f"{numeric}-{slug}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def find_feature(feature_list_path, feature_id):
|
|
52
|
+
"""Find feature in feature-list.json."""
|
|
53
|
+
with open(feature_list_path) as f:
|
|
54
|
+
data = json.load(f)
|
|
55
|
+
for feat in data.get("features", []):
|
|
56
|
+
if feat.get("id") == feature_id:
|
|
57
|
+
return feat
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def detect_pipeline_state(state_dir, feature_id):
|
|
62
|
+
"""Check dev-pipeline/state/ for feature status."""
|
|
63
|
+
result = {
|
|
64
|
+
"status": "unknown",
|
|
65
|
+
"retry_count": 0,
|
|
66
|
+
"last_session_id": None,
|
|
67
|
+
"last_session_dir": None,
|
|
68
|
+
"has_state": False,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
status_file = os.path.join(state_dir, "features", feature_id, "status.json")
|
|
72
|
+
if not os.path.isfile(status_file):
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
with open(status_file) as f:
|
|
77
|
+
status_data = json.load(f)
|
|
78
|
+
result["has_state"] = True
|
|
79
|
+
result["status"] = status_data.get("status", "unknown")
|
|
80
|
+
result["retry_count"] = status_data.get("retry_count", 0)
|
|
81
|
+
|
|
82
|
+
sessions = status_data.get("sessions", [])
|
|
83
|
+
if sessions:
|
|
84
|
+
last = sessions[-1]
|
|
85
|
+
sid = last.get("session_id", "")
|
|
86
|
+
result["last_session_id"] = sid
|
|
87
|
+
result["last_session_dir"] = os.path.join(
|
|
88
|
+
state_dir, "features", feature_id, "sessions", sid
|
|
89
|
+
)
|
|
90
|
+
except (json.JSONDecodeError, IOError):
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
return result
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def detect_artifacts(project_root, feature_slug):
|
|
97
|
+
"""Check .prizmkit/specs/{slug}/ for planning artifacts."""
|
|
98
|
+
specs_dir = os.path.join(project_root, ".prizmkit", "specs", feature_slug)
|
|
99
|
+
result = {
|
|
100
|
+
"spec_exists": False,
|
|
101
|
+
"plan_exists": False,
|
|
102
|
+
"spec_path": None,
|
|
103
|
+
"plan_path": None,
|
|
104
|
+
"plan_tasks_total": 0,
|
|
105
|
+
"plan_tasks_completed": 0,
|
|
106
|
+
"other_artifacts": [],
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if not os.path.isdir(specs_dir):
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
spec_path = os.path.join(specs_dir, "spec.md")
|
|
113
|
+
plan_path = os.path.join(specs_dir, "plan.md")
|
|
114
|
+
|
|
115
|
+
if os.path.isfile(spec_path):
|
|
116
|
+
result["spec_exists"] = True
|
|
117
|
+
result["spec_path"] = os.path.relpath(spec_path, project_root)
|
|
118
|
+
|
|
119
|
+
if os.path.isfile(plan_path):
|
|
120
|
+
result["plan_exists"] = True
|
|
121
|
+
result["plan_path"] = os.path.relpath(plan_path, project_root)
|
|
122
|
+
|
|
123
|
+
# Count tasks in plan.md (look for checkbox pattern)
|
|
124
|
+
try:
|
|
125
|
+
with open(plan_path) as f:
|
|
126
|
+
content = f.read()
|
|
127
|
+
# Match both [x] and [ ] patterns (task checkboxes)
|
|
128
|
+
completed = len(re.findall(r"^\s*-\s*\[x\]", content, re.MULTILINE | re.IGNORECASE))
|
|
129
|
+
pending = len(re.findall(r"^\s*-\s*\[ \]", content, re.MULTILINE))
|
|
130
|
+
result["plan_tasks_total"] = completed + pending
|
|
131
|
+
result["plan_tasks_completed"] = completed
|
|
132
|
+
except IOError:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
# Check for other artifacts
|
|
136
|
+
try:
|
|
137
|
+
for name in os.listdir(specs_dir):
|
|
138
|
+
if name not in ("spec.md", "plan.md") and os.path.isfile(
|
|
139
|
+
os.path.join(specs_dir, name)
|
|
140
|
+
):
|
|
141
|
+
result["other_artifacts"].append(name)
|
|
142
|
+
except IOError:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
return result
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def detect_git_state(project_root, feature_slug):
|
|
149
|
+
"""Check git for branch existence, uncommitted changes, commits ahead."""
|
|
150
|
+
result = {
|
|
151
|
+
"feature_branch": f"feat/{feature_slug}",
|
|
152
|
+
"branch_exists": False,
|
|
153
|
+
"on_feature_branch": False,
|
|
154
|
+
"uncommitted_files": 0,
|
|
155
|
+
"staged_files": 0,
|
|
156
|
+
"commits_ahead_of_main": 0,
|
|
157
|
+
"current_branch": "",
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# Current branch
|
|
161
|
+
current = run_git(["branch", "--show-current"], cwd=project_root)
|
|
162
|
+
result["current_branch"] = current
|
|
163
|
+
|
|
164
|
+
# Check if feature branch exists
|
|
165
|
+
branch_name = f"feat/{feature_slug}"
|
|
166
|
+
branches_output = run_git(["branch", "--list", branch_name], cwd=project_root)
|
|
167
|
+
if branches_output.strip():
|
|
168
|
+
result["branch_exists"] = True
|
|
169
|
+
|
|
170
|
+
# Also check without feat/ prefix (some pipelines use different naming)
|
|
171
|
+
if not result["branch_exists"]:
|
|
172
|
+
alt_branch = f"feature/{feature_slug}"
|
|
173
|
+
alt_output = run_git(["branch", "--list", alt_branch], cwd=project_root)
|
|
174
|
+
if alt_output.strip():
|
|
175
|
+
result["branch_exists"] = True
|
|
176
|
+
result["feature_branch"] = alt_branch
|
|
177
|
+
|
|
178
|
+
result["on_feature_branch"] = current == result["feature_branch"]
|
|
179
|
+
|
|
180
|
+
# Uncommitted changes (working tree)
|
|
181
|
+
diff_stat = run_git(["diff", "--stat"], cwd=project_root)
|
|
182
|
+
if diff_stat:
|
|
183
|
+
# Count "N files changed" from the summary line
|
|
184
|
+
lines = diff_stat.strip().split("\n")
|
|
185
|
+
result["uncommitted_files"] = max(0, len(lines) - 1) # exclude summary line
|
|
186
|
+
|
|
187
|
+
# Staged changes
|
|
188
|
+
staged_stat = run_git(["diff", "--cached", "--stat"], cwd=project_root)
|
|
189
|
+
if staged_stat:
|
|
190
|
+
lines = staged_stat.strip().split("\n")
|
|
191
|
+
result["staged_files"] = max(0, len(lines) - 1)
|
|
192
|
+
|
|
193
|
+
# Commits ahead of main
|
|
194
|
+
main_branch = "main"
|
|
195
|
+
# Try to detect default branch
|
|
196
|
+
for candidate in ["main", "master"]:
|
|
197
|
+
check = run_git(["rev-parse", "--verify", candidate], cwd=project_root)
|
|
198
|
+
if check:
|
|
199
|
+
main_branch = candidate
|
|
200
|
+
break
|
|
201
|
+
|
|
202
|
+
log_output = run_git(
|
|
203
|
+
["log", f"{main_branch}..HEAD", "--oneline"], cwd=project_root
|
|
204
|
+
)
|
|
205
|
+
if log_output:
|
|
206
|
+
result["commits_ahead_of_main"] = len(log_output.strip().split("\n"))
|
|
207
|
+
|
|
208
|
+
return result
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def detect_code_changes(project_root, main_branch="main"):
|
|
212
|
+
"""Analyze code changes relative to main branch.
|
|
213
|
+
|
|
214
|
+
Filters out pipeline/config files that aren't source code — only counts
|
|
215
|
+
files that represent actual implementation work.
|
|
216
|
+
"""
|
|
217
|
+
# Files/patterns that are pipeline artifacts, not implementation code
|
|
218
|
+
IGNORED_FILES = {
|
|
219
|
+
"feature-list.json",
|
|
220
|
+
"bug-fix-list.json",
|
|
221
|
+
"package-lock.json",
|
|
222
|
+
"yarn.lock",
|
|
223
|
+
"pnpm-lock.yaml",
|
|
224
|
+
}
|
|
225
|
+
IGNORED_PREFIXES = (
|
|
226
|
+
".prizmkit/",
|
|
227
|
+
"dev-pipeline/state/",
|
|
228
|
+
".prizm-docs/",
|
|
229
|
+
".claude/",
|
|
230
|
+
".codebuddy/",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def is_source_file(filepath):
|
|
234
|
+
"""Return True if this file represents implementation code."""
|
|
235
|
+
basename = os.path.basename(filepath)
|
|
236
|
+
if basename in IGNORED_FILES:
|
|
237
|
+
return False
|
|
238
|
+
for prefix in IGNORED_PREFIXES:
|
|
239
|
+
if filepath.startswith(prefix):
|
|
240
|
+
return False
|
|
241
|
+
return True
|
|
242
|
+
|
|
243
|
+
result = {
|
|
244
|
+
"files_modified": 0,
|
|
245
|
+
"files_added": 0,
|
|
246
|
+
"files_deleted": 0,
|
|
247
|
+
"test_files_touched": 0,
|
|
248
|
+
"directories_touched": [],
|
|
249
|
+
"has_changes": False,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# Get diff stat relative to main
|
|
253
|
+
diff_output = run_git(
|
|
254
|
+
["diff", main_branch, "--name-status"], cwd=project_root
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Also include uncommitted changes
|
|
258
|
+
uncommitted = run_git(["diff", "--name-status"], cwd=project_root)
|
|
259
|
+
untracked = run_git(
|
|
260
|
+
["ls-files", "--others", "--exclude-standard"], cwd=project_root
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
all_files = set()
|
|
264
|
+
dirs = set()
|
|
265
|
+
|
|
266
|
+
if diff_output:
|
|
267
|
+
for line in diff_output.strip().split("\n"):
|
|
268
|
+
if not line.strip():
|
|
269
|
+
continue
|
|
270
|
+
parts = line.split("\t", 1)
|
|
271
|
+
if len(parts) < 2:
|
|
272
|
+
continue
|
|
273
|
+
status, filepath = parts[0], parts[1]
|
|
274
|
+
if not is_source_file(filepath):
|
|
275
|
+
continue
|
|
276
|
+
all_files.add(filepath)
|
|
277
|
+
if status.startswith("M"):
|
|
278
|
+
result["files_modified"] += 1
|
|
279
|
+
elif status.startswith("A"):
|
|
280
|
+
result["files_added"] += 1
|
|
281
|
+
elif status.startswith("D"):
|
|
282
|
+
result["files_deleted"] += 1
|
|
283
|
+
|
|
284
|
+
if uncommitted:
|
|
285
|
+
for line in uncommitted.strip().split("\n"):
|
|
286
|
+
if not line.strip():
|
|
287
|
+
continue
|
|
288
|
+
parts = line.split("\t", 1)
|
|
289
|
+
if len(parts) >= 2:
|
|
290
|
+
filepath = parts[1]
|
|
291
|
+
if not is_source_file(filepath):
|
|
292
|
+
continue
|
|
293
|
+
all_files.add(filepath)
|
|
294
|
+
result["files_modified"] += 1
|
|
295
|
+
|
|
296
|
+
if untracked:
|
|
297
|
+
for filepath in untracked.strip().split("\n"):
|
|
298
|
+
if filepath.strip() and is_source_file(filepath.strip()):
|
|
299
|
+
all_files.add(filepath.strip())
|
|
300
|
+
result["files_added"] += 1
|
|
301
|
+
|
|
302
|
+
# Analyze file set
|
|
303
|
+
test_patterns = re.compile(
|
|
304
|
+
r"(test|spec|__tests__|\.test\.|\.spec\.)", re.IGNORECASE
|
|
305
|
+
)
|
|
306
|
+
for filepath in all_files:
|
|
307
|
+
if test_patterns.search(filepath):
|
|
308
|
+
result["test_files_touched"] += 1
|
|
309
|
+
parent = os.path.dirname(filepath)
|
|
310
|
+
if parent:
|
|
311
|
+
# Keep first two levels for readability
|
|
312
|
+
parts = parent.split(os.sep)
|
|
313
|
+
dirs.add(os.sep.join(parts[:2]) + "/")
|
|
314
|
+
|
|
315
|
+
result["directories_touched"] = sorted(dirs)
|
|
316
|
+
result["has_changes"] = len(all_files) > 0
|
|
317
|
+
|
|
318
|
+
return result
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def determine_recovery(artifacts, git_state, code_changes, pipeline):
|
|
322
|
+
"""Recommend a recovery action based on detected state."""
|
|
323
|
+
has_spec = artifacts["spec_exists"]
|
|
324
|
+
has_plan = artifacts["plan_exists"]
|
|
325
|
+
has_code = code_changes["has_changes"]
|
|
326
|
+
has_commits = git_state["commits_ahead_of_main"] > 0
|
|
327
|
+
tasks_total = artifacts["plan_tasks_total"]
|
|
328
|
+
tasks_done = artifacts["plan_tasks_completed"]
|
|
329
|
+
|
|
330
|
+
# Scenario D: Already committed
|
|
331
|
+
if has_commits:
|
|
332
|
+
return {
|
|
333
|
+
"recommended_action": "complete_post_commit",
|
|
334
|
+
"recommended_phase": "review",
|
|
335
|
+
"scenario": "D",
|
|
336
|
+
"reason": f"{git_state['commits_ahead_of_main']} commit(s) ahead of main — implementation may be complete",
|
|
337
|
+
"remaining_work": "code review + retrospective + merge",
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
# Scenario A: Implementation in progress
|
|
341
|
+
if has_plan and has_code:
|
|
342
|
+
if tasks_total > 0 and tasks_done == tasks_total:
|
|
343
|
+
return {
|
|
344
|
+
"recommended_action": "review_and_commit",
|
|
345
|
+
"recommended_phase": "review",
|
|
346
|
+
"scenario": "A",
|
|
347
|
+
"reason": f"all {tasks_total} plan tasks completed, code changes present",
|
|
348
|
+
"remaining_work": "code review + commit",
|
|
349
|
+
}
|
|
350
|
+
else:
|
|
351
|
+
tasks_remaining = tasks_total - tasks_done if tasks_total > 0 else "unknown"
|
|
352
|
+
return {
|
|
353
|
+
"recommended_action": "continue_implementation",
|
|
354
|
+
"recommended_phase": "implement",
|
|
355
|
+
"scenario": "A",
|
|
356
|
+
"reason": f"spec and plan exist, {tasks_done}/{tasks_total} tasks completed, code changes present",
|
|
357
|
+
"remaining_work": f"{tasks_remaining} tasks + review + commit",
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
# Scenario B: Only planning artifacts
|
|
361
|
+
if has_spec or has_plan:
|
|
362
|
+
if has_plan:
|
|
363
|
+
return {
|
|
364
|
+
"recommended_action": "start_implementation",
|
|
365
|
+
"recommended_phase": "implement",
|
|
366
|
+
"scenario": "B",
|
|
367
|
+
"reason": "spec and plan exist, no code changes yet",
|
|
368
|
+
"remaining_work": f"{tasks_total} tasks + review + commit",
|
|
369
|
+
}
|
|
370
|
+
else:
|
|
371
|
+
return {
|
|
372
|
+
"recommended_action": "generate_plan",
|
|
373
|
+
"recommended_phase": "plan",
|
|
374
|
+
"scenario": "B",
|
|
375
|
+
"reason": "spec exists but no plan — session interrupted during planning",
|
|
376
|
+
"remaining_work": "plan + implement + review + commit",
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
# Scenario C: Code changes but no artifacts
|
|
380
|
+
if has_code:
|
|
381
|
+
return {
|
|
382
|
+
"recommended_action": "adopt_and_continue",
|
|
383
|
+
"recommended_phase": "review",
|
|
384
|
+
"scenario": "C",
|
|
385
|
+
"reason": "code changes found but no prizmkit artifacts — possible manual work or artifacts cleaned",
|
|
386
|
+
"remaining_work": "review + commit",
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
# Scenario E: Nothing found
|
|
390
|
+
return {
|
|
391
|
+
"recommended_action": "start_fresh",
|
|
392
|
+
"recommended_phase": "none",
|
|
393
|
+
"scenario": "E",
|
|
394
|
+
"reason": "no artifacts, no code changes, no commits — feature was never executed or fully cleaned",
|
|
395
|
+
"remaining_work": "full pipeline run",
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def main():
|
|
400
|
+
parser = argparse.ArgumentParser(
|
|
401
|
+
description="Detect recovery state for an interrupted feature session"
|
|
402
|
+
)
|
|
403
|
+
parser.add_argument("--feature-id", required=True, help="Feature ID (e.g., F-007)")
|
|
404
|
+
parser.add_argument(
|
|
405
|
+
"--feature-list",
|
|
406
|
+
default="feature-list.json",
|
|
407
|
+
help="Path to feature-list.json (default: feature-list.json)",
|
|
408
|
+
)
|
|
409
|
+
parser.add_argument(
|
|
410
|
+
"--state-dir",
|
|
411
|
+
default=None,
|
|
412
|
+
help="Pipeline state directory (default: dev-pipeline/state)",
|
|
413
|
+
)
|
|
414
|
+
parser.add_argument(
|
|
415
|
+
"--project-root",
|
|
416
|
+
default=None,
|
|
417
|
+
help="Project root directory (default: auto-detect from git)",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
args = parser.parse_args()
|
|
421
|
+
|
|
422
|
+
# Resolve project root
|
|
423
|
+
if args.project_root:
|
|
424
|
+
project_root = os.path.abspath(args.project_root)
|
|
425
|
+
else:
|
|
426
|
+
git_root = run_git(["rev-parse", "--show-toplevel"])
|
|
427
|
+
project_root = git_root if git_root else os.getcwd()
|
|
428
|
+
|
|
429
|
+
# Resolve state dir
|
|
430
|
+
state_dir = args.state_dir or os.path.join(project_root, "dev-pipeline", "state")
|
|
431
|
+
|
|
432
|
+
# Resolve feature list path
|
|
433
|
+
feature_list_path = args.feature_list
|
|
434
|
+
if not os.path.isabs(feature_list_path):
|
|
435
|
+
feature_list_path = os.path.join(project_root, feature_list_path)
|
|
436
|
+
|
|
437
|
+
# Find feature
|
|
438
|
+
if not os.path.isfile(feature_list_path):
|
|
439
|
+
print(
|
|
440
|
+
json.dumps({"error": f"Feature list not found: {feature_list_path}"}),
|
|
441
|
+
file=sys.stderr,
|
|
442
|
+
)
|
|
443
|
+
sys.exit(1)
|
|
444
|
+
|
|
445
|
+
feature = find_feature(feature_list_path, args.feature_id)
|
|
446
|
+
if not feature:
|
|
447
|
+
print(
|
|
448
|
+
json.dumps(
|
|
449
|
+
{
|
|
450
|
+
"error": f"Feature {args.feature_id} not found in {feature_list_path}"
|
|
451
|
+
}
|
|
452
|
+
),
|
|
453
|
+
file=sys.stderr,
|
|
454
|
+
)
|
|
455
|
+
sys.exit(1)
|
|
456
|
+
|
|
457
|
+
title = feature.get("title", "untitled")
|
|
458
|
+
slug = compute_slug(args.feature_id, title)
|
|
459
|
+
|
|
460
|
+
# Run all detection phases
|
|
461
|
+
pipeline = detect_pipeline_state(state_dir, args.feature_id)
|
|
462
|
+
artifacts = detect_artifacts(project_root, slug)
|
|
463
|
+
git_state = detect_git_state(project_root, slug)
|
|
464
|
+
code_changes = detect_code_changes(project_root)
|
|
465
|
+
recovery = determine_recovery(artifacts, git_state, code_changes, pipeline)
|
|
466
|
+
|
|
467
|
+
# Build report
|
|
468
|
+
report = {
|
|
469
|
+
"feature_id": args.feature_id,
|
|
470
|
+
"feature_title": title,
|
|
471
|
+
"feature_slug": slug,
|
|
472
|
+
"pipeline": pipeline,
|
|
473
|
+
"artifacts": artifacts,
|
|
474
|
+
"git": git_state,
|
|
475
|
+
"code": code_changes,
|
|
476
|
+
"recovery": recovery,
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
print(json.dumps(report, indent=2))
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
if __name__ == "__main__":
|
|
483
|
+
main()
|