prizmkit 1.1.129 → 1.1.131
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/dev-pipeline/.env.example +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +445 -8
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +87 -66
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +53 -3
- package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +55 -25
- package/bundled/dev-pipeline/prizmkit_runtime/runner_status.py +4 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +15 -1
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +398 -6
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +61 -3
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +64 -4
- package/bundled/dev-pipeline/scripts/prompt_framework.py +156 -7
- package/bundled/dev-pipeline/scripts/update-bug-status.py +53 -0
- package/bundled/dev-pipeline/scripts/update-checkpoint.py +294 -69
- package/bundled/dev-pipeline/scripts/update-feature-status.py +57 -0
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +53 -0
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +2 -2
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +20 -14
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +21 -4
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +18 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +1 -1
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +5 -15
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-test.md +21 -0
- package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +10 -10
- package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +16 -0
- package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +26 -23
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +12 -8
- package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +1 -1
- package/bundled/dev-pipeline/templates/sections/refactor-phase-test.md +21 -0
- package/bundled/dev-pipeline/tests/test_checkpoint_state.py +362 -3
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +404 -24
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +132 -0
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +91 -0
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +411 -18
- package/bundled/dev-pipeline/tests/test_unified_cli.py +3 -3
- package/bundled/skills/_metadata.json +62 -25
- package/bundled/skills/app-planner/SKILL.md +4 -3
- package/bundled/skills/bug-fix-workflow/SKILL.md +89 -180
- package/bundled/skills/bug-planner/SKILL.md +12 -27
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +131 -220
- package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +36 -90
- package/bundled/skills/feature-pipeline-launcher/SKILL.md +129 -271
- package/bundled/skills/feature-pipeline-launcher/references/configuration.md +35 -76
- package/bundled/skills/feature-planner/SKILL.md +27 -22
- package/bundled/skills/feature-planner/assets/planning-guide.md +4 -4
- package/bundled/skills/feature-workflow/SKILL.md +94 -121
- package/bundled/skills/prizmkit/SKILL.md +130 -94
- package/bundled/skills/prizmkit/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-code-review/SKILL.md +114 -84
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +1 -1
- package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-committer/SKILL.md +131 -47
- package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-implement/SKILL.md +99 -73
- package/bundled/skills/prizmkit-implement/references/implementation-subagent-procedure.md +1 -1
- package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-init/SKILL.md +5 -5
- package/bundled/skills/prizmkit-plan/SKILL.md +132 -105
- package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-retrospective/SKILL.md +111 -53
- package/bundled/skills/prizmkit-retrospective/references/knowledge-injection-steps.md +7 -6
- package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +5 -4
- package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-test/SKILL.md +28 -6
- package/bundled/skills/prizmkit-test/references/examples.md +1 -1
- package/bundled/skills/prizmkit-test/references/test-report-template.md +1 -1
- package/bundled/skills/prizmkit-test/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-workflow/SKILL.md +266 -0
- package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/recovery-workflow/SKILL.md +62 -241
- package/bundled/skills/recovery-workflow/evals/evals.json +17 -33
- package/bundled/skills/recovery-workflow/references/detection.md +28 -52
- package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +105 -439
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +136 -275
- package/bundled/skills/refactor-pipeline-launcher/references/configuration.md +53 -88
- package/bundled/skills/refactor-planner/SKILL.md +12 -57
- package/bundled/skills/refactor-planner/references/behavior-preservation.md +111 -230
- package/bundled/skills/refactor-planner/references/error-recovery.md +37 -0
- package/bundled/skills/refactor-planner/references/planning-phases.md +11 -17
- package/bundled/skills/refactor-planner/scripts/validate-and-generate-refactor.py +1 -1
- package/bundled/skills/refactor-workflow/SKILL.md +98 -139
- package/bundled/templates/project-memory-template.md +4 -4
- package/package.json +1 -1
- package/src/index.js +4 -0
- package/src/prompts.js +10 -3
- package/src/scaffold.js +1 -1
- package/bundled/dev-pipeline/tests/test-deploy-safety.sh +0 -223
- package/bundled/skills/feature-pipeline-launcher/scripts/preflight-check.py +0 -462
|
@@ -1,37 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
|
|
4
|
-
Instead of having the AI hand-write JSON, this script reads the checkpoint,
|
|
5
|
-
validates the update, and writes it back atomically.
|
|
6
|
-
|
|
7
|
-
Usage:
|
|
8
|
-
python3 update-checkpoint.py --checkpoint-path <path> --step <skill-or-id> --status <status>
|
|
9
|
-
python3 update-checkpoint.py --checkpoint-path <path> --step prizmkit-implement --status completed
|
|
10
|
-
python3 update-checkpoint.py --checkpoint-path <path> --step S04 --status in_progress
|
|
11
|
-
python3 update-checkpoint.py --checkpoint-path <path> --step prizmkit-code-review --status failed --note "max rounds exhausted"
|
|
12
|
-
|
|
13
|
-
Exit codes:
|
|
14
|
-
0 = success
|
|
15
|
-
1 = validation error (step not found, invalid status, ambiguous skill, etc.)
|
|
16
|
-
2 = file error (not found, corrupted JSON)
|
|
17
|
-
|
|
18
|
-
Output (stdout): JSON with {ok: true, step_id, skill, old_status, new_status} or {ok: false, error}
|
|
19
|
-
"""
|
|
2
|
+
"""Atomically update one semantic L4 workflow checkpoint."""
|
|
20
3
|
|
|
21
4
|
import argparse
|
|
22
5
|
import json
|
|
23
6
|
import os
|
|
24
7
|
import sys
|
|
25
8
|
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
26
10
|
|
|
27
11
|
_PIPELINE_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
28
12
|
if _PIPELINE_ROOT not in sys.path:
|
|
29
13
|
sys.path.insert(0, _PIPELINE_ROOT)
|
|
30
14
|
|
|
31
15
|
from prizmkit_runtime.checkpoint_state import ( # noqa: E402
|
|
16
|
+
MAX_OUTER_REPAIR_ROUNDS,
|
|
17
|
+
REVIEW_RESULTS,
|
|
32
18
|
STEP_ID_PATTERN,
|
|
19
|
+
TERMINAL_STEP_STATUSES,
|
|
20
|
+
TEST_RESULTS,
|
|
33
21
|
VALID_CHECKPOINT_STATUSES,
|
|
22
|
+
VALID_REPAIR_SCOPES,
|
|
23
|
+
VALID_TERMINAL_STATUSES,
|
|
34
24
|
load_checkpoint_state,
|
|
25
|
+
semantic_state_key,
|
|
26
|
+
validate_checkpoint_data,
|
|
35
27
|
)
|
|
36
28
|
|
|
37
29
|
VALID_STATUSES = VALID_CHECKPOINT_STATUSES
|
|
@@ -48,15 +40,15 @@ def _load_checkpoint(path):
|
|
|
48
40
|
try:
|
|
49
41
|
with open(path, "r", encoding="utf-8") as f:
|
|
50
42
|
data = json.load(f)
|
|
51
|
-
except json.JSONDecodeError as
|
|
52
|
-
return None, "Corrupted JSON: {}".format(
|
|
53
|
-
except IOError as
|
|
54
|
-
return None, "Cannot read file: {}".format(
|
|
43
|
+
except json.JSONDecodeError as exc:
|
|
44
|
+
return None, "Corrupted JSON: {}".format(exc)
|
|
45
|
+
except IOError as exc:
|
|
46
|
+
return None, "Cannot read file: {}".format(exc)
|
|
55
47
|
return data, None
|
|
56
48
|
|
|
57
49
|
|
|
58
50
|
def _write_checkpoint(path, data):
|
|
59
|
-
"""Write checkpoint JSON atomically using a temp file
|
|
51
|
+
"""Write checkpoint JSON atomically using a temp file and rename."""
|
|
60
52
|
parent = os.path.dirname(os.path.abspath(path))
|
|
61
53
|
if parent and not os.path.isdir(parent):
|
|
62
54
|
os.makedirs(parent, exist_ok=True)
|
|
@@ -70,24 +62,24 @@ def _write_checkpoint(path, data):
|
|
|
70
62
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
71
63
|
f.write("\n")
|
|
72
64
|
os.replace(tmp_path, os.path.abspath(path))
|
|
73
|
-
except (IOError, OSError) as
|
|
65
|
+
except (IOError, OSError) as exc:
|
|
74
66
|
if tmp_path is not None and os.path.exists(tmp_path):
|
|
75
67
|
os.unlink(tmp_path)
|
|
76
|
-
return "Cannot write file: {}".format(
|
|
68
|
+
return "Cannot write file: {}".format(exc)
|
|
77
69
|
return None
|
|
78
70
|
|
|
79
71
|
|
|
80
72
|
def _find_step(steps, identifier):
|
|
81
73
|
"""Find a step by exact ID or unambiguous skill name."""
|
|
82
74
|
if STEP_ID_PATTERN.match(identifier):
|
|
83
|
-
for
|
|
75
|
+
for index, step in enumerate(steps):
|
|
84
76
|
if step.get("id") == identifier:
|
|
85
|
-
return
|
|
77
|
+
return index, step, None
|
|
86
78
|
return None, None, "Step '{}' not found.".format(identifier)
|
|
87
79
|
|
|
88
80
|
matches = [
|
|
89
|
-
(
|
|
90
|
-
for
|
|
81
|
+
(index, step)
|
|
82
|
+
for index, step in enumerate(steps)
|
|
91
83
|
if step.get("skill") == identifier
|
|
92
84
|
]
|
|
93
85
|
if len(matches) == 1:
|
|
@@ -103,11 +95,161 @@ def _find_step(steps, identifier):
|
|
|
103
95
|
return None, None, "Step '{}' not found.".format(identifier)
|
|
104
96
|
|
|
105
97
|
|
|
106
|
-
def
|
|
107
|
-
""
|
|
98
|
+
def _predecessors_terminal(steps, index):
|
|
99
|
+
return all(step.get("status") in TERMINAL_STEP_STATUSES for step in steps[:index])
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _mandatory_predecessor_verdict_error(steps, index, workflow_type):
|
|
104
|
+
if workflow_type not in {"feature-pipeline", "bugfix-pipeline", "refactor-pipeline"}:
|
|
105
|
+
return None
|
|
106
|
+
preceding = steps[:index]
|
|
107
|
+
review = next((item for item in preceding if item.get("skill") == "prizmkit-code-review"), None)
|
|
108
|
+
test = next((item for item in preceding if item.get("skill") == "prizmkit-test"), None)
|
|
109
|
+
if review is None and test is None:
|
|
110
|
+
return None
|
|
111
|
+
skill = steps[index].get("skill")
|
|
112
|
+
if skill == "prizmkit-test":
|
|
113
|
+
if not review or review.get("status") != "completed" or review.get("stage_result") != "REVIEW_PASS":
|
|
114
|
+
return "Test stage requires a completed REVIEW_PASS predecessor"
|
|
115
|
+
if skill in {"prizmkit-retrospective", "prizmkit-committer", "completion-summary", "bug-report", "refactor-report"}:
|
|
116
|
+
if not review or review.get("status") != "completed" or review.get("stage_result") != "REVIEW_PASS":
|
|
117
|
+
return "Finalization requires a completed REVIEW_PASS predecessor"
|
|
118
|
+
if not test or test.get("status") != "completed" or test.get("stage_result") != "TEST_PASS":
|
|
119
|
+
return "Finalization requires a completed TEST_PASS predecessor"
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
def _normalize_semantic_update(semantic_update):
|
|
123
|
+
values = dict(semantic_update or {})
|
|
124
|
+
evidence = values.pop("authoritative_evidence_paths", None)
|
|
125
|
+
if evidence is not None:
|
|
126
|
+
evidence = {key: value for key, value in evidence.items() if value}
|
|
127
|
+
if evidence:
|
|
128
|
+
values["authoritative_evidence_paths"] = evidence
|
|
129
|
+
return {key: value for key, value in values.items() if value is not None}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _validate_semantic_update(step, semantic_update):
|
|
133
|
+
stage_result = semantic_update.get("stage_result")
|
|
134
|
+
skill = step.get("skill")
|
|
135
|
+
if stage_result in REVIEW_RESULTS and skill != "prizmkit-code-review":
|
|
136
|
+
return "Review verdicts may only be recorded on prizmkit-code-review"
|
|
137
|
+
if stage_result in TEST_RESULTS and skill != "prizmkit-test":
|
|
138
|
+
return "Test verdicts may only be recorded on prizmkit-test"
|
|
139
|
+
repair_scope = semantic_update.get("repair_scope")
|
|
140
|
+
if repair_scope is not None and repair_scope not in VALID_REPAIR_SCOPES:
|
|
141
|
+
return "Invalid repair_scope '{}'".format(repair_scope)
|
|
142
|
+
repair_round = semantic_update.get("repair_round")
|
|
143
|
+
if repair_round is not None and not 0 <= repair_round <= MAX_OUTER_REPAIR_ROUNDS:
|
|
144
|
+
return "repair_round must be between 0 and {}".format(MAX_OUTER_REPAIR_ROUNDS)
|
|
145
|
+
terminal_status = semantic_update.get("terminal_status")
|
|
146
|
+
if terminal_status is not None and terminal_status not in VALID_TERMINAL_STATUSES:
|
|
147
|
+
return "Invalid terminal_status '{}'".format(terminal_status)
|
|
148
|
+
evidence = semantic_update.get("authoritative_evidence_paths")
|
|
149
|
+
if evidence is not None and set(evidence) != {"manifest", "verdict", "validation"}:
|
|
150
|
+
return "Authoritative test evidence requires manifest, verdict, and validation paths"
|
|
151
|
+
if terminal_status == "WORKFLOW_BLOCKED" and not semantic_update.get("blocked_reason"):
|
|
152
|
+
return "WORKFLOW_BLOCKED requires blocked_reason"
|
|
153
|
+
return None
|
|
154
|
+
|
|
108
155
|
|
|
109
|
-
|
|
110
|
-
"""
|
|
156
|
+
def _apply_repair_policy(l4_state, stage_result, repair_scope):
|
|
157
|
+
if stage_result not in {"REVIEW_NEEDS_FIXES", "TEST_FAIL", "TEST_BLOCKED"}:
|
|
158
|
+
return
|
|
159
|
+
current_round = l4_state.get("repair_round", 0)
|
|
160
|
+
if not isinstance(current_round, int) or isinstance(current_round, bool):
|
|
161
|
+
current_round = MAX_OUTER_REPAIR_ROUNDS
|
|
162
|
+
if stage_result == "TEST_BLOCKED":
|
|
163
|
+
l4_state.update(
|
|
164
|
+
terminal_status="WORKFLOW_BLOCKED",
|
|
165
|
+
blocked_reason="test_blocked",
|
|
166
|
+
current_stage=None,
|
|
167
|
+
next_stage=None,
|
|
168
|
+
)
|
|
169
|
+
return
|
|
170
|
+
if current_round >= MAX_OUTER_REPAIR_ROUNDS:
|
|
171
|
+
l4_state.update(
|
|
172
|
+
repair_round=MAX_OUTER_REPAIR_ROUNDS,
|
|
173
|
+
terminal_status="WORKFLOW_BLOCKED",
|
|
174
|
+
blocked_reason="repair_round_limit",
|
|
175
|
+
current_stage=None,
|
|
176
|
+
next_stage=None,
|
|
177
|
+
)
|
|
178
|
+
return
|
|
179
|
+
if repair_scope not in VALID_REPAIR_SCOPES:
|
|
180
|
+
l4_state.update(
|
|
181
|
+
terminal_status="WORKFLOW_BLOCKED",
|
|
182
|
+
blocked_reason="unsafe_repair_scope",
|
|
183
|
+
current_stage=None,
|
|
184
|
+
next_stage=None,
|
|
185
|
+
)
|
|
186
|
+
return
|
|
187
|
+
l4_state["repair_round"] = current_round + 1
|
|
188
|
+
l4_state["terminal_status"] = None
|
|
189
|
+
l4_state["blocked_reason"] = None
|
|
190
|
+
l4_state["next_stage"] = "prizmkit-implement"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _sync_cursor(data, l4_state):
|
|
194
|
+
active = next(
|
|
195
|
+
(step for step in data["steps"] if step.get("status") not in TERMINAL_STEP_STATUSES),
|
|
196
|
+
None,
|
|
197
|
+
)
|
|
198
|
+
if l4_state.get("terminal_status"):
|
|
199
|
+
l4_state["current_stage"] = None
|
|
200
|
+
l4_state["next_stage"] = None
|
|
201
|
+
return
|
|
202
|
+
l4_state["current_stage"] = active.get("skill") if active else None
|
|
203
|
+
l4_state["next_stage"] = active.get("skill") if active else None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _checkpoint_project_root(checkpoint_path):
|
|
209
|
+
"""Resolve the project root from a conventional .prizmkit checkpoint path."""
|
|
210
|
+
path = Path(os.path.abspath(checkpoint_path))
|
|
211
|
+
for parent in (path.parent, *path.parents):
|
|
212
|
+
if parent.name == ".prizmkit":
|
|
213
|
+
return str(parent.parent)
|
|
214
|
+
return str(path.parent)
|
|
215
|
+
|
|
216
|
+
def _finalize_semantics(checkpoint_path, data, state_key):
|
|
217
|
+
"""Derive a deterministic terminal status after an atomic mutation."""
|
|
218
|
+
candidate = validate_checkpoint_data(
|
|
219
|
+
data,
|
|
220
|
+
path=checkpoint_path,
|
|
221
|
+
exists=True,
|
|
222
|
+
project_root=_checkpoint_project_root(checkpoint_path),
|
|
223
|
+
)
|
|
224
|
+
l4_state = data[state_key]
|
|
225
|
+
if candidate.valid and candidate.semantic.completion_ready:
|
|
226
|
+
l4_state.update(
|
|
227
|
+
terminal_status="WORKFLOW_COMPLETED",
|
|
228
|
+
blocked_reason=None,
|
|
229
|
+
current_stage=None,
|
|
230
|
+
next_stage=None,
|
|
231
|
+
)
|
|
232
|
+
return
|
|
233
|
+
all_terminal = all(
|
|
234
|
+
step.get("status") in TERMINAL_STEP_STATUSES for step in data["steps"]
|
|
235
|
+
)
|
|
236
|
+
if all_terminal and not l4_state.get("terminal_status"):
|
|
237
|
+
l4_state.update(
|
|
238
|
+
terminal_status="WORKFLOW_BLOCKED",
|
|
239
|
+
blocked_reason=candidate.semantic.error_code or "incomplete_semantic_evidence",
|
|
240
|
+
current_stage=None,
|
|
241
|
+
next_stage=None,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def update_checkpoint(
|
|
246
|
+
checkpoint_path,
|
|
247
|
+
step_identifier,
|
|
248
|
+
new_status,
|
|
249
|
+
note=None,
|
|
250
|
+
semantic_update=None,
|
|
251
|
+
):
|
|
252
|
+
"""Update one step and its cohesive semantic L4 state atomically."""
|
|
111
253
|
if new_status not in VALID_STATUSES:
|
|
112
254
|
return {
|
|
113
255
|
"ok": False,
|
|
@@ -120,23 +262,106 @@ def update_checkpoint(checkpoint_path, step_identifier, new_status, note=None):
|
|
|
120
262
|
if err:
|
|
121
263
|
return {"ok": False, "error": err}
|
|
122
264
|
|
|
123
|
-
|
|
265
|
+
index, step, find_error = _find_step(data["steps"], step_identifier)
|
|
124
266
|
if step is None:
|
|
125
267
|
available = [
|
|
126
|
-
"{} ({})".format(
|
|
127
|
-
for
|
|
268
|
+
"{} ({})".format(item.get("id", "?"), item.get("skill", "?"))
|
|
269
|
+
for item in data["steps"]
|
|
128
270
|
]
|
|
129
271
|
suffix = " Available: {}".format(", ".join(available)) if available else ""
|
|
272
|
+
return {"ok": False, "error": "{}{}".format(find_error or "Step not found.", suffix)}
|
|
273
|
+
|
|
274
|
+
if new_status in {"in_progress", "completed", "skipped"} and not _predecessors_terminal(data["steps"], index):
|
|
130
275
|
return {
|
|
131
276
|
"ok": False,
|
|
132
|
-
"error": "{}{}
|
|
277
|
+
"error": "Step '{}' cannot become {} before all predecessors are completed or skipped".format(
|
|
278
|
+
step.get("id"), new_status
|
|
279
|
+
),
|
|
133
280
|
}
|
|
281
|
+
if new_status in {"in_progress", "completed"}:
|
|
282
|
+
verdict_error = _mandatory_predecessor_verdict_error(
|
|
283
|
+
data["steps"], index, str(data.get("workflow_type") or "")
|
|
284
|
+
)
|
|
285
|
+
if verdict_error:
|
|
286
|
+
return {"ok": False, "error": verdict_error}
|
|
287
|
+
|
|
288
|
+
semantic_update = _normalize_semantic_update(semantic_update)
|
|
289
|
+
semantic_error = _validate_semantic_update(step, semantic_update)
|
|
290
|
+
if semantic_error:
|
|
291
|
+
return {"ok": False, "error": semantic_error}
|
|
292
|
+
|
|
293
|
+
stage_result = semantic_update.get("stage_result")
|
|
294
|
+
if new_status == "completed" and step.get("skill") == "prizmkit-code-review":
|
|
295
|
+
stage_result = stage_result or step.get("stage_result")
|
|
296
|
+
if stage_result != "REVIEW_PASS":
|
|
297
|
+
return {"ok": False, "error": "Code Review completion requires REVIEW_PASS"}
|
|
298
|
+
if new_status == "completed" and step.get("skill") == "prizmkit-test":
|
|
299
|
+
stage_result = stage_result or step.get("stage_result")
|
|
300
|
+
if stage_result != "TEST_PASS":
|
|
301
|
+
return {"ok": False, "error": "Test completion requires TEST_PASS"}
|
|
302
|
+
evidence = semantic_update.get("authoritative_evidence_paths") or step.get("authoritative_evidence_paths")
|
|
303
|
+
if not isinstance(evidence, dict) or set(evidence) != {"manifest", "verdict", "validation"}:
|
|
304
|
+
return {"ok": False, "error": "Test completion requires all authoritative evidence paths"}
|
|
134
305
|
|
|
135
306
|
old_status = step.get("status", "unknown")
|
|
136
307
|
step["status"] = new_status
|
|
137
|
-
|
|
138
308
|
if note:
|
|
139
309
|
step["note"] = note
|
|
310
|
+
if stage_result:
|
|
311
|
+
step["stage_result"] = stage_result
|
|
312
|
+
if semantic_update.get("authoritative_evidence_paths"):
|
|
313
|
+
step["authoritative_evidence_paths"] = semantic_update["authoritative_evidence_paths"]
|
|
314
|
+
if new_status == "completed" and step.get("skill") == "prizmkit-test":
|
|
315
|
+
candidate = validate_checkpoint_data(
|
|
316
|
+
data,
|
|
317
|
+
path=checkpoint_path,
|
|
318
|
+
exists=True,
|
|
319
|
+
project_root=_checkpoint_project_root(checkpoint_path),
|
|
320
|
+
)
|
|
321
|
+
if not candidate.valid or not candidate.semantic.evidence_valid:
|
|
322
|
+
return {
|
|
323
|
+
"ok": False,
|
|
324
|
+
"error": "Test completion requires validator-attested authoritative evidence",
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
workflow_type = str(data.get("workflow_type") or "")
|
|
328
|
+
state_key = semantic_state_key(workflow_type)
|
|
329
|
+
l4_state = data.setdefault(state_key, {})
|
|
330
|
+
if not isinstance(l4_state, dict):
|
|
331
|
+
return {"ok": False, "error": "Invalid semantic state object '{}'".format(state_key)}
|
|
332
|
+
for key in (
|
|
333
|
+
"stage_result",
|
|
334
|
+
"repair_scope",
|
|
335
|
+
"repair_round",
|
|
336
|
+
"authoritative_evidence_paths",
|
|
337
|
+
"blocked_reason",
|
|
338
|
+
"terminal_status",
|
|
339
|
+
):
|
|
340
|
+
if key in semantic_update:
|
|
341
|
+
l4_state[key] = semantic_update[key]
|
|
342
|
+
l4_state["schema_version"] = 1
|
|
343
|
+
l4_state["orchestrator"] = "prizmkit-l4"
|
|
344
|
+
if stage_result:
|
|
345
|
+
l4_state["stage_result"] = stage_result
|
|
346
|
+
if semantic_update.get("authoritative_evidence_paths"):
|
|
347
|
+
l4_state["authoritative_evidence_paths"] = semantic_update["authoritative_evidence_paths"]
|
|
348
|
+
_apply_repair_policy(l4_state, stage_result, semantic_update.get("repair_scope") or l4_state.get("repair_scope"))
|
|
349
|
+
_sync_cursor(data, l4_state)
|
|
350
|
+
_finalize_semantics(checkpoint_path, data, state_key)
|
|
351
|
+
|
|
352
|
+
final_state = validate_checkpoint_data(
|
|
353
|
+
data,
|
|
354
|
+
path=checkpoint_path,
|
|
355
|
+
exists=True,
|
|
356
|
+
project_root=_checkpoint_project_root(checkpoint_path),
|
|
357
|
+
)
|
|
358
|
+
if not final_state.valid:
|
|
359
|
+
return {
|
|
360
|
+
"ok": False,
|
|
361
|
+
"error": "Update would create invalid checkpoint ({}): {}".format(
|
|
362
|
+
final_state.error_code, final_state.error_message
|
|
363
|
+
),
|
|
364
|
+
}
|
|
140
365
|
|
|
141
366
|
err = _write_checkpoint(checkpoint_path, data)
|
|
142
367
|
if err:
|
|
@@ -148,46 +373,46 @@ def update_checkpoint(checkpoint_path, step_identifier, new_status, note=None):
|
|
|
148
373
|
"skill": step.get("skill"),
|
|
149
374
|
"old_status": old_status,
|
|
150
375
|
"new_status": new_status,
|
|
376
|
+
"semantic": final_state.semantic_snapshot,
|
|
151
377
|
}
|
|
152
378
|
|
|
153
379
|
|
|
154
380
|
def main():
|
|
155
381
|
parser = argparse.ArgumentParser(
|
|
156
|
-
description="Safely update a workflow-checkpoint.json step status."
|
|
157
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
158
|
-
epilog=(
|
|
159
|
-
"Examples:\n"
|
|
160
|
-
" %(prog)s --checkpoint-path .prizmkit/specs/my-feature/workflow-checkpoint.json "
|
|
161
|
-
"--step prizmkit-implement --status completed\n"
|
|
162
|
-
" %(prog)s --checkpoint-path .prizmkit/bugfix/B-001/workflow-checkpoint.json "
|
|
163
|
-
"--step S04 --status in_progress\n"
|
|
164
|
-
" %(prog)s --checkpoint-path .prizmkit/refactor/R-001/workflow-checkpoint.json "
|
|
165
|
-
"--step prizmkit-code-review --status failed --note 'max rounds exhausted'"
|
|
166
|
-
),
|
|
167
|
-
)
|
|
168
|
-
parser.add_argument(
|
|
169
|
-
"--checkpoint-path", required=True, help="Path to workflow-checkpoint.json"
|
|
170
|
-
)
|
|
171
|
-
parser.add_argument(
|
|
172
|
-
"--step",
|
|
173
|
-
required=True,
|
|
174
|
-
help="Step identifier (skill name like 'prizmkit-implement' or step ID like 'S04')",
|
|
175
|
-
)
|
|
176
|
-
parser.add_argument(
|
|
177
|
-
"--status",
|
|
178
|
-
required=True,
|
|
179
|
-
choices=sorted(VALID_STATUSES),
|
|
180
|
-
help="New status for the step",
|
|
181
|
-
)
|
|
182
|
-
parser.add_argument(
|
|
183
|
-
"--note",
|
|
184
|
-
default=None,
|
|
185
|
-
help="Optional note to attach to the step (e.g., failure reason)",
|
|
382
|
+
description="Safely update a semantic workflow-checkpoint.json step status."
|
|
186
383
|
)
|
|
384
|
+
parser.add_argument("--checkpoint-path", required=True)
|
|
385
|
+
parser.add_argument("--step", required=True)
|
|
386
|
+
parser.add_argument("--status", required=True, choices=sorted(VALID_STATUSES))
|
|
387
|
+
parser.add_argument("--note", default=None)
|
|
388
|
+
parser.add_argument("--stage-result", choices=sorted(REVIEW_RESULTS | TEST_RESULTS), default=None)
|
|
389
|
+
parser.add_argument("--repair-scope", choices=sorted(VALID_REPAIR_SCOPES), default=None)
|
|
390
|
+
parser.add_argument("--repair-round", type=int, default=None)
|
|
391
|
+
parser.add_argument("--manifest-path", default=None)
|
|
392
|
+
parser.add_argument("--verdict-path", default=None)
|
|
393
|
+
parser.add_argument("--validation-path", default=None)
|
|
394
|
+
parser.add_argument("--blocked-reason", default=None)
|
|
395
|
+
parser.add_argument("--terminal-status", choices=sorted(VALID_TERMINAL_STATUSES), default=None)
|
|
187
396
|
|
|
188
397
|
args = parser.parse_args()
|
|
398
|
+
semantic_update = {
|
|
399
|
+
"stage_result": args.stage_result,
|
|
400
|
+
"repair_scope": args.repair_scope,
|
|
401
|
+
"repair_round": args.repair_round,
|
|
402
|
+
"authoritative_evidence_paths": {
|
|
403
|
+
"manifest": args.manifest_path,
|
|
404
|
+
"verdict": args.verdict_path,
|
|
405
|
+
"validation": args.validation_path,
|
|
406
|
+
},
|
|
407
|
+
"blocked_reason": args.blocked_reason,
|
|
408
|
+
"terminal_status": args.terminal_status,
|
|
409
|
+
}
|
|
189
410
|
result = update_checkpoint(
|
|
190
|
-
args.checkpoint_path,
|
|
411
|
+
args.checkpoint_path,
|
|
412
|
+
args.step,
|
|
413
|
+
args.status,
|
|
414
|
+
args.note,
|
|
415
|
+
semantic_update,
|
|
191
416
|
)
|
|
192
417
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
193
418
|
error = result.get("error", "")
|
|
@@ -50,6 +50,7 @@ SESSION_STATUS_VALUES = [
|
|
|
50
50
|
"infra_error",
|
|
51
51
|
"context_overflow",
|
|
52
52
|
"stalled_context_continuation",
|
|
53
|
+
"workflow_blocked",
|
|
53
54
|
"commit_missing",
|
|
54
55
|
"docs_missing",
|
|
55
56
|
"merge_conflict",
|
|
@@ -152,6 +153,11 @@ def parse_args():
|
|
|
152
153
|
default=None,
|
|
153
154
|
help="JSON progress fingerprint captured after the session.",
|
|
154
155
|
)
|
|
156
|
+
parser.add_argument(
|
|
157
|
+
"--checkpoint-state",
|
|
158
|
+
default=None,
|
|
159
|
+
help="JSON semantic L4 checkpoint snapshot captured after the session.",
|
|
160
|
+
)
|
|
155
161
|
return parser.parse_args()
|
|
156
162
|
|
|
157
163
|
|
|
@@ -358,6 +364,37 @@ def _load_progress_fingerprint(raw):
|
|
|
358
364
|
return {"unparsed": str(raw)}
|
|
359
365
|
|
|
360
366
|
|
|
367
|
+
SEMANTIC_CHECKPOINT_FIELDS = (
|
|
368
|
+
"current_stage",
|
|
369
|
+
"next_stage",
|
|
370
|
+
"stage_result",
|
|
371
|
+
"repair_scope",
|
|
372
|
+
"repair_round",
|
|
373
|
+
"authoritative_evidence_paths",
|
|
374
|
+
"blocked_reason",
|
|
375
|
+
"terminal_status",
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _load_checkpoint_state(raw):
|
|
380
|
+
if not raw:
|
|
381
|
+
return {}
|
|
382
|
+
try:
|
|
383
|
+
data = json.loads(raw)
|
|
384
|
+
except (TypeError, ValueError):
|
|
385
|
+
return {}
|
|
386
|
+
if not isinstance(data, dict):
|
|
387
|
+
return {}
|
|
388
|
+
return {key: data.get(key) for key in SEMANTIC_CHECKPOINT_FIELDS}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _sync_semantic_checkpoint(status_data, args):
|
|
392
|
+
semantic = _load_checkpoint_state(getattr(args, "checkpoint_state", None))
|
|
393
|
+
if semantic:
|
|
394
|
+
status_data.update(semantic)
|
|
395
|
+
return {key: status_data.get(key) for key in SEMANTIC_CHECKPOINT_FIELDS}
|
|
396
|
+
|
|
397
|
+
|
|
361
398
|
def _set_continuation_common(status_data, args, session_id):
|
|
362
399
|
if args.active_dev_branch:
|
|
363
400
|
status_data["active_dev_branch"] = args.active_dev_branch
|
|
@@ -886,6 +923,7 @@ def action_update(args, feature_list_path, state_dir):
|
|
|
886
923
|
fingerprint = _load_progress_fingerprint(args.progress_fingerprint)
|
|
887
924
|
if fingerprint is not None:
|
|
888
925
|
fs["last_progress_fingerprint"] = fingerprint
|
|
926
|
+
semantic_checkpoint = _sync_semantic_checkpoint(fs, args)
|
|
889
927
|
|
|
890
928
|
|
|
891
929
|
if session_status == "success":
|
|
@@ -984,6 +1022,19 @@ def action_update(args, feature_list_path, state_dir):
|
|
|
984
1022
|
if err:
|
|
985
1023
|
error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
|
|
986
1024
|
return
|
|
1025
|
+
elif session_status == "workflow_blocked":
|
|
1026
|
+
# Semantic terminal state: preserve partial work without consuming any retry budget.
|
|
1027
|
+
new_status = "failed"
|
|
1028
|
+
fs["continuation_pending"] = False
|
|
1029
|
+
fs["needs_attention"] = True
|
|
1030
|
+
fs["terminal_status"] = "WORKFLOW_BLOCKED"
|
|
1031
|
+
fs["blocked_reason"] = (
|
|
1032
|
+
semantic_checkpoint.get("blocked_reason") or "workflow_blocked"
|
|
1033
|
+
)
|
|
1034
|
+
err = update_feature_in_list(feature_list_path, feature_id, new_status)
|
|
1035
|
+
if err:
|
|
1036
|
+
error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
|
|
1037
|
+
return
|
|
987
1038
|
elif session_status == "stalled_context_continuation":
|
|
988
1039
|
# No-progress context-overflow stall pauses automation without consuming retry budgets.
|
|
989
1040
|
set_stalled_context_continuation_metadata(fs, args, session_id)
|
|
@@ -1052,6 +1103,7 @@ def action_update(args, feature_list_path, state_dir):
|
|
|
1052
1103
|
"infra_error_count": fs.get("infra_error_count", 0),
|
|
1053
1104
|
"max_infra_retries": max_infra_retries,
|
|
1054
1105
|
"resume_from_phase": fs.get("resume_from_phase"),
|
|
1106
|
+
"checkpoint_state": semantic_checkpoint,
|
|
1055
1107
|
"updated_at": fs["updated_at"],
|
|
1056
1108
|
}
|
|
1057
1109
|
if auto_skipped_features:
|
|
@@ -1068,6 +1120,11 @@ def action_update(args, feature_list_path, state_dir):
|
|
|
1068
1120
|
summary["restart_policy"] = "context_overflow_continuation"
|
|
1069
1121
|
summary.update(continuation_metadata_summary(fs))
|
|
1070
1122
|
summary["artifacts_preserved"] = True
|
|
1123
|
+
elif session_status == "workflow_blocked":
|
|
1124
|
+
summary["restart_policy"] = "terminal_blocked"
|
|
1125
|
+
summary.update(continuation_metadata_summary(fs))
|
|
1126
|
+
summary["checkpoint_state"] = semantic_checkpoint
|
|
1127
|
+
summary["artifacts_preserved"] = True
|
|
1071
1128
|
elif session_status == "stalled_context_continuation":
|
|
1072
1129
|
summary["restart_policy"] = "needs_attention"
|
|
1073
1130
|
summary.update(continuation_metadata_summary(fs))
|
|
@@ -47,6 +47,7 @@ SESSION_STATUS_VALUES = [
|
|
|
47
47
|
"infra_error",
|
|
48
48
|
"context_overflow",
|
|
49
49
|
"stalled_context_continuation",
|
|
50
|
+
"workflow_blocked",
|
|
50
51
|
"commit_missing",
|
|
51
52
|
"docs_missing",
|
|
52
53
|
"merge_conflict",
|
|
@@ -100,6 +101,7 @@ def parse_args():
|
|
|
100
101
|
parser.add_argument("--continuation-summary-path", default=None, help="Optional continuation summary artifact path.")
|
|
101
102
|
parser.add_argument("--no-progress-count", type=int, default=None, help="Consecutive context-overflow sessions without objective progress.")
|
|
102
103
|
parser.add_argument("--progress-fingerprint", default=None, help="JSON progress fingerprint captured after the session.")
|
|
104
|
+
parser.add_argument("--checkpoint-state", default=None, help="JSON semantic L4 checkpoint snapshot captured after the session.")
|
|
103
105
|
return parser.parse_args()
|
|
104
106
|
|
|
105
107
|
|
|
@@ -252,6 +254,37 @@ def _load_progress_fingerprint(raw):
|
|
|
252
254
|
return {"unparsed": str(raw)}
|
|
253
255
|
|
|
254
256
|
|
|
257
|
+
SEMANTIC_CHECKPOINT_FIELDS = (
|
|
258
|
+
"current_stage",
|
|
259
|
+
"next_stage",
|
|
260
|
+
"stage_result",
|
|
261
|
+
"repair_scope",
|
|
262
|
+
"repair_round",
|
|
263
|
+
"authoritative_evidence_paths",
|
|
264
|
+
"blocked_reason",
|
|
265
|
+
"terminal_status",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _load_checkpoint_state(raw):
|
|
270
|
+
if not raw:
|
|
271
|
+
return {}
|
|
272
|
+
try:
|
|
273
|
+
data = json.loads(raw)
|
|
274
|
+
except (TypeError, ValueError):
|
|
275
|
+
return {}
|
|
276
|
+
if not isinstance(data, dict):
|
|
277
|
+
return {}
|
|
278
|
+
return {key: data.get(key) for key in SEMANTIC_CHECKPOINT_FIELDS}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _sync_semantic_checkpoint(status_data, args):
|
|
282
|
+
semantic = _load_checkpoint_state(getattr(args, "checkpoint_state", None))
|
|
283
|
+
if semantic:
|
|
284
|
+
status_data.update(semantic)
|
|
285
|
+
return {key: status_data.get(key) for key in SEMANTIC_CHECKPOINT_FIELDS}
|
|
286
|
+
|
|
287
|
+
|
|
255
288
|
def _set_continuation_common(status_data, args, session_id):
|
|
256
289
|
if args.active_dev_branch:
|
|
257
290
|
status_data["active_dev_branch"] = args.active_dev_branch
|
|
@@ -498,6 +531,7 @@ def action_update(args, refactor_list_path, state_dir):
|
|
|
498
531
|
fingerprint = _load_progress_fingerprint(args.progress_fingerprint)
|
|
499
532
|
if fingerprint is not None:
|
|
500
533
|
rs["last_progress_fingerprint"] = fingerprint
|
|
534
|
+
semantic_checkpoint = _sync_semantic_checkpoint(rs, args)
|
|
501
535
|
|
|
502
536
|
|
|
503
537
|
if session_status == "success":
|
|
@@ -564,6 +598,19 @@ def action_update(args, refactor_list_path, state_dir):
|
|
|
564
598
|
if err:
|
|
565
599
|
error_out("Failed to update .prizmkit/plans/refactor-list.json: {}".format(err))
|
|
566
600
|
return
|
|
601
|
+
elif session_status == "workflow_blocked":
|
|
602
|
+
# Semantic terminal state: preserve partial work without consuming any retry budget.
|
|
603
|
+
new_status = "failed"
|
|
604
|
+
rs["continuation_pending"] = False
|
|
605
|
+
rs["needs_attention"] = True
|
|
606
|
+
rs["terminal_status"] = "WORKFLOW_BLOCKED"
|
|
607
|
+
rs["blocked_reason"] = (
|
|
608
|
+
semantic_checkpoint.get("blocked_reason") or "workflow_blocked"
|
|
609
|
+
)
|
|
610
|
+
err = update_refactor_in_list(refactor_list_path, refactor_id, new_status)
|
|
611
|
+
if err:
|
|
612
|
+
error_out("Failed to update .prizmkit/plans/refactor-list.json: {}".format(err))
|
|
613
|
+
return
|
|
567
614
|
elif session_status == "stalled_context_continuation":
|
|
568
615
|
# No-progress context-overflow stall pauses automation without consuming retry budgets.
|
|
569
616
|
set_stalled_context_continuation_metadata(rs, args, session_id)
|
|
@@ -627,6 +674,7 @@ def action_update(args, refactor_list_path, state_dir):
|
|
|
627
674
|
"infra_error_count": rs.get("infra_error_count", 0),
|
|
628
675
|
"max_infra_retries": max_infra_retries,
|
|
629
676
|
"resume_from_phase": rs.get("resume_from_phase"),
|
|
677
|
+
"checkpoint_state": semantic_checkpoint,
|
|
630
678
|
"updated_at": rs["updated_at"],
|
|
631
679
|
}
|
|
632
680
|
if auto_skipped_refactors:
|
|
@@ -643,6 +691,11 @@ def action_update(args, refactor_list_path, state_dir):
|
|
|
643
691
|
summary["restart_policy"] = "context_overflow_continuation"
|
|
644
692
|
summary.update(continuation_metadata_summary(rs))
|
|
645
693
|
summary["artifacts_preserved"] = True
|
|
694
|
+
elif session_status == "workflow_blocked":
|
|
695
|
+
summary["restart_policy"] = "terminal_blocked"
|
|
696
|
+
summary.update(continuation_metadata_summary(rs))
|
|
697
|
+
summary["checkpoint_state"] = semantic_checkpoint
|
|
698
|
+
summary["artifacts_preserved"] = True
|
|
646
699
|
elif session_status == "stalled_context_continuation":
|
|
647
700
|
summary["restart_policy"] = "needs_attention"
|
|
648
701
|
summary.update(continuation_metadata_summary(rs))
|
|
@@ -30,9 +30,9 @@ Infer what needs to be done from the feature context above and follow the standa
|
|
|
30
30
|
|
|
31
31
|
3. **Implement**: Run `/prizmkit-implement` with `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` to execute the plan using TDD (write tests first, then implement).
|
|
32
32
|
|
|
33
|
-
4. **Review**: Run `/prizmkit-code-review` with `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/`. The current Main Agent is the only Code Review executor. It reviews the complete current change for up to ten rounds, directly repairs accepted findings,
|
|
33
|
+
4. **Review**: Run `/prizmkit-code-review` with `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/`. The current Main Agent is the only Code Review executor. It reviews the complete current change for up to ten internal rounds, directly repairs accepted findings, and requires the last `## Final Result`. `REVIEW_PASS` proceeds to test; `REVIEW_NEEDS_FIXES` increments the shared outer repair round and routes implement → code-review → test. Do not delegate review or use another review entry point.
|
|
34
34
|
|
|
35
|
-
5. **Test**:
|
|
35
|
+
5. **Test**: After review passes, run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/`. Treat the Markdown report only as a derived view. Consume `.prizmkit/test/evidence/<evidence-id>/manifest.json`, `verdict.json`, and `validation.json`; verify identity/scope, every manifest hash, verdict/validator agreement, and current target hashes before accepting `TEST_PASS`. `TEST_FAIL/test-infrastructure` routes implement → test; production/runtime/schema/dependency/public-interface routes implement → code-review → test; unknown unsafe scope blocks. `TEST_BLOCKED` allows bounded environment recovery or a test-stage resume but never speculative production edits. Stop outer repairs after three rounds; preserve evidence and truthful recovery metadata.
|
|
36
36
|
|
|
37
37
|
6. **Retrospective**: Run `/prizmkit-retrospective` to sync `.prizmkit/prizm-docs/` with code changes.
|
|
38
38
|
|