From 99b875d2b9fbf938f053ac0cb18aedd0ec561f64 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 1 May 2026 00:18:53 +0000
Subject: [PATCH 1/8] feat: use SystemMessageConfig with SectionOverride for
worker prompts
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Refactor worker prompt construction from string concatenation in
BuildWorkerPrompt() to use the SDK's SystemMessageConfig with Customize
mode and SectionOverride. This places worker-specific content in the
appropriate system prompt sections instead of the user message:
- Identity/charter → SystemPromptSections.Identity (Append)
- Tool honesty policy → SystemPromptSections.ToolEfficiency (Append)
- Worktree note → SystemPromptSections.EnvironmentContext (Append)
- Shared context → SystemPromptSections.CustomInstructions (Append)
Key changes:
- Add BuildWorkerSystemMessageSections() to construct section overrides
- Add systemMessageSections parameter to CreateSessionAsync
- Update BuildFreshSessionConfig to detect workers and add sections
- Simplify BuildWorkerPrompt to only contain task-specific content
- Update group creation flows to pass sections at session creation time
Fixes #496
Co-authored-by: copilot-agentic-workflow[bot] <224017+copilot-agentic-workflow[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../WorkerSystemMessageTests.cs | 52 +++++++
PolyPilot.Tests/WorkerToolHonestyTests.cs | 118 +++++++++++++--
.../Services/CopilotService.Organization.cs | 137 ++++++++++++------
PolyPilot/Services/CopilotService.cs | 73 ++++++++--
4 files changed, 313 insertions(+), 67 deletions(-)
create mode 100644 PolyPilot.IntegrationTests/WorkerSystemMessageTests.cs
diff --git a/PolyPilot.IntegrationTests/WorkerSystemMessageTests.cs b/PolyPilot.IntegrationTests/WorkerSystemMessageTests.cs
new file mode 100644
index 0000000000..ce351213c2
--- /dev/null
+++ b/PolyPilot.IntegrationTests/WorkerSystemMessageTests.cs
@@ -0,0 +1,52 @@
+using PolyPilot.IntegrationTests.Fixtures;
+
+namespace PolyPilot.IntegrationTests;
+
+///
+/// Integration tests verifying that multi-agent worker sessions use
+/// SystemMessageConfig with section overrides for identity, tool policy,
+/// worktree, and shared context (issue #496).
+///
+[Collection("PolyPilot")]
+[Trait("Category", "WorkerSystemMessage")]
+public class WorkerSystemMessageTests : IntegrationTestBase
+{
+ public WorkerSystemMessageTests(AppFixture app, ITestOutputHelper output)
+ : base(app, output) { }
+
+ [Fact]
+ public async Task Dashboard_ShowsMultiAgentGroupCreation()
+ {
+ await WaitForCdpReadyAsync();
+
+ // Verify the dashboard is accessible — the multi-agent group creation
+ // UI is on the dashboard. If the dashboard loads, the underlying
+ // CreateMultiAgentGroupAsync (which now passes worker system message
+ // sections) is available.
+ var dashboardExists = await WaitForAsync("#dashboard-page", TimeSpan.FromSeconds(10));
+ if (!dashboardExists)
+ {
+ // Try navigating to dashboard
+ await NavigateToAsync("Dashboard", "#dashboard-page");
+ dashboardExists = await ExistsAsync("#dashboard-page");
+ }
+
+ // Even if dashboard element ID isn't present, the app should be responsive
+ var bodyText = await GetTextAsync("body");
+ Assert.False(string.IsNullOrWhiteSpace(bodyText), "App body should have content");
+ Output.WriteLine($"Dashboard content preview: {bodyText[..Math.Min(bodyText.Length, 200)]}");
+
+ await ScreenshotAsync("dashboard-worker-system-message");
+ }
+
+ [Fact]
+ public async Task App_RespondsToApiStatus()
+ {
+ // Verify the app is running and responsive — this confirms that the
+ // refactored CreateSessionAsync (with systemMessageSections parameter)
+ // didn't break app initialization.
+ var status = await GetJsonAsync("/api/status");
+ Assert.True(status.TryGetProperty("agentReady", out var ready));
+ Output.WriteLine($"Agent ready: {ready}");
+ }
+}
diff --git a/PolyPilot.Tests/WorkerToolHonestyTests.cs b/PolyPilot.Tests/WorkerToolHonestyTests.cs
index 5d5314592a..b6949bccc9 100644
--- a/PolyPilot.Tests/WorkerToolHonestyTests.cs
+++ b/PolyPilot.Tests/WorkerToolHonestyTests.cs
@@ -1,3 +1,4 @@
+using GitHub.Copilot.SDK;
using Microsoft.Extensions.DependencyInjection;
using PolyPilot.Models;
using PolyPilot.Services;
@@ -19,26 +20,115 @@ private CopilotService CreateService()
new RepoManager(), services.BuildServiceProvider(), new StubDemoService());
}
- #region Worker Prompt Tool-Honesty Instructions
+ #region Worker Prompt — Task-Only Content
[Fact]
- public void WorkerPrompt_ContainsToolHonestyInstructions()
+ public void WorkerPrompt_ContainsOnlyTaskContent()
{
- var svc = CreateService();
- var method = typeof(CopilotService).GetMethod("BuildWorkerPrompt",
- System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
- Assert.NotNull(method);
+ var workerPrompt = CopilotService.BuildWorkerPrompt("Fix the tests", "Run the unit tests");
+
+ Assert.Contains("Original User Request", workerPrompt);
+ Assert.Contains("Fix the tests", workerPrompt);
+ Assert.Contains("Your Assigned Task", workerPrompt);
+ Assert.Contains("Run the unit tests", workerPrompt);
+ // System-level content is now in sections, not the user prompt
+ Assert.DoesNotContain("CRITICAL: Tool Usage & Honesty Policy", workerPrompt);
+ Assert.DoesNotContain("NEVER fabricate", workerPrompt);
+ }
- var workerPrompt = (string)method!.Invoke(null, new object[] {
+ #endregion
+
+ #region Worker System Message Sections — Tool Honesty
+
+ [Fact]
+ public void WorkerSystemMessageSections_ContainsToolHonestyInstructions()
+ {
+ var sections = CopilotService.BuildWorkerSystemMessageSections(
"You are a worker agent. Complete the following task thoroughly.",
- "", "", "Fix the tests", "Run the unit tests"
- })!;
+ worktreeNote: "",
+ sharedContext: "");
+
+ Assert.True(sections.ContainsKey(SystemPromptSections.ToolEfficiency));
+ var toolSection = sections[SystemPromptSections.ToolEfficiency];
+ Assert.Equal(SectionOverrideAction.Append, toolSection.Action);
+ Assert.Contains("CRITICAL: Tool Usage & Honesty Policy", toolSection.Content);
+ Assert.Contains("NEVER fabricate", toolSection.Content);
+ Assert.Contains("TOOL_FAILURE:", toolSection.Content);
+ Assert.Contains("REPORT THE FAILURE", toolSection.Content);
+ Assert.Contains("NEVER evaluate or assess", toolSection.Content);
+ }
- Assert.Contains("CRITICAL: Tool Usage & Honesty Policy", workerPrompt);
- Assert.Contains("NEVER fabricate", workerPrompt);
- Assert.Contains("TOOL_FAILURE:", workerPrompt);
- Assert.Contains("REPORT THE FAILURE", workerPrompt);
- Assert.Contains("NEVER evaluate or assess", workerPrompt);
+ [Fact]
+ public void WorkerSystemMessageSections_ContainsIdentity()
+ {
+ var charter = "You are a code review specialist.";
+ var sections = CopilotService.BuildWorkerSystemMessageSections(
+ charter, worktreeNote: "", sharedContext: "");
+
+ Assert.True(sections.ContainsKey(SystemPromptSections.Identity));
+ var identitySection = sections[SystemPromptSections.Identity];
+ Assert.Equal(SectionOverrideAction.Append, identitySection.Action);
+ Assert.Contains(charter, identitySection.Content);
+ Assert.Contains("synthesized with other workers", identitySection.Content);
+ }
+
+ [Fact]
+ public void WorkerSystemMessageSections_IncludesWorktreeNote()
+ {
+ var worktreeNote = "\n\n## Your Worktree\nYou have an isolated git worktree at `/tmp/wt` (branch: main).\n";
+ var sections = CopilotService.BuildWorkerSystemMessageSections(
+ "worker", worktreeNote: worktreeNote, sharedContext: "");
+
+ Assert.True(sections.ContainsKey(SystemPromptSections.EnvironmentContext));
+ var envSection = sections[SystemPromptSections.EnvironmentContext];
+ Assert.Equal(SectionOverrideAction.Append, envSection.Action);
+ Assert.Contains("/tmp/wt", envSection.Content);
+ }
+
+ [Fact]
+ public void WorkerSystemMessageSections_OmitsWorktreeWhenEmpty()
+ {
+ var sections = CopilotService.BuildWorkerSystemMessageSections(
+ "worker", worktreeNote: "", sharedContext: "");
+
+ Assert.False(sections.ContainsKey(SystemPromptSections.EnvironmentContext));
+ }
+
+ [Fact]
+ public void WorkerSystemMessageSections_IncludesSharedContext()
+ {
+ var sharedContext = "Always use TDD. Run tests before committing.";
+ var sections = CopilotService.BuildWorkerSystemMessageSections(
+ "worker", worktreeNote: "", sharedContext: sharedContext);
+
+ Assert.True(sections.ContainsKey(SystemPromptSections.CustomInstructions));
+ var customSection = sections[SystemPromptSections.CustomInstructions];
+ Assert.Equal(SectionOverrideAction.Append, customSection.Action);
+ Assert.Contains("Team Context", customSection.Content);
+ Assert.Contains(sharedContext, customSection.Content);
+ }
+
+ [Fact]
+ public void WorkerSystemMessageSections_OmitsSharedContextWhenEmpty()
+ {
+ var sections = CopilotService.BuildWorkerSystemMessageSections(
+ "worker", worktreeNote: "", sharedContext: "");
+
+ Assert.False(sections.ContainsKey(SystemPromptSections.CustomInstructions));
+ }
+
+ [Fact]
+ public void WorkerSystemMessageSections_AllSectionsUseAppendAction()
+ {
+ var sections = CopilotService.BuildWorkerSystemMessageSections(
+ "You are a specialist.",
+ "\n\n## Your Worktree\nAt /tmp/wt (branch: dev).\n",
+ "Shared team decisions.");
+
+ foreach (var (key, section) in sections)
+ {
+ Assert.Equal(SectionOverrideAction.Append, section.Action);
+ }
}
#endregion
diff --git a/PolyPilot/Services/CopilotService.Organization.cs b/PolyPilot/Services/CopilotService.Organization.cs
index 13d064685e..23875426e3 100644
--- a/PolyPilot/Services/CopilotService.Organization.cs
+++ b/PolyPilot/Services/CopilotService.Organization.cs
@@ -2,6 +2,7 @@
using System.Diagnostics;
using System.Text.Json;
using System.Text.RegularExpressions;
+using GitHub.Copilot.SDK;
using Microsoft.Extensions.DependencyInjection;
using PolyPilot.Models;
@@ -127,7 +128,14 @@ public async Task CreateMultiAgentGroupAsync(string groupName, string or
while (_sessions.ContainsKey(workerName) || Organization.Sessions.Any(s => s.SessionName == workerName))
workerName = $"{groupName}-Worker-{i}-{suffix++}";
- var workerSession = await CreateSessionAsync(workerName, workerModel, null);
+ // Build worker system message sections with default identity (no charter in simple path)
+ var workerSections = BuildWorkerSystemMessageSections(
+ "You are a worker agent. Complete the following task thoroughly.",
+ worktreeNote: "",
+ sharedContext: "");
+
+ var workerSession = await CreateSessionAsync(workerName, workerModel, null,
+ systemMessageSections: workerSections);
var workerMeta = GetOrCreateSessionMeta(workerSession.Name);
workerMeta.GroupId = group.Id;
workerMeta.Role = MultiAgentRole.Worker;
@@ -2498,30 +2506,10 @@ private async Task ExecuteWorkerAsync(string workerName, string ta
var sw = System.Diagnostics.Stopwatch.StartNew();
await EnsureSessionModelAsync(workerName, cancellationToken);
- // Use per-worker system prompt if set, otherwise generic.
- // Note: .github/copilot-instructions.md is auto-loaded by the SDK for each session's working directory,
- // so workers already inherit repo-level copilot instructions without explicit injection here.
- var meta = GetSessionMeta(workerName);
- var identity = !string.IsNullOrEmpty(meta?.SystemPrompt)
- ? meta.SystemPrompt
- : "You are a worker agent. Complete the following task thoroughly.";
-
- // Inject shared context (e.g., Squad decisions.md) if the group has it
- var group = meta != null ? Organization.Groups.FirstOrDefault(g => g.Id == meta.GroupId) : null;
- var sharedPrefix = !string.IsNullOrEmpty(group?.SharedContext)
- ? $"## Team Context (shared knowledge)\n{group.SharedContext}\n\n"
- : "";
-
- // Inject worktree awareness if the worker has an isolated worktree
- var wtInfo = meta?.WorktreeId != null
- ? _repoManager.Worktrees.FirstOrDefault(wt => wt.Id == meta.WorktreeId) : null;
- var worktreeNote = wtInfo != null && group?.WorktreeStrategy != WorktreeStrategy.Shared
- ? $"\n\n## Your Worktree\nYou have an isolated git worktree at `{wtInfo.Path}` (branch: {wtInfo.Branch}). " +
- "You can safely run any git operations without affecting other workers. " +
- "To check out a PR: `git fetch origin pull//head:pr- && git checkout pr-`\n"
- : "";
-
- var workerPrompt = BuildWorkerPrompt(identity, worktreeNote, sharedPrefix, originalPrompt, task);
+ // Worker identity, worktree note, shared context, and tool honesty policy are now
+ // delivered via SystemMessageConfig sections (set at session creation/revival time).
+ // The user prompt contains only the task-specific content.
+ var workerPrompt = BuildWorkerPrompt(originalPrompt, task);
const int maxRetries = 2;
var dispatchTime = DateTimeOffset.UtcNow;
@@ -3042,17 +3030,67 @@ private bool IsEventsFileActive(string? sessionId)
catch { return null; }
}
- private static string BuildWorkerPrompt(string identity, string worktreeNote, string sharedPrefix, string originalPrompt, string task)
+ ///
+ /// Build a user-facing prompt for a worker that contains only the task-specific content.
+ /// System-level content (identity, tool policy, worktree, shared context) is now delivered
+ /// via SystemMessageConfig sections — see .
+ ///
+ internal static string BuildWorkerPrompt(string originalPrompt, string task)
{
- return $"{identity}{worktreeNote}\n\nYour response will be collected and synthesized with other workers' responses.\n\n" +
- "## CRITICAL: Tool Usage & Honesty Policy\n" +
- "- You MUST use your CLI tools (file reads, builds, tests, grep, etc.) to complete your task. Do NOT rely on assumptions or memory.\n" +
- "- If a tool call fails or is unavailable, REPORT THE FAILURE explicitly. Say what you tried, what failed, and why.\n" +
- "- NEVER fabricate, invent, or assume tool outputs. If you cannot run a tool, say so — do NOT generate plausible-looking results.\n" +
- "- NEVER evaluate or assess code, tests, or behavior without actually running the relevant tools first.\n" +
- "- If you cannot complete your task because tools are unavailable, respond with: " +
- "\"TOOL_FAILURE: [description of what failed and why]\"\n\n" +
- $"{sharedPrefix}## Original User Request (context)\n{originalPrompt}\n\n## Your Assigned Task\n{task}";
+ return $"## Original User Request (context)\n{originalPrompt}\n\n## Your Assigned Task\n{task}";
+ }
+
+ ///
+ /// Build section overrides for a worker session's system message.
+ /// Uses the SDK's SystemMessageConfig Customize mode to place worker-specific content
+ /// in the appropriate system prompt sections instead of concatenating into the user message.
+ ///
+ internal static Dictionary BuildWorkerSystemMessageSections(
+ string identity, string worktreeNote, string sharedContext)
+ {
+ var sections = new Dictionary();
+
+ // Worker charter/identity appended to the Identity section
+ sections[SystemPromptSections.Identity] = new SectionOverride
+ {
+ Action = SectionOverrideAction.Append,
+ Content = $"\n\n{identity}\n\nYour response will be collected and synthesized with other workers' responses."
+ };
+
+ // Tool honesty policy appended to the ToolEfficiency section
+ sections[SystemPromptSections.ToolEfficiency] = new SectionOverride
+ {
+ Action = SectionOverrideAction.Append,
+ Content = "\n\n## CRITICAL: Tool Usage & Honesty Policy\n" +
+ "- You MUST use your CLI tools (file reads, builds, tests, grep, etc.) to complete your task. Do NOT rely on assumptions or memory.\n" +
+ "- If a tool call fails or is unavailable, REPORT THE FAILURE explicitly. Say what you tried, what failed, and why.\n" +
+ "- NEVER fabricate, invent, or assume tool outputs. If you cannot run a tool, say so — do NOT generate plausible-looking results.\n" +
+ "- NEVER evaluate or assess code, tests, or behavior without actually running the relevant tools first.\n" +
+ "- If you cannot complete your task because tools are unavailable, respond with: " +
+ "\"TOOL_FAILURE: [description of what failed and why]\""
+ };
+
+ // Worktree note appended to EnvironmentContext section
+ if (!string.IsNullOrEmpty(worktreeNote))
+ {
+ sections[SystemPromptSections.EnvironmentContext] = new SectionOverride
+ {
+ Action = SectionOverrideAction.Append,
+ Content = worktreeNote
+ };
+ }
+
+ // Shared context (e.g., Squad decisions.md) appended to CustomInstructions section
+ if (!string.IsNullOrEmpty(sharedContext))
+ {
+ sections[SystemPromptSections.CustomInstructions] = new SectionOverride
+ {
+ Action = SectionOverrideAction.Append,
+ Content = $"\n\n## Team Context (shared knowledge)\n{sharedContext}"
+ };
+ }
+
+ return sections;
}
private string BuildSynthesisPrompt(string originalPrompt, List results)
@@ -3836,10 +3874,30 @@ public string GetEffectiveModel(string sessionName)
}
var workerModel = ModelHelper.ResolvePreferredModel(preset.WorkerModels[i], AvailableModels, "claude-opus-4.6");
var workerWorkDir = workerWorkDirs[i] ?? orchWorkDir ?? workingDirectory;
- Debug($"[WorktreeStrategy] Worker '{workerName}': wtId={workerWtIds[i] ?? "(none)"}, dir={workerWorkDir ?? "(null)"}");
+ var systemPrompt = preset.WorkerSystemPrompts != null && i < preset.WorkerSystemPrompts.Length
+ ? preset.WorkerSystemPrompts[i] : null;
+
+ // Build worker system message sections from known charter, worktree, and shared context.
+ // This uses SDK Customize mode so identity, tool policy, worktree, and shared context
+ // are structured as system prompt sections instead of concatenated into the user prompt.
+ var effectiveWtId = workerWtIds[i] ?? orchWtId ?? worktreeId;
+ var wtInfo = effectiveWtId != null
+ ? _repoManager.Worktrees.FirstOrDefault(wt => wt.Id == effectiveWtId) : null;
+ var worktreeNote = wtInfo != null && group.WorktreeStrategy != WorktreeStrategy.Shared
+ ? $"\n\n## Your Worktree\nYou have an isolated git worktree at `{wtInfo.Path}` (branch: {wtInfo.Branch}). " +
+ "You can safely run any git operations without affecting other workers. " +
+ "To check out a PR: `git fetch origin pull//head:pr- && git checkout pr-`\n"
+ : "";
+ var workerSections = BuildWorkerSystemMessageSections(
+ systemPrompt ?? "You are a worker agent. Complete the following task thoroughly.",
+ worktreeNote,
+ group.SharedContext ?? "");
+
+ Debug($"[WorktreeStrategy] Worker '{workerName}': wtId={effectiveWtId ?? "(none)"}, dir={workerWorkDir ?? "(null)"}");
try
{
- await CreateSessionAsync(workerName, workerModel, workerWorkDir, ct);
+ await CreateSessionAsync(workerName, workerModel, workerWorkDir, ct,
+ systemMessageSections: workerSections);
}
catch (Exception ex)
{
@@ -3849,15 +3907,12 @@ public string GetEffectiveModel(string sessionName)
MoveSession(workerName, group.Id);
SetSessionRole(workerName, MultiAgentRole.Worker);
SetSessionPreferredModel(workerName, workerModel);
- var systemPrompt = preset.WorkerSystemPrompts != null && i < preset.WorkerSystemPrompts.Length
- ? preset.WorkerSystemPrompts[i] : null;
var meta = GetSessionMeta(workerName);
if (meta != null)
{
- meta.WorktreeId = workerWtIds[i] ?? orchWtId ?? worktreeId;
+ meta.WorktreeId = effectiveWtId;
if (systemPrompt != null) meta.SystemPrompt = systemPrompt;
}
- var effectiveWtId = workerWtIds[i] ?? orchWtId ?? worktreeId;
if (effectiveWtId != null && _sessions.TryGetValue(workerName, out var workerState))
workerState.Info.WorktreeId = effectiveWtId;
}
diff --git a/PolyPilot/Services/CopilotService.cs b/PolyPilot/Services/CopilotService.cs
index 57840e5f44..01f50b5274 100644
--- a/PolyPilot/Services/CopilotService.cs
+++ b/PolyPilot/Services/CopilotService.cs
@@ -2710,7 +2710,7 @@ await FinalizeResumedSessionUiStateAsync(
private static Task AutoApprovePermissions(PermissionRequest request, PermissionInvocation invocation)
=> Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved });
- public async Task CreateSessionAsync(string name, string? model = null, string? workingDirectory = null, CancellationToken cancellationToken = default, string? groupId = null)
+ public async Task CreateSessionAsync(string name, string? model = null, string? workingDirectory = null, CancellationToken cancellationToken = default, string? groupId = null, Dictionary? systemMessageSections = null)
{
// In demo mode, create a local mock session
if (IsDemoMode)
@@ -2826,11 +2826,18 @@ ALWAYS run the relaunch script as the final step after making changes to this pr
McpServers = mcpServers,
SkillDirectories = skillDirs,
Tools = new List { ShowImageTool.CreateFunction() },
- SystemMessage = new SystemMessageConfig
- {
- Mode = SystemMessageMode.Append,
- Content = systemContent.ToString()
- },
+ SystemMessage = systemMessageSections != null
+ ? new SystemMessageConfig
+ {
+ Mode = SystemMessageMode.Customize,
+ Sections = systemMessageSections,
+ Content = systemContent.ToString()
+ }
+ : new SystemMessageConfig
+ {
+ Mode = SystemMessageMode.Append,
+ Content = systemContent.ToString()
+ },
// Auto-approve all tool permission requests so worker sessions (which have no
// interactive user) can execute tools without getting "Permission denied".
OnPermissionRequest = AutoApprovePermissions,
@@ -4343,7 +4350,8 @@ private Task FinalizeResumedSessionUiStateAsync(
///
/// Build a fresh SessionConfig with MCP servers, skill directories, and system message.
/// Mirrors the reconnect handler's "Session not found" path to ensure revived/fresh sessions
- /// have full external tool access.
+ /// have full external tool access. For worker sessions, uses Customize mode with section
+ /// overrides for identity, tool policy, worktree, and shared context.
///
private SessionConfig BuildFreshSessionConfig(SessionState state, List? tools = null)
{
@@ -4371,6 +4379,11 @@ ALWAYS run the relaunch script as the final step after making changes to this pr
// Add MCP server awareness so the model can guide users when MCP tools fail
AppendMcpServerGuidance(systemContent, mcpServers);
var finalTools = tools ?? new List { ShowImageTool.CreateFunction() };
+
+ // For worker sessions, build section overrides so identity, tool policy, worktree, and
+ // shared context are delivered via structured SystemMessageConfig sections.
+ var workerSections = BuildWorkerSectionsForSession(state.Info.Name);
+
var config = new SessionConfig
{
Model = Models.ModelHelper.NormalizeToSlug(state.Info.Model) ?? DefaultModel,
@@ -4378,11 +4391,18 @@ ALWAYS run the relaunch script as the final step after making changes to this pr
McpServers = mcpServers,
SkillDirectories = skillDirs,
Tools = finalTools,
- SystemMessage = new SystemMessageConfig
- {
- Mode = SystemMessageMode.Append,
- Content = systemContent.ToString()
- },
+ SystemMessage = workerSections != null
+ ? new SystemMessageConfig
+ {
+ Mode = SystemMessageMode.Customize,
+ Sections = workerSections,
+ Content = systemContent.ToString()
+ }
+ : new SystemMessageConfig
+ {
+ Mode = SystemMessageMode.Append,
+ Content = systemContent.ToString()
+ },
OnPermissionRequest = AutoApprovePermissions,
InfiniteSessions = new InfiniteSessionConfig { Enabled = true },
};
@@ -4390,9 +4410,38 @@ ALWAYS run the relaunch script as the final step after making changes to this pr
Debug($"[FRESH-CONFIG] Includes {mcpServers.Count} MCP server(s)");
if (skillDirs != null)
Debug($"[FRESH-CONFIG] Includes {skillDirs.Count} skill dir(s)");
+ if (workerSections != null)
+ Debug($"[FRESH-CONFIG] Worker session — {workerSections.Count} system message section(s)");
return config;
}
+ ///
+ /// Build worker system message sections for a session if it's a multi-agent worker.
+ /// Returns null for non-worker sessions.
+ ///
+ private Dictionary? BuildWorkerSectionsForSession(string sessionName)
+ {
+ var meta = GetSessionMeta(sessionName);
+ if (meta?.Role != MultiAgentRole.Worker) return null;
+
+ var identity = !string.IsNullOrEmpty(meta.SystemPrompt)
+ ? meta.SystemPrompt
+ : "You are a worker agent. Complete the following task thoroughly.";
+
+ var group = Organization.Groups.FirstOrDefault(g => g.Id == meta.GroupId);
+ var sharedContext = group?.SharedContext ?? "";
+
+ var wtInfo = meta.WorktreeId != null
+ ? _repoManager.Worktrees.FirstOrDefault(wt => wt.Id == meta.WorktreeId) : null;
+ var worktreeNote = wtInfo != null && group?.WorktreeStrategy != WorktreeStrategy.Shared
+ ? $"\n\n## Your Worktree\nYou have an isolated git worktree at `{wtInfo.Path}` (branch: {wtInfo.Branch}). " +
+ "You can safely run any git operations without affecting other workers. " +
+ "To check out a PR: `git fetch origin pull//head:pr- && git checkout pr-`\n"
+ : "";
+
+ return BuildWorkerSystemMessageSections(identity, worktreeNote, sharedContext);
+ }
+
public async Task AbortSessionAsync(string sessionName, bool markAsInterrupted = false)
{
// Provider sessions manage their own cancellation
From d46de87bed9e35d826d07103ff88cc1056a54fd2 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 1 May 2026 01:15:12 +0000
Subject: [PATCH 2/8] fix: move dynamic content into section overrides for
Customize mode
MergeDynamicContentIntoSections pipes relaunch instructions and MCP
guidance into EnvironmentContext section overrides instead of relying
on Content being honored alongside Sections in Customize mode.
Also uses thread-safe SnapshotSessionMetas/SnapshotGroups in
BuildWorkerSectionsForSession to prevent collection-modified crashes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
PolyPilot/Services/CopilotService.cs | 48 ++++++++++++++++++++++++----
1 file changed, 42 insertions(+), 6 deletions(-)
diff --git a/PolyPilot/Services/CopilotService.cs b/PolyPilot/Services/CopilotService.cs
index 01f50b5274..3f0d97d232 100644
--- a/PolyPilot/Services/CopilotService.cs
+++ b/PolyPilot/Services/CopilotService.cs
@@ -2144,6 +2144,40 @@ The user can also check configured servers with the /mcp command.
");
}
+ ///
+ /// Merges dynamic content (relaunch instructions, MCP guidance) into section overrides
+ /// so it's delivered via sections rather than .
+ /// This avoids relying on Content being honored alongside Sections in
+ /// mode.
+ ///
+ internal static Dictionary MergeDynamicContentIntoSections(
+ Dictionary sections, string dynamicContent)
+ {
+ if (string.IsNullOrWhiteSpace(dynamicContent))
+ return sections;
+
+ // Merge into EnvironmentContext — this content is environment-specific guidance
+ // (relaunch script instructions, MCP server awareness).
+ if (sections.TryGetValue(SystemPromptSections.EnvironmentContext, out var existing))
+ {
+ sections[SystemPromptSections.EnvironmentContext] = new SectionOverride
+ {
+ Action = SectionOverrideAction.Append,
+ Content = existing.Content + "\n" + dynamicContent
+ };
+ }
+ else
+ {
+ sections[SystemPromptSections.EnvironmentContext] = new SectionOverride
+ {
+ Action = SectionOverrideAction.Append,
+ Content = dynamicContent
+ };
+ }
+
+ return sections;
+ }
+
///
/// Discover all available skills from installed plugins and project-level skill directories.
/// Returns a list of (Name, Description, Source) tuples.
@@ -2830,8 +2864,7 @@ ALWAYS run the relaunch script as the final step after making changes to this pr
? new SystemMessageConfig
{
Mode = SystemMessageMode.Customize,
- Sections = systemMessageSections,
- Content = systemContent.ToString()
+ Sections = MergeDynamicContentIntoSections(systemMessageSections, systemContent.ToString()),
}
: new SystemMessageConfig
{
@@ -4395,8 +4428,7 @@ ALWAYS run the relaunch script as the final step after making changes to this pr
? new SystemMessageConfig
{
Mode = SystemMessageMode.Customize,
- Sections = workerSections,
- Content = systemContent.ToString()
+ Sections = MergeDynamicContentIntoSections(workerSections, systemContent.ToString()),
}
: new SystemMessageConfig
{
@@ -4421,14 +4453,18 @@ ALWAYS run the relaunch script as the final step after making changes to this pr
///
private Dictionary? BuildWorkerSectionsForSession(string sessionName)
{
- var meta = GetSessionMeta(sessionName);
+ // Use thread-safe snapshots — this method is called from background threads
+ // (BuildFreshSessionConfig via reconnect/revival paths in Task.Run).
+ var metas = SnapshotSessionMetas();
+ var meta = metas.FirstOrDefault(m => m.SessionName == sessionName);
if (meta?.Role != MultiAgentRole.Worker) return null;
var identity = !string.IsNullOrEmpty(meta.SystemPrompt)
? meta.SystemPrompt
: "You are a worker agent. Complete the following task thoroughly.";
- var group = Organization.Groups.FirstOrDefault(g => g.Id == meta.GroupId);
+ var groups = SnapshotGroups();
+ var group = groups.FirstOrDefault(g => g.Id == meta.GroupId);
var sharedContext = group?.SharedContext ?? "";
var wtInfo = meta.WorktreeId != null
From 63dfe6a2c9cc83655d08eaeeba5c7c24801cd010 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 1 May 2026 01:15:13 +0000
Subject: [PATCH 3/8] fix: re-include fresh dynamic context in worker dispatch
prompt
BuildWorkerPrompt now accepts optional freshIdentity and
freshSharedContext params. ExecuteWorkerAsync re-reads current
meta.SystemPrompt and group.SharedContext at dispatch time, ensuring
mid-session changes (e.g., edited decisions.md or SetSessionSystemPrompt)
are reflected even though system message sections are baked at creation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Services/CopilotService.Organization.cs | 35 +++++++++++++++----
1 file changed, 28 insertions(+), 7 deletions(-)
diff --git a/PolyPilot/Services/CopilotService.Organization.cs b/PolyPilot/Services/CopilotService.Organization.cs
index 23875426e3..a87b844048 100644
--- a/PolyPilot/Services/CopilotService.Organization.cs
+++ b/PolyPilot/Services/CopilotService.Organization.cs
@@ -2506,10 +2506,18 @@ private async Task ExecuteWorkerAsync(string workerName, string ta
var sw = System.Diagnostics.Stopwatch.StartNew();
await EnsureSessionModelAsync(workerName, cancellationToken);
- // Worker identity, worktree note, shared context, and tool honesty policy are now
+ // Worker identity, worktree note, shared context, and tool honesty policy are
// delivered via SystemMessageConfig sections (set at session creation/revival time).
- // The user prompt contains only the task-specific content.
- var workerPrompt = BuildWorkerPrompt(originalPrompt, task);
+ // However, dynamic state (SharedContext, SystemPrompt) may change after creation
+ // (e.g., user edits decisions.md or calls SetSessionSystemPrompt). Re-read fresh
+ // values and include them in the user prompt to ensure dispatch-time freshness.
+ var meta = GetSessionMeta(workerName);
+ var group = meta?.GroupId != null
+ ? Organization.Groups.FirstOrDefault(g => g.Id == meta.GroupId) : null;
+ var freshSharedContext = group?.SharedContext ?? "";
+ var freshIdentity = meta?.SystemPrompt;
+
+ var workerPrompt = BuildWorkerPrompt(originalPrompt, task, freshIdentity, freshSharedContext);
const int maxRetries = 2;
var dispatchTime = DateTimeOffset.UtcNow;
@@ -3031,13 +3039,26 @@ private bool IsEventsFileActive(string? sessionId)
}
///
- /// Build a user-facing prompt for a worker that contains only the task-specific content.
- /// System-level content (identity, tool policy, worktree, shared context) is now delivered
+ /// Build a user-facing prompt for a worker that contains the task-specific content.
+ /// System-level content (identity, tool policy, worktree, shared context) is delivered
/// via SystemMessageConfig sections — see .
+ /// Fresh dynamic context (identity, shared context) is included in the user prompt when
+ /// provided, ensuring dispatch-time changes are picked up even when sections are stale.
///
- internal static string BuildWorkerPrompt(string originalPrompt, string task)
+ internal static string BuildWorkerPrompt(string originalPrompt, string task,
+ string? freshIdentity = null, string? freshSharedContext = null)
{
- return $"## Original User Request (context)\n{originalPrompt}\n\n## Your Assigned Task\n{task}";
+ var sb = new System.Text.StringBuilder();
+ sb.Append($"## Original User Request (context)\n{originalPrompt}\n\n## Your Assigned Task\n{task}");
+
+ // Include fresh dynamic context so mid-session changes to identity/SharedContext
+ // are reflected even though system message sections are baked at creation time.
+ if (!string.IsNullOrEmpty(freshIdentity))
+ sb.Append($"\n\n## Your Role\n{freshIdentity}");
+ if (!string.IsNullOrEmpty(freshSharedContext))
+ sb.Append($"\n\n## Team Context (latest)\n{freshSharedContext}");
+
+ return sb.ToString();
}
///
From 1617aeb4907d664cc6b58ffea941e771979a68d2 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 1 May 2026 01:15:14 +0000
Subject: [PATCH 4/8] test: add MergeDynamicContent tests, fix structural
guards, rename smoke tests
- Add 5 new tests: BuildWorkerPrompt with fresh identity/SharedContext,
MergeDynamicContentIntoSections merge/create/no-op behaviors
- ConnectionRecoveryTests: add Customize mode + BuildWorkerSectionsForSession
assertions to catch regressions in the worker branch
- Rename integration tests to AppBootstrap to honestly reflect scope
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../WorkerSystemMessageTests.cs | 12 ++--
PolyPilot.Tests/ConnectionRecoveryTests.cs | 2 +
PolyPilot.Tests/WorkerToolHonestyTests.cs | 69 +++++++++++++++++++
3 files changed, 77 insertions(+), 6 deletions(-)
diff --git a/PolyPilot.IntegrationTests/WorkerSystemMessageTests.cs b/PolyPilot.IntegrationTests/WorkerSystemMessageTests.cs
index ce351213c2..4429629738 100644
--- a/PolyPilot.IntegrationTests/WorkerSystemMessageTests.cs
+++ b/PolyPilot.IntegrationTests/WorkerSystemMessageTests.cs
@@ -3,19 +3,19 @@
namespace PolyPilot.IntegrationTests;
///
-/// Integration tests verifying that multi-agent worker sessions use
-/// SystemMessageConfig with section overrides for identity, tool policy,
-/// worktree, and shared context (issue #496).
+/// Smoke tests verifying app bootstrap succeeds after the SystemMessageConfig
+/// refactoring (issue #496). These don't test section overrides directly —
+/// they confirm the refactored CreateSessionAsync doesn't break initialization.
///
[Collection("PolyPilot")]
-[Trait("Category", "WorkerSystemMessage")]
+[Trait("Category", "AppBootstrap")]
public class WorkerSystemMessageTests : IntegrationTestBase
{
public WorkerSystemMessageTests(AppFixture app, ITestOutputHelper output)
: base(app, output) { }
[Fact]
- public async Task Dashboard_ShowsMultiAgentGroupCreation()
+ public async Task AppBootstrap_DashboardLoads()
{
await WaitForCdpReadyAsync();
@@ -40,7 +40,7 @@ public async Task Dashboard_ShowsMultiAgentGroupCreation()
}
[Fact]
- public async Task App_RespondsToApiStatus()
+ public async Task AppBootstrap_RespondsToApiStatus()
{
// Verify the app is running and responsive — this confirms that the
// refactored CreateSessionAsync (with systemMessageSections parameter)
diff --git a/PolyPilot.Tests/ConnectionRecoveryTests.cs b/PolyPilot.Tests/ConnectionRecoveryTests.cs
index aaa4bc7393..361fd02adc 100644
--- a/PolyPilot.Tests/ConnectionRecoveryTests.cs
+++ b/PolyPilot.Tests/ConnectionRecoveryTests.cs
@@ -338,6 +338,8 @@ public void SendPromptAsync_FreshSessionConfig_IncludesSystemMessage()
var helperBlock = source.Substring(helperIdx, Math.Min(3000, source.Length - helperIdx));
Assert.Contains("SystemMessage = ", helperBlock);
Assert.Contains("SystemMessageMode.Append", helperBlock);
+ Assert.Contains("SystemMessageMode.Customize", helperBlock);
+ Assert.Contains("BuildWorkerSectionsForSession", helperBlock);
}
[Fact]
diff --git a/PolyPilot.Tests/WorkerToolHonestyTests.cs b/PolyPilot.Tests/WorkerToolHonestyTests.cs
index b6949bccc9..d1a5ba0b75 100644
--- a/PolyPilot.Tests/WorkerToolHonestyTests.cs
+++ b/PolyPilot.Tests/WorkerToolHonestyTests.cs
@@ -34,6 +34,31 @@ public void WorkerPrompt_ContainsOnlyTaskContent()
// System-level content is now in sections, not the user prompt
Assert.DoesNotContain("CRITICAL: Tool Usage & Honesty Policy", workerPrompt);
Assert.DoesNotContain("NEVER fabricate", workerPrompt);
+ // No dynamic context when called without optional params
+ Assert.DoesNotContain("Your Role", workerPrompt);
+ Assert.DoesNotContain("Team Context (latest)", workerPrompt);
+ }
+
+ [Fact]
+ public void WorkerPrompt_IncludesFreshIdentityWhenProvided()
+ {
+ var workerPrompt = CopilotService.BuildWorkerPrompt(
+ "Fix the tests", "Run the unit tests",
+ freshIdentity: "You are a security auditor.");
+
+ Assert.Contains("Your Role", workerPrompt);
+ Assert.Contains("You are a security auditor.", workerPrompt);
+ }
+
+ [Fact]
+ public void WorkerPrompt_IncludesFreshSharedContextWhenProvided()
+ {
+ var workerPrompt = CopilotService.BuildWorkerPrompt(
+ "Fix the tests", "Run the unit tests",
+ freshSharedContext: "Always use TDD.");
+
+ Assert.Contains("Team Context (latest)", workerPrompt);
+ Assert.Contains("Always use TDD.", workerPrompt);
}
#endregion
@@ -133,6 +158,50 @@ public void WorkerSystemMessageSections_AllSectionsUseAppendAction()
#endregion
+ #region MergeDynamicContentIntoSections
+
+ [Fact]
+ public void MergeDynamicContent_AddsToEnvironmentContext_WhenNoExistingSection()
+ {
+ var sections = CopilotService.BuildWorkerSystemMessageSections(
+ "worker", worktreeNote: "", sharedContext: "");
+
+ Assert.False(sections.ContainsKey(SystemPromptSections.EnvironmentContext));
+
+ var merged = CopilotService.MergeDynamicContentIntoSections(sections, "MCP guidance here");
+
+ Assert.True(merged.ContainsKey(SystemPromptSections.EnvironmentContext));
+ Assert.Contains("MCP guidance here", merged[SystemPromptSections.EnvironmentContext].Content);
+ }
+
+ [Fact]
+ public void MergeDynamicContent_MergesWithExistingEnvironmentContext()
+ {
+ var sections = CopilotService.BuildWorkerSystemMessageSections(
+ "worker", worktreeNote: "\n\n## Your Worktree\nAt /tmp/wt\n", sharedContext: "");
+
+ Assert.True(sections.ContainsKey(SystemPromptSections.EnvironmentContext));
+
+ var merged = CopilotService.MergeDynamicContentIntoSections(sections, "Relaunch instructions");
+
+ var envContent = merged[SystemPromptSections.EnvironmentContext].Content;
+ Assert.Contains("/tmp/wt", envContent);
+ Assert.Contains("Relaunch instructions", envContent);
+ }
+
+ [Fact]
+ public void MergeDynamicContent_NoOpWhenContentEmpty()
+ {
+ var sections = CopilotService.BuildWorkerSystemMessageSections(
+ "worker", worktreeNote: "", sharedContext: "");
+
+ var merged = CopilotService.MergeDynamicContentIntoSections(sections, " ");
+
+ Assert.False(merged.ContainsKey(SystemPromptSections.EnvironmentContext));
+ }
+
+ #endregion
+
#region BuildSynthesisPrompt Tool-Verification Instructions
[Fact]
From 0941e3e287a0d89223ffc99567584c80f649de4f Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 1 May 2026 01:35:03 +0000
Subject: [PATCH 5/8] fix: use thread-safe snapshots in ExecuteWorkerAsync for
Organization reads
ExecuteWorkerAsync runs on background threads (dispatched via Task.WhenAll
from orchestration). It was reading Organization.Groups and
Organization.Sessions directly via GetSessionMeta/FirstOrDefault without
synchronization, risking InvalidOperationException if the UI thread mutates
these lists concurrently.
Round 1 fixed this in BuildWorkerSectionsForSession but missed the same
pattern in ExecuteWorkerAsync. Now uses SnapshotSessionMetas() and
SnapshotGroups() for thread-safe reads.
Co-authored-by: copilot-agentic-workflow[bot] <224017+copilot-agentic-workflow[bot]@users.noreply.github.com>
---
PolyPilot/Services/CopilotService.Organization.cs | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/PolyPilot/Services/CopilotService.Organization.cs b/PolyPilot/Services/CopilotService.Organization.cs
index a87b844048..a353cbe5d1 100644
--- a/PolyPilot/Services/CopilotService.Organization.cs
+++ b/PolyPilot/Services/CopilotService.Organization.cs
@@ -2511,9 +2511,13 @@ private async Task ExecuteWorkerAsync(string workerName, string ta
// However, dynamic state (SharedContext, SystemPrompt) may change after creation
// (e.g., user edits decisions.md or calls SetSessionSystemPrompt). Re-read fresh
// values and include them in the user prompt to ensure dispatch-time freshness.
- var meta = GetSessionMeta(workerName);
+ // Use thread-safe snapshots — ExecuteWorkerAsync runs on background threads
+ // (dispatched via Task.WhenAll from orchestration).
+ var metas = SnapshotSessionMetas();
+ var meta = metas.FirstOrDefault(m => m.SessionName == workerName);
+ var groups = SnapshotGroups();
var group = meta?.GroupId != null
- ? Organization.Groups.FirstOrDefault(g => g.Id == meta.GroupId) : null;
+ ? groups.FirstOrDefault(g => g.Id == meta.GroupId) : null;
var freshSharedContext = group?.SharedContext ?? "";
var freshIdentity = meta?.SystemPrompt;
From fe306b55c087a83169aaf9713091b0aae255e23b Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 1 May 2026 01:35:04 +0000
Subject: [PATCH 6/8] refactor: rename WorkerSystemMessageTests to
AppBootstrapTests
Addresses review finding: integration tests are smoke tests that verify
app bootstrap, not worker system message behavior. Renaming the class
and file to honestly reflect what they test.
Co-authored-by: copilot-agentic-workflow[bot] <224017+copilot-agentic-workflow[bot]@users.noreply.github.com>
---
.../{WorkerSystemMessageTests.cs => AppBootstrapTests.cs} | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
rename PolyPilot.IntegrationTests/{WorkerSystemMessageTests.cs => AppBootstrapTests.cs} (93%)
diff --git a/PolyPilot.IntegrationTests/WorkerSystemMessageTests.cs b/PolyPilot.IntegrationTests/AppBootstrapTests.cs
similarity index 93%
rename from PolyPilot.IntegrationTests/WorkerSystemMessageTests.cs
rename to PolyPilot.IntegrationTests/AppBootstrapTests.cs
index 4429629738..04070522f8 100644
--- a/PolyPilot.IntegrationTests/WorkerSystemMessageTests.cs
+++ b/PolyPilot.IntegrationTests/AppBootstrapTests.cs
@@ -9,9 +9,9 @@ namespace PolyPilot.IntegrationTests;
///
[Collection("PolyPilot")]
[Trait("Category", "AppBootstrap")]
-public class WorkerSystemMessageTests : IntegrationTestBase
+public class AppBootstrapTests : IntegrationTestBase
{
- public WorkerSystemMessageTests(AppFixture app, ITestOutputHelper output)
+ public AppBootstrapTests(AppFixture app, ITestOutputHelper output)
: base(app, output) { }
[Fact]
From 141f2df4f12a681e04d8a350bd10b5e8c3552764 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 1 May 2026 02:38:26 +0000
Subject: [PATCH 7/8] fix: eliminate duplicate identity/shared context from
user prompt (finding 13)
Identity and shared context are now delivered exclusively via SystemMessageConfig
sections. Removes freshIdentity/freshSharedContext from BuildWorkerPrompt and
ExecuteWorkerAsync to avoid token waste and conflicting instructions when system
sections contain stale creation-time values.
Adds fresh worktree note to BuildWorkerPrompt instead (finding 10) since worktree
branch/path can change between session creation and dispatch.
Updates tests to match new BuildWorkerPrompt signature.
Co-authored-by: copilot-agentic-workflow[bot] <224017+copilot-agentic-workflow[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
PolyPilot.Tests/WorkerToolHonestyTests.cs | 27 ++++++++----
.../Services/CopilotService.Organization.cs | 43 +++++++++++--------
2 files changed, 43 insertions(+), 27 deletions(-)
diff --git a/PolyPilot.Tests/WorkerToolHonestyTests.cs b/PolyPilot.Tests/WorkerToolHonestyTests.cs
index d1a5ba0b75..97d335d6fe 100644
--- a/PolyPilot.Tests/WorkerToolHonestyTests.cs
+++ b/PolyPilot.Tests/WorkerToolHonestyTests.cs
@@ -37,28 +37,39 @@ public void WorkerPrompt_ContainsOnlyTaskContent()
// No dynamic context when called without optional params
Assert.DoesNotContain("Your Role", workerPrompt);
Assert.DoesNotContain("Team Context (latest)", workerPrompt);
+ Assert.DoesNotContain("Current Worktree", workerPrompt);
}
[Fact]
- public void WorkerPrompt_IncludesFreshIdentityWhenProvided()
+ public void WorkerPrompt_IncludesFreshWorktreeNoteWhenProvided()
{
var workerPrompt = CopilotService.BuildWorkerPrompt(
"Fix the tests", "Run the unit tests",
- freshIdentity: "You are a security auditor.");
+ freshWorktreeNote: "\n\n## Your Worktree\nAt `/tmp/wt` (branch: main).\n");
- Assert.Contains("Your Role", workerPrompt);
- Assert.Contains("You are a security auditor.", workerPrompt);
+ Assert.Contains("Current Worktree (latest)", workerPrompt);
+ Assert.Contains("/tmp/wt", workerPrompt);
}
[Fact]
- public void WorkerPrompt_IncludesFreshSharedContextWhenProvided()
+ public void WorkerPrompt_OmitsWorktreeWhenEmpty()
{
var workerPrompt = CopilotService.BuildWorkerPrompt(
"Fix the tests", "Run the unit tests",
- freshSharedContext: "Always use TDD.");
+ freshWorktreeNote: "");
- Assert.Contains("Team Context (latest)", workerPrompt);
- Assert.Contains("Always use TDD.", workerPrompt);
+ Assert.DoesNotContain("Current Worktree", workerPrompt);
+ }
+
+ [Fact]
+ public void WorkerPrompt_DoesNotDuplicateIdentityOrSharedContext()
+ {
+ // Identity and shared context are delivered via system message sections only.
+ // The user prompt must NOT include them to avoid token waste and conflicting instructions.
+ var workerPrompt = CopilotService.BuildWorkerPrompt("Fix the tests", "Run the unit tests");
+
+ Assert.DoesNotContain("Your Role", workerPrompt);
+ Assert.DoesNotContain("Team Context", workerPrompt);
}
#endregion
diff --git a/PolyPilot/Services/CopilotService.Organization.cs b/PolyPilot/Services/CopilotService.Organization.cs
index a353cbe5d1..25560e453f 100644
--- a/PolyPilot/Services/CopilotService.Organization.cs
+++ b/PolyPilot/Services/CopilotService.Organization.cs
@@ -2506,11 +2506,11 @@ private async Task ExecuteWorkerAsync(string workerName, string ta
var sw = System.Diagnostics.Stopwatch.StartNew();
await EnsureSessionModelAsync(workerName, cancellationToken);
- // Worker identity, worktree note, shared context, and tool honesty policy are
- // delivered via SystemMessageConfig sections (set at session creation/revival time).
- // However, dynamic state (SharedContext, SystemPrompt) may change after creation
- // (e.g., user edits decisions.md or calls SetSessionSystemPrompt). Re-read fresh
- // values and include them in the user prompt to ensure dispatch-time freshness.
+ // Worker identity, shared context, and tool honesty policy are delivered via
+ // SystemMessageConfig sections (set at session creation/revival time) — not
+ // duplicated in the user prompt to avoid token waste and conflicting instructions.
+ // Worktree info is re-read at dispatch time because branch/path can change
+ // between session creation and dispatch (e.g., git checkout, worktree reassignment).
// Use thread-safe snapshots — ExecuteWorkerAsync runs on background threads
// (dispatched via Task.WhenAll from orchestration).
var metas = SnapshotSessionMetas();
@@ -2518,10 +2518,17 @@ private async Task ExecuteWorkerAsync(string workerName, string ta
var groups = SnapshotGroups();
var group = meta?.GroupId != null
? groups.FirstOrDefault(g => g.Id == meta.GroupId) : null;
- var freshSharedContext = group?.SharedContext ?? "";
- var freshIdentity = meta?.SystemPrompt;
- var workerPrompt = BuildWorkerPrompt(originalPrompt, task, freshIdentity, freshSharedContext);
+ // Recompute worktree note from current meta — may differ from creation-time value
+ var wtInfo = meta?.WorktreeId != null
+ ? _repoManager.Worktrees.FirstOrDefault(wt => wt.Id == meta.WorktreeId) : null;
+ var freshWorktreeNote = wtInfo != null && group?.WorktreeStrategy != WorktreeStrategy.Shared
+ ? $"\n\n## Your Worktree\nYou have an isolated git worktree at `{wtInfo.Path}` (branch: {wtInfo.Branch}). " +
+ "You can safely run any git operations without affecting other workers. " +
+ "To check out a PR: `git fetch origin pull//head:pr- && git checkout pr-`\n"
+ : "";
+
+ var workerPrompt = BuildWorkerPrompt(originalPrompt, task, freshWorktreeNote);
const int maxRetries = 2;
var dispatchTime = DateTimeOffset.UtcNow;
@@ -3044,23 +3051,21 @@ private bool IsEventsFileActive(string? sessionId)
///
/// Build a user-facing prompt for a worker that contains the task-specific content.
- /// System-level content (identity, tool policy, worktree, shared context) is delivered
- /// via SystemMessageConfig sections — see .
- /// Fresh dynamic context (identity, shared context) is included in the user prompt when
- /// provided, ensuring dispatch-time changes are picked up even when sections are stale.
+ /// System-level content (identity, tool policy, shared context) is delivered via
+ /// SystemMessageConfig sections — see .
+ /// Only worktree info is included here for dispatch-time freshness, since branch/path
+ /// can change between session creation and dispatch.
///
internal static string BuildWorkerPrompt(string originalPrompt, string task,
- string? freshIdentity = null, string? freshSharedContext = null)
+ string? freshWorktreeNote = null)
{
var sb = new System.Text.StringBuilder();
sb.Append($"## Original User Request (context)\n{originalPrompt}\n\n## Your Assigned Task\n{task}");
- // Include fresh dynamic context so mid-session changes to identity/SharedContext
- // are reflected even though system message sections are baked at creation time.
- if (!string.IsNullOrEmpty(freshIdentity))
- sb.Append($"\n\n## Your Role\n{freshIdentity}");
- if (!string.IsNullOrEmpty(freshSharedContext))
- sb.Append($"\n\n## Team Context (latest)\n{freshSharedContext}");
+ // Include fresh worktree note so mid-session worktree changes (branch checkout,
+ // reassignment) are reflected even though system message sections are baked at creation.
+ if (!string.IsNullOrEmpty(freshWorktreeNote))
+ sb.Append($"\n\n## Current Worktree (latest)\n{freshWorktreeNote}");
return sb.ToString();
}
From f51b9e0da164130dfd17fd365daf3639f52dab84 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 1 May 2026 02:38:28 +0000
Subject: [PATCH 8/8] fix: pass worker system message sections in
RecreateSessionAsync (finding 11)
RecreateSessionAsync now calls BuildWorkerSectionsForSession before closing the
session, preserving worker identity, tool honesty, worktree, and shared context
sections when a worker's model is changed on a zero-history session.
Also adds mutation warning to MergeDynamicContentIntoSections XML doc (finding 12).
Co-authored-by: copilot-agentic-workflow[bot] <224017+copilot-agentic-workflow[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
PolyPilot/Services/CopilotService.cs | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/PolyPilot/Services/CopilotService.cs b/PolyPilot/Services/CopilotService.cs
index 3f0d97d232..e13597f19c 100644
--- a/PolyPilot/Services/CopilotService.cs
+++ b/PolyPilot/Services/CopilotService.cs
@@ -2149,6 +2149,8 @@ The user can also check configured servers with the /mcp command.
/// so it's delivered via sections rather than .
/// This avoids relying on Content being honored alongside Sections in
/// mode.
+ /// Warning: Mutates in-place and returns the
+ /// same reference. Callers must pass a fresh dictionary if the original must not be modified.
///
internal static Dictionary MergeDynamicContentIntoSections(
Dictionary sections, string dynamicContent)
@@ -3341,10 +3343,15 @@ await _bridgeClient.CreateSessionWithWorktreeAsync(new CreateSessionWithWorktree
// Preserve group assignment so the new session stays in the same group (e.g., codespace group)
var meta = Organization.Sessions.FirstOrDefault(m => m.SessionName == name);
var groupId = meta?.GroupId;
-
+
+ // For worker sessions, build system message sections BEFORE closing (meta is still available).
+ // Without this, the recreated session loses identity, tool honesty, worktree, and shared context.
+ var workerSections = BuildWorkerSectionsForSession(name);
+
await CloseSessionAsync(name);
- return await CreateSessionAsync(name, newModel, workingDir, groupId: groupId);
+ return await CreateSessionAsync(name, newModel, workingDir, groupId: groupId,
+ systemMessageSections: workerSections);
}
///