@oh-my-pi/pi-coding-agent 14.6.6 → 14.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/examples/hooks/handoff.ts +1 -1
  3. package/examples/hooks/qna.ts +1 -1
  4. package/examples/sdk/03-custom-prompt.ts +7 -4
  5. package/examples/sdk/README.md +1 -1
  6. package/package.json +7 -7
  7. package/src/autoresearch/index.ts +48 -44
  8. package/src/cli/read-cli.ts +58 -0
  9. package/src/cli.ts +1 -0
  10. package/src/commands/read.ts +40 -0
  11. package/src/commit/agentic/agent.ts +1 -1
  12. package/src/commit/analysis/conventional.ts +1 -1
  13. package/src/commit/analysis/summary.ts +1 -1
  14. package/src/commit/changelog/generate.ts +1 -1
  15. package/src/commit/map-reduce/map-phase.ts +1 -1
  16. package/src/commit/map-reduce/reduce-phase.ts +1 -1
  17. package/src/config/settings-schema.ts +39 -0
  18. package/src/edit/line-hash.ts +34 -4
  19. package/src/edit/modes/hashline.ts +201 -6
  20. package/src/edit/streaming.ts +4 -1
  21. package/src/export/html/index.ts +1 -1
  22. package/src/extensibility/extensions/runner.ts +3 -3
  23. package/src/extensibility/extensions/types.ts +4 -4
  24. package/src/main.ts +3 -3
  25. package/src/memories/index.ts +1 -1
  26. package/src/modes/components/agent-dashboard.ts +1 -1
  27. package/src/modes/components/read-tool-group.ts +4 -9
  28. package/src/modes/components/tool-execution.ts +4 -0
  29. package/src/modes/controllers/event-controller.ts +2 -0
  30. package/src/modes/rpc/rpc-types.ts +1 -1
  31. package/src/modes/utils/context-usage.ts +12 -5
  32. package/src/modes/utils/ui-helpers.ts +1 -0
  33. package/src/prompts/system/project-prompt.md +36 -0
  34. package/src/prompts/system/system-prompt.md +0 -29
  35. package/src/prompts/tools/github.md +1 -0
  36. package/src/prompts/tools/read.md +15 -14
  37. package/src/sdk.ts +29 -28
  38. package/src/session/agent-session.ts +20 -12
  39. package/src/session/compaction/branch-summarization.ts +1 -1
  40. package/src/session/compaction/compaction.ts +3 -3
  41. package/src/session/session-dump-format.ts +10 -5
  42. package/src/session/streaming-output.ts +1 -1
  43. package/src/system-prompt.ts +35 -3
  44. package/src/task/executor.ts +4 -3
  45. package/src/tools/fetch.ts +4 -4
  46. package/src/tools/gh.ts +187 -0
  47. package/src/tools/inspect-image.ts +1 -1
  48. package/src/tools/output-meta.ts +1 -1
  49. package/src/tools/path-utils.ts +11 -0
  50. package/src/tools/read.ts +388 -204
  51. package/src/tools/search.ts +1 -1
  52. package/src/tools/sqlite-reader.ts +1 -1
  53. package/src/utils/commit-message-generator.ts +1 -1
  54. package/src/utils/title-generator.ts +1 -1
  55. package/src/web/search/providers/anthropic.ts +1 -1
  56. package/src/workspace-tree.ts +396 -0
@@ -6,29 +6,30 @@ The `read` tool is multi-purpose and more capable than it looks — inspects fil
6
6
  - For URLs, `read` fetches the page and returns clean extracted text/markdown by default (reader-mode). It handles HTML pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom, JSON endpoints, PDFs, etc. You **SHOULD** reach for `read` — not a browser/puppeteer tool — for fetching and inspecting web content.
7
7
 
8
8
  ## Parameters
9
- - `path` — file path or URL (required)
10
- - `sel` — optional selector for line ranges or raw mode
9
+ - `path` — file path or URL (required). Append `:<sel>` for line ranges or raw mode (for example `src/foo.ts:50-200` or `src/foo.ts:raw`).
11
10
  - `timeout` — seconds, for URLs only
12
11
 
13
12
  ## Selectors
14
13
 
15
- |`sel` value|Behavior|
14
+ |`path` suffix|Behavior|
16
15
  |---|---|
