prizmkit 1.1.98 → 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.
Files changed (79) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/README.md +5 -5
  3. package/bundled/dev-pipeline/launch-bugfix-daemon.sh +1 -1
  4. package/bundled/dev-pipeline/launch-feature-daemon.sh +1 -1
  5. package/bundled/dev-pipeline/launch-refactor-daemon.sh +1 -1
  6. package/bundled/dev-pipeline/lib/common.sh +244 -25
  7. package/bundled/dev-pipeline/reset-bug.sh +32 -2
  8. package/bundled/dev-pipeline/reset-feature.sh +32 -2
  9. package/bundled/dev-pipeline/reset-refactor.sh +32 -2
  10. package/bundled/dev-pipeline/run-bugfix.sh +133 -18
  11. package/bundled/dev-pipeline/run-feature.sh +134 -18
  12. package/bundled/dev-pipeline/run-refactor.sh +133 -18
  13. package/bundled/dev-pipeline/scripts/check-session-status.py +74 -1
  14. package/bundled/dev-pipeline/scripts/continuation.py +374 -0
  15. package/bundled/dev-pipeline/scripts/detect-stuck.py +5 -5
  16. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +29 -35
  17. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +19 -25
  18. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +19 -25
  19. package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +1 -1
  20. package/bundled/dev-pipeline/scripts/init-pipeline.py +1 -1
  21. package/bundled/dev-pipeline/scripts/init-refactor-pipeline.py +1 -1
  22. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +146 -16
  23. package/bundled/dev-pipeline/scripts/update-bug-status.py +220 -13
  24. package/bundled/dev-pipeline/scripts/update-feature-status.py +244 -18
  25. package/bundled/dev-pipeline/scripts/update-refactor-status.py +220 -13
  26. package/bundled/dev-pipeline/templates/agent-prompts/dev-implement.md +1 -1
  27. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +2 -2
  28. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +3 -3
  29. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +3 -3
  30. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +1 -1
  31. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -3
  32. package/bundled/dev-pipeline/templates/sections/log-size-awareness.md +31 -66
  33. package/bundled/dev-pipeline/templates/session-status-schema.json +1 -1
  34. package/bundled/dev-pipeline/tests/conftest.py +1 -0
  35. package/bundled/dev-pipeline/tests/test_auto_skip.py +518 -8
  36. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +103 -0
  37. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +60 -0
  38. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +43 -0
  39. package/bundled/dev-pipeline-windows/lib/common.ps1 +172 -10
  40. package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +95 -11
  41. package/bundled/dev-pipeline-windows/lib/reset.ps1 +34 -0
  42. package/bundled/dev-pipeline-windows/reset-bug.ps1 +1 -0
  43. package/bundled/dev-pipeline-windows/reset-feature.ps1 +1 -0
  44. package/bundled/dev-pipeline-windows/reset-refactor.ps1 +1 -0
  45. package/bundled/dev-pipeline-windows/scripts/check-session-status.py +74 -1
  46. package/bundled/dev-pipeline-windows/scripts/continuation.py +374 -0
  47. package/bundled/dev-pipeline-windows/scripts/detect-stuck.py +5 -5
  48. package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +29 -35
  49. package/bundled/dev-pipeline-windows/scripts/generate-bugfix-prompt.py +20 -25
  50. package/bundled/dev-pipeline-windows/scripts/generate-refactor-prompt.py +20 -25
  51. package/bundled/dev-pipeline-windows/scripts/init-bugfix-pipeline.py +1 -1
  52. package/bundled/dev-pipeline-windows/scripts/init-pipeline.py +1 -1
  53. package/bundled/dev-pipeline-windows/scripts/init-refactor-pipeline.py +1 -1
  54. package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +146 -16
  55. package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +220 -13
  56. package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +244 -18
  57. package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +220 -13
  58. package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-implement.md +1 -1
  59. package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +2 -2
  60. package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +3 -3
  61. package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +3 -3
  62. package/bundled/dev-pipeline-windows/templates/bugfix-bootstrap-prompt.md +1 -1
  63. package/bundled/dev-pipeline-windows/templates/refactor-bootstrap-prompt.md +3 -3
  64. package/bundled/dev-pipeline-windows/templates/sections/log-size-awareness.md +31 -68
  65. package/bundled/dev-pipeline-windows/templates/session-status-schema.json +1 -1
  66. package/bundled/skills/_metadata.json +1 -1
  67. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +2 -2
  68. package/bundled/skills/feature-pipeline-launcher/SKILL.md +2 -2
  69. package/bundled/skills/recovery-workflow/SKILL.md +2 -2
  70. package/bundled/skills/recovery-workflow/evals/evals.json +2 -2
  71. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +2 -2
  72. package/bundled/skills-windows/bugfix-pipeline-launcher/SKILL.md +2 -2
  73. package/bundled/skills-windows/feature-pipeline-launcher/SKILL.md +2 -2
  74. package/bundled/skills-windows/recovery-workflow/SKILL.md +2 -2
  75. package/bundled/skills-windows/recovery-workflow/evals/evals.json +2 -2
  76. package/bundled/skills-windows/refactor-pipeline-launcher/SKILL.md +2 -2
  77. package/package.json +1 -1
  78. package/bundled/dev-pipeline/scripts/monitor-log.sh +0 -104
  79. 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,
