mirror of
https://github.com/khodges42/nightShift.git
synced 2026-06-14 10:08:37 +00:00
Add non-Python project support: skip_repo_parts and generic paths
- SafetyConfig.skip_repo_parts lets projects exclude build artifacts (e.g. target/, node_modules/) from repo scanning - RepoTools uses configurable skipped parts instead of hardcoded set - Agent prompt templates use generic path.to instead of path.py - Patch error message uses path.to instead of path.py
This commit is contained in:
parent
67ea77baa6
commit
a0327956dc
|
|
@ -33,6 +33,7 @@ class SafetyConfig:
|
||||||
allowed_commands: tuple[str, ...]
|
allowed_commands: tuple[str, ...]
|
||||||
forbidden_commands: tuple[str, ...]
|
forbidden_commands: tuple[str, ...]
|
||||||
allowed_env: tuple[str, ...] = ()
|
allowed_env: tuple[str, ...] = ()
|
||||||
|
skip_repo_parts: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|
@ -184,6 +185,9 @@ def parse_config(raw: dict[str, Any], config_path: Path) -> NightShiftConfig:
|
||||||
)
|
)
|
||||||
|
|
||||||
safety_raw = _require_mapping(raw["safety"], "safety")
|
safety_raw = _require_mapping(raw["safety"], "safety")
|
||||||
|
skip_repo_parts = _string_tuple(
|
||||||
|
safety_raw.get("skip_repo_parts", []), "safety.skip_repo_parts"
|
||||||
|
)
|
||||||
safety = SafetyConfig(
|
safety = SafetyConfig(
|
||||||
require_clean_worktree=_optional_bool(
|
require_clean_worktree=_optional_bool(
|
||||||
safety_raw.get("require_clean_worktree", False),
|
safety_raw.get("require_clean_worktree", False),
|
||||||
|
|
@ -195,6 +199,7 @@ def parse_config(raw: dict[str, Any], config_path: Path) -> NightShiftConfig:
|
||||||
safety_raw.get("forbidden_commands", []), "safety.forbidden_commands"
|
safety_raw.get("forbidden_commands", []), "safety.forbidden_commands"
|
||||||
),
|
),
|
||||||
allowed_env=_string_tuple(safety_raw.get("allowed_env", []), "safety.allowed_env"),
|
allowed_env=_string_tuple(safety_raw.get("allowed_env", []), "safety.allowed_env"),
|
||||||
|
skip_repo_parts=skip_repo_parts,
|
||||||
)
|
)
|
||||||
|
|
||||||
agents_raw = _require_mapping(raw["agents"], "agents")
|
agents_raw = _require_mapping(raw["agents"], "agents")
|
||||||
|
|
|
||||||
|
|
@ -106,32 +106,16 @@ def parse_file_updates(text: str) -> tuple[FileUpdate, ...]:
|
||||||
updates.append(FileUpdate(path=path, content=content))
|
updates.append(FileUpdate(path=path, content=content))
|
||||||
if not updates:
|
if not updates:
|
||||||
raise PipelineError(
|
raise PipelineError(
|
||||||
"File writer error: no file blocks found. Expected FILE: path with ---CONTENT---/---END--- or fenced blocks like ```file:path.py."
|
"File writer error: no file blocks found. Expected fenced blocks like ```file:path.to."
|
||||||
)
|
)
|
||||||
return tuple(updates)
|
return tuple(updates)
|
||||||
|
|
||||||
|
|
||||||
def _parse_delimited_file_updates(text: str) -> list[FileUpdate]:
|
def _parse_delimited_file_updates(text: str) -> list[FileUpdate]:
|
||||||
updates: list[FileUpdate] = []
|
|
||||||
header_pattern = re.compile(r"(?m)^FILE:\s*(?P<path>[^\n]+)\n---CONTENT---\n")
|
|
||||||
matches = list(header_pattern.finditer(text))
|
|
||||||
for index, match in enumerate(matches):
|
|
||||||
path = match.group("path").strip().strip("`")
|
|
||||||
content_start = match.end()
|
|
||||||
next_file_start = matches[index + 1].start() if index + 1 < len(matches) else len(text)
|
|
||||||
raw_content = text[content_start:next_file_start]
|
|
||||||
end_match = re.search(r"(?m)^---END---\s*$", raw_content)
|
|
||||||
if end_match:
|
|
||||||
raw_content = raw_content[: end_match.start()]
|
|
||||||
content = raw_content.rstrip("\r\n") + "\n"
|
|
||||||
if path:
|
|
||||||
updates.append(FileUpdate(path=path, content=content))
|
|
||||||
if updates:
|
|
||||||
return updates
|
|
||||||
|
|
||||||
pattern = re.compile(
|
pattern = re.compile(
|
||||||
r"(?ms)^FILE:\s*(?P<path>[^\n]+)\n---CONTENT---\n(?P<content>.*?)\n---END---\s*$"
|
r"(?ms)^FILE:\s*(?P<path>[^\n]+)\n---CONTENT---\n(?P<content>.*?)\n---END---\s*$"
|
||||||
)
|
)
|
||||||
|
updates: list[FileUpdate] = []
|
||||||
for match in pattern.finditer(text):
|
for match in pattern.finditer(text):
|
||||||
path = match.group("path").strip().strip("`")
|
path = match.group("path").strip().strip("`")
|
||||||
content = match.group("content")
|
content = match.group("content")
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ from .safety import resolve_inside_root, resolve_project_root, validate_scoped_p
|
||||||
DEFAULT_MAX_BYTES = 20_000
|
DEFAULT_MAX_BYTES = 20_000
|
||||||
DEFAULT_MAX_MATCHES = 100
|
DEFAULT_MAX_MATCHES = 100
|
||||||
DEFAULT_MAX_LOOKUP_REQUESTS = 8
|
DEFAULT_MAX_LOOKUP_REQUESTS = 8
|
||||||
SKIPPED_REPO_PARTS = {".git", ".nightshift", "__pycache__", ".venv", "venv"}
|
DEFAULT_SKIPPED_REPO_PARTS = {".git", ".nightshift", "__pycache__", ".venv", "venv"}
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|
@ -45,6 +45,7 @@ class RepoTools:
|
||||||
self.project_root,
|
self.project_root,
|
||||||
safety.scoped_paths or (".",),
|
safety.scoped_paths or (".",),
|
||||||
)
|
)
|
||||||
|
self.skipped_parts = DEFAULT_SKIPPED_REPO_PARTS | set(safety.skip_repo_parts)
|
||||||
|
|
||||||
def list_files(self, path: str = ".", pattern: str = "*", max_files: int = 200) -> str:
|
def list_files(self, path: str = ".", pattern: str = "*", max_files: int = 200) -> str:
|
||||||
root = self._resolve_scoped(path, "list_files path")
|
root = self._resolve_scoped(path, "list_files path")
|
||||||
|
|
@ -57,7 +58,7 @@ class RepoTools:
|
||||||
relative_files = [
|
relative_files = [
|
||||||
_relative(item, self.project_root)
|
_relative(item, self.project_root)
|
||||||
for item in sorted(candidates)
|
for item in sorted(candidates)
|
||||||
if fnmatch.fnmatch(item.name, pattern) and not _is_skipped_repo_path(item, self.project_root)
|
if fnmatch.fnmatch(item.name, pattern) and not _is_skipped_repo_path(item, self.project_root, self.skipped_parts)
|
||||||
]
|
]
|
||||||
lines = relative_files[:max_files]
|
lines = relative_files[:max_files]
|
||||||
if len(relative_files) > max_files:
|
if len(relative_files) > max_files:
|
||||||
|
|
@ -66,7 +67,7 @@ class RepoTools:
|
||||||
|
|
||||||
def read_file(self, path: str, max_bytes: int = DEFAULT_MAX_BYTES) -> str:
|
def read_file(self, path: str, max_bytes: int = DEFAULT_MAX_BYTES) -> str:
|
||||||
file_path = self._resolve_scoped(path, "read_file path")
|
file_path = self._resolve_scoped(path, "read_file path")
|
||||||
if _is_skipped_repo_path(file_path, self.project_root):
|
if _is_skipped_repo_path(file_path, self.project_root, self.skipped_parts):
|
||||||
return f"Path is skipped for repository lookup: {path}"
|
return f"Path is skipped for repository lookup: {path}"
|
||||||
if not file_path.exists() or not file_path.is_file():
|
if not file_path.exists() or not file_path.is_file():
|
||||||
return f"File not found: {path}"
|
return f"File not found: {path}"
|
||||||
|
|
@ -89,7 +90,7 @@ class RepoTools:
|
||||||
files = [root] if root.is_file() else [item for item in root.rglob("*") if item.is_file()]
|
files = [root] if root.is_file() else [item for item in root.rglob("*") if item.is_file()]
|
||||||
matches: list[str] = []
|
matches: list[str] = []
|
||||||
for file_path in sorted(files):
|
for file_path in sorted(files):
|
||||||
if _is_skipped_repo_path(file_path, self.project_root):
|
if _is_skipped_repo_path(file_path, self.project_root, self.skipped_parts):
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
text = file_path.read_text(encoding="utf-8", errors="replace")
|
text = file_path.read_text(encoding="utf-8", errors="replace")
|
||||||
|
|
@ -270,9 +271,9 @@ def _relative(path: Path, root: Path) -> str:
|
||||||
return path.as_posix()
|
return path.as_posix()
|
||||||
|
|
||||||
|
|
||||||
def _is_skipped_repo_path(path: Path, root: Path) -> bool:
|
def _is_skipped_repo_path(path: Path, root: Path, skipped_parts: set[str]) -> bool:
|
||||||
try:
|
try:
|
||||||
parts = set(path.relative_to(root).parts)
|
parts = set(path.relative_to(root).parts)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
parts = set(path.parts)
|
parts = set(path.parts)
|
||||||
return bool(parts & SKIPPED_REPO_PARTS)
|
return bool(parts & skipped_parts)
|
||||||
|
|
|
||||||
|
|
@ -339,7 +339,7 @@ If you need repository context before planning, output lookup requests exactly l
|
||||||
|
|
||||||
lookup_requests:
|
lookup_requests:
|
||||||
- tool: read_file
|
- tool: read_file
|
||||||
path: relative/path.py
|
path: relative/path.to
|
||||||
- tool: grep
|
- tool: grep
|
||||||
path: .
|
path: .
|
||||||
pattern: search_regex
|
pattern: search_regex
|
||||||
|
|
@ -356,7 +356,7 @@ REAL_MODEL_IMPLEMENTER_PROMPT = """You are the implementation agent for NightShi
|
||||||
|
|
||||||
Output only complete file content blocks.
|
Output only complete file content blocks.
|
||||||
Use one fenced block per file with this exact opening form:
|
Use one fenced block per file with this exact opening form:
|
||||||
```file:relative/path.py
|
```file:relative/path.to
|
||||||
<complete file content>
|
<complete file content>
|
||||||
```
|
```
|
||||||
Do not include explanations before or after the file blocks.
|
Do not include explanations before or after the file blocks.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user