Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions apps/server/src/j5/agents/agentPersonaPrompts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { assert, describe, it } from "@effect/vitest";

import {
BUILDER_AGENT_PERSONA_INSTRUCTIONS_V1,
getBuiltInAgentPersonaInstructions,
} from "./agentPersonaPrompts.ts";

describe("agent persona prompts", () => {
it("defines Builder's inputs, review work, output, and publication boundary", () => {
assert.include(BUILDER_AGENT_PERSONA_INSTRUCTIONS_V1, "Plan Handoff");
assert.include(BUILDER_AGENT_PERSONA_INSTRUCTIONS_V1, "Diagnosis Handoff");
assert.include(BUILDER_AGENT_PERSONA_INSTRUCTIONS_V1, "Review Inbox");
assert.include(BUILDER_AGENT_PERSONA_INSTRUCTIONS_V1, "Critic and Sentry");
assert.include(BUILDER_AGENT_PERSONA_INSTRUCTIONS_V1, "Code Complete Handoff");
assert.include(BUILDER_AGENT_PERSONA_INSTRUCTIONS_V1, "Never commit or push");
});

it("resolves only the implemented Builder definition version", () => {
assert.equal(
getBuiltInAgentPersonaInstructions({ personaId: "builder", definitionVersion: 1 }),
BUILDER_AGENT_PERSONA_INSTRUCTIONS_V1,
);
assert.isUndefined(
getBuiltInAgentPersonaInstructions({ personaId: "builder", definitionVersion: 2 }),
);
assert.isUndefined(
getBuiltInAgentPersonaInstructions({ personaId: "critic", definitionVersion: 1 }),
);
});
});
55 changes: 55 additions & 0 deletions apps/server/src/j5/agents/agentPersonaPrompts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { BuiltInAgentPersonaId } from "@t3tools/contracts";

export const BUILDER_AGENT_PERSONA_INSTRUCTIONS_V1 = `<agent_persona id="builder" version="1">
# Builder

You implement one approved Plan Handoff, Diagnosis Handoff, or Review Inbox in the current workspace. When the input is a Review Inbox, apply the validated findings from Critic and Sentry. Return a Code Complete Handoff when the work is ready for review.

## Operating contract

- Treat the governing handoff as the scope contract. Inspect repository instructions and current state before editing.
- Make the smallest complete implementation that satisfies the handoff. Add or update focused tests when behavior changes.
- Validate the changed behavior with the narrowest relevant tests, checks, or builds. Distinguish source failures from environment or harness failures.
- For Critic and Sentry findings, record each finding as applied, refuted with evidence, or unresolved. Do not silently skip a finding or expand beyond it.
- Preserve unrelated user changes. Stop and report a conflict when the requested work cannot be completed safely within the handoff.

## Authority boundary

- You may edit workspace files and run build, test, lint, formatting, and inspection commands needed for the implementation.
- Never commit or push. Never open, update, approve, close, or merge a pull request. Do not perform those actions even when the handoff asks for them.
- Do not claim publication or review approval. A Publisher owns publication; Critic and Sentry own independent review.

## Required output: Code Complete Handoff

Return a concise Code Complete Handoff containing:

1. The governing handoff and implemented scope.
2. Changed paths and a behavior summary.
3. Critic and Sentry finding dispositions, when provided.
4. Tests and checks run with their results.
5. Known limitations, residual risks, and unverified areas.
6. The review-ready diff identity when available.
7. An explicit statement that no commit or push was performed.
</agent_persona>`;

export function getBuiltInAgentPersonaInstructions(input: {
readonly personaId: BuiltInAgentPersonaId;
readonly definitionVersion: number;
}): string | undefined {
return input.personaId === "builder" && input.definitionVersion === 1
? BUILDER_AGENT_PERSONA_INSTRUCTIONS_V1
: undefined;
}

/**
* Adapter-side composition helpers. Upstream adapters call exactly one of these where they
* already build developer or system instructions, so persona prompt plumbing stays here.
*/
export const withAgentPersonaInstructions = (
base: string | undefined,
persona: string | undefined,
): string | undefined =>
persona === undefined ? base : base === undefined ? persona : `${base}\n\n${persona}`;

export const agentPersonaPromptSuffix = (persona: string | undefined): string =>
persona === undefined ? "" : `\n\n${persona}`;
16 changes: 16 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,22 @@ describe("ClaudeAdapterV2 native protocol logging", () => {
}
});

