kcode-pi 0.1.18 → 0.1.20
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/README.md +12 -1
- package/extensions/kingdee-harness.ts +50 -7
- package/extensions/kingdee-header.ts +25 -43
- package/extensions/kingdee-tools.ts +7 -8
- package/package.json +1 -1
- package/src/harness/evidence.ts +106 -0
- package/src/harness/format.ts +7 -1
- package/src/harness/gates.ts +72 -44
- package/src/harness/plan-steps.ts +2 -1
- package/src/harness/sdk-policy.ts +2 -0
- package/src/harness/state.ts +13 -2
- package/src/harness/tdd-policy.ts +2 -0
- package/src/harness/types.ts +10 -2
- package/src/official/kingdee-skills.ts +6 -6
package/README.md
CHANGED
|
@@ -293,6 +293,7 @@ kcode start --provider openai --model gpt-4o
|
|
|
293
293
|
```text
|
|
294
294
|
/kd-start [--product 产品] [--version 版本] <需求>
|
|
295
295
|
/kd-product <产品> [--version 版本]
|
|
296
|
+
/kd-risk <low|medium|high> <原因>
|
|
296
297
|
/kd-status
|
|
297
298
|
/kd-runs
|
|
298
299
|
/kd-switch <run-id>
|
|
@@ -300,7 +301,7 @@ kcode start --provider openai --model gpt-4o
|
|
|
300
301
|
/kd-finish
|
|
301
302
|
/kd-gate
|
|
302
303
|
/kd-advance [阶段]
|
|
303
|
-
/kd-artifact [阶段] [内容]
|
|
304
|
+
/kd-artifact [阶段] [内容] [--replace]
|
|
304
305
|
/kd-answer Q-001 <答案>
|
|
305
306
|
```
|
|
306
307
|
|
|
@@ -312,6 +313,14 @@ kcode start --provider openai --model gpt-4o
|
|
|
312
313
|
|
|
313
314
|
`/kd-start` 会创建新的功能点 run,并立即触发 Agent 进入 `discuss` 阶段。如果未识别出产品画像,例如显示 `(unknown/unknown)`,下一步应先确认产品、版本和技术栈,而不是直接写代码。
|
|
314
315
|
|
|
316
|
+
进入 `ship` 前必须确认风险等级:
|
|
317
|
+
|
|
318
|
+
```text
|
|
319
|
+
/kd-risk low 已完成本地构建和元数据检查,无残余交付风险
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
`/kd-artifact` 默认只创建缺失阶段文档。若阶段文档已存在,带内容调用不会直接覆盖;确认要整体替换时必须追加 `--replace`。
|
|
323
|
+
|
|
315
324
|
暂停后接续:
|
|
316
325
|
|
|
317
326
|
```text
|
|
@@ -376,6 +385,7 @@ KCode 会把金蝶开发需求纳入 Harness 工作流。你可以直接输入
|
|
|
376
385
|
.pi/kd/runs/<run-id>/VERIFY.md
|
|
377
386
|
.pi/kd/runs/<run-id>/SHIP.md
|
|
378
387
|
.pi/kd/runs/<run-id>/evidence/
|
|
388
|
+
.pi/kd/runs/<run-id>/evidence/index.json
|
|
379
389
|
```
|
|
380
390
|
|
|
381
391
|
下次重新 `kcode start` 时,KCode 会提示当前项目的 active run。执行 `/kd-resume` 后,KCode 会读取项目常驻上下文、active run 状态和已生成的阶段文档,再继续当前功能点的当前阶段。已完成或暂停的功能点仍保留在 `.pi/kd/runs/<run-id>/`,可用 `/kd-runs` 查看,用 `/kd-switch <run-id>` 切回。
|
|
@@ -403,6 +413,7 @@ ship 汇总变更、验证证据、风险和后续事项
|
|
|
403
413
|
- 写生产源码前必须已有 `evidence/tdd-red.md`,内容可以是 API/基类/方法签名检查、元数据检查、编译检查、既有测试框架或外部接口最小验证的失败输出。
|
|
404
414
|
- 进入 `execute` 后,只允许写入 `PLAN.md` 明确列出的源码文件;如果临时发现要改新文件,必须先回到 plan 更新 `PLAN.md`。
|
|
405
415
|
- 进入 `verify` 前,`EXECUTION.md` 必须逐个完成 `PLAN.md` 中的所有 `STEP-###`,并为每个步骤记录真实存在的 `evidence/...` 文件;同时必须已有 `evidence/tdd-red.md` 和 `evidence/tdd-green.md`。
|
|
416
|
+
- evidence 文件必须登记在 `evidence/index.json` 中。KCode 内置工具和 evidence 写入路径会自动维护索引;不要手工塞文件绕过门禁。
|
|
406
417
|
- `evidence/tdd-green.md` 必须包含真实成功输出和 `Exit: 0` 或 `退出码:0`;不能写“需在开发环境验证”“待验证”“未执行”等不确定结论。
|
|
407
418
|
- 如果门禁提示 evidence 内容无效,不要反复改文案或补关键词;必须重新运行 `PLAN.md` 中声明的真实验证命令,并记录命令、Exit、STDOUT/STDERR 或工具输出。
|
|
408
419
|
- Java / 苍穹 / 星空旗舰版的语法和编译验证优先使用当前项目 Gradle 命令,例如 `.\gradlew.bat build`、`./gradlew build` 或 `.\gradlew.bat :模块:build`。
|
|
@@ -15,7 +15,9 @@ import {
|
|
|
15
15
|
switchActiveRun,
|
|
16
16
|
updateProductProfile,
|
|
17
17
|
updatePhaseArtifact,
|
|
18
|
+
updateRisk,
|
|
18
19
|
} from "../src/harness/state.ts";
|
|
20
|
+
import type { KdRisk } from "../src/harness/types.ts";
|
|
19
21
|
import { readArtifact } from "../src/harness/artifacts.ts";
|
|
20
22
|
import { flagshipWriteBlockReason, isSourceLikePath, planWriteBlockReason } from "../src/harness/path-policy.ts";
|
|
21
23
|
import { sdkSignatureProductionWriteBlockReason } from "../src/harness/sdk-policy.ts";
|
|
@@ -30,18 +32,19 @@ function requireRun(cwd: string): ReturnType<typeof readActiveRun> {
|
|
|
30
32
|
return readActiveRun(cwd);
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
function parseArtifactArgs(args: string, currentPhase: KdPhase): { phase: KdPhase; content?: string } | undefined {
|
|
34
|
-
const
|
|
35
|
-
|
|
35
|
+
function parseArtifactArgs(args: string, currentPhase: KdPhase): { phase: KdPhase; content?: string; replace: boolean } | undefined {
|
|
36
|
+
const replace = /\s*(^|\s)--replace(\s|$)/.test(args);
|
|
37
|
+
const trimmed = args.replace(/\s*(^|\s)--replace(?=\s|$)/g, " ").trim();
|
|
38
|
+
if (!trimmed) return { phase: currentPhase, replace };
|
|
36
39
|
|
|
37
40
|
const [first, ...rest] = trimmed.split(/\s+/);
|
|
38
41
|
if (isKdPhase(first)) {
|
|
39
42
|
const contentStart = trimmed.indexOf(first) + first.length;
|
|
40
43
|
const content = trimmed.slice(contentStart).trim();
|
|
41
|
-
return { phase: first, content: content || undefined };
|
|
44
|
+
return { phase: first, content: content || undefined, replace };
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
return { phase: currentPhase, content: trimmed };
|
|
47
|
+
return { phase: currentPhase, content: trimmed, replace };
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
function parseStartArgs(args: string): { goal: string; product?: string; version?: string } {
|
|
@@ -73,6 +76,15 @@ function parseProductArgs(args: string): { product: string; version?: string } |
|
|
|
73
76
|
return { product, version: parsed.version };
|
|
74
77
|
}
|
|
75
78
|
|
|
79
|
+
function parseRiskArgs(args: string): { risk: KdRisk; reason: string } | undefined {
|
|
80
|
+
const [first, ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
81
|
+
const risk = first?.toLowerCase();
|
|
82
|
+
if (risk !== "low" && risk !== "medium" && risk !== "high") return undefined;
|
|
83
|
+
const reason = rest.join(" ").trim();
|
|
84
|
+
if (!reason) return undefined;
|
|
85
|
+
return { risk, reason };
|
|
86
|
+
}
|
|
87
|
+
|
|
76
88
|
function shouldStartHarnessFromInput(text: string): boolean {
|
|
77
89
|
if (!text.trim() || text.trim().startsWith("/")) return false;
|
|
78
90
|
return KINGDEE_INTENT_PATTERN.test(text);
|
|
@@ -320,6 +332,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
320
332
|
run = createActiveRun(ctx.cwd, event.text);
|
|
321
333
|
if (ctx.hasUI) {
|
|
322
334
|
ctx.ui.notify(`已启动 Kingdee Harness run:${run.id}(${run.profile?.product}/${run.profile?.techStack})`, "info");
|
|
335
|
+
if (run.profile?.product === "unknown") {
|
|
336
|
+
ctx.ui.notify("产品画像未识别。下一步先执行 /kd-product <flagship|cosmic|xinghan|cangqiong|enterprise>。", "warning");
|
|
337
|
+
}
|
|
323
338
|
}
|
|
324
339
|
}
|
|
325
340
|
|
|
@@ -390,6 +405,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
390
405
|
|
|
391
406
|
const run = createActiveRun(ctx.cwd, goal, parsed.product, parsed.version);
|
|
392
407
|
ctx.ui.notify(`已启动 Kingdee Harness run:${run.id}(${run.profile?.product}/${run.profile?.techStack})`, "info");
|
|
408
|
+
if (run.profile?.product === "unknown") {
|
|
409
|
+
ctx.ui.notify("产品画像未识别。下一步先执行 /kd-product <flagship|cosmic|xinghan|cangqiong|enterprise>。", "warning");
|
|
410
|
+
}
|
|
393
411
|
sendWorkflowPrompt(pi, ctx, run, `继续 KCode Harness run ${run.id}:${goal}`);
|
|
394
412
|
},
|
|
395
413
|
});
|
|
@@ -467,6 +485,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
467
485
|
},
|
|
468
486
|
});
|
|
469
487
|
|
|
488
|
+
pi.registerCommand("kd-risk", {
|
|
489
|
+
description: "设置当前 Kingdee run 风险等级:/kd-risk <low|medium|high> <原因>",
|
|
490
|
+
handler: async (args, ctx) => {
|
|
491
|
+
const run = requireRun(ctx.cwd);
|
|
492
|
+
if (!run) {
|
|
493
|
+
ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const risk = parseRiskArgs(args);
|
|
498
|
+
if (!risk) {
|
|
499
|
+
ctx.ui.notify("用法:/kd-risk <low|medium|high> <原因>", "error");
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const updated = updateRisk(ctx.cwd, run, risk.risk, risk.reason);
|
|
504
|
+
ctx.ui.notify(`风险等级:${updated.riskAssessment?.level}(${updated.riskAssessment?.reason})`, "info");
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
|
|
470
508
|
pi.registerCommand("kd-advance", {
|
|
471
509
|
description: `推进 Kingdee run 到下一阶段,或指定阶段:${PHASE_ORDER.join("|")}`,
|
|
472
510
|
handler: async (args, ctx) => {
|
|
@@ -492,7 +530,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
492
530
|
});
|
|
493
531
|
|
|
494
532
|
pi.registerCommand("kd-artifact", {
|
|
495
|
-
description: "
|
|
533
|
+
description: "创建阶段文档,或显式覆盖阶段文档:/kd-artifact [阶段] [内容] [--replace]",
|
|
496
534
|
handler: async (args, ctx) => {
|
|
497
535
|
const run = requireRun(ctx.cwd);
|
|
498
536
|
if (!run) {
|
|
@@ -502,7 +540,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
502
540
|
|
|
503
541
|
const parsed = parseArtifactArgs(args, run.phase);
|
|
504
542
|
if (!parsed) {
|
|
505
|
-
ctx.ui.notify("用法:/kd-artifact [阶段] [内容]", "error");
|
|
543
|
+
ctx.ui.notify("用法:/kd-artifact [阶段] [内容] [--replace]", "error");
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (parsed.content && readArtifact(ctx.cwd, run, parsed.phase) !== undefined && !parsed.replace) {
|
|
548
|
+
ctx.ui.notify(`拒绝覆盖 ${parsed.phase} 阶段文档。若确认要整体替换,请追加 --replace;追加内容应让 Agent 更新具体章节。`, "warning");
|
|
506
549
|
return;
|
|
507
550
|
}
|
|
508
551
|
|
|
@@ -1,52 +1,33 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
1
|
import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { inspectGate } from "../src/harness/gates.ts";
|
|
3
|
+
import { readActiveRun } from "../src/harness/state.ts";
|
|
4
|
+
import type { ActiveRun, GateResult } from "../src/harness/types.ts";
|
|
5
|
+
import { formatProductProfile } from "../src/product/profile.ts";
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
phase?: KdPhase;
|
|
10
|
-
product?: string;
|
|
11
|
-
profile?: {
|
|
12
|
-
product?: string;
|
|
13
|
-
techStack?: string;
|
|
14
|
-
language?: string;
|
|
15
|
-
};
|
|
16
|
-
risk?: "low" | "medium" | "high";
|
|
17
|
-
gate?: {
|
|
18
|
-
passed?: boolean;
|
|
19
|
-
reason?: string;
|
|
20
|
-
};
|
|
7
|
+
function formatProduct(run: ActiveRun | undefined): string {
|
|
8
|
+
if (!run) return "未选择";
|
|
9
|
+
if (run.profile?.product === "unknown") return "未确认";
|
|
10
|
+
return formatProductProfile(run.profile);
|
|
21
11
|
}
|
|
22
12
|
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
if (!existsSync(activeRunPath)) return undefined;
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
return JSON.parse(readFileSync(activeRunPath, "utf8")) as ActiveRun;
|
|
29
|
-
} catch {
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
13
|
+
function formatPhase(phase: ActiveRun["phase"] | undefined): string {
|
|
14
|
+
return phase ?? "空闲";
|
|
32
15
|
}
|
|
33
16
|
|
|
34
|
-
function
|
|
35
|
-
if (!
|
|
36
|
-
|
|
37
|
-
const techStack = run.profile?.techStack ?? "unknown";
|
|
38
|
-
const language = run.profile?.language ?? "unknown";
|
|
39
|
-
return `${product}/${techStack}/${language}`;
|
|
17
|
+
function formatGate(gate: GateResult | undefined): string {
|
|
18
|
+
if (!gate) return "门禁:待检查";
|
|
19
|
+
return gate.passed ? "门禁:通过" : "门禁:阻塞";
|
|
40
20
|
}
|
|
41
21
|
|
|
42
|
-
function
|
|
43
|
-
return
|
|
22
|
+
function riskLevel(run: ActiveRun | undefined): string {
|
|
23
|
+
return run?.riskAssessment?.level ?? "未知";
|
|
44
24
|
}
|
|
45
25
|
|
|
46
|
-
function
|
|
47
|
-
if (
|
|
48
|
-
if (
|
|
49
|
-
|
|
26
|
+
function riskColor(risk: string): "error" | "warning" | "muted" | "success" {
|
|
27
|
+
if (risk === "high") return "error";
|
|
28
|
+
if (risk === "medium") return "warning";
|
|
29
|
+
if (risk === "未知") return "muted";
|
|
30
|
+
return "success";
|
|
50
31
|
}
|
|
51
32
|
|
|
52
33
|
function padOrTrim(text: string, width: number): string {
|
|
@@ -73,10 +54,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
73
54
|
return {
|
|
74
55
|
render(width: number): string[] {
|
|
75
56
|
const run = readActiveRun(ctx.cwd);
|
|
57
|
+
const gateState = run ? inspectGate(ctx.cwd, run) : undefined;
|
|
76
58
|
const phase = formatPhase(run?.phase);
|
|
77
59
|
const product = formatProduct(run);
|
|
78
|
-
const gate = formatGate(
|
|
79
|
-
const risk = run
|
|
60
|
+
const gate = formatGate(gateState);
|
|
61
|
+
const risk = riskLevel(run);
|
|
80
62
|
const runId = run?.id ?? "无";
|
|
81
63
|
|
|
82
64
|
const status = [
|
|
@@ -85,9 +67,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
85
67
|
theme.fg("muted", " | 产品:"),
|
|
86
68
|
theme.fg("text", product),
|
|
87
69
|
theme.fg("muted", " | 风险:"),
|
|
88
|
-
theme.fg(risk
|
|
70
|
+
theme.fg(riskColor(risk), risk),
|
|
89
71
|
theme.fg("muted", " | "),
|
|
90
|
-
theme.fg(
|
|
72
|
+
theme.fg(gateState?.passed === false ? "error" : "muted", gate),
|
|
91
73
|
].join("");
|
|
92
74
|
|
|
93
75
|
return [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dirname, join } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import {
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
4
|
import { Type } from "@earendil-works/pi-ai";
|
|
5
5
|
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { formatSearchResults, formatTableSchema } from "../src/knowledge/format.ts";
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
} from "../src/official/kingdee-skills.ts";
|
|
24
24
|
import { resolveWorkspacePath } from "../src/platform/path.ts";
|
|
25
25
|
import { readActiveRun } from "../src/harness/state.ts";
|
|
26
|
-
import {
|
|
26
|
+
import { writeEvidenceFile } from "../src/harness/evidence.ts";
|
|
27
27
|
import { SDK_SIGNATURE_EVIDENCE } from "../src/harness/sdk-policy.ts";
|
|
28
28
|
|
|
29
29
|
const extensionDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -433,10 +433,10 @@ function writeSdkSignatureEvidence(cwd: string, content: string): string | undef
|
|
|
433
433
|
const run = readActiveRun(cwd);
|
|
434
434
|
if (!run) return undefined;
|
|
435
435
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
436
|
+
return writeEvidenceFile(
|
|
437
|
+
cwd,
|
|
438
|
+
run,
|
|
439
|
+
SDK_SIGNATURE_EVIDENCE,
|
|
440
440
|
[
|
|
441
441
|
"# SDK 签名证据",
|
|
442
442
|
"",
|
|
@@ -448,7 +448,6 @@ function writeSdkSignatureEvidence(cwd: string, content: string): string | undef
|
|
|
448
448
|
"```",
|
|
449
449
|
"",
|
|
450
450
|
].join("\n"),
|
|
451
|
-
"
|
|
451
|
+
{ kind: "sdk-signature", command: "kd_sdk_signature", exitCode: 0 },
|
|
452
452
|
);
|
|
453
|
-
return path;
|
|
454
453
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import type { ActiveRun } from "./types.ts";
|
|
4
|
+
import { runArtifactPath, runRoot } from "./paths.ts";
|
|
5
|
+
|
|
6
|
+
export interface EvidenceEntry {
|
|
7
|
+
path: string;
|
|
8
|
+
kind: string;
|
|
9
|
+
command?: string;
|
|
10
|
+
exitCode?: number;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
size: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface EvidenceIndex {
|
|
17
|
+
version: 1;
|
|
18
|
+
entries: EvidenceEntry[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const EVIDENCE_INDEX = "evidence/index.json";
|
|
22
|
+
|
|
23
|
+
export function evidenceIndexPath(cwd: string, run: ActiveRun): string {
|
|
24
|
+
return runArtifactPath(cwd, run, EVIDENCE_INDEX);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function readEvidenceIndex(cwd: string, run: ActiveRun): EvidenceIndex {
|
|
28
|
+
const path = evidenceIndexPath(cwd, run);
|
|
29
|
+
if (!existsSync(path)) return { version: 1, entries: [] };
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as EvidenceIndex;
|
|
32
|
+
return parsed.version === 1 && Array.isArray(parsed.entries) ? parsed : { version: 1, entries: [] };
|
|
33
|
+
} catch {
|
|
34
|
+
return { version: 1, entries: [] };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function hasEvidenceEntry(cwd: string, run: ActiveRun, path: string): boolean {
|
|
39
|
+
const normalized = normalizeEvidencePath(path);
|
|
40
|
+
return readEvidenceIndex(cwd, run).entries.some((entry) => normalizeEvidencePath(entry.path) === normalized);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function writeEvidenceFile(
|
|
44
|
+
cwd: string,
|
|
45
|
+
run: ActiveRun,
|
|
46
|
+
path: string,
|
|
47
|
+
content: string,
|
|
48
|
+
options: { kind?: string; command?: string; exitCode?: number } = {},
|
|
49
|
+
): string {
|
|
50
|
+
const normalized = normalizeEvidencePath(path);
|
|
51
|
+
if (!normalized.startsWith("evidence/") || normalized === EVIDENCE_INDEX) {
|
|
52
|
+
throw new Error(`非法 evidence 路径:${path}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const absolutePath = runArtifactPath(cwd, run, normalized);
|
|
56
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
57
|
+
writeFileSync(absolutePath, content.endsWith("\n") ? content : `${content}\n`, "utf8");
|
|
58
|
+
recordEvidence(cwd, run, normalized, options);
|
|
59
|
+
return absolutePath;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function recordEvidence(
|
|
63
|
+
cwd: string,
|
|
64
|
+
run: ActiveRun,
|
|
65
|
+
path: string,
|
|
66
|
+
options: { kind?: string; command?: string; exitCode?: number } = {},
|
|
67
|
+
): void {
|
|
68
|
+
const normalized = normalizeEvidencePath(path);
|
|
69
|
+
const absolutePath = join(runRoot(cwd, run), normalized);
|
|
70
|
+
if (!existsSync(absolutePath)) return;
|
|
71
|
+
|
|
72
|
+
const now = new Date().toISOString();
|
|
73
|
+
const size = statSync(absolutePath).size;
|
|
74
|
+
const index = readEvidenceIndex(cwd, run);
|
|
75
|
+
const existing = index.entries.find((entry) => normalizeEvidencePath(entry.path) === normalized);
|
|
76
|
+
if (existing) {
|
|
77
|
+
existing.kind = options.kind ?? existing.kind;
|
|
78
|
+
existing.command = options.command ?? existing.command;
|
|
79
|
+
existing.exitCode = options.exitCode ?? existing.exitCode;
|
|
80
|
+
existing.updatedAt = now;
|
|
81
|
+
existing.size = size;
|
|
82
|
+
} else {
|
|
83
|
+
index.entries.push({
|
|
84
|
+
path: normalized,
|
|
85
|
+
kind: options.kind ?? inferKind(normalized),
|
|
86
|
+
command: options.command,
|
|
87
|
+
exitCode: options.exitCode,
|
|
88
|
+
createdAt: now,
|
|
89
|
+
updatedAt: now,
|
|
90
|
+
size,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const indexPath = evidenceIndexPath(cwd, run);
|
|
95
|
+
mkdirSync(dirname(indexPath), { recursive: true });
|
|
96
|
+
writeFileSync(indexPath, `${JSON.stringify(index, null, 2)}\n`, "utf8");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function inferKind(path: string): string {
|
|
100
|
+
const name = path.split("/").at(-1) ?? path;
|
|
101
|
+
return name.replace(/\.(md|txt|json)$/i, "");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeEvidencePath(path: string): string {
|
|
105
|
+
return path.replace(/\\/g, "/").replace(/^\/+/, "").replace(/^\.\//, "");
|
|
106
|
+
}
|
package/src/harness/format.ts
CHANGED
|
@@ -21,7 +21,7 @@ export function formatStatus(cwd: string, run: ActiveRun | undefined): string {
|
|
|
21
21
|
`下一阶段:${next}`,
|
|
22
22
|
`产品:${formatProductProfile(refreshed.profile)}`,
|
|
23
23
|
`版本:${refreshed.version ?? "未选择"}`,
|
|
24
|
-
`风险:${refreshed
|
|
24
|
+
`风险:${formatRisk(refreshed)}`,
|
|
25
25
|
`门禁:${refreshed.gate.passed ? "通过" : "阻塞"}`,
|
|
26
26
|
refreshed.gate.reason ? `原因:${refreshed.gate.reason}` : undefined,
|
|
27
27
|
"",
|
|
@@ -30,3 +30,9 @@ export function formatStatus(cwd: string, run: ActiveRun | undefined): string {
|
|
|
30
30
|
.filter(Boolean)
|
|
31
31
|
.join("\n");
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
function formatRisk(run: ActiveRun): string {
|
|
35
|
+
const level = run.riskAssessment?.level ?? "未知";
|
|
36
|
+
const reason = run.riskAssessment?.reason?.trim();
|
|
37
|
+
return reason ? `${level}(${reason})` : level;
|
|
38
|
+
}
|
package/src/harness/gates.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { executionStepsBlockReason, planStepsBlockReason } from "./plan-steps.ts
|
|
|
9
9
|
import { tddPlanBlockReason, tddVerifyBlockReason } from "./tdd-policy.ts";
|
|
10
10
|
import { SDK_SIGNATURE_EVIDENCE, hasValidSdkSignatureEvidence, requiresSdkSignatureEvidence } from "./sdk-policy.ts";
|
|
11
11
|
import { runRoot } from "./paths.ts";
|
|
12
|
+
import { EVIDENCE_INDEX, hasEvidenceEntry } from "./evidence.ts";
|
|
12
13
|
|
|
13
14
|
const REQUIRED_MARKERS: Partial<Record<KdPhase, string[]>> = {
|
|
14
15
|
plan: ["## 验证命令"],
|
|
@@ -21,77 +22,79 @@ const COSMIC_METADATA_EVIDENCE = "evidence/cosmic-metadata.json";
|
|
|
21
22
|
const COSMIC_API_EVIDENCE = "evidence/cosmic-api.txt";
|
|
22
23
|
const KSQL_LINT_EVIDENCE = "evidence/ksql-lint.txt";
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
const currentIndex = PHASE_ORDER.indexOf(run.phase);
|
|
26
|
-
const missing: string[] = [];
|
|
27
|
-
|
|
28
|
-
for (let i = 0; i <= currentIndex; i++) {
|
|
29
|
-
const phase = PHASE_ORDER[i];
|
|
30
|
-
const artifact = PHASE_ARTIFACTS[phase];
|
|
31
|
-
if (!artifactExists(cwd, run, artifact)) missing.push(artifact);
|
|
32
|
-
}
|
|
25
|
+
type GateMode = "inspect" | "enter";
|
|
33
26
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const evidenceProblem = inspectEvidence(cwd, run, run.phase);
|
|
37
|
-
const questionProblem = inspectOpenQuestions(run);
|
|
38
|
-
const reasonParts = [];
|
|
39
|
-
if (missing.length > 0) reasonParts.push(missingArtifactsReason([...new Set(missing)]));
|
|
40
|
-
if (markerProblem) reasonParts.push(markerProblem);
|
|
41
|
-
if (stepProblem) reasonParts.push(stepProblem);
|
|
42
|
-
if (evidenceProblem) reasonParts.push(evidenceProblem);
|
|
43
|
-
if (questionProblem) reasonParts.push(questionProblem);
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
passed: reasonParts.length === 0,
|
|
47
|
-
reason: reasonParts.length > 0 ? reasonParts.join("; ") : undefined,
|
|
48
|
-
checkedAt: new Date().toISOString(),
|
|
49
|
-
};
|
|
27
|
+
export function inspectGate(cwd: string, run: ActiveRun): GateResult {
|
|
28
|
+
return gateResult(collectGateProblems(cwd, run, run.phase, "inspect"));
|
|
50
29
|
}
|
|
51
30
|
|
|
52
31
|
export function canEnterPhase(cwd: string, run: ActiveRun, target: KdPhase): GateResult {
|
|
53
|
-
|
|
32
|
+
return gateResult(collectGateProblems(cwd, run, target, "enter"));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function collectGateProblems(cwd: string, run: ActiveRun, phase: KdPhase, mode: GateMode): string[] {
|
|
36
|
+
const phaseIndex = PHASE_ORDER.indexOf(phase);
|
|
54
37
|
const missing: string[] = [];
|
|
55
38
|
const reasonParts: string[] = [];
|
|
56
39
|
|
|
57
|
-
if (
|
|
58
|
-
reasonParts.push(
|
|
59
|
-
"不能离开 discuss:产品画像未知。下一步:根据需求或用户回答确认产品,然后执行 /kd-product <flagship|cosmic|xinghan|cangqiong|enterprise>;如果无法判断,先用 kd_question 只问一个最阻塞的产品确认问题。",
|
|
60
|
-
);
|
|
40
|
+
if (phase !== "discuss" && !isKnownProduct(run.profile?.product ?? run.product)) {
|
|
41
|
+
reasonParts.push(unknownProductReason());
|
|
61
42
|
}
|
|
62
43
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
44
|
+
if (phaseIndex >= PHASE_ORDER.indexOf("ship") && !hasRiskAssessment(cwd, run)) {
|
|
45
|
+
reasonParts.push(unknownRiskReason());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const lastRequiredArtifactIndex = mode === "inspect" ? phaseIndex : phaseIndex - 1;
|
|
49
|
+
for (let i = 0; i <= lastRequiredArtifactIndex; i++) {
|
|
50
|
+
const requiredPhase = PHASE_ORDER[i];
|
|
51
|
+
const artifact = PHASE_ARTIFACTS[requiredPhase];
|
|
66
52
|
if (!artifactExists(cwd, run, artifact)) missing.push(artifact);
|
|
67
53
|
}
|
|
68
54
|
|
|
69
|
-
if (
|
|
55
|
+
if (mode === "enter" && phase === "execute" && !artifactExists(cwd, run, PHASE_ARTIFACTS.plan)) {
|
|
70
56
|
missing.push(PHASE_ARTIFACTS.plan);
|
|
71
57
|
}
|
|
72
58
|
|
|
73
|
-
const
|
|
59
|
+
const markerProblem = mode === "inspect" ? inspectMarkers(cwd, run, phase) : undefined;
|
|
60
|
+
const flagshipPathProblem = mode === "enter" && phase === "execute" ? flagshipPlanBlockReason(cwd, run, readArtifact(cwd, run, "plan") ?? "") : undefined;
|
|
61
|
+
const stepProblem = inspectStepState(cwd, run, phase);
|
|
62
|
+
const evidenceProblem = mode === "inspect" ? inspectEvidence(cwd, run, phase) : undefined;
|
|
63
|
+
const questionProblem = inspectOpenQuestions(run);
|
|
64
|
+
|
|
74
65
|
if (flagshipPathProblem) {
|
|
75
66
|
reasonParts.push(flagshipPathProblem);
|
|
76
67
|
}
|
|
77
|
-
|
|
78
|
-
|
|
68
|
+
if (markerProblem) {
|
|
69
|
+
reasonParts.push(markerProblem);
|
|
70
|
+
}
|
|
79
71
|
if (stepProblem) {
|
|
80
72
|
reasonParts.push(stepProblem);
|
|
81
73
|
}
|
|
82
|
-
|
|
83
|
-
|
|
74
|
+
if (evidenceProblem) {
|
|
75
|
+
reasonParts.push(evidenceProblem);
|
|
76
|
+
}
|
|
84
77
|
if (questionProblem) {
|
|
85
78
|
reasonParts.push(questionProblem);
|
|
86
79
|
}
|
|
87
80
|
|
|
88
|
-
|
|
81
|
+
if (mode === "enter") {
|
|
82
|
+
missing.push(...missingEvidenceForPhase(cwd, run, phase));
|
|
89
83
|
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
if (phase === "ship" && !artifactExists(cwd, run, PHASE_ARTIFACTS.verify)) {
|
|
85
|
+
missing.push(PHASE_ARTIFACTS.verify);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (missing.length > 0) {
|
|
90
|
+
const uniqueMissing = [...new Set(missing)];
|
|
91
|
+
reasonParts.push(mode === "inspect" ? missingArtifactsReason(uniqueMissing) : missingForTargetReason(phase, uniqueMissing));
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
return reasonParts;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function gateResult(reasonParts: string[]): GateResult {
|
|
95
98
|
const reason = reasonParts.length > 0 ? reasonParts.join("; ") : undefined;
|
|
96
99
|
return {
|
|
97
100
|
passed: !reason,
|
|
@@ -100,6 +103,30 @@ export function canEnterPhase(cwd: string, run: ActiveRun, target: KdPhase): Gat
|
|
|
100
103
|
};
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
function unknownProductReason(): string {
|
|
107
|
+
return "不能离开 discuss:产品画像未知。下一步:根据需求或用户回答确认产品,然后执行 /kd-product <flagship|cosmic|xinghan|cangqiong|enterprise>;如果无法判断,先用 kd_question 只问一个最阻塞的产品确认问题。";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function hasRiskAssessment(cwd: string, run: ActiveRun): boolean {
|
|
111
|
+
const level = run.riskAssessment?.level;
|
|
112
|
+
if (!level) return false;
|
|
113
|
+
if (run.riskAssessment?.reason.trim()) return true;
|
|
114
|
+
return riskSectionHasContent(readArtifact(cwd, run, "verify") ?? "") || riskSectionHasContent(readArtifact(cwd, run, "ship") ?? "");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function unknownRiskReason(): string {
|
|
118
|
+
return "不能进入 ship:风险等级或风险原因未知。下一步:根据 VERIFY.md 和 SHIP.md 的残余风险执行 /kd-risk <low|medium|high> <原因>,或在风险章节写入真实风险说明后再刷新门禁。";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function riskSectionHasContent(content: string): boolean {
|
|
122
|
+
const match = content.match(/##\s*(残余风险|风险)\s*\r?\n([\s\S]*?)(?=\r?\n##\s+|$)/);
|
|
123
|
+
if (!match) return false;
|
|
124
|
+
return match[2]
|
|
125
|
+
.split(/\r?\n/)
|
|
126
|
+
.map((line) => line.trim())
|
|
127
|
+
.some((line) => line && !/^[-*]?\s*(无|未知|待确认|待补充|none|unknown)$/i.test(line));
|
|
128
|
+
}
|
|
129
|
+
|
|
103
130
|
function inspectOpenQuestions(run: ActiveRun): string | undefined {
|
|
104
131
|
const open = (run.questions ?? []).filter((question) => question.status === "open" && question.blocking);
|
|
105
132
|
if (open.length === 0) return undefined;
|
|
@@ -172,7 +199,8 @@ function requiredEvidenceForPhase(cwd: string, run: ActiveRun, phase: KdPhase):
|
|
|
172
199
|
|
|
173
200
|
function evidenceArtifactSatisfied(cwd: string, run: ActiveRun, artifact: string): boolean {
|
|
174
201
|
if (artifact === SDK_SIGNATURE_EVIDENCE) return hasValidSdkSignatureEvidence(cwd, run);
|
|
175
|
-
return existsSync(join(runRoot(cwd, run), artifact));
|
|
202
|
+
if (artifact === EVIDENCE_INDEX) return existsSync(join(runRoot(cwd, run), artifact));
|
|
203
|
+
return existsSync(join(runRoot(cwd, run), artifact)) && hasEvidenceEntry(cwd, run, artifact);
|
|
176
204
|
}
|
|
177
205
|
|
|
178
206
|
function missingArtifactsReason(artifacts: string[]): string {
|
|
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { ActiveRun } from "./types.ts";
|
|
4
4
|
import { runRoot } from "./paths.ts";
|
|
5
|
+
import { hasEvidenceEntry } from "./evidence.ts";
|
|
5
6
|
|
|
6
7
|
export interface PlanStep {
|
|
7
8
|
id: string;
|
|
@@ -49,7 +50,7 @@ export function executionStepsBlockReason(cwd: string, run: ActiveRun, plan: str
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
const evidencePaths = [...line.matchAll(EVIDENCE_PATTERN)].map((match) => normalizeEvidencePath(match[0]));
|
|
52
|
-
if (evidencePaths.length === 0 || !evidencePaths.some((path) => existsSync(join(runRoot(cwd, run), path)))) {
|
|
53
|
+
if (evidencePaths.length === 0 || !evidencePaths.some((path) => existsSync(join(runRoot(cwd, run), path)) && hasEvidenceEntry(cwd, run, path))) {
|
|
53
54
|
missingEvidence.push(step.id);
|
|
54
55
|
}
|
|
55
56
|
}
|
|
@@ -3,6 +3,7 @@ import { isAbsolute, join, relative } from "node:path";
|
|
|
3
3
|
import type { ActiveRun } from "./types.ts";
|
|
4
4
|
import { runRoot } from "./paths.ts";
|
|
5
5
|
import { isSourceLikePath } from "./path-policy.ts";
|
|
6
|
+
import { hasEvidenceEntry } from "./evidence.ts";
|
|
6
7
|
|
|
7
8
|
export const SDK_SIGNATURE_EVIDENCE = "evidence/sdk-signature.md";
|
|
8
9
|
|
|
@@ -15,6 +16,7 @@ export function hasValidSdkSignatureEvidence(cwd: string, run: ActiveRun): boole
|
|
|
15
16
|
if (!requiresSdkSignatureEvidence(run)) return true;
|
|
16
17
|
const path = join(runRoot(cwd, run), SDK_SIGNATURE_EVIDENCE);
|
|
17
18
|
if (!existsSync(path)) return false;
|
|
19
|
+
if (!hasEvidenceEntry(cwd, run, SDK_SIGNATURE_EVIDENCE)) return false;
|
|
18
20
|
|
|
19
21
|
const content = readFileSync(path, "utf8");
|
|
20
22
|
return /(退出码|Exit)\s*[::]\s*0/i.test(content) && /(来源|Sources?)\s*[::]/i.test(content);
|
package/src/harness/state.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
-
import type { ActiveRun, KdPhase, KdQuestion } from "./types.ts";
|
|
2
|
+
import type { ActiveRun, KdPhase, KdQuestion, KdRisk, KdRiskSource } from "./types.ts";
|
|
3
3
|
import { PHASE_ARTIFACTS, isKdPhase, nextPhase } from "./types.ts";
|
|
4
4
|
import { activeRunPath, kdDir, runStatePath, runsDir } from "./paths.ts";
|
|
5
5
|
import { defaultArtifactContent, ensureArtifact, ensureRunDirectories, writeArtifact } from "./artifacts.ts";
|
|
@@ -79,7 +79,6 @@ export function createActiveRun(cwd: string, goal: string, productInput?: string
|
|
|
79
79
|
product: profile.product,
|
|
80
80
|
version,
|
|
81
81
|
profile,
|
|
82
|
-
risk: "unknown",
|
|
83
82
|
artifacts: {},
|
|
84
83
|
questions: [],
|
|
85
84
|
gate: {
|
|
@@ -143,6 +142,18 @@ export function updateProductProfile(cwd: string, run: ActiveRun, productInput:
|
|
|
143
142
|
return run;
|
|
144
143
|
}
|
|
145
144
|
|
|
145
|
+
export function updateRisk(cwd: string, run: ActiveRun, risk: KdRisk, reason: string, source: KdRiskSource = "manual"): ActiveRun {
|
|
146
|
+
run.riskAssessment = {
|
|
147
|
+
level: risk,
|
|
148
|
+
reason: reason.trim(),
|
|
149
|
+
source,
|
|
150
|
+
updatedAt: new Date().toISOString(),
|
|
151
|
+
};
|
|
152
|
+
run.gate = inspectGate(cwd, run);
|
|
153
|
+
writeActiveRun(cwd, run);
|
|
154
|
+
return run;
|
|
155
|
+
}
|
|
156
|
+
|
|
146
157
|
export function ensurePhaseArtifact(cwd: string, run: ActiveRun, phase: KdPhase): string {
|
|
147
158
|
const path = ensureArtifact(cwd, run, phase, defaultArtifactContent(phase));
|
|
148
159
|
run.artifacts[phase] = PHASE_ARTIFACTS[phase];
|
|
@@ -3,6 +3,7 @@ import { isAbsolute, join, relative } from "node:path";
|
|
|
3
3
|
import type { ActiveRun } from "./types.ts";
|
|
4
4
|
import { runRoot } from "./paths.ts";
|
|
5
5
|
import { isSourceLikePath } from "./path-policy.ts";
|
|
6
|
+
import { hasEvidenceEntry } from "./evidence.ts";
|
|
6
7
|
|
|
7
8
|
export const TDD_RED_EVIDENCE = "evidence/tdd-red.md";
|
|
8
9
|
export const TDD_GREEN_EVIDENCE = "evidence/tdd-green.md";
|
|
@@ -45,6 +46,7 @@ function validateTddEvidence(cwd: string, run: ActiveRun, kind: "red" | "green")
|
|
|
45
46
|
const evidenceName = kind === "red" ? TDD_RED_EVIDENCE : TDD_GREEN_EVIDENCE;
|
|
46
47
|
const evidencePath = join(runRoot(cwd, run), evidenceName);
|
|
47
48
|
if (!existsSync(evidencePath)) return { valid: false, reason: `缺少 ${evidenceName}` };
|
|
49
|
+
if (!hasEvidenceEntry(cwd, run, evidenceName)) return { valid: false, reason: `${evidenceName} 未登记到 evidence/index.json` };
|
|
48
50
|
|
|
49
51
|
const content = readFileSync(evidencePath, "utf8");
|
|
50
52
|
const uncertainty = uncertaintyReason(content);
|
package/src/harness/types.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import type { KdProduct, ProductProfile } from "../product/profile.ts";
|
|
2
2
|
|
|
3
3
|
export type KdPhase = "discuss" | "spec" | "plan" | "execute" | "verify" | "ship";
|
|
4
|
-
export type KdRisk = "
|
|
4
|
+
export type KdRisk = "low" | "medium" | "high";
|
|
5
5
|
export type KdRunStatus = "active" | "paused" | "done";
|
|
6
|
+
export type KdRiskSource = "manual" | "verify" | "ship";
|
|
7
|
+
|
|
8
|
+
export interface RiskAssessment {
|
|
9
|
+
level: KdRisk;
|
|
10
|
+
reason: string;
|
|
11
|
+
source: KdRiskSource;
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
}
|
|
6
14
|
|
|
7
15
|
export interface GateResult {
|
|
8
16
|
passed: boolean;
|
|
@@ -34,7 +42,7 @@ export interface ActiveRun {
|
|
|
34
42
|
product?: KdProduct;
|
|
35
43
|
version?: string;
|
|
36
44
|
profile?: ProductProfile;
|
|
37
|
-
|
|
45
|
+
riskAssessment?: RiskAssessment;
|
|
38
46
|
artifacts: Record<string, string>;
|
|
39
47
|
gate: GateResult;
|
|
40
48
|
questions?: KdQuestion[];
|
|
@@ -5,7 +5,7 @@ import { promisify } from "node:util";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import type { ProductProfile } from "../product/profile.ts";
|
|
7
7
|
import { readActiveRun } from "../harness/state.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { writeEvidenceFile } from "../harness/evidence.ts";
|
|
9
9
|
import { searchKnowledge } from "../knowledge/search.ts";
|
|
10
10
|
import { formatSearchResults } from "../knowledge/format.ts";
|
|
11
11
|
import { resolveWorkspacePath } from "../platform/path.ts";
|
|
@@ -174,15 +174,15 @@ export function writeOfficialEvidence(cwd: string, evidenceFile: OfficialEvidenc
|
|
|
174
174
|
const run = readActiveRun(cwd);
|
|
175
175
|
if (!run) return undefined;
|
|
176
176
|
|
|
177
|
-
const path = runArtifactPath(cwd, run, join("evidence", evidenceFile));
|
|
178
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
179
|
-
|
|
180
177
|
const content =
|
|
181
178
|
evidenceFile === "cosmic-metadata.json"
|
|
182
179
|
? formatJsonEvidence(evidenceFile, result)
|
|
183
180
|
: `${formatCommandResult(result)}\nCaptured: ${new Date().toISOString()}\n`;
|
|
184
|
-
|
|
185
|
-
|
|
181
|
+
return writeEvidenceFile(cwd, run, join("evidence", evidenceFile), content, {
|
|
182
|
+
kind: evidenceFile.replace(/\.(txt|json)$/i, ""),
|
|
183
|
+
command: result.command,
|
|
184
|
+
exitCode: result.exitCode,
|
|
185
|
+
});
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
function formatJsonEvidence(evidenceFile: OfficialEvidenceFile, result: CommandResult): string {
|