prizmkit 1.1.26 → 1.1.29
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/run-bugfix.sh +3 -8
- package/bundled/dev-pipeline/run-feature.sh +3 -8
- package/bundled/dev-pipeline/run-recovery.sh +691 -0
- package/bundled/dev-pipeline/run-refactor.sh +3 -8
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +759 -0
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/recovery-workflow/SKILL.md +35 -0
- package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +111 -42
- package/package.json +1 -1
- package/src/upgrade.js +18 -11
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate a recovery bootstrap prompt from detection output.
|
|
3
|
+
|
|
4
|
+
Reads the JSON output of detect-recovery-state.py, determines which workflow
|
|
5
|
+
was interrupted and at which phase, then assembles a comprehensive bootstrap
|
|
6
|
+
prompt that explicitly enumerates every remaining phase with full instructions.
|
|
7
|
+
|
|
8
|
+
Unlike the feature/bugfix prompt generators that use template files, this script
|
|
9
|
+
builds the prompt programmatically because recovery prompts vary dramatically
|
|
10
|
+
by workflow type and phase.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
python3 generate-recovery-prompt.py \
|
|
14
|
+
--detection-json <path> \
|
|
15
|
+
--output <path> \
|
|
16
|
+
[--project-root <path>] \
|
|
17
|
+
[--session-id <id>]
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
from utils import load_json_file, setup_logging
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
LOGGER = setup_logging("generate-recovery-prompt")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ============================================================
|
|
33
|
+
# Phase instruction maps — one per workflow type
|
|
34
|
+
#
|
|
35
|
+
# Each phase maps to (name, instructions) where instructions are
|
|
36
|
+
# adapted for autonomous (non-interactive) recovery mode.
|
|
37
|
+
# These must be kept in sync with the corresponding SKILL.md files.
|
|
38
|
+
# ============================================================
|
|
39
|
+
|
|
40
|
+
BUGFIX_PHASES = {
|
|
41
|
+
0: (
|
|
42
|
+
"Branch Setup",
|
|
43
|
+
"""\
|
|
44
|
+
Check current branch. You should already be on a fix/* branch from the
|
|
45
|
+
interrupted session. If so, continue on it. If somehow on main, create
|
|
46
|
+
a new fix branch:
|
|
47
|
+
```bash
|
|
48
|
+
git checkout -b fix/{bug_id}-recovery
|
|
49
|
+
```""",
|
|
50
|
+
),
|
|
51
|
+
1: (
|
|
52
|
+
"Deep Bug Diagnosis",
|
|
53
|
+
"""\
|
|
54
|
+
Read the bug description and all available artifacts to understand the bug.
|
|
55
|
+
Since this is an autonomous recovery session, skip interactive Q&A.
|
|
56
|
+
Use whatever information is available:
|
|
57
|
+
- Read bug entry from `.prizmkit/plans/bug-fix-list.json` if bug ID is known
|
|
58
|
+
- Read any existing artifacts in `.prizmkit/bugfix/{bug_id}/`
|
|
59
|
+
- Read relevant source code and test files
|
|
60
|
+
- Read `.prizm-docs/` for affected modules
|
|
61
|
+
|
|
62
|
+
Produce a bug summary with: symptom, reproduction steps, expected behavior,
|
|
63
|
+
affected files, and root cause hypothesis.""",
|
|
64
|
+
),
|
|
65
|
+
2: (
|
|
66
|
+
"Triage",
|
|
67
|
+
"""\
|
|
68
|
+
Locate affected code and identify root cause:
|
|
69
|
+
1. Read `.prizm-docs/root.prizm` then relevant L1/L2 docs for affected modules
|
|
70
|
+
2. Read files mentioned in the bug description or error/stack trace
|
|
71
|
+
3. Check `.prizm-docs/` TRAPS for known patterns
|
|
72
|
+
4. Classify: root cause (confirmed/suspected), blast radius, fix complexity
|
|
73
|
+
5. Log your diagnosis (no need to ask for user confirmation in autonomous mode)""",
|
|
74
|
+
),
|
|
75
|
+
3: (
|
|
76
|
+
"Reproduce",
|
|
77
|
+
"""\
|
|
78
|
+
Create a failing test that proves the bug exists:
|
|
79
|
+
1. Write a reproduction test: `<module>.test.ts` with test case `should handle <bug scenario>`
|
|
80
|
+
2. Run the test — confirm it FAILS (red)
|
|
81
|
+
3. If the bug is hard to reproduce automatically, write a best-effort test
|
|
82
|
+
and proceed""",
|
|
83
|
+
),
|
|
84
|
+
4: (
|
|
85
|
+
"Fix",
|
|
86
|
+
"""\
|
|
87
|
+
Implement the minimal fix (red → green):
|
|
88
|
+
1. Read fix-plan.md if it exists for the planned approach
|
|
89
|
+
2. Change the minimum code to fix the root cause — do NOT refactor
|
|
90
|
+
3. Run the reproduction test — must PASS (green)
|
|
91
|
+
4. Run the full module test suite — must pass (no regressions)
|
|
92
|
+
5. If regressions occur, fix them (max 3 attempts)""",
|
|
93
|
+
),
|
|
94
|
+
5: (
|
|
95
|
+
"Review",
|
|
96
|
+
"""\
|
|
97
|
+
Verify fix quality:
|
|
98
|
+
1. Self-review: does the fix address root cause (not just symptom)?
|
|
99
|
+
Edge cases covered? Follows project conventions?
|
|
100
|
+
2. Run full test suite one final time
|
|
101
|
+
3. If any issues found, fix and re-review (max 3 rounds)""",
|
|
102
|
+
),
|
|
103
|
+
6: (
|
|
104
|
+
"User Verification",
|
|
105
|
+
"""\
|
|
106
|
+
Since this is an autonomous recovery session, substitute automated verification
|
|
107
|
+
for manual user testing:
|
|
108
|
+
1. Run the full test suite
|
|
109
|
+
2. Verify ALL tests pass
|
|
110
|
+
3. If tests fail, fix and retry (max 3 attempts)
|
|
111
|
+
4. Proceed to next phase once all tests are green""",
|
|
112
|
+
),
|
|
113
|
+
7: (
|
|
114
|
+
"Commit & Merge",
|
|
115
|
+
"""\
|
|
116
|
+
Commit the fix and finalize:
|
|
117
|
+
1. Run `/prizmkit-retrospective` (structural sync only — update file counts,
|
|
118
|
+
interfaces, dependencies in .prizm-docs/)
|
|
119
|
+
2. Stage all changed files explicitly (NEVER use `git add -A` or `git add .`)
|
|
120
|
+
3. Run `/prizmkit-committer` with commit prefix `fix(<scope>): <description>`
|
|
121
|
+
4. Verify working tree is clean: `git status --short`
|
|
122
|
+
5. Write `fix-report.md` to `.prizmkit/bugfix/{bug_id}/fix-report.md` with:
|
|
123
|
+
- Root cause summary
|
|
124
|
+
- Fix description
|
|
125
|
+
- Files changed
|
|
126
|
+
- Test results""",
|
|
127
|
+
),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
FEATURE_PHASES = {
|
|
131
|
+
1: (
|
|
132
|
+
"Brainstorm",
|
|
133
|
+
"""\
|
|
134
|
+
Since this is an autonomous recovery session, work with whatever context is
|
|
135
|
+
available. Read existing project files, `.prizm-docs/`, and any user-provided
|
|
136
|
+
materials to understand the requirements. Skip interactive Q&A.
|
|
137
|
+
Produce a requirements summary if one doesn't already exist.""",
|
|
138
|
+
),
|
|
139
|
+
2: (
|
|
140
|
+
"Plan",
|
|
141
|
+
"""\
|
|
142
|
+
Invoke `/feature-planner` skill with the requirements summary to generate
|
|
143
|
+
`.prizmkit/plans/feature-list.json`. Validate the output exists and contains
|
|
144
|
+
properly structured features.""",
|
|
145
|
+
),
|
|
146
|
+
3: (
|
|
147
|
+
"Launch",
|
|
148
|
+
"""\
|
|
149
|
+
Invoke `/feature-pipeline-launcher` skill:
|
|
150
|
+
- Input: path to `.prizmkit/plans/feature-list.json`
|
|
151
|
+
- The launcher handles execution mode selection and prerequisites
|
|
152
|
+
- Let the launcher present options and manage the pipeline start
|
|
153
|
+
|
|
154
|
+
If `/feature-pipeline-launcher` is not available, run the pipeline directly:
|
|
155
|
+
```bash
|
|
156
|
+
./dev-pipeline/run-feature.sh run .prizmkit/plans/feature-list.json
|
|
157
|
+
```""",
|
|
158
|
+
),
|
|
159
|
+
4: (
|
|
160
|
+
"Monitor",
|
|
161
|
+
"""\
|
|
162
|
+
Check pipeline status and report results:
|
|
163
|
+
```bash
|
|
164
|
+
python3 dev-pipeline/scripts/update-feature-status.py \\
|
|
165
|
+
--feature-list .prizmkit/plans/feature-list.json \\
|
|
166
|
+
--state-dir .prizmkit/state/features \\
|
|
167
|
+
--action status
|
|
168
|
+
```
|
|
169
|
+
Report completion status for each feature.""",
|
|
170
|
+
),
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
REFACTOR_PHASES = {
|
|
174
|
+
1: (
|
|
175
|
+
"Brainstorm",
|
|
176
|
+
"""\
|
|
177
|
+
Since this is an autonomous recovery session, work with whatever context is
|
|
178
|
+
available. Read existing project files, `.prizm-docs/`, and any materials
|
|
179
|
+
to understand the refactoring goals. Skip interactive Q&A.
|
|
180
|
+
Produce a refactoring goals summary if one doesn't already exist.""",
|
|
181
|
+
),
|
|
182
|
+
2: (
|
|
183
|
+
"Plan",
|
|
184
|
+
"""\
|
|
185
|
+
Invoke `/refactor-planner` skill with the goals summary to generate
|
|
186
|
+
`.prizmkit/plans/refactor-list.json`. Validate the output exists and contains
|
|
187
|
+
properly structured refactor items.""",
|
|
188
|
+
),
|
|
189
|
+
3: (
|
|
190
|
+
"Launch",
|
|
191
|
+
"""\
|
|
192
|
+
Invoke `/refactor-pipeline-launcher` skill:
|
|
193
|
+
- Input: path to `.prizmkit/plans/refactor-list.json`
|
|
194
|
+
- The launcher handles execution mode selection and prerequisites
|
|
195
|
+
- Let the launcher present options and manage the pipeline start
|
|
196
|
+
|
|
197
|
+
If `/refactor-pipeline-launcher` is not available, run the pipeline directly:
|
|
198
|
+
```bash
|
|
199
|
+
./dev-pipeline/run-refactor.sh run .prizmkit/plans/refactor-list.json
|
|
200
|
+
```""",
|
|
201
|
+
),
|
|
202
|
+
4: (
|
|
203
|
+
"Monitor",
|
|
204
|
+
"""\
|
|
205
|
+
Check pipeline status and report results:
|
|
206
|
+
```bash
|
|
207
|
+
python3 dev-pipeline/scripts/update-refactor-status.py \\
|
|
208
|
+
--refactor-list .prizmkit/plans/refactor-list.json \\
|
|
209
|
+
--state-dir .prizmkit/state/refactor \\
|
|
210
|
+
--action status
|
|
211
|
+
```
|
|
212
|
+
Report completion status for each refactor item.""",
|
|
213
|
+
),
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Maps workflow_type to (phase_map, all_phases_ordered)
|
|
217
|
+
WORKFLOW_REGISTRY = {
|
|
218
|
+
"bug-fix-workflow": (BUGFIX_PHASES, [0, 1, 2, 3, 4, 5, 6, 7]),
|
|
219
|
+
"feature-workflow": (FEATURE_PHASES, [1, 2, 3, 4]),
|
|
220
|
+
"refactor-workflow": (REFACTOR_PHASES, [1, 2, 3, 4]),
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ============================================================
|
|
225
|
+
# Artifact reading
|
|
226
|
+
# ============================================================
|
|
227
|
+
|
|
228
|
+
def read_file_safe(path, max_chars=8000):
|
|
229
|
+
"""Read a file, truncate if too large. Returns content or None."""
|
|
230
|
+
if not os.path.isfile(path):
|
|
231
|
+
return None
|
|
232
|
+
try:
|
|
233
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
234
|
+
content = f.read()
|
|
235
|
+
if len(content) > max_chars:
|
|
236
|
+
content = content[:max_chars] + "\n\n... (truncated)"
|
|
237
|
+
return content
|
|
238
|
+
except (IOError, UnicodeDecodeError):
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def read_bugfix_artifacts(project_root, bug_id):
|
|
243
|
+
"""Read existing bug fix artifacts for context injection."""
|
|
244
|
+
if not bug_id:
|
|
245
|
+
return {}
|
|
246
|
+
bugfix_dir = os.path.join(project_root, ".prizmkit", "bugfix", bug_id)
|
|
247
|
+
artifacts = {}
|
|
248
|
+
for name in [
|
|
249
|
+
"fix-plan.md", "spec.md", "plan.md",
|
|
250
|
+
"context-snapshot.md", "fix-report.md",
|
|
251
|
+
]:
|
|
252
|
+
path = os.path.join(bugfix_dir, name)
|
|
253
|
+
content = read_file_safe(path)
|
|
254
|
+
if content:
|
|
255
|
+
artifacts[name] = content
|
|
256
|
+
return artifacts
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def read_feature_artifacts(project_root):
|
|
260
|
+
"""Read existing feature workflow artifacts."""
|
|
261
|
+
artifacts = {}
|
|
262
|
+
# Feature list
|
|
263
|
+
for location in [
|
|
264
|
+
os.path.join(project_root, ".prizmkit", "plans", "feature-list.json"),
|
|
265
|
+
os.path.join(project_root, "feature-list.json"),
|
|
266
|
+
]:
|
|
267
|
+
content = read_file_safe(location)
|
|
268
|
+
if content:
|
|
269
|
+
artifacts["feature-list.json"] = content
|
|
270
|
+
break
|
|
271
|
+
return artifacts
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def read_refactor_artifacts(project_root):
|
|
275
|
+
"""Read existing refactor workflow artifacts."""
|
|
276
|
+
artifacts = {}
|
|
277
|
+
for location in [
|
|
278
|
+
os.path.join(project_root, ".prizmkit", "plans", "refactor-list.json"),
|
|
279
|
+
os.path.join(project_root, "refactor-list.json"),
|
|
280
|
+
]:
|
|
281
|
+
content = read_file_safe(location)
|
|
282
|
+
if content:
|
|
283
|
+
artifacts["refactor-list.json"] = content
|
|
284
|
+
break
|
|
285
|
+
return artifacts
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def get_code_diff_summary(project_root, main_branch="main"):
|
|
289
|
+
"""Get a summary of code changes for context."""
|
|
290
|
+
try:
|
|
291
|
+
result = subprocess.run(
|
|
292
|
+
["git", "diff", main_branch, "--stat"],
|
|
293
|
+
capture_output=True, text=True, cwd=project_root, timeout=10,
|
|
294
|
+
)
|
|
295
|
+
if result.stdout.strip():
|
|
296
|
+
return result.stdout.strip()
|
|
297
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
298
|
+
pass
|
|
299
|
+
return "(no diff available)"
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def read_bug_description(project_root, bug_id):
|
|
303
|
+
"""Try to read bug description from bug-fix-list.json."""
|
|
304
|
+
if not bug_id:
|
|
305
|
+
return None
|
|
306
|
+
for location in [
|
|
307
|
+
os.path.join(project_root, ".prizmkit", "plans", "bug-fix-list.json"),
|
|
308
|
+
os.path.join(project_root, "bug-fix-list.json"),
|
|
309
|
+
]:
|
|
310
|
+
if os.path.isfile(location):
|
|
311
|
+
try:
|
|
312
|
+
with open(location, "r", encoding="utf-8") as f:
|
|
313
|
+
data = json.load(f)
|
|
314
|
+
for bug in data.get("bugs", []):
|
|
315
|
+
if bug.get("id") == bug_id:
|
|
316
|
+
return bug
|
|
317
|
+
except (json.JSONDecodeError, IOError):
|
|
318
|
+
pass
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# ============================================================
|
|
323
|
+
# Prompt assembly
|
|
324
|
+
# ============================================================
|
|
325
|
+
|
|
326
|
+
def get_remaining_phases(workflow_type, current_phase):
|
|
327
|
+
"""Get list of remaining phase numbers (inclusive of current)."""
|
|
328
|
+
phase_map, all_phases = WORKFLOW_REGISTRY.get(workflow_type, ({}, []))
|
|
329
|
+
remaining = [p for p in all_phases if p >= current_phase]
|
|
330
|
+
return remaining, phase_map
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def format_artifact_section(artifacts):
|
|
334
|
+
"""Format artifacts dict as markdown sections."""
|
|
335
|
+
if not artifacts:
|
|
336
|
+
return "(no artifacts found from previous session)"
|
|
337
|
+
sections = []
|
|
338
|
+
for name, content in sorted(artifacts.items()):
|
|
339
|
+
sections.append(
|
|
340
|
+
"### {name}\n\n```\n{content}\n```".format(
|
|
341
|
+
name=name, content=content,
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
return "\n\n".join(sections)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def build_bugfix_prompt(detection, project_root):
|
|
348
|
+
"""Build recovery prompt for bug-fix-workflow."""
|
|
349
|
+
context = detection.get("context", {})
|
|
350
|
+
bug_id = context.get("bug_id", "UNKNOWN")
|
|
351
|
+
branch = context.get("branch", "unknown")
|
|
352
|
+
phase = detection.get("phase", 1)
|
|
353
|
+
phase_name = detection.get("phase_name", "Unknown")
|
|
354
|
+
git_state = detection.get("git", {})
|
|
355
|
+
code_state = detection.get("code", {})
|
|
356
|
+
recovery = detection.get("recovery", {})
|
|
357
|
+
|
|
358
|
+
# Read artifacts
|
|
359
|
+
artifacts = read_bugfix_artifacts(project_root, bug_id)
|
|
360
|
+
bug_desc = read_bug_description(project_root, bug_id)
|
|
361
|
+
diff_summary = get_code_diff_summary(project_root, "main")
|
|
362
|
+
|
|
363
|
+
# Get remaining phases
|
|
364
|
+
remaining, phase_map = get_remaining_phases("bug-fix-workflow", phase)
|
|
365
|
+
|
|
366
|
+
# Build prompt
|
|
367
|
+
lines = []
|
|
368
|
+
lines.append("# Recovery Session — Bug Fix Workflow")
|
|
369
|
+
lines.append("")
|
|
370
|
+
lines.append("## Context")
|
|
371
|
+
lines.append("")
|
|
372
|
+
lines.append("You are RECOVERING an interrupted bug-fix-workflow session.")
|
|
373
|
+
lines.append("")
|
|
374
|
+
lines.append("- **Bug ID**: {}".format(bug_id))
|
|
375
|
+
lines.append("- **Branch**: {}".format(branch))
|
|
376
|
+
lines.append("- **Interrupted at**: Phase {} — {}".format(phase, phase_name))
|
|
377
|
+
lines.append("- **Remaining work**: {}".format(recovery.get("remaining_work", "unknown")))
|
|
378
|
+
lines.append("")
|
|
379
|
+
|
|
380
|
+
lines.append("## CRITICAL RULES")
|
|
381
|
+
lines.append("")
|
|
382
|
+
lines.append("1. You MUST complete ALL remaining phases listed below — do NOT stop after implementation")
|
|
383
|
+
lines.append("2. Execute phases in ORDER. Do NOT skip any phase.")
|
|
384
|
+
lines.append("3. After the LAST phase, output a recovery summary.")
|
|
385
|
+
lines.append("4. This is a NON-INTERACTIVE autonomous session — proceed without asking for user input.")
|
|
386
|
+
lines.append("5. Use `/prizmkit-code-review`, `/prizmkit-committer`, `/prizmkit-retrospective` as specified in each phase.")
|
|
387
|
+
lines.append("6. When staging files for commit, always use explicit file names — NEVER use `git add -A` or `git add .`.")
|
|
388
|
+
lines.append("")
|
|
389
|
+
|
|
390
|
+
# Bug description (if available)
|
|
391
|
+
if bug_desc:
|
|
392
|
+
lines.append("## Bug Description (from bug-fix-list.json)")
|
|
393
|
+
lines.append("")
|
|
394
|
+
lines.append("- **ID**: {}".format(bug_desc.get("id", bug_id)))
|
|
395
|
+
lines.append("- **Title**: {}".format(bug_desc.get("title", "(untitled)")))
|
|
396
|
+
lines.append("- **Description**: {}".format(bug_desc.get("description", "(none)")))
|
|
397
|
+
lines.append("- **Severity**: {}".format(bug_desc.get("severity", "(unset)")))
|
|
398
|
+
if bug_desc.get("acceptance_criteria"):
|
|
399
|
+
lines.append("- **Acceptance Criteria**:")
|
|
400
|
+
for ac in bug_desc["acceptance_criteria"]:
|
|
401
|
+
lines.append(" - {}".format(ac))
|
|
402
|
+
lines.append("")
|
|
403
|
+
|
|
404
|
+
# Git state
|
|
405
|
+
lines.append("## Git State")
|
|
406
|
+
lines.append("")
|
|
407
|
+
lines.append("- **Branch**: {}".format(branch))
|
|
408
|
+
lines.append("- **Commits ahead of main**: {}".format(
|
|
409
|
+
git_state.get("commits_ahead_of_main", 0)))
|
|
410
|
+
lines.append("- **Uncommitted files**: {}".format(
|
|
411
|
+
git_state.get("uncommitted_files", 0)))
|
|
412
|
+
lines.append("- **Staged files**: {}".format(
|
|
413
|
+
git_state.get("staged_files", 0)))
|
|
414
|
+
lines.append("")
|
|
415
|
+
|
|
416
|
+
# Code changes summary
|
|
417
|
+
if code_state.get("has_changes"):
|
|
418
|
+
lines.append("## Code Changes (from interrupted session)")
|
|
419
|
+
lines.append("")
|
|
420
|
+
lines.append("- Files modified: {}".format(code_state.get("files_modified", 0)))
|
|
421
|
+
lines.append("- Files added: {}".format(code_state.get("files_added", 0)))
|
|
422
|
+
lines.append("- Files deleted: {}".format(code_state.get("files_deleted", 0)))
|
|
423
|
+
lines.append("- Test files touched: {}".format(code_state.get("test_files_touched", 0)))
|
|
424
|
+
lines.append("- Directories touched: {}".format(
|
|
425
|
+
", ".join(code_state.get("directories_touched", [])) or "(none)"))
|
|
426
|
+
lines.append("")
|
|
427
|
+
lines.append("### Diff Summary")
|
|
428
|
+
lines.append("")
|
|
429
|
+
lines.append("```")
|
|
430
|
+
lines.append(diff_summary)
|
|
431
|
+
lines.append("```")
|
|
432
|
+
lines.append("")
|
|
433
|
+
|
|
434
|
+
# Existing artifacts
|
|
435
|
+
if artifacts:
|
|
436
|
+
lines.append("## Existing Artifacts (from interrupted session)")
|
|
437
|
+
lines.append("")
|
|
438
|
+
lines.append(format_artifact_section(artifacts))
|
|
439
|
+
lines.append("")
|
|
440
|
+
|
|
441
|
+
# Remaining phases
|
|
442
|
+
lines.append("---")
|
|
443
|
+
lines.append("")
|
|
444
|
+
lines.append("## Remaining Phases — Execute ALL of these in order")
|
|
445
|
+
lines.append("")
|
|
446
|
+
|
|
447
|
+
for i, phase_num in enumerate(remaining):
|
|
448
|
+
name, instructions = phase_map.get(phase_num, ("Unknown", ""))
|
|
449
|
+
# Substitute {bug_id} in instructions
|
|
450
|
+
instructions = instructions.replace("{bug_id}", bug_id)
|
|
451
|
+
|
|
452
|
+
label = "CURRENT PHASE" if i == 0 else ""
|
|
453
|
+
if label:
|
|
454
|
+
lines.append("### Phase {}: {} — {}".format(phase_num, name, label))
|
|
455
|
+
else:
|
|
456
|
+
lines.append("### Phase {}: {}".format(phase_num, name))
|
|
457
|
+
lines.append("")
|
|
458
|
+
lines.append(instructions)
|
|
459
|
+
lines.append("")
|
|
460
|
+
|
|
461
|
+
# Completion section
|
|
462
|
+
lines.append("---")
|
|
463
|
+
lines.append("")
|
|
464
|
+
lines.append("## FINAL: Recovery Summary")
|
|
465
|
+
lines.append("")
|
|
466
|
+
lines.append("After ALL phases above are complete, output:")
|
|
467
|
+
lines.append("")
|
|
468
|
+
lines.append("```")
|
|
469
|
+
lines.append("Recovery complete.")
|
|
470
|
+
lines.append(" Workflow: bug-fix-workflow")
|
|
471
|
+
lines.append(" Bug: {}".format(bug_id))
|
|
472
|
+
lines.append(" Recovered from: Phase {} ({})".format(phase, phase_name))
|
|
473
|
+
lines.append(" Completed: {}".format(
|
|
474
|
+
" → ".join("Phase {} ({})".format(p, phase_map.get(p, ("?",))[0])
|
|
475
|
+
for p in remaining)))
|
|
476
|
+
lines.append(" Commit: <commit hash>")
|
|
477
|
+
lines.append("```")
|
|
478
|
+
lines.append("")
|
|
479
|
+
|
|
480
|
+
# Reminders
|
|
481
|
+
lines.append("## Reminders")
|
|
482
|
+
lines.append("")
|
|
483
|
+
lines.append("- All bug-fix artifacts go in `.prizmkit/bugfix/{}/`".format(bug_id))
|
|
484
|
+
lines.append("- Commit with `fix(<scope>): <description>` prefix")
|
|
485
|
+
lines.append("- Do NOT ask for user input — this is autonomous")
|
|
486
|
+
lines.append("- Do NOT stop before completing ALL remaining phases")
|
|
487
|
+
lines.append("- `/prizmkit-committer` is MANDATORY — do NOT skip the commit phase")
|
|
488
|
+
lines.append("- `/prizmkit-retrospective` is MANDATORY — do NOT skip the docs sync phase")
|
|
489
|
+
|
|
490
|
+
return "\n".join(lines)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def build_feature_prompt(detection, project_root):
|
|
494
|
+
"""Build recovery prompt for feature-workflow."""
|
|
495
|
+
context = detection.get("context", {})
|
|
496
|
+
branch = context.get("branch", "unknown")
|
|
497
|
+
phase = detection.get("phase", 1)
|
|
498
|
+
phase_name = detection.get("phase_name", "Unknown")
|
|
499
|
+
recovery = detection.get("recovery", {})
|
|
500
|
+
|
|
501
|
+
artifacts = read_feature_artifacts(project_root)
|
|
502
|
+
remaining, phase_map = get_remaining_phases("feature-workflow", phase)
|
|
503
|
+
|
|
504
|
+
lines = []
|
|
505
|
+
lines.append("# Recovery Session — Feature Workflow")
|
|
506
|
+
lines.append("")
|
|
507
|
+
lines.append("## Context")
|
|
508
|
+
lines.append("")
|
|
509
|
+
lines.append("You are RECOVERING an interrupted feature-workflow session.")
|
|
510
|
+
lines.append("")
|
|
511
|
+
lines.append("- **Branch**: {}".format(branch))
|
|
512
|
+
lines.append("- **Interrupted at**: Phase {} — {}".format(phase, phase_name))
|
|
513
|
+
lines.append("- **Remaining work**: {}".format(recovery.get("remaining_work", "unknown")))
|
|
514
|
+
lines.append("")
|
|
515
|
+
|
|
516
|
+
lines.append("## CRITICAL RULES")
|
|
517
|
+
lines.append("")
|
|
518
|
+
lines.append("1. You MUST complete ALL remaining phases listed below — do NOT stop early")
|
|
519
|
+
lines.append("2. Execute phases in ORDER. Do NOT skip any phase.")
|
|
520
|
+
lines.append("3. After the LAST phase, output a recovery summary.")
|
|
521
|
+
lines.append("4. This is a NON-INTERACTIVE autonomous session — proceed without asking for user input.")
|
|
522
|
+
lines.append("")
|
|
523
|
+
|
|
524
|
+
if artifacts:
|
|
525
|
+
lines.append("## Existing Artifacts")
|
|
526
|
+
lines.append("")
|
|
527
|
+
lines.append(format_artifact_section(artifacts))
|
|
528
|
+
lines.append("")
|
|
529
|
+
|
|
530
|
+
lines.append("---")
|
|
531
|
+
lines.append("")
|
|
532
|
+
lines.append("## Remaining Phases — Execute ALL of these in order")
|
|
533
|
+
lines.append("")
|
|
534
|
+
|
|
535
|
+
for i, phase_num in enumerate(remaining):
|
|
536
|
+
name, instructions = phase_map.get(phase_num, ("Unknown", ""))
|
|
537
|
+
label = "CURRENT PHASE" if i == 0 else ""
|
|
538
|
+
if label:
|
|
539
|
+
lines.append("### Phase {}: {} — {}".format(phase_num, name, label))
|
|
540
|
+
else:
|
|
541
|
+
lines.append("### Phase {}: {}".format(phase_num, name))
|
|
542
|
+
lines.append("")
|
|
543
|
+
lines.append(instructions)
|
|
544
|
+
lines.append("")
|
|
545
|
+
|
|
546
|
+
lines.append("---")
|
|
547
|
+
lines.append("")
|
|
548
|
+
lines.append("## FINAL: Recovery Summary")
|
|
549
|
+
lines.append("")
|
|
550
|
+
lines.append("After ALL phases above are complete, output:")
|
|
551
|
+
lines.append("")
|
|
552
|
+
lines.append("```")
|
|
553
|
+
lines.append("Recovery complete.")
|
|
554
|
+
lines.append(" Workflow: feature-workflow")
|
|
555
|
+
lines.append(" Recovered from: Phase {} ({})".format(phase, phase_name))
|
|
556
|
+
lines.append(" Completed: {}".format(
|
|
557
|
+
" → ".join("Phase {} ({})".format(p, phase_map.get(p, ("?",))[0])
|
|
558
|
+
for p in remaining)))
|
|
559
|
+
lines.append("```")
|
|
560
|
+
lines.append("")
|
|
561
|
+
|
|
562
|
+
lines.append("## Reminders")
|
|
563
|
+
lines.append("")
|
|
564
|
+
lines.append("- Use `/feature-pipeline-launcher` to start the pipeline (Phase 3)")
|
|
565
|
+
lines.append("- Do NOT ask for user input — this is autonomous")
|
|
566
|
+
lines.append("- Do NOT stop before completing ALL remaining phases")
|
|
567
|
+
|
|
568
|
+
return "\n".join(lines)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def build_refactor_prompt(detection, project_root):
|
|
572
|
+
"""Build recovery prompt for refactor-workflow."""
|
|
573
|
+
context = detection.get("context", {})
|
|
574
|
+
branch = context.get("branch", "unknown")
|
|
575
|
+
phase = detection.get("phase", 1)
|
|
576
|
+
phase_name = detection.get("phase_name", "Unknown")
|
|
577
|
+
recovery = detection.get("recovery", {})
|
|
578
|
+
|
|
579
|
+
artifacts = read_refactor_artifacts(project_root)
|
|
580
|
+
remaining, phase_map = get_remaining_phases("refactor-workflow", phase)
|
|
581
|
+
|
|
582
|
+
lines = []
|
|
583
|
+
lines.append("# Recovery Session — Refactor Workflow")
|
|
584
|
+
lines.append("")
|
|
585
|
+
lines.append("## Context")
|
|
586
|
+
lines.append("")
|
|
587
|
+
lines.append("You are RECOVERING an interrupted refactor-workflow session.")
|
|
588
|
+
lines.append("")
|
|
589
|
+
lines.append("- **Branch**: {}".format(branch))
|
|
590
|
+
lines.append("- **Interrupted at**: Phase {} — {}".format(phase, phase_name))
|
|
591
|
+
lines.append("- **Remaining work**: {}".format(recovery.get("remaining_work", "unknown")))
|
|
592
|
+
lines.append("")
|
|
593
|
+
|
|
594
|
+
lines.append("## CRITICAL RULES")
|
|
595
|
+
lines.append("")
|
|
596
|
+
lines.append("1. You MUST complete ALL remaining phases listed below — do NOT stop early")
|
|
597
|
+
lines.append("2. Execute phases in ORDER. Do NOT skip any phase.")
|
|
598
|
+
lines.append("3. After the LAST phase, output a recovery summary.")
|
|
599
|
+
lines.append("4. This is a NON-INTERACTIVE autonomous session — proceed without asking for user input.")
|
|
600
|
+
lines.append("")
|
|
601
|
+
|
|
602
|
+
if artifacts:
|
|
603
|
+
lines.append("## Existing Artifacts")
|
|
604
|
+
lines.append("")
|
|
605
|
+
lines.append(format_artifact_section(artifacts))
|
|
606
|
+
lines.append("")
|
|
607
|
+
|
|
608
|
+
lines.append("---")
|
|
609
|
+
lines.append("")
|
|
610
|
+
lines.append("## Remaining Phases — Execute ALL of these in order")
|
|
611
|
+
lines.append("")
|
|
612
|
+
|
|
613
|
+
for i, phase_num in enumerate(remaining):
|
|
614
|
+
name, instructions = phase_map.get(phase_num, ("Unknown", ""))
|
|
615
|
+
label = "CURRENT PHASE" if i == 0 else ""
|
|
616
|
+
if label:
|
|
617
|
+
lines.append("### Phase {}: {} — {}".format(phase_num, name, label))
|
|
618
|
+
else:
|
|
619
|
+
lines.append("### Phase {}: {}".format(phase_num, name))
|
|
620
|
+
lines.append("")
|
|
621
|
+
lines.append(instructions)
|
|
622
|
+
lines.append("")
|
|
623
|
+
|
|
624
|
+
lines.append("---")
|
|
625
|
+
lines.append("")
|
|
626
|
+
lines.append("## FINAL: Recovery Summary")
|
|
627
|
+
lines.append("")
|
|
628
|
+
lines.append("After ALL phases above are complete, output:")
|
|
629
|
+
lines.append("")
|
|
630
|
+
lines.append("```")
|
|
631
|
+
lines.append("Recovery complete.")
|
|
632
|
+
lines.append(" Workflow: refactor-workflow")
|
|
633
|
+
lines.append(" Recovered from: Phase {} ({})".format(phase, phase_name))
|
|
634
|
+
lines.append(" Completed: {}".format(
|
|
635
|
+
" → ".join("Phase {} ({})".format(p, phase_map.get(p, ("?",))[0])
|
|
636
|
+
for p in remaining)))
|
|
637
|
+
lines.append("```")
|
|
638
|
+
lines.append("")
|
|
639
|
+
|
|
640
|
+
lines.append("## Reminders")
|
|
641
|
+
lines.append("")
|
|
642
|
+
lines.append("- Use `/refactor-pipeline-launcher` to start the pipeline (Phase 3)")
|
|
643
|
+
lines.append("- Do NOT ask for user input — this is autonomous")
|
|
644
|
+
lines.append("- Do NOT stop before completing ALL remaining phases")
|
|
645
|
+
|
|
646
|
+
return "\n".join(lines)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
# ============================================================
|
|
650
|
+
# Main
|
|
651
|
+
# ============================================================
|
|
652
|
+
|
|
653
|
+
PROMPT_BUILDERS = {
|
|
654
|
+
"bug-fix-workflow": build_bugfix_prompt,
|
|
655
|
+
"feature-workflow": build_feature_prompt,
|
|
656
|
+
"refactor-workflow": build_refactor_prompt,
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def parse_args():
|
|
661
|
+
parser = argparse.ArgumentParser(
|
|
662
|
+
description="Generate a recovery bootstrap prompt from detection output",
|
|
663
|
+
)
|
|
664
|
+
parser.add_argument(
|
|
665
|
+
"--detection-json",
|
|
666
|
+
required=True,
|
|
667
|
+
help="Path to JSON file with detect-recovery-state.py output",
|
|
668
|
+
)
|
|
669
|
+
parser.add_argument(
|
|
670
|
+
"--output",
|
|
671
|
+
required=True,
|
|
672
|
+
help="Output path for the rendered prompt",
|
|
673
|
+
)
|
|
674
|
+
parser.add_argument(
|
|
675
|
+
"--project-root",
|
|
676
|
+
default=None,
|
|
677
|
+
help="Project root directory (default: auto-detect from git)",
|
|
678
|
+
)
|
|
679
|
+
parser.add_argument(
|
|
680
|
+
"--session-id",
|
|
681
|
+
default=None,
|
|
682
|
+
help="Session ID for this recovery session",
|
|
683
|
+
)
|
|
684
|
+
return parser.parse_args()
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def resolve_project_root(given_root):
|
|
688
|
+
"""Resolve project root from argument or git."""
|
|
689
|
+
if given_root:
|
|
690
|
+
return os.path.abspath(given_root)
|
|
691
|
+
# Auto-detect: script is at dev-pipeline/scripts/, project root is 2 levels up
|
|
692
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
693
|
+
return os.path.dirname(os.path.dirname(script_dir))
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def emit_failure(message):
|
|
697
|
+
"""Emit standardized failure JSON and exit."""
|
|
698
|
+
print(json.dumps({"success": False, "error": message}, indent=2, ensure_ascii=False))
|
|
699
|
+
sys.exit(1)
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def main():
|
|
703
|
+
args = parse_args()
|
|
704
|
+
project_root = resolve_project_root(args.project_root)
|
|
705
|
+
|
|
706
|
+
# Load detection JSON
|
|
707
|
+
detection, err = load_json_file(args.detection_json)
|
|
708
|
+
if err:
|
|
709
|
+
emit_failure("Detection JSON error: {}".format(err))
|
|
710
|
+
|
|
711
|
+
if not detection.get("detected"):
|
|
712
|
+
emit_failure("No interrupted workflow detected — nothing to recover")
|
|
713
|
+
|
|
714
|
+
workflow_type = detection.get("workflow_type")
|
|
715
|
+
if workflow_type not in PROMPT_BUILDERS:
|
|
716
|
+
emit_failure("Unknown workflow type: {}".format(workflow_type))
|
|
717
|
+
|
|
718
|
+
# Build prompt
|
|
719
|
+
builder = PROMPT_BUILDERS[workflow_type]
|
|
720
|
+
prompt_content = builder(detection, project_root)
|
|
721
|
+
|
|
722
|
+
# Write output
|
|
723
|
+
output_path = os.path.abspath(args.output)
|
|
724
|
+
output_dir = os.path.dirname(output_path)
|
|
725
|
+
if output_dir and not os.path.isdir(output_dir):
|
|
726
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
727
|
+
|
|
728
|
+
try:
|
|
729
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
730
|
+
f.write(prompt_content)
|
|
731
|
+
except IOError as e:
|
|
732
|
+
emit_failure("Cannot write output: {}".format(str(e)))
|
|
733
|
+
|
|
734
|
+
# Success JSON
|
|
735
|
+
phase = detection.get("phase", 0)
|
|
736
|
+
phase_name = detection.get("phase_name", "Unknown")
|
|
737
|
+
remaining, _ = get_remaining_phases(workflow_type, phase)
|
|
738
|
+
output = {
|
|
739
|
+
"success": True,
|
|
740
|
+
"workflow_type": workflow_type,
|
|
741
|
+
"resume_phase": phase,
|
|
742
|
+
"resume_phase_name": phase_name,
|
|
743
|
+
"remaining_phases": remaining,
|
|
744
|
+
"prompt_path": output_path,
|
|
745
|
+
}
|
|
746
|
+
print(json.dumps(output, indent=2, ensure_ascii=False))
|
|
747
|
+
sys.exit(0)
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
if __name__ == "__main__":
|
|
751
|
+
try:
|
|
752
|
+
main()
|
|
753
|
+
except KeyboardInterrupt:
|
|
754
|
+
emit_failure("generate-recovery-prompt interrupted")
|
|
755
|
+
except SystemExit:
|
|
756
|
+
raise
|
|
757
|
+
except Exception as exc:
|
|
758
|
+
LOGGER.exception("Unhandled exception in generate-recovery-prompt")
|
|
759
|
+
emit_failure("Unexpected error: {}".format(str(exc)))
|