prizmkit 1.1.102 → 1.1.104
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/daemon.py +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +4 -2
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +1 -0
- package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +52 -2
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +18 -10
- package/bundled/dev-pipeline/tests/test_unified_cli.py +76 -0
- package/bundled/skills/_metadata.json +1 -1
- package/package.json +1 -1
package/bundled/VERSION.json
CHANGED
|
@@ -320,7 +320,7 @@ def _payload_command(family: RunnerFamily, paths, list_path: Path, options: Daem
|
|
|
320
320
|
|
|
321
321
|
|
|
322
322
|
def _daemon_env(options: DaemonStartOptions) -> dict[str, str]:
|
|
323
|
-
env: dict[str, str] = {}
|
|
323
|
+
env: dict[str, str] = {"PRIZMKIT_LIVE_OUTPUT": "0"}
|
|
324
324
|
for token in shlex.split(options.env_overrides):
|
|
325
325
|
if "=" in token:
|
|
326
326
|
key, value = token.split("=", 1)
|
|
@@ -37,10 +37,11 @@ class RunnerEnvironment:
|
|
|
37
37
|
|
|
38
38
|
stop_on_failure: bool = False
|
|
39
39
|
auto_push: bool = False
|
|
40
|
-
use_worktree: bool =
|
|
40
|
+
use_worktree: bool = False
|
|
41
41
|
dev_branch: str = ""
|
|
42
42
|
enable_deploy: bool = False
|
|
43
43
|
verbose: bool = False
|
|
44
|
+
live_output: bool = True
|
|
44
45
|
heartbeat_interval_seconds: int = 30
|
|
45
46
|
heartbeat_stale_threshold_seconds: int = 600
|
|
46
47
|
stale_kill_threshold_seconds: int = 600
|
|
@@ -54,10 +55,11 @@ class RunnerEnvironment:
|
|
|
54
55
|
return cls(
|
|
55
56
|
stop_on_failure=_truthy(values.get("STOP_ON_FAILURE")),
|
|
56
57
|
auto_push=_truthy(values.get("AUTO_PUSH")),
|
|
57
|
-
use_worktree=
|
|
58
|
+
use_worktree=_truthy(values.get("USE_WORKTREE")),
|
|
58
59
|
dev_branch=values.get("DEV_BRANCH", ""),
|
|
59
60
|
enable_deploy=_truthy(values.get("ENABLE_DEPLOY")),
|
|
60
61
|
verbose=_truthy(values.get("VERBOSE")),
|
|
62
|
+
live_output=not _falsey(values.get("PRIZMKIT_LIVE_OUTPUT")),
|
|
61
63
|
heartbeat_interval_seconds=_int_value(values, "HEARTBEAT_INTERVAL", 30),
|
|
62
64
|
heartbeat_stale_threshold_seconds=_int_value(
|
|
63
65
|
values,
|
|
@@ -220,6 +220,7 @@ def _process_item(
|
|
|
220
220
|
heartbeat_path=session_paths.heartbeat_json,
|
|
221
221
|
effort=config.effort,
|
|
222
222
|
verbose=env.verbose,
|
|
223
|
+
live_output=env.live_output,
|
|
223
224
|
use_stream_json=stream_json,
|
|
224
225
|
stale_kill_threshold_seconds=env.stale_kill_threshold_seconds,
|
|
225
226
|
)
|
|
@@ -8,6 +8,7 @@ import os
|
|
|
8
8
|
import queue
|
|
9
9
|
import shutil
|
|
10
10
|
import subprocess
|
|
11
|
+
import sys
|
|
11
12
|
import threading
|
|
12
13
|
import time
|
|
13
14
|
from collections import Counter
|
|
@@ -45,6 +46,7 @@ class AISessionConfig:
|
|
|
45
46
|
heartbeat_path: Path | None = None
|
|
46
47
|
effort: str | None = None
|
|
47
48
|
verbose: bool = False
|
|
49
|
+
live_output: bool = False
|
|
48
50
|
use_stream_json: bool = False
|
|
49
51
|
stream_json_format: str = ""
|
|
50
52
|
permission_mode: str = "dangerously-skip-permissions"
|
|
@@ -363,12 +365,45 @@ def _write_progress_summary(progress_path: Path | None, summary: ProgressSummary
|
|
|
363
365
|
progress_path.write_text(json.dumps(summary.to_dict(), indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
364
366
|
|
|
365
367
|
|
|
368
|
+
def _emit_live_line(message: str) -> None:
|
|
369
|
+
"""Print foreground runner status without interfering with file logging."""
|
|
370
|
+
print(message, file=sys.stderr, flush=True)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _emit_live_output(line: str) -> None:
|
|
374
|
+
"""Forward AI CLI output for verbose foreground runs."""
|
|
375
|
+
sys.stdout.write(line)
|
|
376
|
+
sys.stdout.flush()
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _emit_session_start(config: AISessionConfig, command: AISessionCommand, session_log: Path, backup_log: Path) -> None:
|
|
380
|
+
_emit_live_line("[prizmkit] AI session started")
|
|
381
|
+
_emit_live_line(f"[prizmkit] Provider: {command.provider}")
|
|
382
|
+
_emit_live_line(f"[prizmkit] Prompt: {config.prompt_path}")
|
|
383
|
+
_emit_live_line(f"[prizmkit] Session log: {session_log}")
|
|
384
|
+
_emit_live_line(f"[prizmkit] Backup log: {backup_log}")
|
|
385
|
+
if config.progress_path is not None:
|
|
386
|
+
_emit_live_line(f"[prizmkit] Progress: {config.progress_path}")
|
|
387
|
+
if config.heartbeat_path is not None:
|
|
388
|
+
_emit_live_line(f"[prizmkit] Heartbeat: {config.heartbeat_path}")
|
|
389
|
+
if config.verbose:
|
|
390
|
+
_emit_live_line("[prizmkit] VERBOSE=1: forwarding live AI CLI output below")
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _emit_session_finish(result: AISessionResult) -> None:
|
|
394
|
+
duration = max(0.0, result.ended_epoch - result.started_epoch)
|
|
395
|
+
status = result.termination_reason or f"exit_code={result.exit_code}"
|
|
396
|
+
_emit_live_line(f"[prizmkit] AI session finished ({status}, {duration:.1f}s)")
|
|
397
|
+
_emit_live_line(f"[prizmkit] Session log: {result.session_log}")
|
|
398
|
+
|
|
399
|
+
|
|
366
400
|
def _reader_thread(
|
|
367
401
|
pipe,
|
|
368
402
|
session_log: Path,
|
|
369
403
|
backup_log: Path,
|
|
370
404
|
output_queue: "queue.Queue[str]",
|
|
371
405
|
progress_path: Path | None = None,
|
|
406
|
+
live_output: bool = False,
|
|
372
407
|
) -> None:
|
|
373
408
|
lines: list[str] = []
|
|
374
409
|
tracker = None
|
|
@@ -387,6 +422,8 @@ def _reader_thread(
|
|
|
387
422
|
with backup_log.open("a", encoding="utf-8") as backup_fh:
|
|
388
423
|
backup_fh.write(line)
|
|
389
424
|
output_queue.put(line)
|
|
425
|
+
if live_output:
|
|
426
|
+
_emit_live_output(line)
|
|
390
427
|
lines.append(line)
|
|
391
428
|
if progress_path is not None:
|
|
392
429
|
if tracker is not None:
|
|
@@ -472,12 +509,22 @@ class AISessionLauncher:
|
|
|
472
509
|
environment_unsets=("CLAUDECODE",),
|
|
473
510
|
stdin_text=command.stdin_text,
|
|
474
511
|
)
|
|
512
|
+
if self.config.live_output:
|
|
513
|
+
_emit_session_start(self.config, command, session_log, backup_log)
|
|
514
|
+
|
|
475
515
|
handle = launch_process(spec, stderr=subprocess.STDOUT)
|
|
476
516
|
assert handle.popen is not None
|
|
477
517
|
output_queue: queue.Queue[str] = queue.Queue()
|
|
478
518
|
reader = threading.Thread(
|
|
479
519
|
target=_reader_thread,
|
|
480
|
-
args=(
|
|
520
|
+
args=(
|
|
521
|
+
handle.popen.stdout,
|
|
522
|
+
session_log,
|
|
523
|
+
backup_log,
|
|
524
|
+
output_queue,
|
|
525
|
+
self.config.progress_path,
|
|
526
|
+
self.config.live_output and self.config.verbose,
|
|
527
|
+
),
|
|
481
528
|
daemon=True,
|
|
482
529
|
)
|
|
483
530
|
reader.start()
|
|
@@ -544,7 +591,7 @@ class AISessionLauncher:
|
|
|
544
591
|
self.config.progress_path.write_text(json.dumps(merged_progress, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
545
592
|
final_summary = _summary_from_progress_dict(merged_progress, summary)
|
|
546
593
|
ended = time.time()
|
|
547
|
-
|
|
594
|
+
result = AISessionResult(
|
|
548
595
|
command=command,
|
|
549
596
|
exit_code=handle.popen.returncode,
|
|
550
597
|
timed_out=timed_out,
|
|
@@ -558,3 +605,6 @@ class AISessionLauncher:
|
|
|
558
605
|
started_epoch=started,
|
|
559
606
|
ended_epoch=ended,
|
|
560
607
|
)
|
|
608
|
+
if self.config.live_output:
|
|
609
|
+
_emit_session_finish(result)
|
|
610
|
+
return result
|
|
@@ -103,7 +103,7 @@ def test_python_parity_suite_declares_coverage_for_all_legacy_unix_snapshot_scen
|
|
|
103
103
|
"envResolution": {
|
|
104
104
|
"verboseAndStreamJson": ["test_python_env_resolution_scenarios_match_snapshot_contracts"],
|
|
105
105
|
"effortModelAndCli": ["test_ai_session_command_builders_match_unix_runtime_snapshot"],
|
|
106
|
-
"worktreeIgnored": ["
|
|
106
|
+
"worktreeIgnored": ["test_runner_environment_disables_worktree_by_default_and_accepts_truthy_opt_in"],
|
|
107
107
|
"autoPushFlag": ["test_successful_worktree_session_merges_cleans_branch_and_honors_auto_push"],
|
|
108
108
|
},
|
|
109
109
|
"aiCliCommands": {
|
|
@@ -320,13 +320,14 @@ def test_runner_models_parse_legacy_args_and_environment(tmp_path):
|
|
|
320
320
|
assert env.strict_behavior_check is False
|
|
321
321
|
|
|
322
322
|
|
|
323
|
-
def
|
|
323
|
+
def test_runner_environment_disables_worktree_by_default_and_accepts_truthy_opt_in():
|
|
324
324
|
from prizmkit_runtime.runner_models import RunnerEnvironment
|
|
325
325
|
|
|
326
|
-
assert RunnerEnvironment.from_env({}).use_worktree is
|
|
327
|
-
assert RunnerEnvironment.from_env({"USE_WORKTREE": "
|
|
328
|
-
|
|
329
|
-
|
|
326
|
+
assert RunnerEnvironment.from_env({}).use_worktree is False
|
|
327
|
+
assert RunnerEnvironment.from_env({"USE_WORKTREE": "unexpected"}).use_worktree is False
|
|
328
|
+
|
|
329
|
+
for truthy in ("1", "true", "yes", "on"):
|
|
330
|
+
assert RunnerEnvironment.from_env({"USE_WORKTREE": truthy}).use_worktree is True
|
|
330
331
|
|
|
331
332
|
for falsey in ("0", "false", "no", "off"):
|
|
332
333
|
assert RunnerEnvironment.from_env({"USE_WORKTREE": falsey}).use_worktree is False
|
|
@@ -639,10 +640,11 @@ def _install_runner_session_fakes(monkeypatch, runners, *, statuses=("success",)
|
|
|
639
640
|
|
|
640
641
|
|
|
641
642
|
@pytest.mark.parametrize(("kind", "item_id", "branch"), [("feature", "F-001", "dev/F-001-test"), ("bugfix", "B-001", "bugfix/B-001-test")])
|
|
642
|
-
def
|
|
643
|
+
def test_feature_and_bugfix_explicit_worktree_launches_from_worktree_and_renders_worktree_root(monkeypatch, tmp_path, kind, item_id, branch):
|
|
643
644
|
from prizmkit_runtime import runners
|
|
644
645
|
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
645
646
|
|
|
647
|
+
monkeypatch.setenv("USE_WORKTREE", "1")
|
|
646
648
|
monkeypatch.setenv("DEV_BRANCH", branch)
|
|
647
649
|
paths = _make_paths(tmp_path / kind)
|
|
648
650
|
family = family_for(kind, paths)
|
|
@@ -672,6 +674,7 @@ def test_successful_worktree_session_merges_cleans_branch_and_honors_auto_push(m
|
|
|
672
674
|
from prizmkit_runtime import runners
|
|
673
675
|
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
674
676
|
|
|
677
|
+
monkeypatch.setenv("USE_WORKTREE", "1")
|
|
675
678
|
monkeypatch.setenv("DEV_BRANCH", "dev/F-001-test")
|
|
676
679
|
monkeypatch.setenv("AUTO_PUSH", "1")
|
|
677
680
|
paths = _make_paths(tmp_path)
|
|
@@ -692,6 +695,7 @@ def test_worktree_merge_conflict_writes_session_status_and_updates_merge_conflic
|
|
|
692
695
|
from prizmkit_runtime import runners
|
|
693
696
|
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
694
697
|
|
|
698
|
+
monkeypatch.setenv("USE_WORKTREE", "1")
|
|
695
699
|
monkeypatch.setenv("DEV_BRANCH", "dev/F-001-test")
|
|
696
700
|
paths = _make_paths(tmp_path)
|
|
697
701
|
family = family_for("feature", paths)
|
|
@@ -719,6 +723,7 @@ def test_terminal_worktree_failure_saves_wip_and_removes_worktree_without_deleti
|
|
|
719
723
|
from prizmkit_runtime import runners
|
|
720
724
|
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
721
725
|
|
|
726
|
+
monkeypatch.setenv("USE_WORKTREE", "1")
|
|
722
727
|
monkeypatch.setenv("DEV_BRANCH", "dev/F-001-test")
|
|
723
728
|
paths = _make_paths(tmp_path)
|
|
724
729
|
family = family_for("feature", paths)
|
|
@@ -735,7 +740,7 @@ def test_terminal_worktree_failure_saves_wip_and_removes_worktree_without_deleti
|
|
|
735
740
|
assert observed.updates[-1][0] == "crashed"
|
|
736
741
|
|
|
737
742
|
|
|
738
|
-
def
|
|
743
|
+
def test_explicit_worktree_materializes_ignored_support_assets_before_prompt_and_launch(monkeypatch, tmp_path):
|
|
739
744
|
from prizmkit_runtime import runners
|
|
740
745
|
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
741
746
|
from prizmkit_runtime.runner_models import PromptGenerationResult, SessionClassification, family_for, parse_invocation
|
|
@@ -766,6 +771,7 @@ def test_default_worktree_materializes_ignored_support_assets_before_prompt_and_
|
|
|
766
771
|
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
|
|
767
772
|
launch_cwds = []
|
|
768
773
|
|
|
774
|
+
monkeypatch.setenv("USE_WORKTREE", "1")
|
|
769
775
|
monkeypatch.setenv("DEV_BRANCH", "dev/F-001-support-assets")
|
|
770
776
|
monkeypatch.setattr(
|
|
771
777
|
runners,
|
|
@@ -830,11 +836,10 @@ def test_default_worktree_materializes_ignored_support_assets_before_prompt_and_
|
|
|
830
836
|
assert not launch_cwds[0].exists()
|
|
831
837
|
|
|
832
838
|
|
|
833
|
-
def
|
|
839
|
+
def test_default_no_worktree_preserves_main_worktree_branch_launch(monkeypatch, tmp_path):
|
|
834
840
|
from prizmkit_runtime import runners
|
|
835
841
|
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
836
842
|
|
|
837
|
-
monkeypatch.setenv("USE_WORKTREE", "0")
|
|
838
843
|
monkeypatch.setenv("DEV_BRANCH", "dev/F-001-test")
|
|
839
844
|
paths = _make_paths(tmp_path)
|
|
840
845
|
family = family_for("feature", paths)
|
|
@@ -863,6 +868,7 @@ def test_context_overflow_reuses_worktree_and_skips_cleanup_until_terminal_outco
|
|
|
863
868
|
from prizmkit_runtime import runners
|
|
864
869
|
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
865
870
|
|
|
871
|
+
monkeypatch.setenv("USE_WORKTREE", "1")
|
|
866
872
|
monkeypatch.setenv("DEV_BRANCH", "dev/F-001-test")
|
|
867
873
|
paths = _make_paths(tmp_path)
|
|
868
874
|
family = family_for("feature", paths)
|
|
@@ -1117,6 +1123,7 @@ def test_python_daemon_controlled_start_status_stop_matches_snapshot(tmp_path):
|
|
|
1117
1123
|
"#!/usr/bin/env python3\n"
|
|
1118
1124
|
"import os, sys, time\n"
|
|
1119
1125
|
"print('payload env=' + os.environ.get('FOO', ''))\n"
|
|
1126
|
+
"print('payload live=' + os.environ.get('PRIZMKIT_LIVE_OUTPUT', ''))\n"
|
|
1120
1127
|
"print('payload args=' + ' '.join(sys.argv[1:]))\n"
|
|
1121
1128
|
"sys.stdout.flush()\n"
|
|
1122
1129
|
"time.sleep(30)\n",
|
|
@@ -1147,6 +1154,7 @@ def test_python_daemon_controlled_start_status_stop_matches_snapshot(tmp_path):
|
|
|
1147
1154
|
assert json.loads(stop.render())["message"] in {"stopped", "already exited", "not running"}
|
|
1148
1155
|
log_text = (paths.feature_state_dir / "pipeline-daemon.log").read_text(encoding="utf-8")
|
|
1149
1156
|
assert "FOO=bar" in log_text or "payload env=bar" in log_text
|
|
1157
|
+
assert "payload live=0" in log_text
|
|
1150
1158
|
assert "--project-root" in log_text
|
|
1151
1159
|
|
|
1152
1160
|
|
|
@@ -617,6 +617,82 @@ def test_session_dual_writes_progress_and_recovers_backup(tmp_path):
|
|
|
617
617
|
assert second_log.read_text(encoding="utf-8") == "larger recovered log"
|
|
618
618
|
|
|
619
619
|
|
|
620
|
+
def test_session_live_output_prints_paths_and_verbose_cli_output(tmp_path, capsys):
|
|
621
|
+
from prizmkit_runtime.sessions import AISessionConfig, AISessionLauncher
|
|
622
|
+
|
|
623
|
+
fake_cli = tmp_path / "live_cli.py"
|
|
624
|
+
fake_cli.write_text(
|
|
625
|
+
"#!/usr/bin/env python3\n"
|
|
626
|
+
"print('live-line', flush=True)\n",
|
|
627
|
+
encoding="utf-8",
|
|
628
|
+
)
|
|
629
|
+
fake_cli.chmod(0o755)
|
|
630
|
+
prompt = tmp_path / "prompt.md"
|
|
631
|
+
prompt.write_text("hello", encoding="utf-8")
|
|
632
|
+
session_log = tmp_path / "state" / "sessions" / "sess-live" / "logs" / "session.log"
|
|
633
|
+
progress = session_log.with_name("progress.json")
|
|
634
|
+
heartbeat = session_log.with_name("heartbeat.json")
|
|
635
|
+
|
|
636
|
+
result = AISessionLauncher(
|
|
637
|
+
AISessionConfig(
|
|
638
|
+
cli_command=str(fake_cli),
|
|
639
|
+
platform="codebuddy",
|
|
640
|
+
model=None,
|
|
641
|
+
prompt_path=prompt,
|
|
642
|
+
cwd=tmp_path,
|
|
643
|
+
log_path=session_log,
|
|
644
|
+
progress_path=progress,
|
|
645
|
+
heartbeat_path=heartbeat,
|
|
646
|
+
verbose=True,
|
|
647
|
+
live_output=True,
|
|
648
|
+
)
|
|
649
|
+
).run()
|
|
650
|
+
|
|
651
|
+
captured = capsys.readouterr()
|
|
652
|
+
assert result.exit_code == 0
|
|
653
|
+
assert "live-line" in captured.out
|
|
654
|
+
assert "[prizmkit] AI session started" in captured.err
|
|
655
|
+
assert f"[prizmkit] Prompt: {prompt}" in captured.err
|
|
656
|
+
assert f"[prizmkit] Session log: {session_log}" in captured.err
|
|
657
|
+
assert f"[prizmkit] Progress: {progress}" in captured.err
|
|
658
|
+
assert f"[prizmkit] Heartbeat: {heartbeat}" in captured.err
|
|
659
|
+
assert "[prizmkit] AI session finished" in captured.err
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def test_session_live_output_does_not_forward_cli_output_without_verbose(tmp_path, capsys):
|
|
663
|
+
from prizmkit_runtime.sessions import AISessionConfig, AISessionLauncher
|
|
664
|
+
|
|
665
|
+
fake_cli = tmp_path / "quiet_cli.py"
|
|
666
|
+
fake_cli.write_text(
|
|
667
|
+
"#!/usr/bin/env python3\n"
|
|
668
|
+
"print('hidden-line', flush=True)\n",
|
|
669
|
+
encoding="utf-8",
|
|
670
|
+
)
|
|
671
|
+
fake_cli.chmod(0o755)
|
|
672
|
+
prompt = tmp_path / "prompt.md"
|
|
673
|
+
prompt.write_text("hello", encoding="utf-8")
|
|
674
|
+
session_log = tmp_path / "state" / "sessions" / "sess-quiet" / "logs" / "session.log"
|
|
675
|
+
|
|
676
|
+
result = AISessionLauncher(
|
|
677
|
+
AISessionConfig(
|
|
678
|
+
cli_command=str(fake_cli),
|
|
679
|
+
platform="codebuddy",
|
|
680
|
+
model=None,
|
|
681
|
+
prompt_path=prompt,
|
|
682
|
+
cwd=tmp_path,
|
|
683
|
+
log_path=session_log,
|
|
684
|
+
live_output=True,
|
|
685
|
+
)
|
|
686
|
+
).run()
|
|
687
|
+
|
|
688
|
+
captured = capsys.readouterr()
|
|
689
|
+
assert result.exit_code == 0
|
|
690
|
+
assert "hidden-line" not in captured.out
|
|
691
|
+
assert "hidden-line" in session_log.read_text(encoding="utf-8")
|
|
692
|
+
assert "[prizmkit] AI session started" in captured.err
|
|
693
|
+
assert "[prizmkit] AI session finished" in captured.err
|
|
694
|
+
|
|
695
|
+
|
|
620
696
|
def test_session_final_progress_merge_preserves_tracker_fatal_error(tmp_path):
|
|
621
697
|
from prizmkit_runtime.sessions import AISessionConfig, AISessionLauncher
|
|
622
698
|
|