kcode-pi 0.1.27 → 0.1.31

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,52 @@
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
+
9
55
  ## 0.1.27 - 2026-06-07
10
56
 
11
57
  ### 修复
@@ -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
+ ```
@@ -24,7 +24,9 @@ import { flagshipWriteBlockReason, isSourceLikePath, planWriteBlockReason } from
24
24
  import { sdkSignatureProductionWriteBlockReason } from "../src/harness/sdk-policy.ts";
25
25
  import { tddProductionWriteBlockReason } from "../src/harness/tdd-policy.ts";
26
26
  import { windowsPathHint } from "../src/platform/path.ts";
27
- 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";
28
30
 
29
31
  function requireRun(cwd: string): ReturnType<typeof readActiveRun> {
30
32
  return readActiveRun(cwd);
@@ -275,9 +277,47 @@ const kdQuestionTool = defineTool({
275
277
  },
276
278
  });
277
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
+
278
317
  export default function (pi: ExtensionAPI) {
279
318
  pi.registerTool(kdPlanStatusTool);
280
319
  pi.registerTool(kdQuestionTool);
320
+ pi.registerTool(createKdVerifyResultTool(pi));
281
321
 
282
322
  pi.on("session_start", async (_event, ctx) => {
283
323
  const run = readActiveRun(ctx.cwd);
@@ -294,6 +334,7 @@ export default function (pi: ExtensionAPI) {
294
334
 
295
335
  pi.on("input", async (event, ctx) => {
296
336
  if (event.source === "extension") return { action: "continue" };
337
+ if (isSubagentChild()) return { action: "continue" };
297
338
 
298
339
  let run = readActiveRun(ctx.cwd);
299
340
  if (!run && shouldStartHarnessFromInput(event.text)) {
@@ -324,6 +365,29 @@ export default function (pi: ExtensionAPI) {
324
365
  return { block: true, reason };
325
366
  }
326
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
+
327
391
  if (event.toolName !== "write" && event.toolName !== "edit") return undefined;
328
392
 
329
393
  const reason = codeWriteBlockReason(ctx.cwd, path) ?? flagshipWriteBlockReason(readActiveRun(ctx.cwd), path, ctx.cwd);
@@ -551,6 +615,37 @@ export default function (pi: ExtensionAPI) {
551
615
  autoAdvanceCommand(pi, ctx, readActiveRun(ctx.cwd) ?? run, `${answered.id} 已回答。`);
552
616
  },
553
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
+ }
554
649
  }
555
650
 
556
651
  function formatQuestions(run: NonNullable<ReturnType<typeof readActiveRun>>): string {
@@ -4,6 +4,96 @@ import { readActiveRun } from "../src/harness/state.ts";
4
4
  import type { ActiveRun, GateResult } from "../src/harness/types.ts";
5
5
  import { formatProductProfile } from "../src/product/profile.ts";
6
6
 
7
+ /** ANSI escape sequence pattern: CSI, OSC, APC. */
8
+ const ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]|\x1b\][^\x07]*\x07|\x1b_[^\x1b]*\x1b\\/g;
9
+
10
+ /** Visible width of a string (strips ANSI codes; CJK/wide chars = 2 columns). */
11
+ function visibleWidth(str: string): number {
12
+ let width = 0;
13
+ const clean = str.replace(ANSI_RE, "");
14
+ for (const ch of clean) {
15
+ const code = ch.codePointAt(0)!;
16
+ width +=
17
+ code >= 0x1100 &&
18
+ !(code >= 0x00a0 && code <= 0x00ff) &&
19
+ ((code >= 0x1100 && code <= 0x115f) ||
20
+ (code >= 0x2329 && code <= 0x232a) ||
21
+ (code >= 0x2e80 && code <= 0x303e) ||
22
+ (code >= 0x3040 && code <= 0x3247) ||
23
+ (code >= 0x3250 && code <= 0x4dbf) ||
24
+ (code >= 0x4e00 && code <= 0xa4c6) ||
25
+ (code >= 0xa960 && code <= 0xa97c) ||
26
+ (code >= 0xac00 && code <= 0xd7a3) ||
27
+ (code >= 0xf900 && code <= 0xfaff) ||
28
+ (code >= 0xfe10 && code <= 0xfe19) ||
29
+ (code >= 0xfe30 && code <= 0xfe6b) ||
30
+ (code >= 0xff01 && code <= 0xff60) ||
31
+ (code >= 0xffe0 && code <= 0xffe6) ||
32
+ (code >= 0x1f300 && code <= 0x1f9ff) ||
33
+ (code >= 0x20000 && code <= 0x2fffd))
34
+ ? 2
35
+ : 1;
36
+ }
37
+ return width;
38
+ }
39
+
40
+ /**
41
+ * If the line's visible width exceeds `maxWidth`, truncate visible characters
42
+ * and append `>` so the result fits. Preserves ANSI codes in the kept portion
43
+ * and appends SGR reset before the `>`. No padding — pi-tui only requires
44
+ * visibleWidth <= width.
45
+ */
46
+ function clipLine(text: string, maxWidth: number): string {
47
+ if (maxWidth <= 0) return "";
48
+ const vw = visibleWidth(text);
49
+ if (vw <= maxWidth) return text;
50
+
51
+ const targetW = maxWidth - 1; // reserve 1 col for ">"
52
+ let result = "";
53
+ let visibleSoFar = 0;
54
+ let i = 0;
55
+
56
+ while (i < text.length && visibleSoFar < targetW) {
57
+ // Preserve ANSI escape sequences
58
+ if (text[i] === "\x1b") {
59
+ const m = text.slice(i).match(/^(?:\[[0-9;]*[A-Za-z]|\][^\x07]*\x07|_[^\x1b]*\x1b\\)/);
60
+ if (m) {
61
+ result += m[0];
62
+ i += m[0].length;
63
+ continue;
64
+ }
65
+ }
66
+
67
+ const ch = text[i];
68
+ const code = ch.codePointAt(0)!;
69
+ const wide =
70
+ (code >= 0x1100 && code <= 0x115f) ||
71
+ (code >= 0x2329 && code <= 0x232a) ||
72
+ (code >= 0x2e80 && code <= 0x303e) ||
73
+ (code >= 0x3040 && code <= 0x3247) ||
74
+ (code >= 0x3250 && code <= 0x4dbf) ||
75
+ (code >= 0x4e00 && code <= 0xa4c6) ||
76
+ (code >= 0xa960 && code <= 0xa97c) ||
77
+ (code >= 0xac00 && code <= 0xd7a3) ||
78
+ (code >= 0xf900 && code <= 0xfaff) ||
79
+ (code >= 0xfe10 && code <= 0xfe19) ||
80
+ (code >= 0xfe30 && code <= 0xfe6b) ||
81
+ (code >= 0xff01 && code <= 0xff60) ||
82
+ (code >= 0xffe0 && code <= 0xffe6) ||
83
+ (code >= 0x1f300 && code <= 0x1f9ff) ||
84
+ (code >= 0x20000 && code <= 0x2fffd);
85
+ const charW = wide ? 2 : 1;
86
+
87
+ if (visibleSoFar + charW > targetW) break;
88
+
89
+ result += ch;
90
+ visibleSoFar += charW;
91
+ i++;
92
+ }
93
+
94
+ return result + "\x1b[0m>";
95
+ }
96
+
7
97
  function formatProduct(run: ActiveRun | undefined): string {
8
98
  if (!run) return "未选择";
9
99
  if (run.profile?.product === "unknown") return "未确认";
@@ -43,12 +133,6 @@ function riskColor(risk: string): "error" | "warning" | "muted" | "success" {
43
133
  return "success";
44
134
  }
45
135
 
46
- function padOrTrim(text: string, width: number): string {
47
- if (width <= 0) return "";
48
- if (text.length > width) return text.slice(0, Math.max(0, width - 1)) + ">";
49
- return text + " ".repeat(width - text.length);
50
- }
51
-
52
136
  function logoLines(theme: Theme): string[] {
53
137
  const accent = (text: string) => theme.fg("accent", text);
54
138
  const muted = (text: string) => theme.fg("muted", text);
@@ -87,9 +171,9 @@ export default function (pi: ExtensionAPI) {
87
171
 
88
172
  return [
89
173
  "",
90
- ...logoLines(theme).map((line) => padOrTrim(line, width)),
91
- padOrTrim(status, width),
92
- padOrTrim(theme.fg("dim", `run:${runId}`), width),
174
+ ...logoLines(theme).map((line) => clipLine(line, width)),
175
+ clipLine(status, width),
176
+ clipLine(theme.fg("dim", `run:${runId}`), width),
93
177
  "",
94
178
  ];
95
179
  },