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.
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/README.md +6 -7
- package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +3 -3
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +0 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +83 -12
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +38 -2
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +497 -154
- package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +98 -9
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +37 -28
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +4 -3
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +62 -38
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +57 -49
- package/bundled/dev-pipeline/templates/sections/critical-paths-agent.md +0 -1
- package/bundled/dev-pipeline/templates/sections/critical-paths-full.md +0 -1
- package/bundled/dev-pipeline/templates/sections/directory-convention-agent.md +2 -2
- package/bundled/dev-pipeline/templates/sections/directory-convention-full.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +1 -0
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +1 -0
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +46 -0
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +308 -0
- package/bundled/dev-pipeline/tests/test_unified_cli.py +1 -0
- package/bundled/skills/_metadata.json +1 -1
- package/package.json +1 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/package.json
CHANGED
|
@@ -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."
|