prizmkit 1.1.99 → 1.1.100
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/lib/common.sh +244 -25
- package/bundled/dev-pipeline/reset-bug.sh +30 -0
- package/bundled/dev-pipeline/reset-feature.sh +30 -0
- package/bundled/dev-pipeline/reset-refactor.sh +30 -0
- package/bundled/dev-pipeline/run-bugfix.sh +128 -13
- package/bundled/dev-pipeline/run-feature.sh +130 -14
- package/bundled/dev-pipeline/run-refactor.sh +128 -13
- package/bundled/dev-pipeline/scripts/check-session-status.py +74 -1
- package/bundled/dev-pipeline/scripts/continuation.py +374 -0
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +27 -30
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +15 -20
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +15 -20
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +146 -16
- package/bundled/dev-pipeline/scripts/update-bug-status.py +214 -6
- package/bundled/dev-pipeline/scripts/update-feature-status.py +237 -6
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +214 -6
- package/bundled/dev-pipeline/templates/agent-prompts/dev-implement.md +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +2 -2
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +3 -3
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +3 -3
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -3
- package/bundled/dev-pipeline/templates/sections/log-size-awareness.md +31 -66
- package/bundled/dev-pipeline/templates/session-status-schema.json +1 -1
- package/bundled/dev-pipeline/tests/conftest.py +1 -0
- package/bundled/dev-pipeline/tests/test_auto_skip.py +510 -0
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +103 -0
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +60 -0
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +43 -0
- package/bundled/dev-pipeline-windows/lib/common.ps1 +172 -10
- package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +93 -9
- package/bundled/dev-pipeline-windows/lib/reset.ps1 +34 -0
- package/bundled/dev-pipeline-windows/reset-bug.ps1 +1 -0
- package/bundled/dev-pipeline-windows/reset-feature.ps1 +1 -0
- package/bundled/dev-pipeline-windows/reset-refactor.ps1 +1 -0
- package/bundled/dev-pipeline-windows/scripts/check-session-status.py +74 -1
- package/bundled/dev-pipeline-windows/scripts/continuation.py +374 -0
- package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +27 -30
- package/bundled/dev-pipeline-windows/scripts/generate-bugfix-prompt.py +16 -20
- package/bundled/dev-pipeline-windows/scripts/generate-refactor-prompt.py +16 -20
- package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +146 -16
- package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +214 -6
- package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +237 -6
- package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +214 -6
- package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-implement.md +1 -1
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +2 -2
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +3 -3
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +3 -3
- package/bundled/dev-pipeline-windows/templates/bugfix-bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline-windows/templates/refactor-bootstrap-prompt.md +3 -3
- package/bundled/dev-pipeline-windows/templates/sections/log-size-awareness.md +31 -68
- package/bundled/dev-pipeline-windows/templates/session-status-schema.json +1 -1
- package/bundled/skills/_metadata.json +1 -1
- package/package.json +1 -1
- package/bundled/dev-pipeline/scripts/monitor-log.sh +0 -104
- package/bundled/dev-pipeline-windows/scripts/monitor-log.ps1 +0 -102
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
5
8
|
|
|
6
9
|
from update_feature_status import (
|
|
7
10
|
auto_skip_blocked_features,
|
|
@@ -70,6 +73,455 @@ def _read_statuses(fl_path):
|
|
|
70
73
|
return {f["id"]: f["status"] for f in data["features"]}
|
|
71
74
|
|
|
72
75
|
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
# context_overflow retry semantics
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
class TestContextOverflowRetrySemantics:
|
|
83
|
+
def _script_cases(self, scripts_dir):
|
|
84
|
+
return [
|
|
85
|
+
{
|
|
86
|
+
"script": scripts_dir / "update-feature-status.py",
|
|
87
|
+
"list_option": "--feature-list",
|
|
88
|
+
"id_option": "--feature-id",
|
|
89
|
+
"id": "F-001",
|
|
90
|
+
"list_name": "feature-list.json",
|
|
91
|
+
"collection": "features",
|
|
92
|
+
"list_payload": {
|
|
93
|
+
"features": [_make_feature("F-001", "Context Overflow", status="in_progress")],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"script": scripts_dir / "update-bug-status.py",
|
|
98
|
+
"list_option": "--bug-list",
|
|
99
|
+
"id_option": "--bug-id",
|
|
100
|
+
"id": "B-001",
|
|
101
|
+
"list_name": "bug-fix-list.json",
|
|
102
|
+
"collection": "bugs",
|
|
103
|
+
"list_payload": {
|
|
104
|
+
"bugs": [{
|
|
105
|
+
"id": "B-001",
|
|
106
|
+
"title": "Context Overflow Bug",
|
|
107
|
+
"description": "desc",
|
|
108
|
+
"severity": "medium",
|
|
109
|
+
"priority": "medium",
|
|
110
|
+
"status": "in_progress",
|
|
111
|
+
}],
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"script": scripts_dir / "update-refactor-status.py",
|
|
116
|
+
"list_option": "--refactor-list",
|
|
117
|
+
"id_option": "--refactor-id",
|
|
118
|
+
"id": "R-001",
|
|
119
|
+
"list_name": "refactor-list.json",
|
|
120
|
+
"collection": "refactors",
|
|
121
|
+
"list_payload": {
|
|
122
|
+
"refactors": [{
|
|
123
|
+
"id": "R-001",
|
|
124
|
+
"title": "Context Overflow Refactor",
|
|
125
|
+
"description": "desc",
|
|
126
|
+
"complexity": "medium",
|
|
127
|
+
"priority": "medium",
|
|
128
|
+
"status": "in_progress",
|
|
129
|
+
"dependencies": [],
|
|
130
|
+
}],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
def _run_case_update(self, case, list_path, state_dir, session_status, session_id="sess-1", extra_args=None):
|
|
136
|
+
cmd = [
|
|
137
|
+
"python3",
|
|
138
|
+
str(case["script"]),
|
|
139
|
+
case["list_option"],
|
|
140
|
+
str(list_path),
|
|
141
|
+
"--state-dir",
|
|
142
|
+
str(state_dir),
|
|
143
|
+
case["id_option"],
|
|
144
|
+
case["id"],
|
|
145
|
+
"--session-id",
|
|
146
|
+
session_id,
|
|
147
|
+
"--session-status",
|
|
148
|
+
session_status,
|
|
149
|
+
"--max-retries",
|
|
150
|
+
"3",
|
|
151
|
+
"--max-infra-retries",
|
|
152
|
+
"3",
|
|
153
|
+
"--action",
|
|
154
|
+
"update",
|
|
155
|
+
]
|
|
156
|
+
if extra_args:
|
|
157
|
+
cmd.extend(extra_args)
|
|
158
|
+
result = subprocess.run(cmd, text=True, capture_output=True, check=True)
|
|
159
|
+
return json.loads(result.stdout)
|
|
160
|
+
|
|
161
|
+
def _runtime_status(self, state_dir, item_id):
|
|
162
|
+
with open(Path(state_dir) / item_id / "status.json", encoding="utf-8") as fh:
|
|
163
|
+
return json.load(fh)
|
|
164
|
+
|
|
165
|
+
def _assert_no_continuation_runtime_state(self, status):
|
|
166
|
+
for key in [
|
|
167
|
+
"active_dev_branch",
|
|
168
|
+
"base_branch",
|
|
169
|
+
"continuation_pending",
|
|
170
|
+
"continuation_reason",
|
|
171
|
+
"last_context_overflow_session_id",
|
|
172
|
+
"previous_session_id",
|
|
173
|
+
"last_fatal_error_code",
|
|
174
|
+
"continuation_summary_path",
|
|
175
|
+
"no_progress_count",
|
|
176
|
+
"last_progress_fingerprint",
|
|
177
|
+
"needs_attention",
|
|
178
|
+
"stalled_context_continuation",
|
|
179
|
+
"stalled_reason",
|
|
180
|
+
"context_overflow_count",
|
|
181
|
+
"continuation_count",
|
|
182
|
+
]:
|
|
183
|
+
assert key not in status
|
|
184
|
+
|
|
185
|
+
def test_context_overflow_stays_in_progress_and_records_continuation_metadata(self, tmp_path):
|
|
186
|
+
scripts_dir = Path(__file__).resolve().parents[1] / "scripts"
|
|
187
|
+
|
|
188
|
+
for case in self._script_cases(scripts_dir):
|
|
189
|
+
case_dir = tmp_path / case["id"]
|
|
190
|
+
state_dir = case_dir / "state"
|
|
191
|
+
list_path = case_dir / case["list_name"]
|
|
192
|
+
session_dir = state_dir / case["id"] / "sessions" / "sess-1"
|
|
193
|
+
session_dir.mkdir(parents=True)
|
|
194
|
+
(session_dir / "session.log").write_text("context overflow diagnostics", encoding="utf-8")
|
|
195
|
+
write_json_file(str(list_path), case["list_payload"])
|
|
196
|
+
status_path = state_dir / case["id"] / "status.json"
|
|
197
|
+
write_json_file(str(status_path), {
|
|
198
|
+
"retry_count": 2,
|
|
199
|
+
"infra_error_count": 1,
|
|
200
|
+
"sessions": [],
|
|
201
|
+
"last_session_id": None,
|
|
202
|
+
"resume_from_phase": None,
|
|
203
|
+
})
|
|
204
|
+
before_mtime = list_path.stat().st_mtime_ns
|
|
205
|
+
time.sleep(0.01)
|
|
206
|
+
|
|
207
|
+
summary = self._run_case_update(
|
|
208
|
+
case,
|
|
209
|
+
list_path,
|
|
210
|
+
state_dir,
|
|
211
|
+
"context_overflow",
|
|
212
|
+
extra_args=[
|
|
213
|
+
"--active-dev-branch",
|
|
214
|
+
"dev/existing-branch",
|
|
215
|
+
"--base-branch",
|
|
216
|
+
"main",
|
|
217
|
+
"--last-fatal-error-code",
|
|
218
|
+
"context_overflow",
|
|
219
|
+
"--continuation-summary-path",
|
|
220
|
+
str(session_dir / "continuation-summary.md"),
|
|
221
|
+
],
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
assert summary["new_status"] == "in_progress"
|
|
225
|
+
assert summary["retry_count"] == 2
|
|
226
|
+
assert summary["infra_error_count"] == 1
|
|
227
|
+
assert summary["context_overflow_count"] == 1
|
|
228
|
+
assert summary["continuation_count"] == 1
|
|
229
|
+
assert summary["continuation_pending"] is True
|
|
230
|
+
assert summary["restart_policy"] == "context_overflow_continuation"
|
|
231
|
+
assert summary["artifacts_preserved"] is True
|
|
232
|
+
assert "cleanup_performed" not in summary
|
|
233
|
+
assert (session_dir / "session.log").exists()
|
|
234
|
+
assert list_path.stat().st_mtime_ns == before_mtime
|
|
235
|
+
|
|
236
|
+
list_data, _ = load_json_file(str(list_path))
|
|
237
|
+
assert list_data[case["collection"]][0]["status"] == "in_progress"
|
|
238
|
+
|
|
239
|
+
runtime = self._runtime_status(state_dir, case["id"])
|
|
240
|
+
assert runtime["retry_count"] == 2
|
|
241
|
+
assert runtime["infra_error_count"] == 1
|
|
242
|
+
assert runtime["active_dev_branch"] == "dev/existing-branch"
|
|
243
|
+
assert runtime["base_branch"] == "main"
|
|
244
|
+
assert runtime["continuation_pending"] is True
|
|
245
|
+
assert runtime["continuation_reason"] == "context_overflow"
|
|
246
|
+
assert runtime["last_context_overflow_session_id"] == "sess-1"
|
|
247
|
+
assert runtime["last_fatal_error_code"] == "context_overflow"
|
|
248
|
+
assert runtime["context_overflow_count"] == 1
|
|
249
|
+
assert runtime["continuation_count"] == 1
|
|
250
|
+
assert runtime["continuation_summary_path"].endswith("continuation-summary.md")
|
|
251
|
+
assert runtime["last_session_id"] == "sess-1"
|
|
252
|
+
assert runtime.get("last_infra_error_session_id") is None
|
|
253
|
+
|
|
254
|
+
get_next = subprocess.run(
|
|
255
|
+
[
|
|
256
|
+
"python3",
|
|
257
|
+
str(case["script"]),
|
|
258
|
+
case["list_option"],
|
|
259
|
+
str(list_path),
|
|
260
|
+
"--state-dir",
|
|
261
|
+
str(state_dir),
|
|
262
|
+
"--action",
|
|
263
|
+
"get_next",
|
|
264
|
+
],
|
|
265
|
+
text=True,
|
|
266
|
+
capture_output=True,
|
|
267
|
+
check=True,
|
|
268
|
+
)
|
|
269
|
+
selected = json.loads(get_next.stdout)
|
|
270
|
+
assert selected["active_dev_branch"] == "dev/existing-branch"
|
|
271
|
+
assert selected["base_branch"] == "main"
|
|
272
|
+
assert selected["continuation_pending"] is True
|
|
273
|
+
assert selected["continuation_reason"] == "context_overflow"
|
|
274
|
+
assert selected["last_context_overflow_session_id"] == "sess-1"
|
|
275
|
+
assert selected["last_fatal_error_code"] == "context_overflow"
|
|
276
|
+
assert selected["context_overflow_count"] == 1
|
|
277
|
+
assert selected["continuation_count"] == 1
|
|
278
|
+
assert selected["continuation_summary_path"].endswith("continuation-summary.md")
|
|
279
|
+
|
|
280
|
+
def test_context_overflow_from_non_started_item_marks_in_progress_once(self, tmp_path):
|
|
281
|
+
scripts_dir = Path(__file__).resolve().parents[1] / "scripts"
|
|
282
|
+
|
|
283
|
+
for case in self._script_cases(scripts_dir):
|
|
284
|
+
case_dir = tmp_path / f"pending-{case['id']}"
|
|
285
|
+
state_dir = case_dir / "state"
|
|
286
|
+
list_path = case_dir / case["list_name"]
|
|
287
|
+
payload = json.loads(json.dumps(case["list_payload"]))
|
|
288
|
+
payload[case["collection"]][0]["status"] = "pending"
|
|
289
|
+
write_json_file(str(list_path), payload)
|
|
290
|
+
|
|
291
|
+
summary = self._run_case_update(
|
|
292
|
+
case,
|
|
293
|
+
list_path,
|
|
294
|
+
state_dir,
|
|
295
|
+
"context_overflow",
|
|
296
|
+
extra_args=["--active-dev-branch", "dev/new", "--base-branch", "main"],
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
list_data, _ = load_json_file(str(list_path))
|
|
300
|
+
assert summary["new_status"] == "in_progress"
|
|
301
|
+
assert list_data[case["collection"]][0]["status"] == "in_progress"
|
|
302
|
+
|
|
303
|
+
def test_success_clears_pending_continuation_fields_but_preserves_counters(self, tmp_path):
|
|
304
|
+
scripts_dir = Path(__file__).resolve().parents[1] / "scripts"
|
|
305
|
+
|
|
306
|
+
for case in self._script_cases(scripts_dir):
|
|
307
|
+
case_dir = tmp_path / f"success-{case['id']}"
|
|
308
|
+
state_dir = case_dir / "state"
|
|
309
|
+
list_path = case_dir / case["list_name"]
|
|
310
|
+
write_json_file(str(list_path), case["list_payload"])
|
|
311
|
+
status_path = state_dir / case["id"] / "status.json"
|
|
312
|
+
status_path.parent.mkdir(parents=True)
|
|
313
|
+
write_json_file(str(status_path), {
|
|
314
|
+
"retry_count": 0,
|
|
315
|
+
"infra_error_count": 0,
|
|
316
|
+
"active_dev_branch": "dev/existing-branch",
|
|
317
|
+
"base_branch": "main",
|
|
318
|
+
"continuation_pending": True,
|
|
319
|
+
"continuation_reason": "context_overflow",
|
|
320
|
+
"last_context_overflow_session_id": "sess-overflow",
|
|
321
|
+
"last_fatal_error_code": "context_overflow",
|
|
322
|
+
"continuation_summary_path": "summary.md",
|
|
323
|
+
"context_overflow_count": 2,
|
|
324
|
+
"continuation_count": 2,
|
|
325
|
+
"sessions": [],
|
|
326
|
+
"last_session_id": None,
|
|
327
|
+
"resume_from_phase": None,
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
summary = self._run_case_update(case, list_path, state_dir, "success", session_id="sess-success")
|
|
331
|
+
|
|
332
|
+
assert summary["new_status"] == "completed"
|
|
333
|
+
runtime = self._runtime_status(state_dir, case["id"])
|
|
334
|
+
for key in [
|
|
335
|
+
"active_dev_branch",
|
|
336
|
+
"base_branch",
|
|
337
|
+
"continuation_pending",
|
|
338
|
+
"continuation_reason",
|
|
339
|
+
"last_context_overflow_session_id",
|
|
340
|
+
"last_fatal_error_code",
|
|
341
|
+
"continuation_summary_path",
|
|
342
|
+
]:
|
|
343
|
+
assert key not in runtime
|
|
344
|
+
assert runtime["context_overflow_count"] == 2
|
|
345
|
+
assert runtime["continuation_count"] == 2
|
|
346
|
+
|
|
347
|
+
def test_manual_reset_clean_and_unskip_clear_all_continuation_state(self, tmp_path):
|
|
348
|
+
scripts_dir = Path(__file__).resolve().parents[1] / "scripts"
|
|
349
|
+
|
|
350
|
+
for action in ["reset", "clean", "unskip"]:
|
|
351
|
+
for case in self._script_cases(scripts_dir):
|
|
352
|
+
case_dir = tmp_path / f"{action}-{case['id']}"
|
|
353
|
+
state_dir = case_dir / "state"
|
|
354
|
+
list_path = case_dir / case["list_name"]
|
|
355
|
+
payload = json.loads(json.dumps(case["list_payload"]))
|
|
356
|
+
payload[case["collection"]][0]["status"] = "failed"
|
|
357
|
+
write_json_file(str(list_path), payload)
|
|
358
|
+
status_path = state_dir / case["id"] / "status.json"
|
|
359
|
+
status_path.parent.mkdir(parents=True)
|
|
360
|
+
write_json_file(str(status_path), {
|
|
361
|
+
"retry_count": 3,
|
|
362
|
+
"infra_error_count": 2,
|
|
363
|
+
"active_dev_branch": "dev/existing-branch",
|
|
364
|
+
"base_branch": "main",
|
|
365
|
+
"continuation_pending": True,
|
|
366
|
+
"continuation_reason": "context_overflow",
|
|
367
|
+
"last_context_overflow_session_id": "sess-overflow",
|
|
368
|
+
"previous_session_id": "sess-overflow",
|
|
369
|
+
"last_fatal_error_code": "context_overflow",
|
|
370
|
+
"continuation_summary_path": "continuation-summary.md",
|
|
371
|
+
"no_progress_count": 1,
|
|
372
|
+
"last_progress_fingerprint": {"checkpoint": "old"},
|
|
373
|
+
"needs_attention": True,
|
|
374
|
+
"stalled_context_continuation": True,
|
|
375
|
+
"stalled_reason": "context_overflow_without_git_checkpoint_or_artifact_progress",
|
|
376
|
+
"context_overflow_count": 2,
|
|
377
|
+
"continuation_count": 2,
|
|
378
|
+
"sessions": ["old"],
|
|
379
|
+
"last_session_id": "old",
|
|
380
|
+
"resume_from_phase": 4,
|
|
381
|
+
})
|
|
382
|
+
continuation_files = []
|
|
383
|
+
if action == "clean":
|
|
384
|
+
stored_summary = case_dir / "continuation-summary.md"
|
|
385
|
+
stored_summary.write_text("stored", encoding="utf-8")
|
|
386
|
+
continuation_files.append(stored_summary)
|
|
387
|
+
if case["id_option"] == "--feature-id":
|
|
388
|
+
conventional_summary = case_dir / ".prizmkit" / "specs" / "001-context-overflow" / "continuation-summary.md"
|
|
389
|
+
elif case["id_option"] == "--bug-id":
|
|
390
|
+
conventional_summary = case_dir / ".prizmkit" / "bugfix" / case["id"] / "continuation-summary.md"
|
|
391
|
+
else:
|
|
392
|
+
conventional_summary = case_dir / ".prizmkit" / "refactor" / case["id"] / "continuation-summary.md"
|
|
393
|
+
conventional_summary.parent.mkdir(parents=True)
|
|
394
|
+
conventional_summary.write_text("conventional", encoding="utf-8")
|
|
395
|
+
continuation_files.append(conventional_summary)
|
|
396
|
+
|
|
397
|
+
cmd = [
|
|
398
|
+
"python3",
|
|
399
|
+
str(case["script"]),
|
|
400
|
+
case["list_option"],
|
|
401
|
+
str(list_path),
|
|
402
|
+
"--state-dir",
|
|
403
|
+
str(state_dir),
|
|
404
|
+
"--action",
|
|
405
|
+
action,
|
|
406
|
+
]
|
|
407
|
+
if action in ("reset", "clean"):
|
|
408
|
+
cmd.extend([case["id_option"], case["id"]])
|
|
409
|
+
elif action == "unskip":
|
|
410
|
+
cmd.extend([case["id_option"], case["id"]])
|
|
411
|
+
if action == "clean":
|
|
412
|
+
cmd.extend(["--project-root", str(case_dir)])
|
|
413
|
+
if case["id_option"] == "--feature-id":
|
|
414
|
+
cmd.extend(["--feature-slug", "001-context-overflow"])
|
|
415
|
+
|
|
416
|
+
result = subprocess.run(cmd, text=True, capture_output=True, check=True)
|
|
417
|
+
assert result.returncode == 0
|
|
418
|
+
for continuation_file in continuation_files:
|
|
419
|
+
assert not continuation_file.exists()
|
|
420
|
+
runtime = self._runtime_status(state_dir, case["id"])
|
|
421
|
+
self._assert_no_continuation_runtime_state(runtime)
|
|
422
|
+
assert runtime["retry_count"] == 0
|
|
423
|
+
assert runtime["infra_error_count"] == 0
|
|
424
|
+
assert runtime["last_infra_error_session_id"] is None
|
|
425
|
+
list_data, _ = load_json_file(str(list_path))
|
|
426
|
+
assert list_data[case["collection"]][0]["status"] == "pending"
|
|
427
|
+
def test_unix_reset_scripts_warn_before_manual_continuation_reset(self, tmp_path):
|
|
428
|
+
project_root = Path(__file__).resolve().parents[2]
|
|
429
|
+
script_cases = [
|
|
430
|
+
{
|
|
431
|
+
"script": "reset-feature.sh",
|
|
432
|
+
"list_name": "feature-list.json",
|
|
433
|
+
"state_root": "features",
|
|
434
|
+
"item_id": "F-001",
|
|
435
|
+
"list_payload": {
|
|
436
|
+
"features": [_make_feature("F-001", "Context Overflow", status="failed")],
|
|
437
|
+
},
|
|
438
|
+
"pipeline_error": "run-feature.sh run",
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
"script": "reset-bug.sh",
|
|
442
|
+
"list_name": "bug-fix-list.json",
|
|
443
|
+
"state_root": "bugfix",
|
|
444
|
+
"item_id": "B-001",
|
|
445
|
+
"list_payload": {
|
|
446
|
+
"bugs": [{
|
|
447
|
+
"id": "B-001",
|
|
448
|
+
"title": "Context Overflow Bug",
|
|
449
|
+
"description": "desc",
|
|
450
|
+
"severity": "medium",
|
|
451
|
+
"priority": "medium",
|
|
452
|
+
"status": "failed",
|
|
453
|
+
}],
|
|
454
|
+
},
|
|
455
|
+
"pipeline_error": "run-bugfix.sh run",
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
"script": "reset-refactor.sh",
|
|
459
|
+
"list_name": "refactor-list.json",
|
|
460
|
+
"state_root": "refactor",
|
|
461
|
+
"item_id": "R-001",
|
|
462
|
+
"list_payload": {
|
|
463
|
+
"refactors": [{
|
|
464
|
+
"id": "R-001",
|
|
465
|
+
"title": "Context Overflow Refactor",
|
|
466
|
+
"description": "desc",
|
|
467
|
+
"complexity": "medium",
|
|
468
|
+
"priority": "medium",
|
|
469
|
+
"status": "failed",
|
|
470
|
+
"dependencies": [],
|
|
471
|
+
}],
|
|
472
|
+
},
|
|
473
|
+
"pipeline_error": "run-refactor.sh run",
|
|
474
|
+
},
|
|
475
|
+
]
|
|
476
|
+
|
|
477
|
+
for case in script_cases:
|
|
478
|
+
case_dir = tmp_path / case["item_id"]
|
|
479
|
+
prizmkit_dir = case_dir / ".prizmkit"
|
|
480
|
+
state_dir = prizmkit_dir / "state" / case["state_root"]
|
|
481
|
+
item_state_dir = state_dir / case["item_id"]
|
|
482
|
+
item_state_dir.mkdir(parents=True)
|
|
483
|
+
write_json_file(str(prizmkit_dir / "plans" / case["list_name"]), case["list_payload"])
|
|
484
|
+
write_json_file(str(state_dir / "pipeline.json"), {"status": "running"})
|
|
485
|
+
write_json_file(str(item_state_dir / "status.json"), {
|
|
486
|
+
"retry_count": 1,
|
|
487
|
+
"infra_error_count": 0,
|
|
488
|
+
"continuation_pending": True,
|
|
489
|
+
"continuation_reason": "context_overflow",
|
|
490
|
+
"active_dev_branch": "dev/manual-continuation",
|
|
491
|
+
"sessions": [],
|
|
492
|
+
})
|
|
493
|
+
subprocess.run(["git", "init", "-q"], cwd=case_dir, check=True)
|
|
494
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=case_dir, check=True)
|
|
495
|
+
subprocess.run(["git", "config", "user.name", "Test User"], cwd=case_dir, check=True)
|
|
496
|
+
(case_dir / "README.md").write_text("test\n", encoding="utf-8")
|
|
497
|
+
subprocess.run(["git", "add", "README.md"], cwd=case_dir, check=True)
|
|
498
|
+
subprocess.run(["git", "commit", "-q", "-m", "init"], cwd=case_dir, check=True)
|
|
499
|
+
|
|
500
|
+
result = subprocess.run(
|
|
501
|
+
["bash", str(project_root / "dev-pipeline" / case["script"]), case["item_id"], str(prizmkit_dir / "plans" / case["list_name"])],
|
|
502
|
+
cwd=case_dir,
|
|
503
|
+
text=True,
|
|
504
|
+
capture_output=True,
|
|
505
|
+
env={
|
|
506
|
+
**os.environ,
|
|
507
|
+
"PROJECT_ROOT": str(case_dir),
|
|
508
|
+
"PRIZMKIT_DIR": str(prizmkit_dir),
|
|
509
|
+
"PIPELINE_DIR": str(project_root / "dev-pipeline"),
|
|
510
|
+
},
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
output = result.stdout + result.stderr
|
|
514
|
+
assert "Manual reset will abandon automatic continuation progress" in output
|
|
515
|
+
assert "Pending continuation branch: dev/manual-continuation" in output
|
|
516
|
+
assert output.index("Manual reset will abandon automatic continuation progress") < output.index("Resetting")
|
|
517
|
+
assert result.returncode == 0, output
|
|
518
|
+
runtime = self._runtime_status(state_dir, case["item_id"])
|
|
519
|
+
self._assert_no_continuation_runtime_state(runtime)
|
|
520
|
+
assert runtime["retry_count"] == 0
|
|
521
|
+
assert runtime["infra_error_count"] == 0
|
|
522
|
+
assert runtime["last_infra_error_session_id"] is None
|
|
523
|
+
|
|
524
|
+
|
|
73
525
|
# ---------------------------------------------------------------------------
|
|
74
526
|
# TERMINAL_STATUSES
|
|
75
527
|
# ---------------------------------------------------------------------------
|
|
@@ -579,3 +1031,61 @@ class TestAutoSkipIntegration:
|
|
|
579
1031
|
assert "status" not in fs
|
|
580
1032
|
statuses = _read_statuses(fl_path)
|
|
581
1033
|
assert statuses["F-001"] == "pending"
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
def test_context_overflow_records_no_progress_fingerprint_and_stall(tmp_path):
|
|
1037
|
+
scripts_dir = Path(__file__).resolve().parents[1] / 'scripts'
|
|
1038
|
+
feature_list = tmp_path / 'feature-list.json'
|
|
1039
|
+
state_dir = tmp_path / 'state'
|
|
1040
|
+
write_json_file(str(feature_list), _make_feature_list([
|
|
1041
|
+
_make_feature('F-001', 'Context Continuation', status='in_progress')
|
|
1042
|
+
]))
|
|
1043
|
+
fingerprint = json.dumps({
|
|
1044
|
+
'git_diff_fingerprint': 'same',
|
|
1045
|
+
'checkpoint_cursor': 'S02:pending',
|
|
1046
|
+
'artifact_fingerprint': 'artifact',
|
|
1047
|
+
'head_commit': 'abc123',
|
|
1048
|
+
})
|
|
1049
|
+
|
|
1050
|
+
first = subprocess.run([
|
|
1051
|
+
'python3', str(scripts_dir / 'update-feature-status.py'),
|
|
1052
|
+
'--feature-list', str(feature_list),
|
|
1053
|
+
'--state-dir', str(state_dir),
|
|
1054
|
+
'--feature-id', 'F-001',
|
|
1055
|
+
'--session-id', 's1',
|
|
1056
|
+
'--session-status', 'context_overflow',
|
|
1057
|
+
'--active-dev-branch', 'dev/F-001',
|
|
1058
|
+
'--base-branch', 'main',
|
|
1059
|
+
'--no-progress-count', '1',
|
|
1060
|
+
'--progress-fingerprint', fingerprint,
|
|
1061
|
+
'--action', 'update',
|
|
1062
|
+
], text=True, capture_output=True, check=True)
|
|
1063
|
+
first_summary = json.loads(first.stdout)
|
|
1064
|
+
assert first_summary['new_status'] == 'in_progress'
|
|
1065
|
+
assert first_summary['retry_count'] == 0
|
|
1066
|
+
assert first_summary['infra_error_count'] == 0
|
|
1067
|
+
assert first_summary['no_progress_count'] == 1
|
|
1068
|
+
|
|
1069
|
+
second = subprocess.run([
|
|
1070
|
+
'python3', str(scripts_dir / 'update-feature-status.py'),
|
|
1071
|
+
'--feature-list', str(feature_list),
|
|
1072
|
+
'--state-dir', str(state_dir),
|
|
1073
|
+
'--feature-id', 'F-001',
|
|
1074
|
+
'--session-id', 's2',
|
|
1075
|
+
'--session-status', 'stalled_context_continuation',
|
|
1076
|
+
'--active-dev-branch', 'dev/F-001',
|
|
1077
|
+
'--base-branch', 'main',
|
|
1078
|
+
'--no-progress-count', '2',
|
|
1079
|
+
'--progress-fingerprint', fingerprint,
|
|
1080
|
+
'--action', 'update',
|
|
1081
|
+
], text=True, capture_output=True, check=True)
|
|
1082
|
+
second_summary = json.loads(second.stdout)
|
|
1083
|
+
status = load_feature_status(str(state_dir), 'F-001')
|
|
1084
|
+
assert second_summary['new_status'] == 'failed'
|
|
1085
|
+
assert second_summary['retry_count'] == 0
|
|
1086
|
+
assert second_summary['infra_error_count'] == 0
|
|
1087
|
+
assert second_summary['needs_attention'] is True
|
|
1088
|
+
assert second_summary['stalled_context_continuation'] is True
|
|
1089
|
+
assert second_summary['stalled_reason'] == 'context_overflow_without_git_checkpoint_or_artifact_progress'
|
|
1090
|
+
assert status['no_progress_count'] == 2
|
|
1091
|
+
assert status['last_progress_fingerprint']['head_commit'] == 'abc123'
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import os
|
|
4
4
|
import re
|
|
5
5
|
import time
|
|
6
|
+
from argparse import Namespace
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
from generate_bootstrap_prompt import (
|
|
@@ -18,7 +19,9 @@ from generate_bootstrap_prompt import (
|
|
|
18
19
|
assemble_sections,
|
|
19
20
|
generate_checkpoint_definition,
|
|
20
21
|
merge_checkpoint_state,
|
|
22
|
+
load_log_size_section,
|
|
21
23
|
)
|
|
24
|
+
from continuation import append_continuation_handoff, checkpoint_cursor
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
def scoped_report_text(verdict="PASS", boundary_missing=0,
|
|
@@ -656,3 +659,103 @@ class TestScopedFeatureTestGate:
|
|
|
656
659
|
merged = merge_checkpoint_state(existing, fresh, str(tmp_path))
|
|
657
660
|
|
|
658
661
|
assert merged["steps"][0]["status"] == "pending"
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
# ---------------------------------------------------------------------------
|
|
665
|
+
# Active compaction harness rollback
|
|
666
|
+
# ---------------------------------------------------------------------------
|
|
667
|
+
|
|
668
|
+
class TestHeadlessRecoveryReframing:
|
|
669
|
+
def test_log_size_awareness_is_passive_checkpoint_guidance(self):
|
|
670
|
+
content = load_log_size_section(str(Path("dev-pipeline/scripts").resolve()))
|
|
671
|
+
|
|
672
|
+
assert "Session Log Budget Awareness" in content
|
|
673
|
+
assert "passive checkpoint guidance" in content
|
|
674
|
+
assert "durable checkpoints and artifacts" in content
|
|
675
|
+
assert "fresh continuation session" in content
|
|
676
|
+
assert "optional interactive convenience only" in content
|
|
677
|
+
assert "COMPACT_NEEDED" not in content
|
|
678
|
+
assert "monitor-log" not in content
|
|
679
|
+
assert "background monitoring subagent" not in content
|
|
680
|
+
assert "Execute `/compact`" not in content
|
|
681
|
+
assert "run `/compact`" not in content.lower()
|
|
682
|
+
|
|
683
|
+
def test_assembled_prompts_do_not_spawn_log_monitor_or_require_compact(self):
|
|
684
|
+
sections_dir = Path("dev-pipeline/templates/sections").resolve()
|
|
685
|
+
|
|
686
|
+
for mode in ("lite", "standard", "full"):
|
|
687
|
+
rendered = "\n".join(
|
|
688
|
+
section for _name, section in assemble_sections(
|
|
689
|
+
mode, str(sections_dir), init_done=True, is_resume=False,
|
|
690
|
+
critic_enabled=False, browser_enabled=False,
|
|
691
|
+
)
|
|
692
|
+
)
|
|
693
|
+
assert "Headless recovery must rely on durable checkpoints" in rendered
|
|
694
|
+
assert "fresh continuation session" in rendered
|
|
695
|
+
assert "monitor-log" not in rendered
|
|
696
|
+
assert "COMPACT_NEEDED" not in rendered
|
|
697
|
+
assert "log-size monitor" not in rendered
|
|
698
|
+
assert "The log-size monitor is the only allowed" not in rendered
|
|
699
|
+
assert "Run this command and keep it running" not in rendered
|
|
700
|
+
assert "Execute `/compact`" not in rendered
|
|
701
|
+
assert "run `/compact` after" not in rendered
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
# ---------------------------------------------------------------------------
|
|
705
|
+
# continuation handoff
|
|
706
|
+
# ---------------------------------------------------------------------------
|
|
707
|
+
|
|
708
|
+
class TestContinuationHandoff:
|
|
709
|
+
def test_addendum_and_summary_include_required_context(self, tmp_path):
|
|
710
|
+
args = Namespace(
|
|
711
|
+
output=str(tmp_path / ".prizmkit/state/features/F-123/sessions/F-123-new/bootstrap-prompt.md"),
|
|
712
|
+
state_dir=str(tmp_path / ".prizmkit/state/features"),
|
|
713
|
+
continuation_mode="context_overflow",
|
|
714
|
+
previous_session_id="F-123-old",
|
|
715
|
+
continuation_count="2",
|
|
716
|
+
context_overflow_count="2",
|
|
717
|
+
active_dev_branch="dev/F-123",
|
|
718
|
+
base_branch="main",
|
|
719
|
+
continuation_summary_path="",
|
|
720
|
+
)
|
|
721
|
+
checkpoint = {"steps": [
|
|
722
|
+
{"id": "S01", "skill": "test-baseline", "status": "completed"},
|
|
723
|
+
{"id": "S02", "skill": "context-snapshot", "status": "skipped"},
|
|
724
|
+
{"id": "S03", "skill": "prizmkit-implement", "status": "pending"},
|
|
725
|
+
]}
|
|
726
|
+
|
|
727
|
+
rendered = append_continuation_handoff(
|
|
728
|
+
"NORMAL BOOTSTRAP", args, str(tmp_path), "feature", "F-123",
|
|
729
|
+
"123-continuation", checkpoint,
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
assert rendered.startswith("NORMAL BOOTSTRAP")
|
|
733
|
+
assert "## Continuation Addendum" in rendered
|
|
734
|
+
assert "This is an automatic continuation after context overflow." in rendered
|
|
735
|
+
assert "This is a fresh headless AI CLI session for the same task." in rendered
|
|
736
|
+
assert "Continue on the current branch/worktree." in rendered
|
|
737
|
+
assert "Do not reset, clean, switch branches, or discard work." in rendered
|
|
738
|
+
assert "Start with `git status` and `git diff`." in rendered
|
|
739
|
+
assert "Treat completed/skipped checkpoint steps as already done." in rendered
|
|
740
|
+
assert "Do not read the previous full `session.log` unless explicitly necessary." in rendered
|
|
741
|
+
assert "prefer synthesized artifacts and summaries over logs" in rendered
|
|
742
|
+
assert "Do not attempt `/compact` as the recovery mechanism." in rendered
|
|
743
|
+
assert ".prizmkit/specs/123-continuation/workflow-checkpoint.json" in rendered
|
|
744
|
+
assert ".prizmkit/state/features/F-123/sessions/F-123-new/logs/session.log" in rendered
|
|
745
|
+
assert ".prizmkit/state/features/F-123/sessions/F-123-new/logs/progress.json" in rendered
|
|
746
|
+
assert ".prizmkit/state/features/F-123/sessions/F-123-new/logs/ai.pid" in rendered
|
|
747
|
+
|
|
748
|
+
summary = tmp_path / ".prizmkit/specs/123-continuation/continuation-summary.md"
|
|
749
|
+
assert summary.exists()
|
|
750
|
+
summary_text = summary.read_text(encoding="utf-8")
|
|
751
|
+
assert "Previous session id: F-123-old" in summary_text
|
|
752
|
+
assert "First pending/in-progress checkpoint step: S03 (prizmkit-implement)" in summary_text
|
|
753
|
+
assert "Workflow checkpoint: .prizmkit/specs/123-continuation/workflow-checkpoint.json" in summary_text
|
|
754
|
+
|
|
755
|
+
def test_checkpoint_cursor_prefers_in_progress_before_pending(self):
|
|
756
|
+
checkpoint = {"steps": [
|
|
757
|
+
{"id": "S01", "skill": "done", "status": "completed"},
|
|
758
|
+
{"id": "S02", "skill": "active", "status": "in_progress"},
|
|
759
|
+
{"id": "S03", "skill": "later", "status": "pending"},
|
|
760
|
+
]}
|
|
761
|
+
assert checkpoint_cursor(checkpoint)["first_cursor"] == "S02 (active)"
|