prizmkit 1.1.107 → 1.1.108
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/bin/create-prizmkit.js +4 -0
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/prizmkit_runtime/config.py +81 -14
- package/bundled/dev-pipeline/prizmkit_runtime/heartbeat.py +19 -8
- package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +0 -1
- package/bundled/dev-pipeline/prizmkit_runtime/launch_profiles.py +18 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +22 -2
- package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +3 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +20 -1
- package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +151 -30
- package/bundled/dev-pipeline/scripts/continuation.py +0 -1
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +6 -46
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +7 -34
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +40 -33
- package/bundled/dev-pipeline/scripts/utils.py +123 -0
- package/bundled/dev-pipeline/templates/agent-prompts/critic-plan-challenge.md +4 -3
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +8 -7
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +25 -36
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +16 -31
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +4 -16
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -15
- package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +14 -48
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +7 -13
- package/bundled/dev-pipeline/templates/sections/feature-context.md +0 -4
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification-auto.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification-opencli.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +0 -14
- package/bundled/dev-pipeline/templates/sections/phase-commit.md +0 -14
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-agent-suffix.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-base.md +0 -2
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-lite-suffix.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-critic-plan-full.md +6 -8
- package/bundled/dev-pipeline/templates/sections/phase-critic-plan.md +3 -7
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-plan-agent.md +2 -9
- package/bundled/dev-pipeline/templates/sections/phase-plan-lite.md +1 -8
- package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +15 -16
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +2 -9
- package/bundled/dev-pipeline/templates/sections/phase0-init.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase0-test-baseline.md +2 -15
- package/bundled/dev-pipeline/tests/conftest.py +0 -1
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +350 -23
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +36 -17
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +117 -14
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +79 -2
- package/bundled/dev-pipeline/tests/test_unified_cli.py +658 -13
- package/bundled/skills/_metadata.json +4 -4
- package/bundled/skills/app-planner/SKILL.md +1 -1
- package/bundled/skills/app-planner/references/architecture-decisions.md +1 -1
- package/bundled/skills/bug-fix-workflow/SKILL.md +174 -128
- package/bundled/skills/bug-planner/SKILL.md +101 -15
- package/bundled/skills/bug-planner/references/severity-rules.md +20 -8
- package/bundled/skills/feature-planner/SKILL.md +87 -8
- package/bundled/skills/feature-planner/assets/planning-guide.md +34 -10
- package/bundled/skills/feature-workflow/SKILL.md +100 -81
- package/bundled/skills/{prizm-kit → prizmkit}/SKILL.md +1 -1
- package/bundled/skills/prizmkit-code-review/SKILL.md +1 -1
- package/bundled/skills/refactor-planner/SKILL.md +88 -7
- package/bundled/skills/refactor-planner/references/fast-path.md +2 -1
- package/bundled/skills/refactor-planner/references/planning-phases.md +29 -11
- package/bundled/skills/refactor-workflow/SKILL.md +116 -57
- package/bundled/templates/project-memory-template.md +1 -1
- package/package.json +1 -1
- package/src/ai-cli-launch.js +194 -0
- package/src/config.js +26 -14
- package/src/index.js +11 -44
- package/src/platforms.js +10 -3
- package/src/prompts.js +60 -2
- package/src/scaffold.js +26 -14
- package/bundled/dev-pipeline/scripts/prizmkit-test-gate.py +0 -446
- package/bundled/dev-pipeline/templates/sections/log-size-awareness.md +0 -20
- package/bundled/dev-pipeline/tests/test_prizmkit_test_gate.py +0 -107
|
@@ -6,6 +6,7 @@ import importlib.util
|
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
8
|
import queue
|
|
9
|
+
import shlex
|
|
9
10
|
import shutil
|
|
10
11
|
import subprocess
|
|
11
12
|
import sys
|
|
@@ -17,6 +18,7 @@ from dataclasses import dataclass, field
|
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
|
|
19
20
|
from .heartbeat import HeartbeatConfig, HeartbeatMonitor, HeartbeatState, observe_heartbeat_state, write_stale_marker
|
|
21
|
+
from .launch_profiles import HeadlessLaunchProfile
|
|
20
22
|
from .processes import ProcessHandle, ProcessSpec, launch_process, terminate_process_tree
|
|
21
23
|
from .status import ProgressSummary
|
|
22
24
|
|
|
@@ -52,12 +54,14 @@ class AISessionConfig:
|
|
|
52
54
|
permission_mode: str = "dangerously-skip-permissions"
|
|
53
55
|
total_timeout_seconds: float | None = None
|
|
54
56
|
stale_kill_threshold_seconds: float = 0
|
|
57
|
+
heartbeat_stale_threshold_seconds: float = 0
|
|
55
58
|
stale_kill_grace_seconds: float = 10
|
|
56
59
|
live_progress_interval_seconds: float = 30
|
|
57
60
|
live_banner: bool = True
|
|
58
61
|
suppress_stream_output: bool = False
|
|
59
62
|
codex_subagent_timeout_seconds: int = 3300
|
|
60
63
|
extra_args: tuple[str, ...] = ()
|
|
64
|
+
launch_profile: HeadlessLaunchProfile | None = None
|
|
61
65
|
environment_overrides: Mapping[str, str] = field(default_factory=dict)
|
|
62
66
|
|
|
63
67
|
|
|
@@ -69,6 +73,7 @@ class AISessionCommand:
|
|
|
69
73
|
stdin_text: str | None
|
|
70
74
|
provider: str
|
|
71
75
|
prompt_mode: str
|
|
76
|
+
prompt_text: str | None = None
|
|
72
77
|
|
|
73
78
|
|
|
74
79
|
@dataclass(frozen=True)
|
|
@@ -106,7 +111,7 @@ HelpRunner = Callable[[Sequence[str]], str]
|
|
|
106
111
|
|
|
107
112
|
def _normalize_platform(platform: str | None) -> str:
|
|
108
113
|
normalized = (platform or "").strip().lower()
|
|
109
|
-
return normalized if normalized in {"claude", "codex", "codebuddy"} else "codebuddy"
|
|
114
|
+
return normalized if normalized in {"claude", "codex", "codebuddy", "custom"} else "codebuddy"
|
|
110
115
|
|
|
111
116
|
|
|
112
117
|
def _prompt_text(path: Path) -> str:
|
|
@@ -156,6 +161,11 @@ def build_ai_session_command(config: AISessionConfig) -> AISessionCommand:
|
|
|
156
161
|
"""Build provider-specific AI CLI argv without spawning a process."""
|
|
157
162
|
platform = _normalize_platform(config.platform)
|
|
158
163
|
prompt = _prompt_text(config.prompt_path)
|
|
164
|
+
if config.launch_profile is not None and config.launch_profile.is_custom:
|
|
165
|
+
argv = [config.cli_command, config.launch_profile.prompt_arg, prompt]
|
|
166
|
+
argv.extend(config.launch_profile.extra_args)
|
|
167
|
+
argv.extend(config.extra_args)
|
|
168
|
+
return AISessionCommand(tuple(argv), None, "custom", "argument", prompt)
|
|
159
169
|
if platform == "claude":
|
|
160
170
|
argv = [
|
|
161
171
|
config.cli_command,
|
|
@@ -174,7 +184,7 @@ def build_ai_session_command(config: AISessionConfig) -> AISessionCommand:
|
|
|
174
184
|
if config.model:
|
|
175
185
|
argv.extend(["--model", config.model])
|
|
176
186
|
argv.extend(config.extra_args)
|
|
177
|
-
return AISessionCommand(tuple(argv), None, "claude", "argument")
|
|
187
|
+
return AISessionCommand(tuple(argv), None, "claude", "argument", prompt)
|
|
178
188
|
|
|
179
189
|
if platform == "codex":
|
|
180
190
|
argv = [
|
|
@@ -197,7 +207,7 @@ def build_ai_session_command(config: AISessionConfig) -> AISessionCommand:
|
|
|
197
207
|
argv.extend(["--config", f"model_reasoning_effort={config.effort}"])
|
|
198
208
|
argv.extend(config.extra_args)
|
|
199
209
|
argv.append("-")
|
|
200
|
-
return AISessionCommand(tuple(argv), prompt, "codex", "stdin")
|
|
210
|
+
return AISessionCommand(tuple(argv), prompt, "codex", "stdin", prompt)
|
|
201
211
|
|
|
202
212
|
argv = [config.cli_command, "--print", "-y", "--disallowedTools", "EnterWorktree"]
|
|
203
213
|
if config.verbose:
|
|
@@ -209,7 +219,18 @@ def build_ai_session_command(config: AISessionConfig) -> AISessionCommand:
|
|
|
209
219
|
if config.model:
|
|
210
220
|
argv.extend(["--model", config.model])
|
|
211
221
|
argv.extend(config.extra_args)
|
|
212
|
-
return AISessionCommand(tuple(argv), prompt, "codebuddy", "stdin")
|
|
222
|
+
return AISessionCommand(tuple(argv), prompt, "codebuddy", "stdin", prompt)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def format_start_command(command: AISessionCommand) -> str:
|
|
226
|
+
"""Return a readable command string with the injected prompt redacted."""
|
|
227
|
+
redacted: list[str] = []
|
|
228
|
+
for token in command.argv:
|
|
229
|
+
if command.prompt_text is not None and token == command.prompt_text:
|
|
230
|
+
redacted.append("<prompt>")
|
|
231
|
+
else:
|
|
232
|
+
redacted.append(token)
|
|
233
|
+
return " ".join("<prompt>" if token == "<prompt>" else shlex.quote(token) for token in redacted)
|
|
213
234
|
|
|
214
235
|
|
|
215
236
|
def default_backup_log_path(session_log: Path, home: Path | None = None) -> Path:
|
|
@@ -220,15 +241,16 @@ def default_backup_log_path(session_log: Path, home: Path | None = None) -> Path
|
|
|
220
241
|
return root / ".prizmkit" / "session-backups" / f"{session_id}.log"
|
|
221
242
|
|
|
222
243
|
|
|
223
|
-
def write_session_preamble(session_log: Path, backup_log: Path,
|
|
244
|
+
def write_session_preamble(session_log: Path, backup_log: Path, command: AISessionCommand, model: str | None) -> None:
|
|
224
245
|
"""Write the session preamble to both primary and backup logs."""
|
|
225
246
|
session_log.parent.mkdir(parents=True, exist_ok=True)
|
|
226
247
|
backup_log.parent.mkdir(parents=True, exist_ok=True)
|
|
227
248
|
preamble = "\n".join(
|
|
228
249
|
[
|
|
229
250
|
"=== PRIZMKIT SESSION START ===",
|
|
230
|
-
|
|
251
|
+
"Prompt: <prompt>",
|
|
231
252
|
f"Model: {model or 'default'}",
|
|
253
|
+
f"StartCommand: {format_start_command(command)}",
|
|
232
254
|
f"Started at epoch: {time.time():.6f}",
|
|
233
255
|
"--- OUTPUT BELOW ---",
|
|
234
256
|
"",
|
|
@@ -368,6 +390,60 @@ def _write_progress_summary(progress_path: Path | None, summary: ProgressSummary
|
|
|
368
390
|
progress_path.write_text(json.dumps(summary.to_dict(), indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
369
391
|
|
|
370
392
|
|
|
393
|
+
def _progress_refresh_key(data: Mapping[str, object]) -> tuple[object, ...]:
|
|
394
|
+
event_counts = data.get("event_type_counts")
|
|
395
|
+
return (
|
|
396
|
+
int(data.get("message_count") or 0),
|
|
397
|
+
tuple(sorted(event_counts.items())) if isinstance(event_counts, Mapping) else (),
|
|
398
|
+
str(data.get("current_tool") or ""),
|
|
399
|
+
str(data.get("current_phase") or ""),
|
|
400
|
+
int(data.get("total_tool_calls") or 0),
|
|
401
|
+
int(data.get("active_subagent_count") or 0),
|
|
402
|
+
int(data.get("subagent_spawn_count") or 0),
|
|
403
|
+
str(data.get("child_activity_signature") or ""),
|
|
404
|
+
int(data.get("child_total_bytes") or 0),
|
|
405
|
+
str(data.get("fatal_error_code") or ""),
|
|
406
|
+
str(data.get("terminal_result_text") or ""),
|
|
407
|
+
tuple(str(item) for item in data.get("errors", []) if item) if isinstance(data.get("errors"), list) else (),
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _write_progress_data(progress_path: Path, data: Mapping[str, object]) -> None:
|
|
412
|
+
progress_path.parent.mkdir(parents=True, exist_ok=True)
|
|
413
|
+
progress_path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _start_idle_progress_refresh(
|
|
417
|
+
progress_path: Path,
|
|
418
|
+
tracker: object,
|
|
419
|
+
tracker_lock: threading.Lock,
|
|
420
|
+
stop_event: threading.Event,
|
|
421
|
+
interval_seconds: float = 1.0,
|
|
422
|
+
) -> tuple[threading.Thread, Callable[[bool], None]]:
|
|
423
|
+
"""Refresh progress.json from child activity while parent output is idle."""
|
|
424
|
+
last_key: tuple[object, ...] | None = None
|
|
425
|
+
|
|
426
|
+
def maybe_write(force: bool = False) -> None:
|
|
427
|
+
nonlocal last_key
|
|
428
|
+
with tracker_lock:
|
|
429
|
+
data = tracker.to_dict()
|
|
430
|
+
key = _progress_refresh_key(data)
|
|
431
|
+
if force or key != last_key:
|
|
432
|
+
_write_progress_data(progress_path, data)
|
|
433
|
+
last_key = key
|
|
434
|
+
|
|
435
|
+
def refresh_loop() -> None:
|
|
436
|
+
while not stop_event.wait(max(0.1, interval_seconds)):
|
|
437
|
+
try:
|
|
438
|
+
maybe_write(False)
|
|
439
|
+
except Exception:
|
|
440
|
+
continue
|
|
441
|
+
|
|
442
|
+
thread = threading.Thread(target=refresh_loop, name="prizmkit-progress-refresh", daemon=True)
|
|
443
|
+
thread.start()
|
|
444
|
+
return thread, maybe_write
|
|
445
|
+
|
|
446
|
+
|
|
371
447
|
def _emit_live_line(message: str) -> None:
|
|
372
448
|
"""Print foreground runner status without interfering with file logging."""
|
|
373
449
|
print(message, file=sys.stderr, flush=True)
|
|
@@ -394,7 +470,13 @@ def _format_elapsed(seconds: float) -> str:
|
|
|
394
470
|
|
|
395
471
|
|
|
396
472
|
def _format_stale_minutes(seconds: float) -> str:
|
|
397
|
-
|
|
473
|
+
total = max(0, int(seconds))
|
|
474
|
+
minutes, remainder = divmod(total, 60)
|
|
475
|
+
if minutes == 0:
|
|
476
|
+
return f"{remainder}s"
|
|
477
|
+
if remainder == 0:
|
|
478
|
+
return f"{minutes}m"
|
|
479
|
+
return f"{minutes}m{remainder:02d}s"
|
|
398
480
|
|
|
399
481
|
|
|
400
482
|
def _read_json_object(path: Path | None) -> dict[str, object]:
|
|
@@ -447,8 +529,13 @@ def _compact_progress_line(
|
|
|
447
529
|
log_size = session_log.stat().st_size
|
|
448
530
|
except OSError:
|
|
449
531
|
log_size = 0
|
|
450
|
-
glyph = "⏸" if heartbeat_state is not None and heartbeat_state.details == "no progress" else "▶"
|
|
451
532
|
stale_seconds = heartbeat_state.stale_seconds if heartbeat_state is not None else 0
|
|
533
|
+
show_stale = bool(
|
|
534
|
+
heartbeat_state is not None
|
|
535
|
+
and heartbeat_state.details == "no progress"
|
|
536
|
+
and (heartbeat_state.stale or (stale_threshold_seconds > 0 and stale_seconds >= stale_threshold_seconds))
|
|
537
|
+
)
|
|
538
|
+
glyph = "⏸" if show_stale else "▶"
|
|
452
539
|
parts = [
|
|
453
540
|
f" {glyph} [HEARTBEAT] {_format_elapsed(time.time() - started_epoch)}",
|
|
454
541
|
f"log: {_format_bytes(log_size)}",
|
|
@@ -472,7 +559,9 @@ def _compact_progress_line(
|
|
|
472
559
|
tool_calls = int(data.get("total_tool_calls") or 0)
|
|
473
560
|
if tool_calls:
|
|
474
561
|
parts.append(f"{tool_calls} tool calls")
|
|
475
|
-
|
|
562
|
+
if show_stale:
|
|
563
|
+
threshold = f"/{_format_stale_minutes(stale_threshold_seconds)}" if stale_threshold_seconds > 0 else ""
|
|
564
|
+
parts.append(f"stale: {_format_stale_minutes(stale_seconds)}{threshold}")
|
|
476
565
|
return " | ".join(parts)
|
|
477
566
|
|
|
478
567
|
|
|
@@ -539,12 +628,23 @@ def _reader_thread(
|
|
|
539
628
|
) -> None:
|
|
540
629
|
lines: list[str] = []
|
|
541
630
|
tracker = None
|
|
631
|
+
tracker_lock = threading.Lock()
|
|
632
|
+
progress_refresh_stop = threading.Event()
|
|
633
|
+
progress_refresh_thread: threading.Thread | None = None
|
|
634
|
+
refresh_progress: Callable[[bool], None] | None = None
|
|
542
635
|
tracker_class = _load_progress_tracker_class() if progress_path is not None else None
|
|
543
636
|
if tracker_class is not None:
|
|
544
637
|
try:
|
|
545
638
|
tracker = tracker_class(session_log)
|
|
546
639
|
except Exception:
|
|
547
640
|
tracker = None
|
|
641
|
+
if progress_path is not None and tracker is not None:
|
|
642
|
+
progress_refresh_thread, refresh_progress = _start_idle_progress_refresh(
|
|
643
|
+
progress_path,
|
|
644
|
+
tracker,
|
|
645
|
+
tracker_lock,
|
|
646
|
+
progress_refresh_stop,
|
|
647
|
+
)
|
|
548
648
|
try:
|
|
549
649
|
for line in iter(pipe.readline, ""):
|
|
550
650
|
if not line:
|
|
@@ -560,18 +660,26 @@ def _reader_thread(
|
|
|
560
660
|
if progress_path is not None:
|
|
561
661
|
if tracker is not None:
|
|
562
662
|
stripped = line.strip()
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
progress_path
|
|
663
|
+
with tracker_lock:
|
|
664
|
+
if stripped:
|
|
665
|
+
try:
|
|
666
|
+
tracker.process_event(json.loads(stripped))
|
|
667
|
+
except json.JSONDecodeError:
|
|
668
|
+
tracker.last_text_snippet = stripped[:120]
|
|
669
|
+
tracker._detect_terminal_error(stripped, require_error_context=True)
|
|
670
|
+
data = tracker.to_dict()
|
|
671
|
+
_write_progress_data(progress_path, data)
|
|
572
672
|
else:
|
|
573
673
|
_write_progress_summary(progress_path, parse_stream_progress_lines(lines))
|
|
574
674
|
finally:
|
|
675
|
+
if refresh_progress is not None:
|
|
676
|
+
try:
|
|
677
|
+
refresh_progress(True)
|
|
678
|
+
except Exception:
|
|
679
|
+
pass
|
|
680
|
+
progress_refresh_stop.set()
|
|
681
|
+
if progress_refresh_thread is not None:
|
|
682
|
+
progress_refresh_thread.join(timeout=2)
|
|
575
683
|
try:
|
|
576
684
|
pipe.close()
|
|
577
685
|
except Exception:
|
|
@@ -632,7 +740,7 @@ class AISessionLauncher:
|
|
|
632
740
|
command = build_ai_session_command(self.config)
|
|
633
741
|
session_log = self.config.log_path or self.config.cwd / ".prizmkit" / "state" / "session.log"
|
|
634
742
|
backup_log = self.config.backup_log_path or default_backup_log_path(session_log)
|
|
635
|
-
write_session_preamble(session_log, backup_log,
|
|
743
|
+
write_session_preamble(session_log, backup_log, command, self.config.model)
|
|
636
744
|
|
|
637
745
|
spec = ProcessSpec(
|
|
638
746
|
command=command.argv,
|
|
@@ -667,9 +775,12 @@ class AISessionLauncher:
|
|
|
667
775
|
|
|
668
776
|
heartbeat = None
|
|
669
777
|
live_progress_interval = max(1.0, float(self.config.live_progress_interval_seconds or 30))
|
|
670
|
-
|
|
778
|
+
kill_threshold_seconds = max(0.0, float(self.config.stale_kill_threshold_seconds or 0))
|
|
779
|
+
stale_threshold_seconds = max(0.0, float(self.config.heartbeat_stale_threshold_seconds or 0))
|
|
780
|
+
if stale_threshold_seconds <= 0:
|
|
781
|
+
stale_threshold_seconds = kill_threshold_seconds
|
|
671
782
|
heartbeat_path = self.config.heartbeat_path
|
|
672
|
-
if heartbeat_path is None and
|
|
783
|
+
if heartbeat_path is None and kill_threshold_seconds > 0:
|
|
673
784
|
heartbeat_path = session_log.with_name("heartbeat.json")
|
|
674
785
|
if heartbeat_path is not None:
|
|
675
786
|
heartbeat = HeartbeatMonitor(
|
|
@@ -681,7 +792,7 @@ class AISessionLauncher:
|
|
|
681
792
|
marker_dir=session_log.parent,
|
|
682
793
|
interval_seconds=0.1,
|
|
683
794
|
stale_after_seconds=stale_threshold_seconds,
|
|
684
|
-
kill_after_seconds=
|
|
795
|
+
kill_after_seconds=kill_threshold_seconds,
|
|
685
796
|
grace_seconds=self.config.stale_kill_grace_seconds,
|
|
686
797
|
),
|
|
687
798
|
)
|
|
@@ -727,27 +838,37 @@ class AISessionLauncher:
|
|
|
727
838
|
terminate_process_tree(handle, "interrupted", grace_seconds=self.config.stale_kill_grace_seconds)
|
|
728
839
|
raise
|
|
729
840
|
if self.config.live_output and (not self.config.verbose or self.config.suppress_stream_output):
|
|
730
|
-
|
|
731
|
-
started,
|
|
841
|
+
final_heartbeat_state = _display_heartbeat_state(
|
|
732
842
|
session_log,
|
|
733
843
|
self.config.progress_path,
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
fallback_state=display_heartbeat_state,
|
|
844
|
+
heartbeat.state if heartbeat is not None else None,
|
|
845
|
+
display_heartbeat_state,
|
|
737
846
|
interval_seconds=live_progress_interval,
|
|
738
847
|
stale_threshold_seconds=stale_threshold_seconds,
|
|
739
848
|
)
|
|
849
|
+
display_heartbeat_state = final_heartbeat_state
|
|
850
|
+
if final_heartbeat_state.details == "progress":
|
|
851
|
+
final_line = _compact_progress_line(
|
|
852
|
+
started,
|
|
853
|
+
session_log,
|
|
854
|
+
self.config.progress_path,
|
|
855
|
+
final_heartbeat_state,
|
|
856
|
+
stale_threshold_seconds=stale_threshold_seconds,
|
|
857
|
+
)
|
|
858
|
+
if final_line != last_progress_line:
|
|
859
|
+
_emit_live_line(final_line)
|
|
860
|
+
last_progress_line = final_line
|
|
740
861
|
reader.join(timeout=5)
|
|
741
862
|
heartbeat_result = heartbeat.stop() if heartbeat is not None else None
|
|
742
863
|
if heartbeat_result and heartbeat_result.killed:
|
|
743
864
|
termination_reason = heartbeat_result.reason
|
|
744
865
|
stale_marker = heartbeat_result.marker_path
|
|
745
|
-
if termination_reason is None and
|
|
866
|
+
if termination_reason is None and kill_threshold_seconds > 0 and handle.popen.returncode is None:
|
|
746
867
|
stale_marker = write_stale_marker(
|
|
747
868
|
session_log.parent,
|
|
748
869
|
reason="stale_session",
|
|
749
|
-
stale_seconds=
|
|
750
|
-
threshold=
|
|
870
|
+
stale_seconds=kill_threshold_seconds,
|
|
871
|
+
threshold=kill_threshold_seconds,
|
|
751
872
|
)
|
|
752
873
|
termination_reason = "stale_session"
|
|
753
874
|
recovery = recover_session_log(session_log, backup_log)
|
|
@@ -294,7 +294,6 @@ def _build_addendum(context):
|
|
|
294
294
|
"- Continue from the first pending/in-progress checkpoint step: `{}`.".format(cursor["first_cursor"]),
|
|
295
295
|
"- Do not read the previous full `session.log` unless explicitly necessary.",
|
|
296
296
|
"- If previous runtime context is needed, prefer synthesized artifacts and summaries over logs.",
|
|
297
|
-
"- Do not attempt `/compact` as the recovery mechanism.",
|
|
298
297
|
"",
|
|
299
298
|
"### Preserved Durable Context",
|
|
300
299
|
"",
|
|
@@ -27,7 +27,7 @@ import os
|
|
|
27
27
|
import re
|
|
28
28
|
import sys
|
|
29
29
|
|
|
30
|
-
from utils import enrich_global_context, load_json_file,
|
|
30
|
+
from utils import enrich_global_context, load_json_file, resolve_prompt_platform, setup_logging
|
|
31
31
|
from continuation import add_continuation_args, append_continuation_handoff, is_continuation
|
|
32
32
|
|
|
33
33
|
|
|
@@ -173,16 +173,6 @@ def _parse_positive_int_env(name, default):
|
|
|
173
173
|
|
|
174
174
|
|
|
175
175
|
|
|
176
|
-
def load_log_size_section(script_dir):
|
|
177
|
-
"""Load passive checkpoint guidance for normal prompt generation."""
|
|
178
|
-
section_path = os.path.join(
|
|
179
|
-
script_dir, "..", "templates", "sections", "log-size-awareness.md",
|
|
180
|
-
)
|
|
181
|
-
content, err = read_text_file(section_path)
|
|
182
|
-
if err:
|
|
183
|
-
return ""
|
|
184
|
-
return content
|
|
185
|
-
|
|
186
176
|
def format_acceptance_criteria(criteria):
|
|
187
177
|
"""Format acceptance criteria as a markdown bullet list."""
|
|
188
178
|
if not criteria:
|
|
@@ -650,7 +640,7 @@ def determine_pipeline_mode(complexity):
|
|
|
650
640
|
SECTION_TO_SKILL = {
|
|
651
641
|
"phase0-init": ("prizmkit-init", "Project Bootstrap",
|
|
652
642
|
[".prizmkit/prizm-docs/root.prizm", ".prizmkit/config.json"]),
|
|
653
|
-
"phase0-test-baseline":
|
|
643
|
+
"phase0-test-baseline": None,
|
|
654
644
|
"phase-context-snapshot": ("context-snapshot", "Build Context Snapshot",
|
|
655
645
|
[".prizmkit/specs/{slug}/context-snapshot.md"]),
|
|
656
646
|
"phase-specify-plan": ("context-snapshot-and-plan", "Specify & Plan",
|
|
@@ -1443,11 +1433,6 @@ def assemble_sections(pipeline_mode, sections_dir, init_done, is_resume,
|
|
|
1443
1433
|
sections.append(("context-budget-rules",
|
|
1444
1434
|
load_section(sections_dir, "context-budget-rules.md")))
|
|
1445
1435
|
|
|
1446
|
-
# --- Session checkpoint guidance ---
|
|
1447
|
-
if os.path.isfile(os.path.join(sections_dir, "log-size-awareness.md")):
|
|
1448
|
-
sections.append(("log-size-awareness",
|
|
1449
|
-
load_section(sections_dir, "log-size-awareness.md")))
|
|
1450
|
-
|
|
1451
1436
|
# --- Directory Convention (tier-specific) ---
|
|
1452
1437
|
if pipeline_mode == "lite":
|
|
1453
1438
|
dc_file = "directory-convention-lite.md"
|
|
@@ -1692,27 +1677,12 @@ def build_replacements(args, feature, features, global_context, script_dir):
|
|
|
1692
1677
|
"""Build the full dict of placeholder -> replacement value."""
|
|
1693
1678
|
project_root = _args_project_root(args, script_dir)
|
|
1694
1679
|
|
|
1695
|
-
# Resolve paths - platform-aware agent/team resolution
|
|
1696
|
-
platform
|
|
1680
|
+
# Resolve paths - platform-aware agent/team resolution.
|
|
1681
|
+
# Mixed `--platform all` installs must render commands for the actual AI
|
|
1682
|
+
# environment, not whichever platform directory happens to be checked first.
|
|
1683
|
+
platform = resolve_prompt_platform(project_root)
|
|
1697
1684
|
home_dir = os.path.expanduser("~")
|
|
1698
1685
|
|
|
1699
|
-
# Auto-detect platform if not set
|
|
1700
|
-
if not platform:
|
|
1701
|
-
has_codex = os.path.isdir(os.path.join(project_root, ".codex", "agents"))
|
|
1702
|
-
has_claude = os.path.isdir(os.path.join(project_root, ".claude", "agents"))
|
|
1703
|
-
has_codebuddy = os.path.isdir(os.path.join(project_root, ".codebuddy", "agents"))
|
|
1704
|
-
if has_codex:
|
|
1705
|
-
platform = "codex"
|
|
1706
|
-
elif has_claude:
|
|
1707
|
-
platform = "claude"
|
|
1708
|
-
elif has_codebuddy:
|
|
1709
|
-
platform = "codebuddy"
|
|
1710
|
-
else:
|
|
1711
|
-
raise RuntimeError(
|
|
1712
|
-
"PrizmKit agents not found. None of .codex/agents/, .claude/agents/, or .codebuddy/agents/ exists. "
|
|
1713
|
-
"Run `npx prizmkit install` first, or set PRIZMKIT_PLATFORM=codex|claude|codebuddy explicitly."
|
|
1714
|
-
)
|
|
1715
|
-
|
|
1716
1686
|
if platform == "claude":
|
|
1717
1687
|
# Claude Code: agents in .claude/agents/, no native team config
|
|
1718
1688
|
agents_dir = os.path.join(project_root, ".claude", "agents")
|
|
@@ -1922,7 +1892,6 @@ def build_replacements(args, feature, features, global_context, script_dir):
|
|
|
1922
1892
|
dev_url = f"http://localhost:{dev_port}"
|
|
1923
1893
|
|
|
1924
1894
|
replacements = {
|
|
1925
|
-
"{{LOG_SIZE_AWARENESS}}": load_log_size_section(script_dir),
|
|
1926
1895
|
"{{RUN_ID}}": args.run_id,
|
|
1927
1896
|
"{{SESSION_ID}}": args.session_id,
|
|
1928
1897
|
"{{FEATURE_ID}}": args.feature_id,
|
|
@@ -1938,7 +1907,6 @@ def build_replacements(args, feature, features, global_context, script_dir):
|
|
|
1938
1907
|
),
|
|
1939
1908
|
"{{GLOBAL_CONTEXT}}": format_global_context(global_context, project_root),
|
|
1940
1909
|
"{{PROJECT_BRIEF}}": _read_project_brief(project_root),
|
|
1941
|
-
"{{PLATFORM_CONVENTIONS}}": read_platform_conventions(project_root),
|
|
1942
1910
|
"{{TEAM_CONFIG_PATH}}": team_config_path,
|
|
1943
1911
|
"{{DEV_SUBAGENT_PATH}}": dev_subagent,
|
|
1944
1912
|
"{{REVIEWER_SUBAGENT_PATH}}": reviewer_subagent,
|
|
@@ -2058,9 +2026,6 @@ def main():
|
|
|
2058
2026
|
agent_prompt_replacements = load_active_agent_prompts(templates_dir)
|
|
2059
2027
|
replacements.update(agent_prompt_replacements)
|
|
2060
2028
|
|
|
2061
|
-
if is_continuation(args):
|
|
2062
|
-
replacements["{{LOG_SIZE_AWARENESS}}"] = ""
|
|
2063
|
-
|
|
2064
2029
|
# Extract state needed for assembly
|
|
2065
2030
|
pipeline_mode = replacements.get("{{PIPELINE_MODE}}", "lite")
|
|
2066
2031
|
init_done = replacements.get("{{INIT_DONE}}", "false") == "true"
|
|
@@ -2080,11 +2045,6 @@ def main():
|
|
|
2080
2045
|
retry_count=int(args.retry_count),
|
|
2081
2046
|
browser_tool=browser_tool,
|
|
2082
2047
|
)
|
|
2083
|
-
if is_continuation(args):
|
|
2084
|
-
sections = [
|
|
2085
|
-
(name, content) for name, content in sections
|
|
2086
|
-
if name != "log-size-awareness"
|
|
2087
|
-
]
|
|
2088
2048
|
rendered = render_from_sections(sections, replacements)
|
|
2089
2049
|
except FileNotFoundError as exc:
|
|
2090
2050
|
LOGGER.warning(
|
|
@@ -19,7 +19,7 @@ import os
|
|
|
19
19
|
import re
|
|
20
20
|
import sys
|
|
21
21
|
|
|
22
|
-
from utils import enrich_global_context, load_json_file,
|
|
22
|
+
from utils import enrich_global_context, load_json_file, read_checkpoint_system_section, resolve_prompt_platform, setup_logging
|
|
23
23
|
from continuation import add_continuation_args, append_continuation_handoff, is_continuation
|
|
24
24
|
|
|
25
25
|
|
|
@@ -77,19 +77,6 @@ def _parse_positive_int_env(name, default):
|
|
|
77
77
|
return parsed
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
def load_log_size_section(script_dir):
|
|
81
|
-
"""Load passive checkpoint guidance for normal prompt generation."""
|
|
82
|
-
section_path = os.path.join(
|
|
83
|
-
script_dir, "..", "templates", "sections", "log-size-awareness.md",
|
|
84
|
-
)
|
|
85
|
-
if not os.path.isfile(section_path):
|
|
86
|
-
return ""
|
|
87
|
-
|
|
88
|
-
content, err = read_text_file(section_path)
|
|
89
|
-
if err:
|
|
90
|
-
return ""
|
|
91
|
-
return content
|
|
92
|
-
|
|
93
80
|
def find_bug(bugs, bug_id):
|
|
94
81
|
"""Find and return the bug dict matching the given ID."""
|
|
95
82
|
for bug in bugs:
|
|
@@ -261,17 +248,9 @@ def build_replacements(args, bug, global_context, script_dir):
|
|
|
261
248
|
project_root = _args_project_root(args, script_dir)
|
|
262
249
|
|
|
263
250
|
# Platform-aware agent/team path resolution
|
|
264
|
-
platform =
|
|
251
|
+
platform = resolve_prompt_platform(project_root)
|
|
265
252
|
home_dir = os.path.expanduser("~")
|
|
266
253
|
|
|
267
|
-
if not platform:
|
|
268
|
-
if os.path.isdir(os.path.join(project_root, ".codex", "agents")):
|
|
269
|
-
platform = "codex"
|
|
270
|
-
elif os.path.isdir(os.path.join(project_root, ".claude", "agents")):
|
|
271
|
-
platform = "claude"
|
|
272
|
-
else:
|
|
273
|
-
platform = "codebuddy"
|
|
274
|
-
|
|
275
254
|
if platform == "claude":
|
|
276
255
|
agents_dir = os.path.join(project_root, ".claude", "agents")
|
|
277
256
|
team_config_path = os.path.join(project_root, ".claude", "team-info.json")
|
|
@@ -348,7 +327,6 @@ def build_replacements(args, bug, global_context, script_dir):
|
|
|
348
327
|
browser_verify_steps = " # (reproduce bug and verify fix)"
|
|
349
328
|
|
|
350
329
|
replacements = {
|
|
351
|
-
"{{LOG_SIZE_AWARENESS}}": load_log_size_section(script_dir),
|
|
352
330
|
"{{SESSION_LOG_PATH}}": os.path.join(
|
|
353
331
|
".prizmkit", "state", "bugfix", args.bug_id,
|
|
354
332
|
"sessions", args.session_id, "logs", "session.log",
|
|
@@ -368,12 +346,15 @@ def build_replacements(args, bug, global_context, script_dir):
|
|
|
368
346
|
),
|
|
369
347
|
"{{ENVIRONMENT}}": format_environment(bug.get("environment")),
|
|
370
348
|
"{{GLOBAL_CONTEXT}}": format_global_context(global_context, project_root),
|
|
371
|
-
"{{PLATFORM_CONVENTIONS}}": read_platform_conventions(project_root),
|
|
372
349
|
"{{TEAM_CONFIG_PATH}}": team_config_path,
|
|
373
350
|
"{{DEV_SUBAGENT_PATH}}": dev_subagent,
|
|
374
351
|
"{{REVIEWER_SUBAGENT_PATH}}": reviewer_subagent,
|
|
375
352
|
"{{SESSION_STATUS_PATH}}": session_status_path,
|
|
376
353
|
"{{PROJECT_ROOT}}": project_root,
|
|
354
|
+
"{{CHECKPOINT_PATH}}": os.path.join(
|
|
355
|
+
".prizmkit", "bugfix", args.bug_id, "workflow-checkpoint.json",
|
|
356
|
+
),
|
|
357
|
+
"{{CHECKPOINT_SYSTEM}}": read_checkpoint_system_section(script_dir),
|
|
377
358
|
"{{FIX_SCOPE}}": fix_scope,
|
|
378
359
|
"{{TIMESTAMP}}": "", # Placeholder, agent fills in the timestamp
|
|
379
360
|
"{{BROWSER_ENABLED}}": "true" if browser_enabled else "false",
|
|
@@ -629,15 +610,6 @@ def main():
|
|
|
629
610
|
|
|
630
611
|
# Build replacements
|
|
631
612
|
replacements = build_replacements(args, bug, global_context, script_dir)
|
|
632
|
-
if is_continuation(args):
|
|
633
|
-
replacements["{{LOG_SIZE_AWARENESS}}"] = ""
|
|
634
|
-
|
|
635
|
-
# Add checkpoint path to replacements
|
|
636
|
-
checkpoint_rel = os.path.join(
|
|
637
|
-
".prizmkit", "bugfix", args.bug_id, "workflow-checkpoint.json",
|
|
638
|
-
)
|
|
639
|
-
replacements["{{CHECKPOINT_PATH}}"] = checkpoint_rel
|
|
640
|
-
|
|
641
613
|
# Render the template
|
|
642
614
|
rendered = render_template(template_content, replacements, bug)
|
|
643
615
|
|
|
@@ -648,6 +620,7 @@ def main():
|
|
|
648
620
|
|
|
649
621
|
# Generate checkpoint file
|
|
650
622
|
project_root = _args_project_root(args, script_dir)
|
|
623
|
+
checkpoint_rel = replacements["{{CHECKPOINT_PATH}}"]
|
|
651
624
|
checkpoint_path = os.path.join(project_root, checkpoint_rel)
|
|
652
625
|
checkpoint_dir = os.path.dirname(checkpoint_path)
|
|
653
626
|
os.makedirs(checkpoint_dir, exist_ok=True)
|