prizmkit 1.1.1 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/adapters/claude/agent-adapter.js +18 -0
  3. package/bundled/adapters/claude/command-adapter.js +1 -27
  4. package/bundled/agents/prizm-dev-team-critic.md +2 -0
  5. package/bundled/agents/prizm-dev-team-dev.md +2 -0
  6. package/bundled/agents/prizm-dev-team-reviewer.md +2 -0
  7. package/bundled/dev-pipeline/README.md +63 -63
  8. package/bundled/dev-pipeline/assets/feature-list-example.json +1 -1
  9. package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +1 -1
  10. package/bundled/dev-pipeline/{launch-daemon.sh → launch-feature-daemon.sh} +33 -33
  11. package/bundled/dev-pipeline/launch-refactor-daemon.sh +454 -0
  12. package/bundled/dev-pipeline/lib/branch.sh +1 -1
  13. package/bundled/dev-pipeline/reset-feature.sh +3 -3
  14. package/bundled/dev-pipeline/reset-refactor.sh +312 -0
  15. package/bundled/dev-pipeline/{retry-bug.sh → retry-bugfix.sh} +47 -59
  16. package/bundled/dev-pipeline/retry-feature.sh +41 -54
  17. package/bundled/dev-pipeline/retry-refactor.sh +358 -0
  18. package/bundled/dev-pipeline/run-bugfix.sh +6 -0
  19. package/bundled/dev-pipeline/{run.sh → run-feature.sh} +31 -31
  20. package/bundled/dev-pipeline/run-refactor.sh +787 -0
  21. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +177 -10
  22. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +419 -0
  23. package/bundled/dev-pipeline/scripts/init-refactor-pipeline.py +393 -0
  24. package/bundled/dev-pipeline/scripts/update-refactor-status.py +726 -0
  25. package/bundled/dev-pipeline/templates/agent-prompts/critic-code-challenge.md +13 -0
  26. package/bundled/dev-pipeline/templates/agent-prompts/critic-plan-challenge.md +7 -0
  27. package/bundled/dev-pipeline/templates/agent-prompts/dev-fix.md +7 -0
  28. package/bundled/dev-pipeline/templates/agent-prompts/dev-implement.md +26 -0
  29. package/bundled/dev-pipeline/templates/agent-prompts/dev-resume.md +5 -0
  30. package/bundled/dev-pipeline/templates/agent-prompts/reviewer-analyze.md +5 -0
  31. package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +12 -0
  32. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +29 -2
  33. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +8 -7
  34. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +11 -10
  35. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +2 -3
  36. package/bundled/dev-pipeline/templates/feature-list-schema.json +1 -1
  37. package/bundled/dev-pipeline/templates/refactor-list-schema.json +159 -0
  38. package/bundled/dev-pipeline/templates/sections/ac-verification-checklist.md +13 -0
  39. package/bundled/dev-pipeline/templates/sections/feature-context.md +1 -1
  40. package/bundled/dev-pipeline/templates/sections/phase-analyze-agent.md +9 -8
  41. package/bundled/dev-pipeline/templates/sections/phase-analyze-full.md +9 -8
  42. package/bundled/dev-pipeline/templates/sections/phase-browser-verification.md +2 -1
  43. package/bundled/dev-pipeline/templates/sections/phase-critic-code.md +8 -10
  44. package/bundled/dev-pipeline/templates/sections/phase-critic-plan-full.md +9 -10
  45. package/bundled/dev-pipeline/templates/sections/phase-critic-plan.md +8 -9
  46. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +7 -10
  47. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +8 -15
  48. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +7 -12
  49. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +8 -19
  50. package/bundled/dev-pipeline/templates/sections/test-failure-recovery.md +75 -0
  51. package/bundled/skills/_metadata.json +33 -6
  52. package/bundled/skills/app-planner/SKILL.md +105 -320
  53. package/bundled/skills/app-planner/assets/app-design-guide.md +101 -0
  54. package/bundled/skills/app-planner/references/frontend-design-guide.md +1 -1
  55. package/bundled/skills/app-planner/references/project-brief-guide.md +49 -80
  56. package/bundled/skills/bug-fix-workflow/SKILL.md +2 -2
  57. package/bundled/skills/bug-planner/SKILL.md +68 -5
  58. package/bundled/skills/bug-planner/scripts/validate-bug-list.py +3 -2
  59. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +19 -5
  60. package/bundled/skills/{dev-pipeline-launcher → feature-pipeline-launcher}/SKILL.md +32 -32
  61. package/bundled/skills/feature-planner/SKILL.md +337 -0
  62. package/bundled/skills/{app-planner → feature-planner}/assets/evaluation-guide.md +4 -4
  63. package/bundled/skills/{app-planner → feature-planner}/assets/planning-guide.md +3 -171
  64. package/bundled/skills/{app-planner → feature-planner}/references/browser-interaction.md +6 -5
  65. package/bundled/skills/feature-planner/references/decomposition-patterns.md +75 -0
  66. package/bundled/skills/{app-planner → feature-planner}/references/error-recovery.md +8 -8
  67. package/bundled/skills/{app-planner → feature-planner}/references/incremental-feature-planning.md +1 -1
  68. package/bundled/skills/{app-planner/references/new-app-planning.md → feature-planner/references/new-project-planning.md} +1 -1
  69. package/bundled/skills/{app-planner → feature-planner}/scripts/validate-and-generate.py +4 -4
  70. package/bundled/skills/feature-workflow/SKILL.md +23 -23
  71. package/bundled/skills/prizm-kit/SKILL.md +1 -3
  72. package/bundled/skills/prizmkit-analyze/SKILL.md +2 -5
  73. package/bundled/skills/prizmkit-code-review/SKILL.md +2 -2
  74. package/bundled/skills/prizmkit-committer/SKILL.md +4 -8
  75. package/bundled/skills/prizmkit-deploy/SKILL.md +1 -5
  76. package/bundled/skills/prizmkit-implement/SKILL.md +3 -50
  77. package/bundled/skills/prizmkit-init/SKILL.md +5 -77
  78. package/bundled/skills/prizmkit-plan/SKILL.md +1 -12
  79. package/bundled/skills/prizmkit-prizm-docs/SKILL.md +6 -24
  80. package/bundled/skills/prizmkit-prizm-docs/assets/PRIZM-SPEC.md +21 -0
  81. package/bundled/skills/prizmkit-retrospective/SKILL.md +12 -117
  82. package/bundled/skills/recovery-workflow/SKILL.md +166 -316
  83. package/bundled/skills/recovery-workflow/evals/evals.json +29 -13
  84. package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +232 -274
  85. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +352 -0
  86. package/bundled/skills/refactor-planner/SKILL.md +436 -0
  87. package/bundled/skills/refactor-planner/assets/planning-guide.md +292 -0
  88. package/bundled/skills/refactor-planner/references/behavior-preservation.md +301 -0
  89. package/bundled/skills/refactor-planner/references/refactor-scoping-guide.md +221 -0
  90. package/bundled/skills/refactor-planner/scripts/validate-and-generate-refactor.py +786 -0
  91. package/bundled/skills/refactor-workflow/SKILL.md +299 -319
  92. package/package.json +1 -1
  93. package/src/clean.js +3 -3
  94. package/src/scaffold.js +6 -6
  95. package/bundled/skills/prizmkit-plan/assets/spec-template.md +0 -56
  96. package/bundled/skills/prizmkit-plan/references/clarify-guide.md +0 -67
  97. package/src/config.js +0 -504
  98. package/src/prompts.js +0 -210
  99. /package/bundled/skills/{dev-pipeline-launcher → feature-pipeline-launcher}/scripts/preflight-check.py +0 -0
