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.
Files changed (57) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/lib/common.sh +244 -25
  3. package/bundled/dev-pipeline/reset-bug.sh +30 -0
  4. package/bundled/dev-pipeline/reset-feature.sh +30 -0
  5. package/bundled/dev-pipeline/reset-refactor.sh +30 -0
  6. package/bundled/dev-pipeline/run-bugfix.sh +128 -13
  7. package/bundled/dev-pipeline/run-feature.sh +130 -14
  8. package/bundled/dev-pipeline/run-refactor.sh +128 -13
  9. package/bundled/dev-pipeline/scripts/check-session-status.py +74 -1
  10. package/bundled/dev-pipeline/scripts/continuation.py +374 -0
  11. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +27 -30
  12. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +15 -20
  13. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +15 -20
  14. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +146 -16
  15. package/bundled/dev-pipeline/scripts/update-bug-status.py +214 -6
  16. package/bundled/dev-pipeline/scripts/update-feature-status.py +237 -6
  17. package/bundled/dev-pipeline/scripts/update-refactor-status.py +214 -6
  18. package/bundled/dev-pipeline/templates/agent-prompts/dev-implement.md +1 -1
  19. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +2 -2
  20. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +3 -3
  21. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +3 -3
  22. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +1 -1
  23. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -3
  24. package/bundled/dev-pipeline/templates/sections/log-size-awareness.md +31 -66
  25. package/bundled/dev-pipeline/templates/session-status-schema.json +1 -1
  26. package/bundled/dev-pipeline/tests/conftest.py +1 -0
  27. package/bundled/dev-pipeline/tests/test_auto_skip.py +510 -0
  28. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +103 -0
  29. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +60 -0
  30. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +43 -0
  31. package/bundled/dev-pipeline-windows/lib/common.ps1 +172 -10
  32. package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +93 -9
  33. package/bundled/dev-pipeline-windows/lib/reset.ps1 +34 -0
  34. package/bundled/dev-pipeline-windows/reset-bug.ps1 +1 -0
  35. package/bundled/dev-pipeline-windows/reset-feature.ps1 +1 -0
  36. package/bundled/dev-pipeline-windows/reset-refactor.ps1 +1 -0
  37. package/bundled/dev-pipeline-windows/scripts/check-session-status.py +74 -1
  38. package/bundled/dev-pipeline-windows/scripts/continuation.py +374 -0
  39. package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +27 -30
  40. package/bundled/dev-pipeline-windows/scripts/generate-bugfix-prompt.py +16 -20
  41. package/bundled/dev-pipeline-windows/scripts/generate-refactor-prompt.py +16 -20
  42. package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +146 -16
  43. package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +214 -6
  44. package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +237 -6
  45. package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +214 -6
  46. package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-implement.md +1 -1
  47. package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +2 -2
  48. package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +3 -3
  49. package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +3 -3
  50. package/bundled/dev-pipeline-windows/templates/bugfix-bootstrap-prompt.md +1 -1
  51. package/bundled/dev-pipeline-windows/templates/refactor-bootstrap-prompt.md +3 -3
  52. package/bundled/dev-pipeline-windows/templates/sections/log-size-awareness.md +31 -68
  53. package/bundled/dev-pipeline-windows/templates/session-status-schema.json +1 -1
  54. package/bundled/skills/_metadata.json +1 -1
  55. package/package.json +1 -1
  56. package/bundled/dev-pipeline/scripts/monitor-log.sh +0 -104
  57. 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)"