kcode-pi 0.1.39 → 0.1.42

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.
@@ -4,8 +4,20 @@ description: 验证当前金蝶实现并收集证据。
4
4
 
5
5
  使用 `kd-verify` skill。
6
6
 
7
- 执行 `PLAN.md` 中的验证命令。验证命令完成后必须调用 `kd_verify_result` 记录 `command`、`exitCode` 和关键输出;不要手工绕过 Harness 的验证结果记录。
8
-
9
- 用户补充说明:
7
+ 执行验证阶段。输入:
10
8
 
11
9
  {{args}}
10
+
11
+ 执行协议:
12
+
13
+ - 读取当前 run、`PLAN.md`、`EXECUTION.md` 和 evidence。
14
+ - 只执行或记录 `PLAN.md` 声明的验证命令;替代命令必须说明等价依据。
15
+ - 验证命令完成后必须调用 `kd_verify_result` 记录 `command`、`exitCode`、关键 stdout/stderr 和结论。
16
+ - 外部系统、BOS 注册、人工功能测试和生产验证不能由 LLM 代办;只记录用户提供的证据来源、测试数据和结果。
17
+ - 验证失败时进入修复循环,不得进入 ship。
18
+
19
+ 输出契约:
20
+
21
+ - `kd_verify_result` 记录结果。
22
+ - 通过或失败证据路径。
23
+ - 唯一下一动作。
@@ -46,7 +46,7 @@ description: 金蝶苍穹/Cosmic 平台 Java 插件开发技能,适用于苍
46
46
  3. 通过官方适配器验证产品事实。
47
47
  - 代码引用字段、表单 ID、单据名称、枚举/下拉值、实体 ID、操作编码、SQL 字段时,使用 `kd_cosmic_metadata`。
48
48
  - 类名、方法名、方法签名、继承关系、Override 签名不确定时,优先使用 `kd_sdk_signature` 从当前项目实际 SDK jar 中验证;`kd_cosmic_api` 只作为随包知识线索和兜底。
49
- - 多个表单或字段尽量合并到一次元数据查询。
49
+ - 多个表单或字段能够合并查询时,合并为一次元数据查询。
50
50
  - 元数据查询和 API 签名查询互不依赖时并行执行。
51
51
 
52
52
  4. 只有插件类型、生命周期方法、字段元数据和 API 签名都明确后,才生成或修改代码。
@@ -64,7 +64,7 @@ description: 金蝶苍穹/Cosmic 平台 Java 插件开发技能,适用于苍
64
64
 
65
65
  - 不臆造字段 key、表单 ID、操作名、枚举值、表名或 SDK 方法;本地 SDK 查不到且编译未验证时,不把知识库结果当作最终签名事实。
66
66
  - 不把 Enterprise C# 命名空间或生命周期假设用于苍穹/Cosmic 平台 Java。
67
- - 星空旗舰版不允许猜目录或写 demo/sample;如果无法判断真实代码位置,先询问或更新计划,不要直接创建新目录。
67
+ - 星空旗舰版不允许猜目录或写 demo/sample;无法判断真实代码位置时,先登记阻塞问题或更新计划,不得直接创建新目录。
68
68
  - 不在数据绑定前的初始化阶段操作 UI 控件。
69
69
  - 不在平台期望修改入参数据实体的事务钩子里另起保存。
70
70
  - 不在循环中查询或保存,除非计划中明确说明无法批量处理并接受风险。
@@ -26,7 +26,7 @@ description: 金蝶苍穹/Cosmic 平台 Java 插件和 KSQL 代码审查技能
26
26
 
27
27
  - P0:阻断问题,可能导致崩溃、数据损坏、事务失效、安全暴露、严重资源泄漏或核心功能不可用。
28
28
  - P1:高风险问题,可能影响生产性能、稳定性、扩展性或可维护性。
29
- - P2:规范和可维护性问题,建议在合适时机修复。
29
+ - P2:规范和可维护性问题,应在计划窗口修复。
30
30
 
31
31
  ## P0 重点
32
32
 
@@ -79,7 +79,7 @@ description: 金蝶苍穹/Cosmic 平台 Java 插件和 KSQL 代码审查技能
79
79
  - 文件和行号。
80
80
  - 具体问题。
81
81
  - 为什么在 Cosmic 中有风险。
82
- - 修复建议。
82
+ - 修复指令。
83
83
 
84
84
  随后说明:
85
85
 
@@ -51,7 +51,7 @@ description: 金蝶 Cosmic 体系 Java 单元测试生成和审查技能,适
51
51
  - 需要构造的 `DynamicObject` 字段和集合结构。
52
52
  - 被测代码中应由测试暴露而不是绕过的 bug。
53
53
 
54
- POJO 或简单枚举场景可以简要说明后直接实现。
54
+ POJO 或简单枚举场景允许在说明依据后直接实现。
55
55
 
56
56
  ## 测试质量规则
57
57
 
@@ -70,7 +70,7 @@ POJO 或简单枚举场景可以简要说明后直接实现。
70
70
 
71
71
  - 优先使用项目已有 `DynamicObjectMocker`、`CommonMockObject`、`BaseTest`、`BasePluginTest`。
72
72
  - `DataSet` 和查询行只 mock 实际 select 并读取的字段。
73
- - 查询别名里包含点号时,如果平台 mocker 无法安全表示,使用 mock 的 `DynamicObject`。
73
+ - 查询别名里包含点号且平台 mocker 无法安全表示时,使用 mock 的 `DynamicObject`。
74
74
  - 对 `kd.bos.db.DB`,遵循项目已有 Class defrost 后再静态 mock 的模式。
75
75
  - 对带分布式缓存行为的 helper,先搜索已有测试中的专用 mock helper,不直接 `mockStatic`。
76
76
 
@@ -79,7 +79,7 @@ POJO 或简单枚举场景可以简要说明后直接实现。
79
79
  写完测试后:
80
80
 
