mirror of
https://github.com/khodges42/nightShift.git
synced 2026-06-14 18:18:36 +00:00
Examples, readme logo fix
This commit is contained in:
parent
12e2c99a75
commit
4e502ba494
|
|
@ -1,6 +1,9 @@
|
||||||
# NightShift
|
# NightShift
|
||||||
|
|
||||||

|
<p align="center">
|
||||||
|
<img src="docs/images/logo.png" width="220">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
Auditable local-first AI coding pipelines.
|
Auditable local-first AI coding pipelines.
|
||||||
|
|
||||||
|
|
|
||||||
95
examples/quickstart-lisp/agents/fake_code_writer.py
Normal file
95
examples/quickstart-lisp/agents/fake_code_writer.py
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
"""Fake code writer for the NightShift end-to-end quickstart."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import difflib
|
||||||
|
|
||||||
|
|
||||||
|
FILES = {
|
||||||
|
"lisp.py": '''"""Tiny Lisp parser used by the NightShift quickstart."""
|
||||||
|
|
||||||
|
|
||||||
|
def tokenize(source):
|
||||||
|
spaced = source.replace("(", " ( ").replace(")", " ) ")
|
||||||
|
return spaced.split()
|
||||||
|
|
||||||
|
|
||||||
|
def parse(source):
|
||||||
|
tokens = tokenize(source)
|
||||||
|
if not tokens:
|
||||||
|
raise ValueError("empty expression")
|
||||||
|
expression = _parse_tokens(tokens)
|
||||||
|
if tokens:
|
||||||
|
raise ValueError("unexpected trailing tokens")
|
||||||
|
return expression
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_tokens(tokens):
|
||||||
|
if not tokens:
|
||||||
|
raise ValueError("unexpected end of input")
|
||||||
|
token = tokens.pop(0)
|
||||||
|
if token == "(":
|
||||||
|
values = []
|
||||||
|
while tokens and tokens[0] != ")":
|
||||||
|
values.append(_parse_tokens(tokens))
|
||||||
|
if not tokens:
|
||||||
|
raise ValueError("unbalanced parentheses")
|
||||||
|
tokens.pop(0)
|
||||||
|
return values
|
||||||
|
if token == ")":
|
||||||
|
raise ValueError("unexpected closing parenthesis")
|
||||||
|
return _atom(token)
|
||||||
|
|
||||||
|
|
||||||
|
def _atom(token):
|
||||||
|
try:
|
||||||
|
return int(token)
|
||||||
|
except ValueError:
|
||||||
|
return token
|
||||||
|
''',
|
||||||
|
"tests/test_lisp.py": """import unittest
|
||||||
|
|
||||||
|
from lisp import parse
|
||||||
|
|
||||||
|
|
||||||
|
class ParserTests(unittest.TestCase):
|
||||||
|
def test_parses_numbers(self):
|
||||||
|
self.assertEqual(parse("42"), 42)
|
||||||
|
|
||||||
|
def test_parses_symbols(self):
|
||||||
|
self.assertEqual(parse("answer"), "answer")
|
||||||
|
|
||||||
|
def test_parses_nested_lists(self):
|
||||||
|
self.assertEqual(parse("(+ 1 (* 2 3))"), ["+", 1, ["*", 2, 3]])
|
||||||
|
|
||||||
|
def test_rejects_unbalanced_parentheses(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
parse("(+ 1 2")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
chunks: list[str] = []
|
||||||
|
for relative_path, desired in FILES.items():
|
||||||
|
path = Path(relative_path)
|
||||||
|
current = path.read_text(encoding="utf-8") if path.exists() else ""
|
||||||
|
chunks.append(f"diff --git a/{relative_path} b/{relative_path}\n")
|
||||||
|
chunks.extend(
|
||||||
|
difflib.unified_diff(
|
||||||
|
current.splitlines(keepends=True),
|
||||||
|
desired.splitlines(keepends=True),
|
||||||
|
fromfile=f"a/{relative_path}",
|
||||||
|
tofile=f"b/{relative_path}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print("".join(chunks), end="")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -27,7 +27,7 @@ agents:
|
||||||
|
|
||||||
implementer:
|
implementer:
|
||||||
backend: command
|
backend: command
|
||||||
command: echo
|
command: python agents/fake_code_writer.py
|
||||||
system_prompt: agents/implementer.md
|
system_prompt: agents/implementer.md
|
||||||
|
|
||||||
reviewer:
|
reviewer:
|
||||||
|
|
@ -45,9 +45,24 @@ pipeline:
|
||||||
output: plan.md
|
output: plan.md
|
||||||
|
|
||||||
- id: implement
|
- id: implement
|
||||||
type: agent
|
type: code_writer
|
||||||
agent: implementer
|
agent: implementer
|
||||||
output: implementation-log.md
|
output: proposed.patch
|
||||||
|
|
||||||
|
- id: normalize
|
||||||
|
type: patch_normalizer
|
||||||
|
output: normalized.patch
|
||||||
|
|
||||||
|
- id: validate_patch
|
||||||
|
type: patch_validator
|
||||||
|
output: patch-validation.md
|
||||||
|
max_files: 4
|
||||||
|
max_lines: 400
|
||||||
|
|
||||||
|
- id: apply_patch
|
||||||
|
type: patch_apply
|
||||||
|
mode: apply
|
||||||
|
output: patch-apply-output.txt
|
||||||
|
|
||||||
- id: test
|
- id: test
|
||||||
type: command
|
type: command
|
||||||
|
|
@ -56,6 +71,7 @@ pipeline:
|
||||||
output: test-output.txt
|
output: test-output.txt
|
||||||
shell: true
|
shell: true
|
||||||
timeout_seconds: 60
|
timeout_seconds: 60
|
||||||
|
on_fail: implement
|
||||||
|
|
||||||
- id: review
|
- id: review
|
||||||
type: agent_review
|
type: agent_review
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from dataclasses import replace
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
|
@ -404,6 +405,114 @@ Acceptance Criteria:
|
||||||
self.assertEqual(result.status, "failed")
|
self.assertEqual(result.status, "failed")
|
||||||
self.assertIn("forbidden path", result.reason)
|
self.assertIn("forbidden path", result.reason)
|
||||||
|
|
||||||
|
def test_patch_apply_stage_applies_patch(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(
|
||||||
|
[
|
||||||
|
"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"),
|
||||||
|
StageConfig(id="apply", type="patch_apply", mode="apply"),
|
||||||
|
)
|
||||||
|
config = make_config(root, stages)
|
||||||
|
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((root / "app.py").read_text(encoding="utf-8"), "new\n")
|
||||||
|
self.assertTrue((task_dir / "applied.patch").exists())
|
||||||
|
self.assertTrue((task_dir / "patch-apply-output.txt").exists())
|
||||||
|
self.assertTrue((task_dir / "git-status-before-patch-apply.txt").exists())
|
||||||
|
self.assertTrue((task_dir / "git-status-after-patch-apply.txt").exists())
|
||||||
|
|
||||||
|
def test_test_failure_repairs_with_second_patch(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(
|
||||||
|
[
|
||||||
|
"from pathlib import Path",
|
||||||
|
"current = Path('app.py').read_text()",
|
||||||
|
"old, new = ('bad', 'new') if current == 'bad\\n' else ('old', 'bad')",
|
||||||
|
"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"),
|
||||||
|
StageConfig(id="apply", type="patch_apply", mode="apply"),
|
||||||
|
StageConfig(
|
||||||
|
id="test",
|
||||||
|
type="command",
|
||||||
|
commands=('python -c "from pathlib import Path; raise SystemExit(0 if Path(\'app.py\').read_text() == \'new\\\\n\' else 1)"',),
|
||||||
|
output="test-output.txt",
|
||||||
|
on_fail="write",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
config = make_config(
|
||||||
|
root,
|
||||||
|
stages,
|
||||||
|
max_retries=1,
|
||||||
|
)
|
||||||
|
config = replace(
|
||||||
|
config,
|
||||||
|
safety=SafetyConfig(
|
||||||
|
require_clean_worktree=False,
|
||||||
|
scoped_paths=(".",),
|
||||||
|
allowed_commands=('python -c "from pathlib import Path; raise SystemExit(0 if Path(\'app.py\').read_text() == \'new\\\\n\' else 1)"',),
|
||||||
|
forbidden_commands=("rm -rf",),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
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.assertEqual((root / "app.py").read_text(encoding="utf-8"), "new\n")
|
||||||
|
self.assertTrue((task_dir / "repair-1.patch").exists())
|
||||||
|
self.assertTrue((task_dir / "repair-summary-1.md").exists())
|
||||||
|
|
||||||
|
|
||||||
def _write_common_files(root: Path) -> None:
|
def _write_common_files(root: Path) -> None:
|
||||||
(root / "nightshift.yaml").write_text("project:\n name: test\n", encoding="utf-8")
|
(root / "nightshift.yaml").write_text("project:\n name: test\n", encoding="utf-8")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user