prizmkit 1.1.105 → 1.1.107

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 (36) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +0 -1
  3. package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +83 -12
  4. package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +93 -6
  5. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +0 -8
  6. package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +9 -1
  7. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +421 -61
  8. package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +102 -17
  9. package/bundled/dev-pipeline/scripts/continuation.py +0 -4
  10. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +7 -39
  11. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +1 -31
  12. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +1 -31
  13. package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +5 -6
  14. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +28 -28
  15. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +36 -52
  16. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +34 -54
  17. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +1 -1
  18. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -3
  19. package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +2 -3
  20. package/bundled/dev-pipeline/templates/sections/log-size-awareness.md +2 -2
  21. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +4 -9
  22. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +4 -10
  23. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +3 -3
  24. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +3 -3
  25. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +49 -2
  26. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +17 -0
  27. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +50 -1
  28. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +560 -1
  29. package/bundled/dev-pipeline/tests/test_unified_cli.py +208 -0
  30. package/bundled/skills/_metadata.json +1 -1
  31. package/bundled/skills/prizmkit-code-review/SKILL.md +12 -13
  32. package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +10 -10
  33. package/package.json +1 -1
  34. package/bundled/dev-pipeline/templates/agent-prompts/dev-fix.md +0 -7
  35. package/bundled/dev-pipeline/templates/agent-prompts/dev-resume.md +0 -5
  36. package/bundled/skills/prizmkit-code-review/references/dev-agent-prompt.md +0 -30
@@ -16,7 +16,7 @@ from collections.abc import Callable, Mapping, Sequence
16
16
  from dataclasses import dataclass, field
17
17
  from pathlib import Path
18
18
 
19
- from .heartbeat import HeartbeatConfig, HeartbeatMonitor, write_stale_marker
19
+ from .heartbeat import HeartbeatConfig, HeartbeatMonitor, HeartbeatState, observe_heartbeat_state, write_stale_marker
20
20
  from .processes import ProcessHandle, ProcessSpec, launch_process, terminate_process_tree
21
21
  from .status import ProgressSummary
22
22
 
@@ -393,6 +393,10 @@ def _format_elapsed(seconds: float) -> str:
393
393
  return f"{minutes}m{remainder:02d}s"
394
394
 
395
395
 
396
+ def _format_stale_minutes(seconds: float) -> str:
397
+ return f"{max(0, int(seconds)) // 60}m"
398
+
399
+
396
400
  def _read_json_object(path: Path | None) -> dict[str, object]:
397
401
  if path is None or not path.is_file():
398
402
  return {}
@@ -403,14 +407,50 @@ def _read_json_object(path: Path | None) -> dict[str, object]:
403
407
  return data if isinstance(data, dict) else {}
404
408
 
405
409
 
406
- def _compact_progress_line(started_epoch: float, session_log: Path, progress_path: Path | None) -> str:
410
+ def _display_heartbeat_state(
411
+ session_log: Path,
412
+ progress_path: Path | None,
413
+ monitor_state: HeartbeatState | None,
414
+ fallback_state: HeartbeatState | None,
415
+ *,
416
+ interval_seconds: float,
417
+ stale_threshold_seconds: float,
418
+ ) -> HeartbeatState:
419
+ if monitor_state is not None and monitor_state.last_seen_epoch is not None:
420
+ return monitor_state
421
+ return observe_heartbeat_state(
422
+ HeartbeatConfig(
423
+ heartbeat_file=session_log.with_name("heartbeat.json"),
424
+ session_log=session_log,
425
+ progress_file=progress_path,
426
+ interval_seconds=interval_seconds,
427
+ stale_after_seconds=stale_threshold_seconds,
428
+ kill_after_seconds=0,
429
+ ),
430
+ fallback_state,
431
+ )
432
+
433
+
434
+ def _compact_progress_line(
435
+ started_epoch: float,
436
+ session_log: Path,
437
+ progress_path: Path | None,
438
+ heartbeat_state: HeartbeatState | None = None,
439
+ *,
440
+ stale_threshold_seconds: float = 0,
441
+ ) -> str:
407
442
  data = _read_json_object(progress_path)
