prizmkit 1.1.111 → 1.1.112

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 (110) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/agents/prizm-dev-team-reviewer.md +12 -1
  3. package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +1 -0
  4. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +19 -73
  5. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +283 -421
  6. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +1 -1
  7. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +259 -481
  8. package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +34 -0
  9. package/bundled/dev-pipeline/scripts/init-pipeline.py +12 -0
  10. package/bundled/dev-pipeline/scripts/init-refactor-pipeline.py +24 -1
  11. package/bundled/dev-pipeline/scripts/prompt_framework.py +312 -0
  12. package/bundled/dev-pipeline/scripts/update-bug-status.py +87 -20
  13. package/bundled/dev-pipeline/scripts/update-feature-status.py +82 -16
  14. package/bundled/dev-pipeline/scripts/update-refactor-status.py +68 -28
  15. package/bundled/dev-pipeline/scripts/utils.py +171 -0
  16. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
  17. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +10 -18
  18. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +14 -25
  19. package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +9 -1
  20. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +11 -8
  21. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +122 -182
  22. package/bundled/dev-pipeline/templates/sections/bugfix-artifacts.md +16 -0
  23. package/bundled/dev-pipeline/templates/sections/bugfix-critical-paths.md +12 -0
  24. package/bundled/dev-pipeline/templates/sections/bugfix-mission.md +9 -0
  25. package/bundled/dev-pipeline/templates/sections/bugfix-phase-commit-report.md +31 -0
  26. package/bundled/dev-pipeline/templates/sections/bugfix-phase-critic.md +22 -0
  27. package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +23 -0
  28. package/bundled/dev-pipeline/templates/sections/bugfix-phase-implement.md +20 -0
  29. package/bundled/dev-pipeline/templates/sections/bugfix-phase-init.md +9 -0
  30. package/bundled/dev-pipeline/templates/sections/bugfix-phase-manual-verification.md +15 -0
  31. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +29 -0
  32. package/bundled/dev-pipeline/templates/sections/bugfix-reminders.md +11 -0
  33. package/bundled/dev-pipeline/templates/sections/bugfix-session-context.md +7 -0
  34. package/bundled/dev-pipeline/templates/sections/bugfix-session-status.md +31 -0
  35. package/bundled/dev-pipeline/templates/sections/bugfix-task-contract.md +40 -0
  36. package/bundled/dev-pipeline/templates/sections/failure-capture.md +2 -2
  37. package/bundled/dev-pipeline/templates/sections/phase-browser-verification-auto.md +1 -1
  38. package/bundled/dev-pipeline/templates/sections/refactor-artifacts.md +17 -0
  39. package/bundled/dev-pipeline/templates/sections/refactor-critical-paths.md +13 -0
  40. package/bundled/dev-pipeline/templates/sections/refactor-mission.md +9 -0
  41. package/bundled/dev-pipeline/templates/sections/refactor-phase-commit-report.md +37 -0
  42. package/bundled/dev-pipeline/templates/sections/refactor-phase-critic.md +22 -0
  43. package/bundled/dev-pipeline/templates/sections/refactor-phase-implement.md +16 -0
  44. package/bundled/dev-pipeline/templates/sections/refactor-phase-init.md +9 -0
  45. package/bundled/dev-pipeline/templates/sections/refactor-phase-plan.md +22 -0
  46. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +19 -0
  47. package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +11 -0
  48. package/bundled/dev-pipeline/templates/sections/refactor-session-context.md +7 -0
  49. package/bundled/dev-pipeline/templates/sections/refactor-session-status.md +28 -0
  50. package/bundled/dev-pipeline/templates/sections/refactor-task-contract.md +52 -0
  51. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +17 -5
  52. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +197 -0
  53. package/bundled/dev-pipeline/tests/test_unified_cli.py +6 -2
  54. package/bundled/dev-pipeline/tests/test_utils.py +66 -1
  55. package/bundled/skills/_metadata.json +1 -1
  56. package/bundled/skills/app-planner/SKILL.md +8 -7
  57. package/bundled/skills/app-planner/references/project-brief-guide.md +2 -2
  58. package/bundled/skills/app-planner/references/rules/backend/derivation-rules.md +1 -1
  59. package/bundled/skills/app-planner/references/rules/frontend/derivation-rules.md +1 -1
  60. package/bundled/skills/app-planner/references/rules-configuration.md +53 -26
  61. package/bundled/skills/bug-fix-workflow/SKILL.md +191 -336
  62. package/bundled/skills/bug-planner/SKILL.md +6 -5
  63. package/bundled/skills/bug-planner/references/critic-and-verification.md +3 -3
  64. package/bundled/skills/bug-planner/references/schema-validation.md +2 -1
  65. package/bundled/skills/bug-planner/scripts/validate-bug-list.py +30 -1
  66. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +16 -16
  67. package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +2 -2
  68. package/bundled/skills/feature-pipeline-launcher/SKILL.md +19 -11
  69. package/bundled/skills/feature-planner/SKILL.md +9 -11
  70. package/bundled/skills/feature-planner/assets/planning-guide.md +1 -1
  71. package/bundled/skills/feature-planner/references/decomposition-patterns.md +1 -1
  72. package/bundled/skills/feature-planner/references/error-recovery.md +2 -1
  73. package/bundled/skills/feature-planner/references/incremental-feature-planning.md +2 -2
  74. package/bundled/skills/feature-planner/references/new-project-planning.md +35 -39
  75. package/bundled/skills/feature-planner/scripts/validate-and-generate.py +1 -1
  76. package/bundled/skills/feature-workflow/SKILL.md +169 -298
  77. package/bundled/skills/prizmkit/SKILL.md +103 -57
  78. package/bundled/skills/prizmkit-code-review/SKILL.md +97 -116
  79. package/bundled/skills/prizmkit-code-review/references/review-report-template.md +1 -0
  80. package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +12 -1
  81. package/bundled/skills/prizmkit-committer/SKILL.md +59 -47
  82. package/bundled/skills/prizmkit-deploy/SKILL.md +162 -381
  83. package/bundled/skills/prizmkit-deploy/references/ssh-adapter-flow.md +220 -0
  84. package/bundled/skills/prizmkit-implement/SKILL.md +51 -40
  85. package/bundled/skills/prizmkit-init/SKILL.md +4 -3
  86. package/bundled/skills/prizmkit-plan/SKILL.md +85 -70
  87. package/bundled/skills/prizmkit-prizm-docs/SKILL.md +94 -73
  88. package/bundled/skills/prizmkit-prizm-docs/assets/prizm-docs-format.md +20 -18
  89. package/bundled/skills/prizmkit-prizm-docs/references/op-update.md +18 -14
  90. package/bundled/skills/prizmkit-retrospective/SKILL.md +56 -48
  91. package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +79 -27
  92. package/bundled/skills/prizmkit-test/SKILL.md +138 -141
  93. package/bundled/skills/prizmkit-test/references/examples.md +10 -8
  94. package/bundled/skills/prizmkit-test/references/test-generation-steps.md +1 -1
  95. package/bundled/skills/prizmkit-test/references/test-report-template.md +2 -2
  96. package/bundled/skills/recovery-workflow/SKILL.md +195 -256
  97. package/bundled/skills/recovery-workflow/evals/evals.json +21 -13
  98. package/bundled/skills/recovery-workflow/references/detection.md +48 -39
  99. package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +258 -322
  100. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +29 -11
  101. package/bundled/skills/refactor-planner/SKILL.md +2 -2
  102. package/bundled/skills/refactor-planner/references/behavior-preservation.md +24 -19
  103. package/bundled/skills/refactor-planner/references/fast-path.md +2 -4
  104. package/bundled/skills/refactor-planner/references/planning-phases.md +2 -2
  105. package/bundled/skills/refactor-workflow/SKILL.md +173 -307
  106. package/package.json +1 -1
  107. package/src/scaffold.js +5 -0
  108. package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +0 -6
  109. package/bundled/skills/feature-workflow/references/brainstorm-guide.md +0 -137
  110. package/bundled/skills/refactor-workflow/references/brainstorm-guide.md +0 -116
