@zhuan-ai/zhuanspec 2.9.5 → 2.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/cli/hooks.js +8 -8
  2. package/dist/cli/index.js +1 -0
  3. package/dist/commands/design.d.ts +12 -2
  4. package/dist/commands/design.js +83 -10
  5. package/dist/commands/progress.js +11 -0
  6. package/dist/commands/review.d.ts +1 -25
  7. package/dist/commands/review.js +51 -408
  8. package/dist/core/completions/command-registry.js +4 -0
  9. package/dist/core/hooks/collect-knowledge.d.ts +13 -5
  10. package/dist/core/hooks/collect-knowledge.js +56 -164
  11. package/dist/core/hooks/deviation-check.js +159 -55
  12. package/dist/core/hooks/init.js +46 -0
  13. package/dist/core/hooks/post-apply.js +2 -0
  14. package/dist/core/hooks/pre-archive.js +13 -14
  15. package/dist/core/hooks/pre-review.d.ts +27 -0
  16. package/dist/core/hooks/pre-review.js +169 -0
  17. package/dist/core/hooks/record-progress.d.ts +25 -0
  18. package/dist/core/hooks/record-progress.js +88 -1
  19. package/dist/core/hooks/review-hooks.js +176 -7
  20. package/dist/core/hooks/review-orchestrator.js +58 -11
  21. package/dist/core/hooks/tdd-phase-hook.d.ts +59 -0
  22. package/dist/core/hooks/tdd-phase-hook.js +313 -0
  23. package/dist/core/init.d.ts +1 -0
  24. package/dist/core/init.js +45 -2
  25. package/dist/core/templates/agents-template.d.ts +1 -1
  26. package/dist/core/templates/agents-template.js +139 -23
  27. package/dist/core/templates/skill-templates.js +21 -1
  28. package/dist/core/templates/slash-command-templates.js +163 -63
  29. package/dist/core/templates/tasks-template.js +89 -0
  30. package/dist/core/update.d.ts +3 -0
  31. package/dist/core/update.js +85 -2
  32. package/dist/core/validation/strict-rules.d.ts +47 -0
  33. package/dist/core/validation/strict-rules.js +589 -42
  34. package/dist/utils/git-repo-detector.js +1 -1
  35. package/dist/utils/phase-utils.js +14 -1
  36. package/package.json +22 -20
@@ -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
@@ -12,6 +12,7 @@
12
12
  * - Out of scope → block with deviation handling options
13
13
  */
14
14
  import path from 'path';
15
+ import fs from 'fs';
15
16
  import { FileSystemUtils } from '../../utils/file-system.js';
16
17
  import { GitRepoDetector } from '../../utils/git-repo-detector.js';
17
18
  import { checkPreApplyConditions } from './pre-apply.js';
@@ -35,11 +36,33 @@ const IMPLICIT_CONVENTION_KEYWORDS = [
35
36
  '默认行为', '默认配置', '默认值', '隐式规则',
36
37
  'implicit convention', 'default behavior', 'convention',
37
38
  ];
38
- // User correction keywords for deviation detection trigger
39
+ // User correction/pitfall keywords — bilingual, flexible matching
39
40
  const USER_CORRECTION_KEYWORDS = [
40
- '不对', '错了', '这里应该是', '应该是', '不是这样',
41
- '这里不对', '改一下', '需要修改', '修改提案',
42
- 'wrong', 'should be', 'not right', 'need to fix',
41
+ // 中文纠错
42
+ '不对', '错了', '不是', '有问题', '有个问题', '这里有问题',
43
+ '这样不行', '不行', '有bug', '有Bug', '踩坑', '坑了',
44
+ '应该是', '应该改', '需要修改', '修改一下', '改一下',
45
+ '不应该', '这里错了', '你理解错了', '理解错误', '理解有误',
46
+ '不是这个意思', '不是这样的', '重做', '重新来', '逻辑不对',
47
+ '实现不对', '方向错了', '写错了', '搞错了',
48
+ // English corrections
49
+ 'wrong', 'incorrect', "that's wrong", "you're wrong", 'not right',
50
+ 'should be', 'should have been', "that's not", 'no no', 'wait no',
51
+ 'fix this', 'this is wrong', 'this is incorrect', 'mistake', 'mistaken',
52
+ 'broken', 'not working', "doesn't work", "doesn't seem right",
53
+ 'pitfall', 'gotcha', 'issue here', 'problem here',
54
+ 'actually no', 'actually wrong', 'you misunderstood', 'misunderstood',
55
+ 'not what i meant', 'not what i wanted', 'redo this', 'redo that',
56
+ ];
57
+ // Edit intent keywords — bilingual, for Apply phase scope check
58
+ const EDIT_INTENT_KEYWORDS = [
59
+ // 中文
60
+ '修改', '改', '新增', '添加', '增加', '删除', '移除', '重构',
61
+ '优化', '更新', '替换', '重写', '实现', '完成', '做一下',
62
+ // English
63
+ 'add', 'change', 'edit', 'update', 'refactor', 'remove', 'delete',
64
+ 'replace', 'rewrite', 'implement', 'fix', 'create', 'write',
65
+ 'modify', 'revise', 'adjust',
43
66
  ];
