@zhuan-ai/zhuanspec 2.15.1 → 2.15.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.
package/dist/cli/index.js CHANGED
@@ -272,7 +272,7 @@ progressCmd
272
272
  // Progress set-phase subcommand
273
273
  progressCmd
274
274
  .command('set-phase <change-id> <phase>')
275
- .description('Set phase for a change (e.g., propose, apply, review, archive)')
275
+ .description('Set phase for a change (idle, techDesign, propose, apply, review, archive). Workflow-entry phases (idle/techDesign/propose) auto-create the change directory if missing.')
276
276
  .action(async (changeId, phase) => {
277
277
  try {
278
278
  const progressCommand = new ProgressCommand();
@@ -31,10 +31,11 @@ export class AccuracyCommand {
31
31
  if (await FileSystemUtils.fileExists(progressPath)) {
32
32
  progress = await recoverProgressJsonForWrite(progressPath);
33
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.');
34
+ // 兼容新旧路径:旧路径有 snapshotTimestamp,新路径有 linesAdded
35
+ if (!progress || (!progress.accuracy?.snapshotTimestamp && !(progress.linesAdded > 0))) {
36
+ console.log('ℹ️ No accuracy data found for this change.');
37
+ console.log(' Accuracy data is accumulated during the Apply phase.');
38
+ console.log(' Make sure the apply phase has produced code changes.');
38
39
  return;
39
40
  }
40
41
  // Handle --override
@@ -81,8 +82,8 @@ export class AccuracyCommand {
81
82
  userCorrectionLines: accResult.userCorrectionLines,
82
83
  overridden: accResult.overridden,
83
84
  },
84
- snapshot: progress.accuracy,
85
- correctionEdits: progress.accuracy.correctionEdits || [],
85
+ snapshot: progress.accuracy ?? null,
86
+ correctionEdits: progress.accuracy?.correctionEdits || [],
86
87
  }, null, 2));
87
88
  }
88
89
  else {
@@ -96,12 +97,12 @@ export class AccuracyCommand {
96
97
  }
97
98
  console.log(` AI产出: ${accResult.aiTotalLines} 行`);
98
99
  console.log(` 用户纠正: ${accResult.userCorrectionLines} 行`);
99
- if (progress.accuracy.snapshotTimestamp) {
100
+ if (progress.accuracy?.snapshotTimestamp) {
100
101
  console.log(` 快照时间: ${progress.accuracy.snapshotTimestamp}`);
101
102
  }
102
103
  console.log('');
103
104
  // Show per-file correction details
104
- const edits = progress.accuracy.correctionEdits || [];
105
+ const edits = progress.accuracy?.correctionEdits || [];
105
106
  if (edits.length > 0) {
106
107
  console.log('📝 纠正明细:');
107
108
  const byFile = new Map();
@@ -32,6 +32,10 @@ export declare class ProgressCommand {
32
32
  /**
33
33
  * Set phase for a change
34
34
  * Usage: zhuanspec progress set-phase <change-id> <phase>
35
+ *
36
+ * Workflow-entry phases (idle/techDesign/propose) auto-create the change
37
+ * directory if it does not exist; later phases (apply/review/archive)
38
+ * require the directory to already exist to avoid typo-creating ghost dirs.
35
39
  */
36
40
  setPhase(changeId: string, phase: string): Promise<void>;
37
41
  /**
@@ -350,19 +350,30 @@ export class ProgressCommand {
350
350
  /**
351
351
  * Set phase for a change
352
352
  * Usage: zhuanspec progress set-phase <change-id> <phase>
353
+ *
354
+ * Workflow-entry phases (idle/techDesign/propose) auto-create the change
355
+ * directory if it does not exist; later phases (apply/review/archive)
356
+ * require the directory to already exist to avoid typo-creating ghost dirs.
353
357
  */
354
358
  async setPhase(changeId, phase) {
359
+ // Validate phase first (fail fast on user typos so we don't surface
360
+ // misleading "Change not found" errors when the real issue is the phase name).
361
+ // Match case-insensitively but keep the canonical casing from PHASE_ORDER
362
+ // (e.g. user input 'techdesign' / 'TECHDESIGN' / 'techDesign' all map to 'techDesign').
363
+ const normalizedPhase = PHASE_ORDER.find(p => p.toLowerCase() === phase.toLowerCase());
364
+ if (!normalizedPhase) {
365
+ throw new Error(`Invalid phase: ${phase}. Valid phases: ${PHASE_ORDER.join(', ')}`);
366
+ }
355
367
  const cwd = resolveZhuanSpecRoot();
356
368
  const changeDir = path.join(cwd, 'zhuanspec', 'changes', changeId);
357
- if (!await FileSystemUtils.directoryExists(changeDir)) {
369
+ const dirExists = await FileSystemUtils.directoryExists(changeDir);
370
+ // Workflow-entry phases may create the change directory; later phases must not.
371
+ const ENTRY_PHASES = ['idle', 'techDesign', 'propose'];
372
+ if (!dirExists && !ENTRY_PHASES.includes(normalizedPhase)) {
358
373
  throw new Error(`Change '${changeId}' not found`);
359
374
  }
360
- // Validate phase
361
- const normalizedPhase = phase.toLowerCase();
362
- if (!PHASE_ORDER.includes(normalizedPhase)) {
363
- throw new Error(`Invalid phase: ${phase}. Valid phases: ${PHASE_ORDER.join(', ')}`);
364
- }
365
- // Update progress.json
375
+ // Update progress.json (initializeProgress creates metrics/ recursively,
376
+ // which also creates the change directory itself when missing).
366
377
  await initializeProgress(changeId, normalizedPhase);
367
378
  // Read and display result
368
379
  const progressPath = path.join(changeDir, 'metrics', 'progress.json');
@@ -405,6 +416,29 @@ export class ProgressCommand {
405
416
  catch {
406
417
  // ignore log append errors
407
418
  }
419
+ // Write correctionContext to progress.json (替代 snapshot 的精准归因标记)
420
+ // 此后 record-progress(post-tool) 检测到 correctionContext.active=true 时,
421
+ // 代码变更行数不再累加到 linesAdded,改为累加到 accuracy.userCorrectionLines
422
+ try {
423
+ const progressPath = path.join(changeDir, 'metrics', 'progress.json');
424
+ if (await FileSystemUtils.fileExists(progressPath)) {
425
+ const raw = await FileSystemUtils.readFile(progressPath);
426
+ const progress = JSON.parse(raw);
427
+ const now = new Date();
428
+ progress.correctionContext = {
429
+ active: true,
430
+ resolvedAt: now.toISOString(),
431
+ path: options?.path || 'A',
432
+ expiresAt: new Date(now.getTime() + 30 * 60 * 1000).toISOString(),
433
+ phase: progress.phase || 'apply',
434
+ };
435
+ progress.lastUpdatedAt = now.toISOString();
436
+ await fs.writeFile(progressPath, JSON.stringify(progress, null, 2), 'utf-8');
437
+ }
438
+ }
439
+ catch {
440
+ // gracef衰退化 — correctionContext 写入失败不阻断主流程
441
+ }
408
442
  // Optionally mark askedPitfallSaved=true in progress.json
409
443
  if (options?.markPitfallSaved) {
410
444
  const progressPath = path.join(changeDir, 'metrics', 'progress.json');
@@ -61,8 +61,10 @@ export class ReviewCommand {
61
61
  catch (err) {
62
62
  console.warn(`⚠️ Failed to update progress.phase to 'review': ${err instanceof Error ? err.message : String(err)}`);
63
63
  }
64
- // Accuracy snapshot 补救:若 apply 未走 post-apply hook(如手动 /zhuanspec:review)导致快照缺失,
65
- // 在进入 review 的那一刻以 progress.linesAdded 作为 AI 基线衱5一份,避免首次准确率数据丢失。
64
+ // Accuracy snapshot 兜底补救:若 apply 未走 post-apply hook(如手动 /zhuanspec:review
65
+ // 导致快照缺失,在进入 review 的那一刻以 progress.linesAdded 作为 AI 基线补一份。
66
+ // NOTE: correctionContext 精准归因上线后,新 change 不再依赖此快照;
67
+ // 保留仅用于向后兼容已有 snapshotTimestamp 的旧 change 数据。
66
68
  try {
67
69
  const created = await ensureAccuracySnapshot(changeDir);
68
70
  if (created) {
@@ -62,8 +62,8 @@ export async function runCollectKnowledge(filePath) {
62
62
  const knowledgeDir = path.join(cwd, 'zhuanspec', 'knowledge');
63
63
  const hasKnowledge = await FileSystemUtils.directoryExists(knowledgeDir);
64
64
  const knowledgeRef = hasKnowledge
65
- ? '参考 `zhuanspec/knowledge/` 下已有文件的格式'
66
- : '保存到 `zhuanspec/knowledge/troubleshooting/`、`best-practices/` 或 `implicit-conventions/` 对应目录';
65
+ ? '参考 `zhuanspec/knowledge/` 下已有文件的格式(统一 Front Matter + 三类分目录)'
66
+ : '将写入 `zhuanspec/knowledge/troubleshooting/`、`best-practices/` 或 `implicit-conventions/` 对应目录(仅此三类)';
67
67
  // Phase-specific capture focus to avoid duplicate entries across apply and archive
68
68
  const phaseGuide = phase === 'archive'
69
69
  ? [
@@ -92,7 +92,8 @@ export async function runCollectKnowledge(filePath) {
92
92
  '询问方式:',
93
93
  '> "这次改动是否值得记录为项目知识?(回复是/否)"',
94
94
  '',
95
- `用户确认后,用 Write 工具写入知识文件,${knowledgeRef}。`,
95
+ `用户确认后,**必须调用 \`@skill:zhuanspec:knowledge\` (模式 A)** 完成写入,由 skill 统一完成分类决策树、去重扫描、Front Matter 模板套用、index.md 更新。`,
96
+ `**禁止绕过 skill 直接 Write/Edit 创建 \`zhuanspec/knowledge/\` 下的任何文件**;${knowledgeRef}。`,
96
97
  '如果本次只是常规代码修改,忽略此提示即可。',
97
98
  ].join('\n');
98
99
  return {
@@ -60,6 +60,13 @@ const USER_CORRECTION_KEYWORDS = [
60
60
  'not what i meant', 'not what i wanted', 'redo this', 'redo that',
61
61
  // 补强:严谨性/边界/精度相关(英文)
62
62
  'not rigorous', 'edge case', 'loses precision', 'not precise',
63
+ // 补强:建议/优化类(语气较委婉的纠偏,避免单字"应该"/"优化"误触发)
64
+ '应该用', '应该按', '应该先', '应该走', '本应', '其实应该',
65
+ '优化一下', '优化下', '这里能优化', '需要优化', '可以优化',
66
+ '这块优化', '这段优化', '能不能优化',
67
+ // English(建议/优化类)
68
+ 'should use', 'should follow', 'optimize this', 'needs optimization',
69
+ 'can be optimized', 'better to',
63
70
  ];
64
71
  // Edit intent keywords — bilingual, for Apply phase scope check
65
72
  const EDIT_INTENT_KEYWORDS = [
@@ -50,6 +50,8 @@ export async function executePostApply(changeDir, repoRoot) {
50
50
  if (!deviationResult.detected) {
51
51
  try {
52
52
  // 2a. Snapshot AI baseline before review triggers (accuracy metric)
53
+ // NOTE: 此快照调用对新的 correctionContext 机制已非必须(精准归因由 resolve-correction
54
+ // → correctionContext → record-progress 三角完成),保留仅用于向后兼容旧 change 数据。
53
55
  await triggerAccuracySnapshot(changeDir);
54
56
  console.log('Post-apply: Triggering automatic review...');
55
57
  execSync(`zhuanspec review`, {
@@ -31,12 +31,11 @@ async function runPostArchiveHook(archivedChange) {
31
31
  else {
32
32
  await FileSystemUtils.writeFile(indexPath, `# Knowledge Index\n\n## Archive Log\n${line}\n`);
33
33
  }
34
- // 2. Collect change knowledge (extract decisions, best practices, implicit conventions)
34
+ // 2. Collect change knowledge (extract best practices, implicit conventions)
35
+ // 注意:知识库只保留三类目录 troubleshooting / best-practices / implicit-conventions,
36
+ // 原“决策类”内容合并进 best-practices;踩坑类走 zhuanspec:knowledge skill 单独沉淀。
35
37
  console.log(chalk.gray(' → 提取设计文档中的知识记录...'));
36
38
  const knowledgeResult = await collectChangeKnowledge(zhuanspecDir, archivedChange, knowledgeDir);
37
- if (knowledgeResult.decisions > 0) {
38
- console.log(chalk.gray(` - 决策记录:${knowledgeResult.decisions} 条`));
39
- }
40
39
  if (knowledgeResult.bestPractices > 0) {
41
40
  console.log(chalk.gray(` - 最佳实践:${knowledgeResult.bestPractices} 条`));
42
41
  }
@@ -49,13 +48,12 @@ async function runPostArchiveHook(archivedChange) {
49
48
  if (tempFilesCleaned > 0) {
50
49
  console.log(chalk.gray(` - 已清理:${tempFilesCleaned} 个`));
51
50
  }
52
- const totalEntries = knowledgeResult.decisions + knowledgeResult.bestPractices + knowledgeResult.implicitConventions;
51
+ const totalEntries = knowledgeResult.bestPractices + knowledgeResult.implicitConventions;
53
52
  console.log(chalk.green(`✅ 知识收集完成:共 ${totalEntries} 条记录`));
54
53
  return {
55
54
  continue: true,
56
- systemMessage: `Post-archive: knowledge updated for ${archivedChange}, ${knowledgeResult.decisions} decisions, ${knowledgeResult.bestPractices} best practices, ${knowledgeResult.implicitConventions} implicit conventions, ${tempFilesCleaned} temp files cleaned`,
55
+ systemMessage: `Post-archive: knowledge updated for ${archivedChange}, ${knowledgeResult.bestPractices} best practices, ${knowledgeResult.implicitConventions} implicit conventions, ${tempFilesCleaned} temp files cleaned`,
57
56
  hookSpecificOutput: {
58
- decisionsCollected: knowledgeResult.decisions,
59
57
  bestPracticesCollected: knowledgeResult.bestPractices,
60
58
  implicitConventionsCollected: knowledgeResult.implicitConventions,
61
59
  tempFilesCleaned,
@@ -68,7 +66,6 @@ async function runPostArchiveHook(archivedChange) {
68
66
  */
69
67
  async function collectChangeKnowledge(zhuanspecDir, archivedChange, knowledgeDir) {
70
68
  const result = {
71
- decisions: 0,
72
69
  bestPractices: 0,
73
70
  implicitConventions: 0,
74
71
  };
@@ -115,21 +112,19 @@ async function collectChangeKnowledge(zhuanspecDir, archivedChange, knowledgeDir
115
112
  return result;
116
113
  }
117
114
  try {
118
- // Create knowledge subdirectories
119
- const decisionsDir = path.join(knowledgeDir, 'decisions');
115
+ // Create knowledge subdirectories(仅三类:best-practices / implicit-conventions / troubleshooting;
116
+ // troubleshooting zhuanspec:knowledge skill 单独写入,此处不做归档提取)
120
117
  const bestPracticesDir = path.join(knowledgeDir, 'best-practices');
121
118
  const implicitConventionsDir = path.join(knowledgeDir, 'implicit-conventions');
122
- await FileSystemUtils.createDirectory(decisionsDir);
123
119
  await FileSystemUtils.createDirectory(bestPracticesDir);
124
120
  await FileSystemUtils.createDirectory(implicitConventionsDir);
125
121
  // Define regex patterns for each knowledge type (Chinese and English)
122
+ // 原“决策类”关键字统一归入 best-practices
126
123
  const patterns = {
127
- decisions: /^##\s+(决策|Decision|技术决策|架构决策)[\s\S]*?(?=^##[^#]|$)/gm,
128
- bestPractices: /^##\s+(最佳实践|Best\s+Practice|Best\s+Practices|推荐做法|建议|注意事项)[\s\S]*?(?=^##[^#]|$)/gm,
124
+ bestPractices: /^##\s+(最佳实践|Best\s+Practice|Best\s+Practices|推荐做法|建议|注意事项|决策|Decision|技术决策|架构决策)[\s\S]*?(?=^##[^#]|$)/gm,
129
125
  implicitConventions: /^##\s+(隐式约定|Implicit\s+Convention|默认行为|约定|惯例|约定事项)[\s\S]*?(?=^##[^#]|$)/gm,
130
126
  };
131
127
  const targetDirs = {
132
- decisions: decisionsDir,
133
128
  bestPractices: bestPracticesDir,
134
129
  implicitConventions: implicitConventionsDir,
135
130
  };
@@ -147,9 +142,7 @@ async function collectChangeKnowledge(zhuanspecDir, archivedChange, knowledgeDir
147
142
  const timestamp = getBeijingTimeForFilename();
148
143
  const fileName = `${archivedChange}-${type}-${timestamp}.md`;
149
144
  const filePath = path.join(targetDirs[type], fileName);
150
- const typeLabel = type === 'decisions' ? 'Decision Records' :
151
- type === 'bestPractices' ? 'Best Practices' :
152
- 'Implicit Conventions';
145
+ const typeLabel = type === 'bestPractices' ? 'Best Practices' : 'Implicit Conventions';
153
146
  const doc = `# ${typeLabel} from ${archivedChange}
154
147
 
155
148
  Extracted on: ${getBeijingTime()}
@@ -204,10 +204,30 @@ export interface CorrectionEditRecord {
204
204
  filePath: string;
205
205
  editLines: number;
206
206
  source: 'ai' | 'user';
207
- strategy: 'L1' | 'L2' | 'L3';
207
+ strategy: 'L1' | 'L2' | 'L3' | 'correction-context';
208
208
  agentType?: string;
209
209
  timestamp: string;
210
210
  }
211
+ /**
212
+ * 纠偏上下文标记 — 替代 accuracy snapshot 的精准归因机制。
213
+ *
214
+ * 由 resolve-correction CLI 在用户选择纠偏路径后写入 progress.json,
215
+ * 由 record-progress(post-tool) 在每次 Write/Edit 时消费:
216
+ * active=true → 行数进 userCorrectionLines(不进 linesAdded)
217
+ *
218
+ * 自动过期:
219
+ * - TTL 30 分钟(expiresAt)
220
+ * - 或 phase 切换时清除
221
+ *
222
+ * 设计意图:用"事中标记"替代"事后快照",消除对 apply→review 时机依赖。
223
+ */
224
+ export interface CorrectionContext {
225
+ active: boolean;
226
+ resolvedAt: string;
227
+ path: 'A' | 'B' | 'C' | 'D';
228
+ expiresAt: string;
229
+ phase: string;
230
+ }
211
231
  export interface ProgressData {
212
232
  changeId: string;
213
233
  sessionId: string;
@@ -250,6 +270,7 @@ export interface ProgressData {
250
270
  }>;
251
271
  lastEvent?: string;
252
272
  accuracy?: AccuracyData;
273
+ correctionContext?: CorrectionContext;
253
274
  }
254
275
  export declare function recordProgressHook(options: RecordProgressOptions): Promise<void>;
255
276
  /**
@@ -11,7 +11,7 @@ import { promises as fsPromises, existsSync, readFileSync, unlinkSync } from 'fs
11
11
  import { FileSystemUtils } from '../../utils/file-system.js';
12
12
  import { PHASE_ORDER } from '../../utils/phase-utils.js';
13
13
  import { resolveZhuanSpecRoot } from '../../utils/resolve-root.js';
14
- import { isCodeFile, determineEditSource, accumulateUserCorrection } from '../metrics/code-accuracy.js';
14
+ import { isCodeFile } from '../metrics/code-accuracy.js';
15
15
  import { detectHookHost, sanitizeCodexEnvelope } from '../../utils/hook-host.js';
16
16
  const fs = fsPromises;
17
17
  /**
@@ -313,13 +313,23 @@ function isProposalFile(filePath, changePath) {
313
313
  }
314
314
  /**
315
315
  * Generate a brief summary of proposal file changes from tool input
316
+ * 兑底不同工具的字段名:
317
+ * Write → content(完整文件内容)
318
+ * Edit → new_string(替换后的片段)
319
+ * MultiEdit → edits[0].new_string(取首项作为摘要)
316
320
  */
317
321
  function generateProposalChangeSummary(toolInput) {
318
322
  const content = typeof toolInput.content === 'string' ? toolInput.content : '';
319
- if (!content)
323
+ const newString = typeof toolInput.new_string === 'string' ? toolInput.new_string : '';
324
+ const edits = Array.isArray(toolInput.edits)
325
+ ? toolInput.edits
326
+ : [];
327
+ const firstEditNewString = edits.length > 0 && typeof edits[0]?.new_string === 'string' ? edits[0].new_string : '';
328
+ const text = content || newString || firstEditNewString;
329
+ if (!text)
320
330
  return 'File modified';
321
331
  // Extract first meaningful line as summary
322
- const lines = content.split('\n').filter(l => l.trim().length > 0);
332
+ const lines = text.split('\n').filter(l => l.trim().length > 0);
323
333
  if (lines.length === 0)
324
334
  return 'File modified';
325
335
  const firstLine = lines[0].trim();
@@ -380,6 +390,19 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
380
390
  if (progress.accuracy.userCorrectionLines === undefined)
381
391
  progress.accuracy.userCorrectionLines = 0;
382
392
  }
393
+ // Correction context recovery: check TTL expiration
394
+ if (progress.correctionContext) {
395
+ if (progress.correctionContext.active && progress.correctionContext.expiresAt) {
396
+ const expiresAt = new Date(progress.correctionContext.expiresAt).getTime();
397
+ if (Date.now() > expiresAt) {
398
+ progress.correctionContext.active = false;
399
+ }
400
+ }
401
+ // Auto-clear on phase change (context belongs to the phase it was created in)
402
+ if (progress.correctionContext.active && progress.correctionContext.phase !== progress.phase) {
403
+ progress.correctionContext.active = false;
404
+ }
405
+ }
383
406
  progress.stats = progress.stats || {
384
407
  tokenUsageTotal: 0,
385
408
  contextLoad: 0,
@@ -483,7 +506,7 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
483
506
  progress.currentNode = process.env.ZHUANSPEC_CURRENT_NODE || phase;
484
507
  progress.currentTask = currentTask || progress.currentTask;
485
508
  progress.toolCalls.push(toolCall);
486
- const WRITE_TOOLS = new Set(['Write', 'Edit', 'NotebookEdit']);
509
+ const WRITE_TOOLS = new Set(['Write', 'Edit', 'MultiEdit', 'NotebookEdit']);
487
510
  if (filePath && WRITE_TOOLS.has(toolName) && !progress.filesModified.includes(filePath)) {
488
511
  // Exclude ZhuanSpec internal files (spec/metrics/doc files inside zhuanspec/ dir)
489
512
  const absFilePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
@@ -494,11 +517,27 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
494
517
  }
495
518
  // Detect proposal file changes and record them
496
519
  const changePath = path.join(zhuanspecDir, 'changes', changeId);
497
- const WRITE_EDIT_TOOLS = new Set(['Write', 'Edit']);
520
+ const WRITE_EDIT_TOOLS = new Set(['Write', 'Edit', 'MultiEdit']);
498
521
  if (filePath && WRITE_EDIT_TOOLS.has(toolName) && isProposalFile(filePath, changePath)) {
499
522
  const relativeProposalPath = path.relative(changePath, path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath));
500
- const linesContent = typeof toolInput.content === 'string' ? toolInput.content : '';
501
- const contentLines = linesContent ? linesContent.split('\n').length : 0;
523
+ // 提案文件变更行数:按工具类型取不同字段,避免 Edit/MultiEdit 全记为 0
524
+ let contentLines = 0;
525
+ if (toolName === 'Write') {
526
+ const c = typeof toolInput.content === 'string' ? toolInput.content : '';
527
+ contentLines = c ? c.split('\n').length : 0;
528
+ }
529
+ else if (toolName === 'Edit') {
530
+ const ns = typeof toolInput.new_string === 'string' ? toolInput.new_string : '';
531
+ const os = typeof toolInput.old_string === 'string' ? toolInput.old_string : '';
532
+ contentLines = Math.max(ns ? ns.split('\n').length : 0, os ? os.split('\n').length : 0);
533
+ }
534
+ else if (toolName === 'MultiEdit' && Array.isArray(toolInput.edits)) {
535
+ for (const ed of toolInput.edits) {
536
+ const ns = ed?.new_string ? ed.new_string.split('\n').length : 0;
537
+ const os = ed?.old_string ? ed.old_string.split('\n').length : 0;
538
+ contentLines += Math.max(ns, os);
539
+ }
540
+ }
502
541
  const record = {
503
542
  changeRecordId: `pc-${Date.now()}`,
504
543
  timestamp,
@@ -529,27 +568,125 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
529
568
  }
530
569
  progress.proposalChanges.push(record);
531
570
  }
532
- // Try to estimate lines from content
533
- // 仅统计代码白名单文件,避免 .md/.json/.yml 等非代码文件污染 linesAdded。
534
- // accuracy 快照基线 (snapshotAiBaseline) 直接读 progress.linesAdded,因此必须在累计阶段就过滤。
535
- const content = stdinData.tool_input?.content;
571
+ // === 行数分账:correctionContext 精准归因(替代 snapshot 机制) ===
572
+ // 核心逻辑:
573
+ // correctionContext.active=true → 纠偏上下文内,行数进 userCorrectionLines
574
+ // correctionContext.active=false 正常 AI 产出,行数进 linesAdded
575
+ //
576
+ // 兑底:.pending-correction 文件存在也视同“纠偏中”,解决并行 Bash(resolve-correction)
577
+ // 与 Edit 的竞态(Edit 先完成时 correctionContext 尚未写入)。
578
+ const pendingMarkerPath = path.join(changeDir, '.pending-correction');
579
+ const pendingExists = existsSync(pendingMarkerPath);
580
+ // 不同工具的变更文本字段不同:
581
+ // Write → tool_input.content(完整文件内容)
582
+ // Edit → tool_input.new_string 与 tool_input.old_string
583
+ // MultiEdit → tool_input.edits[] 每项含 old_string / new_string
584
+ const toolInputContent = stdinData.tool_input?.content;
585
+ const toolInputNewString = stdinData.tool_input?.new_string;
586
+ const toolInputOldString = stdinData.tool_input?.old_string;
587
+ const toolInputEdits = stdinData.tool_input?.edits;
588
+ // 按工具类型计算本次变更涉及的行数
589
+ let changeLines = 0;
590
+ let contentSource = 'none';
591
+ if (toolName === 'Write') {
592
+ if (toolInputContent) {
593
+ changeLines = toolInputContent.split('\n').length;
594
+ contentSource = 'content';
595
+ }
596
+ }
597
+ else if (toolName === 'Edit') {
598
+ const newLines = toolInputNewString ? toolInputNewString.split('\n').length : 0;
599
+ const oldLines = toolInputOldString ? toolInputOldString.split('\n').length : 0;
600
+ // 取 max(new, old):体现本次编辑触及的代码量,兼容纯新增/纯删除/替换
601
+ changeLines = Math.max(newLines, oldLines);
602
+ contentSource = (toolInputNewString || toolInputOldString) ? 'new_string' : 'none';
603
+ }
604
+ else if (toolName === 'MultiEdit' && Array.isArray(toolInputEdits)) {
605
+ for (const edit of toolInputEdits) {
606
+ const newLines = edit?.new_string ? edit.new_string.split('\n').length : 0;
607
+ const oldLines = edit?.old_string ? edit.old_string.split('\n').length : 0;
608
+ changeLines += Math.max(newLines, oldLines);
609
+ }
610
+ contentSource = toolInputEdits.length > 0 ? 'edits[]' : 'none';
611
+ }
536
612
  let estimatedLines = 0;
537
- if (content && filePath && isCodeFile(filePath)) {
538
- const lines = content.split('\n').length;
539
- if (toolName === 'Write') {
540
- progress.linesAdded += lines;
541
- estimatedLines = lines;
613
+ const inCorrectionContext = !!(progress.correctionContext?.active) || pendingExists;
614
+ // === 调试日志:在 changeDir 下写 .correction-debug.log,方便排查“为什么没记录上” ===
615
+ const correctionDebugLog = (branch, addedTo, delta) => {
616
+ try {
617
+ const debugPath = path.join(changeDir, '.correction-debug.log');
618
+ const ccActive = !!(progress.correctionContext?.active);
619
+ const ccPath = progress.correctionContext?.path;
620
+ const ccExpiresAt = progress.correctionContext?.expiresAt;
621
+ const ccPhase = progress.correctionContext?.phase;
622
+ const isCode = !!(filePath && isCodeFile(filePath));
623
+ const entry = {
624
+ ts: getBeijingTime(),
625
+ tool: toolName,
626
+ file: filePath || null,
627
+ isCodeFile: isCode,
628
+ contentSource,
629
+ changeLines,
630
+ pendingExists,
631
+ correctionContext: progress.correctionContext
632
+ ? { active: ccActive, path: ccPath, expiresAt: ccExpiresAt, phase: ccPhase, currentPhase: progress.phase }
633
+ : null,
634
+ inCorrectionContext,
635
+ branch,
636
+ addedTo,
637
+ delta,
638
+ accuracy: {
639
+ userCorrectionLines: progress.accuracy?.userCorrectionLines ?? 0,
640
+ linesAdded: progress.linesAdded,
641
+ },
642
+ };
643
+ void fs.appendFile(debugPath, JSON.stringify(entry) + '\n', 'utf-8').catch(() => { });
542
644
  }
543
- else if (toolName === 'Edit') {
544
- progress.linesAdded += 1;
545
- estimatedLines = 1;
645
+ catch {
646
+ // 日志失败不阻断主流程
647
+ }
648
+ };
649
+ if (changeLines > 0 && filePath && isCodeFile(filePath)) {
650
+ estimatedLines = changeLines;
651
+ if (inCorrectionContext) {
652
+ // 纠偏上下文:行数计入 userCorrectionLines,不进 linesAdded
653
+ if (!progress.accuracy) {
654
+ progress.accuracy = {
655
+ aiLinesAdded: 0, aiLinesModified: 0, aiTotalLines: 0,
656
+ userCorrectionLines: 0, accuracyRate: 1.0,
657
+ correctionEdits: [],
658
+ };
659
+ }
660
+ progress.accuracy.userCorrectionLines = (progress.accuracy.userCorrectionLines || 0) + changeLines;
661
+ progress.accuracy.correctionEdits = progress.accuracy.correctionEdits || [];
662
+ progress.accuracy.correctionEdits.push({
663
+ filePath,
664
+ editLines: changeLines,
665
+ source: 'user',
666
+ strategy: 'correction-context',
667
+ agentType: progress.correctionContext?.path,
668
+ timestamp,
669
+ });
670
+ correctionDebugLog(`${toolName}/correction`, 'userCorrectionLines', changeLines);
671
+ }
672
+ else {
673
+ // 正常 AI 产出
674
+ progress.linesAdded += changeLines;
675
+ correctionDebugLog(`${toolName}/normal`, 'linesAdded', changeLines);
546
676
  }
547
677
  }
548
- // Accuracy tracking: accumulate user correction lines after snapshot
549
- const snapshotExists = !!(progress.accuracy?.snapshotTimestamp);
550
- if (isCodeFile(filePath) && snapshotExists && WRITE_EDIT_TOOLS.has(toolName) && estimatedLines > 0) {
551
- const result = determineEditSource(phase, snapshotExists, stdinData);
552
- accumulateUserCorrection(progress, estimatedLines, filePath, result.source, result.strategy, result.agentType);
678
+ else {
679
+ // 未进入计量分支:记录原因(非代码文件 / 字段缺失 / 非写入工具)
680
+ if (toolName === 'Write' || toolName === 'Edit' || toolName === 'MultiEdit') {
681
+ const reason = !filePath
682
+ ? 'no-file-path'
683
+ : !isCodeFile(filePath)
684
+ ? 'non-code-file'
685
+ : changeLines === 0
686
+ ? `empty-${contentSource}`
687
+ : 'unknown';
688
+ correctionDebugLog(`skipped/${reason}`, 'skipped', 0);
689
+ }
553
690
  }
554
691
  const tokenUsageFromEnv = Number(process.env.ZHUANSPEC_TOKEN_USAGE || 0);
555
692
  const contextLoadFromEnv = Number(process.env.ZHUANSPEC_CONTEXT_LOAD || 0);
@@ -245,18 +245,34 @@ async function runSummarize() {
245
245
  .map((c, i) => ` ${i + 1}. ${c.promptPreview || '(no preview)'}`)
246
246
  .join('\n');
247
247
  pitfallAdditionalContext = [
248
- '## ⚠️ ZhuanSpec 沉淀询问(收尾必做)',
248
+ '## ⚠️ ZhuanSpec 知识沉淀询问(收尾必做)',
249
249
  '',
250
- `本轮会话发生过 ${n} 次用户纠偏,请在收尾响应中**必须**调用 \`askUserQuestion\` 以二选一方式询问用户:`,
251
- ' - 是,沉淀到 `zhuanspec/knowledge/troubleshooting/<timestamp>-<slug>.md`',
252
- ' - 否,本次不沉淀',
250
+ `本轮会话发生过 ${n} 次用户纠偏且尚未沉淀,必须按以下顺序处理(严禁跳步,严禁自行 Write 知识文件):`,
253
251
  '',
254
- '**纠偏记录摘要(最近 3 条):**',
252
+ '### 必须执行的步骤',
253
+ '',
254
+ `1. **用 askUserQuestion 询问用户**(二选一):`,
255
+ ' - header: "踩坑沉淀"',
256
+ ` - question: "本轮 Apply 阶段发生过 ${n} 次用户纠偏,是否沉淀为项目知识?"`,
257
+ ' - 选项 1: "是,沉淀"',
258
+ ' - 选项 2: "否,跳过"',
259
+ '',
260
+ `2. **用户选"是"**:**必须调用 \`@skill:zhuanspec:knowledge --mode=correction --correction <change-id>\`** (或等价的 \`/zhuanspec:knowledge\` slash command 并在交互中选择模式 B)完成沉淀。`,
261
+ ` - change-id 值为 \`${changeId}\``,
262
+ ' - **禁止直接 Write/Edit 创建 \`zhuanspec/knowledge/\` 下的任何文件**,禁止自行判定分类或文件名格式;所有写入由 skill 统一执行。',
263
+ ' - skill 将自动完成:读取 .correction-log → 分类决策树 → 去重扫描 → 套用统一 Front Matter → 写入对应分类目录 → 更新 index.md → 标记 resolve-correction。',
264
+ '',
265
+ `3. **用户选"否"**:运行 \`zhuanspec progress resolve-correction ${changeId} --mark-pitfall-saved\` 标记已处理,不写入任何知识文件。`,
266
+ '',
267
+ '### 纠偏记录摘要(最近 3 条 promptPreview)',
255
268
  previews || ' (none)',
256
269
  '',
257
- '用户回答后,请将结果记到 progress.json 的 `askedPitfallSaved` 字段(可通过 CLI 或直接写入),避免重复询问。',
270
+ '### 红线提示',
271
+ '- 禁止绕过 skill 直接 Write/Edit 知识文件(等同于绕过质量门禁)。',
272
+ '- 禁止新建 decisions/ 或三类目录以外的分类。',
273
+ '- skill 模式 B 完成后会自动写入 askedPitfallSaved=true,无需人工重复调用 CLI。',
258
274
  ].join('\n');
259
- systemMessage += `\n⚠️ 检测到 ${n} 次用户纠偏未沉淀,请按下方指令调用 askUserQuestion。\n`;
275
+ systemMessage += `\n⚠️ 检测到 ${n} 次用户纠偏未沉淀——请按上方说明调 askUserQuestion,用户选是后必须调用 @skill:zhuanspec:knowledge 而非自行 Write。\n`;
260
276
  }
261
277
  return {
262
278
  continue: true,
package/dist/core/init.js CHANGED
@@ -564,17 +564,19 @@ export class InitCommand {
564
564
  if (!(await FileSystemUtils.fileExists(indexPath))) {
565
565
  const knowledgeIndexContent = `# Knowledge Index
566
566
 
567
+ 本项目知识库仅保留三类目录:踩坑、最佳实践、隐式约定。所有写入必须经 \`@skill:zhuanspec:knowledge\` 统一完成。
568
+
567
569
  ## Troubleshooting
568
570
 
569
- <!-- Entries will be added by collect-knowledge hook -->
571
+ <!-- 踩坑 / bug / 错误做法 / 陷阱规避,由 zhuanspec:knowledge skill 统一写入 -->
570
572
 
571
573
  ## Best Practices
572
574
 
573
- <!-- Project-specific best practices -->
575
+ <!-- 可复用的最佳实践 / 设计决策落地,由 zhuanspec:knowledge skill 统一写入 -->
574
576
 
575
577
  ## Implicit Conventions
576
578
 
577
- <!-- Conventions discovered during implementation -->
579
+ <!-- 隐式约定 / 默认行为 / 团队惯例,由 zhuanspec:knowledge skill 统一写入 -->
578
580
  `;
579
581
  await FileSystemUtils.writeFile(indexPath, knowledgeIndexContent);
580
582
  }
@@ -37,15 +37,18 @@ export declare function snapshotAiBaseline(progress: ProgressData, changeDir: st
37
37
  */
38
38
  export declare function ensureAccuracySnapshot(changeDir: string): Promise<boolean>;
39
39
  /**
40
- * 快照后用户纠正行数累计
40
+ * 用户纠正行数累计(legacy——新代码优先用 record-progress.ts 内联的 correctionContext 逻辑)。
41
+ *
42
+ * 保留此函数用于向后兼容(旧 post-apply/review 路径仍可调用)。
43
+ *
41
44
  * @param progress - progress.json 数据
42
45
  * @param editLines - 本次 Edit 操作的变更行数
43
46
  * @param filePath - 文件路径(用于过滤)
44
47
  * @param source - 'ai' | 'user'
45
- * @param strategy - 判定策略 'L1' | 'L2' | 'L3'
48
+ * @param strategy - 判定策略 'L1' | 'L2' | 'L3' | 'correction-context'
46
49
  * @param agentType - 可选 agent 类型
47
50
  */
48
- export declare function accumulateUserCorrection(progress: ProgressData, editLines: number, filePath: string, source: 'ai' | 'user', strategy: 'L1' | 'L2' | 'L3', agentType?: string): void;
51
+ export declare function accumulateUserCorrection(progress: ProgressData, editLines: number, filePath: string, source: 'ai' | 'user', strategy: 'L1' | 'L2' | 'L3' | 'correction-context', agentType?: string): void;
49
52
  export interface AccuracyResult {
50
53
  rate: number;
51
54
  rateRaw: number;
@@ -54,8 +57,14 @@ export interface AccuracyResult {
54
57
  overridden: boolean;
55
58
  }
56
59
  /**
57
- * 计算首次代码准确率
58
- * rate = 1 - (userCorrectionLines / aiTotalLines),clamp 到 [0, 1]
60
+ * 计算首次代码准确率。
61
+ *
62
+ * 新的精准归因模型(基于 correctionContext):
63
+ * aiTotalLines = progress.linesAdded(正常 AI 产出,不含纠偏行数)
64
+ * userCorrectionLines = progress.accuracy.userCorrectionLines(纠偏上下文内累计)
65
+ * rate = 1 - (userCorrectionLines / aiTotalLines),clamp 到 [0, 1]
66
+ *
67
+ * 向后兼容:若 accuracy 中存在 snapshot 基线(旧数据),优先用旧字段计算。
59
68
  */
60
69
  export declare function computeAccuracy(progress: ProgressData): AccuracyResult;
61
70
  export interface OverrideResult {
@@ -51,12 +51,13 @@ export async function snapshotAiBaseline(progress, changeDir) {
51
51
  aiLinesAdded,
52
52
  aiLinesModified,
53
53
  aiTotalLines,
54
- userCorrectionLines: 0,
55
- accuracyRate: 1.0, // 初始为100%,快照时无纠正
54
+ // 保留已有纠正数据(correctionContext 机制可能在快照前已累积),而非硬编码为 0
55
+ userCorrectionLines: progress.accuracy?.userCorrectionLines || 0,
56
+ accuracyRate: 1.0,
56
57
  accuracyRateRaw: undefined,
57
58
  lastComputedAt: undefined,
58
- overrideHistory: [],
59
- correctionEdits: [],
59
+ overrideHistory: progress.accuracy?.overrideHistory || [],
60
+ correctionEdits: progress.accuracy?.correctionEdits || [],
60
61
  };
61
62
  progress.accuracy = accuracy;
62
63
  // 写入独立的 accuracy.json
@@ -113,22 +114,36 @@ export async function ensureAccuracySnapshot(changeDir) {
113
114
  // 用户纠正行数累计
114
115
  // ============================================================
115
116
  /**
116
- * 快照后用户纠正行数累计
117
+ * 用户纠正行数累计(legacy——新代码优先用 record-progress.ts 内联的 correctionContext 逻辑)。
118
+ *
119
+ * 保留此函数用于向后兼容(旧 post-apply/review 路径仍可调用)。
120
+ *
117
121
  * @param progress - progress.json 数据
118
122
  * @param editLines - 本次 Edit 操作的变更行数
119
123
  * @param filePath - 文件路径(用于过滤)
120
124
  * @param source - 'ai' | 'user'
121
- * @param strategy - 判定策略 'L1' | 'L2' | 'L3'
125
+ * @param strategy - 判定策略 'L1' | 'L2' | 'L3' | 'correction-context'
122
126
  * @param agentType - 可选 agent 类型
123
127
  */
124
128
  export function accumulateUserCorrection(progress, editLines, filePath, source, strategy, agentType) {
125
- // 快照不存在 → 不累计纠正
126
- if (!progress.accuracy?.snapshotTimestamp)
129
+ // 新路径:correctionContext 存在时直接允许
130
+ const isCorrectionContext = !!(progress.correctionContext?.active);
131
+ const isLegacySnapshot = !!(progress.accuracy?.snapshotTimestamp);
132
+ // 既无快照也无 correctionContext → 不累计纠正
133
+ if (!isCorrectionContext && !isLegacySnapshot)
127
134
  return;
128
135
  // 非代码文件 → 不累计纠正
129
136
  if (!isCodeFile(filePath))
130
137
  return;
131
- // source=user 且快照已建立才累计
138
+ // 确保 accuracy 结构存在
139
+ if (!progress.accuracy) {
140
+ progress.accuracy = {
141
+ aiLinesAdded: 0, aiLinesModified: 0, aiTotalLines: 0,
142
+ userCorrectionLines: 0, accuracyRate: 1.0,
143
+ correctionEdits: [],
144
+ };
145
+ }
146
+ // 仅 source=user 时累计
132
147
  if (source === 'user') {
133
148
  if (progress.accuracy.userCorrectionLines === undefined) {
134
149
  progress.accuracy.userCorrectionLines = 0;
@@ -150,20 +165,37 @@ export function accumulateUserCorrection(progress, editLines, filePath, source,
150
165
  progress.accuracy.correctionEdits.push(record);
151
166
  }
152
167
  /**
153
- * 计算首次代码准确率
154
- * rate = 1 - (userCorrectionLines / aiTotalLines),clamp 到 [0, 1]
168
+ * 计算首次代码准确率。
169
+ *
170
+ * 新的精准归因模型(基于 correctionContext):
171
+ * aiTotalLines = progress.linesAdded(正常 AI 产出,不含纠偏行数)
172
+ * userCorrectionLines = progress.accuracy.userCorrectionLines(纠偏上下文内累计)
173
+ * rate = 1 - (userCorrectionLines / aiTotalLines),clamp 到 [0, 1]
174
+ *
175
+ * 向后兼容:若 accuracy 中存在 snapshot 基线(旧数据),优先用旧字段计算。
155
176
  */
156
177
  export function computeAccuracy(progress) {
157
178
  const accuracy = progress.accuracy;
158
- if (!accuracy) {
159
- return { rate: 1.0, rateRaw: 1.0, aiTotalLines: 0, userCorrectionLines: 0, overridden: false };
179
+ // 向后兼容:旧快照/旧数据路径(有 aiTotalLines 数据即可走旧路径)
180
+ if (accuracy?.aiTotalLines && accuracy.aiTotalLines > 0) {
181
+ const aiTotal = accuracy.aiTotalLines || 0;
182
+ const correction = accuracy.userCorrectionLines || 0;
183
+ const rateRaw = aiTotal > 0
184
+ ? Math.max(0, Math.min(1, 1 - correction / aiTotal))
185
+ : 1.0;
186
+ const hasOverride = !!(accuracy.overrideHistory && accuracy.overrideHistory.length > 0);
187
+ const rate = hasOverride
188
+ ? accuracy.overrideHistory[accuracy.overrideHistory.length - 1].overriddenRate
189
+ : rateRaw;
190
+ return { rate, rateRaw, aiTotalLines: aiTotal, userCorrectionLines: correction, overridden: hasOverride };
160
191
  }
161
- const aiTotal = accuracy.aiTotalLines || 0;
162
- const correction = accuracy.userCorrectionLines || 0;
192
+ // 新路径:基于 correctionContext 的精准归因
193
+ const aiTotal = progress.linesAdded || 0;
194
+ const correction = accuracy?.userCorrectionLines || 0;
163
195
  const rateRaw = aiTotal > 0
164
196
  ? Math.max(0, Math.min(1, 1 - correction / aiTotal))
165
197
  : 1.0;
166
- const hasOverride = !!(accuracy.overrideHistory && accuracy.overrideHistory.length > 0);
198
+ const hasOverride = !!(accuracy?.overrideHistory && accuracy.overrideHistory.length > 0);
167
199
  const rate = hasOverride
168
200
  ? accuracy.overrideHistory[accuracy.overrideHistory.length - 1].overriddenRate
169
201
  : rateRaw;
@@ -584,7 +584,9 @@ ${baseGuardrails}
584
584
  * 条件 2:任务类型是否适合 TDD?判定依据是**是否存在可测试行为**,与技术栈/前后端无关。适合(后端业务逻辑/接口实现/复杂查询,以及前端组件交互/Hooks/表单校验/utils/services 等可测行为)→ \`tddApplyAgent\`;不适合(DDL/配置/DTO/ES索引/Mapper XML/前端纯静态展示)→ 降级为 \`applyAgent\`
585
585
  * 决策记录必须注入 subagent prompt 并写入任务报告(Agent 类型/标注情况/任务类型/TDD 适用性)
586
586
  - **Rules 和 Knowledge 注入**:编排 Agent MUST 在每个 subagent prompt 中注入 Rules(根据 @skill)和 Knowledge(踩坑警告、最佳实践)。
587
- - **偏差处理**:PreToolUse Hook 检测到提案范围外修改时弹出选项式交互(更新提案/Bug修复豁免/取消)。3次偏差后强制更新提案。`;
587
+ - **偏差处理**:PreToolUse Hook 检测到提案范围外修改时弹出选项式交互(更新提案/Bug修复豁免/取消)。3次偏差后强制更新提案。
588
+ - **⚠️ 纠偏处理(红线,禁止委派 subagent)**:当 \`progress.json\` 中 \`correctionContext.active === true\` 时,主 agent **必须亲自处理所有代码修改**,**严禁**将纠偏任务分发给 subagent(applyAgent/tddApplyAgent)。原因:(1) 纠偏代码修改必须走主 agent 的 PostToolUse hook 才能正确计入 \`userCorrectionLines\`;(2) subagent 无法感知纠偏上下文,可能扩大修改范围。违反此约束将导致准确率数据丢失。
589
+ - **correctionContext 写入时序**:\`resolve-correction\` CLI 写入 \`correctionContext\` 后,主 agent 必须在**修改任何代码之前**确认标记已写入(检查 progress.json 中 \`correctionContext.active === true\`),确保后续的 PostToolUse hook 能将修改行数路由到 \`userCorrectionLines\`。`;
588
590
  const applyReferences = `**参考**
589
591
  - 如果在实施过程中需要提案的额外上下文,请使用 \`zhuanspec show <id> --json --deltas-only\`。`;
590
592
  const archiveSteps = `**前置检查**:
@@ -754,6 +756,26 @@ const reviewGuardrails = `${baseGuardrails}\n- **门禁审查阶段**:review
754
756
  - 轨道 4 产物 \`closure-check-result.json\` 必须由 AI 实际扫描代码后写入,禁止伪造 \`overallStatus = PASS\`。
755
757
  - **Schema 版本**:新生成的 \`code-review-result.json\` / \`unit-test-result.json\` / \`closure-check-result.json\` 必须包含 \`"schemaVersion": 2\` 字段以启用 Skill 真实性硬校验;缺失或 < 2 时 CLI 会按历史数据豁免并打 ⚠️ 警告,禁止借 legacy 通道规避校验。`;
756
758
  const reviewSteps = `**步骤**
759
+ 0. **⚠️ 纠偏沉淀优先检查(Phase 切换前强制执行)**:
760
+ - 读取 \`zhuanspec/changes/<change-id>/metrics/progress.json\`,检查以下两个字段:
761
+ - \`corrections\` 数组是否存在且有元素(\`length > 0\`)
762
+ - \`askedPitfallSaved\` 字段是否为 \`true\`
763
+ - **如果满足条件(corrections 非空 且 askedPitfallSaved !== true)**:
764
+ - **立即调用 \`AskUserQuestion\` 工具**,以二选一方式询问用户:
765
+ \`\`\`
766
+ header: "踩坑沉淀"
767
+ question: "本轮 Apply 阶段发生过 N 次用户纠偏(N 为 corrections 数组的实际长度,请列出最近 3 条 promptPreview 摘要),是否沉淀为项目知识?"
768
+ 选项 1: "是,沉淀"
769
+ 选项 2: "否,跳过"
770
+ \`\`\`
771
+ - **用户选择"是"时**:
772
+ 1. **必须调用 \`@skill:zhuanspec:knowledge --mode=correction --correction <change-id>\`**(或等价的 \`/zhuanspec:knowledge\` slash command 并在交互中选择模式 B)来完成沉淀
773
+ 2. 本 skill 将自动完成:读取 \`.correction-log\` → 分类决策树归入 \`troubleshooting/\` / \`best-practices/\` / \`implicit-conventions/\` → 去重扫描 → 套用统一 Front Matter → 写入知识文件 → 更新 \`knowledge/index.md\` → 运行 \`zhuanspec progress resolve-correction <change-id> --mark-pitfall-saved\`
774
+ 3. **红线**:**禁止主 agent 自行使用 Write/Edit 创建 \`zhuanspec/knowledge/\` 下的任何文件**,禁止自行判定分类或文件名格式;禁止新建 \`decisions/\` 或三类目录以外的分类。
775
+ - **用户选择"否"时**:
776
+ - 运行 \`zhuanspec progress resolve-correction <change-id> --mark-pitfall-saved\` 标记已处理,不写入任何知识文件
777
+ - **如果不满足条件(无纠偏或已沉淀)**:直接进入下一步
778
+
757
779
  1. **切换 Phase 到 review(第一步,必须立即执行)**:
758
780
  - 如果此提示已包含特定的变更 ID,请使用该值;否则运行 \`zhuanspec list\` 显示活跃变更并询问用户要审查哪个
759
781
  - **必须立即运行**:\`zhuanspec progress set-phase <change-id> review\`
@@ -858,62 +880,125 @@ const reviewReferences = `**参考**
858
880
  - \`generate-mockito-unit-test\` Skill 结果由 AI 写入 \`unit-test-result.json\`(必须登记 \`newTestsGenerated\` 或 \`skipReason\`)
859
881
  - \`code-review-expert\` Skill 结果由 AI 写入 \`code-review-result.json\`(必须登记 \`skillReportPath\`,指向 Skill 生成的 \`doc/code-review-*.md\` 报告)
860
882
  - 轨道 4 闭环检查结果由 AI 写入 \`closure-check-result.json\`,要求 \`overallStatus = PASS\` 且 \`skeletonCodeCount = 0\``;
861
- const knowledgeGuardrails = `${baseGuardrails}\n- **知识管理阶段**:knowledge 命令用于管理项目级知识库(最佳实践、陷阱、隐式约定)。
883
+ const knowledgeGuardrails = `${baseGuardrails}
884
+ - **知识管理阶段**:knowledge 命令用于管理项目级知识库。**只保留三类目录**:\`troubleshooting/\`(踩坑/bug/错误做法)、\`best-practices/\`(可复用的最佳实践、设计决策落地)、\`implicit-conventions/\`(隐式约定、默认行为、团队惯例)。**禁止新建其他分类目录**(如 decisions/、architecture/ 等)。
862
885
  - **跨变更积累**:知识不绑定单个变更,是长期积累的项目资产。
886
+ - **入口收口(红线)**:所有写入 \`zhuanspec/knowledge/\` 的动作都必须通过本 skill 完成。
887
+ - Stop hook / review 步骤 0 检出 \`corrections.length > 0 && askedPitfallSaved !== true\` 且用户确认沉淀时,主 agent **只能调用本 skill(模式 B)** 来写入,严禁直接使用 Write/Edit 创建 knowledge 文件,严禁跳过分类决策树,严禁自拟文件名格式。
888
+ - PostToolUse collect-knowledge hook 的提示同理——用户确认后必须调用本 skill,而非自由 Write。
889
+ - 违反此红线等同于绕过质量门禁。
890
+ - **Front Matter 强制**:每篇知识文件必须包含统一前置元信息(发现时间 / 来源变更 / 关键词 / 来源 / 背景 / 问题 / 解决方案 / 相关代码),与项目参考样例 \`zhuanspec/knowledge/best-practices/oms-call-pattern.md\` 格式一致。
863
891
  - **职责分离**:
864
- - **CLI 自动提取**:归档时自动从变更目录(proposal.mdtasks.md、design.md、specs/*.md)提取结构化知识
865
- - **AI 手动补充**:从本次会话记忆中补充无法自动提取的知识(如隐式约定发现、对话中的关键决策)`;
892
+ - **CLI 自动提取**(归档阶段 post-archive):从变更目录 design.md/proposal.md/tasks.md/specs 抽取结构化知识。CLI 的"决策类"输出归并到 \`best-practices/\`,不再写入 \`decisions/\`。
893
+ - **AI 手动补充**(本 skill 模式 A):从会话记忆中补充无法自动提取的知识。
894
+ - **纠偏沉淀**(本 skill 模式 B):对接 apply 阶段 \`.correction-log\` 的纠偏记录。`;
866
895
  const knowledgeSteps = `**步骤**
867
- 1. **选择操作类型**:
868
- - 查看:列出当前知识库内容
869
- - 添加:从会话记忆手动添加新的知识条目(CLI 无法自动提取的内容)
870
- - 搜索:按关键词搜索相关知识
871
-
872
- 2. **查看知识库**:
873
- - 运行 \`ls zhuanspec/knowledge/\` 查看目录结构
874
- - 阅读 \`zhuanspec/knowledge/index.md\` 查看索引
875
- - 使用 \`cat zhuanspec/knowledge/decisions/*.md\` 查看决策记录
876
- - 使用 \`cat zhuanspec/knowledge/best-practices/*.md\` 查看最佳实践
877
-
878
- 3. **从会话添加知识条目**(手动补充 CLI 无法自动提取的内容):
879
- - 选择分类目录(decisions/best-practices/implicit-conventions)
880
- - 基于本次会话中的发现,使用模板创建条目:
881
- \`\`\`markdown
882
- # [标题]
883
896
 
884
- **发现时间**: [YYYY-MM-DD]
885
- **来源变更**: [change-id](可选)
886
- **关键词**: [关键词列表]
887
- **来源**: 会话记忆(AI 补充)
897
+ skill 支持两种模式,触发方式不同、流程不同:
898
+ - **模式 A(manual)**:用户手动触发 \`/zhuanspec:knowledge\`,或显式传参 \`--mode=manual\`;用于查看、搜索、从会话记忆补充知识。
899
+ - **模式 B(correction)**:由 Stop hook / review 步骤 0 / PostToolUse collect-knowledge hook 引导调用,或显式传参 \`--correction <change-id>\` / \`--mode=correction\`;用于将 apply 阶段的纠偏记录规范化沉淀为踩坑/约定/最佳实践。
888
900
 
889
- ## 背景
901
+ ## 通用资产:分类决策树(两种模式共用)
890
902
 
891
- [发现该知识点的背景描述 - 来自本次对话]
903
+ 对每条待沉淀内容,按以下决策树**逐项**判定分类(必须显式回答三问):
904
+ 1. 这是"做错了/踩坑/bug/错误做法/需要规避的陷阱"吗?→ \`troubleshooting/\`
905
+ 2. 这是"某个默认行为 / 隐式约定 / 框架默认值 / 团队未明文但必须遵守的惯例"吗?→ \`implicit-conventions/\`
906
+ 3. 这是"可复用的最佳实践 / 成功经验 / 设计决策落地方案(如异步调用模式、批量处理范式)"吗?→ \`best-practices/\`
892
907
 
893
- ## 问题/经验
908
+ 同一条内容若同时命中多类,按"踩坑 > 约定 > 最佳实践"优先级归入**更靠前**的一类。禁止新建 decisions/ 或其他目录。
894
909
 
895
- [具体问题或经验描述 - CLI 无法自动提取的内容]
910
+ ## 通用资产:统一 Front Matter 模板(两种模式共用)
896
911
 
897
- ## 解决方案/建议
912
+ \`\`\`markdown
913
+ # <简短主题>
898
914
 
899
- [解决方案或最佳实践建议]
915
+ **发现时间**: <YYYY-MM-DD>
916
+ **来源变更**: <change-id 或留空>
917
+ **关键词**: <逗号分隔 5-10 个检索关键词>
918
+ **来源**: <"用户纠偏" | "会话记忆(AI 补充)" | "技术方案对比" | 具体来源描述>
900
919
 
901
- ## 相关代码
920
+ ## 背景
902
921
 
903
- - \`[相关文件路径]\` - [说明]
904
- \`\`\`
905
- - **优先补充**:
906
- - 对话中发现但未写入文档的隐式约定
907
- - 调试过程中发现的陷阱
908
- - 团队特有的最佳实践
909
-
910
- 4. **搜索知识**:
911
- - 使用 \`rg "[关键词]" zhuanspec/knowledge/\` 进行搜索
912
- - 结合变更上下文进行知识推荐`;
922
+ <为什么出现、在什么场景下发现>
923
+
924
+ ## 问题 / 经验
925
+
926
+ <具体问题描述 或 可复用经验的触发条件>
927
+
928
+ ## 解决方案 / 建议
929
+
930
+ <正确做法 / 最佳模式 / 规避方式;给出错误示例 vs 正确示例(代码块)更佳>
931
+
932
+ ## 相关代码
933
+
934
+ - \`<相对路径>\` - <说明>
935
+ \`\`\`
936
+
937
+ 文件命名:\`<YYYYMMDD>-<kebab-slug>.md\`;slug 由主题生成(例:\`20260506-apply-enum-hardcoding.md\`),不得使用随机字符串。
938
+
939
+ ---
940
+
941
+ ## 模式 A:手动补充 / 查看 / 搜索
942
+
943
+ 1. **选择子操作**:查看 / 搜索 / 添加(三选一)。
944
+ 2. **查看**:
945
+ - \`ls zhuanspec/knowledge/\` 查看三类目录
946
+ - \`cat zhuanspec/knowledge/index.md\` 查看索引
947
+ - \`cat zhuanspec/knowledge/best-practices/*.md\` / \`troubleshooting/\` / \`implicit-conventions/\` 查看具体条目
948
+ 3. **搜索**:
949
+ - \`rg "<关键词>" zhuanspec/knowledge/\`
950
+ - 结合 \`index.md\` 关键词段快速锁定
951
+ 4. **从会话添加**:
952
+ - 先按**分类决策树**判定归属目录;**禁止**凭习惯写到 decisions/
953
+ - 先 \`rg "<主关键词>" zhuanspec/knowledge/<category>/\` 做去重扫描:若命中高度相似条目 → 在已有文件追加 \`## 追加:<日期>\` 章节,不新建文件
954
+ - 否则新建 \`zhuanspec/knowledge/<category>/<YYYYMMDD>-<kebab-slug>.md\`,严格套用上面的 Front Matter 模板
955
+ - **同步更新** \`zhuanspec/knowledge/index.md\`:在对应分区(\`## Troubleshooting\` / \`## Best Practices\` / \`## Implicit Conventions\`)追加一行 \`- [<标题>](<category>/<文件名>) — <关键词摘要>\`
956
+
957
+ ---
958
+
959
+ ## 模式 B:纠偏沉淀(Apply 阶段收尾 / Review 步骤 0 / PostToolUse 触发)
960
+
961
+ 1. **确认 change-id**:
962
+ - 优先使用入参 \`--correction <change-id>\`
963
+ - 否则读取 \`zhuanspec/changes/*/metrics/progress.json\`,定位 \`corrections.length > 0 && askedPitfallSaved !== true\` 的 change-id
964
+ - 同时存在多个时,调用 \`askUserQuestion\` 让用户二选一,严禁随意选择
965
+ 2. **读取原始纠偏记录**(两份必须都读):
966
+ - \`zhuanspec/changes/<change-id>/.correction-log\`(JSONL,每行含 \`ts\` / \`event\` / \`promptPreview\` / \`reason?\`)
967
+ - \`zhuanspec/changes/<change-id>/metrics/progress.json\` 的 \`corrections\` 数组
968
+ 3. **用户二次确认(强制)**:
969
+ - 将最近 3 条 promptPreview 摘要打印给用户
970
+ - 调 \`askUserQuestion\`:\`是,沉淀 / 否,跳过\`
971
+ - 用户选"否"时,直接跳到步骤 7 只做 resolve-correction --mark-pitfall-saved,不写入任何文件
972
+ 4. **逐条分类**:对每条 correction 走上面的「分类决策树」,得到 \`<category>\`。
973
+ 5. **去重扫描**:对每条(含其关键词)先执行 \`rg "<主关键词>" zhuanspec/knowledge/<category>/\`:
974
+ - 命中相似条目 → 在已有文件追加 \`## 追加:<日期> — <change-id>\` 章节
975
+ - 未命中 → 新建 \`zhuanspec/knowledge/<category>/<YYYYMMDD>-<kebab-slug>.md\`
976
+ 6. **写入文件**(严格使用统一 Front Matter 模板):
977
+ - \`**来源**\` 字段固定写 \`用户纠偏(change: <change-id>)\`
978
+ - \`**来源变更**\` 字段固定写 \`<change-id>\`
979
+ - \`## 背景\` / \`## 问题 / 经验\` / \`## 解决方案 / 建议\` 必须基于 \`.correction-log\` 原始 promptPreview 与当次会话上下文还原,**禁止编造**
980
+ - 同步更新 \`zhuanspec/knowledge/index.md\` 对应分区
981
+ 7. **标记已处理(强制收尾,无论步骤 3 用户选是/否)**:
982
+ - 运行 \`zhuanspec progress resolve-correction <change-id> --mark-pitfall-saved\`
983
+ - 确认 \`progress.json.askedPitfallSaved === true\`,避免下次 Stop hook 重复询问
984
+ 8. **输出摘要**:列出本轮新建 / 追加的知识文件路径 + index.md 更新行;报告 \`<change-id>\` 的纠偏沉淀已完成。
985
+
986
+ ---
987
+
988
+ ## 硬约束回顾
989
+ - 所有写入 knowledge 目录的动作只能经本 skill 完成;禁止主 agent 或其他流程自行 Write。
990
+ - 只能写入三类目录之一,禁止新建 decisions/。
991
+ - 文件名、Front Matter、index.md 更新三者缺一不可。
992
+ - 模式 B 必须以 \`resolve-correction --mark-pitfall-saved\` 收尾。`;
913
993
  const knowledgeReferences = `**参考**
914
- - CLI 归档时自动从变更目录提取结构化知识(design.md 中的决策、proposal.md 中的注意事项等)
915
- - 本 skill 用于从会话记忆中补充无法自动提取的知识
916
- - 知识库目录结构:decisions(决策记录)、best-practices(最佳实践)、implicit-conventions(隐式约定)`;
994
+ - 参考样例(格式基准):项目 \`zhuanspec/knowledge/best-practices/oms-call-pattern.md\`、\`zhuanspec/knowledge/troubleshooting/20260506-apply-pitfalls.md\`、\`zhuanspec/knowledge/implicit-conventions/outbound-cis-inventory-lock.md\`
995
+ - 知识库目录结构(仅此三类):
996
+ - \`troubleshooting/\` — 踩坑、bug、错误做法、陷阱规避
997
+ - \`best-practices/\` — 可复用的最佳实践、设计决策落地
998
+ - \`implicit-conventions/\` — 隐式约定、默认行为、团队惯例
999
+ - CLI 归档时自动从变更目录提取知识(post-archive hook);所有"决策类"输出归入 \`best-practices/\`
1000
+ - 模式 B 依赖文件:\`zhuanspec/changes/<change-id>/.correction-log\`(JSONL)、\`metrics/progress.json.corrections\`
1001
+ - 模式 B 收尾命令:\`zhuanspec progress resolve-correction <change-id> --mark-pitfall-saved\``;
917
1002
  export const slashCommandBodies = {
918
1003
  proposal: [proposalGuardrails, proposalOutputFormat, proposalSteps, proposalReferences].join('\n\n'),
919
1004
  design: [designGuardrails, designSteps, designReferences].join('\n\n'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhuan-ai/zhuanspec",
3
- "version": "2.15.1",
3
+ "version": "2.15.7",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "zhuanspec",
@@ -39,26 +39,6 @@
39
39
  "!dist/**/__tests__",
40
40
  "!dist/**/*.map"
41
41
  ],
42
- "scripts": {
43
- "lint": "eslint src/",
44
- "build": "node build.js",
45
- "dev": "tsc --watch",
46
- "dev:cli": "pnpm build && node bin/zhuanspec.js",
47
- "test": "vitest run",
48
- "test:watch": "vitest",
49
- "test:ui": "vitest --ui",
50
- "test:coverage": "vitest --coverage",
51
- "test:postinstall": "node scripts/postinstall.js",
52
- "prepare": "npm run build",
53
- "prepublishOnly": "npm run build",
54
- "postinstall": "node scripts/postinstall.js",
55
- "check:pack-version": "node scripts/pack-version-check.mjs",
56
- "diagnose:cursor": "node scripts/diagnose-cursor-commands.js",
57
- "release": "pnpm run release:ci",
58
- "release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
59
- "release:local": "pnpm exec changeset version && pnpm run check:pack-version && pnpm exec changeset publish",
60
- "changeset": "changeset"
61
- },
62
42
  "engines": {
63
43
  "node": ">=20.19.0"
64
44
  },
@@ -80,5 +60,23 @@
80
60
  "ora": "^8.2.0",
81
61
  "yaml": "^2.8.2",
82
62
  "zod": "^4.0.17"
63
+ },
64
+ "scripts": {
65
+ "lint": "eslint src/",
66
+ "build": "node build.js",
67
+ "dev": "tsc --watch",
68
+ "dev:cli": "pnpm build && node bin/zhuanspec.js",
69
+ "test": "vitest run",
70
+ "test:watch": "vitest",
71
+ "test:ui": "vitest --ui",
72
+ "test:coverage": "vitest --coverage",
73
+ "test:postinstall": "node scripts/postinstall.js",
74
+ "postinstall": "node scripts/postinstall.js",
75
+ "check:pack-version": "node scripts/pack-version-check.mjs",
76
+ "diagnose:cursor": "node scripts/diagnose-cursor-commands.js",
77
+ "release": "pnpm run release:ci",
78
+ "release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
79
+ "release:local": "pnpm exec changeset version && pnpm run check:pack-version && pnpm exec changeset publish",
80
+ "changeset": "changeset"
83
81
  }
84
- }
82
+ }