@zhuan-ai/zhuanspec 2.11.19 → 2.12.7

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 (49) hide show
  1. package/dist/cli/index.js +53 -0
  2. package/dist/commands/accuracy.d.ts +15 -0
  3. package/dist/commands/accuracy.js +154 -0
  4. package/dist/commands/progress.d.ts +17 -0
  5. package/dist/commands/progress.js +128 -1
  6. package/dist/commands/review.js +31 -2
  7. package/dist/core/configurators/codex.d.ts +137 -0
  8. package/dist/core/configurators/codex.js +296 -0
  9. package/dist/core/configurators/registry.js +3 -0
  10. package/dist/core/configurators/slash/codex.d.ts +39 -8
  11. package/dist/core/configurators/slash/codex.js +158 -112
  12. package/dist/core/dashboard/metrics.d.ts +9 -0
  13. package/dist/core/dashboard/metrics.js +12 -0
  14. package/dist/core/hooks/collect-knowledge.js +7 -2
  15. package/dist/core/hooks/deviation-check.d.ts +11 -1
  16. package/dist/core/hooks/deviation-check.js +275 -14
  17. package/dist/core/hooks/deviation-handler.js +2 -2
  18. package/dist/core/hooks/init.js +42 -6
  19. package/dist/core/hooks/post-apply.js +21 -4
  20. package/dist/core/hooks/pre-apply.js +8 -0
  21. package/dist/core/hooks/record-progress.d.ts +39 -0
  22. package/dist/core/hooks/record-progress.js +143 -21
  23. package/dist/core/hooks/review-hooks.d.ts +26 -1
  24. package/dist/core/hooks/review-hooks.js +166 -12
  25. package/dist/core/hooks/summarize.js +61 -4
  26. package/dist/core/metrics/code-accuracy.d.ts +83 -0
  27. package/dist/core/metrics/code-accuracy.js +250 -0
  28. package/dist/core/styles/palette.d.ts +1 -0
  29. package/dist/core/styles/palette.js +3 -1
  30. package/dist/core/templates/agents-root-stub.d.ts +1 -1
  31. package/dist/core/templates/agents-root-stub.js +15 -0
  32. package/dist/core/templates/agents-template.d.ts +1 -1
  33. package/dist/core/templates/agents-template.js +40 -19
  34. package/dist/core/templates/codex-agents-template.d.ts +53 -0
  35. package/dist/core/templates/codex-agents-template.js +177 -0
  36. package/dist/core/templates/codex-hooks-template.d.ts +52 -0
  37. package/dist/core/templates/codex-hooks-template.js +114 -0
  38. package/dist/core/templates/codex-skills-template.d.ts +39 -0
  39. package/dist/core/templates/codex-skills-template.js +69 -0
  40. package/dist/core/templates/slash-command-templates.js +124 -48
  41. package/dist/core/validation/strict-rules.js +23 -0
  42. package/dist/utils/hook-host.d.ts +47 -0
  43. package/dist/utils/hook-host.js +129 -0
  44. package/dist/utils/hook-merge.d.ts +29 -0
  45. package/dist/utils/hook-merge.js +72 -0
  46. package/dist/utils/phase-utils.js +3 -3
  47. package/package.json +20 -22
  48. package/dist/core/hooks/review-orchestrator.d.ts +0 -40
  49. package/dist/core/hooks/review-orchestrator.js +0 -202
package/dist/cli/index.js CHANGED
@@ -15,6 +15,7 @@ import { ShowCommand } from '../commands/show.js';
15
15
  import { CompletionCommand } from '../commands/completion.js';
16
16
  import { ReviewCommand } from '../commands/review.js';
17
17
  import { ProgressCommand } from '../commands/progress.js';
18
+ import { AccuracyCommand } from '../commands/accuracy.js';
18
19
  import { discoverSkills, formatSkillsList } from '../core/skill-discovery.js';
19
20
  import { registerConfigCommand } from '../commands/config.js';
20
21
  import { registerArtifactWorkflowCommands } from '../commands/artifact-workflow.js';
@@ -283,6 +284,58 @@ progressCmd
283
284
  process.exit(1);
284
285
  }
285
286
  });
