Examples, readme logo fix

This commit is contained in:
K. Hodges 2026-05-17 10:06:18 -07:00
parent 12e2c99a75
commit 4e502ba494
4 changed files with 227 additions and 4 deletions

View File

@ -1,6 +1,9 @@
# NightShift # NightShift
![NightShift logo](docs/images/logo.png) <p align="center">
<img src="docs/images/logo.png" width="220">
</p>
Auditable local-first AI coding pipelines. Auditable local-first AI coding pipelines.

View 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()

View File

@ -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

View File

@ -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")