408
- try:
409
- log_size = session_log.stat().st_size
410
- except OSError:
411
- log_size = 0
443
+ if heartbeat_state is not None:
444
+ log_size = heartbeat_state.log_size
445
+ else:
446
+ try:
447
+ log_size = session_log.stat().st_size
448
+ except OSError:
449
+ log_size = 0
450
+ glyph = "⏸" if heartbeat_state is not None and heartbeat_state.details == "no progress" else "▶"
451
+ stale_seconds = heartbeat_state.stale_seconds if heartbeat_state is not None else 0
412
452
  parts = [
413
- f" [HEARTBEAT] {_format_elapsed(time.time() - started_epoch)}",
453
+ f" {glyph} [HEARTBEAT] {_format_elapsed(time.time() - started_epoch)}",
414
454
  f"log: {_format_bytes(log_size)}",
415
455
  ]
416
456
  child_bytes = int(data.get("child_total_bytes") or 0)
@@ -432,15 +472,40 @@ def _compact_progress_line(started_epoch: float, session_log: Path, progress_pat
432
472
  tool_calls = int(data.get("total_tool_calls") or 0)
433
473
  if tool_calls:
434
474
  parts.append(f"{tool_calls} tool calls")
475
+ parts.append(f"stale: {_format_stale_minutes(stale_seconds)}/{_format_stale_minutes(stale_threshold_seconds)}")
435
476
  return " | ".join(parts)
436
477
 
437
478
 
438
- def _emit_progress_tick(started_epoch: float, session_log: Path, progress_path: Path | None, last_line: str) -> str:
439
- line = _compact_progress_line(started_epoch, session_log, progress_path)
479
+ def _emit_progress_tick(
480
+ started_epoch: float,
481
+ session_log: Path,
482
+ progress_path: Path | None,
483
+ last_line: str,
484
+ *,
485
+ monitor_state: HeartbeatState | None = None,
486
+ fallback_state: HeartbeatState | None = None,
487
+ interval_seconds: float = 30,
488
+ stale_threshold_seconds: float = 0,
489
+ ) -> tuple[str, HeartbeatState]:
490
+ heartbeat_state = _display_heartbeat_state(
491
+ session_log,
492
+ progress_path,
493
+ monitor_state,
494
+ fallback_state,
495
+ interval_seconds=interval_seconds,
496
+ stale_threshold_seconds=stale_threshold_seconds,
497
+ )
498
+ line = _compact_progress_line(
499
+ started_epoch,
500
+ session_log,
501
+ progress_path,
502
+ heartbeat_state,
503
+ stale_threshold_seconds=stale_threshold_seconds,
504
+ )
440
505
  if line != last_line:
441
506
  _emit_live_line(line)
442
- return line
443
- return last_line
507
+ return line, heartbeat_state
508
+ return last_line, heartbeat_state
444
509
 
445
510
 
446
511
  def _emit_session_start(config: AISessionConfig, command: AISessionCommand, session_log: Path, backup_log: Path) -> None:
@@ -601,6 +666,8 @@ class AISessionLauncher:
601
666
  handle.popen.stdin.close()
602
667
 
603
668
  heartbeat = None
669
+ live_progress_interval = max(1.0, float(self.config.live_progress_interval_seconds or 30))
670
+ stale_threshold_seconds = max(0.0, float(self.config.stale_kill_threshold_seconds or 0))
604
671
  heartbeat_path = self.config.heartbeat_path
605
672
  if heartbeat_path is None and self.config.stale_kill_threshold_seconds > 0:
606
673
  heartbeat_path = session_log.with_name("heartbeat.json")
@@ -613,8 +680,8 @@ class AISessionLauncher:
613
680
  progress_file=self.config.progress_path,
614
681
  marker_dir=session_log.parent,
615
682
  interval_seconds=0.1,
616
- stale_after_seconds=self.config.stale_kill_threshold_seconds,
617
- kill_after_seconds=self.config.stale_kill_threshold_seconds,
683
+ stale_after_seconds=stale_threshold_seconds,
684
+ kill_after_seconds=stale_threshold_seconds,
618
685
  grace_seconds=self.config.stale_kill_grace_seconds,
619
686
  ),
620
687
  )
@@ -626,6 +693,7 @@ class AISessionLauncher:
626
693
  stale_marker = None
627
694
  last_progress_line = ""
628
695
  last_progress_emit = 0.0
696
+ display_heartbeat_state = None
629
697
  try:
630
698
  while True:
631
699
  timeout = 0.2
@@ -642,16 +710,33 @@ class AISessionLauncher:
642
710
  break
643
711
  except subprocess.TimeoutExpired:
644
712
  if self.config.live_output and (not self.config.verbose or self.config.suppress_stream_output):
