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.
- package/bundled/VERSION.json +3 -3
- package/bundled/adapters/claude/agent-adapter.js +18 -0
- package/bundled/adapters/claude/command-adapter.js +1 -27
- package/bundled/agents/prizm-dev-team-critic.md +2 -0
- package/bundled/agents/prizm-dev-team-dev.md +2 -0
- package/bundled/agents/prizm-dev-team-reviewer.md +2 -0
- package/bundled/dev-pipeline/README.md +63 -63
- package/bundled/dev-pipeline/assets/feature-list-example.json +1 -1
- package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +1 -1
- package/bundled/dev-pipeline/{launch-daemon.sh → launch-feature-daemon.sh} +33 -33
- package/bundled/dev-pipeline/launch-refactor-daemon.sh +454 -0
- package/bundled/dev-pipeline/lib/branch.sh +1 -1
- package/bundled/dev-pipeline/reset-feature.sh +3 -3
- package/bundled/dev-pipeline/reset-refactor.sh +312 -0
- package/bundled/dev-pipeline/{retry-bug.sh → retry-bugfix.sh} +47 -59
- package/bundled/dev-pipeline/retry-feature.sh +41 -54
- package/bundled/dev-pipeline/retry-refactor.sh +358 -0
- package/bundled/dev-pipeline/run-bugfix.sh +6 -0
- package/bundled/dev-pipeline/{run.sh → run-feature.sh} +31 -31
- package/bundled/dev-pipeline/run-refactor.sh +787 -0
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +177 -10
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +419 -0
- package/bundled/dev-pipeline/scripts/init-refactor-pipeline.py +393 -0
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +726 -0
- package/bundled/dev-pipeline/templates/agent-prompts/critic-code-challenge.md +13 -0
- package/bundled/dev-pipeline/templates/agent-prompts/critic-plan-challenge.md +7 -0
- package/bundled/dev-pipeline/templates/agent-prompts/dev-fix.md +7 -0
- package/bundled/dev-pipeline/templates/agent-prompts/dev-implement.md +26 -0
- package/bundled/dev-pipeline/templates/agent-prompts/dev-resume.md +5 -0
- package/bundled/dev-pipeline/templates/agent-prompts/reviewer-analyze.md +5 -0
- package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +12 -0
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +29 -2
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +8 -7
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +11 -10
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +2 -3
- package/bundled/dev-pipeline/templates/feature-list-schema.json +1 -1
- package/bundled/dev-pipeline/templates/refactor-list-schema.json +159 -0
- package/bundled/dev-pipeline/templates/sections/ac-verification-checklist.md +13 -0
- package/bundled/dev-pipeline/templates/sections/feature-context.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-analyze-agent.md +9 -8
- package/bundled/dev-pipeline/templates/sections/phase-analyze-full.md +9 -8
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification.md +2 -1
- package/bundled/dev-pipeline/templates/sections/phase-critic-code.md +8 -10
- package/bundled/dev-pipeline/templates/sections/phase-critic-plan-full.md +9 -10
- package/bundled/dev-pipeline/templates/sections/phase-critic-plan.md +8 -9
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +7 -10
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +8 -15
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +7 -12
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +8 -19
- package/bundled/dev-pipeline/templates/sections/test-failure-recovery.md +75 -0
- package/bundled/skills/_metadata.json +33 -6
- package/bundled/skills/app-planner/SKILL.md +105 -320
- package/bundled/skills/app-planner/assets/app-design-guide.md +101 -0
- package/bundled/skills/app-planner/references/frontend-design-guide.md +1 -1
- package/bundled/skills/app-planner/references/project-brief-guide.md +49 -80
- package/bundled/skills/bug-fix-workflow/SKILL.md +2 -2
- package/bundled/skills/bug-planner/SKILL.md +68 -5
- package/bundled/skills/bug-planner/scripts/validate-bug-list.py +3 -2
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +19 -5
- package/bundled/skills/{dev-pipeline-launcher → feature-pipeline-launcher}/SKILL.md +32 -32
- package/bundled/skills/feature-planner/SKILL.md +337 -0
- package/bundled/skills/{app-planner → feature-planner}/assets/evaluation-guide.md +4 -4
- package/bundled/skills/{app-planner → feature-planner}/assets/planning-guide.md +3 -171
- package/bundled/skills/{app-planner → feature-planner}/references/browser-interaction.md +6 -5
- package/bundled/skills/feature-planner/references/decomposition-patterns.md +75 -0
- package/bundled/skills/{app-planner → feature-planner}/references/error-recovery.md +8 -8
- package/bundled/skills/{app-planner → feature-planner}/references/incremental-feature-planning.md +1 -1
- package/bundled/skills/{app-planner/references/new-app-planning.md → feature-planner/references/new-project-planning.md} +1 -1
- package/bundled/skills/{app-planner → feature-planner}/scripts/validate-and-generate.py +4 -4
- package/bundled/skills/feature-workflow/SKILL.md +23 -23
- package/bundled/skills/prizm-kit/SKILL.md +1 -3
- package/bundled/skills/prizmkit-analyze/SKILL.md +2 -5
- package/bundled/skills/prizmkit-code-review/SKILL.md +2 -2
- package/bundled/skills/prizmkit-committer/SKILL.md +4 -8
- package/bundled/skills/prizmkit-deploy/SKILL.md +1 -5
- package/bundled/skills/prizmkit-implement/SKILL.md +3 -50
- package/bundled/skills/prizmkit-init/SKILL.md +5 -77
- package/bundled/skills/prizmkit-plan/SKILL.md +1 -12
- package/bundled/skills/prizmkit-prizm-docs/SKILL.md +6 -24
- package/bundled/skills/prizmkit-prizm-docs/assets/PRIZM-SPEC.md +21 -0
- package/bundled/skills/prizmkit-retrospective/SKILL.md +12 -117
- package/bundled/skills/recovery-workflow/SKILL.md +166 -316
- package/bundled/skills/recovery-workflow/evals/evals.json +29 -13
- package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +232 -274
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +352 -0
- package/bundled/skills/refactor-planner/SKILL.md +436 -0
- package/bundled/skills/refactor-planner/assets/planning-guide.md +292 -0
- package/bundled/skills/refactor-planner/references/behavior-preservation.md +301 -0
- package/bundled/skills/refactor-planner/references/refactor-scoping-guide.md +221 -0
- package/bundled/skills/refactor-planner/scripts/validate-and-generate-refactor.py +786 -0
- package/bundled/skills/refactor-workflow/SKILL.md +299 -319
- package/package.json +1 -1
- package/src/clean.js +3 -3
- package/src/scaffold.js +6 -6
- package/bundled/skills/prizmkit-plan/assets/spec-template.md +0 -56
- package/bundled/skills/prizmkit-plan/references/clarify-guide.md +0 -67
- package/src/config.js +0 -504
- package/src/prompts.js +0 -210
- /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
|
|
255
|
-
|
|
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
|
-
|
|
746
|
-
|
|
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
|
-
" #
|
|
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
|
|
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
|
-
|
|
1021
|
-
|
|
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)))
|