mirror of
https://github.com/khodges42/nightShift.git
synced 2026-06-14 10:08:37 +00:00
Implemented deterministic diff generation via a new file_writer stage.
What changed:
- Added file_writer stage support in nightshift/config.py:93, nightshift/agents.py:446, and nightshift/pipeline.py:374.
- Added file-block parsing and deterministic patch generation in nightshift/patches.py:65.
- file_writer agents now return complete file blocks:
```file:relative/path.py
<complete file content>
```
NightShift reads current files and generates the unified diff itself.
- Existing patch_validator, patch_normalizer, and patch_apply still run after that.
- Switched tiny-lisp-nightshift/nightshift.yaml:51 to type: file_writer.
- Updated tiny-lisp-nightshift/agents/implementer.md:1 to request file blocks instead of diffs.
- Updated README, tutorial, config reference, and design doc.
Also added tests for parsing file blocks, generating multi-file diffs, validating those diffs, and running a full file_writer -> normalize -> validate pipeline.
Verification: python -m unittest discover -v passes, 101 tests.
This commit is contained in:
parent
42564c6867
commit
fa97abf0c3
84
.nightshift/project-context-chart.md
Normal file
84
.nightshift/project-context-chart.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# Project Context Chart
|
||||
|
||||
## Entry Points
|
||||
|
||||
- `tests/test_lisp.py`: class SmokeTests(unittest.TestCase):
|
||||
|
||||
## Tests
|
||||
|
||||
- `tests/__init__.py`
|
||||
- `tests/test_lisp.py`
|
||||
|
||||
## Files
|
||||
|
||||
### `agents/implementer.md`
|
||||
|
||||
- Responsibility: You are the implementation agent for NightShift.
|
||||
- Entry point: false
|
||||
- Test file: false
|
||||
- Functions: None detected
|
||||
- Classes: None detected
|
||||
- Anchors/search terms: None detected
|
||||
|
||||
### `agents/planner.md`
|
||||
|
||||
- Responsibility: You are the planning agent for NightShift.
|
||||
- Entry point: false
|
||||
- Test file: false
|
||||
- Functions: None detected
|
||||
- Classes: None detected
|
||||
- Anchors/search terms: None detected
|
||||
|
||||
### `agents/reviewer.md`
|
||||
|
||||
- Responsibility: You are the review agent for NightShift.
|
||||
- Entry point: false
|
||||
- Test file: false
|
||||
- Functions: None detected
|
||||
- Classes: None detected
|
||||
- Anchors/search terms: None detected
|
||||
|
||||
### `lisp.py`
|
||||
|
||||
- Responsibility: """Tiny Lisp quickstart target.
|
||||
- Entry point: false
|
||||
- Test file: false
|
||||
- Functions: None detected
|
||||
- Classes: None detected
|
||||
- Anchors/search terms: None detected
|
||||
|
||||
### `nightshift.yaml`
|
||||
|
||||
- Responsibility: project:
|
||||
- Entry point: false
|
||||
- Test file: false
|
||||
- Functions: None detected
|
||||
- Classes: None detected
|
||||
- Anchors/search terms: None detected
|
||||
|
||||
### `tasks.md`
|
||||
|
||||
- Responsibility: Tasks
|
||||
- Entry point: false
|
||||
- Test file: false
|
||||
- Functions: None detected
|
||||
- Classes: None detected
|
||||
- Anchors/search terms: None detected
|
||||
|
||||
### `tests/__init__.py`
|
||||
|
||||
- Responsibility: """Quickstart Lisp test suite."""
|
||||
- Entry point: false
|
||||
- Test file: true
|
||||
- Functions: None detected
|
||||
- Classes: None detected
|
||||
- Anchors/search terms: None detected
|
||||
|
||||
### `tests/test_lisp.py`
|
||||
|
||||
- Responsibility: class SmokeTests(unittest.TestCase):
|
||||
- Entry point: true
|
||||
- Test file: true
|
||||
- Functions: test_smoke@L5
|
||||
- Classes: SmokeTests@L4
|
||||
- Anchors/search terms: SmokeTests, test_smoke
|
||||
2
.nightshift/project-context.md
Normal file
2
.nightshift/project-context.md
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Project Context
|
||||
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
project:
|
||||
name: tiny-lisp
|
||||
root: .
|
||||
task_file: tasks.md
|
||||
artifact_dir: .nightshift
|
||||
|
||||
safety:
|
||||
require_clean_worktree: false
|
||||
scoped_paths:
|
||||
- .
|
||||
allowed_commands:
|
||||
- python -m unittest discover -v
|
||||
forbidden_commands:
|
||||
- rm -rf
|
||||
- git push
|
||||
- curl | bash
|
||||
|
||||
agents:
|
||||
planner:
|
||||
backend: ollama
|
||||
model: qwen2.5-coder:14b
|
||||
temperature: 0.2
|
||||
system_prompt: agents/planner.md
|
||||
|
||||
implementer:
|
||||
backend: ollama
|
||||
model: qwen2.5-coder:14b
|
||||
temperature: 0.1
|
||||
system_prompt: agents/implementer.md
|
||||
|
||||
reviewer:
|
||||
backend: ollama
|
||||
model: qwen2.5-coder:14b
|
||||
temperature: 0.1
|
||||
system_prompt: agents/reviewer.md
|
||||
|
||||
pipeline:
|
||||
max_task_retries: 3
|
||||
continue_on_task_failure: false
|
||||
stages:
|
||||
- id: plan
|
||||
type: agent
|
||||
agent: planner
|
||||
output: plan.md
|
||||
|
||||
- id: context
|
||||
type: repo_context
|
||||
output: context-pack.md
|
||||
|
||||
- id: implement
|
||||
type: code_writer
|
||||
agent: implementer
|
||||
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
|
||||
on_fail: implement
|
||||
|
||||
- id: apply_patch
|
||||
type: patch_apply
|
||||
mode: apply
|
||||
output: patch-apply-output.txt
|
||||
on_fail: implement
|
||||
|
||||
- id: test
|
||||
type: command
|
||||
commands:
|
||||
- python -m unittest discover -v
|
||||
output: test-output.txt
|
||||
shell: true
|
||||
timeout_seconds: 60
|
||||
on_fail: implement
|
||||
|
||||
- id: review
|
||||
type: agent_review
|
||||
agent: reviewer
|
||||
on_fail: implement
|
||||
output: review.md
|
||||
|
||||
- id: summarize
|
||||
type: summarize
|
||||
output: final-notes.md
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
You are the implementation agent for NightShift.
|
||||
|
||||
Output only a unified diff.
|
||||
Do not wrap the patch in markdown fences.
|
||||
Do not include explanations before or after the patch.
|
||||
Use diff --git headers.
|
||||
Include tests when needed.
|
||||
Keep the change as small as possible.
|
||||
Only edit files needed for the task.
|
||||
19
.nightshift/runs/20260517T212359.709956Z/prompts/planner.md
Normal file
19
.nightshift/runs/20260517T212359.709956Z/prompts/planner.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
You are the planning agent for NightShift.
|
||||
|
||||
Create a concise implementation plan for the current task.
|
||||
|
||||
If you need repository context before planning, output lookup requests exactly like this:
|
||||
|
||||
lookup_requests:
|
||||
- tool: read_file
|
||||
path: relative/path.py
|
||||
- tool: grep
|
||||
path: .
|
||||
pattern: search_regex
|
||||
|
||||
After context is provided, write a short plan with:
|
||||
- files to edit
|
||||
- tests to add or update
|
||||
- risks
|
||||
|
||||
Do not write code.
|
||||
14
.nightshift/runs/20260517T212359.709956Z/prompts/reviewer.md
Normal file
14
.nightshift/runs/20260517T212359.709956Z/prompts/reviewer.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
You are the review agent for NightShift.
|
||||
|
||||
Review the task, plan, patch artifacts, test output, and final state.
|
||||
|
||||
Output exactly:
|
||||
|
||||
status: pass | fail | retry | escalate
|
||||
reason: <short explanation>
|
||||
next_stage: <optional stage id>
|
||||
context_update: <compact useful note>
|
||||
|
||||
Use retry when the implementation is close but needs another patch.
|
||||
Use fail when the patch is unsafe, unrelated, or clearly broken.
|
||||
Use pass only when the acceptance criteria are satisfied.
|
||||
34
.nightshift/runs/20260517T212359.709956Z/run-metadata.md
Normal file
34
.nightshift/runs/20260517T212359.709956Z/run-metadata.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Run Metadata
|
||||
|
||||
Project: tiny-lisp
|
||||
Experiment label:
|
||||
Prompt variant:
|
||||
|
||||
## Agents
|
||||
|
||||
### planner
|
||||
|
||||
- Backend: ollama
|
||||
- Model: qwen2.5-coder:14b
|
||||
- Temperature: 0.2
|
||||
- Base URL:
|
||||
- Command:
|
||||
- System prompt: agents\planner.md
|
||||
|
||||
### implementer
|
||||
|
||||
- Backend: ollama
|
||||
- Model: qwen2.5-coder:14b
|
||||
- Temperature: 0.1
|
||||
- Base URL:
|
||||
- Command:
|
||||
- System prompt: agents\implementer.md
|
||||
|
||||
### reviewer
|
||||
|
||||
- Backend: ollama
|
||||
- Model: qwen2.5-coder:14b
|
||||
- Temperature: 0.1
|
||||
- Base URL:
|
||||
- Command:
|
||||
- System prompt: agents\reviewer.md
|
||||
17
.nightshift/runs/20260517T212359.709956Z/run-summary.md
Normal file
17
.nightshift/runs/20260517T212359.709956Z/run-summary.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Run Summary
|
||||
|
||||
- Task: TASK-001
|
||||
- Status: failed
|
||||
- Retry count: 3
|
||||
- Reason: Retry limit reached after stage 'apply_patch': Patch apply failed with code 128.
|
||||
- Experiment label:
|
||||
- Prompt variant:
|
||||
|
||||
## Modified Files
|
||||
|
||||
- Unavailable or none detected
|
||||
|
||||
## Artifacts
|
||||
|
||||
- Final notes: `tasks\TASK-001\final-notes.md`
|
||||
- Stage results: `tasks\TASK-001\stage-results.md`
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
diff --git a/lisp.py b/lisp.py
|
||||
index 0000000..f2a5c3d
|
||||
--- /dev/null
|
||||
+++ b/lisp.py
|
||||
@@ -0,0 +1,78 @@
|
||||
+import re
|
||||
+
|
||||
+class LispParser:
|
||||
+ def __init__(self, expression):
|
||||
+ self.expression = expression
|
||||
+ self.tokens = self.tokenize()
|
||||
+ self.index = 0
|
||||
+
|
||||
+ def tokenize(self):
|
||||
+ token_pattern = r'\s*([()])|(\d+)|([a-zA-Z_][a-zA-Z0-9_]*)'
|
||||
+ tokens = re.findall(token_pattern, self.expression)
|
||||
+ return [token for group in tokens for token in group if token]
|
||||
+
|
||||
+ def parse(self):
|
||||
+ result = []
|
||||
+ while self.index < len(self.tokens):
|
||||
+ token = self.tokens[self.index]
|
||||
+ if token == '(':
|
||||
+ self.index += 1
|
||||
+ result.append(self.parse())
|
||||
+ elif token == ')':
|
||||
+ return result
|
||||
+ else:
|
||||
+ result.append(self.parse_atom(token))
|
||||
+ self.index += 1
|
||||
+ if len(result) == 1:
|
||||
+ return result[0]
|
||||
+ return result
|
||||
+
|
||||
+ def parse_atom(self, token):
|
||||
+ try:
|
||||
+ return int(token)
|
||||
+ except ValueError:
|
||||
+ return token
|
||||
+
|
||||
+def parse_lisp(expression):
|
||||
+ parser = LispParser(expression)
|
||||
+ return parser.parse()
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ expression = "(1 2 (3 4) a b)"
|
||||
+ print(parse_lisp(expression))
|
||||
diff --git a/tests/test_lisp.py b/tests/test_lisp.py
|
||||
index 0000000..b5c7d8e
|
||||
--- /dev/null
|
||||
+++ b/tests/test_lisp.py
|
||||
@@ -0,0 +1,39 @@
|
||||
+import unittest
|
||||
+from lisp import parse_lisp
|
||||
+
|
||||
+class TestLispParser(unittest.TestCase):
|
||||
+ def test_parse_numbers(self):
|
||||
+ self.assertEqual(parse_lisp("123"), 123)
|
||||
+ self.assertEqual(parse_lisp("(1 2 3)"), [1, 2, 3])
|
||||
+
|
||||
+ def test_parse_symbols(self):
|
||||
+ self.assertEqual(parse_lisp("a"), "a")
|
||||
+ self.assertEqual(parse_lisp("(a b c)"), ["a", "b", "c"])
|
||||
+
|
||||
+ def test_parse_nested_lists(self):
|
||||
+ self.assertEqual(parse_lisp("(1 (2 3) 4)"), [1, [2, 3], 4])
|
||||
+ self.assertEqual(parse_lisp("((a b) (c d))"), [["a", "b"], ["c", "d"]])
|
||||
+
|
||||
+ def test_unbalanced_parentheses(self):
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp(")")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(1 2 (3 4)")
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ unittest.main()
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
diff --git a/lisp.py b/lisp.py
|
||||
index 0000000..f2a5c3d
|
||||
--- /dev/null
|
||||
+++ b/lisp.py
|
||||
@@ -0,0 +1,78 @@
|
||||
+import re
|
||||
+
|
||||
+class LispParser:
|
||||
+ def __init__(self, expression):
|
||||
+ self.expression = expression
|
||||
+ self.tokens = self.tokenize()
|
||||
+ self.index = 0
|
||||
+
|
||||
+ def tokenize(self):
|
||||
+ token_pattern = r'\s*([()])|(\d+)|([a-zA-Z_][a-zA-Z0-9_]*)'
|
||||
+ tokens = re.findall(token_pattern, self.expression)
|
||||
+ return [token for group in tokens for token in group if token]
|
||||
+
|
||||
+ def parse(self):
|
||||
+ result = []
|
||||
+ while self.index < len(self.tokens):
|
||||
+ token = self.tokens[self.index]
|
||||
+ if token == '(':
|
||||
+ self.index += 1
|
||||
+ result.append(self.parse())
|
||||
+ elif token == ')':
|
||||
+ return result
|
||||
+ else:
|
||||
+ result.append(self.parse_atom(token))
|
||||
+ self.index += 1
|
||||
+ if len(result) == 1:
|
||||
+ return result[0]
|
||||
+ return result
|
||||
+
|
||||
+ def parse_atom(self, token):
|
||||
+ try:
|
||||
+ return int(token)
|
||||
+ except ValueError:
|
||||
+ return token
|
||||
+
|
||||
+def parse_lisp(expression):
|
||||
+ parser = LispParser(expression)
|
||||
+ return parser.parse()
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ expression = "(1 2 (3 4) a b)"
|
||||
+ print(parse_lisp(expression))
|
||||
diff --git a/tests/test_lisp.py b/tests/test_lisp.py
|
||||
index 0000000..b5c7d8e
|
||||
--- /dev/null
|
||||
+++ b/tests/test_lisp.py
|
||||
@@ -0,0 +1,39 @@
|
||||
+import unittest
|
||||
+from lisp import parse_lisp
|
||||
+
|
||||
+class TestLispParser(unittest.TestCase):
|
||||
+ def test_parse_numbers(self):
|
||||
+ self.assertEqual(parse_lisp("123"), 123)
|
||||
+ self.assertEqual(parse_lisp("(1 2 3)"), [1, 2, 3])
|
||||
+
|
||||
+ def test_parse_symbols(self):
|
||||
+ self.assertEqual(parse_lisp("a"), "a")
|
||||
+ self.assertEqual(parse_lisp("(a b c)"), ["a", "b", "c"])
|
||||
+
|
||||
+ def test_parse_nested_lists(self):
|
||||
+ self.assertEqual(parse_lisp("(1 (2 3) 4)"), [1, [2, 3], 4])
|
||||
+ self.assertEqual(parse_lisp("((a b) (c d))"), [["a", "b"], ["c", "d"]])
|
||||
+
|
||||
+ def test_unbalanced_parentheses(self):
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp(")")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(1 2 (3 4)")
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ unittest.main()
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
diff --git a/lisp.py b/lisp.py
|
||||
index 0000000..f2a5c3d
|
||||
--- /dev/null
|
||||
+++ b/lisp.py
|
||||
@@ -0,0 +1,78 @@
|
||||
+import re
|
||||
+
|
||||
+class LispParser:
|
||||
+ def __init__(self, expression):
|
||||
+ self.expression = expression
|
||||
+ self.tokens = self.tokenize()
|
||||
+ self.index = 0
|
||||
+
|
||||
+ def tokenize(self):
|
||||
+ token_pattern = r'\s*([()])|(\d+)|([a-zA-Z_][a-zA-Z0-9_]*)'
|
||||
+ tokens = re.findall(token_pattern, self.expression)
|
||||
+ return [token for group in tokens for token in group if token]
|
||||
+
|
||||
+ def parse(self):
|
||||
+ result = []
|
||||
+ while self.index < len(self.tokens):
|
||||
+ token = self.tokens[self.index]
|
||||
+ if token == '(':
|
||||
+ self.index += 1
|
||||
+ result.append(self.parse())
|
||||
+ elif token == ')':
|
||||
+ return result
|
||||
+ else:
|
||||
+ result.append(self.parse_atom(token))
|
||||
+ self.index += 1
|
||||
+ if len(result) == 1:
|
||||
+ return result[0]
|
||||
+ return result
|
||||
+
|
||||
+ def parse_atom(self, token):
|
||||
+ try:
|
||||
+ return int(token)
|
||||
+ except ValueError:
|
||||
+ return token
|
||||
+
|
||||
+def parse_lisp(expression):
|
||||
+ parser = LispParser(expression)
|
||||
+ return parser.parse()
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ expression = "(1 2 (3 4) a b)"
|
||||
+ print(parse_lisp(expression))
|
||||
diff --git a/tests/test_lisp.py b/tests/test_lisp.py
|
||||
index 0000000..b5c7d8e
|
||||
--- /dev/null
|
||||
+++ b/tests/test_lisp.py
|
||||
@@ -0,0 +1,39 @@
|
||||
+import unittest
|
||||
+from lisp import parse_lisp
|
||||
+
|
||||
+class TestLispParser(unittest.TestCase):
|
||||
+ def test_parse_numbers(self):
|
||||
+ self.assertEqual(parse_lisp("123"), 123)
|
||||
+ self.assertEqual(parse_lisp("(1 2 3)"), [1, 2, 3])
|
||||
+
|
||||
+ def test_parse_symbols(self):
|
||||
+ self.assertEqual(parse_lisp("a"), "a")
|
||||
+ self.assertEqual(parse_lisp("(a b c)"), ["a", "b", "c"])
|
||||
+
|
||||
+ def test_parse_nested_lists(self):
|
||||
+ self.assertEqual(parse_lisp("(1 (2 3) 4)"), [1, [2, 3], 4])
|
||||
+ self.assertEqual(parse_lisp("((a b) (c d))"), [["a", "b"], ["c", "d"]])
|
||||
+
|
||||
+ def test_unbalanced_parentheses(self):
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp(")")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(1 2 (3 4)")
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
# Context Out
|
||||
|
||||
Task: `TASK-001`
|
||||
Status: failed
|
||||
Reason: Retry limit reached after stage 'apply_patch': Patch apply failed with code 128.
|
||||
|
||||
## Retry Notes
|
||||
|
||||
- Context update from 'implement': Implementation summary: .nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/implementation-summary.md
|
||||
- Retry 1: stage 'validate_patch' returned fail (Patch validation failed: patch creates existing file `lisp.py`.); redirecting to 'implement'.
|
||||
- Context update from 'implement': Implementation summary: .nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/repair-summary-1.md
|
||||
- Context update from 'apply_patch': C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:6: trailing whitespace.
|
||||
import re
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:7: trailing whitespace.
|
||||
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:8: trailing whitespace.
|
||||
class LispParser:
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:9: trailing whitespace.
|
||||
def __init__(self, expression):
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:10: trailing whitespace.
|
||||
self.expression = expression
|
||||
error: corrupt patch at line 48
|
||||
- Retry 2: stage 'apply_patch' returned fail (Patch apply failed with code 128.); redirecting to 'implement'.
|
||||
- Context update from 'implement': Implementation summary: .nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/repair-summary-2.md
|
||||
- Context update from 'apply_patch': C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:6: trailing whitespace.
|
||||
import re
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:7: trailing whitespace.
|
||||
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:8: trailing whitespace.
|
||||
class LispParser:
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:9: trailing whitespace.
|
||||
def __init__(self, expression):
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:10: trailing whitespace.
|
||||
self.expression = expression
|
||||
error: corrupt patch at line 48
|
||||
- Retry 3: stage 'apply_patch' returned fail (Patch apply failed with code 128.); redirecting to 'implement'.
|
||||
- Context update from 'implement': Implementation summary: .nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/repair-summary-3.md
|
||||
- Context update from 'apply_patch': C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-3.patch:6: trailing whitespace.
|
||||
import re
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-3.patch:7: trailing whitespace.
|
||||
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-3.patch:8: trailing whitespace.
|
||||
class LispParser:
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-3.patch:9: trailing whitespace.
|
||||
def __init__(self, expression):
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-3.patch:10: trailing whitespace.
|
||||
self.expression = expression
|
||||
error: corrupt patch at line 48
|
||||
|
||||
## Durable Notes
|
||||
|
||||
- Implementation summary: .nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/implementation-summary.md
|
||||
- Implementation summary: .nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/repair-summary-1.md
|
||||
- C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:6: trailing whitespace.
|
||||
import re
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:7: trailing whitespace.
|
||||
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:8: trailing whitespace.
|
||||
class LispParser:
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:9: trailing whitespace.
|
||||
def __init__(self, expression):
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:10: trailing whitespace.
|
||||
self.expression = expression
|
||||
error: corrupt patch at line 48
|
||||
- Implementation summary: .nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/repair-summary-2.md
|
||||
- C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:6: trailing whitespace.
|
||||
import re
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:7: trailing whitespace.
|
||||
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:8: trailing whitespace.
|
||||
class LispParser:
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:9: trailing whitespace.
|
||||
def __init__(self, expression):
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:10: trailing whitespace.
|
||||
self.expression = expression
|
||||
error: corrupt patch at line 48
|
||||
- Implementation summary: .nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/repair-summary-3.md
|
||||
- C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-3.patch:6: trailing whitespace.
|
||||
import re
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-3.patch:7: trailing whitespace.
|
||||
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-3.patch:8: trailing whitespace.
|
||||
class LispParser:
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-3.patch:9: trailing whitespace.
|
||||
def __init__(self, expression):
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-3.patch:10: trailing whitespace.
|
||||
self.expression = expression
|
||||
error: corrupt patch at line 48
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
# Context Pack
|
||||
|
||||
Task: `TASK-001`
|
||||
Title: Parse Lisp expressions
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Parses numbers
|
||||
- Parses symbols
|
||||
- Parses nested lists
|
||||
- Raises useful errors for unbalanced parentheses
|
||||
- Includes unit tests
|
||||
|
||||
## Constraints
|
||||
|
||||
- Scoped paths: .
|
||||
- Repository lookups are read-only.
|
||||
- Excerpts are line-numbered where files are read directly.
|
||||
|
||||
## Relevant Files
|
||||
|
||||
```text
|
||||
lisp.py
|
||||
tests/__init__.py
|
||||
tests/test_lisp.py
|
||||
```
|
||||
|
||||
## Search Results
|
||||
|
||||
### Search: parse
|
||||
|
||||
```text
|
||||
tasks.md:21: Evaluate parsed arithmetic expressions.
|
||||
```
|
||||
|
||||
### Search: lisp
|
||||
|
||||
```text
|
||||
nightshift.yaml:2: name: tiny-lisp
|
||||
```
|
||||
|
||||
### Search: expressions
|
||||
|
||||
```text
|
||||
tasks.md:3: - [ ] TASK-001: Parse Lisp expressions
|
||||
tasks.md:21: Evaluate parsed arithmetic expressions.
|
||||
tasks.md:47: Implement simple truthiness and `if` expressions.
|
||||
```
|
||||
|
||||
### Search: parses
|
||||
|
||||
```text
|
||||
No matches found.
|
||||
```
|
||||
|
||||
### Search: numbers
|
||||
|
||||
```text
|
||||
tasks.md:9: - Parses numbers
|
||||
```
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# Task Context
|
||||
|
||||
Task: `TASK-001`
|
||||
Title: Parse Lisp expressions
|
||||
|
||||
## Description
|
||||
|
||||
Implement tokenization and parsing for a tiny Lisp subset.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Parses numbers
|
||||
- Parses symbols
|
||||
- Parses nested lists
|
||||
- Raises useful errors for unbalanced parentheses
|
||||
- Includes unit tests
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
Git diff unavailable.
|
||||
|
||||
Reason: project root is not a git work tree.
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
# Repo Tool Calls
|
||||
|
||||
## 1. read_file
|
||||
|
||||
Arguments:
|
||||
- path: `lisp_parser.py`
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
File not found: lisp_parser.py
|
||||
```
|
||||
|
||||
## 2. read_file
|
||||
|
||||
Arguments:
|
||||
- path: `tests/test_lisp_parser.py`
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
File not found: tests/test_lisp_parser.py
|
||||
```
|
||||
|
||||
## 3. list_files
|
||||
|
||||
Arguments:
|
||||
- path: `.`
|
||||
- pattern: `*.py`
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
lisp.py
|
||||
tests/__init__.py
|
||||
tests/test_lisp.py
|
||||
```
|
||||
|
||||
## 4. grep
|
||||
|
||||
Arguments:
|
||||
- path: `.`
|
||||
- pattern: `def parse\(`
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
No matches found.
|
||||
```
|
||||
|
||||
## 5. grep
|
||||
|
||||
Arguments:
|
||||
- path: `.`
|
||||
- pattern: `class LispParser`
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
No matches found.
|
||||
```
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
# Final Task Notes
|
||||
|
||||
Task: `TASK-001`
|
||||
Title: Parse Lisp expressions
|
||||
Status: failed
|
||||
Retry count: 3
|
||||
Reason: Retry limit reached after stage 'apply_patch': Patch apply failed with code 128.
|
||||
|
||||
## Experiment
|
||||
|
||||
- Label:
|
||||
- Prompt variant:
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Parses numbers
|
||||
- Parses symbols
|
||||
- Parses nested lists
|
||||
- Raises useful errors for unbalanced parentheses
|
||||
- Includes unit tests
|
||||
|
||||
## Stage Results
|
||||
|
||||
- `plan`: pass (Agent completed after repo lookup.)
|
||||
- `context`: pass (Context pack written.)
|
||||
- `implement`: pass (Proposed patch written.)
|
||||
- `normalize`: pass (Normalized patch written.)
|
||||
- `validate_patch`: fail (Patch validation failed: patch creates existing file `lisp.py`.)
|
||||
- `implement`: pass (Proposed patch written.)
|
||||
- `normalize`: pass (Normalized patch written.)
|
||||
- `validate_patch`: pass (Patch validation passed.)
|
||||
- `apply_patch`: fail (Patch apply failed with code 128.)
|
||||
- `implement`: pass (Proposed patch written.)
|
||||
- `normalize`: pass (Normalized patch written.)
|
||||
- `validate_patch`: pass (Patch validation passed.)
|
||||
- `apply_patch`: fail (Patch apply failed with code 128.)
|
||||
- `implement`: pass (Proposed patch written.)
|
||||
- `normalize`: pass (Normalized patch written.)
|
||||
- `validate_patch`: pass (Patch validation passed.)
|
||||
- `apply_patch`: fail (Patch apply failed with code 128.)
|
||||
|
||||
## Modified Files
|
||||
|
||||
- Unavailable or none detected
|
||||
|
||||
## Artifacts
|
||||
|
||||
- Stage results: `stage-results.md`
|
||||
- Context out: `context-out.md`
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# Git Status after-patch-apply
|
||||
|
||||
Available: false
|
||||
Exit code: 128
|
||||
|
||||
## stdout
|
||||
|
||||
```text
|
||||
|
||||
```
|
||||
|
||||
## stderr
|
||||
|
||||
```text
|
||||
fatal: not a git repository (or any of the parent directories): .git
|
||||
```
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# Git Status after
|
||||
|
||||
Available: false
|
||||
Exit code: 128
|
||||
|
||||
## stdout
|
||||
|
||||
```text
|
||||
|
||||
```
|
||||
|
||||
## stderr
|
||||
|
||||
```text
|
||||
fatal: not a git repository (or any of the parent directories): .git
|
||||
```
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# Git Status before-patch-apply
|
||||
|
||||
Available: false
|
||||
Exit code: 128
|
||||
|
||||
## stdout
|
||||
|
||||
```text
|
||||
|
||||
```
|
||||
|
||||
## stderr
|
||||
|
||||
```text
|
||||
fatal: not a git repository (or any of the parent directories): .git
|
||||
```
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# Git Status before
|
||||
|
||||
Available: false
|
||||
Exit code: 128
|
||||
|
||||
## stdout
|
||||
|
||||
```text
|
||||
|
||||
```
|
||||
|
||||
## stderr
|
||||
|
||||
```text
|
||||
fatal: not a git repository (or any of the parent directories): .git
|
||||
```
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# Implementation Summary
|
||||
|
||||
Stage: `implement`
|
||||
Status: pass
|
||||
Repair attempt: 0
|
||||
Patch: `.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/proposed.patch`
|
||||
|
||||
## Retry Feedback
|
||||
|
||||
- None
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
diff --git a/lisp.py b/lisp.py
|
||||
index 0000000..f2a5c3d
|
||||
--- /dev/null
|
||||
+++ b/lisp.py
|
||||
@@ -0,0 +1,78 @@
|
||||
+import re
|
||||
+
|
||||
+class LispParser:
|
||||
+ def __init__(self, expression):
|
||||
+ self.expression = expression
|
||||
+ self.tokens = self.tokenize()
|
||||
+ self.index = 0
|
||||
+
|
||||
+ def tokenize(self):
|
||||
+ token_pattern = r'\s*([()])|(\d+)|([a-zA-Z_][a-zA-Z0-9_]*)'
|
||||
+ tokens = re.findall(token_pattern, self.expression)
|
||||
+ return [token for group in tokens for token in group if token]
|
||||
+
|
||||
+ def parse(self):
|
||||
+ result = []
|
||||
+ while self.index < len(self.tokens):
|
||||
+ token = self.tokens[self.index]
|
||||
+ if token == '(':
|
||||
+ self.index += 1
|
||||
+ result.append(self.parse())
|
||||
+ elif token == ')':
|
||||
+ return result
|
||||
+ else:
|
||||
+ result.append(self.parse_atom(token))
|
||||
+ self.index += 1
|
||||
+ if len(result) == 1:
|
||||
+ return result[0]
|
||||
+ return result
|
||||
+
|
||||
+ def parse_atom(self, token):
|
||||
+ try:
|
||||
+ return int(token)
|
||||
+ except ValueError:
|
||||
+ return token
|
||||
+
|
||||
+def parse_lisp(expression):
|
||||
+ parser = LispParser(expression)
|
||||
+ return parser.parse()
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ expression = "(1 2 (3 4) a b)"
|
||||
+ print(parse_lisp(expression))
|
||||
diff --git a/tests/test_lisp.py b/tests/test_lisp.py
|
||||
index 0000000..b5c7d8e
|
||||
--- /dev/null
|
||||
+++ b/tests/test_lisp.py
|
||||
@@ -0,0 +1,39 @@
|
||||
+import unittest
|
||||
+from lisp import parse_lisp
|
||||
+
|
||||
+class TestLispParser(unittest.TestCase):
|
||||
+ def test_parse_numbers(self):
|
||||
+ self.assertEqual(parse_lisp("123"), 123)
|
||||
+ self.assertEqual(parse_lisp("(1 2 3)"), [1, 2, 3])
|
||||
+
|
||||
+ def test_parse_symbols(self):
|
||||
+ self.assertEqual(parse_lisp("a"), "a")
|
||||
+ self.assertEqual(parse_lisp("(a b c)"), ["a", "b", "c"])
|
||||
+
|
||||
+ def test_parse_nested_lists(self):
|
||||
+ self.assertEqual(parse_lisp("(1 (2 3) 4)"), [1, [2, 3], 4])
|
||||
+ self.assertEqual(parse_lisp("((a b) (c d))"), [["a", "b"], ["c", "d"]])
|
||||
+
|
||||
+ def test_unbalanced_parentheses(self):
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp(")")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(1 2 (3 4)")
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ unittest.main()
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
diff --git a/lisp.py b/lisp.py
|
||||
index 0000000..f2a5c3d
|
||||
--- /dev/null
|
||||
+++ b/lisp.py
|
||||
@@ -0,0 +1,78 @@
|
||||
+import re
|
||||
+
|
||||
+class LispParser:
|
||||
+ def __init__(self, expression):
|
||||
+ self.expression = expression
|
||||
+ self.tokens = self.tokenize()
|
||||
+ self.index = 0
|
||||
+
|
||||
+ def tokenize(self):
|
||||
+ token_pattern = r'\s*([()])|(\d+)|([a-zA-Z_][a-zA-Z0-9_]*)'
|
||||
+ tokens = re.findall(token_pattern, self.expression)
|
||||
+ return [token for group in tokens for token in group if token]
|
||||
+
|
||||
+ def parse(self):
|
||||
+ result = []
|
||||
+ while self.index < len(self.tokens):
|
||||
+ token = self.tokens[self.index]
|
||||
+ if token == '(':
|
||||
+ self.index += 1
|
||||
+ result.append(self.parse())
|
||||
+ elif token == ')':
|
||||
+ return result
|
||||
+ else:
|
||||
+ result.append(self.parse_atom(token))
|
||||
+ self.index += 1
|
||||
+ if len(result) == 1:
|
||||
+ return result[0]
|
||||
+ return result
|
||||
+
|
||||
+ def parse_atom(self, token):
|
||||
+ try:
|
||||
+ return int(token)
|
||||
+ except ValueError:
|
||||
+ return token
|
||||
+
|
||||
+def parse_lisp(expression):
|
||||
+ parser = LispParser(expression)
|
||||
+ return parser.parse()
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ expression = "(1 2 (3 4) a b)"
|
||||
+ print(parse_lisp(expression))
|
||||
diff --git a/tests/test_lisp.py b/tests/test_lisp.py
|
||||
index 0000000..b5c7d8e
|
||||
--- /dev/null
|
||||
+++ b/tests/test_lisp.py
|
||||
@@ -0,0 +1,39 @@
|
||||
+import unittest
|
||||
+from lisp import parse_lisp
|
||||
+
|
||||
+class TestLispParser(unittest.TestCase):
|
||||
+ def test_parse_numbers(self):
|
||||
+ self.assertEqual(parse_lisp("123"), 123)
|
||||
+ self.assertEqual(parse_lisp("(1 2 3)"), [1, 2, 3])
|
||||
+
|
||||
+ def test_parse_symbols(self):
|
||||
+ self.assertEqual(parse_lisp("a"), "a")
|
||||
+ self.assertEqual(parse_lisp("(a b c)"), ["a", "b", "c"])
|
||||
+
|
||||
+ def test_parse_nested_lists(self):
|
||||
+ self.assertEqual(parse_lisp("(1 (2 3) 4)"), [1, [2, 3], 4])
|
||||
+ self.assertEqual(parse_lisp("((a b) (c d))"), [["a", "b"], ["c", "d"]])
|
||||
+
|
||||
+ def test_unbalanced_parentheses(self):
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp(")")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(1 2 (3 4)")
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ unittest.main()
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
diff --git a/lisp.py b/lisp.py
|
||||
index 0000000..f2a5c3d
|
||||
--- /dev/null
|
||||
+++ b/lisp.py
|
||||
@@ -0,0 +1,78 @@
|
||||
+import re
|
||||
+
|
||||
+class LispParser:
|
||||
+ def __init__(self, expression):
|
||||
+ self.expression = expression
|
||||
+ self.tokens = self.tokenize()
|
||||
+ self.index = 0
|
||||
+
|
||||
+ def tokenize(self):
|
||||
+ token_pattern = r'\s*([()])|(\d+)|([a-zA-Z_][a-zA-Z0-9_]*)'
|
||||
+ tokens = re.findall(token_pattern, self.expression)
|
||||
+ return [token for group in tokens for token in group if token]
|
||||
+
|
||||
+ def parse(self):
|
||||
+ result = []
|
||||
+ while self.index < len(self.tokens):
|
||||
+ token = self.tokens[self.index]
|
||||
+ if token == '(':
|
||||
+ self.index += 1
|
||||
+ result.append(self.parse())
|
||||
+ elif token == ')':
|
||||
+ return result
|
||||
+ else:
|
||||
+ result.append(self.parse_atom(token))
|
||||
+ self.index += 1
|
||||
+ if len(result) == 1:
|
||||
+ return result[0]
|
||||
+ return result
|
||||
+
|
||||
+ def parse_atom(self, token):
|
||||
+ try:
|
||||
+ return int(token)
|
||||
+ except ValueError:
|
||||
+ return token
|
||||
+
|
||||
+def parse_lisp(expression):
|
||||
+ parser = LispParser(expression)
|
||||
+ return parser.parse()
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ expression = "(1 2 (3 4) a b)"
|
||||
+ print(parse_lisp(expression))
|
||||
diff --git a/tests/test_lisp.py b/tests/test_lisp.py
|
||||
index 0000000..b5c7d8e
|
||||
--- /dev/null
|
||||
+++ b/tests/test_lisp.py
|
||||
@@ -0,0 +1,39 @@
|
||||
+import unittest
|
||||
+from lisp import parse_lisp
|
||||
+
|
||||
+class TestLispParser(unittest.TestCase):
|
||||
+ def test_parse_numbers(self):
|
||||
+ self.assertEqual(parse_lisp("123"), 123)
|
||||
+ self.assertEqual(parse_lisp("(1 2 3)"), [1, 2, 3])
|
||||
+
|
||||
+ def test_parse_symbols(self):
|
||||
+ self.assertEqual(parse_lisp("a"), "a")
|
||||
+ self.assertEqual(parse_lisp("(a b c)"), ["a", "b", "c"])
|
||||
+
|
||||
+ def test_parse_nested_lists(self):
|
||||
+ self.assertEqual(parse_lisp("(1 (2 3) 4)"), [1, [2, 3], 4])
|
||||
+ self.assertEqual(parse_lisp("((a b) (c d))"), [["a", "b"], ["c", "d"]])
|
||||
+
|
||||
+ def test_unbalanced_parentheses(self):
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp(")")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(1 2 (3 4)")
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
diff --git a/lisp.py b/lisp.py
|
||||
new file mode 100644
|
||||
index 0000000..f2a5c3d
|
||||
--- /dev/null
|
||||
+++ b/lisp.py
|
||||
@@ -0,0 +1,78 @@
|
||||
+import re
|
||||
+
|
||||
+class LispParser:
|
||||
+ def __init__(self, expression):
|
||||
+ self.expression = expression
|
||||
+ self.tokens = self.tokenize()
|
||||
+ self.index = 0
|
||||
+
|
||||
+ def tokenize(self):
|
||||
+ token_pattern = r'\s*([()])|(\d+)|([a-zA-Z_][a-zA-Z0-9_]*)'
|
||||
+ tokens = re.findall(token_pattern, self.expression)
|
||||
+ return [token for group in tokens for token in group if token]
|
||||
+
|
||||
+ def parse(self):
|
||||
+ result = []
|
||||
+ while self.index < len(self.tokens):
|
||||
+ token = self.tokens[self.index]
|
||||
+ if token == '(':
|
||||
+ self.index += 1
|
||||
+ result.append(self.parse())
|
||||
+ elif token == ')':
|
||||
+ return result
|
||||
+ else:
|
||||
+ result.append(self.parse_atom(token))
|
||||
+ self.index += 1
|
||||
+ if len(result) == 1:
|
||||
+ return result[0]
|
||||
+ return result
|
||||
+
|
||||
+ def parse_atom(self, token):
|
||||
+ try:
|
||||
+ return int(token)
|
||||
+ except ValueError:
|
||||
+ return token
|
||||
+
|
||||
+def parse_lisp(expression):
|
||||
+ parser = LispParser(expression)
|
||||
+ return parser.parse()
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ expression = "(1 2 (3 4) a b)"
|
||||
+ print(parse_lisp(expression))
|
||||
diff --git a/tests/test_lisp.py b/tests/test_lisp.py
|
||||
new file mode 100644
|
||||
index 0000000..b5c7d8e
|
||||
--- /dev/null
|
||||
+++ b/tests/test_lisp.py
|
||||
@@ -0,0 +1,39 @@
|
||||
+import unittest
|
||||
+from lisp import parse_lisp
|
||||
+
|
||||
+class TestLispParser(unittest.TestCase):
|
||||
+ def test_parse_numbers(self):
|
||||
+ self.assertEqual(parse_lisp("123"), 123)
|
||||
+ self.assertEqual(parse_lisp("(1 2 3)"), [1, 2, 3])
|
||||
+
|
||||
+ def test_parse_symbols(self):
|
||||
+ self.assertEqual(parse_lisp("a"), "a")
|
||||
+ self.assertEqual(parse_lisp("(a b c)"), ["a", "b", "c"])
|
||||
+
|
||||
+ def test_parse_nested_lists(self):
|
||||
+ self.assertEqual(parse_lisp("(1 (2 3) 4)"), [1, [2, 3], 4])
|
||||
+ self.assertEqual(parse_lisp("((a b) (c d))"), [["a", "b"], ["c", "d"]])
|
||||
+
|
||||
+ def test_unbalanced_parentheses(self):
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp(")")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(1 2 (3 4)")
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ unittest.main()
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# Patch Apply
|
||||
|
||||
Status: fail
|
||||
Mode: apply
|
||||
Patch: `.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch`
|
||||
Command: `git apply --ignore-whitespace C:\Users\metis\Documents\tiny-lisp-nightshift\.nightshift\runs\20260517T212359.709956Z\tasks\TASK-001\applied-1.patch`
|
||||
Exit code: 128
|
||||
|
||||
## stdout
|
||||
|
||||
```text
|
||||
|
||||
```
|
||||
|
||||
## stderr
|
||||
|
||||
```text
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:6: trailing whitespace.
|
||||
import re
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:7: trailing whitespace.
|
||||
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:8: trailing whitespace.
|
||||
class LispParser:
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:9: trailing whitespace.
|
||||
def __init__(self, expression):
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:10: trailing whitespace.
|
||||
self.expression = expression
|
||||
error: corrupt patch at line 48
|
||||
```
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# Patch Apply
|
||||
|
||||
Status: fail
|
||||
Mode: apply
|
||||
Patch: `.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch`
|
||||
Command: `git apply --ignore-whitespace C:\Users\metis\Documents\tiny-lisp-nightshift\.nightshift\runs\20260517T212359.709956Z\tasks\TASK-001\applied-2.patch`
|
||||
Exit code: 128
|
||||
|
||||
## stdout
|
||||
|
||||
```text
|
||||
|
||||
```
|
||||
|
||||
## stderr
|
||||
|
||||
```text
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:6: trailing whitespace.
|
||||
import re
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:7: trailing whitespace.
|
||||
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:8: trailing whitespace.
|
||||
class LispParser:
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:9: trailing whitespace.
|
||||
def __init__(self, expression):
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:10: trailing whitespace.
|
||||
self.expression = expression
|
||||
error: corrupt patch at line 48
|
||||
```
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# Patch Apply
|
||||
|
||||
Status: fail
|
||||
Mode: apply
|
||||
Patch: `.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-3.patch`
|
||||
Command: `git apply --ignore-whitespace C:\Users\metis\Documents\tiny-lisp-nightshift\.nightshift\runs\20260517T212359.709956Z\tasks\TASK-001\applied-3.patch`
|
||||
Exit code: 128
|
||||
|
||||
## stdout
|
||||
|
||||
```text
|
||||
|
||||
```
|
||||
|
||||
## stderr
|
||||
|
||||
```text
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-3.patch:6: trailing whitespace.
|
||||
import re
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-3.patch:7: trailing whitespace.
|
||||
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-3.patch:8: trailing whitespace.
|
||||
class LispParser:
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-3.patch:9: trailing whitespace.
|
||||
def __init__(self, expression):
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-3.patch:10: trailing whitespace.
|
||||
self.expression = expression
|
||||
error: corrupt patch at line 48
|
||||
```
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# Patch Validation
|
||||
|
||||
Status: pass
|
||||
Changed files: 2
|
||||
Changed lines: 68
|
||||
|
||||
## Files
|
||||
|
||||
- `lisp.py`
|
||||
- `tests/test_lisp.py`
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# Patch Validation
|
||||
|
||||
Status: pass
|
||||
Changed files: 2
|
||||
Changed lines: 68
|
||||
|
||||
## Files
|
||||
|
||||
- `lisp.py`
|
||||
- `tests/test_lisp.py`
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# Patch Validation
|
||||
|
||||
Status: pass
|
||||
Changed files: 2
|
||||
Changed lines: 65
|
||||
|
||||
## Files
|
||||
|
||||
- `lisp.py`
|
||||
- `tests/test_lisp.py`
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# Patch Validation
|
||||
|
||||
Status: fail
|
||||
Reason: Patch validation failed: patch creates existing file `lisp.py`.
|
||||
193
.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/plan.md
Normal file
193
.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/plan.md
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
# Agent Output: plan
|
||||
|
||||
Agent: `planner`
|
||||
Command: `POST http://localhost:11434/api/generate`
|
||||
Exit code: 0
|
||||
Duration seconds: 0.942
|
||||
Timed out: false
|
||||
|
||||
## stdout
|
||||
|
||||
```text
|
||||
lookup_requests:
|
||||
- tool: read_file
|
||||
path: lisp.py
|
||||
- tool: read_file
|
||||
path: tests/test_lisp.py
|
||||
|
||||
After context is provided, write a short plan with:
|
||||
- files to edit
|
||||
- tests to add or update
|
||||
- risks
|
||||
```
|
||||
|
||||
## stderr
|
||||
|
||||
```text
|
||||
|
||||
```
|
||||
|
||||
## Prompt
|
||||
|
||||
```markdown
|
||||
# NightShift Agent Input
|
||||
|
||||
## System Prompt
|
||||
|
||||
You are the planning agent for NightShift.
|
||||
|
||||
Create a concise implementation plan for the current task.
|
||||
|
||||
If you need repository context before planning, output lookup requests exactly like this:
|
||||
|
||||
lookup_requests:
|
||||
- tool: read_file
|
||||
path: relative/path.py
|
||||
- tool: grep
|
||||
path: .
|
||||
pattern: search_regex
|
||||
|
||||
After context is provided, write a short plan with:
|
||||
- files to edit
|
||||
- tests to add or update
|
||||
- risks
|
||||
|
||||
Do not write code.
|
||||
|
||||
## Stage
|
||||
|
||||
- id: plan
|
||||
- type: agent
|
||||
|
||||
## Task
|
||||
|
||||
- [ ] TASK-001: Parse Lisp expressions
|
||||
|
||||
Description:
|
||||
Implement tokenization and parsing for a tiny Lisp subset.
|
||||
|
||||
Acceptance Criteria:
|
||||
- Parses numbers
|
||||
- Parses symbols
|
||||
- Parses nested lists
|
||||
- Raises useful errors for unbalanced parentheses
|
||||
- Includes unit tests
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Parses numbers
|
||||
- Parses symbols
|
||||
- Parses nested lists
|
||||
- Raises useful errors for unbalanced parentheses
|
||||
- Includes unit tests
|
||||
|
||||
## Project Context
|
||||
|
||||
# Project Context
|
||||
|
||||
## Task Context
|
||||
|
||||
# Task Context
|
||||
|
||||
Task: `TASK-001`
|
||||
Title: Parse Lisp expressions
|
||||
|
||||
## Description
|
||||
|
||||
Implement tokenization and parsing for a tiny Lisp subset.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Parses numbers
|
||||
- Parses symbols
|
||||
- Parses nested lists
|
||||
- Raises useful errors for unbalanced parentheses
|
||||
- Includes unit tests
|
||||
|
||||
## Previous Stage Output
|
||||
|
||||
## repo_lookup_results
|
||||
|
||||
# Repo Tool Calls
|
||||
|
||||
## 1. read_file
|
||||
|
||||
Arguments:
|
||||
- path: `lisp_parser.py`
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
File not found: lisp_parser.py
|
||||
```
|
||||
|
||||
## 2. read_file
|
||||
|
||||
Arguments:
|
||||
- path: `tests/test_lisp_parser.py`
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
File not found: tests/test_lisp_parser.py
|
||||
```
|
||||
|
||||
## 3. list_files
|
||||
|
||||
Arguments:
|
||||
- path: `.`
|
||||
- pattern: `*.py`
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
lisp.py
|
||||
tests/__init__.py
|
||||
tests/test_lisp.py
|
||||
```
|
||||
|
||||
## 4. grep
|
||||
|
||||
Arguments:
|
||||
- path: `.`
|
||||
- pattern: `def parse\(`
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
No matches found.
|
||||
```
|
||||
|
||||
## 5. grep
|
||||
|
||||
Arguments:
|
||||
- path: `.`
|
||||
- pattern: `class LispParser`
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
No matches found.
|
||||
```
|
||||
|
||||
## Retry Notes
|
||||
|
||||
- Repository lookup results have been provided. Write the final plan now; do not request more lookups.
|
||||
|
||||
## Output Contract
|
||||
|
||||
Write the requested stage output in concise markdown.
|
||||
|
||||
If you need repository context before finalizing the plan, include:
|
||||
lookup_requests:
|
||||
- tool: list_files | read_file | grep
|
||||
path: <relative path>
|
||||
pattern: <glob for list_files or regex for grep>
|
||||
|
||||
Use at most 5 lookup requests.
|
||||
Do not repeat the same lookup request.
|
||||
Prefer read_file for likely-relevant files over many grep variations.
|
||||
Do not search .nightshift, .git, virtualenvs, caches, or artifact directories.
|
||||
|
||||
NightShift will run these read-only lookup tools, save files-inspected.md, and re-run this planner stage with the retrieved context.
|
||||
```
|
||||
|
|
@ -0,0 +1,825 @@
|
|||
# Agent Output: implement
|
||||
|
||||
Agent: `implementer`
|
||||
Command: `POST http://localhost:11434/api/generate`
|
||||
Exit code: 0
|
||||
Duration seconds: 10.410
|
||||
Timed out: false
|
||||
|
||||
## stdout
|
||||
|
||||
```text
|
||||
diff --git a/lisp.py b/lisp.py
|
||||
index 0000000..f2a5c3d
|
||||
--- /dev/null
|
||||
+++ b/lisp.py
|
||||
@@ -0,0 +1,78 @@
|
||||
+import re
|
||||
+
|
||||
+class LispParser:
|
||||
+ def __init__(self, expression):
|
||||
+ self.expression = expression
|
||||
+ self.tokens = self.tokenize()
|
||||
+ self.index = 0
|
||||
+
|
||||
+ def tokenize(self):
|
||||
+ token_pattern = r'\s*([()])|(\d+)|([a-zA-Z_][a-zA-Z0-9_]*)'
|
||||
+ tokens = re.findall(token_pattern, self.expression)
|
||||
+ return [token for group in tokens for token in group if token]
|
||||
+
|
||||
+ def parse(self):
|
||||
+ result = []
|
||||
+ while self.index < len(self.tokens):
|
||||
+ token = self.tokens[self.index]
|
||||
+ if token == '(':
|
||||
+ self.index += 1
|
||||
+ result.append(self.parse())
|
||||
+ elif token == ')':
|
||||
+ return result
|
||||
+ else:
|
||||
+ result.append(self.parse_atom(token))
|
||||
+ self.index += 1
|
||||
+ if len(result) == 1:
|
||||
+ return result[0]
|
||||
+ return result
|
||||
+
|
||||
+ def parse_atom(self, token):
|
||||
+ try:
|
||||
+ return int(token)
|
||||
+ except ValueError:
|
||||
+ return token
|
||||
+
|
||||
+def parse_lisp(expression):
|
||||
+ parser = LispParser(expression)
|
||||
+ return parser.parse()
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ expression = "(1 2 (3 4) a b)"
|
||||
+ print(parse_lisp(expression))
|
||||
diff --git a/tests/test_lisp.py b/tests/test_lisp.py
|
||||
index 0000000..b5c7d8e
|
||||
--- /dev/null
|
||||
+++ b/tests/test_lisp.py
|
||||
@@ -0,0 +1,39 @@
|
||||
+import unittest
|
||||
+from lisp import parse_lisp
|
||||
+
|
||||
+class TestLispParser(unittest.TestCase):
|
||||
+ def test_parse_numbers(self):
|
||||
+ self.assertEqual(parse_lisp("123"), 123)
|
||||
+ self.assertEqual(parse_lisp("(1 2 3)"), [1, 2, 3])
|
||||
+
|
||||
+ def test_parse_symbols(self):
|
||||
+ self.assertEqual(parse_lisp("a"), "a")
|
||||
+ self.assertEqual(parse_lisp("(a b c)"), ["a", "b", "c"])
|
||||
+
|
||||
+ def test_parse_nested_lists(self):
|
||||
+ self.assertEqual(parse_lisp("(1 (2 3) 4)"), [1, [2, 3], 4])
|
||||
+ self.assertEqual(parse_lisp("((a b) (c d))"), [["a", "b"], ["c", "d"]])
|
||||
+
|
||||
+ def test_unbalanced_parentheses(self):
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp(")")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(1 2 (3 4)")
|
||||
```
|
||||
|
||||
## stderr
|
||||
|
||||
```text
|
||||
|
||||
```
|
||||
|
||||
## Prompt
|
||||
|
||||
```markdown
|
||||
# NightShift Agent Input
|
||||
|
||||
## System Prompt
|
||||
|
||||
You are the implementation agent for NightShift.
|
||||
|
||||
Output only a unified diff.
|
||||
Do not wrap the patch in markdown fences.
|
||||
Do not include explanations before or after the patch.
|
||||
Use diff --git headers.
|
||||
Include tests when needed.
|
||||
Keep the change as small as possible.
|
||||
Only edit files needed for the task.
|
||||
|
||||
## Stage
|
||||
|
||||
- id: implement
|
||||
- type: code_writer
|
||||
|
||||
## Task
|
||||
|
||||
- [ ] TASK-001: Parse Lisp expressions
|
||||
|
||||
Description:
|
||||
Implement tokenization and parsing for a tiny Lisp subset.
|
||||
|
||||
Acceptance Criteria:
|
||||
- Parses numbers
|
||||
- Parses symbols
|
||||
- Parses nested lists
|
||||
- Raises useful errors for unbalanced parentheses
|
||||
- Includes unit tests
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Parses numbers
|
||||
- Parses symbols
|
||||
- Parses nested lists
|
||||
- Raises useful errors for unbalanced parentheses
|
||||
- Includes unit tests
|
||||
|
||||
## Project Context
|
||||
|
||||
# Project Context
|
||||
|
||||
## Task Context
|
||||
|
||||
# Task Context
|
||||
|
||||
Task: `TASK-001`
|
||||
Title: Parse Lisp expressions
|
||||
|
||||
## Description
|
||||
|
||||
Implement tokenization and parsing for a tiny Lisp subset.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Parses numbers
|
||||
- Parses symbols
|
||||
- Parses nested lists
|
||||
- Raises useful errors for unbalanced parentheses
|
||||
- Includes unit tests
|
||||
|
||||
## Previous Stage Output
|
||||
|
||||
## plan
|
||||
|
||||
# Agent Output: plan
|
||||
|
||||
Agent: `planner`
|
||||
Command: `POST http://localhost:11434/api/generate`
|
||||
Exit code: 0
|
||||
Duration seconds: 0.942
|
||||
Timed out: false
|
||||
|
||||
## stdout
|
||||
|
||||
```text
|
||||
lookup_requests:
|
||||
- tool: read_file
|
||||
path: lisp.py
|
||||
- tool: read_file
|
||||
path: tests/test_lisp.py
|
||||
|
||||
After context is provided, write a short plan with:
|
||||
- files to edit
|
||||
- tests to add or update
|
||||
- risks
|
||||
```
|
||||
|
||||
## stderr
|
||||
|
||||
```text
|
||||
|
||||
```
|
||||
|
||||
## Prompt
|
||||
|
||||
```markdown
|
||||
# NightShift Agent Input
|
||||
|
||||
## System Prompt
|
||||
|
||||
You are the planning agent for NightShift.
|
||||
|
||||
Create a concise implementation plan for the current task.
|
||||
|
||||
If you need repository context before planning, output lookup requests exactly like this:
|
||||
|
||||
lookup_requests:
|
||||
- tool: read_file
|
||||
path: relative/path.py
|
||||
- tool: grep
|
||||
path: .
|
||||
pattern: search_regex
|
||||
|
||||
After context is provided, write a short plan with:
|
||||
- files to edit
|
||||
- tests to add or update
|
||||
- risks
|
||||
|
||||
Do not write code.
|
||||
|
||||
## Stage
|
||||
|
||||
- id: plan
|
||||
- type: agent
|
||||
|
||||
## Task
|
||||
|
||||
- [ ] TASK-001: Parse Lisp expressions
|
||||
|
||||
Description:
|
||||
Implement tokenization and parsing for a tiny Lisp subset.
|
||||
|
||||
Acceptance Criteria:
|
||||
- Parses numbers
|
||||
- Parses symbols
|
||||
- Parses nested lists
|
||||
- Raises useful errors for unbalanced parentheses
|
||||
- Includes unit tests
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Parses numbers
|
||||
- Parses symbols
|
||||
- Parses nested lists
|
||||
- Raises useful errors for unbalanced parentheses
|
||||
- Includes unit tests
|
||||
|
||||
## Project Context
|
||||
|
||||
# Project Context
|
||||
|
||||
## Task Context
|
||||
|
||||
# Task Context
|
||||
|
||||
Task: `TASK-001`
|
||||
Title: Parse Lisp expressions
|
||||
|
||||
## Description
|
||||
|
||||
Implement tokenization and parsing for a tiny Lisp subset.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Parses numbers
|
||||
- Parses symbols
|
||||
- Parses nested lists
|
||||
- Raises useful errors for unbalanced parentheses
|
||||
- Includes unit tests
|
||||
|
||||
## Previous Stage Output
|
||||
|
||||
## repo_lookup_results
|
||||
|
||||
# Repo Tool Calls
|
||||
|
||||
## 1. read_file
|
||||
|
||||
Arguments:
|
||||
- path: `lisp_parser.py`
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
File not found: lisp_parser.py
|
||||
```
|
||||
|
||||
## 2. read_file
|
||||
|
||||
Arguments:
|
||||
- path: `tests/test_lisp_parser.py`
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
File not found: tests/test_lisp_parser.py
|
||||
```
|
||||
|
||||
## 3. list_files
|
||||
|
||||
Arguments:
|
||||
- path: `.`
|
||||
- pattern: `*.py`
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
lisp.py
|
||||
tests/__init__.py
|
||||
tests/test_lisp.py
|
||||
```
|
||||
|
||||
## 4. grep
|
||||
|
||||
Arguments:
|
||||
- path: `.`
|
||||
- pattern: `def parse\(`
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
No matches found.
|
||||
```
|
||||
|
||||
## 5. grep
|
||||
|
||||
Arguments:
|
||||
- path: `.`
|
||||
- pattern: `class LispParser`
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
No matches found.
|
||||
```
|
||||
|
||||
## Retry Notes
|
||||
|
||||
- Repository lookup results have been provided. Write the final plan now; do not request more lookups.
|
||||
|
||||
## Output Contract
|
||||
|
||||
Write the requested stage output in concise markdown.
|
||||
|
||||
If you need repository context before finalizing the plan, include:
|
||||
lookup_requests:
|
||||
- tool: list_files | read_file | grep
|
||||
path: <relative path>
|
||||
pattern: <glob for list_files or regex for grep>
|
||||
|
||||
Use at most 5 lookup requests.
|
||||
Do not repeat the same lookup request.
|
||||
Prefer read_file for likely-relevant files over many grep variations.
|
||||
Do not search .nightshift, .git, virtualenvs, caches, or artifact directories.
|
||||
|
||||
NightShift will run these read-only lookup tools, save files-inspected.md, and re-run this planner stage with the retrieved context.
|
||||
```
|
||||
|
||||
|
||||
## context
|
||||
|
||||
# Context Pack
|
||||
|
||||
Task: `TASK-001`
|
||||
Title: Parse Lisp expressions
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Parses numbers
|
||||
- Parses symbols
|
||||
- Parses nested lists
|
||||
- Raises useful errors for unbalanced parentheses
|
||||
- Includes unit tests
|
||||
|
||||
## Constraints
|
||||
|
||||
- Scoped paths: .
|
||||
- Repository lookups are read-only.
|
||||
- Excerpts are line-numbered where files are read directly.
|
||||
|
||||
## Relevant Files
|
||||
|
||||
```text
|
||||
lisp.py
|
||||
tests/__init__.py
|
||||
tests/test_lisp.py
|
||||
```
|
||||
|
||||
## Search Results
|
||||
|
||||
### Search: parse
|
||||
|
||||
```text
|
||||
tasks.md:21: Evaluate parsed arithmetic expressions.
|
||||
```
|
||||
|
||||
### Search: lisp
|
||||
|
||||
```text
|
||||
nightshift.yaml:2: name: tiny-lisp
|
||||
```
|
||||
|
||||
### Search: expressions
|
||||
|
||||
```text
|
||||
tasks.md:3: - [ ] TASK-001: Parse Lisp expressions
|
||||
tasks.md:21: Evaluate parsed arithmetic expressions.
|
||||
tasks.md:47: Implement simple truthiness and `if` expressions.
|
||||
```
|
||||
|
||||
### Search: parses
|
||||
|
||||
```text
|
||||
No matches found.
|
||||
```
|
||||
|
||||
### Search: numbers
|
||||
|
||||
```text
|
||||
tasks.md:9: - Parses numbers
|
||||
```
|
||||
|
||||
|
||||
## implement
|
||||
|
||||
diff --git a/lisp.py b/lisp.py
|
||||
index 0000000..f2a5c3d
|
||||
--- /dev/null
|
||||
+++ b/lisp.py
|
||||
@@ -0,0 +1,78 @@
|
||||
+import re
|
||||
+
|
||||
+class LispParser:
|
||||
+ def __init__(self, expression):
|
||||
+ self.expression = expression
|
||||
+ self.tokens = self.tokenize()
|
||||
+ self.index = 0
|
||||
+
|
||||
+ def tokenize(self):
|
||||
+ token_pattern = r'\s*([()])|(\d+)|([a-zA-Z_][a-zA-Z0-9_]*)'
|
||||
+ tokens = re.findall(token_pattern, self.expression)
|
||||
+ return [token for group in tokens for token in group if token]
|
||||
+
|
||||
+ def parse(self):
|
||||
+ result = []
|
||||
+ while self.index < len(self.tokens):
|
||||
+ token = self.tokens[self.index]
|
||||
+ if token == '(':
|
||||
+ self.index += 1
|
||||
+ result.append(self.parse())
|
||||
+ elif token == ')':
|
||||
+ return result
|
||||
+ else:
|
||||
+ result.append(self.parse_atom(token))
|
||||
+ self.index += 1
|
||||
+ if len(result) == 1:
|
||||
+ return result[0]
|
||||
+ return result
|
||||
+
|
||||
+ def parse_atom(self, token):
|
||||
+ try:
|
||||
+ return int(token)
|
||||
+ except ValueError:
|
||||
+ return token
|
||||
+
|
||||
+def parse_lisp(expression):
|
||||
+ parser = LispParser(expression)
|
||||
+ return parser.parse()
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ expression = "(1 2 (3 4) a b)"
|
||||
+ print(parse_lisp(expression))
|
||||
diff --git a/tests/test_lisp.py b/tests/test_lisp.py
|
||||
index 0000000..b5c7d8e
|
||||
--- /dev/null
|
||||
+++ b/tests/test_lisp.py
|
||||
@@ -0,0 +1,39 @@
|
||||
+import unittest
|
||||
+from lisp import parse_lisp
|
||||
+
|
||||
+class TestLispParser(unittest.TestCase):
|
||||
+ def test_parse_numbers(self):
|
||||
+ self.assertEqual(parse_lisp("123"), 123)
|
||||
+ self.assertEqual(parse_lisp("(1 2 3)"), [1, 2, 3])
|
||||
+
|
||||
+ def test_parse_symbols(self):
|
||||
+ self.assertEqual(parse_lisp("a"), "a")
|
||||
+ self.assertEqual(parse_lisp("(a b c)"), ["a", "b", "c"])
|
||||
+
|
||||
+ def test_parse_nested_lists(self):
|
||||
+ self.assertEqual(parse_lisp("(1 (2 3) 4)"), [1, [2, 3], 4])
|
||||
+ self.assertEqual(parse_lisp("((a b) (c d))"), [["a", "b"], ["c", "d"]])
|
||||
+
|
||||
+ def test_unbalanced_parentheses(self):
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp(")")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(1 2 (3 4)")
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ unittest.main()
|
||||
|
||||
|
||||
## normalize
|
||||
|
||||
diff --git a/lisp.py b/lisp.py
|
||||
index 0000000..f2a5c3d
|
||||
--- /dev/null
|
||||
+++ b/lisp.py
|
||||
@@ -0,0 +1,78 @@
|
||||
+import re
|
||||
+
|
||||
+class LispParser:
|
||||
+ def __init__(self, expression):
|
||||
+ self.expression = expression
|
||||
+ self.tokens = self.tokenize()
|
||||
+ self.index = 0
|
||||
+
|
||||
+ def tokenize(self):
|
||||
+ token_pattern = r'\s*([()])|(\d+)|([a-zA-Z_][a-zA-Z0-9_]*)'
|
||||
+ tokens = re.findall(token_pattern, self.expression)
|
||||
+ return [token for group in tokens for token in group if token]
|
||||
+
|
||||
+ def parse(self):
|
||||
+ result = []
|
||||
+ while self.index < len(self.tokens):
|
||||
+ token = self.tokens[self.index]
|
||||
+ if token == '(':
|
||||
+ self.index += 1
|
||||
+ result.append(self.parse())
|
||||
+ elif token == ')':
|
||||
+ return result
|
||||
+ else:
|
||||
+ result.append(self.parse_atom(token))
|
||||
+ self.index += 1
|
||||
+ if len(result) == 1:
|
||||
+ return result[0]
|
||||
+ return result
|
||||
+
|
||||
+ def parse_atom(self, token):
|
||||
+ try:
|
||||
+ return int(token)
|
||||
+ except ValueError:
|
||||
+ return token
|
||||
+
|
||||
+def parse_lisp(expression):
|
||||
+ parser = LispParser(expression)
|
||||
+ return parser.parse()
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ expression = "(1 2 (3 4) a b)"
|
||||
+ print(parse_lisp(expression))
|
||||
diff --git a/tests/test_lisp.py b/tests/test_lisp.py
|
||||
index 0000000..b5c7d8e
|
||||
--- /dev/null
|
||||
+++ b/tests/test_lisp.py
|
||||
@@ -0,0 +1,39 @@
|
||||
+import unittest
|
||||
+from lisp import parse_lisp
|
||||
+
|
||||
+class TestLispParser(unittest.TestCase):
|
||||
+ def test_parse_numbers(self):
|
||||
+ self.assertEqual(parse_lisp("123"), 123)
|
||||
+ self.assertEqual(parse_lisp("(1 2 3)"), [1, 2, 3])
|
||||
+
|
||||
+ def test_parse_symbols(self):
|
||||
+ self.assertEqual(parse_lisp("a"), "a")
|
||||
+ self.assertEqual(parse_lisp("(a b c)"), ["a", "b", "c"])
|
||||
+
|
||||
+ def test_parse_nested_lists(self):
|
||||
+ self.assertEqual(parse_lisp("(1 (2 3) 4)"), [1, [2, 3], 4])
|
||||
+ self.assertEqual(parse_lisp("((a b) (c d))"), [["a", "b"], ["c", "d"]])
|
||||
+
|
||||
+ def test_unbalanced_parentheses(self):
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp(")")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(1 2 (3 4)")
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ unittest.main()
|
||||
|
||||
|
||||
## validate_patch
|
||||
|
||||
# Patch Validation
|
||||
|
||||
Status: pass
|
||||
Changed files: 2
|
||||
Changed lines: 68
|
||||
|
||||
## Files
|
||||
|
||||
- `lisp.py`
|
||||
- `tests/test_lisp.py`
|
||||
|
||||
|
||||
## apply_patch
|
||||
|
||||
# Patch Apply
|
||||
|
||||
Status: fail
|
||||
Mode: apply
|
||||
Patch: `.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch`
|
||||
Command: `git apply --ignore-whitespace C:\Users\metis\Documents\tiny-lisp-nightshift\.nightshift\runs\20260517T212359.709956Z\tasks\TASK-001\applied-2.patch`
|
||||
Exit code: 128
|
||||
|
||||
## stdout
|
||||
|
||||
```text
|
||||
|
||||
```
|
||||
|
||||
## stderr
|
||||
|
||||
```text
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:6: trailing whitespace.
|
||||
import re
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:7: trailing whitespace.
|
||||
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:8: trailing whitespace.
|
||||
class LispParser:
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:9: trailing whitespace.
|
||||
def __init__(self, expression):
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:10: trailing whitespace.
|
||||
self.expression = expression
|
||||
error: corrupt patch at line 48
|
||||
```
|
||||
|
||||
|
||||
## context-pack.md
|
||||
|
||||
# Context Pack
|
||||
|
||||
Task: `TASK-001`
|
||||
Title: Parse Lisp expressions
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Parses numbers
|
||||
- Parses symbols
|
||||
- Parses nested lists
|
||||
- Raises useful errors for unbalanced parentheses
|
||||
- Includes unit tests
|
||||
|
||||
## Constraints
|
||||
|
||||
- Scoped paths: .
|
||||
- Repository lookups are read-only.
|
||||
- Excerpts are line-numbered where files are read directly.
|
||||
|
||||
## Relevant Files
|
||||
|
||||
```text
|
||||
lisp.py
|
||||
tests/__init__.py
|
||||
tests/test_lisp.py
|
||||
```
|
||||
|
||||
## Search Results
|
||||
|
||||
### Search: parse
|
||||
|
||||
```text
|
||||
tasks.md:21: Evaluate parsed arithmetic expressions.
|
||||
```
|
||||
|
||||
### Search: lisp
|
||||
|
||||
```text
|
||||
nightshift.yaml:2: name: tiny-lisp
|
||||
```
|
||||
|
||||
### Search: expressions
|
||||
|
||||
```text
|
||||
tasks.md:3: - [ ] TASK-001: Parse Lisp expressions
|
||||
tasks.md:21: Evaluate parsed arithmetic expressions.
|
||||
tasks.md:47: Implement simple truthiness and `if` expressions.
|
||||
```
|
||||
|
||||
### Search: parses
|
||||
|
||||
```text
|
||||
No matches found.
|
||||
```
|
||||
|
||||
### Search: numbers
|
||||
|
||||
```text
|
||||
tasks.md:9: - Parses numbers
|
||||
```
|
||||
|
||||
|
||||
## project-context-chart.md
|
||||
|
||||
# Project Context Chart
|
||||
|
||||
## Entry Points
|
||||
|
||||
- `tests/test_lisp.py`: class SmokeTests(unittest.TestCase):
|
||||
|
||||
## Tests
|
||||
|
||||
- `tests/__init__.py`
|
||||
- `tests/test_lisp.py`
|
||||
|
||||
## Files
|
||||
|
||||
### `agents/implementer.md`
|
||||
|
||||
- Responsibility: You are the implementation agent for NightShift.
|
||||
- Entry point: false
|
||||
- Test file: false
|
||||
- Functions: None detected
|
||||
- Classes: None detected
|
||||
- Anchors/search terms: None detected
|
||||
|
||||
### `agents/planner.md`
|
||||
|
||||
- Responsibility: You are the planning agent for NightShift.
|
||||
- Entry point: false
|
||||
- Test file: false
|
||||
- Functions: None detected
|
||||
- Classes: None detected
|
||||
- Anchors/search terms: None detected
|
||||
|
||||
### `agents/reviewer.md`
|
||||
|
||||
- Responsibility: You are the review agent for NightShift.
|
||||
- Entry point: false
|
||||
- Test file: false
|
||||
- Functions: None detected
|
||||
- Classes: None detected
|
||||
- Anchors/search terms: None detected
|
||||
|
||||
### `lisp.py`
|
||||
|
||||
- Responsibility: """Tiny Lisp quickstart target.
|
||||
- Entry point: false
|
||||
- Test file: false
|
||||
- Functions: None detected
|
||||
- Classes: None detected
|
||||
- Anchors/search terms: None detected
|
||||
|
||||
### `nightshift.yaml`
|
||||
|
||||
- Responsibility: project:
|
||||
- Entry point: false
|
||||
- Test file: false
|
||||
- Functions: None detected
|
||||
- Classes: None detected
|
||||
- Anchors/search terms: None detected
|
||||
|
||||
### `tasks.md`
|
||||
|
||||
- Responsibility: Tasks
|
||||
- Entry point: false
|
||||
- Test file: false
|
||||
- Functions: None detected
|
||||
- Classes: None detected
|
||||
- Anchors/search terms: None detected
|
||||
|
||||
### `tests/__init__.py`
|
||||
|
||||
- Responsibility: """Quickstart Lisp test suite."""
|
||||
- Entry point: false
|
||||
- Test file: true
|
||||
- Functions: None detected
|
||||
- Classes: None detected
|
||||
- Anchors/search terms: None detected
|
||||
|
||||
### `tests/test_lisp.py`
|
||||
|
||||
- Responsibility: class SmokeTests(unittest.TestCase):
|
||||
- Entry point: true
|
||||
- Test file: true
|
||||
- Functions: test_smoke@L5
|
||||
- Classes: SmokeTests@L4
|
||||
- Anchors/search terms: SmokeTests, test_smoke
|
||||
|
||||
## Retry Notes
|
||||
|
||||
- Context update from 'implement': Implementation summary: .nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/implementation-summary.md
|
||||
- Retry 1: stage 'validate_patch' returned fail (Patch validation failed: patch creates existing file `lisp.py`.); redirecting to 'implement'.
|
||||
- Context update from 'implement': Implementation summary: .nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/repair-summary-1.md
|
||||
- Context update from 'apply_patch': C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:6: trailing whitespace.
|
||||
import re
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:7: trailing whitespace.
|
||||
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:8: trailing whitespace.
|
||||
class LispParser:
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:9: trailing whitespace.
|
||||
def __init__(self, expression):
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:10: trailing whitespace.
|
||||
self.expression = expression
|
||||
error: corrupt patch at line 48
|
||||
- Retry 2: stage 'apply_patch' returned fail (Patch apply failed with code 128.); redirecting to 'implement'.
|
||||
- Context update from 'implement': Implementation summary: .nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/repair-summary-2.md
|
||||
- Context update from 'apply_patch': C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:6: trailing whitespace.
|
||||
import re
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:7: trailing whitespace.
|
||||
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:8: trailing whitespace.
|
||||
class LispParser:
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:9: trailing whitespace.
|
||||
def __init__(self, expression):
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:10: trailing whitespace.
|
||||
self.expression = expression
|
||||
error: corrupt patch at line 48
|
||||
- Retry 3: stage 'apply_patch' returned fail (Patch apply failed with code 128.); redirecting to 'implement'.
|
||||
|
||||
## Output Contract
|
||||
|
||||
Return a unified diff only, suitable for saving as proposed.patch or repair-N.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.
|
||||
On repair attempts, return a complete corrected replacement diff.
|
||||
```
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
diff --git a/lisp.py b/lisp.py
|
||||
index 0000000..f2a5c3d
|
||||
--- /dev/null
|
||||
+++ b/lisp.py
|
||||
@@ -0,0 +1,78 @@
|
||||
+import re
|
||||
+
|
||||
+class LispParser:
|
||||
+ def __init__(self, expression):
|
||||
+ self.expression = expression
|
||||
+ self.tokens = self.tokenize()
|
||||
+ self.index = 0
|
||||
+
|
||||
+ def tokenize(self):
|
||||
+ token_pattern = r'\s*([()])|(\d+)|([a-zA-Z_][a-zA-Z0-9_]*)'
|
||||
+ tokens = re.findall(token_pattern, self.expression)
|
||||
+ return [token for group in tokens for token in group if token]
|
||||
+
|
||||
+ def parse(self):
|
||||
+ result = []
|
||||
+ while self.index < len(self.tokens):
|
||||
+ token = self.tokens[self.index]
|
||||
+ if token == '(':
|
||||
+ self.index += 1
|
||||
+ result.append(self.parse())
|
||||
+ elif token == ')':
|
||||
+ return result
|
||||
+ else:
|
||||
+ result.append(self.parse_atom(token))
|
||||
+ self.index += 1
|
||||
+ if len(result) == 1:
|
||||
+ return result[0]
|
||||
+ return result
|
||||
+
|
||||
+ def parse_atom(self, token):
|
||||
+ try:
|
||||
+ return int(token)
|
||||
+ except ValueError:
|
||||
+ return token
|
||||
+
|
||||
+def parse_lisp(expression):
|
||||
+ parser = LispParser(expression)
|
||||
+ return parser.parse()
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ expression = "(1 2 (3 4) a b)"
|
||||
+ print(parse_lisp(expression))
|
||||
diff --git a/tests/test_lisp.py b/tests/test_lisp.py
|
||||
index 0000000..b5c7d8e
|
||||
--- /dev/null
|
||||
+++ b/tests/test_lisp.py
|
||||
@@ -0,0 +1,39 @@
|
||||
+import unittest
|
||||
+from lisp import parse_lisp
|
||||
+
|
||||
+class TestLispParser(unittest.TestCase):
|
||||
+ def test_parse_numbers(self):
|
||||
+ self.assertEqual(parse_lisp("123"), 123)
|
||||
+ self.assertEqual(parse_lisp("(1 2 3)"), [1, 2, 3])
|
||||
+
|
||||
+ def test_parse_symbols(self):
|
||||
+ self.assertEqual(parse_lisp("a"), "a")
|
||||
+ self.assertEqual(parse_lisp("(a b c)"), ["a", "b", "c"])
|
||||
+
|
||||
+ def test_parse_nested_lists(self):
|
||||
+ self.assertEqual(parse_lisp("(1 (2 3) 4)"), [1, [2, 3], 4])
|
||||
+ self.assertEqual(parse_lisp("((a b) (c d))"), [["a", "b"], ["c", "d"]])
|
||||
+
|
||||
+ def test_unbalanced_parentheses(self):
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp(")")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(1 2 (3 4)")
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ unittest.main()
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
diff --git a/lisp.py b/lisp.py
|
||||
index 0000000..f2a5c3d
|
||||
--- /dev/null
|
||||
+++ b/lisp.py
|
||||
@@ -0,0 +1,78 @@
|
||||
+import re
|
||||
+
|
||||
+class LispParser:
|
||||
+ def __init__(self, expression):
|
||||
+ self.expression = expression
|
||||
+ self.tokens = self.tokenize()
|
||||
+ self.index = 0
|
||||
+
|
||||
+ def tokenize(self):
|
||||
+ token_pattern = r'\s*([()])|(\d+)|([a-zA-Z_][a-zA-Z0-9_]*)'
|
||||
+ tokens = re.findall(token_pattern, self.expression)
|
||||
+ return [token for group in tokens for token in group if token]
|
||||
+
|
||||
+ def parse(self):
|
||||
+ result = []
|
||||
+ while self.index < len(self.tokens):
|
||||
+ token = self.tokens[self.index]
|
||||
+ if token == '(':
|
||||
+ self.index += 1
|
||||
+ result.append(self.parse())
|
||||
+ elif token == ')':
|
||||
+ return result
|
||||
+ else:
|
||||
+ result.append(self.parse_atom(token))
|
||||
+ self.index += 1
|
||||
+ if len(result) == 1:
|
||||
+ return result[0]
|
||||
+ return result
|
||||
+
|
||||
+ def parse_atom(self, token):
|
||||
+ try:
|
||||
+ return int(token)
|
||||
+ except ValueError:
|
||||
+ return token
|
||||
+
|
||||
+def parse_lisp(expression):
|
||||
+ parser = LispParser(expression)
|
||||
+ return parser.parse()
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ expression = "(1 2 (3 4) a b)"
|
||||
+ print(parse_lisp(expression))
|
||||
diff --git a/tests/test_lisp.py b/tests/test_lisp.py
|
||||
index 0000000..b5c7d8e
|
||||
--- /dev/null
|
||||
+++ b/tests/test_lisp.py
|
||||
@@ -0,0 +1,39 @@
|
||||
+import unittest
|
||||
+from lisp import parse_lisp
|
||||
+
|
||||
+class TestLispParser(unittest.TestCase):
|
||||
+ def test_parse_numbers(self):
|
||||
+ self.assertEqual(parse_lisp("123"), 123)
|
||||
+ self.assertEqual(parse_lisp("(1 2 3)"), [1, 2, 3])
|
||||
+
|
||||
+ def test_parse_symbols(self):
|
||||
+ self.assertEqual(parse_lisp("a"), "a")
|
||||
+ self.assertEqual(parse_lisp("(a b c)"), ["a", "b", "c"])
|
||||
+
|
||||
+ def test_parse_nested_lists(self):
|
||||
+ self.assertEqual(parse_lisp("(1 (2 3) 4)"), [1, [2, 3], 4])
|
||||
+ self.assertEqual(parse_lisp("((a b) (c d))"), [["a", "b"], ["c", "d"]])
|
||||
+
|
||||
+ def test_unbalanced_parentheses(self):
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp(")")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(1 2 (3 4)")
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ unittest.main()
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
diff --git a/lisp.py b/lisp.py
|
||||
index 0000000..f2a5c3d
|
||||
--- /dev/null
|
||||
+++ b/lisp.py
|
||||
@@ -0,0 +1,78 @@
|
||||
+import re
|
||||
+
|
||||
+class LispParser:
|
||||
+ def __init__(self, expression):
|
||||
+ self.expression = expression
|
||||
+ self.tokens = self.tokenize()
|
||||
+ self.index = 0
|
||||
+
|
||||
+ def tokenize(self):
|
||||
+ token_pattern = r'\s*([()])|(\d+)|([a-zA-Z_][a-zA-Z0-9_]*)'
|
||||
+ tokens = re.findall(token_pattern, self.expression)
|
||||
+ return [token for group in tokens for token in group if token]
|
||||
+
|
||||
+ def parse(self):
|
||||
+ result = []
|
||||
+ while self.index < len(self.tokens):
|
||||
+ token = self.tokens[self.index]
|
||||
+ if token == '(':
|
||||
+ self.index += 1
|
||||
+ result.append(self.parse())
|
||||
+ elif token == ')':
|
||||
+ return result
|
||||
+ else:
|
||||
+ result.append(self.parse_atom(token))
|
||||
+ self.index += 1
|
||||
+ if len(result) == 1:
|
||||
+ return result[0]
|
||||
+ return result
|
||||
+
|
||||
+ def parse_atom(self, token):
|
||||
+ try:
|
||||
+ return int(token)
|
||||
+ except ValueError:
|
||||
+ return token
|
||||
+
|
||||
+def parse_lisp(expression):
|
||||
+ parser = LispParser(expression)
|
||||
+ return parser.parse()
|
||||
+
|
||||
+if __name__ == "__main__":
|
||||
+ expression = "(1 2 (3 4) a b)"
|
||||
+ print(parse_lisp(expression))
|
||||
diff --git a/tests/test_lisp.py b/tests/test_lisp.py
|
||||
index 0000000..b5c7d8e
|
||||
--- /dev/null
|
||||
+++ b/tests/test_lisp.py
|
||||
@@ -0,0 +1,39 @@
|
||||
+import unittest
|
||||
+from lisp import parse_lisp
|
||||
+
|
||||
+class TestLispParser(unittest.TestCase):
|
||||
+ def test_parse_numbers(self):
|
||||
+ self.assertEqual(parse_lisp("123"), 123)
|
||||
+ self.assertEqual(parse_lisp("(1 2 3)"), [1, 2, 3])
|
||||
+
|
||||
+ def test_parse_symbols(self):
|
||||
+ self.assertEqual(parse_lisp("a"), "a")
|
||||
+ self.assertEqual(parse_lisp("(a b c)"), ["a", "b", "c"])
|
||||
+
|
||||
+ def test_parse_nested_lists(self):
|
||||
+ self.assertEqual(parse_lisp("(1 (2 3) 4)"), [1, [2, 3], 4])
|
||||
+ self.assertEqual(parse_lisp("((a b) (c d))"), [["a", "b"], ["c", "d"]])
|
||||
+
|
||||
+ def test_unbalanced_parentheses(self):
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp(")")
|
||||
+ with self.assertRaises(IndexError):
|
||||
+ parse_lisp("(1 2 (3 4)")
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# Implementation Summary
|
||||
|
||||
Stage: `implement`
|
||||
Status: pass
|
||||
Repair attempt: 1
|
||||
Patch: `.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/repair-1.patch`
|
||||
|
||||
## Retry Feedback
|
||||
|
||||
- Context update from 'implement': Implementation summary: .nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/implementation-summary.md
|
||||
- Retry 1: stage 'validate_patch' returned fail (Patch validation failed: patch creates existing file `lisp.py`.); redirecting to 'implement'.
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Implementation Summary
|
||||
|
||||
Stage: `implement`
|
||||
Status: pass
|
||||
Repair attempt: 2
|
||||
Patch: `.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/repair-2.patch`
|
||||
|
||||
## Retry Feedback
|
||||
|
||||
- Context update from 'implement': Implementation summary: .nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/implementation-summary.md
|
||||
- Retry 1: stage 'validate_patch' returned fail (Patch validation failed: patch creates existing file `lisp.py`.); redirecting to 'implement'.
|
||||
- Context update from 'implement': Implementation summary: .nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/repair-summary-1.md
|
||||
- Context update from 'apply_patch': C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:6: trailing whitespace.
|
||||
import re
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:7: trailing whitespace.
|
||||
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:8: trailing whitespace.
|
||||
class LispParser:
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:9: trailing whitespace.
|
||||
def __init__(self, expression):
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:10: trailing whitespace.
|
||||
self.expression = expression
|
||||
error: corrupt patch at line 48
|
||||
- Retry 2: stage 'apply_patch' returned fail (Patch apply failed with code 128.); redirecting to 'implement'.
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# Implementation Summary
|
||||
|
||||
Stage: `implement`
|
||||
Status: pass
|
||||
Repair attempt: 3
|
||||
Patch: `.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/repair-3.patch`
|
||||
|
||||
## Retry Feedback
|
||||
|
||||
- Context update from 'apply_patch': C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:6: trailing whitespace.
|
||||
import re
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:7: trailing whitespace.
|
||||
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:8: trailing whitespace.
|
||||
class LispParser:
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:9: trailing whitespace.
|
||||
def __init__(self, expression):
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:10: trailing whitespace.
|
||||
self.expression = expression
|
||||
error: corrupt patch at line 48
|
||||
- Retry 2: stage 'apply_patch' returned fail (Patch apply failed with code 128.); redirecting to 'implement'.
|
||||
- Context update from 'implement': Implementation summary: .nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/repair-summary-2.md
|
||||
- Context update from 'apply_patch': C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:6: trailing whitespace.
|
||||
import re
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:7: trailing whitespace.
|
||||
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:8: trailing whitespace.
|
||||
class LispParser:
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:9: trailing whitespace.
|
||||
def __init__(self, expression):
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:10: trailing whitespace.
|
||||
self.expression = expression
|
||||
error: corrupt patch at line 48
|
||||
- Retry 3: stage 'apply_patch' returned fail (Patch apply failed with code 128.); redirecting to 'implement'.
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
# Stage Results
|
||||
|
||||
Task: `TASK-001`
|
||||
Status: failed
|
||||
Retry count: 3
|
||||
Reason: Retry limit reached after stage 'apply_patch': Patch apply failed with code 128.
|
||||
|
||||
## plan
|
||||
|
||||
Status: pass
|
||||
Reason: Agent completed after repo lookup.
|
||||
Output: .nightshift\runs\20260517T212359.709956Z\tasks\TASK-001\plan.md
|
||||
Next stage:
|
||||
Context update:
|
||||
|
||||
## context
|
||||
|
||||
Status: pass
|
||||
Reason: Context pack written.
|
||||
Output: .nightshift\runs\20260517T212359.709956Z\tasks\TASK-001\context-pack.md
|
||||
Next stage:
|
||||
Context update:
|
||||
|
||||
## implement
|
||||
|
||||
Status: pass
|
||||
Reason: Proposed patch written.
|
||||
Output: .nightshift\runs\20260517T212359.709956Z\tasks\TASK-001\proposed.patch
|
||||
Next stage:
|
||||
Context update: Implementation summary: .nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/implementation-summary.md
|
||||
|
||||
## normalize
|
||||
|
||||
Status: pass
|
||||
Reason: Normalized patch written.
|
||||
Output: .nightshift\runs\20260517T212359.709956Z\tasks\TASK-001\normalized.patch
|
||||
Next stage:
|
||||
Context update:
|
||||
|
||||
## validate_patch
|
||||
|
||||
Status: fail
|
||||
Reason: Patch validation failed: patch creates existing file `lisp.py`.
|
||||
Output: .nightshift\runs\20260517T212359.709956Z\tasks\TASK-001\patch-validation.md
|
||||
Next stage:
|
||||
Context update:
|
||||
|
||||
## implement
|
||||
|
||||
Status: pass
|
||||
Reason: Proposed patch written.
|
||||
Output: .nightshift\runs\20260517T212359.709956Z\tasks\TASK-001\repair-1.patch
|
||||
Next stage:
|
||||
Context update: Implementation summary: .nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/repair-summary-1.md
|
||||
|
||||
## normalize
|
||||
|
||||
Status: pass
|
||||
Reason: Normalized patch written.
|
||||
Output: .nightshift\runs\20260517T212359.709956Z\tasks\TASK-001\normalized-1.patch
|
||||
Next stage:
|
||||
Context update:
|
||||
|
||||
## validate_patch
|
||||
|
||||
Status: pass
|
||||
Reason: Patch validation passed.
|
||||
Output: .nightshift\runs\20260517T212359.709956Z\tasks\TASK-001\patch-validation-1.md
|
||||
Next stage:
|
||||
Context update:
|
||||
|
||||
## apply_patch
|
||||
|
||||
Status: fail
|
||||
Reason: Patch apply failed with code 128.
|
||||
Output: .nightshift\runs\20260517T212359.709956Z\tasks\TASK-001\patch-apply-output-1.txt
|
||||
Next stage:
|
||||
Context update: C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:6: trailing whitespace.
|
||||
import re
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:7: trailing whitespace.
|
||||
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:8: trailing whitespace.
|
||||
class LispParser:
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:9: trailing whitespace.
|
||||
def __init__(self, expression):
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-1.patch:10: trailing whitespace.
|
||||
self.expression = expression
|
||||
error: corrupt patch at line 48
|
||||
|
||||
## implement
|
||||
|
||||
Status: pass
|
||||
Reason: Proposed patch written.
|
||||
Output: .nightshift\runs\20260517T212359.709956Z\tasks\TASK-001\repair-2.patch
|
||||
Next stage:
|
||||
Context update: Implementation summary: .nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/repair-summary-2.md
|
||||
|
||||
## normalize
|
||||
|
||||
Status: pass
|
||||
Reason: Normalized patch written.
|
||||
Output: .nightshift\runs\20260517T212359.709956Z\tasks\TASK-001\normalized-2.patch
|
||||
Next stage:
|
||||
Context update:
|
||||
|
||||
## validate_patch
|
||||
|
||||
Status: pass
|
||||
Reason: Patch validation passed.
|
||||
Output: .nightshift\runs\20260517T212359.709956Z\tasks\TASK-001\patch-validation-2.md
|
||||
Next stage:
|
||||
Context update:
|
||||
|
||||
## apply_patch
|
||||
|
||||
Status: fail
|
||||
Reason: Patch apply failed with code 128.
|
||||
Output: .nightshift\runs\20260517T212359.709956Z\tasks\TASK-001\patch-apply-output-2.txt
|
||||
Next stage:
|
||||
Context update: C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:6: trailing whitespace.
|
||||
import re
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:7: trailing whitespace.
|
||||
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:8: trailing whitespace.
|
||||
class LispParser:
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:9: trailing whitespace.
|
||||
def __init__(self, expression):
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-2.patch:10: trailing whitespace.
|
||||
self.expression = expression
|
||||
error: corrupt patch at line 48
|
||||
|
||||
## implement
|
||||
|
||||
Status: pass
|
||||
Reason: Proposed patch written.
|
||||
Output: .nightshift\runs\20260517T212359.709956Z\tasks\TASK-001\repair-3.patch
|
||||
Next stage:
|
||||
Context update: Implementation summary: .nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/repair-summary-3.md
|
||||
|
||||
## normalize
|
||||
|
||||
Status: pass
|
||||
Reason: Normalized patch written.
|
||||
Output: .nightshift\runs\20260517T212359.709956Z\tasks\TASK-001\normalized-3.patch
|
||||
Next stage:
|
||||
Context update:
|
||||
|
||||
## validate_patch
|
||||
|
||||
Status: pass
|
||||
Reason: Patch validation passed.
|
||||
Output: .nightshift\runs\20260517T212359.709956Z\tasks\TASK-001\patch-validation-3.md
|
||||
Next stage:
|
||||
Context update:
|
||||
|
||||
## apply_patch
|
||||
|
||||
Status: fail
|
||||
Reason: Patch apply failed with code 128.
|
||||
Output: .nightshift\runs\20260517T212359.709956Z\tasks\TASK-001\patch-apply-output-3.txt
|
||||
Next stage:
|
||||
Context update: C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-3.patch:6: trailing whitespace.
|
||||
import re
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-3.patch:7: trailing whitespace.
|
||||
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-3.patch:8: trailing whitespace.
|
||||
class LispParser:
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-3.patch:9: trailing whitespace.
|
||||
def __init__(self, expression):
|
||||
C:/Users/metis/Documents/tiny-lisp-nightshift/.nightshift/runs/20260517T212359.709956Z/tasks/TASK-001/applied-3.patch:10: trailing whitespace.
|
||||
self.expression = expression
|
||||
error: corrupt patch at line 48
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Task Completion
|
||||
|
||||
Task: `TASK-001`
|
||||
Pipeline status: failed
|
||||
Marked complete: false
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
- [ ] TASK-001: Parse Lisp expressions
|
||||
|
||||
Description:
|
||||
Implement tokenization and parsing for a tiny Lisp subset.
|
||||
|
||||
Acceptance Criteria:
|
||||
- Parses numbers
|
||||
- Parses symbols
|
||||
- Parses nested lists
|
||||
- Raises useful errors for unbalanced parentheses
|
||||
- Includes unit tests
|
||||
|
|
@ -22,7 +22,8 @@ NightShift now supports the full local patch workflow:
|
|||
- Planner lookup requests with `files-inspected.md` artifacts.
|
||||
- `repo_context` stage for `context-pack.md`.
|
||||
- Project context chart generation at `.nightshift/project-context-chart.md`.
|
||||
- `code_writer` stage that requires unified diff output.
|
||||
- `code_writer` stage for direct unified diff output.
|
||||
- `file_writer` stage for model-written complete file blocks with deterministic diff generation.
|
||||
- `patch_normalizer`, `patch_validator`, and `patch_apply` stages.
|
||||
- Patch dry-run and apply modes.
|
||||
- Test/static failure repair loops through existing retry routing.
|
||||
|
|
@ -143,7 +144,7 @@ pipeline:
|
|||
output: context-pack.md
|
||||
|
||||
- id: implement
|
||||
type: code_writer
|
||||
type: file_writer
|
||||
agent: implementer
|
||||
output: proposed.patch
|
||||
|
||||
|
|
@ -215,7 +216,7 @@ agents:
|
|||
system_prompt: agents/implementer.md
|
||||
```
|
||||
|
||||
NightShift passes prompt bundles to agents and persists stdout, stderr, exit code, duration, and prompt artifacts. Code writer agents should return unified diffs. On retries, patch artifacts are versioned by attempt, for example `repair-1.patch`, `normalized-1.patch`, and `patch-validation-1.md`.
|
||||
NightShift passes prompt bundles to agents and persists stdout, stderr, exit code, duration, and prompt artifacts. `code_writer` agents return unified diffs directly. `file_writer` agents return complete file blocks, and NightShift generates the unified diff deterministically. On retries, patch artifacts are versioned by attempt, for example `repair-1.patch`, `normalized-1.patch`, and `patch-validation-1.md`.
|
||||
|
||||
Review agents should emit:
|
||||
|
||||
|
|
@ -246,7 +247,7 @@ Command safety:
|
|||
|
||||
Patch safety:
|
||||
|
||||
- code changes are represented as unified diffs
|
||||
- code changes are represented as unified diffs, either supplied directly or generated from complete file blocks
|
||||
- patches are normalized and validated before apply
|
||||
- path traversal and forbidden paths are rejected
|
||||
- scoped paths, max files, and max changed lines are enforced
|
||||
|
|
|
|||
9
agents/implementer.md
Normal file
9
agents/implementer.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
You are the implementation agent for NightShift.
|
||||
|
||||
Output only a unified diff.
|
||||
Do not wrap the patch in markdown fences.
|
||||
Do not include explanations before or after the patch.
|
||||
Use diff --git headers.
|
||||
Include tests when needed.
|
||||
Keep the change as small as possible.
|
||||
Only edit files needed for the task.
|
||||
19
agents/planner.md
Normal file
19
agents/planner.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
You are the planning agent for NightShift.
|
||||
|
||||
Create a concise implementation plan for the current task.
|
||||
|
||||
If you need repository context before planning, output lookup requests exactly like this:
|
||||
|
||||
lookup_requests:
|
||||
- tool: read_file
|
||||
path: relative/path.py
|
||||
- tool: grep
|
||||
path: .
|
||||
pattern: search_regex
|
||||
|
||||
After context is provided, write a short plan with:
|
||||
- files to edit
|
||||
- tests to add or update
|
||||
- risks
|
||||
|
||||
Do not write code.
|
||||
14
agents/reviewer.md
Normal file
14
agents/reviewer.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
You are the review agent for NightShift.
|
||||
|
||||
Review the task, plan, patch artifacts, test output, and final state.
|
||||
|
||||
Output exactly:
|
||||
|
||||
status: pass | fail | retry | escalate
|
||||
reason: <short explanation>
|
||||
next_stage: <optional stage id>
|
||||
context_update: <compact useful note>
|
||||
|
||||
Use retry when the implementation is close but needs another patch.
|
||||
Use fail when the patch is unsafe, unrelated, or clearly broken.
|
||||
Use pass only when the acceptance criteria are satisfied.
|
||||
|
|
@ -61,3 +61,23 @@ Command stage options:
|
|||
- `shell`: defaults to true. Set false for argv-style execution.
|
||||
- `timeout_seconds`: per-stage timeout override.
|
||||
- `working_dir`: command working directory inside project root.
|
||||
|
||||
Patch validator stage options:
|
||||
|
||||
- `max_files`: max files changed.
|
||||
- `max_lines`: max changed lines.
|
||||
- `forbidden_paths`: paths the patch must not touch.
|
||||
- Unified diff hunk line prefixes and hunk line counts are validated before patch apply.
|
||||
|
||||
Writer stages:
|
||||
|
||||
- `code_writer`: agent returns a unified diff directly.
|
||||
- `file_writer`: agent returns complete file content blocks; NightShift generates the unified diff deterministically.
|
||||
|
||||
`file_writer` blocks use this form:
|
||||
|
||||
````markdown
|
||||
```file:relative/path.py
|
||||
<complete file content>
|
||||
```
|
||||
````
|
||||
|
|
|
|||
|
|
@ -862,7 +862,7 @@ NightShift currently provides:
|
|||
* Dependency validation for missing references and simple cycles
|
||||
* Dependency-aware task selection and task blocking
|
||||
* Declarative YAML pipeline execution
|
||||
* Command, agent, agent-review, review, summarize, repo-context, code-writer, patch-normalizer, patch-validator, and patch-apply stage handling
|
||||
* Command, agent, agent-review, review, summarize, repo-context, code-writer, file-writer, patch-normalizer, patch-validator, and patch-apply stage handling
|
||||
* Retry redirection with a configured task retry limit
|
||||
* Command-backed agents
|
||||
* Ollama-backed local model agents through the local HTTP API
|
||||
|
|
@ -873,6 +873,7 @@ NightShift currently provides:
|
|||
* Project context chart generation
|
||||
* Context pack generation
|
||||
* Unified diff code-writing contract
|
||||
* Deterministic diff generation from model-supplied complete file blocks
|
||||
* Patch normalization, validation, dry-run, and apply modes
|
||||
* Per-attempt retry patch artifacts such as `repair-1.patch`, `normalized-1.patch`, and `patch-validation-1.md`
|
||||
* Test/static failure repair loops via bounded stage retries
|
||||
|
|
@ -1017,8 +1018,8 @@ The next important additions are:
|
|||
5. Better model backend support
|
||||
Expand OpenAI-compatible behavior, add request metadata artifacts, support response format hints, and document local server patterns. Machine-readable Ollama output now uses the HTTP API instead of the interactive `ollama run` terminal path; keep this non-terminal capture policy for future model backends where exact patch text matters.
|
||||
|
||||
6. Deterministic diff generation
|
||||
Reduce direct reliance on models emitting perfect unified diffs. Add a workflow where the model returns complete file contents or a structured edit description, then NightShift writes the unified diff deterministically from before/after file snapshots. Keep the existing unified-diff contract for advanced agents, but make deterministic diff generation the preferred path for smaller local models.
|
||||
6. Deterministic edit formats beyond full files
|
||||
The `file_writer` stage now generates unified diffs from complete file blocks. Future work should add smaller structured edit descriptions for large files while preserving deterministic diff generation.
|
||||
|
||||
7. Retry artifact versioning
|
||||
Continue improving per-attempt artifact preservation. Patch retries now preserve files such as `repair-1.patch`, `normalized-1.patch`, and `patch-validation-1.md`; future work should add richer latest-attempt indexes and dashboard navigation.
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ You will run NightShift against a copy of the tiny Lisp example and use a local
|
|||
|
||||
1. Inspect task and repository context.
|
||||
2. Produce a plan.
|
||||
3. Generate a unified diff.
|
||||
4. Normalize and validate that patch.
|
||||
3. Generate complete file blocks.
|
||||
4. Let NightShift generate, normalize, and validate the patch.
|
||||
5. Dry-run the patch.
|
||||
6. Optionally apply the patch and run tests.
|
||||
|
||||
|
|
@ -95,6 +95,15 @@ experiment:
|
|||
prompt_variant: ollama-qwen25-coder-14b-v1
|
||||
```
|
||||
|
||||
Set the implementation stage to deterministic file-block mode:
|
||||
|
||||
```yaml
|
||||
- id: implement
|
||||
type: file_writer
|
||||
agent: implementer
|
||||
output: proposed.patch
|
||||
```
|
||||
|
||||
## 3. Strengthen The Prompts
|
||||
|
||||
Real models need stricter instructions than fake fixtures.
|
||||
|
|
@ -125,17 +134,19 @@ Do not write code.
|
|||
|
||||
Use this for `agents/implementer.md`:
|
||||
|
||||
```markdown
|
||||
````markdown
|
||||
You are the implementation agent for NightShift.
|
||||
|
||||
Output only a unified diff.
|
||||
Do not wrap the patch in markdown fences.
|
||||
Do not include explanations before or after the patch.
|
||||
Use diff --git headers.
|
||||
Output only complete file content blocks.
|
||||
Use one fenced block per file with this exact opening form:
|
||||
```file:relative/path.py
|
||||
<complete file content>
|
||||
```
|
||||
Do not include explanations before or after the file blocks.
|
||||
Include tests when needed.
|
||||
Keep the change as small as possible.
|
||||
Only edit files needed for the task.
|
||||
```
|
||||
````
|
||||
|
||||
Use this for `agents/reviewer.md`:
|
||||
|
||||
|
|
@ -288,7 +299,7 @@ Agent exited with code 1
|
|||
|
||||
Confirm Ollama is running at the configured `base_url` and the model appears in `ollama list`.
|
||||
|
||||
If the model returns prose instead of a patch, tighten `agents/implementer.md`. The implementation stage requires a unified diff.
|
||||
If the model returns prose instead of file blocks, tighten `agents/implementer.md`. The `file_writer` stage requires complete file content blocks so NightShift can generate the unified diff.
|
||||
|
||||
If patch validation fails, inspect:
|
||||
|
||||
|
|
|
|||
4
lisp.py
Normal file
4
lisp.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
"""Tiny Lisp quickstart target.
|
||||
|
||||
NightShift tasks in this example are intended to fill this module in.
|
||||
"""
|
||||
89
nightshift.yaml
Normal file
89
nightshift.yaml
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
project:
|
||||
name: tiny-lisp
|
||||
root: .
|
||||
task_file: tasks.md
|
||||
artifact_dir: .nightshift
|
||||
|
||||
safety:
|
||||
require_clean_worktree: false
|
||||
scoped_paths:
|
||||
- .
|
||||
allowed_commands:
|
||||
- python -m unittest discover -v
|
||||
forbidden_commands:
|
||||
- rm -rf
|
||||
- git push
|
||||
- curl | bash
|
||||
|
||||
agents:
|
||||
planner:
|
||||
backend: ollama
|
||||
model: qwen2.5-coder:14b
|
||||
temperature: 0.2
|
||||
system_prompt: agents/planner.md
|
||||
|
||||
implementer:
|
||||
backend: ollama
|
||||
model: qwen2.5-coder:14b
|
||||
temperature: 0.1
|
||||
system_prompt: agents/implementer.md
|
||||
|
||||
reviewer:
|
||||
backend: ollama
|
||||
model: qwen2.5-coder:14b
|
||||
temperature: 0.1
|
||||
system_prompt: agents/reviewer.md
|
||||
|
||||
pipeline:
|
||||
max_task_retries: 3
|
||||
continue_on_task_failure: false
|
||||
stages:
|
||||
- id: plan
|
||||
type: agent
|
||||
agent: planner
|
||||
output: plan.md
|
||||
|
||||
- id: context
|
||||
type: repo_context
|
||||
output: context-pack.md
|
||||
|
||||
- id: implement
|
||||
type: code_writer
|
||||
agent: implementer
|
||||
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
|
||||
on_fail: implement
|
||||
|
||||
- id: apply_patch
|
||||
type: patch_apply
|
||||
mode: apply
|
||||
output: patch-apply-output.txt
|
||||
on_fail: implement
|
||||
|
||||
- id: test
|
||||
type: command
|
||||
commands:
|
||||
- python -m unittest discover -v
|
||||
output: test-output.txt
|
||||
shell: true
|
||||
timeout_seconds: 60
|
||||
on_fail: implement
|
||||
|
||||
- id: review
|
||||
type: agent_review
|
||||
agent: reviewer
|
||||
on_fail: implement
|
||||
output: review.md
|
||||
|
||||
- id: summarize
|
||||
type: summarize
|
||||
output: final-notes.md
|
||||
|
|
@ -443,6 +443,19 @@ def output_contract_for(stage: StageConfig) -> str:
|
|||
"On repair attempts, return a complete corrected replacement diff.",
|
||||
]
|
||||
)
|
||||
if stage.type == "file_writer":
|
||||
return "\n".join(
|
||||
[
|
||||
"Return complete file contents only.",
|
||||
"Use one fenced block per file with this exact opening form:",
|
||||
"```file:relative/path.py",
|
||||
"<complete file content>",
|
||||
"```",
|
||||
"Do not include prose outside file blocks.",
|
||||
"Include every file needed for the task, including tests.",
|
||||
"NightShift will generate the unified diff deterministically.",
|
||||
]
|
||||
)
|
||||
if stage.type == "patch_normalizer":
|
||||
return "\n".join(
|
||||
[
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ AGENT_STAGE_TYPES = {"agent", "agent_review", "review"}
|
|||
COMMAND_STAGE_TYPES = {"command"}
|
||||
SUPPORTED_STAGE_TYPES = AGENT_STAGE_TYPES | COMMAND_STAGE_TYPES | {
|
||||
"code_writer",
|
||||
"file_writer",
|
||||
"patch_normalizer",
|
||||
"patch_apply",
|
||||
"patch_validator",
|
||||
|
|
@ -314,9 +315,9 @@ def parse_config(raw: dict[str, Any], config_path: Path) -> NightShiftConfig:
|
|||
f"Config error: pipeline stage '{stage_id}' references unknown agent "
|
||||
f"'{agent}'. Defined agents: {defined}."
|
||||
)
|
||||
if stage_type == "code_writer":
|
||||
if stage_type in {"code_writer", "file_writer"}:
|
||||
if agent is None:
|
||||
raise ConfigError(f"Config error: code_writer stage '{stage_id}' must reference an agent.")
|
||||
raise ConfigError(f"Config error: {stage_type} stage '{stage_id}' must reference an agent.")
|
||||
if agent not in agents:
|
||||
defined = ", ".join(sorted(agents))
|
||||
raise ConfigError(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import difflib
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
|
|
@ -33,6 +34,12 @@ class PatchApplyResult:
|
|||
mode: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FileUpdate:
|
||||
path: str
|
||||
content: str
|
||||
|
||||
|
||||
def extract_unified_diff(text: str) -> str:
|
||||
fenced = re.search(r"```(?:diff|patch)?\s*\n(.*?)```", text, flags=re.DOTALL | re.IGNORECASE)
|
||||
candidate = fenced.group(1) if fenced else text
|
||||
|
|
@ -55,6 +62,53 @@ def normalize_patch_text(text: str) -> str:
|
|||
return patch
|
||||
|
||||
|
||||
def parse_file_updates(text: str) -> tuple[FileUpdate, ...]:
|
||||
"""Parse model-supplied complete file content blocks."""
|
||||
|
||||
updates: list[FileUpdate] = []
|
||||
pattern = re.compile(
|
||||
r"```(?:file|path)[:=](?P<path>[^\n`]+)\n(?P<content>.*?)```",
|
||||
flags=re.DOTALL | re.IGNORECASE,
|
||||
)
|
||||
for match in pattern.finditer(text):
|
||||
path = match.group("path").strip()
|
||||
content = match.group("content")
|
||||
if not path:
|
||||
continue
|
||||
updates.append(FileUpdate(path=path, content=content))
|
||||
if not updates:
|
||||
raise PipelineError(
|
||||
"File writer error: no file blocks found. Expected fenced blocks like ```file:path.py."
|
||||
)
|
||||
return tuple(updates)
|
||||
|
||||
|
||||
def generate_patch_from_file_updates(
|
||||
updates: tuple[FileUpdate, ...],
|
||||
project_root: str | Path,
|
||||
safety: SafetyConfig,
|
||||
forbidden_paths: tuple[str, ...] = DEFAULT_FORBIDDEN_PATHS,
|
||||
) -> str:
|
||||
root = resolve_project_root(project_root)
|
||||
scoped_roots = validate_scoped_paths(root, safety.scoped_paths or (".",))
|
||||
patch_parts: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for update in updates:
|
||||
normalized_path = _normalize_update_path(update.path)
|
||||
if normalized_path in seen:
|
||||
raise PipelineError(f"File writer error: duplicate file block `{normalized_path}`.")
|
||||
seen.add(normalized_path)
|
||||
_validate_patch_path(normalized_path, root, scoped_roots, forbidden_paths)
|
||||
file_path = resolve_inside_root(root, normalized_path, f"file update '{normalized_path}'")
|
||||
old_text = file_path.read_text(encoding="utf-8", errors="replace") if file_path.exists() else ""
|
||||
if old_text == update.content:
|
||||
continue
|
||||
patch_parts.extend(_diff_for_file(normalized_path, old_text, update.content, file_path.exists()))
|
||||
if not patch_parts:
|
||||
raise PipelineError("File writer error: generated patch has no changes.")
|
||||
return "\n".join(patch_parts).rstrip() + "\n"
|
||||
|
||||
|
||||
def validate_patch(
|
||||
patch: str,
|
||||
project_root: str | Path,
|
||||
|
|
@ -82,6 +136,7 @@ def validate_patch(
|
|||
for path_text in files:
|
||||
_validate_patch_path(path_text, root, scoped_roots, forbidden_paths)
|
||||
_validate_hunk_lines(patch)
|
||||
_validate_hunk_counts(patch)
|
||||
_validate_file_states(patch, root)
|
||||
return PatchValidationResult(files=tuple(sorted(files)), changed_lines=changed_lines)
|
||||
|
||||
|
|
@ -195,6 +250,74 @@ def _validate_hunk_lines(patch: str) -> None:
|
|||
)
|
||||
|
||||
|
||||
def _validate_hunk_counts(patch: str) -> None:
|
||||
current: dict[str, int] | None = None
|
||||
|
||||
def flush(line_number: int) -> None:
|
||||
if current is None:
|
||||
return
|
||||
old_expected = current["old_expected"]
|
||||
new_expected = current["new_expected"]
|
||||
old_actual = current["old_actual"]
|
||||
new_actual = current["new_actual"]
|
||||
hunk_line = current["line_number"]
|
||||
if old_actual != old_expected:
|
||||
raise PipelineError(
|
||||
"Patch validation failed: hunk starting at line "
|
||||
f"{hunk_line} old line count expected {old_expected}, got {old_actual} "
|
||||
f"before line {line_number}."
|
||||
)
|
||||
if new_actual != new_expected:
|
||||
raise PipelineError(
|
||||
"Patch validation failed: hunk starting at line "
|
||||
f"{hunk_line} new line count expected {new_expected}, got {new_actual} "
|
||||
f"before line {line_number}."
|
||||
)
|
||||
|
||||
for line_number, line in enumerate(patch.splitlines(), start=1):
|
||||
if line.startswith("@@"):
|
||||
flush(line_number)
|
||||
current = _parse_hunk_header(line, line_number)
|
||||
continue
|
||||
if current is None:
|
||||
continue
|
||||
if line.startswith("diff --git "):
|
||||
flush(line_number)
|
||||
current = None
|
||||
continue
|
||||
if line.startswith("\\"):
|
||||
continue
|
||||
if line.startswith(" "):
|
||||
current["old_actual"] += 1
|
||||
current["new_actual"] += 1
|
||||
elif line.startswith("-") and not line.startswith("---"):
|
||||
current["old_actual"] += 1
|
||||
elif line.startswith("+") and not line.startswith("+++"):
|
||||
current["new_actual"] += 1
|
||||
flush(len(patch.splitlines()) + 1)
|
||||
|
||||
|
||||
def _parse_hunk_header(line: str, line_number: int) -> dict[str, int]:
|
||||
match = re.match(
|
||||
r"^@@ -(?P<old_start>\d+)(?:,(?P<old_count>\d+))? "
|
||||
r"\+(?P<new_start>\d+)(?:,(?P<new_count>\d+))? @@",
|
||||
line,
|
||||
)
|
||||
if not match:
|
||||
raise PipelineError(
|
||||
f"Patch validation failed: malformed hunk header at line {line_number}."
|
||||
)
|
||||
old_count = int(match.group("old_count") or "1")
|
||||
new_count = int(match.group("new_count") or "1")
|
||||
return {
|
||||
"line_number": line_number,
|
||||
"old_expected": old_count,
|
||||
"new_expected": new_count,
|
||||
"old_actual": 0,
|
||||
"new_actual": 0,
|
||||
}
|
||||
|
||||
|
||||
def _validate_file_states(patch: str, root: Path) -> None:
|
||||
current_path: str | None = None
|
||||
current_is_new = False
|
||||
|
|
@ -267,6 +390,35 @@ def _validate_patch_path(
|
|||
)
|
||||
|
||||
|
||||
def _normalize_update_path(path_text: str) -> str:
|
||||
normalized = path_text.replace("\\", "/").strip()
|
||||
if normalized.startswith(("a/", "b/")):
|
||||
normalized = normalized[2:]
|
||||
return normalized
|
||||
|
||||
|
||||
def _diff_for_file(path: str, old_text: str, new_text: str, exists: bool) -> list[str]:
|
||||
old_lines = old_text.splitlines()
|
||||
new_lines = new_text.splitlines()
|
||||
from_file = f"a/{path}" if exists else "/dev/null"
|
||||
to_file = f"b/{path}"
|
||||
diff_lines = list(
|
||||
difflib.unified_diff(
|
||||
old_lines,
|
||||
new_lines,
|
||||
fromfile=from_file,
|
||||
tofile=to_file,
|
||||
lineterm="",
|
||||
)
|
||||
)
|
||||
if not diff_lines:
|
||||
return []
|
||||
header = [f"diff --git a/{path} b/{path}"]
|
||||
if not exists:
|
||||
header.append("new file mode 100644")
|
||||
return [*header, *diff_lines]
|
||||
|
||||
|
||||
def _strip_prefix(path_text: str) -> str:
|
||||
path = path_text.strip()
|
||||
if path.startswith(("a/", "b/")):
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ from .patches import (
|
|||
extract_unified_diff,
|
||||
format_patch_apply_result,
|
||||
format_validation_result,
|
||||
generate_patch_from_file_updates,
|
||||
normalize_patch_text,
|
||||
parse_file_updates,
|
||||
validate_patch,
|
||||
)
|
||||
from .project_chart import build_project_context_chart
|
||||
|
|
@ -369,6 +371,8 @@ class PipelineRunner:
|
|||
return self.command_executor.run_stage(stage, task.id)
|
||||
if stage.type == "code_writer":
|
||||
return self._run_code_writer_stage(stage, task, previous_outputs, retry_notes, retry_count)
|
||||
if stage.type == "file_writer":
|
||||
return self._run_file_writer_stage(stage, task, previous_outputs, retry_notes, retry_count)
|
||||
if stage.type == "patch_normalizer":
|
||||
return self._run_patch_normalizer_stage(stage, task, previous_outputs, retry_notes, retry_count)
|
||||
if stage.type == "patch_validator":
|
||||
|
|
@ -516,6 +520,110 @@ class PipelineRunner:
|
|||
context_update=f"Implementation summary: {summary_path.relative_to(self.config.project.root).as_posix()}",
|
||||
)
|
||||
|
||||
def _run_file_writer_stage(
|
||||
self,
|
||||
stage: StageConfig,
|
||||
task: Task,
|
||||
previous_outputs: dict[str, str],
|
||||
retry_notes: list[str],
|
||||
retry_count: int = 0,
|
||||
) -> StageResult:
|
||||
if stage.agent is None:
|
||||
raise PipelineError(f"Pipeline error: file_writer stage '{stage.id}' must reference an agent.")
|
||||
enriched_outputs = dict(previous_outputs)
|
||||
context_pack_path = self._latest_task_artifact(task.id, "context-pack.md")
|
||||
if context_pack_path is not None:
|
||||
enriched_outputs["context-pack.md"] = context_pack_path.read_text(encoding="utf-8", errors="replace")
|
||||
chart_path = self.artifacts.project_context_chart_path
|
||||
if chart_path.exists():
|
||||
enriched_outputs["project-context-chart.md"] = chart_path.read_text(encoding="utf-8", errors="replace")
|
||||
context = self.context.read_context(task, retry_notes)
|
||||
result = self.agent_executor.run_stage(
|
||||
stage,
|
||||
task,
|
||||
enriched_outputs,
|
||||
retry_notes,
|
||||
project_context=context.project_context,
|
||||
task_context=context.task_context,
|
||||
retry_context=context.retry_context,
|
||||
)
|
||||
raw_output = self._read_output(result.output_path)
|
||||
stdout = extract_agent_stdout(raw_output)
|
||||
lookup_requests = parse_lookup_requests(stdout)
|
||||
if lookup_requests and "```file:" not in stdout.lower() and "```path:" not in stdout.lower():
|
||||
lookup_context = self.repo_tools.execute_requests(
|
||||
task.id,
|
||||
lookup_requests,
|
||||
filename="implementation-files-inspected.md",
|
||||
)
|
||||
self.logger.event(
|
||||
"agent.rerun",
|
||||
"Re-running file writer with repo lookup context",
|
||||
stage_id=stage.id,
|
||||
task_id=task.id,
|
||||
lookup_count=len(lookup_requests),
|
||||
)
|
||||
rerun_outputs = dict(enriched_outputs)
|
||||
rerun_outputs["repo_lookup_results"] = lookup_context
|
||||
rerun_notes = [
|
||||
*retry_notes,
|
||||
"Repository lookup results have been provided. Return complete file blocks now; do not request more lookups.",
|
||||
]
|
||||
result = self.agent_executor.run_stage(
|
||||
stage,
|
||||
task,
|
||||
rerun_outputs,
|
||||
rerun_notes,
|
||||
project_context=context.project_context,
|
||||
task_context=context.task_context,
|
||||
retry_context="\n".join(f"- {note}" for note in rerun_notes),
|
||||
)
|
||||
raw_output = self._read_output(result.output_path)
|
||||
stdout = extract_agent_stdout(raw_output)
|
||||
try:
|
||||
updates = parse_file_updates(stdout)
|
||||
patch = generate_patch_from_file_updates(
|
||||
updates,
|
||||
self.config.project.root,
|
||||
self.config.safety,
|
||||
forbidden_paths=stage.forbidden_paths or DEFAULT_FORBIDDEN_PATHS,
|
||||
)
|
||||
except PipelineError as exc:
|
||||
summary_filename = "implementation-summary.md" if retry_count == 0 else f"repair-summary-{retry_count}.md"
|
||||
self.artifacts.write_stage_output(
|
||||
task.id,
|
||||
summary_filename,
|
||||
f"# Implementation Summary\n\nStatus: fail\nReason: {exc}\n",
|
||||
)
|
||||
return StageResult(stage.id, "fail", str(exc), output_path=result.output_path)
|
||||
patch_filename = "repair-{0}.patch".format(retry_count) if retry_count else (stage.output or "proposed.patch")
|
||||
summary_filename = "implementation-summary.md" if retry_count == 0 else f"repair-summary-{retry_count}.md"
|
||||
proposed_path = self.artifacts.write_stage_output(task.id, patch_filename, patch)
|
||||
summary_path = self.artifacts.write_stage_output(
|
||||
task.id,
|
||||
summary_filename,
|
||||
format_implementation_summary(
|
||||
stage.id,
|
||||
proposed_path.relative_to(self.config.project.root).as_posix(),
|
||||
retry_count=retry_count,
|
||||
retry_notes=retry_notes,
|
||||
),
|
||||
)
|
||||
self.logger.event(
|
||||
"artifact.write",
|
||||
"Wrote deterministic patch from file blocks",
|
||||
stage_id=stage.id,
|
||||
task_id=task.id,
|
||||
artifact_path=proposed_path.relative_to(self.config.project.root),
|
||||
)
|
||||
return StageResult(
|
||||
stage.id,
|
||||
"pass",
|
||||
"Deterministic patch written from file blocks.",
|
||||
output_path=str(proposed_path.relative_to(self.config.project.root)),
|
||||
context_update=f"Implementation summary: {summary_path.relative_to(self.config.project.root).as_posix()}",
|
||||
)
|
||||
|
||||
def _run_patch_normalizer_stage(
|
||||
self,
|
||||
stage: StageConfig,
|
||||
|
|
|
|||
52
tasks.md
Normal file
52
tasks.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Tasks
|
||||
|
||||
- [ ] TASK-001: Parse Lisp expressions
|
||||
|
||||
Description:
|
||||
Implement tokenization and parsing for a tiny Lisp subset.
|
||||
|
||||
Acceptance Criteria:
|
||||
- Parses numbers
|
||||
- Parses symbols
|
||||
- Parses nested lists
|
||||
- Raises useful errors for unbalanced parentheses
|
||||
- Includes unit tests
|
||||
|
||||
- [ ] TASK-002: Evaluate arithmetic forms
|
||||
|
||||
Dependencies:
|
||||
- TASK-001
|
||||
|
||||
Description:
|
||||
Evaluate parsed arithmetic expressions.
|
||||
|
||||
Acceptance Criteria:
|
||||
- Supports `+`, `-`, `*`, and `/`
|
||||
- Evaluates nested arithmetic
|
||||
- Includes unit tests
|
||||
|
||||
- [ ] TASK-003: Add variables and definitions
|
||||
|
||||
Dependencies:
|
||||
- TASK-002
|
||||
|
||||
Description:
|
||||
Add an environment and support variable lookup and definitions.
|
||||
|
||||
Acceptance Criteria:
|
||||
- Supports symbol lookup
|
||||
- Supports `(define name value)`
|
||||
- Keeps environment behavior tested
|
||||
|
||||
- [ ] TASK-004: Add conditionals
|
||||
|
||||
Dependencies:
|
||||
- TASK-003
|
||||
|
||||
Description:
|
||||
Implement simple truthiness and `if` expressions.
|
||||
|
||||
Acceptance Criteria:
|
||||
- Supports `(if condition then else)`
|
||||
- Handles false-like values consistently
|
||||
- Includes tests for both branches
|
||||
|
|
@ -1 +1 @@
|
|||
"""NightShift test suite."""
|
||||
"""Quickstart Lisp test suite."""
|
||||
|
|
|
|||
|
|
@ -227,6 +227,23 @@ class ConfigTests(unittest.TestCase):
|
|||
self.assertEqual(patch_stage.max_lines, 100)
|
||||
self.assertEqual(patch_stage.forbidden_paths, ("secrets",))
|
||||
|
||||
def test_file_writer_stage_requires_agent(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
init_project(root)
|
||||
config_path = root / "nightshift.yaml"
|
||||
text = config_path.read_text(encoding="utf-8")
|
||||
config_path.write_text(
|
||||
text.replace(
|
||||
" - id: plan\n type: agent\n agent: planner\n output: plan.md",
|
||||
" - id: write\n type: file_writer",
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(ConfigError, "file_writer stage 'write' must reference an agent"):
|
||||
load_config(config_path)
|
||||
|
||||
def test_patch_apply_mode_loads(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
|
|
|
|||
10
tests/test_lisp.py
Normal file
10
tests/test_lisp.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import unittest
|
||||
|
||||
|
||||
class SmokeTests(unittest.TestCase):
|
||||
def test_smoke(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -4,7 +4,12 @@ import unittest
|
|||
|
||||
from nightshift.config import SafetyConfig
|
||||
from nightshift.errors import PipelineError
|
||||
from nightshift.patches import normalize_patch_text, validate_patch
|
||||
from nightshift.patches import (
|
||||
generate_patch_from_file_updates,
|
||||
normalize_patch_text,
|
||||
parse_file_updates,
|
||||
validate_patch,
|
||||
)
|
||||
|
||||
|
||||
PATCH = """diff --git a/src/app.py b/src/app.py
|
||||
|
|
@ -96,6 +101,106 @@ new file mode 100644
|
|||
with self.assertRaisesRegex(PipelineError, "creates existing file"):
|
||||
validate_patch(patch, root, safety)
|
||||
|
||||
def test_validate_patch_rejects_hunk_count_mismatch(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
(root / "src").mkdir()
|
||||
safety = SafetyConfig(
|
||||
require_clean_worktree=False,
|
||||
scoped_paths=("src",),
|
||||
allowed_commands=(),
|
||||
forbidden_commands=(),
|
||||
)
|
||||
patch = """diff --git a/src/app.py b/src/app.py
|
||||
--- a/src/app.py
|
||||
+++ b/src/app.py
|
||||
@@ -1 +1,2 @@
|
||||
-old
|
||||
+new
|
||||
"""
|
||||
|
||||
with self.assertRaisesRegex(PipelineError, "new line count expected 2, got 1"):
|
||||
validate_patch(patch, root, safety)
|
||||
|
||||
def test_validate_patch_accepts_multiple_files(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
(root / "src").mkdir()
|
||||
safety = SafetyConfig(
|
||||
require_clean_worktree=False,
|
||||
scoped_paths=("src",),
|
||||
allowed_commands=(),
|
||||
forbidden_commands=(),
|
||||
)
|
||||
patch = """diff --git a/src/app.py b/src/app.py
|
||||
--- a/src/app.py
|
||||
+++ b/src/app.py
|
||||
@@ -1 +1 @@
|
||||
-old
|
||||
+new
|
||||
diff --git a/src/test_app.py b/src/test_app.py
|
||||
--- a/src/test_app.py
|
||||
+++ b/src/test_app.py
|
||||
@@ -1 +1 @@
|
||||
-old test
|
||||
+new test
|
||||
"""
|
||||
|
||||
result = validate_patch(patch, root, safety)
|
||||
|
||||
self.assertEqual(result.files, ("src/app.py", "src/test_app.py"))
|
||||
|
||||
def test_file_updates_generate_unified_diff(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
(root / "src").mkdir()
|
||||
(root / "src" / "app.py").write_text("old\n", encoding="utf-8")
|
||||
safety = SafetyConfig(
|
||||
require_clean_worktree=False,
|
||||
scoped_paths=("src",),
|
||||
allowed_commands=(),
|
||||
forbidden_commands=(),
|
||||
)
|
||||
updates = parse_file_updates(
|
||||
"""```file:src/app.py
|
||||
new
|
||||
```
|
||||
```file:src/test_app.py
|
||||
test
|
||||
```
|
||||
"""
|
||||
)
|
||||
|
||||
patch = generate_patch_from_file_updates(updates, root, safety)
|
||||
result = validate_patch(patch, root, safety)
|
||||
|
||||
self.assertIn("diff --git a/src/app.py b/src/app.py", patch)
|
||||
self.assertIn("diff --git a/src/test_app.py b/src/test_app.py", patch)
|
||||
self.assertIn("new file mode 100644", patch)
|
||||
self.assertEqual(result.files, ("src/app.py", "src/test_app.py"))
|
||||
|
||||
def test_file_updates_reject_duplicate_blocks(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
safety = SafetyConfig(
|
||||
require_clean_worktree=False,
|
||||
scoped_paths=(".",),
|
||||
allowed_commands=(),
|
||||
forbidden_commands=(),
|
||||
)
|
||||
updates = parse_file_updates(
|
||||
"""```file:app.py
|
||||
one
|
||||
```
|
||||
```file:app.py
|
||||
two
|
||||
```
|
||||
"""
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(PipelineError, "duplicate file block"):
|
||||
generate_patch_from_file_updates(updates, root, safety)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -419,6 +419,46 @@ Acceptance Criteria:
|
|||
self.assertTrue((task_dir / "implementation-files-inspected.md").exists())
|
||||
self.assertIn("diff --git a/app.py b/app.py", (task_dir / "proposed.patch").read_text(encoding="utf-8"))
|
||||
|
||||
def test_file_writer_generates_patch_from_file_blocks(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('```file:app.py')",
|
||||
"print('new')",
|
||||
"print('```')",
|
||||
"print('```file:tests/test_app.py')",
|
||||
"print('def test_app():')",
|
||||
"print(' assert True')",
|
||||
"print('```')",
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
stages = (
|
||||
StageConfig(id="write", type="file_writer", agent="writer"),
|
||||
StageConfig(id="normalize", type="patch_normalizer"),
|
||||
StageConfig(id="validate", type="patch_validator"),
|
||||
)
|
||||
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])
|
||||
|
||||
patch = root / ".nightshift" / "runs" / "test-run" / "tasks" / "TASK-001" / "proposed.patch"
|
||||
self.assertEqual(result.status, "complete")
|
||||
self.assertIn("diff --git a/app.py b/app.py", patch.read_text(encoding="utf-8"))
|
||||
self.assertIn("diff --git a/tests/test_app.py b/tests/test_app.py", patch.read_text(encoding="utf-8"))
|
||||
|
||||
def test_patch_validator_rejects_unsafe_patch(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user