prizmkit 1.1.104 → 1.1.106

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.
Files changed (24) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/README.md +6 -7
  3. package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +3 -3
  4. package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +0 -1
  5. package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +83 -12
  6. package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +38 -2
  7. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +497 -154
  8. package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +98 -9
  9. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +37 -28
  10. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +4 -3
  11. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +62 -38
  12. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +57 -49
  13. package/bundled/dev-pipeline/templates/sections/critical-paths-agent.md +0 -1
  14. package/bundled/dev-pipeline/templates/sections/critical-paths-full.md +0 -1
  15. package/bundled/dev-pipeline/templates/sections/directory-convention-agent.md +2 -2
  16. package/bundled/dev-pipeline/templates/sections/directory-convention-full.md +2 -2
  17. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +1 -0
  18. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +1 -0
  19. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +46 -0
  20. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +308 -0
  21. package/bundled/dev-pipeline/tests/test_unified_cli.py +1 -0
  22. package/bundled/skills/_metadata.json +1 -1
  23. package/package.json +1 -1
  24. package/bundled/dev-pipeline/templates/agent-prompts/dev-implement.md +0 -71
@@ -1,8 +1,8 @@
1
1
  ## PrizmKit Directory Convention
2
2
 
3
3
  ```
4
- .prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md ← orchestrator writes Sections 1-4; Dev appends Implementation Log; Reviewer appends Review Notes
4
+ .prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md ← orchestrator writes Sections 1-4 + Implementation Log; Reviewer appends Review Notes
5
5
  .prizmkit/specs/{{FEATURE_SLUG}}/plan.md ← includes Tasks section
6
6
  ```
7
7
 
8
- **`context-snapshot.md`** is the shared knowledge base. Orchestrator writes Sections 1-4; Dev appends Implementation Log; Reviewer appends Review Notes. Append-only after initial creation.
8
+ **`context-snapshot.md`** is the shared knowledge base. Orchestrator writes Sections 1-4 and the Implementation Log; Reviewer appends Review Notes. Append-only after initial creation.
@@ -1,9 +1,9 @@
1
1
  ## PrizmKit Directory Convention
2
2
 
3
3
  ```
4
- .prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md ← orchestrator writes Sections 1-4; Dev appends Implementation Log; Reviewer appends Review Notes
4
+ .prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md ← orchestrator writes Sections 1-4 + Implementation Log; Reviewer appends Review Notes
5
5
  .prizmkit/specs/{{FEATURE_SLUG}}/spec.md
6
6
  .prizmkit/specs/{{FEATURE_SLUG}}/plan.md ← includes Tasks section
7
7
  ```
8
8
 
9
- **`context-snapshot.md`** is the shared knowledge base. Orchestrator writes Sections 1-4; Dev appends Implementation Log; Reviewer appends Review Notes. This eliminates redundant I/O across all agents.
9
+ **`context-snapshot.md`** is the shared knowledge base. Orchestrator writes Sections 1-4 and the Implementation Log; Reviewer appends Review Notes. This eliminates redundant I/O across review and orchestration phases.
@@ -22,6 +22,7 @@ grep -c '^\- \[ \]' .prizmkit/specs/{{FEATURE_SLUG}}/plan.md 2>/dev/null || true
22
22
  - Implements task-by-task, marking each `[x]` immediately
23
23
  - Creates/updates L2 `.prizm` docs when creating new modules
24
24
  - Defers scoped/full test execution until after the code-review loop completes
25
+ - Preserves Reviewer/Critic behavior; only the top-level implementation Dev handoff is removed
25
26
  - If plan.md has >5 tasks: update checkpoints after every 3 tasks
26
27
 
27
28
  **3c. Focused checks only (no test gate here)**:
@@ -23,6 +23,7 @@ Save pre-existing failing tests as `BASELINE_FAILURES`.
23
23
  - Implements task-by-task, marking each `[x]` immediately
