Skip to content

Commit 011ead5

Browse files
committed
fix: simplify list files and fix log properties
1 parent 2e5bdfc commit 011ead5

File tree

4 files changed

+157
-122
lines changed

4 files changed

+157
-122
lines changed

moatless/actions/list_files.py

Lines changed: 49 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
logger = logging.getLogger(__name__)
1515

1616
# Default directories to always ignore
17-
DEFAULT_IGNORED_DIRS = [".git", ".cursor", ".mvn", ".venv"]
17+
DEFAULT_IGNORED_DIRS = [".git", ".venv"]
1818

1919

2020
class ListFilesArgs(ActionArguments):
@@ -32,6 +32,10 @@ class ListFilesArgs(ActionArguments):
3232
default=100,
3333
description="Maximum number of results (files and directories) to return.",
3434
)
35+
show_hidden: bool = Field(
36+
default=False,
37+
description="Whether to show hidden files and directories (starting with '.'). Always excludes .git and .venv.",
38+
)
3539

3640
model_config = ConfigDict(title="ListFiles")
3741

@@ -45,6 +49,8 @@ def short_summary(self) -> str:
4549
param_str += f", recursive={self.recursive}"
4650
if self.max_results != 100:
4751
param_str += f", max_results={self.max_results}"
52+
if self.show_hidden:
53+
param_str += f", show_hidden={self.show_hidden}"
4854
return f"{self.name}({param_str})"
4955

