@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.
- package/bin/zhuanspec-hook.js +3 -0
- package/dist/cli/hooks.d.ts +14 -0
- package/dist/cli/hooks.js +465 -0
- package/dist/cli/index.js +100 -0
- package/dist/commands/artifact-workflow.js +15 -34
- package/dist/commands/design.d.ts +42 -0
- package/dist/commands/design.js +337 -0
- package/dist/commands/progress.d.ts +32 -0
- package/dist/commands/progress.js +278 -0
- package/dist/commands/review.d.ts +32 -0
- package/dist/commands/review.js +472 -0
- package/dist/commands/validate.d.ts +14 -0
- package/dist/commands/validate.js +161 -10
- package/dist/core/archive.d.ts +1 -0
- package/dist/core/archive.js +45 -3
- package/dist/core/completions/command-registry.js +67 -0
- package/dist/core/configurators/slash/amazon-q.js +32 -2
- package/dist/core/configurators/slash/antigravity.js +8 -2
- package/dist/core/configurators/slash/auggie.js +16 -1
- package/dist/core/configurators/slash/base.js +1 -1
- package/dist/core/configurators/slash/claude.d.ts +4 -0
- package/dist/core/configurators/slash/claude.js +56 -1
- package/dist/core/configurators/slash/cline.js +8 -2
- package/dist/core/configurators/slash/codebuddy.js +22 -1
- package/dist/core/configurators/slash/codex.js +21 -0
- package/dist/core/configurators/slash/costrict.js +15 -0
- package/dist/core/configurators/slash/crush.js +22 -1
- package/dist/core/configurators/slash/cursor.js +22 -1
- package/dist/core/configurators/slash/factory.js +16 -1
- package/dist/core/configurators/slash/gemini.js +8 -2
- package/dist/core/configurators/slash/github-copilot.js +19 -1
- package/dist/core/configurators/slash/iflow.js +22 -1
- package/dist/core/configurators/slash/kilocode.js +4 -1
- package/dist/core/configurators/slash/opencode.js +27 -0
- package/dist/core/configurators/slash/qoder.d.ts +4 -0
- package/dist/core/configurators/slash/qoder.js +59 -1
- package/dist/core/configurators/slash/qwen.js +8 -2
- package/dist/core/configurators/slash/roocode.js +8 -2
- package/dist/core/configurators/slash/windsurf.js +8 -2
- package/dist/core/dashboard/metrics.d.ts +33 -0
- package/dist/core/dashboard/metrics.js +114 -0
- package/dist/core/hooks/collect-knowledge.d.ts +16 -0
- package/dist/core/hooks/collect-knowledge.js +203 -0
- package/dist/core/hooks/context-load-hook.d.ts +25 -0
- package/dist/core/hooks/context-load-hook.js +159 -0
- package/dist/core/hooks/deviation-check.d.ts +27 -0
- package/dist/core/hooks/deviation-check.js +403 -0
- package/dist/core/hooks/deviation-handler.d.ts +43 -0
- package/dist/core/hooks/deviation-handler.js +98 -0
- package/dist/core/hooks/init.d.ts +14 -0
- package/dist/core/hooks/init.js +244 -0
- package/dist/core/hooks/notify-milestone.d.ts +14 -0
- package/dist/core/hooks/notify-milestone.js +170 -0
- package/dist/core/hooks/post-apply.d.ts +29 -0
- package/dist/core/hooks/post-apply.js +173 -0
- package/dist/core/hooks/post-archive.d.ts +7 -0
- package/dist/core/hooks/post-archive.js +208 -0
- package/dist/core/hooks/pre-apply.d.ts +34 -0
- package/dist/core/hooks/pre-apply.js +139 -0
- package/dist/core/hooks/pre-archive.d.ts +7 -0
- package/dist/core/hooks/pre-archive.js +50 -0
- package/dist/core/hooks/record-progress.d.ts +49 -0
- package/dist/core/hooks/record-progress.js +494 -0
- package/dist/core/hooks/review-hooks.d.ts +89 -0
- package/dist/core/hooks/review-hooks.js +345 -0
- package/dist/core/hooks/review-orchestrator.d.ts +40 -0
- package/dist/core/hooks/review-orchestrator.js +146 -0
- package/dist/core/hooks/summarize.d.ts +15 -0
- package/dist/core/hooks/summarize.js +282 -0
- package/dist/core/hooks/user-input-hook.d.ts +25 -0
- package/dist/core/hooks/user-input-hook.js +179 -0
- package/dist/core/init.d.ts +8 -0
- package/dist/core/init.js +251 -23
- package/dist/core/parsers/requirement-blocks.js +13 -10
- package/dist/core/templates/agents-template.d.ts +1 -1
- package/dist/core/templates/agents-template.js +510 -243
- package/dist/core/templates/index.d.ts +1 -0
- package/dist/core/templates/index.js +1 -0
- package/dist/core/templates/skill-templates.js +46 -152
- package/dist/core/templates/slash-command-templates.d.ts +1 -1
- package/dist/core/templates/slash-command-templates.js +352 -20
- package/dist/core/templates/tasks-template.d.ts +7 -0
- package/dist/core/templates/tasks-template.js +130 -24
- package/dist/core/templates/tdd-tasks-template.d.ts +3 -0
- package/dist/core/templates/tdd-tasks-template.js +91 -38
- package/dist/core/templates/test-cases-template.d.ts +41 -0
- package/dist/core/templates/test-cases-template.js +128 -0
- package/dist/core/validation/strict-rules.d.ts +44 -5
- package/dist/core/validation/strict-rules.js +302 -8
- package/dist/core/validation/validator.js +52 -2
- package/dist/core/view.d.ts +1 -0
- package/dist/core/view.js +60 -2
- package/dist/mcp/index.d.ts +28 -0
- package/dist/mcp/index.js +31 -0
- package/dist/utils/file-system.d.ts +1 -0
- package/dist/utils/file-system.js +11 -0
- package/dist/utils/item-discovery.js +24 -2
- package/dist/utils/phase-utils.d.ts +36 -0
- package/dist/utils/phase-utils.js +117 -0
- package/package.json +22 -23
- package/schemas/spec-driven/schema.yaml +45 -31
- package/schemas/spec-driven/templates/spec.md +142 -5
- 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
|