feat(worker): add glob custom tool and route code_path globs to it

This commit is contained in:
ezl-keygraph
2026-06-15 20:03:26 +05:30
parent 58d0defea7
commit d18e928a6a
3 changed files with 58 additions and 8 deletions
@@ -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}}
+2 -1
View File
@@ -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.
+54 -5
View File
@@ -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: {} };
},
});
}