kcode-pi 0.1.23 → 0.1.24

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,25 @@
4
4
 
5
5
  ## 未发布
6
6
 
7
+ - 暂无。
8
+
9
+ ## 0.1.24 - 2026-06-07
10
+
7
11
  ### 新增
8
12
 
9
13
  - 新增更新日志文档,并在 README 文档导航中提供入口。
14
+ - 新增 `src/harness/prompt.ts`,集中管理运行时 Harness prompt。
15
+ - 新增 `src/harness/messages.ts`,集中管理 Harness 门禁、证据、路径、SDK/TDD 阻断消息。
16
+
17
+ ### 改进
18
+
19
+ - 收敛运行时 prompt,只保留状态、最近阶段资料、项目上下文摘要、当前阶段任务和核心约束。
20
+ - 精简 `kd-plan`、`kd-execute`、`kd-verify` 命令入口提示,减少和 skills、门禁规则的重复。
21
+ - 移除不存在的 IDE 相关验证表述,保留 Gradle/dotnet 的真实构建验证策略。
22
+
23
+ ### 验证
24
+
25
+ - `npm run release:check` 通过,包含 TypeScript 检查、CLI 构建、Harness smoke、KCode 命令 smoke、SDK 签名 smoke、package smoke 和 pack dry run。
10
26
 
11
27
  ## 0.1.23 - 2026-06-07
12
28
 
@@ -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)。
@@ -22,8 +22,8 @@ import { readArtifact } from "../src/harness/artifacts.ts";
22
22
  import { flagshipWriteBlockReason, isSourceLikePath, planWriteBlockReason } from "../src/harness/path-policy.ts";
23
23
  import { sdkSignatureProductionWriteBlockReason } from "../src/harness/sdk-policy.ts";
24
24
  import { tddProductionWriteBlockReason } from "../src/harness/tdd-policy.ts";
25
- import { readProjectContext } from "../src/context/project-context.ts";
26
25
  import { windowsPathHint } from "../src/platform/path.ts";
26
+ import { workflowPromptForRun } from "../src/harness/prompt.ts";
27
27
 
