import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"; import type { ActiveRun, KdActionCommit, KdContextEntry, KdFact, KdHarnessMode, KdPhase, KdQuestion, KdResumeSnapshot, KdRisk, KdRiskSource, KdSourceAnchorSnapshot, KdToolResultContract, KdWriteTransaction } from "./types.ts"; import { PHASE_ARTIFACTS, nextPhaseForRun, phaseIncludedInRun, phaseOrderForMode, phaseOrderForRun } from "./types.ts"; import { activeRunPath, kdDir, runStatePath, runsDir } from "./paths.ts"; import { defaultArtifactContent, ensureArtifact, ensureRunDirectories, writeArtifact } from "./artifacts.ts"; import { canEnterPhase, inspectGate } from "./gates.ts"; import { appendLedgerEvent } from "./ledger.ts"; import { resolveProductProfile } from "../product/profile.ts"; import { factFromAnsweredQuestion, factKey, inferFactLabelFromQuestion, invalidConfirmationAnswerReason, invalidFactValueReason, upsertFact } from "./question-memory.ts"; import { canonicalFactLabel } from "./prompt-policy.ts"; import { inferHarnessMode } from "./mode-policy.ts"; import { hydrateRun, normalizeQuestionOptions } from "./run-sanitizer.ts"; import { defaultWorkingSet, updateWorkingSetFocus, updateWorkingSetFromTool } from "./working-set.ts"; import { createToolResultContract, type ToolResultContractInput } from "./tool-result-contract.ts"; import { canonicalSourceAnchorPath, createSourceAnchorSnapshot, type SourceAnchorSnapshotInput } from "./source-anchors.ts"; export function readActiveRun(cwd: string): ActiveRun | undefined { const path = activeRunPath(cwd); if (!existsSync(path)) return undefined; try { const active = hydrateRun(JSON.parse(readFileSync(path, "utf8")) as ActiveRun); if (!active) return undefined; return readRun(cwd, active.id) ?? active; } catch { return undefined; } } export function writeActiveRun(cwd: string, run: ActiveRun): void { mkdirSync(kdDir(cwd), { recursive: true }); mkdirSync(runsDir(cwd), { recursive: true }); run.status = "active"; run.updatedAt = new Date().toISOString(); writeRunState(cwd, run); writeFileSync(activeRunPath(cwd), `${JSON.stringify(run, null, 2)}\n`, "utf8"); } export function readRun(cwd: string, id: string): ActiveRun | undefined { const path = runStatePath(cwd, id); if (!existsSync(path)) return undefined; try { return hydrateRun(JSON.parse(readFileSync(path, "utf8")) as ActiveRun); } catch { return undefined; } } function latestRun(cwd: string, run: ActiveRun): ActiveRun { return readRun(cwd, run.id) ?? run; } function syncRunObject(target: ActiveRun, source: ActiveRun): void { Object.assign(target, source); } export function listRuns(cwd: string): ActiveRun[] { const root = runsDir(cwd); if (!existsSync(root)) return []; return readdirSync(root, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry) => readRun(cwd, entry.name)) .filter((run): run is ActiveRun => Boolean(run)) .sort((a, b) => (b.updatedAt ?? b.createdAt ?? b.id).localeCompare(a.updatedAt ?? a.createdAt ?? a.id)); } export function switchActiveRun(cwd: string, id: string): ActiveRun | undefined { const run = readRun(cwd, id); if (!run) return undefined; writeActiveRun(cwd, run); appendLedgerEvent(cwd, run, { type: "run.switched", summary: "切换为 active run。", }); return readActiveRun(cwd); } export function finishActiveRun(cwd: string, run: ActiveRun): ActiveRun { run.status = "done"; run.updatedAt = new Date().toISOString(); run.gate = inspectGate(cwd, run); writeRunState(cwd, run); appendLedgerEvent(cwd, run, { type: "run.finished", summary: "标记 run 完成并清除 active run。", data: { gatePassed: run.gate.passed, gateReason: run.gate.reason }, }); const activePath = activeRunPath(cwd); if (existsSync(activePath)) unlinkSync(activePath); return run; } export function createActiveRun(cwd: string, goal: string, productInput?: string, version?: string, modeInput?: KdHarnessMode): ActiveRun { const profile = resolveProductProfile(productInput); const now = new Date().toISOString(); const mode = modeInput ?? inferHarnessMode(goal); const initialPhase = phaseOrderForMode(mode)[0]; const run: ActiveRun = { id: createRunId(goal), goal, phase: initialPhase, mode, cwd, status: "active", createdAt: now, updatedAt: now, product: profile.product, version, profile, artifacts: {}, questions: [], facts: [], contextEntries: [], toolResults: [], actionCommits: [], writeTransactions: [], sourceAnchors: [], workingSet: defaultWorkingSet({ phase: initialPhase, goal }), gate: { passed: false, reason: "进入下一阶段前必须完成 CONTEXT.md 并确认产品画像", checkedAt: new Date().toISOString(), }, }; ensureRunDirectories(cwd, run); writeArtifact(cwd, run, "discuss", defaultArtifactContent("discuss", goal, profile)); run.gate = inspectGate(cwd, run); writeActiveRun(cwd, run); appendLedgerEvent(cwd, run, { type: "run.created", summary: "创建 active Harness run。", data: { mode: run.mode, product: run.profile?.product ?? run.product, platform: run.profile?.platform, }, }); return run; } export function recordRunContext( cwd: string, run: ActiveRun, input: { text: string; kind?: KdContextEntry["kind"]; sourceRefs?: string[] }, ): KdContextEntry | undefined { const text = input.text.trim(); if (!text) return undefined; const current = latestRun(cwd, run); const existing = Array.isArray(run.contextEntries) ? run.contextEntries : []; const currentExisting = Array.isArray(current.contextEntries) ? current.contextEntries : existing; const duplicate = currentExisting.find((entry) => entry.phase === current.phase && entry.kind === (input.kind ?? "user-input") && normalizeMemoryText(entry.text) === normalizeMemoryText(text)); if (duplicate) return duplicate; const entry: KdContextEntry = { id: createContextEntryId(currentExisting.length + 1), phase: current.phase, kind: input.kind ?? "user-input", text, sourceRefs: input.sourceRefs?.map((source) => source.trim()).filter(Boolean), createdAt: new Date().toISOString(), }; current.contextEntries = [...currentExisting, entry]; writeActiveRun(cwd, current); syncRunObject(run, current); appendLedgerEvent(cwd, current, { type: "context.recorded", summary: trimSummary(text), data: { id: entry.id, kind: entry.kind, sourceRefs: entry.sourceRefs }, }); return entry; } export function recordToolResultContract(cwd: string, run: ActiveRun, input: ToolResultContractInput): KdToolResultContract { const current = latestRun(cwd, run); const existing = Array.isArray(current.toolResults) ? current.toolResults : []; const result = createToolResultContract(input, existing.length + 1); current.toolResults = [...existing, result].slice(-40); current.workingSet = updateWorkingSetFromTool(current, result); writeActiveRun(cwd, current); syncRunObject(run, current); appendLedgerEvent(cwd, current, { type: "tool.result.recorded", summary: result.summary, data: { id: result.id, toolName: result.toolName, status: result.status, kind: result.kind, paths: result.paths, modifiedFiles: result.modifiedFiles, evidencePaths: result.evidencePaths, }, }); return result; } export function recordActionCommit( cwd: string, run: ActiveRun, input: Omit & { phase?: KdPhase }, ): KdActionCommit { const current = latestRun(cwd, run); const existing = Array.isArray(current.actionCommits) ? current.actionCommits : []; const commit: KdActionCommit = { id: `A-${String(existing.length + 1).padStart(3, "0")}`, phase: input.phase ?? current.phase, focus: input.focus, intent: input.intent, allowed: input.allowed, reason: input.reason, requiredAction: input.requiredAction, source: input.source, createdAt: new Date().toISOString(), }; current.actionCommits = [...existing, commit].slice(-60); current.workingSet = updateWorkingSetFocus(current, { focus: commit.focus, activeGap: commit.reason, currentAction: commit.requiredAction, blockedBy: commit.allowed ? undefined : commit.reason, }); writeActiveRun(cwd, current); syncRunObject(run, current); appendLedgerEvent(cwd, current, { type: "action.committed", summary: `${commit.intent} ${commit.allowed ? "allowed" : "blocked"}:${commit.requiredAction}`, data: { id: commit.id, focus: commit.focus, intent: commit.intent, allowed: commit.allowed, source: commit.source }, }); return commit; } export function recordWriteTransaction(cwd: string, run: ActiveRun, transaction: KdWriteTransaction): KdWriteTransaction { const current = latestRun(cwd, run); const existing = Array.isArray(current.writeTransactions) ? current.writeTransactions : []; const next = [...existing.filter((item) => item.id !== transaction.id), transaction].slice(-30); current.writeTransactions = next; writeActiveRun(cwd, current); syncRunObject(run, current); appendLedgerEvent(cwd, current, { type: "write.transaction.recorded", summary: `${transaction.id} ${transaction.status}:${transaction.path}`, data: { id: transaction.id, path: transaction.path, status: transaction.status, checks: transaction.checks, reason: transaction.reason }, }); return transaction; } export function recordSourceAnchorSnapshot(cwd: string, run: ActiveRun, input: SourceAnchorSnapshotInput): KdSourceAnchorSnapshot { const current = latestRun(cwd, run); const existing = Array.isArray(current.sourceAnchors) ? current.sourceAnchors : []; const snapshot = createSourceAnchorSnapshot({ ...input, path: canonicalSourceAnchorPath(cwd, input.path) }, existing.length + 1); const snapshotKey = snapshot.path.replace(/\\/g, "/").toLowerCase(); current.sourceAnchors = [...existing.filter((item) => canonicalSourceAnchorPath(cwd, item.path).replace(/\\/g, "/").toLowerCase() !== snapshotKey), snapshot].slice(-40); writeActiveRun(cwd, current); syncRunObject(run, current); appendLedgerEvent(cwd, current, { type: "source.anchor.recorded", summary: `${snapshot.id} ${snapshot.path}:${snapshot.refs.length} anchors`, data: { id: snapshot.id, path: snapshot.path, fileHash: snapshot.fileHash, refs: snapshot.refs.slice(0, 12).map((ref) => `${ref.line}#${ref.hash}`) }, }); return snapshot; } export function addQuestion( cwd: string, run: ActiveRun, input: { question: string; reason?: string; contextSummary?: string; sourceRefs?: string[]; factLabel?: string; proposedFactValue?: string; options?: Array<{ label: string; description?: string }>; choices?: string[]; multiple?: boolean; customAnswer?: boolean; blocking?: boolean; }, ): KdQuestion { const existing = Array.isArray(run.questions) ? run.questions : []; const canonicalLabel = input.factLabel?.trim() ? canonicalFactLabel(input.factLabel) : inferFactLabelFromQuestion(input.question); const options = normalizeQuestionOptions(input.options, input.choices); const duplicate = existing.find( (question) => question.status === "open" && question.blocking && ((canonicalLabel && question.factLabel && factKey(question.factLabel) === factKey(canonicalLabel)) || normalizeQuestion(question.question) === normalizeQuestion(input.question)), ); if (duplicate) return duplicate; const question: KdQuestion = { id: createQuestionId(existing.length + 1), phase: run.phase, question: input.question.trim(), reason: input.reason?.trim() || undefined, contextSummary: input.contextSummary?.trim() || undefined, sourceRefs: input.sourceRefs?.map((source) => source.trim()).filter(Boolean), factLabel: canonicalLabel, proposedFactValue: input.proposedFactValue?.trim() || undefined, options, choices: options?.map((option) => option.label), multiple: input.multiple === true, customAnswer: input.customAnswer !== false, blocking: input.blocking ?? true, status: "open", createdAt: new Date().toISOString(), }; const priorGateReason = run.gate.reason; run.questions = [...existing, question]; if (question.blocking && run.resumeSnapshot?.status !== "open") { run.resumeSnapshot = createResumeSnapshot(run, question, input.contextSummary, input.sourceRefs, priorGateReason); } run.gate = inspectGate(cwd, run); run.workingSet = workingSetAfterQuestionAsked(run, question); writeActiveRun(cwd, run); appendLedgerEvent(cwd, run, { type: "question.asked", summary: trimSummary(question.question), data: { id: question.id, blocking: question.blocking, factLabel: question.factLabel, sourceRefs: question.sourceRefs, }, }); return question; } export function answerQuestion(cwd: string, run: ActiveRun, id: string, answer: string): KdQuestion | undefined { const question = (Array.isArray(run.questions) ? run.questions : []).find((item) => item.id === id); if (!question) return undefined; if (question.status !== "open") return undefined; const trimmed = answer.trim(); if (!trimmed) return undefined; if (question.factLabel && question.proposedFactValue && invalidConfirmationAnswerReason(trimmed)) return undefined; if (question.factLabel && !question.proposedFactValue && invalidFactValueReason(trimmed)) return undefined; question.status = "answered"; question.answer = trimmed; question.answeredAt = new Date().toISOString(); applyQuestionFact(run, question); applyRepairQuestionAnswer(cwd, run, question); run.resumeSnapshot = answerResumeSnapshot(run.resumeSnapshot, question); run.gate = inspectGate(cwd, run); run.workingSet = workingSetAfterQuestionAnswered(run, question); writeActiveRun(cwd, run); appendLedgerEvent(cwd, run, { type: "question.answered", summary: trimSummary(question.question), data: { id: question.id, factLabel: question.factLabel, answer: trimSummary(trimmed, 120), }, }); return question; } export function reviseFact( cwd: string, run: ActiveRun, input: { factLabel: string; value: string; reason?: string }, ): KdFact | undefined { const label = input.factLabel.trim(); const value = input.value.trim(); if (!label || !value) return undefined; const canonicalLabel = canonicalFactLabel(label); if (!canonicalLabel) return undefined; if (invalidFactValueReason(value)) return undefined; const now = new Date().toISOString(); const fact: KdFact = { key: factKey(canonicalLabel), label: canonicalLabel, value, status: "current", source: "manual", reason: input.reason?.trim() || undefined, createdAt: now, updatedAt: now, }; run.facts = upsertFact(run.facts, fact); run.gate = inspectGate(cwd, run); run.workingSet = workingSetAfterFactRevision(run, fact); writeActiveRun(cwd, run); appendLedgerEvent(cwd, run, { type: "fact.revised", summary: `${fact.label}:${trimSummary(fact.value, 120)}`, data: { label: fact.label, source: fact.source, reason: fact.reason }, }); return fact; } export function openBlockingQuestions(run: ActiveRun): KdQuestion[] { return (Array.isArray(run.questions) ? run.questions : []).filter((question) => question.status === "open" && question.blocking); } export function updateProductProfile(cwd: string, run: ActiveRun, productInput: string, version?: string): ActiveRun { const profile = resolveProductProfile(productInput); run.product = profile.product; run.profile = profile; if (version !== undefined) run.version = version; run.gate = inspectGate(cwd, run); writeActiveRun(cwd, run); appendLedgerEvent(cwd, run, { type: "product.updated", summary: `产品画像更新为 ${run.profile.product}/${run.profile.platform}/${run.profile.language}。`, data: { product: run.profile.product, platform: run.profile.platform, language: run.profile.language, version: run.version }, }); return run; } export function updateRisk(cwd: string, run: ActiveRun, risk: KdRisk, reason: string, source: KdRiskSource = "manual"): ActiveRun { run.riskAssessment = { level: risk, reason: reason.trim(), source, updatedAt: new Date().toISOString(), }; run.gate = inspectGate(cwd, run); writeActiveRun(cwd, run); appendLedgerEvent(cwd, run, { type: "risk.updated", summary: `${risk}:${trimSummary(reason, 160)}`, data: { level: risk, source }, }); return run; } export function updateHarnessMode(cwd: string, run: ActiveRun, mode: KdHarnessMode): { run: ActiveRun; changed: boolean; message: string } { if (!phaseIncludedInRun({ mode }, run.phase)) { return { run, changed: false, message: `不能切换到 ${mode}:当前阶段 ${run.phase} 不属于该模式。可选阶段:${phaseOrderForMode(mode).join(", ")}`, }; } run.mode = mode; run.gate = inspectGate(cwd, run); writeActiveRun(cwd, run); appendLedgerEvent(cwd, run, { type: "mode.updated", summary: `Harness 模式更新为 ${mode}。`, data: { mode, phaseOrder: phaseOrderForRun(run) }, }); return { run, changed: true, message: `已设置 Harness 模式:${mode}(${phaseOrderForRun(run).join(" -> ")})`, }; } export function ensurePhaseArtifact(cwd: string, run: ActiveRun, phase: KdPhase): string { const path = ensureArtifact(cwd, run, phase, defaultArtifactContent(phase, run.goal, run.profile)); run.artifacts[phase] = PHASE_ARTIFACTS[phase]; run.gate = inspectGate(cwd, run); writeActiveRun(cwd, run); appendLedgerEvent(cwd, run, { type: "artifact.updated", summary: `阶段文档已就绪:${phase}。`, data: { phase, path }, }); return path; } export function updatePhaseArtifact(cwd: string, run: ActiveRun, phase: KdPhase, content: string): string { const path = writeArtifact(cwd, run, phase, content); run.gate = inspectGate(cwd, run); writeActiveRun(cwd, run); appendLedgerEvent(cwd, run, { type: "artifact.updated", summary: `阶段文档已更新:${phase}。`, data: { phase, path }, }); return path; } export function advanceRun(cwd: string, run: ActiveRun, requestedPhase?: KdPhase): { run: ActiveRun; message: string } { const target = requestedPhase ?? nextPhaseForRun(run); if (!target) { const gate = inspectGate(cwd, run); if (!gate.passed) { run.gate = gate; writeActiveRun(cwd, run); appendLedgerEvent(cwd, run, { type: "phase.advance.blocked", summary: gate.reason ?? `当前最终阶段 ${run.phase} 的门禁仍阻塞。`, data: { target: "done", gatePassed: gate.passed }, }); return { run, message: gate.reason ?? `当前最终阶段 ${run.phase} 的门禁仍阻塞` }; } return { run, message: `Run 已处于最终阶段:${run.phase}` }; } if (!phaseIncludedInRun(run, target)) { return { run, message: `阶段 ${target} 不属于当前 Harness 模式 ${run.mode}。可选阶段:${runPhaseList(run)}` }; } const gate = canEnterPhase(cwd, run, target); if (!gate.passed) { run.gate = gate; writeActiveRun(cwd, run); appendLedgerEvent(cwd, run, { type: "phase.advance.blocked", summary: gate.reason ?? `不能进入 ${target}。`, data: { target, gatePassed: gate.passed }, }); return { run, message: gate.reason ?? `不能进入 ${target}` }; } const fromPhase = run.phase; run.phase = target; run.resumeSnapshot = archiveAnsweredResumeSnapshot(run.resumeSnapshot); ensurePhaseArtifact(cwd, run, target); run.gate = inspectGate(cwd, run); writeActiveRun(cwd, run); appendLedgerEvent(cwd, run, { type: "phase.advanced", phase: target, summary: `${fromPhase} -> ${target}`, data: { from: fromPhase, to: target }, }); return { run, message: `已推进 Kingdee run 到阶段:${target}` }; } export function advanceRunIfReady(cwd: string, run: ActiveRun): { run: ActiveRun; advanced: boolean; message: string } { const current = readRun(cwd, run.id) ?? run; const target = nextPhaseForRun(current); if (!target) { const gate = inspectGate(cwd, current); if (!gate.passed) { current.gate = gate; writeActiveRun(cwd, current); return { run: current, advanced: false, message: gate.reason ?? `当前最终阶段 ${current.phase} 的门禁仍阻塞` }; } return { run: current, advanced: false, message: `Run 已处于最终阶段:${current.phase}` }; } const result = advanceRun(cwd, current, target); return { run: result.run, advanced: result.run.phase === target, message: result.message, }; } export function refreshGate(cwd: string, run: ActiveRun): ActiveRun { run.gate = inspectGate(cwd, run); writeActiveRun(cwd, run); appendLedgerEvent(cwd, run, { type: "gate.refreshed", summary: run.gate.passed ? "门禁通过。" : run.gate.reason ?? "门禁阻塞。", data: { passed: run.gate.passed }, }); return run; } function writeRunState(cwd: string, run: ActiveRun): void { mkdirSync(runsDir(cwd), { recursive: true }); ensureRunDirectories(cwd, run); writeFileSync(runStatePath(cwd, run), `${JSON.stringify(run, null, 2)}\n`, "utf8"); } function workingSetAfterQuestionAsked(run: ActiveRun, question: KdQuestion) { if (!question.blocking) { return updateWorkingSetFocus(run, { focus: run.phase, activeGap: question.reason ?? question.question, currentAction: `已记录非阻塞问题 ${question.id};继续当前阶段任务,禁止把该问题当作门禁事实。`, }); } return updateWorkingSetFocus(run, { focus: "blocking-question", activeGap: question.reason ?? question.question, currentAction: `等待 ${question.id} 的用户答案;第一动作必须记录答案,禁止继续提问、计划或编码。`, blockedBy: question.reason ?? question.question, }); } function workingSetAfterQuestionAnswered(run: ActiveRun, answered: KdQuestion) { const nextOpen = openBlockingQuestions(run)[0]; if (nextOpen) { return updateWorkingSetFocus(run, { focus: "blocking-question", activeGap: nextOpen.reason ?? nextOpen.question, currentAction: `已记录 ${answered.id} 的答案;下一步只处理 ${nextOpen.id} 的用户答案,禁止重复询问 ${answered.factLabel ?? answered.question}。`, blockedBy: nextOpen.reason ?? nextOpen.question, }); } if (!run.gate.passed) { return updateWorkingSetFocus(run, { focus: "gate", activeGap: run.gate.reason ?? answered.reason ?? answered.question, currentAction: `已记录 ${answered.id} 的答案;刷新门禁并处理当前阻塞,禁止重复询问 ${answered.factLabel ?? answered.question}。`, blockedBy: run.gate.reason, }); } return updateWorkingSetFocus(run, { focus: run.phase, activeGap: answered.reason ?? answered.question, currentAction: `已记录 ${answered.id} 的答案;从已确认事实继续 ${run.phase} 阶段任务,禁止重复询问 ${answered.factLabel ?? answered.question}。`, blockedBy: undefined, }); } function workingSetAfterFactRevision(run: ActiveRun, fact: KdFact) { if (!run.gate.passed) { return updateWorkingSetFocus(run, { focus: "gate", activeGap: run.gate.reason ?? `${fact.label} 已修订`, currentAction: `已修订事实 ${fact.label};刷新门禁并处理当前阻塞,禁止继续使用被取代的事实值。`, blockedBy: run.gate.reason, }); } return updateWorkingSetFocus(run, { focus: run.phase, activeGap: `${fact.label} 已修订`, currentAction: `已修订事实 ${fact.label};按当前阶段继续,禁止继续使用被取代的事实值。`, blockedBy: undefined, }); } function applyQuestionFact(run: ActiveRun, question: KdQuestion): void { const fact = factFromAnsweredQuestion(question, question.answeredAt); if (!fact) return; run.facts = upsertFact(run.facts, fact); } function applyRepairQuestionAnswer(cwd: string, run: ActiveRun, question: KdQuestion): void { if (!isRepairLimitQuestion(question)) return; const answer = question.answer?.trim() ?? ""; const now = new Date().toISOString(); const attempts = run.repair?.attempts ?? 0; const maxAttempts = run.repair?.maxAttempts ?? 3; const extendedMaxAttempts = attempts + maxAttempts; if (/^继续修复\b|^继续|continue/i.test(answer)) { run.phase = "execute"; ensureArtifact(cwd, run, "execute", defaultArtifactContent("execute", run.goal, run.profile)); run.repair = { attempts, maxAttempts: extendedMaxAttempts, lastFailureEvidence: run.repair?.lastFailureEvidence, lastFailureSignature: run.repair?.lastFailureSignature, status: "repairing", updatedAt: now, }; return; } if (/^回到\s*plan\b|^回到计划|^返回\s*plan\b|^重新计划/i.test(answer)) { run.phase = "plan"; ensureArtifact(cwd, run, "plan", defaultArtifactContent("plan", run.goal, run.profile)); run.repair = { attempts, maxAttempts: extendedMaxAttempts, lastFailureEvidence: run.repair?.lastFailureEvidence, lastFailureSignature: run.repair?.lastFailureSignature, status: "idle", updatedAt: now, }; return; } if (/^停止|^stop\b/i.test(answer)) { run.repair = { attempts: run.repair?.attempts ?? maxAttempts, maxAttempts, lastFailureEvidence: run.repair?.lastFailureEvidence, lastFailureSignature: run.repair?.lastFailureSignature, status: "blocked", updatedAt: now, }; } } function isRepairLimitQuestion(question: KdQuestion): boolean { return ( question.phase === "verify" && /验证失败已达到/.test(question.question) && (Array.isArray(question.choices) ? question.choices.includes("继续修复") : true) ); } function runPhaseList(run: Pick): string { return phaseOrderForRun(run).join(", "); } function createResumeSnapshot( run: ActiveRun, question: KdQuestion, contextSummary: string | undefined, sourceRefs: string[] | undefined, gateReason: string | undefined, ): KdResumeSnapshot { const summary = [contextSummary?.trim(), question.reason?.trim(), `阻塞问题:${question.question.trim()}`].filter(Boolean).join("\n"); return { phase: run.phase, contextSummary: summary, sourceRefs: sourceRefs?.map((source) => source.trim()).filter(Boolean), gateReason: gateReason?.trim() || undefined, nextAction: `等待 ${question.id} 的用户答案;记录答案后从该断点继续,先复用已知缺口和来源,禁止重复询问已确认事实。`, pendingQuestionId: question.id, status: "open", updatedAt: new Date().toISOString(), }; } function answerResumeSnapshot(snapshot: KdResumeSnapshot | undefined, question: KdQuestion): KdResumeSnapshot | undefined { if (!snapshot || snapshot.pendingQuestionId !== question.id) return snapshot; return { ...snapshot, status: "answered", nextAction: `已记录 ${question.id} 的答案;下一步必须刷新门禁,并从断点继续处理剩余缺口,禁止重新开始或重复询问 ${question.factLabel ?? question.question}。`, updatedAt: new Date().toISOString(), }; } function archiveAnsweredResumeSnapshot(snapshot: KdResumeSnapshot | undefined): KdResumeSnapshot | undefined { if (!snapshot || snapshot.status !== "answered") return snapshot; return { ...snapshot, status: "archived", nextAction: "恢复断点已归档;后续阶段以 run.facts、问题审计、阶段文档和当前门禁为准。", updatedAt: new Date().toISOString(), }; } function createRunId(goal: string): string { const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "").replace("T", "-"); const slug = goal .toLowerCase() .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 40); return slug ? `${stamp}-${slug}` : stamp; } function createQuestionId(sequence: number): string { return `Q-${String(sequence).padStart(3, "0")}`; } function createContextEntryId(sequence: number): string { return `C-${String(sequence).padStart(3, "0")}`; } function normalizeQuestion(question: string): string { return question.trim().replace(/\s+/g, "").replace(/[??。;;,,]/g, ""); } function normalizeMemoryText(text: string): string { return text.trim().replace(/\s+/g, " ").toLowerCase(); } function trimSummary(text: string, maxLength = 220): string { const normalized = text.trim().replace(/\s+/g, " "); return normalized.length <= maxLength ? normalized : `${normalized.slice(0, maxLength)}...`; }