prompt: inject full ignore-aware file list at boot (gitignore-respected, capped 5000)
walk() at boot -> Session.workspaceFiles -> systemPrompt Environment section + subagent system prompts. read_many_files still on-demand, but the agent now sees the tree without a tool call.
This commit is contained in:
+10
@@ -170,6 +170,14 @@ const promptHistory = await store.loadHistory();
|
||||
|
||||
const installedPlugins = has('--no-plugins') ? { plugins: [], errors: [] } : await registry.loadInstalledPlugins();
|
||||
|
||||
// Full ignore-aware file list injected into the system prompt at boot.
|
||||
// Same walk as `@` completion (5000 cap, .gitignore + .shiroignore, skips .git/node_modules),
|
||||
// so a filtered repo stays small even though it's "full".
|
||||
let workspaceFiles: string[] = [];
|
||||
try {
|
||||
for await (const rel of walk({ limit: 5000 })) workspaceFiles.push(rel);
|
||||
} catch {}
|
||||
|
||||
/** Installed entries, as `kind:name`, so the registry list can mark what is already here. */
|
||||
async function installedNames(): Promise<Set<string>> {
|
||||
const names = new Set<string>();
|
||||
@@ -321,6 +329,7 @@ const session = new Session({
|
||||
skills,
|
||||
plugins,
|
||||
agent: agentVariant,
|
||||
...(workspaceFiles.length > 0 ? { workspaceFiles } : {}),
|
||||
...(cfg.toolSets ? { toolSets: cfg.toolSets } : {}),
|
||||
...(cfg.permission ? { permissions: cfg.permission } : {}),
|
||||
// Headless has no one to answer, so the tool is withheld rather than left to hang.
|
||||
@@ -343,6 +352,7 @@ const session = new Session({
|
||||
model: languageModel ?? unconfiguredModel,
|
||||
subagentModel,
|
||||
subagentModelId: cfg.subagentModel,
|
||||
...(workspaceFiles.length > 0 ? { workspaceFiles } : {}),
|
||||
onUsage: (u) => recordSubagent(u),
|
||||
...(headless ? {} : { report: subagents.emit }),
|
||||
// A worker's writes go through the parent's rules and the parent's
|
||||
|
||||
+3
-1
@@ -20,6 +20,8 @@ export type PromptParts = {
|
||||
canAsk?: boolean;
|
||||
/** MCP server names — listed by name only so their schemas cost nothing until mcp_call. */
|
||||
mcpServers?: readonly string[];
|
||||
/** Ignore-aware workspace file list injected at boot (gitignore-respected, capped). */
|
||||
workspaceFiles?: readonly string[];
|
||||
};
|
||||
|
||||
type ToolDoc = { name: string; line: string };
|
||||
@@ -215,7 +217,7 @@ export function systemPrompt(parts: PromptParts): string {
|
||||
Environment
|
||||
- Workspace root: ${cwd}
|
||||
- Platform: ${process.platform}
|
||||
- Paths are resolved inside the workspace. Anything outside it is refused.
|
||||
- Paths are resolved inside the workspace. Anything outside it is refused.${parts.workspaceFiles && parts.workspaceFiles.length > 0 ? `\n- Workspace files (${parts.workspaceFiles.length}, gitignore-respected, capped 5000):\n${parts.workspaceFiles.join('\n')}` : ''}
|
||||
|
||||
Tools available to you now
|
||||
${renderTools(toolNames)}${mcpServers.length > 0 ? `\n\nMCP servers (${mcpServers.length}): ${mcpServers.join(', ')} — tools are NOT in the prompt. Use mcp_list to see what each exposes, mcp_inspect for a tool\'s schema, then mcp_call to run it. Each mcp_call needs approval like a built-in.` : ''}
|
||||
|
||||
@@ -98,6 +98,8 @@ export type SessionOptions = {
|
||||
learnerModel?: LanguageModel;
|
||||
/** Emit learner notices to the UI. */
|
||||
onNotice?: (text: string) => void;
|
||||
/** Ignore-aware file list injected into the system prompt at boot; gitignore-respected. */
|
||||
workspaceFiles?: readonly string[];
|
||||
/** Disable background auto-learn (tests). */
|
||||
disableAutoLearn?: boolean;
|
||||
};
|
||||
@@ -487,6 +489,7 @@ export class Session {
|
||||
availableTools: this.activeTools(),
|
||||
canAsk: this.opts.ask !== undefined && this.activeTools().includes('ask'),
|
||||
...(this.mcpServerNamesForPrompt() ? { mcpServers: this.mcpServerNamesForPrompt() } : {}),
|
||||
...(this.opts.workspaceFiles && this.opts.workspaceFiles.length > 0 ? { workspaceFiles: this.opts.workspaceFiles } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+9
-1
@@ -103,6 +103,11 @@ Rules:
|
||||
it said, and anything you could not finish. The parent cannot see your transcript.`,
|
||||
};
|
||||
|
||||
function withWorkspaceFiles(base: string, workspaceFiles?: readonly string[]): string {
|
||||
if (!workspaceFiles || workspaceFiles.length === 0) return base;
|
||||
return `${base}\n\nWorkspace files (${workspaceFiles.length}, gitignore-respected, capped 5000):\n${workspaceFiles.join('\n')}`;
|
||||
}
|
||||
|
||||
/** One line of detail for the panel: the argument that identifies the call. */
|
||||
const summarize = (input: unknown): string => {
|
||||
if (input === null || typeof input !== 'object') return String(input);
|
||||
@@ -148,6 +153,7 @@ async function runOne(
|
||||
model: LanguageModel;
|
||||
subagentModel?: LanguageModel;
|
||||
cwd?: string;
|
||||
workspaceFiles?: readonly string[];
|
||||
maxSteps?: number;
|
||||
report?: SubagentReporter;
|
||||
approve?: SubagentApproval;
|
||||
@@ -170,7 +176,7 @@ async function runOne(
|
||||
const model = flavour === 'explore' ? (opts.subagentModel ?? opts.model) : opts.model;
|
||||
const result = streamText({
|
||||
model,
|
||||
system: PROMPTS[flavour](opts.cwd ?? process.cwd()),
|
||||
system: withWorkspaceFiles(PROMPTS[flavour](opts.cwd ?? process.cwd()), opts.workspaceFiles),
|
||||
messages: [{ role: 'user', content: spec.prompt }],
|
||||
tools: TOOLS[flavour],
|
||||
stopWhen: isStepCount(opts.maxSteps ?? 20),
|
||||
@@ -236,6 +242,7 @@ export function createTaskTool(opts: {
|
||||
/** Its id, so the parent can price the subagent's spend separately. */
|
||||
subagentModelId?: string;
|
||||
cwd?: string;
|
||||
workspaceFiles?: readonly string[];
|
||||
maxSteps?: number;
|
||||
report?: SubagentReporter;
|
||||
/** Parent-owned approval for a worker's gated calls. Omit to disable `worker`. */
|
||||
@@ -304,6 +311,7 @@ export function createTaskTool(opts: {
|
||||
model: opts.model,
|
||||
subagentModel: opts.subagentModel,
|
||||
cwd: opts.cwd,
|
||||
workspaceFiles: opts.workspaceFiles,
|
||||
maxSteps: opts.maxSteps,
|
||||
report: opts.report,
|
||||
approve: opts.approve,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import { systemPrompt } from '../src/prompt';
|
||||
|
||||
test('workspaceFiles are injected gitignore-style when provided', () => {
|
||||
const prompt = systemPrompt({ cwd: '/repo', workspaceFiles: ['src/app.ts', 'README.md'] });
|
||||
expect(prompt).toContain('Workspace files (2, gitignore-respected');
|
||||
expect(prompt).toContain('src/app.ts');
|
||||
expect(prompt).toContain('README.md');
|
||||
});
|
||||
|
||||
test('workspaceFiles omitted when empty', () => {
|
||||
const prompt = systemPrompt({ cwd: '/repo', workspaceFiles: [] });
|
||||
expect(prompt).not.toContain('Workspace files');
|
||||
});
|
||||
|
||||
test('workspaceFiles undefined leaves prompt unchanged', () => {
|
||||
const prompt = systemPrompt({ cwd: '/repo' });
|
||||
expect(prompt).not.toContain('Workspace files');
|
||||
});
|
||||
Reference in New Issue
Block a user