@zhuan-ai/zhuanspec 2.15.7 → 2.16.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.
Files changed (43) hide show
  1. package/dist/cli/hooks.d.ts +13 -1
  2. package/dist/cli/hooks.js +75 -2
  3. package/dist/cli/index.js +111 -4
  4. package/dist/commands/accuracy.js +5 -1
  5. package/dist/commands/knowledge.d.ts +21 -0
  6. package/dist/commands/knowledge.js +80 -0
  7. package/dist/commands/progress.d.ts +78 -0
  8. package/dist/commands/progress.js +349 -3
  9. package/dist/core/archive.js +28 -0
  10. package/dist/core/configurators/codex.d.ts +55 -1
  11. package/dist/core/configurators/codex.js +234 -2
  12. package/dist/core/corrections/select-candidates.d.ts +58 -0
  13. package/dist/core/corrections/select-candidates.js +357 -0
  14. package/dist/core/hooks/collect-knowledge.js +80 -5
  15. package/dist/core/hooks/deviation-check.d.ts +26 -1
  16. package/dist/core/hooks/deviation-check.js +32 -184
  17. package/dist/core/hooks/knowledge-index.d.ts +84 -0
  18. package/dist/core/hooks/knowledge-index.js +270 -0
  19. package/dist/core/hooks/post-archive.js +25 -13
  20. package/dist/core/hooks/record-progress.d.ts +64 -1
  21. package/dist/core/hooks/record-progress.js +224 -54
  22. package/dist/core/hooks/summarize.js +107 -20
  23. package/dist/core/hooks/user-input-hook.d.ts +38 -2
  24. package/dist/core/hooks/user-input-hook.js +346 -21
  25. package/dist/core/init.d.ts +16 -0
  26. package/dist/core/init.js +208 -11
  27. package/dist/core/metrics/code-accuracy.d.ts +152 -3
  28. package/dist/core/metrics/code-accuracy.js +323 -19
  29. package/dist/core/templates/agents-root-stub.d.ts +1 -1
  30. package/dist/core/templates/agents-root-stub.js +1 -0
  31. package/dist/core/templates/agents-template.d.ts +1 -1
  32. package/dist/core/templates/agents-template.js +27 -25
  33. package/dist/core/templates/codex-hooks-template.d.ts +37 -0
  34. package/dist/core/templates/codex-hooks-template.js +33 -2
  35. package/dist/core/templates/slash-command-templates.js +208 -66
  36. package/dist/core/templates/tasks-template.js +66 -14
  37. package/dist/core/update.js +17 -0
  38. package/dist/core/validation/strict-rules.d.ts +28 -0
  39. package/dist/core/validation/strict-rules.js +284 -0
  40. package/dist/utils/hook-merge.d.ts +6 -0
  41. package/dist/utils/hook-merge.js +33 -2
  42. package/dist/utils/phase-utils.js +5 -0
  43. package/package.json +22 -20
@@ -10,8 +10,10 @@ import path from 'path';
10
10
  import { promises as fs } from 'fs';
11
11
  import { FileSystemUtils } from '../utils/file-system.js';
12
12
  import { PHASE_ORDER } from '../utils/phase-utils.js';
13
- import { initializeProgress } from '../core/hooks/record-progress.js';
13
+ import { initializeProgress, atomicWriteJson, getBeijingTime } from '../core/hooks/record-progress.js';
14
+ import { recomputeAccuracyRate, persistAccuracyJson, getOrCreatePhaseBucket, applyPhaseOverride, confirmPhaseAccuracy, appendAccuracyDebugLog, } from '../core/metrics/code-accuracy.js';
14
15
  import { resolveZhuanSpecRoot } from '../utils/resolve-root.js';
