@zhuan-ai/zhuanspec 2.16.0 → 2.16.3

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 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';
@@ -329,6 +330,23 @@ progressCmd
329
330
  process.exit(1);
330
331
  }
331
332
  });
333
+ // Progress list-corrections subcommand (review-step0 踩坑沉淀决策清单)
334
+ progressCmd
335
+ .command('list-corrections <change-id>')
336
+ .description('Render pending correction candidates (filter trivial acks + dedup) for review-step0 decision making')
337
+ .option('--out <path>', 'Output file path (default: zhuanspec/changes/<id>/review/pending-corrections.md)')
338
+ .option('--json', 'Print result as JSON to stdout instead of writing markdown file')
339
+ .action(async (changeId, options) => {
340
+ try {
341
+ const progressCommand = new ProgressCommand();
342
+ await progressCommand.listCorrections(changeId, options);
343
+ }
344
+ catch (error) {
345
+ console.log();
346
+ ora().fail(`Error: ${error.message}`);
347
+ process.exit(1);
348
+ }
349
+ });
332
350
  // Progress mark-baseline subcommand (Task 0 手动路径)
333
351
  progressCmd
334
352
  .command('mark-baseline <change-id> <phase>')
@@ -507,6 +525,25 @@ program
507
525
  process.exit(1);
508
526
  }
509
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
+ });
510
547
  // Top-level show command
511
548
  program
512
549
  .command('show [item-name]')
