Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b7db693
feat(kimi-code): add -w, --worktree [name] flag for isolated sessions
rrva Jun 16, 2026
3974aa3
docs(reference): document -w, --worktree flag in EN and ZH
rrva Jun 16, 2026
c61f5d5
feat(kimi-code): add bundled word lists for --worktree auto-generated…
rrva Jun 16, 2026
3bbf152
fix(kimi-code): harden --worktree startup cleanup and name validation
rrva Jun 16, 2026
a74eb19
fix(kimi-code): correct --worktree cleanup and prepareWorktree signature
rrva Jun 16, 2026
91be1d7
feat(agent-core): surface worktree metadata in agent system prompt
rrva Jun 16, 2026
6a86ada
fix(kimi-code): address Codex review feedback on --worktree flag
rrva Jun 16, 2026
ab83d9f
fix(kimi-code): preserve worktree metadata for /new sessions
rrva Jun 16, 2026
4710074
fix(kimi-code): address second round of Codex review feedback
rrva Jun 16, 2026
42902f6
fix(kimi-code): address third round of Codex review feedback
rrva Jun 16, 2026
950156e
feat(kimi-code): add -w, --worktree [name] flag for isolated sessions
rrva Jun 16, 2026
b8a2539
docs(reference): document -w, --worktree flag in EN and ZH
rrva Jun 16, 2026
751c395
feat(kimi-code): add bundled word lists for --worktree auto-generated…
rrva Jun 16, 2026
389a09b
fix(kimi-code): harden --worktree startup cleanup and name validation
rrva Jun 16, 2026
dfd32f2
fix(kimi-code): correct --worktree cleanup and prepareWorktree signature
rrva Jun 16, 2026
d24988a
feat(agent-core): surface worktree metadata in agent system prompt
rrva Jun 16, 2026
a80cdc2
fix(kimi-code): address Codex review feedback on --worktree flag
rrva Jun 16, 2026
b1e69b4
fix(kimi-code): preserve worktree metadata for /new sessions
rrva Jun 16, 2026
3c0958f
fix(kimi-code): address second round of Codex review feedback
rrva Jun 16, 2026
5f4ea9f
fix(kimi-code): address third round of Codex review feedback
rrva Jun 16, 2026
35f9612
Merge branch 'main' into feat/worktree-cli-flag
rrva Jun 18, 2026
8e4c290
Merge branch 'fork/feat/worktree-cli-flag' into feat/worktree-cli-flag
rrva Jun 18, 2026
0379c3a
Merge branch 'main' into feat/worktree-cli-flag
rrva Jun 22, 2026
3cea709
Merge branch 'main' into feat/worktree-cli-flag
rrva Jun 23, 2026
e60c17f
Merge branch 'main' into feat/worktree-cli-flag
rrva Jun 25, 2026
44b1296
refactor(kimi-code): isolate --worktree into feature modules
rrva Jun 25, 2026
49fa0b2
fix(kimi-code): address worktree review findings
rrva Jun 26, 2026
2a6e0aa
Merge remote-tracking branch 'origin/main' into feat/worktree-cli-flag
rrva Jun 26, 2026
f04656d
Merge branch 'main' into feat/worktree-cli-flag
rrva Jun 30, 2026
043de86
Merge branch 'main' into feat/worktree-cli-flag
rrva Jul 1, 2026
9cd5098
Merge branch 'main' into feat/worktree-cli-flag
rrva Jul 2, 2026
180c924
Merge remote-tracking branch 'origin/main' into feat/worktree-cli-flag
rrva Jul 3, 2026
b2a5dd9
Merge branch 'main' into feat/worktree-cli-flag
rrva Jul 5, 2026
7c4b24d
Merge branch 'main' into feat/worktree-cli-flag
rrva Jul 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/add-worktree-flag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/agent-core": minor
"@moonshot-ai/kimi-code": minor
---

Add `-w, --worktree [name]` flag to create a new git worktree for the session, and surface the worktree metadata in the agent system prompt.
12 changes: 11 additions & 1 deletion apps/kimi-code/src/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,13 @@ export function createProgram(
)
.addOption(new Option('--yes').hideHelp().default(false))
.addOption(new Option('--auto-approve').hideHelp().default(false))
.option('--plan', 'Start in plan mode.', false);
.option('--plan', 'Start in plan mode.', false)
.addOption(
new Option(
'-w, --worktree [name]',
'Create a new git worktree for this session (optionally specify a name).',
).argParser((val: string | boolean) => (val === true ? '' : (val as string))),
);