@@ -101,6 +101,12 @@ def parse_args():
101
101
  default=None,
102
102
  help="Override critic enablement (default: read from feature field)",
103
103
  )
104
+ parser.add_argument(
105
+ "--extract-baselines",
106
+ action="store_true",
107
+ help="Run tests and extract baseline failures (slower, optional)",
108
+ )
109
+
104
110
  return parser.parse_args()
105
111
 
106
112
 
@@ -154,6 +160,101 @@ def format_acceptance_criteria(criteria):
154
160
  lines.append("- {}".format(item))
155
161
  return "\n".join(lines)
156
162
 
163
+ def detect_test_commands(project_root):
164
+ """
165
+ Auto-detect test commands based on project structure.
166
+
167
+ Returns: space-separated string of test commands (e.g., "npm test go test ./...")
168
+ """
169
+ test_commands = []
170
+
171
+ # Check for npm/package.json
172
+ if os.path.exists(os.path.join(project_root, "package.json")):
173
+ test_commands.append("npm test")
174
+
175
+ # Check for Go
176
+ if os.path.exists(os.path.join(project_root, "go.mod")):
177
+ test_commands.append("go test ./...")
178
+
179
+ # Check for Rust/Cargo
180
+ if os.path.exists(os.path.join(project_root, "Cargo.toml")):
181
+ test_commands.append("cargo test")
182
+
183
+ # Check for Python pytest
184
+ if os.path.exists(os.path.join(project_root, "pytest.ini")) or \
185
+ os.path.exists(os.path.join(project_root, "setup.py")):
186
+ test_commands.append("pytest")
187
+
188
+ # Check for Make test target
189
+ makefile_path = os.path.join(project_root, "Makefile")
190
+ if os.path.exists(makefile_path):
191
+ try:
192
+ with open(makefile_path, 'r') as f:
193
+ if 'test:' in f.read():
194
+ test_commands.append("make test")
195
+ except Exception:
196
+ pass
197
+
198
+ # Return deduplicated space-separated list
199
+ return " ".join(dict.fromkeys(test_commands)) if test_commands else ""
200
+
201
+
202
+ def extract_baseline_failures(test_cmd, project_root):
203
+ """
204
+ Run test command and extract failing tests.
205
+
206
+ Returns: semicolon-separated list of failing test names
207
+ """
208
+ if not test_cmd or test_cmd.startswith("(auto-detection"):
209
+ return ""
210
+
211
+ try:
212
+ import subprocess
213
+ original_cwd = os.getcwd()
214
+ os.chdir(project_root)
215
+
216
+ result = subprocess.run(
217
+ test_cmd,
218
+ shell=True,
219
+ capture_output=True,
220
+ text=True,
221
+ timeout=120
222
+ )
223
+
224
+ os.chdir(original_cwd)
225
+
226
+ output = result.stdout + result.stderr
227
+ failures = []
228
+
229
+ for line in output.split('\n'):
230
+ if 'FAILED' in line and '::' in line:
231
+ parts = line.split('FAILED')
232
+ if len(parts) > 1:
233
+ test_name = parts[1].strip().split(' ')[0]
234
+ if test_name and test_name not in failures:
235
+ failures.append(test_name)
236
+
237
+ return ";".join(failures) if failures else ""
238
+
239
+ except Exception as e:
240
+ return f"(error: {str(e)})"
241
+ finally:
242
+ try:
243
+ os.chdir(original_cwd)
244
+ except Exception:
245
+ pass
246
+
247
+
248
+ def format_ac_checklist(acceptance_criteria):
249
+ """Format acceptance criteria as a markdown checkbox list."""
250
+ if not acceptance_criteria:
251
+ return "- [ ] (no acceptance criteria specified)"
252
+ lines = []
253
+ for item in acceptance_criteria:
254
+ lines.append("- [ ] {}".format(item))
255
+ return "\n".join(lines)
256
+
257
+
157
258
 