287
+ // Progress resolve-correction subcommand
288
+ progressCmd
289
+ .command('resolve-correction <change-id>')
290
+ .description('Clear pending user-correction marker (Apply phase)')
291
+ .option('--path <path>', 'Chosen resolution path: A|B|C|D')
292
+ .option('--note <text>', 'Optional note to attach to the correction-log entry')
293
+ .option('--mark-pitfall-saved', 'Also set askedPitfallSaved=true in progress.json to suppress Stop-hook ask')
294
+ .action(async (changeId, options) => {
295
+ try {
296
+ const progressCommand = new ProgressCommand();
297
+ await progressCommand.resolveCorrection(changeId, options);
298
+ }
299
+ catch (error) {
300
+ console.log();
301
+ ora().fail(`Error: ${error.message}`);
302
+ process.exit(1);
303
+ }
304
+ });
305
+ // Progress show-correction subcommand
306
+ progressCmd
307
+ .command('show-correction <change-id>')
308
+ .description('Show pending-correction status and recent correction-log entries')
309
+ .option('--tail <n>', 'Show last N log entries (default 10)', (v) => parseInt(v, 10))
310
+ .action(async (changeId, options) => {
311
+ try {
312
+ const progressCommand = new ProgressCommand();
313
+ await progressCommand.showCorrection(changeId, options);
314
+ }
315
+ catch (error) {
316
+ console.log();
317
+ ora().fail(`Error: ${error.message}`);
318
+ process.exit(1);
319
+ }
320
+ });
321
+ // Accuracy command
322
+ program
323
+ .command('accuracy [change-name]')
324
+ .description('Show Initial Code Accuracy Rate for a change')
325
+ .option('--json', 'Output as JSON')
326
+ .option('--force', 'Force recompute accuracy')
327
+ .option('--override <rate>', 'Manually override accuracy rate (0.0~1.0)')
328
+ .action(async (changeName, options) => {
329
+ try {
330
+ const accuracyCommand = new AccuracyCommand();
331
+ await accuracyCommand.execute(changeName, options);
332
+ }
333
+ catch (error) {
334
+ console.log();
335
+ ora().fail(`Error: ${error.message}`);
336
+ process.exit(1);
337
+ }
338
+ });
286
339
  program
287
340
  .command('archive [change-name]')
