@zhuan-ai/zhuanspec 2.15.7 → 2.16.0
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/hooks.d.ts +13 -1
- package/dist/cli/hooks.js +75 -2
- package/dist/cli/index.js +94 -4
- package/dist/commands/accuracy.js +5 -1
- package/dist/commands/knowledge.d.ts +21 -0
- package/dist/commands/knowledge.js +80 -0
- package/dist/commands/progress.d.ts +66 -0
- package/dist/commands/progress.js +306 -1
- package/dist/core/archive.js +28 -0
- package/dist/core/configurators/codex.d.ts +55 -1
- package/dist/core/configurators/codex.js +234 -2
- package/dist/core/hooks/collect-knowledge.js +66 -0
- package/dist/core/hooks/deviation-check.d.ts +26 -1
- package/dist/core/hooks/deviation-check.js +32 -184
- package/dist/core/hooks/knowledge-index.d.ts +84 -0
- package/dist/core/hooks/knowledge-index.js +270 -0
- package/dist/core/hooks/record-progress.d.ts +63 -1
- package/dist/core/hooks/record-progress.js +208 -54
- package/dist/core/hooks/summarize.js +90 -2
- package/dist/core/hooks/user-input-hook.d.ts +38 -2
- package/dist/core/hooks/user-input-hook.js +346 -21
- package/dist/core/init.d.ts +11 -0
- package/dist/core/init.js +43 -4
- package/dist/core/metrics/code-accuracy.d.ts +152 -3
- package/dist/core/metrics/code-accuracy.js +323 -19
- package/dist/core/templates/agents-root-stub.d.ts +1 -1
- package/dist/core/templates/agents-root-stub.js +1 -0
- package/dist/core/templates/agents-template.d.ts +1 -1
- package/dist/core/templates/agents-template.js +21 -4
- package/dist/core/templates/codex-hooks-template.d.ts +37 -0
- package/dist/core/templates/codex-hooks-template.js +33 -2
- package/dist/core/templates/slash-command-templates.js +144 -28
- package/dist/core/templates/tasks-template.js +66 -14
- package/dist/core/update.js +17 -0
- package/dist/core/validation/strict-rules.d.ts +28 -0
- package/dist/core/validation/strict-rules.js +284 -0
- package/dist/utils/hook-merge.d.ts +6 -0
- package/dist/utils/hook-merge.js +33 -2
- package/package.json +1 -1
|
@@ -10,7 +10,8 @@ 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';
|
|
15
16
|
export class ProgressCommand {
|
|
16
17
|
/**
|
|
@@ -385,6 +386,19 @@ export class ProgressCommand {
|
|
|
385
386
|
* Resolve pending user-correction: remove `.pending-correction`, append
|
|
386
387
|
* resolved entry to `.correction-log`, and optionally mark pitfall saved.
|
|
387
388
|
* Usage: zhuanspec progress resolve-correction <change-id> [--path A|B|C|D] [--note <text>] [--mark-pitfall-saved]
|
|
389
|
+
*
|
|
390
|
+
* @deprecated v2.15.16 Sprint 3 起,`.correction-log` 已从事实源中下线,新 change
|
|
391
|
+
* 的纠偏信号由 user-input-hook 直接写入 `user_inputs.json` 的 correctionSignal /
|
|
392
|
+
* postBaseline / resolution 字段并由 review/knowledge 链路消费。
|
|
393
|
+
*
|
|
394
|
+
* 本命令仅保留给 v2.15.14 及更早版本创建的老 change 做手动兼容:
|
|
395
|
+
* - 仍会删除 `.pending-correction` marker(新旧 change 都需要)
|
|
396
|
+
* - 仍会 append 一条 resolved 记录到 `.correction-log`(仅老 change 会读)
|
|
397
|
+
* - 仍会写 `progress.correctionContext` 用于后续 resolveCorrection 窗口(30 min)
|
|
398
|
+
*
|
|
399
|
+
* 下一个 minor 版本计划迁移:writer 切换到 user_inputs.json.resolution,
|
|
400
|
+
* 届时本命令将退化为一次性提示 + marker 清理。
|
|
401
|
+
* TODO(Sprint 3.2):迁移到 user_inputs.json.resolution 字段
|
|
388
402
|
*/
|
|
389
403
|
async resolveCorrection(changeId, options) {
|
|
390
404
|
const cwd = resolveZhuanSpecRoot();
|
|
@@ -393,8 +407,24 @@ export class ProgressCommand {
|
|
|
393
407
|
throw new Error(`Change '${changeId}' not found`);
|
|
394
408
|
}
|
|
395
409
|
const pendingMarker = path.join(changeDir, '.pending-correction');
|
|
410
|
+
// TODO(Sprint 3.2):.correction-log 已于 v2.15.16 下线,此处仅用于老 change 兼容
|
|
396
411
|
const logPath = path.join(changeDir, '.correction-log');
|
|
397
412
|
let hadMarker = false;
|
|
413
|
+
// Task 6:先读 marker(可能包含 promptId/phaseInputIndex/promptSnippet)再删除
|
|
414
|
+
let markerMeta = {};
|
|
415
|
+
try {
|
|
416
|
+
const markerRaw = await FileSystemUtils.readFile(pendingMarker);
|
|
417
|
+
const parsed = JSON.parse(markerRaw);
|
|
418
|
+
markerMeta = {
|
|
419
|
+
promptId: parsed.promptId,
|
|
420
|
+
phaseInputIndex: parsed.phaseInputIndex,
|
|
421
|
+
promptSnippet: parsed.promptSnippet,
|
|
422
|
+
reason: parsed.reason,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
// marker 不存在或解析失败均不阻断
|
|
427
|
+
}
|
|
398
428
|
// Remove pending marker (safe if missing)
|
|
399
429
|
try {
|
|
400
430
|
await fs.unlink(pendingMarker);
|
|
@@ -404,6 +434,7 @@ export class ProgressCommand {
|
|
|
404
434
|
// ignore missing marker
|
|
405
435
|
}
|
|
406
436
|
// Append resolved entry to correction-log
|
|
437
|
+
// TODO(Sprint 3.2):迁移到 user_inputs.json.resolution 字段,届时删除此块
|
|
407
438
|
const entry = {
|
|
408
439
|
ts: new Date().toISOString(),
|
|
409
440
|
event: 'resolved',
|
|
@@ -431,6 +462,11 @@ export class ProgressCommand {
|
|
|
431
462
|
path: options?.path || 'A',
|
|
432
463
|
expiresAt: new Date(now.getTime() + 30 * 60 * 1000).toISOString(),
|
|
433
464
|
phase: progress.phase || 'apply',
|
|
465
|
+
// Task 6:三路径 triggeredBy 回填——从 marker 或 user_inputs.json 最新一条拼接元数据
|
|
466
|
+
source: 'correction-keyword',
|
|
467
|
+
promptId: markerMeta.promptId,
|
|
468
|
+
promptIndex: markerMeta.phaseInputIndex,
|
|
469
|
+
promptSnippet: markerMeta.promptSnippet,
|
|
434
470
|
};
|
|
435
471
|
progress.lastUpdatedAt = now.toISOString();
|
|
436
472
|
await fs.writeFile(progressPath, JSON.stringify(progress, null, 2), 'utf-8');
|
|
@@ -464,6 +500,10 @@ export class ProgressCommand {
|
|
|
464
500
|
if (options?.markPitfallSaved)
|
|
465
501
|
console.log(` - askedPitfallSaved: true`);
|
|
466
502
|
console.log();
|
|
503
|
+
// v2.15.16 Sprint 3:友好提示——新 change 无需调用此命令
|
|
504
|
+
console.log('\x1b[90m[deprecated] 此命令仅用于 v2.15.14 及更早版本的老 change 兼容。\n' +
|
|
505
|
+
' 新 change 的纠偏状态已由 user-input-hook 自动写入 user_inputs.json,\n' +
|
|
506
|
+
' 无需手动 resolve。\x1b[0m\n');
|
|
467
507
|
}
|
|
468
508
|
/**
|
|
469
509
|
* Show current pending correction status and recent correction-log entries.
|
|
@@ -500,6 +540,7 @@ export class ProgressCommand {
|
|
|
500
540
|
console.log('Pending correction: none');
|
|
501
541
|
}
|
|
502
542
|
// Correction log tail
|
|
543
|
+
// TODO(Sprint 3.2):.correction-log 已下线,此段仅展示老 change 历史记录
|
|
503
544
|
if (await FileSystemUtils.fileExists(logPath)) {
|
|
504
545
|
const content = await FileSystemUtils.readFile(logPath);
|
|
505
546
|
const lines = content.split('\n').filter(l => l.trim().length > 0);
|
|
@@ -517,6 +558,9 @@ export class ProgressCommand {
|
|
|
517
558
|
}
|
|
518
559
|
else {
|
|
519
560
|
console.log('\nCorrection log: (empty)');
|
|
561
|
+
// v2.15.16 Sprint 3:新 change 不再写 .correction-log,提示正确的查询渠道
|
|
562
|
+
console.log('\x1b[90m 提示:自 v2.15.16 起,纠偏事件由 user-input-hook 写入 user_inputs.json。\n' +
|
|
563
|
+
' 查看当前 change 纠偏汇总请看:zhuanspec/changes/<id>/user_inputs.json\x1b[0m');
|
|
520
564
|
}
|
|
521
565
|
// askedPitfallSaved status
|
|
522
566
|
const progressPath = path.join(changeDir, 'metrics', 'progress.json');
|
|
@@ -531,5 +575,266 @@ export class ProgressCommand {
|
|
|
531
575
|
}
|
|
532
576
|
console.log();
|
|
533
577
|
}
|
|
578
|
+
/**
|
|
579
|
+
* Mark phase baseline manually.
|
|
580
|
+
* Usage: zhuanspec progress mark-baseline <change-id> <phase> [--force]
|
|
581
|
+
*
|
|
582
|
+
* 仅允许 techDesign / propose / apply / review 四个 phase;
|
|
583
|
+
* 若已有 markedAt 且未指定 --force,则报告不改;
|
|
584
|
+
* 否则写入 markedBy='manual-cli' 并 atomicWriteJson 保存。
|
|
585
|
+
*/
|
|
586
|
+
async markBaseline(changeId, phase, options) {
|
|
587
|
+
const ALLOWED = ['techDesign', 'propose', 'apply', 'review'];
|
|
588
|
+
const normalized = ALLOWED.find(p => p.toLowerCase() === phase.toLowerCase());
|
|
589
|
+
if (!normalized) {
|
|
590
|
+
throw new Error(`Invalid phase for mark-baseline: ${phase}. Allowed: ${ALLOWED.join(', ')}`);
|
|
591
|
+
}
|
|
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 progressPath = path.join(changeDir, 'metrics', 'progress.json');
|
|
598
|
+
if (!await FileSystemUtils.fileExists(progressPath)) {
|
|
599
|
+
throw new Error(`progress.json not found: ${progressPath}`);
|
|
600
|
+
}
|
|
601
|
+
const progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
|
|
602
|
+
progress.phaseBaselines = progress.phaseBaselines || {};
|
|
603
|
+
const existing = progress.phaseBaselines[normalized];
|
|
604
|
+
if (existing?.markedAt && !options?.force) {
|
|
605
|
+
console.log(`\nℹ️ Phase baseline already marked: ${normalized} @ ${existing.markedAt} (by ${existing.markedBy}). Use --force to overwrite.\n`);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const now = getBeijingTime();
|
|
609
|
+
const entry = {
|
|
610
|
+
markedAt: now,
|
|
611
|
+
markedBy: 'manual-cli',
|
|
612
|
+
firstStopAt: existing?.firstStopAt,
|
|
613
|
+
};
|
|
614
|
+
progress.phaseBaselines[normalized] = entry;
|
|
615
|
+
progress.lastUpdatedAt = now;
|
|
616
|
+
await atomicWriteJson(progressPath, progress);
|
|
617
|
+
// 埋点:baseline.mark
|
|
618
|
+
appendAccuracyDebugLog(changeDir, {
|
|
619
|
+
type: 'baseline.mark',
|
|
620
|
+
phase: normalized,
|
|
621
|
+
trigger: 'manual-cli',
|
|
622
|
+
changeId,
|
|
623
|
+
input: { force: !!options?.force },
|
|
624
|
+
output: {
|
|
625
|
+
markedBy: 'manual-cli',
|
|
626
|
+
existed: !!existing?.markedAt,
|
|
627
|
+
markedAt: now,
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
console.log(`\n✓ Phase baseline marked: ${changeId} / ${normalized} @ ${now} (by manual-cli${existing?.markedAt ? ', force-overwrote' : ''})\n`);
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Recompute phaseAccuracy buckets and accuracyRate from scratch.
|
|
634
|
+
* Usage: zhuanspec progress recompute <change-id>
|
|
635
|
+
*
|
|
636
|
+
* 流程:
|
|
637
|
+
* 1. 清空 accuracy.phaseAccuracy
|
|
638
|
+
* 2. 遍历 accuracy.correctionEdits,按 (phase, trackedKind) 累加到桶 userCorrectionLines
|
|
639
|
+
* 3. 将非纠偏的 AI 记录按 edit.phase + edit.trackedKind 累加到桶 aiLinesAdded / aiTotalLines
|
|
640
|
+
* 4. recomputeAccuracyRate + persistAccuracyJson
|
|
641
|
+
*
|
|
642
|
+
* 设计说明:历史 correctionEdits 中 source==='user' 的 edit 即为纠偏,
|
|
643
|
+
* source==='ai' 的 edit 则视为 AI 新增行数;顶层聚合字段保持不变。
|
|
644
|
+
*/
|
|
645
|
+
async recompute(changeId) {
|
|
646
|
+
const cwd = resolveZhuanSpecRoot();
|
|
647
|
+
const changeDir = path.join(cwd, 'zhuanspec', 'changes', changeId);
|
|
648
|
+
if (!await FileSystemUtils.directoryExists(changeDir)) {
|
|
649
|
+
throw new Error(`Change '${changeId}' not found`);
|
|
650
|
+
}
|
|
651
|
+
const progressPath = path.join(changeDir, 'metrics', 'progress.json');
|
|
652
|
+
if (!await FileSystemUtils.fileExists(progressPath)) {
|
|
653
|
+
throw new Error(`progress.json not found: ${progressPath}`);
|
|
654
|
+
}
|
|
655
|
+
const progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
|
|
656
|
+
if (!progress.accuracy) {
|
|
657
|
+
console.log(`\nℹ️ No accuracy data to recompute for '${changeId}'.\n`);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const edits = progress.accuracy.correctionEdits || [];
|
|
661
|
+
// 步骤 1:清空桶
|
|
662
|
+
progress.accuracy.phaseAccuracy = [];
|
|
663
|
+
let rebuiltBucketEntries = 0;
|
|
664
|
+
const ALLOWED_PHASES = ['techDesign', 'propose', 'apply', 'review'];
|
|
665
|
+
for (const edit of edits) {
|
|
666
|
+
const ph = edit.phase;
|
|
667
|
+
const kind = edit.trackedKind;
|
|
668
|
+
if (!ph || !kind || !ALLOWED_PHASES.includes(ph))
|
|
669
|
+
continue;
|
|
670
|
+
const bucket = getOrCreatePhaseBucket(progress, ph, kind);
|
|
671
|
+
const lines = edit.editLines || 0;
|
|
672
|
+
if (edit.source === 'user') {
|
|
673
|
+
bucket.userCorrectionLines += lines;
|
|
674
|
+
const promptId = edit.triggeredBy?.promptId;
|
|
675
|
+
if (promptId && !bucket.correctionPromptIds.includes(promptId)) {
|
|
676
|
+
bucket.correctionPromptIds.push(promptId);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
else if (edit.source === 'ai') {
|
|
680
|
+
bucket.aiLinesAdded += lines;
|
|
681
|
+
bucket.aiTotalLines += lines;
|
|
682
|
+
}
|
|
683
|
+
bucket.lastUpdatedAt = edit.timestamp || bucket.lastUpdatedAt;
|
|
684
|
+
rebuiltBucketEntries++;
|
|
685
|
+
}
|
|
686
|
+
recomputeAccuracyRate(progress, { changeDir, trigger: 'cli-recompute' });
|
|
687
|
+
progress.lastUpdatedAt = getBeijingTime();
|
|
688
|
+
await atomicWriteJson(progressPath, progress);
|
|
689
|
+
await persistAccuracyJson(changeDir, progress);
|
|
690
|
+
const buckets = progress.accuracy.phaseAccuracy || [];
|
|
691
|
+
console.log(`\n✓ Recomputed accuracy for '${changeId}'`);
|
|
692
|
+
console.log(` - correctionEdits processed: ${edits.length} (accepted=${rebuiltBucketEntries})`);
|
|
693
|
+
console.log(` - phaseAccuracy buckets: ${buckets.length}`);
|
|
694
|
+
for (const b of buckets) {
|
|
695
|
+
console.log(` [${b.phase}/${b.trackedKind}] aiTotal=${b.aiTotalLines} correction=${b.userCorrectionLines} rate=${(b.accuracyRate * 100).toFixed(2)}%`);
|
|
696
|
+
}
|
|
697
|
+
const topRate = (progress.accuracy.accuracyRate ?? 1) * 100;
|
|
698
|
+
console.log(` - top-level accuracyRate: ${topRate.toFixed(2)}%\n`);
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Override accuracy rate for a specific phase.
|
|
702
|
+
* Usage: zhuanspec progress override-phase <change-id> <phase> --rate <0..1> --reason <text>
|
|
703
|
+
*
|
|
704
|
+
* 完整保留纠正前的桶级快照(rate/raw/aiTotal/correction)到 overrideHistory,
|
|
705
|
+
* 供事后差异分析 "raw vs overridden delta" 反向驱动改进。
|
|
706
|
+
*/
|
|
707
|
+
async overridePhase(changeId, phase, options) {
|
|
708
|
+
const ALLOWED = ['techDesign', 'propose', 'apply', 'review'];
|
|
709
|
+
const normalized = ALLOWED.find(p => p.toLowerCase() === phase.toLowerCase());
|
|
710
|
+
if (!normalized) {
|
|
711
|
+
throw new Error(`Invalid phase for override-phase: ${phase}. Allowed: ${ALLOWED.join(', ')}`);
|
|
712
|
+
}
|
|
713
|
+
const rate = parseFloat(options.rate);
|
|
714
|
+
if (Number.isNaN(rate) || rate < 0 || rate > 1) {
|
|
715
|
+
throw new Error('Rate must be a number between 0.0 and 1.0');
|
|
716
|
+
}
|
|
717
|
+
const reason = (options.reason ?? '').trim();
|
|
718
|
+
if (!reason) {
|
|
719
|
+
throw new Error('Missing --reason (required for audit trail)');
|
|
720
|
+
}
|
|
721
|
+
const cwd = resolveZhuanSpecRoot();
|
|
722
|
+
const changeDir = path.join(cwd, 'zhuanspec', 'changes', changeId);
|
|
723
|
+
if (!await FileSystemUtils.directoryExists(changeDir)) {
|
|
724
|
+
throw new Error(`Change '${changeId}' not found`);
|
|
725
|
+
}
|
|
726
|
+
const progressPath = path.join(changeDir, 'metrics', 'progress.json');
|
|
727
|
+
if (!await FileSystemUtils.fileExists(progressPath)) {
|
|
728
|
+
throw new Error(`progress.json not found: ${progressPath}`);
|
|
729
|
+
}
|
|
730
|
+
const progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
|
|
731
|
+
const buckets = progress.accuracy?.phaseAccuracy?.filter(b => b.phase === normalized) ?? [];
|
|
732
|
+
if (buckets.length === 0) {
|
|
733
|
+
throw new Error(`No phaseAccuracy bucket for ${changeId}/${normalized}. Historical data or phase has no tracked edits yet.`);
|
|
734
|
+
}
|
|
735
|
+
const result = applyPhaseOverride(progress, normalized, rate, reason, {
|
|
736
|
+
changeDir,
|
|
737
|
+
trigger: 'manual-cli',
|
|
738
|
+
});
|
|
739
|
+
progress.lastUpdatedAt = getBeijingTime();
|
|
740
|
+
await atomicWriteJson(progressPath, progress);
|
|
741
|
+
await persistAccuracyJson(changeDir, progress);
|
|
742
|
+
console.log(`\n✓ Phase accuracy overridden: ${changeId} / ${normalized}`);
|
|
743
|
+
console.log(` - Affected buckets: ${result.affectedBuckets}`);
|
|
744
|
+
for (const d of result.details) {
|
|
745
|
+
console.log(` [${normalized}/${d.trackedKind}] raw=${(d.originalRateRaw * 100).toFixed(2)}% -> overridden=${(d.overriddenRate * 100).toFixed(2)}% (aiTotal=${d.originalAiTotalLines}, correction=${d.originalUserCorrectionLines})`);
|
|
746
|
+
}
|
|
747
|
+
console.log(` - Reason: ${reason}\n`);
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Mark a phase's accuracy as confirmed by user.
|
|
751
|
+
* Usage: zhuanspec progress confirm-phase <change-id> <phase>
|
|
752
|
+
*
|
|
753
|
+
* 对该 phase 的所有桶写入 confirmed: { at, by: 'manual-cli' },
|
|
754
|
+
* 后续 phase-transition-check 判定时会读此标记放行。
|
|
755
|
+
*/
|
|
756
|
+
async confirmPhase(changeId, phase) {
|
|
757
|
+
const ALLOWED = ['techDesign', 'propose', 'apply', 'review'];
|
|
758
|
+
const normalized = ALLOWED.find(p => p.toLowerCase() === phase.toLowerCase());
|
|
759
|
+
if (!normalized) {
|
|
760
|
+
throw new Error(`Invalid phase for confirm-phase: ${phase}. Allowed: ${ALLOWED.join(', ')}`);
|
|
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}. Nothing to confirm.`);
|
|
775
|
+
}
|
|
776
|
+
const result = confirmPhaseAccuracy(progress, normalized, 'manual-cli', { changeDir });
|
|
777
|
+
progress.lastUpdatedAt = getBeijingTime();
|
|
778
|
+
await atomicWriteJson(progressPath, progress);
|
|
779
|
+
await persistAccuracyJson(changeDir, progress);
|
|
780
|
+
console.log(`\n✓ Phase accuracy confirmed: ${changeId} / ${normalized} (buckets=${result.affectedBuckets}, by=manual-cli)\n`);
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Show per-bucket snapshot for a specific phase (read-only).
|
|
784
|
+
* Usage: zhuanspec progress show-phase-accuracy <change-id> <phase>
|
|
785
|
+
*
|
|
786
|
+
* 用于 phase-transition-check 阻断后 AI 先查看数据,再调 AskUserQuestion 纠正。
|
|
787
|
+
*/
|
|
788
|
+
async showPhaseAccuracy(changeId, phase, options) {
|
|
789
|
+
const ALLOWED = ['techDesign', 'propose', 'apply', 'review'];
|
|
790
|
+
const normalized = ALLOWED.find(p => p.toLowerCase() === phase.toLowerCase());
|
|
791
|
+
if (!normalized) {
|
|
792
|
+
throw new Error(`Invalid phase: ${phase}. Allowed: ${ALLOWED.join(', ')}`);
|
|
793
|
+
}
|
|
794
|
+
const cwd = resolveZhuanSpecRoot();
|
|
795
|
+
const changeDir = path.join(cwd, 'zhuanspec', 'changes', changeId);
|
|
796
|
+
if (!await FileSystemUtils.directoryExists(changeDir)) {
|
|
797
|
+
throw new Error(`Change '${changeId}' not found`);
|
|
798
|
+
}
|
|
799
|
+
const progressPath = path.join(changeDir, 'metrics', 'progress.json');
|
|
800
|
+
if (!await FileSystemUtils.fileExists(progressPath)) {
|
|
801
|
+
throw new Error(`progress.json not found: ${progressPath}`);
|
|
802
|
+
}
|
|
803
|
+
const progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
|
|
804
|
+
const buckets = progress.accuracy?.phaseAccuracy?.filter(b => b.phase === normalized) ?? [];
|
|
805
|
+
if (options?.json) {
|
|
806
|
+
console.log(JSON.stringify({
|
|
807
|
+
changeId,
|
|
808
|
+
phase: normalized,
|
|
809
|
+
buckets,
|
|
810
|
+
legacy: buckets.length === 0,
|
|
811
|
+
}, null, 2));
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
console.log(`\n📊 Phase accuracy: ${changeId} / ${normalized}`);
|
|
815
|
+
console.log('─'.repeat(60));
|
|
816
|
+
if (buckets.length === 0) {
|
|
817
|
+
console.log(' —(历史数据,无 phaseAccuracy 桶)');
|
|
818
|
+
console.log();
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
for (const b of buckets) {
|
|
822
|
+
const raw = ((b.accuracyRateRaw ?? b.accuracyRate) * 100).toFixed(2);
|
|
823
|
+
const show = ((b.accuracyRate ?? 1) * 100).toFixed(2);
|
|
824
|
+
const overrideCount = b.overrideHistory?.length ?? 0;
|
|
825
|
+
const confirmed = b.confirmed ? `✓ ${b.confirmed.by}@${b.confirmed.at}` : '✗ 未确认';
|
|
826
|
+
console.log(` [${b.phase}/${b.trackedKind}]`);
|
|
827
|
+
console.log(` AI产出: ${b.aiTotalLines} 行 (added=${b.aiLinesAdded}, modified=${b.aiLinesModified})`);
|
|
828
|
+
console.log(` 用户纠偏: ${b.userCorrectionLines} 行 (prompts=${b.correctionPromptIds.length})`);
|
|
829
|
+
console.log(` raw/展示: ${raw}% / ${show}%`);
|
|
830
|
+
console.log(` override次数: ${overrideCount}`);
|
|
831
|
+
console.log(` confirmed: ${confirmed}`);
|
|
832
|
+
if (overrideCount > 0 && b.overrideHistory) {
|
|
833
|
+
const last = b.overrideHistory[overrideCount - 1];
|
|
834
|
+
console.log(` 最近 reason: ${last.reason} (${last.timestamp})`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
console.log();
|
|
838
|
+
}
|
|
534
839
|
}
|
|
535
840
|
//# sourceMappingURL=progress.js.map
|
package/dist/core/archive.js
CHANGED
|
@@ -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
|