81
81
  - 使用 `kd_build` 或计划中的命令运行最窄可行 Gradle test 任务。
82
- - 如果本机缺少业务 jar 或本地配置导致 Gradle 不能运行,明确说明真实阻塞原因,并给出应该运行的 Gradle task。
82
+ - 本机缺少业务 jar 或本地配置导致 Gradle 不能运行时,明确说明真实阻塞原因,并给出应运行的 Gradle task。
83
83
  - 存在 harness run 时,把测试文件和验证结果写入 `.pi/kd/runs/<run-id>/EXECUTION.md`。
84
84
 
85
85
  ## 输出要求
@@ -21,5 +21,7 @@ Rules:
21
21
  - Do not invent SDK APIs, table names, or lifecycle methods.
22
22
  - If the product/version, tech stack, or target object is unknown, mark it as an open question.
23
23
  - Ask unresolved questions one at a time. When using `kd_question`, ask only the single most blocking question and use at most 3 short choices; never submit a numbered checklist of questions.
24
+ - If any open blocking question already exists, do not ask or list another question; record the current answer first.
25
+ - Treat API/SDK documentation as technical evidence only; it cannot establish FormId, fields, entities, plugin events, table names, integration mappings, concurrency policy, or acceptance data.
24
26
  - Never apply enterprise C# rules to Java products, or Java/Cosmic plugin rules to enterprise C#.
25
27
  - Keep context concise; detailed design belongs in `SPEC.md`.
@@ -17,6 +17,8 @@ Rules:
17
17
 
18
18
  - Do not broaden scope silently.
19
19
  - Do not rewrite unrelated modules.
20
+ - Do not write code when implementation contract, data source context, SDK signature evidence, TDD red evidence, or PLAN-approved paths are missing.
21
+ - Do not use API documentation or bundled knowledge to fill business identifiers; stop and ask one blocking question or collect metadata evidence.
20
22
  - For 星空旗舰版, edit only the real target path recorded in `PLAN.md` after inspecting the project. If `code/` exists, follow its actual layout; if it does not, follow the discovered source root or existing target file. Do not create demo/sample code or root-level `src/main/java` by guesswork.
21
23
  - Do not mark work complete until verification runs.
22
24
  - Do not skip planned steps. Every `STEP-###` in `PLAN.md` must be marked complete in `EXECUTION.md` with a real `evidence/...` file before entering verify.
@@ -26,3 +28,4 @@ Rules:
26
28
  - Do not add JUnit, Mockito, NUnit, xUnit, or any extra test jar/framework only to satisfy the gate. Use existing approved project test infrastructure if it already exists.
27
29
  - If the command cannot run, record the blocker instead of marking verification passed.
28
30
  - If implementation needs a plan change, update `PLAN.md` first.
31
+ - External BOS registration, third-party system operation, and manual functional tests must be recorded as user-provided evidence; do not claim the LLM completed them.
@@ -22,6 +22,7 @@ Goal:
22
22
  - Do not plan to add third-party test jars or frameworks only for red/green checks. For Kingdee plugin work, prefer `kd_sdk_signature` against current project jars/dlls, metadata checks, compile/build checks, existing project tests, or minimal external-interface tests.
23
23
  - Define validation commands and expected evidence.
24
24
  - Add rollback or containment notes for medium/high risk work.
25
+ - For enterprise Python plugins, do not invent a local build step; require BOS registration and user-provided functional test evidence in `VERIFY.md`.
25
26
 
26
27
  Gate:
27
28
 
@@ -32,3 +33,5 @@ Gate:
32
33
  - A plan without structured `STEP-001` execution steps is incomplete.
33
34
  - A plan without TDD red/green checks is incomplete.
34
35
  - A plan that relies on unverified Kingdee API names is incomplete; bundled knowledge alone is not enough when local SDK jars/dlls or compile evidence are available.
36
+ - A plan that lacks implementation contract, data source context, integration context, or acceptance data must stop at one blocking question; do not create a broad question list.
37
+ - A plan that writes demo/sample/scaffold or placeholder paths is invalid.
@@ -19,4 +19,5 @@ Rules:
19
19
  - Do not claim completion without verification evidence.
20
20
  - Keep the handoff concise and decision-oriented.
21
21
  - If verification is partial, say so clearly.
22
-
22
+ - Do not describe external system operation, BOS registration, manual functional testing, or production validation as completed unless the user supplied verifiable evidence.
23
+ - Include remaining manual validation boundaries and residual risk explicitly.
@@ -18,6 +18,9 @@ Goal:
18
18
  Rules:
19
19
 
20
20
  - Separate confirmed facts from assumptions.
21
- - Mark every API/table assumption as requiring lookup with `kd_search` or `kd_table`.
21
+ - Write only verifiable facts into the specification; unresolved business identifiers must become a single blocking question.
22
+ - Mark every API/table assumption as requiring lookup with local metadata, SDK signature, build output, or product evidence. Bundled knowledge alone is only a clue.
22
23
  - Do not edit product code in this phase.
23
- - If acceptance criteria are vague, ask targeted questions one at a time or state explicit assumptions. When using `kd_question`, ask only the single most blocking question and use at most 3 short choices; never submit a numbered checklist of questions.
24
+ - If acceptance criteria are vague, ask targeted questions one at a time. When using `kd_question`, ask only the single most blocking question and use at most 3 short choices; never submit a numbered checklist of questions.
25
+ - If any open blocking question already exists, stop and record the answer before continuing.
26
+ - Do not generate implementation templates, sample code, or pseudo-code in `SPEC.md`.
@@ -22,3 +22,5 @@ Rules:
22
22
  - If validation cannot run, state the exact blocker.
23
23
  - Do not hand-edit `VERIFY.md` as the primary result path; `kd_verify_result` owns pass/failure evidence and the repair loop.
24
24
  - Do not ship while verification evidence is missing.
