kcode-pi 0.1.23 → 0.1.27

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.
package/docs/CHANGELOG.md CHANGED
@@ -4,9 +4,61 @@
4
4
 
5
5
  ## 未发布
6
6
 
7
+ - 暂无。
8
+
9
+ ## 0.1.27 - 2026-06-07
10
+
11
+ ### 修复
12
+
13
+ - 强化 active run / RUN.json 读取容错,过滤异常 `questions`、`artifacts`、`riskAssessment`、`gate` 字段,避免旧状态或坏状态触发门禁崩溃。
14
+ - Header 渲染门禁时增加兜底,门禁异常会显示为阻塞状态,不再导致 TUI 进程退出。
15
+ - 阶段产物存在但不可读或误建为目录时,现在按缺失处理,不再在读取 `CONTEXT.md`、`PLAN.md`、`EXECUTION.md` 等文件时抛出异常。
16
+
17
+ ### 验证
18
+
19
+ - `npm run smoke:harness` 通过,覆盖损坏 run state 和损坏 evidence index 的容错。
20
+
21
+ ## 0.1.26 - 2026-06-07
22
+
23
+ ### 修复
24
+
25
+ - 修复已有项目的 `evidence/index.json` 中存在异常 entry 时,Header 门禁渲染可能因读取 `path` 崩溃的问题。
26
+ - `readEvidenceIndex` 现在会过滤无效 evidence entry,`hasEvidenceEntry` 和 evidence 记录逻辑对坏数据保持容错。
27
+
28
+ ### 验证
29
+
30
+ - `npm run smoke:harness` 通过,覆盖损坏 evidence index 不再触发崩溃。
31
+
32
+ ## 0.1.25 - 2026-06-07
33
+
34
+ ### 修复
35
+
36
+ - 修复 `/kd-risk`、`/kd-product`、`/kd-answer`、`/kd-artifact` 等命令在解除门禁后不会自动尝试推进下一阶段的问题。
37
+ - 修复 `kd_question` 回答阻断问题后只刷新门禁、不尝试推进的问题。
38
+ - 修复 `kd_cosmic_config`、`kd_cosmic_metadata`、`kd_cosmic_api`、`kd_sdk_signature`、`kd_ksql_lint` 写入证据后不反馈自动推进结果的问题。
39
+ - 修复官方 evidence 只要文件存在就可能满足门禁的问题;现在要求 evidence index 中记录的退出码为 `0`。
40
+
41
+ ### 验证
42
+
43
+ - `npm run smoke:harness` 通过,覆盖阻断问题回答后的自动推进、风险更新后的自动推进尝试,以及失败 evidence 不能通过门禁。
44
+
45
+ ## 0.1.24 - 2026-06-07
46
+
7
47
  ### 新增
8
48
 
9
49
  - 新增更新日志文档,并在 README 文档导航中提供入口。
50
+ - 新增 `src/harness/prompt.ts`,集中管理运行时 Harness prompt。
51
+ - 新增 `src/harness/messages.ts`,集中管理 Harness 门禁、证据、路径、SDK/TDD 阻断消息。
52
+
53
+ ### 改进
54
+
55
+ - 收敛运行时 prompt,只保留状态、最近阶段资料、项目上下文摘要、当前阶段任务和核心约束。
56
+ - 精简 `kd-plan`、`kd-execute`、`kd-verify` 命令入口提示,减少和 skills、门禁规则的重复。
57
+ - 移除不存在的 IDE 相关验证表述,保留 Gradle/dotnet 的真实构建验证策略。
58
+
59
+ ### 验证
60
+
61
+ - `npm run release:check` 通过,包含 TypeScript 检查、CLI 构建、Harness smoke、KCode 命令 smoke、SDK 签名 smoke、package smoke 和 pack dry run。
10
62
 
11
63
  ## 0.1.23 - 2026-06-07
12
64
 
@@ -89,13 +89,12 @@ Exit: 0
89
89
  退出码:0
90
90
  ```
91
91
 
92
- 不能写:
92
+ 以下内容不能作为绿灯证据:
93
93
 
94
94
  - “需在开发环境验证”
95
95
  - “待验证”
96
96
  - “未执行”
97
97
  - “应该可以”
98
- - “Kingdee IDE 中编译”
99
98
 
100
99
  如果命令无法运行,记录真实阻塞原因和残余风险,不能把它当绿灯。
101
100
 
@@ -107,7 +106,7 @@ Exit: 0
107
106
  evidence/index.json
108
107
  ```
109
108
 
110
- KCode 内置工具和统一 evidence 写入路径会自动维护索引。不要手工塞文件绕过门禁。
109
+ KCode 内置工具和统一 evidence 写入路径会自动维护索引。
111
110
 
112
111
  常见 evidence:
113
112
 
@@ -165,4 +164,4 @@ medium
165
164
  high
166
165
  ```
167
166
 
168
- 风险原因应来自 `VERIFY.md` 和 `SHIP.md` 的真实验证结果,不要为了过门禁随手写 `low`。
167
+ 风险原因应来自 `VERIFY.md` 和 `SHIP.md` 的真实验证结果。
@@ -58,7 +58,7 @@ ship 汇总变更、验证证据、风险和后续事项
58
58
  /kd-advance ship
59
59
  ```
60
60
 
61
- 门禁不通过时,不要绕过。先按 `/kd-gate` 给出的原因补齐产品、文档、计划、证据或风险说明。
61
+ 门禁不通过时,先按 `/kd-gate` 给出的原因补齐产品、文档、计划、证据或风险说明。
62
62
 
63
63
  ## 阶段文档
64
64
 
@@ -107,7 +107,6 @@ evidence/index.json
107
107
  原则:
108
108
 
109
109
  - 一次只问一个当前最阻塞的问题。
110
- - 不要把 FormId、字段、触发时机、弹窗内容打包成问题清单。
111
110
  - 得到答案后再继续问下一个必要问题。
112
111
 
113
112
  ## 多个需求
@@ -143,7 +142,6 @@ KCode 会阻止过早写入 Java/XML/SQL/C# 等产品代码:
143
142
  - 未进入 `execute` 阶段不能写产品代码。
144
143
  - `execute` 只能写 `PLAN.md` 中批准的真实源码文件。
145
144
  - 临时发现要改新文件,先回到 `plan` 更新计划。
146
- - 不允许凭空写 demo、sample、scaffold。
147
145
  - 必须先理解当前业务项目已有目录、模块、包名、基类和本地封装。
148
146
 
149
147
  证据和门禁细节见 [证据和门禁](EVIDENCE_AND_GATES.md)。
@@ -4,6 +4,7 @@ import { formatStatus } from "../src/harness/format.ts";
4
4
  import { PHASE_ORDER, isKdPhase, type KdPhase } from "../src/harness/types.ts";
