import { existsSync } from "node:fs"; import { readFile } from "node:fs/promises"; import { join } from "node:path"; import type { RuneConfig } from "../types.ts"; import { CommandError } from "../cli/errors.ts"; import { getChangeDir } from "./config.ts"; import { parseTasks, validateTaskFormat } from "./task-parser.ts"; import { applyCommandPrefix, getPmPrefix } from "./pm.ts"; export function assembleDiscussPrompt(config: RuneConfig): string { const discuss = config.stages.discuss; if (!discuss) throw new CommandError("讨论阶段未配置", { hint: "请在 .rune/config.yaml 中配置 stages.discuss", }); return applyCommandPrefix(discuss.prompt, config); } export async function assemblePlanPrompt( config: RuneConfig, projectRoot: string, changeName: string, documentName: string, ): Promise { const plan = config.stages.plan; if (!plan) throw new CommandError("规划阶段未配置", { hint: "请在 .rune/config.yaml 中配置 stages.plan", }); const doc = plan.documents.find((d) => d.name === documentName); if (!doc) { throw new CommandError(`文档 "${documentName}" 不在配置的 plan.documents 中`, { hint: `可用文档:${plan.documents.map((d) => d.name).join(", ")}`, }); } const changeDir = getChangeDir(projectRoot, changeName); const parts: string[] = []; parts.push(`# 规划阶段:${changeName}`); parts.push(""); parts.push(`## 文档:${doc.name}.md`); parts.push(doc.prompt); const docPath = join(changeDir, `${doc.name}.md`); if (existsSync(docPath)) { parts.push(`\n文档已存在,请先读取 ${docPath} 查看已有内容,在此基础上修订。`); } if (doc.template) { parts.push(`\n### 格式模板:\n${doc.template}`); } if (doc.depend && doc.depend.length > 0) { parts.push("\n---\n"); parts.push("## 依赖说明\n"); parts.push("本文档依赖以下前置文档:"); for (const dep of doc.depend) { const depPath = join(changeDir, `${dep}.md`); const depCompleted = existsSync(depPath); const status = depCompleted ? "已完成" : "未完成"; parts.push(`- ${dep}.md(${status})`); } parts.push("\n请先阅读已完成的前置文档,确保内容一致。"); } parts.push(`\n请将文档写入目录:${changeDir}`); return applyCommandPrefix(parts.join("\n"), config); } export async function assembleBuildPrompt( config: RuneConfig, projectRoot: string, changeName: string, ): Promise { const build = config.stages.build; if (!build) { throw new CommandError("构建阶段未配置", { hint: "请在 .rune/config.yaml 中配置 stages.build", }); } if (!config.metadata?.tracked) { return applyCommandPrefix(build.prompt, config); } const changeDir = getChangeDir(projectRoot, changeName); const taskPath = join(changeDir, "task.md"); let taskContent: string; try { taskContent = await readFile(taskPath, "utf-8"); } catch { const prefix = getPmPrefix(config); throw new CommandError(`变更 "${changeName}" 尚未完成规划,task.md 不存在`, { hint: `请先完成规划阶段:${prefix} plan ${changeName} task 生成任务文档`, }); } validateTaskFormat(taskContent); const tasks = parseTasks(taskContent); const pendingTasks = tasks.filter((t) => !t.checked); if (pendingTasks.length === 0) { return `所有任务已完成。变更 "${changeName}" 可以归档。`; } const parts: string[] = []; parts.push(`# 构建阶段:${changeName}\n`); parts.push(build.prompt); parts.push(`\n请先读取 ${taskPath} 查看任务列表。`); parts.push(`当前有 ${pendingTasks.length} 个待执行任务,从第一个未完成的任务开始。`); parts.push(`完成后更新 ${taskPath} 中的 checkbox。`); return applyCommandPrefix(parts.join("\n"), config); } export async function assembleArchivePrompt( config: RuneConfig, projectRoot: string, changeName: string, ): Promise { const archive = config.stages.archive; if (!archive) throw new CommandError("归档阶段未配置", { hint: "请在 .rune/config.yaml 中配置 stages.archive", }); const parts: string[] = []; parts.push(`# 归档阶段:${changeName}\n`); if (config.metadata?.tracked) { const changeDir = getChangeDir(projectRoot, changeName); const taskPath = join(changeDir, "task.md"); if (existsSync(taskPath)) { try { const taskContent = await readFile(taskPath, "utf-8"); const tasks = parseTasks(taskContent); const incompleteTasks = tasks.filter((t) => !t.checked); if (incompleteTasks.length > 0) { parts.push("## ⚠️ 警告:存在未完成的任务\n"); parts.push(`请先读取 ${taskPath} 检查是否有未完成的任务。`); parts.push("如有未完成任务,询问用户是否确认在任务未全部完成的情况下归档。"); parts.push("如用户确认,则继续归档;否则中止并返回构建阶段。"); parts.push(""); } } catch { // task.md 读取失败时不追加警告 } } } parts.push(archive.prompt); return applyCommandPrefix(parts.join("\n"), config); }