24
24
  - Creates/updates L2 `.prizm` docs when creating new modules
25
25
  - Defers scoped/full test execution until after the code-review loop completes
26
+ - Preserves Reviewer/Critic behavior; only the top-level implementation Dev handoff is removed
26
27
  - If plan.md has >5 tasks: update checkpoints after every 3 tasks
27
28
 
28
29
  **3c. Focused checks only (no test gate here)**:
@@ -8,6 +8,7 @@ from argparse import Namespace
8
8
  from pathlib import Path
9
9
 
10
10
  from generate_bootstrap_prompt import (
11
+ ACTIVE_AGENT_PROMPTS,
11
12
  compute_feature_slug,
12
13
  find_feature,
13
14
  format_acceptance_criteria,
@@ -21,6 +22,7 @@ from generate_bootstrap_prompt import (
21
22
  build_replacements,
22
23
  generate_checkpoint_definition,
23
24
  merge_checkpoint_state,
25
+ load_active_agent_prompts,
24
26
  load_log_size_section,
25
27
  main as generate_bootstrap_main,
26
28
  )
@@ -809,6 +811,50 @@ class TestHeadlessRecoveryReframing:
809
811
  assert "run `/compact` after" not in rendered
810
812
 
811
813
 
814
+ class TestDirectOrchestratorImplementationPrompts:
815
+ def test_active_agent_prompt_loader_excludes_retired_dev_implement_prompt(self):
816
+ templates_dir = Path("dev-pipeline/templates").resolve()
817
+
818
+ replacements = load_active_agent_prompts(str(templates_dir))
819
+
820
+ assert "dev-implement.md" not in ACTIVE_AGENT_PROMPTS
821
+ assert "{{AGENT_PROMPT_DEV_IMPLEMENT}}" not in replacements
822
+ assert "{{AGENT_PROMPT_CRITIC_PLAN_CHALLENGE}}" in replacements
823
+ assert (templates_dir / "agent-prompts" / "dev-implement.md").exists() is False
824
+
825
+ def test_sections_all_modes_use_direct_orchestrator_implementation(self):
826
+ sections_dir = Path("dev-pipeline/templates/sections").resolve()
827
+
828
+ for mode in ("lite", "standard", "full"):
829
+ rendered = "\n".join(
830
+ section for _name, section in assemble_sections(
831
+ mode, str(sections_dir), init_done=True, is_resume=False,
832
+ critic_enabled=(mode == "full"), browser_enabled=False,
833
+ )
834
+ )
835
+
836
+ assert "/prizmkit-implement" in rendered
837
+ assert "orchestrator) execute" in rendered or "Run `/prizmkit-implement`" in rendered
838
+ assert "Spawn Dev subagent" not in rendered
839
+ assert "Spawn Dev agent" not in rendered
840
+ assert "Implement — Dev" not in rendered
841
+ assert "{{AGENT_PROMPT_DEV_IMPLEMENT}}" not in rendered
842
+ assert "dev-implement.md" not in rendered
843
+ assert "Reviewer Agent → filter → Dev Agent fix" in rendered
844
+
845
+ def test_legacy_tier_templates_use_direct_orchestrator_implementation(self):
846
+ for template_name in ("bootstrap-tier2.md", "bootstrap-tier3.md"):
847
+ rendered = (Path("dev-pipeline/templates") / template_name).read_text(encoding="utf-8")
848
+
849
+ assert "/prizmkit-implement" in rendered
850
+ assert "Implement — Orchestrator Direct" in rendered
851
+ assert "Spawn Dev subagent" not in rendered
852
+ assert "Spawn Dev agent" not in rendered
853
+ assert "Implement — Dev" not in rendered
854
+ assert "{{AGENT_PROMPT_DEV_IMPLEMENT}}" not in rendered
855
+ assert "dev-implement.md" not in rendered
856
+
857
+
812
858
  # ---------------------------------------------------------------------------
813
859
  # continuation handoff
814
860
  # ---------------------------------------------------------------------------
@@ -847,12 +847,24 @@ def test_default_no_worktree_preserves_main_worktree_branch_launch(monkeypatch,
847
847
  invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
848
848
  observed = _install_runner_session_fakes(monkeypatch, runners)
849
849
  plan_names = []
850
+ branch_events = []
851
+ lifecycle_order = []
850
852
 
851
853
  def fake_run_git_plan(project_root, plan):
852
854
  plan_names.append(plan.name)
855
+ if plan.name == "branch_create":
856
+ lifecycle_order.append("branch_create")
857
+ branch_events.append(("create", plan.commands[1].args[2]))
858
+ if plan.name == "branch_merge":
859
+ branch_events.append(("merge", plan.commands[5].args[2]))
853
860
  return SimpleNamespace(ok=True, plan=plan, results=())
854
861
 
862
+ def fake_start_item(*args, **kwargs):
863
+ lifecycle_order.append("start_item")
864
+ return SimpleNamespace(ok=True, text="", data={"new_status": "in_progress"})
865
+
855
866
  monkeypatch.setattr(runners, "run_git_plan", fake_run_git_plan)
867
+ monkeypatch.setattr(runners, "start_item", fake_start_item)
856
868
  monkeypatch.setattr(runners, "ensure_linked_worktree", lambda runtime: (_ for _ in ()).throw(AssertionError("worktree setup should not run")), raising=False)
857
869
 
858
870
  status = runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
@@ -862,6 +874,211 @@ def test_default_no_worktree_preserves_main_worktree_branch_launch(monkeypatch,
862
874
  assert observed.prompt_calls[0].get("execution_root") == paths.project_root
863
875
  assert "branch_create" in plan_names
864
876
  assert "branch_merge" in plan_names
877
+ assert lifecycle_order[:2] == ["branch_create", "start_item"]
878
+ assert branch_events == [("create", "dev/F-001-test"), ("merge", "dev/F-001-test")]
879
+
880
+
881
+ def test_default_branch_names_keep_timestamp_for_each_runner_family(monkeypatch, tmp_path):
882
+ from prizmkit_runtime import runners
883
+ from prizmkit_runtime.runner_models import family_for
884
+
885
+ monkeypatch.setattr(runners.time, "strftime", lambda fmt: "20260707010203")
886
+ paths = _make_paths(tmp_path)
887
+
888
+ assert runners._default_branch_name(family_for("feature", paths), "F-001") == "dev/F-001-20260707010203"
889
+ assert runners._default_branch_name(family_for("bugfix", paths), "B-001") == "bugfix/B-001-20260707010203"
890
+ assert runners._default_branch_name(family_for("refactor", paths), "R-001") == "refactor/R-001-20260707010203"
891
+
892
+
893
+ @pytest.mark.parametrize(
894
+ ("kind", "item_id", "branch"),
895
+ [
896
+ ("feature", "F-001", "dev/F-001-20260707010203"),
897
+ ("bugfix", "B-001", "bugfix/B-001-20260707010203"),
898
+ ("refactor", "R-001", "refactor/R-001-20260707010203"),
899
+ ],
900
+ )
901
+ def test_no_worktree_failure_stays_on_task_branch_without_merge_for_all_families(monkeypatch, tmp_path, kind, item_id, branch):
902
+ from prizmkit_runtime import runners
903
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
904
+
905
+ monkeypatch.setenv("DEV_BRANCH", branch)
906
+ paths = _make_paths(tmp_path / kind)
907
+ family = family_for(kind, paths)
908
+ invocation = parse_invocation(family, "run", ())
909
+ plan_path = {"feature": paths.feature_plan, "bugfix": paths.bugfix_plan, "refactor": paths.refactor_plan}[kind]
910
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": plan_path})
911
+ _install_runner_session_fakes(monkeypatch, runners, statuses=("crashed",), update_status="pending")
912
+ events = []
913
+ final_commits = []
914
+ wip_commits = []
915
+
916
+ def fake_run_git_plan(project_root, plan):
917
+ events.append(plan.name)
918
+ return SimpleNamespace(ok=True, plan=plan, results=())
919
+
920
+ monkeypatch.setattr(runners, "run_git_plan", fake_run_git_plan)
921
+ monkeypatch.setattr(runners, "branch_ensure_return", lambda project_root, base, task_branch: events.append(("return", base, task_branch)))
922
+ monkeypatch.setattr(runners, "commit_final_bookkeeping", lambda project_root, family, item_id, status: final_commits.append((project_root, item_id, status)))
923
+ monkeypatch.setattr(runners, "commit_wip_changes", lambda project_root, family, item_id, status: wip_commits.append((project_root, item_id, status)))
924
+
925
+ status = runners._process_item(family, invocation, item_id, paths, initial_metadata={})
926
+
927
+ assert status == "pending"
928
+ assert events[0] == "branch_create"
929
+ assert "branch_merge" not in events
930
+ assert events[-1] == ("return", "main", branch)
931
+ assert final_commits == [(paths.project_root, item_id, "pending")]
932
+ assert wip_commits == [(paths.project_root, item_id, "pending")]
933
+
934
+
935
+ def test_no_worktree_runner_ignores_recovery_side_effect_branch(monkeypatch, tmp_path):
936
+ from prizmkit_runtime import runners
937
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
938
+
939
+ monkeypatch.delenv("DEV_BRANCH", raising=False)
940
+ paths = _make_paths(tmp_path)
941
+ family = family_for("feature", paths)
942
+ invocation = parse_invocation(family, "run", ())
943
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
944
+ observed = _install_runner_session_fakes(monkeypatch, runners)
945
+ branch_names = []
946
+
947
+ monkeypatch.setenv("DEV_BRANCH", "dev/F-001-recovery-20260707")
948
+ monkeypatch.setattr(runners, "current_branch", lambda _root: "dev/F-001-recovery-20260707")
949
+
950
+ def fake_run_git_plan(project_root, plan):
951
+ if plan.name == "branch_create":
952
+ branch_names.append(plan.commands[1].args[2])
953
+ return SimpleNamespace(ok=True, plan=plan, results=())
954
+
955
+ monkeypatch.setattr(runners, "run_git_plan", fake_run_git_plan)
956
+
957
+ status = runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
958
+
959
+ assert status == "completed"
960
+ assert branch_names and branch_names[0].startswith("dev/F-001-")
961
+ assert "recovery" not in branch_names[0]
962
+ assert observed.updates[-1][2]["active_dev_branch"] == branch_names[0]
963
+ assert observed.updates[-1][2]["base_branch"] == "main"
964
+
965
+
966
+ def test_foreground_runner_emits_structured_feature_progress(monkeypatch, tmp_path, capsys):
967
+ from prizmkit_runtime import runners
968
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
969
+
970
+ monkeypatch.setenv("DEV_BRANCH", "dev/F-001-output")
971
+ paths = _make_paths(tmp_path)
972
+ family = family_for("feature", paths)
973
+ invocation = parse_invocation(family, "run", ())
974
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
975
+ _install_runner_session_fakes(monkeypatch, runners)
976
+ monkeypatch.setattr(runners, "run_git_plan", lambda project_root, plan: SimpleNamespace(ok=True, plan=plan, results=()))
977
+
978
+ status = runners._process_item(
979
+ family,
980
+ invocation,
981
+ "F-001",
982
+ paths,
983
+ initial_metadata={"title": "Readable output", "retry_count": 1, "infra_error_count": 2},
984
+ )
985
+
986
+ captured = capsys.readouterr()
987
+ assert status == "completed"
988
+ assert "────────────────────────────────────────────────────" in captured.err
989
+ assert "Feature: F-001 — Readable output" in captured.err
990
+ assert "Code retry: 1 / 3" in captured.err
991
+ assert "Infrastructure retry: 2 / 3" in captured.err
992
+ assert "Pipeline mode: lite (Tier 1 — Single Agent)" in captured.err
993
+ assert "Agents: 1 (critic: disabled)" in captured.err
994
+ assert "Spawning AI CLI session: F-001-session-1" in captured.err
995
+ assert "Session result: success" in captured.err
996
+
997
+
998
+ def test_no_worktree_interrupt_saves_wip_and_returns_to_base(monkeypatch, tmp_path):
999
+ from prizmkit_runtime import runners
1000
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
1001
+
1002
+ monkeypatch.setenv("DEV_BRANCH", "dev/F-001-interrupt")
1003
+ paths = _make_paths(tmp_path)
1004
+ family = family_for("feature", paths)
1005
+ invocation = parse_invocation(family, "run", ())
1006
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
1007
+ _install_runner_session_fakes(monkeypatch, runners)
1008
+ ensure_calls = []
1009
+ monkeypatch.setattr(runners, "run_git_plan", lambda project_root, plan: SimpleNamespace(ok=True, plan=plan, results=()))
1010
+ monkeypatch.setattr(runners, "branch_ensure_return", lambda project_root, base, branch: ensure_calls.append((base, branch)))
1011
+
1012
+ class InterruptingLauncher:
1013
+ def __init__(self, config):
1014
+ self.config = config
1015
+
1016
+ def run(self):
1017
+ raise KeyboardInterrupt
1018
+
1019
+ monkeypatch.setattr(runners, "AISessionLauncher", InterruptingLauncher)
1020
+
1021
+ with pytest.raises(KeyboardInterrupt):
1022
+ runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
1023
+
1024
+ assert ensure_calls == [("main", "dev/F-001-interrupt")]
1025
+
1026
+
1027
+ def test_bookkeeping_commits_only_runner_owned_paths(tmp_path):
1028
+ from prizmkit_runtime.runner_bookkeeping import commit_final_bookkeeping
1029
+ from prizmkit_runtime.runner_models import RunnerFamily
1030
+
1031
+ subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
1032
+ subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
1033
+ subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
1034
+ plans = tmp_path / ".prizmkit" / "plans"
1035
+ state = tmp_path / ".prizmkit" / "state" / "features"
1036
+ item_state = state / "F-001"
1037
+ plans.mkdir(parents=True)
1038
+ item_state.mkdir(parents=True)
1039
+ plan = plans / "feature-list.json"
1040
+ plan.write_text('{"features": []}', encoding="utf-8")
1041
+ (item_state / "status.json").write_text("{}", encoding="utf-8")
1042
+ source = tmp_path / "src.txt"
1043
+ source.write_text("base", encoding="utf-8")
1044
+ subprocess.run(["git", "add", ".prizmkit/plans/feature-list.json", ".prizmkit/state/features/F-001/status.json", "src.txt"], cwd=tmp_path, check=True)
1045
+ subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
1046
+ plan.write_text('{"features": [{"id": "F-001", "status": "completed"}]}', encoding="utf-8")
1047
+ (item_state / "status.json").write_text('{"last_session_id": "s1"}', encoding="utf-8")
1048
+ source.write_text("dirty source change", encoding="utf-8")
1049
+ family = RunnerFamily(
1050
+ kind="feature",
1051
+ id_prefix="F-",
1052
+ list_key="features",
1053
+ list_arg="--feature-list",
1054
+ item_id_arg="--feature-id",
1055
+ state_dir=state,
1056
+ plan_path=plan,
1057
+ updater_script=tmp_path / "update-feature-status.py",
1058
+ prompt_script=tmp_path / "generate-bootstrap-prompt.py",
1059
+ branch_prefix="dev",
1060
+ artifact_root_name="specs",
1061
+ )
1062
+
1063
+ result = commit_final_bookkeeping(tmp_path, family, "F-001", "completed")
1064
+ committed_files = subprocess.run(
1065
+ ["git", "show", "--name-only", "--format=", "HEAD"],
1066
+ cwd=tmp_path,
1067
+ check=True,
1068
+ stdout=subprocess.PIPE,
1069
+ text=True,
1070
+ ).stdout.splitlines()
1071
+ dirty_files = subprocess.run(
1072
+ ["git", "status", "--porcelain", "--", "src.txt"],
1073
+ cwd=tmp_path,
1074
+ check=True,
1075
+ stdout=subprocess.PIPE,
1076
+ text=True,
1077
+ ).stdout
1078
+
1079
+ assert result.committed is True
1080
+ assert committed_files == [".prizmkit/plans/feature-list.json", ".prizmkit/state/features/F-001/status.json"]
1081
+ assert dirty_files.startswith(" M src.txt")
865
1082
 
866
1083
 
867
1084
  def test_context_overflow_reuses_worktree_and_skips_cleanup_until_terminal_outcome(monkeypatch, tmp_path):
@@ -891,6 +1108,52 @@ def test_context_overflow_reuses_worktree_and_skips_cleanup_until_terminal_outco
891
1108
  assert observed.cleanup_calls[0][1] is True
892
1109
 
893
1110
 
1111
+ def test_classification_accepts_terminal_success_with_completed_checkpoint_without_git_delta(tmp_path):
1112
+ from prizmkit_runtime.runner_classification import classify_session
1113
+ from prizmkit_runtime.runner_models import PromptGenerationResult
1114
+ from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
1115
+ from prizmkit_runtime.status import ProgressSummary
1116
+
1117
+ subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
1118
+ subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
1119
+ subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
1120
+ (tmp_path / ".gitignore").write_text(".prizmkit/*\n", encoding="utf-8")
1121
+ (tmp_path / "base.txt").write_text("base", encoding="utf-8")
1122
+ subprocess.run(["git", "add", ".gitignore", "base.txt"], cwd=tmp_path, check=True)
1123
+ subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
1124
+ base_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True).stdout.strip()
1125
+ artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-done"
1126
+ artifact_dir.mkdir(parents=True)
1127
+ checkpoint = artifact_dir / "workflow-checkpoint.json"
1128
+ checkpoint.write_text(json.dumps({"steps": [{"id": "S01", "skill": "done", "status": "completed"}]}), encoding="utf-8")
1129
+ (artifact_dir / "completion-summary.json").write_text("{}", encoding="utf-8")
1130
+ log = tmp_path / "session.log"
1131
+ log.write_text("terminal success", encoding="utf-8")
1132
+ raw = AISessionResult(
1133
+ command=AISessionCommand(("fake",), None, "claude", "stdin"),
1134
+ exit_code=0,
1135
+ timed_out=False,
1136
+ termination_reason=None,
1137
+ stale_marker=None,
1138
+ fatal_error_code="",
1139
+ progress_summary=ProgressSummary(result_subtype="success", terminal_result_text="Completed; working tree clean"),
1140
+ session_log=log,
1141
+ backup_log=tmp_path / "backup.log",
1142
+ log_recovery=None,
1143
+ started_epoch=0,
1144
+ ended_epoch=1,
1145
+ )
1146
+ prompt = PromptGenerationResult(
1147
+ output_path=tmp_path / "prompt.md",
1148
+ checkpoint_path=checkpoint,
1149
+ artifact_path=artifact_dir,
1150
+ )
1151
+
1152
+ classification = classify_session(raw, project_root=tmp_path, base_head=base_head, prompt=prompt)
1153
+ assert classification.session_status == "success"
1154
+ assert classification.reason == "terminal_success_with_completed_checkpoint"
1155
+
1156
+
894
1157
  def test_classification_preserves_success_before_context_overflow(tmp_path):
895
1158
  from prizmkit_runtime.runner_classification import classify_session
896
1159
  from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
@@ -929,6 +1192,43 @@ def test_classification_preserves_success_before_context_overflow(tmp_path):
929
1192
  assert classification.reason == "commit_since_base"
930
1193
 
931
1194
 
1195
+ def test_classification_crash_with_commit_since_base_does_not_become_success(tmp_path):
1196
+ from prizmkit_runtime.runner_classification import classify_session
1197
+ from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
1198
+ from prizmkit_runtime.status import ProgressSummary
1199
+
1200
+ subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
1201
+ subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
1202
+ subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
1203
+ (tmp_path / "base.txt").write_text("base", encoding="utf-8")
1204
+ subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
1205
+ subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
1206
+ base_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True).stdout.strip()
1207
+ (tmp_path / "done.txt").write_text("done", encoding="utf-8")
1208
+ subprocess.run(["git", "add", "done.txt"], cwd=tmp_path, check=True)
1209
+ subprocess.run(["git", "commit", "-m", "done"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
1210
+ log = tmp_path / "session.log"
1211
+ log.write_text("hard failure", encoding="utf-8")
1212
+ raw = AISessionResult(
1213
+ command=AISessionCommand(("fake",), None, "codebuddy", "stdin"),
1214
+ exit_code=1,
1215
+ timed_out=False,
1216
+ termination_reason=None,
1217
+ stale_marker=None,
1218
+ fatal_error_code="",
1219
+ progress_summary=ProgressSummary(terminal_result_text="hard failure"),
1220
+ session_log=log,
1221
+ backup_log=tmp_path / "backup.log",
1222
+ log_recovery=None,
1223
+ started_epoch=0,
1224
+ ended_epoch=1,
1225
+ )
1226
+
1227
+ classification = classify_session(raw, project_root=tmp_path, base_head=base_head)
1228
+ assert classification.session_status == "crashed"
1229
+ assert classification.reason == "exit_code=1"
1230
+
1231
+
932
1232
  def test_classification_stalls_after_repeated_no_progress_context_overflow(tmp_path):
933
1233
  from prizmkit_runtime.runner_classification import classify_session, progress_fingerprint
934
1234
  from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
@@ -964,6 +1264,14 @@ def test_classification_stalls_after_repeated_no_progress_context_overflow(tmp_p
964
1264
  assert classification.no_progress_count == 2
965
1265
 
966
1266
 
1267
+ def test_recovery_prompt_keeps_existing_branch_instead_of_recovery_suffix():
1268
+ text = (PIPELINE_ROOT / "scripts" / "generate-recovery-prompt.py").read_text(encoding="utf-8")
1269
+
1270
+ assert "fix/{bug_id}-recovery" not in text
1271
+ assert "do not create an additional" in text
1272
+ assert "last-resort fallback" in text
1273
+
1274
+
967
1275
  def test_recovery_detector_resolver_prefers_installed_platform_asset(tmp_path):
968
1276
  from prizmkit_runtime.paths import resolve_runtime_paths
969
1277
  from prizmkit_runtime.runner_recovery import RecoveryDetectorResolver
@@ -853,6 +853,7 @@ def test_git_branch_worktree_plans_preserve_shell_invariants(tmp_path):
853
853
  assert ensure.never_fails is True
854
854
  assert any(command.args[:2] == ("rebase", "--abort") for command in ensure.commands)
855
855
  assert any(command.args[:2] == ("add", "-A") for command in ensure.commands)
856
+ assert not any(command.args[:2] == ("stash", "pop") for command in ensure.commands)
856
857
  assert any(".*/worktrees/**" in " ".join(command.args) for command in save_wip.commands)
857
858
  assert worktree.commands[0].args[:2] == ("worktree", "add")
858
859
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.104",
2
+ "version": "1.1.106",
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.104",
3
+ "version": "1.1.106",
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": {
@@ -1,71 +0,0 @@
1
- "<!-- DEPRECATED (P0-6): This prompt was injected into a Dev subagent spawn.
2
- The orchestrator now implements directly (see phase-implement-agent.md / phase-implement-full.md).
3
- This file is retained only so load_agent_prompts() does not break; it is no longer referenced
4
- by any phase section. Do not add new rules here — add them to the phase-implement-* sections instead.
5
- The rules below are the legacy Dev-subagent version, kept for reference only. -->
6
-
7
- Read {{DEV_SUBAGENT_PATH}}. Implement feature {{FEATURE_ID}} (slug: {{FEATURE_SLUG}}).
8
-
9
- ## Task Summary Card
10
-
11
- **Objective**: Implement {{FEATURE_TITLE}}.
12
-
13
- **Primary files** (see context-snapshot.md Section 4 for full manifest):
14
- - Review plan.md Tasks section for the complete task-to-file mapping.
15
- - Each task's `— file:` suffix identifies the target file.
16
-
17
- **Test command**: `{{TEST_CMD}}`
18
-
19
- **Known baseline failures**: `{{BASELINE_FAILURES}}`
20
-
21
- **DO NOT**:
22
- - Re-read source files already listed in context-snapshot.md Section 4 File Manifest
23
- - Create new files unless a plan.md task explicitly requires it
24
- - Run git commands
25
- - Use mock success data or fake rows in UI/tests
26
-
27
- ## Required Inputs
28
-
29
- 1. Read `.prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md` first.
30
- 2. Use Section 4 File Manifest for targeted reads.
31
- 3. Do not expand scope beyond the Task Contract and Verification Gates.
32
- 4. Do not re-read source files already listed in Section 4 unless implementation details are missing from the manifest.
33
-
34
- ## Work
35
-
36
- Run `/prizmkit-implement` to execute `plan.md`.
37
-
38
- Test command:
39
- `{{TEST_CMD}}`
40
-
41
- Known baseline failures:
42
- `{{BASELINE_FAILURES}}`
43
-
44
- If plan.md has more than 5 tasks, update durable checkpoints/artifacts after every 3 tasks, minimize output, and rely on runner continuation if context overflow occurs. In an interactive Claude Code session operated manually outside the headless pipeline, `/compact` may be used as an optional convenience only.
45
-
46
- ## Required Outputs
47
-
48
- Before returning, append `## Implementation Log` to `context-snapshot.md` with:
49
- - files changed/created
50
- - key decisions
51
- - deviations from plan
52
- - test results
53
- - Verification Gate status
54
- - unresolved blockers, if any
55
-
56
- ## Protocol References
57
-
58
- - Follow the global Context Budget Rules.
59
- - Carry forward the Dev-isolated subset: skip scaffold/generated files listed in `context-snapshot.md`; verify dependency versions before install/build commands that resolve dependencies; after build/compile commands, ensure outputs are ignored and never commit generated artifacts.
60
- - If tests fail, follow this Test Failure Recovery subset: classify failures as baseline, new regression, brittle test, or environment/tooling; fix new regressions and brittle tests while progress is being made; document baseline failures; write `failure-log.md` for blockers.
61
- - Do not run git commands; staging and commit are handled by the orchestrator.
62
- - **Edit safety**: If an Edit fails with 'String to replace not found', grep for the target text before retrying. Never guess file offsets — verify them with a Read or grep first.
63
- - **Read safety**: If 3 consecutive Reads to the same file return 'shorter than offset' or 'Wasted call', STOP and report BLOCKED.
64
- - **Test gate placement**: Do not run mandatory periodic or full-suite tests here. The scoped/full test gate runs after code review; only use tiny targeted checks when needed for local debugging.
65
-
66
- Do not return success unless:
67
- 1. implementation tasks are complete;
68
- 2. `## Implementation Log` exists;
69
- 3. every Verification Gate is verified.
70
-
71
- If any Verification Gate is blocked, write `failure-log.md` and return a blocked/incomplete result instead of success."