registerExportCommand(program);
registerProviderCommand(program);
Expand Down Expand Up @@ -111,6 +117,9 @@ export function createProgram(
const yoloValue = raw['yolo'] === true || raw['yes'] === true || raw['autoApprove'] === true;
const autoValue = raw['auto'] === true;

const rawWorktree = raw['worktree'];
const worktreeValue = rawWorktree === true ? '' : (rawWorktree as string | undefined);

const opts: CLIOptions = {
session: sessionValue,
continue: raw['continue'] as boolean,
Expand All @@ -121,6 +130,7 @@ export function createProgram(
outputFormat: raw['outputFormat'] as CLIOptions['outputFormat'],
prompt: raw['prompt'] as string | undefined,
skillsDirs: raw['skillsDir'] as string[],
worktree: worktreeValue,
};

onMain(opts);
Expand Down
11 changes: 11 additions & 0 deletions apps/kimi-code/src/cli/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export interface CLIOptions {
outputFormat: PromptOutputFormat | undefined;
prompt: string | undefined;
skillsDirs: string[];
worktree?: string;
/** Populated during startup when --worktree is used. */
worktreePath?: string;
/** Populated during startup when --worktree is used. */
parentRepoPath?: string;
}

export interface ValidatedOptions {
Expand Down Expand Up @@ -55,5 +60,11 @@ export function validateOptions(opts: CLIOptions): ValidatedOptions {
if (opts.yolo && opts.auto) {
throw new OptionConflictError('Cannot combine --yolo with --auto.');
}
if (opts.worktree !== undefined && opts.session !== undefined) {
throw new OptionConflictError('Cannot combine --worktree with --session.');
}
if (opts.worktree !== undefined && opts.continue) {
throw new OptionConflictError('Cannot combine --worktree with --continue.');
}
return { options: opts, uiMode: promptMode ? 'print' : 'shell' };
}
17 changes: 14 additions & 3 deletions apps/kimi-code/src/cli/run-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export async function runPrompt(
} else {
await runPromptTurn(session, opts.prompt!, outputFormat, stdout, stderr);
}
writeResumeHint(session.id, outputFormat, stdout, stderr);
writeResumeHint(session.id, outputFormat, stdout, stderr, opts.worktreePath !== undefined ? workDir : undefined);

withTelemetryContext({ sessionId: session.id }).track('exit', {
duration_s: (Date.now() - startedAt) / 1000,
Expand Down Expand Up @@ -290,7 +290,16 @@ async function resolvePromptSession(
}

const model = requireConfiguredModel(opts.model, defaultModel);
const session = await harness.createSession({ workDir, model, permission: 'auto' });
const metadata =
opts.worktreePath !== undefined && opts.parentRepoPath !== undefined
? { worktreePath: opts.worktreePath, parentRepoPath: opts.parentRepoPath }
: undefined;
// Note: --prompt mode intentionally does not auto-remove the worktree on
// exit. Unlike the TUI, a prompt session always produces at least a user
// prompt and assistant response, so the "empty session" cleanup rule does
// not apply; leaving the worktree makes the non-interactive output
// inspectable after the fact.
const session = await harness.createSession({ workDir, model, permission: 'auto', metadata });
installHeadlessHandlers(session);
return {
session,
Expand Down Expand Up @@ -589,8 +598,10 @@ function writeResumeHint(
outputFormat: PromptOutputFormat,
stdout: PromptOutput,
stderr: PromptOutput,
workDir?: string,
): void {
const command = `kimi -r ${sessionId}`;
const command =
workDir !== undefined ? `cd "${workDir}" && kimi -r ${sessionId}` : `kimi -r ${sessionId}`;
const content = `To resume this session: ${command}`;
if (outputFormat === 'stream-json') {
const message: PromptJsonResumeMetaMessage = {
Expand Down
23 changes: 21 additions & 2 deletions apps/kimi-code/src/cli/run-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { execSync } from 'node:child_process';
import { homedir } from 'node:os';
import { join } from 'node:path';

import { removeWorktree } from '#/utils/git/worktree';

import {
setCrashPhase,
setTelemetryContext,
Expand Down Expand Up @@ -133,14 +135,31 @@ export async function runShell(

tui.onExit = async (exitCode = 0) => {
const sessionId = tui.getCurrentSessionId();
const hasContent = tui.hasSessionContent();
const hasContent = tui.hasEverHadSessionContent();
setCrashPhase('shutdown');
trackLifecycle('exit', { duration_s: (Date.now() - startedAt) / 1000 });

// Clean up the git worktree for empty sessions so abandoned worktrees
// do not accumulate. Use the lifetime flag so `/new` does not delete a
// worktree that held an earlier session.
if (!hasContent && opts.worktreePath !== undefined && opts.parentRepoPath !== undefined) {
try {
removeWorktree(opts.parentRepoPath, opts.worktreePath);
Comment thread
rrva marked this conversation as resolved.
Outdated
} catch (cleanupError) {
// Best-effort cleanup only; do not let cleanup failures prevent a clean exit.
log.warn('Failed to clean up git worktree on exit', cleanupError);
}
}

await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS });
const gutter = ' '.repeat(CHROME_GUTTER);
process.stdout.write(`${gutter}Bye!\n`);
if (sessionId !== '' && hasContent) {
process.stderr.write(`\n${gutter}To resume this session: kimi -r ${sessionId}\n`);
const resumeCommand =
opts.worktreePath !== undefined
? `cd "${process.cwd()}" && kimi -r ${sessionId}`
Comment thread
rrva marked this conversation as resolved.
Outdated
: `kimi -r ${sessionId}`;
process.stderr.write(`\n${gutter}To resume this session: ${resumeCommand}\n`);
}
process.exit(exitCode);
};
Expand Down
60 changes: 56 additions & 4 deletions apps/kimi-code/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import type { CLIOptions } from './cli/options';
import { OptionConflictError, validateOptions } from './cli/options';
import { runPrompt } from './cli/run-prompt';
import { runShell } from './cli/run-shell';
import { relative, resolve } from 'node:path';
import { createWorktree, findGitRoot, removeWorktree, WorktreeError } from './utils/git/worktree';
import { formatStartupError } from './cli/startup-error';
import { runPluginNodeEntry } from './cli/sub/plugin-run-node';
import { handleUpgrade } from './cli/sub/upgrade';
Expand All @@ -38,6 +40,22 @@ import { cleanupStaleNativeCacheForCurrent } from './native/native-assets';
import { installNativeModuleHook } from './native/module-hook';
import { runNativeAssetSmokeIfRequested } from './native/smoke';

function prepareWorktree(worktreeName: string): { worktreePath: string; parentRepoPath: string } {
const cwd = process.cwd();
const repoRoot = findGitRoot(cwd);
if (repoRoot === null) {
throw new WorktreeError('--worktree requires the working directory to be inside a git repository.');
}
const worktreePath = createWorktree(repoRoot, worktreeName || undefined);
const relativeCwd = relative(repoRoot, cwd);
const targetCwd =
relativeCwd.length > 0 && relativeCwd !== '.'
? resolve(worktreePath, relativeCwd)
: worktreePath;
process.chdir(targetCwd);
Comment thread
rrva marked this conversation as resolved.
Outdated
return { worktreePath, parentRepoPath: repoRoot };
}

export async function handleMainCommand(opts: CLIOptions, version: string): Promise<void> {
let validated: ReturnType<typeof validateOptions>;
try {
Expand All @@ -58,12 +76,46 @@ export async function handleMainCommand(opts: CLIOptions, version: string): Prom
process.exit(0);
}

if (validated.uiMode === 'print') {
await runPrompt(validated.options, version);
return;
if (opts.worktree !== undefined) {
try {
const { worktreePath, parentRepoPath } = await prepareWorktree(opts.worktree);
Comment thread
rrva marked this conversation as resolved.
Outdated
opts.worktreePath = worktreePath;
opts.parentRepoPath = parentRepoPath;
} catch (error) {
if (error instanceof WorktreeError) {
process.stderr.write(`error: ${error.message}\n`);
process.exit(1);
}
throw error;
}
}

await runShell(validated.options, version);
try {
if (validated.uiMode === 'print') {
await runPrompt(validated.options, version);
return;
}

await runShell(validated.options, version);
} catch (error) {
// If the shell runner failed during startup after we created a worktree,
// the worktree is still empty (no session ran), so clean it up to avoid
// leaks. Print mode intentionally leaves the worktree inspectable even on
// failure, so we do not clean it up here.
if (
validated.uiMode !== 'print' &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clean up prompt worktrees before session creation

When kimi -w -p ... fails before runPrompt creates a session, for example because no model is configured and requireConfiguredModel throws, this condition skips cleanup solely because the mode is print. At that point there is no saved prompt session or output to inspect, so the empty registered worktree under .kimi/worktrees/ is left behind on every retry; only skip cleanup for print-mode failures after a prompt session has actually been created.

Useful? React with 👍 / 👎.

opts.worktreePath !== undefined &&
opts.parentRepoPath !== undefined
) {
try {
removeWorktree(opts.parentRepoPath, opts.worktreePath);
} catch (cleanupError) {
// Best-effort cleanup only; do not let cleanup failures mask the original error.
log.warn('Failed to clean up git worktree after runner startup failed', cleanupError);
}
}
throw error;
}
}

/** `kimi migrate`: launch the migration screen only, then exit. */
Expand Down
32 changes: 31 additions & 1 deletion apps/kimi-code/src/tui/kimi-tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
ApprovalResponse,
BackgroundTaskInfo,
CreateSessionOptions,
JsonObject,
KimiHarness,
PermissionMode,
PromptPart,
Expand Down Expand Up @@ -195,6 +196,16 @@ function createInitialAppState(input: KimiTUIStartupInput): AppState {
};
}

function buildSessionMetadata(cliOptions: CLIOptions): JsonObject | undefined {
if (cliOptions.worktreePath === undefined || cliOptions.parentRepoPath === undefined) {
return undefined;
}
return {
worktreePath: cliOptions.worktreePath,
parentRepoPath: cliOptions.parentRepoPath,
};
}

interface SendMessageOptions {
readonly parts?: readonly PromptPart[];
readonly imageAttachmentIds?: readonly number[];
Expand Down Expand Up @@ -228,6 +239,7 @@ export class KimiTUI {
private startupNotice: string | undefined;
private lastActivityMode: string | undefined;
private lastHistoryContent: string | undefined;
private everHadSessionContent = false;
readonly streamingUI: StreamingUIController;
readonly authFlow: AuthFlowController;
readonly btwPanelController: BtwPanelController;
Expand Down Expand Up @@ -268,6 +280,7 @@ export class KimiTUI {
plan: startupInput.cliOptions.plan,
model: startupInput.cliOptions.model,
startupNotice: startupInput.startupNotice,
metadata: buildSessionMetadata(startupInput.cliOptions),
Comment thread
rrva marked this conversation as resolved.
Outdated
},
};
this.options = tuiOptions;
Expand Down Expand Up @@ -562,6 +575,7 @@ export class KimiTUI {
model: startup.model,
permission: startup.auto ? 'auto' : startup.yolo ? 'yolo' : undefined,
planMode: startup.plan ? true : undefined,
metadata: startup.metadata,
};

try {
Expand Down Expand Up @@ -1023,7 +1037,23 @@ export class KimiTUI {
}

hasSessionContent(): boolean {
return this.state.transcriptEntries.length > 0;
const hasContent = this.state.transcriptEntries.length > 0;
if (hasContent) {
this.everHadSessionContent = true;
}
return hasContent;
}

/**
* Whether any session in this TUI lifetime has had transcript content.
*
* Used for worktree cleanup: after `/new` the current session may be empty,
* but we must not delete a worktree that held an earlier session.
*/
hasEverHadSessionContent(): boolean {
// Refresh the flag in case content was added since the last call.
this.hasSessionContent();
Comment thread
rrva marked this conversation as resolved.
return this.everHadSessionContent;
}

async getStartupMcpMs(): Promise<number> {
Expand Down
2 changes: 2 additions & 0 deletions apps/kimi-code/src/tui/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
GoalChange,
GoalSnapshot,
JsonObject,
ModelAlias,
PermissionMode,
ProviderConfig,
Expand Down Expand Up @@ -199,6 +200,7 @@ export interface TUIStartupOptions {
readonly plan: boolean;
readonly model?: string;
readonly startupNotice?: string;
readonly metadata?: JsonObject;
}

export type TUIStartupState = 'pending' | 'ready' | 'picker';
Expand Down
Loading