17
- |_(omitted)_|Read full file (up to {{DEFAULT_LIMIT}} lines)|
18
- |`50`|Read from line 50 onward|
19
- |`50-200`|Read lines 50-200|
20
- |`50+150`|Read 150 lines starting at line 50|
21
- |`20+1`|Read exactly one line|
16
+ |_(omitted)_|For parseable code files, return a structural summary. Otherwise read from the start (up to {{DEFAULT_LIMIT}} lines).|
17
+ |`:50`|Read from line 50 onward|
18
+ |`:50-200`|Read lines 50-200|
19
+ |`:50+150`|Read 150 lines starting at line 50|
20
+ |`:20+1`|Read exactly one line|
21
+ |`:raw`|Read verbatim text without anchors or summarization|
22
22
 
23
23
  # Filesystem
24
24
  - Reading a directory path returns a list of dirents.
25
25
  {{#if IS_HL_MODE}}
26
- - Reading a file returns lines prefixed with anchors (line+hash): `41th|def alpha():`
26
+ - Reading a file with an explicit selector returns lines prefixed with anchors (line+hash): `41th|def alpha():`
27
27
  {{else}}
28
28
  {{#if IS_LINE_NUMBER_MODE}}
29
- - Reading a file returns lines prefixed with line numbers: `41|def alpha():`
29
+ - Reading a file with an explicit selector returns lines prefixed with line numbers: `41|def alpha():`
30
30
  {{/if}}
31
31
  {{/if}}
32
+ - Reading a parseable code file without a selector returns a structural summary with signatures/declarations kept and large bodies collapsed to `…`. Use `:raw` or an explicit range such as `:1-9999` for verbatim content.
32
33
 
33
34
  # Inspection
34
35
 
@@ -36,7 +37,7 @@ Extracts text from PDF, Word, PowerPoint, Excel, RTF, EPUB, and Jupyter notebook
36
37
 
37
38
  # Directories & Archives
38
39
 
39
- Directories and archive roots return a list of entries. Supports `.tar`, `.tar.gz`, `.tgz`, `.zip`. Use `archive.ext:path/inside/archive` to read contents.
40
+ Directories and archive roots return a list of entries. Supports `.tar`, `.tar.gz`, `.tgz`, `.zip`. Use `archive.ext:path/inside/archive` to read contents, and append a selector to the archive entry such as `archive.zip:dir/file.ts:50-60`.
40
41
 
41
42
  # SQLite Databases
42
43
 
@@ -50,13 +51,13 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
50
51
 
51
52
  # URLs
52
53
 
53
- Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom feeds, JSON endpoints, PDFs at URLs, and similar text-based resources. Returns clean reader-mode text/markdown — no browser required. Use `sel="raw"` for untouched HTML; `timeout` to override the default request timeout.
54
+ Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom feeds, JSON endpoints, PDFs at URLs, and similar text-based resources. Returns clean reader-mode text/markdown — no browser required. Use a `:raw` suffix for untouched HTML; `timeout` to override the default request timeout. URL line selectors require the `L` form, for example `https://example.com/page:L50-L60`.
54
55
  </instruction>
55
56
 
56
57
  <critical>
57
58
  - You **MUST** use `read` for every file, directory, archive, and URL read. `cat`, `head`, `tail`, `less`, `more`, `ls`, `tar`, `unzip`, `curl`, and `wget` are **FORBIDDEN** for inspection — any such Bash call is a bug, regardless of how short or convenient it looks.
58
59
  - You **MUST** prefer `read` over a browser/puppeteer tool for fetching URL content; only use a browser if `read` fails to deliver reasonable content.
59
60
  - You **MUST** always include the `path` parameter — never call `read` with an empty argument object `{}`.
60
- - For specific line ranges, use `sel` (e.g. `sel="50-200"`, `sel="50+150"`) — do **NOT** reach for `sed -n`, `awk NR`, or `head`/`tail` pipelines.
61
- - You **MAY** use `sel` with URL reads; the tool paginates cached fetched output.
61
+ - For specific line ranges, append the selector to `path` (e.g. `path="src/foo.ts:50-200"`, `path="src/foo.ts:50+150"`) — do **NOT** reach for `sed -n`, `awk NR`, or `head`/`tail` pipelines.
62
+ - You **MAY** use path suffix selectors with URL reads; the tool paginates cached fetched output.
62
63
  </critical>
package/src/sdk.ts CHANGED
@@ -102,6 +102,7 @@ import { closeAllConnections } from "./ssh/connection-manager";
102
102
  import { unmountAll } from "./ssh/sshfs-mount";
103
103
  import {
104
104
  type AgentsMdSearch,
105
+ type BuildSystemPromptResult,
105
106
  buildAgentsMdSearch,
106
107
  buildSystemPrompt as buildSystemPromptInternal,
107
108
  buildSystemPromptToolMetadata,
@@ -140,6 +141,7 @@ import { wrapToolWithMetaNotice } from "./tools/output-meta";
140
141
  import { queueResolveHandler } from "./tools/resolve";
141
142
  import { EventBus } from "./utils/event-bus";
142
143
  import { buildNamedToolChoice } from "./utils/tool-choice";
144
+ import { buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
143
145
 
144
146
  // Types
145
147
  export interface CreateAgentSessionOptions {
@@ -165,8 +167,8 @@ export interface CreateAgentSessionOptions {
165
167
  /** Models available for cycling (Ctrl+P in interactive mode) */
166
168
  scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
167
169
 
168
- /** System prompt. String replaces default, function receives default and returns final. */
169
- systemPrompt?: string | ((defaultPrompt: string) => string);
170
+ /** System prompt blocks. Array replaces default, function receives default blocks and returns final blocks. */
171
+ systemPrompt?: string[] | ((defaultPrompt: string[]) => string[]);
170
172
  /** Optional provider-facing session identifier for prompt caches and sticky auth selection.
171
173
  * Keeps persisted session files isolated while reusing provider-side caches. */
172
174
  providerSessionId?: string;
@@ -270,6 +272,7 @@ export type { Skill } from "./extensibility/skills";
270
272
  export type { FileSlashCommand } from "./extensibility/slash-commands";
271
273
  export type { MCPManager, MCPServerConfig, MCPServerConnection, MCPToolsLoadResult } from "./mcp";
272
274
  export type { Tool } from "./tools";
275
+ export { buildDirectoryTree, buildWorkspaceTree, type DirectoryTree, type WorkspaceTree } from "./workspace-tree";
273
276
 
274
277
  export {
275
278
  // Individual tool classes (for custom usage)
@@ -399,9 +402,12 @@ export interface BuildSystemPromptOptions {
399
402
  }
400
403
 
401
404
  /**
402
- * Build the default system prompt.
405
+ * Build the default provider-facing system prompt blocks.
406
+ *
407
+ * The returned `systemPrompt` preserves the stable harness prompt and dynamic project context
408
+ * as separate entries so providers can cache prompt prefixes without concatenating blocks.
403
409
  */
404
- export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise<string> {
410
+ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise<BuildSystemPromptResult> {
405
411
  return await buildSystemPromptInternal({
406
412
  cwd: options.cwd,
407
413
  skills: options.skills,
@@ -652,7 +658,7 @@ function buildMCPPromptCommands(manager: MCPManager): LoadedCustomCommand[] {
652
658
  * const { session } = await createAgentSession({
653
659
  * model: myModel,
654
660
  * getApiKey: async () => Bun.env.MY_KEY,
655
- * systemPrompt: 'You are helpful.',
661
+ * systemPrompt: ['You are helpful.'],
656
662
  * tools: codingTools({ cwd: getProjectDir() }),
657
663
  * skills: [],
658
664
  * sessionManager: SessionManager.inMemory(),
@@ -680,6 +686,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
680
686
  // (~200ms on large repos) and only needs `cwd`, so it can overlap with everything that follows.
681
687
  const agentsMdSearchPromise: Promise<AgentsMdSearch> = logger.time("buildAgentsMdSearch", buildAgentsMdSearch, cwd);
682
688
  agentsMdSearchPromise.catch(() => {});
689
+ const workspaceTreePromise: Promise<WorkspaceTree> = logger.time("buildWorkspaceTree", buildWorkspaceTree, cwd);
690
+ workspaceTreePromise.catch(() => {});
683
691
 
684
692
  // Independent discoveries that depend only on cwd/agentDir — kicked off in parallel and awaited
685
693
  // at their respective consumer sites. Their work can overlap with model resolution, secret loading,
@@ -1330,7 +1338,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1330
1338
  const repeatToolDescriptions = settings.get("repeatToolDescriptions");
1331
1339
  const eagerTasks = settings.get("task.eager");
1332
1340
  const intentField = settings.get("tools.intentTracing") || $flag("PI_INTENT_TRACING") ? INTENT_FIELD : undefined;
1333
- const rebuildSystemPrompt = async (toolNames: string[], tools: Map<string, AgentTool>): Promise<string> => {
1341
+ const rebuildSystemPrompt = async (
1342
+ toolNames: string[],
1343
+ tools: Map<string, AgentTool>,
1344
+ ): Promise<BuildSystemPromptResult> => {
1334
1345
  toolContextStore.setToolNames(toolNames);
1335
1346
  const discoverableMCPTools = mcpDiscoveryEnabled ? collectDiscoverableMCPTools(tools.values()) : [];
1336
1347
  const discoverableMCPSummary = summarizeDiscoverableMCPTools(discoverableMCPTools);
@@ -1380,33 +1391,18 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1380
1391
  eagerTasks,
1381
1392
  secretsEnabled,
1382
1393
  agentsMdSearch: agentsMdSearchPromise,
1394
+ workspaceTree: workspaceTreePromise,
1383
1395
  });
1384
1396
 
1385
1397
  if (options.systemPrompt === undefined) {
1386
1398
  return defaultPrompt;
1387
1399
  }
1388
- if (typeof options.systemPrompt === "string") {
1389
- return await buildSystemPromptInternal({
1390
- cwd,
1391
- skills,
1392
- contextFiles,
1393
- tools: promptTools,
1394
- toolNames,
1395
- rules: rulebookRules,
1396
- alwaysApplyRules,
1397
- skillsSettings: settings.getGroup("skills"),
1398
- customPrompt: options.systemPrompt,
1399
- appendSystemPrompt: appendPrompt,
1400
- repeatToolDescriptions,
1401
- intentField,
1402
- mcpDiscoveryMode: hasDiscoverableMCPTools,
1403
- mcpDiscoveryServerSummaries: discoverableMCPSummary.servers.map(formatDiscoverableMCPToolServerSummary),
1404
- eagerTasks,
1405
- secretsEnabled,
1406
- agentsMdSearch: agentsMdSearchPromise,
1407
- });
1400
+ if (Array.isArray(options.systemPrompt)) {
1401
+ return { systemPrompt: options.systemPrompt };
1408
1402
  }
1409
- return options.systemPrompt(defaultPrompt);
1403
+ return {
1404
+ systemPrompt: options.systemPrompt(defaultPrompt.systemPrompt),
1405
+ };
1410
1406
  };
1411
1407
 
1412
1408
  const toolNamesFromRegistry = Array.from(toolRegistry.keys());
@@ -1472,7 +1468,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1472
1468
  }
1473
1469
  }
1474
1470
 
1475
- const systemPrompt = await logger.time("buildSystemPrompt", rebuildSystemPrompt, initialToolNames, toolRegistry);
1471
+ const { systemPrompt } = await logger.time(
1472
+ "buildSystemPrompt",
1473
+ rebuildSystemPrompt,
1474
+ initialToolNames,
1475
+ toolRegistry,
1476
+ );
1476
1477
 
1477
1478
  const promptTemplates = await promptTemplatesPromise;
1478
1479
  toolSession.promptTemplates = promptTemplates;
@@ -245,8 +245,8 @@ export interface AgentSessionConfig {
245
245
  onResponse?: SimpleStreamOptions["onResponse"];
246
246
  /** Current session message-to-LLM conversion pipeline */
247
247
  convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
248
- /** System prompt builder that can consider tool availability */
249
- rebuildSystemPrompt?: (toolNames: string[], tools: Map<string, AgentTool>) => Promise<string>;
248
+ /** System prompt builder that can consider tool availability. Returns ordered provider-facing blocks. */
249
+ rebuildSystemPrompt?: (toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>;
250
250
  /**
251
251
  * Optional accessor for live MCP server instructions. Read by the session's
252
252
  * `rebuildSystemPrompt`-skip optimization to detect server-side instruction
@@ -520,9 +520,11 @@ export class AgentSession {
520
520
  #onPayload: SimpleStreamOptions["onPayload"] | undefined;
521
521
  #onResponse: SimpleStreamOptions["onResponse"] | undefined;
522
522
  #convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
523
- #rebuildSystemPrompt: ((toolNames: string[], tools: Map<string, AgentTool>) => Promise<string>) | undefined;
523
+ #rebuildSystemPrompt:
524
+ | ((toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>)
525
+ | undefined;
524
526
  #getMcpServerInstructions: (() => Map<string, string> | undefined) | undefined;
525
- #baseSystemPrompt: string;
527
+ #baseSystemPrompt: string[];
526
528
  /**
527
529
  * Signature of the (toolNames, tool descriptions) tuple passed to the most
528
530
  * recent successful `rebuildSystemPrompt` call. Used to skip redundant rebuilds
@@ -2083,8 +2085,8 @@ export class AgentSession {
2083
2085
  getLastAssistantMessage(): AssistantMessage | undefined {
2084
2086
  return this.#findLastAssistantMessage();
2085
2087
  }
2086
- /** Current effective system prompt (includes any per-turn extension modifications) */
2087
- get systemPrompt(): string {
2088
+ /** Current effective system prompt blocks (includes any per-turn extension modifications) */
2089
+ get systemPrompt(): string[] {
2088
2090
  return this.agent.state.systemPrompt;
2089
2091
  }
2090
2092
 
@@ -2281,7 +2283,8 @@ export class AgentSession {
2281
2283
  if (this.#rebuildSystemPrompt) {
2282
2284
  const signature = this.#computeAppliedToolSignature(validToolNames, tools);
2283
2285
  if (signature !== this.#lastAppliedToolSignature) {
2284
- this.#baseSystemPrompt = await this.#rebuildSystemPrompt(validToolNames, this.#toolRegistry);
2286
+ const built = await this.#rebuildSystemPrompt(validToolNames, this.#toolRegistry);
2287
+ this.#baseSystemPrompt = built.systemPrompt;
2285
2288
  this.agent.setSystemPrompt(this.#baseSystemPrompt);
2286
2289
  this.#lastAppliedToolSignature = signature;
2287
2290
  }
@@ -2324,7 +2327,8 @@ export class AgentSession {
2324
2327
  async refreshBaseSystemPrompt(): Promise<void> {
2325
2328
  if (!this.#rebuildSystemPrompt) return;
2326
2329
  const activeToolNames = this.getActiveToolNames();
2327
- this.#baseSystemPrompt = await this.#rebuildSystemPrompt(activeToolNames, this.#toolRegistry);
2330
+ const built = await this.#rebuildSystemPrompt(activeToolNames, this.#toolRegistry);
2331
+ this.#baseSystemPrompt = built.systemPrompt;
2328
2332
  this.agent.setSystemPrompt(this.#baseSystemPrompt);
2329
2333
  // Refresh the cached signature so a subsequent `#applyActiveToolsByName` with
2330
2334
  // the same tool set does not re-rebuild on top of the explicit refresh we
@@ -2335,14 +2339,14 @@ export class AgentSession {
2335
2339
  this.#lastAppliedToolSignature = this.#computeAppliedToolSignature(activeToolNames, activeTools);
2336
2340
  }
2337
2341
 
2338
- async #buildSystemPromptForAgentStart(promptText: string): Promise<string> {
2342
+ async #buildSystemPromptForAgentStart(promptText: string): Promise<string[]> {
2339
2343
  const backend = resolveMemoryBackend(this.settings);
2340
2344
  if (!backend.beforeAgentStartPrompt) return this.#baseSystemPrompt;
2341
2345
 
2342
2346
  try {
2343
2347
  const injected = await backend.beforeAgentStartPrompt(this, promptText);
2344
2348
  if (!injected) return this.#baseSystemPrompt;
2345
- return `${this.#baseSystemPrompt}\n\n${injected}`;
2349
+ return [...this.#baseSystemPrompt, injected];
2346
2350
  } catch (err) {
2347
2351
  logger.debug("Memory backend beforeAgentStartPrompt failed", {
2348
2352
  backend: backend.id,
@@ -4215,7 +4219,11 @@ export class AgentSession {
4215
4219
  apiKey,
4216
4220
  customInstructions,
4217
4221
  compactionAbortController.signal,
4218
- { promptOverride: hookPrompt, extraContext: hookContext, remoteInstructions: this.#baseSystemPrompt },
4222
+ {
4223
+ promptOverride: hookPrompt,
4224
+ extraContext: hookContext,
4225
+ remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
4226
+ },
4219
4227
  );
4220
4228
  summary = result.summary;
4221
4229
  shortSummary = result.shortSummary;
@@ -5328,7 +5336,7 @@ export class AgentSession {
5328
5336
  compactResult = await compact(preparation, candidate, apiKey, undefined, autoCompactionSignal, {
5329
5337
  promptOverride: hookPrompt,
5330
5338
  extraContext: hookContext,
5331
- remoteInstructions: this.#baseSystemPrompt,
5339
+ remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
5332
5340
  initiatorOverride: "agent",
5333
5341
  });
5334
5342
  break;
@@ -290,7 +290,7 @@ export async function generateBranchSummary(
290
290
  // Call LLM for summarization
291
291
  const response = await completeSimple(
292
292
  model,
293
- { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
293
+ { systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT], messages: summarizationMessages },
294
294
  { apiKey, signal, maxTokens: 2048 },
295
295
  );
296
296
 
@@ -1019,7 +1019,7 @@ export async function generateSummary(
1019
1019
 
1020
1020
  const response = await completeSimple(
1021
1021
  model,
1022
- { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
1022
+ { systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT], messages: summarizationMessages },
1023
1023
  { maxTokens, signal, apiKey, reasoning: Effort.High, initiatorOverride: options?.initiatorOverride },
1024
1024
  );
1025
1025
 
@@ -1066,7 +1066,7 @@ async function generateShortSummary(
1066
1066
  const response = await completeSimple(
1067
1067
  model,
1068
1068
  {
1069
- systemPrompt: SUMMARIZATION_SYSTEM_PROMPT,
1069
+ systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT],
1070
1070
  messages: [{ role: "user", content: [{ type: "text", text: promptText }], timestamp: Date.now() }],
1071
1071
  },
1072
1072
  { maxTokens, signal, apiKey, reasoning: Effort.High, initiatorOverride: options?.initiatorOverride },
@@ -1386,7 +1386,7 @@ async function generateTurnPrefixSummary(
1386
1386
 
1387
1387
  const response = await completeSimple(
1388
1388
  model,
1389
- { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
1389
+ { systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT], messages: summarizationMessages },
1390
1390
  { maxTokens, signal, apiKey, reasoning: Effort.High, initiatorOverride },
1391
1391
  );
1392
1392
 
@@ -25,7 +25,7 @@ export interface SessionDumpToolInfo {
25
25
 
26
26
  export interface FormatSessionDumpTextOptions {
27
27
  messages: readonly AgentMessage[];
28
- systemPrompt?: string | null;
28
+ systemPrompt?: readonly string[] | null;
29
29
  model?: Model | null;
30
30
  thinkingLevel?: ThinkingLevel | string | null;
31
31
  tools?: readonly SessionDumpToolInfo[];
@@ -64,11 +64,16 @@ function formatArgsAsXml(args: Record<string, unknown>, indent = "\t"): string {
64
64
  export function formatSessionDumpText(options: FormatSessionDumpTextOptions): string {
65
65
  const lines: string[] = [];
66
66
 
67
- const systemPrompt = options.systemPrompt;
68
- if (systemPrompt) {
67
+ const systemPrompt = options.systemPrompt?.filter(prompt => prompt.length > 0) ?? [];
68
+ if (systemPrompt.length > 0) {
69
69
  lines.push("## System Prompt\n");
70
- lines.push(systemPrompt);
71
- lines.push("\n");
70
+ for (let index = 0; index < systemPrompt.length; index++) {
71
+ if (systemPrompt.length > 1) {
72
+ lines.push(`### System Prompt ${index + 1}\n`);
73
+ }
74
+ lines.push(systemPrompt[index]);
75
+ lines.push("\n");
76
+ }
72
77
  }
73
78
 
74
79
  const model = options.model;
@@ -759,7 +759,7 @@ export function formatHeadTruncationNotice(
759
759
  const totalFileLines = options.totalFileLines ?? truncation.totalLines;
760
760
  const endLineDisplay = startLineDisplay + (truncation.outputLines ?? truncation.totalLines) - 1;
761
761
  const nextOffset = endLineDisplay + 1;
762
- const notice = `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use sel=${nextOffset} to continue]`;
762
+ const notice = `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use :${nextOffset} to continue]`;
763
763
  return `\n\n${notice}`;
764
764
  }
765
765
 
@@ -13,7 +13,9 @@ import type { SkillsSettings } from "./config/settings";
13
13
  import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile } from "./discovery";
14
14
  import { loadSkills, type Skill } from "./extensibility/skills";
15
15
  import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
16
+ import projectPromptTemplate from "./prompts/system/project-prompt.md" with { type: "text" };
16
17
  import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
18
+ import { buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
17
19
 
18
20
  interface AlwaysApplyRule {
19
21
  name: string;
@@ -409,12 +411,20 @@ export interface BuildSystemPromptOptions {
409
411
  secretsEnabled?: boolean;
410
412
  /** Pre-loaded AGENTS.md search (skips discovery if provided). May be a Promise to allow early kick-off. */
411
413
  agentsMdSearch?: AgentsMdSearch | Promise<AgentsMdSearch>;
414
+ /** Pre-loaded workspace tree (skips discovery if provided). May be a Promise to allow early kick-off. */
415
+ workspaceTree?: WorkspaceTree | Promise<WorkspaceTree>;
416
+ }
417
+
418
+ /** Result of building provider-facing system prompt messages. */
419
+ export interface BuildSystemPromptResult {
420
+ /** Ordered system prompt blocks. Providers should preserve entries as distinct messages/blocks. */
421
+ systemPrompt: string[];
412
422
  }
413
423
 
414
424
  /** Build the system prompt with tools, guidelines, and context */
415
- export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise<string> {
425
+ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise<BuildSystemPromptResult> {
416
426
  if ($env.NULL_PROMPT === "true") {
417
- return "";
427
+ return { systemPrompt: [] };
418
428
  }
419
429
 
420
430
  const {
@@ -435,6 +445,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
435
445
  eagerTasks = false,
436
446
  secretsEnabled = false,
437
447
  agentsMdSearch: providedAgentsMdSearch,
448
+ workspaceTree: providedWorkspaceTree,
438
449
  } = options;
439
450
  const resolvedCwd = cwd ?? getProjectDir();
440
451
 
@@ -449,6 +460,10 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
449
460
  providedAgentsMdSearch !== undefined
450
461
  ? Promise.resolve(providedAgentsMdSearch)
451
462
  : logger.time("buildAgentsMdSearch", buildAgentsMdSearch, resolvedCwd);
463
+ const workspaceTreePromise =
464
+ providedWorkspaceTree !== undefined
465
+ ? Promise.resolve(providedWorkspaceTree)
466
+ : logger.time("buildWorkspaceTree", buildWorkspaceTree, resolvedCwd);
452
467
  const skillsPromise: Promise<Skill[]> =
453
468
  providedSkills !== undefined
454
469
  ? Promise.resolve(providedSkills)
@@ -463,6 +478,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
463
478
  contextFilesPromise,
464
479
  agentsMdSearchPromise,
465
480
  skillsPromise,
481
+ workspaceTreePromise,
466
482
  ]).then(
467
483
  ([
468
484
  resolvedCustomPrompt,
@@ -471,6 +487,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
471
487
  contextFiles,
472
488
  agentsMdSearch,
473
489
  skills,
490
+ workspaceTree,
474
491
  ]) => ({
475
492
  resolvedCustomPrompt,
476
493
  resolvedAppendPrompt,
@@ -478,6 +495,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
478
495
  contextFiles,
479
496
  agentsMdSearch,
480
497
  skills,
498
+ workspaceTree,
481
499
  }),
482
500
  );
483
501
  })();
@@ -501,6 +519,12 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
501
519
  pattern: `AGENTS.md depth ${AGENTS_MD_MIN_DEPTH}-${AGENTS_MD_MAX_DEPTH}`,
502
520
  files: [],
503
521
  };
522
+ let workspaceTree: WorkspaceTree = {
523
+ rootPath: resolvedCwd,
524
+ rendered: "",
525
+ truncated: false,
526
+ totalLines: 0,
527
+ };
504
528
  let skills: Skill[] = providedSkills ?? [];
505
529
 
506
530
  if (prepResult.type === "timeout") {
@@ -524,6 +548,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
524
548
  contextFiles = dedupeExactContextFiles(prepResult.value.contextFiles);
525
549
  agentsMdSearch = prepResult.value.agentsMdSearch;
526
550
  skills = prepResult.value.skills;
551
+ workspaceTree = prepResult.value.workspaceTree;
527
552
  }
528
553
 
529
554
  const date = new Date().toISOString().slice(0, 10);
@@ -578,6 +603,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
578
603
  environment,
579
604
  contextFiles,
580
605
  agentsMdSearch,
606
+ workspaceTree,
581
607
  skills: filteredSkills,
582
608
  rules: rules ?? [],
583
609
  alwaysApplyRules: injectedAlwaysApplyRules,
@@ -599,5 +625,11 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
599
625
  rendered += `\n\n<critical>\nThe \`${reportToolIssueToolName}\` tool is available for automated QA. If ANY tool you call returns output that is unexpected, incorrect, malformed, or otherwise inconsistent with what you anticipated given the tool's described behavior and your parameters, call \`${reportToolIssueToolName}\` with the tool name and a concise description of the discrepancy. Do not hesitate to report — false positives are acceptable.\n</critical>`;
600
626
  }
601
627
 
602
- return rendered;
628
+ const systemPrompt = [rendered];
629
+ const projectPrompt = resolvedCustomPrompt ? "" : prompt.render(projectPromptTemplate, data).trim();
630
+ if (projectPrompt) {
631
+ systemPrompt.push(projectPrompt);
632
+ }
633
+
634
+ return { systemPrompt };
603
635
  }
@@ -967,9 +967,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
967
967
  contextFiles: options.contextFiles,
968
968
  skills: options.skills,
969
969
  promptTemplates: options.promptTemplates,
970
- systemPrompt: defaultPrompt =>
970
+ systemPrompt: defaultPrompt => [
971
971
  prompt.render(subagentSystemPromptTemplate, {
972
- base: defaultPrompt,
972
+ base: defaultPrompt.join("\n\n"),
973
973
  agent: agent.systemPrompt,
974
974
  worktree: worktree ?? "",
975
975
  outputSchema: normalizedOutputSchema,
@@ -977,6 +977,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
977
977
  ircPeers: ircEnabled ? renderIrcPeerRoster(id) : "",
978
978
  ircSelfId: ircEnabled ? id : "",
979
979
  }),
980
+ ],
980
981
  sessionManager,
981
982
  hasUI: false,
982
983
  spawns: spawnsEnv,
@@ -1016,7 +1017,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1016
1017
  }
1017
1018
 
1018
1019
  session.sessionManager.appendSessionInit({
1019
- systemPrompt: session.agent.state.systemPrompt,
1020
+ systemPrompt: session.agent.state.systemPrompt.join("\n\n"),
1020
1021
  task,
1021
1022
  tools: session.getActiveToolNames(),
1022
1023
  outputSchema,
@@ -148,20 +148,20 @@ export interface ParsedReadUrlTarget {
148
148
  limit?: number;
149
149
  }
150
150
 
151
- export function parseReadUrlTarget(readPath: string, sel?: string): ParsedReadUrlTarget | null {
152
- const embedded = sel ? undefined : tryExtractEmbeddedUrlSelector(readPath);
151
+ export function parseReadUrlTarget(readPath: string): ParsedReadUrlTarget | null {
152
+ const embedded = tryExtractEmbeddedUrlSelector(readPath);
153
153
  const urlPath = embedded?.path ?? readPath;
154
154
  if (!isReadableUrlPath(urlPath)) {
155
155
  return null;
156
156
  }
157
157
 
158
- const selector = sel ?? embedded?.sel;
158
+ const selector = embedded?.sel;
159
159
  const raw = selector === "raw";
160
160
  const lineMatch = selector ? URL_LINE_RANGE_RE.exec(selector) : null;
161
161
  if (lineMatch) {
162
162
  const startLine = Number.parseInt(lineMatch[1]!, 10);
163
163
  if (startLine < 1) {
164
- throw new ToolError("sel=0 is invalid; lines are 1-indexed. Use sel=1.");
164
+ throw new ToolError("URL line selector 0 is invalid; lines are 1-indexed. Use :L1.");
165
165
  }
166
166
  const sep = lineMatch[2];
167
167
  const rhs = lineMatch[3] ? Number.parseInt(lineMatch[3], 10) : undefined;