16
+ import { selectCorrectionCandidates, renderPendingCorrectionsMarkdown, } from '../core/corrections/select-candidates.js';
15
17
  export class ProgressCommand {
16
18
  /**
17
19
  * Recover damaged progress.json using three-tier fallback strategy
@@ -385,6 +387,19 @@ export class ProgressCommand {
385
387
  * Resolve pending user-correction: remove `.pending-correction`, append
386
388
  * resolved entry to `.correction-log`, and optionally mark pitfall saved.
387
389
  * Usage: zhuanspec progress resolve-correction <change-id> [--path A|B|C|D] [--note <text>] [--mark-pitfall-saved]
390
+ *
391
+ * @deprecated v2.15.16 Sprint 3 起,`.correction-log` 已从事实源中下线,新 change
392
+ * 的纠偏信号由 user-input-hook 直接写入 `user_inputs.json` 的 correctionSignal /
393
+ * postBaseline / resolution 字段并由 review/knowledge 链路消费。
394
+ *
395
+ * 本命令仅保留给 v2.15.14 及更早版本创建的老 change 做手动兼容:
396
+ * - 仍会删除 `.pending-correction` marker(新旧 change 都需要)
397
+ * - 仍会 append 一条 resolved 记录到 `.correction-log`(仅老 change 会读)
398
+ * - 仍会写 `progress.correctionContext` 用于后续 resolveCorrection 窗口(30 min)
399
+ *
400
+ * 下一个 minor 版本计划迁移:writer 切换到 user_inputs.json.resolution,
401
+ * 届时本命令将退化为一次性提示 + marker 清理。
402
+ * TODO(Sprint 3.2):迁移到 user_inputs.json.resolution 字段
388
403
  */
389
404
  async resolveCorrection(changeId, options) {
390
405
  const cwd = resolveZhuanSpecRoot();
@@ -393,8 +408,24 @@ export class ProgressCommand {
393
408
  throw new Error(`Change '${changeId}' not found`);
394
409
  }
395
410
  const pendingMarker = path.join(changeDir, '.pending-correction');
411
+ // TODO(Sprint 3.2):.correction-log 已于 v2.15.16 下线,此处仅用于老 change 兼容
396
412
  const logPath = path.join(changeDir, '.correction-log');
397
413
  let hadMarker = false;
414
+ // Task 6:先读 marker(可能包含 promptId/phaseInputIndex/promptSnippet)再删除
415
+ let markerMeta = {};
416
+ try {
417
+ const markerRaw = await FileSystemUtils.readFile(pendingMarker);
418
+ const parsed = JSON.parse(markerRaw);
419
+ markerMeta = {
420
+ promptId: parsed.promptId,
421
+ phaseInputIndex: parsed.phaseInputIndex,
422
+ promptSnippet: parsed.promptSnippet,
423
+ reason: parsed.reason,
424
+ };
425
+ }
426
+ catch {
427
+ // marker 不存在或解析失败均不阻断
428
+ }
398
429
  // Remove pending marker (safe if missing)
399
430
  try {
400
431
  await fs.unlink(pendingMarker);
@@ -404,6 +435,7 @@ export class ProgressCommand {
404
435
  // ignore missing marker
405
436
  }
406
437
  // Append resolved entry to correction-log
438
+ // TODO(Sprint 3.2):迁移到 user_inputs.json.resolution 字段,届时删除此块
407
439
  const entry = {
408
440
  ts: new Date().toISOString(),
409
441
  event: 'resolved',
@@ -431,8 +463,16 @@ export class ProgressCommand {
431
463
  path: options?.path || 'A',
432
464
  expiresAt: new Date(now.getTime() + 30 * 60 * 1000).toISOString(),
433
465
  phase: progress.phase || 'apply',
466
+ // Task 6:三路径 triggeredBy 回填——从 marker 或 user_inputs.json 最新一条拼接元数据
467
+ source: 'correction-keyword',
468
+ promptId: markerMeta.promptId,
469
+ promptIndex: markerMeta.phaseInputIndex,
470
+ promptSnippet: markerMeta.promptSnippet,
434
471
  };
435
- progress.lastUpdatedAt = now.toISOString();
472
+ // v2.15.16:与 phaseDurations.startedAt / lastUpdatedAt 等字段对齐,统一使用北京时间
473
+ // naive 字符串。correctionContext.resolvedAt / expiresAt 仍保留 ISOString —— 它们
474
+ // 走 30min TTL(new Date(expiresAt) < Date.now())比较,ISO 解析更稳健。
475
+ progress.lastUpdatedAt = getBeijingTime();
436
476
  await fs.writeFile(progressPath, JSON.stringify(progress, null, 2), 'utf-8');
437
477
  }
438
478
  }
@@ -447,7 +487,8 @@ export class ProgressCommand {
447
487
  const raw = await FileSystemUtils.readFile(progressPath);
448
488
  const progress = JSON.parse(raw);
449
489
  progress.askedPitfallSaved = true;
450
- progress.lastUpdatedAt = new Date().toISOString();
490
+ // v2.15.16:统一北京时间格式
491
+ progress.lastUpdatedAt = getBeijingTime();
451
492
  await fs.writeFile(progressPath, JSON.stringify(progress, null, 2), 'utf-8');
452
493
  }
453
494
  catch {
@@ -464,6 +505,10 @@ export class ProgressCommand {
464
505
  if (options?.markPitfallSaved)
465
506
  console.log(` - askedPitfallSaved: true`);
466
507
  console.log();
508
+ // v2.15.16 Sprint 3:友好提示——新 change 无需调用此命令
509
+ console.log('\x1b[90m[deprecated] 此命令仅用于 v2.15.14 及更早版本的老 change 兼容。\n' +
510
+ ' 新 change 的纠偏状态已由 user-input-hook 自动写入 user_inputs.json,\n' +
511
+ ' 无需手动 resolve。\x1b[0m\n');
467
512
  }
468
513
  /**
469
514
  * Show current pending correction status and recent correction-log entries.
@@ -500,6 +545,7 @@ export class ProgressCommand {
500
545
  console.log('Pending correction: none');
501
546
  }
502
547
  // Correction log tail
548
+ // TODO(Sprint 3.2):.correction-log 已下线,此段仅展示老 change 历史记录
503
549
  if (await FileSystemUtils.fileExists(logPath)) {
504
550
  const content = await FileSystemUtils.readFile(logPath);
505
551
  const lines = content.split('\n').filter(l => l.trim().length > 0);
@@ -517,6 +563,9 @@ export class ProgressCommand {
517
563
  }
518
564
  else {
519
565
  console.log('\nCorrection log: (empty)');
566
+ // v2.15.16 Sprint 3:新 change 不再写 .correction-log,提示正确的查询渠道
567
+ console.log('\x1b[90m 提示:自 v2.15.16 起,纠偏事件由 user-input-hook 写入 user_inputs.json。\n' +
568
+ ' 查看当前 change 纠偏汇总请看:zhuanspec/changes/<id>/user_inputs.json\x1b[0m');
520
569
  }
521
570
  // askedPitfallSaved status
522
571
  const progressPath = path.join(changeDir, 'metrics', 'progress.json');
@@ -531,5 +580,302 @@ export class ProgressCommand {
531
580
  }
532
581
  console.log();
533
582
  }
583
+ /**
584
+ * List pending correction candidates with trivial-ack filtering and dedup.
585
+ * Usage: zhuanspec progress list-corrections <change-id> [--out <path>] [--json]
586
+ *
587
+ * 默认会把 markdown 清单写到 `zhuanspec/changes/<id>/review/pending-corrections.md`,
588
+ * 供 review 阶段「踩坑沉淀」步骤的 askUserQuestion 三选项决策使用。
589
+ * `--json` 模式只走 stdout JSON,不写文件,便于 skill / 测试程序消费。
590
+ */
591
+ async listCorrections(changeId, options) {
592
+ const cwd = resolveZhuanSpecRoot();
593
+ const changeDir = path.join(cwd, 'zhuanspec', 'changes', changeId);
594
+ if (!await FileSystemUtils.directoryExists(changeDir)) {
595
+ throw new Error(`Change '${changeId}' not found`);
596
+ }
597
+ const result = await selectCorrectionCandidates(cwd, changeId);
598
+ if (options?.json) {
599
+ // stdout JSON 模式:用于程序化消费(不写文件)
600
+ console.log(JSON.stringify(result, null, 2));
601
+ return;
602
+ }
603
+ const markdown = renderPendingCorrectionsMarkdown(result);
604
+ const outPath = options?.out
605
+ ? (path.isAbsolute(options.out) ? options.out : path.join(cwd, options.out))
606
+ : path.join(changeDir, 'review', 'pending-corrections.md');
607
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
608
+ await fs.writeFile(outPath, markdown, 'utf-8');
609
+ const rel = path.relative(cwd, outPath) || outPath;
610
+ console.log(`\n✓ Pending corrections written to: ${rel}`);
611
+ console.log(` - 数据源: ${result.level === 1 ? 'Level 1 (correctionSignal)' : result.level === 2 ? 'Level 2 (summary 关键词)' : '无'}`);
612
+ console.log(` - 原始条目: ${result.totalRaw}`);
613
+ console.log(` - 有效候选: ${result.candidates.length}`);
614
+ console.log(` - 已过滤: ${result.dropped.length}`);
615
+ if (result.notice)
616
+ console.log(` - 提示: ${result.notice}`);
617
+ console.log('');
618
+ }
619
+ /**
620
+ * Mark phase baseline manually.
621
+ * Usage: zhuanspec progress mark-baseline <change-id> <phase> [--force]
622
+ *
623
+ * 仅允许 techDesign / propose / apply / review 四个 phase;
624
+ * 若已有 markedAt 且未指定 --force,则报告不改;
625
+ * 否则写入 markedBy='manual-cli' 并 atomicWriteJson 保存。
626
+ */
627
+ async markBaseline(changeId, phase, options) {
628
+ const ALLOWED = ['techDesign', 'propose', 'apply', 'review'];
629
+ const normalized = ALLOWED.find(p => p.toLowerCase() === phase.toLowerCase());
630
+ if (!normalized) {
631
+ throw new Error(`Invalid phase for mark-baseline: ${phase}. Allowed: ${ALLOWED.join(', ')}`);
632
+ }
633
+ const cwd = resolveZhuanSpecRoot();
634
+ const changeDir = path.join(cwd, 'zhuanspec', 'changes', changeId);
635
+ if (!await FileSystemUtils.directoryExists(changeDir)) {
636
+ throw new Error(`Change '${changeId}' not found`);
637
+ }
638
+ const progressPath = path.join(changeDir, 'metrics', 'progress.json');
639
+ if (!await FileSystemUtils.fileExists(progressPath)) {
640
+ throw new Error(`progress.json not found: ${progressPath}`);
641
+ }
642
+ const progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
643
+ progress.phaseBaselines = progress.phaseBaselines || {};
644
+ const existing = progress.phaseBaselines[normalized];
645
+ if (existing?.markedAt && !options?.force) {
646
+ console.log(`\nℹ️ Phase baseline already marked: ${normalized} @ ${existing.markedAt} (by ${existing.markedBy}). Use --force to overwrite.\n`);
647
+ return;
648
+ }
649
+ const now = getBeijingTime();
650
+ const entry = {
651
+ markedAt: now,
652
+ markedBy: 'manual-cli',
653
+ firstStopAt: existing?.firstStopAt,
654
+ };
655
+ progress.phaseBaselines[normalized] = entry;
656
+ progress.lastUpdatedAt = now;
657
+ await atomicWriteJson(progressPath, progress);
658
+ // 埋点:baseline.mark
659
+ appendAccuracyDebugLog(changeDir, {
660
+ type: 'baseline.mark',
661
+ phase: normalized,
662
+ trigger: 'manual-cli',
663
+ changeId,
664
+ input: { force: !!options?.force },
665
+ output: {
666
+ markedBy: 'manual-cli',
667
+ existed: !!existing?.markedAt,
668
+ markedAt: now,
669
+ },
670
+ });
671
+ console.log(`\n✓ Phase baseline marked: ${changeId} / ${normalized} @ ${now} (by manual-cli${existing?.markedAt ? ', force-overwrote' : ''})\n`);
672
+ }
673
+ /**
674
+ * Recompute phaseAccuracy buckets and accuracyRate from scratch.
675
+ * Usage: zhuanspec progress recompute <change-id>
676
+ *
677
+ * 流程:
678
+ * 1. 清空 accuracy.phaseAccuracy
679
+ * 2. 遍历 accuracy.correctionEdits,按 (phase, trackedKind) 累加到桶 userCorrectionLines
680
+ * 3. 将非纠偏的 AI 记录按 edit.phase + edit.trackedKind 累加到桶 aiLinesAdded / aiTotalLines
681
+ * 4. recomputeAccuracyRate + persistAccuracyJson
682
+ *
683
+ * 设计说明:历史 correctionEdits 中 source==='user' 的 edit 即为纠偏,
684
+ * source==='ai' 的 edit 则视为 AI 新增行数;顶层聚合字段保持不变。
685
+ */
686
+ async recompute(changeId) {
687
+ const cwd = resolveZhuanSpecRoot();
688
+ const changeDir = path.join(cwd, 'zhuanspec', 'changes', changeId);
689
+ if (!await FileSystemUtils.directoryExists(changeDir)) {
690
+ throw new Error(`Change '${changeId}' not found`);
691
+ }
692
+ const progressPath = path.join(changeDir, 'metrics', 'progress.json');
693
+ if (!await FileSystemUtils.fileExists(progressPath)) {
694
+ throw new Error(`progress.json not found: ${progressPath}`);
695
+ }
696
+ const progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
697
+ if (!progress.accuracy) {
698
+ console.log(`\nℹ️ No accuracy data to recompute for '${changeId}'.\n`);
699
+ return;
700
+ }
701
+ const edits = progress.accuracy.correctionEdits || [];
702
+ // 步骤 1:清空桶
703
+ progress.accuracy.phaseAccuracy = [];
704
+ let rebuiltBucketEntries = 0;
705
+ const ALLOWED_PHASES = ['techDesign', 'propose', 'apply', 'review'];
706
+ for (const edit of edits) {
707
+ const ph = edit.phase;
708
+ const kind = edit.trackedKind;
709
+ if (!ph || !kind || !ALLOWED_PHASES.includes(ph))
710
+ continue;
711
+ const bucket = getOrCreatePhaseBucket(progress, ph, kind);
712
+ const lines = edit.editLines || 0;
713
+ if (edit.source === 'user') {
714
+ bucket.userCorrectionLines += lines;
715
+ const promptId = edit.triggeredBy?.promptId;
716
+ if (promptId && !bucket.correctionPromptIds.includes(promptId)) {
717
+ bucket.correctionPromptIds.push(promptId);
718
+ }
719
+ }
720
+ else if (edit.source === 'ai') {
721
+ bucket.aiLinesAdded += lines;
722
+ bucket.aiTotalLines += lines;
723
+ }
724
+ bucket.lastUpdatedAt = edit.timestamp || bucket.lastUpdatedAt;
725
+ rebuiltBucketEntries++;
726
+ }
727
+ recomputeAccuracyRate(progress, { changeDir, trigger: 'cli-recompute' });
728
+ progress.lastUpdatedAt = getBeijingTime();
729
+ await atomicWriteJson(progressPath, progress);
730
+ await persistAccuracyJson(changeDir, progress);
731
+ const buckets = progress.accuracy.phaseAccuracy || [];
732
+ console.log(`\n✓ Recomputed accuracy for '${changeId}'`);
733
+ console.log(` - correctionEdits processed: ${edits.length} (accepted=${rebuiltBucketEntries})`);
734
+ console.log(` - phaseAccuracy buckets: ${buckets.length}`);
735
+ for (const b of buckets) {
736
+ console.log(` [${b.phase}/${b.trackedKind}] aiTotal=${b.aiTotalLines} correction=${b.userCorrectionLines} rate=${(b.accuracyRate * 100).toFixed(2)}%`);
737
+ }
738
+ const topRate = (progress.accuracy.accuracyRate ?? 1) * 100;
739
+ console.log(` - top-level accuracyRate: ${topRate.toFixed(2)}%\n`);
740
+ }
741
+ /**
742
+ * Override accuracy rate for a specific phase.
743
+ * Usage: zhuanspec progress override-phase <change-id> <phase> --rate <0..1> --reason <text>
744
+ *
745
+ * 完整保留纠正前的桶级快照(rate/raw/aiTotal/correction)到 overrideHistory,
746
+ * 供事后差异分析 "raw vs overridden delta" 反向驱动改进。
747
+ */
748
+ async overridePhase(changeId, phase, options) {
749
+ const ALLOWED = ['techDesign', 'propose', 'apply', 'review'];
750
+ const normalized = ALLOWED.find(p => p.toLowerCase() === phase.toLowerCase());
751
+ if (!normalized) {
752
+ throw new Error(`Invalid phase for override-phase: ${phase}. Allowed: ${ALLOWED.join(', ')}`);
753
+ }
754
+ const rate = parseFloat(options.rate);
755
+ if (Number.isNaN(rate) || rate < 0 || rate > 1) {
756
+ throw new Error('Rate must be a number between 0.0 and 1.0');
757
+ }
758
+ const reason = (options.reason ?? '').trim();
759
+ if (!reason) {
760
+ throw new Error('Missing --reason (required for audit trail)');
761
+ }
762
+ const cwd = resolveZhuanSpecRoot();
763
+ const changeDir = path.join(cwd, 'zhuanspec', 'changes', changeId);
764
+ if (!await FileSystemUtils.directoryExists(changeDir)) {
765
+ throw new Error(`Change '${changeId}' not found`);
766
+ }
767
+ const progressPath = path.join(changeDir, 'metrics', 'progress.json');
768
+ if (!await FileSystemUtils.fileExists(progressPath)) {
769
+ throw new Error(`progress.json not found: ${progressPath}`);
770
+ }
771
+ const progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
772
+ const buckets = progress.accuracy?.phaseAccuracy?.filter(b => b.phase === normalized) ?? [];
773
+ if (buckets.length === 0) {
774
+ throw new Error(`No phaseAccuracy bucket for ${changeId}/${normalized}. Historical data or phase has no tracked edits yet.`);
775
+ }
776
+ const result = applyPhaseOverride(progress, normalized, rate, reason, {
777
+ changeDir,
778
+ trigger: 'manual-cli',
779
+ });
780
+ progress.lastUpdatedAt = getBeijingTime();
781
+ await atomicWriteJson(progressPath, progress);
782
+ await persistAccuracyJson(changeDir, progress);
783
+ console.log(`\n✓ Phase accuracy overridden: ${changeId} / ${normalized}`);
784
+ console.log(` - Affected buckets: ${result.affectedBuckets}`);
785
+ for (const d of result.details) {
786
+ console.log(` [${normalized}/${d.trackedKind}] raw=${(d.originalRateRaw * 100).toFixed(2)}% -> overridden=${(d.overriddenRate * 100).toFixed(2)}% (aiTotal=${d.originalAiTotalLines}, correction=${d.originalUserCorrectionLines})`);
787
+ }
788
+ console.log(` - Reason: ${reason}\n`);
789
+ }
790
+ /**
791
+ * Mark a phase's accuracy as confirmed by user.
792
+ * Usage: zhuanspec progress confirm-phase <change-id> <phase>
793
+ *
794
+ * 对该 phase 的所有桶写入 confirmed: { at, by: 'manual-cli' },
795
+ * 后续 phase-transition-check 判定时会读此标记放行。
796
+ */
797
+ async confirmPhase(changeId, phase) {
798
+ const ALLOWED = ['techDesign', 'propose', 'apply', 'review'];
799
+ const normalized = ALLOWED.find(p => p.toLowerCase() === phase.toLowerCase());
800
+ if (!normalized) {
801
+ throw new Error(`Invalid phase for confirm-phase: ${phase}. Allowed: ${ALLOWED.join(', ')}`);
802
+ }
803
+ const cwd = resolveZhuanSpecRoot();
804
+ const changeDir = path.join(cwd, 'zhuanspec', 'changes', changeId);
805
+ if (!await FileSystemUtils.directoryExists(changeDir)) {
806
+ throw new Error(`Change '${changeId}' not found`);
807
+ }
808
+ const progressPath = path.join(changeDir, 'metrics', 'progress.json');
809
+ if (!await FileSystemUtils.fileExists(progressPath)) {
810
+ throw new Error(`progress.json not found: ${progressPath}`);
811
+ }
812
+ const progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
813
+ const buckets = progress.accuracy?.phaseAccuracy?.filter(b => b.phase === normalized) ?? [];
814
+ if (buckets.length === 0) {
815
+ throw new Error(`No phaseAccuracy bucket for ${changeId}/${normalized}. Nothing to confirm.`);
816
+ }
817
+ const result = confirmPhaseAccuracy(progress, normalized, 'manual-cli', { changeDir });
818
+ progress.lastUpdatedAt = getBeijingTime();
819
+ await atomicWriteJson(progressPath, progress);
820
+ await persistAccuracyJson(changeDir, progress);
821
+ console.log(`\n✓ Phase accuracy confirmed: ${changeId} / ${normalized} (buckets=${result.affectedBuckets}, by=manual-cli)\n`);
822
+ }
823
+ /**
824
+ * Show per-bucket snapshot for a specific phase (read-only).
825
+ * Usage: zhuanspec progress show-phase-accuracy <change-id> <phase>
826
+ *
827
+ * 用于 phase-transition-check 阻断后 AI 先查看数据,再调 AskUserQuestion 纠正。
828
+ */
829
+ async showPhaseAccuracy(changeId, phase, options) {
830
+ const ALLOWED = ['techDesign', 'propose', 'apply', 'review'];
831
+ const normalized = ALLOWED.find(p => p.toLowerCase() === phase.toLowerCase());
832
+ if (!normalized) {
833
+ throw new Error(`Invalid phase: ${phase}. Allowed: ${ALLOWED.join(', ')}`);
834
+ }
835
+ const cwd = resolveZhuanSpecRoot();
836
+ const changeDir = path.join(cwd, 'zhuanspec', 'changes', changeId);
837
+ if (!await FileSystemUtils.directoryExists(changeDir)) {
838
+ throw new Error(`Change '${changeId}' not found`);
839
+ }
840
+ const progressPath = path.join(changeDir, 'metrics', 'progress.json');
841
+ if (!await FileSystemUtils.fileExists(progressPath)) {
842
+ throw new Error(`progress.json not found: ${progressPath}`);
843
+ }
844
+ const progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
845
+ const buckets = progress.accuracy?.phaseAccuracy?.filter(b => b.phase === normalized) ?? [];
846
+ if (options?.json) {
847
+ console.log(JSON.stringify({
848
+ changeId,
849
+ phase: normalized,
850
+ buckets,
851
+ legacy: buckets.length === 0,
852
+ }, null, 2));
853
+ return;
854
+ }
855
+ console.log(`\n📊 Phase accuracy: ${changeId} / ${normalized}`);
856
+ console.log('─'.repeat(60));
857
+ if (buckets.length === 0) {
858
+ console.log(' —(历史数据,无 phaseAccuracy 桶)');
859
+ console.log();
860
+ return;
861
+ }
862
+ for (const b of buckets) {
863
+ const raw = ((b.accuracyRateRaw ?? b.accuracyRate) * 100).toFixed(2);
864
+ const show = ((b.accuracyRate ?? 1) * 100).toFixed(2);
865
+ const overrideCount = b.overrideHistory?.length ?? 0;
866
+ const confirmed = b.confirmed ? `✓ ${b.confirmed.by}@${b.confirmed.at}` : '✗ 未确认';
867
+ console.log(` [${b.phase}/${b.trackedKind}]`);
868
+ console.log(` AI产出: ${b.aiTotalLines} 行 (added=${b.aiLinesAdded}, modified=${b.aiLinesModified})`);
869
+ console.log(` 用户纠偏: ${b.userCorrectionLines} 行 (prompts=${b.correctionPromptIds.length})`);
870
+ console.log(` raw/展示: ${raw}% / ${show}%`);
871
+ console.log(` override次数: ${overrideCount}`);
872
+ console.log(` confirmed: ${confirmed}`);
873
+ if (overrideCount > 0 && b.overrideHistory) {
874
+ const last = b.overrideHistory[overrideCount - 1];
875
+ console.log(` 最近 reason: ${last.reason} (${last.timestamp})`);
876
+ }
877
+ }
878
+ console.log();
879
+ }
534
880
  }
535
881
  //# sourceMappingURL=progress.js.map
@@ -165,6 +165,34 @@ export class ArchiveCommand {
165
165
  }
166
166
  // Single source of truth for archive gate: preArchiveHook (review/tests/spec-code consistency)
167
167
  await preArchiveHook({ change: changeName, json: false });
168
+ // === Task 5: 归档前 auto-confirm 未确认的分阶段准确率桶 ===
169
+ // 预期效果:archive 不可逆前,对所有 phaseAccuracy 桶写入 confirmed: { at, by: 'auto-archive' }
170
+ // 历史数据(无 phaseAccuracy 或空)自然跳过;失败不阻断归档主流程
171
+ try {
172
+ const { confirmPhaseAccuracy } = await import('./metrics/code-accuracy.js');
173
+ const progressPath = path.join(changeDir, 'metrics', 'progress.json');
174
+ try {
175
+ await fs.access(progressPath);
176
+ const progressRaw = await fs.readFile(progressPath, 'utf-8');
177
+ const progress = JSON.parse(progressRaw);
178
+ const buckets = progress?.accuracy?.phaseAccuracy ?? [];
179
+ const pending = buckets.filter((b) => !b.confirmed);
180
+ if (pending.length > 0) {
181
+ const phases = Array.from(new Set(pending.map((b) => b.phase)));
182
+ for (const ph of phases) {
183
+ confirmPhaseAccuracy(progress, ph, 'auto-archive', { changeDir });
184
+ }
185
+ await fs.writeFile(progressPath, JSON.stringify(progress, null, 2), 'utf-8');
186
+ console.log(chalk.gray(`已自动确认 ${pending.length} 个分阶段准确率桶(by: auto-archive)`));
187
+ }
188
+ }
189
+ catch {
190
+ // progress.json 不存在或解析失败→历史数据/空变更,安全跳过
191
+ }
192
+ }
193
+ catch {
194
+ // 模块导入异常或其他非预期错误,不阻断归档
195
+ }
168
196
  // Handle spec updates unless skipSpecs flag is set
169
197
  if (options.skipSpecs) {
170
198
  console.log('跳过规范更新(已提供 --skip-specs 标志)。');
@@ -13,12 +13,22 @@
13
13
  *
14
14
  * ~/.codex/ (or $CODEX_HOME)
15
15
  * config.toml - Per-project [projects."…"] trusted_level = "trusted"
16
- * block (wrapped in TRUST markers keyed by absolute path)
16
+ * block (wrapped in TRUST markers keyed by absolute path).
17
+ * Also receives [hooks.state."<abs>:<event>:<i>:<j>"]
18
+ * entries grafted from other already-trusted projects,
19
+ * wrapped in HOOK_STATE markers keyed by config.toml path.
17
20
  *
18
21
  * Codex does **not** load project-scoped hooks/subagents unless the project
19
22
  * is explicitly marked trusted in the global config (see openai/codex#14601),
20
23
  * so we opportunistically write that trust entry during `zhuanspec init`.
21
24
  *
25
+ * Codex CLI >= 0.129 additionally requires each hook's content hash to be
26
+ * pre-approved (openai/codex#20321) or the user sees "N hooks need review".
27
+ * The `trusted_hash` in `[hooks.state.<key>]` depends only on normalised hook
28
+ * *content* (event + matcher + command), not on the key path, so we clone a
29
+ * previously approved hash to the current project's key to skip the manual
30
+ * review prompt in fresh workspaces.
31
+ *
22
32
  * Idempotency:
23
33
  * - Hooks block is regenerated inside ZHUANSPEC markers on every run.
24
34
  * - Trust entries are keyed by absolute path; repeated runs no-op.
@@ -50,6 +60,24 @@ export interface CodexConfigureResult {
50
60
  * installs to avoid user confusion. Empty when nothing needed removing.
51
61
  */
52
62
  legacyPromptsCleaned: string[];
63
+ /**
64
+ * Summary of per-hook content-trust seeding in `~/.codex/config.toml`.
65
+ * Codex CLI >= 0.129 requires every hook handler's SHA-256 fingerprint to
66
+ * be pre-approved via the TUI; we clone hashes from already-trusted
67
+ * projects so freshly initialised workspaces skip the `/hooks` review.
68
+ */
69
+ hookTrust: CodexHookTrustResult;
70
+ }
71
+ /** Outcome of the `ensureHookTrustSeed` pass. */
72
+ export interface CodexHookTrustResult {
73
+ /** Slot suffixes (`<event>:<group>:<handler>`) whose trusted_hash was grafted. */
74
+ written: string[];
75
+ /** Slot suffixes without any seed hash in the global config — user must approve once. */
76
+ skippedNoSeed: string[];
77
+ /** Slot suffixes already owned by a pre-existing, unmanaged `[hooks.state]` entry. */
78
+ skippedNative: string[];
79
+ /** True iff at least one seed hash was found (for any slot) in the global config. */
80
+ seedAvailable: boolean;
53
81
  }
54
82
  export declare class CodexConfigurator implements ToolConfigurator {
55
83
  name: string;
@@ -133,5 +161,31 @@ export declare class CodexConfigurator implements ToolConfigurator {
133
161
  * when the block already matches).
134
162
  */
135
163
  private replaceTrustBlock;
164
+ /**
165
+ * Graft `[hooks.state."<key>"] trusted_hash = "sha256:..."` entries for the
166
+ * current project from any other project that the user has already approved
167
+ * via Codex's `/hooks` TUI. The grafted block is wrapped in HOOK_STATE
168
+ * markers keyed by `configTomlPath` so it stays idempotent across runs and
169
+ * coexists with HOOK_STATE blocks for sibling projects in the same file.
170
+ *
171
+ * Strategy:
172
+ * - Strip our own existing block first to avoid self-referencing seeds.
173
+ * - Scan remaining `[hooks.state."<path>:<event>:<grp>:<hdl>"]` tables;
174
+ * index their `trusted_hash` by the `<event>:<grp>:<hdl>` suffix so the
175
+ * same logical hook in any other project can serve as a seed.
176
+ * - For each ZhuanSpec default slot:
177
+ * * If a native (unmanaged) entry already owns the same key in this
178
+ * project, skip to avoid TOML duplicate-key errors.
179
+ * * Else if a seed exists, emit a managed entry pointing the user's
180
+ * project key at the known-good hash.
181
+ * * Else record the slot in `skippedNoSeed` so callers can surface a
182
+ * "please approve once in Codex" hint.
183
+ *
184
+ * Failures are non-fatal: any unexpected error is swallowed and reported
185
+ * via an empty result with `seedAvailable=false`, since hook trust is a
186
+ * UX optimisation — missing it just means the user sees the regular
187
+ * Codex review prompt.
188
+ */
189
+ private ensureHookTrustSeed;
136
190
  }
137
191
  //# sourceMappingURL=codex.d.ts.map