@ghyper9023/pi-dev-workflow 0.6.0 → 0.6.2
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 +3 -1
- package/agents/workflow/planner-agent.md +2 -2
- package/agents/workflow/reviewer-agent.md +11 -2
- package/extensions/append-system.ts +36 -0
- package/extensions/workflow-engine.ts +95 -43
- package/package.json +1 -1
- package/prompts/APPEND_SYSTEM.md +53 -6
- package/tests/test-workflow-engine-bugs.mjs +155 -27
package/README.md
CHANGED
|
@@ -14,7 +14,8 @@ pi install git:github.com/cherish-ltt/pi-dev-workflow
|
|
|
14
14
|
|
|
15
15
|
然后 `/reload` 热加载即可使用所有功能。
|
|
16
16
|
> [!NOTE]
|
|
17
|
-
> 本pi-package存在sub-agent功能,会与其他使用sub-agent功能的pi-package冲突,但冲突可以修复,请提交issue或自行处理。
|
|
17
|
+
> - 本pi-package存在sub-agent功能,会与其他使用sub-agent功能的pi-package冲突,但冲突可以修复,请提交issue或自行处理。
|
|
18
|
+
> - 本pi-package添加了`APPEND_SYSTEM.md`,存在默认使用中文等特色设定,需要自行修改`prompts/APPEND_SYSTEM.md`。
|
|
18
19
|
|
|
19
20
|
## 目录结构
|
|
20
21
|
|
|
@@ -54,6 +55,7 @@ pi-package/
|
|
|
54
55
|
│ └── to-prd/
|
|
55
56
|
│ └── SKILL.md # 从对话上下文生成 PRD 文档
|
|
56
57
|
├── extensions/
|
|
58
|
+
│ ├── append-system.ts # 追加APPEND_SYSTEM.md提示词
|
|
57
59
|
│ ├── dev-prompts.ts # 提示词优化向导(/dev-* 命令)
|
|
58
60
|
│ ├── git-commands.ts # git-sub-agent 命令
|
|
59
61
|
│ ├── grill-me-agent.ts # Grill + PRD 运行时:方案追问完善、PRD 生成
|
|
@@ -51,7 +51,7 @@ extra-args:
|
|
|
51
51
|
|
|
52
52
|
请严格按照以下 Markdown 格式生成计划文件:
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
````markdown
|
|
55
55
|
# {功能名称} — 实施计划
|
|
56
56
|
|
|
57
57
|
## 概述
|
|
@@ -128,4 +128,4 @@ extra-args:
|
|
|
128
128
|
## ⚠️ 关键注意事项与风险防御
|
|
129
129
|
* **潜在风险点**:注意 `discountPrice` 为空时的默认回退机制,避免在生产环境引发 `NaN` 错误。
|
|
130
130
|
* **手动确认**:需确保上游网关已放行新字段,否则本地集成测试通过后线上也可能获取不到数据。
|
|
131
|
-
|
|
131
|
+
````
|
|
@@ -54,7 +54,7 @@ extra-args:
|
|
|
54
54
|
|
|
55
55
|
写入 `.pi-dev-output/pi-review/md/` 的文件必须采用以下格式:
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
````markdown
|
|
58
58
|
# 🔍 代码审查报告 — {功能/任务名称}
|
|
59
59
|
|
|
60
60
|
## 📊 审计摘要
|
|
@@ -64,7 +64,7 @@ extra-args:
|
|
|
64
64
|
|
|
65
65
|
| 🔴 严重 (Critical) | 🟡 中等 (Medium) | 🟢 低优先级 (Low) |
|
|
66
66
|
| :---: | :---: | :---: |
|
|
67
|
-
|
|
|
67
|
+
| 1 | 2 | 3 |
|
|
68
68
|
|
|
69
69
|
---
|
|
70
70
|
|
|
@@ -85,7 +85,16 @@ extra-args:
|
|
|
85
85
|
```
|
|
86
86
|
### [🟡 中等] 示例:`src/components/List.tsx` 重复渲染隐患
|
|
87
87
|
...
|
|
88
|
+
|
|
89
|
+
### [🟢 低优先级] 示例:`src/application/mod.rs` 未格式化
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
[REVIEW_SUMMARY]
|
|
94
|
+
{"maxSeverity":"critical","critical":1,"medium":2,"low":3}
|
|
95
|
+
[/REVIEW_SUMMARY]
|
|
88
96
|
```
|
|
97
|
+
````
|
|
89
98
|
|
|
90
99
|
---
|
|
91
100
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* APPEND_SYSTEM.md 注入扩展
|
|
3
|
+
*
|
|
4
|
+
* 在每次 agent 启动前,将 prompts/APPEND_SYSTEM.md 的内容追加到 system prompt 末尾。
|
|
5
|
+
* 这样用户安装此包后,自动生效,无需手动配置 ~/.pi/agent/APPEND_SYSTEM.md。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
|
|
12
|
+
export default function (pi: ExtensionAPI) {
|
|
13
|
+
// 缓存文件内容,避免每次读取磁盘
|
|
14
|
+
let cachedContent: string | null = null;
|
|
15
|
+
|
|
16
|
+
function getAppendContent(): string {
|
|
17
|
+
if (cachedContent !== null) return cachedContent;
|
|
18
|
+
try {
|
|
19
|
+
const appendPath = join(__dirname, "..", "prompts", "APPEND_SYSTEM.md");
|
|
20
|
+
cachedContent = readFileSync(appendPath, "utf-8");
|
|
21
|
+
} catch {
|
|
22
|
+
// 文件不存在时静默降级,不阻塞启动
|
|
23
|
+
cachedContent = "";
|
|
24
|
+
}
|
|
25
|
+
return cachedContent;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pi.on("before_agent_start", async (event, _ctx) => {
|
|
29
|
+
const content = getAppendContent();
|
|
30
|
+
if (!content) return; // 无可追加内容
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
systemPrompt: (event.systemPrompt || "") + "\n\n" + content,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -389,20 +389,16 @@ function getGitDiffChanges(cwd: string): GitFileChange[] {
|
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
/**
|
|
392
|
-
*
|
|
393
|
-
*
|
|
392
|
+
* 检查路径是否为工作流生成的产物路径(.pi-dev-output/),
|
|
393
|
+
* 若是则不应出现在 A: M: D: 文件变更列表中。
|
|
394
|
+
* 该目录已加入 .gitignore,git diff 本身已忽略,
|
|
395
|
+
* 此函数作为防御性过滤。
|
|
394
396
|
*/
|
|
395
|
-
function
|
|
396
|
-
|
|
397
|
-
case "new": case "write": case "create": case "add": case "created": case "added":
|
|
398
|
-
return "A";
|
|
399
|
-
case "delete": case "remove": case "deleted": case "removed":
|
|
400
|
-
return "D";
|
|
401
|
-
default:
|
|
402
|
-
return "M";
|
|
403
|
-
}
|
|
397
|
+
function isWorkflowArtifactPath(filePath: string): boolean {
|
|
398
|
+
return filePath.startsWith(".pi-dev-output/");
|
|
404
399
|
}
|
|
405
400
|
|
|
401
|
+
|
|
406
402
|
/**
|
|
407
403
|
* Check whether a file's current content hash differs from its baseline.
|
|
408
404
|
* Returns true if the hash changed or the file is no longer accessible.
|
|
@@ -453,6 +449,7 @@ function captureBaseline(cwd: string): void {
|
|
|
453
449
|
* loop-group, then reviewed by reviewer) without being collapsed into one.
|
|
454
450
|
* Without :stepIndex, the Set dedup would wrongly skip a file modified in
|
|
455
451
|
* a later step just because it was already tracked in an earlier step.
|
|
452
|
+
*
|
|
456
453
|
*/
|
|
457
454
|
function updateToolsFromGit(cwd: string, stepIndex: number, agentName: string): void {
|
|
458
455
|
const currentChanges = getGitDiffChanges(cwd);
|
|
@@ -480,7 +477,7 @@ function updateToolsFromGit(cwd: string, stepIndex: number, agentName: string):
|
|
|
480
477
|
// and should NOT be reported as user repo changes. Real git clients (VSCode, Zed) also
|
|
481
478
|
// ignore this directory (it's in .gitignore). This filter is defense-in-depth.
|
|
482
479
|
// Note: git always uses forward slashes in paths, even on Windows.
|
|
483
|
-
if (change.path
|
|
480
|
+
if (isWorkflowArtifactPath(change.path)) continue;
|
|
484
481
|
|
|
485
482
|
seen.add(`${change.path}:${stepIndex}`);
|
|
486
483
|
const type: FileChangeEntry["type"] =
|
|
@@ -1094,35 +1091,81 @@ async function runAgentWithProgress(
|
|
|
1094
1091
|
}
|
|
1095
1092
|
}
|
|
1096
1093
|
|
|
1097
|
-
//
|
|
1098
|
-
//
|
|
1099
|
-
//
|
|
1100
|
-
//
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1094
|
+
// ── Start git diff polling for real file changes ──
|
|
1095
|
+
// This replaces the unreliable regex-based tool call sniffing that was removed.
|
|
1096
|
+
// git diff --name-status is the same approach used by VSCode, Zed, and every
|
|
1097
|
+
// professional git client — it's deterministic, noise-free, and 100% accurate.
|
|
1098
|
+
let _gitPollTimer: ReturnType<typeof setInterval> | null = null;
|
|
1099
|
+
const _pollSeen = new Set<string>();
|
|
1100
|
+
if (_workflowCwd) {
|
|
1101
|
+
_gitPollTimer = setInterval(() => {
|
|
1102
|
+
try {
|
|
1103
|
+
const currentChanges = getGitDiffChanges(_workflowCwd);
|
|
1104
|
+
for (const change of currentChanges) {
|
|
1105
|
+
const key = `${change.path}:${stepIndex}`;
|
|
1106
|
+
if (_pollSeen.has(key)) continue;
|
|
1107
|
+
_pollSeen.add(key);
|
|
1108
|
+
|
|
1109
|
+
if (isWorkflowArtifactPath(change.path)) continue;
|
|
1110
|
+
if (_workflowBaseline.length > 0) {
|
|
1111
|
+
const baseEntry = _workflowBaseline.find(b => b.path === change.path);
|
|
1112
|
+
if (baseEntry) {
|
|
1113
|
+
if (!hasContentChanged(_workflowCwd, change.path, baseEntry.hash)) continue;
|
|
1114
|
+
// ⭐ 同步删除已变更的基线条目,与 updateToolsFromGit 行为一致
|
|
1115
|
+
_workflowBaseline = _workflowBaseline.filter(b => b.path !== change.path);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// ⭐ Also record in _workflowFileChanges so the completion report file tree
|
|
1120
|
+
// and revertStepChanges have accurate data. The final updateToolsFromGit call
|
|
1121
|
+
// will skip this via seen, so we must record it here.
|
|
1122
|
+
const type: FileChangeEntry["type"] =
|
|
1123
|
+
change.status === "A" ? "new" :
|
|
1124
|
+
change.status === "D" ? "delete" :
|
|
1125
|
+
"edit";
|
|
1126
|
+
_workflowFileChanges.push({
|
|
1127
|
+
agent: agentName,
|
|
1128
|
+
stepIndex,
|
|
1129
|
+
type,
|
|
1130
|
+
filePath: change.path,
|
|
1131
|
+
timestamp: new Date().toISOString(),
|
|
1132
|
+
});
|
|
1133
|
+
addWidgetSubStepTool(stepIndex, agentName, `${change.status} ${change.path}`);
|
|
1134
|
+
_widgetExtraToolCount++;
|
|
1135
|
+
}
|
|
1136
|
+
} catch { /* 静默忽略 git 错误 */ }
|
|
1137
|
+
}, 5000);
|
|
1138
|
+
_gitPollTimer.unref();
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Run the sub-agent with progress reporting
|
|
1142
|
+
// The toolMatch regex sniffing has been removed — it was the primary source of
|
|
1143
|
+
// false positives in the A: M: D: panel (matching AI natural language as tool calls).
|
|
1144
|
+
// File change detection is now handled entirely by git diff polling (above) and
|
|
1145
|
+
// the final updateToolsFromGit call (below), both of which use git's own API.
|
|
1146
|
+
let result;
|
|
1147
|
+
try {
|
|
1148
|
+
result = await spawnSubagent(agent, task, _workflowCwd, signal, timeoutMs, (progress) => {
|
|
1149
|
+
// Detect output file paths — only match .pi-dev-output paths belonging to current workflow
|
|
1150
|
+
if (_workflowId) {
|
|
1151
|
+
const outputMatch = progress.match(/\.pi-dev-output\/[a-zA-Z0-9_\/\.-]+/i);
|
|
1152
|
+
if (outputMatch) {
|
|
1153
|
+
const pathCandidate = outputMatch[0]!.trim();
|
|
1154
|
+
// ⭐ 严格白名单:拒绝中文、引号、括号等非路径字符
|
|
1155
|
+
if (pathCandidate.length > 15 && pathCandidate.length < 300 &&
|
|
1156
|
+
pathCandidate.includes(_workflowId) &&
|
|
1157
|
+
/^[\w.\/-]+$/.test(pathCandidate)) {
|
|
1158
|
+
addWidgetSubStepOutput(stepIndex, agentName, pathCandidate);
|
|
1159
|
+
}
|
|
1122
1160
|
}
|
|
1123
1161
|
}
|
|
1162
|
+
}, _workflowId ? { workflowId: _workflowId } : undefined);
|
|
1163
|
+
} finally {
|
|
1164
|
+
if (_gitPollTimer) {
|
|
1165
|
+
clearInterval(_gitPollTimer);
|
|
1166
|
+
_gitPollTimer = null;
|
|
1124
1167
|
}
|
|
1125
|
-
}
|
|
1168
|
+
}
|
|
1126
1169
|
|
|
1127
1170
|
const agentDuration = Date.now() - agentStartTime;
|
|
1128
1171
|
|
|
@@ -1142,23 +1185,30 @@ async function runAgentWithProgress(
|
|
|
1142
1185
|
// from code snippets). Instead, we rely entirely on git diff --name-status, which is
|
|
1143
1186
|
// the same approach used by VSCode, Zed, and every other professional git client.
|
|
1144
1187
|
// It's deterministic, noise-free, and always produces correct relative paths.
|
|
1145
|
-
const allOutput = (result.output || "") + "\n" + (result.stderr || "");
|
|
1146
1188
|
const finalOutput = extractFinalOutput(result.output) || result.output;
|
|
1147
|
-
const searchText = allOutput + "\n" + finalOutput;
|
|
1148
1189
|
|
|
1149
1190
|
// Find output file paths (plans, reviews) for display in the "output:" section
|
|
1150
1191
|
// These are workflow artifacts (.pi-dev-output/) — only show files belonging to
|
|
1151
1192
|
// the CURRENT workflow to avoid cross-contamination from previous runs.
|
|
1152
1193
|
// Note: file change detection itself relies exclusively on git diff below, not text parsing.
|
|
1194
|
+
// ⭐ IMPORTANT: Only search in CLEAN extracted text (finalOutput), NOT raw JSON output.
|
|
1195
|
+
// The old approach concatenated allOutput (raw JSON + stderr) into searchText and
|
|
1196
|
+
// matched against it, producing false positives from JSON stringified content.
|
|
1197
|
+
// By restricting to extractFinalOutput's clean text, we avoid matching against
|
|
1198
|
+
// JSON-escaped paths, code snippets, or natural language in raw agent output.
|
|
1199
|
+
const cleanText = finalOutput || result.output || "";
|
|
1153
1200
|
const seenOutputs = new Set<string>();
|
|
1154
1201
|
if (_workflowId) {
|
|
1155
1202
|
// Only match .pi-dev-output paths containing the current workflow UUID
|
|
1156
1203
|
const escapedId = _workflowId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1157
|
-
const outputPattern = new RegExp(`\\.pi-dev-output\\/[
|
|
1204
|
+
const outputPattern = new RegExp(`\\.pi-dev-output\\/[a-zA-Z0-9_\\/.-]*${escapedId}[a-zA-Z0-9_\\/.-]*`, 'g');
|
|
1158
1205
|
let m;
|
|
1159
|
-
while ((m = outputPattern.exec(
|
|
1206
|
+
while ((m = outputPattern.exec(cleanText)) !== null) {
|
|
1160
1207
|
const path_ = m[0]!.trim();
|
|
1161
1208
|
if (path_.length > 15 && path_.length < 300 && !seenOutputs.has(path_)) {
|
|
1209
|
+
// ⭐ 严格白名单:仅允许 [a-zA-Z0-9_./-] 字符
|
|
1210
|
+
// 拒绝包含中文、引号、括号、空格等非路径字符的乱码匹配
|
|
1211
|
+
if (!/^[\w.\/-]+$/.test(path_)) continue;
|
|
1162
1212
|
seenOutputs.add(path_);
|
|
1163
1213
|
addWidgetSubStepOutput(stepIndex, agentName, path_);
|
|
1164
1214
|
}
|
|
@@ -1167,9 +1217,11 @@ async function runAgentWithProgress(
|
|
|
1167
1217
|
// No workflow ID (unlikely): match .pi-dev-output paths conservatively
|
|
1168
1218
|
const outputPattern = /\.pi-dev-output\/[a-zA-Z0-9_\/-]+\.[a-zA-Z0-9]+/g;
|
|
1169
1219
|
let m;
|
|
1170
|
-
while ((m = outputPattern.exec(
|
|
1220
|
+
while ((m = outputPattern.exec(cleanText)) !== null) {
|
|
1171
1221
|
const path_ = m[0]!.trim();
|
|
1172
1222
|
if (path_.length > 15 && path_.length < 300 && !seenOutputs.has(path_)) {
|
|
1223
|
+
// ⭐ 严格白名单
|
|
1224
|
+
if (!/^[\w.\/-]+$/.test(path_)) continue;
|
|
1173
1225
|
seenOutputs.add(path_);
|
|
1174
1226
|
addWidgetSubStepOutput(stepIndex, agentName, path_);
|
|
1175
1227
|
}
|
package/package.json
CHANGED
package/prompts/APPEND_SYSTEM.md
CHANGED
|
@@ -1,12 +1,59 @@
|
|
|
1
|
-
|
|
1
|
+
# 必须严格遵守 - 全局适用
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## 一、职责定义
|
|
4
4
|
|
|
5
|
-
-
|
|
5
|
+
- **技术准确优先于迎合用户信念**。专注事实和解决问题,提供直接、客观的技术信息,避免不必要的夸张、赞美或情感认同。
|
|
6
|
+
- 避免过度认同(如“你说得完全对”)。
|
|
7
|
+
- **绝不给出时间预估**(不说“几分钟”“大约5分钟”“2-3周,我们以后在做”等)。
|
|
8
|
+
- **默认不使用 emoji**,除非用户明确要求。
|
|
9
|
+
- 输出在命令行显示,回答应**简短精炼**。
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
## 二、核心行为约束
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
### 1. 不要多管闲事
|
|
14
|
+
- 不添加未要求的功能、不重构、不做“改进”。修复一个 bug 不需要清理周边代码。
|
|
15
|
+
|
|
16
|
+
### 2. 不要过度设计
|
|
17
|
+
- 不为不可能发生的场景添加错误处理、回退或校验。信任内部代码和框架保证。只在系统边界(用户输入、外部API)校验。
|
|
18
|
+
|
|
19
|
+
### 3. 不要提前抽象
|
|
20
|
+
- 不为一次性操作创建 helper、utility 或抽象层。不为假想的未来需求设计。**三行重复代码也好过一个过早出现的抽象**。
|
|
21
|
+
|
|
22
|
+
### 4. 不要乱建文件
|
|
23
|
+
- 除非绝对必要,**不创建新文件**。始终优先修改现有文件。
|
|
24
|
+
- **绝不主动创建文档文件(\*.md)或 README**。
|
|
25
|
+
|
|
26
|
+
### 5. 不改没读过的代码
|
|
27
|
+
- 绝不提议修改没读过的代码。必须先阅读和理解再修改。
|
|
28
|
+
|
|
29
|
+
### 6. 默认不写注释
|
|
30
|
+
- 只在“为什么这样做”不显然时写注释(隐藏约束、微妙不变量、特定 bug 规避)。
|
|
31
|
+
- 不解释代码“做了什么”——用良好命名的标识符表达。
|
|
32
|
+
- 不在注释里引用当前任务、修复内容或调用方。
|
|
33
|
+
- 不删除现有注释,除非删除其描述的代码。
|
|
34
|
+
|
|
35
|
+
### 7. 如实汇报,不粉饰
|
|
36
|
+
- 测试失败就说失败,带上相关输出。
|
|
37
|
+
- 若未运行验证步骤,明确说明,不暗示成功。
|
|
38
|
+
- 输出显示失败时,绝不声称“所有测试通过”。
|
|
39
|
+
- 不压制或简化失败检查来制造表面绿色。
|
|
40
|
+
- 不把未完成或损坏的工作说成已完成。
|
|
41
|
+
|
|
42
|
+
## 三、动态语言配置
|
|
43
|
+
|
|
44
|
+
- **始终使用用户(user-prompt)的语言回复**。所有解释、注释、与用户的交流都使用该语言。
|
|
45
|
+
- 技术术语和代码标识符保持原样。
|
|
46
|
+
- 未能清楚了解到用户(user-prompt)的语言时,默认使用简体中文+英文专业名词
|
|
47
|
+
|
|
48
|
+
## 四、task完成
|
|
49
|
+
|
|
50
|
+
- 默认情况下,使用简洁的语言总结任务
|
|
51
|
+
- 如果用户有具体的要求,在任务完后按用户要求输出内容
|
|
52
|
+
- 在完成task后绝不主动commit或push,但可以在任务总结末尾提醒用户(非处于子代理时):
|
|
10
53
|
1. review刚才的改动(输入/skill:review-html或1)
|
|
11
54
|
2. 提交commit,自动message(输入/git-commit或2)
|
|
12
|
-
3. 提交commit,自动message,并push(输入/git-commit-push或3)
|
|
55
|
+
3. 提交commit,自动message,并push(输入/git-commit-push或3)
|
|
56
|
+
|
|
57
|
+
## 五、核心理念
|
|
58
|
+
|
|
59
|
+
> **高效达成请求结果,不过度工程。无范围蔓延。只实现直接要求的内容。无假想设计,无必要抽象,默认无注释。不为不可能条件加校验。无时间预估。无 emoji。无过度认同。准确、客观、简洁。始终牢记自己的身份和职责。**
|
|
@@ -502,12 +502,13 @@ assertTrue(hasWorkSummaryKey, "executeSingleStep 中保留 '工作总结' 条目
|
|
|
502
502
|
const hasWorkSummaryUpdate = fullSingleStep.includes('updateChainContext');
|
|
503
503
|
assertTrue(hasWorkSummaryUpdate, "executeSingleStep 中仍有 updateChainContext 调用(工作总结)");
|
|
504
504
|
|
|
505
|
-
// Verify the removed summary keys are no longer present
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
505
|
+
// Verify the removed summary keys are no longer present in updateChainContext calls
|
|
506
|
+
// (comments may still reference them for historical context — that's fine)
|
|
507
|
+
const chainCtxCalls = [...fullSingleStep.matchAll(/updateChainContext\(`([^`]+)`/g)].map(m => m[1]);
|
|
508
|
+
const plannerInCode = chainCtxCalls.some(k => k.includes('计划制定摘要'));
|
|
509
|
+
assertFalse(plannerInCode, "updateChainContext 中已不使用 '计划制定摘要' key");
|
|
510
|
+
const docWriterInCode = chainCtxCalls.some(k => k.includes('文档更新摘要'));
|
|
511
|
+
assertFalse(docWriterInCode, "updateChainContext 中已不使用 '文档更新摘要' key");
|
|
511
512
|
|
|
512
513
|
|
|
513
514
|
console.log("\n═══ Bug H 测试 — Agent 前置元数据解析 ═══\n");
|
|
@@ -654,20 +655,25 @@ console.log("\n");
|
|
|
654
655
|
|
|
655
656
|
console.log("═══ Bug J 测试 — _workflowFileChanges 仅来自 git diff ═══\n");
|
|
656
657
|
|
|
657
|
-
// ── Test J1: _workflowFileChanges.push
|
|
658
|
-
console.log("📋 测试 J1: _workflowFileChanges.push
|
|
658
|
+
// ── Test J1: _workflowFileChanges.push 出现在 updateToolsFromGit 和 git diff 轮询 ──
|
|
659
|
+
console.log("📋 测试 J1: _workflowFileChanges.push 出现在 updateToolsFromGit 和 git diff 轮询\n");
|
|
659
660
|
|
|
660
661
|
// Count all _workflowFileChanges.push occurrences
|
|
662
|
+
// There should be 2: one in git diff polling (runAgentWithProgress) and one in updateToolsFromGit
|
|
661
663
|
const pushMatches = [...source.matchAll(/_workflowFileChanges\.push\(/g)];
|
|
662
|
-
assertEq(pushMatches.length,
|
|
664
|
+
assertEq(pushMatches.length, 2, "_workflowFileChanges.push 出现 2 次(轮询 + updateToolsFromGit)");
|
|
663
665
|
|
|
664
|
-
// Verify the
|
|
665
|
-
// the function body of updateToolsFromGit contains the push.
|
|
666
|
+
// Verify the push is inside updateToolsFromGit
|
|
666
667
|
const updateToolsStart = source.indexOf("function updateToolsFromGit");
|
|
667
668
|
const nextFuncStart = source.indexOf("function saveCheckpoint", updateToolsStart);
|
|
668
669
|
const updateToolsBody = source.slice(updateToolsStart, nextFuncStart);
|
|
669
|
-
|
|
670
|
-
|
|
670
|
+
assertTrue(updateToolsBody.includes("_workflowFileChanges.push"), "updateToolsFromGit 函数体内包含 _workflowFileChanges.push");
|
|
671
|
+
|
|
672
|
+
// Verify the push is also inside the git diff polling (setInterval) in runAgentWithProgress
|
|
673
|
+
const runAgentStart = source.indexOf("async function runAgentWithProgress");
|
|
674
|
+
const runAgentEnd = source.indexOf("async function executeSingleStep", runAgentStart);
|
|
675
|
+
const runAgentBody = source.slice(runAgentStart, runAgentEnd);
|
|
676
|
+
assertTrue(runAgentBody.includes("_workflowFileChanges.push"), "git diff 轮询(setInterval)中包含 _workflowFileChanges.push");
|
|
671
677
|
|
|
672
678
|
// ── Test J2: addWidgetSubStepTool 不再污染 _workflowFileChanges ──
|
|
673
679
|
console.log("\n📋 测试 J2: addWidgetSubStepTool 不再污染 _workflowFileChanges\n");
|
|
@@ -677,7 +683,7 @@ const addWidgetFunc = source.slice(
|
|
|
677
683
|
source.indexOf("function addWidgetSubStepOutput")
|
|
678
684
|
);
|
|
679
685
|
const hasNoFileChangesRef = !addWidgetFunc.includes("_workflowFileChanges");
|
|
680
|
-
assertTrue(hasNoFileChangesRef, "addWidgetSubStepTool
|
|
686
|
+
assertTrue(hasNoFileChangesRef, "addWidgetSubStepTool 函数体中不再引用 _workflowFileChanges");
|
|
681
687
|
|
|
682
688
|
// ── Test J3: executeSingleStep 中不再有基于 _workflowFileChanges 的统计 ──
|
|
683
689
|
console.log("\n📋 测试 J3: executeSingleStep 中不再有 '执行摘要' 类 chain context\n");
|
|
@@ -694,8 +700,11 @@ const loopGroupBody = source.slice(loopGroupStart);
|
|
|
694
700
|
const hasNoCodeImplSummary = !loopGroupBody.includes('"代码实施摘要"') && !loopGroupBody.includes('"代码精简摘要"');
|
|
695
701
|
assertTrue(hasNoCodeImplSummary, "executeLoopGroup 中不再有 '代码实施摘要' 或 '代码精简摘要' chain context");
|
|
696
702
|
|
|
697
|
-
|
|
698
|
-
|
|
703
|
+
// Verify the removed review feedback keys are no longer present in updateChainContext calls
|
|
704
|
+
// (comments may still reference them for historical context — that's fine)
|
|
705
|
+
const chainCtxCallsLoop = [...loopGroupBody.matchAll(/updateChainContext\(`([^`]+)`/g)].map(m => m[1]);
|
|
706
|
+
const reviewInCode = chainCtxCallsLoop.some(k => k.includes('代码审查反馈') || k.includes('精简审查反馈'));
|
|
707
|
+
assertFalse(reviewInCode, "updateChainContext 中已不使用 '代码审查反馈' 或 '精简审查反馈' key");
|
|
699
708
|
|
|
700
709
|
// ── Test J5: 工作总结条目在所有 agent 函数中仍保留 ──
|
|
701
710
|
console.log("\n📋 测试 J5: '工作总结' 条目在所有 agent 函数中仍保留\n");
|
|
@@ -733,11 +742,14 @@ console.log("\n📋 测试 K3: outputPathPatterns 已简化\n");
|
|
|
733
742
|
const hasOutputPathPatterns = source.includes("const outputPathPatterns = [");
|
|
734
743
|
assertFalse(hasOutputPathPatterns, "outputPathPatterns 数组已移除(替换为 inline workflowId 过滤)");
|
|
735
744
|
|
|
736
|
-
// ── Test K4: updateToolsFromGit 有
|
|
737
|
-
console.log("\n📋 测试 K4: updateToolsFromGit
|
|
745
|
+
// ── Test K4: updateToolsFromGit 有 isWorkflowArtifactPath 过滤 ──
|
|
746
|
+
console.log("\n📋 测试 K4: updateToolsFromGit 使用 isWorkflowArtifactPath 过滤\n");
|
|
747
|
+
|
|
748
|
+
const hasIsWorkflowArtifactPath = source.includes('isWorkflowArtifactPath(change.path)');
|
|
749
|
+
assertTrue(hasIsWorkflowArtifactPath, "updateToolsFromGit 使用 isWorkflowArtifactPath 跳过 .pi-dev-output/ 路径");
|
|
738
750
|
|
|
739
|
-
const
|
|
740
|
-
assertTrue(
|
|
751
|
+
const hasIsWorkflowArtifactFunc = source.includes('function isWorkflowArtifactPath');
|
|
752
|
+
assertTrue(hasIsWorkflowArtifactFunc, "isWorkflowArtifactPath 辅助函数存在");
|
|
741
753
|
|
|
742
754
|
// ── Test K5: output 路径匹配使用 escapedId + workflowId ──
|
|
743
755
|
console.log("\n📋 测试 K5: output 路径使用 workflowId 过滤\n");
|
|
@@ -751,15 +763,19 @@ console.log("\n📋 测试 K6: progress handler 使用 workflowId\n");
|
|
|
751
763
|
const hasIncludesWorkflowId = source.includes("pathCandidate.includes(_workflowId)");
|
|
752
764
|
assertTrue(hasIncludesWorkflowId, "progress handler 使用 pathCandidate.includes(_workflowId)");
|
|
753
765
|
|
|
754
|
-
// ── Test K7: addWidgetSubStepTool 只来自 git diff
|
|
755
|
-
console.log("\n📋 测试 K7: addWidgetSubStepTool
|
|
766
|
+
// ── Test K7: addWidgetSubStepTool 只来自 git diff(regex 嗅探已移除)──
|
|
767
|
+
console.log("\n📋 测试 K7: addWidgetSubStepTool 仅来自 git diff\n");
|
|
756
768
|
|
|
757
|
-
// Verify the
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
769
|
+
// Verify the toolMatch regex sniffing has been removed (source 1)
|
|
770
|
+
const hasToolMatchRegex = source.includes('const toolMatch = progress.match(/(edit|read|write|new|bash|grep|find|ls|delete|remove)');
|
|
771
|
+
assertFalse(hasToolMatchRegex, "progress handler 中已移除 toolMatch 正则 (source 1)");
|
|
772
|
+
|
|
773
|
+
// Verify the updateToolsFromGit call site still exists
|
|
761
774
|
assertTrue(source.includes("addWidgetSubStepTool(stepIndex, agentName, `${change.status} ${change.path}`)"), "updateToolsFromGit 调用 addWidgetSubStepTool");
|
|
762
|
-
|
|
775
|
+
|
|
776
|
+
// Verify git diff polling timer exists (replacement for regex sniffing)
|
|
777
|
+
const hasGitPollTimer = source.includes('_gitPollTimer = setInterval');
|
|
778
|
+
assertTrue(hasGitPollTimer, "git diff 轮询定时器已添加");
|
|
763
779
|
|
|
764
780
|
// ── Test K8: addWidgetSubStepOutput 仍存在 (output 路径展示有用) ──
|
|
765
781
|
console.log("\n📋 测试 K8: addWidgetSubStepOutput 仍保留\n");
|
|
@@ -769,6 +785,118 @@ assertTrue(hasAddWidgetOutput, "addWidgetSubStepOutput 函数声明仍保留");
|
|
|
769
785
|
|
|
770
786
|
|
|
771
787
|
|
|
788
|
+
console.log("\n═══ Bug L 测试 — output 路径白名单验证 ═══\n");
|
|
789
|
+
|
|
790
|
+
// ── Test L1: output 路径白名单验证 ──
|
|
791
|
+
console.log("\n📋 测试 L1: output 路径白名单逻辑\n");
|
|
792
|
+
|
|
793
|
+
function isValidOutputPath(path) {
|
|
794
|
+
return /^[\w.\/-]+$/.test(path) &&
|
|
795
|
+
path.length > 15 && path.length < 300 &&
|
|
796
|
+
path.includes("019e57e7");
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// 应接受的合法路径
|
|
800
|
+
assertTrue(isValidOutputPath(".pi-dev-output/pi-plans/20260524-test-019e57e7.md"), "合法路径应被接受");
|
|
801
|
+
assertTrue(isValidOutputPath(".pi-dev-output/pi-review/md/review-20260524-019e57e7.md"), "review 路径应被接受");
|
|
802
|
+
|
|
803
|
+
// 应拒绝的乱码路径
|
|
804
|
+
assertFalse(isValidOutputPath(".pi-dev-output/pi-plans/中grepUUID\"019e57e7\"找到)"), "含中文路径应被拒绝");
|
|
805
|
+
assertFalse(isValidOutputPath(".pi-dev-output/pi-plans/(测试)019e57e7.md"), "含括号路径应被拒绝");
|
|
806
|
+
|
|
807
|
+
// ── Test L2: post-completion output 检测仅搜索 clean text ──
|
|
808
|
+
console.log("\n📋 测试 L2: output 检测仅搜索 clean text(source 2 修复验证)\n");
|
|
809
|
+
|
|
810
|
+
// 验证源代码中 outputPattern.exec 搜索的是 cleanText 而非 searchText
|
|
811
|
+
const hasSearchTextOutput = source.includes('outputPattern.exec(searchText)');
|
|
812
|
+
assertFalse(hasSearchTextOutput, "output 检测不再搜索 searchText (raw JSON)");
|
|
813
|
+
|
|
814
|
+
// 验证替换为 cleanText
|
|
815
|
+
const hasCleanTextOutput = source.includes('outputPattern.exec(cleanText)');
|
|
816
|
+
assertTrue(hasCleanTextOutput, "output 检测搜索 cleanText (extracted text)");
|
|
817
|
+
|
|
818
|
+
// ── Test L3: post-completion output 有白名单验证 ──
|
|
819
|
+
console.log("\n📋 测试 L3: post-completion output 有白名单验证\n");
|
|
820
|
+
|
|
821
|
+
const hasWhitelistCheck = source.includes('/^[\\w.\\/-]+\$/.test(path_)');
|
|
822
|
+
assertTrue(hasWhitelistCheck, "post-completion output 检测包含 /^[\\w.\\/-]+\$/ 白名单验证");
|
|
823
|
+
|
|
824
|
+
// ── Test L4: progress handler output 检测使用 whitelist regex ──
|
|
825
|
+
console.log("\n📋 测试 L4: progress handler output 检测使用 whitelist regex\n");
|
|
826
|
+
|
|
827
|
+
const hasProgressWhitelist = source.includes('.pi-dev-output\\/[a-zA-Z0-9_\\/\\.-]+');
|
|
828
|
+
assertTrue(hasProgressWhitelist, "progress handler 使用 [a-zA-Z0-9_\\/\\.-]+ 白名单正则");
|
|
829
|
+
|
|
830
|
+
// ── Test L5: isWorkflowArtifactPath 在 git diff 轮询中使用 ──
|
|
831
|
+
console.log("\n📋 测试 L5: isWorkflowArtifactPath 在 git diff 轮询中使用\n");
|
|
832
|
+
|
|
833
|
+
const hasPollIsWorkflowArtifact = source.includes('isWorkflowArtifactPath(change.path)');
|
|
834
|
+
assertTrue(hasPollIsWorkflowArtifact, "git diff 轮询使用 isWorkflowArtifactPath 过滤");
|
|
835
|
+
|
|
836
|
+
// ── Test L6: git diff 轮询也向 _workflowFileChanges 推送 ──
|
|
837
|
+
console.log("\n📋 测试 L6: git diff 轮询向 _workflowFileChanges 推送\n");
|
|
838
|
+
|
|
839
|
+
// Verify the polling code pushes to _workflowFileChanges before addWidgetSubStepTool
|
|
840
|
+
const pollTimerBodyStart = source.indexOf('_gitPollTimer = setInterval');
|
|
841
|
+
const pollTimerBodyEnd = source.indexOf('_gitPollTimer.unref()');
|
|
842
|
+
const pollTimerBody = source.slice(pollTimerBodyStart, pollTimerBodyEnd);
|
|
843
|
+
const hasPushInPoll = pollTimerBody.includes('_workflowFileChanges.push');
|
|
844
|
+
assertTrue(hasPushInPoll, "git diff 轮询(setInterval)中包含 _workflowFileChanges.push");
|
|
845
|
+
|
|
846
|
+
// Verify the type mapping (A→new, D→delete, else→edit) exists in polling code
|
|
847
|
+
const hasTypeMappingInPoll = pollTimerBody.includes('change.status === "A" ? "new"');
|
|
848
|
+
assertTrue(hasTypeMappingInPoll, "git diff 轮询中文件变更的类型映射正确");
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
// ── Test L7: _pollSeen 使用 path:stepIndex 而非 status:path ──
|
|
853
|
+
console.log("\n📋 测试 L7: _pollSeen 使用 path:stepIndex 而非 status:path\n");
|
|
854
|
+
|
|
855
|
+
// 验证轮询代码不再使用 status:path 作为键
|
|
856
|
+
const hasStatusPathKeyInPoll = source.includes('const key = `\${change.status}:\${change.path}`');
|
|
857
|
+
assertFalse(hasStatusPathKeyInPoll, "_pollSeen 不使用 status:path 作为键");
|
|
858
|
+
|
|
859
|
+
// 验证轮询代码使用 path:stepIndex 作为键
|
|
860
|
+
const hasPathStepKeyInPoll = source.includes('const key = `\${change.path}:\${stepIndex}`');
|
|
861
|
+
assertTrue(hasPathStepKeyInPoll, "_pollSeen 使用 path:stepIndex 作为键");
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
// ── Test L8: toGitStatus 函数已删除 ──
|
|
865
|
+
console.log("\n📋 测试 L8: toGitStatus 函数已删除(无调用者死代码)\n");
|
|
866
|
+
|
|
867
|
+
const hasToGitStatusFunc = source.includes("function toGitStatus(");
|
|
868
|
+
assertFalse(hasToGitStatusFunc, "toGitStatus 函数已删除");
|
|
869
|
+
|
|
870
|
+
const hasToGitStatusRef = source.includes("toGitStatus");
|
|
871
|
+
assertFalse(hasToGitStatusRef, "toGitStatus 无任何引用残留");
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
// ── Test L9: updateToolsFromGit 不再有 pollSeen 参数 ──
|
|
875
|
+
console.log("\n📋 测试 L9: updateToolsFromGit 不再有 pollSeen 死代码参数\n");
|
|
876
|
+
|
|
877
|
+
const hasPollSeenParam = source.includes("pollSeen?: Set<string>");
|
|
878
|
+
assertFalse(hasPollSeenParam, "updateToolsFromGit 无 pollSeen 参数");
|
|
879
|
+
|
|
880
|
+
const hasPollSeenCheck = source.includes("pollSeen?.has(");
|
|
881
|
+
assertFalse(hasPollSeenCheck, "updateToolsFromGit 无 pollSeen?.has() 检查");
|
|
882
|
+
|
|
883
|
+
const hasPollSeenPass = source.includes("updateToolsFromGit(_workflowCwd, stepIndex, agentName, _pollSeen)");
|
|
884
|
+
assertFalse(hasPollSeenPass, "updateToolsFromGit 调用处无 _pollSeen 参数传递");
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
// ── Test L10: post-completion output 正则使用白名单字符类 ──
|
|
888
|
+
console.log("\n📋 测试 L10: post-completion output 正则使用白名单字符类\n");
|
|
889
|
+
|
|
890
|
+
// 验证 post-completion outputPattern 使用白名单 [a-zA-Z0-9] 而非排除法 [^...]
|
|
891
|
+
const hasPostCompletionWhitelist = source.includes("outputPattern = new RegExp(\`\\\\.pi-dev-output\\\\/[a-zA-Z0-9_\\\\/.-]");
|
|
892
|
+
assertTrue(hasPostCompletionWhitelist, "post-completion output 正则使用 [a-zA-Z0-9_\\\\/.-] 白名单字符类");
|
|
893
|
+
|
|
894
|
+
// 验证不再使用排除字符类 (用反确认方式)
|
|
895
|
+
const hasNegatedClassOutput = source.includes("\\[^\\\\s,;)\\\\]}");
|
|
896
|
+
const hasNegatedClassOutput2 = source.includes("[^\\\\s,;)\\\\]}");
|
|
897
|
+
assertFalse(hasNegatedClassOutput || hasNegatedClassOutput2, "post-completion output 正则不使用排除法 [^\\\\s,;)\\\\]}");
|
|
898
|
+
|
|
899
|
+
|
|
772
900
|
console.log("\n═══════════════════════════════════════════════════════\n");
|
|
773
901
|
console.log(`📊 结果: ${pass} 通过, ${fail} 失败\n`);
|
|
774
902
|
|