fix: support bounded recursive glob across lessons

This commit is contained in:
Haoran
2026-08-23 11:28:14 +08:00
parent 1342a990a2
commit 61c3e64595
19 changed files with 189 additions and 83 deletions

View File

@@ -74,7 +74,12 @@ def run_edit(path, old_text, new_text):
def run_glob(pattern): def run_glob(pattern):
import glob as g import glob as g
return "\n".join(g.glob(pattern, root_dir=WORKDIR)) matches = sorted(set(g.glob(
pattern, root_dir=WORKDIR, recursive=True)))
shown = matches[:200]
if len(matches) > 200:
shown.append("... (more matches omitted; narrow the pattern)")
return "\n".join(shown)
``` ```
--- ---

View File

@@ -74,7 +74,12 @@ def run_edit(path, old_text, new_text):
def run_glob(pattern): def run_glob(pattern):
import glob as g import glob as g
return "\n".join(g.glob(pattern, root_dir=WORKDIR)) matches = sorted(set(g.glob(
pattern, root_dir=WORKDIR, recursive=True)))
shown = matches[:200]
if len(matches) > 200:
shown.append("... (more matches omitted; narrow the pattern)")
return "\n".join(shown)
``` ```
--- ---

View File

@@ -74,7 +74,12 @@ def run_edit(path, old_text, new_text):
def run_glob(pattern): def run_glob(pattern):
import glob as g import glob as g
return "\n".join(g.glob(pattern, root_dir=WORKDIR)) matches = sorted(set(g.glob(
pattern, root_dir=WORKDIR, recursive=True)))
shown = matches[:200]
if len(matches) > 200:
shown.append("... (more matches omitted; narrow the pattern)")
return "\n".join(shown)
``` ```
--- ---

View File

