@zhuan-ai/zhuanspec 2.16.2 → 2.16.4
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/index.js +20 -0
- package/dist/commands/progress.js +15 -3
- package/dist/commands/validate-reports.d.ts +17 -0
- package/dist/commands/validate-reports.js +76 -0
- package/dist/core/configurators/codex.js +11 -1
- package/dist/core/hooks/record-progress.d.ts +45 -0
- package/dist/core/hooks/record-progress.js +345 -128
- package/dist/core/hooks/user-input-hook.js +4 -1
- package/dist/core/metrics/code-accuracy.d.ts +76 -0
- package/dist/core/metrics/code-accuracy.js +130 -0
- package/dist/core/templates/codex-hooks-template.d.ts +27 -0
- package/dist/core/templates/codex-hooks-template.js +81 -0
- package/dist/core/templates/slash-command-templates.js +20 -1
- package/dist/core/templates/tasks-template.js +6 -0
- package/dist/core/templates/tdd-tasks-template.js +8 -0
- package/dist/core/validation/report-schema.d.ts +74 -0
- package/dist/core/validation/report-schema.js +254 -0
- package/dist/utils/line-diff.d.ts +33 -0
- package/dist/utils/line-diff.js +85 -0
- package/dist/utils/phase-utils.d.ts +13 -0
- package/dist/utils/phase-utils.js +34 -6
- package/package.json +20 -22
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply 阶段任务报告 schema 校验。
|
|
3
|
+
*
|
|
4
|
+
* 背景:编排 Agent 在长任务尾部容易"指令衰减",跳过 Agent tool / spawn_agent
|
|
5
|
+
* 的 subagent 调用,自己在主线程顺手把任务做掉再补一份偷工的报告。这种偷工
|
|
6
|
+
* 报告会缺失 tdd-apply-agent.md / apply-agent.md 模板里强制的章节
|
|
7
|
+
* (TDD Phase / Verify RED / Verify GREEN / Mock Gate / Skill 调用记录 ...)。
|
|
8
|
+
*
|
|
9
|
+
* 本模块对 `zhuanspec/changes/<id>/reports/task-*.md` 做轻量结构校验:
|
|
10
|
+
* - 必备公共字段:`## 状态报告` / Status / Task ID / Report File / Agent 选择决策 / Agent 类型
|
|
11
|
+
* - 当 Agent 类型 = `tddApplyAgent`:再校验 TDD Phase / 上下文就绪摘要 /
|
|
12
|
+
* 验收点映射表 / TDD 执行过程(Verify RED + Verify GREEN)/ 测试运行结果 /
|
|
13
|
+
* Self-Review 发现 / Issues
|
|
14
|
+
* - 当 Agent 类型 = `applyAgent`:校验实施内容 / 修改文件 / Self-Review 发现 / Issues
|
|
15
|
+
*
|
|
16
|
+
* 与 tasks.md 交叉对账:若任务在 tasks.md 里带 `@test-case:TC-XXX`,则该任务的
|
|
17
|
+
* 报告原则上必须由 `tddApplyAgent` 产出(除非报告里显式声明降级原因,对应
|
|
18
|
+
* apply-agent.md 中的"降级使用 applyAgent"路径)。
|
|
19
|
+
*/
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import { promises as fs } from 'fs';
|
|
22
|
+
/** Common required fields for ANY task report (applyAgent or tddApplyAgent). */
|
|
23
|
+
const COMMON_REQUIRED = [
|
|
24
|
+
'## 状态报告',
|
|
25
|
+
'Status',
|
|
26
|
+
'Task ID',
|
|
27
|
+
'Report File',
|
|
28
|
+
'### Agent 选择决策',
|
|
29
|
+
'Agent 类型',
|
|
30
|
+
];
|
|
31
|
+
/** Additional required sections when declaredAgentType === 'tddApplyAgent'. */
|
|
32
|
+
const TDD_REQUIRED = [
|
|
33
|
+
'Wave',
|
|
34
|
+
'Execution Mode',
|
|
35
|
+
'TDD Phase',
|
|
36
|
+
'### 上下文就绪摘要',
|
|
37
|
+
'### Skill 调用记录',
|
|
38
|
+
'### 验收点映射表',
|
|
39
|
+
'### TDD 执行过程',
|
|
40
|
+
'#### Verify RED',
|
|
41
|
+
'#### Verify GREEN',
|
|
42
|
+
'#### Mock Gate',
|
|
43
|
+
'### 测试内容',
|
|
44
|
+
'### 测试运行结果',
|
|
45
|
+
'### Self-Review 发现',
|
|
46
|
+
'### Issues/Concerns',
|
|
47
|
+
];
|
|
48
|
+
/** Additional required sections when declaredAgentType === 'applyAgent'. */
|
|
49
|
+
const APPLY_REQUIRED = [
|
|
50
|
+
'### 实施内容',
|
|
51
|
+
'### 修改文件',
|
|
52
|
+
'### Self-Review 发现',
|
|
53
|
+
'### Issues/Concerns',
|
|
54
|
+
];
|
|
55
|
+
const TASK_REPORT_FILE_RE = /^task-(\d+\.\d+(?:[a-z])?)-report\.md$/i;
|
|
56
|
+
const AGENT_TYPE_LINE_RE = /\*\*Agent\s+类型\*\*\s*[::]\s*([A-Za-z]+)/;
|
|
57
|
+
const TASK_LINE_RE = /^-\s+\[[ xX]\]\s+(\d+\.\d+)\s+/;
|
|
58
|
+
/**
|
|
59
|
+
* Inspect a single report file. Pure string-level checks — no side effects.
|
|
60
|
+
*
|
|
61
|
+
* @param taskId The task id (derived from filename).
|
|
62
|
+
* @param content The raw markdown content of the report file.
|
|
63
|
+
* @param expected The agent type expected from tasks.md (default 'unknown').
|
|
64
|
+
*/
|
|
65
|
+
export function checkReportContent(taskId, filePath, content, expected) {
|
|
66
|
+
const declaredAgentType = detectAgentType(content);
|
|
67
|
+
const hasDowngradeJustification = /降级|不适合\s*TDD|TDD\s*适用性\s*[::]\s*不适合/.test(content);
|
|
68
|
+
const missing = [];
|
|
69
|
+
const issues = [];
|
|
70
|
+
// 1) Common required fields/sections.
|
|
71
|
+
for (const token of COMMON_REQUIRED) {
|
|
72
|
+
if (!content.includes(token))
|
|
73
|
+
missing.push(token);
|
|
74
|
+
}
|
|
75
|
+
// 2) Type-specific required sections.
|
|
76
|
+
const typeRequired = declaredAgentType === 'tddApplyAgent'
|
|
77
|
+
? TDD_REQUIRED
|
|
78
|
+
: declaredAgentType === 'applyAgent'
|
|
79
|
+
? APPLY_REQUIRED
|
|
80
|
+
: [];
|
|
81
|
+
for (const token of typeRequired) {
|
|
82
|
+
if (!content.includes(token))
|
|
83
|
+
missing.push(token);
|
|
84
|
+
}
|
|
85
|
+
// 3) Aggregate ERROR-level issues from missing sections.
|
|
86
|
+
if (missing.length > 0) {
|
|
87
|
+
issues.push({
|
|
88
|
+
level: 'ERROR',
|
|
89
|
+
taskId,
|
|
90
|
+
message: `报告缺失必需章节/字段:${missing.join(', ')}`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// 4) Cross-check against expected agent type.
|
|
94
|
+
if (expected === 'tddApplyAgent' &&
|
|
95
|
+
declaredAgentType === 'applyAgent' &&
|
|
96
|
+
!hasDowngradeJustification) {
|
|
97
|
+
issues.push({
|
|
98
|
+
level: 'ERROR',
|
|
99
|
+
taskId,
|
|
100
|
+
message: 'tasks.md 标注 @test-case 应使用 tddApplyAgent;报告声明 applyAgent 但未给出降级理由("TDD 适用性: 不适合(...)")',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (declaredAgentType === 'unknown') {
|
|
104
|
+
issues.push({
|
|
105
|
+
level: 'ERROR',
|
|
106
|
+
taskId,
|
|
107
|
+
message: '报告未声明 Agent 类型(缺失 "Agent 类型: applyAgent | tddApplyAgent")',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// 5) Heuristic: tddApplyAgent must show real test command output evidence.
|
|
111
|
+
if (declaredAgentType === 'tddApplyAgent') {
|
|
112
|
+
const hasTestEvidence = /mvn\s+test|npm\s+(?:test|run\s+test)|pnpm\s+test|yarn\s+test|gradle\s+test|vitest|jest/i.test(content);
|
|
113
|
+
if (!hasTestEvidence) {
|
|
114
|
+
issues.push({
|
|
115
|
+
level: 'ERROR',
|
|
116
|
+
taskId,
|
|
117
|
+
message: 'tddApplyAgent 报告缺少测试运行证据(未发现 mvn test / npm test / vitest / jest 等命令痕迹)',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const valid = issues.every((issue) => issue.level !== 'ERROR');
|
|
122
|
+
return {
|
|
123
|
+
taskId,
|
|
124
|
+
filePath,
|
|
125
|
+
declaredAgentType,
|
|
126
|
+
expectedAgentType: expected,
|
|
127
|
+
hasDowngradeJustification,
|
|
128
|
+
missing,
|
|
129
|
+
issues,
|
|
130
|
+
valid,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Validate all task reports under `<changeDir>/reports/`.
|
|
135
|
+
* Cross-checks against `<changeDir>/tasks.md` to detect missing reports and
|
|
136
|
+
* agent-type mismatches.
|
|
137
|
+
*/
|
|
138
|
+
export async function validateTaskReports(changeDir) {
|
|
139
|
+
const changeId = path.basename(changeDir);
|
|
140
|
+
const reportsDir = path.join(changeDir, 'reports');
|
|
141
|
+
// Read tasks.md to figure out the expected task list and agent type per task.
|
|
142
|
+
const expectedByTaskId = await parseExpectedAgentTypes(path.join(changeDir, 'tasks.md'));
|
|
143
|
+
let entries = [];
|
|
144
|
+
try {
|
|
145
|
+
entries = await fs.readdir(reportsDir);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// No reports directory at all — report every expected task as missing.
|
|
149
|
+
return {
|
|
150
|
+
changeId,
|
|
151
|
+
reportsDir,
|
|
152
|
+
totalReports: 0,
|
|
153
|
+
passedReports: 0,
|
|
154
|
+
failedReports: 0,
|
|
155
|
+
missingReports: Array.from(expectedByTaskId.keys()),
|
|
156
|
+
reports: [],
|
|
157
|
+
valid: expectedByTaskId.size === 0,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const reportFiles = entries
|
|
161
|
+
.filter((e) => TASK_REPORT_FILE_RE.test(e))
|
|
162
|
+
.sort();
|
|
163
|
+
const seenTaskIds = new Set();
|
|
164
|
+
const results = [];
|
|
165
|
+
for (const file of reportFiles) {
|
|
166
|
+
const match = file.match(TASK_REPORT_FILE_RE);
|
|
167
|
+
if (!match)
|
|
168
|
+
continue;
|
|
169
|
+
const taskId = match[1];
|
|
170
|
+
seenTaskIds.add(taskId);
|
|
171
|
+
const filePath = path.join(reportsDir, file);
|
|
172
|
+
let content;
|
|
173
|
+
try {
|
|
174
|
+
content = await fs.readFile(filePath, 'utf-8');
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
results.push({
|
|
178
|
+
taskId,
|
|
179
|
+
filePath,
|
|
180
|
+
declaredAgentType: 'unknown',
|
|
181
|
+
expectedAgentType: expectedByTaskId.get(taskId) ?? 'unknown',
|
|
182
|
+
hasDowngradeJustification: false,
|
|
183
|
+
missing: ['<file unreadable>'],
|
|
184
|
+
issues: [
|
|
185
|
+
{
|
|
186
|
+
level: 'ERROR',
|
|
187
|
+
taskId,
|
|
188
|
+
message: '报告文件无法读取',
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
valid: false,
|
|
192
|
+
});
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const expected = expectedByTaskId.get(taskId) ?? 'unknown';
|
|
196
|
+
results.push(checkReportContent(taskId, filePath, content, expected));
|
|
197
|
+
}
|
|
198
|
+
// Tasks declared in tasks.md but never produced a report.
|
|
199
|
+
const missingReports = [];
|
|
200
|
+
for (const taskId of expectedByTaskId.keys()) {
|
|
201
|
+
if (!seenTaskIds.has(taskId))
|
|
202
|
+
missingReports.push(taskId);
|
|
203
|
+
}
|
|
204
|
+
const passedReports = results.filter((r) => r.valid).length;
|
|
205
|
+
const failedReports = results.length - passedReports;
|
|
206
|
+
return {
|
|
207
|
+
changeId,
|
|
208
|
+
reportsDir,
|
|
209
|
+
totalReports: results.length,
|
|
210
|
+
passedReports,
|
|
211
|
+
failedReports,
|
|
212
|
+
missingReports,
|
|
213
|
+
reports: results,
|
|
214
|
+
valid: failedReports === 0 && missingReports.length === 0,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/** Detect declared agent type from a report's body. */
|
|
218
|
+
function detectAgentType(content) {
|
|
219
|
+
const m = content.match(AGENT_TYPE_LINE_RE);
|
|
220
|
+
if (!m)
|
|
221
|
+
return 'unknown';
|
|
222
|
+
const v = m[1].trim();
|
|
223
|
+
if (v === 'tddApplyAgent')
|
|
224
|
+
return 'tddApplyAgent';
|
|
225
|
+
if (v === 'applyAgent')
|
|
226
|
+
return 'applyAgent';
|
|
227
|
+
return 'unknown';
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Parse `tasks.md` to determine the expected agent type for each task.
|
|
231
|
+
* Tasks tagged with `@test-case:TC-XXX` are expected to use `tddApplyAgent`;
|
|
232
|
+
* everything else defaults to `applyAgent`.
|
|
233
|
+
*/
|
|
234
|
+
async function parseExpectedAgentTypes(tasksFile) {
|
|
235
|
+
const result = new Map();
|
|
236
|
+
let content;
|
|
237
|
+
try {
|
|
238
|
+
content = await fs.readFile(tasksFile, 'utf-8');
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
const lines = content.split(/\r?\n/);
|
|
244
|
+
for (const line of lines) {
|
|
245
|
+
const m = line.match(TASK_LINE_RE);
|
|
246
|
+
if (!m)
|
|
247
|
+
continue;
|
|
248
|
+
const taskId = m[1];
|
|
249
|
+
const hasTestCase = /@test-case\s*:/.test(line);
|
|
250
|
+
result.set(taskId, hasTestCase ? 'tddApplyAgent' : 'applyAgent');
|
|
251
|
+
}
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
//# sourceMappingURL=report-schema.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 行级 diff 计算工具
|
|
3
|
+
*
|
|
4
|
+
* 用途:替代 record-progress.ts 中的 Math.max(newLines, oldLines),
|
|
5
|
+
* 只统计真正发生变化的行数(增 + 删),不计入未修改的上下文锚点。
|
|
6
|
+
*
|
|
7
|
+
* 例:
|
|
8
|
+
* old = "A\nB\nC\nD"
|
|
9
|
+
* new = "A\nX\nC\nD"
|
|
10
|
+
* max(4,4) = 4 ← 旧口径(含上下文)
|
|
11
|
+
* diff = 2 ← 新口径(B 删除 + X 新增)
|
|
12
|
+
*
|
|
13
|
+
* 算法:基于 LCS(Longest Common Subsequence)的行级最小编辑距离。
|
|
14
|
+
* 对小规模编辑(< 1000 行)足够快,且无外部依赖。
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* 计算两段文本的行级净改动行数(添加行数 + 删除行数)。
|
|
18
|
+
*
|
|
19
|
+
* - 仅有 oldText:纯删除,返回旧文本行数
|
|
20
|
+
* - 仅有 newText:纯新增,返回新文本行数
|
|
21
|
+
* - 两者都有:基于 LCS 的最小编辑距离
|
|
22
|
+
*
|
|
23
|
+
* 注意:
|
|
24
|
+
* - 完全相同(无变化)返回 0
|
|
25
|
+
* - 修改一行 = 1 删 + 1 增 = 2
|
|
26
|
+
* - 与 unified diff 的 +/- 行数概念一致
|
|
27
|
+
*
|
|
28
|
+
* @param oldText 旧文本(Edit 的 old_string)
|
|
29
|
+
* @param newText 新文本(Edit 的 new_string)
|
|
30
|
+
* @returns 净改动行数(>= 0)
|
|
31
|
+
*/
|
|
32
|
+
export declare function countLineDiff(oldText: string | undefined | null, newText: string | undefined | null): number;
|
|
33
|
+
//# sourceMappingURL=line-diff.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 行级 diff 计算工具
|
|
3
|
+
*
|
|
4
|
+
* 用途:替代 record-progress.ts 中的 Math.max(newLines, oldLines),
|
|
5
|
+
* 只统计真正发生变化的行数(增 + 删),不计入未修改的上下文锚点。
|
|
6
|
+
*
|
|
7
|
+
* 例:
|
|
8
|
+
* old = "A\nB\nC\nD"
|
|
9
|
+
* new = "A\nX\nC\nD"
|
|
10
|
+
* max(4,4) = 4 ← 旧口径(含上下文)
|
|
11
|
+
* diff = 2 ← 新口径(B 删除 + X 新增)
|
|
12
|
+
*
|
|
13
|
+
* 算法:基于 LCS(Longest Common Subsequence)的行级最小编辑距离。
|
|
14
|
+
* 对小规模编辑(< 1000 行)足够快,且无外部依赖。
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* 计算两段文本的行级净改动行数(添加行数 + 删除行数)。
|
|
18
|
+
*
|
|
19
|
+
* - 仅有 oldText:纯删除,返回旧文本行数
|
|
20
|
+
* - 仅有 newText:纯新增,返回新文本行数
|
|
21
|
+
* - 两者都有:基于 LCS 的最小编辑距离
|
|
22
|
+
*
|
|
23
|
+
* 注意:
|
|
24
|
+
* - 完全相同(无变化)返回 0
|
|
25
|
+
* - 修改一行 = 1 删 + 1 增 = 2
|
|
26
|
+
* - 与 unified diff 的 +/- 行数概念一致
|
|
27
|
+
*
|
|
28
|
+
* @param oldText 旧文本(Edit 的 old_string)
|
|
29
|
+
* @param newText 新文本(Edit 的 new_string)
|
|
30
|
+
* @returns 净改动行数(>= 0)
|
|
31
|
+
*/
|
|
32
|
+
export function countLineDiff(oldText, newText) {
|
|
33
|
+
const oldStr = oldText ?? '';
|
|
34
|
+
const newStr = newText ?? '';
|
|
35
|
+
if (oldStr === newStr)
|
|
36
|
+
return 0;
|
|
37
|
+
if (!oldStr)
|
|
38
|
+
return newStr.split('\n').length;
|
|
39
|
+
if (!newStr)
|
|
40
|
+
return oldStr.split('\n').length;
|
|
41
|
+
const oldLines = oldStr.split('\n');
|
|
42
|
+
const newLines = newStr.split('\n');
|
|
43
|
+
const lcs = lcsLength(oldLines, newLines);
|
|
44
|
+
// 删除行数 = 旧行 - LCS;新增行数 = 新行 - LCS
|
|
45
|
+
return (oldLines.length - lcs) + (newLines.length - lcs);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 计算两个字符串数组的 LCS 长度(动态规划,O(n*m) 时间,O(min) 空间)。
|
|
49
|
+
*
|
|
50
|
+
* 内部辅助函数。使用滚动数组避免 O(n*m) 空间开销。
|
|
51
|
+
*
|
|
52
|
+
* @param a 数组 A(旧行)
|
|
53
|
+
* @param b 数组 B(新行)
|
|
54
|
+
* @returns 最长公共子序列长度
|
|
55
|
+
*/
|
|
56
|
+
function lcsLength(a, b) {
|
|
57
|
+
const n = a.length;
|
|
58
|
+
const m = b.length;
|
|
59
|
+
if (n === 0 || m === 0)
|
|
60
|
+
return 0;
|
|
61
|
+
// 让 b 作为短边,节省空间
|
|
62
|
+
const [shortArr, longArr] = m <= n ? [b, a] : [a, b];
|
|
63
|
+
const shortLen = shortArr.length;
|
|
64
|
+
const longLen = longArr.length;
|
|
65
|
+
// 滚动数组:prev / curr,长度 = shortLen + 1
|
|
66
|
+
let prev = new Array(shortLen + 1).fill(0);
|
|
67
|
+
let curr = new Array(shortLen + 1).fill(0);
|
|
68
|
+
for (let i = 1; i <= longLen; i++) {
|
|
69
|
+
for (let j = 1; j <= shortLen; j++) {
|
|
70
|
+
if (longArr[i - 1] === shortArr[j - 1]) {
|
|
71
|
+
curr[j] = prev[j - 1] + 1;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
curr[j] = Math.max(prev[j], curr[j - 1]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// 滚动:把 curr 拷给 prev,curr 清零
|
|
78
|
+
const tmp = prev;
|
|
79
|
+
prev = curr;
|
|
80
|
+
curr = tmp;
|
|
81
|
+
curr.fill(0);
|
|
82
|
+
}
|
|
83
|
+
return prev[shortLen];
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=line-diff.js.map
|
|
@@ -9,6 +9,19 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export type Phase = 'idle' | 'techDesign' | 'propose' | 'apply' | 'review' | 'archive';
|
|
11
11
|
export declare const PHASE_ORDER: Phase[];
|
|
12
|
+
/**
|
|
13
|
+
* v2.16+:阶段“活跃耗时”计算的空闲阈值。
|
|
14
|
+
*
|
|
15
|
+
* 设计背景:原本 progress.json 中的 phaseDurations[].durationMs / stats.durationMs 都是
|
|
16
|
+
* 墙钟差,中断/午休/过夜/AskUserQuestion 长时间挂起都会被当成阶段耗时。
|
|
17
|
+
* 从 v2.16 起,相邻两次 record-progress 心跳间隔 < PHASE_IDLE_THRESHOLD_MS 才计入
|
|
18
|
+
* 活跃耗时(durationMs);超过阈值的 gap 被归类为空闲,计入 idleMs / idleSegments,
|
|
19
|
+
* 不计入 durationMs。
|
|
20
|
+
*
|
|
21
|
+
* 30 分钟阈值是取舍结果:避免误伤正常的本地阅读 / 思考 / 短暂离开,只剔除
|
|
22
|
+
* 明显的长时间中断、过夜、确认久未响应等场景。
|
|
23
|
+
*/
|
|
24
|
+
export declare const PHASE_IDLE_THRESHOLD_MS: number;
|
|
12
25
|
export declare const PHASE_MARKERS: Partial<Record<Phase, string>>;
|
|
13
26
|
/**
|
|
14
27
|
* Set phase in progress.json with full data preservation
|
|
@@ -9,8 +9,21 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import path from 'path';
|
|
11
11
|
import { FileSystemUtils } from './file-system.js';
|
|
12
|
-
import { recoverProgressJsonForWrite, getBeijingTime, atomicWriteJson, } from '../core/hooks/record-progress.js';
|
|
12
|
+
import { recoverProgressJsonForWrite, getBeijingTime, atomicWriteJson, tickActivePhaseDuration, } from '../core/hooks/record-progress.js';
|
|
13
13
|
export const PHASE_ORDER = ['idle', 'techDesign', 'propose', 'apply', 'review', 'archive'];
|
|
14
|
+
/**
|
|
15
|
+
* v2.16+:阶段“活跃耗时”计算的空闲阈值。
|
|
16
|
+
*
|
|
17
|
+
* 设计背景:原本 progress.json 中的 phaseDurations[].durationMs / stats.durationMs 都是
|
|
18
|
+
* 墙钟差,中断/午休/过夜/AskUserQuestion 长时间挂起都会被当成阶段耗时。
|
|
19
|
+
* 从 v2.16 起,相邻两次 record-progress 心跳间隔 < PHASE_IDLE_THRESHOLD_MS 才计入
|
|
20
|
+
* 活跃耗时(durationMs);超过阈值的 gap 被归类为空闲,计入 idleMs / idleSegments,
|
|
21
|
+
* 不计入 durationMs。
|
|
22
|
+
*
|
|
23
|
+
* 30 分钟阈值是取舍结果:避免误伤正常的本地阅读 / 思考 / 短暂离开,只剔除
|
|
24
|
+
* 明显的长时间中断、过夜、确认久未响应等场景。
|
|
25
|
+
*/
|
|
26
|
+
export const PHASE_IDLE_THRESHOLD_MS = 30 * 60 * 1000;
|
|
14
27
|
export const PHASE_MARKERS = {
|
|
15
28
|
techDesign: '.tech-design',
|
|
16
29
|
apply: '.approved',
|
|
@@ -83,6 +96,10 @@ export async function setPhase(changeDir, phase) {
|
|
|
83
96
|
durationMs: 0,
|
|
84
97
|
taskCount: 0,
|
|
85
98
|
completedTaskCount: 0,
|
|
99
|
+
wallClockMs: 0,
|
|
100
|
+
idleMs: 0,
|
|
101
|
+
lastActiveAt: timestamp,
|
|
102
|
+
_lastActiveEpoch: Date.now(),
|
|
86
103
|
}],
|
|
87
104
|
proposalChanges: [],
|
|
88
105
|
stats: { tokenUsageTotal: 0, contextLoad: 0, durationMs: { techDesign: 0, propose: 0, apply: 0, review: 0, archive: 0 } },
|
|
@@ -119,6 +136,10 @@ export async function setPhase(changeDir, phase) {
|
|
|
119
136
|
durationMs: 0,
|
|
120
137
|
taskCount: 0,
|
|
121
138
|
completedTaskCount: 0,
|
|
139
|
+
wallClockMs: 0,
|
|
140
|
+
idleMs: 0,
|
|
141
|
+
lastActiveAt: timestamp,
|
|
142
|
+
_lastActiveEpoch: Date.now(),
|
|
122
143
|
}],
|
|
123
144
|
proposalChanges: [],
|
|
124
145
|
stats: { tokenUsageTotal: 0, contextLoad: 0, durationMs: { techDesign: 0, propose: 0, apply: 0, review: 0, archive: 0 } },
|
|
@@ -135,6 +156,10 @@ export async function setPhase(changeDir, phase) {
|
|
|
135
156
|
durationMs: 0,
|
|
136
157
|
taskCount: 0,
|
|
137
158
|
completedTaskCount: 0,
|
|
159
|
+
wallClockMs: 0,
|
|
160
|
+
idleMs: 0,
|
|
161
|
+
lastActiveAt: timestamp,
|
|
162
|
+
_lastActiveEpoch: Date.now(),
|
|
138
163
|
});
|
|
139
164
|
}
|
|
140
165
|
if (previousPhase !== phase) {
|
|
@@ -144,14 +169,13 @@ export async function setPhase(changeDir, phase) {
|
|
|
144
169
|
timestamp,
|
|
145
170
|
triggeredBy: 'setPhase',
|
|
146
171
|
});
|
|
147
|
-
// Update phaseDurations:
|
|
172
|
+
// Update phaseDurations: close previous phase with tick
|
|
148
173
|
const prevPhaseDuration = progress.phaseDurations.find(pd => pd.phase === previousPhase && !pd.endedAt);
|
|
149
174
|
if (prevPhaseDuration) {
|
|
175
|
+
// 封段前 tick 一次,确保最后一段活跃时间被累加
|
|
176
|
+
const nowEpoch = Date.now();
|
|
177
|
+
tickActivePhaseDuration(prevPhaseDuration, nowEpoch, timestamp);
|
|
150
178
|
prevPhaseDuration.endedAt = timestamp;
|
|
151
|
-
prevPhaseDuration.lastUpdatedAt = timestamp;
|
|
152
|
-
const startTime = new Date(prevPhaseDuration.startedAt).getTime();
|
|
153
|
-
const endTime = new Date(timestamp).getTime();
|
|
154
|
-
prevPhaseDuration.durationMs = endTime > startTime ? endTime - startTime : 0;
|
|
155
179
|
}
|
|
156
180
|
// Add new phase duration record
|
|
157
181
|
progress.phaseDurations.push({
|
|
@@ -161,6 +185,10 @@ export async function setPhase(changeDir, phase) {
|
|
|
161
185
|
durationMs: 0,
|
|
162
186
|
taskCount: 0,
|
|
163
187
|
completedTaskCount: 0,
|
|
188
|
+
wallClockMs: 0,
|
|
189
|
+
idleMs: 0,
|
|
190
|
+
lastActiveAt: timestamp,
|
|
191
|
+
_lastActiveEpoch: Date.now(),
|
|
164
192
|
});
|
|
165
193
|
}
|
|
166
194
|
progress.phase = phase;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhuan-ai/zhuanspec",
|
|
3
|
-
"version": "2.16.
|
|
3
|
+
"version": "2.16.4",
|
|
4
4
|
"description": "AI-native system for spec-driven development",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"zhuanspec",
|
|
@@ -39,26 +39,6 @@
|
|
|
39
39
|
"!dist/**/__tests__",
|
|
40
40
|
"!dist/**/*.map"
|
|
41
41
|
],
|
|
42
|
-
"scripts": {
|
|
43
|
-
"lint": "eslint src/",
|
|
44
|
-
"build": "node build.js",
|
|
45
|
-
"dev": "tsc --watch",
|
|
46
|
-
"dev:cli": "pnpm build && node bin/zhuanspec.js",
|
|
47
|
-
"test": "vitest run",
|
|
48
|
-
"test:watch": "vitest",
|
|
49
|
-
"test:ui": "vitest --ui",
|
|
50
|
-
"test:coverage": "vitest --coverage",
|
|
51
|
-
"test:postinstall": "node scripts/postinstall.js",
|
|
52
|
-
"prepare": "npm run build",
|
|
53
|
-
"prepublishOnly": "npm run build",
|
|
54
|
-
"postinstall": "node scripts/postinstall.js",
|
|
55
|
-
"check:pack-version": "node scripts/pack-version-check.mjs",
|
|
56
|
-
"diagnose:cursor": "node scripts/diagnose-cursor-commands.js",
|
|
57
|
-
"release": "pnpm run release:ci",
|
|
58
|
-
"release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
|
|
59
|
-
"release:local": "pnpm exec changeset version && pnpm run check:pack-version && pnpm exec changeset publish",
|
|
60
|
-
"changeset": "changeset"
|
|
61
|
-
},
|
|
62
42
|
"engines": {
|
|
63
43
|
"node": ">=20.19.0"
|
|
64
44
|
},
|
|
@@ -80,5 +60,23 @@
|
|
|
80
60
|
"ora": "^8.2.0",
|
|
81
61
|
"yaml": "^2.8.2",
|
|
82
62
|
"zod": "^4.0.17"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"lint": "eslint src/",
|
|
66
|
+
"build": "node build.js",
|
|
67
|
+
"dev": "tsc --watch",
|
|
68
|
+
"dev:cli": "pnpm build && node bin/zhuanspec.js",
|
|
69
|
+
"test": "vitest run",
|
|
70
|
+
"test:watch": "vitest",
|
|
71
|
+
"test:ui": "vitest --ui",
|
|
72
|
+
"test:coverage": "vitest --coverage",
|
|
73
|
+
"test:postinstall": "node scripts/postinstall.js",
|
|
74
|
+
"postinstall": "node scripts/postinstall.js",
|
|
75
|
+
"check:pack-version": "node scripts/pack-version-check.mjs",
|
|
76
|
+
"diagnose:cursor": "node scripts/diagnose-cursor-commands.js",
|
|
77
|
+
"release": "pnpm run release:ci",
|
|
78
|
+
"release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
|
|
79
|
+
"release:local": "pnpm exec changeset version && pnpm run check:pack-version && pnpm exec changeset publish",
|
|
80
|
+
"changeset": "changeset"
|
|
83
81
|
}
|
|
84
|
-
}
|
|
82
|
+
}
|