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();
|
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. */
|
/** Installed entries, as `kind:name`, so the registry list can mark what is already here. */
|
||||||
async function installedNames(): Promise<Set<string>> {
|
async function installedNames(): Promise<Set<string>> {
|
||||||
const names = new Set<string>();
|
const names = new Set<string>();
|
||||||
@@ -321,6 +329,7 @@ const session = new Session({
|
|||||||
skills,
|
skills,
|
||||||
plugins,
|
plugins,
|
||||||
agent: agentVariant,
|
agent: agentVariant,
|
||||||
|
...(workspaceFiles.length > 0 ? { workspaceFiles } : {}),
|
||||||
...(cfg.toolSets ? { toolSets: cfg.toolSets } : {}),
|
...(cfg.toolSets ? { toolSets: cfg.toolSets } : {}),
|
||||||
...(cfg.permission ? { permissions: cfg.permission } : {}),
|
...(cfg.permission ? { permissions: cfg.permission } : {}),
|
||||||
// Headless has no one to answer, so the tool is withheld rather than left to hang.
|
// 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,
|
model: languageModel ?? unconfiguredModel,
|
||||||
subagentModel,
|
subagentModel,
|
||||||
subagentModelId: cfg.subagentModel,
|
subagentModelId: cfg.subagentModel,
|
||||||
|
...(workspaceFiles.length > 0 ? { workspaceFiles } : {}),
|
||||||
onUsage: (u) => recordSubagent(u),
|
onUsage: (u) => recordSubagent(u),
|
||||||
...(headless ? {} : { report: subagents.emit }),
|
...(headless ? {} : { report: subagents.emit }),
|
||||||
// A worker's writes go through the parent's rules and the parent's
|
// 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;
|
canAsk?: boolean;
|
||||||
/** MCP server names — listed by name only so their schemas cost nothing until mcp_call. */
|
/** MCP server names — listed by name only so their schemas cost nothing until mcp_call. */
|
||||||
mcpServers?: readonly string[];
|
mcpServers?: readonly string[];
|
||||||
|
/** Ignore-aware workspace file list injected at boot (gitignore-respected, capped). */
|
||||||
|
workspaceFiles?: readonly string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type ToolDoc = { name: string; line: string };
|
type ToolDoc = { name: string; line: string };
|
||||||
@@ -215,7 +217,7 @@ export function systemPrompt(parts: PromptParts): string {
|
|||||||
Environment
|
Environment
|
||||||
- Workspace root: ${cwd}
|
- Workspace root: ${cwd}
|
||||||
- Platform: ${process.platform}
|
- 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
|
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.` : ''}
|
${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;
|
learnerModel?: LanguageModel;
|
||||||
/** Emit learner notices to the UI. */
|
/** Emit learner notices to the UI. */
|
||||||
onNotice?: (text: string) => void;
|
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). */
|
/** Disable background auto-learn (tests). */
|
||||||
disableAutoLearn?: boolean;
|
disableAutoLearn?: boolean;
|
||||||
};
|
};
|
||||||
@@ -487,6 +489,7 @@ export class Session {
|
|||||||
availableTools: this.activeTools(),
|
availableTools: this.activeTools(),
|
||||||
canAsk: this.opts.ask !== undefined && this.activeTools().includes('ask'),
|
canAsk: this.opts.ask !== undefined && this.activeTools().includes('ask'),
|
||||||
...(this.mcpServerNamesForPrompt() ? { mcpServers: this.mcpServerNamesForPrompt() } : {}),
|
...(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.`,
|
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. */
|
/** One line of detail for the panel: the argument that identifies the call. */
|
||||||
const summarize = (input: unknown): string => {
|
const summarize = (input: unknown): string => {
|
||||||
if (input === null || typeof input !== 'object') return String(input);
|
if (input === null || typeof input !== 'object') return String(input);
|
||||||
@@ -148,6 +153,7 @@ async function runOne(
|
|||||||
model: LanguageModel;
|
model: LanguageModel;
|
||||||
subagentModel?: LanguageModel;
|
subagentModel?: LanguageModel;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
|
workspaceFiles?: readonly string[];
|
||||||
maxSteps?: number;
|
maxSteps?: number;
|
||||||
report?: SubagentReporter;
|
report?: SubagentReporter;
|
||||||
approve?: SubagentApproval;
|
approve?: SubagentApproval;
|
||||||
@@ -170,7 +176,7 @@ async function runOne(
|
|||||||
const model = flavour === 'explore' ? (opts.subagentModel ?? opts.model) : opts.model;
|
const model = flavour === 'explore' ? (opts.subagentModel ?? opts.model) : opts.model;
|
||||||
const result = streamText({
|
const result = streamText({
|
||||||
model,
|
model,
|
||||||
system: PROMPTS[flavour](opts.cwd ?? process.cwd()),
|
system: withWorkspaceFiles(PROMPTS[flavour](opts.cwd ?? process.cwd()), opts.workspaceFiles),
|
||||||
messages: [{ role: 'user', content: spec.prompt }],
|
messages: [{ role: 'user', content: spec.prompt }],
|
||||||
tools: TOOLS[flavour],
|
tools: TOOLS[flavour],
|
||||||
stopWhen: isStepCount(opts.maxSteps ?? 20),
|
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. */
|
/** Its id, so the parent can price the subagent's spend separately. */
|
||||||
subagentModelId?: string;
|
subagentModelId?: string;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
|
workspaceFiles?: readonly string[];
|
||||||
maxSteps?: number;
|
maxSteps?: number;
|
||||||
report?: SubagentReporter;
|
report?: SubagentReporter;
|
||||||
/** Parent-owned approval for a worker's gated calls. Omit to disable `worker`. */
|
/** Parent-owned approval for a worker's gated calls. Omit to disable `worker`. */
|
||||||
@@ -304,6 +311,7 @@ export function createTaskTool(opts: {
|
|||||||
model: opts.model,
|
model: opts.model,
|
||||||
subagentModel: opts.subagentModel,
|
subagentModel: opts.subagentModel,
|
||||||
cwd: opts.cwd,
|
cwd: opts.cwd,
|
||||||
|
workspaceFiles: opts.workspaceFiles,
|
||||||
maxSteps: opts.maxSteps,
|
maxSteps: opts.maxSteps,
|
||||||
report: opts.report,
|
report: opts.report,
|
||||||
approve: opts.approve,
|
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