@oh-my-pi/pi-coding-agent 16.2.0 → 16.2.1

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.
@@ -1,5 +1,16 @@
1
1
  import type { ActiveRepoContext } from "../utils/active-repo-context";
2
2
  export declare function formatActiveRepoWatchdogPrompt(activeRepoContext: ActiveRepoContext): string;
3
+ /**
4
+ * Render the project context files (AGENTS.md and the like) into a block for the
5
+ * advisor's system prompt, mirroring how the primary agent receives them. Gives
6
+ * the read-only reviewer the user's standing project instructions so it can hold
7
+ * the driving agent to them instead of advising against project conventions it
8
+ * cannot otherwise see. Returns undefined when there are no context files.
9
+ */
10
+ export declare function formatAdvisorContextPrompt(contextFiles: ReadonlyArray<{
11
+ path: string;
12
+ content: string;
13
+ }>): string | undefined;
3
14
  /**
4
15
  * Discover and load WATCHDOG.md files walking up from cwd, project .omp folder, and user agent dir.
5
16
  * Returns formatted watchdog file blocks ready to be appended to the advisor system prompt.
@@ -8,6 +8,7 @@ interface AcpEventMapperOptions {
8
8
  getMessageId?: (message: unknown) => string | undefined;
9
9
  getMessageProgress?: (message: unknown) => MessageProgress | undefined;
10
10
  getToolArgs?: (toolCallId: string) => unknown;
11
+ resolveImageData?: (data: string, mimeType: string | undefined) => string;
11
12
  /**
12
13
  * Session cwd. Tool call locations sent to ACP clients must be absolute
13
14
  * (the editor host needs them to open or focus files). When provided,
@@ -239,6 +239,12 @@ export interface AgentSessionConfig {
239
239
  advisorReadOnlyTools?: AgentTool[];
240
240
  /** Preloaded watchdog prompt content for the advisor. */
241
241
  advisorWatchdogPrompt?: string;
242
+ /**
243
+ * Preloaded project context files (AGENTS.md, etc.) rendered as a system-prompt
244
+ * block for the advisor — the same standing instructions the primary agent
245
+ * receives, so the reviewer holds the agent to them.
246
+ */
247
+ advisorContextPrompt?: string;
242
248
  /**
243
249
  * Strip tool descriptions from provider-bound tool specs on side requests
244
250
  * (handoff). Must match the session-start value used to build the system
@@ -27,6 +27,8 @@ export declare class BlobStore {
27
27
  putSync(data: Buffer, options?: BlobPutOptions): BlobPutResult;
28
28
  /** Read blob by hash, returns Buffer or null if not found. */
29
29
  get(hash: string): Promise<Buffer | null>;
30
+ /** Synchronous variant of {@link get}. */
31
+ getSync(hash: string): Buffer | null;
30
32
  /** Check if a blob exists. */
31
33
  has(hash: string): Promise<boolean>;
32
34
  }
@@ -62,3 +64,5 @@ export declare function resolveImageDataUrl(blobStore: BlobStore, data: string):
62
64
  * If the blob is missing, logs a warning and returns a placeholder.
63
65
  */
64
66
  export declare function resolveImageData(blobStore: BlobStore, data: string): Promise<string>;
67
+ /** Synchronous variant of {@link resolveImageData}. */
68
+ export declare function resolveImageDataSync(blobStore: BlobStore, data: string): string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "16.2.0",
4
+ "version": "16.2.1",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -55,17 +55,17 @@
55
55
  "@agentclientprotocol/sdk": "0.25.0",
56
56
  "@babel/parser": "^7.29.7",
57
57
  "@mozilla/readability": "^0.6.0",
