prizmkit 1.1.111 → 1.1.113
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/codex/skill-adapter.js +4 -0
- package/bundled/agents/prizm-dev-team-reviewer.md +12 -1
- package/bundled/dev-pipeline/SCHEMA_ANALYSIS.md +10 -10
- package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +1 -0
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +20 -73
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +290 -421
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +1 -1
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +259 -481
- package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +34 -0
- package/bundled/dev-pipeline/scripts/init-pipeline.py +12 -0
- package/bundled/dev-pipeline/scripts/init-refactor-pipeline.py +24 -1
- package/bundled/dev-pipeline/scripts/prompt_framework.py +312 -0
- package/bundled/dev-pipeline/scripts/update-bug-status.py +87 -20
- package/bundled/dev-pipeline/scripts/update-feature-status.py +82 -16
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +68 -28
- package/bundled/dev-pipeline/scripts/utils.py +171 -0
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +7 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +17 -19
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +21 -26
- package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +9 -1
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +11 -8
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +122 -182
- package/bundled/dev-pipeline/templates/sections/bugfix-artifacts.md +16 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-critical-paths.md +12 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-mission.md +9 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-commit-report.md +31 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-critic.md +22 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +23 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-implement.md +20 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-init.md +9 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-manual-verification.md +15 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +29 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-reminders.md +11 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-session-context.md +7 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-session-status.md +31 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-task-contract.md +40 -0
- package/bundled/dev-pipeline/templates/sections/failure-capture.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification-auto.md +8 -2
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification-opencli.md +7 -1
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification.md +7 -1
- package/bundled/dev-pipeline/templates/sections/refactor-artifacts.md +17 -0
- package/bundled/dev-pipeline/templates/sections/refactor-critical-paths.md +13 -0
- package/bundled/dev-pipeline/templates/sections/refactor-mission.md +9 -0
- package/bundled/dev-pipeline/templates/sections/refactor-phase-commit-report.md +37 -0
- package/bundled/dev-pipeline/templates/sections/refactor-phase-critic.md +22 -0
- package/bundled/dev-pipeline/templates/sections/refactor-phase-implement.md +16 -0
- package/bundled/dev-pipeline/templates/sections/refactor-phase-init.md +9 -0
- package/bundled/dev-pipeline/templates/sections/refactor-phase-plan.md +22 -0
- package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +19 -0
- package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +11 -0
- package/bundled/dev-pipeline/templates/sections/refactor-session-context.md +7 -0
- package/bundled/dev-pipeline/templates/sections/refactor-session-status.md +28 -0
- package/bundled/dev-pipeline/templates/sections/refactor-task-contract.md +52 -0
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +104 -26
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +14 -0
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +239 -0
- package/bundled/dev-pipeline/tests/test_unified_cli.py +6 -2
- package/bundled/dev-pipeline/tests/test_utils.py +66 -1
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/app-planner/SKILL.md +8 -7
- package/bundled/skills/app-planner/references/project-brief-guide.md +2 -2
- package/bundled/skills/app-planner/references/rules/backend/derivation-rules.md +1 -1
- package/bundled/skills/app-planner/references/rules/frontend/derivation-rules.md +1 -1
- package/bundled/skills/app-planner/references/rules-configuration.md +53 -26
- package/bundled/skills/bug-fix-workflow/SKILL.md +191 -336
- package/bundled/skills/bug-planner/SKILL.md +6 -5
- package/bundled/skills/bug-planner/references/critic-and-verification.md +3 -3
- package/bundled/skills/bug-planner/references/schema-validation.md +2 -1
- package/bundled/skills/bug-planner/scripts/validate-bug-list.py +70 -1
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +21 -21
- package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +2 -2
- package/bundled/skills/feature-pipeline-launcher/SKILL.md +25 -26
- package/bundled/skills/feature-planner/SKILL.md +9 -11
- package/bundled/skills/feature-planner/assets/planning-guide.md +1 -1
- package/bundled/skills/feature-planner/references/decomposition-patterns.md +1 -1
- package/bundled/skills/feature-planner/references/error-recovery.md +2 -1
- package/bundled/skills/feature-planner/references/incremental-feature-planning.md +2 -2
- package/bundled/skills/feature-planner/references/new-project-planning.md +35 -39
- package/bundled/skills/feature-planner/scripts/validate-and-generate.py +1 -1
- package/bundled/skills/feature-workflow/SKILL.md +170 -298
- package/bundled/skills/prizmkit/SKILL.md +103 -57
- package/bundled/skills/prizmkit-code-review/SKILL.md +97 -116
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +1 -0
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +14 -3
- package/bundled/skills/prizmkit-committer/SKILL.md +59 -47
- package/bundled/skills/prizmkit-deploy/SKILL.md +162 -381
- package/bundled/skills/prizmkit-deploy/references/database-setup.md +12 -3
- package/bundled/skills/prizmkit-deploy/references/ssh-adapter-flow.md +220 -0
- package/bundled/skills/prizmkit-implement/SKILL.md +51 -40
- package/bundled/skills/prizmkit-init/SKILL.md +4 -3
- package/bundled/skills/prizmkit-plan/SKILL.md +85 -70
- package/bundled/skills/prizmkit-prizm-docs/SKILL.md +94 -73
- package/bundled/skills/prizmkit-prizm-docs/assets/prizm-docs-format.md +20 -18
- package/bundled/skills/prizmkit-prizm-docs/references/op-init.md +11 -13
- package/bundled/skills/prizmkit-prizm-docs/references/op-update.md +18 -14
- package/bundled/skills/prizmkit-retrospective/SKILL.md +56 -48
- package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +79 -27
- package/bundled/skills/prizmkit-test/SKILL.md +138 -141
- package/bundled/skills/prizmkit-test/references/boundary-coverage-protocol.md +1 -1
- package/bundled/skills/prizmkit-test/references/examples.md +11 -9
- package/bundled/skills/prizmkit-test/references/test-generation-steps.md +1 -1
- package/bundled/skills/prizmkit-test/references/test-report-template.md +2 -2
- package/bundled/skills/recovery-workflow/SKILL.md +195 -256
- package/bundled/skills/recovery-workflow/evals/evals.json +21 -13
- package/bundled/skills/recovery-workflow/references/detection.md +48 -39
- package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +258 -322
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +32 -13
- package/bundled/skills/refactor-planner/SKILL.md +2 -2
- package/bundled/skills/refactor-planner/references/behavior-preservation.md +24 -19
- package/bundled/skills/refactor-planner/references/fast-path.md +2 -4
- package/bundled/skills/refactor-planner/references/planning-phases.md +2 -2
- package/bundled/skills/refactor-workflow/SKILL.md +174 -307
- package/package.json +1 -1
- package/src/scaffold.js +5 -0
- package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +0 -6
- package/bundled/skills/feature-workflow/references/brainstorm-guide.md +0 -137
- package/bundled/skills/refactor-workflow/references/brainstorm-guide.md +0 -116
|
@@ -15,6 +15,8 @@ import re
|
|
|
15
15
|
import sys
|
|
16
16
|
from datetime import datetime, timezone
|
|
17
17
|
|
|
18
|
+
from utils import stable_topological_order
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
EXPECTED_SCHEMA = "dev-pipeline-bug-fix-list-v1"
|
|
20
22
|
BUG_ID_PATTERN = re.compile(r"^B-\d{3}$")
|
|
@@ -198,6 +200,38 @@ def validate_bugs(bugs):
|
|
|
198
200
|
)
|
|
199
201
|
)
|
|
200
202
|
|
|
203
|
+
# Validate dependencies
|
|
204
|
+
deps = bug.get("dependencies")
|
|
205
|
+
if deps is not None and not isinstance(deps, list):
|
|
206
|
+
errors.append(
|
|
207
|
+
"Bug '{}' dependencies must be an array".format(
|
|
208
|
+
bid or "index {}".format(i)
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
elif isinstance(deps, list):
|
|
212
|
+
for dep in deps:
|
|
213
|
+
if not isinstance(dep, str) or not BUG_ID_PATTERN.match(dep):
|
|
214
|
+
errors.append(
|
|
215
|
+
"Bug '{}' has invalid dependency '{}' "
|
|
216
|
+
"(must match B-NNN pattern)".format(
|
|
217
|
+
bid or "index {}".format(i), dep
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
elif dep == bid:
|
|
221
|
+
errors.append(
|
|
222
|
+
"Bug '{}' cannot depend on itself".format(
|
|
223
|
+
bid or "index {}".format(i)
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
topo_errors = stable_topological_order(
|
|
228
|
+
bugs,
|
|
229
|
+
id_key="id",
|
|
230
|
+
dependencies_key="dependencies",
|
|
231
|
+
item_label="Bug",
|
|
232
|
+
)[1]
|
|
233
|
+
errors.extend(topo_errors)
|
|
234
|
+
|
|
201
235
|
return errors, seen_ids
|
|
202
236
|
|
|
203
237
|
|
|
@@ -16,6 +16,8 @@ import sys
|
|
|
16
16
|
from collections import deque
|
|
17
17
|
from datetime import datetime, timezone
|
|
18
18
|
|
|
19
|
+
from utils import stable_topological_order
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
EXPECTED_SCHEMA = "dev-pipeline-feature-list-v1"
|
|
21
23
|
FEATURE_ID_PATTERN = re.compile(r"^F-\d{3}(-[A-Z])?$")
|
|
@@ -168,6 +170,16 @@ def validate_features(features):
|
|
|
168
170
|
errors.append(
|
|
169
171
|
"Feature '{}' depends on unknown feature '{}'".format(fid, dep)
|
|
170
172
|
)
|
|
173
|
+
elif dep == fid:
|
|
174
|
+
errors.append("Feature '{}' cannot depend on itself".format(fid))
|
|
175
|
+
|
|
176
|
+
topo_errors = stable_topological_order(
|
|
177
|
+
features,
|
|
178
|
+
id_key="id",
|
|
179
|
+
dependencies_key="dependencies",
|
|
180
|
+
item_label="Feature",
|
|
181
|
+
)[1]
|
|
182
|
+
errors.extend(topo_errors)
|
|
171
183
|
|
|
172
184
|
return errors, feature_ids
|
|
173
185
|
|
|
@@ -16,7 +16,7 @@ import re
|
|
|
16
16
|
import sys
|
|
17
17
|
from datetime import datetime, timezone
|
|
18
18
|
|
|
19
|
-
from utils import load_json_file
|
|
19
|
+
from utils import load_json_file, stable_topological_order
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
EXPECTED_SCHEMA = "dev-pipeline-refactor-list-v1"
|
|
@@ -215,6 +215,29 @@ def validate_refactors(refactors):
|
|
|
215
215
|
rid or "index {}".format(i)
|
|
216
216
|
)
|
|
217
217
|
)
|
|
218
|
+
elif isinstance(deps, list):
|
|
219
|
+
for dep in deps:
|
|
220
|
+
if not isinstance(dep, str) or not REFACTOR_ID_PATTERN.match(dep):
|
|
221
|
+
errors.append(
|
|
222
|
+
"Refactor '{}' has invalid dependency '{}' "
|
|
223
|
+
"(must match R-NNN pattern)".format(
|
|
224
|
+
rid or "index {}".format(i), dep
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
elif dep == rid:
|
|
228
|
+
errors.append(
|
|
229
|
+
"Refactor '{}' cannot depend on itself".format(
|
|
230
|
+
rid or "index {}".format(i)
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
topo_errors = stable_topological_order(
|
|
235
|
+
refactors,
|
|
236
|
+
id_key="id",
|
|
237
|
+
dependencies_key="dependencies",
|
|
238
|
+
item_label="Refactor",
|
|
239
|
+
)[1]
|
|
240
|
+
errors.extend(topo_errors)
|
|
218
241
|
|
|
219
242
|
return errors, seen_ids
|
|
220
243
|
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""Shared prompt rendering and checkpoint helpers for dev-pipeline generators.
|
|
2
|
+
|
|
3
|
+
This module intentionally stays small: prompt phase ordering remains explicit in
|
|
4
|
+
family-specific generators, while common rendering/checkpoint mechanics live here
|
|
5
|
+
so feature, bugfix, and refactor prompts do not drift.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import glob
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def read_text_file(path):
|
|
14
|
+
"""Read and return the text content of a file, plus an error string."""
|
|
15
|
+
abs_path = os.path.abspath(path)
|
|
16
|
+
if not os.path.isfile(abs_path):
|
|
17
|
+
return None, "File not found: {}".format(abs_path)
|
|
18
|
+
try:
|
|
19
|
+
with open(abs_path, "r", encoding="utf-8") as f:
|
|
20
|
+
return f.read(), None
|
|
21
|
+
except IOError as exc:
|
|
22
|
+
return None, "Cannot read file: {}".format(str(exc))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_section(sections_dir, name):
|
|
26
|
+
"""Load a section file from the sections directory."""
|
|
27
|
+
path = os.path.join(sections_dir, name)
|
|
28
|
+
if not os.path.isfile(path):
|
|
29
|
+
raise FileNotFoundError("Section file not found: {}".format(path))
|
|
30
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
31
|
+
return f.read()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def render_from_sections(sections, replacements):
|
|
35
|
+
"""Join assembled sections and replace placeholders with two passes."""
|
|
36
|
+
content = "\n".join(text for _, text in sections if text is not None)
|
|
37
|
+
for _pass in range(2):
|
|
38
|
+
for placeholder, value in replacements.items():
|
|
39
|
+
content = content.replace(placeholder, str(value))
|
|
40
|
+
return content
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def replace_placeholders(content, replacements):
|
|
44
|
+
"""Replace placeholders in arbitrary content with two passes."""
|
|
45
|
+
for _pass in range(2):
|
|
46
|
+
for placeholder, value in replacements.items():
|
|
47
|
+
content = content.replace(placeholder, str(value))
|
|
48
|
+
return content
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def validate_rendered(content, required_markers=None, logger=None):
|
|
52
|
+
"""Validate rendered prompt content for placeholder and section drift."""
|
|
53
|
+
warnings = []
|
|
54
|
+
errors = []
|
|
55
|
+
|
|
56
|
+
unreplaced = re.findall(r"\{\{[A-Z][A-Z_0-9]+\}\}", content)
|
|
57
|
+
if unreplaced:
|
|
58
|
+
warnings.append(
|
|
59
|
+
"Unreplaced placeholders: {}".format(", ".join(sorted(set(unreplaced))))
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
unclosed_if = re.findall(r"\{\{(?:IF|END_IF)_[A-Z_]+\}\}", content)
|
|
63
|
+
if unclosed_if:
|
|
64
|
+
errors.append(
|
|
65
|
+
"Unclosed conditional blocks: {}".format(", ".join(sorted(set(unclosed_if))))
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
for marker, label in required_markers or []:
|
|
69
|
+
if marker not in content:
|
|
70
|
+
errors.append("Missing required section: {}".format(label))
|
|
71
|
+
|
|
72
|
+
if logger is not None:
|
|
73
|
+
for warning in warnings:
|
|
74
|
+
logger.warning("VALIDATE: %s", warning)
|
|
75
|
+
for error in errors:
|
|
76
|
+
logger.error("VALIDATE: %s", error)
|
|
77
|
+
|
|
78
|
+
return len(errors) == 0, warnings, errors
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def write_output(output_path, content):
|
|
82
|
+
"""Write rendered prompt content to output_path."""
|
|
83
|
+
abs_path = os.path.abspath(output_path)
|
|
84
|
+
output_dir = os.path.dirname(abs_path)
|
|
85
|
+
if output_dir and not os.path.isdir(output_dir):
|
|
86
|
+
try:
|
|
87
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
88
|
+
except OSError as exc:
|
|
89
|
+
return "Cannot create output directory: {}".format(str(exc))
|
|
90
|
+
try:
|
|
91
|
+
with open(abs_path, "w", encoding="utf-8") as f:
|
|
92
|
+
f.write(content)
|
|
93
|
+
except IOError as exc:
|
|
94
|
+
return "Cannot write output file: {}".format(str(exc))
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def resolve_artifacts(artifact_templates, slug):
|
|
99
|
+
"""Replace {slug} placeholders in checkpoint artifact templates."""
|
|
100
|
+
return [artifact.replace("{slug}", slug) for artifact in artifact_templates]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def artifact_exists(project_root, artifact):
|
|
104
|
+
"""Return whether an artifact path or glob exists under project_root."""
|
|
105
|
+
full_path = os.path.join(project_root, artifact)
|
|
106
|
+
if glob.has_magic(full_path):
|
|
107
|
+
return bool(glob.glob(full_path))
|
|
108
|
+
return os.path.exists(full_path)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def completed_step_artifacts_valid(project_root, artifacts):
|
|
112
|
+
"""Return whether generic completed-step artifacts are present."""
|
|
113
|
+
return all(artifact_exists(project_root, artifact) for artifact in artifacts)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def generate_checkpoint_from_sections(
|
|
117
|
+
sections,
|
|
118
|
+
step_mapping,
|
|
119
|
+
pipeline_mode,
|
|
120
|
+
workflow_type,
|
|
121
|
+
item_id,
|
|
122
|
+
item_slug,
|
|
123
|
+
session_id,
|
|
124
|
+
*,
|
|
125
|
+
init_done=False,
|
|
126
|
+
split_steps=None,
|
|
127
|
+
):
|
|
128
|
+
"""Generate a linear checkpoint definition from assembled prompt sections."""
|
|
129
|
+
steps = []
|
|
130
|
+
step_counter = 1
|
|
131
|
+
prev_step_id = None
|
|
132
|
+
split_steps = split_steps or {}
|
|
133
|
+
|
|
134
|
+
for section_name, _content in sections:
|
|
135
|
+
if section_name not in step_mapping:
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
mapping = step_mapping[section_name]
|
|
139
|
+
if mapping is None:
|
|
140
|
+
for skill, name, artifacts in split_steps.get(section_name, []):
|
|
141
|
+
step_id = "S{:02d}".format(step_counter)
|
|
142
|
+
steps.append({
|
|
143
|
+
"id": step_id,
|
|
144
|
+
"skill": skill,
|
|
145
|
+
"name": name,
|
|
146
|
+
"status": "pending",
|
|
147
|
+
"required_artifacts": resolve_artifacts(artifacts, item_slug),
|
|
148
|
+
"depends_on": prev_step_id,
|
|
149
|
+
})
|
|
150
|
+
prev_step_id = step_id
|
|
151
|
+
step_counter += 1
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
skill, name, artifacts = mapping
|
|
155
|
+
step_id = "S{:02d}".format(step_counter)
|
|
156
|
+
status = "pending"
|
|
157
|
+
if init_done and section_name in ("phase0-init", "phase0-test-baseline"):
|
|
158
|
+
status = "skipped"
|
|
159
|
+
|
|
160
|
+
steps.append({
|
|
161
|
+
"id": step_id,
|
|
162
|
+
"skill": skill,
|
|
163
|
+
"name": name,
|
|
164
|
+
"status": status,
|
|
165
|
+
"required_artifacts": resolve_artifacts(artifacts, item_slug),
|
|
166
|
+
"depends_on": prev_step_id,
|
|
167
|
+
})
|
|
168
|
+
prev_step_id = step_id
|
|
169
|
+
step_counter += 1
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
"version": 1,
|
|
173
|
+
"workflow_type": workflow_type,
|
|
174
|
+
"pipeline_mode": pipeline_mode,
|
|
175
|
+
"item_id": item_id,
|
|
176
|
+
"item_slug": item_slug,
|
|
177
|
+
"session_id": session_id,
|
|
178
|
+
"steps": steps,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def merge_checkpoint_state(existing, fresh, project_root, validator=None, logger=None):
|
|
183
|
+
"""Merge existing checkpoint state into a freshly generated definition."""
|
|
184
|
+
validator = validator or (lambda _skill, artifacts: completed_step_artifacts_valid(project_root, artifacts))
|
|
185
|
+
existing_status = {}
|
|
186
|
+
existing_artifacts = {}
|
|
187
|
+
fresh_artifacts = {
|
|
188
|
+
step["skill"]: step.get("required_artifacts", [])
|
|
189
|
+
for step in fresh.get("steps", [])
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for step in existing.get("steps", []):
|
|
193
|
+
existing_status[step["skill"]] = step.get("status", "pending")
|
|
194
|
+
existing_artifacts[step["skill"]] = step.get("required_artifacts", [])
|
|
195
|
+
|
|
196
|
+
valid_completed = set()
|
|
197
|
+
for skill_key, status in existing_status.items():
|
|
198
|
+
if status == "completed":
|
|
199
|
+
artifacts = fresh_artifacts.get(skill_key, existing_artifacts.get(skill_key, []))
|
|
200
|
+
if validator(skill_key, artifacts):
|
|
201
|
+
valid_completed.add(skill_key)
|
|
202
|
+
elif logger is not None:
|
|
203
|
+
logger.warning(
|
|
204
|
+
"Step '%s' was completed but artifacts missing — resetting to pending",
|
|
205
|
+
skill_key,
|
|
206
|
+
)
|
|
207
|
+
elif status == "skipped":
|
|
208
|
+
valid_completed.add(skill_key)
|
|
209
|
+
|
|
210
|
+
chain_broken = False
|
|
211
|
+
for step in fresh.get("steps", []):
|
|
212
|
+
if chain_broken:
|
|
213
|
+
step["status"] = "pending"
|
|
214
|
+
continue
|
|
215
|
+
previous = existing_status.get(step["skill"])
|
|
216
|
+
if step["skill"] in valid_completed:
|
|
217
|
+
step["status"] = previous
|
|
218
|
+
else:
|
|
219
|
+
chain_broken = True
|
|
220
|
+
step["status"] = "pending"
|
|
221
|
+
|
|
222
|
+
return fresh
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def determine_mode_from_complexity(value):
|
|
226
|
+
"""Map a coarse complexity/severity value to lite/standard/full."""
|
|
227
|
+
mapping = {
|
|
228
|
+
"low": "lite",
|
|
229
|
+
"medium": "lite",
|
|
230
|
+
"minor": "lite",
|
|
231
|
+
"high": "standard",
|
|
232
|
+
"major": "standard",
|
|
233
|
+
"critical": "full",
|
|
234
|
+
}
|
|
235
|
+
return mapping.get(str(value or "").lower(), "lite")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def normalize_mode(value, default="lite"):
|
|
239
|
+
"""Normalize a mode string to lite/standard/full."""
|
|
240
|
+
value = str(value or "").lower()
|
|
241
|
+
return value if value in {"lite", "standard", "full"} else default
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def mode_agent_count(pipeline_mode, critic_enabled=False, critic_count=1):
|
|
245
|
+
"""Return the prompt metadata agent count for a mode."""
|
|
246
|
+
base = {"lite": 1, "standard": 3, "full": 3}.get(pipeline_mode, 1)
|
|
247
|
+
return base + (int(critic_count or 1) if critic_enabled else 0)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def detect_dev_server_port(project_root):
|
|
251
|
+
"""Return a detected dev-server port string, or an unknown placeholder."""
|
|
252
|
+
unknown = "<UNKNOWN_DETECT_FROM_PROJECT_CONFIG>"
|
|
253
|
+
pkg_path = os.path.join(project_root, "package.json")
|
|
254
|
+
try:
|
|
255
|
+
if os.path.isfile(pkg_path):
|
|
256
|
+
import json
|
|
257
|
+
with open(pkg_path, "r", encoding="utf-8") as f:
|
|
258
|
+
pkg = json.load(f)
|
|
259
|
+
scripts = pkg.get("scripts", {}) if isinstance(pkg, dict) else {}
|
|
260
|
+
dev_script = scripts.get("dev", "")
|
|
261
|
+
port_match = re.search(r"(?:-p|--port)\s+([0-9]+)", dev_script)
|
|
262
|
+
if port_match:
|
|
263
|
+
return port_match.group(1)
|
|
264
|
+
for env_file in [".env.local", ".env"]:
|
|
265
|
+
env_path = os.path.join(project_root, env_file)
|
|
266
|
+
if not os.path.isfile(env_path):
|
|
267
|
+
continue
|
|
268
|
+
with open(env_path, "r", encoding="utf-8") as env:
|
|
269
|
+
for line in env:
|
|
270
|
+
match = re.match(r"[A-Z0-9_]*(?:URL|ORIGIN|HOST)\s*=\s*.*?:([0-9]+)", line.strip())
|
|
271
|
+
if match:
|
|
272
|
+
return match.group(1)
|
|
273
|
+
except Exception:
|
|
274
|
+
return unknown
|
|
275
|
+
return unknown
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def browser_enabled_from_item(item):
|
|
279
|
+
"""Return browser enabled/tool/steps with mandatory-attempt defaults."""
|
|
280
|
+
browser_interaction = item.get("browser_interaction") if isinstance(item, dict) else None
|
|
281
|
+
env_value = os.environ.get("BROWSER_VERIFY", "").lower()
|
|
282
|
+
if env_value == "false":
|
|
283
|
+
return False, "auto", "", False
|
|
284
|
+
|
|
285
|
+
enabled = True
|
|
286
|
+
tool = "auto"
|
|
287
|
+
blocking = False
|
|
288
|
+
steps = []
|
|
289
|
+
|
|
290
|
+
if isinstance(browser_interaction, bool):
|
|
291
|
+
enabled = browser_interaction
|
|
292
|
+
elif isinstance(browser_interaction, dict):
|
|
293
|
+
enabled = browser_interaction.get("enabled", True)
|
|
294
|
+
tool = browser_interaction.get("tool", "auto")
|
|
295
|
+
blocking = bool(browser_interaction.get("blocking", False))
|
|
296
|
+
steps = browser_interaction.get("verify_steps", []) or []
|
|
297
|
+
|
|
298
|
+
if tool not in ("playwright-cli", "opencli", "auto"):
|
|
299
|
+
tool = "auto"
|
|
300
|
+
|
|
301
|
+
if steps:
|
|
302
|
+
rendered_steps = "\n".join(
|
|
303
|
+
" # Goal {}: {}".format(index + 1, step)
|
|
304
|
+
for index, step in enumerate(steps)
|
|
305
|
+
)
|
|
306
|
+
else:
|
|
307
|
+
rendered_steps = (
|
|
308
|
+
" # (mandatory attempt: discover whether this project exposes "
|
|
309
|
+
"a browser-verifiable app; verify relevant behavior if applicable)"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
return enabled, tool, rendered_steps, blocking
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
Handles eight actions:
|
|
5
5
|
- get_next: Find the next bug to process based on priority and severity
|
|
6
|
-
- start: Mark a bug as
|
|
6
|
+
- start: Mark a bug as in_progress when a session starts
|
|
7
7
|
- update: Update a bug's status based on session outcome
|
|
8
8
|
- status: Print a formatted overview of all bugs
|
|
9
9
|
- pause: Save pipeline state for graceful shutdown
|
|
@@ -31,6 +31,8 @@ from utils import (
|
|
|
31
31
|
error_out,
|
|
32
32
|
pad_right,
|
|
33
33
|
_build_progress_bar,
|
|
34
|
+
read_checkpoint_cursor,
|
|
35
|
+
dependency_rank_map,
|
|
34
36
|
)
|
|
35
37
|
|
|
36
38
|
|
|
@@ -215,6 +217,36 @@ def reset_continuation_metadata(status_data):
|
|
|
215
217
|
STALL_REASON = "context_overflow_without_git_checkpoint_or_artifact_progress"
|
|
216
218
|
|
|
217
219
|
|
|
220
|
+
def _read_bugfix_checkpoint_phase(project_root, bug_id):
|
|
221
|
+
"""Read the phase index from the bugfix checkpoint file.
|
|
222
|
+
|
|
223
|
+
Returns an integer step index (e.g., 3 for S03) or None if the checkpoint
|
|
224
|
+
is not available. The value is consumed by the terminal status display as
|
|
225
|
+
``CP-BF-<n>``.
|
|
226
|
+
"""
|
|
227
|
+
checkpoint_path = os.path.join(
|
|
228
|
+
os.path.abspath(project_root) if project_root else _default_project_root(),
|
|
229
|
+
".prizmkit", "bugfix", bug_id, "workflow-checkpoint.json",
|
|
230
|
+
)
|
|
231
|
+
return read_checkpoint_cursor(checkpoint_path)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _checkpoint_phase_or_cached(project_root, bug_id, status_data):
|
|
235
|
+
"""Return live checkpoint phase, falling back to cached status data."""
|
|
236
|
+
checkpoint_phase = _read_bugfix_checkpoint_phase(project_root, bug_id)
|
|
237
|
+
if checkpoint_phase is not None:
|
|
238
|
+
return checkpoint_phase
|
|
239
|
+
return status_data.get("resume_from_phase")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _sync_checkpoint_phase(project_root, bug_id, status_data):
|
|
243
|
+
"""Cache live checkpoint phase in status.json runtime data."""
|
|
244
|
+
status_data["resume_from_phase"] = _read_bugfix_checkpoint_phase(
|
|
245
|
+
project_root, bug_id,
|
|
246
|
+
)
|
|
247
|
+
return status_data.get("resume_from_phase")
|
|
248
|
+
|
|
249
|
+
|
|
218
250
|
def _load_progress_fingerprint(raw):
|
|
219
251
|
if not raw:
|
|
220
252
|
return None
|
|
@@ -298,13 +330,14 @@ def continuation_metadata_summary(status_data):
|
|
|
298
330
|
# Action: get_next
|
|
299
331
|
# ---------------------------------------------------------------------------
|
|
300
332
|
|
|
301
|
-
def action_get_next(bug_list_data, state_dir):
|
|
333
|
+
def action_get_next(bug_list_data, state_dir, project_root=None):
|
|
302
334
|
"""Find the next bug to process.
|
|
303
335
|
|
|
304
336
|
Priority logic:
|
|
305
337
|
1. Skip terminal statuses (completed, failed, skipped, needs_info)
|
|
306
|
-
2.
|
|
307
|
-
3.
|
|
338
|
+
2. Only consider bugs whose dependencies are all completed
|
|
339
|
+
3. Prefer in_progress bugs (interrupted session resume) over pending
|
|
340
|
+
4. Within each status class, use stable topological list order
|
|
308
341
|
"""
|
|
309
342
|
bugs = bug_list_data.get("bugs", [])
|
|
310
343
|
if not bugs:
|
|
@@ -324,6 +357,16 @@ def action_get_next(bug_list_data, state_dir):
|
|
|
324
357
|
bs = load_bug_status(state_dir, bid)
|
|
325
358
|
status_data_map[bid] = bs
|
|
326
359
|
|
|
360
|
+
topo_rank, topo_errors = dependency_rank_map(
|
|
361
|
+
bugs,
|
|
362
|
+
id_key="id",
|
|
363
|
+
dependencies_key="dependencies",
|
|
364
|
+
item_label="Bug",
|
|
365
|
+
)
|
|
366
|
+
if topo_errors:
|
|
367
|
+
print("PIPELINE_BLOCKED")
|
|
368
|
+
return
|
|
369
|
+
|
|
327
370
|
# Check if all bugs are terminal
|
|
328
371
|
non_terminal = [
|
|
329
372
|
b for b in bugs
|
|
@@ -334,10 +377,22 @@ def action_get_next(bug_list_data, state_dir):
|
|
|
334
377
|
print("PIPELINE_COMPLETE")
|
|
335
378
|
return
|
|
336
379
|
|
|
380
|
+
eligible = []
|
|
381
|
+
for bug in non_terminal:
|
|
382
|
+
deps = bug.get("dependencies", [])
|
|
383
|
+
if not isinstance(deps, list):
|
|
384
|
+
continue
|
|
385
|
+
if all(status_map.get(dep_id, "pending") == "completed" for dep_id in deps):
|
|
386
|
+
eligible.append(bug)
|
|
387
|
+
|
|
388
|
+
if not eligible:
|
|
389
|
+
print("PIPELINE_BLOCKED")
|
|
390
|
+
return
|
|
391
|
+
|
|
337
392
|
# Separate in_progress from pending
|
|
338
393
|
in_progress_bugs = []
|
|
339
394
|
pending_bugs = []
|
|
340
|
-
for bug in
|
|
395
|
+
for bug in eligible:
|
|
341
396
|
bid = bug.get("id")
|
|
342
397
|
bstatus = status_map.get(bid, "pending")
|
|
343
398
|
if bstatus == "in_progress":
|
|
@@ -345,18 +400,16 @@ def action_get_next(bug_list_data, state_dir):
|
|
|
345
400
|
elif bstatus == "pending":
|
|
346
401
|
pending_bugs.append(bug)
|
|
347
402
|
|
|
348
|
-
_PRIORITY_ORDER = {"high": 0, "medium": 1, "low": 2}
|
|
349
|
-
|
|
350
|
-
def sort_key(b):
|
|
351
|
-
severity = b.get("severity", "medium")
|
|
352
|
-
sev_order = SEVERITY_PRIORITY.get(severity, 2)
|
|
353
|
-
priority = _PRIORITY_ORDER.get(b.get("priority", "low"), 2)
|
|
354
|
-
return (sev_order, priority)
|
|
355
|
-
|
|
356
403
|
if in_progress_bugs:
|
|
357
|
-
candidates = sorted(
|
|
404
|
+
candidates = sorted(
|
|
405
|
+
in_progress_bugs,
|
|
406
|
+
key=lambda b: topo_rank.get(b.get("id"), len(bugs))
|
|
407
|
+
)
|
|
358
408
|
elif pending_bugs:
|
|
359
|
-
candidates = sorted(
|
|
409
|
+
candidates = sorted(
|
|
410
|
+
pending_bugs,
|
|
411
|
+
key=lambda b: topo_rank.get(b.get("id"), len(bugs))
|
|
412
|
+
)
|
|
360
413
|
else:
|
|
361
414
|
# All remaining bugs are in non-terminal but also non-pending/in_progress states
|
|
362
415
|
print("PIPELINE_BLOCKED")
|
|
@@ -365,6 +418,10 @@ def action_get_next(bug_list_data, state_dir):
|
|
|
365
418
|
chosen = candidates[0]
|
|
366
419
|
chosen_id = chosen["id"]
|
|
367
420
|
chosen_status_data = status_data_map.get(chosen_id, {})
|
|
421
|
+
project_root = os.path.abspath(project_root) if project_root else _default_project_root()
|
|
422
|
+
resume_phase = _checkpoint_phase_or_cached(
|
|
423
|
+
project_root, chosen_id, chosen_status_data,
|
|
424
|
+
)
|
|
368
425
|
|
|
369
426
|
result = {
|
|
370
427
|
"bug_id": chosen_id,
|
|
@@ -372,7 +429,7 @@ def action_get_next(bug_list_data, state_dir):
|
|
|
372
429
|
"severity": chosen.get("severity", "medium"),
|
|
373
430
|
"retry_count": chosen_status_data.get("retry_count", 0),
|
|
374
431
|
"infra_error_count": chosen_status_data.get("infra_error_count", 0),
|
|
375
|
-
"resume_from_phase":
|
|
432
|
+
"resume_from_phase": resume_phase,
|
|
376
433
|
}
|
|
377
434
|
result.update(continuation_metadata_summary(chosen_status_data))
|
|
378
435
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
@@ -502,6 +559,10 @@ def action_update(args, bug_list_path, state_dir):
|
|
|
502
559
|
bs["last_session_id"] = session_id
|
|
503
560
|
|
|
504
561
|
bs["updated_at"] = now_iso()
|
|
562
|
+
# Sync resume_from_phase from checkpoint (single source of truth)
|
|
563
|
+
_sync_checkpoint_phase(
|
|
564
|
+
args.project_root or _default_project_root(), bug_id, bs,
|
|
565
|
+
)
|
|
505
566
|
|
|
506
567
|
err = save_bug_status(state_dir, bug_id, bs)
|
|
507
568
|
if err:
|
|
@@ -676,9 +737,10 @@ SEVERITY_ICONS = {
|
|
|
676
737
|
}
|
|
677
738
|
|
|
678
739
|
|
|
679
|
-
def action_status(bug_list_data, state_dir):
|
|
740
|
+
def action_status(bug_list_data, state_dir, project_root=None):
|
|
680
741
|
bugs = bug_list_data.get("bugs", [])
|
|
681
742
|
project_name = bug_list_data.get("project_name", "Unknown")
|
|
743
|
+
project_root = os.path.abspath(project_root) if project_root else _default_project_root()
|
|
682
744
|
|
|
683
745
|
counts = {"completed": 0, "in_progress": 0, "failed": 0, "pending": 0, "needs_info": 0, "skipped": 0}
|
|
684
746
|
bug_lines = []
|
|
@@ -696,7 +758,12 @@ def action_status(bug_list_data, state_dir):
|
|
|
696
758
|
bs = load_bug_status(state_dir, bid)
|
|
697
759
|
retry_count = bs.get("retry_count", 0)
|
|
698
760
|
max_retries_val = bs.get("max_retries", 3)
|
|
699
|
-
|
|
761
|
+
# Read phase cursor from checkpoint (single source of truth). The
|
|
762
|
+
# status.json field is only a cache/fallback for missing checkpoints.
|
|
763
|
+
resume_phase = _checkpoint_phase_or_cached(project_root, bid, bs)
|
|
764
|
+
if resume_phase != bs.get("resume_from_phase"):
|
|
765
|
+
bs["resume_from_phase"] = resume_phase
|
|
766
|
+
save_bug_status(state_dir, bid, bs)
|
|
700
767
|
|
|
701
768
|
if bstatus in counts:
|
|
702
769
|
counts[bstatus] += 1
|
|
@@ -1096,13 +1163,13 @@ def main():
|
|
|
1096
1163
|
error_out("Cannot load bug fix list: {}".format(err))
|
|
1097
1164
|
|
|
1098
1165
|
if args.action == "get_next":
|
|
1099
|
-
action_get_next(bug_list_data, args.state_dir)
|
|
1166
|
+
action_get_next(bug_list_data, args.state_dir, args.project_root)
|
|
1100
1167
|
elif args.action == "start":
|
|
1101
1168
|
action_start(args, args.bug_list, args.state_dir)
|
|
1102
1169
|
elif args.action == "update":
|
|
1103
1170
|
action_update(args, args.bug_list, args.state_dir)
|
|
1104
1171
|
elif args.action == "status":
|
|
1105
|
-
action_status(bug_list_data, args.state_dir)
|
|
1172
|
+
action_status(bug_list_data, args.state_dir, args.project_root)
|
|
1106
1173
|
elif args.action == "reset":
|
|
1107
1174
|
action_reset(args, args.bug_list, args.state_dir)
|
|
1108
1175
|
elif args.action == "clean":
|