kcode-pi 0.1.24 → 0.1.30

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 CHANGED
@@ -108,6 +108,8 @@ enterprise 金蝶企业版 / C#
108
108
  /kd-advance [阶段]
109
109
  /kd-artifact [阶段] [内容] [--replace]
110
110
  /kd-answer Q-001 <答案>
111
+ /kd-review [审查重点]
112
+ /kd-delegate <research|doc|code|review|verify> <任务> [--dry-run]
111
113
  ```
112
114
 
113
115
  完整说明见 [命令参考](docs/COMMAND_REFERENCE.md)。
@@ -129,6 +131,7 @@ kd_sdk_signature 从当前项目实际 SDK jar/dll 中读取类和方法签
129
131
  kd_ksql_lint 运行 KSQL/SQL lint
130
132
  kd_build 按产品画像执行或 dry-run 构建
131
133
  kd_debug 分析金蝶日志和堆栈
134
+ kd_subagent 将调研、文档、代码、验证或交叉审查委派给隔离子 agent
132
135
  ```
133
136
 
134
137
  工具细节和使用顺序见 [Harness 工作流](docs/HARNESS_WORKFLOW.md)。
package/docs/CHANGELOG.md CHANGED
@@ -6,6 +6,88 @@
6
6
 
7
7
  - 暂无。
8
8
 
9
+ ## 0.1.30 - 2026-06-07
10
+
11
+ ### 新增
12
+
13
+ - 新增 `kd_subagent` 工具,用隔离 Pi 子进程执行调研、文档、代码、验证和交叉审查任务。
14
+ - 新增 `/kd-review` 命令,用只读子 agent 执行交叉自查。
15
+ - 新增 `/kd-delegate` 命令,支持按 `research/doc/code/review/verify` 角色委派任务,并可用 `--dry-run` 预览上下文包。
16
+ - `kd_subagent` 支持单任务、只读角色并行 `tasks` 和链式 `chain` 三种模式。
17
+
18
+ ### 改进
19
+
20
+ - 子 agent 上下文由 `src/harness/delegation.ts` 集中生成,明确写入边界、主状态机边界和禁止递归委派。
21
+ - 工作流 prompt 增加自动委派策略:复杂调研、交叉审查和可并行拆分任务可主动调用 `kd_subagent`,但不自动推进阶段。
22
+ - 子 agent 进程使用角色环境标记和工具白名单;child 环境不注册 `kd_subagent`,避免递归委派。
23
+ - `research/review/verify` 为只读角色;`doc` 只能写 README、docs/ 和当前 run 阶段文档;`code` 只能在 `execute` 阶段写产品源码并继续受 PLAN/TDD/SDK 门禁约束。
24
+
25
+ ### 验证
26
+
27
+ - `npm run smoke:harness` 覆盖委派参数解析、上下文包关键约束和写入边界预览。
28
+
29
+ ## 0.1.29 - 2026-06-07
30
+
31
+ ### 修复
32
+
33
+ - 修复自动修复循环达到上限后,即使后续验证通过也会被遗留阻断问题继续卡住的问题。
34
+ - 强化 `kd_verify_result` 输入容错,避免坏 payload 触发类似 `undefined.replace/trim` 的崩溃。
35
+ - 限制验证结果只能在 `verify` 阶段或自动修复中的 `execute` 阶段记录,避免非验证阶段污染 `VERIFY.md` 和 evidence。
36
+ - 收敛 `/kd-verify` 与 `kd-verify` skill 的验证结果入口,要求通过 `kd_verify_result` 进入同一修复闭环。
37
+
38
+ ### 验证
39
+
40
+ - `npm run smoke:harness` 覆盖 repair 阻断问题关闭、坏验证 payload 容错和非法阶段拒绝。
41
+
42
+ ## 0.1.28 - 2026-06-07
43
+
44
+ ### 新增
45
+
46
+ - 新增 `kd_verify_result` 工具和 `/kd-verify-result` 命令,用于记录验证命令结果。
47
+ - 验证失败时自动写入 `evidence/verify-failure-###.md`,切回 `execute` 阶段并注入修复上下文。
48
+ - 验证通过时写入 `evidence/verify-pass.md`,重置修复状态并继续尝试推进。
49
+ - 自动修复循环默认最多 3 轮;达到上限后创建阻断问题,避免无限修复。
50
+
51
+ ### 验证
52
+
53
+ - `npm run smoke:harness` 通过,覆盖验证失败回到 execute、三轮失败阻塞、验证通过重置修复状态。
54
+
55
+ ## 0.1.27 - 2026-06-07
56
+
57
+ ### 修复
58
+
59
+ - 强化 active run / RUN.json 读取容错,过滤异常 `questions`、`artifacts`、`riskAssessment`、`gate` 字段,避免旧状态或坏状态触发门禁崩溃。
60
+ - Header 渲染门禁时增加兜底,门禁异常会显示为阻塞状态,不再导致 TUI 进程退出。
61
+ - 阶段产物存在但不可读或误建为目录时,现在按缺失处理,不再在读取 `CONTEXT.md`、`PLAN.md`、`EXECUTION.md` 等文件时抛出异常。
62
+
63
+ ### 验证
64
+
65
+ - `npm run smoke:harness` 通过,覆盖损坏 run state 和损坏 evidence index 的容错。
66
+
67
+ ## 0.1.26 - 2026-06-07
68
+
69
+ ### 修复
70
+
71
+ - 修复已有项目的 `evidence/index.json` 中存在异常 entry 时,Header 门禁渲染可能因读取 `path` 崩溃的问题。
72
+ - `readEvidenceIndex` 现在会过滤无效 evidence entry,`hasEvidenceEntry` 和 evidence 记录逻辑对坏数据保持容错。
73
+
74
+ ### 验证
75
+
76
+ - `npm run smoke:harness` 通过,覆盖损坏 evidence index 不再触发崩溃。
77
+
78
+ ## 0.1.25 - 2026-06-07
79
+
80
+ ### 修复
81
+
82
+ - 修复 `/kd-risk`、`/kd-product`、`/kd-answer`、`/kd-artifact` 等命令在解除门禁后不会自动尝试推进下一阶段的问题。
83
+ - 修复 `kd_question` 回答阻断问题后只刷新门禁、不尝试推进的问题。
84
+ - 修复 `kd_cosmic_config`、`kd_cosmic_metadata`、`kd_cosmic_api`、`kd_sdk_signature`、`kd_ksql_lint` 写入证据后不反馈自动推进结果的问题。
85
+ - 修复官方 evidence 只要文件存在就可能满足门禁的问题;现在要求 evidence index 中记录的退出码为 `0`。
86
+
87
+ ### 验证
88
+
89
+ - `npm run smoke:harness` 通过,覆盖阻断问题回答后的自动推进、风险更新后的自动推进尝试,以及失败 evidence 不能通过门禁。
90
+
9
91
  ## 0.1.24 - 2026-06-07
