From d18e928a6a619bd79d3c8ea6b8b27be5ff5b718b Mon Sep 17 00:00:00 2001 From: ezl-keygraph Date: Mon, 15 Jun 2026 20:03:26 +0530 Subject: [PATCH] feat(worker): add glob custom tool and route code_path globs to it --- .../prompts/shared/_code-path-rules.txt | 4 +- apps/worker/src/ai/pi-executor.ts | 3 +- apps/worker/src/ai/tools.ts | 59 +++++++++++++++++-- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/apps/worker/prompts/shared/_code-path-rules.txt b/apps/worker/prompts/shared/_code-path-rules.txt index eb95b47..4fa83d4 100644 --- a/apps/worker/prompts/shared/_code-path-rules.txt +++ b/apps/worker/prompts/shared/_code-path-rules.txt @@ -2,8 +2,8 @@ Source-code routing. Each rule is tagged `[FILE]` (literal path) or `[GLOB]` (pattern). All paths are repository-relative. How to apply (focus rules): -- For `[FILE]` entries — delegate analysis to the Task tool. -- For `[GLOB]` entries — invoke the Glob tool to enumerate matches, then delegate analysis of every match to the Task tool. +- For `[FILE]` entries — delegate analysis to the `task` tool. +- For `[GLOB]` entries — use the `glob` tool to enumerate matches, then delegate analysis of every match to the `task` tool. Avoid — out of scope. Skip entirely; the tool layer will block any access attempts. {{CODE_RULES_AVOID}} diff --git a/apps/worker/src/ai/pi-executor.ts b/apps/worker/src/ai/pi-executor.ts index b8ea197..c9ce658 100644 --- a/apps/worker/src/ai/pi-executor.ts +++ b/apps/worker/src/ai/pi-executor.ts @@ -40,7 +40,7 @@ import { } from './output-formatters.js'; import { createProgressManager } from './progress-manager.js'; import { permissionConfigPath } from './settings-writer.js'; -import { createTaskTool, createTodoWriteTool } from './tools.js'; +import { createGlobTool, createTaskTool, createTodoWriteTool } from './tools.js'; declare global { var SHANNON_DISABLE_LOADER: boolean | undefined; @@ -249,6 +249,7 @@ export async function runPiPrompt( ...(resourceLoader && { resourceLoader }), }), createTodoWriteTool(auditLogger), + createGlobTool(sourceDir), ...(callerTools ?? []), ]; // pi's `tools` allowlist gates custom tools too — list every custom name. diff --git a/apps/worker/src/ai/tools.ts b/apps/worker/src/ai/tools.ts index 7096457..585ffd7 100644 --- a/apps/worker/src/ai/tools.ts +++ b/apps/worker/src/ai/tools.ts @@ -5,12 +5,12 @@ // as published by the Free Software Foundation. /** - * Universal custom tools registered for every agent: `task` and `todo_write`. + * Universal custom tools registered for every agent: `task`, `todo_write`, and `glob`. * - * These replace the previous harness built-ins that pi does not ship. `task` - * delegates a focused sub-task to an in-process child session (the Task sub-agent - * replacement); `todo_write` is a full-state-replace planning scratchpad mirrored - * to the workflow log. + * These replace harness built-ins that pi does not ship. `task` delegates a focused + * sub-task to an in-process child session (the Task sub-agent replacement); + * `todo_write` is a full-state-replace planning scratchpad mirrored to the workflow + * log; `glob` is fast-glob file matching (pi has no `Glob` built-in). */ import type { ThinkingLevel } from '@earendil-works/pi-agent-core'; @@ -25,6 +25,7 @@ import { type ToolDefinition, } from '@earendil-works/pi-coding-agent'; import { Type } from 'typebox'; +import { fs, glob, path } from 'zx'; import type { AuditLogger } from './audit-logger.js'; /** Tool surface for child sessions: read/search plus `write`+`bash` to author and run scripts. */ @@ -154,3 +155,51 @@ export function createTodoWriteTool(auditLogger: AuditLogger): ToolDefinition { }, }); } + +/** + * The `glob` tool — fast file pattern matching (pi ships no `Glob` built-in). + * + * Backed by the same fast-glob engine that classifies code_path rules as `[GLOB]` + * (see utils/glob.ts `isGlobPattern`), so it enumerates exactly the patterns the + * routing tags as globs — including `**` and `{a,b}`, which pi's `find` would not + * match the same way. Returns absolute paths, most-recently-modified first. + */ +export function createGlobTool(cwd: string): ToolDefinition { + return defineTool({ + name: 'glob', + label: 'Glob', + description: + 'Fast file pattern matching. Supports glob patterns like "**/*.ts" or "src/**/*.{js,ts}". Returns ' + + 'matching file paths sorted by modification time (most recent first), one per line, or "No files found".', + promptSnippet: 'glob: find files by name pattern', + parameters: Type.Object({ + pattern: Type.String({ description: 'The glob pattern to match files against.' }), + path: Type.Optional(Type.String({ description: 'Directory to search in. Omit to search the repository root.' })), + }), + execute: async (_toolCallId, params) => { + const searchRoot = params.path ? path.resolve(cwd, params.path) : cwd; + const matches = await glob.globby(params.pattern, { + cwd: searchRoot, + absolute: true, + dot: true, + onlyFiles: true, + followSymbolicLinks: false, + }); + if (matches.length === 0) { + return { content: [{ type: 'text' as const, text: 'No files found' }], details: {} }; + } + // Sort by mtime (most recent first) to match the canonical Glob contract. + const withMtime = await Promise.all( + matches.map(async (file) => { + try { + return { file, mtime: (await fs.stat(file)).mtimeMs }; + } catch { + return { file, mtime: 0 }; + } + }), + ); + withMtime.sort((a, b) => b.mtime - a.mtime); + return { content: [{ type: 'text' as const, text: withMtime.map((m) => m.file).join('\n') }], details: {} }; + }, + }); +}