@zhuan-ai/zhuanspec 2.2.4 → 2.4.8

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 (103) hide show
  1. package/bin/zhuanspec-hook.js +3 -0
  2. package/dist/cli/hooks.d.ts +14 -0
  3. package/dist/cli/hooks.js +465 -0
  4. package/dist/cli/index.js +100 -0
  5. package/dist/commands/artifact-workflow.js +15 -34
  6. package/dist/commands/design.d.ts +42 -0
  7. package/dist/commands/design.js +337 -0
  8. package/dist/commands/progress.d.ts +32 -0
  9. package/dist/commands/progress.js +278 -0
  10. package/dist/commands/review.d.ts +32 -0
  11. package/dist/commands/review.js +472 -0
  12. package/dist/commands/validate.d.ts +14 -0
  13. package/dist/commands/validate.js +161 -10
  14. package/dist/core/archive.d.ts +1 -0
  15. package/dist/core/archive.js +45 -3
  16. package/dist/core/completions/command-registry.js +67 -0
  17. package/dist/core/configurators/slash/amazon-q.js +32 -2
  18. package/dist/core/configurators/slash/antigravity.js +8 -2
  19. package/dist/core/configurators/slash/auggie.js +16 -1
  20. package/dist/core/configurators/slash/base.js +1 -1
  21. package/dist/core/configurators/slash/claude.d.ts +4 -0
  22. package/dist/core/configurators/slash/claude.js +56 -1
  23. package/dist/core/configurators/slash/cline.js +8 -2
  24. package/dist/core/configurators/slash/codebuddy.js +22 -1
  25. package/dist/core/configurators/slash/codex.js +21 -0
  26. package/dist/core/configurators/slash/costrict.js +15 -0
  27. package/dist/core/configurators/slash/crush.js +22 -1
  28. package/dist/core/configurators/slash/cursor.js +22 -1
  29. package/dist/core/configurators/slash/factory.js +16 -1
  30. package/dist/core/configurators/slash/gemini.js +8 -2
  31. package/dist/core/configurators/slash/github-copilot.js +19 -1
  32. package/dist/core/configurators/slash/iflow.js +22 -1
  33. package/dist/core/configurators/slash/kilocode.js +4 -1
  34. package/dist/core/configurators/slash/opencode.js +27 -0
  35. package/dist/core/configurators/slash/qoder.d.ts +4 -0
  36. package/dist/core/configurators/slash/qoder.js +59 -1
  37. package/dist/core/configurators/slash/qwen.js +8 -2
  38. package/dist/core/configurators/slash/roocode.js +8 -2
  39. package/dist/core/configurators/slash/windsurf.js +8 -2
  40. package/dist/core/dashboard/metrics.d.ts +33 -0
  41. package/dist/core/dashboard/metrics.js +114 -0
  42. package/dist/core/hooks/collect-knowledge.d.ts +16 -0
  43. package/dist/core/hooks/collect-knowledge.js +203 -0
  44. package/dist/core/hooks/context-load-hook.d.ts +25 -0
  45. package/dist/core/hooks/context-load-hook.js +159 -0
  46. package/dist/core/hooks/deviation-check.d.ts +27 -0
  47. package/dist/core/hooks/deviation-check.js +403 -0
  48. package/dist/core/hooks/deviation-handler.d.ts +43 -0
  49. package/dist/core/hooks/deviation-handler.js +98 -0
  50. package/dist/core/hooks/init.d.ts +14 -0
  51. package/dist/core/hooks/init.js +244 -0
  52. package/dist/core/hooks/notify-milestone.d.ts +14 -0
  53. package/dist/core/hooks/notify-milestone.js +170 -0
  54. package/dist/core/hooks/post-apply.d.ts +29 -0
  55. package/dist/core/hooks/post-apply.js +173 -0
  56. package/dist/core/hooks/post-archive.d.ts +7 -0
  57. package/dist/core/hooks/post-archive.js +208 -0
  58. package/dist/core/hooks/pre-apply.d.ts +34 -0
  59. package/dist/core/hooks/pre-apply.js +139 -0
  60. package/dist/core/hooks/pre-archive.d.ts +7 -0
  61. package/dist/core/hooks/pre-archive.js +50 -0
  62. package/dist/core/hooks/record-progress.d.ts +49 -0
  63. package/dist/core/hooks/record-progress.js +494 -0
  64. package/dist/core/hooks/review-hooks.d.ts +89 -0
  65. package/dist/core/hooks/review-hooks.js +345 -0
  66. package/dist/core/hooks/review-orchestrator.d.ts +40 -0
  67. package/dist/core/hooks/review-orchestrator.js +146 -0
  68. package/dist/core/hooks/summarize.d.ts +15 -0
  69. package/dist/core/hooks/summarize.js +282 -0
  70. package/dist/core/hooks/user-input-hook.d.ts +25 -0
  71. package/dist/core/hooks/user-input-hook.js +179 -0
  72. package/dist/core/init.d.ts +8 -0
  73. package/dist/core/init.js +251 -23
  74. package/dist/core/parsers/requirement-blocks.js +13 -10
  75. package/dist/core/templates/agents-template.d.ts +1 -1
  76. package/dist/core/templates/agents-template.js +510 -243
  77. package/dist/core/templates/index.d.ts +1 -0
  78. package/dist/core/templates/index.js +1 -0
  79. package/dist/core/templates/skill-templates.js +46 -152
  80. package/dist/core/templates/slash-command-templates.d.ts +1 -1
  81. package/dist/core/templates/slash-command-templates.js +352 -20
  82. package/dist/core/templates/tasks-template.d.ts +7 -0
  83. package/dist/core/templates/tasks-template.js +130 -24
  84. package/dist/core/templates/tdd-tasks-template.d.ts +3 -0
  85. package/dist/core/templates/tdd-tasks-template.js +91 -38
  86. package/dist/core/templates/test-cases-template.d.ts +41 -0
  87. package/dist/core/templates/test-cases-template.js +128 -0
  88. package/dist/core/validation/strict-rules.d.ts +44 -5
  89. package/dist/core/validation/strict-rules.js +302 -8
  90. package/dist/core/validation/validator.js +52 -2
  91. package/dist/core/view.d.ts +1 -0
  92. package/dist/core/view.js +60 -2
  93. package/dist/mcp/index.d.ts +28 -0
  94. package/dist/mcp/index.js +31 -0
  95. package/dist/utils/file-system.d.ts +1 -0
  96. package/dist/utils/file-system.js +11 -0
  97. package/dist/utils/item-discovery.js +24 -2
  98. package/dist/utils/phase-utils.d.ts +36 -0
  99. package/dist/utils/phase-utils.js +117 -0
  100. package/package.json +22 -23
  101. package/schemas/spec-driven/schema.yaml +45 -31
  102. package/schemas/spec-driven/templates/spec.md +142 -5
  103. package/schemas/spec-driven/templates/tasks.md +73 -9
