Align MCP/CLI naming and resolve default model/mode server-side (#291)

* Align create_agent naming and resolve defaults server-side

* Fix test assertions for default model/mode resolution

- Use config.model and config.modeId instead of non-existent snapshot.model
- Strip legacy "default" model ID in normalizeConfig
- Update runtimeInfo assertion to expect resolved default model

* Fix CI: update CLI test for --title, fix formatting in checkout-git
This commit is contained in:
Mohamed Boudra
2026-04-12 22:37:59 +08:00
committed by GitHub
parent 7c18170d6a
commit 64b9b61223
11 changed files with 105 additions and 60 deletions
+7 -4
View File
@@ -1,4 +1,4 @@
import { Command } from "commander"; import { Command, Option } from "commander";
import { import {
getStructuredAgentResponse, getStructuredAgentResponse,
StructuredAgentResponseError, StructuredAgentResponseError,
@@ -22,7 +22,8 @@ export function addRunOptions(cmd: Command): Command {
.description("Create and start an agent with a task") .description("Create and start an agent with a task")
.argument("<prompt>", "The task/prompt for the agent") .argument("<prompt>", "The task/prompt for the agent")
.option("-d, --detach", "Run in background (detached)") .option("-d, --detach", "Run in background (detached)")
.option("--name <name>", "Assign a name/title to the agent") .option("--title <title>", "Assign a title to the agent")
.addOption(new Option("--name <name>", "Hidden alias for --title").hideHelp())
.option( .option(
"--provider <provider>", "--provider <provider>",
"Agent provider, or provider/model (e.g. codex or codex/gpt-5.4)", "Agent provider, or provider/model (e.g. codex or codex/gpt-5.4)",
@@ -82,6 +83,7 @@ export const agentRunSchema: OutputSchema<AgentRunResult> = {
export interface AgentRunOptions extends CommandOptions { export interface AgentRunOptions extends CommandOptions {
detach?: boolean; detach?: boolean;
title?: string;
name?: string; name?: string;
provider?: string; provider?: string;
model?: string; model?: string;
@@ -325,6 +327,7 @@ export async function runRunCommand(
} }
const resolvedProviderModel = resolveProviderAndModel(options); const resolvedProviderModel = resolveProviderAndModel(options);
const resolvedTitle = options.title ?? options.name;
let client; let client;
try { try {
@@ -414,7 +417,7 @@ export async function runRunCommand(
structuredAgent = await client.createAgent({ structuredAgent = await client.createAgent({
provider: resolvedProviderModel.provider, provider: resolvedProviderModel.provider,
cwd, cwd,
title: options.name, title: resolvedTitle,
modeId: options.mode, modeId: options.mode,
model: resolvedProviderModel.model, model: resolvedProviderModel.model,
thinkingOptionId, thinkingOptionId,
@@ -513,7 +516,7 @@ export async function runRunCommand(
const agent = await client.createAgent({ const agent = await client.createAgent({
provider: resolvedProviderModel.provider, provider: resolvedProviderModel.provider,
cwd, cwd,
title: options.name, title: resolvedTitle,
modeId: options.mode, modeId: options.mode,
model: resolvedProviderModel.model, model: resolvedProviderModel.model,
thinkingOptionId, thinkingOptionId,
+1 -1
View File
@@ -54,7 +54,7 @@ try {
assert.strictEqual(result.exitCode, 0, "run --help should exit 0"); assert.strictEqual(result.exitCode, 0, "run --help should exit 0");
assert(result.stdout.includes("-d"), "help should mention -d flag"); assert(result.stdout.includes("-d"), "help should mention -d flag");
assert(result.stdout.includes("--detach"), "help should mention --detach flag"); assert(result.stdout.includes("--detach"), "help should mention --detach flag");
assert(result.stdout.includes("--name"), "help should mention --name option"); assert(result.stdout.includes("--title"), "help should mention --title option");
assert(result.stdout.includes("--provider"), "help should mention --provider option"); assert(result.stdout.includes("--provider"), "help should mention --provider option");
assert(result.stdout.includes("--mode"), "help should mention --mode option"); assert(result.stdout.includes("--mode"), "help should mention --mode option");
assert(result.stdout.includes("--cwd"), "help should mention --cwd option"); assert(result.stdout.includes("--cwd"), "help should mention --cwd option");
@@ -71,7 +71,6 @@ import type {
AgentSessionConfig, AgentSessionConfig,
} from "../server/agent/agent-sdk-types.js"; } from "../server/agent/agent-sdk-types.js";
import type { MutableDaemonConfig, MutableDaemonConfigPatch } from "../shared/messages.js"; import type { MutableDaemonConfig, MutableDaemonConfigPatch } from "../shared/messages.js";
import { getAgentProviderDefinition } from "../server/agent/provider-manifest.js";
import { isRelayClientWebSocketUrl } from "../shared/daemon-endpoints.js"; import { isRelayClientWebSocketUrl } from "../shared/daemon-endpoints.js";
import { import {
asUint8Array, asUint8Array,
@@ -3938,10 +3937,6 @@ function resolveAgentConfig(options: CreateAgentRequestOptions): AgentSessionCon
throw new Error("createAgent requires provider and cwd"); throw new Error("createAgent requires provider and cwd");
} }
if (!merged.modeId) {
merged.modeId = getAgentProviderDefinition(merged.provider).defaultModeId ?? undefined;
}
return { return {
...merged, ...merged,
provider: merged.provider, provider: merged.provider,
@@ -104,7 +104,7 @@ export async function createAgentManagementMcpServer(
.min(1, "Title is required") .min(1, "Title is required")
.max(60, "Title must be 60 characters or fewer") .max(60, "Title must be 60 characters or fewer")
.describe("Short descriptive title (<= 60 chars) summarizing the agent's focus."), .describe("Short descriptive title (<= 60 chars) summarizing the agent's focus."),
agentType: AgentProviderEnum.optional().describe( provider: AgentProviderEnum.optional().describe(
"Optional agent implementation to spawn. Defaults to 'claude'.", "Optional agent implementation to spawn. Defaults to 'claude'.",
), ),
model: z.string().optional().describe("Model to use (e.g. claude-sonnet-4-20250514)"), model: z.string().optional().describe("Model to use (e.g. claude-sonnet-4-20250514)"),
@@ -114,7 +114,10 @@ export async function createAgentManagementMcpServer(
.string() .string()
.optional() .optional()
.describe("Optional task to start immediately after creation (non-blocking)."), .describe("Optional task to start immediately after creation (non-blocking)."),
initialMode: z.string().describe("Required session mode to configure before the first run."), mode: z
.string()
.optional()
.describe("Optional session mode to configure before the first run."),
worktreeName: z worktreeName: z
.string() .string()
.optional() .optional()
@@ -159,9 +162,9 @@ export async function createAgentManagementMcpServer(
async (args) => { async (args) => {
const { const {
cwd, cwd,
agentType, provider,
initialPrompt, initialPrompt,
initialMode, mode,
worktreeName, worktreeName,
baseBranch, baseBranch,
background = false, background = false,
@@ -171,9 +174,9 @@ export async function createAgentManagementMcpServer(
labels, labels,
} = args as { } = args as {
cwd: string; cwd: string;
agentType?: AgentProvider; provider?: AgentProvider;
initialPrompt?: string; initialPrompt?: string;
initialMode: string; mode?: string;
worktreeName?: string; worktreeName?: string;
baseBranch?: string; baseBranch?: string;
background?: boolean; background?: boolean;
@@ -201,13 +204,13 @@ export async function createAgentManagementMcpServer(
worktreeConfig = worktree; worktreeConfig = worktree;
} }
const provider: AgentProvider = agentType ?? "claude"; const resolvedProvider: AgentProvider = provider ?? "claude";
const normalizedTitle = title?.trim() ?? null; const normalizedTitle = title?.trim() ?? null;
const snapshot = await agentManager.createAgent( const snapshot = await agentManager.createAgent(
{ {
provider, provider: resolvedProvider,
cwd: resolvedCwd, cwd: resolvedCwd,
modeId: initialMode, modeId: mode,
title: normalizedTitle ?? undefined, title: normalizedTitle ?? undefined,
model, model,
thinkingOptionId: thinking, thinkingOptionId: thinking,
@@ -98,6 +98,22 @@ class TestAgentClient implements AgentClient {
return new TestAgentSession(config); return new TestAgentSession(config);
} }
async listModels() {
return [
{
provider: "codex",
id: "gpt-5.4",
label: "GPT-5.4",
isDefault: true,
},
{
provider: "codex",
id: "gpt-5.4-mini",
label: "GPT-5.4 Mini",
},
];
}
async resumeSession( async resumeSession(
_handle: AgentPersistenceHandle, _handle: AgentPersistenceHandle,
config?: Partial<AgentSessionConfig>, config?: Partial<AgentSessionConfig>,
@@ -212,7 +228,7 @@ function createFeature(overrides: Partial<AgentFeature> = {}): AgentFeature {
describe("AgentManager", () => { describe("AgentManager", () => {
const logger = createTestLogger(); const logger = createTestLogger();
test("normalizeConfig does not inject default model when omitted", async () => { test("normalizeConfig injects the provider default model when omitted", 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");
const storage = new AgentStorage(storagePath, logger); const storage = new AgentStorage(storagePath, logger);
@@ -230,7 +246,8 @@ describe("AgentManager", () => {
cwd: workdir, cwd: workdir,
}); });
expect(snapshot.model).toBeUndefined(); expect(snapshot.config.model).toBe("gpt-5.4");
expect(snapshot.config.modeId).toBe("auto");
}); });
test("normalizeConfig strips legacy 'default' model id", async () => { test("normalizeConfig strips legacy 'default' model id", async () => {
@@ -252,7 +269,8 @@ describe("AgentManager", () => {
model: "default", model: "default",
}); });
expect(snapshot.model).toBeUndefined(); expect(snapshot.config.model).toBe("gpt-5.4");
expect(snapshot.config.modeId).toBe("auto");
}); });
test("createAgent passes daemon launch env through the provider launch context", async () => { test("createAgent passes daemon launch env through the provider launch context", async () => {
@@ -292,6 +310,8 @@ describe("AgentManager", () => {
expect(client.lastConfig).toEqual({ expect(client.lastConfig).toEqual({
provider: "codex", provider: "codex",
cwd: workdir, cwd: workdir,
model: "gpt-5.4",
modeId: "auto",
}); });
expect(client.lastLaunchContext).toEqual({ expect(client.lastLaunchContext).toEqual({
env: { env: {
@@ -1432,7 +1452,7 @@ describe("AgentManager", () => {
cwd: workdir, cwd: workdir,
}); });
expect(snapshot.runtimeInfo?.model ?? null).toBeNull(); expect(snapshot.runtimeInfo?.model).toBe("gpt-5.4");
await manager.runAgent(snapshot.id, "hello"); await manager.runAgent(snapshot.id, "hello");
@@ -33,7 +33,7 @@ import type {
PersistedAgentDescriptor, PersistedAgentDescriptor,
} from "./agent-sdk-types.js"; } from "./agent-sdk-types.js";
import type { AgentStorage } from "./agent-storage.js"; import type { AgentStorage } from "./agent-storage.js";
import { AGENT_PROVIDER_IDS } from "./provider-manifest.js"; import { AGENT_PROVIDER_IDS, getAgentProviderDefinition } from "./provider-manifest.js";
export { AGENT_LIFECYCLE_STATUSES, type AgentLifecycleStatus }; export { AGENT_LIFECYCLE_STATUSES, type AgentLifecycleStatus };
@@ -2568,7 +2568,31 @@ export class AgentManager {
if (typeof normalized.model === "string") { if (typeof normalized.model === "string") {
const trimmed = normalized.model.trim(); const trimmed = normalized.model.trim();
normalized.model = trimmed.length > 0 ? trimmed : undefined; normalized.model = trimmed.length > 0 && trimmed !== "default" ? trimmed : undefined;
}
if (!normalized.model) {
const client = this.clients.get(normalized.provider);
if (client) {
try {
const models = await client.listModels();
const defaultModel = models.find((model) => model.isDefault) ?? models[0];
if (defaultModel) {
normalized.model = defaultModel.id;
}
} catch {
// Provider may not support model listing — leave model undefined
}
}
}
if (!normalized.modeId) {
try {
normalized.modeId =
getAgentProviderDefinition(normalized.provider).defaultModeId ?? undefined;
} catch {
// Unknown provider
}
} }
return normalized; return normalized;
@@ -151,8 +151,8 @@ describe("agent MCP end-to-end (offline)", () => {
args: { args: {
cwd: agentCwd, cwd: agentCwd,
title: "MCP e2e smoke", title: "MCP e2e smoke",
agentType: "claude", provider: "claude",
initialMode: "bypassPermissions", mode: "bypassPermissions",
initialPrompt, initialPrompt,
background: false, background: false,
}, },
@@ -242,8 +242,8 @@ describe("agent MCP end-to-end (offline)", () => {
args: { args: {
cwd: agentCwd, cwd: agentCwd,
title: "Injected MCP", title: "Injected MCP",
agentType: "claude", provider: "claude",
initialMode: "bypassPermissions", mode: "bypassPermissions",
initialPrompt: "reply with done and stop", initialPrompt: "reply with done and stop",
background: true, background: true,
}, },
@@ -265,8 +265,8 @@ describe("agent MCP end-to-end (offline)", () => {
args: { args: {
cwd: disabledAgentCwd, cwd: disabledAgentCwd,
title: "No injected MCP", title: "No injected MCP",
agentType: "claude", provider: "claude",
initialMode: "bypassPermissions", mode: "bypassPermissions",
initialPrompt: "reply with done and stop", initialPrompt: "reply with done and stop",
background: true, background: true,
}, },
@@ -362,8 +362,8 @@ describe("agent MCP end-to-end (offline)", () => {
args: { args: {
cwd: repoRoot, cwd: repoRoot,
title: "MCP worktree setup terminals", title: "MCP worktree setup terminals",
agentType: "claude", provider: "claude",
initialMode: "bypassPermissions", mode: "bypassPermissions",
initialPrompt: "say done and stop", initialPrompt: "say done and stop",
worktreeName: "mcp-worktree-setup-test", worktreeName: "mcp-worktree-setup-test",
baseBranch: "main", baseBranch: "main",
@@ -121,9 +121,9 @@ describe("MCP parity end-to-end", () => {
const payload = await callToolStructured(topLevelClient, "create_agent", { const payload = await callToolStructured(topLevelClient, "create_agent", {
cwd, cwd,
title: "Parity agent", title: "Parity agent",
agentType: "claude", provider: "claude",
initialPrompt: "say done and stop", initialPrompt: "say done and stop",
initialMode: "bypassPermissions", mode: "bypassPermissions",
background: true, background: true,
...args, ...args,
}); });
@@ -133,7 +133,7 @@ describe("MCP parity end-to-end", () => {
async function createChildAgent(args?: Partial<StructuredContent>): Promise<string> { async function createChildAgent(args?: Partial<StructuredContent>): Promise<string> {
const payload = await callToolStructured(agentScopedClient, "create_agent", { const payload = await callToolStructured(agentScopedClient, "create_agent", {
title: "Parity child", title: "Parity child",
agentType: "claude", provider: "claude",
initialPrompt: "say done and stop", initialPrompt: "say done and stop",
background: true, background: true,
...args, ...args,
@@ -207,9 +207,9 @@ describe("MCP parity end-to-end", () => {
const parentPayload = await callToolStructured(topLevelClient, "create_agent", { const parentPayload = await callToolStructured(topLevelClient, "create_agent", {
cwd: parentAgentCwd, cwd: parentAgentCwd,
title: "MCP parity parent", title: "MCP parity parent",
agentType: "claude", provider: "claude",
initialPrompt: "say done and stop", initialPrompt: "say done and stop",
initialMode: "bypassPermissions", mode: "bypassPermissions",
background: true, background: true,
}); });
parentAgentId = parentPayload.agentId as string; parentAgentId = parentPayload.agentId as string;
@@ -65,7 +65,7 @@ describe("create_agent MCP tool", () => {
const missingTitle = await tool.inputSchema.safeParseAsync({ const missingTitle = await tool.inputSchema.safeParseAsync({
cwd: existingCwd, cwd: existingCwd,
initialMode: "default", mode: "default",
initialPrompt: "test", initialPrompt: "test",
}); });
expect(missingTitle.success).toBe(false); expect(missingTitle.success).toBe(false);
@@ -73,7 +73,7 @@ describe("create_agent MCP tool", () => {
const tooLong = await tool.inputSchema.safeParseAsync({ const tooLong = await tool.inputSchema.safeParseAsync({
cwd: existingCwd, cwd: existingCwd,
initialMode: "default", mode: "default",
title: "x".repeat(61), title: "x".repeat(61),
initialPrompt: "test", initialPrompt: "test",
}); });
@@ -82,7 +82,7 @@ describe("create_agent MCP tool", () => {
const ok = await tool.inputSchema.safeParseAsync({ const ok = await tool.inputSchema.safeParseAsync({
cwd: existingCwd, cwd: existingCwd,
initialMode: "default", mode: "default",
title: "Short title", title: "Short title",
initialPrompt: "test", initialPrompt: "test",
}); });
@@ -95,7 +95,7 @@ describe("create_agent MCP tool", () => {
const tool = (server as any)._registeredTools["create_agent"]; const tool = (server as any)._registeredTools["create_agent"];
const parsed = await tool.inputSchema.safeParseAsync({ const parsed = await tool.inputSchema.safeParseAsync({
cwd: existingCwd, cwd: existingCwd,
initialMode: "default", mode: "default",
title: "Short title", title: "Short title",
}); });
expect(parsed.success).toBe(false); expect(parsed.success).toBe(false);
@@ -194,7 +194,7 @@ describe("create_agent MCP tool", () => {
await tool.callback({ await tool.callback({
cwd: existingCwd, cwd: existingCwd,
title: "Config test", title: "Config test",
initialMode: "default", mode: "default",
initialPrompt: "Do work", initialPrompt: "Do work",
model: "claude-sonnet-4-20250514", model: "claude-sonnet-4-20250514",
thinking: "think-hard", thinking: "think-hard",
@@ -248,7 +248,7 @@ describe("create_agent MCP tool", () => {
await tool.callback({ await tool.callback({
cwd: "subdir", cwd: "subdir",
title: "Child", title: "Child",
agentType: "codex", provider: "codex",
initialPrompt: "Do work", initialPrompt: "Do work",
}); });
@@ -287,7 +287,7 @@ describe("create_agent MCP tool", () => {
await tool.callback({ await tool.callback({
cwd: existingCwd, cwd: existingCwd,
title: "Injected config test", title: "Injected config test",
initialMode: "default", mode: "default",
initialPrompt: "Do work", initialPrompt: "Do work",
}); });
+11 -11
View File
@@ -284,7 +284,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
.min(1, "Title is required") .min(1, "Title is required")
.max(60, "Title must be 60 characters or fewer") .max(60, "Title must be 60 characters or fewer")
.describe("Short descriptive title (<= 60 chars) summarizing the agent's focus."), .describe("Short descriptive title (<= 60 chars) summarizing the agent's focus."),
agentType: AgentProviderEnum.optional().describe( provider: AgentProviderEnum.optional().describe(
"Optional agent implementation to spawn. Defaults to 'claude'.", "Optional agent implementation to spawn. Defaults to 'claude'.",
), ),
model: z.string().optional().describe("Model to use (e.g. claude-sonnet-4-20250514)"), model: z.string().optional().describe("Model to use (e.g. claude-sonnet-4-20250514)"),
@@ -321,7 +321,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
.min(1, "Title is required") .min(1, "Title is required")
.max(60, "Title must be 60 characters or fewer") .max(60, "Title must be 60 characters or fewer")
.describe("Short descriptive title (<= 60 chars) summarizing the agent's focus."), .describe("Short descriptive title (<= 60 chars) summarizing the agent's focus."),
agentType: AgentProviderEnum.optional().describe( provider: AgentProviderEnum.optional().describe(
"Optional agent implementation to spawn. Defaults to 'claude'.", "Optional agent implementation to spawn. Defaults to 'claude'.",
), ),
model: z.string().optional().describe("Model to use (e.g. claude-sonnet-4-20250514)"), model: z.string().optional().describe("Model to use (e.g. claude-sonnet-4-20250514)"),
@@ -332,7 +332,10 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
.trim() .trim()
.min(1, "initialPrompt is required") .min(1, "initialPrompt is required")
.describe("Required first task to run immediately after creation."), .describe("Required first task to run immediately after creation."),
initialMode: z.string().describe("Required session mode to configure before the first run."), mode: z
.string()
.optional()
.describe("Optional session mode to configure before the first run."),
worktreeName: z worktreeName: z
.string() .string()
.optional() .optional()
@@ -359,10 +362,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
const createAgentInputSchema = callerAgentId ? agentToAgentInputSchema : topLevelInputSchema; const createAgentInputSchema = callerAgentId ? agentToAgentInputSchema : topLevelInputSchema;
const agentToAgentCreateAgentArgsSchema = z.object(agentToAgentInputSchema); const agentToAgentCreateAgentArgsSchema = z.object(agentToAgentInputSchema);
const topLevelCreateAgentArgsSchema = z.object({ const topLevelCreateAgentArgsSchema = z.object(topLevelInputSchema);
...topLevelInputSchema,
initialMode: topLevelInputSchema.initialMode.optional(),
});
if (options.voiceOnly || options.enableVoiceTools || callerContext?.enableVoiceTools) { if (options.voiceOnly || options.enableVoiceTools || callerContext?.enableVoiceTools) {
server.registerTool( server.registerTool(
@@ -447,7 +447,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
if (callerAgentId) { if (callerAgentId) {
const callerArgs = agentToAgentCreateAgentArgsSchema.parse(args); const callerArgs = agentToAgentCreateAgentArgsSchema.parse(args);
provider = callerArgs.agentType ?? "claude"; provider = callerArgs.provider ?? "claude";
initialPrompt = callerArgs.initialPrompt; initialPrompt = callerArgs.initialPrompt;
background = callerArgs.background ?? false; background = callerArgs.background ?? false;
normalizedTitle = callerArgs.title.trim(); normalizedTitle = callerArgs.title.trim();
@@ -472,7 +472,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
} }
} else { } else {
const topLevelArgs = topLevelCreateAgentArgsSchema.parse(args); const topLevelArgs = topLevelCreateAgentArgsSchema.parse(args);
provider = topLevelArgs.agentType ?? "claude"; provider = topLevelArgs.provider ?? "claude";
initialPrompt = topLevelArgs.initialPrompt; initialPrompt = topLevelArgs.initialPrompt;
background = topLevelArgs.background ?? false; background = topLevelArgs.background ?? false;
normalizedTitle = topLevelArgs.title.trim(); normalizedTitle = topLevelArgs.title.trim();
@@ -480,7 +480,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
thinking = topLevelArgs.thinking; thinking = topLevelArgs.thinking;
labels = topLevelArgs.labels; labels = topLevelArgs.labels;
notifyOnFinish = topLevelArgs.notifyOnFinish ?? false; notifyOnFinish = topLevelArgs.notifyOnFinish ?? false;
const { cwd, initialMode, worktreeName, baseBranch } = topLevelArgs; const { cwd, mode, worktreeName, baseBranch } = topLevelArgs;
resolvedCwd = expandUserPath(cwd); resolvedCwd = expandUserPath(cwd);
@@ -499,7 +499,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
worktreeConfig = worktree; worktreeConfig = worktree;
} }
resolvedMode = initialMode; resolvedMode = mode;
} }
const childAgentDefaultLabels = callerContext?.childAgentDefaultLabels; const childAgentDefaultLabels = callerContext?.childAgentDefaultLabels;
+4 -4
View File
@@ -1006,10 +1006,10 @@ async function getAheadOfOrigin(cwd: string, currentBranch: string): Promise<num
return Number.isNaN(count) ? null : count; return Number.isNaN(count) ? null : count;
} catch { } catch {
try { try {
const { stdout } = await execAsync( const { stdout } = await execAsync(`git rev-list --count ${currentBranch}`, {
`git rev-list --count ${currentBranch}`, cwd,
{ cwd, env: READ_ONLY_GIT_ENV }, env: READ_ONLY_GIT_ENV,
); });
const count = Number.parseInt(stdout.trim(), 10); const count = Number.parseInt(stdout.trim(), 10);
return Number.isNaN(count) ? null : count; return Number.isNaN(count) ? null : count;
} catch { } catch {