5
5
  import {
6
6
  advanceRun,
7
+ advanceRunIfReady,
7
8
  addQuestion,
8
9
  answerQuestion,
9
10
  createActiveRun,
@@ -22,8 +23,8 @@ import { readArtifact } from "../src/harness/artifacts.ts";
22
23
  import { flagshipWriteBlockReason, isSourceLikePath, planWriteBlockReason } from "../src/harness/path-policy.ts";
23
24
  import { sdkSignatureProductionWriteBlockReason } from "../src/harness/sdk-policy.ts";
24
25
  import { tddProductionWriteBlockReason } from "../src/harness/tdd-policy.ts";
25
- import { readProjectContext } from "../src/context/project-context.ts";
26
26
  import { windowsPathHint } from "../src/platform/path.ts";
27
+ import { workflowPromptForRun } from "../src/harness/prompt.ts";
27
28
 
28
29
  function requireRun(cwd: string): ReturnType<typeof readActiveRun> {
29
30
  return readActiveRun(cwd);
@@ -109,75 +110,24 @@ function sendWorkflowPrompt(pi: ExtensionAPI, ctx: ExtensionContext, run: NonNul
109
110
  if (ctx.hasUI) ctx.ui.notify("KCode 工作流消息已排队。", "info");
110
111
  }
111
112
 
112
- function workflowPromptForRun(cwd: string, run: NonNullable<ReturnType<typeof readActiveRun>>, userText: string): string {
113
- const status = formatStatus(cwd, run);
114
- const memory = workflowMemoryForRun(cwd, run);
115
- const phaseGuidance = phaseGuidanceForRun(run);
116
-
117
- return [
118
- "用户输入:",
119
- userText,
120
- "",
121
- "KCode 项目常驻上下文:",
122
- readProjectContext(cwd) ?? "未生成。请在终端运行 `kcode context --refresh` 后继续;在生成前不要猜测项目目录。",
123
- "",
124
- "KCode Harness 状态:",
125
- status,
126
- "",
127
- "KCode 本次工作流本地文档:",
128
- memory,
129
- "",
130
- "需求来源处理:如果用户输入的是本地文件路径,先读取该文件并抽取需求;如果是目录,先列出候选需求文档再读取最相关文件;如果是在线文档链接,先尝试访问或导出可读内容。遇到登录、权限或防下载限制时,只问一个最阻塞问题:请用户提供可访问链接、导出文件路径或关键内容。不要要求用户改写成 KCode 专用格式。",
131
- "",
132
- `当前 active run 对应本次需求目标:${run.goal ?? run.id}。如果用户补充的是同一目标下的需求文档或细节,先纳入当前阶段文档;如果是无关目标,再要求用户运行 /kd-start <新需求> 创建新 run,或 /kd-switch <run-id> 切换已有 run。`,
133
- "需要用户确认时,kd_question 一次只能问一个当前最阻塞的问题;不要把 FormId、触发时机、库存条件、弹窗内容、插件位置等打包成清单。选项最多 3 个;交互模式下会弹出选择/输入对话并自动记录答案。",
134
- "",
135
- phaseGuidance,
136
- "必须先理解当前业务项目已有目录、模块、包名、基类和本地封装,再决定文件位置和实现方式。",
137
- "禁止凭记忆、模型知识或随包知识库直接编写 SDK 方法调用。Java/C# 代码中出现的 SDK 类、方法、构造器、枚举和属性,必须来自 kd_sdk_signature 对当前项目 jar/dll 的成功结果、项目构建输出或官方元数据证据。",
138
- "kd_search、kd_cosmic_api 和随包知识只能用于找线索;没有 evidence/sdk-signature.md 或明确构建证据时,不得进入 execute,也不得写生产源码。",
139
- "路径规则:在 Windows 工作区内,优先使用项目相对路径;如需绝对路径必须使用 `D:\\...` 这类 Windows 路径,禁止把路径改写成 `/mnt/d/...`、`/d/...` 等 WSL/MSYS 风格路径。",
140
- "execute 阶段只能写 PLAN.md 明确列出的源码文件;如果目标文件不在计划内,必须先回到 plan 更新 PLAN.md。",
141
- "写生产源码前必须先有红灯证据 evidence/tdd-red.md;Java/C# 还必须有 SDK 签名证据 evidence/sdk-signature.md。红绿证据可以是 kd_sdk_signature 本地 SDK 签名、API/基类/方法签名、元数据、编译、既有测试框架或外部接口最小验证,不要为了测试引入额外 jar。",
142
- "语法/编译验证必须使用真实项目构建命令:Java/Cosmic/苍穹/星空旗舰版使用当前项目 Gradle 命令,例如 `./gradlew build`、`.\\gradlew.bat build` 或 `:模块:build`;C#/企业版使用 `dotnet build` 或 `dotnet build <.sln/.csproj>`。",
143
- "不要写“Kingdee IDE 中编译”作为验证方式或绿灯证据;命令无法运行时,记录真实阻塞原因和残余风险,不能标记为通过。",
144
- "如果门禁提示 evidence 内容无效,不要通过改写结论、补关键词或反复读取文件来过关;必须重新运行 PLAN.md 中声明的真实验证命令,并记录命令、Exit、STDOUT/STDERR 或工具输出。",
145
- ].join("\n");
146
- }
147
-
148
- function phaseGuidanceForRun(run: NonNullable<ReturnType<typeof readActiveRun>>): string {
149
- const guidance: Record<KdPhase, string> = {
150
- discuss:
151
- "当前处于 discuss。先理解需求来源,判断是一条需求还是一组需求;梳理可确认事实、边界、开放问题和后续需要验证的产品信息;不要编辑产品代码。",
152
- spec:
153
- "当前处于 spec。先把需求写成可验收条目;如果是一组需求,明确每条需求的验收标准、依赖、风险和批次关系;不要编辑产品代码。",
154
- plan:
155
- "当前处于 plan。先检查当前项目结构,写明真实目标路径、允许修改文件、查证项、验证命令和回滚说明;如果是一组需求,按批次组织计划;必须明确声明是否涉及产品实现、构建、元数据或 SDK 查证;不要编辑产品代码。",
156
- execute:
157
- "当前处于 execute。只能实现 PLAN.md 中批准的内容和文件。先读取 PLAN.md 和实际项目文件,遵循既有结构写真实可用代码;不要写 demo/sample/scaffold。",
158
- verify:
159
- "当前处于 verify。先使用 kd-verify 收集验证证据,不要继续扩大代码改动。",
160
- ship:
161
- "当前处于 ship。整理发布摘要、验证证据、风险和后续事项,不要继续扩大代码改动。",
162
- };
163
- return guidance[run.phase];
164
- }
165
-
166
- function workflowMemoryForRun(cwd: string, run: NonNullable<ReturnType<typeof readActiveRun>>): string {
167
- const phases = PHASE_ORDER.slice(0, PHASE_ORDER.indexOf(run.phase) + 1);
168
- return phases
169
- .map((phase) => {
170
- const content = readArtifact(cwd, run, phase);
171
- if (!content) return undefined;
172
- return [`## ${phase}`, trimForPrompt(content, 6000)].join("\n");
173
- })
174
- .filter(Boolean)
175
- .join("\n\n") || "暂无阶段文档。";
113
+ function autoAdvanceCommand(
114
+ pi: ExtensionAPI,
115
+ ctx: ExtensionContext,
116
+ run: NonNullable<ReturnType<typeof readActiveRun>>,
117
+ reason: string,
118
+ ): ReturnType<typeof advanceRunIfReady> {
119
+ const result = advanceRunIfReady(ctx.cwd, run);
120
+ if (result.advanced) {
121
+ if (ctx.hasUI) ctx.ui.notify(result.message, "info");
122
+ sendWorkflowPrompt(pi, ctx, result.run, `${reason}\n继续 KCode Harness run ${result.run.id}:${result.run.goal ?? "未知需求"}`);
123
+ } else if (!result.message.includes("最终阶段")) {
124
+ if (ctx.hasUI) ctx.ui.notify(`门禁仍阻塞:${result.message}`, "warning");
125
+ }
126
+ return result;
176
127
  }
177
128
 
178
- function trimForPrompt(content: string, maxLength: number): string {
179
- if (content.length <= maxLength) return content;
180
- return `${content.slice(0, maxLength)}\n\n[...文档过长,已截断;如需完整内容必须读取本地文件...]`;
129
+ function autoAdvanceTool(cwd: string, run: NonNullable<ReturnType<typeof readActiveRun>>): ReturnType<typeof advanceRunIfReady> {
130
+ return advanceRunIfReady(cwd, run);
181
131
  }
182
132
 
183
133
  function codeWriteBlockReason(cwd: string, path: string | undefined): string | undefined {
@@ -218,11 +168,11 @@ const kdQuestionTool = defineTool({
218
168
  name: "kd_question",
219
169
  label: "KD 问题",
220
170
  description:
221
- "创建、回答或列出金蝶 Harness 结构化问题。每次只能问一个最阻塞的短问题,不要批量列清单。",
171
+ "创建、回答或列出金蝶 Harness 结构化问题。每次只记录一个最阻塞的短问题。",
222
172
  parameters: Type.Object({
223
173
  action: Type.Optional(Type.String({ description: "操作类型:ask、answer 或 list,默认 ask。" })),
224
174
  id: Type.Optional(Type.String({ description: "回答问题时的问题编号,例如 Q-001。" })),
225
- question: Type.Optional(Type.String({ description: "提问内容。只能是一个短问题,不要写编号清单或多个问题。" })),
175
+ question: Type.Optional(Type.String({ description: "提问内容。使用一个短问题。" })),
226
176
  answer: Type.Optional(Type.String({ description: "用户答案,action=answer 时使用。" })),
227
177
  reason: Type.Optional(Type.String({ description: "说明为什么这个问题会阻塞当前阶段。" })),
228
178
  choices: Type.Optional(Type.Array(Type.String(), { description: "可选项,最多 3 个简短选项。" })),
@@ -259,9 +209,10 @@ const kdQuestionTool = defineTool({
259
209
  };
260
210
  }
261
211
  appendQuestionEventToArtifact(ctx.cwd, run, [`- 已回答 ${answered.id}:${answered.answer}`]);
212
+ const auto = autoAdvanceTool(ctx.cwd, run);
262
213
  return {
263
- content: [{ type: "text", text: `已记录 ${answered.id} 的答案,并刷新门禁。` }],
264
- details: { question: answered, gate: readActiveRun(ctx.cwd)?.gate },
214
+ content: [{ type: "text", text: `已记录 ${answered.id} 的答案。${auto.advanced ? auto.message : `门禁仍阻塞:${auto.message}`}` }],
215
+ details: { question: answered, gate: readActiveRun(ctx.cwd)?.gate, autoAdvance: auto },
265
216
  };
266
217
  }
267
218
 
@@ -311,9 +262,10 @@ const kdQuestionTool = defineTool({
311
262
  const answered = answerQuestion(ctx.cwd, run, question.id, interactiveAnswer);
312
263
  if (answered) {
313
264
  appendQuestionEventToArtifact(ctx.cwd, run, [`- 已回答 ${answered.id}:${answered.answer}`]);
265
+ const auto = autoAdvanceTool(ctx.cwd, run);
314
266
  return {
315
- content: [{ type: "text", text: `用户已回答 ${answered.id}:${answered.answer}` }],
316
- details: { question: answered, gate: readActiveRun(ctx.cwd)?.gate, answered: true },
267
+ content: [{ type: "text", text: `用户已回答 ${answered.id}:${answered.answer}。${auto.advanced ? auto.message : `门禁仍阻塞:${auto.message}`}` }],
268
+ details: { question: answered, gate: readActiveRun(ctx.cwd)?.gate, answered: true, autoAdvance: auto },
317
269
  };
318
270
  }
319
271
  }
@@ -367,7 +319,7 @@ export default function (pi: ExtensionAPI) {
367
319
  const path = typeof input.path === "string" ? input.path : undefined;
368
320
  const hint = path ? windowsPathHint(path) : undefined;
369
321
  if (hint && ["read", "write", "edit"].includes(event.toolName)) {
370
- const reason = `当前是 Windows 工作区,不能使用 WSL/MSYS 路径 ${path}。下一步:改用项目相对路径;如果必须使用绝对路径,使用 Windows 路径 ${hint};不要再尝试 /mnt/d 或 /d 路径。`;
322
+ const reason = `当前是 Windows 工作区,路径 ${path} 不是本项目使用的路径形式。下一步:改用项目相对路径;如果必须使用绝对路径,使用 Windows 路径 ${hint}。`;
371
323
  if (ctx.hasUI) ctx.ui.notify(reason, "warning");
372
324
  return { block: true, reason };
373
325
  }
@@ -498,6 +450,7 @@ export default function (pi: ExtensionAPI) {
498
450
 
499
451
  const updated = updateProductProfile(ctx.cwd, run, parsed.product, parsed.version);
500
452
  ctx.ui.notify(`产品画像:${updated.profile?.product}/${updated.profile?.techStack}/${updated.profile?.language}`, "info");
453
+ autoAdvanceCommand(pi, ctx, updated, "产品画像已更新。");
501
454
  },
502
455
  });
503
456
 
@@ -518,6 +471,7 @@ export default function (pi: ExtensionAPI) {
518
471
 
519
472
  const updated = updateRisk(ctx.cwd, run, risk.risk, risk.reason);
520
473
  ctx.ui.notify(`风险等级:${updated.riskAssessment?.level}(${updated.riskAssessment?.reason})`, "info");
474
+ autoAdvanceCommand(pi, ctx, updated, "风险评估已更新。");
521
475
  },
522
476
  });
523
477
 
@@ -569,6 +523,7 @@ export default function (pi: ExtensionAPI) {
569
523
  ? updatePhaseArtifact(ctx.cwd, run, parsed.phase, parsed.content)
570
524
  : ensurePhaseArtifact(ctx.cwd, run, parsed.phase);
571
525
  ctx.ui.notify(`阶段文档已就绪:${path}`, "info");
526
+ autoAdvanceCommand(pi, ctx, readActiveRun(ctx.cwd) ?? run, `${parsed.phase} 阶段文档已更新。`);
572
527
  },
573
528
  });
574
529
 
@@ -593,6 +548,7 @@ export default function (pi: ExtensionAPI) {
593
548
  }
594
549
  appendQuestionEventToArtifact(ctx.cwd, run, [`- 已回答 ${answered.id}:${answered.answer}`]);
595
550
  ctx.ui.notify(`已记录 ${answered.id} 的答案`, "info");
551
+ autoAdvanceCommand(pi, ctx, readActiveRun(ctx.cwd) ?? run, `${answered.id} 已回答。`);
596
552
  },
597
553
  });
598
554
  }
