The bug was that validate_patch had no on_fail, so NightShift stopped instead of sending that feedback back to implement.

This commit is contained in:
K. Hodges 2026-05-17 13:56:36 -07:00
parent 809ec92e0e
commit e079c9088d
6 changed files with 58 additions and 0 deletions

View File

@ -172,6 +172,7 @@ pipeline:
- id: validate_patch
type: patch_validator
output: patch-validation.md
on_fail: implement
- id: apply_patch
type: patch_apply

View File

@ -156,6 +156,7 @@ pipeline:
output: patch-validation.md
max_files: 8
max_lines: 800
on_fail: implement
- id: apply_patch
type: patch_apply

View File

@ -251,6 +251,7 @@ Patch validator:
output: patch-validation.md
max_files: 4
max_lines: 400
on_fail: implement
forbidden_paths:
- .git
- .nightshift

View File

@ -62,6 +62,7 @@ pipeline:
output: patch-validation.md
max_files: 4
max_lines: 400
on_fail: implement
- id: apply_patch
type: patch_apply

View File

@ -468,6 +468,7 @@ def output_contract_for(stage: StageConfig) -> str:
"Return a unified diff only, suitable for saving as proposed.patch.",
"Do not include prose outside the patch.",
"Use diff --git headers and hunk headers.",
"For existing files, do not use new file mode or /dev/null headers.",
]
)
if stage.type == "patch_normalizer":

View File

@ -454,6 +454,59 @@ Acceptance Criteria:
self.assertEqual(result.status, "failed")
self.assertIn("forbidden path", result.reason)
def test_patch_validation_failure_can_retry_implementation(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
_write_common_files(root)
(root / "app.py").write_text("old\n", encoding="utf-8")
(root / "fake_writer.py").write_text(
"\n".join(
[
"import sys",
"prompt = sys.stdin.read()",
"new_file_patch = 'Retry 1:' not in prompt",
"if new_file_patch:",
" print('diff --git a/app.py b/app.py')",
" print('new file mode 100644')",
" print('--- /dev/null')",
" print('+++ b/app.py')",
" print('@@ -0,0 +1 @@')",
" print('+bad')",
"else:",
" print('diff --git a/app.py b/app.py')",
" print('--- a/app.py')",
" print('+++ b/app.py')",
" print('@@ -1 +1 @@')",
" print('-old')",
" print('+new')",
]
),
encoding="utf-8",
)
stages = (
StageConfig(id="write", type="code_writer", agent="writer"),
StageConfig(id="normalize", type="patch_normalizer"),
StageConfig(id="validate", type="patch_validator", on_fail="write"),
)
config = make_config(root, stages, max_retries=1)
config.agents["writer"] = AgentConfig(
id="writer",
backend="command",
command="python fake_writer.py",
system_prompt=Path("planner.md"),
)
runner = PipelineRunner(config, ArtifactStore(root, ".nightshift", run_id="test-run"))
result = runner.run_task(parse_tasks(TASK_MD)[0])
task_dir = root / ".nightshift" / "runs" / "test-run" / "tasks" / "TASK-001"
self.assertEqual(result.status, "complete")
self.assertEqual(result.retry_count, 1)
self.assertTrue(
any("creates existing file" in stage.reason for stage in result.stage_results)
)
self.assertTrue((task_dir / "repair-1.patch").exists())
def test_patch_apply_stage_applies_patch(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)