diff --git a/README.md b/README.md
index d731f92c..a39d9063 100644
--- a/README.md
+++ b/README.md
@@ -147,7 +147,7 @@ An orchestration layer for AI agent teams across Claude, Codex, and OpenCode.
- **Built-in code editor** — edit project files with Git support without leaving the app
-- **Branch strategy** — choose via prompt: single branch or git worktree per agent
+- **Branch strategy** - choose per teammate at launch: use the main checkout or run selected agents in their own git worktree. You can still spell out branch rules in the provisioning prompt.
- **Team member stats** — global performance statistics per member
@@ -247,13 +247,13 @@ Yes. Every task shows a full diff view where you can accept, reject, or comment
What happens if an agent gets stuck?
-Send a direct message to course-correct, or stop and restart from the process dashboard. If an agent needs your input, you'll get a notification and the task will show a distinct badge on the board.
+Send a direct message to course-correct, or stop and restart from the process dashboard. Agent Teams also has a nudge system: the app can send a short control message when there is a clear reason to wake an agent up, such as after a known rate-limit cooldown, when a teammate has not synced with its current task or review, or when progress appears stalled. Nudges are guarded and rate limited, so they are meant to help the agent continue, not spam it. If an agent needs your input, you'll get a notification and the task will show a distinct badge on the board.
Does it support multiple projects and teams?
-Yes. Run multiple teams in one project or across different projects, even simultaneously. To avoid Git conflicts, ask agents to use git worktree in your provisioning prompt.
+Yes. Run multiple teams in one project or across different projects, even simultaneously. To avoid Git conflicts, enable git worktree isolation for selected teammates when launching the team, and use the provisioning prompt for any extra branch or merge rules.
---
diff --git a/src/main/services/team/TeamMcpConfigBuilder.ts b/src/main/services/team/TeamMcpConfigBuilder.ts
index 995e0cff..bb134149 100644
--- a/src/main/services/team/TeamMcpConfigBuilder.ts
+++ b/src/main/services/team/TeamMcpConfigBuilder.ts
@@ -442,6 +442,17 @@ async function resolveNodePath(options?: McpLaunchSpecResolveOptions): Promise {
[
'#!/bin/sh',
'if [ "$1" = "-e" ]; then',
- ' printf "%s" "$FAKE_NODE_PATH"',
+ ' printf "{\\"execPath\\":\\"%s\\",\\"version\\":\\"%s\\"}" "$FAKE_NODE_PATH" "22.0.0"',
' exit 0',
'fi',
'echo "unexpected node args: $*" >&2',
diff --git a/test/main/services/team/TeamMcpConfigBuilder.test.ts b/test/main/services/team/TeamMcpConfigBuilder.test.ts
index e6d1120d..95825470 100644
--- a/test/main/services/team/TeamMcpConfigBuilder.test.ts
+++ b/test/main/services/team/TeamMcpConfigBuilder.test.ts
@@ -476,13 +476,21 @@ describe('TeamMcpConfigBuilder', () => {
}
});
- it('skips strict shell env lookup when fast Node lookup succeeds from a minimal GUI PATH', async () => {
+ it('prefers strict shell env lookup over fast Node lookup from a minimal GUI PATH', async () => {
mockBuiltWorkspaceEntryAvailable();
const previousPath = process.env.PATH;
process.env.PATH = ['/usr/bin', '/bin', '/usr/sbin', '/sbin'].join(path.delimiter);
- hoisted.execCliMock.mockResolvedValue({
- stdout: nodeRuntimeProbeStdout('/fast/node'),
- stderr: '',
+ hoisted.resolveInteractiveShellEnvMock.mockResolvedValue({
+ PATH: ['/strict-shell-node-bin', '/usr/bin'].join(path.delimiter),
+ HOME: '/Users/tester',
+ });
+ hoisted.execCliMock.mockImplementation(async (command, _args, options) => {
+ const env = options?.env as NodeJS.ProcessEnv | undefined;
+ if (env?.PATH?.split(path.delimiter)[0] === '/strict-shell-node-bin') {
+ expect(command).toBe('node');
+ return { stdout: nodeRuntimeProbeStdout('/strict-shell-node-bin/node'), stderr: '' };
+ }
+ return { stdout: nodeRuntimeProbeStdout('/fast/node'), stderr: '' };
});
try {
@@ -490,8 +498,10 @@ describe('TeamMcpConfigBuilder', () => {
const configPath = await builder.writeConfigFile();
createdPaths.push(configPath);
- expect(readGeneratedServer(configPath)?.command).toBe('/fast/node');
- expect(hoisted.resolveInteractiveShellEnvMock).not.toHaveBeenCalled();
+ expect(readGeneratedServer(configPath)?.command).toBe('/strict-shell-node-bin/node');
+ expect(hoisted.resolveInteractiveShellEnvMock).toHaveBeenCalledWith(
+ expect.objectContaining({ source: 'mcp-node-runtime' })
+ );
} finally {
if (previousPath === undefined) {
delete process.env.PATH;
diff --git a/test/main/services/team/TeamProvisioningMemberMcpConfig.safe-e2e.test.ts b/test/main/services/team/TeamProvisioningMemberMcpConfig.safe-e2e.test.ts
index b4f06a3d..487871a9 100644
--- a/test/main/services/team/TeamProvisioningMemberMcpConfig.safe-e2e.test.ts
+++ b/test/main/services/team/TeamProvisioningMemberMcpConfig.safe-e2e.test.ts
@@ -27,7 +27,10 @@ vi.mock('@main/services/team/ClaudeBinaryResolver', () => ({
vi.mock('@main/utils/childProcess', () => ({
execCli: vi.fn(async (_binaryPath: string | null, args: string[]) => {
if (args[0] === '-e' && args[1]?.includes('process.execPath')) {
- return { stdout: process.execPath, stderr: '' };
+ return {
+ stdout: JSON.stringify({ execPath: process.execPath, version: process.versions.node }),
+ stderr: '',
+ };
}
if (args.includes('model') && args.includes('list')) {
return {
diff --git a/test/main/services/team/TeamProvisioningService.test.ts b/test/main/services/team/TeamProvisioningService.test.ts
index 642f53cb..9318c8c3 100644
--- a/test/main/services/team/TeamProvisioningService.test.ts
+++ b/test/main/services/team/TeamProvisioningService.test.ts
@@ -52,6 +52,13 @@ vi.mock('@main/services/team/TeamTaskReader', () => ({
vi.mock('@main/utils/childProcess', () => ({
execCli: vi.fn(async (_binaryPath: string | null, args: string[]) => {
+ if (args[0] === '-e' && args[1]?.includes('process.execPath')) {
+ return {
+ stdout: JSON.stringify({ execPath: process.execPath, version: process.versions.node }),
+ stderr: '',
+ };
+ }
+
if (args[0] === 'model') {
return {
stdout: JSON.stringify({
diff --git a/test/main/services/team/TeamProvisioningServicePostCompact.test.ts b/test/main/services/team/TeamProvisioningServicePostCompact.test.ts
index 76541954..126b8b03 100644
--- a/test/main/services/team/TeamProvisioningServicePostCompact.test.ts
+++ b/test/main/services/team/TeamProvisioningServicePostCompact.test.ts
@@ -80,6 +80,13 @@ async function setupRunningTeam(teamName: string) {
const { child, writeSpy } = createFakeChild();
vi.mocked(spawnCli).mockReturnValue(child as any);
vi.mocked(execCli).mockImplementation(async (_binaryPath, args) => {
+ if (args[0] === '-e' && args[1]?.includes('process.execPath')) {
+ return {
+ stdout: JSON.stringify({ execPath: process.execPath, version: process.versions.node }),
+ stderr: '',
+ };
+ }
+
const providerIndex = args.indexOf('--provider');
const providerId = providerIndex >= 0 ? args[providerIndex + 1] : 'anthropic';
if (args[0] === 'model' && args[1] === 'list') {
diff --git a/test/main/services/team/TeamProvisioningServicePrepare.test.ts b/test/main/services/team/TeamProvisioningServicePrepare.test.ts
index 29107b6e..3b19dedc 100644
--- a/test/main/services/team/TeamProvisioningServicePrepare.test.ts
+++ b/test/main/services/team/TeamProvisioningServicePrepare.test.ts
@@ -31,6 +31,14 @@ vi.mock('@main/services/infrastructure/NotificationManager', () => ({
}));
const defaultExecCliMockImplementation = async (_binaryPath: string | null, args: string[]) => {
+ if (args[0] === '-e' && args[1]?.includes('process.execPath')) {
+ return {
+ stdout: JSON.stringify({ execPath: process.execPath, version: process.versions.node }),
+ stderr: '',
+ exitCode: 0,
+ };
+ }
+
if (args[0] === 'model') {
return {
stdout: JSON.stringify({
diff --git a/test/main/services/team/TeamProvisioningServicePrompts.test.ts b/test/main/services/team/TeamProvisioningServicePrompts.test.ts
index c7363cb3..2a93ec19 100644
--- a/test/main/services/team/TeamProvisioningServicePrompts.test.ts
+++ b/test/main/services/team/TeamProvisioningServicePrompts.test.ts
@@ -26,7 +26,10 @@ vi.mock('@main/services/team/ClaudeBinaryResolver', () => ({
vi.mock('@main/utils/childProcess', () => ({
execCli: vi.fn(async (_binaryPath: string | null, args: string[]) => {
if (args[0] === '-e' && args[1]?.includes('process.execPath')) {
- return { stdout: process.execPath, stderr: '' };
+ return {
+ stdout: JSON.stringify({ execPath: process.execPath, version: process.versions.node }),
+ stderr: '',
+ };
}
if (args.includes('model') && args.includes('list')) {
return {