288
341
  .description('Archive a completed change and update main specs')
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Accuracy Command - Show Initial Code Accuracy Rate
3
+ *
4
+ * Usage: zhuanspec accuracy <change-id>
5
+ * Options: --json, --force, --override <rate>
6
+ */
7
+ export declare class AccuracyCommand {
8
+ execute(changeName?: string, options?: {
9
+ json?: boolean;
10
+ force?: boolean;
11
+ override?: string;
12
+ }): Promise<void>;
13
+ private selectChangeInteractively;
14
+ }
15
+ //# sourceMappingURL=accuracy.d.ts.map
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Accuracy Command - Show Initial Code Accuracy Rate
3
+ *
4
+ * Usage: zhuanspec accuracy <change-id>
5
+ * Options: --json, --force, --override <rate>
6
+ */
7
+ import path from 'path';
8
+ import { promises as fs } from 'fs';
9
+ import { FileSystemUtils } from '../utils/file-system.js';
10
+ import { computeAccuracy, applyOverride, } from '../core/metrics/code-accuracy.js';
11
+ import { atomicWriteJson, recoverProgressJsonForWrite, } from '../core/hooks/record-progress.js';
12
+ export class AccuracyCommand {
13
+ async execute(changeName, options) {
14
+ const cwd = process.cwd();
15
+ if (!changeName) {
16
+ const selected = await this.selectChangeInteractively();
17
+ if (!selected) {
18
+ throw new Error('No change selected. Usage: zhuanspec accuracy <change-id>');
19
+ }
20
+ changeName = selected;
21
+ }
22
+ const changeDir = path.join(cwd, 'zhuanspec', 'changes', changeName);
23
+ if (!await FileSystemUtils.directoryExists(changeDir)) {
24
+ throw new Error(`Change '${changeName}' not found`);
25
+ }
26
+ const metricsDir = path.join(changeDir, 'metrics');
27
+ const accuracyPath = path.join(metricsDir, 'accuracy.json');
28
+ const progressPath = path.join(metricsDir, 'progress.json');
29
+ // Load progress with recovery support
30
+ let progress = null;
31
+ if (await FileSystemUtils.fileExists(progressPath)) {
32
+ progress = await recoverProgressJsonForWrite(progressPath);
33
+ }
34
+ if (!progress || !progress.accuracy?.snapshotTimestamp) {
35
+ console.log('ℹ️ No accuracy snapshot found for this change.');
36
+ console.log(' The snapshot is created after the Apply phase completes.');
37
+ console.log(' Make sure post-apply hook has been executed.');
38
+ return;
39
+ }
40
+ // Handle --override
41
+ if (options?.override !== undefined) {
42
+ const overrideRate = parseFloat(options.override);
43
+ if (isNaN(overrideRate) || overrideRate < 0 || overrideRate > 1) {
44
+ throw new Error('Override rate must be a number between 0.0 and 1.0');
45
+ }
46
+ const result = applyOverride(progress, overrideRate);
47
+ // Persist to progress.json
48
+ await atomicWriteJson(progressPath, progress);
49
+ // Also update accuracy.json
50
+ if (await FileSystemUtils.fileExists(accuracyPath)) {
51
+ try {
52
+ const accuracyData = JSON.parse(await FileSystemUtils.readFile(accuracyPath));
53
+ if (!accuracyData.overrideHistory)
54
+ accuracyData.overrideHistory = [];
55
+ accuracyData.overrideHistory.push({
56
+ timestamp: new Date().toISOString(),
57
+ originalRate: result.originalRate,
58
+ overriddenRate: result.overriddenRate,
59
+ reason: 'CLI --override',
60
+ });
61
+ accuracyData.accuracyRate = result.overriddenRate;
62
+ accuracyData.accuracyRateRaw = result.originalRate;
63
+ await atomicWriteJson(accuracyPath, accuracyData);
64
+ }
65
+ catch {
66
+ // Ignore accuracy.json update errors
67
+ }
68
+ }
69
+ console.log(`✓ Accuracy overridden: ${(result.originalRate * 100).toFixed(1)}% → ${(result.overriddenRate * 100).toFixed(1)}%`);
70
+ return;
71
+ }
72
+ // Compute accuracy
73
+ const accResult = computeAccuracy(progress);
74
+ if (options?.json) {
75
+ console.log(JSON.stringify({
76
+ changeId: changeName,
77
+ accuracy: {
78
+ rate: accResult.rate,
79
+ rateRaw: accResult.rateRaw,
80
+ aiTotalLines: accResult.aiTotalLines,
81
+ userCorrectionLines: accResult.userCorrectionLines,
82
+ overridden: accResult.overridden,
83
+ },
84
+ snapshot: progress.accuracy,
85
+ correctionEdits: progress.accuracy.correctionEdits || [],
86
+ }, null, 2));
87
+ }
88
+ else {
89
+ console.log('');
90
+ console.log(`📊 代码首次准确率: ${changeName}`);
91
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
92
+ console.log('');
93
+ console.log(` 准确率: ${(accResult.rate * 100).toFixed(1)}%${accResult.overridden ? ' (已手动修正)' : ''}`);
94
+ if (accResult.overridden) {
95
+ console.log(` 原始准确率: ${(accResult.rateRaw * 100).toFixed(1)}%`);
96
+ }
97
+ console.log(` AI产出: ${accResult.aiTotalLines} 行`);
98
+ console.log(` 用户纠正: ${accResult.userCorrectionLines} 行`);
99
+ if (progress.accuracy.snapshotTimestamp) {
100
+ console.log(` 快照时间: ${progress.accuracy.snapshotTimestamp}`);
101
+ }
102
+ console.log('');
103
+ // Show per-file correction details
104
+ const edits = progress.accuracy.correctionEdits || [];
105
+ if (edits.length > 0) {
106
+ console.log('📝 纠正明细:');
107
+ const byFile = new Map();
108
+ for (const e of edits) {
109
+ const existing = byFile.get(e.filePath) || { lines: 0, count: 0 };
110
+ existing.lines += e.editLines;
111
+ existing.count += 1;
112
+ byFile.set(e.filePath, existing);
113
+ }
114
+ for (const [file, stats] of byFile) {
115
+ console.log(` ${file}: ${stats.lines} 行 (${stats.count} 次)`);
116
+ }
117
+ console.log('');
118
+ }
119
+ console.log(`💡 手动修正: zhuanspec accuracy ${changeName} --override <0.0~1.0>`);
120
+ console.log('');
121
+ }
122
+ }
123
+ async selectChangeInteractively() {
124
+ const changesDir = path.join(process.cwd(), 'zhuanspec', 'changes');
125
+ let entries = [];
126
+ try {
127
+ entries = await fs.readdir(changesDir, { withFileTypes: true });
128
+ }
129
+ catch {
130
+ throw new Error("No zhuanspec changes directory found. Run 'zhuanspec init' first.");
131
+ }
132
+ const changes = entries
133
+ .filter((entry) => entry.isDirectory() && entry.name !== 'archive' && !entry.name.startsWith('.'))
134
+ .map((entry) => entry.name)
135
+ .sort((a, b) => a.localeCompare(b));
136
+ if (changes.length === 0) {
137
+ throw new Error('No active changes found. Create one first.');
138
+ }
139
+ if (!process.stdin.isTTY) {
140
+ throw new Error(`Missing change name. Available changes: ${changes.join(', ')}`);
141
+ }
142
+ const { select } = await import('@inquirer/prompts');
143
+ try {
144
+ return await select({
145
+ message: '选择要查看准确率的变更',
146
+ choices: changes.map((id) => ({ name: id, value: id })),
147
+ });
148
+ }
149
+ catch {
150
+ return null;
151
+ }
152
+ }
153
+ }
154
+ //# sourceMappingURL=accuracy.js.map
@@ -34,5 +34,22 @@ export declare class ProgressCommand {
34
34
  * Usage: zhuanspec progress set-phase <change-id> <phase>
35
35
  */
36
36
  setPhase(changeId: string, phase: string): Promise<void>;
37
+ /**
38
+ * Resolve pending user-correction: remove `.pending-correction`, append
39
+ * resolved entry to `.correction-log`, and optionally mark pitfall saved.
40
+ * Usage: zhuanspec progress resolve-correction <change-id> [--path A|B|C|D] [--note <text>] [--mark-pitfall-saved]
41
+ */
42
+ resolveCorrection(changeId: string, options?: {
43
+ path?: string;
44
+ note?: string;
45
+ markPitfallSaved?: boolean;
46
+ }): Promise<void>;
47
+ /**
48
+ * Show current pending correction status and recent correction-log entries.
49
+ * Usage: zhuanspec progress show-correction <change-id> [--tail <n>]
50
+ */
51
+ showCorrection(changeId: string, options?: {
52
+ tail?: number;
53
+ }): Promise<void>;
37
54
  }
