@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
package/dist/cli/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { ViewCommand } from '../core/view.js';
|
|
|
11
11
|
import { registerSpecCommand } from '../commands/spec.js';
|
|
12
12
|
import { ChangeCommand } from '../commands/change.js';
|
|
13
13
|
import { ValidateCommand } from '../commands/validate.js';
|
|
14
|
+
import { ValidateReportsCommand } from '../commands/validate-reports.js';
|
|
14
15
|
import { ShowCommand } from '../commands/show.js';
|
|
15
16
|
import { CompletionCommand } from '../commands/completion.js';
|
|
16
17
|
import { ReviewCommand } from '../commands/review.js';
|
|
@@ -524,6 +525,25 @@ program
|
|
|
524
525
|
process.exit(1);
|
|
525
526
|
}
|
|
526
527
|
});
|
|
528
|
+
// Top-level validate-reports command
|
|
529
|
+
program
|
|
530
|
+
.command('validate-reports <change-id>')
|
|
531
|
+
.description('Validate Apply-phase task reports against apply-agent / tdd-apply-agent templates')
|
|
532
|
+
.option('--json', 'Output validation summary as JSON')
|
|
533
|
+
.action(async (changeId, options) => {
|
|
534
|
+
try {
|
|
535
|
+
const cmd = new ValidateReportsCommand();
|
|
536
|
+
await cmd.execute(changeId, options);
|
|
537
|
+
if (typeof process.exitCode === 'number' && process.exitCode !== 0) {
|
|
538
|
+
process.exit(process.exitCode);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
console.log();
|
|
543
|
+
ora().fail(`Error: ${error.message}`);
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
527
547
|
// Top-level show command
|
|
528
548
|
program
|
|
529
549
|
.command('show [item-name]')
|
|
@@ -295,9 +295,13 @@ export class ProgressCommand {
|
|
|
295
295
|
// Progress percentage
|
|
296
296
|
const percentage = totalTasks > 0 ? (completedCount / totalTasks) * 100 : 0;
|
|
297
297
|
const progressBar = this.generateProgressBar(percentage, 12);
|
|
298
|
-
// Duration
|
|
298
|
+
// Duration (active time)
|
|
299
299
|
const durationMs = progress?.stats?.durationMs?.[phase] || 0;
|
|
300
300
|
const durationMin = Math.round(durationMs / 60000);
|
|
301
|
+
const wallClockMs = progress?.stats?.wallClockMs?.[phase] || 0;
|
|
302
|
+
const wallClockMin = Math.round(wallClockMs / 60000);
|
|
303
|
+
const idleMs = progress?.stats?.idleMs?.[phase] || 0;
|
|
304
|
+
const idleMin = Math.round(idleMs / 60000);
|
|
301
305
|
const estimatedRemaining = totalTasks > 0 && completedCount > 0
|
|
302
306
|
? Math.round((durationMin / completedCount) * (totalTasks - completedCount))
|
|
303
307
|
: 0;
|
|
@@ -319,8 +323,16 @@ export class ProgressCommand {
|
|
|
319
323
|
console.log('');
|
|
320
324
|
}
|
|
321
325
|
// Duration info
|
|
322
|
-
if (durationMin > 0) {
|
|
323
|
-
|
|
326
|
+
if (durationMin > 0 || wallClockMin > 0) {
|
|
327
|
+
const parts = [];
|
|
328
|
+
parts.push(`活跃: ${durationMin}m`);
|
|
329
|
+
if (idleMin > 0)
|
|
330
|
+
parts.push(`空闲: ${idleMin}m`);
|
|
331
|
+
if (wallClockMin > 0)
|
|
332
|
+
parts.push(`墙钟: ${wallClockMin}m`);
|
|
333
|
+
if (estimatedRemaining > 0)
|
|
334
|
+
parts.push(`预计剩余: ~${estimatedRemaining}m`);
|
|
335
|
+
console.log(parts.join(' | '));
|
|
324
336
|
}
|
|
325
337
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
326
338
|
console.log('');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `zhuanspec validate-reports <change-id>` — 校验某个 change 下所有
|
|
3
|
+
* Apply 任务报告是否符合 apply-agent.md / tdd-apply-agent.md 模板。
|
|
4
|
+
*
|
|
5
|
+
* 设计要点:
|
|
6
|
+
* - 仅做静态文本/章节检测,不读取代码,零外部依赖。
|
|
7
|
+
* - JSON 输出与人类可读输出双通道,便于 hooks/CI 程序化消费。
|
|
8
|
+
* - 退出码:所有报告通过且无缺失 → 0;否则 1。
|
|
9
|
+
*/
|
|
10
|
+
interface ExecuteOptions {
|
|
11
|
+
json?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare class ValidateReportsCommand {
|
|
14
|
+
execute(changeId: string | undefined, options?: ExecuteOptions): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=validate-reports.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `zhuanspec validate-reports <change-id>` — 校验某个 change 下所有
|
|
3
|
+
* Apply 任务报告是否符合 apply-agent.md / tdd-apply-agent.md 模板。
|
|
4
|
+
*
|
|
5
|
+
* 设计要点:
|
|
6
|
+
* - 仅做静态文本/章节检测,不读取代码,零外部依赖。
|
|
7
|
+
* - JSON 输出与人类可读输出双通道,便于 hooks/CI 程序化消费。
|
|
8
|
+
* - 退出码:所有报告通过且无缺失 → 0;否则 1。
|
|
9
|
+
*/
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { promises as fs } from 'fs';
|
|
13
|
+
import { validateTaskReports, } from '../core/validation/report-schema.js';
|
|
14
|
+
export class ValidateReportsCommand {
|
|
15
|
+
async execute(changeId, options = {}) {
|
|
16
|
+
if (!changeId) {
|
|
17
|
+
console.error('Usage: zhuanspec validate-reports <change-id> [--json]');
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const changeDir = path.join(process.cwd(), 'zhuanspec', 'changes', changeId);
|
|
22
|
+
try {
|
|
23
|
+
await fs.access(changeDir);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
console.error(`未找到 change 目录:${changeDir}`);
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const summary = await validateTaskReports(changeDir);
|
|
31
|
+
if (options.json) {
|
|
32
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
printHumanReadable(summary);
|
|
36
|
+
}
|
|
37
|
+
process.exitCode = summary.valid ? 0 : 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function printHumanReadable(summary) {
|
|
41
|
+
const { changeId, totalReports, passedReports, failedReports, missingReports } = summary;
|
|
42
|
+
console.log(`Change: ${changeId}`);
|
|
43
|
+
console.log(`Reports: total=${totalReports} passed=${passedReports} failed=${failedReports} missing=${missingReports.length}`);
|
|
44
|
+
console.log('');
|
|
45
|
+
for (const report of summary.reports) {
|
|
46
|
+
if (report.valid) {
|
|
47
|
+
console.log(` ${chalk.green('✓')} task-${report.taskId}-report.md (${report.declaredAgentType})`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
console.log(` ${chalk.red('✗')} task-${report.taskId}-report.md (declared=${report.declaredAgentType}, expected=${report.expectedAgentType})`);
|
|
51
|
+
for (const issue of report.issues) {
|
|
52
|
+
const tag = issue.level === 'ERROR' ? chalk.red('ERROR') : chalk.yellow('WARN');
|
|
53
|
+
console.log(` [${tag}] ${issue.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (missingReports.length > 0) {
|
|
57
|
+
console.log('');
|
|
58
|
+
console.log(chalk.red(`缺失报告(tasks.md 列出但 reports/ 中未找到):`));
|
|
59
|
+
for (const taskId of missingReports) {
|
|
60
|
+
console.log(` - task ${taskId}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
console.log('');
|
|
64
|
+
if (summary.valid) {
|
|
65
|
+
console.log(chalk.green('All task reports conform to schema.'));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.log(chalk.red('Some task reports do NOT conform to schema. See errors above.'));
|
|
69
|
+
console.log('');
|
|
70
|
+
console.log('Hints:');
|
|
71
|
+
console.log(' - 缺章节通常意味着报告由主 agent 直写而非 subagent 产出');
|
|
72
|
+
console.log(' - 重新通过 Agent tool / spawn_agent 启动 subagent 重跑对应任务');
|
|
73
|
+
console.log(' - 如果是合理降级,请在 "Agent 选择决策" 中显式写 "TDD 适用性: 不适合(降级原因:...)"');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=validate-reports.js.map
|
|
@@ -38,7 +38,7 @@ import path from 'path';
|
|
|
38
38
|
import os from 'os';
|
|
39
39
|
import { promises as fs } from 'fs';
|
|
40
40
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
41
|
-
import { getCodexHooksTomlBlock, getCodexDefaultHookSlots } from '../templates/codex-hooks-template.js';
|
|
41
|
+
import { getCodexHooksTomlBlock, getCodexDefaultHookSlots, getCodexDefaultHookHashes } from '../templates/codex-hooks-template.js';
|
|
42
42
|
import { getCodexAgentTomls, } from '../templates/codex-agents-template.js';
|
|
43
43
|
import { getCodexSkillFiles } from '../templates/codex-skills-template.js';
|
|
44
44
|
import { CodexSlashCommandConfigurator } from './slash/codex.js';
|
|
@@ -374,6 +374,16 @@ export class CodexConfigurator {
|
|
|
374
374
|
seedBySuffix.set(split.suffix, entry.hash);
|
|
375
375
|
}
|
|
376
376
|
}
|
|
377
|
+
// 2b. Computed hashes take priority over cross-project seeds because
|
|
378
|
+
// seeds from a different source type (e.g. ~/.codex/hooks.json vs
|
|
379
|
+
// .codex/config.toml) may have different hook content and thus wrong
|
|
380
|
+
// hashes. Our computed hashes replicate the exact Codex CLI fingerprint
|
|
381
|
+
// algorithm (NormalizedHookIdentity → canonical JSON → SHA-256) and are
|
|
382
|
+
// guaranteed correct for the current ZhuanSpec default hooks.
|
|
383
|
+
const computedHashes = getCodexDefaultHookHashes();
|
|
384
|
+
for (const [suffix, hash] of computedHashes) {
|
|
385
|
+
seedBySuffix.set(suffix, hash); // always overwrite cross-project seeds
|
|
386
|
+
}
|
|
377
387
|
const seedAvailable = seedBySuffix.size > 0;
|
|
378
388
|
// 3. Plan per-slot writes.
|
|
379
389
|
const written = [];
|
|
@@ -32,6 +32,20 @@ export declare function getBeijingTime(): string;
|
|
|
32
32
|
* Returns format: "2026-04-21 18-35-22" (文件名安全格式)
|
|
33
33
|
*/
|
|
34
34
|
export declare function getBeijingTimeForFilename(): string;
|
|
35
|
+
/**
|
|
36
|
+
* v2.16+ 活跃耗时累加器
|
|
37
|
+
*
|
|
38
|
+
* 每次 PostToolUse 心跳到达时调用,根据 gap = now - lastActiveAt 判断:
|
|
39
|
+
* gap < PHASE_IDLE_THRESHOLD_MS → 活跃,累加到 durationMs
|
|
40
|
+
* gap >= PHASE_IDLE_THRESHOLD_MS → 空闲,累加到 idleMs 并 push idleSegments
|
|
41
|
+
*
|
|
42
|
+
* 同步刷新 wallClockMs、lastActiveAt、lastUpdatedAt。
|
|
43
|
+
* 返回本次增量便于调用方决定是否刷新 stats。
|
|
44
|
+
*/
|
|
45
|
+
export declare function tickActivePhaseDuration(entry: PhaseDurationRecord, nowEpoch?: number, nowTimestamp?: string): {
|
|
46
|
+
activeDelta: number;
|
|
47
|
+
idleDelta: number;
|
|
48
|
+
};
|
|
35
49
|
interface RecordProgressOptions {
|
|
36
50
|
json?: boolean;
|
|
37
51
|
file?: string;
|
|
@@ -143,6 +157,14 @@ interface ClarificationRecord {
|
|
|
143
157
|
}
|
|
144
158
|
/**
|
|
145
159
|
* Phase duration record for tracking time spent in each phase
|
|
160
|
+
*
|
|
161
|
+
* v2.16+:字段语义反转
|
|
162
|
+
* - durationMs:覆盖为“活跃耗时”(剔除空闲段,PHASE_IDLE_THRESHOLD_MS 内累加),
|
|
163
|
+
* 业务主指标,与上报/采集口径保持一致。
|
|
164
|
+
* - wallClockMs:原 durationMs 的语义(endedAt|lastUpdatedAt - startedAt 墙钟差),
|
|
165
|
+
* 仅作审计/调试参考,迁移期老 progress.json 会把老的 durationMs 复制到此字段。
|
|
166
|
+
* - idleMs / idleSegments:活跃耗时之外被剔除的空闲明细(可观测)。
|
|
167
|
+
* - lastActiveAt:最后一次 hook 心跳的北京时间字符串,下次心跳到达时计算 gap。
|
|
146
168
|
*/
|
|
147
169
|
export interface PhaseDurationRecord {
|
|
148
170
|
phase: Phase;
|
|
@@ -152,6 +174,15 @@ export interface PhaseDurationRecord {
|
|
|
152
174
|
durationMs: number;
|
|
153
175
|
taskCount: number;
|
|
154
176
|
completedTaskCount: number;
|
|
177
|
+
wallClockMs?: number;
|
|
178
|
+
idleMs?: number;
|
|
179
|
+
lastActiveAt?: string;
|
|
180
|
+
_lastActiveEpoch?: number;
|
|
181
|
+
idleSegments?: Array<{
|
|
182
|
+
from: string;
|
|
183
|
+
to: string;
|
|
184
|
+
durationMs: number;
|
|
185
|
+
}>;
|
|
155
186
|
}
|
|
156
187
|
/**
|
|
157
188
|
* Proposal change record for tracking modifications to proposal files
|
|
@@ -322,6 +353,20 @@ export interface ProgressData {
|
|
|
322
353
|
review: number;
|
|
323
354
|
archive: number;
|
|
324
355
|
};
|
|
356
|
+
wallClockMs?: {
|
|
357
|
+
techDesign: number;
|
|
358
|
+
propose: number;
|
|
359
|
+
apply: number;
|
|
360
|
+
review: number;
|
|
361
|
+
archive: number;
|
|
362
|
+
};
|
|
363
|
+
idleMs?: {
|
|
364
|
+
techDesign: number;
|
|
365
|
+
propose: number;
|
|
366
|
+
apply: number;
|
|
367
|
+
review: number;
|
|
368
|
+
archive: number;
|
|
369
|
+
};
|
|
325
370
|
};
|
|
326
371
|
proposalChanges: ProposalChangeRecord[];
|
|
327
372
|
corrections?: CorrectionRecord[];
|