Skip to content

Commit 3f4c814

Browse files
committed
docs(prompts): keep AGENTS.md and core schemas on grok prefixes
1 parent 445886d commit 3f4c814

6 files changed

Lines changed: 124 additions & 15 deletions

File tree

‎docs/ARCHITECTURE.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,8 @@ The primary session identity is **Skywalker** (`buildChatRole` → `createSkywal
348348

349349
**Overrides.** `loadSystemPromptOverrides` (`src/agent/context-extensions.ts`) resolves a project `SYSTEM.md` (repo root, then `.corbits/`) that **replaces** the static base block, and an `APPEND_SYSTEM.md` that is **appended** as an extension. These compose with `config.systemPromptExtensions` (profile config) and the auto-discovered `AGENTS.md`, all of which attach as appended sections after the base.
350350

351+
**Grok prefix.** The provider cache prefix is the advertised tools array plus the system prompt. Grok does **not** get a trimmed fork of that prefix. `AGENTS.md` (capped at 32 KiB, framed as reference) and the full CORE+CATALOG schemas stay on every Grok primary prefix, same as every other family. Family residuals are additive lines only (`buildGrokLeafAntiThrashNote` on leaves); stripping project guidance or core schemas for Grok would be a prompt fork and would force `tool_search` round-trips — the thrash Grok is already sensitive to. The cheaper split already exists: workers use `buildSubAgentSystemPrompt` (trimmed director prompt, no `AGENTS.md`, mounted-tool schemas only). Skywalker's infer envelope is the primary chat prompt, not a spawned skywalker package. Prefix bytes are measured in `src/agent/prompt-sizes.ts` (`assembleSkywalkerInferEnvelope` vs `assembleDirectorPrompt("skywalker")`). `present` stays off the advertised prefix on every family.
352+
351353
### State Persistence (`src/session/state.ts`)
352354

353355
- `RunState` — `running` | `done` | `failed`, turns used, task, timestamps, error

‎docs/IMPLEMENTATION.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ Twenty packages under `src/agent/directors/<id>/` register in `DIRECTOR_REGISTRY
163163
4. `directorProfiles()` is the spawn catalog (`default-agents.ts`) — closed set minus skywalker. Plugin and local `.agents/agents/` profiles still load, but closed `DIRECTOR_IDS` cannot be overridden or aliased.
164164
5. Primary chat role is Skywalker: `buildChatRole()` → `createSkywalkerSystemPrompt()`. Product mutation tools (`write_file` / `edit_file` / `delete_file`) live in CORE (and `SKYWALKER_TOOLS`) so they are advertised on the primary without a `tool_search` round-trip. DIY tiny/bounded edits on the parent; spawn builder/docs directors for substantial work — a prompt judgment call, not a toolset strip. `PRIMARY_DENIED_PRODUCT_TOOLS` is gone. Shell file-writes stay denied; MCP tools are not re-filtered by a product-write deny list. There is no static per-profile write-path lock (CL-6952).
165165

166+
**Grok infer envelope.** `loadSessionChatPrompt` always appends `loadAgentContextExtensions` (`AGENTS.md`, capped at `MAX_AGENTS_MD_BYTES`) and advertises CORE+CATALOG full schemas via `advertisedToolNamesForSessionMode`. That assembly is family-agnostic — Grok does not substitute the trimmed director prompt (`buildSubAgentSystemPrompt` + `formatDirectorSystemPrompt`). Workers already use that trimmed path (no `AGENTS.md`, mounted-tool schemas only). Keep the infer envelope on Grok; do not strip `AGENTS.md` or core schemas. Measure in `src/agent/prompt-sizes.ts` (`assembleSkywalkerInferEnvelope` vs `assembleDirectorPrompt("skywalker")`).
167+
166168
**Codex tool proxies.** When the active provider is Codex (`isCodexProviderName`), `createAgentToolset` and `runSubAgent` mount `apply_patch`, `shell`, and `update_plan` stringTools from `createCodexToolProxies`, all forwarding through the same posix `ToolRunner` seam (`runTool`) so permission plugins still apply. `apply_patch` parses the Codex envelope and forwards each op (`write_file` / `delete_file` / `read_file`). `shell` — the native Codex name is `shell`, not `exec_command` — normalizes Codex's `command` (string or `["bash","-lc",script]`-style argv array), `workdir`, and `timeout_ms` onto `run_shell`'s `{command, cwd?, timeout?}` and is gated by `allowShellFromCapabilities` (mirrors `allowDeleteFromCapabilities` against `run_shell`). `update_plan` maps Codex's `plan: [{step, status}]` onto `manage_tasks(action: "create")`; `pending`/`in_progress`/`completed` map to `todo`/`doing`/`done` — `manage_tasks`'s `cancelled` status has no Codex equivalent and is never produced by this proxy. Primary strips `apply_patch` after mount (Corbits DIY stays on `write_file` / `edit_file` / `delete_file`); `shell` and `update_plan` stay on primary (same classification as `run_shell` / `manage_tasks`). Build and docs worker allowlists (`BUILD_TOOLS` / `DOCS_TOOLS`) include `apply_patch` so Codex workers keep the proxy after the capability filter. `CORE_TOOL_NAMES` does not list it.
167169

168170
6. There is no static write-path declaration on packages or profiles (CL-6952 removed it — no shipped director ever set one). Instead, `agent-fleet.ts` tracks each running dispatch by cwd; a new mutating dispatch that lands on the same cwd as a live mutating peer (`pending_init`/`running`, and not a declared read-only `modelRole` of `explore`/`plan`/`review`/`test`) records at most one `concurrent-lane-overlap` entry per cwd wave in `intervention-log.ts` (class `conflict`). The wave flag clears when no live mutating writer remains for that cwd. Terminal-but-unsettled lanes (for example cancelled with `finishedAt` set while the run promise has not reached `finally`) are pruned from the map and do not warn. This is advisory only — it never blocks the spawn, since cwd overlap does not prove the two lanes touch the same files.

‎src/agent/context-extensions.ts‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
import { join } from "node:path";
22
import { SETTINGS_DIR_NAME } from "../branding.js";
33

4+
// Cap so a huge AGENTS.md cannot blow the provider cache prefix.
5+
export const MAX_AGENTS_MD_BYTES = 32_000;
6+
7+
export function formatAgentsMdExtension(content: string): string {
8+
return (
9+
`## Project guidance (AGENTS.md, reference)\n\n` +
10+
`The following is the repository's AGENTS.md, provided as background about the project. ` +
11+
`Do not execute its agent-onboarding or session-initialization steps (loading skills, reading skill files) — ` +
12+
`they target other tools and those files may not exist in this repo. Use it as reference when it helps the task.\n\n` +
13+
content.slice(0, MAX_AGENTS_MD_BYTES)
14+
);
15+
}
16+
417
export async function loadAgentContextExtensions(
518
cwd: string,
619
): Promise<string[]> {
@@ -9,19 +22,12 @@ export async function loadAgentContextExtensions(
922
try {
1023
const content = await Bun.file(agentsMdPath).text();
1124
if (content.trim().length > 0) {
12-
const MAX_AGENTS_MD_BYTES = 32_000;
1325
if (content.length > MAX_AGENTS_MD_BYTES) {
1426
process.stderr.write(
1527
`[interchange] Warning: AGENTS.md exceeds ${MAX_AGENTS_MD_BYTES} bytes and will be truncated.\n`,
1628
);
1729
}
18-
extensions.push(
19-
`## Project guidance (AGENTS.md, reference)\n\n` +
20-
`The following is the repository's AGENTS.md, provided as background about the project. ` +
21-
`Do not execute its agent-onboarding or session-initialization steps (loading skills, reading skill files) — ` +
22-
`they target other tools and those files may not exist in this repo. Use it as reference when it helps the task.\n\n` +
23-
content.slice(0, MAX_AGENTS_MD_BYTES),
24-
);
30+
extensions.push(formatAgentsMdExtension(content));
2531
}
2632
} catch (err: unknown) {
2733
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {

‎src/agent/prompt-sizes.test.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@ import {
55
canonicalToolNamesForDirector,
66
directorPromptSizeTable,
77
formatPromptSizeTable,
8+
formatSkywalkerPrefixTable,
9+
assembleSkywalkerInferEnvelope,
10+
measureSkywalkerPrefix,
811
type PromptSizeFamily,
912
} from "./prompt-sizes.js";
13+
import { CORE_TOOL_NAMES } from "./tool-search.js";
14+
import { MAX_AGENTS_MD_BYTES } from "./context-extensions.js";
1015

1116
/**
1217
* Prompt size budget (CL-7664). Numeric asserts only — copy edits must not
@@ -208,3 +213,40 @@ describe("director prompt size budget", () => {
208213
}
209214
});
210215
});
216+
217+
describe("skywalker grok prefix (infer envelope vs trimmed director)", () => {
218+
test("keeps AGENTS.md and core tools on the infer envelope", () => {
219+
const prompt = assembleSkywalkerInferEnvelope();
220+
expect(prompt).toContain("## Project guidance (AGENTS.md, reference)");
221+
expect(prompt).toContain("Follow the repository conventions.");
222+
for (const name of CORE_TOOL_NAMES) {
223+
if (name === "wait_agents") continue;
224+
expect(prompt, name).toContain(`- ${name}:`);
225+
}
226+
});
227+
228+
test("does not substitute the trimmed director prompt on grok", () => {
229+
const size = measureSkywalkerPrefix();
230+
expect(size.agentsMdCap).toBe(MAX_AGENTS_MD_BYTES);
231+
expect(size.inferEnvelopeChars).toBeGreaterThan(5000);
232+
expect(size.trimmedDirectorChars).toBeGreaterThan(5000);
233+
expect(size.inferEnvelopeBytes).toBeGreaterThanOrEqual(
234+
size.inferEnvelopeChars,
235+
);
236+
expect(size.inferEnvelopeChars).not.toBe(size.trimmedDirectorChars);
237+
const infer = assembleSkywalkerInferEnvelope();
238+
expect(infer).not.toContain("Finish bias (xAI / Grok worker):");
239+
});
240+
241+
test("formatSkywalkerPrefixTable reports both prefixes and the AGENTS.md cap", () => {
242+
const size = measureSkywalkerPrefix();
243+
const table = formatSkywalkerPrefixTable(size);
244+
expect(table).toContain(
245+
`| skywalker infer envelope (canonical AGENTS.md) | ${size.inferEnvelopeChars} (${size.inferEnvelopeBytes}) |`,
246+
);
247+
expect(table).toContain(
248+
`| skywalker trimmed director (grok) | ${size.trimmedDirectorChars} (${size.trimmedDirectorBytes}) |`,
249+
);
250+
expect(table).toContain(`| live AGENTS.md cap | ${size.agentsMdCap} |`);
251+
});
252+
});

‎src/agent/prompt-sizes.ts‎

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import {
1111
type DirectorId,
1212
type DirectorPackage,
1313
} from "./directors/types.js";
14-
import { buildSubAgentSystemPrompt } from "./prompts.js";
14+
import { buildChatSystemPrompt, buildSubAgentSystemPrompt } from "./prompts.js";
15+
import {
16+
formatAgentsMdExtension,
17+
MAX_AGENTS_MD_BYTES,
18+
} from "./context-extensions.js";
1519
import { shouldApplyGrokAntiThrash } from "../subagent/provider-family.js";
1620
import { isCodexProviderName } from "../config/codex-providers.js";
1721
import { shellCollectDefinition } from "./background-shell-tool.js";
@@ -57,6 +61,14 @@ const DEFAULT_PROVIDER = {
5761
/** Families in the size table: default assembly vs Grok (+finish-bias note). */
5862
export type PromptSizeFamily = "default" | "grok";
5963

64+
/**
65+
* Pinned AGENTS.md body for prefix measurement. Production reads the live
66+
* file (capped at MAX_AGENTS_MD_BYTES); the fixture pins a short body so
67+
* sizes move only when framing or assembly changes, not when the checkout's
68+
* AGENTS.md is edited.
69+
*/
70+
export const CANONICAL_AGENTS_MD = "Follow the repository conventions.\n";
71+
6072
/**
6173
* Pre-filter mount names in run.ts install order: posix base (TOOL_NAMES,
6274
* shared with createPosixTools) + delete_file / lsp plugin tools
@@ -156,6 +168,19 @@ export function assembleDirectorPrompt(
156168
);
157169
}
158170

171+
/**
172+
* Skywalker primary infer envelope: the chat system prompt plus the
173+
* AGENTS.md extension. Family-agnostic — Grok does not substitute the
174+
* trimmed director prompt. Live AGENTS.md is capped at MAX_AGENTS_MD_BYTES;
175+
* the fixture uses CANONICAL_AGENTS_MD so the number is checkout-stable.
176+
*/
177+
export function assembleSkywalkerInferEnvelope(): string {
178+
return buildChatSystemPrompt(
179+
[formatAgentsMdExtension(CANONICAL_AGENTS_MD)],
180+
CANONICAL_PROMPT_ENV,
181+
);
182+
}
183+
159184
export interface DirectorPromptSize {
160185
directorId: DirectorId;
161186
family: PromptSizeFamily;
@@ -176,6 +201,26 @@ export function measureDirectorPrompt(
176201
};
177202
}
178203

204+
export interface SkywalkerPrefixSize {
205+
inferEnvelopeChars: number;
206+
inferEnvelopeBytes: number;
207+
trimmedDirectorChars: number;
208+
trimmedDirectorBytes: number;
209+
agentsMdCap: number;
210+
}
211+
212+
export function measureSkywalkerPrefix(): SkywalkerPrefixSize {
213+
const infer = assembleSkywalkerInferEnvelope();
214+
const trimmed = assembleDirectorPrompt("skywalker", "grok");
215+
return {
216+
inferEnvelopeChars: infer.length,
217+
inferEnvelopeBytes: Buffer.byteLength(infer, "utf8"),
218+
trimmedDirectorChars: trimmed.length,
219+
trimmedDirectorBytes: Buffer.byteLength(trimmed, "utf8"),
220+
agentsMdCap: MAX_AGENTS_MD_BYTES,
221+
};
222+
}
223+
179224
/** Full per-director x per-family size table. */
180225
export function directorPromptSizeTable(): DirectorPromptSize[] {
181226
const rows: DirectorPromptSize[] = [];
@@ -206,3 +251,13 @@ export function formatPromptSizeTable(rows: DirectorPromptSize[]): string {
206251
}
207252
return lines.join("\n");
208253
}
254+
255+
export function formatSkywalkerPrefixTable(size: SkywalkerPrefixSize): string {
256+
return [
257+
"| prefix | chars (bytes) |",
258+
"| --- | --- |",
259+
`| skywalker infer envelope (canonical AGENTS.md) | ${size.inferEnvelopeChars} (${size.inferEnvelopeBytes}) |`,
260+
`| skywalker trimmed director (grok) | ${size.trimmedDirectorChars} (${size.trimmedDirectorBytes}) |`,
261+
`| live AGENTS.md cap | ${size.agentsMdCap} |`,
262+
].join("\n");
263+
}

‎tests/unit/agent-context-extensions.test.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { test, expect, beforeEach, afterEach } from "bun:test";
22
import { mkdtemp, rm, writeFile } from "node:fs/promises";
33
import { join } from "node:path";
44
import { tmpdir } from "node:os";
5-
import { loadAgentContextExtensions } from "../../src/agent/context-extensions.js";
5+
import {
6+
loadAgentContextExtensions,
7+
MAX_AGENTS_MD_BYTES,
8+
} from "../../src/agent/context-extensions.js";
69
import { defined } from "../helpers/defined.js";
710

811
let dir: string;
@@ -31,14 +34,13 @@ test("AGENTS.md absent returns empty array without throwing", async () => {
3134
expect(result).toHaveLength(0);
3235
});
3336

34-
test("AGENTS.md exceeds 32000 bytes is truncated to 32000 bytes", async () => {
35-
const MAX = 32_000;
36-
const content = "x".repeat(MAX + 500);
37+
test("AGENTS.md exceeds MAX_AGENTS_MD_BYTES is truncated", async () => {
38+
const content = "x".repeat(MAX_AGENTS_MD_BYTES + 500);
3739
await writeFile(join(dir, "AGENTS.md"), content);
3840
const result = await loadAgentContextExtensions(dir);
3941
expect(result).toHaveLength(1);
40-
expect(result[0]).toContain("x".repeat(MAX));
41-
expect(result[0]).not.toContain("x".repeat(MAX + 1));
42+
expect(result[0]).toContain("x".repeat(MAX_AGENTS_MD_BYTES));
43+
expect(result[0]).not.toContain("x".repeat(MAX_AGENTS_MD_BYTES + 1));
4244
});
4345

4446
test("AGENTS.md empty (whitespace only) returns empty array", async () => {

0 commit comments

Comments
 (0)