58
- "@oh-my-pi/hashline": "16.2.0",
59
- "@oh-my-pi/omp-stats": "16.2.0",
60
- "@oh-my-pi/pi-agent-core": "16.2.0",
61
- "@oh-my-pi/pi-ai": "16.2.0",
62
- "@oh-my-pi/pi-catalog": "16.2.0",
63
- "@oh-my-pi/pi-mnemopi": "16.2.0",
64
- "@oh-my-pi/pi-natives": "16.2.0",
65
- "@oh-my-pi/pi-tui": "16.2.0",
66
- "@oh-my-pi/pi-utils": "16.2.0",
67
- "@oh-my-pi/pi-wire": "16.2.0",
68
- "@oh-my-pi/snapcompact": "16.2.0",
58
+ "@oh-my-pi/hashline": "16.2.1",
59
+ "@oh-my-pi/omp-stats": "16.2.1",
60
+ "@oh-my-pi/pi-agent-core": "16.2.1",
61
+ "@oh-my-pi/pi-ai": "16.2.1",
62
+ "@oh-my-pi/pi-catalog": "16.2.1",
63
+ "@oh-my-pi/pi-mnemopi": "16.2.1",
64
+ "@oh-my-pi/pi-natives": "16.2.1",
65
+ "@oh-my-pi/pi-tui": "16.2.1",
66
+ "@oh-my-pi/pi-utils": "16.2.1",
67
+ "@oh-my-pi/pi-wire": "16.2.1",
68
+ "@oh-my-pi/snapcompact": "16.2.1",
69
69
  "@opentelemetry/api": "^1.9.1",
70
70
  "@opentelemetry/context-async-hooks": "^2.7.1",
71
71
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -16,6 +16,7 @@ import {
16
16
  type AdvisorRuntimeHost,
17
17
  deriveAdvisorTelemetry,
18
18
  formatAdvisorBatchContent,
19
+ formatAdvisorContextPrompt,
19
20
  isAdvisorInterruptImmuneTurnActive,
20
21
  isInterruptingSeverity,
21
22
  resolveAdvisorDeliveryChannel,
@@ -57,6 +58,26 @@ describe("advisor", () => {
57
58
  });
58
59
  });
59
60
 
