Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions nightshift/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
]

HIGH_SIGNAL_PATH_CANDIDATES = [
# JS/TS
"src/lib/auth",
"src/lib/http.ts",
"src/app/api",
Expand All @@ -107,6 +108,22 @@
"lib/auth",
"lib/http.ts",
"app/api",
# Python
"app/models",
"app/auth",
"app/api",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Deduplicate high-signal path candidates

The new entries add duplicates of existing candidates (for example app/api is already present earlier in the same list), which causes high_signal_focus_paths() to emit repeated hints instead of distinct areas to explore. In repos where only a duplicated path exists, the focus list can become ['app/api', 'app/api'] (and similarly for src/api), reducing prompt quality and path diversity for each cycle.

Useful? React with 👍 / 👎.

"src/auth",
"src/api",
# Go
"internal/auth",
"internal/api",
"pkg/api",
"cmd",
# Rust
"src/main.rs",
"src/lib.rs",
"src/api",
# Generic
"server",
"api",
"backend",
Expand Down
33 changes: 19 additions & 14 deletions nightshift/cycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,19 +414,10 @@ def build_prompt(

def high_signal_focus_paths(repo_dir: Path, hot_files: list[str]) -> list[str]:
hot_prefixes = {entry for entry in hot_files if entry}
suggestions: list[str] = []
for candidate in HIGH_SIGNAL_PATH_CANDIDATES:
if any(candidate == hot or candidate.startswith(f"{hot}/") for hot in hot_prefixes):
continue
if not (repo_dir / candidate).exists():
continue
suggestions.append(candidate)
if len(suggestions) >= 5:
break
if suggestions:
return suggestions
fallback = [candidate for candidate in HIGH_SIGNAL_PATH_CANDIDATES if (repo_dir / candidate).exists()]
return fallback[:5]
existing = [c for c in HIGH_SIGNAL_PATH_CANDIDATES if (repo_dir / c).exists()]
filtered = [c for c in existing if not any(c == hot or c.startswith(f"{hot}/") for hot in hot_prefixes)]
candidates = filtered[:5] if filtered else existing[:5]
return candidates


def recent_hot_files(repo_dir: Path) -> list[str]:
Expand Down Expand Up @@ -510,7 +501,21 @@ def parse_cycle_result(


def _extract_shell_command(command: str) -> str:
shell_match = re.search(r"-lc\s+['\"](?P<body>.+?)['\"]$", command)
"""Extract the inner command from common shell wrappers.

Handles patterns like:
- /bin/zsh -lc 'npm run lint'
- bash -c "npm test"
- sh -c 'npm run build'
- raw commands without a wrapper
"""
shell_match = re.search(
r"(?:/(?:usr/)?(?:bin/)?)?"
r"(?:bash|sh|zsh|dash)\s+"
r"(?:-\w+\s+)*"
r"['\"](?P<body>.+?)['\"]$",
command,
)
if shell_match:
return shell_match.group("body").strip()
return command.strip()
Expand Down
26 changes: 26 additions & 0 deletions tests/test_nightshift.py
Original file line number Diff line number Diff line change
Expand Up @@ -1395,6 +1395,32 @@ def test_ignores_non_forbidden_and_duplicates(self) -> None:
result = nightshift.forbidden_cycle_commands(raw_output)
assert result == ["npm run lint"]

def test_detects_bash_c_wrapper(self) -> None:
raw_output = json.dumps(
{
"type": "item.started",
"item": {
"type": "command_execution",
"command": 'bash -c "npm run build"',
},
}
)
result = nightshift.forbidden_cycle_commands(raw_output)
assert result == ["npm run build"]

def test_detects_sh_c_wrapper(self) -> None:
raw_output = json.dumps(
{
"type": "item.started",
"item": {
"type": "command_execution",
"command": "sh -c 'npm test'",
},
}
)
result = nightshift.forbidden_cycle_commands(raw_output)
assert result == ["npm test"]


class TestForbiddenReportedCommands:
def test_detects_forbidden_commands_from_structured_tests_run(self) -> None:
Expand Down