diff --git a/apps/server/scripts/migrate-dev-db.test.ts b/apps/server/scripts/migrate-dev-db.test.ts
index 7170cb06c06..45c2461c687 100644
--- a/apps/server/scripts/migrate-dev-db.test.ts
+++ b/apps/server/scripts/migrate-dev-db.test.ts
@@ -208,7 +208,7 @@ it.layer(NodeServices.layer)("migrate-dev-db", (it) => {
);
assert.includeMembers(
migrated.projectColumns.map(({ name }) => name),
- ["default_thread_env_mode", "favicon_path"],
+ ["default_thread_env_mode", "favicon_path", "additional_instructions"],
);
assert.deepStrictEqual(migrated.upstreamHistory, [
{
@@ -223,6 +223,7 @@ it.layer(NodeServices.layer)("migrate-dev-db", (it) => {
assert.deepStrictEqual(migrated.forkHistory, [
{ migration_id: 1, name: "ComposerDrafts" },
{ migration_id: 2, name: "WorkspacePortAllocations" },
+ { migration_id: 3, name: "ProjectAdditionalInstructions" },
]);
assert.deepStrictEqual(migrated.drafts, [{ thread_id: "stopped-thread" }]);
}),
diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts
index 978ed0cf7b2..21c7175b34c 100644
--- a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts
+++ b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts
@@ -497,6 +497,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
defaultModelSelection: event.payload.defaultModelSelection,
defaultThreadEnvMode: null,
faviconPath: event.payload.faviconPath ?? null,
+ additionalInstructions: event.payload.additionalInstructions ?? null,
scripts: event.payload.scripts,
createdAt: event.payload.createdAt,
updatedAt: event.payload.updatedAt,
@@ -526,6 +527,9 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
...(event.payload.faviconPath !== undefined
? { faviconPath: event.payload.faviconPath }
: {}),
+ ...(event.payload.additionalInstructions !== undefined
+ ? { additionalInstructions: event.payload.additionalInstructions }
+ : {}),
...(event.payload.scripts !== undefined ? { scripts: event.payload.scripts } : {}),
updatedAt: event.payload.updatedAt,
});
diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts
index 83ae3cfe049..56bfdf832b8 100644
--- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts
+++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts
@@ -276,6 +276,7 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
model: "gpt-5-codex",
},
faviconPath: null,
+ additionalInstructions: null,
scripts: [
{
id: "script-1",
@@ -396,6 +397,7 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
model: "gpt-5-codex",
},
faviconPath: null,
+ additionalInstructions: null,
scripts: [
{
id: "script-1",
diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
index c6c5ad1d7e8..60e5fd6c7bb 100644
--- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
+++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
@@ -322,6 +322,7 @@ function mapProjectShellRow(
defaultModelSelection: row.defaultModelSelection,
defaultThreadEnvMode: row.defaultThreadEnvMode,
faviconPath: row.faviconPath ?? null,
+ additionalInstructions: row.additionalInstructions,
scripts: row.scripts,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
@@ -399,6 +400,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
default_model_selection_json AS "defaultModelSelection",
default_thread_env_mode AS "defaultThreadEnvMode",
favicon_path AS "faviconPath",
+ additional_instructions AS "additionalInstructions",
scripts_json AS "scripts",
created_at AS "createdAt",
updated_at AS "updatedAt",
@@ -854,6 +856,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
default_model_selection_json AS "defaultModelSelection",
default_thread_env_mode AS "defaultThreadEnvMode",
favicon_path AS "faviconPath",
+ additional_instructions AS "additionalInstructions",
scripts_json AS "scripts",
created_at AS "createdAt",
updated_at AS "updatedAt",
@@ -878,6 +881,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
default_model_selection_json AS "defaultModelSelection",
default_thread_env_mode AS "defaultThreadEnvMode",
favicon_path AS "faviconPath",
+ additional_instructions AS "additionalInstructions",
scripts_json AS "scripts",
created_at AS "createdAt",
updated_at AS "updatedAt",
@@ -1679,6 +1683,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
defaultModelSelection: row.defaultModelSelection,
defaultThreadEnvMode: row.defaultThreadEnvMode,
faviconPath: row.faviconPath ?? null,
+ additionalInstructions: row.additionalInstructions,
scripts: row.scripts,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
@@ -1810,6 +1815,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
defaultModelSelection: row.defaultModelSelection,
defaultThreadEnvMode: row.defaultThreadEnvMode,
faviconPath: row.faviconPath ?? null,
+ additionalInstructions: row.additionalInstructions,
scripts: row.scripts,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
@@ -2303,6 +2309,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
defaultModelSelection: option.value.defaultModelSelection,
defaultThreadEnvMode: option.value.defaultThreadEnvMode,
faviconPath: option.value.faviconPath ?? null,
+ additionalInstructions: option.value.additionalInstructions,
scripts: option.value.scripts,
createdAt: option.value.createdAt,
updatedAt: option.value.updatedAt,
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
index 1e433e94c35..357f2b5e557 100644
--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
@@ -552,6 +552,47 @@ describe("ProviderCommandReactor", () => {
expect(thread?.session?.runtimeMode).toBe("approval-required");
});
+ it("passes project additional instructions when starting a provider session", async () => {
+ const harness = await createHarness();
+ const now = "2026-01-01T00:00:00.000Z";
+
+ await Effect.runPromise(
+ harness.engine.dispatch({
+ type: "project.meta.update",
+ commandId: CommandId.make("cmd-project-instructions"),
+ projectId: asProjectId("project-1"),
+ additionalInstructions: "Prefer focused tests.",
+ }),
+ );
+ await waitFor(async () =>
+ (await harness.readModel()).projects.some(
+ (project) => project.additionalInstructions === "Prefer focused tests.",
+ ),
+ );
+
+ await Effect.runPromise(
+ harness.engine.dispatch({
+ type: "thread.turn.start",
+ commandId: CommandId.make("cmd-turn-start-instructions"),
+ threadId: ThreadId.make("thread-1"),
+ message: {
+ messageId: asMessageId("user-message-instructions"),
+ role: "user",
+ text: "hello reactor",
+ attachments: [],
+ },
+ interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
+ runtimeMode: "approval-required",
+ createdAt: now,
+ }),
+ );
+
+ await waitFor(() => harness.startSession.mock.calls.length === 1);
+ expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({
+ additionalInstructions: "Prefer focused tests.",
+ });
+ });
+
it("adopts a Codex follow-up when the response id differs from the retained active turn", async () => {
const harness = await createHarness();
const threadId = ThreadId.make("thread-1");
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
index 14e7a979c06..40dfbb0b620 100644
--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
@@ -616,6 +616,9 @@ const make = Effect.gen(function* () {
...(effectiveCwd ? { cwd: effectiveCwd } : {}),
...(thread.title ? { title: thread.title } : {}),
modelSelection: desiredModelSelection,
+ ...(project?.additionalInstructions
+ ? { additionalInstructions: project.additionalInstructions }
+ : {}),
...(input?.resumeCursor !== undefined ? { resumeCursor: input.resumeCursor } : {}),
runtimeMode: desiredRuntimeMode,
});
diff --git a/apps/server/src/orchestration/decider.projectThreadEnvMode.test.ts b/apps/server/src/orchestration/decider.projectThreadEnvMode.test.ts
index afee13343ad..62184edcb21 100644
--- a/apps/server/src/orchestration/decider.projectThreadEnvMode.test.ts
+++ b/apps/server/src/orchestration/decider.projectThreadEnvMode.test.ts
@@ -100,4 +100,36 @@ it.layer(NodeServices.layer)("decider project defaultThreadEnvMode", (it) => {
expect(afterClear.projects[0]?.defaultThreadEnvMode).toBeNull();
}),
);
+
+ it.effect("sets and clears project additional instructions", () =>
+ Effect.gen(function* () {
+ const readModel = yield* projectEvent(createEmptyReadModel(now), seedProjectCreated(1));
+
+ const set = yield* decideOrchestrationCommand({
+ command: {
+ type: "project.meta.update",
+ commandId: CommandId.make("cmd-project-instructions-set"),
+ projectId,
+ additionalInstructions: "Always run focused tests.",
+ },
+ readModel,
+ });
+ const setEvent = Array.isArray(set) ? set[0] : set;
+ const afterSet = yield* projectEvent(readModel, { ...setEvent, sequence: 2 });
+ expect(afterSet.projects[0]?.additionalInstructions).toBe("Always run focused tests.");
+
+ const clear = yield* decideOrchestrationCommand({
+ command: {
+ type: "project.meta.update",
+ commandId: CommandId.make("cmd-project-instructions-clear"),
+ projectId,
+ additionalInstructions: null,
+ },
+ readModel: afterSet,
+ });
+ const clearEvent = Array.isArray(clear) ? clear[0] : clear;
+ const afterClear = yield* projectEvent(afterSet, { ...clearEvent, sequence: 3 });
+ expect(afterClear.projects[0]?.additionalInstructions).toBeNull();
+ }),
+ );
});
diff --git a/apps/server/src/orchestration/decider.ts b/apps/server/src/orchestration/decider.ts
index 4f61955fa6a..5f7a08f18fd 100644
--- a/apps/server/src/orchestration/decider.ts
+++ b/apps/server/src/orchestration/decider.ts
@@ -251,6 +251,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
workspaceRoot: command.workspaceRoot,
defaultModelSelection: command.defaultModelSelection ?? null,
faviconPath: null,
+ additionalInstructions: null,
scripts: [],
createdAt: command.createdAt,
updatedAt: command.createdAt,
@@ -292,6 +293,9 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
? { defaultThreadEnvMode: command.defaultThreadEnvMode }
: {}),
...(command.faviconPath !== undefined ? { faviconPath: command.faviconPath } : {}),
+ ...(command.additionalInstructions !== undefined
+ ? { additionalInstructions: command.additionalInstructions }
+ : {}),
...(command.scripts !== undefined ? { scripts: command.scripts } : {}),
updatedAt: occurredAt,
},
diff --git a/apps/server/src/orchestration/projector.ts b/apps/server/src/orchestration/projector.ts
index f486dcb2bcb..7f282d2990e 100644
--- a/apps/server/src/orchestration/projector.ts
+++ b/apps/server/src/orchestration/projector.ts
@@ -216,6 +216,7 @@ export function projectEvent(
defaultModelSelection: payload.defaultModelSelection,
defaultThreadEnvMode: null,
faviconPath: payload.faviconPath ?? null,
+ additionalInstructions: payload.additionalInstructions ?? null,
scripts: payload.scripts,
createdAt: payload.createdAt,
updatedAt: payload.updatedAt,
@@ -254,6 +255,9 @@ export function projectEvent(
...(payload.faviconPath !== undefined
? { faviconPath: payload.faviconPath }
: {}),
+ ...(payload.additionalInstructions !== undefined
+ ? { additionalInstructions: payload.additionalInstructions }
+ : {}),
...(payload.scripts !== undefined ? { scripts: payload.scripts } : {}),
updatedAt: payload.updatedAt,
}
diff --git a/apps/server/src/persistence/ForkMigrations.test.ts b/apps/server/src/persistence/ForkMigrations.test.ts
index 31f49b84e0f..32a8835a8fa 100644
--- a/apps/server/src/persistence/ForkMigrations.test.ts
+++ b/apps/server/src/persistence/ForkMigrations.test.ts
@@ -48,7 +48,10 @@ legacyForkLayer("ForkMigrations legacy fork upgrade", (it) => {
assert.isTrue(result.repairedLegacyHistory);
assert.deepStrictEqual(result.upstream, [[40, "ProjectionProjectFaviconPath"]]);
- assert.deepStrictEqual(result.fork, [[2, "WorkspacePortAllocations"]]);
+ assert.deepStrictEqual(result.fork, [
+ [2, "WorkspacePortAllocations"],
+ [3, "ProjectAdditionalInstructions"],
+ ]);
const upstreamHistory = yield* sql<{
readonly migration_id: number;
@@ -81,6 +84,7 @@ legacyForkLayer("ForkMigrations legacy fork upgrade", (it) => {
assert.deepStrictEqual(forkHistory, [
{ migration_id: 1, name: "ComposerDrafts" },
{ migration_id: 2, name: "WorkspacePortAllocations" },
+ { migration_id: 3, name: "ProjectAdditionalInstructions" },
]);
const projectColumns = yield* sql<{ readonly name: string }>`
@@ -88,7 +92,7 @@ legacyForkLayer("ForkMigrations legacy fork upgrade", (it) => {
`;
assert.includeMembers(
projectColumns.map(({ name }) => name),
- ["default_thread_env_mode", "favicon_path"],
+ ["default_thread_env_mode", "favicon_path", "additional_instructions"],
);
const draftRows = yield* sql<{
@@ -199,6 +203,7 @@ upstreamLayer("ForkMigrations canonical upstream upgrade", (it) => {
assert.deepStrictEqual(forkHistory, [
{ migration_id: 1, name: "ComposerDrafts" },
{ migration_id: 2, name: "WorkspacePortAllocations" },
+ { migration_id: 3, name: "ProjectAdditionalInstructions" },
]);
}),
);
@@ -223,5 +228,6 @@ it("keeps fork migrations out of the upstream manifest", () => {
assert.deepStrictEqual(forkMigrationManifest, [
[1, "ComposerDrafts"],
[2, "WorkspacePortAllocations"],
+ [3, "ProjectAdditionalInstructions"],
]);
});
diff --git a/apps/server/src/persistence/ForkMigrations.ts b/apps/server/src/persistence/ForkMigrations.ts
index 777afc8872b..afb7745b05e 100644
--- a/apps/server/src/persistence/ForkMigrations.ts
+++ b/apps/server/src/persistence/ForkMigrations.ts
@@ -4,6 +4,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient";
import ForkMigration0001 from "./ForkMigrations/001_ComposerDrafts.ts";
import ForkMigration0002 from "./ForkMigrations/002_WorkspacePortAllocations.ts";
+import ForkMigration0003 from "./ForkMigrations/003_ProjectAdditionalInstructions.ts";
import UpstreamMigration0039 from "./Migrations/039_ProjectionProjectsDefaultThreadEnvMode.ts";
import { runMigrations } from "./Migrations.ts";
@@ -19,6 +20,7 @@ const UPSTREAM_MIGRATION_0039_NAME = "ProjectionProjectsDefaultThreadEnvMode";
export const forkMigrationEntries = [
[1, "ComposerDrafts", ForkMigration0001],
[2, "WorkspacePortAllocations", ForkMigration0002],
+ [3, "ProjectAdditionalInstructions", ForkMigration0003],
] as const;
export const forkMigrationManifest = forkMigrationEntries.map(([id, name]) => [id, name] as const);
diff --git a/apps/server/src/persistence/ForkMigrations/003_ProjectAdditionalInstructions.test.ts b/apps/server/src/persistence/ForkMigrations/003_ProjectAdditionalInstructions.test.ts
new file mode 100644
index 00000000000..41818c6d9ec
--- /dev/null
+++ b/apps/server/src/persistence/ForkMigrations/003_ProjectAdditionalInstructions.test.ts
@@ -0,0 +1,32 @@
+import { assert, it } from "@effect/vitest";
+import * as Effect from "effect/Effect";
+import * as Layer from "effect/Layer";
+import * as SqlClient from "effect/unstable/sql/SqlClient";
+
+import { runForkMigrations } from "../ForkMigrations.ts";
+import { runMigrations } from "../Migrations.ts";
+import * as NodeSqliteClient from "../NodeSqliteClient.ts";
+
+const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory()));
+
+layer("003_ProjectAdditionalInstructions", (it) => {
+ it.effect("adds nullable additional instructions to project projections", () =>
+ Effect.gen(function* () {
+ const sql = yield* SqlClient.SqlClient;
+
+ yield* runMigrations();
+ yield* runForkMigrations({ toMigrationInclusive: 2 });
+ yield* runForkMigrations({ toMigrationInclusive: 3 });
+
+ const columns = yield* sql<{ readonly name: string; readonly notnull: number }>`
+ PRAGMA table_info(projection_projects)
+ `;
+ const additionalInstructions = columns.find(
+ (column) => column.name === "additional_instructions",
+ );
+
+ assert.equal(additionalInstructions?.name, "additional_instructions");
+ assert.equal(additionalInstructions?.notnull, 0);
+ }),
+ );
+});
diff --git a/apps/server/src/persistence/ForkMigrations/003_ProjectAdditionalInstructions.ts b/apps/server/src/persistence/ForkMigrations/003_ProjectAdditionalInstructions.ts
new file mode 100644
index 00000000000..f76ed7b22a8
--- /dev/null
+++ b/apps/server/src/persistence/ForkMigrations/003_ProjectAdditionalInstructions.ts
@@ -0,0 +1,16 @@
+import * as Effect from "effect/Effect";
+import * as SqlClient from "effect/unstable/sql/SqlClient";
+
+export default Effect.gen(function* () {
+ const sql = yield* SqlClient.SqlClient;
+ const columns = yield* sql<{ readonly name: string }>`
+ PRAGMA table_info(projection_projects)
+ `;
+
+ if (!columns.some((column) => column.name === "additional_instructions")) {
+ yield* sql`
+ ALTER TABLE projection_projects
+ ADD COLUMN additional_instructions TEXT
+ `;
+ }
+});
diff --git a/apps/server/src/persistence/Layers/ProjectionProjects.ts b/apps/server/src/persistence/Layers/ProjectionProjects.ts
index ba133bb24a4..61b625c3b32 100644
--- a/apps/server/src/persistence/Layers/ProjectionProjects.ts
+++ b/apps/server/src/persistence/Layers/ProjectionProjects.ts
@@ -37,6 +37,7 @@ const makeProjectionProjectRepository = Effect.gen(function* () {
default_model_selection_json,
default_thread_env_mode,
favicon_path,
+ additional_instructions,
scripts_json,
created_at,
updated_at,
@@ -49,6 +50,7 @@ const makeProjectionProjectRepository = Effect.gen(function* () {
${row.defaultModelSelection !== null ? JSON.stringify(row.defaultModelSelection) : null},
${row.defaultThreadEnvMode},
${row.faviconPath ?? null},
+ ${row.additionalInstructions},
${JSON.stringify(row.scripts)},
${row.createdAt},
${row.updatedAt},
@@ -61,6 +63,7 @@ const makeProjectionProjectRepository = Effect.gen(function* () {
default_model_selection_json = excluded.default_model_selection_json,
default_thread_env_mode = excluded.default_thread_env_mode,
favicon_path = excluded.favicon_path,
+ additional_instructions = excluded.additional_instructions,
scripts_json = excluded.scripts_json,
created_at = excluded.created_at,
updated_at = excluded.updated_at,
@@ -80,6 +83,7 @@ const makeProjectionProjectRepository = Effect.gen(function* () {
default_model_selection_json AS "defaultModelSelection",
default_thread_env_mode AS "defaultThreadEnvMode",
favicon_path AS "faviconPath",
+ additional_instructions AS "additionalInstructions",
scripts_json AS "scripts",
created_at AS "createdAt",
updated_at AS "updatedAt",
@@ -101,6 +105,7 @@ const makeProjectionProjectRepository = Effect.gen(function* () {
default_model_selection_json AS "defaultModelSelection",
default_thread_env_mode AS "defaultThreadEnvMode",
favicon_path AS "faviconPath",
+ additional_instructions AS "additionalInstructions",
scripts_json AS "scripts",
created_at AS "createdAt",
updated_at AS "updatedAt",
diff --git a/apps/server/src/persistence/Layers/ProjectionRepositories.test.ts b/apps/server/src/persistence/Layers/ProjectionRepositories.test.ts
index bebd8fbb4a7..078fc9fb2a6 100644
--- a/apps/server/src/persistence/Layers/ProjectionRepositories.test.ts
+++ b/apps/server/src/persistence/Layers/ProjectionRepositories.test.ts
@@ -34,6 +34,7 @@ projectionRepositoriesLayer("Projection repositories", (it) => {
model: "gpt-5.4",
},
defaultThreadEnvMode: null,
+ additionalInstructions: null,
scripts: [],
createdAt: "2026-03-24T00:00:00.000Z",
updatedAt: "2026-03-24T00:00:00.000Z",
diff --git a/apps/server/src/persistence/Services/ProjectionProjects.ts b/apps/server/src/persistence/Services/ProjectionProjects.ts
index 339439fdfcb..7e7084e2a86 100644
--- a/apps/server/src/persistence/Services/ProjectionProjects.ts
+++ b/apps/server/src/persistence/Services/ProjectionProjects.ts
@@ -9,6 +9,7 @@
import {
IsoDateTime,
ModelSelection,
+ ProjectAdditionalInstructions,
ProjectId,
ProjectScript,
ThreadEnvMode,
@@ -27,6 +28,7 @@ export const ProjectionProject = Schema.Struct({
defaultModelSelection: Schema.NullOr(ModelSelection),
defaultThreadEnvMode: Schema.NullOr(ThreadEnvMode),
faviconPath: Schema.optional(Schema.NullOr(Schema.String)),
+ additionalInstructions: Schema.NullOr(ProjectAdditionalInstructions),
scripts: Schema.Array(ProjectScript),
createdAt: IsoDateTime,
updatedAt: IsoDateTime,
diff --git a/apps/server/src/provider/CodexDeveloperInstructions.ts b/apps/server/src/provider/CodexDeveloperInstructions.ts
index aa7d106e102..badf21af3e6 100644
--- a/apps/server/src/provider/CodexDeveloperInstructions.ts
+++ b/apps/server/src/provider/CodexDeveloperInstructions.ts
@@ -169,6 +169,7 @@ function toSingleLine(value: string): string {
export function buildCodexDeveloperInstructions(
interactionMode: ProviderInteractionMode,
runtime: CodexRuntimeInfo,
+ additionalInstructions?: string,
): string {
const base =
interactionMode === "plan"
@@ -176,5 +177,5 @@ export function buildCodexDeveloperInstructions(
: CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS;
return `${base}
-In case you're asked: you are running in T3 Code through the Codex harness, as ${toSingleLine(runtime.model)} with ${toSingleLine(runtime.reasoningEffort)} reasoning effort. No need to mention this otherwise.`;
+In case you're asked: you are running in T3 Code through the Codex harness, as ${toSingleLine(runtime.model)} with ${toSingleLine(runtime.reasoningEffort)} reasoning effort. No need to mention this otherwise.${additionalInstructions ? `\n\n\n${additionalInstructions}\n` : ""}`;
}
diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts
index 60affcdb183..168caf5b2d5 100644
--- a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts
+++ b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts
@@ -273,6 +273,28 @@ const THREAD_ID = ThreadId.make("thread-claude-1");
const RESUME_THREAD_ID = ThreadId.make("thread-claude-resume");
describe("ClaudeAdapterLive", () => {
+ it.effect("appends project instructions to Claude's preset system prompt", () => {
+ const harness = makeHarness();
+ return Effect.gen(function* () {
+ const adapter = yield* ClaudeAdapter;
+ yield* adapter.startSession({
+ threadId: THREAD_ID,
+ provider: ProviderDriverKind.make("claudeAgent"),
+ additionalInstructions: "Prefer focused tests.",
+ runtimeMode: "full-access",
+ });
+
+ assert.deepEqual(harness.getLastCreateQueryInput()?.options.systemPrompt, {
+ type: "preset",
+ preset: "claude_code",
+ append: "Prefer focused tests.",
+ });
+ }).pipe(
+ Effect.provideService(Random.Random, makeDeterministicRandomService()),
+ Effect.provide(harness.layer),
+ );
+ });
+
it.effect("returns validation error for non-claude provider on startSession", () => {
const harness = makeHarness();
return Effect.gen(function* () {
diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts
index e0b7b402265..977d6671efb 100644
--- a/apps/server/src/provider/Layers/ClaudeAdapter.ts
+++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts
@@ -4162,7 +4162,11 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
...(input.cwd ? { cwd: input.cwd } : {}),
...(apiModelId ? { model: apiModelId } : {}),
pathToClaudeCodeExecutable: claudeBinaryPath,
- systemPrompt: { type: "preset", preset: "claude_code" },
+ systemPrompt: {
+ type: "preset",
+ preset: "claude_code",
+ ...(input.additionalInstructions ? { append: input.additionalInstructions } : {}),
+ },
settingSources: [...CLAUDE_SETTING_SOURCES],
// `ultracode` is a Claude Code setting, not an API effort level. It is
// normalized to `xhigh` above and paired with `settings.ultracode`.
diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts
index b53ecf97dd9..c3bd046def6 100644
--- a/apps/server/src/provider/Layers/CodexAdapter.ts
+++ b/apps/server/src/provider/Layers/CodexAdapter.ts
@@ -1693,6 +1693,9 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
? { resumeCursor: input.resumeCursor }
: {}),
runtimeMode: input.runtimeMode,
+ ...(input.additionalInstructions
+ ? { additionalInstructions: input.additionalInstructions }
+ : {}),
...(input.modelSelection?.instanceId === boundInstanceId
? { model: input.modelSelection.model }
: {}),
diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
index 2a2de683920..16f03c52f0d 100644
--- a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
+++ b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
@@ -249,6 +249,22 @@ describe("buildTurnStartParams", () => {
});
describe("buildCodexDeveloperInstructions", () => {
+ it("appends project instructions in a distinct developer block", () => {
+ const instructions = buildCodexDeveloperInstructions(
+ "default",
+ {
+ model: "gpt-5.4",
+ reasoningEffort: "high",
+ },
+ "Prefer focused tests.",
+ );
+
+ NodeAssert.match(
+ instructions,
+ /\nPrefer focused tests\.\n<\/additional_instructions>$/,
+ );
+ });
+
it("appends runtime info after the mode instructions", () => {
const instructions = buildCodexDeveloperInstructions("default", {
model: "gpt-5.3-codex",
diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts
index 8f887c1b592..21b7227209f 100644
--- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts
+++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts
@@ -107,6 +107,7 @@ export interface CodexSessionRuntimeOptions {
readonly serviceTier?: CodexServiceTier | undefined;
readonly resumeCursor?: CodexResumeCursor;
readonly appServerArgs?: ReadonlyArray;
+ readonly additionalInstructions?: string;
}
export interface CodexSessionRuntimeSendTurnInput {
@@ -340,6 +341,7 @@ function buildCodexCollaborationMode(input: {
readonly interactionMode?: ProviderInteractionMode;
readonly model?: string;
readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort;
+ readonly additionalInstructions?: string;
}): EffectCodexSchema.V2TurnStartParams__CollaborationMode | undefined {
if (input.interactionMode === undefined) {
return undefined;
@@ -351,10 +353,14 @@ function buildCodexCollaborationMode(input: {
settings: {
model,
reasoning_effort: reasoningEffort,
- developer_instructions: buildCodexDeveloperInstructions(input.interactionMode, {
- model,
- reasoningEffort,
- }),
+ developer_instructions: buildCodexDeveloperInstructions(
+ input.interactionMode,
+ {
+ model,
+ reasoningEffort,
+ },
+ input.additionalInstructions,
+ ),
},
};
}
@@ -371,6 +377,7 @@ export function buildTurnStartParams(input: {
readonly serviceTier?: CodexServiceTier;
readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort;
readonly interactionMode?: ProviderInteractionMode;
+ readonly additionalInstructions?: string;
}): Effect.Effect<
CodexTurnStartParamsWithCollaborationMode,
CodexErrors.CodexAppServerProtocolParseError
@@ -391,6 +398,9 @@ export function buildTurnStartParams(input: {
...(input.interactionMode ? { interactionMode: input.interactionMode } : {}),
...(input.model ? { model: input.model } : {}),
...(input.effort ? { effort: input.effort } : {}),
+ ...(input.additionalInstructions
+ ? { additionalInstructions: input.additionalInstructions }
+ : {}),
});
return decodeCodexTurnStartParamsWithCollaborationMode({
@@ -1822,6 +1832,9 @@ export const makeCodexSessionRuntime = (
...(input.serviceTier ? { serviceTier: input.serviceTier } : {}),
...(input.effort ? { effort: input.effort } : {}),
...(input.interactionMode ? { interactionMode: input.interactionMode } : {}),
+ ...(options.additionalInstructions
+ ? { additionalInstructions: options.additionalInstructions }
+ : {}),
});
const rawResponse = yield* client.raw.request("turn/start", params);
const response = yield* decodeV2TurnStartResponse(rawResponse).pipe(
diff --git a/apps/server/src/provider/Layers/CursorAdapter.test.ts b/apps/server/src/provider/Layers/CursorAdapter.test.ts
index cd5cdb7f01a..02368a3808b 100644
--- a/apps/server/src/provider/Layers/CursorAdapter.test.ts
+++ b/apps/server/src/provider/Layers/CursorAdapter.test.ts
@@ -168,6 +168,53 @@ const cursorAdapterTestLayer = it.layer(
);
cursorAdapterTestLayer("CursorAdapterLive", (it) => {
+ it.effect("prefixes project instructions only on the first fresh Cursor prompt", () =>
+ Effect.gen(function* () {
+ const adapter = yield* CursorAdapter;
+ const settings = yield* ServerSettingsService;
+ const threadId = ThreadId.make("cursor-additional-instructions");
+ const tempDir = yield* Effect.promise(() =>
+ NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "cursor-acp-instructions-")),
+ );
+ const requestLogPath = NodePath.join(tempDir, "requests.ndjson");
+ const argvLogPath = NodePath.join(tempDir, "argv.txt");
+ yield* Effect.promise(() => NodeFSP.writeFile(requestLogPath, "", "utf8"));
+ const wrapperPath = yield* Effect.promise(() =>
+ makeProbeWrapper(requestLogPath, argvLogPath),
+ );
+ yield* settings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } });
+
+ yield* adapter.startSession({
+ threadId,
+ provider: ProviderDriverKind.make("cursor"),
+ cwd: process.cwd(),
+ additionalInstructions: "Prefer focused tests.",
+ runtimeMode: "full-access",
+ });
+ yield* adapter.sendTurn({ threadId, input: "First request" });
+ yield* adapter.sendTurn({ threadId, input: "Second request" });
+ yield* adapter.stopSession(threadId);
+
+ const requests = yield* Effect.promise(() => readJsonLines(requestLogPath));
+ const prompts = requests.filter((entry) => entry.method === "session/prompt");
+ const promptBlocks = (entry: Record) =>
+ (entry.params as { prompt?: Array<{ type?: string; text?: string }> } | undefined)
+ ?.prompt ?? [];
+
+ assert.deepStrictEqual(
+ promptBlocks(prompts[0] ?? {}).map((block) => block.text),
+ [
+ "\nPrefer focused tests.\n",
+ "First request",
+ ],
+ );
+ assert.deepStrictEqual(
+ promptBlocks(prompts[1] ?? {}).map((block) => block.text),
+ ["Second request"],
+ );
+ }),
+ );
+
it.effect("starts a session and maps mock ACP prompt flow to runtime events", () =>
Effect.gen(function* () {
const adapter = yield* CursorAdapter;
diff --git a/apps/server/src/provider/Layers/CursorAdapter.ts b/apps/server/src/provider/Layers/CursorAdapter.ts
index 89fe084f7c0..b03b2866e3a 100644
--- a/apps/server/src/provider/Layers/CursorAdapter.ts
+++ b/apps/server/src/provider/Layers/CursorAdapter.ts
@@ -131,6 +131,7 @@ interface CursorSessionContext {
readonly pendingApprovals: Map;
readonly pendingUserInputs: Map;
readonly turns: Array<{ id: TurnId; items: Array }>;
+ additionalInstructionsPending: string | undefined;
lastPlanFingerprint: string | undefined;
activeTurnId: TurnId | undefined;
/** Number of sendTurn prompts currently in flight or being prepared.
@@ -779,6 +780,8 @@ export function makeCursorAdapter(
pendingApprovals,
pendingUserInputs,
turns: [],
+ additionalInstructionsPending:
+ resumeSessionId === undefined ? input.additionalInstructions : undefined,
lastPlanFingerprint: undefined,
activeTurnId: undefined,
promptsInFlight: 0,
@@ -970,6 +973,15 @@ export function makeCursorAdapter(
}
const promptParts: Array = [];
+ const additionalInstructions =
+ steeringTurnId === undefined ? ctx.additionalInstructionsPending : undefined;
+ if (additionalInstructions) {
+ ctx.additionalInstructionsPending = undefined;
+ promptParts.push({
+ type: "text",
+ text: `\n${additionalInstructions}\n`,
+ });
+ }
if (input.input?.trim()) {
promptParts.push({ type: "text", text: input.input.trim() });
}
diff --git a/apps/server/src/provider/Layers/GrokAdapter.ts b/apps/server/src/provider/Layers/GrokAdapter.ts
index 25ab0a16a98..3822209f865 100644
--- a/apps/server/src/provider/Layers/GrokAdapter.ts
+++ b/apps/server/src/provider/Layers/GrokAdapter.ts
@@ -578,6 +578,9 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
...(sessionEnvironment ? { environment: sessionEnvironment } : {}),
childProcessSpawner,
cwd,
+ ...(input.additionalInstructions
+ ? { additionalInstructions: input.additionalInstructions }
+ : {}),
...(resumeSessionId ? { resumeSessionId } : {}),
clientInfo: { name: "t3-code", version: "0.0.0" },
...(mcpSession
diff --git a/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts b/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts
index eea328e05d1..581cdc5c1ac 100644
--- a/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts
+++ b/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts
@@ -281,6 +281,31 @@ const advanceTestClock = (ms: number) =>
TestClock.adjust(`${ms} millis`).pipe(Effect.andThen(Effect.yieldNow));
it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {
+ it.effect("passes project instructions through the OpenCode system field", () =>
+ Effect.gen(function* () {
+ const adapter = yield* OpenCodeAdapter;
+ const threadId = asThreadId("thread-opencode-instructions");
+
+ yield* adapter.startSession({
+ provider: ProviderDriverKind.make("opencode"),
+ threadId,
+ additionalInstructions: "Prefer focused tests.",
+ modelSelection: createModelSelection(
+ ProviderInstanceId.make("opencode"),
+ "anthropic/sonnet",
+ ),
+ runtimeMode: "full-access",
+ });
+ yield* adapter.sendTurn({ threadId, input: "Fix it" });
+
+ NodeAssert.equal(
+ (runtimeMock.state.promptCalls.at(-1) as { system?: string }).system,
+ "Prefer focused tests.",
+ );
+ yield* adapter.stopSession(threadId);
+ }),
+ );
+
it.effect("reuses a configured OpenCode server URL instead of spawning a local server", () =>
Effect.gen(function* () {
const adapter = yield* OpenCodeAdapter;
diff --git a/apps/server/src/provider/Layers/OpenCodeAdapter.ts b/apps/server/src/provider/Layers/OpenCodeAdapter.ts
index 237e0f58415..5b926b8d6a6 100644
--- a/apps/server/src/provider/Layers/OpenCodeAdapter.ts
+++ b/apps/server/src/provider/Layers/OpenCodeAdapter.ts
@@ -223,6 +223,7 @@ function isOpenCodeDefaultTitle(title: string): boolean {
interface OpenCodeSessionContext {
session: ProviderSession;
+ readonly additionalInstructions: string | undefined;
readonly client: OpencodeClient;
readonly server: OpenCodeServerConnection;
readonly directory: string;
@@ -1391,6 +1392,7 @@ export function makeOpenCodeAdapter(
const context: OpenCodeSessionContext = {
session,
+ additionalInstructions: input.additionalInstructions,
client: started.client,
server: started.server,
directory,
@@ -1508,6 +1510,7 @@ export function makeOpenCodeAdapter(
model: parsedModel,
...(context.activeAgent ? { agent: context.activeAgent } : {}),
...(context.activeVariant ? { variant: context.activeVariant } : {}),
+ ...(context.additionalInstructions ? { system: context.additionalInstructions } : {}),
parts: [...(text ? [{ type: "text" as const, text }] : []), ...fileParts],
}),
).pipe(
diff --git a/apps/server/src/provider/Layers/ProviderService.test.ts b/apps/server/src/provider/Layers/ProviderService.test.ts
index 1d1134f1b75..1480f9132e1 100644
--- a/apps/server/src/provider/Layers/ProviderService.test.ts
+++ b/apps/server/src/provider/Layers/ProviderService.test.ts
@@ -1184,7 +1184,7 @@ routing.layer("ProviderServiceLive routing", (it) => {
}),
);
- it.effect("recovers stale sessions for sendTurn using persisted cwd", () =>
+ it.effect("recovers stale sessions using persisted cwd and project instructions", () =>
Effect.gen(function* () {
const provider = yield* ProviderService.ProviderService;
@@ -1193,6 +1193,7 @@ routing.layer("ProviderServiceLive routing", (it) => {
providerInstanceId: codexInstanceId,
threadId: asThreadId("thread-1"),
cwd: "/tmp/project-send-turn",
+ additionalInstructions: "Prefer focused tests.",
runtimeMode: "full-access",
});
@@ -1213,11 +1214,13 @@ routing.layer("ProviderServiceLive routing", (it) => {
const startPayload = resumedStartInput as {
provider?: string;
cwd?: string;
+ additionalInstructions?: string;
resumeCursor?: unknown;
threadId?: string;
};
assert.equal(startPayload.provider, "codex");
assert.equal(startPayload.cwd, "/tmp/project-send-turn");
+ assert.equal(startPayload.additionalInstructions, "Prefer focused tests.");
assert.deepEqual(startPayload.resumeCursor, initial.resumeCursor);
assert.equal(startPayload.threadId, initial.threadId);
}
diff --git a/apps/server/src/provider/Layers/ProviderService.ts b/apps/server/src/provider/Layers/ProviderService.ts
index 5e515f24649..827a6598b10 100644
--- a/apps/server/src/provider/Layers/ProviderService.ts
+++ b/apps/server/src/provider/Layers/ProviderService.ts
@@ -129,6 +129,7 @@ function toRuntimePayloadFromSession(
session: ProviderSession,
extra?: {
readonly modelSelection?: unknown;
+ readonly additionalInstructions?: string;
readonly lastRuntimeEvent?: string;
readonly lastRuntimeEventAt?: string;
},
@@ -139,6 +140,9 @@ function toRuntimePayloadFromSession(
activeTurnId: session.activeTurnId ?? null,
lastError: session.lastError ?? null,
...(extra?.modelSelection !== undefined ? { modelSelection: extra.modelSelection } : {}),
+ ...(extra?.additionalInstructions !== undefined
+ ? { additionalInstructions: extra.additionalInstructions }
+ : {}),
...(extra?.lastRuntimeEvent !== undefined ? { lastRuntimeEvent: extra.lastRuntimeEvent } : {}),
...(extra?.lastRuntimeEventAt !== undefined
? { lastRuntimeEventAt: extra.lastRuntimeEventAt }
@@ -168,6 +172,19 @@ function readPersistedCwd(
return trimmed.length > 0 ? trimmed : undefined;
}
+function readPersistedAdditionalInstructions(
+ runtimePayload: ProviderSessionDirectory.ProviderRuntimeBinding["runtimePayload"],
+): string | undefined {
+ if (!runtimePayload || typeof runtimePayload !== "object" || Array.isArray(runtimePayload)) {
+ return undefined;
+ }
+ const raw =
+ "additionalInstructions" in runtimePayload ? runtimePayload.additionalInstructions : undefined;
+ if (typeof raw !== "string") return undefined;
+ const trimmed = raw.trim();
+ return trimmed.length > 0 ? trimmed : undefined;
+}
+
const dieOnMissingBindingInstanceId = (
operation: string,
payload: {
@@ -278,6 +295,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
threadId: ThreadId,
extra?: {
readonly modelSelection?: unknown;
+ readonly additionalInstructions?: string;
readonly lastRuntimeEvent?: string;
readonly lastRuntimeEventAt?: string;
},
@@ -384,6 +402,9 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
const adapter = yield* registry.getByInstance(bindingInstanceId);
const hasResumeCursor =
input.binding.resumeCursor !== null && input.binding.resumeCursor !== undefined;
+ const persistedAdditionalInstructions = readPersistedAdditionalInstructions(
+ input.binding.runtimePayload,
+ );
const hasActiveSession = yield* adapter.hasSession(input.binding.threadId);
if (hasActiveSession) {
const activeSessions = yield* adapter.listSessions();
@@ -394,6 +415,9 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
yield* upsertSessionBinding(
{ ...existing, providerInstanceId: bindingInstanceId },
input.binding.threadId,
+ persistedAdditionalInstructions
+ ? { additionalInstructions: persistedAdditionalInstructions }
+ : undefined,
);
yield* analytics.record("provider.session.recovered", {
provider: existing.provider,
@@ -428,6 +452,9 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
...(persistedCwd ? { cwd: persistedCwd } : {}),
...(workspaceEnvironment ? { environment: workspaceEnvironment } : {}),
...(persistedModelSelection ? { modelSelection: persistedModelSelection } : {}),
+ ...(persistedAdditionalInstructions
+ ? { additionalInstructions: persistedAdditionalInstructions }
+ : {}),
...(hasResumeCursor ? { resumeCursor: input.binding.resumeCursor } : {}),
runtimeMode: input.binding.runtimeMode ?? "full-access",
})
@@ -443,6 +470,9 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
yield* upsertSessionBinding(
{ ...resumed, providerInstanceId: bindingInstanceId },
input.binding.threadId,
+ persistedAdditionalInstructions
+ ? { additionalInstructions: persistedAdditionalInstructions }
+ : undefined,
);
yield* analytics.record("provider.session.recovered", {
provider: resumed.provider,
@@ -652,6 +682,9 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
});
yield* upsertSessionBinding(sessionWithInstance, threadId, {
modelSelection: input.modelSelection,
+ ...(input.additionalInstructions
+ ? { additionalInstructions: input.additionalInstructions }
+ : {}),
});
yield* analytics.record("provider.session.started", {
provider: sessionWithInstance.provider,
diff --git a/apps/server/src/provider/acp/GrokAcpSupport.test.ts b/apps/server/src/provider/acp/GrokAcpSupport.test.ts
index 02d60976b24..7c5d9b8b83f 100644
--- a/apps/server/src/provider/acp/GrokAcpSupport.test.ts
+++ b/apps/server/src/provider/acp/GrokAcpSupport.test.ts
@@ -33,6 +33,17 @@ describe("buildGrokAcpSpawnInput", () => {
},
});
});
+
+ it("passes additional instructions through Grok's rules option", () => {
+ const spawn = buildGrokAcpSpawnInput(
+ undefined,
+ "/tmp/project",
+ undefined,
+ "Prefer focused tests.",
+ );
+
+ expect(spawn.args).toEqual(["--rules", "Prefer focused tests.", "agent", "stdio"]);
+ });
});
describe("applyGrokAcpModelSelection", () => {
diff --git a/apps/server/src/provider/acp/GrokAcpSupport.ts b/apps/server/src/provider/acp/GrokAcpSupport.ts
index c928b3ed80e..6e5e4e7ddf6 100644
--- a/apps/server/src/provider/acp/GrokAcpSupport.ts
+++ b/apps/server/src/provider/acp/GrokAcpSupport.ts
@@ -27,16 +27,22 @@ interface GrokAcpRuntimeInput extends Omit<
readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"];
readonly grokSettings: GrokAcpRuntimeGrokSettings | null | undefined;
readonly environment?: NodeJS.ProcessEnv;
+ readonly additionalInstructions?: string;
}
export function buildGrokAcpSpawnInput(
grokSettings: GrokAcpRuntimeGrokSettings | null | undefined,
cwd: string,
environment?: NodeJS.ProcessEnv,
+ additionalInstructions?: string,
): AcpSessionRuntime.AcpSpawnInput {
return {
command: grokSettings?.binaryPath || "grok",
- args: ["agent", "stdio"],
+ args: [
+ ...(additionalInstructions ? ["--rules", additionalInstructions] : []),
+ "agent",
+ "stdio",
+ ],
cwd,
env: {
...environment,
@@ -62,7 +68,12 @@ export const makeGrokAcpRuntime = (
const acpContext = yield* Layer.build(
AcpSessionRuntime.layer({
...input,
- spawn: buildGrokAcpSpawnInput(input.grokSettings, input.cwd, input.environment),
+ spawn: buildGrokAcpSpawnInput(
+ input.grokSettings,
+ input.cwd,
+ input.environment,
+ input.additionalInstructions,
+ ),
authMethodId: resolveGrokAuthMethodId(input.environment),
}).pipe(
Layer.provide(
diff --git a/apps/web/src/components/settings/ProjectSettingsPanel.tsx b/apps/web/src/components/settings/ProjectSettingsPanel.tsx
index 516e2e8c5cd..eeb22d52f9a 100644
--- a/apps/web/src/components/settings/ProjectSettingsPanel.tsx
+++ b/apps/web/src/components/settings/ProjectSettingsPanel.tsx
@@ -88,6 +88,7 @@ import { cn } from "../../lib/utils";
import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "../../workspaceTitlebar";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
+import { Textarea } from "../ui/textarea";
import {
Menu,
MenuGroup,
@@ -368,6 +369,7 @@ function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) {
(member) => member.environmentId === group.environmentId && member.id === group.id,
) ?? group.memberProjects[0]!;
const faviconPath = representative.faviconPath ?? null;
+ const additionalInstructions = representative.additionalInstructions ?? null;
const threadCountByMember = useMemo(() => {
const counts = new Map();
@@ -398,6 +400,7 @@ function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) {
defaultModelSelection: ModelSelection | null;
defaultThreadEnvMode: ThreadEnvMode | null;
faviconPath: string | null;
+ additionalInstructions: string | null;
}>,
failureTitle: string,
): Promise> => {
@@ -474,6 +477,15 @@ function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) {
[updateAllMembers],
);
+ const setAdditionalInstructions = useCallback(
+ (instructions: string | null) =>
+ void updateAllMembers(
+ { additionalInstructions: instructions },
+ "Failed to update additional instructions",
+ ),
+ [updateAllMembers],
+ );
+
// ----- favicon -----
const [faviconPickerOpen, setFaviconPickerOpen] = useState(false);
const [isSavingFavicon, setIsSavingFavicon] = useState(false);
@@ -993,6 +1005,35 @@ function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) {
}
/>
+ setAdditionalInstructions(null)}
+ />
+ ) : null
+ }
+ control={
+