@@ -19,6 +19,19 @@ function formatGate(gate: GateResult | undefined): string {
19
19
  return gate.passed ? "门禁:通过" : "门禁:阻塞";
20
20
  }
21
21
 
22
+ function safeInspectGate(cwd: string, run: ActiveRun | undefined): GateResult | undefined {
23
+ if (!run) return undefined;
24
+ try {
25
+ return inspectGate(cwd, run);
26
+ } catch (error) {
27
+ return {
28
+ passed: false,
29
+ reason: error instanceof Error ? error.message : String(error),
30
+ checkedAt: new Date().toISOString(),
31
+ };
32
+ }
33
+ }
34
+
22
35
  function riskLevel(run: ActiveRun | undefined): string {
23
36
  return run?.riskAssessment?.level ?? "未知";
24
37
  }
@@ -54,7 +67,7 @@ export default function (pi: ExtensionAPI) {
54
67
  return {
55
68
  render(width: number): string[] {
56
69
  const run = readActiveRun(ctx.cwd);
57
- const gateState = run ? inspectGate(ctx.cwd, run) : undefined;
70
+ const gateState = safeInspectGate(ctx.cwd, run);
58
71
  const phase = formatPhase(run?.phase);
59
72
  const product = formatProduct(run);
60
73
  const gate = formatGate(gateState);
@@ -22,7 +22,7 @@ import {
22
22
  writeOfficialEvidence,
23
23
  } from "../src/official/kingdee-skills.ts";
24
24
  import { resolveWorkspacePath } from "../src/platform/path.ts";
25
- import { readActiveRun } from "../src/harness/state.ts";
25
+ import { advanceRunIfReady, readActiveRun } from "../src/harness/state.ts";
26
26
  import { writeEvidenceFile } from "../src/harness/evidence.ts";
27
27
  import { SDK_SIGNATURE_EVIDENCE } from "../src/harness/sdk-policy.ts";
28
28
 
@@ -97,12 +97,25 @@ async function runOrDryRun(
97
97
 
98
98
  const result = await runOfficialCommand(command);
99
99
  const evidencePath = evidenceFile ? writeOfficialEvidence(ctx.cwd, evidenceFile, result) : undefined;
100
+ const autoAdvance = evidencePath ? autoAdvanceAfterEvidence(ctx.cwd) : undefined;
100
101
  return {
101
- content: [{ type: "text" as const, text: formatCommandResult(result) }],
102
- details: { command: result.command, exitCode: result.exitCode, evidencePath },
102
+ content: [{ type: "text" as const, text: [formatCommandResult(result), evidencePath ? `已写入证据:${evidencePath}` : undefined, autoAdvance?.text].filter(Boolean).join("\n\n") }],
103
+ details: { command: result.command, exitCode: result.exitCode, evidencePath, autoAdvance: autoAdvance?.details },
103
104
  };
104
105
  }
105
106
 
107
+ function autoAdvanceAfterEvidence(cwd: string): { text: string; details: ReturnType<typeof advanceRunIfReady> } | undefined {
108
+ const run = readActiveRun(cwd);
109
+ if (!run) return undefined;
110
+
111
+ const result = advanceRunIfReady(cwd, run);
112
+ if (result.advanced) {
113
+ return { text: result.message, details: result };
114
+ }
115
+ if (result.message.includes("最终阶段")) return undefined;
116
+ return { text: `门禁仍阻塞:${result.message}`, details: result };
117
+ }
118
+
106
119
  const kdSearchTool = defineTool({
107
120
  name: "kd_search",
108
121
  label: "KD 搜索",
@@ -334,9 +347,10 @@ const kdSdkSignatureTool = defineTool({
334
347
  });
335
348
  const text = formatSdkSignatureResult(result);
336
349
  const evidencePath = result.exitCode === 0 ? writeSdkSignatureEvidence(ctx.cwd, text) : undefined;
350
+ const autoAdvance = evidencePath ? autoAdvanceAfterEvidence(ctx.cwd) : undefined;
337
351
  return {
338
- content: [{ type: "text", text: evidencePath ? `${text}\n\n已写入 SDK 签名证据:${evidencePath}` : text }],
339
- details: { product: profile.product, evidencePath, ...result },
352
+ content: [{ type: "text", text: [text, evidencePath ? `已写入 SDK 签名证据:${evidencePath}` : undefined, autoAdvance?.text].filter(Boolean).join("\n\n") }],
353
+ details: { product: profile.product, evidencePath, autoAdvance: autoAdvance?.details, ...result },
340
354
  };
341
355
  },
342
356
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kcode-pi",
3
- "version": "0.1.23",
3
+ "version": "0.1.27",
4
4
  "description": "面向金蝶开发的 Pi Coding Agent 启动器、工具包和 Harness 工作流",
5
5
  "type": "module",
6
6
  "private": false,
@@ -4,9 +4,9 @@ description: 在 Harness 门禁约束下执行当前金蝶实施计划。
4
4
 
5
5
  使用 `kd-execute` skill。
6
6
 
7
- 编辑代码前,先使用 `kd_plan_status` 检查当前 run。如果缺少 `PLAN.md`、`evidence/sdk-signature.md` 或门禁被阻塞,必须停止并说明缺少的文档或证据。通过后只实现 `PLAN.md` 批准的内容,并更新 `EXECUTION.md`。禁止凭记忆、模型知识或随包知识库猜 SDK 方法签名。
7
+ 先检查当前 run 和门禁。通过后只实现 `PLAN.md` 批准的内容,更新 `EXECUTION.md`,并按计划记录红绿 evidence。
8
8
 
9
- 实现后要按计划运行真实构建检查语法问题:Java 使用当前项目 Gradle 命令,例如 `.\gradlew.bat build` 或 `.\gradlew.bat :模块:build`;C# 使用 `dotnet build` 或 `dotnet build <.sln/.csproj>`。不能写“Kingdee IDE 中编译”作为验证结果;命令未运行就不能记录为绿灯证据。
9
+ 实现后运行计划中的验证命令;命令无法运行时记录阻塞原因和残余风险。
10
10
 
11
11
  用户补充说明:
12
12
 
@@ -4,13 +4,9 @@ description: 为当前金蝶 Harness run 编写实施计划。
4
4
 
5
5
  使用 `kd-plan` skill。
6
6
 
7
- 读取 `CONTEXT.md` 和 `SPEC.md`,编写或更新 `PLAN.md`。必须包含已检查的项目结构、需要查看的文件、预计修改的真实路径、是否涉及产品实现/构建/元数据/SDK 查证、必须查证的金蝶 API/元数据、SDK 签名证据、验证命令和回滚说明。Java/C# SDK 方法签名必须来自 `kd_sdk_signature` 当前项目 jar/dll、项目构建输出或官方元数据,不能凭记忆猜。
7
+ 读取 `CONTEXT.md` 和 `SPEC.md`,编写或更新 `PLAN.md`。重点写清:项目结构、目标路径、允许修改文件、SDK/元数据查证、验证命令和回滚说明。
8
8
 
9
- 验证命令必须贴近真实项目:
10
-
11
- - 苍穹 / 星瀚 / 星空旗舰版 Java:优先使用当前项目 Gradle 构建做语法和编译检查,例如 `./gradlew build`、`./gradlew :模块:build` 或 Windows 下的 `.\gradlew.bat build`。
12
- - C# / 企业版:使用 `dotnet build` 或 `dotnet build <.sln/.csproj>` 做语法和编译检查。
13
- - 不要写“Kingdee IDE 中编译”。如果当前机器缺少依赖导致命令无法运行,要记录真实阻塞原因,不能把未执行的编译当作绿灯证据。
9
+ 验证命令按产品选择:Cosmic Java 用当前项目 Gradle;企业版 C# 用 `dotnet build`。命令无法运行时记录阻塞原因和应运行的具体命令。
14
10
 
15
11
  用户补充说明:
16
12
 
@@ -4,13 +4,7 @@ description: 验证当前金蝶实现并收集证据。
4
4
 
5
5
  使用 `kd-verify` skill。
6
6
 
7
- 执行计划中的验证命令,收集证据,运行可用检查,并更新 `VERIFY.md`。如果某项验证无法运行,记录具体阻塞原因和残余风险。
8
-
9
- 验证优先使用真实构建命令检查语法和编译:
10
-
11
- - 苍穹 / 星瀚 / 星空旗舰版 Java:运行当前项目 Gradle 命令,例如 `.\gradlew.bat build`、`.\gradlew.bat :模块:build` 或同等 Gradle task。
12
- - C# / 企业版:运行 `dotnet build`、`dotnet build <.sln>` 或 `dotnet build <.csproj>`。
13
- - 绿灯证据必须包含实际命令、`Exit: 0` 和输出摘要。不能用“Kingdee IDE 中编译”“需在开发环境验证”替代真实证据。
7
+ 执行 `PLAN.md` 中的验证命令,收集 evidence,并更新 `VERIFY.md`。成功证据记录命令、`Exit: 0` 和输出摘要;命令无法运行时记录阻塞原因和残余风险。
14
8
 
15
9
  用户补充说明:
16
10
 
@@ -79,7 +79,7 @@ POJO 或简单枚举场景可以简要说明后直接实现。
79
79
  写完测试后:
80
80
 
81
81
  - 使用 `kd_build` 或计划中的命令运行最窄可行 Gradle test 任务。
82
- - 如果本机缺少业务 jar 或本地配置导致 Gradle 不能运行,明确说明真实阻塞原因,并给出应该运行的 Gradle task;不要写“在 IDE 中运行”作为验证结论。
82
+ - 如果本机缺少业务 jar 或本地配置导致 Gradle 不能运行,明确说明真实阻塞原因,并给出应该运行的 Gradle task
83
83
  - 存在 harness run 时,把测试文件和验证结果写入 `.pi/kd/runs/<run-id>/EXECUTION.md`。
84
84
 
85
85
  ## 输出要求
@@ -24,5 +24,5 @@ Rules:
24
24
  - Before entering verify, rerun the same check and record passing output in `evidence/tdd-green.md`.
25
25
  - After implementation, run the planned real build command for syntax/compile validation when available: Java uses the project Gradle command such as `.\gradlew.bat build` or `.\gradlew.bat :module:build`; C# uses `dotnet build` or `dotnet build <.sln/.csproj>`.
26
26
  - Do not add JUnit, Mockito, NUnit, xUnit, or any extra test jar/framework only to satisfy the gate. Use existing approved project test infrastructure if it already exists.
27
- - Do not record "compile in Kingdee IDE" or "needs local development environment verification" as green evidence. If the command cannot run, record the blocker instead of marking verification passed.
27
+ - If the command cannot run, record the blocker instead of marking verification passed.
28
28
  - If implementation needs a plan change, update `PLAN.md` first.
@@ -28,7 +28,7 @@ Gate:
28
28
  - Execution must not start without `PLAN.md`.
29
29
  - A plan for 星空旗舰版 is incomplete unless it records the existing project layout and exact target path to edit. If `code/` exists, follow its actual structure; if it does not, record the discovered source root or existing target file.
30
30
  - A plan without validation commands is incomplete.
31
- - A Java/C# plan that uses vague wording such as "compile in Kingdee IDE" instead of a real Gradle or dotnet command is incomplete.
31
+ - A Java/C# plan without a concrete Gradle or dotnet validation command is incomplete.
32
32
  - A plan without structured `STEP-001` execution steps is incomplete.
33
33
  - A plan without TDD red/green checks is incomplete.
34
34
  - A plan that relies on unverified Kingdee API names is incomplete; bundled knowledge alone is not enough when local SDK jars/dlls or compile evidence are available.
@@ -20,5 +20,5 @@ Rules:
20
20
 
21
21
  - Passing unit tests is not enough if acceptance criteria require workflow behavior.
22
22
  - If validation cannot run, state the exact blocker.
23
- - Do not use "Kingdee IDE" as a verification target. Evidence must show the real command, `Exit: 0`, and useful output summary.
23
+ - Passing evidence must show the real command, `Exit: 0`, and useful output summary.
24
24
  - Do not ship while verification evidence is missing.
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
2
  import type { ActiveRun, KdPhase } from "./types.ts";
3
3
  import { PHASE_ARTIFACTS } from "./types.ts";
4
4
  import { runArtifactPath, runRoot } from "./paths.ts";
@@ -10,7 +10,11 @@ export function ensureRunDirectories(cwd: string, run: ActiveRun): void {
10
10
  }
11
11
 
12
12
  export function artifactExists(cwd: string, run: ActiveRun, artifactName: string): boolean {
13
- return existsSync(runArtifactPath(cwd, run, artifactName));
13
+ try {
14
+ return statSync(runArtifactPath(cwd, run, artifactName)).isFile();
15
+ } catch {
16
+ return false;
17
+ }
14
18
  }
15
19
 
16
20
  export function phaseArtifactPath(cwd: string, run: ActiveRun, phase: KdPhase): string {
@@ -19,7 +23,11 @@ export function phaseArtifactPath(cwd: string, run: ActiveRun, phase: KdPhase):
19
23
 
20
24
  export function readArtifact(cwd: string, run: ActiveRun, phase: KdPhase): string | undefined {
21
25
  const path = phaseArtifactPath(cwd, run, phase);
22
- return existsSync(path) ? readFileSync(path, "utf8") : undefined;
26
+ try {
27
+ return existsSync(path) ? readFileSync(path, "utf8") : undefined;
28
+ } catch {
29
+ return undefined;
30
+ }
23
31
  }
24
32
 
25
33
  export function writeArtifact(cwd: string, run: ActiveRun, phase: KdPhase, content: string): string {
@@ -115,10 +123,10 @@ export function defaultArtifactContent(phase: KdPhase, goal?: string, profile?:
115
123
  "- Java 语法/编译检查:优先使用当前项目 Gradle 命令,例如 `./gradlew build`、`.\\gradlew.bat build` 或 `./gradlew :模块:build`。",
116
124
  "- C# 语法/编译检查:使用 `dotnet build`、`dotnet build <.sln>` 或 `dotnet build <.csproj>`。",
117
125
  "- 允许的检查:本地 SDK 签名查证、官方 API/基类/方法查证、元数据查证、kd_check、Gradle/dotnet 构建输出、项目已有测试框架、外部接口最小验证。",
118
- "- 不要为了满足门禁引入第三方测试 jar 或框架。",
119
- "- 不要写“Kingdee IDE 中编译”作为验证方式;命令无法运行时记录真实阻塞原因,不能作为绿灯证据。",
126
+ "- 测试框架:优先使用项目已有测试基础设施。",
127
+ "- 命令无法运行时记录真实阻塞原因和残余风险,不能作为绿灯证据。",
120
128
  "- 如果无法自动化测试,记录一个产品相关、实现前应失败且实现后应通过的检查。",
121
- "- 禁止凭记忆或随包知识库猜 SDK 方法签名;签名事实必须来自当前项目 jar/dll、构建输出或官方元数据。",
129
+ "- SDK 方法签名事实必须来自当前项目 jar/dll、构建输出或官方元数据。",
122
130
  "",
123
131
  "## 验证命令",
124
132
  "",
@@ -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,16 @@ 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
+ import {
14
+ missingArtifactsReason,
15
+ missingEvidenceReason,
16
+ missingForTargetReason,
17
+ missingMarkerReason,
18
+ openQuestionsReason,
19
+ unknownProductReason,
20
+ unknownRiskReason,
21
+ } from "./messages.ts";
13
22
 
14
23
  const REQUIRED_MARKERS: Partial<Record<KdPhase, string[]>> = {
15
24
  plan: ["## 验证命令"],
@@ -104,13 +113,6 @@ function gateResult(reasonParts: string[]): GateResult {
104
113
  };
105
114
  }
106
115
 
107
- function unknownProductReason(declaration: boolean | undefined): string {
108
- if (declaration === undefined) {
109
- return "不能进入 execute:产品画像未知,且 PLAN.md 未明确声明是否涉及产品实现、构建、元数据或 SDK 查证。下一步:先由当前需求和项目计划判断该范围;如果涉及产品实现,执行 /kd-product <flagship|xinghan|cangqiong|enterprise>;如果不涉及,在 PLAN.md 的“产品实现范围”写明“不涉及”及依据。";
110
- }
111
- return "不能进入 execute:PLAN.md 声明涉及产品实现、构建、元数据或 SDK 查证,但产品画像未知。下一步:根据需求、计划或用户回答确认产品,然后执行 /kd-product <flagship|xinghan|cangqiong|enterprise>;如果无法判断,先用 kd_question 只问一个最阻塞的产品确认问题。";
112
- }
113
-
114
116
  function productImplementationDeclaration(cwd: string, run: ActiveRun): boolean | undefined {
115
117
  const plan = readArtifact(cwd, run, "plan") ?? "";
116
118
  const line = plan
@@ -127,14 +129,10 @@ function productImplementationDeclaration(cwd: string, run: ActiveRun): boolean
127
129
  function hasRiskAssessment(cwd: string, run: ActiveRun): boolean {
128
130
  const level = run.riskAssessment?.level;
129
131
  if (!level) return false;
130
- if (run.riskAssessment?.reason.trim()) return true;
132
+ if (typeof run.riskAssessment?.reason === "string" && run.riskAssessment.reason.trim()) return true;
131
133
  return riskSectionHasContent(readArtifact(cwd, run, "verify") ?? "") || riskSectionHasContent(readArtifact(cwd, run, "ship") ?? "");
132
134
  }
133
135
 
134
- function unknownRiskReason(): string {
135
- return "不能进入 ship:风险等级或风险原因未知。下一步:根据 VERIFY.md 和 SHIP.md 的残余风险执行 /kd-risk <low|medium|high> <原因>,或在风险章节写入真实风险说明后再刷新门禁。";
136
- }
137
-
138
136
  function riskSectionHasContent(content: string): boolean {
139
137
  const match = content.match(/##\s*(残余风险|风险)\s*\r?\n([\s\S]*?)(?=\r?\n##\s+|$)/);
140
138
  if (!match) return false;
@@ -145,12 +143,9 @@ function riskSectionHasContent(content: string): boolean {
145
143
  }
146
144
 
147
145
  function inspectOpenQuestions(run: ActiveRun): string | undefined {
148
- 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) : [];
149
147
  if (open.length === 0) return undefined;
150
- return [
151
- `存在未回答的阻断问题:${open.map((question) => `${question.id} ${question.question}`).join(";")}`,
152
- "下一步:先向用户等待或获取答案,然后用 kd_question action=answer id=<问题编号> answer=<用户答案> 记录;不要绕过问题推进阶段。",
153
- ].join("。");
148
+ return openQuestionsReason(open);
154
149
  }
155
150
 
156
151
  function inspectStepState(cwd: string, run: ActiveRun, phase: KdPhase): string | undefined {
@@ -176,7 +171,7 @@ function inspectMarkers(cwd: string, run: ActiveRun, phase: KdPhase): string | u
176
171
 
177
172
  const missing = markers.filter((marker) => !content.includes(marker));
178
173
  if (missing.length === 0) return undefined;
179
- return `${PHASE_ARTIFACTS[phase]} 缺少必需章节:${missing.join(", ")}。下一步:更新 ${PHASE_ARTIFACTS[phase]},补齐这些章节并写入真实内容;不要只添加空标题。`;
174
+ return missingMarkerReason(phase, missing);
180
175
  }
181
176
 
182
177
  function inspectEvidence(cwd: string, run: ActiveRun, phase: KdPhase): string | undefined {
@@ -217,52 +212,13 @@ function requiredEvidenceForPhase(cwd: string, run: ActiveRun, phase: KdPhase):
217
212
  function evidenceArtifactSatisfied(cwd: string, run: ActiveRun, artifact: string): boolean {
218
213
  if (artifact === SDK_SIGNATURE_EVIDENCE) return hasValidSdkSignatureEvidence(cwd, run);
219
214
  if (artifact === EVIDENCE_INDEX) return existsSync(join(runRoot(cwd, run), artifact));
220
- return existsSync(join(runRoot(cwd, run), artifact)) && hasEvidenceEntry(cwd, run, artifact);
221
- }
222
-
223
- function missingArtifactsReason(artifacts: string[]): string {
224
- return [
225
- `缺少必需产物:${artifacts.join(", ")}`,
226
- `下一步:使用 /kd-artifact <阶段> 创建或更新阶段文档,或按当前阶段要求补写 ${artifacts.join(", ")} 的真实内容后再刷新门禁。`,
227
- ].join("。");
228
- }
229
-
230
- function missingForTargetReason(target: KdPhase, artifacts: string[]): string {
231
- const evidence = artifacts.filter((artifact) => artifact.startsWith("evidence/"));
232
- const documents = artifacts.filter((artifact) => !artifact.startsWith("evidence/"));
233
- const actions: string[] = [];
234
- if (documents.length > 0) {
235
- actions.push(`先补齐阶段文档 ${documents.join(", ")},可用 /kd-artifact 创建模板后填入真实分析、计划或验证内容`);
236
- }
237
- if (evidence.length > 0) {
238
- actions.push(evidenceAction(evidence));
239
- }
240
- return `不能进入 ${target}:缺少 ${artifacts.join(", ")}。下一步:${actions.join(";")}。`;
241
- }
242
-
243
- function missingEvidenceReason(artifacts: string[]): string {
244
- return `缺少必需证据:${artifacts.join(", ")}。下一步:${evidenceAction(artifacts)}。`;
245
- }
246
-
247
- function evidenceAction(artifacts: string[]): string {
248
- return artifacts.map(evidenceArtifactAction).join(";");
215
+ return existsSync(join(runRoot(cwd, run), artifact)) && hasSuccessfulEvidenceEntry(cwd, run, artifact);
249
216
  }
250
217
 
251
- function evidenceArtifactAction(artifact: string): string {
252
- switch (artifact) {
253
- case SDK_SIGNATURE_EVIDENCE:
254
- return "运行 kd_sdk_signature,从当前项目真实 SDK jar/dll 查证类、方法、构造器或属性签名,成功后自动写入 evidence/sdk-signature.md";
255
- case COSMIC_CONFIG_EVIDENCE:
256
- return "运行 kd_cosmic_config 生成 evidence/cosmic-config.txt;如果项目没有 ok-cosmic.json,先使用 KCode 默认配置,不要手写假结果";
257
- case COSMIC_METADATA_EVIDENCE:
258
- return "运行 kd_cosmic_metadata 查询目标表单/单据/字段元数据并生成 evidence/cosmic-metadata.json";
259
- case COSMIC_API_EVIDENCE:
260
- return "运行 kd_cosmic_api 查询相关 Cosmic API 线索并生成 evidence/cosmic-api.txt,再用 kd_sdk_signature 或构建输出确认签名";
261
- case KSQL_LINT_EVIDENCE:
262
- return "运行 kd_ksql_lint 校验 KSQL/SQL 交付内容并生成 evidence/ksql-lint.txt";
263
- default:
264
- return `运行 PLAN.md 中声明的验证命令,记录命令、Exit、STDOUT/STDERR 或工具输出到 ${artifact}`;
265
- }
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);
266
222
  }
267
223
 
268
224
  function planHasMetadataRequirement(cwd: string, run: ActiveRun): boolean {
@@ -0,0 +1,113 @@
1
+ import type { KdPhase } from "./types.ts";
2
+ import { PHASE_ARTIFACTS } from "./types.ts";
3
+
4
+ export function unknownProductReason(declaration: boolean | undefined): string {
5
+ if (declaration === undefined) {
6
+ return "不能进入 execute:产品画像未知,且 PLAN.md 未明确声明是否涉及产品实现、构建、元数据或 SDK 查证。下一步:先由当前需求和项目计划判断该范围;如果涉及产品实现,执行 /kd-product <flagship|xinghan|cangqiong|enterprise>;如果不涉及,在 PLAN.md 的“产品实现范围”写明“不涉及”及依据。";
7
+ }
8
+ return "不能进入 execute:PLAN.md 声明涉及产品实现、构建、元数据或 SDK 查证,但产品画像未知。下一步:根据需求、计划或用户回答确认产品,然后执行 /kd-product <flagship|xinghan|cangqiong|enterprise>;如果无法判断,先用 kd_question 只问一个最阻塞的产品确认问题。";
9
+ }
10
+
11
+ export function unknownRiskReason(): string {
12
+ return "不能进入 ship:风险等级或风险原因未知。下一步:根据 VERIFY.md 和 SHIP.md 的残余风险执行 /kd-risk <low|medium|high> <原因>,或在风险章节写入真实风险说明后再刷新门禁。";
13
+ }
14
+
15
+ export function openQuestionsReason(questions: Array<{ id: string; question: string }>): string {
16
+ return [
17
+ `存在未回答的阻断问题:${questions.map((question) => `${question.id} ${question.question}`).join(";")}`,
18
+ "下一步:先获取用户答案,然后用 kd_question action=answer id=<问题编号> answer=<用户答案> 记录。",
19
+ ].join("。");
20
+ }
21
+
22
+ export function missingMarkerReason(phase: KdPhase, missing: string[]): string {
23
+ return `${PHASE_ARTIFACTS[phase]} 缺少必需章节:${missing.join(", ")}。下一步:更新 ${PHASE_ARTIFACTS[phase]},补齐这些章节并写入真实内容。`;
24
+ }
25
+
26
+ export function missingArtifactsReason(artifacts: string[]): string {
27
+ return [
28
+ `缺少必需产物:${artifacts.join(", ")}`,
29
+ `下一步:使用 /kd-artifact <阶段> 创建或更新阶段文档,或按当前阶段要求补写 ${artifacts.join(", ")} 的真实内容后再刷新门禁。`,
30
+ ].join("。");
31
+ }
32
+
33
+ export function missingForTargetReason(target: KdPhase, artifacts: string[]): string {
34
+ const evidence = artifacts.filter((artifact) => artifact.startsWith("evidence/"));
35
+ const documents = artifacts.filter((artifact) => !artifact.startsWith("evidence/"));
36
+ const actions: string[] = [];
37
+ if (documents.length > 0) {
38
+ actions.push(`先补齐阶段文档 ${documents.join(", ")},可用 /kd-artifact 创建模板后填入真实分析、计划或验证内容`);
39
+ }
40
+ if (evidence.length > 0) {
41
+ actions.push(evidenceAction(evidence));
42
+ }
43
+ return `不能进入 ${target}:缺少 ${artifacts.join(", ")}。下一步:${actions.join(";")}。`;
44
+ }
45
+
46
+ export function missingEvidenceReason(artifacts: string[]): string {
47
+ return `缺少必需证据:${artifacts.join(", ")}。下一步:${evidenceAction(artifacts)}。`;
48
+ }
49
+
50
+ export function evidenceAction(artifacts: string[]): string {
51
+ return artifacts.map(evidenceArtifactAction).join(";");
52
+ }
53
+
54
+ export function evidenceArtifactAction(artifact: string): string {
55
+ switch (artifact) {
56
+ case "evidence/sdk-signature.md":
57
+ return "运行 kd_sdk_signature,从当前项目真实 SDK jar/dll 查证类、方法、构造器或属性签名,成功后自动写入 evidence/sdk-signature.md";
58
+ case "evidence/cosmic-config.txt":
59
+ return "运行 kd_cosmic_config 生成 evidence/cosmic-config.txt;如果项目没有 ok-cosmic.json,先使用 KCode 默认配置";
60
+ case "evidence/cosmic-metadata.json":
61
+ return "运行 kd_cosmic_metadata 查询目标表单/单据/字段元数据并生成 evidence/cosmic-metadata.json";
62
+ case "evidence/cosmic-api.txt":
63
+ return "运行 kd_cosmic_api 查询相关 Cosmic API 线索并生成 evidence/cosmic-api.txt,再用 kd_sdk_signature 或构建输出确认签名";
64
+ case "evidence/ksql-lint.txt":
65
+ return "运行 kd_ksql_lint 校验 KSQL/SQL 交付内容并生成 evidence/ksql-lint.txt";
66
+ default:
67
+ return `运行 PLAN.md 中声明的验证命令,记录命令、Exit、STDOUT/STDERR 或工具输出到 ${artifact}`;
68
+ }
69
+ }
70
+
71
+ export function flagshipWriteBlockedReason(path: string): string {
72
+ return `星空旗舰版代码必须跟随当前项目结构写入 code/ 下,不能写到 ${path}。下一步:先读取当前项目 code/ 下的真实模块结构,在 PLAN.md 记录目标源码路径,再把写入路径改为 code/... 下的项目相对路径。`;
73
+ }
74
+
75
+ export function planWriteBlockedReason(path: string): string {
76
+ return `PLAN.md 未批准写入 ${path}。下一步:停止写入该文件,回到 plan 阶段检查项目结构和影响范围,把该文件加入 PLAN.md 的 ## 允许修改的文件 和执行步骤;重新通过门禁后再写。`;
77
+ }
78
+
79
+ export function flagshipPlanNeedsCodePathReason(): string {
80
+ return "不能进入 execute:星空旗舰版 PLAN.md 需要先记录当前项目 code/ 下的实际目标路径。下一步:列出 code/ 下模块,识别当前项目是按云、按应用还是不分模块组织,在 PLAN.md 写明真实目标文件。";
81
+ }
82
+
83
+ export function flagshipPlanNeedsSourcePathReason(): string {
84
+ return "不能进入 execute:PLAN.md 需要先记录已检查当前项目结构,并写明实际源码根或目标文件路径。下一步:读取构建文件和 src/lib/bin 等目录,确认源码根后写入 PLAN.md 的 ## 已检查的项目结构、## 目标源码根 / 路径 和 ## 允许修改的文件。";
85
+ }
86
+
87
+ export function tddPlanMissingReason(): string {
88
+ return "PLAN.md 缺少 ## TDD / 红绿检查。下一步:回到 plan 补充该章节,明确红灯证据、绿灯证据、验证命令或无法自动化时的产品验证替代方案;至少写明 evidence/tdd-red.md、evidence/tdd-green.md 和要运行的真实检查。";
89
+ }
90
+
91
+ export function tddProductionMissingRedReason(path: string, evidenceName: string): string {
92
+ return `不能写生产源码 ${path}:缺少红灯证据 ${evidenceName}。下一步:先运行一个实现前应失败的检查,例如 kd_sdk_signature 方法不存在检查、元数据/API 检查、编译检查、kd_check、项目已有测试或外部接口最小验证;把命令、非 0 Exit 或失败输出写入 evidence/tdd-red.md 后再写生产源码。`;
93
+ }
94
+
95
+ export function tddVerifyBlockedReason(reasons: string[]): string {
96
+ return [
97
+ `不能进入 verify:${reasons.join(";")}。`,
98
+ "修复方式:重新运行 PLAN.md 中声明的同一验证命令或等价的产品验证命令,",
99
+ "并把命令、Exit、STDOUT/STDERR 或明确的工具输出写入对应 evidence 文件。",
100
+ ].join("");
101
+ }
102
+
103
+ export function redEvidenceInvalidReason(evidenceName: string): string {
104
+ return `${evidenceName} 内容无效:需要包含真实失败输出或非 0 退出码`;
105
+ }
106
+
107
+ export function greenEvidenceInvalidReason(evidenceName: string): string {
108
+ return `${evidenceName} 内容无效:绿灯证据需要同时包含成功结论和 Exit: 0/退出码:0`;
109
+ }
110
+
111
+ export function sdkSignatureWriteBlockedReason(path: string, evidenceName: string): string {
112
+ return `不能写生产源码 ${path}:缺少本地 SDK 签名证据 ${evidenceName}。下一步:运行 kd_sdk_signature,从当前项目真实 jar/dll 查证即将使用的 SDK 类、方法、构造器或属性签名;成功生成 evidence/sdk-signature.md 后再写代码。禁止凭记忆或随包知识库猜 SDK API。`;
113
+ }
@@ -1,6 +1,12 @@
1
1
  import type { ActiveRun } from "./types.ts";
2
2
  import { existsSync } from "node:fs";
3
3
  import { isAbsolute, join, relative } from "node:path";
4
+ import {
5
+ flagshipPlanNeedsCodePathReason,
6
+ flagshipPlanNeedsSourcePathReason,
7
+ flagshipWriteBlockedReason,
8
+ planWriteBlockedReason,
9
+ } from "./messages.ts";
4
10
 
5
11
  const SOURCE_EXTENSIONS = new Set([
6
12
  ".java",
@@ -23,7 +29,7 @@ export function flagshipWriteBlockReason(run: ActiveRun | undefined, path: strin
23
29
  if (normalized.startsWith(".pi/")) return undefined;
24
30
  if (cwd && !hasWorkspaceCodeDir(cwd)) return undefined;
25
31
  if (!normalized.startsWith("code/")) {
26
- return `星空旗舰版代码必须跟随当前项目结构写入 code/ 下,不能写到 ${path}。下一步:先读取当前项目 code/ 下的真实模块结构,在 PLAN.md 记录目标源码路径,再把写入路径改为 code/... 下的项目相对路径。`;
32
+ return flagshipWriteBlockedReason(path);
27
33
  }
28
34
 
29
35
  return undefined;
@@ -37,7 +43,7 @@ export function planWriteBlockReason(cwd: string, run: ActiveRun | undefined, pa
37
43
  if (normalized.startsWith(".pi/")) return undefined;
38
44
  if (planMentionsPath(plan, normalized)) return undefined;
39
45
 
40
- return `PLAN.md 未批准写入 ${normalized}。下一步:停止写入该文件,回到 plan 阶段检查项目结构和影响范围,把该文件加入 PLAN.md 的 ## 允许修改的文件 和执行步骤;重新通过门禁后再写。`;
46
+ return planWriteBlockedReason(normalized);
41
47
  }
42
48
 
43
49
  export function flagshipPlanBlockReason(cwd: string, run: ActiveRun | undefined, plan: string): string | undefined {
@@ -45,11 +51,11 @@ export function flagshipPlanBlockReason(cwd: string, run: ActiveRun | undefined,
45
51
 
46
52
  if (hasWorkspaceCodeDir(cwd)) {
47
53
  if (/(?:^|[\s`"'(])code[\\/][^\s`"')]+/i.test(plan)) return undefined;
48
- return "不能进入 execute:星空旗舰版 PLAN.md 必须先记录当前项目 code/ 下的实际目标路径;不要按固定模块规则猜路径。下一步:列出 code/ 下模块,识别当前项目是按云、按应用还是不分模块组织,在 PLAN.md 写明真实目标文件。";
54
+ return flagshipPlanNeedsCodePathReason();
49
55
  }
50
56
 
51
57
  if (planMentionsDiscoveredSourcePath(plan)) return undefined;
52
- return "不能进入 execute:PLAN.md 必须先记录已检查当前项目结构,并写明实际源码根或目标文件路径;当前项目没有 code/ 时更不能猜路径。下一步:读取构建文件和 src/lib/bin 等目录,确认源码根后写入 PLAN.md 的 ## 已检查的项目结构、## 目标源码根 / 路径 和 ## 允许修改的文件。";
58
+ return flagshipPlanNeedsSourcePathReason();
53
59
  }
54
60
 
55
61
  function hasWorkspaceCodeDir(cwd: string): boolean {
@@ -0,0 +1,68 @@
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
+
7
+ export function workflowPromptForRun(cwd: string, run: ActiveRun, userText: string): string {
8
+ const status = formatStatus(cwd, run);
9
+ const memory = workflowMemoryForRun(cwd, run);
10
+ const phaseGuidance = phaseGuidanceForRun(run.phase);
11
+ const projectContext = readProjectContext(cwd);
12
+
13
+ return [
14
+ "用户输入:",
15
+ userText,
16
+ "",
17
+ "KCode Harness 状态:",
18
+ status,
19
+ "",
20
+ "KCode 阶段资料:",
21
+ memory,
22
+ "",
23
+ "项目上下文:",
24
+ projectContext ? trimForPrompt(projectContext, 1200) : "未生成。需要项目结构时先运行或提示用户运行 `kcode context --refresh`。",
25
+ "",
26
+ "当前阶段任务:",
27
+ phaseGuidance,
28
+ "",
29
+ "核心约束:",
30
+ "- 产品代码只在 execute 阶段写入,并限于 PLAN.md 批准的文件。",
31
+ "- Java/C# SDK 签名以当前项目 jar/dll、构建输出或官方元数据为准。",
32
+ "- Java/Cosmic 用当前项目 Gradle;C#/企业版用 dotnet build。",
33
+ "- evidence 记录命令、Exit 和关键输出;命令无法运行时记录阻塞原因。",
34
+ "- Windows 下优先使用项目相对路径;绝对路径使用 D:\\... 形式。",
35
+ ].join("\n");
36
+ }
37
+
38
+ function phaseGuidanceForRun(phase: KdPhase): string {
39
+ const guidance: Record<KdPhase, string> = {
40
+ discuss: "梳理需求来源、范围、已知事实和一个最阻塞的待确认问题。",
41
+ spec: "把需求转成验收标准、数据对象、异常行为、依赖和风险。",
42
+ plan: "检查项目结构,写明目标路径、允许修改文件、查证项、验证命令和回滚说明。",
43
+ execute: "按 PLAN.md 实现,记录步骤结果、变更文件和 evidence。",
44
+ verify: "运行计划中的验证命令,更新 VERIFY.md、证据和残余风险。",
45
+ ship: "整理 SHIP.md,包括摘要、验证证据、风险和后续事项。",
46
+ };
47
+ return guidance[phase];
48
+ }
49
+
50
+ function workflowMemoryForRun(cwd: string, run: ActiveRun): string {
51
+ const currentIndex = PHASE_ORDER.indexOf(run.phase);
52
+ const phases = PHASE_ORDER.slice(Math.max(0, currentIndex - 1), currentIndex + 1);
53
+ return (
54
+ phases
55
+ .map((phase) => {
56
+ const content = readArtifact(cwd, run, phase);
57
+ if (!content) return undefined;
58
+ return [`## ${phase}`, trimForPrompt(content, 1500)].join("\n");
59
+ })
60
+ .filter(Boolean)
61
+ .join("\n\n") || `阶段文档路径:.pi/kd/runs/${run.id}/`
62
+ );
63
+ }
64
+
65
+ function trimForPrompt(content: string, maxLength: number): string {
66
+ if (content.length <= maxLength) return content;
67
+ return `${content.slice(0, maxLength)}\n\n[...已截断;需要完整内容时读取本地文件...]`;
68
+ }
@@ -4,6 +4,7 @@ import type { ActiveRun } from "./types.ts";
4
4
  import { runRoot } from "./paths.ts";
5
5
  import { isSourceLikePath } from "./path-policy.ts";
6
6
  import { hasEvidenceEntry } from "./evidence.ts";
7
+ import { sdkSignatureWriteBlockedReason } from "./messages.ts";
7
8
 
8
9
  export const SDK_SIGNATURE_EVIDENCE = "evidence/sdk-signature.md";
9
10
 
@@ -31,7 +32,7 @@ export function sdkSignatureProductionWriteBlockReason(cwd: string, run: ActiveR
31
32
  if (normalized.startsWith(".pi/")) return undefined;
32
33
  if (hasValidSdkSignatureEvidence(cwd, run)) return undefined;
33
34
 
34
- return `不能写生产源码 ${normalized}:缺少本地 SDK 签名证据 ${SDK_SIGNATURE_EVIDENCE}。下一步:运行 kd_sdk_signature,从当前项目真实 jar/dll 查证即将使用的 SDK 类、方法、构造器或属性签名;成功生成 evidence/sdk-signature.md 后再写代码。禁止凭记忆或随包知识库猜 SDK API。`;
35
+ return sdkSignatureWriteBlockedReason(normalized, SDK_SIGNATURE_EVIDENCE);
35
36
  }
36
37
 
37
38
  function normalizeRelativePath(path: string): string {
@@ -100,7 +100,7 @@ export function addQuestion(
100
100
  run: ActiveRun,
101
101
  input: { question: string; reason?: string; choices?: string[]; blocking?: boolean },
102
102
  ): KdQuestion {
103
- const existing = run.questions ?? [];
103
+ const existing = Array.isArray(run.questions) ? run.questions : [];
104
104
  const question: KdQuestion = {
105
105
  id: createQuestionId(existing.length + 1),
106
106
  phase: run.phase,
@@ -118,7 +118,7 @@ export function addQuestion(
118
118
  }
119
119
 
120
120
  export function answerQuestion(cwd: string, run: ActiveRun, id: string, answer: string): KdQuestion | undefined {
121
- const question = (run.questions ?? []).find((item) => item.id === id);
121
+ const question = (Array.isArray(run.questions) ? run.questions : []).find((item) => item.id === id);
122
122
  if (!question) return undefined;
123
123
  question.status = "answered";
124
124
  question.answer = answer.trim();
@@ -129,7 +129,7 @@ export function answerQuestion(cwd: string, run: ActiveRun, id: string, answer:
129
129
  }
130
130
 
131
131
  export function openBlockingQuestions(run: ActiveRun): KdQuestion[] {
132
- return (run.questions ?? []).filter((question) => question.status === "open" && question.blocking);
132
+ return (Array.isArray(run.questions) ? run.questions : []).filter((question) => question.status === "open" && question.blocking);
133
133
  }
134
134
 
135
135
  export function updateProductProfile(cwd: string, run: ActiveRun, productInput: string, version?: string): ActiveRun {
@@ -189,6 +189,21 @@ export function advanceRun(cwd: string, run: ActiveRun, requestedPhase?: KdPhase
189
189
  return { run, message: `已推进 Kingdee run 到阶段:${target}` };
190
190
  }
191
191
 
192
+ export function advanceRunIfReady(cwd: string, run: ActiveRun): { run: ActiveRun; advanced: boolean; message: string } {
193
+ const current = readRun(cwd, run.id) ?? run;
194
+ const target = nextPhase(current.phase);
195
+ if (!target) {
196
+ return { run: current, advanced: false, message: `Run 已处于最终阶段:${current.phase}` };
197
+ }
198
+
199
+ const result = advanceRun(cwd, current, target);
200
+ return {
201
+ run: result.run,
202
+ advanced: result.run.phase === target,
203
+ message: result.message,
204
+ };
205
+ }
206
+
192
207
  export function refreshGate(cwd: string, run: ActiveRun): ActiveRun {
193
208
  run.gate = inspectGate(cwd, run);
194
209
  writeActiveRun(cwd, run);
@@ -202,19 +217,75 @@ function writeRunState(cwd: string, run: ActiveRun): void {
202
217
  }
203
218
 
204
219
  function hydrateRun(parsed: ActiveRun): ActiveRun | undefined {
205
- if (!parsed.id || !isKdPhase(parsed.phase)) return undefined;
206
- parsed.artifacts ??= {};
207
- parsed.questions ??= [];
208
- parsed.status ??= "active";
209
- parsed.createdAt ??= parsed.updatedAt;
210
- parsed.updatedAt ??= parsed.createdAt;
220
+ if (!parsed || typeof parsed !== "object") return undefined;
221
+ if (typeof parsed.id !== "string" || !parsed.id.trim()) return undefined;
222
+ if (typeof parsed.phase !== "string" || !isKdPhase(parsed.phase)) return undefined;
223
+ parsed.id = parsed.id.trim();
224
+ parsed.artifacts = isPlainObject(parsed.artifacts) ? sanitizeArtifacts(parsed.artifacts) : {};
225
+ parsed.questions = Array.isArray(parsed.questions) ? parsed.questions.map(sanitizeQuestion).filter((question): question is KdQuestion => Boolean(question)) : [];
226
+ parsed.status = parsed.status === "paused" || parsed.status === "done" ? parsed.status : "active";
227
+ parsed.goal = typeof parsed.goal === "string" ? parsed.goal : undefined;
228
+ parsed.version = typeof parsed.version === "string" ? parsed.version : undefined;
229
+ parsed.createdAt = typeof parsed.createdAt === "string" ? parsed.createdAt : typeof parsed.updatedAt === "string" ? parsed.updatedAt : undefined;
230
+ parsed.updatedAt = typeof parsed.updatedAt === "string" ? parsed.updatedAt : parsed.createdAt;
211
231
  const legacyEdition = (parsed as ActiveRun & { edition?: string }).edition;
212
- parsed.profile = parsed.profile ?? profileForProduct(parsed.product ?? resolveProductProfile(legacyEdition).product);
232
+ const product = typeof parsed.profile?.product === "string" ? parsed.profile.product : typeof parsed.product === "string" ? parsed.product : resolveProductProfile(typeof legacyEdition === "string" ? legacyEdition : undefined).product;
233
+ parsed.profile = profileForProduct(product);
213
234
  parsed.product = parsed.profile.product;
214
- parsed.gate ??= { passed: false, checkedAt: new Date().toISOString() };
235
+ parsed.riskAssessment = sanitizeRiskAssessment(parsed.riskAssessment);
236
+ parsed.gate = sanitizeGate(parsed.gate);
215
237
  return parsed;
216
238
  }
217
239
 
240
+ function sanitizeArtifacts(value: Record<string, unknown>): Record<string, string> {
241
+ return Object.fromEntries(Object.entries(value).filter((entry): entry is [string, string] => typeof entry[0] === "string" && typeof entry[1] === "string"));
242
+ }
243
+
244
+ function sanitizeQuestion(value: unknown): KdQuestion | undefined {
245
+ if (!isPlainObject(value)) return undefined;
246
+ const phase = typeof value.phase === "string" && isKdPhase(value.phase) ? value.phase : undefined;
247
+ const question = typeof value.question === "string" ? value.question.trim() : "";
248
+ if (!phase || !question) return undefined;
249
+ const id = typeof value.id === "string" && value.id.trim() ? value.id.trim() : createQuestionId(1);
250
+ return {
251
+ id,
252
+ phase,
253
+ question,
254
+ reason: typeof value.reason === "string" && value.reason.trim() ? value.reason.trim() : undefined,
255
+ choices: Array.isArray(value.choices) ? value.choices.filter((choice): choice is string => typeof choice === "string" && Boolean(choice.trim())).map((choice) => choice.trim()) : undefined,
256
+ blocking: value.blocking !== false,
257
+ status: value.status === "answered" ? "answered" : "open",
258
+ answer: typeof value.answer === "string" ? value.answer : undefined,
259
+ createdAt: typeof value.createdAt === "string" ? value.createdAt : new Date().toISOString(),
260
+ answeredAt: typeof value.answeredAt === "string" ? value.answeredAt : undefined,
261
+ };
262
+ }
263
+
264
+ function sanitizeRiskAssessment(value: unknown): ActiveRun["riskAssessment"] {
265
+ if (!isPlainObject(value)) return undefined;
266
+ const level = value.level;
267
+ if (level !== "low" && level !== "medium" && level !== "high") return undefined;
268
+ return {
269
+ level,
270
+ reason: typeof value.reason === "string" ? value.reason : "",
271
+ source: value.source === "verify" || value.source === "ship" ? value.source : "manual",
272
+ updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : new Date().toISOString(),
273
+ };
274
+ }
275
+
276
+ function sanitizeGate(value: unknown): ActiveRun["gate"] {
277
+ if (!isPlainObject(value)) return { passed: false, checkedAt: new Date().toISOString() };
278
+ return {
279
+ passed: value.passed === true,
280
+ reason: typeof value.reason === "string" ? value.reason : undefined,
281
+ checkedAt: typeof value.checkedAt === "string" ? value.checkedAt : new Date().toISOString(),
282
+ };
283
+ }
284
+
285
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
286
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
287
+ }
288
+
218
289
  function createRunId(goal: string): string {
219
290
  const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "").replace("T", "-");
220
291
  const slug = goal
@@ -4,13 +4,20 @@ import type { ActiveRun } from "./types.ts";
4
4
  import { runRoot } from "./paths.ts";
5
5
  import { isSourceLikePath } from "./path-policy.ts";
6
6
  import { hasEvidenceEntry } from "./evidence.ts";
7
+ import {
8
+ greenEvidenceInvalidReason,
9
+ redEvidenceInvalidReason,
10
+ tddPlanMissingReason,
11
+ tddProductionMissingRedReason,
12
+ tddVerifyBlockedReason,
13
+ } from "./messages.ts";
7
14
 
8
15
  export const TDD_RED_EVIDENCE = "evidence/tdd-red.md";
9
16
  export const TDD_GREEN_EVIDENCE = "evidence/tdd-green.md";
10
17
 
11
18
  export function tddPlanBlockReason(plan: string): string | undefined {
12
19
  if (/##\s*TDD\s*\/\s*红绿检查/i.test(plan)) return undefined;
13
- return "PLAN.md 缺少 ## TDD / 红绿检查。下一步:回到 plan 补充该章节,明确红灯证据、绿灯证据、验证命令或无法自动化时的产品验证替代方案;至少写明 evidence/tdd-red.md、evidence/tdd-green.md 和要运行的真实检查。";
20
+ return tddPlanMissingReason();
14
21
  }
15
22
 
16
23
  export function tddProductionWriteBlockReason(cwd: string, run: ActiveRun | undefined, path: string | undefined): string | undefined {
@@ -22,7 +29,7 @@ export function tddProductionWriteBlockReason(cwd: string, run: ActiveRun | unde
22
29
  if (isTestLikePath(normalized)) return undefined;
23
30
  if (hasValidTddEvidence(cwd, run, "red")) return undefined;
24
31
 
25
- return `不能写生产源码 ${normalized}:缺少红灯证据 ${TDD_RED_EVIDENCE}。下一步:先运行一个实现前应失败的检查,例如 kd_sdk_signature 方法不存在检查、元数据/API 检查、编译检查、kd_check、项目已有测试或外部接口最小验证;把命令、非 0 Exit 或失败输出写入 evidence/tdd-red.md 后再写生产源码。`;
32
+ return tddProductionMissingRedReason(normalized, TDD_RED_EVIDENCE);
26
33
  }
27
34
 
28
35
  export function tddVerifyBlockReason(cwd: string, run: ActiveRun): string | undefined {
@@ -31,11 +38,7 @@ export function tddVerifyBlockReason(cwd: string, run: ActiveRun): string | unde
31
38
  validateTddEvidence(cwd, run, "green"),
32
39
  ].filter((result) => !result.valid);
33
40
  if (problems.length === 0) return undefined;
34
- return [
35
- `不能进入 verify:${problems.map((problem) => problem.reason).join(";")}。`,
36
- "修复方式:不要反复修改 evidence 文案;必须重新运行 PLAN.md 中声明的同一验证命令或等价的产品验证命令,",
37
- "并把命令、Exit、STDOUT/STDERR 或明确的工具输出写入对应 evidence 文件。",
38
- ].join("");
41
+ return tddVerifyBlockedReason(problems.map((problem) => problem.reason ?? "未知 TDD evidence 问题"));
39
42
  }
40
43
 
41
44
  function hasValidTddEvidence(cwd: string, run: ActiveRun, kind: "red" | "green"): boolean {
@@ -58,7 +61,7 @@ function validateTddEvidence(cwd: string, run: ActiveRun, kind: "red" | "green")
58
61
  const hasFailure = /red|fail|failed|failure|error|失败|未通过|Exit\s*[::]\s*[1-9]/i.test(content);
59
62
  return hasFailure
60
63
  ? { valid: true }
61
- : { valid: false, reason: `${evidenceName} 内容无效:必须包含真实失败输出或非 0 退出码,不能只写结论` };
64
+ : { valid: false, reason: redEvidenceInvalidReason(evidenceName) };
62
65
  }
63
66
 
64
67
  const hasGreenExit = /Exit\s*[::]\s*0|退出码\s*[::]\s*0/i.test(content);
@@ -67,7 +70,7 @@ function validateTddEvidence(cwd: string, run: ActiveRun, kind: "red" | "green")
67
70
 
68
71
  return {
69
72
  valid: false,
70
- reason: `${evidenceName} 内容无效:绿灯证据必须同时包含成功结论和 Exit: 0/退出码:0,不能只写“通过”或人工结论`,
73
+ reason: greenEvidenceInvalidReason(evidenceName),
71
74
  };
72
75
  }
73
76