@@ -38,11 +41,11 @@ def _make_feature_list(features):
38
41
 
39
42
  def _init_state(tmp_path, feature_ids):
40
43
  """Create state dir with status.json for each feature."""
41
- state_dir = str(tmp_path / "state")
44
+ state_dir = str(tmp_path / "state" / "features")
42
45
  for fid in feature_ids:
43
- feature_dir = os.path.join(state_dir, "features", fid, "sessions")
46
+ feature_dir = os.path.join(state_dir, fid, "sessions")
44
47
  os.makedirs(feature_dir, exist_ok=True)
45
- status_path = os.path.join(state_dir, "features", fid, "status.json")
48
+ status_path = os.path.join(state_dir, fid, "status.json")
46
49
  write_json_file(status_path, {
47
50
  "feature_id": fid,
48
51
  "status": "pending",
@@ -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
  # ---------------------------------------------------------------------------
@@ -325,7 +777,7 @@ class TestInfraErrorUpdate:
325
777
  features = [_make_feature("F-001", "Root", status="in_progress")]
326
778
  fl_path = _write_fl(tmp_path, features)
327
779
  state_dir = _init_state(tmp_path, ["F-001"])
328
- status_path = os.path.join(state_dir, "features", "F-001", "status.json")
780
+ status_path = os.path.join(state_dir, "F-001", "status.json")
329
781
  fs = load_feature_status(state_dir, "F-001")
330
782
  fs["retry_count"] = 2
331
783
  write_json_file(status_path, fs)
@@ -357,7 +809,7 @@ class TestInfraErrorUpdate:
357
809
  features = [_make_feature("F-001", "Root", status="in_progress")]
358
810
  fl_path = _write_fl(tmp_path, features)
359
811
  state_dir = _init_state(tmp_path, ["F-001"])
360
- status_path = os.path.join(state_dir, "features", "F-001", "status.json")
812
+ status_path = os.path.join(state_dir, "F-001", "status.json")
361
813
  fs = load_feature_status(state_dir, "F-001")
362
814
  fs["retry_count"] = 1
363
815
  fs["infra_error_count"] = 2
@@ -389,7 +841,7 @@ class TestInfraErrorUpdate:
389
841
  features = [_make_feature("F-001", "Root", status="failed")]
390
842
  fl_path = _write_fl(tmp_path, features)
391
843
  state_dir = _init_state(tmp_path, ["F-001"])
392
- status_path = os.path.join(state_dir, "features", "F-001", "status.json")
844
+ status_path = os.path.join(state_dir, "F-001", "status.json")
393
845
  fs = load_feature_status(state_dir, "F-001")
394
846
  fs["retry_count"] = 3
395
847
  fs["infra_error_count"] = 3
@@ -416,7 +868,7 @@ class TestInfraErrorUpdate:
416
868
  features = [_make_feature("F-001", "Root", status="failed")]
417
869
  fl_path = _write_fl(tmp_path, features)
418
870
  state_dir = _init_state(tmp_path, ["F-001"])
419
- status_path = os.path.join(state_dir, "features", "F-001", "status.json")
871
+ status_path = os.path.join(state_dir, "F-001", "status.json")
420
872
  fs = load_feature_status(state_dir, "F-001")
421
873
  fs["retry_count"] = 3
422
874
  fs["infra_error_count"] = 3
@@ -563,7 +1015,7 @@ class TestAutoSkipIntegration:
563
1015
  fl_path = _write_fl(tmp_path, features)
564
1016
  state_dir = _init_state(tmp_path, ["F-001"])
565
1017
  # Simulate retry_count = 3
566
- fs_path = os.path.join(state_dir, "features", "F-001", "status.json")
1018
+ fs_path = os.path.join(state_dir, "F-001", "status.json")
567
1019
  with open(fs_path) as f:
568
1020
  fs = json.load(f)
569
1021
  fs["retry_count"] = 3
@@ -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'