kcode-pi 0.1.24 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,297 @@
1
+ import { readProjectContext } from "../context/project-context.ts";
2
+ import { readArtifact } from "./artifacts.ts";
3
+ import { formatStatus } from "./format.ts";
4
+ import type { ActiveRun, KdPhase } from "./types.ts";
5
+ import { PHASE_ORDER } from "./types.ts";
6
+ import { isAbsolute, relative } from "node:path";
7
+
8
+ export type DelegationRole = "research" | "doc" | "code" | "review" | "verify";
9
+
10
+ export interface DelegationRequest {
11
+ role: DelegationRole;
12
+ task: string;
13
+ }
14
+
15
+ export type DelegationMode = "single" | "parallel" | "chain";
16
+
17
+ export interface ParsedDelegationArgs extends DelegationRequest {
18
+ dryRun: boolean;
19
+ }
20
+
21
+ const ROLE_GUIDANCE: Record<DelegationRole, string[]> = {
22
+ research: [
23
+ "只读调研项目、文档、SDK 线索和现有实现。",
24
+ "输出压缩结论、证据文件/代码位置、风险和建议下一步。",
25
+ "不要修改文件,不要推进 Harness 状态。",
26
+ ],
27
+ doc: [
28
+ "只写当前任务明确要求的文档或阶段产物。",
29
+ "如果需要改阶段文档,保持内容和当前 Harness 阶段一致。",
30
+ "不要修改产品源码,不要推进 Harness 状态。",
31
+ ],
32
+ code: [
33
+ "只有当前 run 处于 execute 阶段时才允许修改产品代码。",
34
+ "只修改 PLAN.md 批准的文件;发现新文件需求时停止并说明需要回到 plan。",
35
+ "完成后列出变更文件、执行步骤证据和需要主 agent 验证的命令。",
36
+ ],
37
+ review: [
38
+ "只读交叉自查,不修改文件。",
39
+ "优先找 bug、状态机漏洞、门禁绕过、证据缺口和测试缺口。",
40
+ "输出 findings,按严重程度排序并带文件/函数引用;明确是否阻止发布。",
41
+ ],
42
+ verify: [
43
+ "只读分析计划中的验证命令、失败证据和风险。",
44
+ "需要实际运行验证时,把命令和原因交回主 agent 执行。",
45
+ "验证结果由主 agent 通过 kd_verify_result 记录。",
46
+ "不要手工绕过 Harness 的 VERIFY/evidence 记录闭环。",
47
+ ],
48
+ };
49
+
50
+ const ROLE_ALLOWED_WRITES: Record<DelegationRole, string> = {
51
+ research: "none",
52
+ doc: "docs-and-current-phase-artifact",
53
+ code: "plan-approved-files-only",
54
+ review: "none",
55
+ verify: "evidence-through-kd_verify_result",
56
+ };
57
+
58
+ export const DEFAULT_REVIEW_TASK = "审查当前 run 和最近变更,重点找状态机漏洞、门禁绕过、证据缺口、提示词分散和测试缺口。";
59
+ export const CHILD_AGENT_USER_TASK = "执行 KCode 子 agent 委派任务。";
60
+ export const KD_SUBAGENT_TOOL_DESCRIPTION =
61
+ "将调研、文档、代码、验证或交叉审查任务委派给隔离 Pi 子进程。主 Harness 仍负责阶段推进、证据和门禁。";
62
+ export const KD_DELEGATE_USAGE = "用法:/kd-delegate <research|doc|code|review|verify> <任务> [--dry-run]";
63
+ export const KD_SUBAGENT_INVALID_PARAMS = "kd_subagent 需要 role=research|doc|code|review|verify 和非空 task,或提供 tasks/chain。";
64
+ export const KD_SUBAGENT_PARALLEL_ROLE_ERROR = "kd_subagent 并行模式只允许 research、review、verify 这类只读角色。doc/code 必须串行执行。";
65
+ export const KD_REVIEW_COMMAND_DESCRIPTION = "启动只读交叉自查子 agent:/kd-review [审查重点]";
66
+ export const KD_DELEGATE_COMMAND_DESCRIPTION = "委派任务给隔离子 agent:/kd-delegate <research|doc|code|review|verify> <任务> [--dry-run]";
67
+ export const KD_SUBAGENT_SCHEMA_DESCRIPTIONS = {
68
+ role: "单任务角色:research、doc、code、review、verify。",
69
+ task: "单任务内容。",
70
+ taskItemRole: "任务角色。",
71
+ taskItemTask: "任务内容。",
72
+ tasks: "并行任务数组,最多 8 个,最多 4 个同时运行。",
73
+ chain: "链式任务数组,按顺序运行;后一步会收到上一步输出。",
74
+ dryRun: "只预览上下文包,不启动子进程。",
75
+ maxOutputChars: "返回给主 agent 的最大输出字符数,默认 30000。",
76
+ } as const;
77
+
78
+ const READ_ONLY_ROLES = new Set<DelegationRole>(["research", "review", "verify"]);
79
+ const WRITE_TOOL_NAMES = new Set(["write", "edit"]);
80
+ const SHELL_TOOL_NAMES = new Set(["bash", "shell", "shell_command"]);
81
+
82
+ export function isDelegationRole(value: string): value is DelegationRole {
83
+ return value === "research" || value === "doc" || value === "code" || value === "review" || value === "verify";
84
+ }
85
+
86
+ export function parseDelegationArgs(args: string): ParsedDelegationArgs | undefined {
87
+ const tokens = tokenizeArgs(args);
88
+ if (tokens.length === 0) return undefined;
89
+ const roleText = tokens[0]?.toLowerCase();
90
+ if (!roleText || !isDelegationRole(roleText)) return undefined;
91
+
92
+ const dryRun = tokens.includes("--dry-run");
93
+ const task = tokens.slice(1).filter((token) => token !== "--dry-run").join(" ").trim();
94
+ if (!task) return undefined;
95
+ return { role: roleText, task, dryRun };
96
+ }
97
+
98
+ export function delegationBlockReason(role: DelegationRole, run: ActiveRun | undefined): string | undefined {
99
+ if (role === "code") {
100
+ if (!run) return "code 子 agent 需要 active Harness run,且必须处于 execute 阶段。";
101
+ if (run.phase !== "execute") return `code 子 agent 只能在 execute 阶段运行;当前阶段:${run.phase}。`;
102
+ }
103
+ if (role === "verify" && !run) return "verify 子 agent 需要 active Harness run。";
104
+ return undefined;
105
+ }
106
+
107
+ export function isReadOnlyDelegationRole(role: DelegationRole): boolean {
108
+ return READ_ONLY_ROLES.has(role);
109
+ }
110
+
111
+ export function parallelDelegationBlockReason(requests: DelegationRequest[]): string | undefined {
112
+ return requests.some((request) => !isReadOnlyDelegationRole(request.role)) ? KD_SUBAGENT_PARALLEL_ROLE_ERROR : undefined;
113
+ }
114
+
115
+ export function isSubagentChild(env: NodeJS.ProcessEnv = process.env): boolean {
116
+ return env.KCODE_SUBAGENT_CHILD === "1";
117
+ }
118
+
119
+ export function subagentRoleFromEnv(env: NodeJS.ProcessEnv = process.env): DelegationRole | undefined {
120
+ const role = env.KCODE_SUBAGENT_ROLE;
121
+ return typeof role === "string" && isDelegationRole(role) ? role : undefined;
122
+ }
123
+
124
+ export function shouldInjectDelegationGuidance(env: NodeJS.ProcessEnv = process.env): boolean {
125
+ return !isSubagentChild(env);
126
+ }
127
+
128
+ export function subagentAllowedTools(role: DelegationRole): string[] {
129
+ const readTools = ["read", "grep", "find", "ls"];
130
+ if (role === "doc" || role === "code") return [...readTools, "write", "edit"];
131
+ return readTools;
132
+ }
133
+
134
+ export function subagentToolCallBlockReason(input: {
135
+ role: DelegationRole;
136
+ toolName: string;
137
+ path?: string;
138
+ cwd: string;
139
+ run?: ActiveRun;
140
+ sourceLike?: boolean;
141
+ planWriteBlockReason?: string;
142
+ sourceWriteBlockReason?: string;
143
+ }): string | undefined {
144
+ if (SHELL_TOOL_NAMES.has(input.toolName)) {
145
+ return "子 agent 不允许调用 shell 类工具;需要运行命令时交回主 agent 执行。";
146
+ }
147
+ if (!WRITE_TOOL_NAMES.has(input.toolName)) return undefined;
148
+
149
+ if (isReadOnlyDelegationRole(input.role)) {
150
+ return `${input.role} 子 agent 是只读角色,不能写入文件。`;
151
+ }
152
+ if (input.role === "doc") {
153
+ if (isDocWritablePath(input.cwd, input.run, input.path)) return undefined;
154
+ return "doc 子 agent 只能写 README、docs/ 或当前 run 的阶段文档。";
155
+ }
156
+ if (input.role === "code") {
157
+ if (!input.sourceLike) return "code 子 agent 只能写产品源码,其他文件改动必须交回主 agent。";
158
+ if (input.sourceWriteBlockReason) return input.sourceWriteBlockReason;
159
+ if (input.planWriteBlockReason) return input.planWriteBlockReason;
160
+ return undefined;
161
+ }
162
+ return undefined;
163
+ }
164
+
165
+ export function buildDelegationCommandPrompt(request: DelegationRequest, dryRun: boolean): string {
166
+ return [
167
+ "请调用 kd_subagent 完成以下委派任务。",
168
+ `role: ${request.role}`,
169
+ `dryRun: ${dryRun ? "true" : "false"}`,
170
+ "task:",
171
+ request.task,
172
+ "",
173
+ "子 agent 返回后,由主 agent 判断是否需要修改文件、记录 evidence 或推进 Harness。",
174
+ ].join("\n");
175
+ }
176
+
177
+ export function delegationGuidanceForWorkflow(): string {
178
+ return [
179
+ "- 当任务需要大量调研、独立交叉审查、长上下文复盘或可并行拆分时,优先考虑 kd_subagent。",
180
+ "- 自动委派只做旁路工作;主 agent 仍负责采纳结论、修改文件、记录 evidence 和推进阶段。",
181
+ "- code 委派只能在 execute 阶段且限于 PLAN.md 批准文件;review/research 默认只读。",
182
+ ].join("\n");
183
+ }
184
+
185
+ export function buildChainedDelegationRequest(request: DelegationRequest, previousOutput: string): DelegationRequest {
186
+ if (!previousOutput.trim()) return request;
187
+ return {
188
+ role: request.role,
189
+ task: [
190
+ request.task.trim(),
191
+ "",
192
+ "上一子 agent 输出:",
193
+ trimForPrompt(previousOutput, 6000),
194
+ "",
195
+ "请基于上一输出继续当前步骤,并只交付本步骤结论。",
196
+ ].join("\n"),
197
+ };
198
+ }
199
+
200
+ export function buildDelegationPrompt(cwd: string, run: ActiveRun | undefined, request: DelegationRequest): string {
201
+ const roleGuidance = ROLE_GUIDANCE[request.role];
202
+ const projectContext = readProjectContext(cwd);
203
+ const status = run ? formatStatus(cwd, run) : "当前没有 active KCode Harness run。";
204
+ const phaseContext = run ? delegationMemoryForRun(cwd, run) : "无 active run 阶段资料。";
205
+
206
+ return [
207
+ "KCode 子 agent 委派任务。",
208
+ "",
209
+ "角色:",
210
+ request.role,
211
+ "",
212
+ "任务:",
213
+ request.task.trim(),
214
+ "",
215
+ "写入边界:",
216
+ ROLE_ALLOWED_WRITES[request.role],
217
+ "",
218
+ "角色规则:",
219
+ ...roleGuidance.map((item) => `- ${item}`),
220
+ "- 你不是主状态机;不要调用 /kd-advance、/kd-finish 或改变 run 生命周期。",
221
+ "- 不要再创建子 agent;把结果交回主 agent 统一决策。",
222
+ "",
223
+ "Harness 状态:",
224
+ status,
225
+ "",
226
+ "阶段上下文:",
227
+ phaseContext,
228
+ "",
229
+ "项目上下文:",
230
+ projectContext ? trimForPrompt(projectContext, 1200) : "未生成。需要项目结构时读取本地文件或提示主 agent 运行 `kcode context --refresh`。",
231
+ "",
232
+ "输出格式:",
233
+ "- 结论",
234
+ "- 证据/引用",
235
+ "- 风险",
236
+ "- 建议给主 agent 的下一步",
237
+ ].join("\n");
238
+ }
239
+
240
+ export function formatDelegationPreview(cwd: string, run: ActiveRun | undefined, request: DelegationRequest): string {
241
+ return [
242
+ `角色:${request.role}`,
243
+ `写入边界:${ROLE_ALLOWED_WRITES[request.role]}`,
244
+ run ? `Run:${run.id} | 阶段:${run.phase}` : "Run:无 active run",
245
+ "",
246
+ "任务:",
247
+ request.task.trim(),
248
+ "",
249
+ "将发送给隔离子 agent 的上下文包:",
250
+ buildDelegationPrompt(cwd, run, request),
251
+ ].join("\n");
252
+ }
253
+
254
+ function delegationMemoryForRun(cwd: string, run: ActiveRun): string {
255
+ const currentIndex = PHASE_ORDER.indexOf(run.phase);
256
+ const nearby = PHASE_ORDER.slice(Math.max(0, currentIndex - 2), currentIndex + 1);
257
+ const sections = nearby
258
+ .map((phase) => artifactSection(cwd, run, phase))
259
+ .filter((section): section is string => Boolean(section));
260
+ return sections.join("\n\n") || `阶段文档路径:.pi/kd/runs/${run.id}/`;
261
+ }
262
+
263
+ function artifactSection(cwd: string, run: ActiveRun, phase: KdPhase): string | undefined {
264
+ const content = readArtifact(cwd, run, phase);
265
+ if (!content) return undefined;
266
+ return [`## ${phase}`, trimForPrompt(content, 1800)].join("\n");
267
+ }
268
+
269
+ function trimForPrompt(content: string, maxLength: number): string {
270
+ if (content.length <= maxLength) return content;
271
+ return `${content.slice(0, maxLength)}\n\n[...已截断;需要完整内容时读取本地文件...]`;
272
+ }
273
+
274
+ function isDocWritablePath(cwd: string, run: ActiveRun | undefined, path: string | undefined): boolean {
275
+ if (!path) return false;
276
+ const normalized = normalizeWorkspacePath(cwd, path);
277
+ if (normalized === "README.md") return true;
278
+ if (normalized.startsWith("docs/") && /\.md$/i.test(normalized)) return true;
279
+ if (run && normalized.startsWith(`.pi/kd/runs/${run.id}/`) && /\.md$/i.test(normalized)) return true;
280
+ return false;
281
+ }
282
+
283
+ function normalizeWorkspacePath(cwd: string, path: string): string {
284
+ const relativePath = isAbsolute(path) ? relative(cwd, path) : path;
285
+ return relativePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
286
+ }
287
+
288
+ function tokenizeArgs(args: string): string[] {
289
+ const tokens: string[] = [];
290
+ const pattern = /"([^"]*)"|'([^']*)'|(\S+)/g;
291
+ let match: RegExpExecArray | null;
292
+ while ((match = pattern.exec(args)) !== null) {
293
+ const token = match[1] ?? match[2] ?? match[3];
294
+ if (token) tokens.push(token);
295
+ }
296
+ return tokens;
297
+ }
@@ -28,8 +28,9 @@ export function readEvidenceIndex(cwd: string, run: ActiveRun): EvidenceIndex {
28
28
  const path = evidenceIndexPath(cwd, run);
29
29
  if (!existsSync(path)) return { version: 1, entries: [] };
30
30
  try {
31
- const parsed = JSON.parse(readFileSync(path, "utf8")) as EvidenceIndex;
32
- return parsed.version === 1 && Array.isArray(parsed.entries) ? parsed : { version: 1, entries: [] };
31
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<EvidenceIndex>;
32
+ if (parsed.version !== 1 || !Array.isArray(parsed.entries)) return { version: 1, entries: [] };
33
+ return { version: 1, entries: parsed.entries.filter(isEvidenceEntry) };
33
34
  } catch {
34
35
  return { version: 1, entries: [] };
35
36
  }
@@ -37,6 +38,7 @@ export function readEvidenceIndex(cwd: string, run: ActiveRun): EvidenceIndex {
37
38
 
38
39
  export function hasEvidenceEntry(cwd: string, run: ActiveRun, path: string): boolean {
39
40
  const normalized = normalizeEvidencePath(path);
41
+ if (!normalized) return false;
40
42
  return readEvidenceIndex(cwd, run).entries.some((entry) => normalizeEvidencePath(entry.path) === normalized);
41
43
  }
42
44
 
@@ -48,7 +50,7 @@ export function writeEvidenceFile(
48
50
  options: { kind?: string; command?: string; exitCode?: number } = {},
49
51
  ): string {
50
52
  const normalized = normalizeEvidencePath(path);
51
- if (!normalized.startsWith("evidence/") || normalized === EVIDENCE_INDEX) {
53
+ if (!normalized || !normalized.startsWith("evidence/") || normalized === EVIDENCE_INDEX) {
52
54
  throw new Error(`非法 evidence 路径:${path}`);
53
55
  }
54
56
 
@@ -66,6 +68,7 @@ export function recordEvidence(
66
68
  options: { kind?: string; command?: string; exitCode?: number } = {},
67
69
  ): void {
68
70
  const normalized = normalizeEvidencePath(path);
71
+ if (!normalized) return;
69
72
  const absolutePath = join(runRoot(cwd, run), normalized);
70
73
  if (!existsSync(absolutePath)) return;
71
74
 
@@ -96,11 +99,19 @@ export function recordEvidence(
96
99
  writeFileSync(indexPath, `${JSON.stringify(index, null, 2)}\n`, "utf8");
97
100
  }
98
101
 
102
+ function isEvidenceEntry(value: unknown): value is EvidenceEntry {
103
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
104
+ const entry = value as Partial<EvidenceEntry>;
105
+ return typeof entry.path === "string" && entry.path.trim().length > 0;
106
+ }
107
+
99
108
  function inferKind(path: string): string {
100
109
  const name = path.split("/").at(-1) ?? path;
101
110
  return name.replace(/\.(md|txt|json)$/i, "");
102
111
  }
103
112
 
104
- function normalizeEvidencePath(path: string): string {
105
- return path.replace(/\\/g, "/").replace(/^\/+/, "").replace(/^\.\//, "");
113
+ function normalizeEvidencePath(path: unknown): string | undefined {
114
+ if (typeof path !== "string") return undefined;
115
+ const normalized = path.replace(/\\/g, "/").replace(/^\/+/, "").replace(/^\.\//, "");
116
+ return normalized.trim() || undefined;
106
117
  }
@@ -9,7 +9,7 @@ import { executionStepsBlockReason, planStepsBlockReason } from "./plan-steps.ts
9
9
  import { tddPlanBlockReason, tddVerifyBlockReason } from "./tdd-policy.ts";
10
10
  import { SDK_SIGNATURE_EVIDENCE, hasValidSdkSignatureEvidence, requiresSdkSignatureEvidence } from "./sdk-policy.ts";
11
11
  import { runRoot } from "./paths.ts";
12
- import { EVIDENCE_INDEX, hasEvidenceEntry } from "./evidence.ts";
12
+ import { EVIDENCE_INDEX, readEvidenceIndex } from "./evidence.ts";
13
13
  import {
14
14
  missingArtifactsReason,
15
15
  missingEvidenceReason,
@@ -129,7 +129,7 @@ function productImplementationDeclaration(cwd: string, run: ActiveRun): boolean
129
129
  function hasRiskAssessment(cwd: string, run: ActiveRun): boolean {
130
130
  const level = run.riskAssessment?.level;
131
131
  if (!level) return false;
132
- if (run.riskAssessment?.reason.trim()) return true;
132
+ if (typeof run.riskAssessment?.reason === "string" && run.riskAssessment.reason.trim()) return true;
133
133
  return riskSectionHasContent(readArtifact(cwd, run, "verify") ?? "") || riskSectionHasContent(readArtifact(cwd, run, "ship") ?? "");
134
134
  }
135
135
 
@@ -143,7 +143,7 @@ function riskSectionHasContent(content: string): boolean {
143
143
  }
144
144
 
145
145
  function inspectOpenQuestions(run: ActiveRun): string | undefined {
146
- const open = (run.questions ?? []).filter((question) => question.status === "open" && question.blocking);
146
+ const open = Array.isArray(run.questions) ? run.questions.filter((question) => question.status === "open" && question.blocking) : [];
147
147
  if (open.length === 0) return undefined;
148
148
  return openQuestionsReason(open);
149
149
  }
@@ -212,7 +212,13 @@ function requiredEvidenceForPhase(cwd: string, run: ActiveRun, phase: KdPhase):
212
212
  function evidenceArtifactSatisfied(cwd: string, run: ActiveRun, artifact: string): boolean {
213
213
  if (artifact === SDK_SIGNATURE_EVIDENCE) return hasValidSdkSignatureEvidence(cwd, run);
214
214
  if (artifact === EVIDENCE_INDEX) return existsSync(join(runRoot(cwd, run), artifact));
215
- return existsSync(join(runRoot(cwd, run), artifact)) && hasEvidenceEntry(cwd, run, artifact);
215
+ return existsSync(join(runRoot(cwd, run), artifact)) && hasSuccessfulEvidenceEntry(cwd, run, artifact);
216
+ }
217
+
218
+ function hasSuccessfulEvidenceEntry(cwd: string, run: ActiveRun, artifact: string): boolean {
219
+ const normalized = artifact.replace(/\\/g, "/").replace(/^\/+/, "").replace(/^\.\//, "");
220
+ const entry = readEvidenceIndex(cwd, run).entries.find((item) => typeof item.path === "string" && item.path.replace(/\\/g, "/") === normalized);
221
+ return Boolean(entry && entry.exitCode === 0);
216
222
  }
217
223
 
218
224
  function planHasMetadataRequirement(cwd: string, run: ActiveRun): boolean {
@@ -1,5 +1,6 @@
1
1
  import { readProjectContext } from "../context/project-context.ts";
2
2
  import { readArtifact } from "./artifacts.ts";
3
+ import { delegationGuidanceForWorkflow, shouldInjectDelegationGuidance } from "./delegation.ts";
3
4
  import { formatStatus } from "./format.ts";
4
5
  import type { ActiveRun, KdPhase } from "./types.ts";
5
6
  import { PHASE_ORDER } from "./types.ts";
@@ -9,6 +10,7 @@ export function workflowPromptForRun(cwd: string, run: ActiveRun, userText: stri
9
10
  const memory = workflowMemoryForRun(cwd, run);
10
11
  const phaseGuidance = phaseGuidanceForRun(run.phase);
11
12
  const projectContext = readProjectContext(cwd);
13
+ const delegationGuidance = shouldInjectDelegationGuidance() ? ["", "子 agent 委派策略:", delegationGuidanceForWorkflow()] : [];
12
14
 
13
15
  return [
14
16
  "用户输入:",
@@ -32,6 +34,16 @@ export function workflowPromptForRun(cwd: string, run: ActiveRun, userText: stri
32
34
  "- Java/Cosmic 用当前项目 Gradle;C#/企业版用 dotnet build。",
33
35
  "- evidence 记录命令、Exit 和关键输出;命令无法运行时记录阻塞原因。",
34
36
  "- Windows 下优先使用项目相对路径;绝对路径使用 D:\\... 形式。",
37
+ ...delegationGuidance,
38
+ ].join("\n");
39
+ }
40
+
41
+ export function repairPromptForRun(run: ActiveRun): string {
42
+ return [
43
+ "验证失败,进入自动修复循环。",
44
+ `失败证据:${run.repair?.lastFailureEvidence ?? "未知"}`,
45
+ `修复轮次:${run.repair?.attempts ?? 0}/${run.repair?.maxAttempts ?? 3}`,
46
+ "读取失败证据,定位原因,只修改 PLAN.md 批准文件;修复后重新执行同一验证命令并调用 kd_verify_result。",
35
47
  ].join("\n");
36
48
  }
37
49
 
@@ -41,7 +53,7 @@ function phaseGuidanceForRun(phase: KdPhase): string {
41
53
  spec: "把需求转成验收标准、数据对象、异常行为、依赖和风险。",
42
54
  plan: "检查项目结构,写明目标路径、允许修改文件、查证项、验证命令和回滚说明。",
43
55
  execute: "按 PLAN.md 实现,记录步骤结果、变更文件和 evidence。",
44
- verify: "运行计划中的验证命令,更新 VERIFY.md、证据和残余风险。",
56
+ verify: "运行计划中的验证命令,并用 kd_verify_result 记录结果;失败会回到 execute 修复,成功后更新 VERIFY.md、证据和残余风险。",
45
57
  ship: "整理 SHIP.md,包括摘要、验证证据、风险和后续事项。",
46
58
  };
47
59
  return guidance[phase];
@@ -0,0 +1,224 @@
1
+ import type { ActiveRun } from "./types.ts";
2
+ import { defaultArtifactContent, ensureArtifact, readArtifact, writeArtifact } from "./artifacts.ts";
3
+ import { writeEvidenceFile } from "./evidence.ts";
4
+ import { inspectGate } from "./gates.ts";
5
+ import { writeActiveRun } from "./state.ts";
6
+
7
+ export interface VerifyResultInput {
8
+ command: unknown;
9
+ exitCode: unknown;
10
+ stdout?: unknown;
11
+ stderr?: unknown;
12
+ summary?: unknown;
13
+ }
14
+
15
+ export interface VerifyResultOutcome {
16
+ status: "passed" | "repairing" | "blocked" | "rejected";
17
+ run: ActiveRun;
18
+ evidencePath?: string;
19
+ message: string;
20
+ }
21
+
22
+ interface NormalizedVerifyResultInput {
23
+ command: string;
24
+ exitCode: number;
25
+ stdout: string;
26
+ stderr: string;
27
+ summary: string;
28
+ }
29
+
30
+ const DEFAULT_MAX_REPAIR_ATTEMPTS = 3;
31
+
32
+ export function recordVerifyResult(cwd: string, run: ActiveRun, input: VerifyResultInput): VerifyResultOutcome {
33
+ const blockReason = verifyResultBlockReason(run);
34
+ if (blockReason) {
35
+ return {
36
+ status: "rejected",
37
+ run,
38
+ message: blockReason,
39
+ };
40
+ }
41
+
42
+ const normalized = normalizeInput(input);
43
+ if (normalized.exitCode === 0) {
44
+ return recordVerifyPass(cwd, run, normalized);
45
+ }
46
+ return recordVerifyFailure(cwd, run, normalized);
47
+ }
48
+
49
+ function recordVerifyPass(cwd: string, run: ActiveRun, input: NormalizedVerifyResultInput): VerifyResultOutcome {
50
+ const evidence = "evidence/verify-pass.md";
51
+ const evidencePath = writeEvidenceFile(cwd, run, evidence, formatVerifyEvidence("验证通过", input), {
52
+ kind: "verify-pass",
53
+ command: input.command,
54
+ exitCode: input.exitCode,
55
+ });
56
+ appendVerifyRecord(cwd, run, "验证通过", evidence, input);
57
+ closeRepairQuestions(run, evidence);
58
+ run.repair = {
59
+ attempts: 0,
60
+ maxAttempts: run.repair?.maxAttempts ?? DEFAULT_MAX_REPAIR_ATTEMPTS,
61
+ status: "idle",
62
+ updatedAt: new Date().toISOString(),
63
+ };
64
+ run.gate = inspectGate(cwd, run);
65
+ writeActiveRun(cwd, run);
66
+ return {
67
+ status: "passed",
68
+ run,
69
+ evidencePath,
70
+ message: `验证通过,已记录证据:${evidence}`,
71
+ };
72
+ }
73
+
74
+ function recordVerifyFailure(cwd: string, run: ActiveRun, input: NormalizedVerifyResultInput): VerifyResultOutcome {
75
+ const maxAttempts = run.repair?.maxAttempts ?? DEFAULT_MAX_REPAIR_ATTEMPTS;
76
+ const attempts = (run.repair?.attempts ?? 0) + 1;
77
+ const evidence = `evidence/verify-failure-${String(attempts).padStart(3, "0")}.md`;
78
+ const signature = failureSignature(input);
79
+ const evidencePath = writeEvidenceFile(cwd, run, evidence, formatVerifyEvidence("验证失败", input), {
80
+ kind: "verify-failure",
81
+ command: input.command,
82
+ exitCode: input.exitCode,
83
+ });
84
+ appendVerifyRecord(cwd, run, "验证失败", evidence, input);
85
+
86
+ if (attempts >= maxAttempts) {
87
+ run.phase = "verify";
88
+ closeRepairQuestions(run, evidence);
89
+ run.repair = {
90
+ attempts,
91
+ maxAttempts,
92
+ lastFailureEvidence: evidence,
93
+ lastFailureSignature: signature,
94
+ status: "blocked",
95
+ updatedAt: new Date().toISOString(),
96
+ };
97
+ run.questions = [
98
+ ...(Array.isArray(run.questions) ? run.questions : []),
99
+ {
100
+ id: `Q-${String((run.questions?.length ?? 0) + 1).padStart(3, "0")}`,
101
+ phase: "verify",
102
+ question: `验证失败已达到 ${maxAttempts} 轮。是否允许继续修复,或需要回到 plan 调整范围?`,
103
+ reason: `最近失败证据:${evidence}`,
104
+ choices: ["继续修复", "回到 plan", "停止"],
105
+ blocking: true,
106
+ status: "open",
107
+ createdAt: new Date().toISOString(),
108
+ },
109
+ ];
110
+ run.gate = inspectGate(cwd, run);
111
+ writeActiveRun(cwd, run);
112
+ return {
113
+ status: "blocked",
114
+ run,
115
+ evidencePath,
116
+ message: `验证失败已达到 ${maxAttempts} 轮,已记录 ${evidence} 并创建阻断问题。`,
117
+ };
118
+ }
119
+
120
+ run.phase = "execute";
121
+ ensureArtifact(cwd, run, "execute", defaultArtifactContent("execute", run.goal, run.profile));
122
+ appendExecuteRepairRecord(cwd, run, evidence, attempts, maxAttempts);
123
+ run.repair = {
124
+ attempts,
125
+ maxAttempts,
126
+ lastFailureEvidence: evidence,
127
+ lastFailureSignature: signature,
128
+ status: "repairing",
129
+ updatedAt: new Date().toISOString(),
130
+ };
131
+ run.gate = inspectGate(cwd, run);
132
+ writeActiveRun(cwd, run);
133
+ return {
134
+ status: "repairing",
135
+ run,
136
+ evidencePath,
137
+ message: `验证失败,已记录 ${evidence},自动回到 execute 修复(${attempts}/${maxAttempts})。`,
138
+ };
139
+ }
140
+
141
+ function appendVerifyRecord(cwd: string, run: ActiveRun, title: string, evidence: string, input: NormalizedVerifyResultInput): void {
142
+ const existing = readArtifact(cwd, run, "verify") ?? defaultArtifactContent("verify", run.goal, run.profile);
143
+ const section = [
144
+ "",
145
+ `### ${title} - ${new Date().toISOString()}`,
146
+ "",
147
+ `- 命令:${input.command}`,
148
+ `- Exit:${input.exitCode}`,
149
+ `- 证据:${evidence}`,
150
+ input.summary ? `- 摘要:${input.summary}` : undefined,
151
+ "",
152
+ ].filter(Boolean).join("\n");
153
+ writeArtifact(cwd, run, "verify", `${existing.trimEnd()}\n${section}`);
154
+ }
155
+
156
+ function appendExecuteRepairRecord(cwd: string, run: ActiveRun, evidence: string, attempts: number, maxAttempts: number): void {
157
+ const existing = readArtifact(cwd, run, "execute") ?? defaultArtifactContent("execute", run.goal, run.profile);
158
+ const section = [
159
+ "",
160
+ "## 自动修复循环",
161
+ "",
162
+ `- 修复轮次:${attempts}/${maxAttempts}`,
163
+ `- 失败证据:${evidence}`,
164
+ "- 下一步:读取失败证据,分析失败原因,只在 PLAN.md 批准文件内修复;如果需要改未批准文件,回到 plan 更新计划。",
165
+ "",
166
+ ].join("\n");
167
+ writeArtifact(cwd, run, "execute", `${existing.trimEnd()}\n${section}`);
168
+ }
169
+
170
+ function formatVerifyEvidence(title: string, input: NormalizedVerifyResultInput): string {
171
+ return [
172
+ `# ${title}`,
173
+ "",
174
+ `Captured: ${new Date().toISOString()}`,
175
+ `Command: ${input.command}`,
176
+ `Exit: ${input.exitCode}`,
177
+ input.summary ? `Summary: ${input.summary}` : undefined,
178
+ input.stdout.trim() ? `\nSTDOUT:\n${input.stdout.trim()}` : undefined,
179
+ input.stderr.trim() ? `\nSTDERR:\n${input.stderr.trim()}` : undefined,
180
+ "",
181
+ ].filter(Boolean).join("\n");
182
+ }
183
+
184
+ function normalizeInput(input: VerifyResultInput): NormalizedVerifyResultInput {
185
+ const exitCode = typeof input.exitCode === "number" ? input.exitCode : Number(input.exitCode);
186
+ return {
187
+ command: normalizeText(input.command).trim() || "unknown",
188
+ exitCode: Number.isFinite(exitCode) ? Math.trunc(exitCode) : 1,
189
+ stdout: normalizeText(input.stdout),
190
+ stderr: normalizeText(input.stderr),
191
+ summary: normalizeText(input.summary),
192
+ };
193
+ }
194
+
195
+ function normalizeText(value: unknown): string {
196
+ return typeof value === "string" ? value : "";
197
+ }
198
+
199
+ function verifyResultBlockReason(run: ActiveRun): string | undefined {
200
+ if (run.phase === "verify") return undefined;
201
+ if (run.phase === "execute" && run.repair?.status === "repairing") return undefined;
202
+ return `kd_verify_result 只能在 verify 阶段记录,或在自动修复循环的 execute 阶段记录。当前阶段:${run.phase}。`;
203
+ }
204
+
205
+ function closeRepairQuestions(run: ActiveRun, evidence: string): void {
206
+ if (!Array.isArray(run.questions)) return;
207
+ const now = new Date().toISOString();
208
+ for (const question of run.questions) {
209
+ if (question.status !== "open" || !question.blocking || question.phase !== "verify") continue;
210
+ if (!isRepairQuestion(question.question, question.reason)) continue;
211
+ question.status = "answered";
212
+ question.answer = `验证已继续处理,最新证据:${evidence}`;
213
+ question.answeredAt = now;
214
+ }
215
+ }
216
+
217
+ function isRepairQuestion(question: string, reason?: string): boolean {
218
+ return /验证失败已达到/.test(question) || /最近失败证据:evidence\/verify-failure-\d+\.md/.test(reason ?? "");
219
+ }
220
+
221
+ function failureSignature(input: NormalizedVerifyResultInput): string {
222
+ const text = [input.stderr, input.stdout, input.summary].join("\n").replace(/\s+/g, " ").trim();
223
+ return `${input.exitCode}:${text.slice(0, 300)}`;
224
+ }