@@ -1,16 +1,8 @@
1
1
  #!/usr/bin/env python3
2
- """Generate a session-specific bug fix bootstrap prompt from template and .prizmkit/plans/bug-fix-list.json.
3
-
4
- Reads the bugfix-bootstrap-prompt.md template and a .prizmkit/plans/bug-fix-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-bugfix-prompt.py \
10
- --bug-list <path> --bug-id <id> \
11
- --session-id <id> --run-id <id> \
12
- --retry-count <n> --resume-phase <n|null> \
13
- --state-dir <path> --output <path>
2
+ """Generate a session-specific bug fix bootstrap prompt from bug-fix-list.json.
3
+
4
+ The default path uses modular prompt sections. The legacy monolithic template is
5
+ kept only for explicit --template compatibility.
14
6
  """
15
7
 
16
8
  import argparse
@@ -19,20 +11,55 @@ import os
19
11
  import re
20
12
  import sys
21
13
 
22
- from utils import enrich_global_context, helper_replacements, load_json_file, read_checkpoint_system_section, resolve_prompt_platform, setup_logging
23
14
  from continuation import add_continuation_args, append_continuation_handoff, is_continuation
15
+ from prompt_framework import (
16
+ browser_enabled_from_item,
17
+ detect_dev_server_port,
18
+ generate_checkpoint_from_sections,
19
+ load_section,
20
+ merge_checkpoint_state,
21
+ mode_agent_count,
22
+ normalize_mode,
23
+ read_text_file,
24
+ render_from_sections,
25
+ replace_placeholders,
26
+ validate_rendered,
27
+ write_output,
28
+ )
29
+ from utils import enrich_global_context, helper_replacements, load_json_file, read_checkpoint_system_section, resolve_prompt_platform, setup_logging
24
30
 
25
31
 
26
- DEFAULT_MAX_RETRIES = 3
27
-
28
32
  LOGGER = setup_logging("generate-bugfix-prompt")
29
33
 
30
34
 
35
+ # Section name -> checkpoint contract.
36
+ BUGFIX_SECTION_TO_SKILL = {
37
+ "bugfix-phase-init": ("prizmkit-init", "Initialize", [".prizmkit/bugfix/{slug}"]),
38
+ "bugfix-phase-diagnose-plan": (
39
+ "bug-diagnosis-and-plan", "Diagnose & Plan",
40
+ [".prizmkit/bugfix/{slug}/spec.md", ".prizmkit/bugfix/{slug}/plan.md"],
41
+ ),
42
+ "bugfix-phase-critic": ("critic-plan-review", "Critic: Root Cause & Fix Plan Review", []),
43
+ "bugfix-phase-implement": ("prizmkit-implement", "Implement Fix", []),
44
+ "bugfix-phase-review": ("prizmkit-code-review", "Code Review", []),
45
+ "phase-browser": ("browser-verification", "Browser Verification", []),
46
+ "bugfix-phase-commit-report": None,
47
+ }
48
+
49
+ BUGFIX_SPLIT_STEPS = {
50
+ "bugfix-phase-commit-report": [
51
+ ("prizmkit-retrospective", "Retrospective", []),
52
+ ("prizmkit-committer", "Commit", []),
53
+ ("bug-report", "Generate Fix Report", [".prizmkit/bugfix/{slug}/fix-report.md"]),
54
+ ],
55
+ }
56
+
57
+
31
58
  def parse_args():
32
59
  parser = argparse.ArgumentParser(
33
60
  description=(
34
- "Generate a session-specific bug fix bootstrap prompt from a template "
35
- "and .prizmkit/plans/bug-fix-list.json."
61
+ "Generate a session-specific bug fix bootstrap prompt from "
62
+ ".prizmkit/plans/bug-fix-list.json."
36
63
  )
37
64
  )
38
65
  parser.add_argument("--bug-list", required=True, help="Path to .prizmkit/plans/bug-fix-list.json")
@@ -44,27 +71,13 @@ def parse_args():
44
71
  parser.add_argument("--state-dir", default=None, help="State directory (default: .prizmkit/state/bugfix)")
45
72
  parser.add_argument("--output", required=True, help="Output path for the rendered prompt")
46
73
  parser.add_argument("--project-root", default=None, help="AI-visible project root used for rendered prompt paths")
47
- parser.add_argument("--template", default=None, help="Custom template path. Defaults to {script_dir}/../templates/bugfix-bootstrap-prompt.md")
48
- parser.add_argument("--mode", default=None, help="Pipeline execution mode override: lite, standard, full")
49
- parser.add_argument("--critic", default=None, help="Enable critic agent: true/false")
74
+ parser.add_argument("--template", default=None, help="Custom legacy template path. Defaults to section assembly.")
75
+ parser.add_argument("--mode", choices=["lite", "standard", "full"], default=None, help="Pipeline execution mode override")
76
+ parser.add_argument("--critic", choices=["true", "false"], default=None, help="Enable critic agent: true/false")
50
77
  add_continuation_args(parser, "bugfix")
51
-
52
78
  return parser.parse_args()
53
79
 
54
80
 
55
- def read_text_file(path):
56
- """Read and return the text content of a file."""
57
- abs_path = os.path.abspath(path)
58
- if not os.path.isfile(abs_path):
59
- return None, "File not found: {}".format(abs_path)
60
- try:
61
- with open(abs_path, "r", encoding="utf-8") as f:
62
- return f.read(), None
63
- except IOError as e:
64
- return None, "Cannot read file: {}".format(str(e))
65
-
66
-
67
-
68
81
  def _parse_positive_int_env(name, default):
69
82
  """Parse a positive integer environment variable."""
70
83
  value = os.environ.get(name, str(default))
@@ -89,27 +102,26 @@ def format_acceptance_criteria(criteria):
89
102
  """Format acceptance criteria as a markdown bullet list."""
90
103
  if not criteria:
91
104
  return "- (none specified)"
92
- lines = []
93
- for item in criteria:
94
- lines.append("- {}".format(item))
95
- return "\n".join(lines)
105
+ return "\n".join("- {}".format(item) for item in criteria)
96
106
 
97
107
 
98
- def format_global_context(global_context, project_root=None):
99
- """Format global_context dict as a key-value list.
108
+ def format_ac_checklist(criteria):
109
+ """Format acceptance criteria as a checkbox list."""
110
+ if not criteria:
111
+ return "- [ ] (no acceptance criteria specified)"
112
+ return "\n".join("- [ ] {}".format(item) for item in criteria)
113
+
100
114
 
