@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,494 @@
1
+ /**
2
+ * PostToolUse Hook - Record Progress
3
+ *
4
+ * This hook runs after Write/Edit operations to:
5
+ * 1. Record the modification to progress.json
6
+ * 2. Update tasks.md status if applicable
7
+ * 3. Track tool calls for metrics
8
+ */
9
+ import path from 'path';
10
+ import { promises as fsPromises } from 'fs';
11
+ import { FileSystemUtils } from '../../utils/file-system.js';
12
+ import { PHASE_ORDER } from '../../utils/phase-utils.js';
13
+ const fs = fsPromises;
14
+ /**
15
+ * Get Beijing time (UTC+8) as ISO string format
16
+ * Returns format: "2026-04-19T18:35:22.934+08:00"
17
+ */
18
+ export function getBeijingTime() {
19
+ const now = new Date();
20
+ // Beijing time is UTC+8
21
+ const beijingOffset = 8 * 60; // 8 hours in minutes
22
+ const beijingTime = new Date(now.getTime() + beijingOffset * 60 * 1000 - now.getTimezoneOffset() * 60 * 1000);
23
+ // Format as ISO string with +08:00 timezone
24
+ const year = beijingTime.getFullYear();
25
+ const month = String(beijingTime.getMonth() + 1).padStart(2, '0');
26
+ const day = String(beijingTime.getDate()).padStart(2, '0');
27
+ const hours = String(beijingTime.getHours()).padStart(2, '0');
28
+ const minutes = String(beijingTime.getMinutes()).padStart(2, '0');
29
+ const seconds = String(beijingTime.getSeconds()).padStart(2, '0');
30
+ const ms = String(beijingTime.getMilliseconds()).padStart(3, '0');
31
+ return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${ms}+08:00`;
32
+ }
33
+ /**
34
+ * Get Beijing time date string for session ID and filenames
35
+ * Returns format: "2026-04-19T18-35-22"
36
+ */
37
+ export function getBeijingTimeForFilename() {
38
+ const now = new Date();
39
+ const beijingOffset = 8 * 60;
40
+ const beijingTime = new Date(now.getTime() + beijingOffset * 60 * 1000 - now.getTimezoneOffset() * 60 * 1000);
41
+ const year = beijingTime.getFullYear();
42
+ const month = String(beijingTime.getMonth() + 1).padStart(2, '0');
43
+ const day = String(beijingTime.getDate()).padStart(2, '0');
44
+ const hours = String(beijingTime.getHours()).padStart(2, '0');
45
+ const minutes = String(beijingTime.getMinutes()).padStart(2, '0');
46
+ const seconds = String(beijingTime.getSeconds()).padStart(2, '0');
47
+ return `${year}-${month}-${day}T${hours}-${minutes}-${seconds}`;
48
+ }
49
+ export async function recordProgressHook(options) {
50
+ // Read stdin for Claude Code hook input
51
+ let stdinData = {};
52
+ try {
53
+ const stdinContent = await readStdin();
54
+ if (stdinContent) {
55
+ stdinData = JSON.parse(stdinContent);
56
+ }
57
+ }
58
+ catch {
59
+ // Ignore parse errors
60
+ }
61
+ // Get file path from stdin or options
62
+ const filePath = stdinData.tool_input?.file_path
63
+ || options.file
64
+ || '';
65
+ const toolName = stdinData.tool_name || options.tool || 'Write';
66
+ const success = stdinData.tool_result?.success !== false
67
+ && options.success !== 'false';
68
+ const output = await runRecordProgress(filePath, toolName, success, stdinData);
69
+ if (options.json) {
70
+ console.log(JSON.stringify(output));
71
+ }
72
+ else {
73
+ if (output.systemMessage) {
74
+ console.log(output.systemMessage);
75
+ }
76
+ }
77
+ }
78
+ async function readStdin() {
79
+ return new Promise((resolve) => {
80
+ let data = '';
81
+ process.stdin.setEncoding('utf8');
82
+ process.stdin.on('data', chunk => data += chunk);
83
+ process.stdin.on('end', () => resolve(data));
84
+ process.stdin.on('error', () => resolve(null));
85
+ });
86
+ }
87
+ // Extract MCP server name from tool name (e.g., mcp__aicodegather__beforeEditFile)
88
+ function extractMcpServer(toolName) {
89
+ if (!toolName.startsWith('mcp__'))
90
+ return undefined;
91
+ const parts = toolName.split('__');
92
+ return parts.length >= 2 ? parts[1] : undefined;
93
+ }
94
+ // Extract MCP tool name from tool name
95
+ function extractMcpTool(toolName) {
96
+ if (!toolName.startsWith('mcp__'))
97
+ return undefined;
98
+ const parts = toolName.split('__');
99
+ return parts.length >= 3 ? parts[2] : undefined;
100
+ }
101
+ /**
102
+ * Detect active change from progress.json files
103
+ * Scans changes directory metrics progress.json to find non-idle phase
104
+ * Returns null if no active change found
105
+ */
106
+ async function detectActiveChange(cwd) {
107
+ const changesDir = path.join(cwd, 'zhuanspec', 'changes');
108
+ if (!await FileSystemUtils.directoryExists(changesDir)) {
109
+ return null;
110
+ }
111
+ const entries = await fs.readdir(changesDir, { withFileTypes: true });
112
+ for (const entry of entries) {
113
+ if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'archive')
114
+ continue;
115
+ const progressPath = path.join(changesDir, entry.name, 'metrics', 'progress.json');
116
+ if (await FileSystemUtils.fileExists(progressPath)) {
117
+ try {
118
+ const progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
119
+ if (progress.phase && progress.phase !== 'idle' && PHASE_ORDER.includes(progress.phase)) {
120
+ return { changeId: entry.name, phase: progress.phase };
121
+ }
122
+ }
123
+ catch {
124
+ // Ignore parse errors
125
+ }
126
+ }
127
+ }
128
+ return null;
129
+ }
130
+ async function runRecordProgress(filePath, toolName, success, stdinData) {
131
+ const cwd = process.cwd();
132
+ // Priority: progress.json > environment variables
133
+ const activeChange = await detectActiveChange(cwd);
134
+ const phase = activeChange?.phase || process.env.ZHUANSPEC_PHASE || 'idle';
135
+ const changeId = activeChange?.changeId || process.env.ZHUANSPEC_CHANGE_ID || '';
136
+ const currentTask = process.env.ZHUANSPEC_CURRENT_TASK || '';
137
+ // No active change → nothing to record
138
+ if (!changeId || phase === 'idle') {
139
+ return {
140
+ continue: true,
141
+ };
142
+ }
143
+ const zhuanspecDir = path.join(cwd, 'zhuanspec');
144
+ const changeDir = path.join(zhuanspecDir, 'changes', changeId);
145
+ const metricsDir = path.join(changeDir, 'metrics');
146
+ // Ensure metrics directory exists
147
+ await FileSystemUtils.createDirectory(metricsDir);
148
+ const progressPath = path.join(metricsDir, 'progress.json');
149
+ const toolCallsPath = path.join(metricsDir, 'tool_calls.json');
150
+ // Load existing progress or create new
151
+ let progress;
152
+ if (await FileSystemUtils.fileExists(progressPath)) {
153
+ try {
154
+ progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
155
+ progress.phase = progress.phase || phase;
156
+ progress.currentNode = progress.currentNode || phase;
157
+ progress.completedTasks = progress.completedTasks || [];
158
+ progress.toolCalls = progress.toolCalls || [];
159
+ progress.skillCalls = progress.skillCalls || [];
160
+ progress.hookTriggers = progress.hookTriggers || [];
161
+ progress.clarifications = progress.clarifications || [];
162
+ progress.filesModified = progress.filesModified || [];
163
+ progress.linesAdded = progress.linesAdded || 0;
164
+ progress.linesRemoved = progress.linesRemoved || 0;
165
+ progress.deviationCount = progress.deviationCount || 0;
166
+ progress.deviationRecords = progress.deviationRecords || [];
167
+ progress.reviewStats = progress.reviewStats || {
168
+ loopCount: 0,
169
+ criticalFixes: 0,
170
+ testFixes: 0,
171
+ consistencyFixes: 0,
172
+ };
173
+ progress.phaseTransitions = progress.phaseTransitions || [];
174
+ progress.stats = progress.stats || {
175
+ tokenUsageTotal: 0,
176
+ contextLoad: 0,
177
+ durationMs: { propose: 0, apply: 0, review: 0, archive: 0 },
178
+ };
179
+ progress.stats.durationMs = progress.stats.durationMs || {
180
+ propose: 0,
181
+ apply: 0,
182
+ review: 0,
183
+ archive: 0,
184
+ };
185
+ }
186
+ catch {
187
+ progress = createNewProgress(changeId);
188
+ }
189
+ }
190
+ else {
191
+ progress = createNewProgress(changeId);
192
+ }
193
+ // Record tool call with extended tool support
194
+ const toolInput = stdinData.tool_input || {};
195
+ const callId = `call-${Date.now()}`;
196
+ const timestamp = getBeijingTime();
197
+ // Detect and record Skill calls separately
198
+ if (toolName === 'Skill') {
199
+ const skillCall = {
200
+ callId,
201
+ skill: typeof toolInput.skill === 'string' ? toolInput.skill : '',
202
+ args: typeof toolInput.args === 'string' ? toolInput.args : undefined,
203
+ triggeredAt: timestamp,
204
+ phase,
205
+ success,
206
+ };
207
+ progress.skillCalls.push(skillCall);
208
+ }
209
+ // Detect and record AskQuestion calls as clarifications
210
+ if (toolName === 'AskUserQuestion') {
211
+ const questionsData = toolInput.questions || [];
212
+ const answersData = stdinData.tool_result || {};
213
+ const clarification = {
214
+ clarificationId: callId,
215
+ questions: questionsData.map(q => ({
216
+ question: typeof q.question === 'string' ? q.question : '',
217
+ header: typeof q.header === 'string' ? q.header : undefined,
218
+ options: (q.options || []).map(opt => ({
219
+ label: typeof opt.label === 'string' ? opt.label : '',
220
+ description: typeof opt.description === 'string' ? opt.description : undefined,
221
+ })),
222
+ multiSelect: typeof q.multiSelect === 'boolean' ? q.multiSelect : false,
223
+ })),
224
+ answers: answersData.answers || {},
225
+ annotations: answersData.annotations || undefined,
226
+ askedAt: timestamp,
227
+ answeredAt: timestamp, // Assuming answered immediately in hook context
228
+ phase,
229
+ };
230
+ progress.clarifications.push(clarification);
231
+ }
232
+ // Standard tool call record
233
+ const toolCall = {
234
+ callId,
235
+ tool: toolName,
236
+ input: {
237
+ file_path: filePath,
238
+ content: typeof toolInput.content === 'string' ? toolInput.content : undefined,
239
+ // Extended inputs
240
+ pattern: typeof toolInput.pattern === 'string' ? toolInput.pattern : undefined,
241
+ path: typeof toolInput.path === 'string' ? toolInput.path : undefined,
242
+ command: typeof toolInput.command === 'string' ? toolInput.command : undefined,
243
+ skill: typeof toolInput.skill === 'string' ? toolInput.skill : undefined,
244
+ args: typeof toolInput.args === 'string' ? toolInput.args : undefined,
245
+ agent_type: typeof toolInput.subagent_type === 'string' ? toolInput.subagent_type : undefined,
246
+ mcp_server: extractMcpServer(toolName),
247
+ mcp_tool: extractMcpTool(toolName),
248
+ },
249
+ output: {
250
+ success,
251
+ },
252
+ timestamp,
253
+ phase,
254
+ taskId: currentTask,
255
+ };
256
+ // Update progress
257
+ progress.lastUpdatedAt = getBeijingTime();
258
+ // Record phase transition if phase changed
259
+ const previousPhase = progress.phase;
260
+ progress.phase = phase;
261
+ if (previousPhase !== phase) {
262
+ progress.phaseTransitions.push({
263
+ from: previousPhase,
264
+ to: phase,
265
+ timestamp: getBeijingTime(),
266
+ triggeredBy: currentTask || 'manual',
267
+ });
268
+ }
269
+ progress.currentNode = process.env.ZHUANSPEC_CURRENT_NODE || phase;
270
+ progress.currentTask = currentTask || progress.currentTask;
271
+ progress.toolCalls.push(toolCall);
272
+ if (filePath && !progress.filesModified.includes(filePath)) {
273
+ progress.filesModified.push(filePath);
274
+ }
275
+ // Try to estimate lines from content
276
+ const content = stdinData.tool_input?.content;
277
+ if (content) {
278
+ const lines = content.split('\n').length;
279
+ if (toolName === 'Write') {
280
+ progress.linesAdded += lines;
281
+ }
282
+ else if (toolName === 'Edit') {
283
+ // For Edit, we can't easily track lines removed/added
284
+ progress.linesAdded += 1; // Estimate
285
+ }
286
+ }
287
+ const tokenUsageFromEnv = Number(process.env.ZHUANSPEC_TOKEN_USAGE || 0);
288
+ const contextLoadFromEnv = Number(process.env.ZHUANSPEC_CONTEXT_LOAD || 0);
289
+ const tokenUsageFromStdIn = Number(stdinData.tool_result?.tokenUsage || 0);
290
+ const contextLoadFromStdIn = Number(stdinData.tool_result?.contextLoad || 0);
291
+ progress.stats.tokenUsageTotal += tokenUsageFromEnv + tokenUsageFromStdIn;
292
+ progress.stats.contextLoad += contextLoadFromEnv + contextLoadFromStdIn;
293
+ const elapsedMs = Date.now() - new Date(progress.startedAt).getTime();
294
+ if (phase === 'propose')
295
+ progress.stats.durationMs.propose = elapsedMs;
296
+ if (phase === 'apply')
297
+ progress.stats.durationMs.apply = elapsedMs;
298
+ if (phase === 'review')
299
+ progress.stats.durationMs.review = elapsedMs;
300
+ if (phase === 'archive')
301
+ progress.stats.durationMs.archive = elapsedMs;
302
+ // Write progress file
303
+ await FileSystemUtils.writeFile(progressPath, JSON.stringify(progress, null, 2));
304
+ // Also update tool_calls.json separately for detailed tracking
305
+ let toolCallsLog;
306
+ if (await FileSystemUtils.fileExists(toolCallsPath)) {
307
+ try {
308
+ toolCallsLog = JSON.parse(await FileSystemUtils.readFile(toolCallsPath));
309
+ toolCallsLog.toolCalls.push(toolCall);
310
+ }
311
+ catch {
312
+ toolCallsLog = {
313
+ changeId,
314
+ sessionId: progress.sessionId,
315
+ toolCalls: [toolCall],
316
+ };
317
+ }
318
+ }
319
+ else {
320
+ toolCallsLog = {
321
+ changeId,
322
+ sessionId: progress.sessionId,
323
+ toolCalls: [toolCall],
324
+ };
325
+ }
326
+ await FileSystemUtils.writeFile(toolCallsPath, JSON.stringify(toolCallsLog, null, 2));
327
+ // Check if this completes the current task
328
+ // (simplified: if file was modified and we're in apply phase, mark task as potentially complete)
329
+ let taskMarked = '';
330
+ if (phase === 'apply' && currentTask && filePath) {
331
+ const tasksPath = path.join(changeDir, 'tasks.md');
332
+ if (await FileSystemUtils.fileExists(tasksPath)) {
333
+ const tasksContent = await FileSystemUtils.readFile(tasksPath);
334
+ // Check if the task line already has [x] or needs manual confirmation
335
+ const taskPattern = new RegExp(`-\\s*\\[[x ]\\]\\s*${currentTask.replace('.', '\\.')}`);
336
+ const taskMatch = tasksContent.match(taskPattern);
337
+ if (taskMatch && !taskMatch[0].includes('[x]')) {
338
+ // Task is pending - we don't auto-mark it here, but note it for Stop hook
339
+ taskMarked = currentTask;
340
+ }
341
+ }
342
+ }
343
+ // Calculate progress percentage and visual bar
344
+ const totalTasks = progress.totalTasks || 0;
345
+ const completedTasks = progress.completedTasks.length;
346
+ const percentage = totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0;
347
+ const progressBar = generateProgressBar(percentage, 12);
348
+ const progressDisplay = totalTasks > 0
349
+ ? `${progressBar} ${percentage.toFixed(1)}% (${completedTasks}/${totalTasks})`
350
+ : '进度: N/A (任务数待定)';
351
+ return {
352
+ continue: true,
353
+ systemMessage: `📝 Progress recorded: ${toolName} ${filePath}
354
+ | Phase: ${phase} | Task: ${currentTask || 'N/A'}
355
+ | 进度: ${progressDisplay}`,
356
+ hookSpecificOutput: {
357
+ progressUpdated: true,
358
+ taskMarked,
359
+ },
360
+ };
361
+ }
362
+ /**
363
+ * Generate visual progress bar
364
+ * @param percentage - Progress percentage (0-100)
365
+ * @param blocks - Number of blocks in the bar
366
+ */
367
+ function generateProgressBar(percentage, blocks) {
368
+ const filled = Math.round((percentage / 100) * blocks);
369
+ const empty = blocks - filled;
370
+ return '█'.repeat(filled) + '░'.repeat(empty);
371
+ }
372
+ function createNewProgress(changeId) {
373
+ const sessionId = `sess-${getBeijingTimeForFilename()}`;
374
+ return {
375
+ changeId,
376
+ sessionId,
377
+ startedAt: getBeijingTime(),
378
+ lastUpdatedAt: getBeijingTime(),
379
+ phase: process.env.ZHUANSPEC_PHASE || 'idle',
380
+ currentNode: process.env.ZHUANSPEC_CURRENT_NODE || process.env.ZHUANSPEC_PHASE || 'idle',
381
+ currentTask: '',
382
+ completedTasks: [],
383
+ totalTasks: 0,
384
+ toolCalls: [],
385
+ skillCalls: [],
386
+ hookTriggers: [],
387
+ clarifications: [],
388
+ filesModified: [],
389
+ linesAdded: 0,
390
+ linesRemoved: 0,
391
+ deviationCount: 0,
392
+ deviationRecords: [],
393
+ reviewStats: {
394
+ loopCount: 0,
395
+ criticalFixes: 0,
396
+ testFixes: 0,
397
+ consistencyFixes: 0,
398
+ },
399
+ phaseTransitions: [],
400
+ stats: {
401
+ tokenUsageTotal: 0,
402
+ contextLoad: 0,
403
+ durationMs: {
404
+ propose: 0,
405
+ apply: 0,
406
+ review: 0,
407
+ archive: 0,
408
+ },
409
+ },
410
+ };
411
+ }
412
+ /**
413
+ * Initialize progress.json at phase start
414
+ * This function should be called when a change is created, before any files are written.
415
+ * @param changeId - The change ID
416
+ * @param initialPhase - The initial phase to set (default: 'propose')
417
+ */
418
+ export async function initializeProgress(changeId, initialPhase = 'propose') {
419
+ const cwd = process.cwd();
420
+ const zhuanspecDir = path.join(cwd, 'zhuanspec');
421
+ const changeDir = path.join(zhuanspecDir, 'changes', changeId);
422
+ const metricsDir = path.join(changeDir, 'metrics');
423
+ // Ensure metrics directory exists
424
+ await FileSystemUtils.createDirectory(metricsDir);
425
+ const progressPath = path.join(metricsDir, 'progress.json');
426
+ // Create or update progress file
427
+ let progress;
428
+ if (await FileSystemUtils.fileExists(progressPath)) {
429
+ try {
430
+ progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
431
+ // Update phase (support phase transitions)
432
+ progress.phase = initialPhase;
433
+ progress.currentNode = initialPhase;
434
+ }
435
+ catch {
436
+ progress = createNewProgress(changeId);
437
+ progress.phase = initialPhase;
438
+ progress.currentNode = initialPhase;
439
+ }
440
+ }
441
+ else {
442
+ progress = createNewProgress(changeId);
443
+ progress.phase = initialPhase;
444
+ progress.currentNode = initialPhase;
445
+ }
446
+ progress.lastUpdatedAt = getBeijingTime();
447
+ await FileSystemUtils.writeFile(progressPath, JSON.stringify(progress, null, 2));
448
+ }
449
+ /**
450
+ * Record a hook trigger event
451
+ * This function can be called from other hooks to record their execution
452
+ * @param hookName - The name of the hook (e.g., 'init', 'deviation-check')
453
+ * @param hookPhase - The lifecycle phase (SessionStart, PreToolUse, PostToolUse, Stop)
454
+ * @param success - Whether the hook executed successfully
455
+ * @param options - Additional context (toolName, filePath, message, durationMs)
456
+ */
457
+ export async function recordHookTrigger(hookName, hookPhase, success, options) {
458
+ const cwd = process.cwd();
459
+ const activeChange = await detectActiveChange(cwd);
460
+ const phase = activeChange?.phase || process.env.ZHUANSPEC_PHASE || 'idle';
461
+ const changeId = activeChange?.changeId || process.env.ZHUANSPEC_CHANGE_ID || '';
462
+ if (!changeId || phase === 'idle')
463
+ return;
464
+ const zhuanspecDir = path.join(cwd, 'zhuanspec');
465
+ const changeDir = path.join(zhuanspecDir, 'changes', changeId);
466
+ const metricsDir = path.join(changeDir, 'metrics');
467
+ const progressPath = path.join(metricsDir, 'progress.json');
468
+ if (!await FileSystemUtils.fileExists(progressPath))
469
+ return;
470
+ try {
471
+ const progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
472
+ // Ensure hookTriggers array exists
473
+ progress.hookTriggers = progress.hookTriggers || [];
474
+ const hookRecord = {
475
+ hookId: `hook-${Date.now()}`,
476
+ hookName,
477
+ hookPhase,
478
+ triggeredAt: getBeijingTime(),
479
+ phase,
480
+ toolName: options?.toolName,
481
+ filePath: options?.filePath,
482
+ success,
483
+ durationMs: options?.durationMs,
484
+ message: options?.message,
485
+ };
486
+ progress.hookTriggers.push(hookRecord);
487
+ progress.lastUpdatedAt = getBeijingTime();
488
+ await FileSystemUtils.writeFile(progressPath, JSON.stringify(progress, null, 2));
489
+ }
490
+ catch {
491
+ // Ignore errors
492
+ }
493
+ }
494
+ //# sourceMappingURL=record-progress.js.map
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Review Hooks - Result Checkers for Review Phase
3
+ *
4
+ * Three independent hooks for Review phase (Phase 3):
5
+ * 1. code-review-result: Check code-review-expert skill output
6
+ * 2. unit-test-result: Check generate-mockito-unit-test-skill output
7
+ * 3. spec-consistency-result: Check spec-code-consistency output
8
+ *
9
+ * Each hook:
10
+ * - Reads skill output file
11
+ * - Extracts key metrics (Critical count, coverage, consistency)
12
+ * - Decides if fix loop is needed
13
+ * - Controls loop count (max 3)
14
+ */
15
+ declare const MAX_LOOP_COUNT = 3;
16
+ interface CodeReviewOutput {
17
+ criticalCount: number;
18
+ importantCount: number;
19
+ infoCount: number;
20
+ issues: Array<{
21
+ level: 'critical' | 'important' | 'info';
22
+ file: string;
23
+ line?: number;
24
+ message: string;
25
+ ruleId?: string;
26
+ }>;
27
+ sonarStatus?: 'pass' | 'fail' | 'skip';
28
+ loopCount?: number;
29
+ }
30
+ interface HookResult {
31
+ pass: boolean;
32
+ needFix: boolean;
33
+ loopCount: number;
34
+ metrics: Record<string, unknown>;
35
+ fixPrompt?: string;
36
+ nextAction?: 'continue' | 'fix' | 'stop';
37
+ }
38
+ export declare function codeReviewResultCheck(changeId: string, skillOutput?: CodeReviewOutput): Promise<HookResult>;
39
+ interface UnitTestOutput {
40
+ testPassed: boolean;
41
+ passRate: number;
42
+ coverage: number;
43
+ coverageThreshold: number;
44
+ newTestsGenerated: string[];
45
+ failedTests: string[];
46
+ loopCount?: number;
47
+ }
48
+ export declare function unitTestResultCheck(changeId: string, skillOutput?: UnitTestOutput): Promise<HookResult>;
49
+ interface SpecConsistencyOutput {
50
+ consistencyRate: number;
51
+ totalRequirements: number;
52
+ totalScenarios: number;
53
+ coveredScenarios: number;
54
+ uncoveredScenarios: Array<{
55
+ requirementId: string;
56
+ scenarioId: string;
57
+ description: string;
58
+ }>;
59
+ mapping: Array<{
60
+ requirementId: string;
61
+ scenarioId: string;
62
+ codeFile: string;
63
+ codeLine?: number;
64
+ status: 'covered' | 'partial' | 'missing';
65
+ }>;
66
+ loopCount?: number;
67
+ }
68
+ export declare function specConsistencyResultCheck(changeId: string, skillOutput?: SpecConsistencyOutput): Promise<HookResult>;
69
+ export declare function writeCodeReviewOutput(changeId: string, output: CodeReviewOutput): Promise<void>;
70
+ export declare function writeUnitTestOutput(changeId: string, output: UnitTestOutput): Promise<void>;
71
+ export declare function writeSpecConsistencyOutput(changeId: string, output: SpecConsistencyOutput): Promise<void>;
72
+ export { writeSkillOutput };
73
+ declare function writeSkillOutput(changeId: string, filename: string, data: unknown): Promise<void>;
74
+ export interface ReviewReport {
75
+ changeId: string;
76
+ timestamp: string;
77
+ codeReview: HookResult;
78
+ unitTest: HookResult;
79
+ specConsistency: HookResult;
80
+ overallPass: boolean;
81
+ canArchive: boolean;
82
+ }
83
+ export declare function generateReviewReport(changeId: string, results: {
84
+ codeReview: HookResult;
85
+ unitTest: HookResult;
86
+ specConsistency: HookResult;
87
+ }): Promise<string>;
88
+ export { CodeReviewOutput, UnitTestOutput, SpecConsistencyOutput, HookResult, MAX_LOOP_COUNT, };
89
+ //# sourceMappingURL=review-hooks.d.ts.map