feat: 实现 Tier 1 命令级 mock agent

This commit is contained in:
2026-06-09 15:13:32 +08:00
parent 2d5b40379f
commit ac16bfa383

View File

@@ -0,0 +1,119 @@
import { mkdir, writeFile, readFile, rename } from "node:fs/promises";
import { join } from "node:path";
import type { RuneConfig, DocumentConfig } from "../../src/types.ts";
import type { AgentRunner, AgentResult } from "./runner.ts";
import { getChangeDir, getArchiveDir } from "../../src/core/config.ts";
import { parseTasks } from "../../src/core/task-parser.ts";
export class CommandLevelRunner implements AgentRunner {
readonly tier = 1;
async runPlan(
projectDir: string,
changeName: string,
docName: string,
config: RuneConfig,
): Promise<AgentResult> {
const changeDir = getChangeDir(projectDir, changeName);
await mkdir(changeDir, { recursive: true });
const planStage = config.stages.plan;
if (!planStage) {
throw new Error("plan 阶段未配置");
}
const docConfig = planStage.documents.find((d) => d.name === docName);
if (!docConfig) {
throw new Error(`文档 "${docName}" 未在 plan.documents 中配置`);
}
const content = this.renderDocument(docConfig, changeName);
const filePath = join(changeDir, `${docName}.md`);
await writeFile(filePath, content);
return { files: [`${docName}.md`] };
}
async runBuild(
projectDir: string,
changeName: string,
_config: RuneConfig,
): Promise<AgentResult> {
const changeDir = getChangeDir(projectDir, changeName);
const taskPath = join(changeDir, "task.md");
let taskContent: string;
try {
taskContent = await readFile(taskPath, "utf-8");
} catch {
throw new Error(`变更 "${changeName}" 的 task.md 不存在,请先完成规划`);
}
const tasks = parseTasks(taskContent);
const pending = tasks.filter((t) => !t.checked);
if (pending.length === 0) {
return { files: [] };
}
const files: string[] = [];
for (const task of pending) {
const oldLine = `- [ ] ${task.text}`;
const newLine = `- [x] ${task.text}`;
taskContent = taskContent.replace(oldLine, newLine);
const implFile = `${task.text
.replace(/[^a-zA-Z\u4e00-\u9fa5]+/g, "-")
.replace(/^-|-$/g, "")
.toLowerCase()}.ts`;
await writeFile(join(changeDir, implFile), `// ${task.text}\n`);
files.push(implFile);
}
await writeFile(taskPath, taskContent);
files.push("task.md");
return { files };
}
async runArchive(
projectDir: string,
changeName: string,
_config: RuneConfig,
): Promise<AgentResult> {
const changeDir = getChangeDir(projectDir, changeName);
const taskPath = join(changeDir, "task.md");
try {
const taskContent = await readFile(taskPath, "utf-8");
const tasks = parseTasks(taskContent);
const pending = tasks.filter((t) => !t.checked);
if (pending.length > 0) {
throw new Error(`变更 "${changeName}" 存在 ${pending.length} 个未完成任务,无法归档`);
}
} catch (e) {
if (e instanceof Error && e.message.includes("未完成任务")) {
throw e;
}
}
const today = new Date().toISOString().slice(0, 10);
const archiveDir = getArchiveDir(projectDir);
await mkdir(archiveDir, { recursive: true });
const dest = join(archiveDir, `${today}-${changeName}`);
await rename(changeDir, dest);
return { files: [] };
}
private renderDocument(doc: DocumentConfig, changeName: string): string {
if (doc.template) {
return doc.template.replace(/\{\{change-name\}\}/g, changeName) + "\n";
}
return `# ${doc.name}\n\n${doc.prompt}\n`;
}
}
export function createRunner(): AgentRunner {
return new CommandLevelRunner();
}