prizmkit 1.1.153 → 1.1.155
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/codex/skill-adapter.js +1 -1
- package/bundled/adapters/pi/skill-adapter.js +2 -1
- package/bundled/dev-pipeline/.env.example +2 -6
- package/bundled/dev-pipeline/README.md +120 -113
- package/bundled/dev-pipeline/assets/skill-subagent-integration.md +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +355 -15
- package/bundled/dev-pipeline/prizmkit_runtime/cli.py +189 -110
- package/bundled/dev-pipeline/prizmkit_runtime/commands.py +146 -111
- package/bundled/dev-pipeline/prizmkit_runtime/daemon.py +4 -20
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +48 -14
- package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +0 -1
- package/bundled/dev-pipeline/prizmkit_runtime/paths.py +0 -3
- package/bundled/dev-pipeline/prizmkit_runtime/reset.py +122 -15
- package/bundled/dev-pipeline/prizmkit_runtime/reset_preserve.py +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +50 -2
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +1 -10
- package/bundled/dev-pipeline/prizmkit_runtime/runner_prompts.py +3 -3
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +139 -74
- package/bundled/dev-pipeline/prizmkit_runtime/runtime_commit.py +482 -0
- package/bundled/dev-pipeline/prizmkit_runtime/status.py +106 -1
- package/bundled/dev-pipeline/prizmkit_runtime/test_result.py +64 -11
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +145 -367
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +91 -62
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +105 -69
- package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +2 -2
- package/bundled/dev-pipeline/scripts/prompt_framework.py +36 -41
- package/bundled/dev-pipeline/scripts/update-bug-status.py +197 -71
- package/bundled/dev-pipeline/scripts/update-checkpoint.py +77 -16
- package/bundled/dev-pipeline/scripts/update-feature-status.py +46 -121
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +46 -123
- package/bundled/dev-pipeline/scripts/utils.py +119 -0
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +22 -27
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +71 -104
- package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +3 -2
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +59 -111
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +37 -72
- package/bundled/dev-pipeline/templates/sections/artifact-manifest.md +39 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-critical-paths.md +3 -1
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-commit-report.md +8 -25
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +3 -3
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-implement.md +2 -2
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +3 -3
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-test.md +2 -2
- package/bundled/dev-pipeline/templates/sections/bugfix-reminders.md +4 -4
- package/bundled/dev-pipeline/templates/sections/bugfix-session-context.md +0 -1
- package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +7 -4
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +14 -27
- package/bundled/dev-pipeline/templates/sections/feature-completion-summary.md +20 -0
- package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +8 -71
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +7 -7
- package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +3 -3
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +4 -4
- package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +5 -17
- package/bundled/dev-pipeline/templates/sections/refactor-critical-paths.md +3 -1
- package/bundled/dev-pipeline/templates/sections/refactor-phase-commit-report.md +10 -23
- package/bundled/dev-pipeline/templates/sections/refactor-phase-implement.md +2 -2
- package/bundled/dev-pipeline/templates/sections/refactor-phase-plan.md +2 -2
- package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +3 -3
- package/bundled/dev-pipeline/templates/sections/refactor-phase-test.md +2 -2
- package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +7 -7
- package/bundled/dev-pipeline/templates/sections/refactor-session-context.md +0 -1
- package/bundled/dev-pipeline/templates/sections/retrospective.md +12 -0
- package/bundled/dev-pipeline/templates/sections/runtime-commit-handoff.md +35 -0
- package/bundled/dev-pipeline/templates/session-status-schema.json +45 -62
- package/bundled/dev-pipeline/tests/test_auto_skip.py +111 -32
- package/bundled/dev-pipeline/tests/test_checkpoint_state.py +617 -30
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +262 -194
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +62 -23
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +76 -28
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +281 -206
- package/bundled/dev-pipeline/tests/test_recovery_workflow.py +322 -0
- package/bundled/dev-pipeline/tests/test_reset_modes.py +935 -0
- package/bundled/dev-pipeline/tests/test_reset_preserve.py +5 -7
- package/bundled/dev-pipeline/tests/test_runtime_commit.py +608 -0
- package/bundled/dev-pipeline/tests/test_unified_cli.py +617 -188
- package/bundled/skills/_metadata.json +9 -9
- package/bundled/skills/app-planner/SKILL.md +3 -3
- package/bundled/skills/app-planner/references/generated-plan-review.md +2 -1
- package/bundled/skills/app-planner/references/rules/backend/derivation-rules.md +56 -64
- package/bundled/skills/app-planner/references/rules/backend/fixed-rules.md +85 -240
- package/bundled/skills/app-planner/references/rules/backend/question-bank.md +25 -25
- package/bundled/skills/app-planner/references/rules/backend/question-manifest.json +16 -7
- package/bundled/skills/app-planner/references/rules/backend/template.md +74 -92
- package/bundled/skills/app-planner/references/rules/database/derivation-rules.md +93 -93
- package/bundled/skills/app-planner/references/rules/database/fixed-rules.md +66 -173
- package/bundled/skills/app-planner/references/rules/database/question-bank.md +31 -26
- package/bundled/skills/app-planner/references/rules/database/question-manifest.json +13 -4
- package/bundled/skills/app-planner/references/rules/database/template.md +76 -98
- package/bundled/skills/app-planner/references/rules/frontend/derivation-rules.md +68 -95
- package/bundled/skills/app-planner/references/rules/frontend/fixed-rules.md +93 -166
- package/bundled/skills/app-planner/references/rules/frontend/question-bank.md +46 -36
- package/bundled/skills/app-planner/references/rules/frontend/question-manifest.json +11 -2
- package/bundled/skills/app-planner/references/rules/frontend/template.md +79 -257
- package/bundled/skills/app-planner/references/rules/mobile/derivation-rules.md +91 -99
- package/bundled/skills/app-planner/references/rules/mobile/fixed-rules.md +73 -246
- package/bundled/skills/app-planner/references/rules/mobile/question-bank.md +17 -17
- package/bundled/skills/app-planner/references/rules/mobile/question-manifest.json +17 -2
- package/bundled/skills/app-planner/references/rules/mobile/template.md +73 -85
- package/bundled/skills/app-planner/references/rules-configuration.md +133 -65
- package/bundled/skills/app-planner/scripts/validate-rules-configuration.py +291 -0
- package/bundled/skills/bug-planner/references/schema-validation.md +1 -1
- package/bundled/skills/bug-planner/scripts/validate-bug-list.py +1 -1
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +21 -13
- package/bundled/skills/feature-pipeline-launcher/SKILL.md +22 -14
- package/bundled/skills/prizmkit/SKILL.md +17 -18
- package/bundled/skills/prizmkit/references/workflow-state-protocol.md +22 -18
- package/bundled/skills/prizmkit-code-review/SKILL.md +22 -58
- package/bundled/skills/prizmkit-code-review/references/independent-code-review.md +6 -6
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +2 -2
- package/bundled/skills/prizmkit-committer/SKILL.md +82 -132
- package/bundled/skills/prizmkit-deploy/SKILL.md +27 -11
- package/bundled/skills/prizmkit-deploy/assets/deploy-document-template.md +89 -0
- package/bundled/skills/prizmkit-deploy/references/ci-cd-workflows.md +44 -14
- package/bundled/skills/prizmkit-deploy/references/cloud-platform-deploy.md +19 -3
- package/bundled/skills/prizmkit-deploy/references/database-setup.md +22 -12
- package/bundled/skills/prizmkit-deploy/references/deploy-config-schema.md +3 -1
- package/bundled/skills/prizmkit-deploy/references/deploy-history-schema.md +22 -6
- package/bundled/skills/prizmkit-deploy/references/deploy-metadata-schema.json +49 -0
- package/bundled/skills/prizmkit-deploy/references/live-validation-notes.md +1 -1
- package/bundled/skills/prizmkit-deploy/references/pending-input-schema.json +164 -0
- package/bundled/skills/prizmkit-deploy/references/ssh-adapter-flow.md +9 -8
- package/bundled/skills/prizmkit-deploy/references/ssh-execution-flow.md +6 -6
- package/bundled/skills/prizmkit-implement/SKILL.md +30 -66
- package/bundled/skills/prizmkit-implement/references/implementation-subagent-procedure.md +4 -4
- package/bundled/skills/prizmkit-init/SKILL.md +4 -0
- package/bundled/skills/prizmkit-plan/SKILL.md +21 -64
- package/bundled/skills/prizmkit-plan/references/artifact-identity.md +38 -0
- package/bundled/skills/prizmkit-plan/references/independent-plan-review.md +4 -4
- package/bundled/skills/prizmkit-plan/references/review-plan-spec-loop.md +5 -5
- package/bundled/skills/prizmkit-prizm-docs/SKILL.md +11 -1
- package/bundled/skills/prizmkit-retrospective/SKILL.md +63 -77
- package/bundled/skills/prizmkit-retrospective/references/knowledge-injection-steps.md +3 -3
- package/bundled/skills/prizmkit-retrospective/references/retrospective-result-schema.json +138 -0
- package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +17 -23
- package/bundled/skills/prizmkit-test/SKILL.md +64 -51
- package/bundled/skills/prizmkit-test/references/boundary-contract-and-test-double-guidance.md +206 -0
- package/bundled/skills/prizmkit-test/references/independent-test-review.md +17 -9
- package/bundled/skills/prizmkit-test/references/test-coverage-model.md +45 -21
- package/bundled/skills/prizmkit-test/references/test-report-template.md +20 -13
- package/bundled/skills/prizmkit-workflow/SKILL.md +35 -37
- package/bundled/skills/prizmkit-workflow/references/artifact-identity.md +34 -0
- package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +22 -18
- package/bundled/skills/recovery-workflow/SKILL.md +9 -6
- package/bundled/skills/recovery-workflow/evals/evals.json +2 -2
- package/bundled/skills/recovery-workflow/references/detection.md +7 -6
- package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +16 -6
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +21 -13
- package/bundled/templates/project-memory-template.md +19 -11
- package/package.json +1 -1
- package/src/scaffold.js +33 -22
- package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +0 -228
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +0 -767
- package/bundled/dev-pipeline/templates/sections/bugfix-artifacts.md +0 -16
- package/bundled/dev-pipeline/templates/sections/bugfix-session-status.md +0 -31
- package/bundled/dev-pipeline/templates/sections/critical-paths-agent.md +0 -7
- package/bundled/dev-pipeline/templates/sections/critical-paths-lite.md +0 -7
- package/bundled/dev-pipeline/templates/sections/directory-convention-agent.md +0 -9
- package/bundled/dev-pipeline/templates/sections/directory-convention-lite.md +0 -6
- package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +0 -15
- package/bundled/dev-pipeline/templates/sections/phase-commit.md +0 -61
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-agent-suffix.md +0 -24
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-lite-suffix.md +0 -12
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +0 -44
- package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +0 -25
- package/bundled/dev-pipeline/templates/sections/phase-plan-agent.md +0 -20
- package/bundled/dev-pipeline/templates/sections/phase-plan-lite.md +0 -20
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +0 -20
- package/bundled/dev-pipeline/templates/sections/refactor-artifacts.md +0 -17
- package/bundled/dev-pipeline/templates/sections/refactor-session-status.md +0 -28
- package/bundled/dev-pipeline/templates/sections/test-failure-recovery-agent.md +0 -52
- package/bundled/dev-pipeline/templates/sections/test-failure-recovery-lite.md +0 -52
- package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +0 -174
- package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +0 -174
- package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +0 -174
- package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +0 -174
- package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +0 -174
- package/bundled/skills/prizmkit-test/references/external-contract-mock-guidance.md +0 -119
|
@@ -54,23 +54,16 @@ def _load_progress_parser_module():
|
|
|
54
54
|
return module
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def
|
|
57
|
+
def test_cli_help_exposes_only_operation_first_public_namespaces():
|
|
58
58
|
result = run_cli("--help")
|
|
59
59
|
|
|
60
60
|
assert result.returncode == 0
|
|
61
61
|
assert "canonical Python runtime CLI" in result.stdout
|
|
62
|
-
assert "
|
|
63
|
-
for group in [
|
|
64
|
-
"feature",
|
|
65
|
-
"bugfix",
|
|
66
|
-
"refactor",
|
|
67
|
-
"recovery",
|
|
68
|
-
"reset",
|
|
69
|
-
"daemon",
|
|
70
|
-
"status",
|
|
71
|
-
"diagnostics",
|
|
72
|
-
]:
|
|
62
|
+
assert "operation-first" in result.stdout
|
|
63
|
+
for group in ["run", "status", "reset", "daemon", "diagnostics", "helper"]:
|
|
73
64
|
assert group in result.stdout
|
|
65
|
+
for removed in ["feature", "bugfix", "refactor", "recovery"]:
|
|
66
|
+
assert removed not in result.stdout.lower()
|
|
74
67
|
assert "Traceback" not in result.stderr
|
|
75
68
|
|
|
76
69
|
|
|
@@ -79,7 +72,7 @@ def test_cli_help_preserves_runtime_identity_at_narrow_terminal_width():
|
|
|
79
72
|
|
|
80
73
|
assert result.returncode == 0
|
|
81
74
|
assert "canonical Python runtime CLI" in result.stdout
|
|
82
|
-
assert "
|
|
75
|
+
assert "operation-first" in result.stdout
|
|
83
76
|
|
|
84
77
|
|
|
85
78
|
def test_module_entrypoint_help_exposes_same_cli():
|
|
@@ -99,6 +92,45 @@ def test_module_entrypoint_help_exposes_same_cli():
|
|
|
99
92
|
assert "diagnostics" in result.stdout
|
|
100
93
|
|
|
101
94
|
|
|
95
|
+
@pytest.mark.parametrize(
|
|
96
|
+
"args",
|
|
97
|
+
[
|
|
98
|
+
("--help",),
|
|
99
|
+
("run", "--help"),
|
|
100
|
+
("run", "feature", "--help"),
|
|
101
|
+
("run", "bugfix", "--help"),
|
|
102
|
+
("run", "refactor", "--help"),
|
|
103
|
+
("status", "--help"),
|
|
104
|
+
("status", "feature", "--help"),
|
|
105
|
+
("status", "all", "--help"),
|
|
106
|
+
("reset", "--help"),
|
|
107
|
+
("reset", "feature", "--help"),
|
|
108
|
+
("daemon", "--help"),
|
|
109
|
+
("daemon", "feature", "--help"),
|
|
110
|
+
("daemon", "feature", "start", "--help"),
|
|
111
|
+
("daemon", "bugfix", "stop", "--help"),
|
|
112
|
+
("daemon", "refactor", "status", "--help"),
|
|
113
|
+
("daemon", "feature", "logs", "--help"),
|
|
114
|
+
("daemon", "feature", "restart", "--help"),
|
|
115
|
+
("diagnostics", "--help"),
|
|
116
|
+
("diagnostics", "ai-cli", "--help"),
|
|
117
|
+
("helper", "--help"),
|
|
118
|
+
],
|
|
119
|
+
)
|
|
120
|
+
def test_help_at_every_namespace_depth_is_parser_owned_and_never_dispatches(monkeypatch, args):
|
|
121
|
+
from prizmkit_runtime import cli as runtime_cli
|
|
122
|
+
|
|
123
|
+
dispatched = []
|
|
124
|
+
monkeypatch.setattr(runtime_cli, "ensure_supported_python", lambda: None)
|
|
125
|
+
monkeypatch.setattr(runtime_cli, "_dispatch", lambda parsed: dispatched.append(parsed))
|
|
126
|
+
|
|
127
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
128
|
+
runtime_cli.main(list(args))
|
|
129
|
+
|
|
130
|
+
assert exc_info.value.code == 0
|
|
131
|
+
assert dispatched == []
|
|
132
|
+
|
|
133
|
+
|
|
102
134
|
def test_invalid_arguments_report_usage_without_traceback():
|
|
103
135
|
result = run_cli("unknown-command")
|
|
104
136
|
|
|
@@ -108,6 +140,80 @@ def test_invalid_arguments_report_usage_without_traceback():
|
|
|
108
140
|
assert "Traceback" not in result.stderr
|
|
109
141
|
|
|
110
142
|
|
|
143
|
+
@pytest.mark.parametrize(
|
|
144
|
+
"args",
|
|
145
|
+
[
|
|
146
|
+
("recovery",),
|
|
147
|
+
("recovery", "detect"),
|
|
148
|
+
("recovery", "run"),
|
|
149
|
+
("recovery", "describe"),
|
|
150
|
+
("recovery", "run", "--dry-run"),
|
|
151
|
+
],
|
|
152
|
+
)
|
|
153
|
+
def test_former_recovery_invocations_are_unknown_commands(args):
|
|
154
|
+
result = run_cli(*args)
|
|
155
|
+
|
|
156
|
+
assert result.returncode == 2
|
|
157
|
+
assert "invalid choice: 'recovery'" in result.stderr
|
|
158
|
+
assert "Traceback" not in result.stderr
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_former_recovery_invocation_never_reaches_dispatch(monkeypatch):
|
|
162
|
+
from prizmkit_runtime import cli as runtime_cli
|
|
163
|
+
|
|
164
|
+
dispatched = []
|
|
165
|
+
monkeypatch.setattr(runtime_cli, "ensure_supported_python", lambda: None)
|
|
166
|
+
monkeypatch.setattr(runtime_cli, "_dispatch", lambda args: dispatched.append(args))
|
|
167
|
+
|
|
168
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
169
|
+
runtime_cli.main(["recovery", "run", "--dry-run"])
|
|
170
|
+
|
|
171
|
+
assert exc_info.value.code == 2
|
|
172
|
+
assert dispatched == []
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@pytest.mark.parametrize(
|
|
176
|
+
"args",
|
|
177
|
+
[
|
|
178
|
+
("feature", "run"),
|
|
179
|
+
("feature", "status"),
|
|
180
|
+
("feature", "reset"),
|
|
181
|
+
("feature", "unskip"),
|
|
182
|
+
("feature", "test-cli"),
|
|
183
|
+
("feature", "describe"),
|
|
184
|
+
("bugfix", "run"),
|
|
185
|
+
("bugfix", "status"),
|
|
186
|
+
("bugfix", "reset"),
|
|
187
|
+
("bugfix", "unskip"),
|
|
188
|
+
("bugfix", "describe"),
|
|
189
|
+
("refactor", "run"),
|
|
190
|
+
("refactor", "status"),
|
|
191
|
+
("refactor", "reset"),
|
|
192
|
+
("refactor", "unskip"),
|
|
193
|
+
("refactor", "describe"),
|
|
194
|
+
("reset", "describe"),
|
|
195
|
+
("daemon", "feature", "describe"),
|
|
196
|
+
("diagnostics", "describe"),
|
|
197
|
+
("status", "all", "custom-plan.json"),
|
|
198
|
+
("run", "feature", "--unknown-option"),
|
|
199
|
+
("run", "feature", "F-001", "plan.json", "extra"),
|
|
200
|
+
("daemon", "feature", "status", "extra"),
|
|
201
|
+
],
|
|
202
|
+
)
|
|
203
|
+
def test_removed_or_invalid_forms_fail_in_parser_without_runtime_dispatch(monkeypatch, args):
|
|
204
|
+
from prizmkit_runtime import cli as runtime_cli
|
|
205
|
+
|
|
206
|
+
dispatched = []
|
|
207
|
+
monkeypatch.setattr(runtime_cli, "ensure_supported_python", lambda: None)
|
|
208
|
+
monkeypatch.setattr(runtime_cli, "_dispatch", lambda parsed: dispatched.append(parsed))
|
|
209
|
+
|
|
210
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
211
|
+
runtime_cli.main(list(args))
|
|
212
|
+
|
|
213
|
+
assert exc_info.value.code == 2
|
|
214
|
+
assert dispatched == []
|
|
215
|
+
|
|
216
|
+
|
|
111
217
|
def test_python_version_guard_mentions_required_version():
|
|
112
218
|
from prizmkit_runtime.compat import UnsupportedPythonVersion, ensure_supported_python
|
|
113
219
|
|
|
@@ -136,6 +242,7 @@ def test_runtime_paths_support_framework_source_layout():
|
|
|
136
242
|
assert paths.refactor_plan == paths.plans_dir / "refactor-list.json"
|
|
137
243
|
assert paths.feature_state_dir == paths.state_dir / "features"
|
|
138
244
|
assert paths.specs_dir == paths.prizmkit_dir / "specs"
|
|
245
|
+
assert not hasattr(paths, "recovery_state_dir")
|
|
139
246
|
assert all(isinstance(value, Path) for value in paths.path_values())
|
|
140
247
|
|
|
141
248
|
|
|
@@ -154,6 +261,22 @@ def test_runtime_paths_support_installed_layout(tmp_path):
|
|
|
154
261
|
assert paths.logs_dir == paths.state_dir / "logs"
|
|
155
262
|
|
|
156
263
|
|
|
264
|
+
def test_runtime_diagnostics_leave_historical_recovery_state_untouched(tmp_path):
|
|
265
|
+
project = tmp_path / "project"
|
|
266
|
+
historical = project / ".prizmkit" / "state" / "recovery" / "historical-session"
|
|
267
|
+
historical.mkdir(parents=True)
|
|
268
|
+
marker = historical / "marker.json"
|
|
269
|
+
marker.write_bytes(b'{"preserve": true}\n')
|
|
270
|
+
before = {path.relative_to(project).as_posix(): path.read_bytes() for path in historical.rglob("*") if path.is_file()}
|
|
271
|
+
|
|
272
|
+
result = run_cli("--project-root", str(project), "diagnostics", "paths")
|
|
273
|
+
|
|
274
|
+
after = {path.relative_to(project).as_posix(): path.read_bytes() for path in historical.rglob("*") if path.is_file()}
|
|
275
|
+
assert result.returncode == 0
|
|
276
|
+
assert before == after
|
|
277
|
+
assert marker.is_file()
|
|
278
|
+
|
|
279
|
+
|
|
157
280
|
def test_runtime_config_models_environment_and_ai_resolution():
|
|
158
281
|
from prizmkit_runtime.config import AIClientResolution, RuntimeEnvironment, load_runtime_config
|
|
159
282
|
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
@@ -298,59 +421,51 @@ def test_runtime_config_supplied_env_does_not_leak_ambient_path(tmp_path):
|
|
|
298
421
|
|
|
299
422
|
|
|
300
423
|
|
|
301
|
-
|
|
424
|
+
def test_runtime_command_registry_contains_only_executing_operation_first_entries():
|
|
425
|
+
from prizmkit_runtime.commands import find_runtime_command, list_runtime_commands
|
|
302
426
|
|
|
303
427
|
command_keys = {command.key for command in list_runtime_commands()}
|
|
304
|
-
|
|
305
428
|
expected = {
|
|
306
|
-
(
|
|
307
|
-
(
|
|
308
|
-
(
|
|
309
|
-
(
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
(
|
|
313
|
-
|
|
314
|
-
(
|
|
315
|
-
("refactor", None, "run"),
|
|
316
|
-
("refactor", None, "status"),
|
|
317
|
-
("refactor", None, "unskip"),
|
|
318
|
-
("refactor", None, "describe"),
|
|
319
|
-
("recovery", None, "run"),
|
|
320
|
-
("recovery", None, "detect"),
|
|
321
|
-
("recovery", None, "describe"),
|
|
322
|
-
("reset", "feature", "reset"),
|
|
323
|
-
("reset", "bugfix", "reset"),
|
|
324
|
-
("reset", "refactor", "reset"),
|
|
325
|
-
("reset", None, "describe"),
|
|
326
|
-
("status", "feature", "status"),
|
|
327
|
-
("status", "bugfix", "status"),
|
|
328
|
-
("status", "refactor", "status"),
|
|
329
|
-
("status", "all", "status"),
|
|
330
|
-
("diagnostics", None, "paths"),
|
|
331
|
-
("diagnostics", None, "entrypoints"),
|
|
332
|
-
("diagnostics", None, "version"),
|
|
429
|
+
*(('run', family, 'run') for family in ('feature', 'bugfix', 'refactor')),
|
|
430
|
+
*(('status', family, 'status') for family in ('feature', 'bugfix', 'refactor', 'all')),
|
|
431
|
+
*(('reset', family, 'reset') for family in ('feature', 'bugfix', 'refactor')),
|
|
432
|
+
*(('daemon', family, action)
|
|
433
|
+
for family in ('feature', 'bugfix', 'refactor')
|
|
434
|
+
for action in ('start', 'stop', 'status', 'logs', 'restart')),
|
|
435
|
+
*(('diagnostics', None, action)
|
|
436
|
+
for action in ('paths', 'entrypoints', 'version', 'ai-cli')),
|
|
437
|
+
('helper', None, 'run'),
|
|
333
438
|
}
|
|
334
|
-
for target in ["feature", "bugfix", "refactor"]:
|
|
335
|
-
for action in ["start", "stop", "status", "logs", "restart", "describe"]:
|
|
336
|
-
expected.add(("daemon", target, action))
|
|
337
|
-
|
|
338
|
-
assert expected.issubset(command_keys)
|
|
339
439
|
|
|
440
|
+
assert command_keys == expected
|
|
441
|
+
assert all(command.current_entrypoint for command in list_runtime_commands())
|
|
442
|
+
assert all(command.action != "describe" for command in list_runtime_commands())
|
|
443
|
+
assert all(command.group not in {"feature", "bugfix", "refactor", "recovery"} for command in list_runtime_commands())
|
|
444
|
+
for removed in [
|
|
445
|
+
("feature", "run", None),
|
|
446
|
+
("feature", "test-cli", None),
|
|
447
|
+
("reset", "describe", None),
|
|
448
|
+
("daemon", "describe", "feature"),
|
|
449
|
+
("recovery", "detect", None),
|
|
450
|
+
]:
|
|
451
|
+
with pytest.raises(KeyError, match="Unknown runtime command"):
|
|
452
|
+
find_runtime_command(removed[0], removed[1], removed[2])
|
|
340
453
|
|
|
341
|
-
def test_python_runtime_command_output_describes_canonical_boundary():
|
|
342
|
-
result = run_cli("feature", "describe")
|
|
343
454
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
455
|
+
def test_removed_describe_commands_are_rejected():
|
|
456
|
+
for args in [
|
|
457
|
+
("feature", "describe"),
|
|
458
|
+
("reset", "describe"),
|
|
459
|
+
("daemon", "feature", "describe"),
|
|
460
|
+
("diagnostics", "describe"),
|
|
461
|
+
]:
|
|
462
|
+
result = run_cli(*args)
|
|
463
|
+
assert result.returncode == 2
|
|
464
|
+
assert "invalid choice" in result.stderr
|
|
465
|
+
assert "Python runtime command contract" not in result.stdout
|
|
351
466
|
|
|
352
467
|
|
|
353
|
-
def
|
|
468
|
+
def test_operation_first_runtime_arguments_are_validated_then_forwarded_exactly(monkeypatch):
|
|
354
469
|
from prizmkit_runtime import cli as runtime_cli
|
|
355
470
|
|
|
356
471
|
captured = []
|
|
@@ -362,76 +477,131 @@ def test_option_first_runtime_arguments_are_forwarded(monkeypatch):
|
|
|
362
477
|
monkeypatch.setattr(runtime_cli, "ensure_supported_python", lambda: None)
|
|
363
478
|
monkeypatch.setattr(runtime_cli, "handle_runtime_command", fake_handle)
|
|
364
479
|
|
|
365
|
-
|
|
366
|
-
|
|
480
|
+
run_args = [
|
|
481
|
+
"run", "feature", "F-001", "custom.json", "--features", "F-001:F-005",
|
|
482
|
+
"--max-retries", "5", "--max-infra-retries", "4",
|
|
483
|
+
"--resume-phase", "6", "--dry-run",
|
|
484
|
+
]
|
|
485
|
+
assert runtime_cli.main(run_args) == 0
|
|
486
|
+
assert captured[-1] == ("run", "run", "feature", tuple(run_args[2:]))
|
|
487
|
+
|
|
488
|
+
assert runtime_cli.main(["status", "bugfix", "bugs.json"]) == 0
|
|
489
|
+
assert captured[-1] == ("status", "status", "bugfix", ("bugs.json",))
|
|
367
490
|
|
|
368
|
-
assert runtime_cli.main(["daemon", "feature", "start"
|
|
369
|
-
assert captured[-1] == ("daemon", "start", "feature", (
|
|
491
|
+
assert runtime_cli.main(["daemon", "feature", "start"]) == 0
|
|
492
|
+
assert captured[-1] == ("daemon", "start", "feature", ())
|
|
370
493
|
|
|
371
494
|
assert runtime_cli.main(["daemon", "feature", "logs", "--follow"]) == 0
|
|
372
495
|
assert captured[-1] == ("daemon", "logs", "feature", ("--follow",))
|
|
373
496
|
|
|
374
|
-
assert runtime_cli.main(["reset", "feature", "--failed"]) == 0
|
|
375
|
-
assert captured[-1] == ("reset", "reset", "feature", ("--failed",))
|
|
497
|
+
assert runtime_cli.main(["reset", "feature", "--failed", "--preserve-runtime"]) == 0
|
|
498
|
+
assert captured[-1] == ("reset", "reset", "feature", ("--failed", "--preserve-runtime"))
|
|
376
499
|
|
|
377
|
-
|
|
378
|
-
assert
|
|
500
|
+
interspersed_reset = ["reset", "feature", "F-001", "--fresh-checkout", "custom.json"]
|
|
501
|
+
assert runtime_cli.main(interspersed_reset) == 0
|
|
502
|
+
assert captured[-1] == (
|
|
503
|
+
"reset", "reset", "feature", ("F-001", "--fresh-checkout", "custom.json")
|
|
504
|
+
)
|
|
379
505
|
|
|
506
|
+
interspersed_daemon = ["daemon", "feature", "start", "custom.json"]
|
|
507
|
+
assert runtime_cli.main(interspersed_daemon) == 0
|
|
508
|
+
assert captured[-1] == (
|
|
509
|
+
"daemon", "start", "feature", ("custom.json",)
|
|
510
|
+
)
|
|
380
511
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
512
|
+
assert runtime_cli.main(["--project-root", "feature", "run", "feature", "--dry-run"]) == 0
|
|
513
|
+
assert captured[-1] == ("run", "run", "feature", ("--dry-run",))
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def test_pipeline_mode_option_is_rejected():
|
|
517
|
+
foreground = run_cli("run", "feature", "--mode", "full")
|
|
518
|
+
daemon = run_cli("daemon", "feature", "start", "--mode", "full")
|
|
519
|
+
|
|
520
|
+
assert foreground.returncode == 2
|
|
521
|
+
assert daemon.returncode == 2
|
|
522
|
+
assert "unrecognized arguments" in foreground.stderr
|
|
523
|
+
assert "unrecognized arguments" in daemon.stderr
|
|
385
524
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
525
|
+
|
|
526
|
+
@pytest.mark.parametrize(
|
|
527
|
+
("family", "task_id", "extra_args"),
|
|
528
|
+
[
|
|
529
|
+
("feature", "F-007", ("--features", "F-007", "--dry-run")),
|
|
530
|
+
("bugfix", "B-007", ("--max-retries", "4")),
|
|
531
|
+
("refactor", "R-007", ("--resume-phase", "6", "--max-infra-retries", "5")),
|
|
532
|
+
],
|
|
533
|
+
)
|
|
534
|
+
def test_run_family_dispatch_preserves_each_family_argument_tail(
|
|
535
|
+
monkeypatch, family, task_id, extra_args
|
|
536
|
+
):
|
|
537
|
+
from prizmkit_runtime import cli as runtime_cli
|
|
390
538
|
|
|
391
539
|
captured = []
|
|
540
|
+
monkeypatch.setattr(runtime_cli, "ensure_supported_python", lambda: None)
|
|
541
|
+
monkeypatch.setattr(
|
|
542
|
+
runtime_cli,
|
|
543
|
+
"handle_runtime_command",
|
|
544
|
+
lambda group, action, **kwargs: captured.append(
|
|
545
|
+
(group, action, kwargs["target"], kwargs["legacy_args"])
|
|
546
|
+
) or runtime_cli.CommandResult(0, "ok", "ok"),
|
|
547
|
+
)
|
|
548
|
+
argv = ["run", family, task_id, *extra_args]
|
|
392
549
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
return FakeStatus()
|
|
550
|
+
assert runtime_cli.main(argv) == 0
|
|
551
|
+
assert captured == [("run", "run", family, (task_id, *extra_args))]
|
|
396
552
|
|
|
397
|
-
monkeypatch.setattr(runners, "run_status_action", fake_run_status_action)
|
|
398
|
-
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT)
|
|
399
553
|
|
|
400
|
-
|
|
554
|
+
@pytest.mark.parametrize("family", ("feature", "bugfix", "refactor"))
|
|
555
|
+
@pytest.mark.parametrize("legacy_action", ("reset", "un" + "skip"))
|
|
556
|
+
def test_family_level_reset_aliases_are_not_public_commands(family, legacy_action):
|
|
557
|
+
result = run_cli(family, legacy_action)
|
|
401
558
|
|
|
402
|
-
assert result.
|
|
403
|
-
assert
|
|
404
|
-
assert "unskipped" in result.render()
|
|
405
|
-
assert "Python runtime command contract" not in result.render()
|
|
406
|
-
assert captured == [("feature", "unskip", "F-005", "F-005", REPO_ROOT.resolve())]
|
|
559
|
+
assert result.returncode == 2
|
|
560
|
+
assert "invalid choice" in result.stderr
|
|
407
561
|
|
|
408
562
|
|
|
409
|
-
def
|
|
410
|
-
from prizmkit_runtime import
|
|
411
|
-
from prizmkit_runtime.commands import
|
|
563
|
+
def test_diagnostics_ai_cli_reports_family_neutral_resolution(monkeypatch):
|
|
564
|
+
from prizmkit_runtime import config
|
|
565
|
+
from prizmkit_runtime.commands import handle_diagnostics
|
|
412
566
|
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
413
567
|
|
|
414
568
|
class FakeAIClient:
|
|
415
569
|
command = "claude"
|
|
416
570
|
platform = "claude"
|
|
417
571
|
source = "test"
|
|
418
|
-
notes = ()
|
|
572
|
+
notes = ("configured globally",)
|
|
419
573
|
|
|
420
574
|
class FakeConfig:
|
|
421
575
|
ai_client = FakeAIClient()
|
|
422
576
|
runtime_mode = "python-runtime-canonical"
|
|
423
577
|
model = "claude-sonnet-4.6"
|
|
424
578
|
|
|
425
|
-
monkeypatch.setattr(
|
|
579
|
+
monkeypatch.setattr(config, "load_runtime_config", lambda paths: FakeConfig())
|
|
426
580
|
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT)
|
|
427
581
|
|
|
428
|
-
result =
|
|
582
|
+
result = handle_diagnostics("ai-cli", paths)
|
|
429
583
|
rendered = result.render()
|
|
430
584
|
|
|
431
585
|
assert result.exit_code == 0
|
|
432
|
-
assert result.title == "Python
|
|
586
|
+
assert result.title == "Python runtime AI CLI diagnostics"
|
|
433
587
|
assert "detected_cli: claude" in rendered
|
|
434
|
-
assert "
|
|
588
|
+
assert "platform: claude" in rendered
|
|
589
|
+
assert "source: test" in rendered
|
|
590
|
+
assert "runtime_mode: python-runtime-canonical" in rendered
|
|
591
|
+
assert "model: claude-sonnet-4.6" in rendered
|
|
592
|
+
assert "note: configured globally" in rendered
|
|
593
|
+
assert "feature" not in result.title.lower()
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def test_diagnostics_help_exposes_only_real_diagnostic_actions():
|
|
597
|
+
result = run_cli("diagnostics", "--help")
|
|
598
|
+
|
|
599
|
+
assert result.returncode == 0
|
|
600
|
+
for action in ("paths", "entrypoints", "version", "ai-cli"):
|
|
601
|
+
assert action in result.stdout
|
|
602
|
+
assert "describe" not in result.stdout
|
|
603
|
+
assert "commands" not in result.stdout
|
|
604
|
+
assert "test-cli" not in result.stdout
|
|
435
605
|
|
|
436
606
|
|
|
437
607
|
def test_diagnostics_paths_and_entrypoints_are_standard_library_cli_actions():
|
|
@@ -444,8 +614,203 @@ def test_diagnostics_paths_and_entrypoints_are_standard_library_cli_actions():
|
|
|
444
614
|
assert entrypoints_result.returncode == 0
|
|
445
615
|
assert "Python runtime entrypoint inventory" in entrypoints_result.stdout
|
|
446
616
|
assert "runtime:cli:" in entrypoints_result.stdout
|
|
617
|
+
assert "command:run:feature: python3 dev-pipeline/cli.py run feature" in entrypoints_result.stdout
|
|
618
|
+
assert "command:status:all: python3 dev-pipeline/cli.py status all" in entrypoints_result.stdout
|
|
619
|
+
assert "command:diagnostics:ai-cli:" in entrypoints_result.stdout
|
|
620
|
+
assert " describe" not in entrypoints_result.stdout
|
|
621
|
+
assert "test-cli" not in entrypoints_result.stdout
|
|
447
622
|
assert "run-feature.sh" not in entrypoints_result.stdout
|
|
448
623
|
assert "generate-bootstrap-prompt.py" in entrypoints_result.stdout
|
|
624
|
+
assert "generate-recovery-prompt.py" not in entrypoints_result.stdout
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def _write_family_status_plan(paths, family, task_id):
|
|
628
|
+
plan_path, key = {
|
|
629
|
+
"feature": (paths.feature_plan, "features"),
|
|
630
|
+
"bugfix": (paths.bugfix_plan, "bugs"),
|
|
631
|
+
"refactor": (paths.refactor_plan, "refactors"),
|
|
632
|
+
}[family]
|
|
633
|
+
plan_path.parent.mkdir(parents=True, exist_ok=True)
|
|
634
|
+
plan_path.write_text(json.dumps({key: [{"id": task_id, "status": "pending"}]}), encoding="utf-8")
|
|
635
|
+
return plan_path
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _write_family_runtime_status(paths, family, task_id):
|
|
639
|
+
state_dir = {
|
|
640
|
+
"feature": paths.feature_state_dir,
|
|
641
|
+
"bugfix": paths.bugfix_state_dir,
|
|
642
|
+
"refactor": paths.refactor_state_dir,
|
|
643
|
+
}[family]
|
|
644
|
+
status_path = state_dir / task_id / "status.json"
|
|
645
|
+
status_path.parent.mkdir(parents=True, exist_ok=True)
|
|
646
|
+
status_path.write_text(
|
|
647
|
+
json.dumps(
|
|
648
|
+
{
|
|
649
|
+
{
|
|
650
|
+
"feature": "feature_id",
|
|
651
|
+
"bugfix": "bug_id",
|
|
652
|
+
"refactor": "refactor_id",
|
|
653
|
+
}[family]: task_id,
|
|
654
|
+
"retry_count": 0,
|
|
655
|
+
"sessions": [],
|
|
656
|
+
"resume_from_phase": 9,
|
|
657
|
+
}
|
|
658
|
+
),
|
|
659
|
+
encoding="utf-8",
|
|
660
|
+
)
|
|
661
|
+
return status_path
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _file_bytes_under(root):
|
|
665
|
+
if not root.exists():
|
|
666
|
+
return {}
|
|
667
|
+
return {
|
|
668
|
+
path.relative_to(root).as_posix(): path.read_bytes()
|
|
669
|
+
for path in root.rglob("*")
|
|
670
|
+
if path.is_file()
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def test_status_all_aggregates_configured_families_in_fixed_order_and_missing_is_nonfatal(
|
|
675
|
+
tmp_path, monkeypatch
|
|
676
|
+
):
|
|
677
|
+
from prizmkit_runtime import runners
|
|
678
|
+
from prizmkit_runtime.commands import CommandResult, handle_runtime_command
|
|
679
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
680
|
+
|
|
681
|
+
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=tmp_path)
|
|
682
|
+
_write_family_status_plan(paths, "feature", "F-001")
|
|
683
|
+
_write_family_status_plan(paths, "refactor", "R-001")
|
|
684
|
+
before = _file_bytes_under(tmp_path)
|
|
685
|
+
calls = []
|
|
686
|
+
|
|
687
|
+
def fake_status(family, action, legacy_args, runtime_paths):
|
|
688
|
+
calls.append((family, action, legacy_args, runtime_paths))
|
|
689
|
+
return CommandResult(0, f"Python {family} status", f"normal {family} status")
|
|
690
|
+
|
|
691
|
+
monkeypatch.setattr(runners, "run_pipeline_command", fake_status)
|
|
692
|
+
result = handle_runtime_command("status", "status", target="all", paths=paths)
|
|
693
|
+
rendered = result.render()
|
|
694
|
+
|
|
695
|
+
assert result.exit_code == 0
|
|
696
|
+
assert rendered.index("FEATURE\n=======") < rendered.index("BUGFIX\n======")
|
|
697
|
+
assert rendered.index("BUGFIX\n======") < rendered.index("REFACTOR\n========")
|
|
698
|
+
assert "normal feature status" in rendered
|
|
699
|
+
assert "normal refactor status" in rendered
|
|
700
|
+
assert "BUGFIX\n======\nnot configured: plan list not found" in rendered
|
|
701
|
+
assert [(family, action, args) for family, action, args, _paths in calls] == [
|
|
702
|
+
("feature", "status", ()),
|
|
703
|
+
("refactor", "status", ()),
|
|
704
|
+
]
|
|
705
|
+
assert _file_bytes_under(tmp_path) == before
|
|
706
|
+
assert not paths.feature_state_dir.exists()
|
|
707
|
+
assert not paths.bugfix_state_dir.exists()
|
|
708
|
+
assert not paths.refactor_state_dir.exists()
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def test_status_all_uses_real_family_status_handlers_without_filesystem_mutation(tmp_path):
|
|
712
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
713
|
+
|
|
714
|
+
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=tmp_path)
|
|
715
|
+
configured = {
|
|
716
|
+
"feature": "F-001",
|
|
717
|
+
"bugfix": "B-001",
|
|
718
|
+
"refactor": "R-001",
|
|
719
|
+
}
|
|
720
|
+
for family, task_id in configured.items():
|
|
721
|
+
_write_family_status_plan(paths, family, task_id)
|
|
722
|
+
_write_family_runtime_status(paths, family, task_id)
|
|
723
|
+
before = _file_bytes_under(tmp_path)
|
|
724
|
+
|
|
725
|
+
result = run_cli("--project-root", str(tmp_path), "status", "all")
|
|
726
|
+
|
|
727
|
+
assert result.returncode == 0, result.stderr
|
|
728
|
+
assert "FEATURE\n=======" in result.stdout
|
|
729
|
+
assert "Python feature status" in result.stdout
|
|
730
|
+
assert "BUGFIX\n======" in result.stdout
|
|
731
|
+
assert "Python bugfix status" in result.stdout
|
|
732
|
+
assert "REFACTOR\n========" in result.stdout
|
|
733
|
+
assert "Python refactor status" in result.stdout
|
|
734
|
+
assert _file_bytes_under(tmp_path) == before
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def test_status_all_reports_plan_and_handler_errors_but_renders_unaffected_sections(
|
|
738
|
+
tmp_path, monkeypatch
|
|
739
|
+
):
|
|
740
|
+
from prizmkit_runtime import runners
|
|
741
|
+
from prizmkit_runtime.commands import CommandResult, handle_runtime_command
|
|
742
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
743
|
+
|
|
744
|
+
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=tmp_path)
|
|
745
|
+
_write_family_status_plan(paths, "feature", "F-001")
|
|
746
|
+
paths.bugfix_plan.parent.mkdir(parents=True, exist_ok=True)
|
|
747
|
+
paths.bugfix_plan.write_text("{malformed", encoding="utf-8")
|
|
748
|
+
_write_family_status_plan(paths, "refactor", "R-001")
|
|
749
|
+
|
|
750
|
+
def fake_status(family, action, legacy_args, runtime_paths):
|
|
751
|
+
assert action == "status"
|
|
752
|
+
if family == "refactor":
|
|
753
|
+
raise RuntimeError("family handler exploded")
|
|
754
|
+
return CommandResult(0, f"Python {family} status", f"normal {family} status")
|
|
755
|
+
|
|
756
|
+
monkeypatch.setattr(runners, "run_pipeline_command", fake_status)
|
|
757
|
+
result = handle_runtime_command("status", "status", target="all", paths=paths)
|
|
758
|
+
rendered = result.render()
|
|
759
|
+
|
|
760
|
+
assert result.exit_code != 0
|
|
761
|
+
assert "FEATURE\n=======\nPython feature status\nnormal feature status" in rendered
|
|
762
|
+
assert "BUGFIX\n======\nerror: malformed plan list:" in rendered
|
|
763
|
+
assert "REFACTOR\n========\nerror: family handler exploded" in rendered
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def test_status_all_reports_malformed_runtime_unparseable_and_nonzero_results(
|
|
767
|
+
tmp_path, monkeypatch
|
|
768
|
+
):
|
|
769
|
+
from prizmkit_runtime import runners
|
|
770
|
+
from prizmkit_runtime.commands import CommandResult, handle_runtime_command
|
|
771
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
772
|
+
|
|
773
|
+
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=tmp_path)
|
|
774
|
+
_write_family_status_plan(paths, "feature", "F-001")
|
|
775
|
+
_write_family_status_plan(paths, "bugfix", "B-001")
|
|
776
|
+
_write_family_status_plan(paths, "refactor", "R-001")
|
|
777
|
+
malformed_state = paths.feature_state_dir / "F-001" / "status.json"
|
|
778
|
+
malformed_state.parent.mkdir(parents=True, exist_ok=True)
|
|
779
|
+
malformed_state.write_text("not-json", encoding="utf-8")
|
|
780
|
+
|
|
781
|
+
def fake_status(family, action, legacy_args, runtime_paths):
|
|
782
|
+
if family == "bugfix":
|
|
783
|
+
return object()
|
|
784
|
+
return CommandResult(7 if family == "refactor" else 0, f"Python {family} status", "status output")
|
|
785
|
+
|
|
786
|
+
monkeypatch.setattr(runners, "run_pipeline_command", fake_status)
|
|
787
|
+
result = handle_runtime_command("status", "status", target="all", paths=paths)
|
|
788
|
+
rendered = result.render()
|
|
789
|
+
|
|
790
|
+
assert result.exit_code != 0
|
|
791
|
+
assert "FEATURE\n=======\nerror: malformed runtime state F-001/status.json" in rendered
|
|
792
|
+
assert "BUGFIX\n======\nerror: unparseable configured family result" in rendered
|
|
793
|
+
assert "REFACTOR\n========\nerror: Python refactor status\nstatus output" in rendered
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def test_status_all_direct_api_rejects_family_specific_arguments_without_calling_handlers(
|
|
797
|
+
tmp_path, monkeypatch
|
|
798
|
+
):
|
|
799
|
+
from prizmkit_runtime import runners
|
|
800
|
+
from prizmkit_runtime.commands import handle_runtime_command
|
|
801
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
802
|
+
|
|
803
|
+
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=tmp_path)
|
|
804
|
+
called = []
|
|
805
|
+
monkeypatch.setattr(runners, "run_pipeline_command", lambda *args: called.append(args))
|
|
806
|
+
|
|
807
|
+
result = handle_runtime_command(
|
|
808
|
+
"status", "status", target="all", paths=paths, legacy_args=("custom.json",)
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
assert result.exit_code == 2
|
|
812
|
+
assert "does not accept a plan path" in result.render()
|
|
813
|
+
assert called == []
|
|
449
814
|
|
|
450
815
|
|
|
451
816
|
def test_interoperability_preserves_existing_utility_mapping():
|
|
@@ -481,9 +846,55 @@ def test_interoperability_preserves_existing_utility_mapping():
|
|
|
481
846
|
assert actual_scripts == mapped_scripts | explicitly_library_only
|
|
482
847
|
assert explicitly_library_only == {"continuation.py", "prompt_framework.py", "utils.py"}
|
|
483
848
|
assert "parse-stream-progress.py" in mapped_scripts
|
|
849
|
+
assert "generate-recovery-prompt" not in UTILITY_ENTRYPOINTS
|
|
484
850
|
assert "prizmkit-test-gate.py" not in mapped_scripts
|
|
485
851
|
|
|
486
852
|
|
|
853
|
+
def test_obsolete_runtime_recovery_ai_session_sources_are_absent():
|
|
854
|
+
assert not (PIPELINE_ROOT / "prizmkit_runtime" / "runner_recovery.py").exists()
|
|
855
|
+
assert not (PIPELINE_ROOT / "scripts" / "generate-recovery-prompt.py").exists()
|
|
856
|
+
|
|
857
|
+
forbidden_by_source = {
|
|
858
|
+
"prizmkit_runtime/cli.py": ('"recovery"',),
|
|
859
|
+
"prizmkit_runtime/commands.py": ('"recovery": ("run"', 'group == "recovery"'),
|
|
860
|
+
"prizmkit_runtime/runners.py": ("runner_recovery", "run_recovery_command"),
|
|
861
|
+
"prizmkit_runtime/paths.py": ("recovery_state_dir",),
|
|
862
|
+
"prizmkit_runtime/interoperability.py": ("generate-recovery-prompt",),
|
|
863
|
+
}
|
|
864
|
+
for relative_path, forbidden_tokens in forbidden_by_source.items():
|
|
865
|
+
content = (PIPELINE_ROOT / relative_path).read_text(encoding="utf-8")
|
|
866
|
+
for token in forbidden_tokens:
|
|
867
|
+
assert token not in content, f"{relative_path} still contains {token}"
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
@pytest.mark.parametrize(
|
|
871
|
+
("path", "ordinary_command"),
|
|
872
|
+
[
|
|
873
|
+
(REPO_ROOT / "README.md", "python3 ./.prizmkit/dev-pipeline/cli.py run feature F-003"),
|
|
874
|
+
(
|
|
875
|
+
REPO_ROOT / "core" / "templates" / "project-memory-template.md",
|
|
876
|
+
"python3 ./.prizmkit/dev-pipeline/cli.py run <family> <task-id>",
|
|
877
|
+
),
|
|
878
|
+
(
|
|
879
|
+
REPO_ROOT
|
|
880
|
+
/ "core"
|
|
881
|
+
/ "skills"
|
|
882
|
+
/ "orchestration-skill"
|
|
883
|
+
/ "workflows"
|
|
884
|
+
/ "recovery-workflow"
|
|
885
|
+
/ "SKILL.md",
|
|
886
|
+
"python3 ./.prizmkit/dev-pipeline/cli.py run feature F-001",
|
|
887
|
+
),
|
|
888
|
+
],
|
|
889
|
+
)
|
|
890
|
+
def test_canonical_interrupted_task_guidance_uses_recovery_workflow(path, ordinary_command):
|
|
891
|
+
content = path.read_text(encoding="utf-8")
|
|
892
|
+
|
|
893
|
+
assert "recovery-workflow" in content
|
|
894
|
+
assert ordinary_command in content
|
|
895
|
+
assert "cli.py recovery" not in content
|
|
896
|
+
|
|
897
|
+
|
|
487
898
|
def test_boundary_dataclasses_are_cohesive_and_frozen():
|
|
488
899
|
from prizmkit_runtime.gitops import BranchContext, WorktreePolicy
|
|
489
900
|
from prizmkit_runtime.heartbeat import HeartbeatConfig, HeartbeatState
|
|
@@ -876,7 +1287,7 @@ def test_session_preamble_writes_redacted_start_command_for_all_launch_profiles(
|
|
|
876
1287
|
assert str(prompt) not in "\n".join(lines[:6])
|
|
877
1288
|
|
|
878
1289
|
|
|
879
|
-
def
|
|
1290
|
+
def test_headless_review_guidance_maps_stage_local_skill_result():
|
|
880
1291
|
headless_guidance_files = [
|
|
881
1292
|
REPO_ROOT / "dev-pipeline" / "templates" / "sections" / "phase-review-full.md",
|
|
882
1293
|
REPO_ROOT / "dev-pipeline" / "templates" / "bootstrap-tier3.md",
|
|
@@ -892,9 +1303,10 @@ def test_headless_review_guidance_defers_to_skill_review_contract():
|
|
|
892
1303
|
|
|
893
1304
|
for path in headless_guidance_files:
|
|
894
1305
|
text = path.read_text(encoding="utf-8")
|
|
895
|
-
assert "
|
|
896
|
-
assert "
|
|
1306
|
+
assert "returns only `PASS` or `NEEDS_FIXES`" in text, path
|
|
1307
|
+
assert "this injected session" in text, path
|
|
897
1308
|
assert "text final-verdict" in text, path
|
|
1309
|
+
assert "follow the skill's current review contract" not in text, path
|
|
898
1310
|
assert "low=0" not in text and "Reviewer 3" not in text, path
|
|
899
1311
|
assert "forced-worktree changed-content fallback" not in text, path
|
|
900
1312
|
assert "isolation:" not in text, path
|
|
@@ -2198,97 +2610,6 @@ def test_compact_live_heartbeat_stays_out_of_primary_and_backup_logs(tmp_path, c
|
|
|
2198
2610
|
assert "stale:" not in backup_text.get("content", "")
|
|
2199
2611
|
|
|
2200
2612
|
|
|
2201
|
-
def test_recovery_sessions_receive_live_heartbeat_configuration(tmp_path, monkeypatch):
|
|
2202
|
-
import prizmkit_runtime.runner_recovery as runner_recovery
|
|
2203
|
-
|
|
2204
|
-
project = tmp_path / "project"
|
|
2205
|
-
pipeline = tmp_path / "pipeline"
|
|
2206
|
-
recovery_state = tmp_path / "recovery"
|
|
2207
|
-
project.mkdir()
|
|
2208
|
-
pipeline.mkdir()
|
|
2209
|
-
recovery_state.mkdir()
|
|
2210
|
-
paths = type(
|
|
2211
|
-
"Paths",
|
|
2212
|
-
(),
|
|
2213
|
-
{"project_root": project, "pipeline_root": pipeline, "recovery_state_dir": recovery_state},
|
|
2214
|
-
)()
|
|
2215
|
-
captured_config = {}
|
|
2216
|
-
|
|
2217
|
-
monkeypatch.setattr(
|
|
2218
|
-
runner_recovery,
|
|
2219
|
-
"run_recovery_detect",
|
|
2220
|
-
lambda _paths: (0, "{}", "", {"detected": True, "workflow_type": "feature"}),
|
|
2221
|
-
)
|
|
2222
|
-
monkeypatch.setattr(
|
|
2223
|
-
runner_recovery,
|
|
2224
|
-
"load_runtime_config",
|
|
2225
|
-
lambda _paths: type(
|
|
2226
|
-
"Config",
|
|
2227
|
-
(),
|
|
2228
|
-
{
|
|
2229
|
-
"ai_client": type("Client", (), {"command": "fake-cli", "platform": "codebuddy"})(),
|
|
2230
|
-
"model": "model",
|
|
2231
|
-
"effort": "high",
|
|
2232
|
-
},
|
|
2233
|
-
)(),
|
|
2234
|
-
)
|
|
2235
|
-
monkeypatch.setattr(
|
|
2236
|
-
runner_recovery,
|
|
2237
|
-
"RunnerEnvironment",
|
|
2238
|
-
type(
|
|
2239
|
-
"EnvFactory",
|
|
2240
|
-
(),
|
|
2241
|
-
{
|
|
2242
|
-
"from_env": staticmethod(
|
|
2243
|
-
lambda: type(
|
|
2244
|
-
"Env",
|
|
2245
|
-
(),
|
|
2246
|
-
{
|
|
2247
|
-
"verbose": True,
|
|
2248
|
-
"live_output": True,
|
|
2249
|
-
"heartbeat_interval_seconds": 7,
|
|
2250
|
-
"heartbeat_stale_threshold_seconds": 21,
|
|
2251
|
-
"stale_kill_threshold_seconds": 42,
|
|
2252
|
-
},
|
|
2253
|
-
)()
|
|
2254
|
-
)
|
|
2255
|
-
},
|
|
2256
|
-
),
|
|
2257
|
-
)
|
|
2258
|
-
monkeypatch.setattr(runner_recovery, "detect_stream_json_support", lambda *_args: type("Support", (), {"enabled": True})())
|
|
2259
|
-
monkeypatch.setattr(
|
|
2260
|
-
runner_recovery.subprocess,
|
|
2261
|
-
"run",
|
|
2262
|
-
lambda *_args, **_kwargs: type("Completed", (), {"returncode": 0, "stdout": "0", "stderr": ""})(),
|
|
2263
|
-
)
|
|
2264
|
-
monkeypatch.setattr(runner_recovery, "_recovery_outcome", lambda *_args, **_kwargs: ("no_changes", {}))
|
|
2265
|
-
|
|
2266
|
-
class FakeLauncher:
|
|
2267
|
-
def __init__(self, config):
|
|
2268
|
-
captured_config["config"] = config
|
|
2269
|
-
|
|
2270
|
-
def run(self):
|
|
2271
|
-
return type("Result", (), {"exit_code": 0})()
|
|
2272
|
-
|
|
2273
|
-
monkeypatch.setattr(runner_recovery, "AISessionLauncher", FakeLauncher)
|
|
2274
|
-
|
|
2275
|
-
code, stdout, stderr = runner_recovery.run_recovery(paths)
|
|
2276
|
-
config = captured_config["config"]
|
|
2277
|
-
|
|
2278
|
-
assert code == 0
|
|
2279
|
-
assert stdout == "no_changes\n"
|
|
2280
|
-
assert stderr == ""
|
|
2281
|
-
assert config.live_output is True
|
|
2282
|
-
assert config.verbose is True
|
|
2283
|
-
assert config.use_stream_json is True
|
|
2284
|
-
assert config.live_progress_interval_seconds == 7
|
|
2285
|
-
assert config.heartbeat_stale_threshold_seconds == 21
|
|
2286
|
-
assert config.stale_kill_threshold_seconds == 42
|
|
2287
|
-
assert config.live_banner is False
|
|
2288
|
-
assert config.suppress_stream_output is True
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
2613
|
def test_session_summary_reports_normalized_subagents_and_child_liveness(tmp_path, capsys):
|
|
2293
2614
|
import prizmkit_runtime.runners as runners
|
|
2294
2615
|
from prizmkit_runtime.runner_models import SessionPaths
|
|
@@ -2773,6 +3094,60 @@ def test_branch_merge_preserves_preexisting_stash_when_workspace_is_clean(tmp_pa
|
|
|
2773
3094
|
assert (tmp_path / "feature.txt").read_text(encoding="utf-8") == "done\n"
|
|
2774
3095
|
|
|
2775
3096
|
|
|
3097
|
+
def test_branch_merge_fast_forwards_with_dirty_prizmkit_internal_state(tmp_path):
|
|
3098
|
+
from prizmkit_runtime.gitops import (
|
|
3099
|
+
BranchContext,
|
|
3100
|
+
branch_fast_forward_possible,
|
|
3101
|
+
branch_merge_plan,
|
|
3102
|
+
run_git_plan,
|
|
3103
|
+
)
|
|
3104
|
+
|
|
3105
|
+
_init_merge_repo(tmp_path)
|
|
3106
|
+
context = BranchContext("main", "dev/example", tmp_path)
|
|
3107
|
+
subprocess.run(
|
|
3108
|
+
["git", "checkout", "-b", context.working_branch],
|
|
3109
|
+
cwd=tmp_path,
|
|
3110
|
+
check=True,
|
|
3111
|
+
stdout=subprocess.DEVNULL,
|
|
3112
|
+
)
|
|
3113
|
+
(tmp_path / "feature.txt").write_text("done\n", encoding="utf-8")
|
|
3114
|
+
subprocess.run(["git", "add", "feature.txt"], cwd=tmp_path, check=True)
|
|
3115
|
+
subprocess.run(
|
|
3116
|
+
["git", "commit", "-m", "feature"],
|
|
3117
|
+
cwd=tmp_path,
|
|
3118
|
+
check=True,
|
|
3119
|
+
stdout=subprocess.DEVNULL,
|
|
3120
|
+
)
|
|
3121
|
+
manifest = tmp_path / ".prizmkit" / "manifest.json"
|
|
3122
|
+
manifest.write_text('{"version":"session-local"}\n', encoding="utf-8")
|
|
3123
|
+
|
|
3124
|
+
assert branch_fast_forward_possible(tmp_path, context) is True
|
|
3125
|
+
result = run_git_plan(
|
|
3126
|
+
tmp_path,
|
|
3127
|
+
branch_merge_plan(context, rebase=False),
|
|
3128
|
+
)
|
|
3129
|
+
|
|
3130
|
+
assert result.ok is True
|
|
3131
|
+
assert not any(command.args and command.args[0] == "rebase" for command in result.plan.commands)
|
|
3132
|
+
assert subprocess.run(
|
|
3133
|
+
["git", "branch", "--show-current"],
|
|
3134
|
+
cwd=tmp_path,
|
|
3135
|
+
check=True,
|
|
3136
|
+
stdout=subprocess.PIPE,
|
|
3137
|
+
text=True,
|
|
3138
|
+
).stdout.strip() == "main"
|
|
3139
|
+
assert (tmp_path / "feature.txt").read_text(encoding="utf-8") == "done\n"
|
|
3140
|
+
assert json.loads(manifest.read_text(encoding="utf-8"))["version"] == "session-local"
|
|
3141
|
+
assert ".prizmkit/manifest.json" in subprocess.run(
|
|
3142
|
+
["git", "status", "--short"],
|
|
3143
|
+
cwd=tmp_path,
|
|
3144
|
+
check=True,
|
|
3145
|
+
stdout=subprocess.PIPE,
|
|
3146
|
+
text=True,
|
|
3147
|
+
).stdout
|
|
3148
|
+
assert _stash_oids(tmp_path) == ()
|
|
3149
|
+
|
|
3150
|
+
|
|
2776
3151
|
def test_branch_merge_rejects_dirty_task_checkout_without_touching_stashes(tmp_path):
|
|
2777
3152
|
from prizmkit_runtime.gitops import BranchContext, branch_merge_plan, run_git_plan
|
|
2778
3153
|
|
|
@@ -3507,6 +3882,60 @@ def test_materialize_worktree_support_assets_copies_ignored_assets_without_retir
|
|
|
3507
3882
|
assert guarded_worktree_remove(runtime, delete_branch=False).ok
|
|
3508
3883
|
|
|
3509
3884
|
|
|
3885
|
+
def test_linked_worktree_fast_forwards_with_dirty_prizmkit_internal_state(tmp_path):
|
|
3886
|
+
from prizmkit_runtime.gitops import (
|
|
3887
|
+
BranchContext,
|
|
3888
|
+
WorktreePolicy,
|
|
3889
|
+
branch_fast_forward_possible,
|
|
3890
|
+
ensure_linked_worktree,
|
|
3891
|
+
guarded_worktree_remove,
|
|
3892
|
+
merge_linked_worktree,
|
|
3893
|
+
worktree_runtime_context,
|
|
3894
|
+
)
|
|
3895
|
+
|
|
3896
|
+
repo = tmp_path / "repo"
|
|
3897
|
+
_init_temp_repo(repo)
|
|
3898
|
+
config = repo / ".prizmkit" / "config.json"
|
|
3899
|
+
config.parent.mkdir(parents=True)
|
|
3900
|
+
config.write_text('{"mode":"base"}\n', encoding="utf-8")
|
|
3901
|
+
subprocess.run(["git", "add", ".prizmkit/config.json"], cwd=repo, check=True)
|
|
3902
|
+
subprocess.run(
|
|
3903
|
+
["git", "commit", "-m", "track internal config"],
|
|
3904
|
+
cwd=repo,
|
|
3905
|
+
check=True,
|
|
3906
|
+
stdout=subprocess.DEVNULL,
|
|
3907
|
+
)
|
|
3908
|
+
runtime = worktree_runtime_context(
|
|
3909
|
+
BranchContext("main", "dev/internal-dirt", repo),
|
|
3910
|
+
WorktreePolicy(
|
|
3911
|
+
True,
|
|
3912
|
+
cleanup_on_success=True,
|
|
3913
|
+
preserve_on_failure=True,
|
|
3914
|
+
worktree_root=tmp_path / "worktrees",
|
|
3915
|
+
),
|
|
3916
|
+
)
|
|
3917
|
+
assert ensure_linked_worktree(runtime).ok
|
|
3918
|
+
(runtime.worktree_path / "feature.txt").write_text("done\n", encoding="utf-8")
|
|
3919
|
+
subprocess.run(["git", "add", "feature.txt"], cwd=runtime.worktree_path, check=True)
|
|
3920
|
+
subprocess.run(
|
|
3921
|
+
["git", "commit", "-m", "feature"],
|
|
3922
|
+
cwd=runtime.worktree_path,
|
|
3923
|
+
check=True,
|
|
3924
|
+
stdout=subprocess.DEVNULL,
|
|
3925
|
+
)
|
|
3926
|
+
worktree_config = runtime.worktree_path / ".prizmkit" / "config.json"
|
|
3927
|
+
worktree_config.write_text('{"mode":"session-local"}\n', encoding="utf-8")
|
|
3928
|
+
|
|
3929
|
+
assert branch_fast_forward_possible(repo, runtime.context) is True
|
|
3930
|
+
result = merge_linked_worktree(runtime)
|
|
3931
|
+
|
|
3932
|
+
assert result.ok is True
|
|
3933
|
+
assert not any(item.args and item.args[0] == "rebase" for item in result.results)
|
|
3934
|
+
assert (repo / "feature.txt").read_text(encoding="utf-8") == "done\n"
|
|
3935
|
+
assert json.loads(worktree_config.read_text(encoding="utf-8"))["mode"] == "session-local"
|
|
3936
|
+
assert guarded_worktree_remove(runtime, delete_branch=True).ok
|
|
3937
|
+
|
|
3938
|
+
|
|
3510
3939
|
def test_worktree_terminal_failure_wip_commit_preserves_dirty_changes_before_cleanup(tmp_path):
|
|
3511
3940
|
from prizmkit_runtime.gitops import (
|
|
3512
3941
|
BranchContext,
|
|
@@ -3588,9 +4017,9 @@ def test_checkpoint_rewind_preserves_planning_and_resets_implementation_downstre
|
|
|
3588
4017
|
"prizmkit-implement",
|
|
3589
4018
|
"prizmkit-code-review",
|
|
3590
4019
|
"prizmkit-test",
|
|
4020
|
+
"completion-summary",
|
|
3591
4021
|
"prizmkit-retrospective",
|
|
3592
4022
|
"prizmkit-committer",
|
|
3593
|
-
"completion-summary",
|
|
3594
4023
|
]
|
|
3595
4024
|
steps = []
|
|
3596
4025
|
previous = None
|
|
@@ -3622,7 +4051,7 @@ def test_checkpoint_rewind_preserves_planning_and_resets_implementation_downstre
|
|
|
3622
4051
|
"feature_state": {
|
|
3623
4052
|
"schema_version": 1,
|
|
3624
4053
|
"artifact_dir": ".prizmkit/specs/example",
|
|
3625
|
-
"orchestrator": "
|
|
4054
|
+
"orchestrator": "pipeline-runtime",
|
|
3626
4055
|
"stage": "code-review",
|
|
3627
4056
|
"current_stage": "prizmkit-code-review",
|
|
3628
4057
|
"status": "failed",
|