38
55
  //# sourceMappingURL=progress.d.ts.map
@@ -288,7 +288,7 @@ export class ProgressCommand {
288
288
  const completedCount = tasks.filter(t => t.status === 'completed').length;
289
289
  const totalTasks = tasks.length;
290
290
  // Phase index
291
- const phases = ['idle', 'propose', 'apply', 'review', 'archive'];
291
+ const phases = ['idle', 'techDesign', 'propose', 'apply', 'review', 'archive'];
292
292
  const phaseIndex = phases.indexOf(phase) + 1;
293
293
  // Progress percentage
294
294
  const percentage = totalTasks > 0 ? (completedCount / totalTasks) * 100 : 0;
@@ -370,5 +370,132 @@ export class ProgressCommand {
370
370
  console.log(`\n✓ Phase set: ${changeId} -> ${normalizedPhase}`);
371
371
  console.log(`✓ Progress file: ${progressPath}\n`);
372
372
  }
373
+ /**
374
+ * Resolve pending user-correction: remove `.pending-correction`, append
375
+ * resolved entry to `.correction-log`, and optionally mark pitfall saved.
376
+ * Usage: zhuanspec progress resolve-correction <change-id> [--path A|B|C|D] [--note <text>] [--mark-pitfall-saved]
377
+ */
378
+ async resolveCorrection(changeId, options) {
379
+ const cwd = resolveZhuanSpecRoot();
380
+ const changeDir = path.join(cwd, 'zhuanspec', 'changes', changeId);
381
+ if (!await FileSystemUtils.directoryExists(changeDir)) {
382
+ throw new Error(`Change '${changeId}' not found`);
383
+ }
384
+ const pendingMarker = path.join(changeDir, '.pending-correction');
385
+ const logPath = path.join(changeDir, '.correction-log');
386
+ let hadMarker = false;
387
+ // Remove pending marker (safe if missing)
388
+ try {
389
+ await fs.unlink(pendingMarker);
390
+ hadMarker = true;
391
+ }
392
+ catch {
393
+ // ignore missing marker
394
+ }
395
+ // Append resolved entry to correction-log
396
+ const entry = {
397
+ ts: new Date().toISOString(),
398
+ event: 'resolved',
399
+ path: options?.path,
400
+ note: options?.note,
401
+ };
402
+ try {
403
+ await fs.appendFile(logPath, JSON.stringify(entry) + '\n', 'utf-8');
404
+ }
405
+ catch {
406
+ // ignore log append errors
407
+ }
408
+ // Optionally mark askedPitfallSaved=true in progress.json
409
+ if (options?.markPitfallSaved) {
410
+ const progressPath = path.join(changeDir, 'metrics', 'progress.json');
411
+ if (await FileSystemUtils.fileExists(progressPath)) {
412
+ try {
413
+ const raw = await FileSystemUtils.readFile(progressPath);
414
+ const progress = JSON.parse(raw);
415
+ progress.askedPitfallSaved = true;
416
+ progress.lastUpdatedAt = new Date().toISOString();
417
+ await fs.writeFile(progressPath, JSON.stringify(progress, null, 2), 'utf-8');
418
+ }
419
+ catch {
420
+ // ignore progress update errors
421
+ }
422
+ }
423
+ }
424
+ console.log(`\n✓ Correction resolved for change '${changeId}'`);
425
+ console.log(` - Pending marker: ${hadMarker ? 'removed' : 'not present'}`);
426
+ if (options?.path)
427
+ console.log(` - Path: ${options.path}`);
428
+ if (options?.note)
429
+ console.log(` - Note: ${options.note}`);
430
+ if (options?.markPitfallSaved)
431
+ console.log(` - askedPitfallSaved: true`);
432
+ console.log();
433
+ }
434
+ /**
435
+ * Show current pending correction status and recent correction-log entries.
436
+ * Usage: zhuanspec progress show-correction <change-id> [--tail <n>]
437
+ */
438
+ async showCorrection(changeId, options) {
439
+ const cwd = resolveZhuanSpecRoot();
440
+ const changeDir = path.join(cwd, 'zhuanspec', 'changes', changeId);
441
+ if (!await FileSystemUtils.directoryExists(changeDir)) {
442
+ throw new Error(`Change '${changeId}' not found`);
443
+ }
444
+ const pendingMarker = path.join(changeDir, '.pending-correction');
445
+ const logPath = path.join(changeDir, '.correction-log');
446
+ const tail = options?.tail && options.tail > 0 ? options.tail : 10;
447
+ console.log(`\nChange: ${changeId}`);
448
+ console.log('-'.repeat(60));
449
+ // Pending marker
450
+ if (await FileSystemUtils.fileExists(pendingMarker)) {
451
+ try {
452
+ const raw = await FileSystemUtils.readFile(pendingMarker);
453
+ const marker = JSON.parse(raw);
454
+ const ageMin = Math.round((Date.now() - (marker.createdAt || 0)) / 60000);
455
+ console.log(`Pending correction: YES (age ~${ageMin} min)`);
456
+ if (marker.prompt) {
457
+ const preview = String(marker.prompt).replace(/\s+/g, ' ').slice(0, 160);
458
+ console.log(` Prompt: ${preview}${marker.prompt.length > 160 ? '…' : ''}`);
459
+ }
460
+ }
461
+ catch {
462
+ console.log('Pending correction: YES (unparseable marker)');
463
+ }
464
+ }
465
+ else {
466
+ console.log('Pending correction: none');
467
+ }
468
+ // Correction log tail
469
+ if (await FileSystemUtils.fileExists(logPath)) {
470
+ const content = await FileSystemUtils.readFile(logPath);
471
+ const lines = content.split('\n').filter(l => l.trim().length > 0);
472
+ const recent = lines.slice(-tail);
473
+ console.log(`\nRecent correction-log (last ${recent.length} of ${lines.length}):`);
474
+ for (const line of recent) {
475
+ try {
476
+ const rec = JSON.parse(line);
477
+ console.log(` [${rec.ts}] ${rec.event}${rec.path ? ` path=${rec.path}` : ''}${rec.promptPreview ? ` — ${String(rec.promptPreview).slice(0, 80)}` : ''}${rec.note ? ` (${rec.note})` : ''}`);
478
+ }
479
+ catch {
480
+ console.log(` ${line}`);
481
+ }
482
+ }
483
+ }
484
+ else {
485
+ console.log('\nCorrection log: (empty)');
486
+ }
487
+ // askedPitfallSaved status
488
+ const progressPath = path.join(changeDir, 'metrics', 'progress.json');
489
+ if (await FileSystemUtils.fileExists(progressPath)) {
490
+ try {
491
+ const progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
492
+ console.log(`\naskedPitfallSaved: ${progress.askedPitfallSaved === true ? 'true' : 'false'}`);
493
+ }
494
+ catch {
495
+ // ignore
496
+ }
497
+ }
498
+ console.log();
499
+ }
373
500
  }
