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
|
@@ -171,12 +171,12 @@ def test_runtime_config_matches_canonical_ai_cli_precedence(tmp_path):
|
|
|
171
171
|
pipeline = project / ".prizmkit" / "dev-pipeline"
|
|
172
172
|
pipeline.mkdir(parents=True)
|
|
173
173
|
prizmkit_dir = project / ".prizmkit"
|
|
174
|
-
(prizmkit_dir / "config.json").write_text(json.dumps({"ai_cli": "
|
|
174
|
+
(prizmkit_dir / "config.json").write_text(json.dumps({"ai_cli": "cbc", "platform": "claude"}), encoding="utf-8")
|
|
175
175
|
(prizmkit_dir / "manifest.json").write_text(json.dumps({"platform": "codex"}), encoding="utf-8")
|
|
176
176
|
paths = resolve_runtime_paths(pipeline_root=pipeline)
|
|
177
177
|
|
|
178
178
|
config_cli = load_runtime_config(paths, env={"PATH": ""}).ai_client
|
|
179
|
-
assert config_cli.command == "
|
|
179
|
+
assert config_cli.command == "cbc"
|
|
180
180
|
assert config_cli.platform == "codebuddy"
|
|
181
181
|
assert config_cli.source == "prizmkit-config:ai_cli"
|
|
182
182
|
|
|
@@ -195,6 +195,73 @@ def test_runtime_config_matches_canonical_ai_cli_precedence(tmp_path):
|
|
|
195
195
|
assert legacy_alias.source == "environment:CODEBUDDY_CLI"
|
|
196
196
|
|
|
197
197
|
|
|
198
|
+
def test_runtime_config_resolves_custom_ai_cli_launch_profile(tmp_path):
|
|
199
|
+
from prizmkit_runtime.config import RuntimeConfigError, load_runtime_config
|
|
200
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
201
|
+
|
|
202
|
+
project = tmp_path / "project"
|
|
203
|
+
pipeline = project / ".prizmkit" / "dev-pipeline"
|
|
204
|
+
pipeline.mkdir(parents=True)
|
|
205
|
+
prizmkit_dir = project / ".prizmkit"
|
|
206
|
+
(prizmkit_dir / "config.json").write_text(
|
|
207
|
+
json.dumps(
|
|
208
|
+
{
|
|
209
|
+
"ai_cli": "tclaude",
|
|
210
|
+
"ai_cli_launch": {
|
|
211
|
+
"profile": "custom",
|
|
212
|
+
"prompt_arg": "-i",
|
|
213
|
+
"extra_args": ["--verbose", "--path", "two words"],
|
|
214
|
+
},
|
|
215
|
+
}
|
|
216
|
+
),
|
|
217
|
+
encoding="utf-8",
|
|
218
|
+
)
|
|
219
|
+
paths = resolve_runtime_paths(pipeline_root=pipeline)
|
|
220
|
+
|
|
221
|
+
config = load_runtime_config(paths, env={"PATH": ""})
|
|
222
|
+
|
|
223
|
+
assert config.ai_client.command == "tclaude"
|
|
224
|
+
assert config.ai_client.platform == "custom"
|
|
225
|
+
assert config.ai_client.source == "prizmkit-config:ai_cli_launch"
|
|
226
|
+
assert config.ai_client.launch_profile is not None
|
|
227
|
+
assert config.ai_client.launch_profile.prompt_arg == "-i"
|
|
228
|
+
assert config.ai_client.launch_profile.extra_args == ("--verbose", "--path", "two words")
|
|
229
|
+
|
|
230
|
+
(prizmkit_dir / "config.json").write_text(json.dumps({"ai_cli": "tclaude"}), encoding="utf-8")
|
|
231
|
+
with pytest.raises(RuntimeConfigError, match="prompt injection argument"):
|
|
232
|
+
load_runtime_config(paths, env={"PATH": ""})
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
(prizmkit_dir / "config.json").write_text(
|
|
236
|
+
json.dumps(
|
|
237
|
+
{
|
|
238
|
+
"ai_cli": "claude",
|
|
239
|
+
"ai_cli_launch": {"profile": "custom", "prompt_arg": "-i", "extra_args": ["--stale"]},
|
|
240
|
+
}
|
|
241
|
+
),
|
|
242
|
+
encoding="utf-8",
|
|
243
|
+
)
|
|
244
|
+
built_in = load_runtime_config(paths, env={"PATH": ""}).ai_client
|
|
245
|
+
assert built_in.command == "claude"
|
|
246
|
+
assert built_in.platform == "claude"
|
|
247
|
+
assert built_in.source == "prizmkit-config:ai_cli"
|
|
248
|
+
assert built_in.launch_profile is None
|
|
249
|
+
|
|
250
|
+
(prizmkit_dir / "config.json").write_text(
|
|
251
|
+
json.dumps({"ai_cli": "tclaude", "ai_cli_launch": {"prompt_arg": "-i"}}),
|
|
252
|
+
encoding="utf-8",
|
|
253
|
+
)
|
|
254
|
+
with pytest.raises(RuntimeConfigError, match="profile"):
|
|
255
|
+
load_runtime_config(paths, env={"PATH": ""})
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
env_override = load_runtime_config(paths, env={"AI_CLI": "claude", "PATH": ""}).ai_client
|
|
259
|
+
assert env_override.command == "claude"
|
|
260
|
+
assert env_override.platform == "claude"
|
|
261
|
+
assert env_override.source == "environment:AI_CLI"
|
|
262
|
+
assert env_override.launch_profile is None
|
|
263
|
+
|
|
264
|
+
|
|
198
265
|
def test_runtime_config_supplied_env_does_not_leak_ambient_path(tmp_path):
|
|
199
266
|
from prizmkit_runtime.config import load_runtime_config
|
|
200
267
|
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
@@ -393,7 +460,7 @@ def test_interoperability_preserves_existing_utility_mapping():
|
|
|
393
460
|
assert actual_scripts == mapped_scripts | explicitly_library_only
|
|
394
461
|
assert explicitly_library_only == {"continuation.py", "utils.py"}
|
|
395
462
|
assert "parse-stream-progress.py" in mapped_scripts
|
|
396
|
-
assert "prizmkit-test-gate.py" in mapped_scripts
|
|
463
|
+
assert "prizmkit-test-gate.py" not in mapped_scripts
|
|
397
464
|
|
|
398
465
|
|
|
399
466
|
def test_boundary_dataclasses_are_cohesive_and_frozen():
|
|
@@ -445,14 +512,14 @@ def test_runtime_config_loads_prizmkit_env_and_validates_effort(tmp_path):
|
|
|
445
512
|
pipeline.mkdir(parents=True)
|
|
446
513
|
prizmkit_dir = project / ".prizmkit"
|
|
447
514
|
(prizmkit_dir / ".env").write_text(
|
|
448
|
-
"AI_CLI=
|
|
515
|
+
"AI_CLI=claude\nPRIZMKIT_EFFORT=max\nMODEL=env-model\n",
|
|
449
516
|
encoding="utf-8",
|
|
450
517
|
)
|
|
451
518
|
paths = resolve_runtime_paths(pipeline_root=pipeline)
|
|
452
519
|
|
|
453
520
|
config = load_runtime_config(paths, env={"PATH": ""})
|
|
454
521
|
|
|
455
|
-
assert config.ai_client.command == "
|
|
522
|
+
assert config.ai_client.command == "claude"
|
|
456
523
|
assert config.ai_client.platform == "claude"
|
|
457
524
|
assert config.ai_client.source == "environment:AI_CLI"
|
|
458
525
|
assert config.model == "env-model"
|
|
@@ -541,6 +608,212 @@ def test_ai_session_command_builders_match_unix_runtime_snapshot(tmp_path):
|
|
|
541
608
|
assert codebuddy.stdin_text == "hello"
|
|
542
609
|
|
|
543
610
|
|
|
611
|
+
def test_custom_ai_session_command_builder_injects_prompt_and_tokenizes_extra_args(tmp_path):
|
|
612
|
+
from prizmkit_runtime.launch_profiles import HeadlessLaunchProfile
|
|
613
|
+
from prizmkit_runtime.sessions import AISessionConfig, build_ai_session_command
|
|
614
|
+
|
|
615
|
+
prompt = tmp_path / "prompt.md"
|
|
616
|
+
prompt.write_text("injected prompt", encoding="utf-8")
|
|
617
|
+
project = tmp_path / "project"
|
|
618
|
+
project.mkdir()
|
|
619
|
+
|
|
620
|
+
command = build_ai_session_command(
|
|
621
|
+
AISessionConfig(
|
|
622
|
+
cli_command="tclaude",
|
|
623
|
+
platform="custom",
|
|
624
|
+
model="ignored-model",
|
|
625
|
+
prompt_path=prompt,
|
|
626
|
+
cwd=project,
|
|
627
|
+
effort="ignored-effort",
|
|
628
|
+
use_stream_json=True,
|
|
629
|
+
launch_profile=HeadlessLaunchProfile(
|
|
630
|
+
profile="custom",
|
|
631
|
+
prompt_arg="-i",
|
|
632
|
+
extra_args=("--verbose", "--path", "two words"),
|
|
633
|
+
),
|
|
634
|
+
)
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
assert command.argv == ("tclaude", "-i", "injected prompt", "--verbose", "--path", "two words")
|
|
638
|
+
assert command.stdin_text is None
|
|
639
|
+
assert command.provider == "custom"
|
|
640
|
+
assert command.prompt_mode == "argument"
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def test_start_command_serialization_redacts_prompt_and_preserves_provider_flags(tmp_path):
|
|
644
|
+
from prizmkit_runtime.launch_profiles import HeadlessLaunchProfile
|
|
645
|
+
from prizmkit_runtime.sessions import AISessionConfig, build_ai_session_command, format_start_command
|
|
646
|
+
|
|
647
|
+
prompt = tmp_path / "prompt.md"
|
|
648
|
+
secret_prompt = "secret prompt body with spaces"
|
|
649
|
+
prompt.write_text(secret_prompt, encoding="utf-8")
|
|
650
|
+
project = tmp_path / "project"
|
|
651
|
+
project.mkdir()
|
|
652
|
+
|
|
653
|
+
claude = format_start_command(
|
|
654
|
+
build_ai_session_command(
|
|
655
|
+
AISessionConfig(
|
|
656
|
+
cli_command="claude",
|
|
657
|
+
platform="claude",
|
|
658
|
+
model=None,
|
|
659
|
+
prompt_path=prompt,
|
|
660
|
+
cwd=project,
|
|
661
|
+
)
|
|
662
|
+
)
|
|
663
|
+
)
|
|
664
|
+
assert "claude -p <prompt>" in claude
|
|
665
|
+
assert "--disallowedTools EnterWorktree" in claude
|
|
666
|
+
assert secret_prompt not in claude
|
|
667
|
+
|
|
668
|
+
codebuddy = format_start_command(
|
|
669
|
+
build_ai_session_command(
|
|
670
|
+
AISessionConfig(
|
|
671
|
+
cli_command="cbc",
|
|
672
|
+
platform="codebuddy",
|
|
673
|
+
model=None,
|
|
674
|
+
prompt_path=prompt,
|
|
675
|
+
cwd=project,
|
|
676
|
+
)
|
|
677
|
+
)
|
|
678
|
+
)
|
|
679
|
+
assert codebuddy.startswith("cbc --print -y")
|
|
680
|
+
assert "--disallowedTools EnterWorktree" in codebuddy
|
|
681
|
+
assert secret_prompt not in codebuddy
|
|
682
|
+
|
|
683
|
+
codex = format_start_command(
|
|
684
|
+
build_ai_session_command(
|
|
685
|
+
AISessionConfig(
|
|
686
|
+
cli_command="codex",
|
|
687
|
+
platform="codex",
|
|
688
|
+
model=None,
|
|
689
|
+
prompt_path=prompt,
|
|
690
|
+
cwd=project,
|
|
691
|
+
)
|
|
692
|
+
)
|
|
693
|
+
)
|
|
694
|
+
assert "--profile no-worktree" in codex
|
|
695
|
+
assert "exec --cd" in codex
|
|
696
|
+
assert codex.endswith(" -")
|
|
697
|
+
assert secret_prompt not in codex
|
|
698
|
+
|
|
699
|
+
custom = format_start_command(
|
|
700
|
+
build_ai_session_command(
|
|
701
|
+
AISessionConfig(
|
|
702
|
+
cli_command="tclaude",
|
|
703
|
+
platform="custom",
|
|
704
|
+
model=None,
|
|
705
|
+
prompt_path=prompt,
|
|
706
|
+
cwd=project,
|
|
707
|
+
launch_profile=HeadlessLaunchProfile(
|
|
708
|
+
profile="custom",
|
|
709
|
+
prompt_arg="-i",
|
|
710
|
+
extra_args=("--custom-flag", "two words", secret_prompt),
|
|
711
|
+
),
|
|
712
|
+
)
|
|
713
|
+
)
|
|
714
|
+
)
|
|
715
|
+
assert custom == "tclaude -i <prompt> --custom-flag 'two words' <prompt>"
|
|
716
|
+
assert custom.count("<prompt>") == 2
|
|
717
|
+
assert secret_prompt not in custom
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def test_session_preamble_writes_redacted_start_command_for_all_launch_profiles(tmp_path, monkeypatch):
|
|
721
|
+
import prizmkit_runtime.sessions as sessions
|
|
722
|
+
from prizmkit_runtime.launch_profiles import HeadlessLaunchProfile
|
|
723
|
+
|
|
724
|
+
fake_cli = tmp_path / "fake_cli.py"
|
|
725
|
+
fake_cli.write_text(
|
|
726
|
+
"#!/usr/bin/env python3\n"
|
|
727
|
+
"import json, sys\n"
|
|
728
|
+
"_stdin = sys.stdin.read()\n"
|
|
729
|
+
"print(json.dumps({'type':'result','subtype':'success','usage':{'tokens':1},'result':'ok'}), flush=True)\n",
|
|
730
|
+
encoding="utf-8",
|
|
731
|
+
)
|
|
732
|
+
fake_cli.chmod(0o755)
|
|
733
|
+
|
|
734
|
+
backup_texts = {}
|
|
735
|
+
original_recover = sessions.recover_session_log
|
|
736
|
+
|
|
737
|
+
def spy_recover(primary, backup, min_size=1024):
|
|
738
|
+
if backup.is_file():
|
|
739
|
+
backup_texts[str(backup)] = backup.read_text(encoding="utf-8")
|
|
740
|
+
return original_recover(primary, backup, min_size)
|
|
741
|
+
|
|
742
|
+
monkeypatch.setattr(sessions, "recover_session_log", spy_recover)
|
|
743
|
+
|
|
744
|
+
cases = [
|
|
745
|
+
("claude", "claude", None, "--disallowedTools EnterWorktree", "-p <prompt>"),
|
|
746
|
+
("codebuddy", "codebuddy", None, "--disallowedTools EnterWorktree", "--print -y"),
|
|
747
|
+
("codex", "codex", None, "--profile no-worktree", "exec --cd"),
|
|
748
|
+
(
|
|
749
|
+
"custom",
|
|
750
|
+
"custom",
|
|
751
|
+
HeadlessLaunchProfile(profile="custom", prompt_arg="-i", extra_args=("--custom-flag", "two words")),
|
|
752
|
+
"--custom-flag 'two words'",
|
|
753
|
+
"-i <prompt>",
|
|
754
|
+
),
|
|
755
|
+
]
|
|
756
|
+
|
|
757
|
+
for label, platform, launch_profile, expected_flag, expected_prompt_marker in cases:
|
|
758
|
+
secret_prompt = f"secret prompt body for {label}"
|
|
759
|
+
prompt = tmp_path / f"{label}-prompt.md"
|
|
760
|
+
prompt.write_text(secret_prompt, encoding="utf-8")
|
|
761
|
+
session_log = tmp_path / "state" / "sessions" / f"sess-{label}" / "logs" / "session.log"
|
|
762
|
+
backup_log = tmp_path / "home" / ".prizmkit" / "session-backups" / f"sess-{label}.log"
|
|
763
|
+
|
|
764
|
+
result = sessions.AISessionLauncher(
|
|
765
|
+
sessions.AISessionConfig(
|
|
766
|
+
cli_command=str(fake_cli),
|
|
767
|
+
platform=platform,
|
|
768
|
+
model=None,
|
|
769
|
+
prompt_path=prompt,
|
|
770
|
+
cwd=tmp_path,
|
|
771
|
+
log_path=session_log,
|
|
772
|
+
backup_log_path=backup_log,
|
|
773
|
+
launch_profile=launch_profile,
|
|
774
|
+
)
|
|
775
|
+
).run()
|
|
776
|
+
|
|
777
|
+
assert result.exit_code == 0
|
|
778
|
+
primary_text = session_log.read_text(encoding="utf-8")
|
|
779
|
+
backup_text = backup_texts[str(backup_log)]
|
|
780
|
+
for text in (primary_text, backup_text):
|
|
781
|
+
lines = text.splitlines()
|
|
782
|
+
assert lines[0] == "=== PRIZMKIT SESSION START ==="
|
|
783
|
+
assert lines[1] == "Prompt: <prompt>"
|
|
784
|
+
assert lines[2] == "Model: default"
|
|
785
|
+
assert lines[3].startswith("StartCommand: ")
|
|
786
|
+
assert expected_flag in lines[3]
|
|
787
|
+
assert expected_prompt_marker in lines[3]
|
|
788
|
+
assert lines[4].startswith("Started at epoch: ")
|
|
789
|
+
assert lines[5] == "--- OUTPUT BELOW ---"
|
|
790
|
+
assert secret_prompt not in "\n".join(lines[:6])
|
|
791
|
+
assert str(prompt) not in "\n".join(lines[:6])
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def test_headless_subagent_prompt_guidance_keeps_no_worktree_boundary_visible():
|
|
795
|
+
guidance_files = [
|
|
796
|
+
REPO_ROOT / "dev-pipeline" / "templates" / "sections" / "phase-critic-plan.md",
|
|
797
|
+
REPO_ROOT / "dev-pipeline" / "templates" / "sections" / "phase-critic-plan-full.md",
|
|
798
|
+
REPO_ROOT / "dev-pipeline" / "templates" / "agent-prompts" / "critic-plan-challenge.md",
|
|
799
|
+
REPO_ROOT / "dev-pipeline" / "templates" / "bootstrap-tier2.md",
|
|
800
|
+
REPO_ROOT / "dev-pipeline" / "templates" / "bootstrap-tier3.md",
|
|
801
|
+
REPO_ROOT / "core" / "skills" / "prizmkit-skill" / "prizmkit-code-review" / "SKILL.md",
|
|
802
|
+
]
|
|
803
|
+
|
|
804
|
+
for path in guidance_files:
|
|
805
|
+
text = path.read_text(encoding="utf-8")
|
|
806
|
+
assert "default/no worktree" in text, path
|
|
807
|
+
assert "do not request worktree isolation" in text.lower(), path
|
|
808
|
+
assert ".claude/worktrees/..." in text, path
|
|
809
|
+
assert ".prizmkit/state/worktrees/..." in text, path
|
|
810
|
+
assert "USE_WORKTREE" in text, path
|
|
811
|
+
|
|
812
|
+
for path in guidance_files[:-1]:
|
|
813
|
+
text = path.read_text(encoding="utf-8")
|
|
814
|
+
assert "Read existing source files in the modules this plan touches" not in text, path
|
|
815
|
+
|
|
816
|
+
|
|
544
817
|
def test_progress_parser_adds_required_metadata(tmp_path):
|
|
545
818
|
import importlib.util
|
|
546
819
|
|
|
@@ -617,6 +890,93 @@ def test_session_dual_writes_progress_and_recovers_backup(tmp_path):
|
|
|
617
890
|
assert second_log.read_text(encoding="utf-8") == "larger recovered log"
|
|
618
891
|
|
|
619
892
|
|
|
893
|
+
def test_custom_session_launch_writes_existing_logs_and_progress(tmp_path):
|
|
894
|
+
from prizmkit_runtime.launch_profiles import HeadlessLaunchProfile
|
|
895
|
+
from prizmkit_runtime.sessions import AISessionConfig, AISessionLauncher
|
|
896
|
+
|
|
897
|
+
fake_cli = tmp_path / "custom_cli.py"
|
|
898
|
+
fake_cli.write_text(
|
|
899
|
+
"#!/usr/bin/env python3\n"
|
|
900
|
+
"import json, sys\n"
|
|
901
|
+
"assert sys.argv[1] == '-i'\n"
|
|
902
|
+
"assert sys.argv[2] == 'hello custom'\n"
|
|
903
|
+
"assert sys.argv[3:] == ['--verbose', '--path', 'two words']\n"
|
|
904
|
+
"print(json.dumps({'type':'result','subtype':'success','usage':{'tokens':1},'result':'ok'}), flush=True)\n",
|
|
905
|
+
encoding="utf-8",
|
|
906
|
+
)
|
|
907
|
+
fake_cli.chmod(0o755)
|
|
908
|
+
prompt = tmp_path / "prompt.md"
|
|
909
|
+
prompt.write_text("hello custom", encoding="utf-8")
|
|
910
|
+
session_log = tmp_path / "state" / "sessions" / "sess-custom" / "logs" / "session.log"
|
|
911
|
+
progress = session_log.with_name("progress.json")
|
|
912
|
+
|
|
913
|
+
result = AISessionLauncher(
|
|
914
|
+
AISessionConfig(
|
|
915
|
+
cli_command=str(fake_cli),
|
|
916
|
+
platform="custom",
|
|
917
|
+
model=None,
|
|
918
|
+
prompt_path=prompt,
|
|
919
|
+
cwd=tmp_path,
|
|
920
|
+
log_path=session_log,
|
|
921
|
+
progress_path=progress,
|
|
922
|
+
launch_profile=HeadlessLaunchProfile(
|
|
923
|
+
profile="custom",
|
|
924
|
+
prompt_arg="-i",
|
|
925
|
+
extra_args=("--verbose", "--path", "two words"),
|
|
926
|
+
),
|
|
927
|
+
)
|
|
928
|
+
).run()
|
|
929
|
+
|
|
930
|
+
assert result.exit_code == 0
|
|
931
|
+
assert result.command.argv[1:4] == ("-i", "hello custom", "--verbose")
|
|
932
|
+
assert session_log.exists()
|
|
933
|
+
assert progress.exists()
|
|
934
|
+
progress_data = json.loads(progress.read_text(encoding="utf-8"))
|
|
935
|
+
assert progress_data["event_type_counts"]["result"] == 1
|
|
936
|
+
assert progress_data["has_usage"] is True
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
@pytest.mark.parametrize("platform", ["claude", "codex", "codebuddy"])
|
|
940
|
+
def test_builtin_provider_launches_write_session_artifacts(tmp_path, platform):
|
|
941
|
+
from prizmkit_runtime.sessions import AISessionConfig, AISessionLauncher
|
|
942
|
+
|
|
943
|
+
fake_cli = tmp_path / f"{platform}_cli.py"
|
|
944
|
+
fake_cli.write_text(
|
|
945
|
+
"#!/usr/bin/env python3\n"
|
|
946
|
+
"import json, sys\n"
|
|
947
|
+
"_stdin = sys.stdin.read()\n"
|
|
948
|
+
"print(json.dumps({'type':'result','subtype':'success','usage':{'tokens':1},'result':'ok'}), flush=True)\n",
|
|
949
|
+
encoding="utf-8",
|
|
950
|
+
)
|
|
951
|
+
fake_cli.chmod(0o755)
|
|
952
|
+
prompt = tmp_path / f"{platform}-prompt.md"
|
|
953
|
+
prompt.write_text(f"hello {platform}", encoding="utf-8")
|
|
954
|
+
session_log = tmp_path / "state" / "sessions" / f"sess-{platform}" / "logs" / "session.log"
|
|
955
|
+
progress = session_log.with_name("progress.json")
|
|
956
|
+
heartbeat = session_log.with_name("heartbeat.json")
|
|
957
|
+
|
|
958
|
+
result = AISessionLauncher(
|
|
959
|
+
AISessionConfig(
|
|
960
|
+
cli_command=str(fake_cli),
|
|
961
|
+
platform=platform,
|
|
962
|
+
model=None,
|
|
963
|
+
prompt_path=prompt,
|
|
964
|
+
cwd=tmp_path,
|
|
965
|
+
log_path=session_log,
|
|
966
|
+
progress_path=progress,
|
|
967
|
+
heartbeat_path=heartbeat,
|
|
968
|
+
use_stream_json=True,
|
|
969
|
+
)
|
|
970
|
+
).run()
|
|
971
|
+
|
|
972
|
+
progress_data = json.loads(progress.read_text(encoding="utf-8"))
|
|
973
|
+
assert result.exit_code == 0
|
|
974
|
+
assert session_log.exists()
|
|
975
|
+
assert heartbeat.exists()
|
|
976
|
+
assert progress_data["event_type_counts"]["result"] == 1
|
|
977
|
+
assert progress_data["has_usage"] is True
|
|
978
|
+
|
|
979
|
+
|
|
620
980
|
def test_session_live_output_prints_paths_and_verbose_cli_output(tmp_path, capsys):
|
|
621
981
|
from prizmkit_runtime.sessions import AISessionConfig, AISessionLauncher
|
|
622
982
|
|
|
@@ -659,7 +1019,7 @@ def test_session_live_output_prints_paths_and_verbose_cli_output(tmp_path, capsy
|
|
|
659
1019
|
assert "[prizmkit] AI session finished" in captured.err
|
|
660
1020
|
|
|
661
1021
|
|
|
662
|
-
def
|
|
1022
|
+
def test_compact_progress_line_hides_stale_until_no_progress(tmp_path):
|
|
663
1023
|
from prizmkit_runtime.heartbeat import HeartbeatState
|
|
664
1024
|
from prizmkit_runtime.sessions import _compact_progress_line
|
|
665
1025
|
|
|
@@ -679,10 +1039,14 @@ def test_compact_progress_line_includes_stale_and_switches_glyph(tmp_path):
|
|
|
679
1039
|
encoding="utf-8",
|
|
680
1040
|
)
|
|
681
1041
|
running = HeartbeatState(last_seen_epoch=1.0, stale=False, details="progress", stale_seconds=0, log_size=4 * 1024 * 1024)
|
|
682
|
-
|
|
1042
|
+
brief_pause = HeartbeatState(last_seen_epoch=1.0, stale=False, details="no progress", stale_seconds=125, log_size=4 * 1024 * 1024)
|
|
1043
|
+
paused = HeartbeatState(last_seen_epoch=1.0, stale=True, details="no progress", stale_seconds=15 * 60, log_size=4 * 1024 * 1024)
|
|
1044
|
+
resumed = HeartbeatState(last_seen_epoch=3.0, stale=False, details="progress", stale_seconds=0, log_size=5 * 1024 * 1024)
|
|
683
1045
|
|
|
684
1046
|
running_line = _compact_progress_line(0, session_log, progress, running, stale_threshold_seconds=15 * 60)
|
|
1047
|
+
brief_pause_line = _compact_progress_line(0, session_log, progress, brief_pause, stale_threshold_seconds=15 * 60)
|
|
685
1048
|
paused_line = _compact_progress_line(0, session_log, progress, paused, stale_threshold_seconds=15 * 60)
|
|
1049
|
+
resumed_line = _compact_progress_line(0, session_log, progress, resumed, stale_threshold_seconds=15 * 60)
|
|
686
1050
|
|
|
687
1051
|
assert "▶ [HEARTBEAT]" in running_line
|
|
688
1052
|
assert "log: 4MB" in running_line
|
|
@@ -691,12 +1055,18 @@ def test_compact_progress_line_includes_stale_and_switches_glyph(tmp_path):
|
|
|
691
1055
|
assert "tool: Read" in running_line
|
|
692
1056
|
assert "msgs: 465" in running_line
|
|
693
1057
|
assert "412 tool calls" in running_line
|
|
694
|
-
assert "stale:
|
|
1058
|
+
assert "stale:" not in running_line
|
|
1059
|
+
assert "▶ [HEARTBEAT]" in brief_pause_line
|
|
1060
|
+
assert "stale:" not in brief_pause_line
|
|
695
1061
|
assert "⏸ [HEARTBEAT]" in paused_line
|
|
696
|
-
assert "stale:
|
|
1062
|
+
assert "stale: 15m/15m" in paused_line
|
|
1063
|
+
assert "▶ [HEARTBEAT]" in resumed_line
|
|
1064
|
+
assert "log: 5MB" in resumed_line
|
|
1065
|
+
assert "stale:" not in resumed_line
|
|
697
1066
|
|
|
698
1067
|
|
|
699
|
-
def
|
|
1068
|
+
def test_heartbeat_observation_stale_accumulates_by_elapsed_time_and_resets_on_progress(tmp_path, monkeypatch):
|
|
1069
|
+
import prizmkit_runtime.heartbeat as heartbeat
|
|
700
1070
|
from prizmkit_runtime.heartbeat import HeartbeatConfig, observe_heartbeat_state
|
|
701
1071
|
|
|
702
1072
|
session_log = tmp_path / "session.log"
|
|
@@ -710,19 +1080,164 @@ def test_heartbeat_observation_stale_accumulates_and_resets_on_progress(tmp_path
|
|
|
710
1080
|
interval_seconds=60,
|
|
711
1081
|
stale_after_seconds=15 * 60,
|
|
712
1082
|
)
|
|
1083
|
+
ticks = iter([1000.0, 1025.0, 1090.0, 1095.0])
|
|
1084
|
+
monkeypatch.setattr(heartbeat.time, "time", lambda: next(ticks))
|
|
713
1085
|
|
|
714
1086
|
first = observe_heartbeat_state(config)
|
|
715
1087
|
stale = observe_heartbeat_state(config, first)
|
|
1088
|
+
older = observe_heartbeat_state(config, stale)
|
|
716
1089
|
progress.write_text('{"current_phase":"plan","current_tool":"Edit","message_count":1,"total_tool_calls":2,"child_total_bytes":10}\n', encoding="utf-8")
|
|
717
|
-
reset = observe_heartbeat_state(config,
|
|
1090
|
+
reset = observe_heartbeat_state(config, older)
|
|
718
1091
|
|
|
719
1092
|
assert first.details == "progress"
|
|
720
1093
|
assert first.stale_seconds == 0
|
|
1094
|
+
assert first.last_seen_epoch == 1000.0
|
|
721
1095
|
assert stale.details == "no progress"
|
|
722
|
-
assert stale.stale_seconds ==
|
|
1096
|
+
assert stale.stale_seconds == 25.0
|
|
1097
|
+
assert older.stale_seconds == 90.0
|
|
723
1098
|
assert reset.details == "progress"
|
|
724
1099
|
assert reset.stale_seconds == 0
|
|
1100
|
+
assert reset.last_seen_epoch == 1095.0
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
def test_heartbeat_state_tracks_progress_json_child_activity(tmp_path, monkeypatch):
|
|
1105
|
+
import prizmkit_runtime.heartbeat as heartbeat
|
|
1106
|
+
from prizmkit_runtime.heartbeat import HeartbeatConfig, observe_heartbeat_state
|
|
1107
|
+
|
|
1108
|
+
progress = tmp_path / "progress.json"
|
|
1109
|
+
progress.write_text(
|
|
1110
|
+
json.dumps({"child_activity_signature": "child:1", "child_total_bytes": 10}),
|
|
1111
|
+
encoding="utf-8",
|
|
1112
|
+
)
|
|
1113
|
+
config = HeartbeatConfig(
|
|
1114
|
+
heartbeat_file=tmp_path / "heartbeat.json",
|
|
1115
|
+
session_log=tmp_path / "missing-session.log",
|
|
1116
|
+
progress_file=progress,
|
|
1117
|
+
stale_after_seconds=60,
|
|
1118
|
+
)
|
|
1119
|
+
ticks = iter([10.0, 20.0, 25.0])
|
|
1120
|
+
monkeypatch.setattr(heartbeat.time, "time", lambda: next(ticks))
|
|
1121
|
+
|
|
1122
|
+
first = observe_heartbeat_state(config)
|
|
1123
|
+
quiet = observe_heartbeat_state(config, first)
|
|
1124
|
+
progress.write_text(
|
|
1125
|
+
json.dumps({"child_activity_signature": "child:2", "child_total_bytes": 20}),
|
|
1126
|
+
encoding="utf-8",
|
|
1127
|
+
)
|
|
1128
|
+
child_progress = observe_heartbeat_state(config, quiet)
|
|
1129
|
+
|
|
1130
|
+
assert quiet.details == "no progress"
|
|
1131
|
+
assert quiet.stale_seconds == 10.0
|
|
1132
|
+
assert child_progress.details == "progress"
|
|
1133
|
+
assert child_progress.stale_seconds == 0
|
|
1134
|
+
assert child_progress.child_activity_signature == "child:2"
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
def test_heartbeat_monitor_stale_kill_uses_elapsed_no_progress_threshold(tmp_path):
|
|
1138
|
+
from prizmkit_runtime.sessions import AISessionConfig, AISessionLauncher
|
|
1139
|
+
|
|
1140
|
+
fake_cli = tmp_path / "quiet_cli.py"
|
|
1141
|
+
fake_cli.write_text(
|
|
1142
|
+
"#!/usr/bin/env python3\n"
|
|
1143
|
+
"import time\n"
|
|
1144
|
+
"time.sleep(5)\n",
|
|
1145
|
+
encoding="utf-8",
|
|
1146
|
+
)
|
|
1147
|
+
fake_cli.chmod(0o755)
|
|
1148
|
+
prompt = tmp_path / "prompt.md"
|
|
1149
|
+
prompt.write_text("hello", encoding="utf-8")
|
|
1150
|
+
session_log = tmp_path / "state" / "sessions" / "sess-stale" / "logs" / "session.log"
|
|
1151
|
+
progress = session_log.with_name("progress.json")
|
|
1152
|
+
heartbeat = session_log.with_name("heartbeat.json")
|
|
1153
|
+
|
|
1154
|
+
result = AISessionLauncher(
|
|
1155
|
+
AISessionConfig(
|
|
1156
|
+
cli_command=str(fake_cli),
|
|
1157
|
+
platform="codebuddy",
|
|
1158
|
+
model=None,
|
|
1159
|
+
prompt_path=prompt,
|
|
1160
|
+
cwd=tmp_path,
|
|
1161
|
+
log_path=session_log,
|
|
1162
|
+
progress_path=progress,
|
|
1163
|
+
heartbeat_path=heartbeat,
|
|
1164
|
+
stale_kill_threshold_seconds=0.3,
|
|
1165
|
+
heartbeat_stale_threshold_seconds=0.2,
|
|
1166
|
+
stale_kill_grace_seconds=0.1,
|
|
1167
|
+
)
|
|
1168
|
+
).run()
|
|
1169
|
+
|
|
1170
|
+
marker = session_log.parent / "stale-kill.json"
|
|
1171
|
+
marker_data = json.loads(marker.read_text(encoding="utf-8"))
|
|
1172
|
+
assert result.termination_reason == "stale_session"
|
|
1173
|
+
assert result.stale_marker == marker
|
|
1174
|
+
assert marker_data["reason"] == "stale_session"
|
|
1175
|
+
assert marker_data["threshold"] == pytest.approx(0.3)
|
|
1176
|
+
assert marker_data["stale_seconds"] >= 0
|
|
1177
|
+
|
|
725
1178
|
|
|
1179
|
+
def test_stale_marker_preserves_subsecond_threshold(tmp_path):
|
|
1180
|
+
from prizmkit_runtime.heartbeat import write_stale_marker
|
|
1181
|
+
|
|
1182
|
+
marker = write_stale_marker(
|
|
1183
|
+
tmp_path,
|
|
1184
|
+
reason="stale_session",
|
|
1185
|
+
stale_seconds=0.4,
|
|
1186
|
+
threshold=0.3,
|
|
1187
|
+
)
|
|
1188
|
+
data = json.loads(marker.read_text(encoding="utf-8"))
|
|
1189
|
+
|
|
1190
|
+
assert data["stale_seconds"] == pytest.approx(0.4)
|
|
1191
|
+
assert data["threshold"] == pytest.approx(0.3)
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
def test_idle_progress_refresh_updates_child_activity_without_parent_output(tmp_path, monkeypatch):
|
|
1195
|
+
import prizmkit_runtime.sessions as sessions
|
|
1196
|
+
|
|
1197
|
+
progress = tmp_path / "progress.json"
|
|
1198
|
+
tracker_lock = sessions.threading.Lock()
|
|
1199
|
+
stop_event = sessions.threading.Event()
|
|
1200
|
+
|
|
1201
|
+
class FakeTracker:
|
|
1202
|
+
def __init__(self):
|
|
1203
|
+
self.calls = 0
|
|
1204
|
+
|
|
1205
|
+
def to_dict(self):
|
|
1206
|
+
self.calls += 1
|
|
1207
|
+
return {
|
|
1208
|
+
"message_count": 1,
|
|
1209
|
+
"event_type_counts": {},
|
|
1210
|
+
"current_tool": "Task",
|
|
1211
|
+
"current_phase": "implement",
|
|
1212
|
+
"total_tool_calls": 1,
|
|
1213
|
+
"active_subagent_count": 1,
|
|
1214
|
+
"subagent_spawn_count": 1,
|
|
1215
|
+
"child_activity_signature": f"child:{self.calls}",
|
|
1216
|
+
"child_total_bytes": self.calls,
|
|
1217
|
+
"fatal_error_code": "",
|
|
1218
|
+
"terminal_result_text": "",
|
|
1219
|
+
"errors": [],
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
thread, refresh = sessions._start_idle_progress_refresh(
|
|
1223
|
+
progress,
|
|
1224
|
+
FakeTracker(),
|
|
1225
|
+
tracker_lock,
|
|
1226
|
+
stop_event,
|
|
1227
|
+
interval_seconds=0.1,
|
|
1228
|
+
)
|
|
1229
|
+
try:
|
|
1230
|
+
refresh(True)
|
|
1231
|
+
first = json.loads(progress.read_text(encoding="utf-8"))
|
|
1232
|
+
time.sleep(0.25)
|
|
1233
|
+
second = json.loads(progress.read_text(encoding="utf-8"))
|
|
1234
|
+
finally:
|
|
1235
|
+
stop_event.set()
|
|
1236
|
+
thread.join(timeout=1)
|
|
1237
|
+
|
|
1238
|
+
assert first["child_activity_signature"].startswith("child:")
|
|
1239
|
+
assert second["child_activity_signature"] != first["child_activity_signature"]
|
|
1240
|
+
assert second["child_total_bytes"] > first["child_total_bytes"]
|
|
726
1241
|
|
|
727
1242
|
def test_compact_live_heartbeat_stays_out_of_primary_and_backup_logs(tmp_path, capsys, monkeypatch):
|
|
728
1243
|
import prizmkit_runtime.sessions as sessions
|
|
@@ -770,7 +1285,6 @@ def test_compact_live_heartbeat_stays_out_of_primary_and_backup_logs(tmp_path, c
|
|
|
770
1285
|
primary_text = session_log.read_text(encoding="utf-8")
|
|
771
1286
|
assert result.exit_code == 0
|
|
772
1287
|
assert "[HEARTBEAT]" in captured.err
|
|
773
|
-
assert "stale:" in captured.err
|
|
774
1288
|
assert "logged-payload" in primary_text
|
|
775
1289
|
assert "[HEARTBEAT]" not in primary_text
|
|
776
1290
|
assert "stale:" not in primary_text
|
|
@@ -827,6 +1341,7 @@ def test_recovery_sessions_receive_live_heartbeat_configuration(tmp_path, monkey
|
|
|
827
1341
|
"verbose": True,
|
|
828
1342
|
"live_output": True,
|
|
829
1343
|
"heartbeat_interval_seconds": 7,
|
|
1344
|
+
"heartbeat_stale_threshold_seconds": 21,
|
|
830
1345
|
"stale_kill_threshold_seconds": 42,
|
|
831
1346
|
},
|
|
832
1347
|
)()
|
|
@@ -861,6 +1376,7 @@ def test_recovery_sessions_receive_live_heartbeat_configuration(tmp_path, monkey
|
|
|
861
1376
|
assert config.verbose is True
|
|
862
1377
|
assert config.use_stream_json is True
|
|
863
1378
|
assert config.live_progress_interval_seconds == 7
|
|
1379
|
+
assert config.heartbeat_stale_threshold_seconds == 21
|
|
864
1380
|
assert config.stale_kill_threshold_seconds == 42
|
|
865
1381
|
assert config.live_banner is False
|
|
866
1382
|
assert config.suppress_stream_output is True
|
|
@@ -1235,6 +1751,135 @@ def _init_temp_repo(repo):
|
|
|
1235
1751
|
subprocess.run(["git", "commit", "-m", "base"], cwd=repo, check=True, stdout=subprocess.DEVNULL)
|
|
1236
1752
|
|
|
1237
1753
|
|
|
1754
|
+
def _head_commit_message(repo):
|
|
1755
|
+
return subprocess.run(
|
|
1756
|
+
["git", "log", "-1", "--format=%s"],
|
|
1757
|
+
cwd=repo,
|
|
1758
|
+
check=True,
|
|
1759
|
+
stdout=subprocess.PIPE,
|
|
1760
|
+
text=True,
|
|
1761
|
+
).stdout.strip()
|
|
1762
|
+
|
|
1763
|
+
|
|
1764
|
+
def _head_files(repo):
|
|
1765
|
+
return set(
|
|
1766
|
+
subprocess.run(
|
|
1767
|
+
["git", "show", "--name-only", "--format=", "HEAD"],
|
|
1768
|
+
cwd=repo,
|
|
1769
|
+
check=True,
|
|
1770
|
+
stdout=subprocess.PIPE,
|
|
1771
|
+
text=True,
|
|
1772
|
+
).stdout.split()
|
|
1773
|
+
)
|
|
1774
|
+
|
|
1775
|
+
|
|
1776
|
+
def _short_status(repo):
|
|
1777
|
+
return subprocess.run(
|
|
1778
|
+
["git", "status", "--short"],
|
|
1779
|
+
cwd=repo,
|
|
1780
|
+
check=True,
|
|
1781
|
+
stdout=subprocess.PIPE,
|
|
1782
|
+
text=True,
|
|
1783
|
+
).stdout
|
|
1784
|
+
|
|
1785
|
+
|
|
1786
|
+
def test_preflight_ready_commit_preserves_dirty_unstaged_visible_changes(tmp_path):
|
|
1787
|
+
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
1788
|
+
|
|
1789
|
+
repo = tmp_path / "repo"
|
|
1790
|
+
_init_temp_repo(repo)
|
|
1791
|
+
(repo / "visible.txt").write_text("dirty\n", encoding="utf-8")
|
|
1792
|
+
|
|
1793
|
+
result = commit_preflight_ready_changes(repo, "F-001")
|
|
1794
|
+
|
|
1795
|
+
assert result.committed
|
|
1796
|
+
assert _head_commit_message(repo) == "chore: ready for F-001"
|
|
1797
|
+
assert _head_files(repo) == {"visible.txt"}
|
|
1798
|
+
assert _short_status(repo) == ""
|
|
1799
|
+
|
|
1800
|
+
|
|
1801
|
+
def test_preflight_ready_commit_preserves_dirty_staged_visible_changes(tmp_path):
|
|
1802
|
+
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
1803
|
+
|
|
1804
|
+
repo = tmp_path / "repo"
|
|
1805
|
+
_init_temp_repo(repo)
|
|
1806
|
+
(repo / "staged.txt").write_text("staged\n", encoding="utf-8")
|
|
1807
|
+
subprocess.run(["git", "add", "staged.txt"], cwd=repo, check=True)
|
|
1808
|
+
|
|
1809
|
+
result = commit_preflight_ready_changes(repo, "B-002")
|
|
1810
|
+
|
|
1811
|
+
assert result.committed
|
|
1812
|
+
assert _head_commit_message(repo) == "chore: ready for B-002"
|
|
1813
|
+
assert _head_files(repo) == {"staged.txt"}
|
|
1814
|
+
assert _short_status(repo) == ""
|
|
1815
|
+
|
|
1816
|
+
|
|
1817
|
+
def test_preflight_ready_commit_skips_clean_workspace(tmp_path):
|
|
1818
|
+
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
1819
|
+
|
|
1820
|
+
repo = tmp_path / "repo"
|
|
1821
|
+
_init_temp_repo(repo)
|
|
1822
|
+
before = subprocess.run(["git", "rev-parse", "HEAD"], cwd=repo, check=True, stdout=subprocess.PIPE, text=True).stdout.strip()
|
|
1823
|
+
|
|
1824
|
+
result = commit_preflight_ready_changes(repo, "R-003")
|
|
1825
|
+
|
|
1826
|
+
after = subprocess.run(["git", "rev-parse", "HEAD"], cwd=repo, check=True, stdout=subprocess.PIPE, text=True).stdout.strip()
|
|
1827
|
+
assert not result.attempted
|
|
1828
|
+
assert not result.committed
|
|
1829
|
+
assert after == before
|
|
1830
|
+
|
|
1831
|
+
|
|
1832
|
+
def test_preflight_ready_commit_skips_hidden_support_asset_only_changes(tmp_path):
|
|
1833
|
+
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
1834
|
+
|
|
1835
|
+
repo = tmp_path / "repo"
|
|
1836
|
+
_init_temp_repo(repo)
|
|
1837
|
+
(repo / ".claude").mkdir()
|
|
1838
|
+
(repo / ".claude" / "settings.local.json").write_text("{}\n", encoding="utf-8")
|
|
1839
|
+
before = subprocess.run(["git", "rev-parse", "HEAD"], cwd=repo, check=True, stdout=subprocess.PIPE, text=True).stdout.strip()
|
|
1840
|
+
|
|
1841
|
+
result = commit_preflight_ready_changes(repo, "F-004")
|
|
1842
|
+
|
|
1843
|
+
after = subprocess.run(["git", "rev-parse", "HEAD"], cwd=repo, check=True, stdout=subprocess.PIPE, text=True).stdout.strip()
|
|
1844
|
+
assert not result.attempted
|
|
1845
|
+
assert after == before
|
|
1846
|
+
assert (repo / ".claude" / "settings.local.json").is_file()
|
|
1847
|
+
|
|
1848
|
+
|
|
1849
|
+
def test_preflight_ready_commit_excludes_already_staged_hidden_support_assets(tmp_path):
|
|
1850
|
+
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
1851
|
+
|
|
1852
|
+
repo = tmp_path / "repo"
|
|
1853
|
+
_init_temp_repo(repo)
|
|
1854
|
+
(repo / ".claude").mkdir()
|
|
1855
|
+
(repo / ".claude" / "agent.md").write_text("hidden\n", encoding="utf-8")
|
|
1856
|
+
subprocess.run(["git", "add", "-f", ".claude/agent.md"], cwd=repo, check=True)
|
|
1857
|
+
(repo / "visible.txt").write_text("visible\n", encoding="utf-8")
|
|
1858
|
+
|
|
1859
|
+
result = commit_preflight_ready_changes(repo, "F-005")
|
|
1860
|
+
|
|
1861
|
+
assert result.committed
|
|
1862
|
+
assert _head_commit_message(repo) == "chore: ready for F-005"
|
|
1863
|
+
assert _head_files(repo) == {"visible.txt"}
|
|
1864
|
+
assert "A .claude/agent.md" in _short_status(repo)
|
|
1865
|
+
|
|
1866
|
+
|
|
1867
|
+
def test_preflight_ready_commit_excludes_ignored_tool_worktree_directories(tmp_path):
|
|
1868
|
+
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
1869
|
+
|
|
1870
|
+
repo = tmp_path / "repo"
|
|
1871
|
+
_init_temp_repo(repo)
|
|
1872
|
+
(repo / ".cache" / "worktrees" / "agent").mkdir(parents=True)
|
|
1873
|
+
(repo / ".cache" / "worktrees" / "agent" / "artifact.txt").write_text("tool\n", encoding="utf-8")
|
|
1874
|
+
(repo / "visible.txt").write_text("visible\n", encoding="utf-8")
|
|
1875
|
+
|
|
1876
|
+
result = commit_preflight_ready_changes(repo, "F-006")
|
|
1877
|
+
|
|
1878
|
+
assert result.committed
|
|
1879
|
+
assert _head_files(repo) == {"visible.txt"}
|
|
1880
|
+
assert (repo / ".cache" / "worktrees" / "agent" / "artifact.txt").is_file()
|
|
1881
|
+
|
|
1882
|
+
|
|
1238
1883
|
def test_materialize_worktree_support_assets_copies_ignored_assets_without_retired_shell_sources(tmp_path):
|
|
1239
1884
|
from prizmkit_runtime.gitops import (
|
|
1240
1885
|
BranchContext,
|