101
- If global_context is empty/sparse and project_root is provided,
102
- auto-detect tech stack from project files to fill gaps.
103
- """
115
+ def format_global_context(global_context, project_root=None):
116
+ """Format global_context dict as a key-value list."""
104
117
  if project_root:
105
118
  enrich_global_context(global_context, project_root)
106
-
107
119
  if not global_context:
108
120
  return "- (none specified)"
109
- lines = []
110
- for key, value in sorted(global_context.items()):
111
- lines.append("- **{}**: {}".format(key, value))
112
- return "\n".join(lines)
121
+ return "\n".join(
122
+ "- **{}**: {}".format(key, value)
123
+ for key, value in sorted(global_context.items())
124
+ )
113
125
 
114
126
 
115
127
  def format_error_source_details(error_source):
@@ -120,35 +132,23 @@ def format_error_source_details(error_source):
120
132
  etype = error_source.get("type", "unknown")
121
133
 
122
134
  if etype == "stack_trace" and error_source.get("stack_trace"):
123
- lines.append("- **Stack Trace**:")
124
- lines.append("```")
125
- lines.append(error_source["stack_trace"])
126
- lines.append("```")
135
+ lines.extend(["- **Stack Trace**:", "```", error_source["stack_trace"], "```"])
127
136
  if error_source.get("error_message"):
128
137
  lines.append("- **Error Message**: {}".format(error_source["error_message"]))
129
138
  if etype == "log_pattern" and error_source.get("log_snippet"):
130
- lines.append("- **Log Snippet**:")
131
- lines.append("```")
132
- lines.append(error_source["log_snippet"])
133
- lines.append("```")
139
+ lines.extend(["- **Log Snippet**:", "```", error_source["log_snippet"], "```"])
134
140
  if etype == "failed_test" and error_source.get("failed_test_path"):
135
141
  lines.append("- **Failed Test**: `{}`".format(error_source["failed_test_path"]))
136
142
  if etype == "user_report" and error_source.get("reproduction_steps"):
137
143
  lines.append("- **Reproduction Steps**:")
138
- for i, step in enumerate(error_source["reproduction_steps"], 1):
139
- lines.append(" {}. {}".format(i, step))
144
+ for index, step in enumerate(error_source["reproduction_steps"], 1):
145
+ lines.append(" {}. {}".format(index, step))
140
146
 
141
- if not lines:
142
- lines.append("- (no additional details)")
143
- return "\n".join(lines)
147
+ return "\n".join(lines or ["- (no additional details)"])
144
148
 
145
149
 
146
150
  def format_user_context(user_context):
147
- """Format user_context array as a markdown section.
148
-
149
- Returns empty string if user_context is empty or absent,
150
- so the template placeholder resolves to nothing.
151
- """
151
+ """Format user_context array as a markdown section."""
152
152
  if not user_context or not isinstance(user_context, list):
153
153
  return ""
154
154
  items = [item for item in user_context if isinstance(item, str) and item.strip()]
@@ -157,12 +157,10 @@ def format_user_context(user_context):
157
157
  lines = [
158
158
  "### User-Provided Context (HIGHEST PRIORITY)",
159
159
  "",
160
- "> The following materials were provided by the user. "
161
- "They take precedence over AI inference.",
160
+ "> The following materials were provided by the user. They take precedence over AI inference.",
162
161
  "",
163
162
  ]
164
- for item in items:
165
- lines.append("- {}".format(item))
163
+ lines.extend("- {}".format(item) for item in items)
166
164
  return "\n".join(lines)
167
165
 
168
166
 
@@ -170,64 +168,50 @@ def format_environment(env):
170
168
  """Format environment dict as a key-value list."""
171
169
  if not env or not isinstance(env, dict):
172
170
  return "- (not specified)"
173
- lines = []
174
- for key, value in sorted(env.items()):
175
- if value:
176
- lines.append("- **{}**: {}".format(key, value))
177
- if not lines:
178
- return "- (not specified)"
179
- return "\n".join(lines)
171
+ lines = [
172
+ "- **{}**: {}".format(key, value)
173
+ for key, value in sorted(env.items())
174
+ if value
175
+ ]
176
+ return "\n".join(lines) if lines else "- (not specified)"
180
177
 
181
178
 
182
179
  def get_prev_session_status(state_dir, bug_id):
183
180
  """Read previous session status from state dir if available."""
184
181
  if not state_dir:
185
182
  return "N/A (first run)"
186
-
187
183
  bug_status_path = os.path.join(state_dir, bug_id, "status.json")
188
184
  if not os.path.isfile(bug_status_path):
189
185
  return "N/A (first run)"
190
-
191
186
  try:
192
187
  with open(bug_status_path, "r", encoding="utf-8") as f:
193
188
  bug_status = json.load(f)
194
189
  except (json.JSONDecodeError, IOError):
195
190
  return "N/A (could not read bug status)"
196
-
197
191
  last_session_id = bug_status.get("last_session_id")
198
192
  if not last_session_id:
199
193
  return "N/A (first run)"
200
-
201
- session_status_path = os.path.join(state_dir, bug_id, "sessions",
202
- last_session_id, "session-status.json"
203
- )
194
+ session_status_path = os.path.join(state_dir, bug_id, "sessions", last_session_id, "session-status.json")
204
195
  if not os.path.isfile(session_status_path):
205
196
  return "N/A (previous session status file not found)"
206
-
207
197
  try:
208
198
  with open(session_status_path, "r", encoding="utf-8") as f:
209
199
  session_data = json.load(f)
210
200
  except (json.JSONDecodeError, IOError):
211
201
  return "N/A (could not read previous session status)"
212
-
213
- status = session_data.get("status", "unknown")
214
- checkpoint = session_data.get("checkpoint_reached", "none")
215
- current_phase = session_data.get("current_phase", "unknown")
216
- errors = session_data.get("errors", [])
217
-
218
202
  result = "{} (checkpoint: {}, last phase: {})".format(
219
- status, checkpoint, current_phase
203
+ session_data.get("status", "unknown"),
204
+ session_data.get("checkpoint_reached", "none"),
205
+ session_data.get("current_phase", "unknown"),
220
206
  )
207
+ errors = session_data.get("errors", [])
221
208
  if errors:
222
209
  result += " — errors: {}".format("; ".join(str(e) for e in errors))
223
210
  return result
224
211
 
225
212
 
226
213
  def resolve_project_root(script_dir):
227
- """Resolve project root. Layout-aware:
228
- <project>/.prizmkit/dev-pipeline/scripts/ → <project>
229
- <repo>/dev-pipeline/scripts/ → <repo>
230
- """
214
+ """Resolve project root from script location."""
231
215
  pipeline_dir = os.path.dirname(script_dir)
232
216
  pipeline_parent = os.path.dirname(pipeline_dir)
233
217
  if os.path.basename(pipeline_parent) == ".prizmkit":
@@ -236,18 +220,34 @@ def resolve_project_root(script_dir):
236
220
 
237
221
 
238
222
  def _args_project_root(args, script_dir):
239
- """Return explicit AI-visible project root or script-layout default."""
240
223
  explicit = getattr(args, "project_root", None)
241
224
  if explicit:
242
225
  return os.path.abspath(explicit)
243
226
  return resolve_project_root(script_dir)
244
227
 
245
228
 
229
+ def determine_bugfix_mode(args, bug):
230
+ if args.mode:
231
+ return args.mode
232
+ return normalize_mode(
233
+ {"low": "lite", "medium": "lite", "high": "standard", "critical": "full"}.get(
234
+ str(bug.get("severity", "medium")).lower(), "lite"
235
+ )
236
+ )
237
+
238
+
239
+ def is_recovery_like(args):
240
+ retry_count = 0
241
+ try:
242
+ retry_count = int(getattr(args, "retry_count", 0))
243
+ except (TypeError, ValueError):
244
+ retry_count = 0
245
+ return is_continuation(args) or getattr(args, "resume_phase", "null") != "null" or retry_count > 0
246
+
247
+
246
248
  def build_replacements(args, bug, global_context, script_dir):
247
249
  """Build the full dict of placeholder -> replacement value."""
248
250
  project_root = _args_project_root(args, script_dir)
249
-
250
- # Platform-aware agent/team path resolution
251
251
  platform = resolve_prompt_platform(project_root)
252
252
  home_dir = os.path.expanduser("~")
253
253
 
@@ -259,221 +259,199 @@ def build_replacements(args, bug, global_context, script_dir):
259
259
  team_config_path = os.path.join(project_root, ".codex", "team-info.json")
260
260
  else:
261
261
  agents_dir = os.path.join(project_root, ".codebuddy", "agents")
262
- team_config_path = os.path.join(
263
- home_dir, ".codebuddy", "teams", "prizm-dev-team", "config.json"
264
- )
262
+ team_config_path = os.path.join(home_dir, ".codebuddy", "teams", "prizm-dev-team", "config.json")
265
263
 
266
264
  agent_ext = ".toml" if platform == "codex" else ".md"
267
- dev_subagent = os.path.join(agents_dir, f"prizm-dev-team-dev{agent_ext}")
268
- reviewer_subagent = os.path.join(agents_dir, f"prizm-dev-team-reviewer{agent_ext}")
265
+ dev_subagent = os.path.join(agents_dir, "prizm-dev-team-dev{}".format(agent_ext))
266
+ reviewer_subagent = os.path.join(agents_dir, "prizm-dev-team-reviewer{}".format(agent_ext))
267
+ critic_subagent = os.path.join(agents_dir, "prizm-dev-team-critic{}".format(agent_ext))
269
268
 
270
- # Session status is controlled by the main runner state dir, even when
271
- # rendered project paths are AI-visible worktree paths.
272
269
  state_dir = (
273
270
  os.path.abspath(args.state_dir)
274
271
  if args.state_dir
275
272
  else os.path.join(project_root, ".prizmkit", "state", "bugfix")
276
273
  )
277
- session_status_path = os.path.join(
278
- state_dir, args.bug_id, "sessions", args.session_id,
279
- "session-status.json",
280
- )
274
+ session_status_path = os.path.join(state_dir, args.bug_id, "sessions", args.session_id, "session-status.json")
281
275
 
282
- # Error source
283
276
  error_source = bug.get("error_source", {})
284
277
  error_type = error_source.get("type", "unknown") if isinstance(error_source, dict) else "unknown"
285
-
286
- # Determine fix scope from affected_modules or title
287
278
  affected_modules = bug.get("affected_modules", [])
288
- if affected_modules:
289
- fix_scope = affected_modules[0]
290
- else:
291
- fix_scope = bug.get("title", "unknown").split()[0].lower() if bug.get("title") else "unknown"
292
-
293
- # Determine verification type
294
- vtype = bug.get("verification_type", "automated")
295
-
296
- # Browser interaction - extract from bug if present
297
- browser_interaction = bug.get("browser_interaction")
298
- browser_enabled = False
299
- browser_verify_steps = ""
300
- browser_tool = "auto"
301
-
302
- # Environment override
303
- browser_verify_env = os.environ.get("BROWSER_VERIFY", "").lower()
304
- if browser_verify_env == "false":
305
- browser_interaction = None
306
-
307
- # Extract browser config (same logic as feature pipeline)
308
- if browser_interaction and isinstance(browser_interaction, bool):
309
- browser_enabled = True
310
- browser_tool = "auto"
311
- browser_verify_steps = " # (no specific verify goals reproduce the bug and verify it's fixed)"
312
- elif browser_interaction and isinstance(browser_interaction, dict):
313
- browser_tool = browser_interaction.get("tool", "auto")
314
- if browser_tool not in ("playwright-cli", "opencli", "auto"):
315
- LOGGER.warning("Unknown browser_interaction.tool '%s', defaulting to 'auto'", browser_tool)
316
- browser_tool = "auto"
317
-
318
- steps = browser_interaction.get("verify_steps", [])
319
- if steps:
320
- browser_enabled = True
321
- browser_verify_steps = "\n".join(
322
- " # Step {}: {}".format(i + 1, step)
323
- for i, step in enumerate(steps)
324
- )
325
- elif browser_interaction.get("url") or browser_interaction.get("enabled", True):
326
- browser_enabled = True
327
- browser_verify_steps = " # (reproduce bug and verify fix)"
279
+ fix_scope = affected_modules[0] if affected_modules else (bug.get("title", "unknown").split()[0].lower() if bug.get("title") else "unknown")
280
+ verification_type = bug.get("verification_type", "automated")
281
+
282
+ browser_enabled, browser_tool, browser_verify_steps, browser_blocking = browser_enabled_from_item(bug)
283
+ dev_port = detect_dev_server_port(project_root)
284
+ dev_url = "http://localhost:{}".format(dev_port) if dev_port.isdigit() else "<UNKNOWN_DETECT_FROM_PROJECT_CONFIG>"
285
+
286
+ pipeline_mode = determine_bugfix_mode(args, bug)
287
+ critic_requested = args.critic == "true" if args.critic is not None else bool(bug.get("critic", False))
288
+ critic_enabled = critic_requested and pipeline_mode != "lite"
289
+ if critic_requested and pipeline_mode == "lite":
290
+ LOGGER.warning("Critic requested for bug %s but lite mode skips critic phases", args.bug_id)
291
+ if critic_enabled and not os.path.isfile(critic_subagent):
292
+ LOGGER.warning("Critic enabled but agent file not found: %s. Critic phase will be skipped.", critic_subagent)
293
+ critic_enabled = False
294
+
295
+ recovery_like = is_recovery_like(args)
296
+ manual_or_hybrid = verification_type in ("manual", "hybrid")
297
+ manual_policy = (
298
+ "Recovery-class session: automated verification may substitute for manual/hybrid UAT when evidence is documented."
299
+ if manual_or_hybrid and recovery_like
300
+ else "Fresh manual/hybrid bugfix: stop as partial before commit after automated checks; manual UAT is required."
301
+ if manual_or_hybrid
302
+ else "Automated verification: complete tests/review and proceed to commit when gates pass."
303
+ )
328
304
 
329
305
  replacements = {
330
- "{{SESSION_LOG_PATH}}": os.path.join(
331
- ".prizmkit", "state", "bugfix", args.bug_id,
332
- "sessions", args.session_id, "logs", "session.log",
333
- ),
306
+ "{{SESSION_LOG_PATH}}": os.path.join(".prizmkit", "state", "bugfix", args.bug_id, "sessions", args.session_id, "logs", "session.log"),
334
307
  "{{RUN_ID}}": args.run_id,
335
308
  "{{SESSION_ID}}": args.session_id,
336
309
  "{{BUG_ID}}": args.bug_id,
310
+ "{{TASK_ID}}": args.bug_id,
311
+ "{{TASK_TITLE}}": bug.get("title", ""),
337
312
  "{{BUG_TITLE}}": bug.get("title", ""),
338
313
  "{{SEVERITY}}": bug.get("severity", "medium"),
339
- "{{VERIFICATION_TYPE}}": vtype,
314
+ "{{PIPELINE_MODE}}": pipeline_mode,
315
+ "{{CRITIC_ENABLED}}": "true" if critic_enabled else "false",
316
+ "{{CRITIC_COUNT}}": str(bug.get("critic_count", 1)),
317
+ "{{VERIFICATION_TYPE}}": verification_type,
318
+ "{{MANUAL_VERIFICATION_POLICY}}": manual_policy,
340
319
  "{{BUG_DESCRIPTION}}": bug.get("description", ""),
341
320
  "{{USER_CONTEXT}}": format_user_context(bug.get("user_context", [])),
342
321
  "{{ERROR_SOURCE_TYPE}}": error_type,
343
322
  "{{ERROR_SOURCE_DETAILS}}": format_error_source_details(error_source),
344
- "{{ACCEPTANCE_CRITERIA}}": format_acceptance_criteria(
345
- bug.get("acceptance_criteria", [])
346
- ),
323
+ "{{ACCEPTANCE_CRITERIA}}": format_acceptance_criteria(bug.get("acceptance_criteria", [])),
324
+ "{{AC_CHECKLIST}}": format_ac_checklist(bug.get("acceptance_criteria", [])),
347
325
  "{{ENVIRONMENT}}": format_environment(bug.get("environment")),
348
326
  "{{GLOBAL_CONTEXT}}": format_global_context(global_context, project_root),
349
327
  "{{TEAM_CONFIG_PATH}}": team_config_path,
350
328
  "{{DEV_SUBAGENT_PATH}}": dev_subagent,
351
329
  "{{REVIEWER_SUBAGENT_PATH}}": reviewer_subagent,
330
+ "{{CRITIC_SUBAGENT_PATH}}": critic_subagent,
352
331
  "{{SESSION_STATUS_PATH}}": session_status_path,
353
332
  "{{PROJECT_ROOT}}": project_root,
354
- "{{CHECKPOINT_PATH}}": os.path.join(
355
- ".prizmkit", "bugfix", args.bug_id, "workflow-checkpoint.json",
356
- ),
333
+ "{{ARTIFACT_DIR}}": ".prizmkit/bugfix/{}".format(args.bug_id),
334
+ "{{CHECKPOINT_PATH}}": os.path.join(".prizmkit", "bugfix", args.bug_id, "workflow-checkpoint.json"),
357
335
  "{{CHECKPOINT_SYSTEM}}": read_checkpoint_system_section(script_dir),
358
336
  "{{FIX_SCOPE}}": fix_scope,
359
- "{{TIMESTAMP}}": "", # Placeholder, agent fills in the timestamp
337
+ "{{TIMESTAMP}}": "",
360
338
  "{{BROWSER_ENABLED}}": "true" if browser_enabled else "false",
361
339
  "{{BROWSER_TOOL}}": browser_tool,
362
340
  "{{BROWSER_VERIFY_STEPS}}": browser_verify_steps,
341
+ "{{BROWSER_BLOCKING}}": "true" if browser_blocking else "false",
342
+ "{{DEV_PORT}}": dev_port,
343
+ "{{DEV_URL}}": dev_url,
363
344
  }
364
-
365
345
  replacements.update(helper_replacements(script_dir))
366
-
367
346
  return replacements
368
347
 
369
348
 
370
349
  def process_conditional_blocks(content, bug):
371
- """Handle conditional blocks based on verification_type and browser_interaction."""
372
- # Handle verification type blocks
350
+ """Handle legacy conditional blocks based on verification_type and browser_interaction."""
373
351
  vtype = bug.get("verification_type", "automated")
374
352
  is_manual_or_hybrid = vtype in ("manual", "hybrid")
375
-
376
353
  if is_manual_or_hybrid:
377
354
  content = content.replace("{{IF_VERIFICATION_MANUAL_OR_HYBRID}}\n", "")
378
355
  content = content.replace("{{IF_VERIFICATION_MANUAL_OR_HYBRID}}", "")
379
356
  content = content.replace("{{END_IF_VERIFICATION_MANUAL_OR_HYBRID}}\n", "")
380
357
  content = content.replace("{{END_IF_VERIFICATION_MANUAL_OR_HYBRID}}", "")
381
358
  else:
382
- # Remove the entire conditional block
383
- content = re.sub(
384
- r"\{\{IF_VERIFICATION_MANUAL_OR_HYBRID\}\}.*?\{\{END_IF_VERIFICATION_MANUAL_OR_HYBRID\}\}\n?",
385
- "", content, flags=re.DOTALL,
386
- )
359
+ content = re.sub(r"\{\{IF_VERIFICATION_MANUAL_OR_HYBRID\}\}.*?\{\{END_IF_VERIFICATION_MANUAL_OR_HYBRID\}\}\n?", "", content, flags=re.DOTALL)
387
360
 
388
- # Handle browser interaction blocks
389
- browser_interaction = bug.get("browser_interaction")
390
- browser_enabled = False
391
- browser_tool = "auto"
392
-
393
- # Check environment override
394
- browser_verify_env = os.environ.get("BROWSER_VERIFY", "").lower()
395
- if browser_verify_env == "false":
396
- browser_interaction = None
397
-
398
- # Determine if browser is enabled
399
- if browser_interaction:
400
- if isinstance(browser_interaction, bool):
401
- browser_enabled = True
402
- elif isinstance(browser_interaction, dict):
403
- steps = browser_interaction.get("verify_steps", [])
404
- if steps or browser_interaction.get("url") or browser_interaction.get("enabled", True):
405
- browser_enabled = True
406
- browser_tool = browser_interaction.get("tool", "auto")
407
-
408
- # Process browser interaction blocks
361
+ browser_enabled, browser_tool, _steps, _blocking = browser_enabled_from_item(bug)
409
362
  browser_open = "{{IF_BROWSER_INTERACTION}}"
410
363
  browser_close = "{{END_IF_BROWSER_INTERACTION}}"
411
-
412
364
  if browser_enabled:
413
- # Keep content, remove tags
414
- content = content.replace(browser_open + "\n", "")
415
- content = content.replace(browser_open, "")
416
- content = content.replace(browser_close + "\n", "")
417
- content = content.replace(browser_close, "")
365
+ content = content.replace(browser_open + "\n", "").replace(browser_open, "")
366
+ content = content.replace(browser_close + "\n", "").replace(browser_close, "")
418
367
  else:
419
- # Remove entire block
420
- pattern = re.escape(browser_open) + r".*?" + re.escape(browser_close) + r"\n?"
421
- content = re.sub(pattern, "", content, flags=re.DOTALL)
422
-
423
- # Process browser tool selection blocks (nested inside browser interaction)
424
- tool_variants = ["PLAYWRIGHT", "OPENCLI", "AUTO"]
425
- active_variant = {
426
- "playwright-cli": "PLAYWRIGHT",
427
- "opencli": "OPENCLI",
428
- "auto": "AUTO",
429
- }.get(browser_tool, "AUTO")
430
-
431
- for variant in tool_variants:
368
+ content = re.sub(re.escape(browser_open) + r".*?" + re.escape(browser_close) + r"\n?", "", content, flags=re.DOTALL)
369
+
370
+ active_variant = {"playwright-cli": "PLAYWRIGHT", "opencli": "OPENCLI", "auto": "AUTO"}.get(browser_tool, "AUTO")
371
+ for variant in ("PLAYWRIGHT", "OPENCLI", "AUTO"):
432
372
  tool_open = "{{{{IF_BROWSER_TOOL_{}}}}}".format(variant)
433
373
  tool_close = "{{{{END_IF_BROWSER_TOOL_{}}}}}".format(variant)
434
-
435
374
  if variant == active_variant and browser_enabled:
436
- # Keep content, remove tags
437
- content = content.replace(tool_open + "\n", "")
438
- content = content.replace(tool_open, "")
439
- content = content.replace(tool_close + "\n", "")
440
- content = content.replace(tool_close, "")
375
+ content = content.replace(tool_open + "\n", "").replace(tool_open, "")
376
+ content = content.replace(tool_close + "\n", "").replace(tool_close, "")
441
377
  else:
442
- # Remove entire block
443
- pat = re.escape(tool_open) + r".*?" + re.escape(tool_close) + r"\n?"
444
- content = re.sub(pat, "", content, flags=re.DOTALL)
445
-
378
+ content = re.sub(re.escape(tool_open) + r".*?" + re.escape(tool_close) + r"\n?", "", content, flags=re.DOTALL)
446
379
  return content
447
380
 
448
381
 
449
382
  def render_template(template_content, replacements, bug):
450
- """Render the template by processing conditionals and replacing placeholders."""
451
- # Step 1: Process conditional blocks
452
- content = process_conditional_blocks(template_content, bug)
453
-
454
- # Step 2: Replace all {{PLACEHOLDER}} variables (two passes for nested sections)
455
- for _pass in range(2):
456
- for placeholder, value in replacements.items():
457
- content = content.replace(placeholder, value)
458
-
459
- return content
383
+ """Render the legacy template by processing conditionals and placeholders."""
384
+ return replace_placeholders(process_conditional_blocks(template_content, bug), replacements)
385
+
386
+
387
+ def _bugfix_header(pipeline_mode):
388
+ title = "# Dev-Pipeline Bug Fix Session Bootstrap — {}\n".format(pipeline_mode.title())
389
+ desc = {
390
+ "lite": "**Tier 1 — Single Agent**: direct root-cause fix by the main orchestrator.",
391
+ "standard": "**Tier 2 — Orchestrator + optional Critic**: direct implementation with review gates.",
392
+ "full": "**Tier 3 — Full Bugfix Guardrails**: direct implementation with stronger diagnosis and critic checks.",
393
+ }.get(pipeline_mode, "**Bugfix pipeline**")
394
+ return title + "\n" + desc + "\n"
395
+
396
+
397
+ def assemble_bugfix_sections(pipeline_mode, sections_dir, critic_enabled, browser_enabled, manual_gate):
398
+ sections = [
399
+ ("header", _bugfix_header(pipeline_mode)),
400
+ ("bugfix-session-context", load_section(sections_dir, "bugfix-session-context.md")),
401
+ ("mission", load_section(sections_dir, "bugfix-mission.md")),
402
+ ("bugfix-task-contract", load_section(sections_dir, "bugfix-task-contract.md")),
403
+ ("context-budget-rules", load_section(sections_dir, "context-budget-rules.md")),
404
+ ("bugfix-artifacts", load_section(sections_dir, "bugfix-artifacts.md")),
405
+ ("checkpoint-system", load_section(sections_dir, "checkpoint-system.md")),
406
+ ("execution-header", "---\n\n## Execution\n"),
407
+ ("bugfix-phase-init", load_section(sections_dir, "bugfix-phase-init.md")),
408
+ ("bugfix-phase-diagnose-plan", load_section(sections_dir, "bugfix-phase-diagnose-plan.md")),
409
+ ]
410
+ if critic_enabled:
411
+ sections.append(("bugfix-phase-critic", load_section(sections_dir, "bugfix-phase-critic.md")))
412
+ sections.extend([
413
+ ("bugfix-phase-implement", load_section(sections_dir, "bugfix-phase-implement.md")),
414
+ ("bugfix-phase-review", load_section(sections_dir, "bugfix-phase-review.md")),
415
+ ])
416
+ if browser_enabled:
417
+ sections.append(("phase-browser", load_section(sections_dir, "phase-browser-verification-auto.md")))
418
+ if manual_gate:
419
+ sections.append(("bugfix-manual-gate", load_section(sections_dir, "bugfix-phase-manual-verification.md")))
420
+ sections.extend([
421
+ ("bugfix-phase-commit-report", load_section(sections_dir, "bugfix-phase-commit-report.md")),
422
+ ("bugfix-session-status", load_section(sections_dir, "bugfix-session-status.md")),
423
+ ("bugfix-critical-paths", load_section(sections_dir, "bugfix-critical-paths.md")),
424
+ ("failure-capture", load_section(sections_dir, "failure-capture.md")),
425
+ ("bugfix-reminders", load_section(sections_dir, "bugfix-reminders.md")),
426
+ ])
427
+ return sections
428
+
429
+
430
+ def generate_bugfix_checkpoint(bug_id, session_id, pipeline_mode="standard", sections=None):
431
+ """Generate a checkpoint definition for bugfix pipeline."""
432
+ if sections is None:
433
+ sections = [
434
+ ("bugfix-phase-init", ""),
435
+ ("bugfix-phase-diagnose-plan", ""),
436
+ ("bugfix-phase-implement", ""),
437
+ ("bugfix-phase-review", ""),
438
+ ("bugfix-phase-commit-report", ""),
439
+ ]
440
+ return generate_checkpoint_from_sections(
441
+ sections,
442
+ BUGFIX_SECTION_TO_SKILL,
443
+ pipeline_mode,
444
+ "bugfix-pipeline",
445
+ bug_id,
446
+ bug_id,
447
+ session_id,
448
+ split_steps=BUGFIX_SPLIT_STEPS,
449
+ )
460
450
 
461
451
 
462
- def write_output(output_path, content):
463
- """Write the rendered content to the output file."""
464
- abs_path = os.path.abspath(output_path)
465
- output_dir = os.path.dirname(abs_path)
466
- if output_dir and not os.path.isdir(output_dir):
467
- try:
468
- os.makedirs(output_dir, exist_ok=True)
469
- except OSError as e:
470
- return "Cannot create output directory: {}".format(str(e))
471
- try:
472
- with open(abs_path, "w", encoding="utf-8") as f:
473
- f.write(content)
474
- except IOError as e:
475
- return "Cannot write output file: {}".format(str(e))
476
- return None
452
+ def merge_bugfix_checkpoint_state(existing, fresh, project_root):
453
+ """Merge existing bugfix checkpoint state into fresh definition."""
454
+ return merge_checkpoint_state(existing, fresh, project_root, logger=LOGGER)
477
455
 
478
456
 
479
457
  def emit_failure(message):
@@ -482,168 +460,81 @@ def emit_failure(message):
482
460
  sys.exit(1)
483
461
 
484
462
 
485
- # ============================================================
486
- # Checkpoint generation for bugfix pipeline
487
- # ============================================================
488
-
489
- BUGFIX_STEPS = [
490
- ("prizmkit-init", "Initialize", []),
491
- ("bug-diagnosis-and-plan", "Diagnose & Plan",
492
- [".prizmkit/bugfix/{slug}/spec.md",
493
- ".prizmkit/bugfix/{slug}/plan.md"]),
494
- ("prizmkit-implement", "Implement Fix", []),
495
- ("prizmkit-code-review", "Code Review", []),
496
- ("prizmkit-committer", "Commit", ["--headless"]),
497
- ("bug-report", "Generate Fix Report",
498
- [".prizmkit/bugfix/{slug}/fix-report.md"]),
499
- ]
500
-
501
-
502
- def generate_bugfix_checkpoint(bug_id, session_id):
503
- """Generate a checkpoint definition for bugfix pipeline.
504
-
505
- Returns a dict suitable for writing as workflow-checkpoint.json.
506
- """
507
- steps = []
508
- prev_id = None
509
- for i, (skill, name, artifacts) in enumerate(BUGFIX_STEPS, 1):
510
- step_id = "S{:02d}".format(i)
511
- steps.append({
512
- "id": step_id,
513
- "skill": skill,
514
- "name": name,
515
- "status": "pending",
516
- "required_artifacts": [a.replace("{slug}", bug_id) for a in artifacts],
517
- "depends_on": prev_id,
518
- })
519
- prev_id = step_id
520
-
521
- return {
522
- "version": 1,
523
- "workflow_type": "bugfix-pipeline",
524
- "pipeline_mode": "single",
525
- "item_id": bug_id,
526
- "item_slug": bug_id,
527
- "session_id": session_id,
528
- "steps": steps,
529
- }
530
-
531
-
532
- def merge_bugfix_checkpoint_state(existing, fresh, project_root):
533
- """Merge existing bugfix checkpoint state into fresh definition.
534
-
535
- Same logic as feature pipeline: validate artifacts, break chain on
536
- first invalid step.
537
- """
538
- existing_status = {}
539
- existing_artifacts = {}
540
- for step in existing.get("steps", []):
541
- existing_status[step["skill"]] = step["status"]
542
- existing_artifacts[step["skill"]] = step.get("required_artifacts", [])
543
-
544
- valid_completed = set()
545
- for skill_key, status in existing_status.items():
546
- if status == "completed":
547
- artifacts = existing_artifacts.get(skill_key, [])
548
- if all(os.path.exists(os.path.join(project_root, a))
549
- for a in artifacts):
550
- valid_completed.add(skill_key)
551
- else:
552
- LOGGER.warning(
553
- "Step '%s' was completed but artifacts missing — "
554
- "resetting to pending", skill_key
555
- )
556
- elif status == "skipped":
557
- valid_completed.add(skill_key)
558
-
559
- chain_broken = False
560
- for step in fresh["steps"]:
561
- if chain_broken:
562
- step["status"] = "pending"
563
- continue
564
- prev = existing_status.get(step["skill"])
565
- if step["skill"] in valid_completed:
566
- step["status"] = prev
567
- else:
568
- chain_broken = True
569
- step["status"] = "pending"
570
-
571
- return fresh
572
-
573
-
574
463
  def main():
575
464
  args = parse_args()
576
-
577
- # Resolve script directory
578
465
  script_dir = os.path.dirname(os.path.abspath(__file__))
466
+ templates_dir = os.path.join(script_dir, "..", "templates")
467
+ sections_dir = os.path.join(templates_dir, "sections")
579
468
 
580
- # Resolve template path
581
- if args.template:
582
- template_path = args.template
583
- else:
584
- template_path = os.path.join(
585
- script_dir, "..", "templates", "bugfix-bootstrap-prompt.md"
586
- )
587
-
588
- # Load template
589
- template_content, err = read_text_file(template_path)
590
- if err:
591
- emit_failure("Template error: {}".format(err))
592
-
593
- # Load bug fix list
594
469
  bug_list_data, err = load_json_file(args.bug_list)
595
470
  if err:
596
471
  emit_failure("Bug list error: {}".format(err))
597
-
598
- # Extract bugs array
599
472
  bugs = bug_list_data.get("bugs")
600
473
  if not isinstance(bugs, list):
601
474
  emit_failure("Bug fix list does not contain a 'bugs' array")
602
-
603
- # Find the target bug
604
475
  bug = find_bug(bugs, args.bug_id)
605
476
  if bug is None:
606
477
  emit_failure("Bug '{}' not found in bug fix list".format(args.bug_id))
607
-
608
- # Extract global context
609
478
  global_context = bug_list_data.get("global_context", {})
610
479
  if not isinstance(global_context, dict):
611
480
  global_context = {}
612
481
 
613
- # Build replacements
614
482
  replacements = build_replacements(args, bug, global_context, script_dir)
615
- # Render the template
616
- rendered = render_template(template_content, replacements, bug)
483
+ pipeline_mode = replacements["{{PIPELINE_MODE}}"]
484
+ critic_enabled = replacements["{{CRITIC_ENABLED}}"] == "true"
485
+ browser_enabled = replacements["{{BROWSER_ENABLED}}"] == "true"
486
+ manual_gate = bug.get("verification_type", "automated") in ("manual", "hybrid") and not is_recovery_like(args)
487
+
488
+ use_sections = os.path.isdir(sections_dir) and not args.template
489
+ sections = []
490
+ if use_sections:
491
+ try:
492
+ sections = assemble_bugfix_sections(
493
+ pipeline_mode, sections_dir, critic_enabled, browser_enabled, manual_gate,
494
+ )
495
+ rendered = render_from_sections(sections, replacements)
496
+ except FileNotFoundError as exc:
497
+ LOGGER.warning("Bugfix section assembly failed (%s), falling back to legacy template", exc)
498
+ use_sections = False
499
+
500
+ if not use_sections:
501
+ template_path = args.template or os.path.join(templates_dir, "bugfix-bootstrap-prompt.md")
502
+ template_content, err = read_text_file(template_path)
503
+ if err:
504
+ emit_failure("Template error: {}".format(err))
505
+ rendered = render_template(template_content, replacements, bug)
506
+
507
+ is_valid, warnings, errors = validate_rendered(
508
+ rendered,
509
+ required_markers=[
510
+ ("## Your Mission", "Mission section"),
511
+ ("## Execution", "Execution section"),
512
+ ("## Failure Capture", "Failure Capture Protocol"),
513
+ ],
514
+ logger=LOGGER,
515
+ )
516
+ if not is_valid:
517
+ LOGGER.error("Rendered bugfix prompt failed validation: %s", "; ".join(errors))
617
518
 
618
- # Write the output
619
519
  err = write_output(args.output, rendered)
620
520
  if err:
621
521
  emit_failure(err)
622
522
 
623
- # Generate checkpoint file
624
523
  project_root = _args_project_root(args, script_dir)
625
- checkpoint_rel = replacements["{{CHECKPOINT_PATH}}"]
626
- checkpoint_path = os.path.join(project_root, checkpoint_rel)
627
- checkpoint_dir = os.path.dirname(checkpoint_path)
628
- os.makedirs(checkpoint_dir, exist_ok=True)
629
-
630
- checkpoint = generate_bugfix_checkpoint(args.bug_id, args.session_id)
524
+ checkpoint_path = os.path.join(project_root, replacements["{{CHECKPOINT_PATH}}"])
525
+ os.makedirs(os.path.dirname(checkpoint_path), exist_ok=True)
526
+ checkpoint = generate_bugfix_checkpoint(
527
+ args.bug_id, args.session_id, pipeline_mode, sections if use_sections else None,
528
+ )
631
529
 
632
- is_resume = args.resume_phase != "null"
633
- if (is_resume or is_continuation(args)) and os.path.exists(checkpoint_path):
530
+ if (args.resume_phase != "null" or is_continuation(args)) and os.path.exists(checkpoint_path):
634
531
  try:
635
532
  with open(checkpoint_path, "r", encoding="utf-8") as f:
636
533
  existing = json.load(f)
637
- checkpoint = merge_bugfix_checkpoint_state(
638
- existing, checkpoint, project_root,
639
- )
640
- LOGGER.info("Merged existing bugfix checkpoint from %s",
641
- checkpoint_path)
534
+ checkpoint = merge_bugfix_checkpoint_state(existing, checkpoint, project_root)
535
+ LOGGER.info("Merged existing bugfix checkpoint from %s", checkpoint_path)
642
536
  except (json.JSONDecodeError, KeyError) as exc:
643
- LOGGER.warning(
644
- "Existing bugfix checkpoint corrupted (%s) — generating fresh",
645
- exc,
646
- )
537
+ LOGGER.warning("Existing bugfix checkpoint corrupted (%s) — generating fresh", exc)
647
538
 
648
539
  with open(checkpoint_path, "w", encoding="utf-8") as f:
649
540
  json.dump(checkpoint, f, indent=2, ensure_ascii=False)
@@ -651,55 +542,26 @@ def main():
651
542
 
652
543
  if is_continuation(args):
653
544
  rendered = append_continuation_handoff(
654
- rendered, args, project_root, args.task_type, args.bug_id,
655
- args.bug_id, checkpoint,
545
+ rendered, args, project_root, args.task_type, args.bug_id, args.bug_id, checkpoint,
656
546
  )
657
547
  err = write_output(args.output, rendered)
658
548
  if err:
659
549
  emit_failure(err)
660
550
 
661
- # Resolve critic and mode
662
- bug_critic = bug.get("critic", False)
663
- if args.critic is not None:
664
- critic_enabled = str(args.critic).lower() == "true"
665
- else:
666
- critic_enabled = bool(bug_critic)
667
-
668
- pipeline_mode = args.mode or "standard"
669
- agent_count = 5 if critic_enabled else 3
670
-
671
- # Success
672
- bug_model = bug.get("model", "")
673
- # Extract browser interaction state for output
674
- browser_interaction = bug.get("browser_interaction")
675
- browser_enabled = False
676
- browser_tool = "auto"
677
-
678
- browser_verify_env = os.environ.get("BROWSER_VERIFY", "").lower()
679
- if browser_verify_env == "false":
680
- browser_interaction = None
681
-
682
- if browser_interaction:
683
- if isinstance(browser_interaction, bool):
684
- browser_enabled = True
685
- elif isinstance(browser_interaction, dict):
686
- steps = browser_interaction.get("verify_steps", [])
687
- if steps or browser_interaction.get("url") or browser_interaction.get("enabled", True):
688
- browser_enabled = True
689
- browser_tool = browser_interaction.get("tool", "auto")
690
- if browser_tool not in ("playwright-cli", "opencli", "auto"):
691
- browser_tool = "auto"
692
-
551
+ critic_count = int(replacements.get("{{CRITIC_COUNT}}", "1"))
693
552
  output = {
694
553
  "success": True,
695
554
  "output_path": os.path.abspath(args.output),
696
555
  "checkpoint_path": checkpoint_path,
697
- "model": bug_model,
556
+ "model": bug.get("model", ""),
698
557
  "pipeline_mode": pipeline_mode,
699
- "agent_count": agent_count,
558
+ "agent_count": mode_agent_count(pipeline_mode, critic_enabled, critic_count),
700
559
  "critic_enabled": critic_enabled,
701
- "browser_enabled": browser_enabled,
702
- "browser_tool": browser_tool
560
+ "render_mode": "sections" if use_sections else "legacy",
561
+ "validation_warnings": len(warnings),
562
+ "validation_errors": len(errors),
563
+ "browser_enabled": replacements["{{BROWSER_ENABLED}}"] == "true",
564
+ "browser_tool": replacements["{{BROWSER_TOOL}}"],
703
565
  }
704
566
  print(json.dumps(output, indent=2, ensure_ascii=False))
705
567
  sys.exit(0)