374
501
  //# sourceMappingURL=progress.js.map
@@ -2,6 +2,7 @@ import { promises as fs } from 'fs';
2
2
  import path from 'path';
3
3
  import { FileSystemUtils } from '../utils/file-system.js';
4
4
  import { setPhase } from '../utils/phase-utils.js';
5
+ import { ensureAccuracySnapshot } from '../core/metrics/code-accuracy.js';
5
6
  import { codeReviewResultCheck, unitTestResultCheck, specConsistencyResultCheck, generateReviewReport, } from '../core/hooks/review-hooks.js';
6
7
  export class ReviewCommand {
7
8
  async execute(changeName, options) {
@@ -60,6 +61,17 @@ export class ReviewCommand {
60
61
  catch (err) {
61
62
  console.warn(`⚠️ Failed to update progress.phase to 'review': ${err instanceof Error ? err.message : String(err)}`);
62
63
  }
64
+ // Accuracy snapshot 补救:若 apply 未走 post-apply hook(如手动 /zhuanspec:review)导致快照缺失,
65
+ // 在进入 review 的那一刻以 progress.linesAdded 作为 AI 基线衱5一份,避免首次准确率数据丢失。
66
+ try {
67
+ const created = await ensureAccuracySnapshot(changeDir);
68
+ if (created) {
69
+ console.log(`ℹ️ Accuracy snapshot 在 review 进入时补打成功(应优先走 post-apply hook,补打为兑底机制)`);
70
+ }
71
+ }
72
+ catch (err) {
73
+ console.warn(`⚠️ Accuracy snapshot 补打失败(不阻断 review): ${err instanceof Error ? err.message : String(err)}`);
74
+ }
63
75
  // Read hook results and generate report
64
76
  const codeReviewResult = await codeReviewResultCheck(changeName);
65
77
  const unitTestResult = await unitTestResultCheck(changeName);
@@ -82,10 +94,27 @@ export class ReviewCommand {
82
94
  else {
83
95
  console.log(`Review report for ${changeName}`);
84
96
  console.log(`Overall: ${overallPass ? 'PASS ✅' : 'FAIL ❌'}`);
85
- console.log(`Code Review: ${codeReviewResult.pass ? 'PASS' : 'FAIL'} (critical=${codeReviewResult.metrics.criticalCount ?? 0})`);
86
- console.log(`Unit Test: ${unitTestResult.pass ? 'PASS' : 'FAIL'} (coverage=${unitTestResult.metrics.coverage ?? 0}%)`);
87
97
  console.log(`Spec-Code: ${specConsistencyResult.pass ? 'PASS' : 'FAIL'} (consistency=${specConsistencyResult.metrics.consistencyRate ?? 0}%)`);
98
+ console.log(`Unit Test: ${unitTestResult.pass ? 'PASS' : 'FAIL'} (coverage=${unitTestResult.metrics.coverage ?? 0}%)`);
99
+ console.log(`Code Review: ${codeReviewResult.pass ? 'PASS' : 'FAIL'} (critical=${codeReviewResult.metrics.criticalCount ?? 0})`);
88
100
  console.log(`Report: ${path.join('zhuanspec/changes', changeName, 'review/review-report.md')}`);
101
+ // 历史数据兼容提示:legacy schema 豁免时输出 ⚠️ 警告但不影响 PASS
102
+ if (codeReviewResult.metrics.legacyBypass) {
103
+ console.warn(`\n⚠️ [轨道 3 Code Review legacy schema] ${String(codeReviewResult.metrics.legacyReason ?? '')}\n → 下次请在 \`code-review-result.json\` 中设置 \`"schemaVersion": 2\` 并登记 \`skillReportPath\`。`);
104
+ }
105
+ if (unitTestResult.metrics.legacyBypass) {
106
+ console.warn(`\n⚠️ [轨道 2 Unit Test legacy schema] ${String(unitTestResult.metrics.legacyReason ?? '')}\n → 下次请在 \`unit-test-result.json\` 中设置 \`"schemaVersion": 2\` 并登记 \`newTestsGenerated\`/\`skipReason\`。`);
107
+ }
108
+ // 失败时输出中文 fixPrompt,确保用户明确看到失败原因与修复指引
109
+ if (!specConsistencyResult.pass && specConsistencyResult.fixPrompt) {
110
+ console.error(`\n[轨道 1 Spec-Code 失败]${specConsistencyResult.fixPrompt}`);
111
+ }
112
+ if (!unitTestResult.pass && unitTestResult.fixPrompt) {
113
+ console.error(`\n[轨道 2 Unit Test 失败]${unitTestResult.fixPrompt}`);
114
+ }
115
+ if (!codeReviewResult.pass && codeReviewResult.fixPrompt) {
116
+ console.error(`\n[轨道 3 Code Review 失败]${codeReviewResult.fixPrompt}`);
117
+ }
89
118
  }
90
119
  process.exitCode = overallPass ? 0 : 1;
91
120
  }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Codex root configurator.
3
+ *
4
+ * Unlike the Claude configurator (which owns `CLAUDE.md`) the Codex
5
+ * configurator shares `AGENTS.md` with the standard agents stub. Its main
6
+ * responsibility is to materialize the project-level Codex integration
7
+ * artefacts and the matching `trusted_level` entry in the global Codex config:
8
+ *
9
+ * <repo>/.codex/
10
+ * config.toml - ZhuanSpec hooks TOML block (wrapped in markers)
11
+ * agents/ - TOML subagents mirrored from .claude/agents/*.md
12
+ * skills/ - Project-level Skills mirrored from .claude/skills/
13
+ *
14
+ * ~/.codex/ (or $CODEX_HOME)
15
+ * config.toml - Per-project [projects."…"] trusted_level = "trusted"
16
+ * block (wrapped in TRUST markers keyed by absolute path)
17
+ *
18
+ * Codex does **not** load project-scoped hooks/subagents unless the project
19
+ * is explicitly marked trusted in the global config (see openai/codex#14601),
20
+ * so we opportunistically write that trust entry during `zhuanspec init`.
21
+ *
22
+ * Idempotency:
23
+ * - Hooks block is regenerated inside ZHUANSPEC markers on every run.
24
+ * - Trust entries are keyed by absolute path; repeated runs no-op.
25
+ * - Legacy `.codex/hooks.json` from the 2.12.0 implementation is removed.
26
+ */
27
+ import { ToolConfigurator } from './base.js';
28
+ export interface CodexConfigureResult {
29
+ /** Absolute path of `<repo>/.codex/config.toml` that was written. */
30
+ configTomlPath: string;
31
+ /** Absolute paths of subagent role TOMLs that were written. */
32
+ agentsWritten: string[];
33
+ /** Absolute paths of skill files written under `<repo>/.codex/skills/`. */
34
+ skillsWritten: string[];
35
+ /** Absolute path of the global config.toml that received the trust entry. */
36
+ globalConfigPath: string;
37
+ /** Whether a new trust entry was added (false = already trusted). */
38
+ trustEntryAdded: boolean;
39
+ /**
40
+ * Absolute path of a legacy global `~/.codex/hooks.json` detected during
41
+ * configure. Codex warns when hooks are loaded from both `hooks.json` and
42
+ * `config.toml`; we surface the file here so callers can inform the user.
43
+ * `null` when no conflicting file was found.
44
+ */
45
+ conflictingGlobalHooksJson: string | null;
46
+ /**
47
+ * Absolute paths of legacy `~/.codex/prompts/zhuanspec-*.md` files that
48
+ * were cleaned up. Codex >= 0.117 ignores that directory entirely, so we
49
+ * opportunistically remove ZhuanSpec-managed prompts from pre-2.12.5
50
+ * installs to avoid user confusion. Empty when nothing needed removing.
51
+ */
52
+ legacyPromptsCleaned: string[];
53
+ }
54
+ export declare class CodexConfigurator implements ToolConfigurator {
55
+ name: string;
56
+ /** Codex reads AGENTS.md for project instructions (no dedicated CODEX.md). */
57
+ configFileName: string;
58
+ isAvailable: boolean;
59
+ configure(projectPath: string, _zhuanspecDir: string): Promise<void>;
60
+ /**
61
+ * Write:
62
+ * 1. `.codex/agents/zhuanspec-*.toml` mirrored from `.claude/agents/*.md`
63
+ * 2. `.codex/skills/*` mirrored from `.claude/skills/` (directory tree)
64
+ * 3. ZhuanSpec hooks block merged into `.codex/config.toml`
65
+ * 4. Per-project trust entry appended to `$CODEX_HOME/config.toml`
66
+ * 5. Remove legacy `.codex/hooks.json` left over from 2.12.0
67
+ */
68
+ configureWithSummary(projectPath: string): Promise<CodexConfigureResult>;
69
+ /**
70
+ * Mirror `<repo>/.claude/agents/*.md` into `<repo>/.codex/agents/*.toml`.
71
+ * Returns the full file descriptors (not just paths) so the caller can
72
+ * register each role under `[agents.<slug>]` in the parent config.toml.
73
+ */
74
+ private generateAgents;
75
+ /**
76
+ * Mirror `<repo>/.claude/skills/` directory tree into `<repo>/.codex/skills/`.
77
+ * Uses `fs.copyFile` so that executable bits on scripts (`.sh`, `.py`, etc.)
78
+ * are preserved. Silently no-ops when `.claude/skills/` is absent.
79
+ */
80
+ private mirrorSkills;
81
+ /**
82
+ * Merge the ZhuanSpec managed block into `<repo>/.codex/config.toml`. The
83
+ * block contains two coordinated sections:
84
+ *
85
+ * 1. `[agents.<slug>]` registration tables (one per mirrored subagent)
86
+ * 2. `[[hooks.<Event>]]` default lifecycle hook tables
87
+ *
88
+ * User-owned config outside the marker block is preserved verbatim.
89
+ */
90
+ private writeProjectConfigToml;
91
+ /**
92
+ * Build the `[agents.<slug>]` registration TOML block for all mirrored
93
+ * subagents. Each table points to the per-role file under `agents/<slug>.toml`.
94
+ * Returns an empty string when no agents were mirrored (no `.claude/agents`).
95
+ */
96
+ private buildAgentsRegistrationBlock;
97
+ /**
98
+ * Detect whether a global `~/.codex/hooks.json` coexists with our
99
+ * project-level `config.toml` hooks. Codex warns (`loading hooks from both
100
+ * hooks.json and config.toml`) and we cannot safely delete a file the user
101
+ * owns — instead we return its path so the caller can prompt the user.
102
+ */
103
+ private detectGlobalHooksJsonConflict;
104
+ /**
105
+ * Append (or keep) a per-project `[projects."<abs>"] trusted_level="trusted"`
106
+ * entry in `$CODEX_HOME/config.toml`. The block is wrapped in TRUST markers
107
+ * keyed by absolute path so repeated runs are idempotent and multiple
108
+ * projects can coexist in the same file.
109
+ *
110
+ * Conflict resolution with **unmanaged** native entries:
111
+ * Codex CLI's `/trust this workspace` and the legacy 2.12.0 implementation
112
+ * both write a plain `[projects."<abs>"]` table without ZHUANSPEC markers.
113
+ * If we unconditionally append our marked block, TOML parsing blows up
114
+ * with `duplicate key` and Codex refuses to start (observed in the wild).
115
+ * The user's explicit native entry always takes precedence: we detect it,
116
+ * skip our write, and if our marked block co-exists (legacy duplicate) we
117
+ * strip it to restore a parseable config.
118
+ */
119
+ private ensureGlobalTrustEntry;
120
+ /**
121
+ * Detect whether `[projects."<projectPath>"]` is declared OUTSIDE our
122
+ * ZHUANSPEC:TRUST markers. Used to avoid producing TOML `duplicate key`
123
+ * when the user (or legacy 2.12.0) already wrote a native trust entry.
124
+ */
125
+ private hasNativeTrustEntry;
126
+ /** Remove our marker-wrapped trust block for `projectPath` from `content`. */
127
+ private stripTrustBlock;
128
+ /** Build a full `# TRUST:START…END` block for the given project path. */
129
+ private buildTrustBlock;
130
+ /**
131
+ * Replace an existing TRUST block for `projectPath` in-place. Used to keep
132
+ * the block content in sync if the schema ever evolves (currently a no-op
133
+ * when the block already matches).
134
+ */
135
+ private replaceTrustBlock;
136
+ }
137
+ //# sourceMappingURL=codex.d.ts.map