@@ -0,0 +1,244 @@
1
+ /**
2
+ * SessionStart Hook - Initialize ZhuanSpec environment
3
+ *
4
+ * This hook runs when Claude Code starts a new session:
5
+ * 1. Loads project.md and knowledge/ files
6
+ * 2. Sets environment variables for phase tracking
7
+ * 3. Retrieves relevant implicit conventions and change patterns
8
+ */
9
+ import path from 'path';
10
+ import fs from 'fs';
11
+ import { FileSystemUtils } from '../../utils/file-system.js';
12
+ import { initializeProgress, recordHookTrigger, getBeijingTime } from './record-progress.js';
13
+ import { getCurrentPhase, getPhaseFromProgress } from '../../utils/phase-utils.js';
14
+ export async function initHook(options) {
15
+ const startTime = Date.now();
16
+ const output = await runInitHook(options);
17
+ const durationMs = Date.now() - startTime;
18
+ // Record hook trigger
19
+ await recordHookTrigger('init', 'SessionStart', true, {
20
+ message: output.systemMessage,
21
+ durationMs,
22
+ });
23
+ // Write debug log to confirm hook execution
24
+ const cwd = process.cwd();
25
+ const debugLogPath = path.join(cwd, 'zhuanspec', '.hook-debug.log');
26
+ const timestamp = getBeijingTime();
27
+ const logEntry = `[${timestamp}] init-hook executed\n`;
28
+ try {
29
+ await fs.promises.appendFile(debugLogPath, logEntry);
30
+ }
31
+ catch {
32
+ // Ignore write errors
33
+ }
34
+ if (options.json) {
35
+ console.log(JSON.stringify(output));
36
+ }
37
+ else {
38
+ if (output.systemMessage) {
39
+ console.log(output.systemMessage);
40
+ }
41
+ }
42
+ }
43
+ async function runInitHook(_options) {
44
+ const cwd = process.cwd();
45
+ const zhuanspecDir = path.join(cwd, 'zhuanspec');
46
+ // Check if ZhuanSpec is initialized in this project
47
+ if (!await FileSystemUtils.directoryExists(zhuanspecDir)) {
48
+ return {
49
+ continue: true,
50
+ systemMessage: 'ℹ ZhuanSpec not initialized in this project. Run `zhuanspec init` to start.',
51
+ };
52
+ }
53
+ // Load project.md if exists
54
+ const projectMdPath = path.join(zhuanspecDir, 'project.md');
55
+ let projectContext = '';
56
+ if (await FileSystemUtils.fileExists(projectMdPath)) {
57
+ try {
58
+ projectContext = await FileSystemUtils.readFile(projectMdPath);
59
+ }
60
+ catch {
61
+ // Ignore read errors
62
+ }
63
+ }
64
+ // Load knowledge files if exist
65
+ const knowledgeDir = path.join(zhuanspecDir, 'knowledge');
66
+ const knowledgeFiles = [];
67
+ if (await FileSystemUtils.directoryExists(knowledgeDir)) {
68
+ try {
69
+ // Load index.md
70
+ const indexPath = path.join(knowledgeDir, 'index.md');
71
+ if (await FileSystemUtils.fileExists(indexPath)) {
72
+ const content = await FileSystemUtils.readFile(indexPath);
73
+ knowledgeFiles.push(`### index.md\n${content.substring(0, 500)}...`);
74
+ }
75
+ // Load from troubleshooting subdirectory
76
+ const troubleshootingDir = path.join(knowledgeDir, 'troubleshooting');
77
+ if (await FileSystemUtils.directoryExists(troubleshootingDir)) {
78
+ const entries = await fs.promises.readdir(troubleshootingDir);
79
+ for (const entry of entries) {
80
+ if (entry.endsWith('.md')) {
81
+ const filePath = path.join(troubleshootingDir, entry);
82
+ const content = await FileSystemUtils.readFile(filePath);
83
+ knowledgeFiles.push(`### troubleshooting/${entry}\n${content.substring(0, 500)}...`);
84
+ }
85
+ }
86
+ }
87
+ // Load from best-practices subdirectory
88
+ const bestPracticesDir = path.join(knowledgeDir, 'best-practices');
89
+ if (await FileSystemUtils.directoryExists(bestPracticesDir)) {
90
+ const entries = await fs.promises.readdir(bestPracticesDir);
91
+ for (const entry of entries) {
92
+ if (entry.endsWith('.md')) {
93
+ const filePath = path.join(bestPracticesDir, entry);
94
+ const content = await FileSystemUtils.readFile(filePath);
95
+ knowledgeFiles.push(`### best-practices/${entry}\n${content.substring(0, 500)}...`);
96
+ }
97
+ }
98
+ }
99
+ // Load from implicit-conventions subdirectory
100
+ const implicitConventionsDir = path.join(knowledgeDir, 'implicit-conventions');
101
+ if (await FileSystemUtils.directoryExists(implicitConventionsDir)) {
102
+ const entries = await fs.promises.readdir(implicitConventionsDir);
103
+ for (const entry of entries) {
104
+ if (entry.endsWith('.md')) {
105
+ const filePath = path.join(implicitConventionsDir, entry);
106
+ const content = await FileSystemUtils.readFile(filePath);
107
+ knowledgeFiles.push(`### implicit-conventions/${entry}\n${content.substring(0, 500)}...`);
108
+ }
109
+ }
110
+ }
111
+ }
112
+ catch {
113
+ // Ignore read errors
114
+ }
115
+ }
116
+ // Check for .claude/rules files (project-level)
117
+ const claudeRulesDir = path.join(cwd, '.claude', 'rules');
118
+ let rulesFiles = [];
119
+ try {
120
+ if (await FileSystemUtils.directoryExists(claudeRulesDir)) {
121
+ const entries = await fs.promises.readdir(claudeRulesDir);
122
+ rulesFiles = entries.filter(e => e.endsWith('.md') &&
123
+ !e.startsWith('.'));
124
+ }
125
+ }
126
+ catch {
127
+ // Ignore read errors
128
+ }
129
+ // Check for active changes
130
+ const changesDir = path.join(zhuanspecDir, 'changes');
131
+ let activeChanges = [];
132
+ try {
133
+ if (await FileSystemUtils.directoryExists(changesDir)) {
134
+ const entries = await fs.promises.readdir(changesDir);
135
+ activeChanges = entries.filter(e => !e.startsWith('.') &&
136
+ !e.startsWith('archive') &&
137
+ e !== 'archive');
138
+ }
139
+ }
140
+ catch {
141
+ // Ignore read errors
142
+ }
143
+ // Determine current phase based on active changes
144
+ // Priority: progress.json phase > marker files > directory existence
145
+ let phase = 'idle';
146
+ let changeId = '';
147
+ let currentTask = '';
148
+ if (activeChanges.length > 0) {
149
+ for (const change of activeChanges) {
150
+ const changeDir = path.join(changesDir, change);
151
+ // 1. Priority: read phase from progress.json
152
+ const progressPhase = await getPhaseFromProgress(changeDir);
153
+ if (progressPhase) {
154
+ phase = progressPhase;
155
+ changeId = change;
156
+ }
157
+ else {
158
+ // 2. Fallback: comprehensive phase detection
159
+ phase = await getCurrentPhase(changeDir);
160
+ changeId = change;
161
+ }
162
+ // Read currentTask from progress.json (for all phases)
163
+ const progressPath = path.join(changeDir, 'metrics', 'progress.json');
164
+ const legacyProgressPath = path.join(changeDir, 'progress.json');
165
+ const effectiveProgressPath = (await FileSystemUtils.fileExists(progressPath))
166
+ ? progressPath
167
+ : legacyProgressPath;
168
+ if (await FileSystemUtils.fileExists(effectiveProgressPath)) {
169
+ try {
170
+ const progressContent = await FileSystemUtils.readFile(effectiveProgressPath);
171
+ const progress = JSON.parse(progressContent);
172
+ currentTask = progress.currentTask || '';
173
+ }
174
+ catch {
175
+ // Ignore parse errors
176
+ }
177
+ }
178
+ // Found active change, exit loop
179
+ if (phase !== 'idle')
180
+ break;
181
+ }
182
+ }
183
+ // Initialize metrics folder for propose/techDesign phase
184
+ if ((phase === 'propose' || phase === 'techDesign') && changeId) {
185
+ try {
186
+ await initializeProgress(changeId, phase);
187
+ }
188
+ catch {
189
+ // Ignore initialization errors
190
+ }
191
+ }
192
+ // Build system message
193
+ let systemMessage = '';
194
+ if (projectContext) {
195
+ systemMessage += `✓ Loaded project.md (${projectContext.length} chars)\n`;
196
+ }
197
+ if (knowledgeFiles.length > 0) {
198
+ systemMessage += `✓ Loaded ${knowledgeFiles.length} knowledge files\n`;
199
+ }
200
+ if (rulesFiles.length > 0) {
201
+ systemMessage += `✓ Found ${rulesFiles.length} rules files in .claude/rules\n`;
202
+ }
203
+ if (activeChanges.length > 0) {
204
+ systemMessage += `✓ Found ${activeChanges.length} active changes\n`;
205
+ if (phase !== 'idle') {
206
+ systemMessage += `✓ Current phase: ${phase}`;
207
+ if (phase === 'propose') {
208
+ systemMessage += ` (auto-trigger-apply-after-approval)`;
209
+ }
210
+ else if (phase === 'techDesign') {
211
+ systemMessage += ` (run /zhuanspec:proposal to continue)`;
212
+ }
213
+ systemMessage += `\n`;
214
+ }
215
+ }
216
+ // Build additional context for Claude
217
+ let additionalContext = '';
218
+ if (projectContext.length > 0) {
219
+ additionalContext += `## Project Context (from project.md)\n${projectContext.substring(0, 2000)}\n\n`;
220
+ }
221
+ if (knowledgeFiles.length > 0) {
222
+ additionalContext += `## Knowledge Files\n${knowledgeFiles.join('\n\n')}\n\n`;
223
+ }
224
+ if (rulesFiles.length > 0) {
225
+ additionalContext += `## Rules Files Detected\n`;
226
+ additionalContext += `Found ${rulesFiles.length} rule files in .claude/rules: ${rulesFiles.join(', ')}\n`;
227
+ additionalContext += `(These are loaded automatically by Claude Code system)\n\n`;
228
+ }
229
+ return {
230
+ continue: true,
231
+ systemMessage: systemMessage || '✓ ZhuanSpec session initialized',
232
+ hookSpecificOutput: {
233
+ additionalContext: additionalContext || undefined,
234
+ env: {
235
+ ZHUANSPEC_PHASE: phase,
236
+ ZHUANSPEC_CHANGE_ID: changeId,
237
+ ZHUANSPEC_CURRENT_TASK: currentTask,
238
+ ZHUANSPEC_CURRENT_NODE: phase,
239
+ ZHUANSPEC_RESUME_DEVIATION_CHECK: phase === 'apply' ? 'true' : 'false',
240
+ },
241
+ },
242
+ };
243
+ }
244
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Notification Hook - Push Milestone Notifications
3
+ *
4
+ * This hook runs on Notification events to push milestone messages:
5
+ * 1. Phase transitions (e.g., propose → apply)
6
+ * 2. Wave completions
7
+ * 3. All tasks completion
8
+ */
9
+ interface NotifyMilestoneOptions {
10
+ json?: boolean;
11
+ }
12
+ export declare function notifyMilestoneHook(options: NotifyMilestoneOptions): Promise<void>;
13
+ export {};
14
+ //# sourceMappingURL=notify-milestone.d.ts.map
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Notification Hook - Push Milestone Notifications
3
+ *
4
+ * This hook runs on Notification events to push milestone messages:
5
+ * 1. Phase transitions (e.g., propose → apply)
6
+ * 2. Wave completions
7
+ * 3. All tasks completion
8
+ */
9
+ import path from 'path';
10
+ import { FileSystemUtils } from '../../utils/file-system.js';
11
+ export async function notifyMilestoneHook(options) {
12
+ const output = await runNotifyMilestone();
13
+ if (options.json) {
14
+ console.log(JSON.stringify(output));
15
+ }
16
+ else {
17
+ if (output.systemMessage) {
18
+ console.log(output.systemMessage);
19
+ }
20
+ }
21
+ }
22
+ async function runNotifyMilestone() {
23
+ const phase = process.env.ZHUANSPEC_PHASE || 'idle';
24
+ const changeId = process.env.ZHUANSPEC_CHANGE_ID || '';
25
+ // No active change → nothing to notify
26
+ if (!changeId || phase === 'idle') {
27
+ return {
28
+ continue: true,
29
+ };
30
+ }
31
+ const cwd = process.cwd();
32
+ const zhuanspecDir = path.join(cwd, 'zhuanspec');
33
+ const changeDir = path.join(zhuanspecDir, 'changes', changeId);
34
+ const metricsDir = path.join(changeDir, 'metrics');
35
+ // Read progress.json for phase transition info
36
+ const progressPath = path.join(metricsDir, 'progress.json');
37
+ let progress = null;
38
+ if (await FileSystemUtils.fileExists(progressPath)) {
39
+ try {
40
+ progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
41
+ }
42
+ catch {
43
+ // Ignore parse errors
44
+ }
45
+ }
46
+ // Read tasks.md for wave status
47
+ const tasksPath = path.join(changeDir, 'tasks.md');
48
+ let waves = [];
49
+ if (await FileSystemUtils.fileExists(tasksPath)) {
50
+ const tasksContent = await FileSystemUtils.readFile(tasksPath);
51
+ const tasks = parseTasks(tasksContent);
52
+ waves = computeWaves(tasks);
53
+ }
54
+ // Check for milestones
55
+ // 1. Phase transition milestone
56
+ if (progress?.phaseTransitions && progress.phaseTransitions.length > 0) {
57
+ const latestTransition = progress.phaseTransitions[progress.phaseTransitions.length - 1];
58
+ const transitionAge = Date.now() - new Date(latestTransition.timestamp).getTime();
59
+ // Only notify if transition happened within last 60 seconds
60
+ if (transitionAge < 60000) {
61
+ return {
62
+ continue: true,
63
+ systemMessage: `🎯 里程碑: 阶段切换\n从: ${latestTransition.from} → 到: ${latestTransition.to}\n变更: ${changeId}`,
64
+ hookSpecificOutput: {
65
+ milestoneType: 'phase_transition',
66
+ milestoneData: {
67
+ fromPhase: latestTransition.from,
68
+ toPhase: latestTransition.to,
69
+ changeId,
70
+ },
71
+ },
72
+ };
73
+ }
74
+ }
75
+ // 2. Wave completion milestone
76
+ for (const wave of waves) {
77
+ if (wave.status === 'completed') {
78
+ // Check if this wave was just completed (by checking if next wave is in_progress)
79
+ const nextWave = waves.find(w => w.waveId === wave.waveId + 1);
80
+ if (nextWave && nextWave.status === 'in_progress') {
81
+ const completedTaskIds = wave.completedTasks.map(t => t.taskId).join(', ');
82
+ return {
83
+ continue: true,
84
+ systemMessage: `🎯 里程碑: Wave ${wave.waveId} 完成\n已完成任务: ${completedTaskIds}\n下一个 Wave: Wave ${nextWave.waveId}`,
85
+ hookSpecificOutput: {
86
+ milestoneType: 'wave_completion',
87
+ milestoneData: {
88
+ waveId: wave.waveId,
89
+ },
90
+ },
91
+ };
92
+ }
93
+ }
94
+ }
95
+ // 3. All tasks completion milestone
96
+ const allCompleted = waves.length > 0 && waves.every(w => w.status === 'completed');
97
+ if (allCompleted) {
98
+ return {
99
+ continue: true,
100
+ systemMessage: `🎯 里程碑: 全部任务完成\n变更: ${changeId}\n状态: Ready for Review`,
101
+ hookSpecificOutput: {
102
+ milestoneType: 'all_tasks_complete',
103
+ milestoneData: {
104
+ changeId,
105
+ },
106
+ },
107
+ };
108
+ }
109
+ // No milestone to report
110
+ return {
111
+ continue: true,
112
+ };
113
+ }
114
+ function parseTasks(content) {
115
+ const tasks = [];
116
+ const lines = content.split('\n');
117
+ let currentWave = 0;
118
+ for (const line of lines) {
119
+ // Match wave headers: ### Wave 1, ### Wave 2, etc.
120
+ const waveMatch = line.match(/^###\s*Wave\s*(\d+)/i);
121
+ if (waveMatch) {
122
+ currentWave = parseInt(waveMatch[1], 10);
123
+ continue;
124
+ }
125
+ // Match task lines: - [ ] X.Y description or - [x] X.Y description
126
+ const taskMatch = line.match(/^-\s*\[([x ])\]\s*(\d+\.\d+)\s*(.+)/);
127
+ if (taskMatch) {
128
+ const statusChar = taskMatch[1];
129
+ const taskId = taskMatch[2];
130
+ tasks.push({
131
+ taskId,
132
+ status: statusChar === 'x' ? 'completed' : 'pending',
133
+ wave: currentWave,
134
+ });
135
+ }
136
+ }
137
+ return tasks;
138
+ }
139
+ function computeWaves(tasks) {
140
+ const wavesMap = new Map();
141
+ for (const task of tasks) {
142
+ if (!wavesMap.has(task.wave)) {
143
+ wavesMap.set(task.wave, {
144
+ waveId: task.wave,
145
+ tasks: [],
146
+ completedTasks: [],
147
+ status: 'pending',
148
+ });
149
+ }
150
+ const wave = wavesMap.get(task.wave);
151
+ wave.tasks.push(task);
152
+ if (task.status === 'completed') {
153
+ wave.completedTasks.push(task);
154
+ }
155
+ }
156
+ // Determine wave status
157
+ for (const [, wave] of wavesMap) {
158
+ if (wave.completedTasks.length === wave.tasks.length) {
159
+ wave.status = 'completed';
160
+ }
161
+ else if (wave.completedTasks.length > 0) {
162
+ wave.status = 'in_progress';
163
+ }
164
+ else {
165
+ wave.status = 'pending';
166
+ }
167
+ }
168
+ return Array.from(wavesMap.values()).sort((a, b) => a.waveId - b.waveId);
169
+ }
170
+ //# sourceMappingURL=notify-milestone.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Post-Apply Hook
3
+ *
4
+ * Actions after Apply phase completes:
5
+ * 1. Reverse Sync deviation detection
6
+ * 2. Auto-trigger Review if no deviation
7
+ * 3. Update progress.json
8
+ */
9
+ interface PostApplyOptions {
10
+ json?: boolean;
11
+ change?: string;
12
+ }
13
+ interface HookOutput {
14
+ continue: boolean;
15
+ systemMessage?: string;
16
+ hookSpecificOutput?: {
17
+ reviewTriggered?: boolean;
18
+ deviationDetected?: boolean;
19
+ deviationFiles?: string[];
20
+ };
21
+ }
22
+ export declare function postApplyHook(options: PostApplyOptions): Promise<void>;
23
+ /**
24
+ * Execute post-apply actions: deviation detection and review triggering.
25
+ * This function is exported for use by other hooks (e.g., summarize).
26
+ */
27
+ export declare function executePostApply(changeDir: string, repoRoot: string): Promise<HookOutput>;
28
+ export {};
29
+ //# sourceMappingURL=post-apply.d.ts.map
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Post-Apply Hook
3
+ *
4
+ * Actions after Apply phase completes:
5
+ * 1. Reverse Sync deviation detection
6
+ * 2. Auto-trigger Review if no deviation
7
+ * 3. Update progress.json
8
+ */
9
+ import path from 'path';
10
+ import { execSync } from 'child_process';
11
+ import { FileSystemUtils } from '../../utils/file-system.js';
12
+ import { getBeijingTime } from './record-progress.js';
13
+ export async function postApplyHook(options) {
14
+ const output = await runPostApplyHook(options.change || '');
15
+ if (options.json) {
16
+ console.log(JSON.stringify(output));
17
+ }
18
+ else if (output.systemMessage) {
19
+ console.log(output.systemMessage);
20
+ }
21
+ }
22
+ async function runPostApplyHook(changeId) {
23
+ if (!changeId) {
24
+ return {
25
+ continue: true,
26
+ systemMessage: 'Post-apply hook skipped: no change provided',
27
+ };
28
+ }
29
+ const repoRoot = process.cwd();
30
+ const changeDir = path.join(repoRoot, 'zhuanspec', 'changes', changeId);
31
+ return await executePostApply(changeDir, repoRoot);
32
+ }
33
+ /**
34
+ * Execute post-apply actions: deviation detection and review triggering.
35
+ * This function is exported for use by other hooks (e.g., summarize).
36
+ */
37
+ export async function executePostApply(changeDir, repoRoot) {
38
+ // 1. Reverse Sync deviation detection
39
+ const deviationResult = await detectDeviation(changeDir, repoRoot);
40
+ // 2. Auto-trigger Review if no deviation
41
+ if (!deviationResult.detected) {
42
+ try {
43
+ console.log('Post-apply: Triggering automatic review...');
44
+ execSync(`zhuanspec review`, {
45
+ cwd: repoRoot,
46
+ encoding: 'utf-8',
47
+ stdio: 'inherit',
48
+ });
49
+ // 3. Update progress.json
50
+ await updateProgressJson(changeDir, 'review_triggered');
51
+ return {
52
+ continue: true,
53
+ systemMessage: 'Post-apply: Review triggered automatically',
54
+ hookSpecificOutput: {
55
+ reviewTriggered: true,
56
+ deviationDetected: false,
57
+ },
58
+ };
59
+ }
60
+ catch (error) {
61
+ return {
62
+ continue: true,
63
+ systemMessage: 'Post-apply: Review trigger failed (manual review required)',
64
+ hookSpecificOutput: {
65
+ reviewTriggered: false,
66
+ deviationDetected: false,
67
+ },
68
+ };
69
+ }
70
+ }
71
+ // Deviation detected - skip review
72
+ return {
73
+ continue: true,
74
+ systemMessage: `Post-apply: Deviation detected (${deviationResult.files?.length || 0} files outside scope), review skipped`,
75
+ hookSpecificOutput: {
76
+ reviewTriggered: false,
77
+ deviationDetected: true,
78
+ deviationFiles: deviationResult.files,
79
+ },
80
+ };
81
+ }
82
+ /**
83
+ * Detect deviation by comparing proposal scope with actual git diff
84
+ */
85
+ async function detectDeviation(changeDir, repoRoot) {
86
+ // Parse estimated files from proposal
87
+ const proposalPath = path.join(changeDir, 'proposal.md');
88
+ let estimatedFiles = [];
89
+ try {
90
+ const proposalContent = await FileSystemUtils.readFile(proposalPath);
91
+ // Extract affected files from "影响范围" section
92
+ const impactMatch = proposalContent.match(/##\s+(影响范围|Impact)[\s\S]*?(?=##|$)/i);
93
+ if (impactMatch) {
94
+ const impactSection = impactMatch[0];
95
+ // Parse file references
96
+ const fileMatches = impactSection.matchAll(/[`']([^`']+\.(?:java|ts|js|py|go|rs|xml|yaml|json|md))[`']/gi);
97
+ estimatedFiles = Array.from(fileMatches, m => m[1]);
98
+ }
99
+ }
100
+ catch {
101
+ // Cannot read proposal
102
+ }
103
+ // Get actual changed files
104
+ let actualFiles = [];
105
+ try {
106
+ const output = execSync('git diff --name-only HEAD~1 HEAD', {
107
+ cwd: repoRoot,
108
+ encoding: 'utf-8',
109
+ stdio: ['pipe', 'pipe', 'ignore'],
110
+ });
111
+ actualFiles = output.trim().split('\n').filter(Boolean);
112
+ }
113
+ catch {
114
+ // Try git status for uncommitted changes
115
+ try {
116
+ const statusOutput = execSync('git status --porcelain', {
117
+ cwd: repoRoot,
118
+ encoding: 'utf-8',
119
+ stdio: ['pipe', 'pipe', 'ignore'],
120
+ });
121
+ actualFiles = statusOutput
122
+ .trim()
123
+ .split('\n')
124
+ .map(line => line.replace(/^\s*[A-Z?]+\s+/, '').trim())
125
+ .filter(Boolean);
126
+ }
127
+ catch {
128
+ // No git info available
129
+ return { detected: false };
130
+ }
131
+ }
132
+ // Find files outside proposal scope (excluding zhuanspec internal files)
133
+ const outOfScopeFiles = [];
134
+ for (const file of actualFiles) {
135
+ if (file.includes('zhuanspec/'))
136
+ continue; // Allow internal changes
137
+ const inScope = estimatedFiles.some(estimated => file.includes(estimated) || estimated.includes(file));
138
+ if (!inScope && estimatedFiles.length > 0) {
139
+ outOfScopeFiles.push(file);
140
+ }
141
+ }
142
+ return {
143
+ detected: outOfScopeFiles.length > 0,
144
+ files: outOfScopeFiles,
145
+ };
146
+ }
147
+ /**
148
+ * Update progress.json to track phase transitions
149
+ */
150
+ async function updateProgressJson(changeDir, event) {
151
+ const metricsDir = path.join(changeDir, 'metrics');
152
+ const progressPath = path.join(metricsDir, 'progress.json');
153
+ await FileSystemUtils.createDirectory(metricsDir);
154
+ let progress = {};
155
+ try {
156
+ if (await FileSystemUtils.fileExists(progressPath)) {
157
+ const content = await FileSystemUtils.readFile(progressPath);
158
+ progress = JSON.parse(content);
159
+ }
160
+ }
161
+ catch {
162
+ progress = {};
163
+ }
164
+ progress.lastEvent = event;
165
+ progress.lastUpdatedAt = getBeijingTime();
166
+ progress.events = progress.events || [];
167
+ progress.events.push({
168
+ event,
169
+ timestamp: getBeijingTime(),
170
+ });
171
+ await FileSystemUtils.writeFile(progressPath, JSON.stringify(progress, null, 2));
172
+ }
173
+ //# sourceMappingURL=post-apply.js.map
@@ -0,0 +1,7 @@
1
+ interface PostArchiveOptions {
2
+ json?: boolean;
3
+ archived?: string;
4
+ }
5
+ export declare function postArchiveHook(options: PostArchiveOptions): Promise<void>;
6
+ export {};
7
+ //# sourceMappingURL=post-archive.d.ts.map