Add daemon-wide system prompts (#1100)

* Add daemon-wide system prompt setting

* Align settings textarea rendering

* Polish daemon settings layout

* Remove orchestrator prompt injection
This commit is contained in:
Mohamed Boudra
2026-05-19 14:24:56 +00:00
committed by GitHub
parent da0e94fd18
commit 306601a0c3
21 changed files with 449 additions and 124 deletions
+1
View File
@@ -139,6 +139,7 @@ Single file, validated with `PersistedConfigSchema`.
listen: "127.0.0.1:6767", listen: "127.0.0.1:6767",
hostnames: true | string[], // legacy alias `allowedHosts` is migrated on load hostnames: true | string[], // legacy alias `allowedHosts` is migrated on load
mcp: { enabled: boolean, injectIntoAgents: boolean }, mcp: { enabled: boolean, injectIntoAgents: boolean },
appendSystemPrompt: string, // appended to supported provider system/developer prompts
cors: { allowedOrigins: string[] }, cors: { allowedOrigins: string[] },
relay: { enabled: boolean, endpoint: string, publicEndpoint: string, useTls: boolean, publicUseTls: boolean }, relay: { enabled: boolean, endpoint: string, publicEndpoint: string, useTls: boolean, publicUseTls: boolean },
auth: { password: string } // bcrypt hash, optional auth: { password: string } // bcrypt hash, optional
@@ -0,0 +1,58 @@
import type { StyleProp, TextStyle } from "react-native";
import { useMemo } from "react";
import { TextInput, View } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { settingsStyles } from "@/styles/settings";
interface SettingsTextAreaProps {
accessibilityLabel: string;
value: string;
onChangeText: (text: string) => void;
placeholder?: string;
testID?: string;
style?: StyleProp<TextStyle>;
}
export function SettingsTextArea({
accessibilityLabel,
value,
onChangeText,
placeholder,
testID,
style,
}: SettingsTextAreaProps) {
const { theme } = useUnistyles();
const inputStyle = useMemo(() => [styles.input, style], [style]);
return (
<TextInput
testID={testID}
accessibilityLabel={accessibilityLabel}
multiline
value={value}
onChangeText={onChangeText}
placeholder={placeholder}
placeholderTextColor={theme.colors.foregroundMuted}
style={inputStyle}
/>
);
}
export function SettingsTextAreaCard(props: SettingsTextAreaProps) {
return (
<View style={settingsStyles.card}>
<SettingsTextArea {...props} />
</View>
);
}
const styles = StyleSheet.create((theme) => ({
input: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
paddingVertical: theme.spacing[3],
paddingHorizontal: theme.spacing[4],
minHeight: 96,
textAlignVertical: "top",
},
}));
@@ -23,6 +23,7 @@ import { ExternalLink } from "@/components/ui/external-link";
import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet"; import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
import { SettingsTextAreaCard } from "@/components/settings-textarea";
import { SettingsGroup } from "@/screens/settings/settings-group"; import { SettingsGroup } from "@/screens/settings/settings-group";
import { SettingsSection } from "@/screens/settings/settings-section"; import { SettingsSection } from "@/screens/settings/settings-section";
import { settingsStyles } from "@/styles/settings"; import { settingsStyles } from "@/styles/settings";
@@ -621,18 +622,13 @@ function ProjectConfigForm({
testID="worktree-group" testID="worktree-group"
> >
<SettingsSection title="Setup" testID="worktree-setup-section" trailing={setupDocsLink}> <SettingsSection title="Setup" testID="worktree-setup-section" trailing={setupDocsLink}>
<View style={settingsStyles.card}> <SettingsTextAreaCard
<TextInput testID="worktree-setup-input"
testID="worktree-setup-input" accessibilityLabel="Worktree setup commands"
accessibilityLabel="Worktree setup commands" value={draft.setupText}
multiline onChangeText={handleSetupChange}
value={draft.setupText} placeholder="npm install"
onChangeText={handleSetupChange} />
placeholder="npm install"
placeholderTextColor={styles.placeholderColor.color}
style={styles.lifecycleInput}
/>
</View>
</SettingsSection> </SettingsSection>
<SettingsSection <SettingsSection
@@ -641,18 +637,13 @@ function ProjectConfigForm({
trailing={teardownDocsLink} trailing={teardownDocsLink}
flush flush
> >
<View style={settingsStyles.card}> <SettingsTextAreaCard
<TextInput testID="worktree-teardown-input"
testID="worktree-teardown-input" accessibilityLabel="Worktree teardown commands"
accessibilityLabel="Worktree teardown commands" value={draft.teardownText}
multiline onChangeText={handleTeardownChange}
value={draft.teardownText} placeholder="docker compose down"
onChangeText={handleTeardownChange} />
placeholder="docker compose down"
placeholderTextColor={styles.placeholderColor.color}
style={styles.lifecycleInput}
/>
</View>
</SettingsSection> </SettingsSection>
</SettingsGroup> </SettingsGroup>
@@ -1008,18 +999,13 @@ function MetadataPromptSection({ promptKey, value, onChange, flush }: MetadataPr
); );
return ( return (
<SettingsSection title={meta.title} testID={meta.sectionTestID} flush={flush}> <SettingsSection title={meta.title} testID={meta.sectionTestID} flush={flush}>
<View style={settingsStyles.card}> <SettingsTextAreaCard
<TextInput testID={meta.inputTestID}
testID={meta.inputTestID} accessibilityLabel={meta.title}
accessibilityLabel={meta.title} value={value}
multiline onChangeText={handleChange}
value={value} placeholder={meta.placeholder}
onChangeText={handleChange} />
placeholder={meta.placeholder}
placeholderTextColor={styles.placeholderColor.color}
style={styles.lifecycleInput}
/>
</View>
</SettingsSection> </SettingsSection>
); );
} }
@@ -1344,14 +1330,6 @@ const styles = StyleSheet.create((theme) => ({
errorBlock: { errorBlock: {
marginTop: theme.spacing[2], marginTop: theme.spacing[2],
}, },
lifecycleInput: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
paddingVertical: theme.spacing[3],
paddingHorizontal: theme.spacing[4],
minHeight: 96,
textAlignVertical: "top",
},
emptyScripts: { emptyScripts: {
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.sm,
+117 -4
View File
@@ -4,6 +4,7 @@ import { Alert, Pressable, Text, View } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet"; import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
import { AdaptiveRenameModal } from "@/components/rename-modal"; import { AdaptiveRenameModal } from "@/components/rename-modal";
import { SettingsTextAreaCard } from "@/components/settings-textarea";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { LocalDaemonSection } from "@/desktop/components/desktop-updates-section"; import { LocalDaemonSection } from "@/desktop/components/desktop-updates-section";
@@ -379,9 +380,9 @@ function ConnectionRow({
function DaemonSection({ host, isLocalDaemon }: { host: HostProfile; isLocalDaemon: boolean }) { function DaemonSection({ host, isLocalDaemon }: { host: HostProfile; isLocalDaemon: boolean }) {
return ( return (
<> <>
<SettingsSection title="Operations"> <SettingsSection title="Daemon settings">
<RestartDaemonCard host={host} />
<InjectPaseoToolsCard serverId={host.serverId} /> <InjectPaseoToolsCard serverId={host.serverId} />
<AppendSystemPromptCard serverId={host.serverId} />
</SettingsSection> </SettingsSection>
{isLocalDaemon ? ( {isLocalDaemon ? (
<SettingsSection title="Pair devices"> <SettingsSection title="Pair devices">
@@ -544,9 +545,9 @@ function InjectPaseoToolsCard({ serverId }: { serverId: string }) {
<View style={settingsStyles.card} testID="host-page-inject-mcp-card"> <View style={settingsStyles.card} testID="host-page-inject-mcp-card">
<View style={settingsStyles.row}> <View style={settingsStyles.row}>
<View style={settingsStyles.rowContent}> <View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>Inject Paseo tools</Text> <Text style={settingsStyles.rowTitle}>Enable Paseo tools</Text>
<Text style={settingsStyles.rowHint}> <Text style={settingsStyles.rowHint}>
Automatically inject Paseo MCP tools into new agents Agents will be able to manage worktrees, agents and schedules
</Text> </Text>
</View> </View>
<Switch <Switch
@@ -559,6 +560,111 @@ function InjectPaseoToolsCard({ serverId }: { serverId: string }) {
); );
} }
function AppendSystemPromptCard({ serverId }: { serverId: string }) {
const isConnected = useHostRuntimeIsConnected(serverId);
const { config, patchConfig } = useDaemonConfig(serverId);
const persistedPrompt = config?.appendSystemPrompt ?? "";
const [draft, setDraft] = useState(persistedPrompt);
const [isEditing, setIsEditing] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const header = useMemo<SheetHeader>(() => ({ title: "Append system prompt" }), []);
useEffect(() => {
setDraft(persistedPrompt);
}, [persistedPrompt]);
const hasChanges = draft !== persistedPrompt;
const handleOpen = useCallback(() => {
setDraft(persistedPrompt);
setIsEditing(true);
}, [persistedPrompt]);
const handleClose = useCallback(() => {
if (isSaving) return;
setDraft(persistedPrompt);
setIsEditing(false);
}, [isSaving, persistedPrompt]);
const handleSave = useCallback(() => {
setIsSaving(true);
void patchConfig({ appendSystemPrompt: draft })
.then(() => {
setIsEditing(false);
return;
})
.catch((error) => {
console.error("[HostPage] Failed to save append system prompt", error);
})
.finally(() => setIsSaving(false));
}, [draft, patchConfig]);
const handleReset = useCallback(() => {
setDraft(persistedPrompt);
}, [persistedPrompt]);
if (!isConnected) return null;
return (
<>
<View style={settingsStyles.card} testID="host-page-append-system-prompt-card">
<View style={settingsStyles.row}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>System prompt</Text>
<Text style={settingsStyles.rowHint}>Added a system prompt to all agents</Text>
</View>
<Button
variant="outline"
size="sm"
onPress={handleOpen}
testID="host-page-append-system-prompt-edit"
>
Edit
</Button>
</View>
</View>
{isEditing ? (
<AdaptiveModalSheet
header={header}
visible
onClose={handleClose}
testID="host-page-append-system-prompt-sheet"
desktopMaxWidth={560}
>
<SettingsTextAreaCard
testID="host-page-append-system-prompt-input"
accessibilityLabel="Append system prompt"
value={draft}
onChangeText={setDraft}
placeholder="Always keep replies concise."
/>
<View style={styles.appendPromptActions}>
<Button
variant="ghost"
size="sm"
onPress={handleReset}
disabled={!hasChanges || isSaving}
testID="host-page-append-system-prompt-reset"
>
Reset
</Button>
<Button
variant="default"
size="sm"
onPress={handleSave}
disabled={!hasChanges || isSaving}
testID="host-page-append-system-prompt-save"
>
{isSaving ? "Saving..." : "Save"}
</Button>
</View>
</AdaptiveModalSheet>
) : null}
</>
);
}
function PairDeviceRow() { function PairDeviceRow() {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
@@ -631,6 +737,8 @@ function RemoveHostSection({ host, onRemoved }: { host: HostProfile; onRemoved?:
return ( return (
<SettingsSection title="Danger zone" testID="host-page-remove-host-card"> <SettingsSection title="Danger zone" testID="host-page-remove-host-card">
<RestartDaemonCard host={host} />
<View style={settingsStyles.card}> <View style={settingsStyles.card}>
<View style={settingsStyles.row}> <View style={settingsStyles.row}>
<View style={settingsStyles.rowContent}> <View style={settingsStyles.rowContent}>
@@ -755,6 +863,11 @@ const styles = StyleSheet.create((theme) => ({
gap: theme.spacing[2], gap: theme.spacing[2],
marginTop: theme.spacing[4], marginTop: theme.spacing[4],
}, },
appendPromptActions: {
flexDirection: "row",
justifyContent: "flex-end",
gap: theme.spacing[2],
},
emptyCard: { emptyCard: {
padding: theme.spacing[4], padding: theme.spacing[4],
alignItems: "center", alignItems: "center",
@@ -202,7 +202,12 @@ const disabledCodexEntry: ProviderSnapshotEntry = {
}; };
function makeConfig(providers: MutableDaemonConfig["providers"] = {}): MutableDaemonConfig { function makeConfig(providers: MutableDaemonConfig["providers"] = {}): MutableDaemonConfig {
return { mcp: { injectIntoAgents: false }, providers, autoArchiveAfterMerge: false }; return {
mcp: { injectIntoAgents: false },
providers,
autoArchiveAfterMerge: false,
appendSystemPrompt: "",
};
} }
function descendants(el: HTMLElement): HTMLElement[] { function descendants(el: HTMLElement): HTMLElement[] {
@@ -100,12 +100,15 @@ function expectArchivedAgentRecord(
class TestAgentClient implements AgentClient { class TestAgentClient implements AgentClient {
readonly provider = "codex" as const; readonly provider = "codex" as const;
readonly capabilities = TEST_CAPABILITIES; readonly capabilities = TEST_CAPABILITIES;
readonly createdConfigs: AgentSessionConfig[] = [];
readonly resumeOverrides: Array<Partial<AgentSessionConfig> | undefined> = [];
async isAvailable(): Promise<boolean> { async isAvailable(): Promise<boolean> {
return true; return true;
} }
async createSession(config: AgentSessionConfig): Promise<AgentSession> { async createSession(config: AgentSessionConfig): Promise<AgentSession> {
this.createdConfigs.push(config);
return new TestAgentSession(config); return new TestAgentSession(config);
} }
@@ -135,9 +138,11 @@ class TestAgentClient implements AgentClient {
config?: Partial<AgentSessionConfig>, config?: Partial<AgentSessionConfig>,
_launchContext?: AgentLaunchContext, _launchContext?: AgentLaunchContext,
): Promise<AgentSession> { ): Promise<AgentSession> {
this.resumeOverrides.push(config);
return new TestAgentSession({ return new TestAgentSession({
provider: "codex", provider: "codex",
cwd: config?.cwd ?? process.cwd(), cwd: config?.cwd ?? process.cwd(),
daemonAppendSystemPrompt: config?.daemonAppendSystemPrompt,
}); });
} }
} }
@@ -391,6 +396,62 @@ test("normalizeConfig strips legacy 'default' model id", async () => {
expect(snapshot.config.modeId).toBe("auto"); expect(snapshot.config.modeId).toBe("auto");
}); });
test("createAgent injects daemon append system prompt at runtime only", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
const storagePath = join(workdir, "agents");
const storage = new AgentStorage(storagePath, logger);
const client = new TestAgentClient();
const manager = new AgentManager({
clients: {
codex: client,
},
registry: storage,
logger,
appendSystemPrompt: " Daemon instructions. ",
idFactory: () => "00000000-0000-4000-8000-000000000103",
});
const snapshot = await manager.createAgent({
provider: "codex",
cwd: workdir,
systemPrompt: "Agent instructions.",
});
const record = await storage.get(snapshot.id);
expect(client.createdConfigs[0]?.systemPrompt).toBe("Agent instructions.");
expect(client.createdConfigs[0]?.daemonAppendSystemPrompt).toBe("Daemon instructions.");
expect(snapshot.config.daemonAppendSystemPrompt).toBe("Daemon instructions.");
expect(record?.config?.systemPrompt).toBe("Agent instructions.");
expect(record?.config).not.toHaveProperty("daemonAppendSystemPrompt");
});
test("daemon append system prompt is not injected into Pi", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
const storagePath = join(workdir, "agents");
const storage = new AgentStorage(storagePath, logger);
const client = new TestAgentClient();
const manager = new AgentManager({
clients: {
pi: client as unknown as AgentClient,
},
providerDefinitions: {
pi: { enabled: true },
},
registry: storage,
logger,
appendSystemPrompt: "Daemon instructions.",
idFactory: () => "00000000-0000-4000-8000-000000000104",
});
await manager.createAgent({
provider: "pi",
cwd: workdir,
systemPrompt: "Agent instructions.",
});
expect(client.createdConfigs[0]?.daemonAppendSystemPrompt).toBeUndefined();
});
test("setAgentMode persists the selected mode across session reload", async () => { test("setAgentMode persists the selected mode across session reload", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
const storagePath = join(workdir, "agents"); const storagePath = join(workdir, "agents");
@@ -173,6 +173,7 @@ export interface AgentManagerOptions {
durableTimelineStore?: AgentTimelineStore; durableTimelineStore?: AgentTimelineStore;
terminalManager?: TerminalManager | null; terminalManager?: TerminalManager | null;
mcpBaseUrl?: string; mcpBaseUrl?: string;
appendSystemPrompt?: string;
agentStreamCoalesceWindowMs?: number; agentStreamCoalesceWindowMs?: number;
rescueTimeouts?: AgentManagerRescueTimeouts; rescueTimeouts?: AgentManagerRescueTimeouts;
logger: Logger; logger: Logger;
@@ -438,6 +439,7 @@ export class AgentManager {
private readonly backgroundTasks = new Set<Promise<void>>(); private readonly backgroundTasks = new Set<Promise<void>>();
private readonly agentStreamCoalescer: AgentStreamCoalescer; private readonly agentStreamCoalescer: AgentStreamCoalescer;
private mcpBaseUrl: string | null; private mcpBaseUrl: string | null;
private appendSystemPrompt: string;
private onAgentAttention?: AgentAttentionCallback; private onAgentAttention?: AgentAttentionCallback;
private logger: Logger; private logger: Logger;
private readonly rescueTimeouts: Required<AgentManagerRescueTimeouts>; private readonly rescueTimeouts: Required<AgentManagerRescueTimeouts>;
@@ -448,6 +450,7 @@ export class AgentManager {
this.durableTimelineStore = options?.durableTimelineStore; this.durableTimelineStore = options?.durableTimelineStore;
this.onAgentAttention = options?.onAgentAttention; this.onAgentAttention = options?.onAgentAttention;
this.mcpBaseUrl = options?.mcpBaseUrl ?? null; this.mcpBaseUrl = options?.mcpBaseUrl ?? null;
this.appendSystemPrompt = options.appendSystemPrompt ?? "";
this.logger = options.logger.child({ module: "agent", component: "agent-manager" }); this.logger = options.logger.child({ module: "agent", component: "agent-manager" });
this.rescueTimeouts = { this.rescueTimeouts = {
reloadSessionCloseMs: reloadSessionCloseMs:
@@ -502,6 +505,10 @@ export class AgentManager {
this.mcpBaseUrl = url; this.mcpBaseUrl = url;
} }
setAppendSystemPrompt(prompt: string | null | undefined): void {
this.appendSystemPrompt = prompt ?? "";
}
public getMetricsSnapshot(): AgentMetricsSnapshot { public getMetricsSnapshot(): AgentMetricsSnapshot {
const byLifecycle: Record<string, number> = {}; const byLifecycle: Record<string, number> = {};
let withActiveForegroundTurn = 0; let withActiveForegroundTurn = 0;
@@ -816,7 +823,9 @@ export class AgentManager {
}, },
}; };
this.requireEnabledProvider(injectedConfig.provider); this.requireEnabledProvider(injectedConfig.provider);
const normalizedConfig = await this.normalizeConfig(injectedConfig); const normalizedConfig = this.applyDaemonAppendSystemPrompt(
await this.normalizeConfig(injectedConfig),
);
const launchContext = this.buildLaunchContext(resolvedAgentId); const launchContext = this.buildLaunchContext(resolvedAgentId);
const client = await this.requireAvailableClient({ const client = await this.requireAvailableClient({
provider: normalizedConfig.provider, provider: normalizedConfig.provider,
@@ -860,7 +869,9 @@ export class AgentManager {
...overrides, ...overrides,
provider: handle.provider, provider: handle.provider,
} as AgentSessionConfig; } as AgentSessionConfig;
const normalizedConfig = await this.normalizeConfig(mergedConfig); const normalizedConfig = this.applyDaemonAppendSystemPrompt(
await this.normalizeConfig(mergedConfig),
);
const resumeOverrides: Partial<AgentSessionConfig> = { ...overrides }; const resumeOverrides: Partial<AgentSessionConfig> = { ...overrides };
let hasResumeOverrides = overrides !== undefined; let hasResumeOverrides = overrides !== undefined;
@@ -874,6 +885,11 @@ export class AgentManager {
hasResumeOverrides = true; hasResumeOverrides = true;
} }
if (metadata.daemonAppendSystemPrompt !== normalizedConfig.daemonAppendSystemPrompt) {
resumeOverrides.daemonAppendSystemPrompt = normalizedConfig.daemonAppendSystemPrompt;
hasResumeOverrides = true;
}
const launchContext = this.buildLaunchContext(resolvedAgentId); const launchContext = this.buildLaunchContext(resolvedAgentId);
const client = this.requireClient(handle.provider); const client = this.requireClient(handle.provider);
const available = await client.isAvailable(); const available = await client.isAvailable();
@@ -919,7 +935,9 @@ export class AgentManager {
...overrides, ...overrides,
provider, provider,
} as AgentSessionConfig; } as AgentSessionConfig;
const normalizedConfig = await this.normalizeConfig(refreshConfig); const normalizedConfig = this.applyDaemonAppendSystemPrompt(
await this.normalizeConfig(refreshConfig),
);
const launchContext = this.buildLaunchContext(agentId); const launchContext = this.buildLaunchContext(agentId);
const session = handle const session = handle
@@ -3437,6 +3455,26 @@ export class AgentManager {
return normalized; return normalized;
} }
private applyDaemonAppendSystemPrompt(config: AgentSessionConfig): AgentSessionConfig {
if (config.provider === "pi") {
const next = { ...config };
delete next.daemonAppendSystemPrompt;
return next;
}
const trimmed = this.appendSystemPrompt.trim();
if (!trimmed) {
const next = { ...config };
delete next.daemonAppendSystemPrompt;
return next;
}
return {
...config,
daemonAppendSystemPrompt: trimmed,
};
}
private buildLaunchContext(agentId: string): AgentLaunchContext { private buildLaunchContext(agentId: string): AgentLaunchContext {
return { return {
agentId, agentId,
@@ -473,6 +473,11 @@ export interface AgentSessionConfig {
* Mapped by each provider to its native instruction field. * Mapped by each provider to its native instruction field.
*/ */
systemPrompt?: string; systemPrompt?: string;
/**
* Daemon-level instructions appended at runtime. This is deliberately not
* persisted into agent config so daemon setting changes apply cleanly.
*/
daemonAppendSystemPrompt?: string;
modeId?: string; modeId?: string;
model?: string; model?: string;
thinkingOptionId?: string; thinkingOptionId?: string;
@@ -1,50 +0,0 @@
/**
* Returns orchestrator mode instructions to append to the system prompt.
* These instructions are from CLAUDE.md and guide agents on how to work
* effectively in this repository.
*/
export function getOrchestratorModeInstructions(): string {
return `
<orchestrator-mode>
Activation:
- Only activate if the user explicitly says "go into orchestrator mode" (or similar).
- Otherwise, do work directly yourself; do not spawn agents.
Core rules:
- In orchestrator mode, you accomplish tasks only by managing agents; do not perform the work yourself.
- Always prefix agent titles (e.g., "🎭 Feature Implementation", "🎭 Design Discussion").
- Set cwd to the repository root and choose the most permissive mode available.
- If an agent control tool call fails, list agents before launching another; it may just be a wait timeout.
Context management:
- Reuse an existing agent when the next step needs the same context (same files/module/folder or immediate follow-up like investigate → fix in the same area).
- Start a new agent when switching to a different area/module, or when an agent has run long and its context feels stale.
- Prefer sending follow-up prompts to an existing agent to avoid reloading context.
- Use multiple agents when roles diverge (e.g., one for refactor, one for external validation), but default to reuse when context overlaps.
Conversation with agents:
- Engage actively: ask pointed questions, probe risks, and request clarifications before accepting proposals.
- Encourage agents to validate assumptions, consider edge cases, and describe how they will test/verify.
Agent selection guidance:
- Codex: methodical and slower; great for deep debugging, tracing code paths, refactoring, complex features, and design discussions.
- Claude: fast; strong at tool use, agentic control, and managing other agents; may jump to conclusions—ask it to verify.
Clarifying ambiguous requests:
- Research first to understand the current state.
- Ask clarifying questions about what the user wants.
- Present options with trade-offs.
- Get explicit confirmation; never assume.
Investigation vs Implementation:
- Investigate only unless explicitly asked to implement.
- Report findings clearly.
- After investigation, ask for direction before implementing.
Tool usage discipline:
- Do not ask users to run commands—run them yourself.
- Do not repeat the user’s instructions verbatim—summarize them in your own words.
- Be explicit about results—tell the user what happened after every command.
</orchestrator-mode>
`;
}
@@ -81,7 +81,7 @@ import {
import { findExecutable, isCommandAvailable } from "../../../../utils/executable.js"; import { findExecutable, isCommandAvailable } from "../../../../utils/executable.js";
import { withTimeout } from "../../../../utils/promise-timeout.js"; import { withTimeout } from "../../../../utils/promise-timeout.js";
import { execCommand } from "../../../../utils/spawn.js"; import { execCommand } from "../../../../utils/spawn.js";
import { getOrchestratorModeInstructions } from "../../orchestrator-instructions.js"; import { composeSystemPromptParts } from "../../system-prompt.js";
const fsPromises = promises; const fsPromises = promises;
const CLAUDE_SETTING_SOURCES: NonNullable<ClaudeOptions["settingSources"]> = [ const CLAUDE_SETTING_SOURCES: NonNullable<ClaudeOptions["settingSources"]> = [
@@ -2314,9 +2314,9 @@ class ClaudeAgentSession implements AgentSession {
} }
private buildAppendedSystemPrompt(): string { private buildAppendedSystemPrompt(): string {
return [getOrchestratorModeInstructions(), this.config.systemPrompt?.trim()] return (
.filter((entry): entry is string => typeof entry === "string" && entry.length > 0) composeSystemPromptParts(this.config.systemPrompt, this.config.daemonAppendSystemPrompt) ?? ""
.join("\n\n"); );
} }
private buildSdkEnv(extraClaudeOptions: Partial<ClaudeOptions> | undefined): NodeJS.ProcessEnv { private buildSdkEnv(extraClaudeOptions: Partial<ClaudeOptions> | undefined): NodeJS.ProcessEnv {
@@ -2365,7 +2365,7 @@ class ClaudeAgentSession implements AgentSession {
canUseTool: this.handlePermissionRequest, canUseTool: this.handlePermissionRequest,
pathToClaudeCodeExecutable: claudeBinary, pathToClaudeCodeExecutable: claudeBinary,
// Use Claude Code preset system prompt and load CLAUDE.md files // Use Claude Code preset system prompt and load CLAUDE.md files
// Append provider-agnostic system prompt and orchestrator instructions for agents. // Append provider-agnostic system prompts for agents.
systemPrompt: { systemPrompt: {
type: "preset", type: "preset",
preset: "claude_code", preset: "claude_code",
@@ -41,6 +41,7 @@ import os from "node:os";
import path from "node:path"; import path from "node:path";
import { z } from "zod"; import { z } from "zod";
import { renderPromptAttachmentAsText } from "../prompt-attachments.js"; import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
import { composeSystemPromptParts } from "../system-prompt.js";
import { curateAgentActivity } from "../activity-curator.js"; import { curateAgentActivity } from "../activity-curator.js";
import { import {
mapCodexRolloutToolCall, mapCodexRolloutToolCall,
@@ -3026,12 +3027,11 @@ class CodexAppServerAgentSession implements AgentSession {
const settings: Record<string, unknown> = {}; const settings: Record<string, unknown> = {};
if (match.model) settings.model = match.model; if (match.model) settings.model = match.model;
if (match.reasoning_effort) settings.reasoning_effort = match.reasoning_effort; if (match.reasoning_effort) settings.reasoning_effort = match.reasoning_effort;
const developerInstructions = [ const developerInstructions = composeSystemPromptParts(
match.developer_instructions?.trim(), match.developer_instructions,
this.config.systemPrompt?.trim(), this.config.systemPrompt,
] this.config.daemonAppendSystemPrompt,
.filter((entry): entry is string => typeof entry === "string" && entry.length > 0) );
.join("\n\n");
if (developerInstructions) settings.developer_instructions = developerInstructions; if (developerInstructions) settings.developer_instructions = developerInstructions;
if (this.config.model) settings.model = this.config.model; if (this.config.model) settings.model = this.config.model;
const thinkingOptionId = normalizeCodexThinkingOptionId(this.config.thinkingOptionId); const thinkingOptionId = normalizeCodexThinkingOptionId(this.config.thinkingOptionId);
@@ -3161,8 +3161,12 @@ class CodexAppServerAgentSession implements AgentSession {
return; return;
} }
const params: Record<string, unknown> = { threadId: this.currentThreadId }; const params: Record<string, unknown> = { threadId: this.currentThreadId };
if (this.config.systemPrompt?.trim()) { const developerInstructions = composeSystemPromptParts(
params.developerInstructions = this.config.systemPrompt.trim(); this.config.systemPrompt,
this.config.daemonAppendSystemPrompt,
);
if (developerInstructions) {
params.developerInstructions = developerInstructions;
} }
const codexConfig = this.buildCodexInnerConfig(); const codexConfig = this.buildCodexInnerConfig();
if (codexConfig) { if (codexConfig) {
@@ -3257,6 +3261,7 @@ class CodexAppServerAgentSession implements AgentSession {
approvalPolicy: string; approvalPolicy: string;
sandboxPolicyType: string; sandboxPolicyType: string;
hasOutputSchema: boolean; hasOutputSchema: boolean;
hasDeveloperInstructions: boolean;
hasCodexConfig: boolean; hasCodexConfig: boolean;
}> { }> {
const input = await this.buildUserInput(prompt); const input = await this.buildUserInput(prompt);
@@ -3299,8 +3304,12 @@ class CodexAppServerAgentSession implements AgentSession {
if (options?.outputSchema) { if (options?.outputSchema) {
params.outputSchema = normalizeCodexOutputSchema(options.outputSchema); params.outputSchema = normalizeCodexOutputSchema(options.outputSchema);
} }
if (this.config.systemPrompt?.trim()) { const developerInstructions = composeSystemPromptParts(
params.developerInstructions = this.config.systemPrompt.trim(); this.config.systemPrompt,
this.config.daemonAppendSystemPrompt,
);
if (developerInstructions) {
params.developerInstructions = developerInstructions;
} }
const codexConfig = this.buildCodexInnerConfig(); const codexConfig = this.buildCodexInnerConfig();
if (codexConfig) { if (codexConfig) {
@@ -3313,6 +3322,7 @@ class CodexAppServerAgentSession implements AgentSession {
approvalPolicy, approvalPolicy,
sandboxPolicyType, sandboxPolicyType,
hasOutputSchema: Boolean(options?.outputSchema), hasOutputSchema: Boolean(options?.outputSchema),
hasDeveloperInstructions: Boolean(developerInstructions),
hasCodexConfig: Boolean(codexConfig), hasCodexConfig: Boolean(codexConfig),
}; };
} }
@@ -3323,6 +3333,7 @@ class CodexAppServerAgentSession implements AgentSession {
approvalPolicy, approvalPolicy,
sandboxPolicyType, sandboxPolicyType,
hasOutputSchema, hasOutputSchema,
hasDeveloperInstructions,
hasCodexConfig, hasCodexConfig,
}: { }: {
turnId: string; turnId: string;
@@ -3330,6 +3341,7 @@ class CodexAppServerAgentSession implements AgentSession {
approvalPolicy: string; approvalPolicy: string;
sandboxPolicyType: string; sandboxPolicyType: string;
hasOutputSchema: boolean; hasOutputSchema: boolean;
hasDeveloperInstructions: boolean;
hasCodexConfig: boolean; hasCodexConfig: boolean;
}): void { }): void {
this.logger.info( this.logger.info(
@@ -3345,7 +3357,7 @@ class CodexAppServerAgentSession implements AgentSession {
sandboxPolicyType, sandboxPolicyType,
hasCollaborationMode: Boolean(this.resolvedCollaborationMode), hasCollaborationMode: Boolean(this.resolvedCollaborationMode),
hasOutputSchema, hasOutputSchema,
hasDeveloperInstructions: Boolean(this.config.systemPrompt?.trim()), hasDeveloperInstructions,
hasCodexConfig, hasCodexConfig,
}, },
"Starting Codex app-server turn", "Starting Codex app-server turn",
@@ -3407,6 +3419,7 @@ class CodexAppServerAgentSession implements AgentSession {
approvalPolicy: turnStart.approvalPolicy, approvalPolicy: turnStart.approvalPolicy,
sandboxPolicyType: turnStart.sandboxPolicyType, sandboxPolicyType: turnStart.sandboxPolicyType,
hasOutputSchema: turnStart.hasOutputSchema, hasOutputSchema: turnStart.hasOutputSchema,
hasDeveloperInstructions: turnStart.hasDeveloperInstructions,
hasCodexConfig: turnStart.hasCodexConfig, hasCodexConfig: turnStart.hasCodexConfig,
}); });
await this.client.request("turn/start", turnStart.params, TURN_START_TIMEOUT_MS); await this.client.request("turn/start", turnStart.params, TURN_START_TIMEOUT_MS);
@@ -3952,14 +3965,16 @@ class CodexAppServerAgentSession implements AgentSession {
const approvalPolicy = this.config.approvalPolicy ?? preset.approvalPolicy; const approvalPolicy = this.config.approvalPolicy ?? preset.approvalPolicy;
const sandbox = this.config.sandboxMode ?? preset.sandbox; const sandbox = this.config.sandboxMode ?? preset.sandbox;
const innerConfig = this.buildCodexInnerConfig(); const innerConfig = this.buildCodexInnerConfig();
const developerInstructions = composeSystemPromptParts(
this.config.systemPrompt,
this.config.daemonAppendSystemPrompt,
);
const params: Record<string, unknown> = { const params: Record<string, unknown> = {
model, model,
cwd: this.config.cwd ?? null, cwd: this.config.cwd ?? null,
approvalPolicy, approvalPolicy,
sandbox, sandbox,
...(this.config.systemPrompt?.trim() ...(developerInstructions ? { developerInstructions } : {}),
? { developerInstructions: this.config.systemPrompt.trim() }
: {}),
...(innerConfig ? { config: innerConfig } : {}), ...(innerConfig ? { config: innerConfig } : {}),
...(this.ephemeral ? { ephemeral: true } : {}), ...(this.ephemeral ? { ephemeral: true } : {}),
}; };
@@ -60,6 +60,7 @@ import {
} from "./diagnostic-utils.js"; } from "./diagnostic-utils.js";
import { runProviderTurn } from "./provider-runner.js"; import { runProviderTurn } from "./provider-runner.js";
import { renderPromptAttachmentAsText } from "../prompt-attachments.js"; import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
import { composeSystemPromptParts } from "../system-prompt.js";
import { import {
createSdkOpenCodeClient, createSdkOpenCodeClient,
type OpenCodeRuntime, type OpenCodeRuntime,
@@ -2589,6 +2590,10 @@ class OpenCodeAgentSession implements AgentSession {
partTypes: parts.map((p) => p.type), partTypes: parts.map((p) => p.type),
}); });
try { try {
const systemPrompt = composeSystemPromptParts(
this.config.systemPrompt,
this.config.daemonAppendSystemPrompt,
);
const promptResponse = await this.client.session.promptAsync({ const promptResponse = await this.client.session.promptAsync({
sessionID: this.sessionId, sessionID: this.sessionId,
directory: this.config.cwd, directory: this.config.cwd,
@@ -2601,7 +2606,7 @@ class OpenCodeAgentSession implements AgentSession {
}, },
} }
: {}), : {}),
...(this.config.systemPrompt ? { system: this.config.systemPrompt } : {}), ...(systemPrompt ? { system: systemPrompt } : {}),
...(model ? { model } : {}), ...(model ? { model } : {}),
...(effectiveMode ? { agent: effectiveMode } : {}), ...(effectiveMode ? { agent: effectiveMode } : {}),
...(effectiveVariant ? { variant: effectiveVariant } : {}), ...(effectiveVariant ? { variant: effectiveVariant } : {}),
@@ -0,0 +1,10 @@
export function composeSystemPromptParts(
...parts: Array<string | null | undefined>
): string | undefined {
const prompt = parts
.map((part) => part?.trim())
.filter((part): part is string => typeof part === "string" && part.length > 0)
.join("\n\n");
return prompt.length > 0 ? prompt : undefined;
}
+6
View File
@@ -230,6 +230,7 @@ export interface PaseoDaemonConfig {
mcpEnabled?: boolean; mcpEnabled?: boolean;
mcpInjectIntoAgents?: boolean; mcpInjectIntoAgents?: boolean;
autoArchiveAfterMerge?: boolean; autoArchiveAfterMerge?: boolean;
appendSystemPrompt?: string;
staticDir: string; staticDir: string;
mcpDebug: boolean; mcpDebug: boolean;
isDev?: boolean; isDev?: boolean;
@@ -290,6 +291,7 @@ export async function createPaseoDaemon(
]), ]),
), ),
autoArchiveAfterMerge: config.autoArchiveAfterMerge ?? false, autoArchiveAfterMerge: config.autoArchiveAfterMerge ?? false,
appendSystemPrompt: config.appendSystemPrompt ?? "",
}, },
logger, logger,
); );
@@ -516,6 +518,7 @@ export async function createPaseoDaemon(
}, },
providerDefinitions: providerRegistry, providerDefinitions: providerRegistry,
registry: agentStorage, registry: agentStorage,
appendSystemPrompt: config.appendSystemPrompt,
logger, logger,
}); });
@@ -862,6 +865,9 @@ export async function createPaseoDaemon(
daemonConfigStore.onFieldChange("mcp.injectIntoAgents", (value) => { daemonConfigStore.onFieldChange("mcp.injectIntoAgents", (value) => {
agentManager.setMcpBaseUrl(value ? mcpBaseUrl : null); agentManager.setMcpBaseUrl(value ? mcpBaseUrl : null);
}); });
daemonConfigStore.onFieldChange("appendSystemPrompt", (value) => {
agentManager.setAppendSystemPrompt(typeof value === "string" ? value : "");
});
const relayEnabled = config.relayEnabled ?? true; const relayEnabled = config.relayEnabled ?? true;
const relayEndpoint = config.relayEndpoint ?? "relay.paseo.sh:443"; const relayEndpoint = config.relayEndpoint ?? "relay.paseo.sh:443";
const relayPublicEndpoint = config.relayPublicEndpoint ?? relayEndpoint; const relayPublicEndpoint = config.relayPublicEndpoint ?? relayEndpoint;
+14 -2
View File
@@ -256,6 +256,10 @@ function resolveAuthConfig(
: undefined; : undefined;
} }
function resolveAppendSystemPrompt(persisted: ReturnType<typeof loadPersistedConfig>): string {
return persisted.daemon?.appendSystemPrompt ?? "";
}
function resolveStaticLoadConfigSettings( function resolveStaticLoadConfigSettings(
env: NodeJS.ProcessEnv, env: NodeJS.ProcessEnv,
cli: CliConfigOverrides | undefined, cli: CliConfigOverrides | undefined,
@@ -266,6 +270,7 @@ function resolveStaticLoadConfigSettings(
mcpInjectIntoAgents: mcpInjectIntoAgents:
cli?.mcpInjectIntoAgents ?? persisted.daemon?.mcp?.injectIntoAgents ?? false, cli?.mcpInjectIntoAgents ?? persisted.daemon?.mcp?.injectIntoAgents ?? false,
autoArchiveAfterMerge: persisted.daemon?.autoArchiveAfterMerge ?? false, autoArchiveAfterMerge: persisted.daemon?.autoArchiveAfterMerge ?? false,
appendSystemPrompt: resolveAppendSystemPrompt(persisted),
hostnames: mergeHostnames([ hostnames: mergeHostnames([
persisted.daemon?.hostnames, persisted.daemon?.hostnames,
parseHostnamesEnv(env.PASEO_HOSTNAMES ?? env.PASEO_ALLOWED_HOSTS), parseHostnamesEnv(env.PASEO_HOSTNAMES ?? env.PASEO_ALLOWED_HOSTS),
@@ -286,8 +291,14 @@ export function loadConfig(
const persisted = loadPersistedConfig(paseoHome); const persisted = loadPersistedConfig(paseoHome);
const listen = resolveListenAddress(env, options?.cli, persisted); const listen = resolveListenAddress(env, options?.cli, persisted);
const { mcpEnabled, mcpInjectIntoAgents, autoArchiveAfterMerge, hostnames, appBaseUrl } = const {
resolveStaticLoadConfigSettings(env, options?.cli, persisted); mcpEnabled,
mcpInjectIntoAgents,
autoArchiveAfterMerge,
appendSystemPrompt,
hostnames,
appBaseUrl,
} = resolveStaticLoadConfigSettings(env, options?.cli, persisted);
const relay = resolveRelayConfig({ const relay = resolveRelayConfig({
env, env,
@@ -315,6 +326,7 @@ export function loadConfig(
mcpEnabled, mcpEnabled,
mcpInjectIntoAgents, mcpInjectIntoAgents,
autoArchiveAfterMerge, autoArchiveAfterMerge,
appendSystemPrompt,
mcpDebug: env.MCP_DEBUG === "1", mcpDebug: env.MCP_DEBUG === "1",
isDev: resolvePaseoNodeEnv(env) === "development", isDev: resolvePaseoNodeEnv(env) === "development",
agentStoragePath: path.join(paseoHome, "agents"), agentStoragePath: path.join(paseoHome, "agents"),
@@ -107,6 +107,28 @@ describe("DaemonConfigStore", () => {
}); });
}); });
test("patch persists append system prompt into config.json", () => {
const paseoHome = mkdtempSync(path.join(tmpdir(), "paseo-daemon-config-store-"));
tempDirs.push(paseoHome);
const store = new DaemonConfigStore(
paseoHome,
{
mcp: { injectIntoAgents: false },
providers: {},
appendSystemPrompt: "",
},
undefined,
);
store.patch({
appendSystemPrompt: "Prefer terse replies.",
});
const persisted = loadPersistedConfig(paseoHome);
expect(persisted.daemon?.appendSystemPrompt).toBe("Prefer terse replies.");
});
test("patch persists provider additional models into config.json", () => { test("patch persists provider additional models into config.json", () => {
const paseoHome = mkdtempSync(path.join(tmpdir(), "paseo-daemon-config-store-")); const paseoHome = mkdtempSync(path.join(tmpdir(), "paseo-daemon-config-store-"));
tempDirs.push(paseoHome); tempDirs.push(paseoHome);
@@ -186,6 +186,7 @@ function mergeMutableConfigIntoPersistedConfig(params: {
injectIntoAgents: mutable.mcp.injectIntoAgents, injectIntoAgents: mutable.mcp.injectIntoAgents,
}, },
autoArchiveAfterMerge: mutable.autoArchiveAfterMerge, autoArchiveAfterMerge: mutable.autoArchiveAfterMerge,
appendSystemPrompt: mutable.appendSystemPrompt,
}, },
agents: agents:
providerOverrides && Object.keys(providerOverrides).length > 0 providerOverrides && Object.keys(providerOverrides).length > 0
@@ -34,6 +34,18 @@ describe("PersistedConfigSchema daemon auth config", () => {
}); });
}); });
describe("PersistedConfigSchema daemon append system prompt config", () => {
test("accepts optional append system prompt", () => {
const parsed = PersistedConfigSchema.parse({
daemon: {
appendSystemPrompt: "Prefer terse replies.",
},
});
expect(parsed.daemon?.appendSystemPrompt).toBe("Prefer terse replies.");
});
});
describe("PersistedConfigSchema daemon relay config", () => { describe("PersistedConfigSchema daemon relay config", () => {
test("accepts optional relay TLS setting", () => { test("accepts optional relay TLS setting", () => {
const parsed = PersistedConfigSchema.parse({ const parsed = PersistedConfigSchema.parse({
@@ -251,6 +251,7 @@ export const PersistedConfigSchema = z
.passthrough() .passthrough()
.optional(), .optional(),
autoArchiveAfterMerge: z.boolean().optional(), autoArchiveAfterMerge: z.boolean().optional(),
appendSystemPrompt: z.string().optional(),
cors: z cors: z
.object({ .object({
allowedOrigins: z.array(z.string()).optional(), allowedOrigins: z.array(z.string()).optional(),
+2
View File
@@ -114,6 +114,7 @@ export const MutableDaemonConfigSchema = z
.passthrough(), .passthrough(),
providers: z.record(z.string(), MutableDaemonProviderConfigSchema).default({}), providers: z.record(z.string(), MutableDaemonProviderConfigSchema).default({}),
autoArchiveAfterMerge: z.boolean().default(false), autoArchiveAfterMerge: z.boolean().default(false),
appendSystemPrompt: z.string().default(""),
}) })
.passthrough(); .passthrough();
@@ -124,6 +125,7 @@ export const MutableDaemonConfigPatchSchema = z
.record(z.string(), MutableDaemonProviderConfigSchema.partial().passthrough()) .record(z.string(), MutableDaemonProviderConfigSchema.partial().passthrough())
.optional(), .optional(),
autoArchiveAfterMerge: z.boolean().optional(), autoArchiveAfterMerge: z.boolean().optional(),
appendSystemPrompt: z.string().optional(),
}) })
.partial() .partial()
.passthrough(); .passthrough();
@@ -54,6 +54,12 @@
}, },
"additionalProperties": true "additionalProperties": true
}, },
"autoArchiveAfterMerge": {
"type": "boolean"
},
"appendSystemPrompt": {
"type": "string"
},
"cors": { "cors": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -77,6 +83,22 @@
}, },
"publicEndpoint": { "publicEndpoint": {
"type": "string" "type": "string"
},
"useTls": {
"type": "boolean"
},
"publicUseTls": {
"type": "boolean"
}
},
"additionalProperties": false
},
"auth": {
"type": "object",
"properties": {
"password": {
"type": "string",
"pattern": "^\\$2[aby]\\$\\d{2}\\$[./A-Za-z0-9]{53}$"
} }
}, },
"additionalProperties": false "additionalProperties": false
@@ -248,6 +270,10 @@
"type": "string", "type": "string",
"minLength": 1 "minLength": 1
}, },
"language": {
"type": "string",
"minLength": 1
},
"confidenceThreshold": { "confidenceThreshold": {
"type": "number" "type": "number"
} }
@@ -285,6 +311,10 @@
"model": { "model": {
"type": "string", "type": "string",
"minLength": 1 "minLength": 1
},
"language": {
"type": "string",
"minLength": 1
} }
}, },
"additionalProperties": false "additionalProperties": false