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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.102",
3
- "bundledAt": "2026-07-06T15:09:00.037Z",
4
- "bundledFrom": "e918269"
2
+ "frameworkVersion": "1.1.104",
3
+ "bundledAt": "2026-07-06T16:42:02.971Z",
4
+ "bundledFrom": "05c5c13"
5
5
  }
@@ -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 = True
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=not _falsey(values.get("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=(handle.popen.stdout, session_log, backup_log, output_queue, self.config.progress_path),
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
- return AISessionResult(
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": ["test_runner_environment_enables_worktree_by_default_and_accepts_falsey_opt_out"],
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 test_runner_environment_enables_worktree_by_default_and_accepts_falsey_opt_out():
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 True
327
- assert RunnerEnvironment.from_env({"USE_WORKTREE": "1"}).use_worktree is True
328
- assert RunnerEnvironment.from_env({"USE_WORKTREE": "true"}).use_worktree is True
329
- assert RunnerEnvironment.from_env({"USE_WORKTREE": "unexpected"}).use_worktree is True
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 test_feature_and_bugfix_default_worktree_launches_from_worktree_and_renders_worktree_root(monkeypatch, tmp_path, kind, item_id, branch):
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 test_default_worktree_materializes_ignored_support_assets_before_prompt_and_launch(monkeypatch, tmp_path):
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 test_use_worktree_false_preserves_main_worktree_branch_launch(monkeypatch, tmp_path):
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
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.102",
2
+ "version": "1.1.104",
3
3
  "skills": {
4
4
  "prizm-kit": {
5
5
  "description": "Full-lifecycle dev toolkit. Covers spec-driven development, Prizm context docs, code quality, debugging, deployment, and knowledge management.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prizmkit",
3
- "version": "1.1.102",
3
+ "version": "1.1.104",
4
4
  "description": "Create a new PrizmKit-powered project with clean initialization — no framework dev files, just what you need.",
5
5
  "type": "module",
6
6
  "bin": {