5056
@classmethod
@@ -123,32 +129,13 @@ async def execute(
123129
dir_path = list_files_args.directory.rstrip("/")
124130
target_dir = f"./{dir_path}" if dir_path else "."
125131

126-
# Check if git is available in the workspace
127-
git_available = False
128-
try:
129-
git_check = await local_env.execute("git rev-parse --is-inside-work-tree 2>/dev/null || echo 'false'")
130-
git_available = git_check.strip() == "true"
131-
except Exception:
132-
# If the command fails, assume git is not available
133-
git_available = False
134-
135-
# Legacy variables no longer needed due to clearer builders
136-
# (kept minimal changes to surrounding logic)
137-
138-
# Build the commands using helpers for clarity
139-
if git_available:
140-
dirs_command, files_command = build_git_commands(dir_path, target_dir, list_files_args.recursive, self.ignored_dirs)
141-
else:
142-
dirs_command, files_command = build_fs_commands(dir_path, target_dir, list_files_args.recursive, self.ignored_dirs)
132+
# Build the commands using filesystem only
133+
dirs_command, files_command = build_commands(dir_path, target_dir, list_files_args.recursive, self.ignored_dirs)
143134

144135
try:
145136
# Execute commands to get directories and files
146137
try:
147-
# Execute commands to get directories and files
148-
if dirs_command is not None:
149-
dirs_output = await local_env.execute(dirs_command, patch=patch)
150-
else:
151-
dirs_output = ""
138+
dirs_output = await local_env.execute(dirs_command, patch=patch)
152139
files_output = await local_env.execute(files_command, patch=patch)
153140

154141
except Exception as e:
@@ -160,8 +147,7 @@ async def execute(
160147
)
161148
raise # Re-raise if it's a different error
162149

163-
# Process directory results (helpers for clarity)
164-
directories: list[str] = []
150+
# Check for directory not found
165151
if (dirs_output and "No such file or directory" in dirs_output) or (
166152
files_output and "No such file or directory" in files_output
167153
):
@@ -170,25 +156,27 @@ async def execute(
170156
properties={"fail_reason": "directory_not_found"},
171157
)
172158

159+
# Process directory results
160+
directories: list[str] = []
173161
for line in dirs_output.strip().split("\n"):
174162
rel_path = normalize_rel(line.strip())
175163
if not rel_path or rel_path == dir_path:
176164
continue
177165
if not list_files_args.recursive:
178166
dir_name = rel_path.replace(f"{dir_path}/", "") if dir_path else rel_path
179-
if not dir_name or should_skip_dir(dir_name, self.ignored_dirs):
167+
if not dir_name or should_skip_dir(dir_name, list_files_args.show_hidden):
180168
continue
181169
directories.append(dir_name)
182170
else:
183-
if should_skip_dir(rel_path, self.ignored_dirs):
171+
if should_skip_dir(rel_path, list_files_args.show_hidden):
184172
continue
185173
directories.append(rel_path)
186174

187-
# Process file results (helpers for clarity)
175+
# Process file results
188176
files: list[str] = []
189177
for line in files_output.strip().split("\n"):
190178
rel_path = normalize_rel(line.strip())
191-
if not rel_path or should_skip_file(rel_path, self.ignored_dirs):
179+
if not rel_path or should_skip_file(rel_path, list_files_args.show_hidden):
192180
continue
193181

194182
if not list_files_args.recursive:
@@ -217,8 +205,7 @@ async def execute(
217205
"total_files": len(files),
218206
"max_results": list_files_args.max_results,
219207
"results_limited": len(directories) + len(files) == list_files_args.max_results,
220-
"git_available": git_available,
221-
"ignored_dirs": self.ignored_dirs,
208+
"show_hidden": list_files_args.show_hidden,
222209
}
223210

224211
except Exception as e:
@@ -260,10 +247,10 @@ async def execute(
260247
)
261248

262249
recursive_str = " (recursive)" if list_files_args.recursive else ""
263-
gitignore_str = " (respecting .gitignore)" if git_available else ""
250+
hidden_str = " (including hidden)" if list_files_args.show_hidden else ""
264251

265252
message = (
266-
f"Contents of directory '{list_files_args.directory or '(root)'}'{recursive_str}{gitignore_str}\n\n"
253+
f"Contents of directory '{list_files_args.directory or '(root)'}'{recursive_str}{hidden_str}\n\n"
267254
)
268255

269256
if result["directories"]:
@@ -329,30 +316,34 @@ def is_hidden_segment(segments: list[str]) -> bool:
329316
return any(seg.startswith(".") for seg in segments)
330317

331318

332-
def should_skip_dir(rel_path: str, ignored_dirs: list[str]) -> bool:
319+
def should_skip_dir(rel_path: str, show_hidden: bool) -> bool:
333320
if not rel_path:
334321
return True
335-
if rel_path in ignored_dirs or any(rel_path.startswith(f"{d}/") for d in ignored_dirs):
336-
return True
337-
if rel_path.startswith("."):
338-
return True
339-
parts = rel_path.split("/")
340-
if is_hidden_segment(parts):
322+
# Always skip .git and .venv
323+
if rel_path in [".git", ".venv"] or any(rel_path.startswith(f"{d}/") for d in [".git", ".venv"]):
341324
return True
325+
# Skip hidden files/dirs unless show_hidden is True
326+
if not show_hidden:
327+
if rel_path.startswith("."):
328+
return True
329+
parts = rel_path.split("/")
330+
if is_hidden_segment(parts):
331+
return True
342332
return False
343333

344334

345-
def should_skip_file(rel_path: str, ignored_dirs: list[str]) -> bool:
335+
def should_skip_file(rel_path: str, show_hidden: bool) -> bool:
346336
if not rel_path:
347337
return True
348338
parts = rel_path.split("/")
349339
base = parts[-1]
350-
# In ignored dir
351-
if any(f"/{ignored_dir}/" in f"/{rel_path}/" for ignored_dir in ignored_dirs):
352-
return True
353-
# Hidden dir or hidden file
354-
if is_hidden_segment(parts[:-1]) or base.startswith("."):
340+
# Skip files in .git and .venv directories
341+
if any(f"/{ignored_dir}/" in f"/{rel_path}/" for ignored_dir in [".git", ".venv"]):
355342
return True
343+
# Skip hidden files/dirs unless show_hidden is True
344+
if not show_hidden:
345+
if is_hidden_segment(parts[:-1]) or base.startswith("."):
346+
return True
356347
return False
357348

358349

@@ -382,60 +373,24 @@ def apply_limits(directories: list[str], files: list[str], max_results: int) ->
382373
}
383374

384375

385-
def _ignored_prune_expr(ignored_dirs: list[str]) -> str:
386-
if not ignored_dirs:
387-
return ""
388-
names = " -o ".join([f"-name {shlex.quote(d)}" for d in ignored_dirs])
389-
# Wrap in grouping for -prune usage
390-
return f"\( {names} \) -prune -o"
391376

392377