@@ -110,11 +110,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
def run_glob(pattern: str) -> str: def run_glob(pattern: str) -> str:
import glob as g import glob as g
try: try:
results = [] matches = sorted({
for match in g.glob(pattern, root_dir=WORKDIR): match for match in g.glob(
if (WORKDIR / match).resolve().is_relative_to(WORKDIR): pattern, root_dir=WORKDIR, recursive=True)
results.append(match) if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
return "\n".join(results) if results else "(no matches)" })
shown = matches[:200]
if len(matches) > 200:
shown.append("... (more matches omitted; narrow the pattern)")
return "\n".join(shown) if shown else "(no matches)"
except Exception as e: except Exception as e:
return f"Error: {e}" return f"Error: {e}"
@@ -130,7 +134,7 @@ TOOLS = [
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
{"name": "edit_file", "description": "Replace exact text in a file once.", {"name": "edit_file", "description": "Replace exact text in a file once.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
{"name": "glob", "description": "Find files matching a glob pattern.", {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.",
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
] ]

View File

@@ -105,11 +105,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
def run_glob(pattern: str) -> str: def run_glob(pattern: str) -> str:
import glob as g import glob as g
try: try:
results = [] matches = sorted({
for match in g.glob(pattern, root_dir=WORKDIR): match for match in g.glob(
if (WORKDIR / match).resolve().is_relative_to(WORKDIR): pattern, root_dir=WORKDIR, recursive=True)
results.append(match) if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
return "\n".join(results) if results else "(no matches)" })
shown = matches[:200]
if len(matches) > 200:
shown.append("... (more matches omitted; narrow the pattern)")
return "\n".join(shown) if shown else "(no matches)"
except Exception as e: except Exception as e:
return f"Error: {e}" return f"Error: {e}"
@@ -125,7 +129,7 @@ TOOLS = [
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
{"name": "edit_file", "description": "Replace exact text in a file once.", {"name": "edit_file", "description": "Replace exact text in a file once.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
{"name": "glob", "description": "Find files matching a glob pattern.", {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.",
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
] ]

View File

@@ -91,11 +91,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
def run_glob(pattern: str) -> str: def run_glob(pattern: str) -> str:
import glob as g import glob as g
try: try:
results = [] matches = sorted({
for match in g.glob(pattern, root_dir=WORKDIR): match for match in g.glob(
if (WORKDIR / match).resolve().is_relative_to(WORKDIR): pattern, root_dir=WORKDIR, recursive=True)
results.append(match) if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
return "\n".join(results) if results else "(no matches)" })
shown = matches[:200]
if len(matches) > 200:
shown.append("... (more matches omitted; narrow the pattern)")
return "\n".join(shown) if shown else "(no matches)"
except Exception as e: except Exception as e:
return f"Error: {e}" return f"Error: {e}"
@@ -108,7 +112,7 @@ TOOLS = [
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
{"name": "edit_file", "description": "Replace exact text in a file once.", {"name": "edit_file", "description": "Replace exact text in a file once.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
{"name": "glob", "description": "Find files matching a glob pattern.", {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.",
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
] ]

View File

@@ -96,11 +96,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
def run_glob(pattern: str) -> str: def run_glob(pattern: str) -> str:
import glob as g import glob as g
try: try:
results = [] matches = sorted({
for match in g.glob(pattern, root_dir=WORKDIR): match for match in g.glob(
if (WORKDIR / match).resolve().is_relative_to(WORKDIR): pattern, root_dir=WORKDIR, recursive=True)
results.append(match) if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
return "\n".join(results) if results else "(no matches)" })
shown = matches[:200]
if len(matches) > 200:
shown.append("... (more matches omitted; narrow the pattern)")
return "\n".join(shown) if shown else "(no matches)"
except Exception as e: except Exception as e:
return f"Error: {e}" return f"Error: {e}"
@@ -186,7 +190,7 @@ TOOLS = [
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
{"name": "edit_file", "description": "Replace exact text in a file once.", {"name": "edit_file", "description": "Replace exact text in a file once.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
{"name": "glob", "description": "Find files matching a glob pattern.", {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.",
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
# s05: new tool # s05: new tool
{"name": "todo_write", "description": "Create and manage a task list for your current coding session.", {"name": "todo_write", "description": "Create and manage a task list for your current coding session.",

View File

@@ -101,11 +101,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
def run_glob(pattern: str) -> str: def run_glob(pattern: str) -> str:
import glob import glob
try: try:
matches = [] matches = sorted({
for match in glob.glob(pattern, root_dir=WORKDIR): match for match in glob.glob(
if (WORKDIR / match).resolve().is_relative_to(WORKDIR): pattern, root_dir=WORKDIR, recursive=True)
matches.append(match) if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
return "\n".join(matches) if matches else "(no matches)" })
shown = matches[:200]
if len(matches) > 200:
shown.append("... (more matches omitted; narrow the pattern)")
return "\n".join(shown) if shown else "(no matches)"
except Exception as e: except Exception as e:
return f"Error: {e}" return f"Error: {e}"
@@ -119,7 +123,7 @@ BASE_TOOLS = [
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
{"name": "edit_file", "description": "Replace exact text in a file once.", {"name": "edit_file", "description": "Replace exact text in a file once.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
{"name": "glob", "description": "Find files matching a glob pattern.", {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.",
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
] ]

View File

@@ -185,11 +185,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
def run_glob(pattern: str) -> str: def run_glob(pattern: str) -> str:
import glob import glob
try: try:
matches = [] matches = sorted({
for match in glob.glob(pattern, root_dir=WORKDIR): match for match in glob.glob(
if (WORKDIR / match).resolve().is_relative_to(WORKDIR): pattern, root_dir=WORKDIR, recursive=True)
matches.append(match) if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
return "\n".join(matches) if matches else "(no matches)" })
shown = matches[:200]
if len(matches) > 200:
shown.append("... (more matches omitted; narrow the pattern)")
return "\n".join(shown) if shown else "(no matches)"
except Exception as e: except Exception as e:
return f"Error: {e}" return f"Error: {e}"
@@ -203,7 +207,7 @@ TOOLS = [
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
{"name": "edit_file", "description": "Replace exact text in a file once.", {"name": "edit_file", "description": "Replace exact text in a file once.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
{"name": "glob", "description": "Find files matching a glob pattern.", {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.",
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
{"name": "load_skill", "description": "Load the full SKILL.md content by skill name.", {"name": "load_skill", "description": "Load the full SKILL.md content by skill name.",
"input_schema": {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}}, "input_schema": {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}},

View File

@@ -118,11 +118,14 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
def run_glob(pattern: str) -> str: def run_glob(pattern: str) -> str:
try: try:
matches = [ matches = sorted({
match for match in glob.glob(pattern, root_dir=WORKDIR, recursive=True) match for match in glob.glob(pattern, root_dir=WORKDIR, recursive=True)
if (WORKDIR / match).resolve().is_relative_to(WORKDIR) if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
] })
return "\n".join(matches) if matches else "(no matches)" shown = matches[:200]
if len(matches) > 200:
shown.append("... (more matches omitted; narrow the pattern)")
return "\n".join(shown) if shown else "(no matches)"
except Exception as error: except Exception as error:
return f"Error: {error}" return f"Error: {error}"
@@ -136,7 +139,7 @@ BASE_TOOLS = [
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
{"name": "edit_file", "description": "Replace exact text in a file once.", {"name": "edit_file", "description": "Replace exact text in a file once.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
{"name": "glob", "description": "Find files matching a glob pattern.", {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.",
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
] ]
COMPACT_TOOL = { COMPACT_TOOL = {

View File

@@ -579,12 +579,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
def run_glob(pattern: str) -> str: def run_glob(pattern: str) -> str:
try: try:
matches = [ matches = sorted({
match match
for match in glob.glob(pattern, root_dir=WORKDIR) for match in glob.glob(pattern, root_dir=WORKDIR, recursive=True)
if (WORKDIR / match).resolve().is_relative_to(WORKDIR) if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
] })
return "\n".join(matches) if matches else "(no matches)" shown = matches[:200]
if len(matches) > 200:
shown.append("... (more matches omitted; narrow the pattern)")
return "\n".join(shown) if shown else "(no matches)"
except Exception as error: except Exception as error:
return f"Error: {error}" return f"Error: {error}"
@@ -597,7 +600,7 @@ TOOLS = [
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
{"name": "edit_file", "description": "Replace exact text in a file once.", {"name": "edit_file", "description": "Replace exact text in a file once.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
{"name": "glob", "description": "Find files matching a glob pattern.", {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.",
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
] ]

View File

@@ -325,12 +325,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
def run_glob(pattern: str) -> str: def run_glob(pattern: str) -> str:
try: try:
matches = [ matches = sorted({
match match
for match in glob.glob(pattern, root_dir=WORKDIR) for match in glob.glob(pattern, root_dir=WORKDIR, recursive=True)
if (WORKDIR / match).resolve().is_relative_to(WORKDIR) if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
] })
return "\n".join(matches) if matches else "(no matches)" shown = matches[:200]
if len(matches) > 200:
shown.append("... (more matches omitted; narrow the pattern)")
return "\n".join(shown) if shown else "(no matches)"
except Exception as error: except Exception as error:
return f"Error: {error}" return f"Error: {error}"
@@ -392,7 +395,7 @@ TOOLS = [
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}, "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
{"name": "edit_file", "description": "Replace exact text in a file once.", {"name": "edit_file", "description": "Replace exact text in a file once.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
{"name": "glob", "description": "Find files matching a glob pattern.", {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.",
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
{"name": "create_task", "description": "Create a task and return its runtime-generated ID.", {"name": "create_task", "description": "Create a task and return its runtime-generated ID.",
"input_schema": {"type": "object", "properties": {"subject": {"type": "string"}, "description": {"type": "string"}}, "required": ["subject"], "additionalProperties": False}}, "input_schema": {"type": "object", "properties": {"subject": {"type": "string"}, "description": {"type": "string"}}, "required": ["subject"], "additionalProperties": False}},

View File

@@ -156,12 +156,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
def run_glob(pattern: str) -> str: def run_glob(pattern: str) -> str:
try: try:
matches = [ matches = sorted({
match match
for match in glob.glob(pattern, root_dir=WORKDIR) for match in glob.glob(pattern, root_dir=WORKDIR, recursive=True)
if (WORKDIR / match).resolve().is_relative_to(WORKDIR) if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
] })
return "\n".join(matches) if matches else "(no matches)" shown = matches[:200]
if len(matches) > 200:
shown.append("... (more matches omitted; narrow the pattern)")
return "\n".join(shown) if shown else "(no matches)"
except Exception as error: except Exception as error:
return f"Error: {error}" return f"Error: {error}"
@@ -189,7 +192,7 @@ TOOLS = [
"old_text": {"type": "string"}, "old_text": {"type": "string"},
"new_text": {"type": "string"}}, "new_text": {"type": "string"}},
"required": ["path", "old_text", "new_text"]}}, "required": ["path", "old_text", "new_text"]}},
{"name": "glob", "description": "Find files matching a glob pattern.", {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.",
"input_schema": {"type": "object", "input_schema": {"type": "object",
"properties": {"pattern": {"type": "string"}}, "properties": {"pattern": {"type": "string"}},
"required": ["pattern"]}}, "required": ["pattern"]}},

View File

@@ -106,12 +106,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
def run_glob(pattern: str) -> str: def run_glob(pattern: str) -> str:
try: try:
matches = [ matches = sorted({
match match
for match in glob.glob(pattern, root_dir=WORKDIR) for match in glob.glob(pattern, root_dir=WORKDIR, recursive=True)
if (WORKDIR / match).resolve().is_relative_to(WORKDIR) if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
] })
return "\n".join(matches) if matches else "(no matches)" shown = matches[:200]
if len(matches) > 200:
shown.append("... (more matches omitted; narrow the pattern)")
return "\n".join(shown) if shown else "(no matches)"
except Exception as error: except Exception as error:
return f"Error: {error}" return f"Error: {error}"
@@ -137,7 +140,7 @@ TOOLS = [
"old_text": {"type": "string"}, "old_text": {"type": "string"},
"new_text": {"type": "string"}}, "new_text": {"type": "string"}},
"required": ["path", "old_text", "new_text"]}}, "required": ["path", "old_text", "new_text"]}},
{"name": "glob", "description": "Find files matching a glob pattern.", {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.",
"input_schema": {"type": "object", "input_schema": {"type": "object",
"properties": {"pattern": {"type": "string"}}, "properties": {"pattern": {"type": "string"}},
"required": ["pattern"]}}, "required": ["pattern"]}},

View File

@@ -727,7 +727,10 @@ def run_glob(pattern: str, cwd: Path | None = None) -> str:
for path in sorted(base.glob(pattern)) for path in sorted(base.glob(pattern))
if path.resolve().is_relative_to(base) if path.resolve().is_relative_to(base)
] ]
return "\n".join(matches[:200]) or "No files found" shown = matches[:200]
if len(matches) > 200:
shown.append("... (more matches omitted; narrow the pattern)")
return "\n".join(shown) or "No files found"
except Exception as exc: except Exception as exc:
return f"Error: {exc}" return f"Error: {exc}"
@@ -1519,7 +1522,7 @@ BASE_TOOLS = [
"old_text": {"type": "string"}, "old_text": {"type": "string"},
"new_text": {"type": "string"}}, "new_text": {"type": "string"}},
"required": ["path", "old_text", "new_text"]}}, "required": ["path", "old_text", "new_text"]}},
{"name": "glob", "description": "Find files by glob pattern.", {"name": "glob", "description": "Find files by glob pattern; ** matches recursively.",
"input_schema": {"type": "object", "input_schema": {"type": "object",
"properties": {"pattern": {"type": "string"}}, "properties": {"pattern": {"type": "string"}},
"required": ["pattern"]}}, "required": ["pattern"]}},

View File

@@ -109,12 +109,15 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
def run_glob(pattern: str) -> str: def run_glob(pattern: str) -> str:
try: try:
matches = [ matches = sorted({
match match
for match in glob.glob(pattern, root_dir=WORKDIR) for match in glob.glob(pattern, root_dir=WORKDIR, recursive=True)
if (WORKDIR / match).resolve().is_relative_to(WORKDIR.resolve()) if (WORKDIR / match).resolve().is_relative_to(WORKDIR.resolve())
] })
return "\n".join(matches[:200]) if matches else "(no matches)" shown = matches[:200]
if len(matches) > 200:
shown.append("... (more matches omitted; narrow the pattern)")
return "\n".join(shown) if shown else "(no matches)"
except Exception as exc: except Exception as exc:
return f"Error: {exc}" return f"Error: {exc}"
@@ -140,7 +143,7 @@ BASE_TOOLS = [
"old_text": {"type": "string"}, "old_text": {"type": "string"},
"new_text": {"type": "string"}}, "new_text": {"type": "string"}},
"required": ["path", "old_text", "new_text"]}}, "required": ["path", "old_text", "new_text"]}},
{"name": "glob", "description": "Find files by glob pattern.", {"name": "glob", "description": "Find files by glob pattern; ** matches recursively.",
"input_schema": {"type": "object", "input_schema": {"type": "object",
"properties": {"pattern": {"type": "string"}}, "properties": {"pattern": {"type": "string"}},
"required": ["pattern"]}}, "required": ["pattern"]}},

View File

@@ -972,11 +972,15 @@ def run_glob(pattern: str, cwd: Path | None = None) -> str:
import glob as g import glob as g
try: try:
base = (cwd or WORKDIR).resolve() base = (cwd or WORKDIR).resolve()
results = [] matches = sorted({
for match in g.glob(pattern, root_dir=base): match for match in g.glob(
if (base / match).resolve().is_relative_to(base): pattern, root_dir=base, recursive=True)
results.append(match) if (base / match).resolve().is_relative_to(base)
return "\n".join(results) if results else "(no matches)" })
shown = matches[:200]
if len(matches) > 200:
shown.append("... (more matches omitted; narrow the pattern)")
return "\n".join(shown) if shown else "(no matches)"
except Exception as e: except Exception as e:
return f"Error: {e}" return f"Error: {e}"
@@ -1487,7 +1491,7 @@ def spawn_teammate_thread(name: str, role: str, prompt: str,
"old_text": {"type": "string"}, "old_text": {"type": "string"},
"new_text": {"type": "string"}}, "new_text": {"type": "string"}},
"required": ["path", "old_text", "new_text"]}}, "required": ["path", "old_text", "new_text"]}},
{"name": "glob", "description": "Find files by glob pattern.", {"name": "glob", "description": "Find files by glob pattern; ** matches recursively.",
"input_schema": {"type": "object", "input_schema": {"type": "object",
"properties": { "properties": {
"pattern": {"type": "string"}}, "pattern": {"type": "string"}},
@@ -1849,7 +1853,7 @@ SUB_TOOLS = [
"old_text": {"type": "string"}, "old_text": {"type": "string"},
"new_text": {"type": "string"}}, "new_text": {"type": "string"}},
"required": ["path", "old_text", "new_text"]}}, "required": ["path", "old_text", "new_text"]}},
{"name": "glob", "description": "Find files matching a glob pattern.", {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.",
"input_schema": {"type": "object", "input_schema": {"type": "object",
"properties": {"pattern": {"type": "string"}}, "properties": {"pattern": {"type": "string"}},
"required": ["pattern"]}}, "required": ["pattern"]}},
@@ -2776,7 +2780,7 @@ BUILTIN_TOOLS = [
"old_text": {"type": "string"}, "old_text": {"type": "string"},
"new_text": {"type": "string"}}, "new_text": {"type": "string"}},
"required": ["path", "old_text", "new_text"]}}, "required": ["path", "old_text", "new_text"]}},
{"name": "glob", "description": "Find files matching a glob pattern.", {"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.",
"input_schema": {"type": "object", "input_schema": {"type": "object",
"properties": {"pattern": {"type": "string"}}, "properties": {"pattern": {"type": "string"}},
"required": ["pattern"]}}, "required": ["pattern"]}},

View File

@@ -515,7 +515,7 @@ TOOLS = [
}, },
{ {
"name": "glob", "name": "glob",
"description": "Find files matching a glob pattern.", "description": "Find files matching a glob pattern; ** matches recursively.",
"input_schema": { "input_schema": {
"type": "object", "type": "object",
"properties": {"pattern": {"type": "string"}}, "properties": {"pattern": {"type": "string"}},
@@ -797,12 +797,16 @@ class AgentSession:
return f"Edited {path.relative_to(self.workdir)}" return f"Edited {path.relative_to(self.workdir)}"
if name == "glob": if name == "glob":
matches = [ matches = sorted({
match match
for match in glob.glob(str(arguments["pattern"]), root_dir=self.workdir) for match in glob.glob(
str(arguments["pattern"]), root_dir=self.workdir, recursive=True)
if (self.workdir / match).resolve().is_relative_to(self.workdir) if (self.workdir / match).resolve().is_relative_to(self.workdir)
] })
return "\n".join(matches[:200]) if matches else "(no matches)" shown = matches[:200]
if len(matches) > 200:
shown.append("... (more matches omitted; narrow the pattern)")
return "\n".join(shown) if shown else "(no matches)"
raise GoalError(f"unknown tool '{name}'") raise GoalError(f"unknown tool '{name}'")

View File

@@ -30,6 +30,8 @@ LESSONS = tuple(
) )
) )
INTEGRATED_LESSON = ROOT / "s15_integrated_harness" / "code.py" INTEGRATED_LESSON = ROOT / "s15_integrated_harness" / "code.py"
GOAL_LESSON = ROOT / "s17_goal_loop" / "code.py"
GLOB_LESSONS = (*LESSONS[1:], INTEGRATED_LESSON, GOAL_LESSON)
class FakeMessagesApi: class FakeMessagesApi:
@@ -137,6 +139,42 @@ def bash_tool_call():
) )
def run_glob_tool(lesson, workdir: Path, pattern: str) -> str:
if hasattr(lesson, "run_glob"):
return lesson.run_glob(pattern)
session = object.__new__(lesson.AgentSession)
session.workdir = workdir.resolve()
return session._run_tool("glob", {"pattern": pattern})
@pytest.mark.parametrize("lesson_path", GLOB_LESSONS,
ids=lambda path: path.parent.name)
def test_glob_double_star_matches_files_at_any_depth(
tmp_path: Path, lesson_path: Path):
(tmp_path / "root.py").write_text("")
(tmp_path / "one" / "two").mkdir(parents=True)
(tmp_path / "one" / "one.py").write_text("")
(tmp_path / "one" / "two" / "deep.py").write_text("")
lesson = load_lesson(tmp_path, lesson_path)
matches = set(run_glob_tool(lesson, tmp_path, "**/*.py").splitlines())
assert matches == {"root.py", "one/one.py", "one/two/deep.py"}
@pytest.mark.parametrize("lesson_path", GLOB_LESSONS,
ids=lambda path: path.parent.name)
def test_glob_caps_large_result_sets(tmp_path: Path, lesson_path: Path):
for index in range(205):
(tmp_path / f"file-{index:03}.txt").write_text("")
lesson = load_lesson(tmp_path, lesson_path)
lines = run_glob_tool(lesson, tmp_path, "*.txt").splitlines()
assert len(lines) == 201
assert lines[-1] == "... (more matches omitted; narrow the pattern)"
@pytest.mark.parametrize("lesson_path", LESSONS, ids=lambda path: path.parent.name) @pytest.mark.parametrize("lesson_path", LESSONS, ids=lambda path: path.parent.name)
@pytest.mark.parametrize("content", ([], None), ids=("empty-content", "empty-text")) @pytest.mark.parametrize("content", ([], None), ids=("empty-content", "empty-text"))
def test_parent_loop_does_not_append_an_empty_tool_result_turn( def test_parent_loop_does_not_append_an_empty_tool_result_turn(