645
- interval = max(1.0, float(self.config.live_progress_interval_seconds or 30))
646
713
  now = time.time()
647
- if last_progress_emit == 0.0 or now - last_progress_emit >= interval:
648
- last_progress_line = _emit_progress_tick(started, session_log, self.config.progress_path, last_progress_line)
714
+ if last_progress_emit == 0.0 or now - last_progress_emit >= live_progress_interval:
715
+ last_progress_line, display_heartbeat_state = _emit_progress_tick(
716
+ started,
717
+ session_log,
718
+ self.config.progress_path,
719
+ last_progress_line,
720
+ monitor_state=heartbeat.state if heartbeat is not None else None,
721
+ fallback_state=display_heartbeat_state,
722
+ interval_seconds=live_progress_interval,
723
+ stale_threshold_seconds=stale_threshold_seconds,
724
+ )
649
725
  last_progress_emit = now
650
726
  except KeyboardInterrupt:
651
727
  terminate_process_tree(handle, "interrupted", grace_seconds=self.config.stale_kill_grace_seconds)
652
728
  raise
653
729
  if self.config.live_output and (not self.config.verbose or self.config.suppress_stream_output):
654
- _emit_progress_tick(started, session_log, self.config.progress_path, last_progress_line)
730
+ last_progress_line, display_heartbeat_state = _emit_progress_tick(
731
+ started,
732
+ session_log,
733
+ self.config.progress_path,
734
+ last_progress_line,
735
+ monitor_state=heartbeat.state if heartbeat is not None else None,
736
+ fallback_state=display_heartbeat_state,
737
+ interval_seconds=live_progress_interval,
738
+ stale_threshold_seconds=stale_threshold_seconds,
739
+ )
655
740
  reader.join(timeout=5)
656
741
  heartbeat_result = heartbeat.stop() if heartbeat is not None else None
657
742
  if heartbeat_result and heartbeat_result.killed:
@@ -158,8 +158,6 @@ def runtime_paths(args, project_root, task_type, task_id, previous_session_id):
158
158
  "bootstrap_prompt": _rel_path(project_root, output_path),
159
159
  "session_log": _rel_path(project_root, os.path.join(logs_dir, "session.log")),
160
160
  "progress_json": _rel_path(project_root, os.path.join(logs_dir, "progress.json")),
161
- "log_monitor_status": _rel_path(project_root, os.path.join(logs_dir, "log-monitor-status.txt")),
162
- "log_monitor_baseline": _rel_path(project_root, os.path.join(logs_dir, "log-monitor-baseline.txt")),
163
161
  "pid_file": _rel_path(project_root, os.path.join(logs_dir, "ai.pid")),
164
162
  "session_status": _rel_path(project_root, session_status),
165
163
  "previous_session_log": _rel_path(project_root, previous_log),
@@ -321,8 +319,6 @@ def _build_addendum(context):
321
319
  "- Generated bootstrap prompt path: `{}`".format(runtime["bootstrap_prompt"]),
322
320
  "- Session log path: `{}`".format(runtime["session_log"]),
323
321
  "- Progress JSON path: `{}`".format(runtime["progress_json"]),
324
- "- Log monitor status path: `{}`".format(runtime["log_monitor_status"]),
325
- "- Log monitor baseline path: `{}`".format(runtime["log_monitor_baseline"]),
326
322
  "- PID file: `{}`".format(runtime["pid_file"]),
327
323
  "- Session status file: `{}`".format(runtime["session_status"]),
328
324
  "",
@@ -32,7 +32,6 @@ from continuation import add_continuation_args, append_continuation_handoff, is_
32
32
 
33
33
 
34
34
  DEFAULT_MAX_RETRIES = 3
35
- DEFAULT_MAX_LOG_SIZE = 2097152
36
35
 
37
36
  LOGGER = setup_logging("generate-bootstrap-prompt")
38
37
 
@@ -161,27 +160,6 @@ def compute_feature_slug(feature_id, title):
161
160
  return "{}-{}".format(numeric, slug)
162
161
 
163
162
 