393-
def build_fs_commands(dir_path: str, target_dir: str, recursive: bool, ignored_dirs: list[str]) -> tuple[str, str]:
378+
def build_commands(dir_path: str, target_dir: str, recursive: bool, ignored_dirs: list[str]) -> tuple[str, str]:
379+
"""Build simple find commands for directories and files."""
394380
td = shlex.quote(target_dir)
395-
prune = _ignored_prune_expr(ignored_dirs)
381+
382+
# Always exclude .git and .venv directories using prune
383+
prune_expr = "\\( -name .git -o -name .venv \\) -prune -o"
384+
396385
if recursive:
397-
# All directories (excluding the root itself via -mindepth 1)
398-
dirs_cmd = f"find {td} -xdev -mindepth 1 -type d"
399-
if prune:
400-
dirs_cmd = f"find {td} -xdev {prune} -mindepth 1 -type d -print | sort"
401-
else:
402-
dirs_cmd = f"{dirs_cmd} | sort"
403-
files_cmd = f"find {td} -xdev -type f | sort"
386+
# All directories and files recursively
387+
dirs_cmd = f"find {td} -xdev {prune_expr} -mindepth 1 -type d -print | sort"
388+
files_cmd = f"find {td} -xdev {prune_expr} -type f -print | sort"
404389
else:
405390
# Immediate children only
406-
dirs_cmd = f"find {td} -xdev -maxdepth 1 -mindepth 1 -type d"
407-
if prune:
408-
dirs_cmd = f"find {td} -xdev -maxdepth 1 -mindepth 1 {prune} -type d -print | sort"
409-
else:
410-
dirs_cmd = f"{dirs_cmd} | sort"
411-
files_cmd = f"find {td} -xdev -maxdepth 1 -type f | sort"
412-
return dirs_cmd, files_cmd
413-
414-
415-
def build_git_commands(dir_path: str, target_dir: str, recursive: bool, ignored_dirs: list[str]) -> tuple[str, str]:
416-
# Files via git to respect .gitignore
417-
if dir_path:
418-
files_cmd = f"git ls-files --cached --others --exclude-standard -- {shlex.quote(dir_path)}/** | sort"
419-
else:
420-
files_cmd = "git ls-files --cached --others --exclude-standard | sort"
421-
422-
# Directories using find, then filter through git check-ignore
423-
td = shlex.quote(target_dir)
424-
prune = _ignored_prune_expr(ignored_dirs)
425-
if recursive:
426-
base_find = f"find {td} -xdev -mindepth 1 -type d"
427-
if prune:
428-
base_find = f"find {td} -xdev {prune} -mindepth 1 -type d -print"
429-
else:
430-
base_find = f"find {td} -xdev -maxdepth 1 -mindepth 1 -type d"
431-
if prune:
432-
base_find = f"find {td} -xdev -maxdepth 1 -mindepth 1 {prune} -type d -print"
433-
434-
# Keep the existing behavior: check each dir with git check-ignore
435-
dirs_cmd = (
436-
f"{base_find} | while read -r dir; do git check-ignore \"$dir/\" >/dev/null 2>&1 || echo \"$dir\"; done | sort"
437-
)
438-
391+
dirs_cmd = f"find {td} -xdev -maxdepth 1 {prune_expr} -mindepth 1 -type d -print | sort"
392+
files_cmd = f"find {td} -xdev -maxdepth 1 {prune_expr} -type f -print | sort"
393+
439394
return dirs_cmd, files_cmd
440395

441396

moatless/completion/base.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
from moatless.completion.stats import CompletionAttempt, CompletionInvocation
2727
from moatless.component import MoatlessComponent
2828
from moatless.exceptions import CompletionRejectError, CompletionRuntimeError
29+
from moatless.context_data import (
30+
current_node_id,
31+
current_action_step,
32+
current_phase,
33+
)
2934

3035
logger = logging.getLogger(__name__)
3136
tracer = trace.get_tracer(__name__)
@@ -488,16 +493,31 @@ async def _do_completion_with_rate_limit_retry():
488493
if "claude" in self.model:
489494
self._inject_prompt_caching(messages)
490495

