@zhuan-ai/zhuanspec 2.10.0 → 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.
package/dist/cli/index.js CHANGED
@@ -220,6 +220,7 @@ program
220
220
  .command('review [change-name]')
221
221
  .description('Run ZhuanSpec review and unit test verification')
222
222
  .option('--json', 'Output review report as JSON')
223
+ .option('--force', 'Skip task completion check and force review')
223
224
  .option('--test-command <command>', 'Test command to execute (default: npm test)')
224
225
  .option('--coverage-threshold <n>', 'Coverage threshold percentage (default: 80)')
225
226
  .action(async (changeName, options) => {
@@ -18,13 +18,22 @@ export declare class DesignCommand {
18
18
  private bindDesignSourceToChange;
19
19
  private selectChangeInteractively;
20
20
  /**
21
- * Validate design.md for required sections and optionally auto-fix
21
+ * Validate design.md for required sections and optionally auto-fix.
22
+ * Uses dynamic section derivation: always-required sections are checked for all proposals,
23
+ * while conditional sections are only required when techDesign/tech-spec.md content matches.
22
24
  */
23
25
  private validateDesign;
24
26
  /**
25
27
  * Auto-fix design content by adding missing section skeletons
26
28
  */
27
29
  private autoFixDesign;
30
+ /**
31
+ * Derive required design.md sections dynamically.
32
+ * Always-required: 背景/Background, 决策/Decision, 风险/Risks.
33
+ * Conditional sections are added based on techDesign/tech-spec.md content.
34
+ * When no techDesign directory exists, only always-required sections are enforced (backward compat).
35
+ */
36
+ private getRequiredSections;
28
37
  /**
29
38
  * Generate a kebab-case change name from design context
30
39
  */
@@ -32,7 +41,8 @@ export declare class DesignCommand {
32
41
  /**
33
42
  * Create a new change directory for techDesign phase
34
43
  * - Creates change directory using createChange()
35
- * - Creates .tech-design marker file
44
+ * - Creates .tech-design marker file (backward compat)
45
+ * - Creates techDesign/ directory with tech-spec.md placeholder
36
46
  * - Initializes progress.json with phase=techDesign
37
47
  * - Creates design.md skeleton
38
48
  * - Creates doc/ request file
@@ -209,7 +209,9 @@ export class DesignCommand {
209
209
  }
210
210
  }
211
211
  /**
212
- * Validate design.md for required sections and optionally auto-fix
212
+ * Validate design.md for required sections and optionally auto-fix.
213
+ * Uses dynamic section derivation: always-required sections are checked for all proposals,
214
+ * while conditional sections are only required when techDesign/tech-spec.md content matches.
213
215
  */
214
216
  async validateDesign(changeName, autoFix) {
215
217
  const changeDir = path.join(process.cwd(), 'zhuanspec', 'changes', changeName);
@@ -219,15 +221,8 @@ export class DesignCommand {
219
221
  }
220
222
  const content = await FileSystemUtils.readFile(designPath);
221
223
  const issues = [];
222
- // Check required sections: 背景/目标/决策/风险
223
- const requiredSections = [
224
- { pattern: /^##\s+(背景|Background)/im, name: '背景/Background', skeleton: '## 背景\n\n[请补充背景、约束、利益相关者]\n' },
225
- { pattern: /^##\s+(目标|Goals|非目标|Non-Goals)/im, name: '目标/Goals', skeleton: '## 目标 / 非目标\n\n- Goals: [...]\n- Non-Goals: [...]\n' },
226
- { pattern: /^##\s+(决策|Decision)/im, name: '决策/Decision', skeleton: '## 决策\n\n- Decision: [内容和原因]\n- Alternatives considered: [选项 + 理由]\n' },
227
- { pattern: /^##\s+(接口设计|Interface|API)/im, name: '接口设计/Interface', skeleton: '## 接口设计\n\n<!-- 接口签名、入参出参字段类型、枚举值、SCF 方法名 -->\n' },
228
- { pattern: /^##\s+(实现细节|Implementation)/im, name: '实现细节/Implementation', skeleton: '## 实现细节\n\n<!-- 分层实现说明(Component→Application→Domain→Assemble→DAO)、关键逻辑、复用点 -->\n' },
229
- { pattern: /^##\s+(风险|权衡|Risks)/im, name: '风险/Risks', skeleton: '## 风险 / 权衡\n\n- [风险] -> 缓解措施\n' },
230
- ];
224
+ // Dynamically derive required sections based on techDesign content
225
+ const requiredSections = await this.getRequiredSections(changeDir);
231
226
  for (const section of requiredSections) {
232
227
  if (!section.pattern.test(content)) {
233
228
  issues.push({
@@ -291,6 +286,58 @@ export class DesignCommand {
291
286
  }
292
287
  return result;
293
288
  }
289
+ /**
290
+ * Derive required design.md sections dynamically.
291
+ * Always-required: 背景/Background, 决策/Decision, 风险/Risks.
292
+ * Conditional sections are added based on techDesign/tech-spec.md content.
293
+ * When no techDesign directory exists, only always-required sections are enforced (backward compat).
294
+ */
295
+ async getRequiredSections(changePath) {
296
+ // Always-required sections for every proposal
297
+ const sections = [
298
+ { pattern: /^##\s+(背景|Background)/im, name: '背景/Background', skeleton: '## 背景\n\n[请补充背景、约束、利益相关者]\n' },
299
+ { pattern: /^##\s+(决策|Decision)/im, name: '决策/Decision', skeleton: '## 决策\n\n- Decision: [内容和原因]\n- Alternatives considered: [选项 + 理由]\n' },
300
+ { pattern: /^##\s+(风险|权衡|Risks)/im, name: '风险/Risks', skeleton: '## 风险 / 权衡\n\n- [风险] -> 缓解措施\n' },
301
+ ];
302
+ // Conditionally add sections based on tech-spec.md content
303
+ const techSpecPath = path.join(changePath, 'techDesign', 'tech-spec.md');
304
+ if (await FileSystemUtils.fileExists(techSpecPath)) {
305
+ const techContent = await fs.readFile(techSpecPath, 'utf-8');
306
+ // Tech spec has DDL / table creation → require Database Design
307
+ if (/CREATE\s+TABLE|DDL|建表|ALTER\s+TABLE/i.test(techContent)) {
308
+ sections.push({
309
+ pattern: /^##\s+(数据模型|数据库设计|Database\s*Design)/im,
310
+ name: '数据模型/Database Design',
311
+ skeleton: '## 数据模型\n\n<!-- DB schema 变更、实体类字段、索引设计 -->\n',
312
+ });
313
+ }
314
+ // Tech spec has API / interface definitions → require API Contract
315
+ if (/接口|API|SCF|RPC|Controller|@RequestMapping/i.test(techContent)) {
316
+ sections.push({
317
+ pattern: /^##\s+(接口设计|接口契约|API\s*Contract|Interface)/im,
318
+ name: '接口设计/API Contract',
319
+ skeleton: '## 接口设计\n\n<!-- 接口签名、入参出参字段类型、枚举值、SCF 方法名 -->\n',
320
+ });
321
+ }
322
+ // Tech spec has ES / MQ / Cache → require Infrastructure
323
+ if (/ElasticSearch|ES.*索引|mapping|MQ|RocketMQ|Consumer|Redis|缓存|Cache/i.test(techContent)) {
324
+ sections.push({
325
+ pattern: /^##\s+(基础设施|Infrastructure)/im,
326
+ name: '基础设施/Infrastructure',
327
+ skeleton: '## 基础设施\n\n<!-- ES / MQ / Cache 等基础设施设计 -->\n',
328
+ });
329
+ }
330
+ // Tech spec has frontend design → require Frontend Design
331
+ if (/前端|页面|组件|路由|Frontend|Component|React|Vue/i.test(techContent)) {
332
+ sections.push({
333
+ pattern: /^##\s+(前端设计|Frontend\s*Design)/im,
334
+ name: '前端设计/Frontend Design',
335
+ skeleton: '## 前端设计\n\n<!-- 页面布局、组件结构、路由设计 -->\n',
336
+ });
337
+ }
338
+ }
339
+ return sections;
340
+ }
294
341
  /**
295
342
  * Generate a kebab-case change name from design context
296
343
  */
@@ -328,7 +375,8 @@ export class DesignCommand {
328
375
  /**
329
376
  * Create a new change directory for techDesign phase
330
377
  * - Creates change directory using createChange()
331
- * - Creates .tech-design marker file
378
+ * - Creates .tech-design marker file (backward compat)
379
+ * - Creates techDesign/ directory with tech-spec.md placeholder
332
380
  * - Initializes progress.json with phase=techDesign
333
381
  * - Creates design.md skeleton
334
382
  * - Creates doc/ request file
@@ -340,7 +388,11 @@ export class DesignCommand {
340
388
  const changeDir = path.join(projectRoot, 'zhuanspec', 'changes', changeName);
341
389
  // 2. Initialize progress.json with phase=techDesign
342
390
  await initializeProgress(changeName, 'techDesign');
343
- // 3. Create .tech-design marker file
391
+ // 3. Create techDesign/ directory and tech-spec.md placeholder
392
+ const techDesignDir = path.join(changeDir, 'techDesign');
393
+ await FileSystemUtils.createDirectory(techDesignDir);
394
+ await FileSystemUtils.writeFile(path.join(techDesignDir, 'tech-spec.md'), `# Tech Spec\n\ncreated: ${new Date().toISOString()}\nsource: ${context.desc || context.dashenPageId || 'manual'}\n\n> This file will be populated by the techDesign skill.\n`);
395
+ // 3b. Also create .tech-design marker for backward compatibility
344
396
  await FileSystemUtils.writeFile(path.join(changeDir, '.tech-design'), `created: ${new Date().toISOString()}\nsource: ${context.desc || context.dashenPageId || 'manual'}\n`);
345
397
  // 4. Create design.md skeleton
346
398
  await FileSystemUtils.writeFile(path.join(changeDir, 'design.md'), DEFAULT_DESIGN_TEMPLATE);
@@ -312,6 +312,17 @@ export class ProgressCommand {
312
312
  else if (completedCount === totalTasks && totalTasks > 0) {
313
313
  console.log('✅ 全部任务完成,准备进入 Review 阶段');
314
314
  }
315
+ // Proposal changes history
316
+ if (progress?.proposalChanges && progress.proposalChanges.length > 0) {
317
+ console.log('');
318
+ console.log('📝 提案修改历史:');
319
+ for (const change of progress.proposalChanges) {
320
+ console.log(` [${change.timestamp}] ${change.triggeredBy} (${change.phase})${change.relatedDeviationId ? ` ← ${change.relatedDeviationId}` : ''}`);
321
+ for (const file of change.modifiedFiles) {
322
+ console.log(` ${file.changeType}: ${file.filePath} - ${file.summary}`);
323
+ }
324
+ }
325
+ }
315
326
  }
316
327
  generateProgressBar(percentage, blocks) {
317
328
  const filled = Math.round((percentage / 100) * blocks);
@@ -1,6 +1,7 @@
1
1
  export declare class ReviewCommand {
2
2
  execute(changeName?: string, options?: {
3
3
  json?: boolean;
4
+ force?: boolean;
4
5
  }): Promise<void>;
5
6
  private selectChangeInteractively;
6
7
  }
@@ -22,6 +22,19 @@ export class ReviewCommand {
22
22
  if (!proposalExists || !tasksExists) {
23
23
  throw new Error(`Change '${changeName}' is not ready for review. Missing proposal.md or tasks.md`);
24
24
  }
25
+ // === Task completion hard gate ===
26
+ if (tasksExists) {
27
+ const tasksContent = await fs.readFile(tasksPath, 'utf-8');
28
+ const uncompletedTasks = tasksContent.match(/^- \[ \] .+$/gm) || [];
29
+ if (uncompletedTasks.length > 0 && !options?.force) {
30
+ throw new Error(`Change '${changeName}' has ${uncompletedTasks.length} uncompleted tasks. ` +
31
+ `Please complete all tasks before review. Use --force to override.\n` +
32
+ uncompletedTasks.map(t => ` ${t.trim()}`).join('\n'));
33
+ }
34
+ if (uncompletedTasks.length > 0 && options?.force) {
35
+ console.log(`⚠️ Force mode: skipping ${uncompletedTasks.length} uncompleted tasks`);
36
+ }
37
+ }
25
38
  // Check skill output JSON files exist
26
39
  const codeReviewJsonPath = path.join(reviewDir, 'code-review-result.json');
27
40
  const unitTestJsonPath = path.join(reviewDir, 'unit-test-result.json');
@@ -124,6 +124,10 @@ export const COMMAND_REGISTRY = [
124
124
  positionalType: 'change-id',
125
125
  flags: [
126
126
  COMMON_FLAGS.json,
127
+ {
128
+ name: 'force',
129
+ description: 'Skip task completion check and force review',
130
+ },
127
131
  {
128
132
  name: 'test-command',
129
133
  description: 'Test command to execute (default: npm test)',
@@ -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';
@@ -148,6 +149,13 @@ async function runDeviationCheck(filePath, trigger, promptText, _options) {
148
149
  return !estimatedFiles.map(normalizePath).some((scope) => normalized.includes(scope) || scope.includes(normalized));
149
150
  });
150
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');
151
159
  return {
152
160
  continue: false,
153
161
  stopReason: 'Resume deviation detected',
@@ -268,6 +276,34 @@ async function runDeviationCheck(filePath, trigger, promptText, _options) {
268
276
  }
269
277
  // Apply phase → check deviation
270
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
+ }
271
307
  let estimatedFiles = [];
272
308
  let currentTaskFiles = [];
273
309
  // Parse proposal for estimated changes
@@ -293,6 +329,12 @@ async function runDeviationCheck(filePath, trigger, promptText, _options) {
293
329
  }
294
330
  // File is out of scope → deviation detected
295
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');
296
338
  return {
297
339
  continue: false,
298
340
  stopReason: 'Deviation detected: File not in approved proposal scope',
@@ -36,6 +36,22 @@ async function checkVersionUpdate() {
36
36
  return null;
37
37
  }
38
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
+ }
39
55
  export async function initHook(options) {
40
56
  const startTime = Date.now();
41
57
  const output = await runInitHook(options);
@@ -56,6 +72,9 @@ export async function initHook(options) {
56
72
  catch {
57
73
  // Ignore write errors
58
74
  }
75
+ if (output.versionBanner) {
76
+ await updateClaudeHudCustomLine(output.versionBanner);
77
+ }
59
78
  if (options.json) {
60
79
  console.log(JSON.stringify(output));
61
80
  }
@@ -252,12 +271,10 @@ async function runInitHook(_options) {
252
271
  additionalContext += `(These are loaded automatically by Claude Code system)\n\n`;
253
272
  }
254
273
  const versionBanner = await checkVersionUpdate();
255
- const finalMessage = versionBanner
256
- ? versionBanner + '\n' + (systemMessage || '✓ ZhuanSpec session initialized')
257
- : (systemMessage || '✓ ZhuanSpec session initialized');
258
274
  return {
259
275
  continue: true,
260
- systemMessage: finalMessage,
276
+ systemMessage: systemMessage || '✓ ZhuanSpec session initialized',
277
+ versionBanner: versionBanner || undefined,
261
278
  hookSpecificOutput: {
262
279
  additionalContext: additionalContext || undefined,
263
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
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Pre-Review Hook
3
+ *
4
+ * Quality gate checks before entering Review phase:
5
+ * 1. All tasks in tasks.md are completed
6
+ * 2. Progress data is consistent with tasks.md
7
+ * 3. Specs completeness check (all delta specs have content)
8
+ */
9
+ interface CheckResult {
10
+ passed: boolean;
11
+ message: string;
12
+ details?: string[];
13
+ }
14
+ export interface PreReviewResult {
15
+ passed: boolean;
16
+ details: {
17
+ tasksComplete: CheckResult;
18
+ progressConsistent: CheckResult;
19
+ specsComplete: CheckResult;
20
+ };
21
+ }
22
+ /**
23
+ * Run all pre-review checks for a change directory.
24
+ */
25
+ export declare function preReviewHook(changePath: string): Promise<PreReviewResult>;
26
+ export {};
27
+ //# sourceMappingURL=pre-review.d.ts.map
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Pre-Review Hook
3
+ *
4
+ * Quality gate checks before entering Review phase:
5
+ * 1. All tasks in tasks.md are completed
6
+ * 2. Progress data is consistent with tasks.md
7
+ * 3. Specs completeness check (all delta specs have content)
8
+ */
9
+ import path from 'path';
10
+ import { FileSystemUtils } from '../../utils/file-system.js';
11
+ import { readdirSync, existsSync } from 'fs';
12
+ /**
13
+ * Run all pre-review checks for a change directory.
14
+ */
15
+ export async function preReviewHook(changePath) {
16
+ const checks = {
17
+ tasksComplete: await checkTasksCompletion(changePath),
18
+ progressConsistent: await checkProgressConsistency(changePath),
19
+ specsComplete: await checkSpecsCompleteness(changePath),
20
+ };
21
+ return {
22
+ passed: Object.values(checks).every(c => c.passed),
23
+ details: checks,
24
+ };
25
+ }
26
+ /**
27
+ * Check that all tasks in tasks.md are completed.
28
+ */
29
+ async function checkTasksCompletion(changePath) {
30
+ const tasksPath = path.join(changePath, 'tasks.md');
31
+ if (!await FileSystemUtils.fileExists(tasksPath)) {
32
+ return {
33
+ passed: false,
34
+ message: 'tasks.md not found',
35
+ };
36
+ }
37
+ try {
38
+ const content = await FileSystemUtils.readFile(tasksPath);
39
+ const allTasks = content.match(/^- \[[ x]\] .+$/gm) || [];
40
+ const uncompletedTasks = content.match(/^- \[ \] .+$/gm) || [];
41
+ const completedTasks = content.match(/^- \[x\] .+$/gm) || [];
42
+ if (allTasks.length === 0) {
43
+ return {
44
+ passed: false,
45
+ message: 'No tasks found in tasks.md',
46
+ };
47
+ }
48
+ if (uncompletedTasks.length > 0) {
49
+ return {
50
+ passed: false,
51
+ message: `${uncompletedTasks.length}/${allTasks.length} tasks uncompleted`,
52
+ details: uncompletedTasks.map(t => t.trim()),
53
+ };
54
+ }
55
+ return {
56
+ passed: true,
57
+ message: `All ${completedTasks.length} tasks completed`,
58
+ };
59
+ }
60
+ catch {
61
+ return {
62
+ passed: false,
63
+ message: 'Failed to read tasks.md',
64
+ };
65
+ }
66
+ }
67
+ /**
68
+ * Check that progress.json data is consistent with tasks.md counts.
69
+ */
70
+ async function checkProgressConsistency(changePath) {
71
+ const progressPath = path.join(changePath, 'metrics', 'progress.json');
72
+ const tasksPath = path.join(changePath, 'tasks.md');
73
+ if (!await FileSystemUtils.fileExists(progressPath)) {
74
+ return {
75
+ passed: true,
76
+ message: 'No progress.json found (skipped)',
77
+ };
78
+ }
79
+ if (!await FileSystemUtils.fileExists(tasksPath)) {
80
+ return {
81
+ passed: true,
82
+ message: 'No tasks.md found (skipped)',
83
+ };
84
+ }
85
+ try {
86
+ const progressContent = await FileSystemUtils.readFile(progressPath);
87
+ const progress = JSON.parse(progressContent);
88
+ const tasksContent = await FileSystemUtils.readFile(tasksPath);
89
+ const totalFromTasks = (tasksContent.match(/^- \[[ x]\] .+$/gm) || []).length;
90
+ const completedFromTasks = (tasksContent.match(/^- \[x\] .+$/gm) || []).length;
91
+ const totalFromProgress = progress.totalTasks || 0;
92
+ if (totalFromProgress !== totalFromTasks) {
93
+ return {
94
+ passed: false,
95
+ message: `Task count mismatch: progress.json says ${totalFromProgress}, tasks.md has ${totalFromTasks}`,
96
+ details: [
97
+ `progress.json totalTasks: ${totalFromProgress}`,
98
+ `tasks.md total tasks: ${totalFromTasks}`,
99
+ `tasks.md completed: ${completedFromTasks}`,
100
+ ],
101
+ };
102
+ }
103
+ return {
104
+ passed: true,
105
+ message: `Progress consistent: ${completedFromTasks}/${totalFromTasks} tasks`,
106
+ };
107
+ }
108
+ catch {
109
+ return {
110
+ passed: true,
111
+ message: 'Progress consistency check skipped (parse error)',
112
+ };
113
+ }
114
+ }
115
+ /**
116
+ * Check that all delta spec files under specs/ have meaningful content.
117
+ */
118
+ async function checkSpecsCompleteness(changePath) {
119
+ const specsDir = path.join(changePath, 'specs');
120
+ if (!existsSync(specsDir)) {
121
+ return {
122
+ passed: false,
123
+ message: 'specs/ directory not found',
124
+ };
125
+ }
126
+ try {
127
+ const capabilities = readdirSync(specsDir, { withFileTypes: true })
128
+ .filter(e => e.isDirectory())
129
+ .map(e => e.name);
130
+ if (capabilities.length === 0) {
131
+ return {
132
+ passed: false,
133
+ message: 'No capability directories found in specs/',
134
+ };
135
+ }
136
+ const emptySpecs = [];
137
+ for (const cap of capabilities) {
138
+ const specPath = path.join(specsDir, cap, 'spec.md');
139
+ if (!existsSync(specPath)) {
140
+ emptySpecs.push(`${cap}/spec.md (missing)`);
141
+ continue;
142
+ }
143
+ const content = await FileSystemUtils.readFile(specPath);
144
+ // Check if spec has at least one delta marker (ADDED/MODIFIED/REMOVED)
145
+ const hasDelta = /\b(ADDED|MODIFIED|REMOVED)\b/.test(content);
146
+ if (!hasDelta) {
147
+ emptySpecs.push(`${cap}/spec.md (no delta markers)`);
148
+ }
149
+ }
150
+ if (emptySpecs.length > 0) {
151
+ return {
152
+ passed: false,
153
+ message: `${emptySpecs.length} spec(s) incomplete`,
154
+ details: emptySpecs,
155
+ };
156
+ }
157
+ return {
158
+ passed: true,
159
+ message: `All ${capabilities.length} spec(s) complete`,
160
+ };
161
+ }
162
+ catch {
163
+ return {
164
+ passed: false,
165
+ message: 'Failed to check specs completeness',
166
+ };
167
+ }
168
+ }
169
+ //# sourceMappingURL=pre-review.js.map
@@ -98,6 +98,8 @@ interface SkillCallRecord {
98
98
  durationMs?: number;
99
99
  success: boolean;
100
100
  outputSummary?: string;
101
+ afterAction?: string;
102
+ flowResumed?: boolean;
101
103
  }
102
104
  /**
103
105
  * Hook 触发记录
@@ -149,6 +151,28 @@ export interface PhaseDurationRecord {
149
151
  taskCount: number;
150
152
  completedTaskCount: number;
151
153
  }
154
+ /**
155
+ * Proposal change record for tracking modifications to proposal files
156
+ */
157
+ export interface ProposalChangeRecord {
158
+ changeRecordId: string;
159
+ timestamp: string;
160
+ phase: string;
161
+ triggeredBy: 'reverse-sync' | 'manual-edit' | 'ai-supplement' | 'review-fix';
162
+ modifiedFiles: Array<{
163
+ filePath: string;
164
+ changeType: 'modified' | 'created' | 'deleted';
165
+ summary: string;
166
+ diff?: {
167
+ before: string;
168
+ after: string;
169
+ };
170
+ linesAdded?: number;
171
+ linesRemoved?: number;
172
+ }>;
173
+ reason?: string;
174
+ relatedDeviationId?: string;
175
+ }
152
176
  export interface ProgressData {
153
177
  changeId: string;
154
178
  sessionId: string;
@@ -181,6 +205,7 @@ export interface ProgressData {
181
205
  archive: number;
182
206
  };
183
207
  };
208
+ proposalChanges: ProposalChangeRecord[];
184
209
  events?: Array<{
185
210
  event: string;
186
211
  timestamp: string;