prizmkit 1.1.130 → 1.1.134
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/adapters/claude/command-adapter.js +0 -2
- package/bundled/dev-pipeline/.env.example +1 -1
- package/bundled/dev-pipeline/assets/skill-subagent-integration.md +11 -6
- package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +445 -8
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +2 -9
- package/bundled/dev-pipeline/prizmkit_runtime/processes.py +21 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +87 -66
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +53 -3
- package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +55 -25
- package/bundled/dev-pipeline/prizmkit_runtime/runner_status.py +4 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +119 -18
- package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +3 -2
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +411 -17
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +57 -14
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +5 -4
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +55 -2
- package/bundled/dev-pipeline/scripts/prompt_framework.py +152 -1
- package/bundled/dev-pipeline/scripts/update-bug-status.py +53 -0
- package/bundled/dev-pipeline/scripts/update-checkpoint.py +294 -69
- package/bundled/dev-pipeline/scripts/update-feature-status.py +57 -0
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +53 -0
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +5 -5
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +28 -22
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +24 -10
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +20 -2
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-commit-report.md +2 -2
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +1 -1
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-manual-verification.md +5 -10
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +9 -19
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-test.md +21 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-session-status.md +1 -1
- package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +10 -10
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +1 -1
- package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +15 -0
- package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-commit.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +26 -23
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +5 -5
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +15 -11
- package/bundled/dev-pipeline/templates/sections/refactor-phase-commit-report.md +1 -1
- package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +6 -6
- package/bundled/dev-pipeline/templates/sections/refactor-phase-test.md +21 -0
- package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +1 -1
- package/bundled/dev-pipeline/templates/sections/subagent-timeout-recovery.md +1 -1
- package/bundled/dev-pipeline/templates/sections/task-contract.md +4 -0
- package/bundled/dev-pipeline/tests/test_checkpoint_state.py +362 -3
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +349 -48
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +129 -0
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +91 -0
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +567 -23
- package/bundled/dev-pipeline/tests/test_unified_cli.py +37 -10
- package/bundled/skills/_metadata.json +63 -26
- package/bundled/skills/app-planner/SKILL.md +4 -3
- package/bundled/skills/bug-fix-workflow/SKILL.md +89 -180
- package/bundled/skills/bug-planner/SKILL.md +12 -27
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +131 -220
- package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +36 -90
- package/bundled/skills/feature-pipeline-launcher/SKILL.md +129 -271
- package/bundled/skills/feature-pipeline-launcher/references/configuration.md +35 -76
- package/bundled/skills/feature-planner/SKILL.md +27 -22
- package/bundled/skills/feature-planner/assets/planning-guide.md +4 -4
- package/bundled/skills/feature-workflow/SKILL.md +94 -121
- package/bundled/skills/prizmkit/SKILL.md +130 -94
- package/bundled/skills/prizmkit/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-code-review/SKILL.md +132 -86
- package/bundled/skills/prizmkit-code-review/references/independent-code-review.md +243 -0
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +42 -1
- package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-code-review/scripts/render_review_report.py +93 -1
- package/bundled/skills/prizmkit-committer/SKILL.md +125 -48
- package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-implement/SKILL.md +99 -73
- package/bundled/skills/prizmkit-implement/references/implementation-subagent-procedure.md +1 -1
- package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-init/SKILL.md +5 -5
- package/bundled/skills/prizmkit-plan/SKILL.md +146 -105
- package/bundled/skills/prizmkit-plan/assets/plan-template.md +18 -0
- package/bundled/skills/prizmkit-plan/references/independent-plan-review.md +242 -0
- package/bundled/skills/prizmkit-plan/references/review-plan-spec-loop.md +4 -4
- package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-retrospective/SKILL.md +111 -53
- package/bundled/skills/prizmkit-retrospective/references/knowledge-injection-steps.md +7 -6
- package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +5 -4
- package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-test/SKILL.md +52 -6
- package/bundled/skills/prizmkit-test/assets/authoritative-records.schema.json +63 -12
- package/bundled/skills/prizmkit-test/assets/evidence-manifest.schema.json +1 -1
- package/bundled/skills/prizmkit-test/assets/evidence-package-template.json +13 -0
- package/bundled/skills/prizmkit-test/references/evidence-protocol.md +6 -1
- package/bundled/skills/prizmkit-test/references/evidence-request-protocol.md +8 -1
- package/bundled/skills/prizmkit-test/references/examples.md +1 -1
- package/bundled/skills/prizmkit-test/references/test-generation-steps.md +4 -1
- package/bundled/skills/prizmkit-test/references/test-report-template.md +1 -1
- package/bundled/skills/prizmkit-test/references/trusted-evidence-execution.md +15 -0
- package/bundled/skills/prizmkit-test/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-test/scripts/build_test_evidence.py +744 -23
- package/bundled/skills/prizmkit-test/scripts/validate_test_evidence.py +194 -15
- package/bundled/skills/prizmkit-workflow/SKILL.md +266 -0
- package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/recovery-workflow/SKILL.md +62 -241
- package/bundled/skills/recovery-workflow/evals/evals.json +17 -33
- package/bundled/skills/recovery-workflow/references/detection.md +28 -52
- package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +105 -439
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +136 -275
- package/bundled/skills/refactor-pipeline-launcher/references/configuration.md +53 -88
- package/bundled/skills/refactor-planner/SKILL.md +12 -57
- package/bundled/skills/refactor-planner/references/behavior-preservation.md +111 -230
- package/bundled/skills/refactor-planner/references/error-recovery.md +37 -0
- package/bundled/skills/refactor-planner/references/planning-phases.md +11 -17
- package/bundled/skills/refactor-planner/scripts/validate-and-generate-refactor.py +1 -1
- package/bundled/skills/refactor-workflow/SKILL.md +98 -139
- package/bundled/templates/project-memory-template.md +4 -4
- package/package.json +1 -1
- package/src/index.js +4 -0
- package/src/prompts.js +10 -3
- package/src/scaffold.js +1 -1
- package/bundled/dev-pipeline/tests/test-deploy-safety.sh +0 -223
- package/bundled/skills/feature-pipeline-launcher/scripts/preflight-check.py +0 -462
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
import shutil
|
|
6
|
+
import signal
|
|
6
7
|
import subprocess
|
|
7
8
|
import sys
|
|
8
9
|
from pathlib import Path
|
|
@@ -90,6 +91,93 @@ def _item_id_from_next_result(kind, result):
|
|
|
90
91
|
return result.data[key]
|
|
91
92
|
|
|
92
93
|
|
|
94
|
+
def _write_semantic_completion_checkpoint(project_root, artifact_dir, workflow_type="feature-pipeline"):
|
|
95
|
+
validator = (
|
|
96
|
+
project_root / ".claude" / "command-assets" / "prizmkit-test"
|
|
97
|
+
/ "scripts" / "validate_test_evidence.py"
|
|
98
|
+
)
|
|
99
|
+
validator.parent.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
validator.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
|
|
101
|
+
contracts = {
|
|
102
|
+
"feature-pipeline": (
|
|
103
|
+
"feature_state",
|
|
104
|
+
[
|
|
105
|
+
"prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
|
|
106
|
+
"prizmkit-retrospective", "prizmkit-committer", "completion-summary",
|
|
107
|
+
],
|
|
108
|
+
),
|
|
109
|
+
"bugfix-pipeline": (
|
|
110
|
+
"bugfix_state",
|
|
111
|
+
[
|
|
112
|
+
"prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
|
|
113
|
+
"prizmkit-retrospective", "prizmkit-committer", "bug-report",
|
|
114
|
+
],
|
|
115
|
+
),
|
|
116
|
+
"refactor-pipeline": (
|
|
117
|
+
"refactor_state",
|
|
118
|
+
[
|
|
119
|
+
"prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
|
|
120
|
+
"prizmkit-retrospective", "prizmkit-committer", "refactor-report",
|
|
121
|
+
"completion-summary",
|
|
122
|
+
],
|
|
123
|
+
),
|
|
124
|
+
}
|
|
125
|
+
state_key, skills = contracts[workflow_type]
|
|
126
|
+
evidence_id = "semantic-evidence"
|
|
127
|
+
evidence_dir = project_root / ".prizmkit" / "test" / "evidence" / evidence_id
|
|
128
|
+
evidence_dir.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
evidence = {
|
|
130
|
+
"manifest": evidence_dir / "manifest.json",
|
|
131
|
+
"verdict": evidence_dir / "verdict.json",
|
|
132
|
+
"validation": evidence_dir / "validation.json",
|
|
133
|
+
}
|
|
134
|
+
evidence["manifest"].write_text(
|
|
135
|
+
json.dumps({"evidence_id": evidence_id, "final_verdict": "TEST_PASS"}),
|
|
136
|
+
encoding="utf-8",
|
|
137
|
+
)
|
|
138
|
+
evidence["verdict"].write_text(json.dumps({"verdict": "TEST_PASS"}), encoding="utf-8")
|
|
139
|
+
evidence["validation"].write_text(
|
|
140
|
+
json.dumps({"verdict": "TEST_PASS", "result": "passed"}), encoding="utf-8"
|
|
141
|
+
)
|
|
142
|
+
relative_evidence = {
|
|
143
|
+
key: str(path.relative_to(project_root)) for key, path in evidence.items()
|
|
144
|
+
}
|
|
145
|
+
steps = []
|
|
146
|
+
previous = None
|
|
147
|
+
for index, skill in enumerate(skills, start=1):
|
|
148
|
+
step = {
|
|
149
|
+
"id": f"S{index:02d}",
|
|
150
|
+
"skill": skill,
|
|
151
|
+
"name": skill,
|
|
152
|
+
"status": "completed",
|
|
153
|
+
"required_artifacts": [],
|
|
154
|
+
"depends_on": previous,
|
|
155
|
+
}
|
|
156
|
+
if skill == "prizmkit-code-review":
|
|
157
|
+
step["stage_result"] = "REVIEW_PASS"
|
|
158
|
+
if skill == "prizmkit-test":
|
|
159
|
+
step["stage_result"] = "TEST_PASS"
|
|
160
|
+
step["authoritative_evidence_paths"] = relative_evidence
|
|
161
|
+
steps.append(step)
|
|
162
|
+
previous = step["id"]
|
|
163
|
+
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
164
|
+
checkpoint = artifact_dir / "workflow-checkpoint.json"
|
|
165
|
+
checkpoint.write_text(
|
|
166
|
+
json.dumps({
|
|
167
|
+
"version": 1,
|
|
168
|
+
"workflow_type": workflow_type,
|
|
169
|
+
"steps": steps,
|
|
170
|
+
state_key: {
|
|
171
|
+
"repair_round": 0,
|
|
172
|
+
"authoritative_evidence_paths": relative_evidence,
|
|
173
|
+
"terminal_status": "WORKFLOW_COMPLETED",
|
|
174
|
+
},
|
|
175
|
+
}),
|
|
176
|
+
encoding="utf-8",
|
|
177
|
+
)
|
|
178
|
+
return checkpoint
|
|
179
|
+
|
|
180
|
+
|
|
93
181
|
def test_retired_shell_runtime_files_are_absent_from_source():
|
|
94
182
|
retired = [
|
|
95
183
|
"run-feature.sh",
|
|
@@ -346,6 +434,7 @@ def test_runner_models_parse_legacy_args_and_environment(tmp_path):
|
|
|
346
434
|
assert env.stale_kill_threshold_seconds == 9
|
|
347
435
|
assert env.max_retries == 9
|
|
348
436
|
assert env.strict_behavior_check is False
|
|
437
|
+
assert not hasattr(env, "enable_deploy")
|
|
349
438
|
assert not hasattr(env, "max_" + "log_size")
|
|
350
439
|
|
|
351
440
|
|
|
@@ -1443,8 +1532,7 @@ def test_has_branch_completion_evidence_requires_task_branch_ahead_of_base(tmp_p
|
|
|
1443
1532
|
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
|
|
1444
1533
|
subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
|
|
1445
1534
|
artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-done"
|
|
1446
|
-
artifact_dir
|
|
1447
|
-
(artifact_dir / "completion-summary.json").write_text(json.dumps({"completion_notes": ["Completed durable workflow"]}), encoding="utf-8")
|
|
1535
|
+
_write_semantic_completion_checkpoint(tmp_path, artifact_dir)
|
|
1448
1536
|
(tmp_path / "base.txt").write_text("base", encoding="utf-8")
|
|
1449
1537
|
subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
|
|
1450
1538
|
subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
@@ -1505,11 +1593,7 @@ def test_no_worktree_recovers_completed_pending_branch_before_new_session(monkey
|
|
|
1505
1593
|
session_dir = paths.feature_state_dir / "F-001" / "sessions" / "F-001-session-old"
|
|
1506
1594
|
session_dir.mkdir(parents=True)
|
|
1507
1595
|
artifact_dir = paths.project_root / ".prizmkit" / "specs" / "001-low"
|
|
1508
|
-
artifact_dir
|
|
1509
|
-
(artifact_dir / "workflow-checkpoint.json").write_text(
|
|
1510
|
-
json.dumps({"steps": [{"id": "S01", "skill": "done", "name": "Done", "status": "completed"}]}),
|
|
1511
|
-
encoding="utf-8",
|
|
1512
|
-
)
|
|
1596
|
+
_write_semantic_completion_checkpoint(paths.project_root, artifact_dir)
|
|
1513
1597
|
updates = []
|
|
1514
1598
|
events = []
|
|
1515
1599
|
|
|
@@ -1876,26 +1960,29 @@ def test_completed_checkout_blocks_rerun_until_explicit_reset(monkeypatch, tmp_p
|
|
|
1876
1960
|
runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
|
|
1877
1961
|
|
|
1878
1962
|
|
|
1879
|
-
def test_no_worktree_interrupt_saves_wip_and_returns_to_base(monkeypatch, tmp_path):
|
|
1963
|
+
def test_no_worktree_interrupt_saves_wip_and_returns_to_base(monkeypatch, tmp_path, capsys):
|
|
1880
1964
|
from prizmkit_runtime import runners
|
|
1965
|
+
from prizmkit_runtime.gitops import current_branch as real_current_branch
|
|
1966
|
+
from prizmkit_runtime.gitops import run_git_command as real_run_git_command
|
|
1881
1967
|
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
1882
1968
|
from prizmkit_runtime.task_checkout import load_task_checkout
|
|
1883
1969
|
|
|
1884
1970
|
monkeypatch.setenv("DEV_BRANCH", "dev/F-001-interrupt")
|
|
1885
1971
|
paths = _make_paths(tmp_path)
|
|
1972
|
+
_init_bookkeeping_repo(paths.project_root)
|
|
1886
1973
|
family = family_for("feature", paths)
|
|
1887
1974
|
invocation = parse_invocation(family, "run", ())
|
|
1888
1975
|
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
|
|
1889
1976
|
_install_runner_session_fakes(monkeypatch, runners)
|
|
1890
|
-
|
|
1891
|
-
monkeypatch.setattr(runners, "
|
|
1892
|
-
monkeypatch.setattr(runners, "branch_ensure_return", lambda project_root, base, branch: ensure_calls.append((base, branch)))
|
|
1977
|
+
monkeypatch.setattr(runners, "current_branch", real_current_branch)
|
|
1978
|
+
monkeypatch.setattr(runners, "run_git_command", real_run_git_command)
|
|
1893
1979
|
|
|
1894
1980
|
class InterruptingLauncher:
|
|
1895
1981
|
def __init__(self, config):
|
|
1896
1982
|
self.config = config
|
|
1897
1983
|
|
|
1898
1984
|
def run(self):
|
|
1985
|
+
(self.config.cwd / "interrupted.txt").write_text("preserved\n", encoding="utf-8")
|
|
1899
1986
|
raise KeyboardInterrupt
|
|
1900
1987
|
|
|
1901
1988
|
monkeypatch.setattr(runners, "AISessionLauncher", InterruptingLauncher)
|
|
@@ -1904,11 +1991,158 @@ def test_no_worktree_interrupt_saves_wip_and_returns_to_base(monkeypatch, tmp_pa
|
|
|
1904
1991
|
runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
|
|
1905
1992
|
|
|
1906
1993
|
checkout = load_task_checkout(family.state_dir, family.task_type, "F-001")
|
|
1994
|
+
captured = capsys.readouterr()
|
|
1907
1995
|
assert checkout is not None
|
|
1908
1996
|
assert checkout.is_active
|
|
1909
1997
|
assert checkout.active_dev_branch == "dev/F-001-interrupt"
|
|
1910
1998
|
assert checkout.base_branch == "main"
|
|
1911
|
-
assert
|
|
1999
|
+
assert real_current_branch(paths.project_root) == "main"
|
|
2000
|
+
assert subprocess.run(
|
|
2001
|
+
["git", "show", "dev/F-001-interrupt:interrupted.txt"],
|
|
2002
|
+
cwd=paths.project_root,
|
|
2003
|
+
check=True,
|
|
2004
|
+
stdout=subprocess.PIPE,
|
|
2005
|
+
text=True,
|
|
2006
|
+
).stdout == "preserved\n"
|
|
2007
|
+
assert "Interrupted task F-001: work preserved and returned to main" in captured.err
|
|
2008
|
+
|
|
2009
|
+
|
|
2010
|
+
def test_interrupt_return_failure_is_reported_without_forcing_checkout(monkeypatch, tmp_path, capsys):
|
|
2011
|
+
from prizmkit_runtime import runners
|
|
2012
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
2013
|
+
|
|
2014
|
+
monkeypatch.setenv("DEV_BRANCH", "dev/F-001-interrupt")
|
|
2015
|
+
paths = _make_paths(tmp_path)
|
|
2016
|
+
family = family_for("feature", paths)
|
|
2017
|
+
invocation = parse_invocation(family, "run", ())
|
|
2018
|
+
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
|
|
2019
|
+
_install_runner_session_fakes(monkeypatch, runners)
|
|
2020
|
+
monkeypatch.setattr(runners, "run_git_plan", lambda project_root, plan: SimpleNamespace(ok=True, plan=plan, results=()))
|
|
2021
|
+
failure = SimpleNamespace(
|
|
2022
|
+
ok=False,
|
|
2023
|
+
plan=SimpleNamespace(
|
|
2024
|
+
commands=(
|
|
2025
|
+
SimpleNamespace(allow_failure=True),
|
|
2026
|
+
SimpleNamespace(allow_failure=False),
|
|
2027
|
+
)
|
|
2028
|
+
),
|
|
2029
|
+
results=(
|
|
2030
|
+
SimpleNamespace(
|
|
2031
|
+
args=("commit", "--no-verify", "-m", "wip"),
|
|
2032
|
+
return_code=1,
|
|
2033
|
+
stdout="nothing to commit",
|
|
2034
|
+
stderr="",
|
|
2035
|
+
),
|
|
2036
|
+
SimpleNamespace(
|
|
2037
|
+
args=("checkout", "main"),
|
|
2038
|
+
return_code=1,
|
|
2039
|
+
stdout="",
|
|
2040
|
+
stderr="untracked files would be overwritten",
|
|
2041
|
+
),
|
|
2042
|
+
),
|
|
2043
|
+
)
|
|
2044
|
+
monkeypatch.setattr(runners, "branch_ensure_return", lambda project_root, base, branch: failure)
|
|
2045
|
+
|
|
2046
|
+
class InterruptingLauncher:
|
|
2047
|
+
def __init__(self, config):
|
|
2048
|
+
self.config = config
|
|
2049
|
+
|
|
2050
|
+
def run(self):
|
|
2051
|
+
raise KeyboardInterrupt
|
|
2052
|
+
|
|
2053
|
+
monkeypatch.setattr(runners, "AISessionLauncher", InterruptingLauncher)
|
|
2054
|
+
|
|
2055
|
+
with pytest.raises(KeyboardInterrupt):
|
|
2056
|
+
runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
|
|
2057
|
+
|
|
2058
|
+
captured = capsys.readouterr()
|
|
2059
|
+
assert "Could not safely return to main; leaving dev/F-001-interrupt active" in captured.err
|
|
2060
|
+
assert "git checkout main failed: untracked files would be overwritten" in captured.err
|
|
2061
|
+
assert "After resolving the Git blocker, run: git checkout main" in captured.err
|
|
2062
|
+
|
|
2063
|
+
|
|
2064
|
+
def test_worktree_interrupt_preserves_task_worktree_without_returning_caller_checkout(monkeypatch, tmp_path, capsys):
|
|
2065
|
+
from prizmkit_runtime import runners
|
|
2066
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
2067
|
+
|
|
2068
|
+
monkeypatch.setenv("USE_WORKTREE", "1")
|
|
2069
|
+
monkeypatch.setenv("DEV_BRANCH", "dev/F-001-interrupt")
|
|
2070
|
+
paths = _make_paths(tmp_path)
|
|
2071
|
+
family = family_for("feature", paths)
|
|
2072
|
+
invocation = parse_invocation(family, "run", ())
|
|
2073
|
+
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
|
|
2074
|
+
observed = _install_runner_session_fakes(monkeypatch, runners)
|
|
2075
|
+
monkeypatch.setattr(
|
|
2076
|
+
runners,
|
|
2077
|
+
"ensure_linked_worktree",
|
|
2078
|
+
lambda runtime: SimpleNamespace(ok=True, runtime=runtime, reason=""),
|
|
2079
|
+
)
|
|
2080
|
+
return_calls = []
|
|
2081
|
+
monkeypatch.setattr(
|
|
2082
|
+
runners,
|
|
2083
|
+
"branch_ensure_return",
|
|
2084
|
+
lambda *args: return_calls.append(args),
|
|
2085
|
+
)
|
|
2086
|
+
|
|
2087
|
+
class InterruptingLauncher:
|
|
2088
|
+
def __init__(self, config):
|
|
2089
|
+
self.config = config
|
|
2090
|
+
|
|
2091
|
+
def run(self):
|
|
2092
|
+
raise KeyboardInterrupt
|
|
2093
|
+
|
|
2094
|
+
monkeypatch.setattr(runners, "AISessionLauncher", InterruptingLauncher)
|
|
2095
|
+
|
|
2096
|
+
with pytest.raises(KeyboardInterrupt):
|
|
2097
|
+
runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
|
|
2098
|
+
|
|
2099
|
+
captured = capsys.readouterr()
|
|
2100
|
+
assert observed.wip_calls
|
|
2101
|
+
assert return_calls == []
|
|
2102
|
+
assert "Interrupted task F-001: work preserved in" in captured.err
|
|
2103
|
+
assert "task worktree remains active" in captured.err
|
|
2104
|
+
|
|
2105
|
+
|
|
2106
|
+
def test_run_pipeline_command_bridges_sigterm_and_restores_handler(monkeypatch, tmp_path):
|
|
2107
|
+
from prizmkit_runtime import runners
|
|
2108
|
+
from prizmkit_runtime.runner_models import PipelineRunResult
|
|
2109
|
+
|
|
2110
|
+
paths = _make_paths(tmp_path)
|
|
2111
|
+
paths.feature_plan.write_text("{}", encoding="utf-8")
|
|
2112
|
+
previous = signal.getsignal(signal.SIGTERM)
|
|
2113
|
+
observed = []
|
|
2114
|
+
|
|
2115
|
+
def interrupted_pipeline(*args, **kwargs):
|
|
2116
|
+
installed = signal.getsignal(signal.SIGTERM)
|
|
2117
|
+
observed.append(installed)
|
|
2118
|
+
try:
|
|
2119
|
+
installed(signal.SIGTERM, None)
|
|
2120
|
+
except KeyboardInterrupt:
|
|
2121
|
+
return PipelineRunResult(False, 1, "interrupted", "interrupted")
|
|
2122
|
+
raise AssertionError("SIGTERM handler did not interrupt the pipeline")
|
|
2123
|
+
|
|
2124
|
+
monkeypatch.setattr(runners, "_run_pipeline", interrupted_pipeline)
|
|
2125
|
+
|
|
2126
|
+
result = runners.run_pipeline_command("feature", "run", (), paths)
|
|
2127
|
+
|
|
2128
|
+
assert result.exit_code == 1
|
|
2129
|
+
assert result.message == "processed=1 completed=False stopped_reason=interrupted"
|
|
2130
|
+
assert observed and observed[0] not in {previous, signal.SIG_DFL, signal.SIG_IGN}
|
|
2131
|
+
assert signal.getsignal(signal.SIGTERM) is previous
|
|
2132
|
+
|
|
2133
|
+
|
|
2134
|
+
def test_interruption_cleanup_shield_ignores_and_restores_signals():
|
|
2135
|
+
from prizmkit_runtime.processes import shield_interruption_signals
|
|
2136
|
+
|
|
2137
|
+
previous_sigint = signal.getsignal(signal.SIGINT)
|
|
2138
|
+
previous_sigterm = signal.getsignal(signal.SIGTERM)
|
|
2139
|
+
|
|
2140
|
+
with shield_interruption_signals():
|
|
2141
|
+
assert signal.getsignal(signal.SIGINT) is signal.SIG_IGN
|
|
2142
|
+
assert signal.getsignal(signal.SIGTERM) is signal.SIG_IGN
|
|
2143
|
+
|
|
2144
|
+
assert signal.getsignal(signal.SIGINT) is previous_sigint
|
|
2145
|
+
assert signal.getsignal(signal.SIGTERM) is previous_sigterm
|
|
1912
2146
|
|
|
1913
2147
|
|
|
1914
2148
|
@pytest.mark.parametrize(
|
|
@@ -2555,9 +2789,7 @@ def test_classification_accepts_late_infra_error_with_completed_checkpoint_and_c
|
|
|
2555
2789
|
subprocess.run(["git", "add", "done.txt"], cwd=tmp_path, check=True)
|
|
2556
2790
|
subprocess.run(["git", "commit", "-m", "done"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
2557
2791
|
artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-done"
|
|
2558
|
-
artifact_dir
|
|
2559
|
-
checkpoint = artifact_dir / "workflow-checkpoint.json"
|
|
2560
|
-
checkpoint.write_text(json.dumps({"steps": [{"id": "S01", "skill": "done", "name": "Done", "status": "completed"}]}), encoding="utf-8")
|
|
2792
|
+
checkpoint = _write_semantic_completion_checkpoint(tmp_path, artifact_dir)
|
|
2561
2793
|
log = tmp_path / "session.log"
|
|
2562
2794
|
log.write_text("model quota exceeded; API error after commit", encoding="utf-8")
|
|
2563
2795
|
raw = AISessionResult(
|
|
@@ -2579,10 +2811,10 @@ def test_classification_accepts_late_infra_error_with_completed_checkpoint_and_c
|
|
|
2579
2811
|
classification = classify_session(raw, project_root=tmp_path, base_head=base_head, prompt=prompt)
|
|
2580
2812
|
|
|
2581
2813
|
assert classification.session_status == "success"
|
|
2582
|
-
assert classification.reason == "
|
|
2814
|
+
assert classification.reason == "semantic_checkpoint_with_late_runtime_error"
|
|
2583
2815
|
|
|
2584
2816
|
|
|
2585
|
-
def
|
|
2817
|
+
def test_classification_rejects_late_infra_with_completion_summary_only(tmp_path):
|
|
2586
2818
|
from prizmkit_runtime.runner_classification import classify_session
|
|
2587
2819
|
from prizmkit_runtime.runner_models import PromptGenerationResult
|
|
2588
2820
|
from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
|
|
@@ -2627,8 +2859,8 @@ def test_classification_accepts_late_infra_with_completion_artifact_without_new_
|
|
|
2627
2859
|
|
|
2628
2860
|
classification = classify_session(raw, project_root=tmp_path, base_head=side_head, prompt=prompt)
|
|
2629
2861
|
|
|
2630
|
-
assert classification.session_status == "
|
|
2631
|
-
assert classification.reason == "
|
|
2862
|
+
assert classification.session_status == "infra_error"
|
|
2863
|
+
assert classification.reason == "ai_runtime_or_provider_error"
|
|
2632
2864
|
|
|
2633
2865
|
|
|
2634
2866
|
def test_classification_keeps_pre_completion_provider_error_as_infra_error(tmp_path):
|
|
@@ -2681,9 +2913,7 @@ def test_classification_accepts_terminal_success_with_completed_checkpoint_witho
|
|
|
2681
2913
|
subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
2682
2914
|
base_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True).stdout.strip()
|
|
2683
2915
|
artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-done"
|
|
2684
|
-
artifact_dir
|
|
2685
|
-
checkpoint = artifact_dir / "workflow-checkpoint.json"
|
|
2686
|
-
checkpoint.write_text(json.dumps({"steps": [{"id": "S01", "skill": "done", "name": "Done", "status": "completed"}]}), encoding="utf-8")
|
|
2916
|
+
checkpoint = _write_semantic_completion_checkpoint(tmp_path, artifact_dir)
|
|
2687
2917
|
(artifact_dir / "completion-summary.json").write_text(json.dumps({"completion_notes": ["Completed durable workflow"]}), encoding="utf-8")
|
|
2688
2918
|
log = tmp_path / "session.log"
|
|
2689
2919
|
log.write_text("terminal success", encoding="utf-8")
|
|
@@ -2709,7 +2939,7 @@ def test_classification_accepts_terminal_success_with_completed_checkpoint_witho
|
|
|
2709
2939
|
|
|
2710
2940
|
classification = classify_session(raw, project_root=tmp_path, base_head=base_head, prompt=prompt)
|
|
2711
2941
|
assert classification.session_status == "success"
|
|
2712
|
-
assert classification.reason == "
|
|
2942
|
+
assert classification.reason == "terminal_success_with_semantic_checkpoint"
|
|
2713
2943
|
|
|
2714
2944
|
|
|
2715
2945
|
def test_classification_preserves_success_before_context_overflow(tmp_path):
|
|
@@ -3475,3 +3705,317 @@ def test_timeout_and_stale_termination_override_provider_error_markers(tmp_path,
|
|
|
3475
3705
|
|
|
3476
3706
|
assert classification.session_status == expected_status
|
|
3477
3707
|
assert classification.reason == expected_reason
|
|
3708
|
+
|
|
3709
|
+
|
|
3710
|
+
|
|
3711
|
+
@pytest.mark.parametrize("workflow_type", ["feature-pipeline", "bugfix-pipeline", "refactor-pipeline"])
|
|
3712
|
+
def test_semantic_completion_classification_has_cross_family_parity(tmp_path, workflow_type):
|
|
3713
|
+
from prizmkit_runtime.runner_classification import classify_session
|
|
3714
|
+
from prizmkit_runtime.runner_models import PromptGenerationResult
|
|
3715
|
+
from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
|
|
3716
|
+
from prizmkit_runtime.status import ProgressSummary
|
|
3717
|
+
|
|
3718
|
+
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
3719
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
|
|
3720
|
+
subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
|
|
3721
|
+
(tmp_path / "base.txt").write_text("base", encoding="utf-8")
|
|
3722
|
+
subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
|
|
3723
|
+
subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
3724
|
+
artifact = tmp_path / ".prizmkit" / "artifacts" / workflow_type
|
|
3725
|
+
checkpoint = _write_semantic_completion_checkpoint(tmp_path, artifact, workflow_type)
|
|
3726
|
+
log = tmp_path / f"{workflow_type}.log"
|
|
3727
|
+
log.write_text("success", encoding="utf-8")
|
|
3728
|
+
raw = AISessionResult(
|
|
3729
|
+
command=AISessionCommand(("fake",), None, "claude", "stdin"),
|
|
3730
|
+
exit_code=0,
|
|
3731
|
+
timed_out=False,
|
|
3732
|
+
termination_reason=None,
|
|
3733
|
+
stale_marker=None,
|
|
3734
|
+
fatal_error_code="",
|
|
3735
|
+
progress_summary=ProgressSummary(result_subtype="success"),
|
|
3736
|
+
session_log=log,
|
|
3737
|
+
backup_log=tmp_path / "backup.log",
|
|
3738
|
+
log_recovery=None,
|
|
3739
|
+
started_epoch=0,
|
|
3740
|
+
ended_epoch=1,
|
|
3741
|
+
)
|
|
3742
|
+
|
|
3743
|
+
classification = classify_session(
|
|
3744
|
+
raw,
|
|
3745
|
+
project_root=tmp_path,
|
|
3746
|
+
base_head="",
|
|
3747
|
+
prompt=PromptGenerationResult(
|
|
3748
|
+
output_path=tmp_path / "prompt.md",
|
|
3749
|
+
checkpoint_path=checkpoint,
|
|
3750
|
+
artifact_path=artifact,
|
|
3751
|
+
),
|
|
3752
|
+
)
|
|
3753
|
+
|
|
3754
|
+
assert classification.session_status == "success"
|
|
3755
|
+
assert classification.checkpoint_state["terminal_status"] == "WORKFLOW_COMPLETED"
|
|
3756
|
+
|
|
3757
|
+
|
|
3758
|
+
def test_semantic_blocked_classification_is_terminal_and_retry_neutral(tmp_path):
|
|
3759
|
+
from prizmkit_runtime.runner_classification import classify_session
|
|
3760
|
+
from prizmkit_runtime.runner_models import PromptGenerationResult
|
|
3761
|
+
from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
|
|
3762
|
+
from prizmkit_runtime.status import ProgressSummary
|
|
3763
|
+
|
|
3764
|
+
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
3765
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
|
|
3766
|
+
subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
|
|
3767
|
+
(tmp_path / "base.txt").write_text("base", encoding="utf-8")
|
|
3768
|
+
subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
|
|
3769
|
+
subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
3770
|
+
artifact = tmp_path / ".prizmkit" / "specs" / "blocked"
|
|
3771
|
+
checkpoint = _write_semantic_completion_checkpoint(tmp_path, artifact)
|
|
3772
|
+
data = json.loads(checkpoint.read_text(encoding="utf-8"))
|
|
3773
|
+
data["feature_state"].update(
|
|
3774
|
+
terminal_status="WORKFLOW_BLOCKED",
|
|
3775
|
+
blocked_reason="repair_round_limit",
|
|
3776
|
+
repair_round=3,
|
|
3777
|
+
)
|
|
3778
|
+
checkpoint.write_text(json.dumps(data), encoding="utf-8")
|
|
3779
|
+
log = tmp_path / "blocked.log"
|
|
3780
|
+
log.write_text("ambiguous failure", encoding="utf-8")
|
|
3781
|
+
raw = AISessionResult(
|
|
3782
|
+
command=AISessionCommand(("fake",), None, "codebuddy", "stdin"),
|
|
3783
|
+
exit_code=1,
|
|
3784
|
+
timed_out=False,
|
|
3785
|
+
termination_reason=None,
|
|
3786
|
+
stale_marker=None,
|
|
3787
|
+
fatal_error_code="",
|
|
3788
|
+
progress_summary=ProgressSummary(terminal_result_text="ambiguous failure"),
|
|
3789
|
+
session_log=log,
|
|
3790
|
+
backup_log=tmp_path / "backup.log",
|
|
3791
|
+
log_recovery=None,
|
|
3792
|
+
started_epoch=0,
|
|
3793
|
+
ended_epoch=1,
|
|
3794
|
+
)
|
|
3795
|
+
|
|
3796
|
+
classification = classify_session(
|
|
3797
|
+
raw,
|
|
3798
|
+
project_root=tmp_path,
|
|
3799
|
+
base_head="",
|
|
3800
|
+
prompt=PromptGenerationResult(
|
|
3801
|
+
output_path=tmp_path / "prompt.md",
|
|
3802
|
+
checkpoint_path=checkpoint,
|
|
3803
|
+
artifact_path=artifact,
|
|
3804
|
+
),
|
|
3805
|
+
)
|
|
3806
|
+
|
|
3807
|
+
assert classification.session_status == "workflow_blocked"
|
|
3808
|
+
assert classification.reason == "repair_round_limit"
|
|
3809
|
+
assert classification.checkpoint_state["repair_round"] == 3
|
|
3810
|
+
|
|
3811
|
+
|
|
3812
|
+
def test_recovery_outcome_preserves_dirty_partial_work_and_uses_semantic_checkpoint(tmp_path):
|
|
3813
|
+
from prizmkit_runtime.runner_recovery import _recovery_outcome
|
|
3814
|
+
|
|
3815
|
+
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
3816
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
|
|
3817
|
+
subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
|
|
3818
|
+
(tmp_path / "base.txt").write_text("base", encoding="utf-8")
|
|
3819
|
+
subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
|
|
3820
|
+
subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
3821
|
+
(tmp_path / "partial.txt").write_text("preserve me", encoding="utf-8")
|
|
3822
|
+
artifact = tmp_path / ".prizmkit" / "specs" / "done"
|
|
3823
|
+
checkpoint = _write_semantic_completion_checkpoint(tmp_path, artifact)
|
|
3824
|
+
|
|
3825
|
+
outcome, semantic = _recovery_outcome(tmp_path, 1, checkpoint_path=checkpoint)
|
|
3826
|
+
|
|
3827
|
+
assert outcome == "success"
|
|
3828
|
+
assert semantic["terminal_status"] == "WORKFLOW_COMPLETED"
|
|
3829
|
+
assert (tmp_path / "partial.txt").read_text(encoding="utf-8") == "preserve me"
|
|
3830
|
+
assert subprocess.run(
|
|
3831
|
+
["git", "status", "--porcelain", "--", "partial.txt"],
|
|
3832
|
+
cwd=tmp_path,
|
|
3833
|
+
check=True,
|
|
3834
|
+
stdout=subprocess.PIPE,
|
|
3835
|
+
text=True,
|
|
3836
|
+
).stdout.startswith("??")
|
|
3837
|
+
|
|
3838
|
+
|
|
3839
|
+
def test_runtime_semantic_paths_do_not_invoke_composite_workflow():
|
|
3840
|
+
sources = [
|
|
3841
|
+
PIPELINE_ROOT / "prizmkit_runtime" / "checkpoint_state.py",
|
|
3842
|
+
PIPELINE_ROOT / "prizmkit_runtime" / "runner_classification.py",
|
|
3843
|
+
PIPELINE_ROOT / "prizmkit_runtime" / "runner_recovery.py",
|
|
3844
|
+
PIPELINE_ROOT / "prizmkit_runtime" / "runners.py",
|
|
3845
|
+
]
|
|
3846
|
+
|
|
3847
|
+
for source in sources:
|
|
3848
|
+
assert "prizmkit-workflow" not in source.read_text(encoding="utf-8")
|
|
3849
|
+
|
|
3850
|
+
|
|
3851
|
+
|
|
3852
|
+
@pytest.mark.parametrize(
|
|
3853
|
+
("kind", "item_id", "blocked_status"),
|
|
3854
|
+
[("feature", "F-001", "failed"), ("bugfix", "B-001", "needs_info"), ("refactor", "R-001", "failed")],
|
|
3855
|
+
)
|
|
3856
|
+
def test_family_status_persists_identical_semantic_blocked_snapshot_without_retry_spend(
|
|
3857
|
+
tmp_path, kind, item_id, blocked_status
|
|
3858
|
+
):
|
|
3859
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
3860
|
+
from prizmkit_runtime.runner_status import start_item, update_item
|
|
3861
|
+
|
|
3862
|
+
paths = _make_paths(tmp_path)
|
|
3863
|
+
item = {
|
|
3864
|
+
"id": item_id,
|
|
3865
|
+
"title": "Semantic blocked",
|
|
3866
|
+
"status": "pending",
|
|
3867
|
+
"priority": "high",
|
|
3868
|
+
"dependencies": [],
|
|
3869
|
+
}
|
|
3870
|
+
if kind == "bugfix":
|
|
3871
|
+
item.update(severity="high", description="blocked", reproduction_steps=["run"])
|
|
3872
|
+
elif kind == "refactor":
|
|
3873
|
+
item.update(description="blocked", targets=["src"], goals=["preserve"])
|
|
3874
|
+
_write_family_plan(paths, kind, [item])
|
|
3875
|
+
family = family_for(kind, paths)
|
|
3876
|
+
invocation = parse_invocation(family, "run", ())
|
|
3877
|
+
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": family.plan_path})
|
|
3878
|
+
assert start_item(family, invocation, item_id, paths.project_root).ok is True
|
|
3879
|
+
semantic = {
|
|
3880
|
+
"current_stage": None,
|
|
3881
|
+
"next_stage": None,
|
|
3882
|
+
"stage_result": "TEST_BLOCKED",
|
|
3883
|
+
"repair_scope": "production",
|
|
3884
|
+
"repair_round": 2,
|
|
3885
|
+
"authoritative_evidence_paths": {
|
|
3886
|
+
"manifest": ".prizmkit/test/evidence/id/manifest.json",
|
|
3887
|
+
"verdict": ".prizmkit/test/evidence/id/verdict.json",
|
|
3888
|
+
"validation": ".prizmkit/test/evidence/id/validation.json",
|
|
3889
|
+
},
|
|
3890
|
+
"blocked_reason": "test_blocked",
|
|
3891
|
+
"terminal_status": "WORKFLOW_BLOCKED",
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
result = update_item(
|
|
3895
|
+
family,
|
|
3896
|
+
invocation,
|
|
3897
|
+
item_id,
|
|
3898
|
+
"workflow_blocked",
|
|
3899
|
+
"session-1",
|
|
3900
|
+
paths.project_root,
|
|
3901
|
+
checkpoint_state=semantic,
|
|
3902
|
+
)
|
|
3903
|
+
status = json.loads((family.state_dir / item_id / "status.json").read_text(encoding="utf-8"))
|
|
3904
|
+
|
|
3905
|
+
assert result.ok is True
|
|
3906
|
+
assert result.data["new_status"] == blocked_status
|
|
3907
|
+
assert result.data["checkpoint_state"] == semantic
|
|
3908
|
+
assert result.data["restart_policy"] == "terminal_blocked"
|
|
3909
|
+
assert status["retry_count"] == 0
|
|
3910
|
+
assert status["infra_error_count"] == 0
|
|
3911
|
+
for key, value in semantic.items():
|
|
3912
|
+
assert status[key] == value
|
|
3913
|
+
|
|
3914
|
+
|
|
3915
|
+
|
|
3916
|
+
def test_recovery_detector_returns_no_candidate_for_empty_project(tmp_path):
|
|
3917
|
+
import importlib.util
|
|
3918
|
+
|
|
3919
|
+
detector_path = (
|
|
3920
|
+
REPO_ROOT / "core" / "skills" / "orchestration-skill" / "workflows"
|
|
3921
|
+
/ "recovery-workflow" / "scripts" / "detect-recovery-state.py"
|
|
3922
|
+
)
|
|
3923
|
+
spec = importlib.util.spec_from_file_location("recovery_detector_empty", detector_path)
|
|
3924
|
+
module = importlib.util.module_from_spec(spec)
|
|
3925
|
+
spec.loader.exec_module(module)
|
|
3926
|
+
|
|
3927
|
+
report = module.build_report(tmp_path)
|
|
3928
|
+
|
|
3929
|
+
assert report["detected"] is False
|
|
3930
|
+
assert report["candidates"] == []
|
|
3931
|
+
|
|
3932
|
+
|
|
3933
|
+
def test_recovery_detector_returns_active_checkout_command(tmp_path, monkeypatch):
|
|
3934
|
+
import importlib.util
|
|
3935
|
+
|
|
3936
|
+
detector_path = (
|
|
3937
|
+
REPO_ROOT / "core" / "skills" / "orchestration-skill" / "workflows"
|
|
3938
|
+
/ "recovery-workflow" / "scripts" / "detect-recovery-state.py"
|
|
3939
|
+
)
|
|
3940
|
+
spec = importlib.util.spec_from_file_location("recovery_detector_active", detector_path)
|
|
3941
|
+
module = importlib.util.module_from_spec(spec)
|
|
3942
|
+
spec.loader.exec_module(module)
|
|
3943
|
+
|
|
3944
|
+
checkout_path = tmp_path / ".prizmkit" / "state" / "features" / "F-001" / "checkout.json"
|
|
3945
|
+
checkout_path.parent.mkdir(parents=True)
|
|
3946
|
+
checkout_path.write_text(
|
|
3947
|
+
json.dumps({
|
|
3948
|
+
"task_type": "feature",
|
|
3949
|
+
"task_id": "F-001",
|
|
3950
|
+
"state": "active",
|
|
3951
|
+
"active_dev_branch": "dev/F-001-resume",
|
|
3952
|
+
"base_branch": "main",
|
|
3953
|
+
"checkout_mode": "branch",
|
|
3954
|
+
}),
|
|
3955
|
+
encoding="utf-8",
|
|
3956
|
+
)
|
|
3957
|
+
monkeypatch.setattr(module, "branch_exists", lambda *_args: True)
|
|
3958
|
+
|
|
3959
|
+
report = module.build_report(tmp_path)
|
|
3960
|
+
|
|
3961
|
+
assert report["detected"] is True
|
|
3962
|
+
assert report["selection_required"] is False
|
|
3963
|
+
assert report["candidates"][0]["continue_command"] == (
|
|
3964
|
+
"python3 ./.prizmkit/dev-pipeline/cli.py feature run F-001"
|
|
3965
|
+
)
|
|
3966
|
+
|
|
3967
|
+
|
|
3968
|
+
def test_recovery_detector_ignores_completed_checkout(tmp_path, monkeypatch):
|
|
3969
|
+
import importlib.util
|
|
3970
|
+
|
|
3971
|
+
detector_path = (
|
|
3972
|
+
REPO_ROOT / "core" / "skills" / "orchestration-skill" / "workflows"
|
|
3973
|
+
/ "recovery-workflow" / "scripts" / "detect-recovery-state.py"
|
|
3974
|
+
)
|
|
3975
|
+
spec = importlib.util.spec_from_file_location("recovery_detector_completed", detector_path)
|
|
3976
|
+
module = importlib.util.module_from_spec(spec)
|
|
3977
|
+
spec.loader.exec_module(module)
|
|
3978
|
+
|
|
3979
|
+
checkout_path = tmp_path / ".prizmkit" / "state" / "bugfix" / "B-001" / "checkout.json"
|
|
3980
|
+
checkout_path.parent.mkdir(parents=True)
|
|
3981
|
+
checkout_path.write_text(
|
|
3982
|
+
json.dumps({
|
|
3983
|
+
"task_type": "bugfix",
|
|
3984
|
+
"task_id": "B-001",
|
|
3985
|
+
"state": "completed",
|
|
3986
|
+
"active_dev_branch": "bugfix/B-001-done",
|
|
3987
|
+
"base_branch": "main",
|
|
3988
|
+
}),
|
|
3989
|
+
encoding="utf-8",
|
|
3990
|
+
)
|
|
3991
|
+
monkeypatch.setattr(module, "branch_exists", lambda *_args: True)
|
|
3992
|
+
|
|
3993
|
+
report = module.build_report(tmp_path)
|
|
3994
|
+
|
|
3995
|
+
assert report["detected"] is False
|
|
3996
|
+
|
|
3997
|
+
|
|
3998
|
+
def test_l4_runtime_and_recovery_sources_do_not_depend_on_direct_l1_composite():
|
|
3999
|
+
runtime_root = REPO_ROOT / "dev-pipeline"
|
|
4000
|
+
source_files = list(runtime_root.rglob("*.py"))
|
|
4001
|
+
for source in source_files:
|
|
4002
|
+
if "__pycache__" in source.parts or "tests" in source.parts or not source.is_file():
|
|
4003
|
+
continue
|
|
4004
|
+
assert "prizmkit-workflow" not in source.read_text(encoding="utf-8")
|
|
4005
|
+
|
|
4006
|
+
|
|
4007
|
+
def test_recovery_detector_has_no_direct_lifecycle_fallback(tmp_path):
|
|
4008
|
+
import importlib.util
|
|
4009
|
+
|
|
4010
|
+
detector_path = (
|
|
4011
|
+
REPO_ROOT / "core" / "skills" / "orchestration-skill" / "workflows"
|
|
4012
|
+
/ "recovery-workflow" / "scripts" / "detect-recovery-state.py"
|
|
4013
|
+
)
|
|
4014
|
+
spec = importlib.util.spec_from_file_location("recovery_detector_contract", detector_path)
|
|
4015
|
+
module = importlib.util.module_from_spec(spec)
|
|
4016
|
+
spec.loader.exec_module(module)
|
|
4017
|
+
|
|
4018
|
+
report = module.build_report(tmp_path)
|
|
4019
|
+
|
|
4020
|
+
assert "continue_command" not in report
|
|
4021
|
+
assert report["detected"] is False
|