164
- def _format_bytes(n):
165
- """Format a byte count to a human-readable string."""
166
- if n >= 1048576:
167
- return "{}MB".format(n // 1048576)
168
- elif n >= 1024:
169
- return "{}KB".format(n // 1024)
170
- return "{}B".format(n)
171
-
172
-
173
- def _parse_non_negative_int_env(name, default):
174
- """Parse a non-negative integer environment variable."""
175
- value = os.environ.get(name, str(default))
176
- try:
177
- parsed = int(value)
178
- except ValueError:
179
- emit_failure("{} must be a non-negative integer".format(name))
180
- if parsed < 0:
181
- emit_failure("{} must be a non-negative integer".format(name))
182
- return parsed
183
-
184
-
185
163
  def _parse_positive_int_env(name, default):
186
164
  """Parse a positive integer environment variable."""
187
165
  value = os.environ.get(name, str(default))
@@ -196,11 +174,7 @@ def _parse_positive_int_env(name, default):
196
174
 
197
175
 
198
176
  def load_log_size_section(script_dir):
199
- """Load the optional log-size awareness prompt section."""
200
- max_log_size = _parse_non_negative_int_env("MAX_LOG_SIZE", DEFAULT_MAX_LOG_SIZE)
201
- if max_log_size == 0:
202
- return ""
203
-
177
+ """Load passive checkpoint guidance for normal prompt generation."""
204
178
  section_path = os.path.join(
205
179
  script_dir, "..", "templates", "sections", "log-size-awareness.md",
206
180
  )
@@ -1299,17 +1273,16 @@ def load_section(sections_dir, name):
1299
1273
  ACTIVE_AGENT_PROMPTS = {
1300
1274
  "critic-plan-challenge.md": "{{AGENT_PROMPT_CRITIC_PLAN_CHALLENGE}}",
1301
1275
  "reviewer-review.md": "{{AGENT_PROMPT_REVIEWER_REVIEW}}",
1302
- "dev-fix.md": "{{AGENT_PROMPT_DEV_FIX}}",
1303
- "dev-resume.md": "{{AGENT_PROMPT_DEV_RESUME}}",
1304
1276
  }
1305
1277
 
1306
1278
 
1307
1279
  def load_active_agent_prompts(templates_dir):
1308
1280
  """Load explicit active agent prompt templates.
1309
1281
 
1310
- The retired top-level Dev implementation prompt is intentionally not loaded;
1311
- feature implementation is performed by the main orchestrator. Remaining
1312
- prompt placeholders support Critic/Reviewer and review-fix helper flows.
1282
+ Retired Dev implementation and review-fix prompts are intentionally not
1283
+ loaded; feature implementation and accepted review fixes are performed by
1284
+ the main orchestrator. Remaining prompt placeholders support Critic and
1285
+ read-only Reviewer handoffs.
1313
1286
  """
1314
1287
  agent_prompts_dir = os.path.join(templates_dir, "agent-prompts")
1315
1288
  if not os.path.isdir(agent_prompts_dir):
@@ -1470,9 +1443,8 @@ def assemble_sections(pipeline_mode, sections_dir, init_done, is_resume,
1470
1443
  sections.append(("context-budget-rules",
1471
1444
  load_section(sections_dir, "context-budget-rules.md")))
1472
1445
 
1473
- # --- Log Size Awareness (only disabled when MAX_LOG_SIZE is explicitly 0) ---
1474
- max_log_size = _parse_non_negative_int_env("MAX_LOG_SIZE", DEFAULT_MAX_LOG_SIZE)
1475
- if max_log_size != 0 and os.path.isfile(os.path.join(sections_dir, "log-size-awareness.md")):
1446
+ # --- Session checkpoint guidance ---
1447
+ if os.path.isfile(os.path.join(sections_dir, "log-size-awareness.md")):
1476
1448
  sections.append(("log-size-awareness",
1477
1449
  load_section(sections_dir, "log-size-awareness.md")))
1478
1450
 
@@ -1949,8 +1921,6 @@ def build_replacements(args, feature, features, global_context, script_dir):
1949
1921
  pass # Keep default 3000 on any error
1950
1922
  dev_url = f"http://localhost:{dev_port}"
1951
1923
 
1952
- max_log_size = _parse_non_negative_int_env("MAX_LOG_SIZE", DEFAULT_MAX_LOG_SIZE)
1953
-
1954
1924
  replacements = {
1955
1925
  "{{LOG_SIZE_AWARENESS}}": load_log_size_section(script_dir),
1956
1926
  "{{RUN_ID}}": args.run_id,
@@ -1997,8 +1967,6 @@ def build_replacements(args, feature, features, global_context, script_dir):
1997
1967
  "{{COVERAGE_TARGET}}": coverage_target,
1998
1968
  "{{DEV_PORT}}": dev_port,
1999
1969
  "{{DEV_URL}}": dev_url,
2000
- "{{MAX_LOG_SIZE}}": str(max_log_size),
2001
- "{{MAX_LOG_SIZE_HUMAN}}": _format_bytes(max_log_size),
2002
1970
  "{{SESSION_LOG_PATH}}": os.path.join(
2003
1971
  ".prizmkit", "state", "features", args.feature_id,
2004
1972
  "sessions", args.session_id, "logs", "session.log",
@@ -24,7 +24,6 @@ from continuation import add_continuation_args, append_continuation_handoff, is_
24
24
 
25
25
 
26
26
  DEFAULT_MAX_RETRIES = 3
27
- DEFAULT_MAX_LOG_SIZE = 2097152
28
27
 
29
28
  LOGGER = setup_logging("generate-bugfix-prompt")
30
29
 
@@ -66,27 +65,6 @@ def read_text_file(path):
66
65
 
67
66
 
68
67
 
69
- def _format_bytes(n):
70
- """Format a byte count to a human-readable string."""
71
- if n >= 1048576:
72
- return "{}MB".format(n // 1048576)
73
- elif n >= 1024:
74
- return "{}KB".format(n // 1024)
75
- return "{}B".format(n)
76
-
77
-
78
- def _parse_non_negative_int_env(name, default):
79
- """Parse a non-negative integer environment variable."""
80
- value = os.environ.get(name, str(default))
81
- try:
82
- parsed = int(value)
83
- except ValueError:
84
- raise ValueError("{} must be a non-negative integer".format(name))
85
- if parsed < 0:
86
- raise ValueError("{} must be a non-negative integer".format(name))
87
- return parsed
88
-
89
-
90
68
  def _parse_positive_int_env(name, default):
91
69
  """Parse a positive integer environment variable."""
92
70
  value = os.environ.get(name, str(default))
@@ -100,11 +78,7 @@ def _parse_positive_int_env(name, default):
100
78
 
101
79
 
102
80
  def load_log_size_section(script_dir):
103
- """Load the optional log-size awareness prompt section."""
104
- max_log_size = _parse_non_negative_int_env("MAX_LOG_SIZE", DEFAULT_MAX_LOG_SIZE)
105
- if max_log_size == 0:
106
- return ""
107
-
81
+ """Load passive checkpoint guidance for normal prompt generation."""
108
82
  section_path = os.path.join(
109
83
  script_dir, "..", "templates", "sections", "log-size-awareness.md",
110
84
  )
@@ -373,12 +347,8 @@ def build_replacements(args, bug, global_context, script_dir):
373
347
  browser_enabled = True
374
348
  browser_verify_steps = " # (reproduce bug and verify fix)"
375
349
 
376
- max_log_size = _parse_non_negative_int_env("MAX_LOG_SIZE", DEFAULT_MAX_LOG_SIZE)
377
-
378
350
  replacements = {
379
351
  "{{LOG_SIZE_AWARENESS}}": load_log_size_section(script_dir),
380
- "{{MAX_LOG_SIZE}}": str(max_log_size),
381
- "{{MAX_LOG_SIZE_HUMAN}}": _format_bytes(max_log_size),
382
352
  "{{SESSION_LOG_PATH}}": os.path.join(
383
353
  ".prizmkit", "state", "bugfix", args.bug_id,
384
354
  "sessions", args.session_id, "logs", "session.log",
@@ -24,7 +24,6 @@ from continuation import add_continuation_args, append_continuation_handoff, is_
24
24
 
25
25
 
26
26
  DEFAULT_MAX_RETRIES = 3
27
- DEFAULT_MAX_LOG_SIZE = 2097152
28
27
 
29
28
  LOGGER = setup_logging("generate-refactor-prompt")
30
29
 
@@ -156,27 +155,6 @@ def read_text_file(path):
156
155
 
157
156
 
158
157
 
159
- def _format_bytes(n):
160
- """Format a byte count to a human-readable string."""
161
- if n >= 1048576:
162
- return "{}MB".format(n // 1048576)
163
- elif n >= 1024:
164
- return "{}KB".format(n // 1024)
165
- return "{}B".format(n)
166
-
167
-
168
- def _parse_non_negative_int_env(name, default):
169
- """Parse a non-negative integer environment variable."""
170
- value = os.environ.get(name, str(default))
171
- try:
172
- parsed = int(value)
173
- except ValueError:
174
- raise ValueError("{} must be a non-negative integer".format(name))
175
- if parsed < 0:
176
- raise ValueError("{} must be a non-negative integer".format(name))
177
- return parsed
178
-
179
-
180
158
  def _parse_positive_int_env(name, default):
181
159
  """Parse a positive integer environment variable."""
182
160
  value = os.environ.get(name, str(default))
@@ -190,11 +168,7 @@ def _parse_positive_int_env(name, default):
190
168
 
191
169
 
192
170
  def load_log_size_section(script_dir):
193
- """Load the optional log-size awareness prompt section."""
194
- max_log_size = _parse_non_negative_int_env("MAX_LOG_SIZE", DEFAULT_MAX_LOG_SIZE)
195
- if max_log_size == 0:
196
- return ""
197
-
171
+ """Load passive checkpoint guidance for normal prompt generation."""
198
172
  section_path = os.path.join(
199
173
  script_dir, "..", "templates", "sections", "log-size-awareness.md",
200
174
  )
@@ -518,12 +492,8 @@ def build_replacements(args, refactor, refactors, global_context, script_dir):
518
492
  browser_enabled = True
519
493
  browser_verify_steps = " # (validate UI renders correctly and feature still works)"
520
494
 
521
- max_log_size = _parse_non_negative_int_env("MAX_LOG_SIZE", DEFAULT_MAX_LOG_SIZE)
522
-
523
495
  replacements = {
524
496
  "{{LOG_SIZE_AWARENESS}}": load_log_size_section(script_dir),
525
- "{{MAX_LOG_SIZE}}": str(max_log_size),
526
- "{{MAX_LOG_SIZE_HUMAN}}": _format_bytes(max_log_size),
527
497
  "{{SESSION_LOG_PATH}}": os.path.join(
528
498
  ".prizmkit", "state", "refactor", args.refactor_id,
529
499
  "sessions", args.session_id, "logs", "session.log",
@@ -1,7 +1,6 @@
1
1
  "Read {{REVIEWER_SUBAGENT_PATH}}. For feature {{FEATURE_ID}} (slug: {{FEATURE_SLUG}}):
2
- 1. Read `.prizmkit/specs/{{FEATURE_SLUG}}/spec.md` (if it exists) for goals and Verification Gates; if spec.md does not exist, read `.prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md` Section 1 Task Contract instead
3
- 2. Read `.prizmkit/specs/{{FEATURE_SLUG}}/plan.md` for architecture decisions and completed tasks
4
- 3. Run /prizmkit-code-review with artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/. The skill will run its internal review-fix loop (Reviewer filter Dev fix, max 3 rounds) and write review-report.md.
5
- 4. Do not run test suites here. The orchestrator runs `/prizmkit-test` after review completes and then fixes code/tests until the scoped gate passes.
6
- 5. review-report.md will be written to .prizmkit/specs/{{FEATURE_SLUG}}/ by prizmkit-code-review.
7
- Report: verdict (PASS/NEEDS_FIXES), number of rounds, findings fixed/rejected."
2
+ 1. Read `.prizmkit/specs/{{FEATURE_SLUG}}/spec.md` (if it exists) for goals and Verification Gates; if spec.md does not exist, read `.prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md` Section 1 Task Contract instead.
3
+ 2. Read `.prizmkit/specs/{{FEATURE_SLUG}}/plan.md` for architecture decisions and completed tasks.
4
+ 3. Review the current workspace changes and return structured findings only. Do not run `/prizmkit-code-review`, do not apply fixes, do not run test suites, and do not spawn other agents.
5
+ 4. The main orchestrator owns the review-fix loop: Reviewer Agent filter orchestrator fix.
6
+ Report: PASS/NEEDS_FIXES, findings with severity/location/problem/suggestion/verification, and a concise summary."
@@ -50,12 +50,11 @@ You are running in **headless non-interactive mode** with a FINITE context windo
50
50
  4. **One task at a time** — In Phase 3 (implement), complete and test one task before starting the next.
51
51
  5. **Minimize tool output** — Never load full command output into context. First capture to a temp file (`cmd 2>&1 | tee /tmp/out.txt | tail -20`), then scan the head/tail to identify relevant fields, and use targeted filtering (`grep`, `sed`, `awk`) to extract only the information needed for the current task. Only read the filtered result — never the raw full output.
52
52
  6. **No intermediate commits** — Do NOT run `git add`/`git commit` during Phase 1-3. All changes are committed once at the end in Phase 4 via `/prizmkit-committer`.
53
- 7. **Capture test output once** — When running test suites, always use `($TEST_CMD) 2>&1 | tee /tmp/test-out.txt | tail -20`. Then grep `/tmp/test-out.txt` for details. Never re-run the suite just to apply a different filter.
53
+ 7. **Capture test output once** — When running test suites, always use `<test-command> 2>&1 | tee /tmp/test-out.txt | tail -20`. Then grep `/tmp/test-out.txt` for details. Never re-run the suite just to apply a different filter.
54
54
  8. **Scaffold / generated file awareness** — Files created by scaffolding tools (`npm init`, `npx create-*`, etc.) are boilerplate. Tag them, record in context-snapshot.md, and NEVER re-read them. If you need to modify one, edit directly without reading first.
55
55
  9. **Package version verification (BLOCKING)** — Before writing ANY dependency version, verify it exists via registry query (`npm view <pkg> dist-tags.latest`, `pip index versions <pkg>`, etc.). NEVER guess version numbers.
56
56
  10. **Build artifact hygiene** — After build/compile, ensure output is in `.gitignore`. Never commit binaries/build output.
57
- 11. **Anti re-read hard rule (BLOCKING)** — Once you have read a file into context, do NOT read the full file again. Use `grep -n '<pattern>' <file>` or `sed -n '<start>,<end>p' <file>` for specific lines. **20-step window**: full Read forbidden within 20 tool calls of last read. **3-strike**: 3 full Reads of same file → report BLOCKED. Applies to orchestrator AND subagents.
58
- 12. **Read offset safety (BLOCKING)** — If Read returns "shorter than offset"/"File is empty"/"Wasted call" — do NOT retry same offset. Run `wc -l <file>` to get real line count, then `sed -n` for exact range. 2 consecutive offset errors → STOP Reading that file. Never guess offsets — `grep -n '<anchor>'` first.
57
+ 11. **Read offset safety (BLOCKING)** — If Read returns "shorter than offset"/"File is empty"/"Wasted call" — do NOT retry same offset. Run `wc -l <file>` to get real line count, then `sed -n` for exact range. 2 consecutive offset errors STOP Reading that file. Never guess offsets `grep -n '<anchor>'` first.
59
58
 
60
59
  ---
61
60
 
@@ -125,7 +124,7 @@ Before proceeding past CP-1:
125
124
 
126
125
  **CP-1**: plan.md exists with Tasks section.
127
126
 
128
- ### Phase 3: Implement + Test
127
+ ### Phase 3: Implement
129
128
 
130
129
  **Build artifacts**: After any build/compile command (`go build`, `npm run build`, `tsc`, etc.), ensure the output binary or build directory is in `.gitignore`:
131
130
  ```bash
@@ -134,45 +133,46 @@ grep -q '^/binary-name$' .gitignore || echo '/binary-name' >> .gitignore
134
133
  ```
135
134
  Never commit compiled binaries, build output, or generated artifacts.
136
135
 
137
- **3a.** Detect test commands and record baseline:
138
-
139
- You know this project's tech stack. Identify ALL test commands that apply (e.g., `go test ./...`, `npm test`, `cargo test`, `pytest`, `make test`, etc.). Record them as `TEST_CMDS` (one or more commands). Then record baseline:
140
- ```bash
141
- # Run each test command, capture output
142
- ($TEST_CMD) 2>&1 | tee /tmp/test-baseline.txt | tail -20
143
- ```
144
-
145
- **3b.** Run `/prizmkit-implement` — this handles the full implementation cycle:
136
+ **3a.** Run `/prizmkit-implement` this handles the implementation cycle:
146
137
  - Reads plan.md Tasks section from `.prizmkit/specs/{{FEATURE_SLUG}}/`
147
138
  - Reads context from `context-snapshot.md` (Prizm docs, TRAPS, file manifest)
148
139
  - Implements task-by-task, marking each `[x]` immediately
149
140
  - Creates/updates L2 `.prizm` docs when creating new modules or significantly modifying existing ones — AI selectively decides which modules warrant L2 based on complexity and importance
150
- - Defers scoped/full test execution until after review
141
+ - Does not run the scoped/full test gate; testing happens after code review in the PrizmKit Test phase
151
142
  - Writes '## Implementation Log' to `context-snapshot.md`
152
143
 
153
- **3b-checkpoint.** Context management — if plan.md has more than 5 tasks, update durable checkpoints/artifacts after every 3 tasks, minimize output, and continue. If context overflow occurs in a headless run, the runner starts a fresh continuation session from those checkpoints and artifacts. In an interactive Claude Code session operated manually outside the headless pipeline, `/compact` may be used as an optional convenience only.
154
-
155
- **3c.** After implement completes, verify:
144
+ **3b.** After implement completes, verify:
156
145
  1. All tasks in plan.md are `[x]`
157
146
  2. **Verification Gate Self-Check**: for each acceptance criterion, write one line of evidence (test name / file:line / fixture path). Any gate without evidence is BLOCKED, not success.
158
- 3. **Layered test strategy**: do not run mandatory periodic/full-suite tests during implementation. Use only tiny targeted checks when needed for local debugging; the scoped feature test gate runs after review.
159
- 4. Verify each acceptance criterion from Section 1 of context-snapshot.md is met check mentally, do NOT re-read files you already wrote
160
- 5. If any criterion is not met, fix it now using the convergence-based recovery loop below
147
+ 3. Use only tiny targeted checks when needed for local debugging; the scoped feature test gate runs after review.
148
+ 4. If any criterion is not met, fix it within this feature scope before proceeding.
149
+
150
+ **CP-2**: All acceptance criteria met or blocked gates documented in `failure-log.md`. Test execution is deferred until after code review.
161
151
 
162
- **CP-2**: All acceptance criteria met, all tests pass.
152
+ ### Review Code Review
163
153
 
164
- ### Test Failure Recovery (Convergence-Based)
154
+ Review the completed implementation before the scoped feature test gate runs. Use `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` so the code-review skill can read spec, plan, context snapshot, implementation log, and current diff artifacts. Do not require or read a prior `/prizmkit-test` report here; test generation and test-fix looping happen in the next phase.
165
155
 
166
- When tests fail, use convergence recovery — keep fixing while progress is being made:
156
+ Run `/prizmkit-code-review` with `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/`.
167
157
 
168
- 1. **Run tests, record results**: count failures, exclude baseline failures
169
- 2. **Check termination**: All pass → done | Plateau (same failures 3 rounds) → stop | Failures decreased → continue
170
- 3. **Fix and iterate**: analyze, fix, re-run `($TEST_CMD)`, go back to step 1
158
+ The skill runs an internal review-fix loop (Reviewer Agent → filter → orchestrator fix, max 3 rounds) and writes `review-report.md` to the artifact directory.
171
159
 
172
- **Key rule**: If failures decrease (even by 1), plateau counter resets. Do NOT block commit for unresolved failures document and defer to next session.
160
+ **Gate CheckReview Report**:
161
+ After `/prizmkit-code-review` returns, verify the review report:
162
+ ```bash
163
+ grep -q "## Verdict" .prizmkit/specs/{{FEATURE_SLUG}}/review-report.md && echo "GATE:PASS" || echo "GATE:MISSING"
164
+ ```
165
+ If GATE:MISSING:
166
+ - Do not re-run `/prizmkit-code-review` in an unbounded report-repair loop.
167
+ - Perform one bounded status check; retry at most once: inspect the skill output, `review-report.md` path, and any Reviewer agent spawn messages.
168
+ - If the missing report is caused by Reviewer agent spawn or review skill error, retry `/prizmkit-code-review` at most once only if it appears transient.
169
+ - If the report is still missing after that single check/retry, write `failure-log.md` with the spawn/skill error and last observable state, then either perform a safe inline fallback review (spec/plan/diff/tests → write `review-report.md` with `## Verdict`) or stop with a clear recovery failure.
173
170
 
174
- **Hard Round Cap (BLOCKING)**: After **6 total test-fix rounds**, STOP trial-and-error. Write failure-log listing all hypotheses tried. Switch to: read full relevant source section ONCE (via `grep -n`+`sed -n`, never full Read), understand control flow, rewrite failing block in one shot. Do NOT exceed 7 total rounds.
171
+ Read `review-report.md` and check the Verdict:
172
+ - `PASS` → proceed to next phase
173
+ - `NEEDS_FIXES` → the skill exhausted its max rounds; log the remaining findings and proceed (do not retry externally)
175
174
 
175
+ **CP-3**: Review complete, report written.
176
176
 
177
177
  ### Scoped Feature Test Gate — PrizmKit Test
178
178