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

|
||||
<p align="center">
|
||||
<img src="docs/images/logo.png" width="220">
|
||||
</p>
|
||||
|
||||
|
||||
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:
|
||||
backend: command
|
||||
command: echo
|
||||
command: python agents/fake_code_writer.py
|
||||
system_prompt: agents/implementer.md
|
||||
|
||||
reviewer:
|
||||
|
|
@ -45,9 +45,24 @@ pipeline:
|
|||
output: plan.md
|
||||
|
||||
- id: implement
|
||||
type: agent
|
||||
type: code_writer
|
||||
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
|
||||
type: command
|
||||
|
|
@ -56,6 +71,7 @@ pipeline:
|
|||
output: test-output.txt
|
||||
shell: true
|
||||
timeout_seconds: 60
|
||||
on_fail: implement
|
||||
|
||||
- id: review
|
||||
type: agent_review
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from pathlib import Path
|
||||
from dataclasses import replace
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
|
@ -404,6 +405,114 @@ Acceptance Criteria:
|
|||
self.assertEqual(result.status, "failed")
|
||||
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:
|
||||
(root / "nightshift.yaml").write_text("project:\n name: test\n", encoding="utf-8")
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user