Skip to content
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4d26627
feat: add headless command surface
chengluyu Jun 5, 2026
64f0b61
feat: add headless status helpers
chengluyu Jun 5, 2026
7985cc4
feat: add session run locks
chengluyu Jun 5, 2026
f65409f
feat: wire headless status commands
chengluyu Jun 5, 2026
c3513f8
feat: run headless prompt turns
chengluyu Jun 5, 2026
f7848c5
feat: run headless goals
chengluyu Jun 5, 2026
51deeef
feat: handle headless plan approvals
chengluyu Jun 5, 2026
10e2689
fix: serialize headless status writes
chengluyu Jun 5, 2026
bc5b8a9
docs: document headless mode
chengluyu Jun 5, 2026
4eb336e
fix: mark headless runs cancelled on signals
chengluyu Jun 5, 2026
9c78557
docs: record headless trials
chengluyu Jun 5, 2026
cc0beeb
fix: recreate headless output parents
chengluyu Jun 6, 2026
3e34f0b
docs: add headless complex app trials plan
chengluyu Jun 6, 2026
5878704
fix: restore turn ids across resume
chengluyu Jun 6, 2026
2552176
docs: add headless trial wait protocol
chengluyu Jun 7, 2026
b2ec4f7
Merge origin's main in
chengluyu Jun 7, 2026
4ae0154
Merge origin's main in
chengluyu Jun 9, 2026
81e2247
fix(cli): report headless interrupts correctly
chengluyu Jun 9, 2026
1ca95c0
fix(cli): ignore stale headless control requests
chengluyu Jun 9, 2026
4b96f46
fix(cli): keep headless goals running between turns
chengluyu Jun 9, 2026
867cac9
fix(sdk): recover stale corrupt session locks
chengluyu Jun 9, 2026
fe0ab8e
fix(cli): show headless input errors with help
chengluyu Jun 9, 2026
e3b9726
docs(cli): expand headless help guidance
chengluyu Jun 9, 2026
12fa0c3
chore: sanitize headless plan paths
chengluyu Jun 9, 2026
eb22b25
fix(cli): honor headless plan flags
chengluyu Jun 9, 2026
387cac0
fix(cli): return nonzero for blocked headless goals
chengluyu Jun 9, 2026
89fbf31
Merge remote-tracking branch 'origin/main' into feat/go-headless-first
chengluyu Jun 9, 2026
3b00a40
fix(ci): resolve headless merge fallout
chengluyu Jun 9, 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
7 changes: 7 additions & 0 deletions .changeset/headless-scripted-runs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@moonshot-ai/kimi-code": minor
"@moonshot-ai/kimi-code-sdk": minor
"@moonshot-ai/agent-core": minor
---

Add headless CLI runs with status polling, file output, goal control, and session run locking.
4 changes: 4 additions & 0 deletions apps/kimi-code/src/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { registerMigrateCommand } from '#/migration/index';
import { Command, Option } from 'commander';

import type { CLIOptions } from './options';
import { registerHeadlessCommand, type HeadlessCommandHandler } from './headless/commands';
import { registerAcpCommand } from './sub/acp';
import { registerDoctorCommand } from './sub/doctor';
import { registerExportCommand } from './sub/export';
Expand All @@ -20,11 +21,13 @@ export function createProgram(
onMigrate: MigrateCommandHandler,
onPluginNodeRunner: PluginNodeRunnerHandler = () => {},
onUpgrade: UpgradeCommandHandler = () => {},
onHeadless: HeadlessCommandHandler = () => {},
): Command {
const program = new Command(CLI_COMMAND_NAME)
.description('The Starting Point for Next-Gen Agents')
.version(version, '-V, --version')
.allowUnknownOption(false)
.enablePositionalOptions()
.configureHelp({ helpWidth: 100 })
.helpOption('-h, --help', 'Show help.')
.usage('[options] [command]')
Expand Down Expand Up @@ -81,6 +84,7 @@ export function createProgram(
registerLoginCommand(program);
registerDoctorCommand(program);
registerMigrateCommand(program, onMigrate);
registerHeadlessCommand(program, onHeadless);
program
.command('upgrade')
.description('Upgrade Kimi Code to the latest version.')
Expand Down
78 changes: 78 additions & 0 deletions apps/kimi-code/src/cli/headless/approval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { ApprovalHandler, ApprovalRequest, ApprovalResponse } from '@moonshot-ai/kimi-code-sdk';

import type { HeadlessApprovalStatus } from './status-file';
import type { HeadlessWarning } from './status-file';

export interface HeadlessApprovalOptions {
readonly approvePlan: boolean;
readonly rejectPlan: boolean;
readonly onPlanApprovalRequired: (approval: HeadlessApprovalStatus) => void;
}

export function createHeadlessApprovalHandler(options: HeadlessApprovalOptions): ApprovalHandler {
return (request) => {
if (!isPlanApprovalRequest(request)) return { decision: 'approved' };

const approval: HeadlessApprovalStatus = {
kind: 'plan',
toolCallId: request.toolCallId,
decision: options.approvePlan ? 'approved' : options.rejectPlan ? 'rejected' : 'required',
decidedByFlag: options.approvePlan ? 'approve-plan' : options.rejectPlan ? 'reject-plan' : null,
message: options.approvePlan
? 'Plan approved by --approve-plan.'
: options.rejectPlan
? 'Plan rejected by --reject-plan.'
: 'rerun with --approve-plan or --reject-plan',
};
options.onPlanApprovalRequired(approval);

if (options.approvePlan) {
return approvePlanRequest(request);
}
if (options.rejectPlan) {
return {
decision: 'rejected',
selectedLabel: 'Reject and Exit',
feedback: 'Rejected by --reject-plan.',
};
}
return {
decision: 'cancelled',
feedback: 'Plan approval requires --approve-plan or --reject-plan in headless mode.',
};
};
}

export function getUnusedPlanFlagWarning(options: {
readonly approvePlan: boolean;
readonly rejectPlan: boolean;
readonly planApprovalSeen: boolean;
}): HeadlessWarning | null {
if (options.planApprovalSeen) return null;
if (options.approvePlan) {
return {
code: 'PLAN_FLAG_UNUSED',
message: '--approve-plan was set, but no plan approval was requested.',
};
}
if (options.rejectPlan) {
return {
code: 'PLAN_FLAG_UNUSED',
message: '--reject-plan was set, but no plan approval was requested.',
};
}
return null;
}

function isPlanApprovalRequest(request: ApprovalRequest): boolean {
return request.toolName === 'ExitPlanMode' || request.display.kind === 'plan_review';
}

function approvePlanRequest(request: ApprovalRequest): ApprovalResponse {
const firstOption =
request.display.kind === 'plan_review' ? request.display.options?.[0]?.label : undefined;
return {
decision: 'approved',
selectedLabel: firstOption,
};
}
19 changes: 19 additions & 0 deletions apps/kimi-code/src/cli/headless/atomic-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { mkdir, open, rename } from 'node:fs/promises';
import path from 'node:path';

export async function writeAtomicTextFile(filePath: string, text: string): Promise<void> {
await mkdir(path.dirname(filePath), { recursive: true });
const tempPath = `${filePath}.tmp`;
const handle = await open(tempPath, 'w');
try {
await handle.writeFile(text, 'utf8');
await handle.sync();
} finally {
await handle.close();
}
await rename(tempPath, filePath);
}

export async function writeAtomicJsonFile(filePath: string, value: unknown): Promise<void> {
await writeAtomicTextFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
}
Loading