158
259
  def format_global_context(global_context, project_root=None):
159
260
  """Format global_context dict as a key-value list.
@@ -251,8 +352,9 @@ def _read_project_brief(project_root):
251
352
 
252
353
  Returns the file content as a string, or a fallback message if absent.
253
354
  This brief is generated by app-planner during interactive planning and
254
- captures cross-feature design decisions, business intent, and user
255
- preferences that individual feature descriptions may not include.
355
+ captures the user's product ideas as a checklist. Each line is one idea,
356
+ marked [ ] for pending or [x] for completed. Feature sessions should mark
357
+ items [x] and append key file paths when implementing relevant ideas.
256
358
  """
257
359
  brief_path = os.path.join(project_root, "project-brief.md")
258
360
  if os.path.isfile(brief_path):
@@ -445,6 +547,38 @@ def load_section(sections_dir, name):
445
547
  return f.read()
446
548
 
447
549
 
550
+ def load_agent_prompts(templates_dir):
551
+ """Load agent prompt templates from agent-prompts/ directory.
552
+
553
+ Returns a dict of {{AGENT_PROMPT_XXX}} -> prompt content replacements.
554
+ If the directory does not exist, returns an empty dict (backward compat).
555
+ """
556
+ agent_prompts_dir = os.path.join(templates_dir, "agent-prompts")
557
+ if not os.path.isdir(agent_prompts_dir):
558
+ LOGGER.debug("No agent-prompts/ directory found, skipping")
559
+ return {}
560
+
561
+ # Map filename -> placeholder name
562
+ # e.g. dev-implement.md -> {{AGENT_PROMPT_DEV_IMPLEMENT}}
563
+ prompt_map = {}
564
+ for filename in sorted(os.listdir(agent_prompts_dir)):
565
+ if not filename.endswith(".md"):
566
+ continue
567
+ stem = filename[:-3] # remove .md
568
+ placeholder = "{{{{AGENT_PROMPT_{}}}}}".format(
569
+ stem.upper().replace("-", "_")
570
+ )
571
+ filepath = os.path.join(agent_prompts_dir, filename)
572
+ try:
573
+ with open(filepath, "r", encoding="utf-8") as f:
574
+ prompt_map[placeholder] = f.read().strip()
575
+ LOGGER.debug("Loaded agent prompt: %s -> %s", filename, placeholder)
576
+ except IOError as exc:
577
+ LOGGER.warning("Failed to load agent prompt %s: %s", filename, exc)
578
+
579
+ return prompt_map
580
+
581
+
448
582
  def _tier_header(pipeline_mode):
