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.
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +0 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +83 -12
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +93 -6
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +0 -8
- package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +9 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +421 -61
- package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +102 -17
- package/bundled/dev-pipeline/scripts/continuation.py +0 -4
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +7 -39
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +1 -31
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +1 -31
- package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +5 -6
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +28 -28
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +36 -52
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +34 -54
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -3
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +2 -3
- package/bundled/dev-pipeline/templates/sections/log-size-awareness.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +4 -9
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +4 -10
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +3 -3
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +3 -3
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +49 -2
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +17 -0
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +50 -1
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +560 -1
- package/bundled/dev-pipeline/tests/test_unified_cli.py +208 -0
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/prizmkit-code-review/SKILL.md +12 -13
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +10 -10
- package/package.json +1 -1
- package/bundled/dev-pipeline/templates/agent-prompts/dev-fix.md +0 -7
- package/bundled/dev-pipeline/templates/agent-prompts/dev-resume.md +0 -5
- 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
|
|
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
|
-
|
|
409
|
-
log_size =
|
|
410
|
-
|
|
411
|
-
|
|
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"
|
|
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(
|
|
439
|
-
|
|
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=
|
|
617
|
-
kill_after_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 >=
|
|
648
|
-
last_progress_line = _emit_progress_tick(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1311
|
-
feature implementation
|
|
1312
|
-
prompt placeholders support Critic
|
|
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
|
-
# ---
|
|
1474
|
-
|
|
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
|
|
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
|
|
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.
|
|
5
|
-
4.
|
|
6
|
-
|
|
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
|
|
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. **
|
|
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
|
|
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.**
|
|
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
|
-
-
|
|
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
|
|
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.
|
|
159
|
-
4.
|
|
160
|
-
|
|
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
|
-
|
|
152
|
+
### Review — Code Review
|
|
163
153
|
|
|
164
|
-
|
|
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
|
-
|
|
156
|
+
Run `/prizmkit-code-review` with `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/`.
|
|
167
157
|
|
|
168
|
-
|
|
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
|
-
**
|
|
160
|
+
**Gate Check — Review 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
|
-
|
|
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
|
|