prizmkit 1.1.27 → 1.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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)))