@@ -68,6 +68,18 @@ export declare class ProgressCommand {
68
68
  showCorrection(changeId: string, options?: {
69
69
  tail?: number;
70
70
  }): Promise<void>;
71
+ /**
72
+ * List pending correction candidates with trivial-ack filtering and dedup.
73
+ * Usage: zhuanspec progress list-corrections <change-id> [--out <path>] [--json]
74
+ *
75
+ * 默认会把 markdown 清单写到 `zhuanspec/changes/<id>/review/pending-corrections.md`,
76
+ * 供 review 阶段「踩坑沉淀」步骤的 askUserQuestion 三选项决策使用。
77
+ * `--json` 模式只走 stdout JSON,不写文件,便于 skill / 测试程序消费。
78
+ */
79
+ listCorrections(changeId: string, options?: {
80
+ out?: string;
81
+ json?: boolean;
82
+ }): Promise<void>;
71
83
  /**
72
84
  * Mark phase baseline manually.
73
85
  * Usage: zhuanspec progress mark-baseline <change-id> <phase> [--force]
@@ -13,6 +13,7 @@ import { PHASE_ORDER } from '../utils/phase-utils.js';
13
13
  import { initializeProgress, atomicWriteJson, getBeijingTime } from '../core/hooks/record-progress.js';
14
14
  import { recomputeAccuracyRate, persistAccuracyJson, getOrCreatePhaseBucket, applyPhaseOverride, confirmPhaseAccuracy, appendAccuracyDebugLog, } from '../core/metrics/code-accuracy.js';
15
15
  import { resolveZhuanSpecRoot } from '../utils/resolve-root.js';
16
+ import { selectCorrectionCandidates, renderPendingCorrectionsMarkdown, } from '../core/corrections/select-candidates.js';
16
17
  export class ProgressCommand {
17
18
  /**
18
19
  * Recover damaged progress.json using three-tier fallback strategy
@@ -294,9 +295,13 @@ export class ProgressCommand {
294
295
  // Progress percentage
295
296
  const percentage = totalTasks > 0 ? (completedCount / totalTasks) * 100 : 0;
296
297
  const progressBar = this.generateProgressBar(percentage, 12);
297
- // Duration
298
+ // Duration (active time)
298
299
  const durationMs = progress?.stats?.durationMs?.[phase] || 0;
299
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);
300
305
  const estimatedRemaining = totalTasks > 0 && completedCount > 0
301
306
  ? Math.round((durationMin / completedCount) * (totalTasks - completedCount))
302
307
  : 0;
@@ -318,8 +323,16 @@ export class ProgressCommand {
318
323
  console.log('');
319
324
  }
320
325
  // Duration info
321
- if (durationMin > 0) {
322
- console.log(`耗时: ${durationMin}m | 预计剩余: ~${estimatedRemaining}m`);
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(' | '));
323
336
  }
324
337
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
325
338
  console.log('');
@@ -468,7 +481,10 @@ export class ProgressCommand {
468
481
  promptIndex: markerMeta.phaseInputIndex,
469
482
  promptSnippet: markerMeta.promptSnippet,
470
483
  };
471
- progress.lastUpdatedAt = now.toISOString();
484
+ // v2.15.16:与 phaseDurations.startedAt / lastUpdatedAt 等字段对齐,统一使用北京时间
485
+ // naive 字符串。correctionContext.resolvedAt / expiresAt 仍保留 ISOString —— 它们
486
+ // 走 30min TTL(new Date(expiresAt) < Date.now())比较,ISO 解析更稳健。
487
+ progress.lastUpdatedAt = getBeijingTime();
472
488
  await fs.writeFile(progressPath, JSON.stringify(progress, null, 2), 'utf-8');
473
489
  }
474
490
  }
@@ -483,7 +499,8 @@ export class ProgressCommand {
483
499
  const raw = await FileSystemUtils.readFile(progressPath);
484
500
  const progress = JSON.parse(raw);
485
501
  progress.askedPitfallSaved = true;
486
- progress.lastUpdatedAt = new Date().toISOString();
502
+ // v2.15.16:统一北京时间格式
503
+ progress.lastUpdatedAt = getBeijingTime();
487
504
  await fs.writeFile(progressPath, JSON.stringify(progress, null, 2), 'utf-8');
488
505
  }
489
506
  catch {
@@ -575,6 +592,42 @@ export class ProgressCommand {
575
592
  }
576
593
  console.log();
577
594
  }
595
+ /**
596
+ * List pending correction candidates with trivial-ack filtering and dedup.
597
+ * Usage: zhuanspec progress list-corrections <change-id> [--out <path>] [--json]
598
+ *
599
+ * 默认会把 markdown 清单写到 `zhuanspec/changes/<id>/review/pending-corrections.md`,
600
+ * 供 review 阶段「踩坑沉淀」步骤的 askUserQuestion 三选项决策使用。
601
+ * `--json` 模式只走 stdout JSON,不写文件,便于 skill / 测试程序消费。
602
+ */
603
+ async listCorrections(changeId, options) {
604
+ const cwd = resolveZhuanSpecRoot();
605
+ const changeDir = path.join(cwd, 'zhuanspec', 'changes', changeId);
606
+ if (!await FileSystemUtils.directoryExists(changeDir)) {
607
+ throw new Error(`Change '${changeId}' not found`);
608
+ }
609
+ const result = await selectCorrectionCandidates(cwd, changeId);
610
+ if (options?.json) {
611
+ // stdout JSON 模式:用于程序化消费(不写文件)
612
+ console.log(JSON.stringify(result, null, 2));
613
+ return;
614
+ }
615
+ const markdown = renderPendingCorrectionsMarkdown(result);
616
+ const outPath = options?.out
617
+ ? (path.isAbsolute(options.out) ? options.out : path.join(cwd, options.out))
618
+ : path.join(changeDir, 'review', 'pending-corrections.md');
619
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
620
+ await fs.writeFile(outPath, markdown, 'utf-8');
621
+ const rel = path.relative(cwd, outPath) || outPath;
622
+ console.log(`\n✓ Pending corrections written to: ${rel}`);
623
+ console.log(` - 数据源: ${result.level === 1 ? 'Level 1 (correctionSignal)' : result.level === 2 ? 'Level 2 (summary 关键词)' : '无'}`);
624
+ console.log(` - 原始条目: ${result.totalRaw}`);
625
+ console.log(` - 有效候选: ${result.candidates.length}`);
626
+ console.log(` - 已过滤: ${result.dropped.length}`);
627
+ if (result.notice)
628
+ console.log(` - 提示: ${result.notice}`);
629
+ console.log('');
630
+ }
578
631
  /**
579
632
  * Mark phase baseline manually.
580
633
  * Usage: zhuanspec progress mark-baseline <change-id> <phase> [--force]
@@ -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
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Pending corrections candidate selection.
3
+ *
4
+ * 责任:把 `zhuanspec/changes/<id>/metrics/user_inputs.json` 里的纠偏 prompt 加工成
5
+ * "可读、去毛刺、去重、带建议分类" 的候选清单,给 review 阶段的踩坑沉淀决策使用。
6
+ *
7
+ * 不写文件、不改 progress.json,只做"读 + 计算 + 返回结构"。
8
+ */
9
+ export type CandidateCategory = 'troubleshooting' | 'best-practices' | 'implicit-conventions';
10
+ export type CandidateSourceLevel = 1 | 2;
11
+ export interface CorrectionCandidate {
12
+ /** 1-based 序号,对应"用户回复编号清单"的编号 */
13
+ index: number;
14
+ /** 关联的 user_inputs.json.inputId */
15
+ inputId?: string;
16
+ timestamp?: string;
17
+ phase?: string;
18
+ /** 优先 fullPrompt,没有就 summary,再没有就空字符串 */
19
+ text: string;
20
+ /** 截断展示用的预览(≤ 200 字) */
21
+ preview: string;
22
+ /** 命中的纠偏关键词(取 USER_CORRECTION_KEYWORDS / REQUIREMENT_CHANGE_KEYWORDS) */
23
+ matchedKeywords: string[];
24
+ isRequirementChange: boolean;
25
+ /** 启发式建议分类(用户/skill 仍可改) */
26
+ suggestedCategory: CandidateCategory;
27
+ /** 拟用文件名(不含目录前缀) */
28
+ suggestedFilename: string;
29
+ }
30
+ export interface DroppedCandidate {
31
+ /** drop 后保留的原始序号(按出现顺序,1-based)——用于在 markdown 里展示 */
32
+ rawIndex: number;
33
+ inputId?: string;
34
+ timestamp?: string;
35
+ reason: 'empty' | 'trivial-acknowledgement' | 'too-short-no-keyword' | 'duplicate-of' | 'subset-of';
36
+ /** 当 reason 为 duplicate-of / subset-of 时,指向被引用的有效候选编号 */
37
+ refIndex?: number;
38
+ preview: string;
39
+ }
40
+ export interface CandidateSelectionResult {
41
+ changeId: string;
42
+ /** 数据源层级(1: user_inputs 带 correctionSignal;2: 关键词推断) */
43
+ level: CandidateSourceLevel | null;
44
+ totalRaw: number;
45
+ candidates: CorrectionCandidate[];
46
+ dropped: DroppedCandidate[];
47
+ /** 数据完整性提示——读不到 user_inputs.json 时给空候选 + 错误说明 */
48
+ notice?: string;
49
+ }
50
+ /**
51
+ * 从 change 目录抽取并加工纠偏候选。
52
+ */
53
+ export declare function selectCorrectionCandidates(cwd: string, changeId: string): Promise<CandidateSelectionResult>;
54
+ /**
55
+ * 把候选结果渲染成 markdown,用于落到 review/pending-corrections.md。
56
+ */
57
+ export declare function renderPendingCorrectionsMarkdown(result: CandidateSelectionResult): string;
58
+ //# sourceMappingURL=select-candidates.d.ts.map