25
+ - Do not convert skipped external/BOS/manual validation into a pass. Record user-provided evidence source, test data, environment, and result.
26
+ - If validation fails, keep the run in the repair loop; do not summarize delivery or mark residual risk as low.
package/src/cli/kcode.ts CHANGED
@@ -12,6 +12,7 @@ export interface KcodeCliResult {
12
12
 
13
13
  interface PiSettings {
14
14
  packages?: string[];
15
+ shellPath?: string;
15
16
  [key: string]: unknown;
16
17
  }
17
18
 
@@ -100,6 +101,7 @@ export function doctor(cwd: string, args: string[] = []): KcodeCliResult {
100
101
  lines.push(`KCode package:${packageRoot}`);
101
102
  lines.push(`项目配置:${existsSync(settingsPath) ? settingsPath : "未创建,运行 kcode init"}`);
102
103
  lines.push(`项目上下文:${existsSync(projectContextPath) ? projectContextPath : "未创建,运行 kcode context"}`);
104
+ lines.push(`Windows bash:${process.platform === "win32" ? formatWindowsBashStatus(readSettingsSafe(settingsPath)) : "不适用"}`);
103
105
 
104
106
  if (existsSync(settingsPath)) {
105
107
  const settingsResult = readSettingsSafe(settingsPath);
@@ -135,6 +137,10 @@ export function doctor(cwd: string, args: string[] = []): KcodeCliResult {
135
137
  warnings++;
136
138
  lines.push("[WARN] 项目上下文不存在,运行 kcode context --refresh 或 kcode repair。");
137
139
  }
140
+ if (process.platform === "win32" && !findWindowsBash(readSettingsSafe(settingsPath))) {
141
+ warnings++;
142
+ lines.push("[WARN] Pi 内置 bash 在当前 Windows 环境不可用;KCode 已覆盖 bash 为 PowerShell,并提供 kd_find_file/kd_list_dir。安装 Git for Windows 可恢复 Pi 原生 bash。");
143
+ }
138
144
  const npmPrefix = spawnSync("npm", ["prefix", "-g"], { encoding: "utf8" });
139
145
  const npmRoot = spawnSync("npm", ["root", "-g"], { encoding: "utf8" });
140
146
  lines.push(`npm prefix -g:${npmPrefix.status === 0 ? npmPrefix.stdout.trim() : "不可用"}`);
@@ -351,3 +357,24 @@ function helpText(): string {
351
357
  " kcode start 初始化项目配置后启动 KCode 工作环境",
352
358
  ].join("\n");
353
359
  }
360
+
361
+ function formatWindowsBashStatus(settingsResult: ReturnType<typeof readSettingsSafe>): string {
362
+ const bash = findWindowsBash(settingsResult);
363
+ return bash ? `可用(${bash})` : "不可用;KCode 将使用 PowerShell 覆盖工具和 kd_find_file/kd_list_dir";
364
+ }
365
+
366
+ function findWindowsBash(settingsResult?: ReturnType<typeof readSettingsSafe>): string | undefined {
367
+ const configured = settingsResult?.ok && typeof settingsResult.settings.shellPath === "string" ? settingsResult.settings.shellPath : undefined;
368
+ if (configured && existsSync(configured)) return configured;
369
+ const candidates = [
370
+ process.env.ProgramFiles ? join(process.env.ProgramFiles, "Git", "bin", "bash.exe") : undefined,
371
+ process.env["ProgramFiles(x86)"] ? join(process.env["ProgramFiles(x86)"]!, "Git", "bin", "bash.exe") : undefined,
372
+ ...pathEntries().map((entry) => join(entry, "bash.exe")),
373
+ ].filter((item): item is string => Boolean(item));
374
+ return candidates.find((candidate) => existsSync(candidate));
375
+ }
376
+
377
+ function pathEntries(): string[] {
378
+ const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "PATH";
379
+ return (process.env[pathKey] ?? "").split(";").filter(Boolean);
380
+ }
@@ -1,10 +1,6 @@
1
1
  import type { KdPhase } from "./types.ts";
2
2
  import { PHASE_ARTIFACTS } from "./types.ts";
3
3
  import {
4
- DATA_SOURCE_CONTEXT_FIELDS,
5
- IMPLEMENTATION_CONTRACT_FIELDS,
6
- INTEGRATION_CONTEXT_FIELDS,
7
- fieldLabels,
8
4
  questionForMissingLabel,
9
5
  } from "./prompt-policy.ts";
10
6
 
@@ -20,9 +16,12 @@ export function unknownRiskReason(): string {
20
16
  }
21
17
 
22
18
  export function openQuestionsReason(questions: Array<{ id: string; question: string }>): string {
19
+ const first = questions[0];
20
+ if (!first) return "不存在未回答的阻断问题。";
21
+ const remaining = questions.length > 1 ? `;另有 ${questions.length - 1} 个历史 open 问题,需先处理当前问题后再逐个处理` : "";
23
22
  return [
24
- `存在未回答的阻断问题:${questions.map((question) => `${question.id} ${question.question}`).join(";")}`,
25
- "下一步:获取用户答案,然后用 kd_question action=answer id=<问题编号> answer=<用户答案> 记录。",
23
+ `当前未回答的阻断问题:${first.id} ${first.question}${remaining}`,
24
+ `下一步:只获取 ${first.id} 的用户答案,然后用 kd_question action=answer id=${first.id} answer=<用户答案> 记录;禁止继续提出其他问题。`,
26
25
  ].join("。");
27
26
  }
28
27
 
@@ -33,8 +32,8 @@ export function repairBlockedReason(attempts: number, maxAttempts: number, evide
33
32
 
34
33
  export function dataSourcePlanBlockedReason(missing: string[]): string {
35
34
  return [
36
- `不能进入 execute:业务数据源上下文不完整,缺少 ${missing.join("")}。`,
37
- `下一步:停止编码,使用 kd_question 逐项确认 ${fieldLabels(DATA_SOURCE_CONTEXT_FIELDS).join("")}。`,
35
+ missingSummary("业务数据源上下文", missing),
36
+ "下一步:停止编码,只登记当前最阻塞问题;用户回答并记录后,再重新刷新门禁决定下一问。",
38
37
  nextQuestionInstruction(missing),
39
38
  "API 文档只能作为调用线索,不能替代这些业务事实。",
40
39
  ].filter(Boolean).join("");
@@ -42,8 +41,8 @@ export function dataSourcePlanBlockedReason(missing: string[]): string {
42
41
 
43
42
  export function integrationPlanBlockedReason(missing: string[]): string {
44
43
  return [
45
- `不能进入 execute:第三方对接上下文不完整,缺少 ${missing.join("")}。`,
46
- `下一步:停止编码,使用 kd_question 逐项确认 ${fieldLabels(INTEGRATION_CONTEXT_FIELDS).join("")}。`,
44
+ missingSummary("第三方对接上下文", missing),
45
+ "下一步:停止编码,只登记当前最阻塞问题;用户回答并记录后,再重新刷新门禁决定下一问。",
47
46
  nextQuestionInstruction(missing),
48
47
  "缺少这些信息时禁止生成模板代码。",
49
48
  ].filter(Boolean).join("");
@@ -51,8 +50,8 @@ export function integrationPlanBlockedReason(missing: string[]): string {
51
50
 
52
51
  export function implementationPlanBlockedReason(missing: string[]): string {
53
52
  return [
54
- `不能进入 execute:实现就绪信息不完整,缺少 ${missing.join("")}。`,
55
- `下一步:禁止生成模板代码;使用 kd_question 按最阻塞项逐个确认 ${fieldLabels(IMPLEMENTATION_CONTRACT_FIELDS).join("")}。`,
53
+ missingSummary("实现就绪信息", missing),
54
+ "下一步:禁止生成模板代码,只登记当前最阻塞问题;用户回答并记录后,再重新刷新门禁决定下一问。",
56
55
  nextQuestionInstruction(missing),
57
56
  "这些字段是所有插件、服务和集成实现的通用输入,不依赖具体业务场景枚举。",
58
57
  ].filter(Boolean).join("");
@@ -158,15 +157,22 @@ export function dataSourceWriteBlockedReason(path: string, evidenceName: string)
158
157
  }
159
158
 
160
159
  export function integrationWriteBlockedReason(path: string, missing: string[]): string {
161
- return `不能写生产源码 ${path}:第三方对接上下文不完整,缺少 ${missing.join("、")}。下一步:补齐 ${fieldLabels(INTEGRATION_CONTEXT_FIELDS).join("")}。`;
160
+ return `不能写生产源码 ${path}:第三方对接上下文不完整,当前最阻塞缺失:${missing[0] ?? "未知"}。下一步:只确认当前缺失事实,记录后重新刷新门禁;禁止一次性向用户索要完整清单。`;
162
161
  }
163
162
 
164
163
  export function implementationWriteBlockedReason(path: string, missing: string[]): string {
165
- return `不能写生产源码 ${path}:实现就绪信息不完整,缺少 ${missing.join("、")}。下一步:确认 ${fieldLabels(IMPLEMENTATION_CONTRACT_FIELDS).join("")};信息不足时继续提问,禁止生成模板代码。`;
164
+ return `不能写生产源码 ${path}:实现就绪信息不完整,当前最阻塞缺失:${missing[0] ?? "未知"}。下一步:只确认当前缺失事实,记录后重新刷新门禁;信息不足时继续提问,禁止生成模板代码。`;
166
165
  }
167
166
 
168
167
  function nextQuestionInstruction(missing: string[]): string | undefined {
169
168
  const question = questionForMissingLabel(missing[0]);
170
169
  if (!question) return undefined;
171
- return `阻断问题命令:kd_question action=ask factLabel="${missing[0]}" question="${question}" reason="${missing[0]} 缺失会阻塞实现。"。`;
170
+ return `唯一允许的阻断问题命令:kd_question action=ask factLabel="${missing[0]}" question="${question}" reason="${missing[0]} 缺失会阻塞实现。"。`;
171
+ }
172
+
173
+ function missingSummary(scope: string, missing: string[]): string {
174
+ const first = missing[0] ?? "未知";
175
+ const remaining = Math.max(0, missing.length - 1);
176
+ const remainingText = remaining > 0 ? `;另有 ${remaining} 项待后续逐项确认,本轮不得向用户展开` : "";
177
+ return `不能进入 execute:${scope}不完整,当前最阻塞缺失:${first}${remainingText}。`;
172
178
  }
@@ -145,6 +145,7 @@ export const INTEGRATION_CONTEXT_FIELDS: ContractField[] = [
145
145
  export const PROJECT_PERSISTENT_RULES = [
146
146
  "计划或编辑代码前必须读取本文件;本文件过期时先运行 `kcode context --refresh`。",
147
147
  "信息不足时禁止开始编码。必须先登记一个最阻塞的结构化问题,获得可核验答案后再继续;禁止输出 demo/sample/scaffold、模板代码或占位实现。",
148
+ "任何时刻最多只能存在一个 open blocking question;已有未回答阻断问题时,必须先记录用户答案,禁止继续提出、登记或展示其他问题。",
148
149
  "API 文档、SDK 文档和知识库只能证明技术用法,不能替代业务事实。FormId、单据/表单标识、字段/实体/分录标识、插件类型与事件、SQL/KSQL 表名和数据库字段名必须来自用户确认、项目元数据或 evidence。",
149
150
  `产品代码实现前必须具备通用实现契约:${IMPLEMENTATION_CONTRACT_FIELDS.map((field) => field.label).join("、")}。`,
150
151
  `涉及业务数据源时必须具备数据源上下文:${DATA_SOURCE_CONTEXT_FIELDS.map((field) => field.label).join("、")}。`,
@@ -154,9 +155,11 @@ export const PROJECT_PERSISTENT_RULES = [
154
155
  "`run.facts` 是唯一结构化事实源;已回答 questions 仅为审计记录,禁止在读取状态时从历史 question 反推事实。",
155
156
  "`factLabel` 必须使用集中定义的事实标签或别名;未知标签、占位答案、口头确认语、待确认/TODO/TBD/按实际环境等不能解除门禁。",
156
157
  "用户回答 open question 后,先用 `kd_question action=answer` 写入答案;用户更正事实时用 `kd_question action=revise`,禁止重复询问已确认事实。",
158
+ "门禁提示只暴露当前最阻塞缺失事实;后续待确认项只能在当前问题回答后重新刷新门禁逐项生成,禁止一次性向用户展开完整问题清单。",
157
159
  "企业版 Python 插件通常没有本地构建可替代验证;BOS 注册、外部系统操作、人工功能测试和生产环境验证必须由用户提供可核验证据并记录来源。",
158
160
  "提示语集中管理为正式工程指令;禁止口语化、闲聊式、鼓励式提示词进入运行时规则。",
159
161
  "工具使用必须匹配当前会话实际可用工具和操作系统;Windows 默认使用 PowerShell、`rg`、`Get-ChildItem`、`Get-Content`,禁止假设 bash 可用。",
162
+ "Windows 查找文件和目录时优先使用 `kd_find_file`、`kd_list_dir` 或 PowerShell;禁止用 Linux find/ls 语法反复调用 Pi 内置 bash。",
160
163
  "文件定位必须使用真实搜索或目录读取结果;工具不可用时明确说明阻塞原因和需要用户执行的命令,禁止猜测路径、反复自述工具失败或用 kd_subagent 代替基础文件搜索。",
161
164
  "不做旧状态迁移兼容或旧问题答案推断;坏状态只过滤,缺失事实必须重新提问确认。",
162
165
  ];
@@ -166,6 +169,49 @@ export const PROMPT_STYLE_RULES = [
166
169
  "事实不足时生成阻断问题;禁止输出模板代码、占位实现或基于猜测的业务标识。",
167
170
  "每次只提出一个最阻塞问题;问题必须指向可验证事实、数据标识或验收证据。",
168
171
  "引用顺序:当前项目文件、PLAN/SPEC、元数据 evidence、SDK 签名、验证输出。",
172
+ "响应必须说明当前阶段、门禁结论、采用的事实来源和唯一下一动作;禁止同时给出多个并行动作让用户选择。",
173
+ ];
174
+
175
+ export const PROMPT_DECISION_PROTOCOL = [
176
+ "先读取 Harness 状态、当前阶段资料、已问已答事实和项目上下文;禁止只根据用户最后一句话行动。",
177
+ "先判断用户输入是否是在回答 open blocking question;如是,唯一动作是调用 `kd_question action=answer` 记录答案。",
178
+ "再判断当前阶段门禁是否阻塞;阻塞时只执行门禁要求的唯一下一动作,禁止绕过阶段推进。",
179
+ "再判断是否缺少可核验事实;缺失时只登记当前最阻塞的一个问题,禁止展开后续问题。",
180
+ "只有在阶段、门禁、事实和证据均满足时,才允许进入计划、编码、验证或交付动作。",
181
+ ];
182
+
183
+ export const PROMPT_FACT_PROTOCOL = [
184
+ "`run.facts` 是结构化事实唯一来源;`questions` 仅为问答审计记录。",
185
+ "业务事实必须有来源分类:用户明确回答、项目元数据/evidence、当前项目文件、验证输出。",
186
+ "API/SDK 文档只作为技术用法线索;不得补全 FormId、字段标识、实体标识、表名、接口字段映射、并发策略或验收数据。",
187
+ "事实值包含待确认、未知、按实际环境、TODO/TBD、可以、好的、是/否等内容时,视为无效事实。",
188
+ "同一事实存在冲突时,停止推进并使用 `kd_question action=revise` 或登记一个最阻塞更正问题。",
189
+ ];
190
+
191
+ export const PROMPT_QUESTION_PROTOCOL = [
192
+ "任何时刻最多存在一个 open blocking question。",
193
+ "已有 open blocking question 时,禁止继续提问、禁止输出问题清单、禁止登记新问题;唯一动作是获取并记录该问题答案。",
194
+ "`kd_question action=ask` 的问题文本只能包含一个事实点,不能使用编号列表、多个问号或复合事实。",
195
+ "带 `factLabel` 的问题必须使用集中定义标签或别名;产品类型不是 factLabel。",
196
+ "问题登记后停止推进,等待用户回答;不得在同一轮继续生成方案、计划或代码。",
197
+ ];
198
+
199
+ export const PROMPT_OUTPUT_CONTRACT = [
200
+ "阻塞时输出:阻塞原因、唯一下一动作、需要使用的工具或命令;不得输出实现方案或代码片段。",
201
+ "提问时输出:只登记或展示一个问题;不得附带后续问题预告清单。",
202
+ "计划时输出:目标路径、允许修改文件、查证项、验证命令、回滚方式和 evidence 要求。",
203
+ "执行时输出:仅说明已修改文件、证据文件和验证结果;不得把未验证内容写成已完成。",
204
+ "验证失败时输出:失败证据、定位结论、下一轮修复边界;不得直接跳到 ship。",
205
+ ];
206
+
207
+ export const PROMPT_PROHIBITED_BEHAVIORS = [
208
+ "禁止根据 API 文档、SDK 记忆或知识库推断业务数据源。",
209
+ "禁止生成 demo/sample/scaffold、占位实现、伪代码或仅可手工替换的模板。",
210
+ "禁止一次性向用户索要多项业务信息。",
211
+ "禁止重复询问已确认事实或已存在 open blocking question 的问题。",
212
+ "禁止猜测路径、包名、类名、FormId、字段、实体、SQL 表名、数据库字段名或接口字段映射。",
213
+ "禁止用子 agent、shell 失败自述或自由文本计划绕过门禁。",
214
+ "禁止将外部系统操作、BOS 注册、人工功能测试或生产验证描述为 LLM 已完成。",
169
215
  ];
170
216
 
171
217
  export const CORE_WORKFLOW_CONSTRAINTS = [
@@ -174,6 +220,7 @@ export const CORE_WORKFLOW_CONSTRAINTS = [
174
220
  "业务数据源未知时禁止编码;确认目标 FormId/单据或表单、插件类型和事件、字段/实体/分录标识、数据读取写入方式后再编码;SQL/KSQL 同步确认表名和数据库字段名。",
175
221
  "第三方对接确认接口文档、对接方向、触发时机、认证配置、字段映射、并发/幂等、重试超时限流、错误补偿、日志脱敏和验收样例后再编码。",
176
222
  "事实缺失时使用 kd_question 登记一个最阻塞问题;禁止用 API 文档、SDK 知识库或推测替代业务事实。",
223
+ "已有 open blocking question 时禁止继续提问;先记录该问题答案,再根据刷新后的门禁决定下一问。",
177
224
  "用户输入是在回答 open question 时,必须先调用 kd_question action=answer 记录答案,再继续推进或登记下一个问题。",
178
225
  "同一 factLabel 已有当前事实时禁止重复提问;用户明确更正时使用 kd_question action=revise 记录新事实和更正原因。",
179
226
  "run.facts 是唯一结构化事实源;questions 仅作为问答审计记录,禁止从历史 question 反推门禁事实。",
@@ -183,7 +230,7 @@ export const CORE_WORKFLOW_CONSTRAINTS = [
183
230
  "evidence 必须记录命令、Exit 和关键输出;命令无法运行时记录阻塞原因。",
184
231
  "外部系统操作、BOS 注册、人工功能测试和生产环境验证不能由 LLM 代办;必须要求用户提供验证结果或可核验证据,并记录证据来源。",
185
232
  "Windows 路径规则:项目相对路径为默认;绝对路径使用 D:\\... 形式。",
186
- "工具规则:按当前会话实际 shell 和工具执行;Windows 不假设 bash,可用 PowerShell/rg/Get-ChildItem/Get-Content;禁止猜路径或用 kd_subagent 代替基础搜索失败。",
233
+ "工具规则:按当前会话实际 shell 和工具执行;Windows 不假设 bash,优先用 kd_find_file/kd_list_dir/PowerShell;禁止猜路径或用 kd_subagent 代替基础搜索失败。",
187
234
  ];
188
235
 
189
236
  export const PHASE_GUIDANCE: Record<KdPhase, string> = {
@@ -214,6 +261,25 @@ export function fieldLabels(fields: ContractField[]): string[] {
214
261
  return fields.map((field) => field.label);
215
262
  }
216
263
 
264
+ export function allFactLabels(): string[] {
265
+ return fieldLabels([...IMPLEMENTATION_CONTRACT_FIELDS, ...DATA_SOURCE_CONTEXT_FIELDS, ...INTEGRATION_CONTEXT_FIELDS]);
266
+ }
267
+
268
+ export function formatFactLabelCatalog(): string {
269
+ return [
270
+ "## 实现契约事实标签",
271
+ ...formatPromptLines(fieldLabels(IMPLEMENTATION_CONTRACT_FIELDS)),
272
+ "",
273
+ "## 数据源上下文事实标签",
274
+ ...formatPromptLines(fieldLabels(DATA_SOURCE_CONTEXT_FIELDS)),
275
+ "",
276
+ "## 第三方对接事实标签",
277
+ ...formatPromptLines(fieldLabels(INTEGRATION_CONTEXT_FIELDS)),
278
+ "",
279
+ "产品类型/产品画像不是 factLabel;使用 /kd-product <产品> 或先登记无 factLabel 的产品确认问题。",
280
+ ].join("\n");
281
+ }
282
+
217
283
  export function questionForMissingLabel(label: string): string | undefined {
218
284
  const fields = [...IMPLEMENTATION_CONTRACT_FIELDS, ...DATA_SOURCE_CONTEXT_FIELDS, ...INTEGRATION_CONTEXT_FIELDS];
219
285
  return fields.find((field) => field.label === label)?.question;
@@ -4,7 +4,17 @@ import { delegationGuidanceForWorkflow, shouldInjectDelegationGuidance } from ".
4
4
  import { formatStatus } from "./format.ts";
5
5
  import type { ActiveRun, KdPhase } from "./types.ts";
6
6
  import { PHASE_ORDER } from "./types.ts";
7
- import { CORE_WORKFLOW_CONSTRAINTS, PHASE_GUIDANCE, PROMPT_STYLE_RULES, formatPromptLines } from "./prompt-policy.ts";
7
+ import {
8
+ CORE_WORKFLOW_CONSTRAINTS,
9
+ PHASE_GUIDANCE,
10
+ PROMPT_DECISION_PROTOCOL,
11
+ PROMPT_FACT_PROTOCOL,
12
+ PROMPT_OUTPUT_CONTRACT,
13
+ PROMPT_PROHIBITED_BEHAVIORS,
14
+ PROMPT_QUESTION_PROTOCOL,
15
+ PROMPT_STYLE_RULES,
16
+ formatPromptLines,
17
+ } from "./prompt-policy.ts";
8
18
  import { formatQuestionMemory } from "./question-memory.ts";
9
19
 
10
20
  export function workflowPromptForRun(cwd: string, run: ActiveRun, userText: string): string {
@@ -33,6 +43,21 @@ export function workflowPromptForRun(cwd: string, run: ActiveRun, userText: stri
33
43
  "当前阶段任务:",
34
44
  phaseGuidance,
35
45
  "",
46
+ "执行顺序协议:",
47
+ ...formatPromptLines(PROMPT_DECISION_PROTOCOL),
48
+ "",
49
+ "事实处理协议:",
50
+ ...formatPromptLines(PROMPT_FACT_PROTOCOL),
51
+ "",
52
+ "提问协议:",
53
+ ...formatPromptLines(PROMPT_QUESTION_PROTOCOL),
54
+ "",
55
+ "输出契约:",
56
+ ...formatPromptLines(PROMPT_OUTPUT_CONTRACT),
57
+ "",
58
+ "禁止行为:",
59
+ ...formatPromptLines(PROMPT_PROHIBITED_BEHAVIORS),
60
+ "",
36
61
  "工程指令风格:",
37
62
  ...formatPromptLines(PROMPT_STYLE_RULES),
38
63
  "",
@@ -1,5 +1,5 @@
1
1
  import type { ActiveRun, KdFact, KdQuestion } from "./types.ts";
2
- import { canonicalFactLabel } from "./prompt-policy.ts";
2
+ import { allFactLabels, canonicalFactLabel, formatFactLabelCatalog } from "./prompt-policy.ts";
3
3
 
4
4
  export function formatQuestionMemory(run: ActiveRun): string {
5
5
  const questions = Array.isArray(run.questions) ? run.questions : [];
@@ -96,16 +96,20 @@ export function factFromAnsweredQuestion(question: KdQuestion, now = new Date().
96
96
 
97
97
  export function questionAskBlockReason(run: ActiveRun, input: { factLabel?: string }): string | undefined {
98
98
  const rawLabel = input.factLabel?.trim();
99
- if (!rawLabel) return undefined;
100
- const label = canonicalFactLabel(rawLabel);
101
- if (!label) return `未知 factLabel:${rawLabel}。必须使用 KCode 集中定义的实现契约、数据源上下文或第三方对接事实标签。`;
102
- const key = factKey(label);
103
- const open = (Array.isArray(run.questions) ? run.questions : []).find(
104
- (question) => question.status === "open" && question.blocking && question.factLabel && factKey(question.factLabel) === key,
105
- );
106
- if (open) {
99
+ const label = rawLabel ? canonicalFactLabel(rawLabel) : undefined;
100
+ if (rawLabel && !label) return [`未知 factLabel:${rawLabel}。必须使用 KCode 集中定义的实现契约、数据源上下文或第三方对接事实标签。`, "", "执行 `kd_question action=labels` 查看完整合法标签。", "", formatFactLabelCatalog()].join("\n");
101
+ const openBlocking = (Array.isArray(run.questions) ? run.questions : []).filter((question) => question.status === "open" && question.blocking);
102
+ const sameFactOpen = label
103
+ ? openBlocking.find((question) => question.factLabel && factKey(question.factLabel) === factKey(label))
104
+ : undefined;
105
+ const open = sameFactOpen ?? openBlocking[0];
106
+ if (open && sameFactOpen && label) {
107
107
  return `事实标签 ${label} 已有未回答问题 ${open.id},禁止重复提问。下一步:获取用户答案后用 kd_question action=answer id=${open.id} answer=<答案> 记录。`;
108
108
  }
109
+ if (open) {
110
+ return `已有未回答阻断问题 ${open.id}:${open.question}。禁止继续登记其他问题。下一步:只获取该问题答案,并用 kd_question action=answer id=${open.id} answer=<答案> 记录。`;
111
+ }
112
+ if (!label) return undefined;
109
113
  const fact = currentFactForLabel(run, label);
110
114
  if (fact) {
111
115
  return `事实标签 ${label} 已确认为 ${fact.value},禁止重复提问。用户明确更正时使用 kd_question action=revise factLabel="${label}" answer="<新值>" reason="<更正原因>"。`;
@@ -113,6 +117,26 @@ export function questionAskBlockReason(run: ActiveRun, input: { factLabel?: stri
113
117
  return undefined;
114
118
  }
115
119
 
120
+ export function questionFactLabelMismatchReason(input: { question: string; factLabel?: string }): string | undefined {
121
+ const question = input.question.trim();
122
+ const label = input.factLabel?.trim() ? canonicalFactLabel(input.factLabel) : undefined;
123
+ if (!label) return undefined;
124
+ const asksProduct = /产品类型|产品画像|金蝶产品|技术栈|苍穹|星瀚|星空旗舰版|旗舰版|企业版|Cloud\s*BOS|Cosmic|BOS/i.test(question);
125
+ const asksOtherFact = allFactLabels().some((candidate) => candidate !== label && labelAppearsInQuestion(question, candidate));
126
+ const conjunction = /以及|和|并|同时|还有|及|\/|、|,|,/.test(question);
127
+ if (asksProduct && (asksOtherFact || conjunction)) {
128
+ return [
129
+ `问题文本同时要求确认产品类型和 ${label},但 factLabel 只能记录一个结构化事实。`,
130
+ "产品类型/产品画像不是 factLabel;先使用 /kd-product <产品>,或登记一个无 factLabel 的产品确认问题。",
131
+ `确认产品后,再单独登记 ${label}。`,
132
+ ].join("\n");
133
+ }
134
+ if (asksOtherFact && conjunction) {
135
+ return `问题文本包含多个结构化事实,但 factLabel 只能是一个:${label}。必须拆成单事实问题,并且每次只登记当前最阻塞的一个。`;
136
+ }
137
+ return undefined;
138
+ }
139
+
116
140
  export function invalidFactValueReason(value: string): string | undefined {
117
141
  return isInvalidFactValue(value) ? "事实值不是可核验内容,不能使用待确认、未知、按实际环境、TODO/TBD、单独确认/否定或口头确认语解除门禁。" : undefined;
118
142
  }
@@ -218,3 +242,15 @@ function normalizePersistedFact(fact: KdFact): KdFact[] {
218
242
  if (!label || !value || invalidFactValueReason(value)) return [];
219
243
  return [{ ...fact, key: factKey(label), label, value }];
220
244
  }
245
+
246
+ function labelAppearsInQuestion(question: string, label: string): boolean {
247
+ const normalizedQuestion = question.toLowerCase().replace(/\s+/g, "");
248
+ const normalizedLabel = label.toLowerCase().replace(/\s+/g, "");
249
+ if (normalizedQuestion.includes(normalizedLabel)) return true;
250
+ if (label === "目标 FormId/单据或表单标识") return /formid|formid|单据标识|表单标识|目标单据|目标表单/i.test(question);
251
+ if (label === "插件类型和触发事件") return /插件类型|触发事件|生命周期|挂载点|审核事件|保存事件/i.test(question);
252
+ if (label === "字段/实体/分录标识") return /字段标识|实体标识|分录标识|字段|实体|单据体/i.test(question);
253
+ if (label === "数据读取写入方式") return /读取方式|写入方式|读写|取数|数据访问|SQL|KSQL/i.test(question);
254
+ if (label === "SQL/KSQL 表名和数据库字段名") return /表名|数据库字段名|SQL字段|KSQL字段/i.test(question);
255
+ return false;
256
+ }
@@ -0,0 +1,107 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { basename, join, relative } from "node:path";
3
+ import { resolveWorkspacePath } from "../platform/path.ts";
4
+
5
+ const IGNORED_DIRS = new Set([".git", ".idea", ".pi", ".tmp", ".vscode", "bin", "build", "dist", "node_modules", "obj", "out", "target"]);
6
+ const DEFAULT_MAX_RESULTS = 50;
7
+ const MAX_ENTRIES = 50000;
8
+
9
+ export interface FileSearchResult {
10
+ path: string;
11
+ type: "file" | "directory";
12
+ size?: number;
13
+ }
14
+
15
+ export function findFiles(cwd: string, input: { root?: string; name: string; maxResults?: number }): FileSearchResult[] {
16
+ const root = resolveWorkspacePath(cwd, input.root || ".");
17
+ const pattern = input.name.trim();
18
+ if (!pattern) throw new Error("kd_find_file requires a non-empty name.");
19
+ if (!existsSync(root) || !statSync(root).isDirectory()) throw new Error(`Search root is not a directory: ${root}`);
20
+
21
+ const maxResults = clampLimit(input.maxResults);
22
+ const matcher = wildcardMatcher(pattern);
23
+ const results: FileSearchResult[] = [];
24
+ const queue = [root];
25
+ let visited = 0;
26
+
27
+ while (queue.length > 0 && results.length < maxResults && visited < MAX_ENTRIES) {
28
+ const current = queue.shift();
29
+ if (!current) break;
30
+
31
+ let children;
32
+ try {
33
+ children = readdirSync(current, { withFileTypes: true });
34
+ } catch {
35
+ continue;
36
+ }
37
+
38
+ for (const child of children) {
39
+ if (results.length >= maxResults || visited >= MAX_ENTRIES) break;
40
+ const fullPath = join(current, child.name);
41
+ visited++;
42
+
43
+ if (child.isDirectory()) {
44
+ if (!IGNORED_DIRS.has(child.name)) queue.push(fullPath);
45
+ if (matcher(child.name)) results.push({ path: displayPath(cwd, fullPath), type: "directory" });
46
+ continue;
47
+ }
48
+
49
+ if (child.isFile() && matcher(child.name)) {
50
+ let size: number | undefined;
51
+ try {
52
+ size = statSync(fullPath).size;
53
+ } catch {
54
+ size = undefined;
55
+ }
56
+ results.push({ path: displayPath(cwd, fullPath), type: "file", size });
57
+ }
58
+ }
59
+ }
60
+
61
+ return results;
62
+ }
63
+
64
+ export function listDirectory(cwd: string, input: { path?: string; maxEntries?: number }): FileSearchResult[] {
65
+ const dir = resolveWorkspacePath(cwd, input.path || ".");
66
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) throw new Error(`Path is not a directory: ${dir}`);
67
+ const maxEntries = clampLimit(input.maxEntries);
68
+ return readdirSync(dir, { withFileTypes: true })
69
+ .slice(0, maxEntries)
70
+ .map((entry) => {
71
+ const fullPath = join(dir, entry.name);
72
+ if (entry.isDirectory()) return { path: displayPath(cwd, fullPath), type: "directory" as const };
73
+ let size: number | undefined;
74
+ try {
75
+ size = statSync(fullPath).size;
76
+ } catch {
77
+ size = undefined;
78
+ }
79
+ return { path: displayPath(cwd, fullPath), type: "file" as const, size };
80
+ });
81
+ }
82
+
83
+ export function formatFileSearchResults(results: FileSearchResult[]): string {
84
+ if (results.length === 0) return "未找到匹配项。";
85
+ return results.map((item) => `- ${item.type === "directory" ? "[dir]" : "[file]"} ${item.path}${item.size === undefined ? "" : ` (${item.size} bytes)`}`).join("\n");
86
+ }
87
+
88
+ function clampLimit(value: number | undefined): number {
89
+ return Math.max(1, Math.min(Math.trunc(value ?? DEFAULT_MAX_RESULTS), 500));
90
+ }
91
+
92
+ function displayPath(cwd: string, fullPath: string): string {
93
+ const rel = relative(cwd, fullPath);
94
+ if (rel && !rel.startsWith("..") && !/^[a-zA-Z]:/.test(rel)) return rel.replace(/\//g, "\\");
95
+ return fullPath;
96
+ }
97
+
98
+ function wildcardMatcher(pattern: string): (name: string) => boolean {
99
+ const normalized = pattern.trim();
100
+ if (!/[*?]/.test(normalized)) {
101
+ const target = normalized.toLowerCase();
102
+ return (name) => basename(name).toLowerCase() === target;
103
+ }
104
+ const escaped = normalized.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
105
+ const regex = new RegExp(`^${escaped}$`, "i");
106
+ return (name) => regex.test(basename(name));
107
+ }