@zhuan-ai/zhuanspec 2.8.0 → 2.10.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.
@@ -1,17 +1,15 @@
1
1
  /**
2
2
  * PostToolUse Hook - Collect Knowledge
3
3
  *
4
- * This hook runs after Write/Edit operations to:
5
- * 1. Detect pitfall keywords in content or conversation
6
- * 2. Extract and save troubleshooting knowledge
7
- * 3. Record implicit conventions discovered
4
+ * Fires after Write/Edit in Apply phase. Injects a lightweight prompt
5
+ * into Claude's context so the LLM can decide (with user confirmation)
6
+ * whether the change is worth capturing as project knowledge.
7
+ *
8
+ * Design principle: no keyword matching, no auto-save — the LLM decides.
8
9
  */
9
10
  import path from 'path';
10
11
  import { FileSystemUtils } from '../../utils/file-system.js';
11
- import { PITFALL_KEYWORDS, BEST_PRACTICE_KEYWORDS, IMPLICIT_CONVENTION_KEYWORDS } from './deviation-check.js';
12
- import { getBeijingTime, getBeijingTimeForFilename } from './record-progress.js';
13
12
  export async function collectKnowledgeHook(options) {
14
- // Read stdin for Claude Code hook input
15
13
  let stdinData = {};
16
14
  try {
17
15
  const stdinContent = await readStdin();
@@ -22,22 +20,12 @@ export async function collectKnowledgeHook(options) {
22
20
  catch {
23
21
  // Ignore parse errors
24
22
  }
25
- // Get content from stdin or options
26
- const content = stdinData.tool_input?.content
27
- || options.content
28
- || '';
29
23
  const filePath = stdinData.tool_input?.file_path
30
24
  || options.file
31
25
  || '';
32
- const output = await runCollectKnowledge(content, filePath);
33
- if (options.json) {
34
- console.log(JSON.stringify(output));
35
- }
36
- else {
37
- if (output.systemMessage) {
38
- console.log(output.systemMessage);
39
- }
40
- }
26
+ const output = await runCollectKnowledge(filePath);
27
+ // Always output JSON — additionalContext must reach Claude Code
28
+ console.log(JSON.stringify(output));
41
29
  }
42
30
  async function readStdin() {
43
31
  return new Promise((resolve) => {
@@ -48,156 +36,60 @@ async function readStdin() {
48
36
  process.stdin.on('error', () => resolve(null));
49
37
  });
50
38
  }
51
- async function runCollectKnowledge(content, filePath) {
39
+ export async function runCollectKnowledge(filePath) {
52
40
  const phase = process.env.ZHUANSPEC_PHASE || 'idle';
53
41
  const changeId = process.env.ZHUANSPEC_CHANGE_ID || '';
54
- // No active change → nothing to collect
55
- if (!changeId || phase === 'idle') {
56
- return {
57
- continue: true,
58
- };
42
+ // Only apply and archive phases trigger knowledge capture
43
+ // archive is the most important moment — end-of-change reflection
44
+ const isActivePhase = phase === 'apply' || phase === 'archive';
45
+ if (!changeId || !isActivePhase) {
46
+ return { continue: true };
59
47
  }
60
- const cwd = process.cwd();
61
- const zhuanspecDir = path.join(cwd, 'zhuanspec');
62
- const changeDir = path.join(zhuanspecDir, 'changes', changeId);
63
- const knowledgeDir = path.join(changeDir, 'knowledge');
64
- const troubleshootingDir = path.join(knowledgeDir, 'troubleshooting');
65
- const bestPracticesDir = path.join(knowledgeDir, 'best-practices');
66
- const implicitConventionsDir = path.join(knowledgeDir, 'implicit-conventions');
67
- // Check for all keyword types
68
- const contentLower = content.toLowerCase();
69
- const detectedPitfalls = PITFALL_KEYWORDS.filter(kw => contentLower.includes(kw.toLowerCase()));
70
- const detectedBestPractices = BEST_PRACTICE_KEYWORDS.filter(kw => contentLower.includes(kw.toLowerCase()));
71
- const detectedImplicitConventions = IMPLICIT_CONVENTION_KEYWORDS.filter(kw => contentLower.includes(kw.toLowerCase()));
72
- // If no keywords detected, return early
73
- if (detectedPitfalls.length === 0 && detectedBestPractices.length === 0 && detectedImplicitConventions.length === 0) {
74
- return {
75
- continue: true,
76
- };
77
- }
78
- // Create directories as needed
79
- await FileSystemUtils.createDirectory(knowledgeDir);
80
- const collectedTypes = [];
81
- let totalEntries = 0;
82
- // Process pitfalls
83
- if (detectedPitfalls.length > 0) {
84
- await FileSystemUtils.createDirectory(troubleshootingDir);
85
- const pitfall = extractKnowledgeContext(content, filePath, detectedPitfalls, 'troubleshooting');
86
- const timestamp = getBeijingTimeForFilename();
87
- const filename = `pitfall-${timestamp}.md`;
88
- const pitfallPath = path.join(troubleshootingDir, filename);
89
- await FileSystemUtils.writeFile(pitfallPath, formatKnowledgeRecord(pitfall));
90
- updateKnowledgeIndex(knowledgeDir, 'troubleshooting', pitfall.title, filename);
91
- collectedTypes.push('pitfall');
92
- totalEntries++;
93
- }
94
- // Process best practices
95
- if (detectedBestPractices.length > 0) {
96
- await FileSystemUtils.createDirectory(bestPracticesDir);
97
- const bestPractice = extractKnowledgeContext(content, filePath, detectedBestPractices, 'best_practice');
98
- const timestamp = getBeijingTimeForFilename();
99
- const filename = `best-practice-${timestamp}.md`;
100
- const bpPath = path.join(bestPracticesDir, filename);
101
- await FileSystemUtils.writeFile(bpPath, formatKnowledgeRecord(bestPractice));
102
- updateKnowledgeIndex(knowledgeDir, 'best-practices', bestPractice.title, filename);
103
- collectedTypes.push('best practice');
104
- totalEntries++;
105
- }
106
- // Process implicit conventions
107
- if (detectedImplicitConventions.length > 0) {
108
- await FileSystemUtils.createDirectory(implicitConventionsDir);
109
- const implicitConvention = extractKnowledgeContext(content, filePath, detectedImplicitConventions, 'implicit_contract');
110
- const timestamp = getBeijingTimeForFilename();
111
- const filename = `implicit-convention-${timestamp}.md`;
112
- const icPath = path.join(implicitConventionsDir, filename);
113
- await FileSystemUtils.writeFile(icPath, formatKnowledgeRecord(implicitConvention));
114
- updateKnowledgeIndex(knowledgeDir, 'implicit-conventions', implicitConvention.title, filename);
115
- collectedTypes.push('implicit convention');
116
- totalEntries++;
48
+ // Only skip writes to knowledge/ itself to prevent capture loops
49
+ // Archive phase writes to zhuanspec/changes/archive/... should still trigger
50
+ if (filePath.includes('zhuanspec/knowledge/')) {
51
+ return { continue: true };
117
52
  }
53
+ const cwd = process.cwd();
54
+ const knowledgeDir = path.join(cwd, 'zhuanspec', 'knowledge');
55
+ const hasKnowledge = await FileSystemUtils.directoryExists(knowledgeDir);
56
+ const knowledgeRef = hasKnowledge
57
+ ? '参考 `zhuanspec/knowledge/` 下已有文件的格式'
58
+ : '保存到 `zhuanspec/knowledge/troubleshooting/`、`best-practices/` 或 `implicit-conventions/` 对应目录';
59
+ // Phase-specific capture focus to avoid duplicate entries across apply and archive
60
+ const phaseGuide = phase === 'archive'
61
+ ? [
62
+ '**当前处于归档阶段** — 重点捕获整个变更过程的回顾性总结:',
63
+ '- 贯穿整个实施过程的模式或规律',
64
+ '- 事后才意识到的设计决策或取舍',
65
+ '- apply 阶段**未曾记录**的新洞察',
66
+ '',
67
+ '⚠️ **去重要求**:保存前先读取 `zhuanspec/knowledge/` 下已有文件,',
68
+ '若本次内容与已有记录高度重叠,跳过或仅追加补充内容,不要重复新建文件。',
69
+ ]
70
+ : [
71
+ '**当前处于实施阶段** — 重点捕获即时发现的问题:',
72
+ '- 修复了一个非显而易见的 bug 或绕过了一个坑',
73
+ '- 发现了一个隐式约定或框架默认行为',
74
+ '- 总结了一个值得复用的最佳实践',
75
+ '',
76
+ '⚠️ **去重要求**:保存前先读取 `zhuanspec/knowledge/` 下已有文件,',
77
+ '若已有同类记录,跳过或追加到已有文件,不要重复新建。',
78
+ ];
79
+ const additionalContext = [
80
+ `💡 **知识沉淀提示**(针对刚才对 \`${filePath || '该文件'}\` 的改动)`,
81
+ '',
82
+ ...phaseGuide,
83
+ '',
84
+ '询问方式:',
85
+ '> "这次改动是否值得记录为项目知识?(回复是/否)"',
86
+ '',
87
+ `用户确认后,用 Write 工具写入知识文件,${knowledgeRef}。`,
88
+ '如果本次只是常规代码修改,忽略此提示即可。',
89
+ ].join('\n');
118
90
  return {
119
91
  continue: true,
120
- systemMessage: `📝 Knowledge detected and recorded: ${collectedTypes.join(', ')}`,
121
- hookSpecificOutput: {
122
- knowledgeCollected: `${totalEntries} entries`,
123
- pitfallDetected: detectedPitfalls.length > 0,
124
- },
92
+ hookSpecificOutput: { additionalContext },
125
93
  };
126
94
  }
127
- async function updateKnowledgeIndex(knowledgeDir, section, title, filename) {
128
- const indexPath = path.join(knowledgeDir, 'index.md');
129
- const subdir = section === 'troubleshooting' ? 'troubleshooting' :
130
- section === 'best-practices' ? 'best-practices' :
131
- 'implicit-conventions';
132
- const indexEntry = `\n- [${title}](${subdir}/${filename}) - ${getBeijingTime()}\n`;
133
- if (await FileSystemUtils.fileExists(indexPath)) {
134
- const existingIndex = await FileSystemUtils.readFile(indexPath);
135
- // Find section or create it
136
- const sectionHeader = `## ${section}`;
137
- if (existingIndex.includes(sectionHeader)) {
138
- // Append to existing section
139
- const updated = existingIndex.replace(sectionHeader, `${sectionHeader}${indexEntry}`);
140
- await FileSystemUtils.writeFile(indexPath, updated);
141
- }
142
- else {
143
- // Add new section
144
- await FileSystemUtils.writeFile(indexPath, `${existingIndex}\n\n${sectionHeader}${indexEntry}`);
145
- }
146
- }
147
- else {
148
- await FileSystemUtils.writeFile(indexPath, `# Knowledge Index\n\n## troubleshooting\n\n## best-practices\n\n## implicit-conventions\n${indexEntry}`);
149
- }
150
- }
151
- function extractKnowledgeContext(content, filePath, keywords, type) {
152
- // Extract relevant context around keywords
153
- const lines = content.split('\n');
154
- const relevantLines = [];
155
- for (let i = 0; i < lines.length; i++) {
156
- const lineLower = lines[i].toLowerCase();
157
- if (keywords.some(kw => lineLower.includes(kw.toLowerCase()))) {
158
- // Include surrounding context (3 lines before and after)
159
- const start = Math.max(0, i - 3);
160
- const end = Math.min(lines.length, i + 4);
161
- for (let j = start; j < end; j++) {
162
- relevantLines.push(lines[j]);
163
- }
164
- relevantLines.push('---');
165
- }
166
- }
167
- // Generate title from first keyword found
168
- const firstKeyword = keywords[0];
169
- const typeLabel = type === 'troubleshooting' ? 'Issue' :
170
- type === 'best_practice' ? 'Best Practice' :
171
- 'Convention';
172
- const title = `${typeLabel}: ${firstKeyword} in ${filePath}`;
173
- return {
174
- type,
175
- title,
176
- content: relevantLines.join('\n'),
177
- source: filePath,
178
- timestamp: getBeijingTime(),
179
- tags: keywords,
180
- };
181
- }
182
- function formatKnowledgeRecord(entry) {
183
- return `# ${entry.title}
184
-
185
- **Type**: ${entry.type}
186
- **Source**: ${entry.source}
187
- **Timestamp**: ${entry.timestamp}
188
- **Tags**: ${entry.tags.join(', ')}
189
-
190
- ## Context
191
-
192
- ${entry.content}
193
-
194
- ## Resolution
195
-
196
- [To be filled when resolved]
197
-
198
- ## Lessons Learned
199
-
200
- [To be extracted after resolution]
201
- `;
202
- }
203
95
  //# sourceMappingURL=collect-knowledge.js.map
@@ -35,11 +35,33 @@ const IMPLICIT_CONVENTION_KEYWORDS = [
35
35
  '默认行为', '默认配置', '默认值', '隐式规则',
36
36
  'implicit convention', 'default behavior', 'convention',
37
37
  ];
38
- // User correction keywords for deviation detection trigger
38
+ // User correction/pitfall keywords — bilingual, flexible matching
39
39
  const USER_CORRECTION_KEYWORDS = [
40
- '不对', '错了', '这里应该是', '应该是', '不是这样',
41
- '这里不对', '改一下', '需要修改', '修改提案',
42
- 'wrong', 'should be', 'not right', 'need to fix',
40
+ // 中文纠错
41
+ '不对', '错了', '不是', '有问题', '有个问题', '这里有问题',
42
+ '这样不行', '不行', '有bug', '有Bug', '踩坑', '坑了',
43
+ '应该是', '应该改', '需要修改', '修改一下', '改一下',
44
+ '不应该', '这里错了', '你理解错了', '理解错误', '理解有误',
45
+ '不是这个意思', '不是这样的', '重做', '重新来', '逻辑不对',
46
+ '实现不对', '方向错了', '写错了', '搞错了',
47
+ // English corrections
48
+ 'wrong', 'incorrect', "that's wrong", "you're wrong", 'not right',
49
+ 'should be', 'should have been', "that's not", 'no no', 'wait no',
50
+ 'fix this', 'this is wrong', 'this is incorrect', 'mistake', 'mistaken',
51
+ 'broken', 'not working', "doesn't work", "doesn't seem right",
52
+ 'pitfall', 'gotcha', 'issue here', 'problem here',
53
+ 'actually no', 'actually wrong', 'you misunderstood', 'misunderstood',
54
+ 'not what i meant', 'not what i wanted', 'redo this', 'redo that',
55
+ ];
56
+ // Edit intent keywords — bilingual, for Apply phase scope check
57
+ const EDIT_INTENT_KEYWORDS = [
58
+ // 中文
59
+ '修改', '改', '新增', '添加', '增加', '删除', '移除', '重构',
60
+ '优化', '更新', '替换', '重写', '实现', '完成', '做一下',
61
+ // English
62
+ 'add', 'change', 'edit', 'update', 'refactor', 'remove', 'delete',
63
+ 'replace', 'rewrite', 'implement', 'fix', 'create', 'write',
64
+ 'modify', 'revise', 'adjust',
43
65
  ];
44
66
  export async function deviationCheckHook(options) {
45
67
  const startTime = Date.now();
@@ -65,14 +87,16 @@ export async function deviationCheckHook(options) {
65
87
  const trigger = options.trigger || 'pre-tool';
66
88
  const output = await runDeviationCheck(filePath, trigger, promptText, options);
67
89
  const durationMs = Date.now() - startTime;
68
- // Record hook trigger
69
- await recordHookTrigger('deviation-check', 'PreToolUse', output.continue, {
90
+ const hookEvent = trigger === 'post-prompt' ? 'UserPromptSubmit' : 'PreToolUse';
91
+ await recordHookTrigger('deviation-check', hookEvent, output.continue, {
70
92
  toolName,
71
93
  filePath,
72
94
  message: output.systemMessage,
73
95
  durationMs,
74
96
  });
75
- if (options.json) {
97
+ // post-prompt always outputs JSON so Claude Code can read additionalContext
98
+ const useJson = options.json || trigger === 'post-prompt';
99
+ if (useJson) {
76
100
  console.log(JSON.stringify(output));
77
101
  }
78
102
  else {
@@ -98,12 +122,15 @@ async function runDeviationCheck(filePath, trigger, promptText, _options) {
98
122
  const phase = process.env.ZHUANSPEC_PHASE || 'idle';
99
123
  const changeId = process.env.ZHUANSPEC_CHANGE_ID || '';
100
124
  const currentTask = process.env.ZHUANSPEC_CURRENT_TASK || '';
101
- // No change bound or idle phase → allow normal workflow
125
+ // No change bound or idle phase → allow, post-prompt silences itself to avoid noise
102
126
  if (!changeId || phase === 'idle') {
103
- return {
104
- continue: true,
105
- systemMessage: '✓ No active ZhuanSpec change, normal workflow allowed',
106
- };
127
+ if (trigger === 'post-prompt') {
128
+ return { continue: true };
129
+ }
130
+ const msg = !changeId
131
+ ? '✓ 当前无绑定变更,正常工作流'
132
+ : `✓ 变更 [${changeId}] 处于 idle 阶段,正常工作流`;
133
+ return { continue: true, systemMessage: msg };
107
134
  }
108
135
  const cwd = process.cwd();
109
136
  const zhuanspecDir = path.join(cwd, 'zhuanspec');
@@ -136,46 +163,51 @@ async function runDeviationCheck(filePath, trigger, promptText, _options) {
136
163
  systemMessage: '✓ Resume deviation check passed',
137
164
  };
138
165
  }
139
- // Post-prompt check: catch "additional request after apply/interruption" and force reverse-sync first.
166
+ // Post-prompt check: inject proposal context for 2-dimension consistency check in Apply phase.
140
167
  if (phase === 'apply' && trigger === 'post-prompt') {
141
- // Check for user correction keywords
142
- const correctionKeywordFound = USER_CORRECTION_KEYWORDS.some(keyword => promptText.toLowerCase().includes(keyword.toLowerCase()));
143
- if (correctionKeywordFound) {
144
- // User correction detected - trigger deviation handling
145
- return {
146
- continue: false,
147
- stopReason: 'User correction detected in Apply phase',
148
- systemMessage: `⚠️ DEVIATION TRIGGER: 检测到用户纠正关键词,当前阶段为 ${phase}
149
- 请按偏离修正流程处理:
150
- 1. 先修改 spec 文件
151
- 2. 用户确认后再修改代码
152
- 3. 用户确认无误后触发知识沉淀`,
153
- hookSpecificOutput: {
154
- additionalContext: `User input: ${promptText.slice(0, 200)}`,
155
- options: [
156
- '修改 Spec 文件',
157
- '直接修改代码(跳过 Spec)',
158
- '取消修改',
159
- ],
160
- },
161
- };
168
+ // Skip if no proposal exists
169
+ if (!await FileSystemUtils.fileExists(proposalPath)) {
170
+ return { continue: true };
162
171
  }
163
- const extraRequestPattern = /(另外|额外|顺便|再加|同时|also|additionally|by the way|one more)/i;
164
- const editIntentPattern = /(修改|改|新增|删除|重构|optimi[sz]e|add|change|edit|update|refactor|remove)/i;
165
- if (extraRequestPattern.test(promptText) && editIntentPattern.test(promptText)) {
166
- return {
167
- continue: false,
168
- stopReason: 'Post-prompt additional requirement detected',
169
- systemMessage: '⚠️ 检测到 Apply 阶段追加需求,请先更新 proposal/tasks(Reverse Sync)后再实施。',
170
- hookSpecificOutput: {
171
- additionalContext: `Prompt: ${promptText.slice(0, 300)}`,
172
- options: ['更新提案后继续实施', '标记为Bug修复豁免', '取消追加需求'],
173
- },
174
- };
172
+ const lowerPrompt = promptText.toLowerCase();
173
+ // Detect correction/pitfall keywords
174
+ const correctionFound = USER_CORRECTION_KEYWORDS.some(kw => lowerPrompt.includes(kw.toLowerCase()));
175
+ // Detect edit intent
176
+ const hasEditIntent = EDIT_INTENT_KEYWORDS.some(kw => lowerPrompt.includes(kw.toLowerCase()));
177
+ // No action needed if purely read-only and no correction signal
178
+ if (!hasEditIntent && !correctionFound) {
179
+ return { continue: true };
180
+ }
181
+ // Load proposal for context injection
182
+ const proposalContent = await FileSystemUtils.readFile(proposalPath);
183
+ const proposalSummary = extractProposalSummary(proposalContent, 1500);
184
+ const reverseSyncFiles = [
185
+ '`proposal.md`(更新变更原因/影响范围)',
186
+ '`tasks.md`(新增或调整对应任务)',
187
+ '`specs/*/spec.md`(更新受影响的 spec 需求条目)',
188
+ '`design.md`(如存在,更新技术设计决策)',
189
+ ].join('、');
190
+ let additionalContext = '## ⚠️ ZhuanSpec Apply 阶段一致性检测\n\n';
191
+ additionalContext += `**当前变更提案摘要:**\n${proposalSummary}\n\n`;
192
+ additionalContext += `**处理以下用户请求前,请先做两维度一致性检测:**\n\n`;
193
+ additionalContext += `- **维度1(范围)**:用户请求涉及的文件/功能是否在提案中明确提及?\n`;
194
+ additionalContext += ` - 在提案范围内 → 正常实施\n`;
195
+ additionalContext += ` - **超出提案范围** → 先告知用户,执行 Reverse Sync:依次更新 ${reverseSyncFiles},经用户确认后再实施代码\n\n`;
196
+ additionalContext += `- **维度2(逻辑一致性)**:用户请求的逻辑调整是否与提案设计意图一致?\n`;
197
+ additionalContext += ` - 一致 → 正常实施\n`;
198
+ additionalContext += ` - **逻辑有偏差** → 说明偏差点,同样执行 Reverse Sync 更新所有相关提案文件(${reverseSyncFiles}),经用户确认后再实施\n\n`;
199
+ if (correctionFound) {
200
+ additionalContext += `**⚠️ 检测到纠错关键词** — 用户可能遇到了问题/踩坑\n`;
201
+ additionalContext += `修复完成后,请主动询问用户:\n`;
202
+ additionalContext += `> "这个问题是否值得记录为踩坑经验,防止下次再犯?(回复是/否)"\n`;
203
+ additionalContext += `若用户确认,将问题描述和解决方案整理后保存到 \`zhuanspec/knowledge/troubleshooting/\` 目录下的新 .md 文件中。\n\n`;
175
204
  }
176
205
  return {
177
206
  continue: true,
178
- systemMessage: '✓ Post-prompt deviation check passed',
207
+ systemMessage: `✓ ZhuanSpec: Apply 阶段一致性检测已注入上下文${correctionFound ? '(含踩坑沉淀提示)' : ''}`,
208
+ hookSpecificOutput: {
209
+ additionalContext,
210
+ },
179
211
  };
180
212
  }
181
213
  // Propose phase → block all code modifications (Iron Law 1: No Spec No Code)
@@ -287,6 +319,10 @@ ZhuanSpec Iron Law 3: Reverse Sync - When deviation is found, update proposal fi
287
319
  },
288
320
  };
289
321
  }
322
+ // post-prompt in non-apply phases (propose/techDesign/review/...) → silently allow
323
+ if (trigger === 'post-prompt') {
324
+ return { continue: true };
325
+ }
290
326
  // Default: allow
291
327
  return {
292
328
  continue: true,
@@ -295,25 +331,36 @@ ZhuanSpec Iron Law 3: Reverse Sync - When deviation is found, update proposal fi
295
331
  async function parseEstimatedChanges(proposalPath) {
296
332
  try {
297
333
  const content = await FileSystemUtils.readFile(proposalPath);
334
+ const seen = new Set();
298
335
  const files = [];
299
- // Look for "影响范围" or "Affected code" sections
336
+ const addFile = (raw) => {
337
+ const cleaned = raw.replace(/[`'"*[\]()]/g, '').trim();
338
+ if (cleaned.length > 2 && !seen.has(cleaned)) {
339
+ seen.add(cleaned);
340
+ files.push(cleaned);
341
+ }
342
+ };
343
+ // Scan entire proposal for file paths with known extensions
344
+ const filePattern = /(?:^|[\s`'"([\]|])([a-zA-Z0-9_\-./]+\.(?:ts|tsx|js|jsx|java|py|go|rs|md|json|yaml|yml|sql|kt|scala|c|cpp|cs|sh|xml|html|css|scss|vue|svelte))(?:[\s`'")\]|,]|$)/gm;
345
+ let m;
346
+ while ((m = filePattern.exec(content)) !== null) {
347
+ addFile(m[1]);
348
+ }
349
+ // Also collect bullet-point entries under 影响范围/Affected/Impact sections
300
350
  const lines = content.split('\n');
301
351
  let inAffectedSection = false;
302
352
  for (const line of lines) {
303
- if (line.includes('影响范围') || line.includes('Affected code') || line.includes('Affected files')) {
353
+ if (/影响范围|Affected code|Affected files|Impact/i.test(line) && line.startsWith('#')) {
304
354
  inAffectedSection = true;
305
355
  continue;
306
356
  }
307
- if (inAffectedSection && line.startsWith('##')) {
308
- // End of section
357
+ if (inAffectedSection && line.startsWith('#')) {
309
358
  inAffectedSection = false;
310
359
  }
311
360
  if (inAffectedSection && line.trim().startsWith('-')) {
312
- // Extract file path from bullet point
313
361
  const match = line.match(/-\s*(.+)/);
314
- if (match) {
315
- files.push(match[1].trim());
316
- }
362
+ if (match)
363
+ addFile(match[1].trim());
317
364
  }
318
365
  }
319
366
  return files;
@@ -322,6 +369,21 @@ async function parseEstimatedChanges(proposalPath) {
322
369
  return [];
323
370
  }
324
371
  }
372
+ function extractProposalSummary(content, maxLength) {
373
+ const patterns = [
374
+ /^## (?:Summary|摘要|Overview|概述)[\s\S]*?(?=\n## |\n# |$)/im,
375
+ /^## (?:What Changes|变更内容|Changes|修改内容)[\s\S]*?(?=\n## |\n# |$)/im,
376
+ /^## (?:Impact|影响范围|Affected)[\s\S]*?(?=\n## |\n# |$)/im,
377
+ ];
378
+ const sections = [];
379
+ for (const pattern of patterns) {
380
+ const match = content.match(pattern);
381
+ if (match)
382
+ sections.push(match[0].trim());
383
+ }
384
+ const combined = sections.length > 0 ? sections.join('\n\n') : content;
385
+ return combined.length > maxLength ? combined.substring(0, maxLength) + '...' : combined;
386
+ }
325
387
  async function parseCurrentTaskFiles(tasksPath, currentTask) {
326
388
  if (!currentTask) {
327
389
  return [];
@@ -8,9 +8,34 @@
8
8
  */
9
9
  import path from 'path';
10
10
  import fs from 'fs';
11
+ import { exec } from 'child_process';
12
+ import { promisify } from 'util';
13
+ import { fileURLToPath } from 'url';
11
14
  import { FileSystemUtils } from '../../utils/file-system.js';
12
15
  import { initializeProgress, recordHookTrigger, getBeijingTime } from './record-progress.js';
13
16
  import { getCurrentPhase, getPhaseFromProgress } from '../../utils/phase-utils.js';
17
+ const execAsync = promisify(exec);
18
+ async function checkVersionUpdate() {
19
+ try {
20
+ const pkgPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'package.json');
21
+ const pkgContent = JSON.parse(await fs.promises.readFile(pkgPath, 'utf-8'));
22
+ const current = pkgContent.version;
23
+ const { stdout } = await execAsync('npm view @zhuan-ai/zhuanspec version', { timeout: 5000 });
24
+ const latest = stdout.trim();
25
+ if (!latest || !current)
26
+ return null;
27
+ const parse = (v) => v.replace(/^v/, '').split('-')[0].split('.').map(Number);
28
+ const [cM, cm, cp] = parse(current);
29
+ const [lM, lm, lp] = parse(latest);
30
+ const isOutdated = lM !== cM ? lM > cM : lm !== cm ? lm > cm : lp > cp;
31
+ if (!isOutdated)
32
+ return `✓ ZhuanSpec v${current} 已是最新版本,开始 happy coding 吧~`;
33
+ return `⚠️ ZhuanSpec 版本过旧!\n 当前版本: v${current} → 最新版本: v${latest}\n 请执行: 1) npm i -g @zhuan-ai/zhuanspec 2) zhuanspec init(同步最新模板到本项目)`;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
14
39
  export async function initHook(options) {
15
40
  const startTime = Date.now();
16
41
  const output = await runInitHook(options);
@@ -226,9 +251,13 @@ async function runInitHook(_options) {
226
251
  additionalContext += `Found ${rulesFiles.length} rule files in .claude/rules: ${rulesFiles.join(', ')}\n`;
227
252
  additionalContext += `(These are loaded automatically by Claude Code system)\n\n`;
228
253
  }
254
+ const versionBanner = await checkVersionUpdate();
255
+ const finalMessage = versionBanner
256
+ ? versionBanner + '\n' + (systemMessage || '✓ ZhuanSpec session initialized')
257
+ : (systemMessage || '✓ ZhuanSpec session initialized');
229
258
  return {
230
259
  continue: true,
231
- systemMessage: systemMessage || '✓ ZhuanSpec session initialized',
260
+ systemMessage: finalMessage,
232
261
  hookSpecificOutput: {
233
262
  additionalContext: additionalContext || undefined,
234
263
  env: {
@@ -17,29 +17,28 @@ async function runPreArchiveHook(changeId) {
17
17
  return { continue: false, stopReason: 'missing-change', systemMessage: 'Missing --change for pre-archive check' };
18
18
  }
19
19
  const changeDir = path.join(process.cwd(), 'zhuanspec', 'changes', changeId);
20
- const reviewPath = path.join(changeDir, 'metrics', 'review.json');
21
- const testsPath = path.join(changeDir, 'metrics', 'tests.json');
22
- const reviewReportPath = path.join(changeDir, 'review-report.md');
23
- if (!(await FileSystemUtils.fileExists(reviewPath)) ||
24
- !(await FileSystemUtils.fileExists(testsPath)) ||
20
+ const reviewDir = path.join(changeDir, 'review');
21
+ const codeReviewPath = path.join(reviewDir, 'code-review-result.json');
22
+ const unitTestPath = path.join(reviewDir, 'unit-test-result.json');
23
+ const reviewReportPath = path.join(reviewDir, 'review-report.md');
24
+ if (!(await FileSystemUtils.fileExists(codeReviewPath)) ||
25
+ !(await FileSystemUtils.fileExists(unitTestPath)) ||
25
26
  !(await FileSystemUtils.fileExists(reviewReportPath))) {
26
27
  return {
27
28
  continue: false,
28
29
  stopReason: 'review-or-tests-missing',
29
- systemMessage: 'Pre-archive check failed: missing review.json/tests.json/review-report.md',
30
+ systemMessage: 'Pre-archive check failed: missing changes/<id>/review/code-review-result.json, unit-test-result.json, or review-report.md',
30
31
  };
31
32
  }
32
- const review = JSON.parse(await FileSystemUtils.readFile(reviewPath));
33
- const tests = JSON.parse(await FileSystemUtils.readFile(testsPath));
34
- const reviewPassed = Boolean(review.passed);
35
- const testPassed = Number(tests.failed ?? 1) === 0;
36
- const specConsistencyPassed = review?.checks?.specCompliance === true;
37
- const critical = Number(review?.summary?.critical ?? 0);
38
- if (!reviewPassed || !testPassed || critical > 0 || !specConsistencyPassed) {
33
+ const codeReview = JSON.parse(await FileSystemUtils.readFile(codeReviewPath));
34
+ const unitTest = JSON.parse(await FileSystemUtils.readFile(unitTestPath));
35
+ const critical = Number(codeReview?.criticalCount ?? 0);
36
+ const testPassed = Boolean(unitTest?.testPassed);
37
+ if (critical > 0 || !testPassed) {
39
38
  return {
40
39
  continue: false,
41
40
  stopReason: 'quality-gate-failed',
42
- systemMessage: `Pre-archive check failed: review=${reviewPassed}, tests=${testPassed}, specConsistency=${specConsistencyPassed}, critical=${critical}`,
41
+ systemMessage: `Pre-archive check failed: critical=${critical}, testPassed=${testPassed}`,
43
42
  };
44
43
  }
45
44
  return {