496+
# Build LiteLLM metadata: model-level + node context
497+
request_metadata = dict(self.metadata or {})
498+
try:
499+
node_ctx = current_node_id.get()
500+
if node_ctx is not None:
501+
request_metadata["node_id"] = node_ctx
502+
step_ctx = current_action_step.get()
503+
if step_ctx is not None:
504+
request_metadata["action_step"] = step_ctx
505+
phase_ctx = current_phase.get()
506+
if phase_ctx is not None:
507+
request_metadata["phase"] = phase_ctx
508+
except Exception:
509+
pass
510+
491511
completion_kwargs = {
492512
"model": self.model,
493513
"max_tokens": self.max_tokens,
494514
"temperature": self.temperature,
495515
"messages": messages,
496-
"metadata": self.metadata or {},
516+
"metadata": request_metadata,
497517
"timeout": self.timeout,
498518
**self._completion_params,
499519
}
500-
520+
501521
if self.reasoning_effort:
502522
completion_kwargs["allowed_openai_params"] = ["reasoning_effort"]
503523
completion_kwargs["reasoning_effort"] = self.reasoning_effort

moatless/completion/log_handler.py

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,19 @@ def __init__(self, storage: BaseStorage):
1818
super().__init__()
1919
self._storage = storage
2020

21-
async def _get_log_path(self, filename: str | None = None):
21+
async def _get_log_path(
22+
self,
23+
filename: str | None = None,
24+
node_id_override: int | None = None,
25+
action_step_override: int | None = None,
26+
phase_override: str | None = None,
27+
):
2228
now = datetime.now()
23-
node_id = current_node_id.get()
24-
action_step = current_action_step.get()
25-
phase = current_phase.get()
29+
node_id = node_id_override if node_id_override is not None else current_node_id.get()
30+
action_step = (
31+
action_step_override if action_step_override is not None else current_action_step.get()
32+
)
33+
phase = phase_override if phase_override is not None else current_phase.get()
2634

2735
trajectory_key = self._storage.get_trajectory_path()
2836

@@ -54,11 +62,35 @@ async def _get_log_path(self, filename: str | None = None):
5462

5563
return log_path
5664

57-
async def _write_to_file_async(self, data: dict):
58-
log_path = await self._get_log_path()
65+
async def _write_to_file_async(
66+
self,
67+
data: dict,
68+
node_id_override: int | None = None,
69+
action_step_override: int | None = None,
70+
phase_override: str | None = None,
71+
):
72+
log_path = await self._get_log_path(
73+
node_id_override=node_id_override,
74+
action_step_override=action_step_override,
75+
phase_override=phase_override,
76+
)
5977

6078
try:
6179
await self._storage.write(path=log_path, data=data)
80+
# Explicit info log to trace saved location and context
81+
try:
82+
resolved_node = node_id_override if node_id_override is not None else current_node_id.get()
83+
resolved_step = (
84+
action_step_override if action_step_override is not None else current_action_step.get()
85+
)
86+
resolved_phase = phase_override if phase_override is not None else current_phase.get()
87+
logger.debug(
88+
f"Saved completion log to {log_path} "
89+
f"(node={resolved_node}, action_step={resolved_step}, phase={resolved_phase})"
90+
)
91+
except Exception:
92+
# Don't let logging issues affect flow
93+
pass
6294
except Exception as e:
6395
logger.exception(f"Failed to write log to {log_path}. Data: {data}")
6496

@@ -101,7 +133,31 @@ async def async_log_success_event(self, kwargs, response_obj, start_time, end_ti
101133
if "exception" in kwargs:
102134
data["exception"] = self._handle_kwargs_item(kwargs["exception"])
103135

104-
await self._write_to_file_async(data)
136+
# Derive node context from LiteLLM metadata
137+
node_id_override = None
138+
action_step_override = None
139+
phase_override = None
140+
141+
try:
142+
litellm_params = kwargs.get("litellm_params", {})
143+
if isinstance(litellm_params, dict):
144+
meta = litellm_params.get("metadata", {})
145+
if isinstance(meta, dict):
146+
if isinstance(meta.get("node_id"), int):
147+
node_id_override = meta.get("node_id")
148+
if isinstance(meta.get("action_step"), int):
149+
action_step_override = meta.get("action_step")
150+
if isinstance(meta.get("phase"), str):
151+
phase_override = meta.get("phase")
152+
except Exception:
153+
pass
154+
155+
await self._write_to_file_async(
156+
data,
157+
node_id_override=node_id_override,
158+
action_step_override=action_step_override,
159+
phase_override=phase_override,
160+
)
105161

106162
async def async_log_failure_event(self, kwargs, response_obj, start_time, end_time):
107163
logger.warning(f"Processing failure event for node {current_node_id.get()}")

0 commit comments

Comments
 (0)