10
92
 
11
93
  ### 新增
@@ -255,6 +255,37 @@ discuss -> spec -> plan -> execute -> verify -> ship
255
255
  /kd-finish
256
256
  ```
257
257
 
258
+ ### /kd-review
259
+
260
+ 启动只读交叉自查子 agent:
261
+
262
+ ```text
263
+ /kd-review [审查重点]
264
+ ```
265
+
266
+ 用于检查状态机漏洞、门禁绕过、证据缺口、提示词分散和测试缺口。子 agent 不修改文件,主 agent 负责采纳结论和后续修复。
267
+
268
+ ### /kd-delegate
269
+
270
+ 把局部任务委派给隔离子 agent:
271
+
272
+ ```text
273
+ /kd-delegate <research|doc|code|review|verify> <任务> [--dry-run]
274
+ ```
275
+
276
+ 角色:
277
+
278
+ ```text
279
+ research 只读调研,输出压缩结论和证据位置
280
+ doc 写指定文档或阶段产物
281
+ code 只在 execute 阶段修改 PLAN.md 批准文件
282
+ review 只读交叉自查,输出 findings 和是否阻止发布
283
+ verify 只读分析验证命令和失败证据,实际验证由主 agent 执行并用 kd_verify_result 记录
284
+ ```
285
+
286
+ `--dry-run` 只预览上下文包,不启动子进程。
287
+ 复杂任务也可能由主 agent 自动调用 `kd_subagent` 委派;自动委派不会改变 Harness 阶段。
288
+
258
289
  ## 内置工具
259
290
 
260
291
  这些工具多数情况下会由 KCode 自动使用;当需要明确证据或排障时,也可以按下面参数手动调用。
@@ -279,6 +310,30 @@ kd_question action=list
279
310
 
280
311
  一次只能登记一个当前最阻塞的问题,最多 3 个简短选项。
281
312
 
313
+ ### kd_subagent
314
+
315
+ 将局部任务委派给隔离 Pi 子进程:
316
+
317
+ ```text
318
+ kd_subagent role=review task="审查当前 run 的门禁和证据缺口"
319
+ kd_subagent role=research task="查找采购订单保存插件相关代码" dryRun=true
320
+ kd_subagent tasks=[{"role":"research","task":"查找模型层"},{"role":"review","task":"审查状态机"}]
321
+ kd_subagent chain=[{"role":"research","task":"找相关代码"},{"role":"review","task":"基于上一输出审查风险"}]
322
+ ```
323
+
324
+ 参数:
325
+
326
+ ```text
327
+ role 必填,research/doc/code/review/verify
328
+ task 必填,具体委派任务
329
+ tasks 可选,并行任务数组,只允许 research/review/verify,和 role/task/chain 三选一
330
+ chain 可选,链式任务数组,和 role/task/tasks 三选一
331
+ dryRun 可选,只预览上下文包
332
+ maxOutputChars 可选,限制返回给主 agent 的输出长度
333
+ ```
334
+
335
+ 主 Harness 仍负责阶段推进、证据和门禁;子 agent 返回结果后由主 agent 决策下一步。
336
+
282
337
  ### kd_search
283
338
 
284
339
  搜索随包金蝶知识库:
@@ -145,3 +145,36 @@ KCode 会阻止过早写入 Java/XML/SQL/C# 等产品代码:
145
145
  - 必须先理解当前业务项目已有目录、模块、包名、基类和本地封装。
146
146
 
147
147
  证据和门禁细节见 [证据和门禁](EVIDENCE_AND_GATES.md)。
148
+
149
+ ## 子 agent 委派
150
+
151
+ KCode 支持把局部任务委派给隔离子 agent,用来降低长上下文带来的注意力漂移。主 Harness 仍是唯一状态机,负责阶段推进、门禁、证据和风险记录。
152
+
153
+ 触发方式有两种:
154
+
155
+ - 自动:主 agent 在大量调研、独立交叉审查、长上下文复盘或可并行拆分时,可以主动调用 `kd_subagent`。
156
+ - 显式:用户用 `/kd-review` 或 `/kd-delegate` 指定委派任务。
157
+
158
+ 常用入口:
159
+
160
+ ```text
161
+ /kd-review 审查当前实现是否有门禁绕过和测试缺口
162
+ /kd-delegate research 调研采购订单保存插件相关代码
163
+ /kd-delegate doc 更新当前阶段文档 --dry-run
164
+ ```
165
+
166
+ 角色边界:
167
+
168
+ - `research`、`review` 默认只读。
169
+ - `doc` 只写明确指定的文档或阶段产物。
170
+ - `code` 只能在 `execute` 阶段运行,并且只能修改 `PLAN.md` 批准文件。
171
+ - `verify` 只读分析验证命令、失败证据和风险;实际命令和结果记录仍由主 agent 执行。
172
+
173
+ `--dry-run` 会预览发送给子 agent 的上下文包,用来检查上下文是否过长、是否包含不该交给子 agent 的信息。
174
+
175
+ 工具层支持并行和链式委派。并行只允许 `research`、`review`、`verify` 这类只读角色;`doc` 和 `code` 必须串行执行:
176
+
177
+ ```text
178
+ kd_subagent tasks=[{"role":"research","task":"查找模型层"},{"role":"review","task":"审查门禁"}]
179
+ kd_subagent chain=[{"role":"research","task":"找相关代码"},{"role":"review","task":"基于上一输出审查风险"}]
180
+ ```
@@ -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,
@@ -23,7 +24,9 @@ import { flagshipWriteBlockReason, isSourceLikePath, planWriteBlockReason } from
23
24
  import { sdkSignatureProductionWriteBlockReason } from "../src/harness/sdk-policy.ts";
24
25
  import { tddProductionWriteBlockReason } from "../src/harness/tdd-policy.ts";
25
26
  import { windowsPathHint } from "../src/platform/path.ts";
26
- import { workflowPromptForRun } from "../src/harness/prompt.ts";
27
+ import { repairPromptForRun, workflowPromptForRun } from "../src/harness/prompt.ts";
28
+ import { recordVerifyResult, type VerifyResultOutcome } from "../src/harness/repair.ts";
29
+ import { isSubagentChild, subagentRoleFromEnv, subagentToolCallBlockReason } from "../src/harness/delegation.ts";
27
30
 
28
31
  function requireRun(cwd: string): ReturnType<typeof readActiveRun> {
29
32
  return readActiveRun(cwd);
@@ -109,6 +112,26 @@ function sendWorkflowPrompt(pi: ExtensionAPI, ctx: ExtensionContext, run: NonNul
109
112
  if (ctx.hasUI) ctx.ui.notify("KCode 工作流消息已排队。", "info");
110
113
  }
111
114
 
115
+ function autoAdvanceCommand(
116
+ pi: ExtensionAPI,
117
+ ctx: ExtensionContext,
118
+ run: NonNullable<ReturnType<typeof readActiveRun>>,
119
+ reason: string,
120
+ ): ReturnType<typeof advanceRunIfReady> {
121
+ const result = advanceRunIfReady(ctx.cwd, run);
122
+ if (result.advanced) {
123
+ if (ctx.hasUI) ctx.ui.notify(result.message, "info");
124
+ sendWorkflowPrompt(pi, ctx, result.run, `${reason}\n继续 KCode Harness run ${result.run.id}:${result.run.goal ?? "未知需求"}`);
125
+ } else if (!result.message.includes("最终阶段")) {
126
+ if (ctx.hasUI) ctx.ui.notify(`门禁仍阻塞:${result.message}`, "warning");
127
+ }
128
+ return result;
129
+ }
130
+
131
+ function autoAdvanceTool(cwd: string, run: NonNullable<ReturnType<typeof readActiveRun>>): ReturnType<typeof advanceRunIfReady> {
132
+ return advanceRunIfReady(cwd, run);
133
+ }
134
+
112
135
  function codeWriteBlockReason(cwd: string, path: string | undefined): string | undefined {
113
136
  if (!path || !isSourceLikePath(path)) return undefined;
114
137
 
@@ -188,9 +211,10 @@ const kdQuestionTool = defineTool({
188
211
  };
189
212
  }
190
213
  appendQuestionEventToArtifact(ctx.cwd, run, [`- 已回答 ${answered.id}:${answered.answer}`]);
214
+ const auto = autoAdvanceTool(ctx.cwd, run);
191
215
  return {
192
- content: [{ type: "text", text: `已记录 ${answered.id} 的答案,并刷新门禁。` }],
193
- details: { question: answered, gate: readActiveRun(ctx.cwd)?.gate },
216
+ content: [{ type: "text", text: `已记录 ${answered.id} 的答案。${auto.advanced ? auto.message : `门禁仍阻塞:${auto.message}`}` }],
217
+ details: { question: answered, gate: readActiveRun(ctx.cwd)?.gate, autoAdvance: auto },
194
218
  };
195
219
  }
196
220
 
@@ -240,9 +264,10 @@ const kdQuestionTool = defineTool({
240
264
  const answered = answerQuestion(ctx.cwd, run, question.id, interactiveAnswer);
241
265
  if (answered) {
242
266
  appendQuestionEventToArtifact(ctx.cwd, run, [`- 已回答 ${answered.id}:${answered.answer}`]);
267
+ const auto = autoAdvanceTool(ctx.cwd, run);
243
268
  return {
244
- content: [{ type: "text", text: `用户已回答 ${answered.id}:${answered.answer}` }],
245
- details: { question: answered, gate: readActiveRun(ctx.cwd)?.gate, answered: true },
269
+ content: [{ type: "text", text: `用户已回答 ${answered.id}:${answered.answer}。${auto.advanced ? auto.message : `门禁仍阻塞:${auto.message}`}` }],
270
+ details: { question: answered, gate: readActiveRun(ctx.cwd)?.gate, answered: true, autoAdvance: auto },
246
271
  };
247
272
  }
248
273
  }
@@ -252,9 +277,47 @@ const kdQuestionTool = defineTool({
252
277
  },
253
278
  });
254
279
 
280
+ function createKdVerifyResultTool(pi: ExtensionAPI) {
281
+ return defineTool({
282
+ name: "kd_verify_result",
283
+ label: "KD 验证结果",
284
+ description: "记录当前 verify 命令结果。失败时自动写失败证据并回到 execute 修复;成功时记录通过证据并尝试推进。",
285
+ parameters: Type.Object({
286
+ command: Type.String({ description: "实际执行的验证命令。" }),
287
+ exitCode: Type.Number({ description: "验证命令退出码。" }),
288
+ stdout: Type.Optional(Type.String({ description: "验证命令 STDOUT 摘要或完整输出。" })),
289
+ stderr: Type.Optional(Type.String({ description: "验证命令 STDERR 摘要或完整输出。" })),
290
+ summary: Type.Optional(Type.String({ description: "失败原因或通过结论摘要。" })),
291
+ }),
292
+
293
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
294
+ const run = readActiveRun(ctx.cwd);
295
+ if (!run) {
296
+ return {
297
+ content: [{ type: "text", text: "当前没有 active Kingdee Harness run。请先使用 /kd-start <需求> 创建。" }],
298
+ details: { error: "no-active-run" },
299
+ };
300
+ }
301
+ const outcome = recordVerifyResult(ctx.cwd, run, {
302
+ command: params.command,
303
+ exitCode: params.exitCode,
304
+ stdout: params.stdout,
305
+ stderr: params.stderr,
306
+ summary: params.summary,
307
+ });
308
+ handleVerifyOutcome(pi, ctx, outcome);
309
+ return {
310
+ content: [{ type: "text", text: outcome.message }],
311
+ details: { outcome },
312
+ };
313
+ },
314
+ });
315
+ }
316
+
255
317
  export default function (pi: ExtensionAPI) {
256
318
  pi.registerTool(kdPlanStatusTool);
257
319
  pi.registerTool(kdQuestionTool);
320
+ pi.registerTool(createKdVerifyResultTool(pi));
258
321
 
259
322
  pi.on("session_start", async (_event, ctx) => {
260
323
  const run = readActiveRun(ctx.cwd);
@@ -271,6 +334,7 @@ export default function (pi: ExtensionAPI) {
271
334
 
272
335
  pi.on("input", async (event, ctx) => {
273
336
  if (event.source === "extension") return { action: "continue" };
337
+ if (isSubagentChild()) return { action: "continue" };
274
338
 
275
339
  let run = readActiveRun(ctx.cwd);
276
340
  if (!run && shouldStartHarnessFromInput(event.text)) {
@@ -301,6 +365,29 @@ export default function (pi: ExtensionAPI) {
301
365
  return { block: true, reason };
302
366
  }
303
367
 
368
+ const subagentRole = isSubagentChild() ? subagentRoleFromEnv() : undefined;
369
+ if (subagentRole) {
370
+ const run = readActiveRun(ctx.cwd);
371
+ const sourceWriteBlock =
372
+ sdkSignatureProductionWriteBlockReason(ctx.cwd, run, path) ??
373
+ tddProductionWriteBlockReason(ctx.cwd, run, path) ??
374
+ planWriteBlockReason(ctx.cwd, run, path, run ? (readArtifact(ctx.cwd, run, "plan") ?? "") : "") ??
375
+ flagshipWriteBlockReason(run, path, ctx.cwd);
376
+ const reason = subagentToolCallBlockReason({
377
+ role: subagentRole,
378
+ toolName: event.toolName,
379
+ path,
380
+ cwd: ctx.cwd,
381
+ run,
382
+ sourceLike: path ? isSourceLikePath(path) : false,
383
+ sourceWriteBlockReason: sourceWriteBlock,
384
+ });
385
+ if (reason) {
386
+ if (ctx.hasUI) ctx.ui.notify(reason, "warning");
387
+ return { block: true, reason };
388
+ }
389
+ }
390
+
304
391
  if (event.toolName !== "write" && event.toolName !== "edit") return undefined;
305
392
 
306
393
  const reason = codeWriteBlockReason(ctx.cwd, path) ?? flagshipWriteBlockReason(readActiveRun(ctx.cwd), path, ctx.cwd);
@@ -427,6 +514,7 @@ export default function (pi: ExtensionAPI) {
427
514
 
428
515
  const updated = updateProductProfile(ctx.cwd, run, parsed.product, parsed.version);
429
516
  ctx.ui.notify(`产品画像:${updated.profile?.product}/${updated.profile?.techStack}/${updated.profile?.language}`, "info");
517
+ autoAdvanceCommand(pi, ctx, updated, "产品画像已更新。");
430
518
  },
431
519
  });
432
520
 
@@ -447,6 +535,7 @@ export default function (pi: ExtensionAPI) {
447
535
 
448
536
  const updated = updateRisk(ctx.cwd, run, risk.risk, risk.reason);
449
537
  ctx.ui.notify(`风险等级:${updated.riskAssessment?.level}(${updated.riskAssessment?.reason})`, "info");
538
+ autoAdvanceCommand(pi, ctx, updated, "风险评估已更新。");
450
539
  },
451
540
  });
452
541
 
@@ -498,6 +587,7 @@ export default function (pi: ExtensionAPI) {
498
587
  ? updatePhaseArtifact(ctx.cwd, run, parsed.phase, parsed.content)
499
588
  : ensurePhaseArtifact(ctx.cwd, run, parsed.phase);
500
589
  ctx.ui.notify(`阶段文档已就绪:${path}`, "info");
590
+ autoAdvanceCommand(pi, ctx, readActiveRun(ctx.cwd) ?? run, `${parsed.phase} 阶段文档已更新。`);
501
591
  },
502
592
  });
503
593
 
@@ -522,8 +612,40 @@ export default function (pi: ExtensionAPI) {
522
612
  }
523
613
  appendQuestionEventToArtifact(ctx.cwd, run, [`- 已回答 ${answered.id}:${answered.answer}`]);
524
614
  ctx.ui.notify(`已记录 ${answered.id} 的答案`, "info");
615
+ autoAdvanceCommand(pi, ctx, readActiveRun(ctx.cwd) ?? run, `${answered.id} 已回答。`);
525
616
  },
526
617
  });
618
+
619
+ pi.registerCommand("kd-verify-result", {
620
+ description: "记录验证命令结果:/kd-verify-result <exitCode> <command>",
621
+ handler: async (args, ctx) => {
622
+ const run = requireRun(ctx.cwd);
623
+ if (!run) {
624
+ ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
625
+ return;
626
+ }
627
+ const [exitCodeText, ...commandParts] = args.trim().split(/\s+/);
628
+ const exitCode = Number(exitCodeText);
629
+ const command = commandParts.join(" ").trim();
630
+ if (!Number.isFinite(exitCode) || !command) {
631
+ ctx.ui.notify("用法:/kd-verify-result <exitCode> <command>", "error");
632
+ return;
633
+ }
634
+ const outcome = recordVerifyResult(ctx.cwd, run, { command, exitCode });
635
+ ctx.ui.notify(outcome.message, outcome.status === "passed" ? "info" : "warning");
636
+ handleVerifyOutcome(pi, ctx, outcome);
637
+ },
638
+ });
639
+ }
640
+
641
+ function handleVerifyOutcome(pi: ExtensionAPI, ctx: ExtensionContext, outcome: VerifyResultOutcome): void {
642
+ if (outcome.status === "passed") {
643
+ autoAdvanceCommand(pi, ctx, outcome.run, "验证结果已通过。");
644
+ return;
645
+ }
646
+ if (outcome.status === "repairing") {
647
+ sendWorkflowPrompt(pi, ctx, outcome.run, repairPromptForRun(outcome.run));
648
+ }
527
649
  }
528
650
 
529
651
  function formatQuestions(run: NonNullable<ReturnType<typeof readActiveRun>>): string {
@@ -19,6 +19,19 @@ function formatGate(gate: GateResult | undefined): string {
19
19
  return gate.passed ? "门禁:通过" : "门禁:阻塞";
20
20
  }
21
21
 
22
+ function safeInspectGate(cwd: string, run: ActiveRun | undefined): GateResult | undefined {
23
+ if (!run) return undefined;
24
+ try {
25
+ return inspectGate(cwd, run);
26
+ } catch (error) {
27
+ return {
28
+ passed: false,
29
+ reason: error instanceof Error ? error.message : String(error),
30
+ checkedAt: new Date().toISOString(),
31
+ };
32
+ }
33
+ }
34
+
22
35
  function riskLevel(run: ActiveRun | undefined): string {
23
36
  return run?.riskAssessment?.level ?? "未知";
24
37
  }
@@ -54,7 +67,7 @@ export default function (pi: ExtensionAPI) {
54
67
  return {
55
68
  render(width: number): string[] {
56
69
  const run = readActiveRun(ctx.cwd);
57
- const gateState = run ? inspectGate(ctx.cwd, run) : undefined;
70
+ const gateState = safeInspectGate(ctx.cwd, run);
58
71
  const phase = formatPhase(run?.phase);
59
72
  const product = formatProduct(run);
60
73
  const gate = formatGate(gateState);