44
67
  export async function deviationCheckHook(options) {
45
68
  const startTime = Date.now();
@@ -65,14 +88,16 @@ export async function deviationCheckHook(options) {
65
88
  const trigger = options.trigger || 'pre-tool';
66
89
  const output = await runDeviationCheck(filePath, trigger, promptText, options);
67
90
  const durationMs = Date.now() - startTime;
68
- // Record hook trigger
69
- await recordHookTrigger('deviation-check', 'PreToolUse', output.continue, {
91
+ const hookEvent = trigger === 'post-prompt' ? 'UserPromptSubmit' : 'PreToolUse';
92
+ await recordHookTrigger('deviation-check', hookEvent, output.continue, {
70
93
  toolName,
71
94
  filePath,
72
95
  message: output.systemMessage,
73
96
  durationMs,
74
97
  });
75
- if (options.json) {
98
+ // post-prompt always outputs JSON so Claude Code can read additionalContext
99
+ const useJson = options.json || trigger === 'post-prompt';
100
+ if (useJson) {
76
101
  console.log(JSON.stringify(output));
77
102
  }
78
103
  else {
@@ -98,12 +123,15 @@ async function runDeviationCheck(filePath, trigger, promptText, _options) {
98
123
  const phase = process.env.ZHUANSPEC_PHASE || 'idle';
99
124
  const changeId = process.env.ZHUANSPEC_CHANGE_ID || '';
100
125
  const currentTask = process.env.ZHUANSPEC_CURRENT_TASK || '';
101
- // No change bound or idle phase → allow normal workflow
126
+ // No change bound or idle phase → allow, post-prompt silences itself to avoid noise
102
127
  if (!changeId || phase === 'idle') {
103
- return {
104
- continue: true,
105
- systemMessage: '✓ No active ZhuanSpec change, normal workflow allowed',
106
- };
128
+ if (trigger === 'post-prompt') {
129
+ return { continue: true };
130
+ }
131
+ const msg = !changeId
132
+ ? '✓ 当前无绑定变更,正常工作流'
133
+ : `✓ 变更 [${changeId}] 处于 idle 阶段,正常工作流`;
134
+ return { continue: true, systemMessage: msg };
107
135
  }
108
136
  const cwd = process.cwd();
109
137
  const zhuanspecDir = path.join(cwd, 'zhuanspec');
@@ -121,6 +149,13 @@ async function runDeviationCheck(filePath, trigger, promptText, _options) {
121
149
  return !estimatedFiles.map(normalizePath).some((scope) => normalized.includes(scope) || scope.includes(normalized));
122
150
  });
123
151
  if (outOfScope.length > 0) {
152
+ const resumeDeviationId = `dev-resume-${Date.now()}`;
153
+ // Write deviation ID to temp file for record-progress to read (cross-process)
154
+ const metricsDir = path.join(changeDir, 'metrics');
155
+ if (!fs.existsSync(metricsDir)) {
156
+ fs.mkdirSync(metricsDir, { recursive: true });
157
+ }
158
+ fs.writeFileSync(path.join(metricsDir, '.last-deviation-id'), resumeDeviationId, 'utf-8');
124
159
  return {
125
160
  continue: false,
126
161
  stopReason: 'Resume deviation detected',
@@ -136,46 +171,51 @@ async function runDeviationCheck(filePath, trigger, promptText, _options) {
136
171
  systemMessage: '✓ Resume deviation check passed',
137
172
  };
138
173
  }
139
- // Post-prompt check: catch "additional request after apply/interruption" and force reverse-sync first.
174
+ // Post-prompt check: inject proposal context for 2-dimension consistency check in Apply phase.
140
175
  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
- };
176
+ // Skip if no proposal exists
177
+ if (!await FileSystemUtils.fileExists(proposalPath)) {
178
+ return { continue: true };
162
179
  }
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
- };
180
+ const lowerPrompt = promptText.toLowerCase();
181
+ // Detect correction/pitfall keywords
182
+ const correctionFound = USER_CORRECTION_KEYWORDS.some(kw => lowerPrompt.includes(kw.toLowerCase()));
183
+ // Detect edit intent
184
+ const hasEditIntent = EDIT_INTENT_KEYWORDS.some(kw => lowerPrompt.includes(kw.toLowerCase()));
185
+ // No action needed if purely read-only and no correction signal
186
+ if (!hasEditIntent && !correctionFound) {
187
+ return { continue: true };
188
+ }
189
+ // Load proposal for context injection
190
+ const proposalContent = await FileSystemUtils.readFile(proposalPath);
191
+ const proposalSummary = extractProposalSummary(proposalContent, 1500);
192
+ const reverseSyncFiles = [
193
+ '`proposal.md`(更新变更原因/影响范围)',
194
+ '`tasks.md`(新增或调整对应任务)',
195
+ '`specs/*/spec.md`(更新受影响的 spec 需求条目)',
196
+ '`design.md`(如存在,更新技术设计决策)',
197
+ ].join('、');
198
+ let additionalContext = '## ⚠️ ZhuanSpec Apply 阶段一致性检测\n\n';
199
+ additionalContext += `**当前变更提案摘要:**\n${proposalSummary}\n\n`;
200
+ additionalContext += `**处理以下用户请求前,请先做两维度一致性检测:**\n\n`;
201
+ additionalContext += `- **维度1(范围)**:用户请求涉及的文件/功能是否在提案中明确提及?\n`;
202
+ additionalContext += ` - 在提案范围内 → 正常实施\n`;
203
+ additionalContext += ` - **超出提案范围** → 先告知用户,执行 Reverse Sync:依次更新 ${reverseSyncFiles},经用户确认后再实施代码\n\n`;
204
+ additionalContext += `- **维度2(逻辑一致性)**:用户请求的逻辑调整是否与提案设计意图一致?\n`;
205
+ additionalContext += ` - 一致 → 正常实施\n`;
206
+ additionalContext += ` - **逻辑有偏差** → 说明偏差点,同样执行 Reverse Sync 更新所有相关提案文件(${reverseSyncFiles}),经用户确认后再实施\n\n`;
207
+ if (correctionFound) {
208
+ additionalContext += `**⚠️ 检测到纠错关键词** — 用户可能遇到了问题/踩坑\n`;
209
+ additionalContext += `修复完成后,请主动询问用户:\n`;
210
+ additionalContext += `> "这个问题是否值得记录为踩坑经验,防止下次再犯?(回复是/否)"\n`;
211
+ additionalContext += `若用户确认,将问题描述和解决方案整理后保存到 \`zhuanspec/knowledge/troubleshooting/\` 目录下的新 .md 文件中。\n\n`;
175
212
  }
176
213
  return {
177
214
  continue: true,
178
- systemMessage: '✓ Post-prompt deviation check passed',
215
+ systemMessage: `✓ ZhuanSpec: Apply 阶段一致性检测已注入上下文${correctionFound ? '(含踩坑沉淀提示)' : ''}`,
216
+ hookSpecificOutput: {
217
+ additionalContext,
218
+ },
179
219
  };
180
220
  }
181
221
  // Propose phase → block all code modifications (Iron Law 1: No Spec No Code)
@@ -236,6 +276,34 @@ async function runDeviationCheck(filePath, trigger, promptText, _options) {
236
276
  }
237
277
  // Apply phase → check deviation
238
278
  if (phase === 'apply') {
279
+ // === Task completion gate for Apply→Review transition ===
280
+ if (trigger === 'post-prompt') {
281
+ const lowerPrompt = promptText.toLowerCase();
282
+ const reviewTransitionKeywords = [
283
+ 'review', '审查', '进入review', 'review阶段', '代码审查',
284
+ 'zhuanspec review', '/review', '开始review',
285
+ ];
286
+ const isReviewTransition = reviewTransitionKeywords.some(kw => lowerPrompt.includes(kw));
287
+ if (isReviewTransition && await FileSystemUtils.fileExists(tasksPath)) {
288
+ try {
289
+ const tasksContent = await FileSystemUtils.readFile(tasksPath);
290
+ const uncompleted = tasksContent.match(/^- \[ \] .+$/gm) || [];
291
+ if (uncompleted.length > 0) {
292
+ return {
293
+ continue: false,
294
+ stopReason: 'Task completion gate: uncompleted tasks block Review transition',
295
+ systemMessage: `⚠️ BLOCKED: Cannot transition to Review — ${uncompleted.length} tasks uncompleted in tasks.md.\n\nUncompleted tasks:\n${uncompleted.map(t => ` ${t.trim()}`).join('\n')}\n\nPlease complete all tasks before entering Review, or use \`zhuanspec review <id> --force\` to override.`,
296
+ hookSpecificOutput: {
297
+ additionalContext: `Task completion gate blocked Apply→Review transition. ${uncompleted.length} uncompleted tasks found.`,
298
+ },
299
+ };
300
+ }
301
+ }
302
+ catch {
303
+ // If tasks.md read fails, allow transition
304
+ }
305
+ }
306
+ }
239
307
  let estimatedFiles = [];
240
308
  let currentTaskFiles = [];
241
309
  // Parse proposal for estimated changes
@@ -261,6 +329,12 @@ async function runDeviationCheck(filePath, trigger, promptText, _options) {
261
329
  }
262
330
  // File is out of scope → deviation detected
263
331
  const deviationId = `dev-${Date.now()}`;
332
+ // Write deviation ID to temp file for record-progress to read (cross-process)
333
+ const deviationMetricsDir = path.join(changeDir, 'metrics');
334
+ if (!fs.existsSync(deviationMetricsDir)) {
335
+ fs.mkdirSync(deviationMetricsDir, { recursive: true });
336
+ }
337
+ fs.writeFileSync(path.join(deviationMetricsDir, '.last-deviation-id'), deviationId, 'utf-8');
264
338
  return {
265
339
  continue: false,
266
340
  stopReason: 'Deviation detected: File not in approved proposal scope',
@@ -287,6 +361,10 @@ ZhuanSpec Iron Law 3: Reverse Sync - When deviation is found, update proposal fi
287
361
  },
288
362
  };
289
363
  }
364
+ // post-prompt in non-apply phases (propose/techDesign/review/...) → silently allow
365
+ if (trigger === 'post-prompt') {
366
+ return { continue: true };
367
+ }
290
368
  // Default: allow
291
369
  return {
292
370
  continue: true,
@@ -295,25 +373,36 @@ ZhuanSpec Iron Law 3: Reverse Sync - When deviation is found, update proposal fi
295
373
  async function parseEstimatedChanges(proposalPath) {
296
374
  try {
297
375
  const content = await FileSystemUtils.readFile(proposalPath);
376
+ const seen = new Set();
298
377
  const files = [];
299
- // Look for "影响范围" or "Affected code" sections
378
+ const addFile = (raw) => {
379
+ const cleaned = raw.replace(/[`'"*[\]()]/g, '').trim();
380
+ if (cleaned.length > 2 && !seen.has(cleaned)) {
381
+ seen.add(cleaned);
382
+ files.push(cleaned);
383
+ }
384
+ };
385
+ // Scan entire proposal for file paths with known extensions
386
+ 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;
387
+ let m;
388
+ while ((m = filePattern.exec(content)) !== null) {
389
+ addFile(m[1]);
390
+ }
391
+ // Also collect bullet-point entries under 影响范围/Affected/Impact sections
300
392
  const lines = content.split('\n');
301
393
  let inAffectedSection = false;
302
394
  for (const line of lines) {
303
- if (line.includes('影响范围') || line.includes('Affected code') || line.includes('Affected files')) {
395
+ if (/影响范围|Affected code|Affected files|Impact/i.test(line) && line.startsWith('#')) {
304
396
  inAffectedSection = true;
305
397
  continue;
306
398
  }
307
- if (inAffectedSection && line.startsWith('##')) {
308
- // End of section
399
+ if (inAffectedSection && line.startsWith('#')) {
309
400
  inAffectedSection = false;
310
401
  }
311
402
  if (inAffectedSection && line.trim().startsWith('-')) {
312
- // Extract file path from bullet point
313
403
  const match = line.match(/-\s*(.+)/);
314
- if (match) {
315
- files.push(match[1].trim());
316
- }
404
+ if (match)
405
+ addFile(match[1].trim());
317
406
  }
318
407
  }
319
408
  return files;
@@ -322,6 +411,21 @@ async function parseEstimatedChanges(proposalPath) {
322
411
  return [];
323
412
  }
324
413
  }
414
+ function extractProposalSummary(content, maxLength) {
415
+ const patterns = [
416
+ /^## (?:Summary|摘要|Overview|概述)[\s\S]*?(?=\n## |\n# |$)/im,
417
+ /^## (?:What Changes|变更内容|Changes|修改内容)[\s\S]*?(?=\n## |\n# |$)/im,
418
+ /^## (?:Impact|影响范围|Affected)[\s\S]*?(?=\n## |\n# |$)/im,
419
+ ];
420
+ const sections = [];
421
+ for (const pattern of patterns) {
422
+ const match = content.match(pattern);
423
+ if (match)
424
+ sections.push(match[0].trim());
425
+ }
426
+ const combined = sections.length > 0 ? sections.join('\n\n') : content;
427
+ return combined.length > maxLength ? combined.substring(0, maxLength) + '...' : combined;
428
+ }
325
429
  async function parseCurrentTaskFiles(tasksPath, currentTask) {
326
430
  if (!currentTask) {
327
431
  return [];
@@ -8,9 +8,50 @@
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
+ }
39
+ async function updateClaudeHudCustomLine(versionBanner) {
40
+ const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(process.env.HOME || '~', '.claude');
41
+ const hudConfigPath = path.join(configDir, 'plugins', 'claude-hud', 'config.json');
42
+ try {
43
+ const raw = await fs.promises.readFile(hudConfigPath, 'utf-8');
44
+ const config = JSON.parse(raw);
45
+ if (!config.display)
46
+ config.display = {};
47
+ config.display.customLine = versionBanner;
48
+ await fs.promises.writeFile(hudConfigPath, JSON.stringify(config, null, 2));
49
+ return true;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
14
55
  export async function initHook(options) {
15
56
  const startTime = Date.now();
16
57
  const output = await runInitHook(options);
@@ -31,6 +72,9 @@ export async function initHook(options) {
31
72
  catch {
32
73
  // Ignore write errors
33
74
  }
75
+ if (output.versionBanner) {
76
+ await updateClaudeHudCustomLine(output.versionBanner);
77
+ }
34
78
  if (options.json) {
35
79
  console.log(JSON.stringify(output));
36
80
  }
@@ -226,9 +270,11 @@ async function runInitHook(_options) {
226
270
  additionalContext += `Found ${rulesFiles.length} rule files in .claude/rules: ${rulesFiles.join(', ')}\n`;
227
271
  additionalContext += `(These are loaded automatically by Claude Code system)\n\n`;
228
272
  }
273
+ const versionBanner = await checkVersionUpdate();
229
274
  return {
230
275
  continue: true,
231
276
  systemMessage: systemMessage || '✓ ZhuanSpec session initialized',
277
+ versionBanner: versionBanner || undefined,
232
278
  hookSpecificOutput: {
233
279
  additionalContext: additionalContext || undefined,
234
280
  env: {
@@ -189,6 +189,7 @@ async function updateProgressJson(changeDir, event) {
189
189
  reviewStats: { loopCount: 0, criticalFixes: 0, testFixes: 0, consistencyFixes: 0 },
190
190
  phaseTransitions: [],
191
191
  phaseDurations: [],
192
+ proposalChanges: [],
192
193
  stats: { tokenUsageTotal: 0, contextLoad: 0, durationMs: { propose: 0, apply: 0, review: 0, archive: 0 } },
193
194
  };
194
195
  }
@@ -217,6 +218,7 @@ async function updateProgressJson(changeDir, event) {
217
218
  reviewStats: { loopCount: 0, criticalFixes: 0, testFixes: 0, consistencyFixes: 0 },
218
219
  phaseTransitions: [],
219
220
  phaseDurations: [],
221
+ proposalChanges: [],
220
222
  stats: { tokenUsageTotal: 0, contextLoad: 0, durationMs: { propose: 0, apply: 0, review: 0, archive: 0 } },
221
223
  };
222
224
  }