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.
package/dist/cli/kcode.js CHANGED
@@ -73,6 +73,7 @@ export function doctor(cwd, args = []) {
73
73
  lines.push(`KCode package:${packageRoot}`);
74
74
  lines.push(`项目配置:${existsSync(settingsPath) ? settingsPath : "未创建,运行 kcode init"}`);
75
75
  lines.push(`项目上下文:${existsSync(projectContextPath) ? projectContextPath : "未创建,运行 kcode context"}`);
76
+ lines.push(`Windows bash:${process.platform === "win32" ? formatWindowsBashStatus(readSettingsSafe(settingsPath)) : "不适用"}`);
76
77
  if (existsSync(settingsPath)) {
77
78
  const settingsResult = readSettingsSafe(settingsPath);
78
79
  if (!settingsResult.ok) {
@@ -109,6 +110,10 @@ export function doctor(cwd, args = []) {
109
110
  warnings++;
110
111
  lines.push("[WARN] 项目上下文不存在,运行 kcode context --refresh 或 kcode repair。");
111
112
  }
113
+ if (process.platform === "win32" && !findWindowsBash(readSettingsSafe(settingsPath))) {
114
+ warnings++;
115
+ lines.push("[WARN] Pi 内置 bash 在当前 Windows 环境不可用;KCode 已覆盖 bash 为 PowerShell,并提供 kd_find_file/kd_list_dir。安装 Git for Windows 可恢复 Pi 原生 bash。");
116
+ }
112
117
  const npmPrefix = spawnSync("npm", ["prefix", "-g"], { encoding: "utf8" });
113
118
  const npmRoot = spawnSync("npm", ["root", "-g"], { encoding: "utf8" });
114
119
  lines.push(`npm prefix -g:${npmPrefix.status === 0 ? npmPrefix.stdout.trim() : "不可用"}`);
@@ -305,3 +310,22 @@ function helpText() {
305
310
  " kcode start 初始化项目配置后启动 KCode 工作环境",
306
311
  ].join("\n");
307
312
  }
313
+ function formatWindowsBashStatus(settingsResult) {
314
+ const bash = findWindowsBash(settingsResult);
315
+ return bash ? `可用(${bash})` : "不可用;KCode 将使用 PowerShell 覆盖工具和 kd_find_file/kd_list_dir";
316
+ }
317
+ function findWindowsBash(settingsResult) {
318
+ const configured = settingsResult?.ok && typeof settingsResult.settings.shellPath === "string" ? settingsResult.settings.shellPath : undefined;
319
+ if (configured && existsSync(configured))
320
+ return configured;
321
+ const candidates = [
322
+ process.env.ProgramFiles ? join(process.env.ProgramFiles, "Git", "bin", "bash.exe") : undefined,
323
+ process.env["ProgramFiles(x86)"] ? join(process.env["ProgramFiles(x86)"], "Git", "bin", "bash.exe") : undefined,
324
+ ...pathEntries().map((entry) => join(entry, "bash.exe")),
325
+ ].filter((item) => Boolean(item));
326
+ return candidates.find((candidate) => existsSync(candidate));
327
+ }
328
+ function pathEntries() {
329
+ const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "PATH";
330
+ return (process.env[pathKey] ?? "").split(";").filter(Boolean);
331
+ }
@@ -10,10 +10,17 @@ export declare const DATA_SOURCE_CONTEXT_FIELDS: ContractField[];
10
10
  export declare const INTEGRATION_CONTEXT_FIELDS: ContractField[];
11
11
  export declare const PROJECT_PERSISTENT_RULES: string[];
12
12
  export declare const PROMPT_STYLE_RULES: string[];
13
+ export declare const PROMPT_DECISION_PROTOCOL: string[];
14
+ export declare const PROMPT_FACT_PROTOCOL: string[];
15
+ export declare const PROMPT_QUESTION_PROTOCOL: string[];
16
+ export declare const PROMPT_OUTPUT_CONTRACT: string[];
17
+ export declare const PROMPT_PROHIBITED_BEHAVIORS: string[];
13
18
  export declare const CORE_WORKFLOW_CONSTRAINTS: string[];
14
19
  export declare const PHASE_GUIDANCE: Record<KdPhase, string>;
15
20
  export declare const PLAN_REQUIRED_CHECK_LINES: string[];
16
21
  export declare function formatPromptLines(lines: string[]): string[];
17
22
  export declare function fieldLabels(fields: ContractField[]): string[];
23
+ export declare function allFactLabels(): string[];
24
+ export declare function formatFactLabelCatalog(): string;
18
25
  export declare function questionForMissingLabel(label: string): string | undefined;
19
26
  export declare function canonicalFactLabel(label: string): string | undefined;
@@ -133,6 +133,7 @@ export const INTEGRATION_CONTEXT_FIELDS = [
133
133
  export const PROJECT_PERSISTENT_RULES = [
134
134
  "计划或编辑代码前必须读取本文件;本文件过期时先运行 `kcode context --refresh`。",
135
135
  "信息不足时禁止开始编码。必须先登记一个最阻塞的结构化问题,获得可核验答案后再继续;禁止输出 demo/sample/scaffold、模板代码或占位实现。",
136
+ "任何时刻最多只能存在一个 open blocking question;已有未回答阻断问题时,必须先记录用户答案,禁止继续提出、登记或展示其他问题。",
136
137
  "API 文档、SDK 文档和知识库只能证明技术用法,不能替代业务事实。FormId、单据/表单标识、字段/实体/分录标识、插件类型与事件、SQL/KSQL 表名和数据库字段名必须来自用户确认、项目元数据或 evidence。",
137
138
  `产品代码实现前必须具备通用实现契约:${IMPLEMENTATION_CONTRACT_FIELDS.map((field) => field.label).join("、")}。`,
138
139
  `涉及业务数据源时必须具备数据源上下文:${DATA_SOURCE_CONTEXT_FIELDS.map((field) => field.label).join("、")}。`,
@@ -142,9 +143,11 @@ export const PROJECT_PERSISTENT_RULES = [
142
143
  "`run.facts` 是唯一结构化事实源;已回答 questions 仅为审计记录,禁止在读取状态时从历史 question 反推事实。",
143
144
  "`factLabel` 必须使用集中定义的事实标签或别名;未知标签、占位答案、口头确认语、待确认/TODO/TBD/按实际环境等不能解除门禁。",
144
145
  "用户回答 open question 后,先用 `kd_question action=answer` 写入答案;用户更正事实时用 `kd_question action=revise`,禁止重复询问已确认事实。",
146
+ "门禁提示只暴露当前最阻塞缺失事实;后续待确认项只能在当前问题回答后重新刷新门禁逐项生成,禁止一次性向用户展开完整问题清单。",
145
147
  "企业版 Python 插件通常没有本地构建可替代验证;BOS 注册、外部系统操作、人工功能测试和生产环境验证必须由用户提供可核验证据并记录来源。",
146
148
  "提示语集中管理为正式工程指令;禁止口语化、闲聊式、鼓励式提示词进入运行时规则。",
147
149
  "工具使用必须匹配当前会话实际可用工具和操作系统;Windows 默认使用 PowerShell、`rg`、`Get-ChildItem`、`Get-Content`,禁止假设 bash 可用。",
150
+ "Windows 查找文件和目录时优先使用 `kd_find_file`、`kd_list_dir` 或 PowerShell;禁止用 Linux find/ls 语法反复调用 Pi 内置 bash。",
148
151
  "文件定位必须使用真实搜索或目录读取结果;工具不可用时明确说明阻塞原因和需要用户执行的命令,禁止猜测路径、反复自述工具失败或用 kd_subagent 代替基础文件搜索。",
149
152
  "不做旧状态迁移兼容或旧问题答案推断;坏状态只过滤,缺失事实必须重新提问确认。",
150
153
  ];
@@ -153,6 +156,44 @@ export const PROMPT_STYLE_RULES = [
153
156
  "事实不足时生成阻断问题;禁止输出模板代码、占位实现或基于猜测的业务标识。",
154
157
  "每次只提出一个最阻塞问题;问题必须指向可验证事实、数据标识或验收证据。",
155
158
  "引用顺序:当前项目文件、PLAN/SPEC、元数据 evidence、SDK 签名、验证输出。",
159
+ "响应必须说明当前阶段、门禁结论、采用的事实来源和唯一下一动作;禁止同时给出多个并行动作让用户选择。",
160
+ ];
161
+ export const PROMPT_DECISION_PROTOCOL = [
162
+ "先读取 Harness 状态、当前阶段资料、已问已答事实和项目上下文;禁止只根据用户最后一句话行动。",
163
+ "先判断用户输入是否是在回答 open blocking question;如是,唯一动作是调用 `kd_question action=answer` 记录答案。",
164
+ "再判断当前阶段门禁是否阻塞;阻塞时只执行门禁要求的唯一下一动作,禁止绕过阶段推进。",
165
+ "再判断是否缺少可核验事实;缺失时只登记当前最阻塞的一个问题,禁止展开后续问题。",
166
+ "只有在阶段、门禁、事实和证据均满足时,才允许进入计划、编码、验证或交付动作。",
167
+ ];
168
+ export const PROMPT_FACT_PROTOCOL = [
169
+ "`run.facts` 是结构化事实唯一来源;`questions` 仅为问答审计记录。",
170
+ "业务事实必须有来源分类:用户明确回答、项目元数据/evidence、当前项目文件、验证输出。",
171
+ "API/SDK 文档只作为技术用法线索;不得补全 FormId、字段标识、实体标识、表名、接口字段映射、并发策略或验收数据。",
172
+ "事实值包含待确认、未知、按实际环境、TODO/TBD、可以、好的、是/否等内容时,视为无效事实。",
173
+ "同一事实存在冲突时,停止推进并使用 `kd_question action=revise` 或登记一个最阻塞更正问题。",
174
+ ];
175
+ export const PROMPT_QUESTION_PROTOCOL = [
176
+ "任何时刻最多存在一个 open blocking question。",
177
+ "已有 open blocking question 时,禁止继续提问、禁止输出问题清单、禁止登记新问题;唯一动作是获取并记录该问题答案。",
178
+ "`kd_question action=ask` 的问题文本只能包含一个事实点,不能使用编号列表、多个问号或复合事实。",
179
+ "带 `factLabel` 的问题必须使用集中定义标签或别名;产品类型不是 factLabel。",
180
+ "问题登记后停止推进,等待用户回答;不得在同一轮继续生成方案、计划或代码。",
181
+ ];
182
+ export const PROMPT_OUTPUT_CONTRACT = [
183
+ "阻塞时输出:阻塞原因、唯一下一动作、需要使用的工具或命令;不得输出实现方案或代码片段。",
184
+ "提问时输出:只登记或展示一个问题;不得附带后续问题预告清单。",
185
+ "计划时输出:目标路径、允许修改文件、查证项、验证命令、回滚方式和 evidence 要求。",
186
+ "执行时输出:仅说明已修改文件、证据文件和验证结果;不得把未验证内容写成已完成。",
187
+ "验证失败时输出:失败证据、定位结论、下一轮修复边界;不得直接跳到 ship。",
188
+ ];
189
+ export const PROMPT_PROHIBITED_BEHAVIORS = [
190
+ "禁止根据 API 文档、SDK 记忆或知识库推断业务数据源。",
191
+ "禁止生成 demo/sample/scaffold、占位实现、伪代码或仅可手工替换的模板。",
192
+ "禁止一次性向用户索要多项业务信息。",
193
+ "禁止重复询问已确认事实或已存在 open blocking question 的问题。",
194
+ "禁止猜测路径、包名、类名、FormId、字段、实体、SQL 表名、数据库字段名或接口字段映射。",
195
+ "禁止用子 agent、shell 失败自述或自由文本计划绕过门禁。",
196
+ "禁止将外部系统操作、BOS 注册、人工功能测试或生产验证描述为 LLM 已完成。",
156
197
  ];
157
198
  export const CORE_WORKFLOW_CONSTRAINTS = [
158
199
  "产品代码只在 execute 阶段写入,并限于 PLAN.md 批准的文件。",
@@ -160,6 +201,7 @@ export const CORE_WORKFLOW_CONSTRAINTS = [
160
201
  "业务数据源未知时禁止编码;确认目标 FormId/单据或表单、插件类型和事件、字段/实体/分录标识、数据读取写入方式后再编码;SQL/KSQL 同步确认表名和数据库字段名。",
161
202
  "第三方对接确认接口文档、对接方向、触发时机、认证配置、字段映射、并发/幂等、重试超时限流、错误补偿、日志脱敏和验收样例后再编码。",
162
203
  "事实缺失时使用 kd_question 登记一个最阻塞问题;禁止用 API 文档、SDK 知识库或推测替代业务事实。",
204
+ "已有 open blocking question 时禁止继续提问;先记录该问题答案,再根据刷新后的门禁决定下一问。",
163
205
  "用户输入是在回答 open question 时,必须先调用 kd_question action=answer 记录答案,再继续推进或登记下一个问题。",
164
206
  "同一 factLabel 已有当前事实时禁止重复提问;用户明确更正时使用 kd_question action=revise 记录新事实和更正原因。",
165
207
  "run.facts 是唯一结构化事实源;questions 仅作为问答审计记录,禁止从历史 question 反推门禁事实。",
@@ -169,7 +211,7 @@ export const CORE_WORKFLOW_CONSTRAINTS = [
169
211
  "evidence 必须记录命令、Exit 和关键输出;命令无法运行时记录阻塞原因。",
170
212
  "外部系统操作、BOS 注册、人工功能测试和生产环境验证不能由 LLM 代办;必须要求用户提供验证结果或可核验证据,并记录证据来源。",
171
213
  "Windows 路径规则:项目相对路径为默认;绝对路径使用 D:\\... 形式。",
172
- "工具规则:按当前会话实际 shell 和工具执行;Windows 不假设 bash,可用 PowerShell/rg/Get-ChildItem/Get-Content;禁止猜路径或用 kd_subagent 代替基础搜索失败。",
214
+ "工具规则:按当前会话实际 shell 和工具执行;Windows 不假设 bash,优先用 kd_find_file/kd_list_dir/PowerShell;禁止猜路径或用 kd_subagent 代替基础搜索失败。",
173
215
  ];
174
216
  export const PHASE_GUIDANCE = {
175
217
  discuss: "梳理需求来源、范围、已知事实;如缺通用实现契约、数据源或第三方接口关键事实,使用 kd_question 登记一个最阻塞问题。",
@@ -195,6 +237,23 @@ export function formatPromptLines(lines) {
195
237
  export function fieldLabels(fields) {
196
238
  return fields.map((field) => field.label);
197
239
  }
240
+ export function allFactLabels() {
241
+ return fieldLabels([...IMPLEMENTATION_CONTRACT_FIELDS, ...DATA_SOURCE_CONTEXT_FIELDS, ...INTEGRATION_CONTEXT_FIELDS]);
242
+ }
243
+ export function formatFactLabelCatalog() {
244
+ return [
245
+ "## 实现契约事实标签",
246
+ ...formatPromptLines(fieldLabels(IMPLEMENTATION_CONTRACT_FIELDS)),
247
+ "",
248
+ "## 数据源上下文事实标签",
249
+ ...formatPromptLines(fieldLabels(DATA_SOURCE_CONTEXT_FIELDS)),
250
+ "",
251
+ "## 第三方对接事实标签",
252
+ ...formatPromptLines(fieldLabels(INTEGRATION_CONTEXT_FIELDS)),
253
+ "",
254
+ "产品类型/产品画像不是 factLabel;使用 /kd-product <产品> 或先登记无 factLabel 的产品确认问题。",
255
+ ].join("\n");
256
+ }
198
257
  export function questionForMissingLabel(label) {
199
258
  const fields = [...IMPLEMENTATION_CONTRACT_FIELDS, ...DATA_SOURCE_CONTEXT_FIELDS, ...INTEGRATION_CONTEXT_FIELDS];
200
259
  return fields.find((field) => field.label === label)?.question;
package/docs/CHANGELOG.md CHANGED
@@ -6,6 +6,53 @@
6
6
 
7
7
  - 暂无。
8
8
 
9
+ ## 0.1.42 - 2026-06-07
10
+
11
+ ### 改进
12
+
13
+ - 将运行时提示词工程化为集中协议:执行顺序、事实处理、提问、输出契约和禁止行为。
14
+ - 六个阶段入口 prompt 改为正式工程指令,统一要求读取状态、事实、门禁并只执行唯一下一动作。
15
+ - 核心 Harness skills 补充单问题门禁、事实来源、禁止模板代码、人工验证边界和企业版 Python 验证要求。
16
+
17
+ ### 验证
18
+
19
+ - `npm run check`
20
+ - `npm run smoke:harness`
21
+
22
+ ## 0.1.41 - 2026-06-07
23
+
24
+ ### 修复
25
+
26
+ - `kd_question` 增加全局 open blocking 门禁:已有未回答阻断问题时,禁止继续登记第二个问题。
27
+ - 数据源、实现契约和第三方对接门禁只输出当前最阻塞缺失事实和唯一允许的提问命令,不再把完整问题清单作为下一步展示给 LLM。
28
+ - 项目持久规则和命令文档明确:用户回答当前问题并记录后,才能刷新门禁生成下一问。
29
+
30
+ ### 验证
31
+
32
+ - `npm run check`
33
+ - `npm run smoke:harness`
34
+
35
+ ## 0.1.40 - 2026-06-07
36
+
37
+ ### 修复
38
+
39
+ - Windows 下覆盖 Pi 内置 `bash` 工具为 PowerShell 执行,避免未安装 Git Bash 时反复搜索 `C:\Program Files\Git\bin\bash.exe`。
40
+ - 新增 `kd_find_file` 和 `kd_list_dir`,用于不依赖 shell 的文件查找和目录探索。
41
+ - `kcode doctor --deep` 增加 Windows bash 状态诊断,并提示 KCode 的 PowerShell 覆盖和文件工具。
42
+ - 持久规则明确 Windows 文件定位优先使用 `kd_find_file`、`kd_list_dir` 或 PowerShell。
43
+ - `kd_question action=labels` 可列出合法 `factLabel`,未知标签错误会返回完整标签目录,避免 LLM 反复猜标签。
44
+ - `kd_question` 拒绝把产品类型和 FormId/字段/插件事件等多个事实塞进同一个 `factLabel`。
45
+
46
+ ### 验证
47
+
48
+ - `npm run check`
49
+ - `npm run smoke:harness`
50
+ - `npm run smoke:kcode-command`
51
+ - `npm run smoke:package`
52
+ - `npm run smoke:official`
53
+ - `npm run build:cli`
54
+ - `git diff --check`
55
+
9
56
  ## 0.1.39 - 2026-06-07
10
57
 
11
58
  ### 修复
@@ -310,7 +310,8 @@ kd_question action=revise factLabel="目标 FormId/单据或表单标识" answer
310
310
  kd_question action=list
311
311
  ```
312
312
 
313
- 一次只能登记一个当前最阻塞的问题,最多 3 个简短选项。`action=ask` 只登记 open 问题,不弹出输入框、不在工具调用内继续追问。
313
+ 一次只能登记一个当前最阻塞的问题,最多 3 个简短选项。已有任意 open blocking 问题时,`action=ask` 会拒绝登记新问题,必须先用 `action=answer` 记录当前问题答案。`action=ask` 只登记 open 问题,不弹出输入框、不在工具调用内继续追问。
314
+ 使用 `action=labels` 查看合法 `factLabel`。产品类型/产品画像不是 `factLabel`,应使用 `/kd-product <产品>` 或先登记无 `factLabel` 的产品确认问题。
314
315
 
315
316
  结构化事实参数:
316
317
 
@@ -321,7 +322,7 @@ proposedFactValue 可选,确认题候选事实值;用户回答 是/yes 时
321
322
 
322
323
  门禁只消费当前结构化事实。没有 `factLabel` 的问答只作为上下文记录,不直接解除数据源、字段、接口等事实缺口。
323
324
 
324
- `factLabel` 必须使用 KCode 集中定义的实现契约、数据源上下文或第三方对接事实标签,允许使用这些标签的别名并自动规范为正式标签。同一 `factLabel` 已有 open 问题时禁止重复提问;已有当前事实时禁止再次提问,必须使用 `action=revise` 显式修订。`待确认`、`未知`、`按实际环境`、`TODO/TBD`、单独“是/否”和“可以/好的”等口头确认不会解除开放事实问题。带 `proposedFactValue` 的确认题只接受明确肯定或明确否定。
325
+ `factLabel` 必须使用 KCode 集中定义的实现契约、数据源上下文或第三方对接事实标签,允许使用这些标签的别名并自动规范为正式标签。同一 `factLabel` 已有 open 问题时禁止重复提问;已有任意 open blocking 问题时也禁止继续提问;已有当前事实时禁止再次提问,必须使用 `action=revise` 显式修订。`待确认`、`未知`、`按实际环境`、`TODO/TBD`、单独“是/否”和“可以/好的”等口头确认不会解除开放事实问题。带 `proposedFactValue` 的确认题只接受明确肯定或明确否定。
325
326
 
326
327
  当前只有一个 open blocking 问题时,用户下一条普通短答会自动记录为该问题答案;命令、疑问句和明显的新任务不会自动记录。
327
328
 
@@ -108,11 +108,12 @@ evidence/index.json
108
108
 
109
109
  - 一次只问一个当前最阻塞的问题。
110
110
  - 问题登记后保持 open;用户回答后必须用 `/kd-answer` 或 `kd_question action=answer` 记录。
111
+ - 任意时刻最多只能有一个 open blocking 问题;该问题回答前,禁止登记、展示或继续追问其他问题。
111
112
  - 需要解除门禁事实缺口的问题必须带结构化 `factLabel`;确认题必须用 `proposedFactValue` 表示候选事实值。
112
113
  - 同一 `factLabel` 已有 open 问题或当前事实时禁止重复提问;用户更正事实时使用 `kd_question action=revise`,旧事实会标记为 superseded。
113
114
  - 当前只有一个 open blocking 问题时,用户下一条普通短答会自动记录;占位答案不会解除门禁。
114
115
  - 数据源 FormId、字段/实体、插件事件和读写方式必须来自结构化 facts 或 evidence;PLAN 自由文本不能单独证明这些事实。
115
- - 得到答案后再继续问下一个必要问题。
116
+ - 得到答案并刷新门禁后,再决定下一个必要问题;门禁提示不得一次性展开完整问题清单。
116
117
 
117
118
  ## 多个需求
118
119
 
@@ -133,3 +133,36 @@ ENOENT: no such file or directory, access 'D:\mnt\d\projects\xxx\src\main\java\F
133
133
  ```powershell
134
134
  kcode context --refresh
135
135
  ```
136
+
137
+ ## Windows 下 bash 工具一直搜索 Git Bash
138
+
139
+ 现象:
140
+
141
+ ```text
142
+ Searched Git Bash in:
143
+ C:\Program Files\Git\bin\bash.exe
144
+ C:\Program Files (x86)\Git\bin\bash.exe
145
+ ```
146
+
147
+ 原因是 Pi 内置 `bash` 工具在 Windows 上要求 Git Bash、Cygwin 或 MSYS2。KCode 在 Windows 下会覆盖该工具为 PowerShell,并提供 `kd_find_file`、`kd_list_dir` 用于文件查找和目录探索。
148
+
149
+ 处理:
150
+
151
+ ```powershell
152
+ kcode repair
153
+ kcode doctor --deep
154
+ kcode start
155
+ ```
156
+
157
+ 在 KCode 会话中查找文件时使用:
158
+
159
+ ```text
160
+ kd_find_file name=MesApiClient.cs root=C:\Users\Administrator\Desktop\lf-project
161
+ kd_list_dir path=C:\Users\Administrator\Desktop\lf-project
162
+ ```
163
+
164
+ 如果需要 Pi 原生 bash 行为,安装 Git for Windows 后重新运行:
165
+
166
+ ```powershell
167
+ kcode doctor --deep
168
+ ```
@@ -30,7 +30,8 @@ import { windowsPathHint } from "../src/platform/path.ts";
30
30
  import { repairPromptForRun, workflowPromptForRun } from "../src/harness/prompt.ts";
31
31
  import { recordVerifyResult, type VerifyResultOutcome } from "../src/harness/repair.ts";
32
32
  import { isSubagentChild, subagentRoleFromEnv, subagentToolCallBlockReason } from "../src/harness/delegation.ts";
33
- import { questionAskBlockReason } from "../src/harness/question-memory.ts";
33
+ import { questionAskBlockReason, questionFactLabelMismatchReason } from "../src/harness/question-memory.ts";
34
+ import { formatFactLabelCatalog } from "../src/harness/prompt-policy.ts";
34
35
 
35
36
  function requireRun(cwd: string): ReturnType<typeof readActiveRun> {
36
37
  return readActiveRun(cwd);
@@ -175,9 +176,9 @@ const kdQuestionTool = defineTool({
175
176
  name: "kd_question",
176
177
  label: "KD 问题",
177
178
  description:
178
- "创建、回答、修订或列出金蝶 Harness 结构化问题。每次必须只记录一个最阻塞的短问题。",
179
+ "创建、回答、修订、列出金蝶 Harness 结构化问题,或列出合法 factLabel。每次必须只记录一个最阻塞的短问题。",
179
180
  parameters: Type.Object({
180
- action: Type.Optional(Type.String({ description: "操作类型:ask、answer、revise 或 list,默认 ask。" })),
181
+ action: Type.Optional(Type.String({ description: "操作类型:ask、answer、revise、listlabels,默认 ask。" })),
181
182
  id: Type.Optional(Type.String({ description: "回答问题时的问题编号,例如 Q-001。" })),
182
183
  question: Type.Optional(Type.String({ description: "提问内容;必须是一个短问题。" })),
183
184
  answer: Type.Optional(Type.String({ description: "用户答案,action=answer 时使用。" })),
@@ -198,6 +199,9 @@ const kdQuestionTool = defineTool({
198
199
  }
199
200
 
200
201
  const action = (params.action ?? "ask").toLowerCase();
202
+ if (action === "labels") {
203
+ return { content: [{ type: "text", text: formatFactLabelCatalog() }], details: { action: "labels" } };
204
+ }
201
205
  if (action === "list") {
202
206
  const text = formatQuestions(run);
203
207
  return { content: [{ type: "text", text }], details: { questions: run.questions ?? [] } };
@@ -260,7 +264,7 @@ const kdQuestionTool = defineTool({
260
264
 
261
265
  if (action !== "ask") {
262
266
  return {
263
- content: [{ type: "text", text: `未知 kd_question action:${params.action}。有效值:ask、answer、revise、list。` }],
267
+ content: [{ type: "text", text: `未知 kd_question action:${params.action}。有效值:ask、answer、revise、list、labels。` }],
264
268
  details: { error: "unknown-action", action: params.action },
265
269
  };
266
270
  }
@@ -291,6 +295,13 @@ const kdQuestionTool = defineTool({
291
295
  details: { error: "batched-question" },
292
296
  };
293
297
  }
298
+ const mismatchReason = questionFactLabelMismatchReason({ question: params.question, factLabel: params.factLabel });
299
+ if (mismatchReason) {
300
+ return {
301
+ content: [{ type: "text", text: mismatchReason }],
302
+ details: { error: "fact-label-question-mismatch", factLabel: params.factLabel },
303
+ };
304
+ }
294
305
  const askBlockReason = questionAskBlockReason(run, { factLabel: params.factLabel });
295
306
  if (askBlockReason) {
296
307
  return {
@@ -1,6 +1,7 @@
1
1
  import { dirname, join, extname } from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { readFileSync, readFile as fsReadFile } from "node:fs";
4
+ import { spawn } from "node:child_process";
4
5
  import { Type } from "@earendil-works/pi-ai";
5
6
  import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
7
  import { formatSearchResults, formatTableSchema } from "../src/knowledge/format.ts";
@@ -25,6 +26,7 @@ import { resolveWorkspacePath } from "../src/platform/path.ts";
25
26
  import { advanceRunIfReady, readActiveRun } from "../src/harness/state.ts";
26
27
  import { writeEvidenceFile } from "../src/harness/evidence.ts";
27
28
  import { SDK_SIGNATURE_EVIDENCE } from "../src/harness/sdk-policy.ts";
29
+ import { findFiles, formatFileSearchResults, listDirectory } from "../src/tools/file-search.ts";
28
30
 
29
31
  const extensionDir = dirname(fileURLToPath(import.meta.url));
30
32
  const knowledgePath = join(extensionDir, "..", "knowledge");
@@ -479,6 +481,58 @@ const kdDocReadTool = defineTool({
479
481
  },
480
482
  });
481
483
 
484
+ const kdFindFileTool = defineTool({
485
+ name: "kd_find_file",
486
+ label: "KD 文件查找",
487
+ description: "在 Windows/PowerShell 环境下按文件名递归查找文件,不依赖 Pi 内置 bash。查找源码文件时优先使用此工具。",
488
+ parameters: Type.Object({
489
+ name: Type.String({ description: "文件名或通配符,例如 MesApiClient.cs 或 *.cs。" }),
490
+ root: Type.Optional(Type.String({ description: "搜索根目录;支持项目相对路径或 Windows 绝对路径。默认当前项目根。" })),
491
+ maxResults: Type.Optional(Type.Number({ description: "最大结果数,默认 50,最多 500。" })),
492
+ }),
493
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
494
+ try {
495
+ const results = findFiles(ctx.cwd, { root: params.root, name: params.name, maxResults: params.maxResults });
496
+ return { content: [{ type: "text", text: formatFileSearchResults(results) }], details: { count: results.length, root: params.root ?? ".", name: params.name } };
497
+ } catch (error) {
498
+ return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], details: { error: "find-file-failed" } };
499
+ }
500
+ },
501
+ });
502
+
503
+ const kdListDirTool = defineTool({
504
+ name: "kd_list_dir",
505
+ label: "KD 目录列表",
506
+ description: "列出目录内容,不读取目录为文件,不依赖 Pi 内置 bash。探索项目结构时优先使用此工具。",
507
+ parameters: Type.Object({
508
+ path: Type.Optional(Type.String({ description: "目录路径;支持项目相对路径或 Windows 绝对路径。默认当前项目根。" })),
509
+ maxEntries: Type.Optional(Type.Number({ description: "最大返回条目数,默认 50,最多 500。" })),
510
+ }),
511
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
512
+ try {
513
+ const results = listDirectory(ctx.cwd, { path: params.path, maxEntries: params.maxEntries });
514
+ return { content: [{ type: "text", text: formatFileSearchResults(results) }], details: { count: results.length, path: params.path ?? "." } };
515
+ } catch (error) {
516
+ return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], details: { error: "list-dir-failed" } };
517
+ }
518
+ },
519
+ });
520
+
521
+ const windowsPowerShellBashTool = defineTool({
522
+ name: "bash",
523
+ label: "PowerShell",
524
+ description:
525
+ "Windows KCode shell override. Execute a PowerShell command in the current working directory. Do not use Linux/bash syntax; use PowerShell commands such as Get-ChildItem, Select-String, Get-Content, and dotnet.",
526
+ promptSnippet: "Execute Windows PowerShell commands. Use PowerShell syntax, not bash syntax.",
527
+ parameters: Type.Object({
528
+ command: Type.String({ description: "PowerShell command to execute. Use PowerShell syntax, not bash syntax." }),
529
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds." })),
530
+ }),
531
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
532
+ return runPowerShell(params.command, ctx.cwd, params.timeout, signal);
533
+ },
534
+ });
535
+
482
536
  async function extractPdf(filePath: string, maxPages: number): Promise<string> {
483
537
  const { PDFParse } = await import("pdf-parse");
484
538
  const buffer = readFileSync(filePath);
@@ -558,6 +612,7 @@ function extractCsv(filePath: string, maxRows: number): string {
558
612
  }
559
613
 
560
614
  export default function (pi: ExtensionAPI) {
615
+ if (process.platform === "win32") pi.registerTool(windowsPowerShellBashTool);
561
616
  pi.registerTool(kdSearchTool);
562
617
  pi.registerTool(kdTableTool);
563
618
  pi.registerTool(kdCheckTool);
@@ -569,6 +624,8 @@ export default function (pi: ExtensionAPI) {
569
624
  pi.registerTool(kdBuildTool);
570
625
  pi.registerTool(kdDebugTool);
571
626
  pi.registerTool(kdDocReadTool);
627
+ pi.registerTool(kdFindFileTool);
628
+ pi.registerTool(kdListDirTool);
572
629
  }
573
630
 
574
631
  function writeSdkSignatureEvidence(cwd: string, content: string): string | undefined {
@@ -593,3 +650,47 @@ function writeSdkSignatureEvidence(cwd: string, content: string): string | undef
593
650
  { kind: "sdk-signature", command: "kd_sdk_signature", exitCode: 0 },
594
651
  );
595
652
  }
653
+
654
+ function runPowerShell(command: string, cwd: string, timeout: number | undefined, signal: AbortSignal | undefined): Promise<{ content: Array<{ type: "text"; text: string }>; details: Record<string, unknown> }> {
655
+ return new Promise((resolve) => {
656
+ const shell = process.env.ComSpec?.replace(/cmd\.exe$/i, "WindowsPowerShell\\v1.0\\powershell.exe") || "powershell.exe";
657
+ const child = spawn(shell, ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command], {
658
+ cwd,
659
+ windowsHide: true,
660
+ stdio: ["ignore", "pipe", "pipe"],
661
+ });
662
+ let stdout = "";
663
+ let stderr = "";
664
+ let timedOut = false;
665
+ const timeoutHandle =
666
+ timeout && timeout > 0
667
+ ? setTimeout(() => {
668
+ timedOut = true;
669
+ child.kill();
670
+ }, timeout * 1000)
671
+ : undefined;
672
+ const onAbort = () => child.kill();
673
+ if (signal) signal.addEventListener("abort", onAbort, { once: true });
674
+ child.stdout?.on("data", (data) => {
675
+ stdout += data.toString();
676
+ });
677
+ child.stderr?.on("data", (data) => {
678
+ stderr += data.toString();
679
+ });
680
+ child.on("error", (error) => {
681
+ if (timeoutHandle) clearTimeout(timeoutHandle);
682
+ if (signal) signal.removeEventListener("abort", onAbort);
683
+ resolve({ content: [{ type: "text", text: `PowerShell 启动失败:${error.message}` }], details: { shell: "powershell", error: "spawn-failed" } });
684
+ });
685
+ child.on("close", (exitCode) => {
686
+ if (timeoutHandle) clearTimeout(timeoutHandle);
687
+ if (signal) signal.removeEventListener("abort", onAbort);
688
+ const output = [stdout.trimEnd(), stderr.trimEnd()].filter(Boolean).join("\n");
689
+ const suffix = timedOut ? `\n\n[PowerShell timeout after ${timeout}s]` : "";
690
+ resolve({
691
+ content: [{ type: "text", text: `${output || "(no output)"}${suffix}` }],
692
+ details: { shell: "powershell", exitCode, timedOut, command },
693
+ });
694
+ });
695
+ });
696
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kcode-pi",
3
- "version": "0.1.39",
3
+ "version": "0.1.42",
4
4
  "description": "面向金蝶开发的 Pi Coding Agent 启动器、工具包和 Harness 工作流",
5
5
  "type": "module",
6
6
  "private": false,
@@ -4,13 +4,21 @@ description: 开始或继续金蝶 Harness 工作流的需求讨论阶段。
4
4
 
5
5
  使用 `kd-discuss` skill。
6
6
 
7
- 围绕本次请求开始或更新当前金蝶 Harness run:
7
+ 执行需求讨论阶段。输入:
8
8
 
9
9
  {{args}}
10
10
 
11
- 请先读取并理解需求来源,再自行判断这是单需求还是需求组。
11
+ 执行协议:
12
12
 
13
- - 单需求:记录业务目标、可判断的产品/版本、技术栈、目标对象、非目标范围和待确认问题。
14
- - 需求组:从原始文档抽取需求条目、验收标准、优先级、依赖、批次、共同约束和待确认问题。
13
+ - 读取当前 Harness 状态、项目上下文和已问已答事实。
14
+ - 判定输入是单需求还是需求组;依据必须来自用户输入、需求文档或项目文件。
15
+ - 单需求只记录业务目标、已确认产品/版本、技术栈、目标对象、非目标范围和一个最阻塞待确认问题。
16
+ - 需求组只抽取需求条目、验收标准、优先级、依赖、批次和共同约束;缺失事实按最阻塞项逐个确认。
17
+ - 产品画像只有在用户显式指定、文档证据充分或用户确认后才能落为已确认事实。
18
+ - 信息不足时只登记一个 open blocking question;禁止输出问题清单、实现方案或代码。
15
19
 
16
- 不要因为正文里出现产品词就直接落产品画像;只有用户显式指定、文档证据充分,或进入具体产品实现前确认后,才写成已确认产品。
20
+ 输出契约:
21
+
22
+ - 已更新的 `CONTEXT.md` 路径和摘要。
23
+ - 当前门禁结论。
24
+ - 唯一下一动作。
@@ -4,10 +4,22 @@ description: 在 Harness 门禁约束下执行当前金蝶实施计划。
4
4
 
5
5
  使用 `kd-execute` skill。
6
6
 
7
- 先检查当前 run 和门禁。通过后只实现 `PLAN.md` 批准的内容,更新 `EXECUTION.md`,并按计划记录红绿 evidence。
7
+ 执行实施阶段。输入:
8
8
 
9
- 实现后运行计划中的验证命令;命令无法运行时记录阻塞原因和残余风险。
9
+ {{args}}
10
10
 
11
- 用户补充说明:
11
+ 执行协议:
12
12
 
13
- {{args}}
13
+ - 读取当前 run、门禁、`PLAN.md`、已问已答事实和 evidence 状态。
14
+ - 门禁未通过时停止编码,只执行门禁指定的唯一下一动作。
15
+ - 只修改 `PLAN.md` 批准的文件;新增文件必须先回到 plan 批准。
16
+ - 写生产源码前必须已有红灯证据、真实数据源/元数据证据和 SDK 签名证据。
17
+ - 实现后更新 `EXECUTION.md`,记录步骤结果、变更文件、偏离计划说明和 evidence。
18
+ - 验证命令无法运行时记录真实阻塞原因、应运行命令和残余风险,不得声明验证通过。
19
+
20
+ 输出契约:
21
+
22
+ - 修改文件列表。
23
+ - evidence 路径和验证命令结果。
24
+ - 当前门禁结论。
25
+ - 唯一下一动作。
@@ -4,10 +4,20 @@ description: 为当前金蝶 Harness run 编写实施计划。
4
4
 
5
5
  使用 `kd-plan` skill。
6
6
 
7
- 读取 `CONTEXT.md` 和 `SPEC.md`,编写或更新 `PLAN.md`。重点写清:项目结构、目标路径、允许修改文件、SDK/元数据查证、验证命令和回滚说明。
7
+ 执行实施计划阶段。输入:
8
8
 
9
- 验证命令按产品选择:Cosmic Java 用当前项目 Gradle;企业版 C# 用 `dotnet build`。命令无法运行时记录阻塞原因和应运行的具体命令。
9
+ {{args}}
10
10
 
11
- 用户补充说明:
11
+ 执行协议:
12
12
 
13
- {{args}}
13
+ - 读取当前 run、`CONTEXT.md`、`SPEC.md`、已问已答事实和项目结构。
14
+ - `PLAN.md` 必须写明真实目标路径、允许修改文件、实现契约、数据源/元数据查证项、SDK 签名查证项、验证命令、回滚说明和 evidence 要求。
15
+ - 验证命令按产品选择:Cosmic Java 使用当前项目 Gradle;企业版 C# 使用 `dotnet build`;企业版 Python 记录 BOS 注册和用户功能测试证据要求。
16
+ - 命令无法运行时记录真实阻塞原因和用户应运行的具体命令。
17
+ - 缺少关键事实时只登记一个最阻塞问题;禁止写可替换模板计划。
18
+
19
+ 输出契约:
20
+
21
+ - 已更新的 `PLAN.md` 路径和摘要。
22
+ - 门禁结论。
23
+ - 唯一下一动作。
@@ -4,8 +4,19 @@ description: 验证后整理金蝶交付摘要。
4
4
 
5
5
  使用 `kd-ship` skill。
6
6
 
7
- 读取当前 run 的阶段文档,生成 `SHIP.md`,说明行为变化、验证证据、残余风险和后续事项。缺少 `VERIFY.md` 时不得进入交付总结。
8
-
9
- 用户补充说明:
7
+ 执行交付整理阶段。输入:
10
8
 
11
9
  {{args}}
10
+
11
+ 执行协议:
12
+
13
+ - 读取当前 run、`CONTEXT.md`、`SPEC.md`、`PLAN.md`、`EXECUTION.md`、`VERIFY.md`、evidence 和风险评估。
14
+ - 缺少 `VERIFY.md`、验证证据或风险说明时停止交付,只执行门禁指定的唯一下一动作。
15
+ - `SHIP.md` 必须说明行为变化、修改文件、验证证据、残余风险、人工验证边界和后续事项。
16
+ - 不得把未执行验证、外部系统操作或用户未提供的结果写成已完成。
17
+
18
+ 输出契约:
19
+
20
+ - 已更新的 `SHIP.md` 路径和摘要。
21
+ - 验证证据和风险结论。
22
+ - 唯一下一动作。
@@ -4,8 +4,20 @@ description: 为当前金蝶 Harness run 编写需求规格文档。
4
4
 
5
5
  使用 `kd-spec` skill。
6
6
 
7
- 读取当前金蝶 run 的上下文,编写或更新 `SPEC.md`。必须包含验收标准、生命周期/扩展点、数据对象、异常行为、性能约束、假设和风险。
8
-
9
- 用户补充说明:
7
+ 执行需求规格阶段。输入:
10
8
 
11
9
  {{args}}
10
+
11
+ 执行协议:
12
+
13
+ - 读取当前 run、`CONTEXT.md`、已问已答事实和门禁状态。
14
+ - 只把可核验事实写入 `SPEC.md`;未知事实写成阻塞项,不得用假设补齐。
15
+ - `SPEC.md` 必须包含验收标准、生命周期/扩展点、数据对象、字段/实体标识、异常行为、性能约束、依赖和风险。
16
+ - 缺少产品、FormId、字段/实体、插件事件、读写方式、接口契约或验收数据时,只登记一个最阻塞问题。
17
+ - 禁止输出代码、模板实现或多个待答问题。
18
+
19
+ 输出契约:
20
+
21
+ - 已更新的 `SPEC.md` 路径和摘要。
22
+ - 未解决阻塞项或门禁通过结论。
23
+ - 唯一下一动作。