it("adds persona instructions to Claude's system prompt without requiring MCP", () => {
const options = makeClaudeQueryOptions({
modelSelection: CLAUDE_TEST_MODEL_SELECTION,
nativeThreadId: "native-builder-instructions",
resume: false,
cwd: "/workspace",
agentPersonaInstructions: "Builder instructions",
});

assert.isObject(options.systemPrompt);
const systemPrompt = options.systemPrompt as { readonly append?: string };
assert.include(systemPrompt.append ?? "", "Builder instructions");
assert.include(systemPrompt.append ?? "", "<runtime_info>");
assert.notInclude(systemPrompt.append ?? "", T3_CODE_ORCHESTRATION_INSTRUCTIONS);
});

it.effect("writes Claude Agent SDK protocol frames to the native provider log", () =>
Effect.gen(function* () {
const writes: Array<{
Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ import {
makeSubagentConversationArtifacts,
subagentThreadTitle,
} from "../SubagentProjection.ts";
import { agentPersonaPromptSuffix } from "../../j5/agents/agentPersonaPrompts.ts";

export const CLAUDE_PROVIDER = ProviderDriverKind.make("claudeAgent");
export const CLAUDE_AGENT_SDK_QUERY_PROTOCOL = "claude-agent-sdk.query" as const;
Expand Down Expand Up @@ -728,6 +729,7 @@ export function makeClaudeQueryOptions(input: {
readonly onUserDialog?: ClaudeQueryOptions["onUserDialog"];
readonly supportedDialogKinds?: ClaudeQueryOptions["supportedDialogKinds"];
readonly allowDangerouslySkipPermissions?: boolean;
readonly agentPersonaInstructions?: string | undefined;
}): ClaudeAgentSdkQueryOptions {
const compiledSelection = compileClaudeModelSelection(input.modelSelection);
const extraArgs =
Expand Down Expand Up @@ -786,7 +788,8 @@ export function makeClaudeQueryOptions(input: {
preset: "claude_code" as const,
append:
buildRuntimeInstructions({ harness: "Claude Code" }) +
(input.mcpServers === undefined ? "" : T3_CODE_ORCHESTRATION_INSTRUCTIONS),
(input.mcpServers === undefined ? "" : T3_CODE_ORCHESTRATION_INSTRUCTIONS) +
agentPersonaPromptSuffix(input.agentPersonaInstructions),
},
...(Object.keys(extraArgs).length === 0 ? {} : { extraArgs }),
};
Expand Down Expand Up @@ -5277,6 +5280,7 @@ export function makeClaudeAdapterV2(
canUseTool,
onUserDialog,
supportedDialogKinds: ["resume_return"],
agentPersonaInstructions: turnInput.runtimePolicy.agentPersonaInstructions,
}),
})
.pipe(
Expand Down
26 changes: 26 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,32 @@ describe("CodexAdapterV2 runtime policy", () => {
}),
);

it.effect("adds persona instructions without requiring the T3 MCP server", () =>
Effect.gen(function* () {
const params = yield* buildCodexTurnStartParams({
nativeThreadId: "native-builder-instructions",
codexInput: [{ type: "text", text: "implement this handoff" }],
runtimePolicy: {
runtimeMode: "auto-accept-edits",
interactionMode: "default",
cwd: "/workspace",
agentPersonaInstructions: "Builder instructions",
},
modelSelection: {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5.6-sol",
},
hasT3Mcp: false,
});

assert.equal(params.collaborationMode?.mode, "default");
assert.equal(
params.collaborationMode?.settings.developer_instructions,
"Builder instructions",
);
}),
);

it.effect("adds T3 plan-mode developer instructions when the T3 MCP server is attached", () =>
Effect.gen(function* () {
const params = yield* buildCodexTurnStartParams({
Expand Down
7 changes: 5 additions & 2 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ import {
makeSubagentConversationArtifacts,
subagentThreadTitle,
} from "../SubagentProjection.ts";
import { withAgentPersonaInstructions } from "../../j5/agents/agentPersonaPrompts.ts";

const CODEX_PROVIDER = ProviderDriverKind.make("codex");
export const CODEX_DRIVER_KIND = CODEX_PROVIDER;
Expand Down Expand Up @@ -665,12 +666,14 @@ export function buildCodexTurnStartParams(input: {
const effort =
selectedEffort === undefined ? undefined : yield* decodeTurnReasoningEffort(selectedEffort);
const serviceTier = getCodexServiceTierOptionValue(input.modelSelection);
const developerInstructions =
const developerInstructions = withAgentPersonaInstructions(
input.hasT3Mcp !== true
? undefined
: input.runtimePolicy.interactionMode === "plan"
? codexPlanModeDeveloperInstructions(input.browserToolsAvailable ?? true)
: codexDefaultModeDeveloperInstructions(input.browserToolsAvailable ?? true);
: codexDefaultModeDeveloperInstructions(input.browserToolsAvailable ?? true),
input.runtimePolicy.agentPersonaInstructions,
);
const collaborationMode: CodexSchema.ClientRequest__CollaborationMode | undefined =
input.runtimePolicy.interactionMode !== "plan" && developerInstructions === undefined
? undefined
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/orchestration-v2/ProviderAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const ProviderAdapterV2RuntimePolicy = Schema.Struct({
approvalPolicy: Schema.optional(Schema.Unknown),
sandboxPolicy: Schema.optional(Schema.Unknown),
reasoningEffort: Schema.optional(Schema.String),
agentPersonaInstructions: Schema.optional(Schema.String),
});
export type ProviderAdapterV2RuntimePolicy = typeof ProviderAdapterV2RuntimePolicy.Type;

Expand Down
23 changes: 23 additions & 0 deletions apps/server/src/orchestration-v2/RuntimePolicy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as Layer from "effect/Layer";
import * as Option from "effect/Option";

import * as ProjectionProjects from "../persistence/Services/ProjectionProjects.ts";
import { BUILDER_AGENT_PERSONA_INSTRUCTIONS_V1 } from "../j5/agents/agentPersonaPrompts.ts";
import { layerFromProjectRepository, RuntimePolicyV2 } from "./RuntimePolicy.ts";

const projectId = ProjectId.make("project:runtime-policy");
Expand Down Expand Up @@ -161,4 +162,26 @@ it.layer(TestLayer)("RuntimePolicyV2", (it) => {
});
}),
);

it.effect("adds the versioned Builder instructions to its runtime policy", () =>
Effect.gen(function* () {
const policy = yield* RuntimePolicyV2;
const now = yield* DateTime.now;
const thread = {
...makeThread({ now, worktreePath: "/project-worktree" }),
agentPersonaAssignment: {
personaId: "builder",
definitionVersion: 1,
authorityPolicy: "workspace-write",
resolvedRoute: "primary",
resolvedDriver: ProviderDriverKind.make("codex"),
resolvedModelSelection: modelSelection,
},
} satisfies OrchestrationV2AppThread;

const resolved = yield* policy.resolve({ thread, modelSelection });

assert.equal(resolved.agentPersonaInstructions, BUILDER_AGENT_PERSONA_INSTRUCTIONS_V1);
}),
);
});
12 changes: 11 additions & 1 deletion apps/server/src/orchestration-v2/RuntimePolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as Option from "effect/Option";
import * as Schema from "effect/Schema";

import * as ProjectionProjects from "../persistence/Services/ProjectionProjects.ts";
import { getBuiltInAgentPersonaInstructions } from "../j5/agents/agentPersonaPrompts.ts";
import { translateAgentPersonaProviderPolicy } from "../j5/agents/agentPersonaProviderPolicy.ts";
import {
ProviderAdapterV2RuntimePolicy,
Expand Down Expand Up @@ -62,18 +63,27 @@ function runtimePolicyForThread(input: {
readonly thread: OrchestrationV2AppThread;
readonly cwd: string | null;
}): ProviderAdapterV2RuntimePolicyType {
const authorityPolicy = input.thread.agentPersonaAssignment?.authorityPolicy;
const assignment = input.thread.agentPersonaAssignment;
const authorityPolicy = assignment?.authorityPolicy;
const providerPolicy =
authorityPolicy === undefined
? { runtimeMode: input.thread.runtimeMode }
: translateAgentPersonaProviderPolicy(
authorityPolicy,
input.thread.agentPersonaAssignment!.resolvedDriver,
);
const agentPersonaInstructions =
assignment === undefined
? undefined
: getBuiltInAgentPersonaInstructions({
personaId: assignment.personaId,
definitionVersion: assignment.definitionVersion,
});
return ProviderAdapterV2RuntimePolicy.make({
...providerPolicy,
interactionMode: input.thread.interactionMode,
cwd: input.cwd,
...(agentPersonaInstructions === undefined ? {} : { agentPersonaInstructions }),
});
}

Expand Down
Loading