449
583
  """Return the tier-specific header and mission description."""
450
584
  headers = {
@@ -741,9 +875,13 @@ def render_from_sections(sections, replacements):
741
875
  """
742
876
  content = "\n".join(text for _, text in sections)
743
877
 
744
- # Replace all placeholders
745
- for placeholder, value in replacements.items():
746
- content = content.replace(placeholder, value)
878
+ # Replace all placeholders — run twice to handle agent prompt templates
879
+ # that contain their own {{PLACEHOLDER}} variables. First pass injects
880
+ # agent prompt content (e.g. {{AGENT_PROMPT_DEV_IMPLEMENT}} expands to a
881
+ # block containing {{FEATURE_ID}}). Second pass replaces the inner vars.
882
+ for _pass in range(2):
883
+ for placeholder, value in replacements.items():
884
+ content = content.replace(placeholder, value)
747
885
 
748
886
  return content
749
887
 
@@ -954,11 +1092,28 @@ def build_replacements(args, feature, features, global_context, script_dir):
954
1092
  steps = browser_interaction.get("verify_steps", [])
955
1093
  if steps:
956
1094
  browser_verify_steps = "\n".join(
957
- " # Step {}: {}".format(i + 1, step)
1095
+ " # Goal {}: {}".format(i + 1, step)
958
1096
  for i, step in enumerate(steps)
959
1097
  )
960
1098
  else:
961
- browser_verify_steps = " # (no specific verify steps — just open and screenshot)"
1099
+ browser_verify_steps = " # (no specific verify goals — just open and screenshot)"
1100
+
1101
+ # Auto-detect test commands from project structure
1102
+ test_cmd = detect_test_commands(project_root)
1103
+ if not test_cmd:
1104
+ test_cmd = "(auto-detection found no standard test commands; manually specify TEST_CMD)"
1105
+
1106
+ # Optionally extract baseline failures from test execution
1107
+ baseline_failures = ""
1108
+ if args.extract_baselines:
1109
+ baseline_failures = extract_baseline_failures(test_cmd, project_root)
1110
+
1111
+ # Extract coverage target from feature.testing field (new in v2)
1112
+ coverage_target = "80" # Default coverage target
1113
+ testing_config = feature.get("testing", {})
1114
+ if isinstance(testing_config, dict):
1115
+ coverage_target = str(testing_config.get("coverage_target", 80))
1116
+
962
1117
 
963
1118
  replacements = {
964
1119
  "{{RUN_ID}}": args.run_id,
@@ -999,6 +1154,12 @@ def build_replacements(args, feature, features, global_context, script_dir):
999
1154
  "{{BROWSER_URL}}": browser_url,
1000
1155
  "{{BROWSER_SETUP_COMMAND}}": browser_setup_command,
1001
1156
  "{{BROWSER_VERIFY_STEPS}}": browser_verify_steps,
1157
+ "{{AC_CHECKLIST}}": format_ac_checklist(
1158
+ feature.get("acceptance_criteria", [])
1159
+ ),
1160
+ "{{TEST_CMD}}": test_cmd,
1161
+ "{{BASELINE_FAILURES}}": baseline_failures,
1162
+ "{{COVERAGE_TARGET}}": coverage_target,
1002
1163
  }
1003
1164
 
1004
1165
  return replacements, effective_resume, browser_enabled
@@ -1016,9 +1177,11 @@ def render_template(template_content, replacements, resume_phase, browser_enable
1016
1177
  content = process_mode_blocks(content, pipeline_mode, init_done, critic_enabled,
1017
1178
  browser_enabled)
1018
1179
 
1019
- # Step 3: Replace all {{PLACEHOLDER}} variables
1020
- for placeholder, value in replacements.items():
1021
- content = content.replace(placeholder, value)
1180
+ # Step 3: Replace all {{PLACEHOLDER}} variables (two passes for nested
1181
+ # agent prompt templates that may contain their own placeholders)
1182
+ for _pass in range(2):
1183
+ for placeholder, value in replacements.items():
1184
+ content = content.replace(placeholder, value)
1022
1185
 
1023
1186
  return content
1024
1187
 
@@ -1079,6 +1242,10 @@ def main():
1079
1242
  )
1080
1243
  replacements["{{RESUME_PHASE}}"] = effective_resume
1081
1244
 
1245
+ # Load agent prompt templates and merge into replacements
1246
+ agent_prompt_replacements = load_agent_prompts(templates_dir)
1247
+ replacements.update(agent_prompt_replacements)
1248
+
1082
1249
  # Extract state needed for assembly
1083
1250
  pipeline_mode = replacements.get("{{PIPELINE_MODE}}", "lite")
1084
1251
  init_done = replacements.get("{{INIT_DONE}}", "false") == "true"
@@ -0,0 +1,419 @@
1
+ #!/usr/bin/env python3
2
+ """Generate a session-specific refactor bootstrap prompt from template and refactor-list.json.
3
+
4
+ Reads the refactor-bootstrap-prompt.md template and a refactor-list.json, resolves all
5
+ {{PLACEHOLDER}} variables, handles conditional blocks, and writes the rendered
6
+ prompt to the specified output path.
7
+
8
+ Usage:
9
+ python3 generate-refactor-prompt.py \
10
+ --refactor-list <path> --refactor-id <id> \
11
+ --session-id <id> --run-id <id> \
12
+ --retry-count <n> --resume-phase <n|null> \
13
+ --state-dir <path> --output <path>
14
+ """
15
+
16
+ import argparse
17
+ import json
18
+ import os
19
+ import sys
20
+
21
+ from utils import enrich_global_context, load_json_file, setup_logging
22
+
23
+
24
+ DEFAULT_MAX_RETRIES = 3
25
+
26
+ LOGGER = setup_logging("generate-refactor-prompt")
27
+
28
+
29
+ def parse_args():
30
+ parser = argparse.ArgumentParser(
31
+ description=(
32
+ "Generate a session-specific refactor bootstrap prompt from a template "
33
+ "and refactor-list.json."
34
+ )
35
+ )
36
+ parser.add_argument("--refactor-list", required=True, help="Path to refactor-list.json")
37
+ parser.add_argument("--refactor-id", required=True, help="Refactor ID to generate prompt for (e.g. R-001)")
38
+ parser.add_argument("--session-id", required=True, help="Session ID for this pipeline session")
39
+ parser.add_argument("--run-id", required=True, help="Pipeline run ID")
40
+ parser.add_argument("--retry-count", required=True, help="Current retry count")
41
+ parser.add_argument("--resume-phase", required=True, help='Phase to resume from, or "null" for fresh start')
42
+ parser.add_argument("--state-dir", default=None, help="State directory path for reading previous session info")
43
+ parser.add_argument("--output", required=True, help="Output path for the rendered prompt")
44
+ parser.add_argument("--template", default=None, help="Custom template path. Defaults to {script_dir}/../templates/refactor-bootstrap-prompt.md")
45
+ return parser.parse_args()
46
+
47
+
48
+ def read_text_file(path):
49
+ """Read and return the text content of a file."""
50
+ abs_path = os.path.abspath(path)
51
+ if not os.path.isfile(abs_path):
52
+ return None, "File not found: {}".format(abs_path)
53
+ try:
54
+ with open(abs_path, "r", encoding="utf-8") as f:
55
+ return f.read(), None
56
+ except IOError as e:
57
+ return None, "Cannot read file: {}".format(str(e))
58
+
59
+
60
+ def find_refactor(refactors, refactor_id):
61
+ """Find and return the refactor dict matching the given ID."""
62
+ for refactor in refactors:
63
+ if isinstance(refactor, dict) and refactor.get("id") == refactor_id:
64
+ return refactor
65
+ return None
66
+
67
+
68
+ def format_acceptance_criteria(criteria):
69
+ """Format acceptance criteria as a markdown bullet list."""
70
+ if not criteria:
71
+ return "- (none specified)"
72
+ lines = []
73
+ for item in criteria:
74
+ lines.append("- {}".format(item))
75
+ return "\n".join(lines)
76
+
77
+
78
+ def format_global_context(global_context, project_root=None):
79
+ """Format global_context dict as a key-value list.
80
+
81
+ If global_context is empty/sparse and project_root is provided,
82
+ auto-detect tech stack from project files to fill gaps.
83
+ """
84
+ if project_root:
85
+ enrich_global_context(global_context, project_root)
86
+
87
+ if not global_context:
88
+ return "- (none specified)"
89
+ lines = []
90
+ for key, value in sorted(global_context.items()):
91
+ lines.append("- **{}**: {}".format(key, value))
92
+ return "\n".join(lines)
93
+
94
+
95
+ def format_scope(scope):
96
+ """Format scope object into markdown detail lines."""
97
+ if not scope or not isinstance(scope, dict):
98
+ return "- (no scope details)"
99
+ lines = []
100
+
101
+ files = scope.get("files", [])
102
+ if files:
103
+ lines.append("- **Files**:")
104
+ for f in files:
105
+ lines.append(" - `{}`".format(f))
106
+
107
+ modules = scope.get("modules", [])
108
+ if modules:
109
+ lines.append("- **Modules**:")
110
+ for m in modules:
111
+ lines.append(" - `{}`".format(m))
112
+
113
+ if not lines:
114
+ lines.append("- (no scope details)")
115
+ return "\n".join(lines)
116
+
117
+
118
+ def _format_scope_list(scope, key):
119
+ """Extract and format a list from scope by key (files or modules)."""
120
+ if not scope or not isinstance(scope, dict):
121
+ return "- (none specified)"
122
+ items = scope.get(key, [])
123
+ if not items:
124
+ return "- (none specified)"
125
+ return "\n".join("- `{}`".format(item) for item in items)
126
+
127
+
128
+ def format_scope_files(scope):
129
+ """Extract and format just the files list from scope."""
130
+ return _format_scope_list(scope, "files")
131
+
132
+
133
+ def format_scope_modules(scope):
134
+ """Extract and format just the modules list from scope."""
135
+ return _format_scope_list(scope, "modules")
136
+
137
+
138
+ def format_behavior_preservation(bp):
139
+ """Format behavior_preservation object into markdown detail lines."""
140
+ if not bp or not isinstance(bp, dict):
141
+ return "- (no behavior preservation details)"
142
+ lines = []
143
+
144
+ strategy = bp.get("strategy", "unknown")
145
+ lines.append("- **Strategy**: {}".format(strategy))
146
+
147
+ existing_tests = bp.get("existing_tests", [])
148
+ if existing_tests:
149
+ lines.append("- **Existing Tests**:")
150
+ for t in existing_tests:
151
+ lines.append(" - `{}`".format(t))
152
+
153
+ new_tests_needed = bp.get("new_tests_needed", [])
154
+ if new_tests_needed:
155
+ lines.append("- **New Tests Needed**:")
156
+ for t in new_tests_needed:
157
+ lines.append(" - {}".format(t))
158
+
159
+ if len(lines) == 1:
160
+ lines.append("- (no additional details)")
161
+ return "\n".join(lines)
162
+
163
+
164
+ def format_dependencies(dependencies):
165
+ """Format dependencies list as a markdown bullet list."""
166
+ if not dependencies or not isinstance(dependencies, list):
167
+ return "- (none)"
168
+ if len(dependencies) == 0:
169
+ return "- (none)"
170
+ lines = []
171
+ for dep in dependencies:
172
+ lines.append("- `{}`".format(dep))
173
+ return "\n".join(lines)
174
+
175
+
176
+ def get_prev_session_status(state_dir, refactor_id):
177
+ """Read previous session status from state dir if available."""
178
+ if not state_dir:
179
+ return "N/A (first run)"
180
+
181
+ refactor_status_path = os.path.join(state_dir, "refactors", refactor_id, "status.json")
182
+ try:
183
+ with open(refactor_status_path, "r", encoding="utf-8") as f:
184
+ refactor_status = json.load(f)
185
+ except (json.JSONDecodeError, IOError, OSError):
186
+ return "N/A (first run)"
187
+
188
+ last_session_id = refactor_status.get("last_session_id")
189
+ if not last_session_id:
190
+ return "N/A (first run)"
191
+
192
+ session_status_path = os.path.join(
193
+ state_dir, "refactors", refactor_id, "sessions",
194
+ last_session_id, "session-status.json"
195
+ )
196
+ try:
197
+ with open(session_status_path, "r", encoding="utf-8") as f:
198
+ session_data = json.load(f)
199
+ except (json.JSONDecodeError, IOError, OSError):
200
+ return "N/A (previous session status file not found)"
201
+
202
+ status = session_data.get("status", "unknown")
203
+ checkpoint = session_data.get("checkpoint_reached", "none")
204
+ current_phase = session_data.get("current_phase", "unknown")
205
+ errors = session_data.get("errors", [])
206
+
207
+ result = "{} (checkpoint: {}, last phase: {})".format(
208
+ status, checkpoint, current_phase
209
+ )
210
+ if errors:
211
+ result += " — errors: {}".format("; ".join(str(e) for e in errors))
212
+ return result
213
+
214
+
215
+ def resolve_project_root(script_dir):
216
+ """Resolve project root as the parent of dev-pipeline/."""
217
+ dev_pipeline_dir = os.path.dirname(script_dir)
218
+ project_root = os.path.dirname(dev_pipeline_dir)
219
+ return os.path.abspath(project_root)
220
+
221
+
222
+ def build_replacements(args, refactor, global_context, script_dir):
223
+ """Build the full dict of placeholder -> replacement value."""
224
+ project_root = resolve_project_root(script_dir)
225
+
226
+ # Platform-aware agent/team path resolution
227
+ platform = os.environ.get("PRIZMKIT_PLATFORM", "")
228
+ home_dir = os.path.expanduser("~")
229
+
230
+ if not platform:
231
+ if os.path.isdir(os.path.join(project_root, ".claude", "agents")):
232
+ platform = "claude"
233
+ else:
234
+ platform = "codebuddy"
235
+
236
+ if platform == "claude":
237
+ agents_dir = os.path.join(project_root, ".claude", "agents")
238
+ team_config_path = os.path.join(project_root, ".claude", "team-info.json")
239
+ else:
240
+ agents_dir = os.path.join(project_root, ".codebuddy", "agents")
241
+ team_config_path = os.path.join(
242
+ home_dir, ".codebuddy", "teams", "prizm-dev-team", "config.json"
243
+ )
244
+
245
+ dev_subagent = os.path.join(agents_dir, "prizm-dev-team-dev.md")
246
+ reviewer_subagent = os.path.join(agents_dir, "prizm-dev-team-reviewer.md")
247
+
248
+ # Session status path
249
+ session_status_path = os.path.join(
250
+ project_root, "dev-pipeline", "refactor-state", "refactors", args.refactor_id,
251
+ "sessions", args.session_id, "session-status.json"
252
+ )
253
+
254
+ prev_status = get_prev_session_status(args.state_dir, args.refactor_id)
255
+
256
+ # Scope
257
+ scope = refactor.get("scope", {})
258
+
259
+ # Behavior preservation
260
+ bp = refactor.get("behavior_preservation", {})
261
+ behavior_strategy = bp.get("strategy", "test-gate") if isinstance(bp, dict) else "test-gate"
262
+ existing_tests = bp.get("existing_tests", []) if isinstance(bp, dict) else []
263
+ new_tests_needed = bp.get("new_tests_needed", []) if isinstance(bp, dict) else []
264
+
265
+ # Format existing tests
266
+ if existing_tests:
267
+ existing_tests_str = "\n".join("- `{}`".format(t) for t in existing_tests)
268
+ else:
269
+ existing_tests_str = "- (none specified)"
270
+
271
+ # Format new tests needed
272
+ if new_tests_needed:
273
+ new_tests_str = "\n".join("- {}".format(t) for t in new_tests_needed)
274
+ else:
275
+ new_tests_str = "- (none specified)"
276
+
277
+ replacements = {
278
+ "{{RUN_ID}}": args.run_id,
279
+ "{{SESSION_ID}}": args.session_id,
280
+ "{{REFACTOR_ID}}": args.refactor_id,
281
+ "{{REFACTOR_TITLE}}": refactor.get("title", ""),
282
+ "{{REFACTOR_TYPE}}": refactor.get("type", "restructure"),
283
+ "{{SCOPE_FILES}}": format_scope_files(scope),
284
+ "{{SCOPE_MODULES}}": format_scope_modules(scope),
285
+ "{{BEHAVIOR_STRATEGY}}": behavior_strategy,
286
+ "{{EXISTING_TESTS}}": existing_tests_str,
287
+ "{{NEW_TESTS_NEEDED}}": new_tests_str,
288
+ "{{PRIORITY}}": refactor.get("priority", "medium"),
289
+ "{{COMPLEXITY}}": refactor.get("complexity", "medium"),
290
+ "{{RETRY_COUNT}}": str(args.retry_count),
291
+ "{{MAX_RETRIES}}": str(DEFAULT_MAX_RETRIES),
292
+ "{{PREV_SESSION_STATUS}}": prev_status,
293
+ "{{RESUME_PHASE}}": args.resume_phase,
294
+ "{{REFACTOR_DESCRIPTION}}": refactor.get("description", ""),
295
+ "{{ACCEPTANCE_CRITERIA}}": format_acceptance_criteria(
296
+ refactor.get("acceptance_criteria", [])
297
+ ),
298
+ "{{DEPENDENCIES}}": format_dependencies(
299
+ refactor.get("dependencies", [])
300
+ ),
301
+ "{{GLOBAL_CONTEXT}}": format_global_context(global_context, project_root),
302
+ "{{TEAM_CONFIG_PATH}}": team_config_path,
303
+ "{{DEV_SUBAGENT_PATH}}": dev_subagent,
304
+ "{{REVIEWER_SUBAGENT_PATH}}": reviewer_subagent,
305
+ "{{SESSION_STATUS_PATH}}": session_status_path,
306
+ "{{PROJECT_ROOT}}": project_root,
307
+ "{{TIMESTAMP}}": "", # Placeholder — agent fills in timestamp
308
+ }
309
+
310
+ return replacements
311
+
312
+
313
+ def render_template(template_content, replacements):
314
+ """Render the template by replacing all {{PLACEHOLDER}} variables."""
315
+ content = template_content
316
+
317
+ # Replace all {{PLACEHOLDER}} variables
318
+ for placeholder, value in replacements.items():
319
+ content = content.replace(placeholder, value)
320
+
321
+ return content
322
+
323
+
324
+ def write_output(output_path, content):
325
+ """Write the rendered content to the output file."""
326
+ abs_path = os.path.abspath(output_path)
327
+ output_dir = os.path.dirname(abs_path)
328
+ if output_dir and not os.path.isdir(output_dir):
329
+ try:
330
+ os.makedirs(output_dir, exist_ok=True)
331
+ except OSError as e:
332
+ return "Cannot create output directory: {}".format(str(e))
333
+ try:
334
+ with open(abs_path, "w", encoding="utf-8") as f:
335
+ f.write(content)
336
+ except IOError as e:
337
+ return "Cannot write output file: {}".format(str(e))
338
+ return None
339
+
340
+
341
+ def emit_failure(message):
342
+ """Emit standardized failure JSON and exit.
343
+
344
+ Uses a different format than error_out() — includes 'success: false'
345
+ for compatibility with the pipeline's JSON parsing expectations.
346
+ """
347
+ print(json.dumps({"success": False, "error": message}, indent=2, ensure_ascii=False))
348
+ sys.exit(1)
349
+
350
+
351
+ def main():
352
+ args = parse_args()
353
+
354
+ # Resolve script directory
355
+ script_dir = os.path.dirname(os.path.abspath(__file__))
356
+
357
+ # Resolve template path
358
+ if args.template:
359
+ template_path = args.template
360
+ else:
361
+ template_path = os.path.join(
362
+ script_dir, "..", "templates", "refactor-bootstrap-prompt.md"
363
+ )
364
+
365
+ # Load template
366
+ template_content, err = read_text_file(template_path)
367
+ if err:
368
+ emit_failure("Template error: {}".format(err))
369
+
370
+ # Load refactor list
371
+ refactor_list_data, err = load_json_file(args.refactor_list)
372
+ if err:
373
+ emit_failure("Refactor list error: {}".format(err))
374
+
375
+ # Extract refactors array
376
+ refactors = refactor_list_data.get("refactors")
377
+ if not isinstance(refactors, list):
378
+ emit_failure("Refactor list does not contain a 'refactors' array")
379
+
380
+ # Find the target refactor
381
+ refactor = find_refactor(refactors, args.refactor_id)
382
+ if refactor is None:
383
+ emit_failure("Refactor '{}' not found in refactor list".format(args.refactor_id))
384
+
385
+ # Extract global context
386
+ global_context = refactor_list_data.get("global_context", {})
387
+ if not isinstance(global_context, dict):
388
+ global_context = {}
389
+
390
+ # Build replacements
391
+ replacements = build_replacements(args, refactor, global_context, script_dir)
392
+
393
+ # Render the template
394
+ rendered = render_template(template_content, replacements)
395
+
396
+ # Write the output
397
+ err = write_output(args.output, rendered)
398
+ if err:
399
+ emit_failure(err)
400
+
401
+ # Success
402
+ output = {
403
+ "success": True,
404
+ "output_path": os.path.abspath(args.output),
405
+ }
406
+ print(json.dumps(output, indent=2, ensure_ascii=False))
407
+ sys.exit(0)
408
+
409
+
410
+ if __name__ == "__main__":
411
+ try:
412
+ main()
413
+ except KeyboardInterrupt:
414
+ emit_failure("generate-refactor-prompt interrupted")
415
+ except SystemExit:
416
+ raise
417
+ except Exception as exc:
418
+ LOGGER.exception("Unhandled exception in generate-refactor-prompt")
419
+ emit_failure("Unexpected error: {}".format(str(exc)))