kcode-pi 0.1.24 → 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 +36 -0
- package/extensions/kingdee-harness.ts +31 -4
- package/extensions/kingdee-header.ts +14 -1
- package/extensions/kingdee-tools.ts +19 -5
- package/package.json +1 -1
- package/src/harness/artifacts.ts +11 -3
- package/src/harness/evidence.ts +16 -5
- package/src/harness/gates.ts +10 -4
- package/src/harness/state.ts +82 -11
package/docs/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,42 @@
|
|
|
6
6
|
|
|
7
7
|
- 暂无。
|
|
8
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
|
+
|
|
9
45
|
## 0.1.24 - 2026-06-07
|
|
10
46
|
|
|
11
47
|
### 新增
|
|
@@ -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,
|
|
@@ -109,6 +110,26 @@ function sendWorkflowPrompt(pi: ExtensionAPI, ctx: ExtensionContext, run: NonNul
|
|
|
109
110
|
if (ctx.hasUI) ctx.ui.notify("KCode 工作流消息已排队。", "info");
|
|
110
111
|
}
|
|
111
112
|
|
|
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;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function autoAdvanceTool(cwd: string, run: NonNullable<ReturnType<typeof readActiveRun>>): ReturnType<typeof advanceRunIfReady> {
|
|
130
|
+
return advanceRunIfReady(cwd, run);
|
|
131
|
+
}
|
|
132
|
+
|
|
112
133
|
function codeWriteBlockReason(cwd: string, path: string | undefined): string | undefined {
|
|
113
134
|
if (!path || !isSourceLikePath(path)) return undefined;
|
|
114
135
|
|
|
@@ -188,9 +209,10 @@ const kdQuestionTool = defineTool({
|
|
|
188
209
|
};
|
|
189
210
|
}
|
|
190
211
|
appendQuestionEventToArtifact(ctx.cwd, run, [`- 已回答 ${answered.id}:${answered.answer}`]);
|
|
212
|
+
const auto = autoAdvanceTool(ctx.cwd, run);
|
|
191
213
|
return {
|
|
192
|
-
content: [{ type: "text", text: `已记录 ${answered.id}
|
|
193
|
-
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 },
|
|
194
216
|
};
|
|
195
217
|
}
|
|
196
218
|
|
|
@@ -240,9 +262,10 @@ const kdQuestionTool = defineTool({
|
|
|
240
262
|
const answered = answerQuestion(ctx.cwd, run, question.id, interactiveAnswer);
|
|
241
263
|
if (answered) {
|
|
242
264
|
appendQuestionEventToArtifact(ctx.cwd, run, [`- 已回答 ${answered.id}:${answered.answer}`]);
|
|
265
|
+
const auto = autoAdvanceTool(ctx.cwd, run);
|
|
243
266
|
return {
|
|
244
|
-
content: [{ type: "text", text: `用户已回答 ${answered.id}:${answered.answer}` }],
|
|
245
|
-
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 },
|
|
246
269
|
};
|
|
247
270
|
}
|
|
248
271
|
}
|
|
@@ -427,6 +450,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
427
450
|
|
|
428
451
|
const updated = updateProductProfile(ctx.cwd, run, parsed.product, parsed.version);
|
|
429
452
|
ctx.ui.notify(`产品画像:${updated.profile?.product}/${updated.profile?.techStack}/${updated.profile?.language}`, "info");
|
|
453
|
+
autoAdvanceCommand(pi, ctx, updated, "产品画像已更新。");
|
|
430
454
|
},
|
|
431
455
|
});
|
|
432
456
|
|
|
@@ -447,6 +471,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
447
471
|
|
|
448
472
|
const updated = updateRisk(ctx.cwd, run, risk.risk, risk.reason);
|
|
449
473
|
ctx.ui.notify(`风险等级:${updated.riskAssessment?.level}(${updated.riskAssessment?.reason})`, "info");
|
|
474
|
+
autoAdvanceCommand(pi, ctx, updated, "风险评估已更新。");
|
|
450
475
|
},
|
|
451
476
|
});
|
|
452
477
|
|
|
@@ -498,6 +523,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
498
523
|
? updatePhaseArtifact(ctx.cwd, run, parsed.phase, parsed.content)
|
|
499
524
|
: ensurePhaseArtifact(ctx.cwd, run, parsed.phase);
|
|
500
525
|
ctx.ui.notify(`阶段文档已就绪:${path}`, "info");
|
|
526
|
+
autoAdvanceCommand(pi, ctx, readActiveRun(ctx.cwd) ?? run, `${parsed.phase} 阶段文档已更新。`);
|
|
501
527
|
},
|
|
502
528
|
});
|
|
503
529
|
|
|
@@ -522,6 +548,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
522
548
|
}
|
|
523
549
|
appendQuestionEventToArtifact(ctx.cwd, run, [`- 已回答 ${answered.id}:${answered.answer}`]);
|
|
524
550
|
ctx.ui.notify(`已记录 ${answered.id} 的答案`, "info");
|
|
551
|
+
autoAdvanceCommand(pi, ctx, readActiveRun(ctx.cwd) ?? run, `${answered.id} 已回答。`);
|
|
525
552
|
},
|
|
526
553
|
});
|
|
527
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/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 {
|
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,7 @@ import { executionStepsBlockReason, planStepsBlockReason } from "./plan-steps.ts
|
|
|
9
9
|
import { tddPlanBlockReason, tddVerifyBlockReason } from "./tdd-policy.ts";
|
|
10
10
|
import { SDK_SIGNATURE_EVIDENCE, hasValidSdkSignatureEvidence, requiresSdkSignatureEvidence } from "./sdk-policy.ts";
|
|
11
11
|
import { runRoot } from "./paths.ts";
|
|
12
|
-
import { EVIDENCE_INDEX,
|
|
12
|
+
import { EVIDENCE_INDEX, readEvidenceIndex } from "./evidence.ts";
|
|
13
13
|
import {
|
|
14
14
|
missingArtifactsReason,
|
|
15
15
|
missingEvidenceReason,
|
|
@@ -129,7 +129,7 @@ function productImplementationDeclaration(cwd: string, run: ActiveRun): boolean
|
|
|
129
129
|
function hasRiskAssessment(cwd: string, run: ActiveRun): boolean {
|
|
130
130
|
const level = run.riskAssessment?.level;
|
|
131
131
|
if (!level) return false;
|
|
132
|
-
if (run.riskAssessment?.reason.trim()) return true;
|
|
132
|
+
if (typeof run.riskAssessment?.reason === "string" && run.riskAssessment.reason.trim()) return true;
|
|
133
133
|
return riskSectionHasContent(readArtifact(cwd, run, "verify") ?? "") || riskSectionHasContent(readArtifact(cwd, run, "ship") ?? "");
|
|
134
134
|
}
|
|
135
135
|
|
|
@@ -143,7 +143,7 @@ function riskSectionHasContent(content: string): boolean {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
function inspectOpenQuestions(run: ActiveRun): string | undefined {
|
|
146
|
-
const open = (run.questions
|
|
146
|
+
const open = Array.isArray(run.questions) ? run.questions.filter((question) => question.status === "open" && question.blocking) : [];
|
|
147
147
|
if (open.length === 0) return undefined;
|
|
148
148
|
return openQuestionsReason(open);
|
|
149
149
|
}
|
|
@@ -212,7 +212,13 @@ function requiredEvidenceForPhase(cwd: string, run: ActiveRun, phase: KdPhase):
|
|
|
212
212
|
function evidenceArtifactSatisfied(cwd: string, run: ActiveRun, artifact: string): boolean {
|
|
213
213
|
if (artifact === SDK_SIGNATURE_EVIDENCE) return hasValidSdkSignatureEvidence(cwd, run);
|
|
214
214
|
if (artifact === EVIDENCE_INDEX) return existsSync(join(runRoot(cwd, run), artifact));
|
|
215
|
-
return existsSync(join(runRoot(cwd, run), artifact)) &&
|
|
215
|
+
return existsSync(join(runRoot(cwd, run), artifact)) && hasSuccessfulEvidenceEntry(cwd, run, artifact);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function hasSuccessfulEvidenceEntry(cwd: string, run: ActiveRun, artifact: string): boolean {
|
|
219
|
+
const normalized = artifact.replace(/\\/g, "/").replace(/^\/+/, "").replace(/^\.\//, "");
|
|
220
|
+
const entry = readEvidenceIndex(cwd, run).entries.find((item) => typeof item.path === "string" && item.path.replace(/\\/g, "/") === normalized);
|
|
221
|
+
return Boolean(entry && entry.exitCode === 0);
|
|
216
222
|
}
|
|
217
223
|
|
|
218
224
|
function planHasMetadataRequirement(cwd: string, run: ActiveRun): boolean {
|
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
|