28
28
  function requireRun(cwd: string): ReturnType<typeof readActiveRun> {
29
29
  return readActiveRun(cwd);
@@ -109,77 +109,6 @@ function sendWorkflowPrompt(pi: ExtensionAPI, ctx: ExtensionContext, run: NonNul
109
109
  if (ctx.hasUI) ctx.ui.notify("KCode 工作流消息已排队。", "info");
110
110
  }
111
111
 
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") || "暂无阶段文档。";
176
- }
177
-
178
- function trimForPrompt(content: string, maxLength: number): string {
179
- if (content.length <= maxLength) return content;
180
- return `${content.slice(0, maxLength)}\n\n[...文档过长,已截断;如需完整内容必须读取本地文件...]`;
181
- }
182
-
183
112
  function codeWriteBlockReason(cwd: string, path: string | undefined): string | undefined {
184
113
  if (!path || !isSourceLikePath(path)) return undefined;
185
114
 
@@ -218,11 +147,11 @@ const kdQuestionTool = defineTool({
218
147
  name: "kd_question",
219
148
  label: "KD 问题",
220
149
  description:
221
- "创建、回答或列出金蝶 Harness 结构化问题。每次只能问一个最阻塞的短问题,不要批量列清单。",
150
+ "创建、回答或列出金蝶 Harness 结构化问题。每次只记录一个最阻塞的短问题。",
222
151
  parameters: Type.Object({
223
152
  action: Type.Optional(Type.String({ description: "操作类型:ask、answer 或 list,默认 ask。" })),
224
153
  id: Type.Optional(Type.String({ description: "回答问题时的问题编号,例如 Q-001。" })),
225
- question: Type.Optional(Type.String({ description: "提问内容。只能是一个短问题,不要写编号清单或多个问题。" })),
154
+ question: Type.Optional(Type.String({ description: "提问内容。使用一个短问题。" })),
226
155
  answer: Type.Optional(Type.String({ description: "用户答案,action=answer 时使用。" })),
227
156
  reason: Type.Optional(Type.String({ description: "说明为什么这个问题会阻塞当前阶段。" })),
228
157
  choices: Type.Optional(Type.Array(Type.String(), { description: "可选项,最多 3 个简短选项。" })),
@@ -367,7 +296,7 @@ export default function (pi: ExtensionAPI) {
367
296
  const path = typeof input.path === "string" ? input.path : undefined;
368
297
  const hint = path ? windowsPathHint(path) : undefined;
369
298
  if (hint && ["read", "write", "edit"].includes(event.toolName)) {
370
- const reason = `当前是 Windows 工作区,不能使用 WSL/MSYS 路径 ${path}。下一步:改用项目相对路径;如果必须使用绝对路径,使用 Windows 路径 ${hint};不要再尝试 /mnt/d 或 /d 路径。`;
299
+ const reason = `当前是 Windows 工作区,路径 ${path} 不是本项目使用的路径形式。下一步:改用项目相对路径;如果必须使用绝对路径,使用 Windows 路径 ${hint}。`;
371
300
  if (ctx.hasUI) ctx.ui.notify(reason, "warning");
372
301
  return { block: true, reason };
373
302
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kcode-pi",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
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.
@@ -115,10 +115,10 @@ export function defaultArtifactContent(phase: KdPhase, goal?: string, profile?:
115
115
  "- Java 语法/编译检查:优先使用当前项目 Gradle 命令,例如 `./gradlew build`、`.\\gradlew.bat build` 或 `./gradlew :模块:build`。",
116
116
  "- C# 语法/编译检查:使用 `dotnet build`、`dotnet build <.sln>` 或 `dotnet build <.csproj>`。",
117
117
  "- 允许的检查:本地 SDK 签名查证、官方 API/基类/方法查证、元数据查证、kd_check、Gradle/dotnet 构建输出、项目已有测试框架、外部接口最小验证。",
118
- "- 不要为了满足门禁引入第三方测试 jar 或框架。",
119
- "- 不要写“Kingdee IDE 中编译”作为验证方式;命令无法运行时记录真实阻塞原因,不能作为绿灯证据。",
118
+ "- 测试框架:优先使用项目已有测试基础设施。",
119
+ "- 命令无法运行时记录真实阻塞原因和残余风险,不能作为绿灯证据。",
120
120
  "- 如果无法自动化测试,记录一个产品相关、实现前应失败且实现后应通过的检查。",
121
- "- 禁止凭记忆或随包知识库猜 SDK 方法签名;签名事实必须来自当前项目 jar/dll、构建输出或官方元数据。",
121
+ "- SDK 方法签名事实必须来自当前项目 jar/dll、构建输出或官方元数据。",
122
122
  "",
123
123
  "## 验证命令",
124
124
  "",
@@ -10,6 +10,15 @@ 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
12
  import { EVIDENCE_INDEX, hasEvidenceEntry } 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
@@ -131,10 +133,6 @@ function hasRiskAssessment(cwd: string, run: ActiveRun): boolean {
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;
@@ -147,10 +145,7 @@ function riskSectionHasContent(content: string): boolean {
147
145
  function inspectOpenQuestions(run: ActiveRun): string | undefined {
148
146
  const open = (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 {
@@ -220,51 +215,6 @@ function evidenceArtifactSatisfied(cwd: string, run: ActiveRun, artifact: string
220
215
  return existsSync(join(runRoot(cwd, run), artifact)) && hasEvidenceEntry(cwd, run, artifact);
221
216
  }
222
217
 
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(";");
249
- }
250
-
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
- }
266
- }
267
-
268
218
  function planHasMetadataRequirement(cwd: string, run: ActiveRun): boolean {
269
219
  const plan = readArtifact(cwd, run, "plan") ?? "";
270
220
  return /kd_cosmic_metadata|cosmic-metadata|cosmic-metadata\.json|metadata evidence|字段元数据证据|元数据证据/i.test(plan);
@@ -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 {
@@ -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