61
+ describe("formatAdvisorContextPrompt", () => {
62
+ it("renders project context files into a block with path and verbatim content", () => {
63
+ const rendered = formatAdvisorContextPrompt([
64
+ {
65
+ path: "/repo/AGENTS.md",
66
+ content: "Use `bun check`, never `tsc`.\nNo `any` unless absolutely necessary.",
67
+ },
68
+ ]);
69
+ expect(rendered).toBeDefined();
70
+ expect(rendered).toContain('<file path="/repo/AGENTS.md">');
71
+ // Content is injected verbatim (noEscape) so backticks/markup survive for the model.
72
+ expect(rendered).toContain("Use `bun check`, never `tsc`.");
73
+ expect(rendered).toContain("No `any` unless absolutely necessary.");
74
+ });
75
+
76
+ it("returns undefined when there are no context files", () => {
77
+ expect(formatAdvisorContextPrompt([])).toBeUndefined();
78
+ });
79
+ });
80
+
60
81
  describe("formatSessionHistoryMarkdown includeThinking", () => {
61
82
  it("includes thinking text when includeThinking is true", () => {
62
83
  const thinking = "I should check the edge case first.";
@@ -3,6 +3,7 @@ import * as path from "node:path";
3
3
  import { getAgentDir, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
4
4
  import { expandAtImports } from "../discovery/at-imports";
5
5
  import activeRepoWatchdogTemplate from "../prompts/advisor/active-repo-watchdog.md" with { type: "text" };
6
+ import contextFilesTemplate from "../prompts/advisor/context-files.md" with { type: "text" };
6
7
  import type { ActiveRepoContext } from "../utils/active-repo-context";
7
8
  import { repo } from "../utils/git";
8
9
  import { normalizePromptPath } from "../utils/prompt-path";
@@ -15,6 +16,20 @@ export function formatActiveRepoWatchdogPrompt(activeRepoContext: ActiveRepoCont
15
16
  .trim();
16
17
  }
17
18
 
19
+ /**
20
+ * Render the project context files (AGENTS.md and the like) into a block for the
21
+ * advisor's system prompt, mirroring how the primary agent receives them. Gives
22
+ * the read-only reviewer the user's standing project instructions so it can hold
23
+ * the driving agent to them instead of advising against project conventions it
24
+ * cannot otherwise see. Returns undefined when there are no context files.
25
+ */
26
+ export function formatAdvisorContextPrompt(
27
+ contextFiles: ReadonlyArray<{ path: string; content: string }>,
28
+ ): string | undefined {
29
+ if (contextFiles.length === 0) return undefined;
30
+ return prompt.render(contextFilesTemplate, { contextFiles }).trim() || undefined;
31
+ }
32
+
18
33
  /**
19
34
  * Discover and load WATCHDOG.md files walking up from cwd, project .omp folder, and user agent dir.
20
35
  * Returns formatted watchdog file blocks ready to be appended to the advisor system prompt.
@@ -17,6 +17,7 @@ import type { LoadContext, LoadResult } from "../capability/types";
17
17
  import {
18
18
  type ClaudePluginRoot,
19
19
  createSourceMeta,
20
+ expandEnvVarsDeep,
20
21
  listClaudePluginRoots,
21
22
  loadFilesFromDir,
22
23
  scanSkillsFromDir,
@@ -327,8 +328,8 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
327
328
  ...(raw.args !== undefined && { args: substitutePluginRoot(raw.args, root.path) }),
328
329
  ...(raw.env !== undefined && { env: substitutePluginRoot(raw.env, root.path) }),
329
330
  ...(raw.cwd !== undefined && { cwd: substitutePluginRoot(raw.cwd, root.path) }),
330
- ...(raw.url !== undefined && { url: raw.url }),
331
- ...(raw.headers !== undefined && { headers: raw.headers }),
331
+ ...(raw.url !== undefined && { url: expandEnvVarsDeep(raw.url) }),
332
+ ...(raw.headers !== undefined && { headers: expandEnvVarsDeep(raw.headers) }),
332
333
  ...(raw.auth !== undefined && { auth: raw.auth }),
333
334
  ...(raw.oauth !== undefined && { oauth: raw.oauth }),
334
335
  ...(raw.type !== undefined && { transport: raw.type as MCPServer["transport"] }),
@@ -42,7 +42,7 @@ import {
42
42
  } from "@agentclientprotocol/sdk";
43
43
  import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
44
44
  import type { AssistantMessage, Model } from "@oh-my-pi/pi-ai";
45
- import { isEnoent, logger, VERSION } from "@oh-my-pi/pi-utils";
45
+ import { getBlobsDir, isEnoent, logger, VERSION } from "@oh-my-pi/pi-utils";
46
46
  import { disableProvider, enableProvider, reset as resetCapabilities } from "../../capability";
47
47
  import { Settings } from "../../config/settings";
48
48
  import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
@@ -62,6 +62,7 @@ import { loadAllExtensions } from "../../modes/components/extensions/state-manag
62
62
  import { theme } from "../../modes/theme/theme";
63
63
  import { type PlanApprovalDetails, resolveApprovedPlan } from "../../plan-mode/approved-plan";
64
64
  import type { AgentSession, AgentSessionEvent } from "../../session/agent-session";
65
+ import { BlobStore, resolveImageDataSync } from "../../session/blob-store";
65
66
  import { isSilentAbort, SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../../session/messages";
66
67
  import type { UsageStatistics } from "../../session/session-entries";
67
68
  import type { SessionInfo as StoredSessionInfo } from "../../session/session-listing";
@@ -445,6 +446,7 @@ export class AcpAgent implements Agent {
445
446
  #cleanupRegistered = false;
446
447
  #clientCapabilities: ClientCapabilities | undefined;
447
448
  #cancelCleanupTimeoutMs = ACP_CANCEL_CLEANUP_TIMEOUT_MS;
449
+ #blobs = new BlobStore(getBlobsDir());
448
450
 
449
451
  constructor(connection: AgentSideConnection, createSession: CreateAcpSession, initialSession?: AgentSession) {
450
452
  this.#connection = connection;
@@ -1187,11 +1189,21 @@ export class AcpAgent implements Agent {
1187
1189
  }
1188
1190
 
1189
1191
  this.#prepareLiveAssistantMessage(record, event);
1192
+ const imageDataCache = new Map<string, string>();
1193
+ const resolveImageDataForAcp = (data: string, mimeType: string | undefined): string => {
1194
+ const key = `${mimeType ?? ""}\u0000${data}`;
1195
+ const cached = imageDataCache.get(key);
1196
+ if (cached !== undefined) return cached;
1197
+ const resolved = resolveImageDataSync(this.#blobs, data);
1198
+ imageDataCache.set(key, resolved);
1199
+ return resolved;
1200
+ };
1190
1201
  for (const notification of mapAgentSessionEventToAcpSessionUpdates(event, record.session.sessionId, {
1191
1202
  getMessageId: message => this.#getLiveMessageId(record, message),
1192
1203
  getMessageProgress: message => this.#getLiveMessageProgress(record, message),
1193
1204
  getToolArgs: toolCallId => record.toolArgsById.get(toolCallId),
1194
1205
  cwd: record.session.sessionManager.getCwd(),
1206
+ resolveImageData: resolveImageDataForAcp,
1195
1207
  })) {
1196
1208
  await this.#connection.sessionUpdate(notification);
1197
1209
  }
@@ -2052,6 +2064,7 @@ export class AcpAgent implements Agent {
2052
2064
  const notifications = mapAgentSessionEventToAcpSessionUpdates(endEvent, sessionId, {
2053
2065
  cwd,
2054
2066
  getToolArgs: toolCallId => (toolCallId === message.toolCallId ? options.toolArgs : undefined),
2067
+ resolveImageData: (data, _mimeType) => resolveImageDataSync(this.#blobs, data),
2055
2068
  });
2056
2069
  if (options.includeStart === false) {
2057
2070
  return notifications;
@@ -20,6 +20,7 @@ interface AcpEventMapperOptions {
20
20
  getMessageId?: (message: unknown) => string | undefined;
21
21
  getMessageProgress?: (message: unknown) => MessageProgress | undefined;
22
22
  getToolArgs?: (toolCallId: string) => unknown;
23
+ resolveImageData?: (data: string, mimeType: string | undefined) => string;
23
24
  /**
24
25
  * Session cwd. Tool call locations sent to ACP clients must be absolute
25
26
  * (the editor host needs them to open or focus files). When provided,
@@ -179,7 +180,7 @@ export function mapAgentSessionEventToAcpSessionUpdates(
179
180
  case "tool_execution_update": {
180
181
  const content = mergeToolUpdateContent(
181
182
  buildToolStartContent(event.toolName, event.args),
182
- extractToolCallContent(event.partialResult),
183
+ extractToolCallContent(event.partialResult, options),
183
184
  );
184
185
  const update: SessionUpdate = {
185
186
  sessionUpdate: "tool_call_update",
@@ -197,7 +198,10 @@ export function mapAgentSessionEventToAcpSessionUpdates(
197
198
  return [toSessionNotification(sessionId, update)];
198
199
  }
199
200
  case "tool_execution_end": {
200
- const resultContent = [...extractDiffToolCallContent(event.result), ...extractToolCallContent(event.result)];
201
+ const resultContent = [
202
+ ...extractDiffToolCallContent(event.result),
203
+ ...extractToolCallContent(event.result, options),
204
+ ];
201
205
  const content = mergeToolUpdateContent(
202
206
  buildToolStartContent(event.toolName, getToolExecutionEndArgs(event, options)),
203
207
  resultContent,
@@ -641,13 +645,15 @@ function terminalToolCallContent(terminalId: string): ToolCallContent {
641
645
  return { type: "terminal", terminalId };
642
646
  }
643
647
 
644
- function extractToolCallContent(value: unknown): ToolCallContent[] {
645
- const richContent = extractStructuredToolCallContent(value);
648
+ function extractToolCallContent(value: unknown, options: AcpEventMapperOptions): ToolCallContent[] {
649
+ const richContent = extractStructuredToolCallContent(value, options);
650
+ const detailsImageContent = extractDetailsImageToolCallContent(value, options, richContent);
651
+ const combinedContent = [...richContent, ...detailsImageContent];
646
652
  const terminalId = extractTerminalId(value);
647
653
  const content =
648
- terminalId && !hasTerminalContent(richContent, terminalId)
649
- ? [...richContent, terminalToolCallContent(terminalId)]
650
- : richContent;
654
+ terminalId && !hasTerminalContent(combinedContent, terminalId)
655
+ ? [...combinedContent, terminalToolCallContent(terminalId)]
656
+ : combinedContent;
651
657
  const fallbackText = extractReadableText(value);
652
658
  if (!fallbackText) {
653
659
  return content;
@@ -658,7 +664,7 @@ function extractToolCallContent(value: unknown): ToolCallContent[] {
658
664
  return [...content, textToolCallContent(fallbackText)];
659
665
  }
660
666
 
661
- function extractStructuredToolCallContent(value: unknown): ToolCallContent[] {
667
+ function extractStructuredToolCallContent(value: unknown, options: AcpEventMapperOptions): ToolCallContent[] {
662
668
  const blocks = getContentBlocks(value);
663
669
  if (!blocks) {
664
670
  return [];
@@ -666,7 +672,7 @@ function extractStructuredToolCallContent(value: unknown): ToolCallContent[] {
666
672
 
667
673
  const content: ToolCallContent[] = [];
668
674
  for (const block of blocks) {
669
- const toolCallContent = toToolCallContent(block);
675
+ const toolCallContent = toToolCallContent(block, options);
670
676
  if (toolCallContent) {
671
677
  content.push(toolCallContent);
672
678
  }
@@ -685,7 +691,7 @@ function getContentBlocks(value: unknown): unknown[] | undefined {
685
691
  return Array.isArray(content) ? content : undefined;
686
692
  }
687
693
 
688
- function toToolCallContent(value: unknown): ToolCallContent | undefined {
694
+ function toToolCallContent(value: unknown, options: AcpEventMapperOptions): ToolCallContent | undefined {
689
695
  const type = getContentType(value);
690
696
  if (!type) {
691
697
  return undefined;
@@ -697,21 +703,8 @@ function toToolCallContent(value: unknown): ToolCallContent | undefined {
697
703
  return text ? textToolCallContent(text) : undefined;
698
704
  }
699
705
  case "image":
700
- case "audio": {
701
- const data = extractStringProperty<BinaryLikeContent>(value, "data");
702
- const mimeType = extractStringProperty<BinaryLikeContent>(value, "mimeType");
703
- if (!data || !mimeType) {
704
- return undefined;
705
- }
706
- return {
707
- type: "content",
708
- content: {
709
- type,
710
- data,
711
- mimeType,
712
- },
713
- };
714
- }
706
+ case "audio":
707
+ return binaryToolCallContent(type, value, options);
715
708
  case "resource_link": {
716
709
  const uri = extractStringProperty<ResourceLinkLikeContent>(value, "uri");
717
710
  const name = extractStringProperty<ResourceLinkLikeContent>(value, "name");
@@ -769,6 +762,64 @@ function toToolCallContent(value: unknown): ToolCallContent | undefined {
769
762
  }
770
763
  }
771
764
 
765
+ function binaryToolCallContent(
766
+ type: "image" | "audio",
767
+ value: unknown,
768
+ options: AcpEventMapperOptions,
769
+ ): ToolCallContent | undefined {
770
+ const data = extractStringProperty<BinaryLikeContent>(value, "data");
771
+ const mimeType = extractStringProperty<BinaryLikeContent>(value, "mimeType");
772
+ if (!data || !mimeType) {
773
+ return undefined;
774
+ }
775
+ return {
776
+ type: "content",
777
+ content: {
778
+ type,
779
+ data: type === "image" ? (options.resolveImageData?.(data, mimeType) ?? data) : data,
780
+ mimeType,
781
+ },
782
+ };
783
+ }
784
+
785
+ function extractDetailsImageToolCallContent(
786
+ value: unknown,
787
+ options: AcpEventMapperOptions,
788
+ existing: ToolCallContent[],
789
+ ): ToolCallContent[] {
790
+ const images = extractDetailsImages(value);
791
+ if (!images) {
792
+ return [];
793
+ }
794
+ const seen = new Set(existing.map(imageContentKey).filter((key): key is string => key !== undefined));
795
+ const content: ToolCallContent[] = [];
796
+ for (const image of images) {
797
+ const toolCallContent = binaryToolCallContent("image", image, options);
798
+ const key = imageContentKey(toolCallContent);
799
+ if (!toolCallContent || !key || seen.has(key)) {
800
+ continue;
801
+ }
802
+ seen.add(key);
803
+ content.push(toolCallContent);
804
+ }
805
+ return content;
806
+ }
807
+
808
+ function extractDetailsImages(value: unknown): unknown[] | undefined {
809
+ if (typeof value !== "object" || value === null) return undefined;
810
+ const details = (value as DetailsContainer).details;
811
+ if (typeof details !== "object" || details === null) return undefined;
812
+ const images = (details as { images?: unknown }).images;
813
+ return Array.isArray(images) && images.length > 0 ? images : undefined;
814
+ }
815
+
816
+ function imageContentKey(value: ToolCallContent | undefined): string | undefined {
817
+ if (value?.type !== "content" || value.content.type !== "image") {
818
+ return undefined;
819
+ }
820
+ return `${value.content.mimeType}\u0000${value.content.data}`;
821
+ }
822
+
772
823
  function extractEmbeddedResource(
773
824
  value: unknown,
774
825
  ): { uri: string; text: string; mimeType?: string } | { uri: string; blob: string; mimeType?: string } | undefined {
@@ -846,6 +897,12 @@ function extractReadableText(value: unknown): string | undefined {
846
897
  if (text.length > 0) {
847
898
  return normalizeText(text);
848
899
  }
900
+ if (hasBinaryContentBlock(contentBlocks)) {
901
+ return undefined;
902
+ }
903
+ }
904
+ if (extractDetailsImages(value)) {
905
+ return undefined;
849
906
  }
850
907
  if (isTerminalOnlyDetails(value)) {
851
908
  return undefined;
@@ -895,6 +952,13 @@ function getContentType(value: unknown): string | undefined {
895
952
  return typeof type === "string" ? type : undefined;
896
953
  }
897
954
 
955
+ function hasBinaryContentBlock(blocks: unknown[]): boolean {
956
+ return blocks.some(block => {
957
+ const type = getContentType(block);
958
+ return type === "image" || type === "audio";
959
+ });
960
+ }
961
+
898
962
  function extractStringProperty<T extends object>(value: unknown, key: keyof T): string | undefined {
899
963
  if (typeof value !== "object" || value === null || !(key in value)) {
900
964
  return undefined;
@@ -0,0 +1,8 @@
1
+ <project-context>
2
+ These context files carry the user's standing instructions for this project (AGENTS.md and the like). The driving agent is bound by them. Hold the agent to them and flag drift the moment it starts; never advise against what these files mandate.
3
+ {{#each contextFiles}}
4
+ <file path="{{path}}">
5
+ {{content}}
6
+ </file>
7
+ {{/each}}
8
+ </project-context>
package/src/sdk.ts CHANGED
@@ -25,7 +25,12 @@ import { FALLBACK_DIALECT, preferredDialect } from "@oh-my-pi/pi-catalog/identit
25
25
  import type { Component } from "@oh-my-pi/pi-tui";
26
26
  import { $env, $flag, getAgentDir, getProjectDir, logger, postmortem, prompt, Snowflake } from "@oh-my-pi/pi-utils";
27
27
  import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
28
- import { ADVISOR_READONLY_TOOL_NAMES, discoverWatchdogFiles, formatActiveRepoWatchdogPrompt } from "./advisor";
28
+ import {
29
+ ADVISOR_READONLY_TOOL_NAMES,
30
+ discoverWatchdogFiles,
31
+ formatActiveRepoWatchdogPrompt,
32
+ formatAdvisorContextPrompt,
33
+ } from "./advisor";
29
34
  import { type AsyncJob, AsyncJobManager } from "./async";
30
35
  import { AutoLearnController, buildAutoLearnInstructions } from "./autolearn/controller";
31
36
  import { loadCapability } from "./capability";
@@ -2678,11 +2683,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2678
2683
  advisorWatchdogPrompts.push(formatActiveRepoWatchdogPrompt(activeRepoContext));
2679
2684
  }
2680
2685
  const advisorWatchdogPrompt = advisorWatchdogPrompts.length > 0 ? advisorWatchdogPrompts.join("\n\n") : undefined;
2686
+ // Hand the advisor the same project context files (AGENTS.md, etc.) the
2687
+ // primary agent gets in its system prompt, so the read-only reviewer judges
2688
+ // against the user's standing project rules instead of advising blind.
2689
+ const advisorContextPrompt = formatAdvisorContextPrompt(contextFiles);
2681
2690
  // Owned only when this session created the manager; subagents receive a
2682
2691
  // parent's manager via `options.mcpManager` and MUST NOT disconnect it.
2683
2692
  const ownedMcpManager = options.mcpManager ? undefined : mcpManager;
2684
2693
  session = new AgentSession({
2685
2694
  advisorWatchdogPrompt,
2695
+ advisorContextPrompt,
2686
2696
  agent,
2687
2697
  pruneToolDescriptions: inlineToolDescriptors,
2688
2698
  thinkingLevel: autoThinking ? AUTO_THINKING : effectiveThinkingLevel,
@@ -589,6 +589,12 @@ export interface AgentSessionConfig {
589
589
  advisorReadOnlyTools?: AgentTool[];
590
590
  /** Preloaded watchdog prompt content for the advisor. */
591
591
  advisorWatchdogPrompt?: string;
592
+ /**
593
+ * Preloaded project context files (AGENTS.md, etc.) rendered as a system-prompt
594
+ * block for the advisor — the same standing instructions the primary agent
595
+ * receives, so the reviewer holds the agent to them.
596
+ */
597
+ advisorContextPrompt?: string;
592
598
  /**
593
599
  * Strip tool descriptions from provider-bound tool specs on side requests
594
600
  * (handoff). Must match the session-start value used to build the system
@@ -1220,6 +1226,7 @@ export class AgentSession {
1220
1226
  #advisorAdviseTool?: AdviseTool;
1221
1227
  #advisorReadOnlyTools?: AgentTool[];
1222
1228
  #advisorWatchdogPrompt?: string;
1229
+ #advisorContextPrompt?: string;
1223
1230
  #advisorYieldQueueUnsubscribe?: () => void;
1224
1231
  /** Persists the advisor agent's turns to `<session>/__advisor.jsonl` for stats
1225
1232
  * attribution and Agent Hub observability. Undefined when no advisor is active. */
@@ -1643,6 +1650,7 @@ export class AgentSession {
1643
1650
  this.agent.serviceTierResolver = model => this.#effectiveServiceTier(model);
1644
1651
  this.#advisorReadOnlyTools = config.advisorReadOnlyTools;
1645
1652
  this.#advisorWatchdogPrompt = config.advisorWatchdogPrompt;
1653
+ this.#advisorContextPrompt = config.advisorContextPrompt;
1646
1654
  this.#pruneToolDescriptions = config.pruneToolDescriptions === true;
1647
1655
  this.#validateRetryFallbackChains();
1648
1656
  this.#toolRegistry = config.toolRegistry ?? new Map();
@@ -1960,6 +1968,9 @@ export class AgentSession {
1960
1968
  const appendOnlyContext = new AppendOnlyContextManager();
1961
1969
  const advisorThinkingLevel = advisorSel.thinkingLevel ?? ThinkingLevel.Medium;
1962
1970
  const systemPrompt = [advisorSystemPrompt];
1971
+ if (this.#advisorContextPrompt) {
1972
+ systemPrompt.push(this.#advisorContextPrompt);
1973
+ }
1963
1974
  if (this.#advisorWatchdogPrompt) {
1964
1975
  systemPrompt.push(this.#advisorWatchdogPrompt);
1965
1976
  }
@@ -152,6 +152,17 @@ export class BlobStore {
152
152
  }
153
153
  }
154
154
 
155
+ /** Synchronous variant of {@link get}. */
156
+ getSync(hash: string): Buffer | null {
157
+ const blobPath = path.join(this.dir, hash);
158
+ try {
159
+ return fs.readFileSync(blobPath);
160
+ } catch (err) {
161
+ if (isEnoent(err)) return null;
162
+ throw err;
163
+ }
164
+ }
165
+
155
166
  /** Check if a blob exists. */
156
167
  async has(hash: string): Promise<boolean> {
157
168
  try {
@@ -253,3 +264,16 @@ export async function resolveImageData(blobStore: BlobStore, data: string): Prom
253
264
  }
254
265
  return buffer.toString("base64");
255
266
  }
267
+
268
+ /** Synchronous variant of {@link resolveImageData}. */
269
+ export function resolveImageDataSync(blobStore: BlobStore, data: string): string {
270
+ const hash = parseBlobRef(data);
271
+ if (!hash) return data;
272
+
273
+ const buffer = blobStore.getSync(hash);
274
+ if (!buffer) {
275
+ logger.warn("Blob not found for image reference", { hash });
276
+ return data;
277
+ }
278
+ return buffer.toString("base64");
279
+ }