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 +52 -0
- package/docs/EVIDENCE_AND_GATES.md +3 -4
- package/docs/HARNESS_WORKFLOW.md +1 -3
- package/extensions/kingdee-harness.ts +31 -75
- package/extensions/kingdee-header.ts +14 -1
- package/extensions/kingdee-tools.ts +19 -5
- package/package.json +1 -1
- package/prompts/kd-execute.md +2 -2
- package/prompts/kd-plan.md +2 -6
- package/prompts/kd-verify.md +1 -7
- package/skills/kd-cosmic-unittest/SKILL.md +1 -1
- package/skills/kd-execute/SKILL.md +1 -1
- package/skills/kd-plan/SKILL.md +1 -1
- package/skills/kd-verify/SKILL.md +1 -1
- package/src/harness/artifacts.ts +14 -6
- package/src/harness/evidence.ts +16 -5
- package/src/harness/gates.ts +19 -63
- package/src/harness/messages.ts +113 -0
- package/src/harness/path-policy.ts +10 -4
- package/src/harness/prompt.ts +68 -0
- package/src/harness/sdk-policy.ts +2 -1
- package/src/harness/state.ts +82 -11
- package/src/harness/tdd-policy.ts +12 -9
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`
|
|
167
|
+
风险原因应来自 `VERIFY.md` 和 `SHIP.md` 的真实验证结果。
|
package/docs/HARNESS_WORKFLOW.md
CHANGED
|
@@ -58,7 +58,7 @@ ship 汇总变更、验证证据、风险和后续事项
|
|
|
58
58
|
/kd-advance ship
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
""
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
""
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
179
|
-
|
|
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
|
|
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 =
|
|
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 ?
|
|
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
package/prompts/kd-execute.md
CHANGED
|
@@ -4,9 +4,9 @@ description: 在 Harness 门禁约束下执行当前金蝶实施计划。
|
|
|
4
4
|
|
|
5
5
|
使用 `kd-execute` skill。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
先检查当前 run 和门禁。通过后只实现 `PLAN.md` 批准的内容,更新 `EXECUTION.md`,并按计划记录红绿 evidence。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
实现后运行计划中的验证命令;命令无法运行时记录阻塞原因和残余风险。
|
|
10
10
|
|
|
11
11
|
用户补充说明:
|
|
12
12
|
|
package/prompts/kd-plan.md
CHANGED
|
@@ -4,13 +4,9 @@ description: 为当前金蝶 Harness run 编写实施计划。
|
|
|
4
4
|
|
|
5
5
|
使用 `kd-plan` skill。
|
|
6
6
|
|
|
7
|
-
读取 `CONTEXT.md` 和 `SPEC.md`,编写或更新 `PLAN.md
|
|
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
|
|
package/prompts/kd-verify.md
CHANGED
|
@@ -4,13 +4,7 @@ description: 验证当前金蝶实现并收集证据。
|
|
|
4
4
|
|
|
5
5
|
使用 `kd-verify` skill。
|
|
6
6
|
|
|
7
|
-
|
|
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
|
|
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
|
-
-
|
|
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.
|
package/skills/kd-plan/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
-
-
|
|
23
|
+
- Passing evidence must show the real command, `Exit: 0`, and useful output summary.
|
|
24
24
|
- Do not ship while verification evidence is missing.
|
package/src/harness/artifacts.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
"-
|
|
119
|
-
"-
|
|
126
|
+
"- 测试框架:优先使用项目已有测试基础设施。",
|
|
127
|
+
"- 命令无法运行时记录真实阻塞原因和残余风险,不能作为绿灯证据。",
|
|
120
128
|
"- 如果无法自动化测试,记录一个产品相关、实现前应失败且实现后应通过的检查。",
|
|
121
|
-
"-
|
|
129
|
+
"- SDK 方法签名事实必须来自当前项目 jar/dll、构建输出或官方元数据。",
|
|
122
130
|
"",
|
|
123
131
|
"## 验证命令",
|
|
124
132
|
"",
|
package/src/harness/evidence.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
105
|
-
|
|
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
|
}
|
package/src/harness/gates.ts
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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)) &&
|
|
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
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
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
|
|
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
|
|
54
|
+
return flagshipPlanNeedsCodePathReason();
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
if (planMentionsDiscoveredSourcePath(plan)) return undefined;
|
|
52
|
-
return
|
|
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
|
|
35
|
+
return sdkSignatureWriteBlockedReason(normalized, SDK_SIGNATURE_EVIDENCE);
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
function normalizeRelativePath(path: string): string {
|
package/src/harness/state.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
206
|
-
parsed.
|
|
207
|
-
parsed.
|
|
208
|
-
parsed.
|
|
209
|
-
parsed.
|
|
210
|
-
parsed.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
73
|
+
reason: greenEvidenceInvalidReason(evidenceName),
|
|
71
74
|
};
|
|
72
75
|
}
|
|
73
76
|
|