@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,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stop Hook - Summarize Session Progress
|
|
3
|
+
*
|
|
4
|
+
* This hook runs when Claude Code session stops or pauses:
|
|
5
|
+
* 1. Summarize current wave completion status
|
|
6
|
+
* 2. Update ZHUANSPEC_CURRENT_TASK for next task
|
|
7
|
+
* 3. Check if tasks.md is complete → trigger Review phase
|
|
8
|
+
* 4. Generate events.json entries for key events
|
|
9
|
+
*/
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { execSync } from 'child_process';
|
|
12
|
+
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
13
|
+
import { executePostApply } from './post-apply.js';
|
|
14
|
+
import { getBeijingTime } from './record-progress.js';
|
|
15
|
+
export async function summarizeHook(options) {
|
|
16
|
+
const output = await runSummarize();
|
|
17
|
+
if (options.json) {
|
|
18
|
+
console.log(JSON.stringify(output));
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
if (output.systemMessage) {
|
|
22
|
+
console.log(output.systemMessage);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function runSummarize() {
|
|
27
|
+
const phase = process.env.ZHUANSPEC_PHASE || 'idle';
|
|
28
|
+
const changeId = process.env.ZHUANSPEC_CHANGE_ID || '';
|
|
29
|
+
const currentTask = process.env.ZHUANSPEC_CURRENT_TASK || '';
|
|
30
|
+
// No active change → nothing to summarize
|
|
31
|
+
if (!changeId || phase === 'idle') {
|
|
32
|
+
return {
|
|
33
|
+
continue: true,
|
|
34
|
+
systemMessage: 'ℹ No active ZhuanSpec change to summarize',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const cwd = process.cwd();
|
|
38
|
+
const zhuanspecDir = path.join(cwd, 'zhuanspec');
|
|
39
|
+
const changeDir = path.join(zhuanspecDir, 'changes', changeId);
|
|
40
|
+
const metricsDir = path.join(changeDir, 'metrics');
|
|
41
|
+
// Load progress data
|
|
42
|
+
const progressPath = path.join(metricsDir, 'progress.json');
|
|
43
|
+
let progressData = null;
|
|
44
|
+
if (await FileSystemUtils.fileExists(progressPath)) {
|
|
45
|
+
try {
|
|
46
|
+
progressData = JSON.parse(await FileSystemUtils.readFile(progressPath));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Ignore parse errors
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Load tasks.md to analyze wave structure
|
|
53
|
+
const tasksPath = path.join(changeDir, 'tasks.md');
|
|
54
|
+
let tasks = [];
|
|
55
|
+
let waves = [];
|
|
56
|
+
if (await FileSystemUtils.fileExists(tasksPath)) {
|
|
57
|
+
const tasksContent = await FileSystemUtils.readFile(tasksPath);
|
|
58
|
+
tasks = parseTasks(tasksContent);
|
|
59
|
+
waves = computeWaves(tasks);
|
|
60
|
+
}
|
|
61
|
+
// Find current wave
|
|
62
|
+
const currentWave = waves.find(w => w.tasks.includes(currentTask) || w.status === 'in_progress');
|
|
63
|
+
// Calculate completion status
|
|
64
|
+
const allTasksComplete = tasks.length > 0 && tasks.every(t => t.status === 'completed');
|
|
65
|
+
const completedTasksCount = tasks.filter(t => t.status === 'completed').length;
|
|
66
|
+
// Find next task
|
|
67
|
+
let nextTask = '';
|
|
68
|
+
if (!allTasksComplete) {
|
|
69
|
+
const pendingTasks = tasks.filter(t => t.status === 'pending');
|
|
70
|
+
if (pendingTasks.length > 0) {
|
|
71
|
+
nextTask = pendingTasks[0].taskId;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Auto review trigger: only once after all apply tasks complete.
|
|
75
|
+
// First, execute post-apply hook for deviation detection
|
|
76
|
+
let autoReviewTriggered = false;
|
|
77
|
+
let autoReviewPassed = false;
|
|
78
|
+
let deviationDetected = false;
|
|
79
|
+
if (phase === 'apply' && allTasksComplete) {
|
|
80
|
+
// Execute post-apply hook to detect deviations
|
|
81
|
+
const postApplyResult = await executePostApply(changeDir, cwd);
|
|
82
|
+
if (postApplyResult.hookSpecificOutput?.deviationDetected) {
|
|
83
|
+
deviationDetected = true;
|
|
84
|
+
// Don't trigger review if deviation detected
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// No deviation - proceed with auto review
|
|
88
|
+
const autoReviewEnabled = process.env.ZHUANSPEC_AUTO_REVIEW !== 'false';
|
|
89
|
+
const reviewPath = path.join(metricsDir, 'review.json');
|
|
90
|
+
const hasReviewReport = await FileSystemUtils.fileExists(reviewPath);
|
|
91
|
+
if (autoReviewEnabled && !hasReviewReport) {
|
|
92
|
+
try {
|
|
93
|
+
execSync(`zhuanspec review "${changeId}"`, {
|
|
94
|
+
cwd,
|
|
95
|
+
stdio: 'pipe',
|
|
96
|
+
encoding: 'utf-8',
|
|
97
|
+
});
|
|
98
|
+
autoReviewTriggered = true;
|
|
99
|
+
autoReviewPassed = true;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
autoReviewTriggered = true;
|
|
103
|
+
autoReviewPassed = false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Calculate metrics
|
|
109
|
+
const sessionDurationMs = progressData
|
|
110
|
+
? Date.now() - new Date(progressData.startedAt).getTime()
|
|
111
|
+
: 0;
|
|
112
|
+
// Write events.json entry
|
|
113
|
+
const eventsPath = path.join(metricsDir, 'events.json');
|
|
114
|
+
const event = {
|
|
115
|
+
eventId: `evt-${Date.now()}`,
|
|
116
|
+
type: 'session_summary',
|
|
117
|
+
timestamp: getBeijingTime(),
|
|
118
|
+
data: {
|
|
119
|
+
phase,
|
|
120
|
+
currentTask,
|
|
121
|
+
currentWaveId: currentWave?.waveId,
|
|
122
|
+
tasksCompleted: completedTasksCount,
|
|
123
|
+
tasksTotal: tasks.length,
|
|
124
|
+
allTasksComplete,
|
|
125
|
+
nextTask,
|
|
126
|
+
toolCallsCount: progressData?.toolCalls.length || 0,
|
|
127
|
+
filesModifiedCount: progressData?.filesModified.length || 0,
|
|
128
|
+
linesAdded: progressData?.linesAdded || 0,
|
|
129
|
+
sessionDurationMs,
|
|
130
|
+
tokenUsageTotal: progressData?.stats?.tokenUsageTotal || 0,
|
|
131
|
+
contextLoad: progressData?.stats?.contextLoad || 0,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
let eventsLog;
|
|
135
|
+
if (await FileSystemUtils.fileExists(eventsPath)) {
|
|
136
|
+
try {
|
|
137
|
+
eventsLog = JSON.parse(await FileSystemUtils.readFile(eventsPath));
|
|
138
|
+
eventsLog.events.push(event);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
eventsLog = {
|
|
142
|
+
changeId,
|
|
143
|
+
sessionId: progressData?.sessionId || 'unknown',
|
|
144
|
+
events: [event],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
eventsLog = {
|
|
150
|
+
changeId,
|
|
151
|
+
sessionId: progressData?.sessionId || 'unknown',
|
|
152
|
+
events: [event],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
await FileSystemUtils.writeFile(eventsPath, JSON.stringify(eventsLog, null, 2));
|
|
156
|
+
// Timeout detection - check if task has been idle for too long
|
|
157
|
+
const TIMEOUT_THRESHOLD_SECONDS = Number(process.env.ZHUANSPEC_TIMEOUT_SECONDS) || 300;
|
|
158
|
+
let timeoutWarning = '';
|
|
159
|
+
if (progressData && currentTask && phase === 'apply') {
|
|
160
|
+
const lastActiveTime = new Date(progressData.lastUpdatedAt).getTime();
|
|
161
|
+
const idleSeconds = Math.round((Date.now() - lastActiveTime) / 1000);
|
|
162
|
+
if (idleSeconds > TIMEOUT_THRESHOLD_SECONDS) {
|
|
163
|
+
const idleMinutes = Math.round(idleSeconds / 60);
|
|
164
|
+
timeoutWarning = `⚠️ 超时警告: 任务 ${currentTask} 可能卡住\n最后活跃: ${idleMinutes}m ago\n建议: 检查任务执行状态\n`;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Build summary message
|
|
168
|
+
let systemMessage = `📊 ZhuanSpec Session Summary\n\n`;
|
|
169
|
+
systemMessage += `Phase: ${phase}\n`;
|
|
170
|
+
systemMessage += `Change: ${changeId}\n`;
|
|
171
|
+
if (currentTask) {
|
|
172
|
+
systemMessage += `Current Task: ${currentTask}\n`;
|
|
173
|
+
}
|
|
174
|
+
if (tasks.length > 0) {
|
|
175
|
+
systemMessage += `Tasks Progress: ${completedTasksCount}/${tasks.length} completed\n`;
|
|
176
|
+
if (currentWave) {
|
|
177
|
+
systemMessage += `Current Wave: Wave ${currentWave.waveId} (${currentWave.completedTasks.length}/${currentWave.tasks.length} completed)\n`;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Add timeout warning if detected
|
|
181
|
+
if (timeoutWarning) {
|
|
182
|
+
systemMessage += `\n${timeoutWarning}`;
|
|
183
|
+
}
|
|
184
|
+
if (progressData) {
|
|
185
|
+
systemMessage += `\nMetrics:\n`;
|
|
186
|
+
systemMessage += `- Tool Calls: ${progressData.toolCalls.length}\n`;
|
|
187
|
+
systemMessage += `- Files Modified: ${progressData.filesModified.length}\n`;
|
|
188
|
+
systemMessage += `- Lines Added: ${progressData.linesAdded}\n`;
|
|
189
|
+
systemMessage += `- Session Duration: ${Math.round(sessionDurationMs / 1000)}s\n`;
|
|
190
|
+
systemMessage += `- Token Usage: ${progressData.stats?.tokenUsageTotal || 0}\n`;
|
|
191
|
+
systemMessage += `- Context Load: ${progressData.stats?.contextLoad || 0}\n`;
|
|
192
|
+
}
|
|
193
|
+
if (allTasksComplete) {
|
|
194
|
+
systemMessage += `\n✅ All tasks completed! Ready for Review phase.\n`;
|
|
195
|
+
if (deviationDetected) {
|
|
196
|
+
systemMessage += `⚠️ DEVIATION DETECTED. Please update proposal (Reverse Sync) before proceeding to Review.\n`;
|
|
197
|
+
}
|
|
198
|
+
else if (autoReviewTriggered) {
|
|
199
|
+
systemMessage += autoReviewPassed
|
|
200
|
+
? `✅ Auto review executed successfully.\n`
|
|
201
|
+
: `⚠️ Auto review executed but failed. Please check zhuanspec review output.\n`;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else if (nextTask) {
|
|
205
|
+
systemMessage += `\n➡️ Next Task: ${nextTask}\n`;
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
continue: true,
|
|
209
|
+
systemMessage,
|
|
210
|
+
hookSpecificOutput: {
|
|
211
|
+
waveCompleted: currentWave?.waveId,
|
|
212
|
+
nextTask,
|
|
213
|
+
allTasksComplete,
|
|
214
|
+
autoReviewTriggered,
|
|
215
|
+
metrics: {
|
|
216
|
+
totalToolCalls: progressData?.toolCalls.length || 0,
|
|
217
|
+
totalFilesModified: progressData?.filesModified.length || 0,
|
|
218
|
+
totalLinesAdded: progressData?.linesAdded || 0,
|
|
219
|
+
sessionDurationMs,
|
|
220
|
+
tokenUsageTotal: progressData?.stats?.tokenUsageTotal || 0,
|
|
221
|
+
contextLoad: progressData?.stats?.contextLoad || 0,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function parseTasks(content) {
|
|
227
|
+
const tasks = [];
|
|
228
|
+
const lines = content.split('\n');
|
|
229
|
+
let currentWave = 0;
|
|
230
|
+
for (const line of lines) {
|
|
231
|
+
// Match wave headers: ### Wave 1, ### Wave 2, etc.
|
|
232
|
+
const waveMatch = line.match(/^###\s*Wave\s*(\d+)/i);
|
|
233
|
+
if (waveMatch) {
|
|
234
|
+
currentWave = parseInt(waveMatch[1], 10);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
// Match task lines: - [ ] X.Y description or - [x] X.Y description
|
|
238
|
+
const taskMatch = line.match(/^-\s*\[([x ])\]\s*(\d+\.\d+)\s*(.+)/);
|
|
239
|
+
if (taskMatch) {
|
|
240
|
+
const statusChar = taskMatch[1];
|
|
241
|
+
const taskId = taskMatch[2];
|
|
242
|
+
tasks.push({
|
|
243
|
+
taskId,
|
|
244
|
+
status: statusChar === 'x' ? 'completed' : 'pending',
|
|
245
|
+
wave: currentWave,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return tasks;
|
|
250
|
+
}
|
|
251
|
+
function computeWaves(tasks) {
|
|
252
|
+
const wavesMap = new Map();
|
|
253
|
+
for (const task of tasks) {
|
|
254
|
+
if (!wavesMap.has(task.wave)) {
|
|
255
|
+
wavesMap.set(task.wave, {
|
|
256
|
+
waveId: task.wave,
|
|
257
|
+
tasks: [],
|
|
258
|
+
completedTasks: [],
|
|
259
|
+
status: 'pending',
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
const wave = wavesMap.get(task.wave);
|
|
263
|
+
wave.tasks.push(task.taskId);
|
|
264
|
+
if (task.status === 'completed') {
|
|
265
|
+
wave.completedTasks.push(task.taskId);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Determine wave status
|
|
269
|
+
for (const [waveId, wave] of wavesMap) {
|
|
270
|
+
if (wave.completedTasks.length === wave.tasks.length) {
|
|
271
|
+
wave.status = 'completed';
|
|
272
|
+
}
|
|
273
|
+
else if (wave.completedTasks.length > 0) {
|
|
274
|
+
wave.status = 'in_progress';
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
wave.status = 'pending';
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return Array.from(wavesMap.values()).sort((a, b) => a.waveId - b.waveId);
|
|
281
|
+
}
|
|
282
|
+
//# sourceMappingURL=summarize.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Input Hook
|
|
3
|
+
*
|
|
4
|
+
* Records user input summary during ZhuanSpec workflow.
|
|
5
|
+
* Tracks user interventions, corrections, confirmations, and additional requests.
|
|
6
|
+
*/
|
|
7
|
+
interface UserInputOutput {
|
|
8
|
+
continue: boolean;
|
|
9
|
+
systemMessage?: string;
|
|
10
|
+
hookSpecificOutput?: {
|
|
11
|
+
inputRecorded: boolean;
|
|
12
|
+
triggerType: string;
|
|
13
|
+
summaryLength: number;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Main hook function - records user input
|
|
18
|
+
*/
|
|
19
|
+
export declare function userInputHook(content: string, responseTimeMs?: number): Promise<UserInputOutput>;
|
|
20
|
+
/**
|
|
21
|
+
* CLI entry point
|
|
22
|
+
*/
|
|
23
|
+
export declare function runUserInputHook(): Promise<void>;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=user-input-hook.d.ts.map
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Input Hook
|
|
3
|
+
*
|
|
4
|
+
* Records user input summary during ZhuanSpec workflow.
|
|
5
|
+
* Tracks user interventions, corrections, confirmations, and additional requests.
|
|
6
|
+
*/
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
9
|
+
import { getBeijingTime } from './record-progress.js';
|
|
10
|
+
/**
|
|
11
|
+
* Detect trigger type from user input content
|
|
12
|
+
*/
|
|
13
|
+
function detectTriggerType(content) {
|
|
14
|
+
const lowerContent = content.toLowerCase();
|
|
15
|
+
// Correction patterns
|
|
16
|
+
if (lowerContent.includes('不对') || lowerContent.includes('错误') ||
|
|
17
|
+
lowerContent.includes('修复') || lowerContent.includes('修改') ||
|
|
18
|
+
lowerContent.includes('问题') || lowerContent.includes('bug')) {
|
|
19
|
+
return 'correction';
|
|
20
|
+
}
|
|
21
|
+
// Additional patterns
|
|
22
|
+
if (lowerContent.includes('另外') || lowerContent.includes('还有') ||
|
|
23
|
+
lowerContent.includes('追加') || lowerContent.includes('补充') ||
|
|
24
|
+
lowerContent.includes('加上')) {
|
|
25
|
+
return 'additional';
|
|
26
|
+
}
|
|
27
|
+
// Confirmation patterns
|
|
28
|
+
if (lowerContent.includes('好的') || lowerContent.includes('确认') ||
|
|
29
|
+
lowerContent.includes('同意') || lowerContent.includes('批准') ||
|
|
30
|
+
lowerContent.includes('继续') || lowerContent.includes('plan approved')) {
|
|
31
|
+
return 'confirmation';
|
|
32
|
+
}
|
|
33
|
+
// Clarification patterns
|
|
34
|
+
if (lowerContent.includes('?') || lowerContent.includes('为什么') ||
|
|
35
|
+
lowerContent.includes('怎么') || lowerContent.includes('如何') ||
|
|
36
|
+
lowerContent.includes('什么') || lowerContent.includes('澄清')) {
|
|
37
|
+
return 'clarification';
|
|
38
|
+
}
|
|
39
|
+
// Initial request (typically long first message)
|
|
40
|
+
if (content.length > 200) {
|
|
41
|
+
return 'initial';
|
|
42
|
+
}
|
|
43
|
+
return 'other';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Extract file references from user input
|
|
47
|
+
*/
|
|
48
|
+
function extractAffectedFiles(content) {
|
|
49
|
+
const filePatterns = [
|
|
50
|
+
/(?:file|文件|路径):\s*([^\s,]+)/gi,
|
|
51
|
+
/([a-zA-Z0-9_/-]+\.(?:java|ts|js|md|json|xml))/gi,
|
|
52
|
+
/zhuanspec\/[^\s]+/gi
|
|
53
|
+
];
|
|
54
|
+
const files = [];
|
|
55
|
+
for (const pattern of filePatterns) {
|
|
56
|
+
const matches = content.match(pattern) || [];
|
|
57
|
+
files.push(...matches);
|
|
58
|
+
}
|
|
59
|
+
return [...new Set(files)].slice(0, 10); // Limit to 10 unique files
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Generate summary from user input (max 200 chars)
|
|
63
|
+
*/
|
|
64
|
+
function generateSummary(content) {
|
|
65
|
+
// Truncate to 200 chars for summary
|
|
66
|
+
if (content.length <= 200) {
|
|
67
|
+
return content.trim();
|
|
68
|
+
}
|
|
69
|
+
// Extract first sentence or first 200 chars
|
|
70
|
+
const firstSentence = content.split(/[.!?\n]/)[0];
|
|
71
|
+
if (firstSentence.length <= 200) {
|
|
72
|
+
return firstSentence.trim();
|
|
73
|
+
}
|
|
74
|
+
return content.slice(0, 197) + '...';
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Main hook function - records user input
|
|
78
|
+
*/
|
|
79
|
+
export async function userInputHook(content, responseTimeMs) {
|
|
80
|
+
const phase = process.env.ZHUANSPEC_PHASE || 'idle';
|
|
81
|
+
const changeId = process.env.ZHUANSPEC_CHANGE_ID || '';
|
|
82
|
+
// Skip if not in active phase
|
|
83
|
+
if (!changeId || phase === 'idle') {
|
|
84
|
+
return { continue: true };
|
|
85
|
+
}
|
|
86
|
+
// Skip empty content
|
|
87
|
+
if (!content || content.trim().length < 10) {
|
|
88
|
+
return { continue: true };
|
|
89
|
+
}
|
|
90
|
+
const cwd = process.cwd();
|
|
91
|
+
const zhuanspecDir = path.join(cwd, 'zhuanspec');
|
|
92
|
+
const metricsDir = path.join(zhuanspecDir, 'changes', changeId, 'metrics');
|
|
93
|
+
// Ensure metrics directory exists
|
|
94
|
+
await FileSystemUtils.createDirectory(metricsDir);
|
|
95
|
+
const userInputPath = path.join(metricsDir, 'user_inputs.json');
|
|
96
|
+
// Create input record
|
|
97
|
+
const inputRecord = {
|
|
98
|
+
inputId: `input-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
99
|
+
timestamp: getBeijingTime(),
|
|
100
|
+
phase,
|
|
101
|
+
summary: generateSummary(content),
|
|
102
|
+
triggerType: detectTriggerType(content),
|
|
103
|
+
affectedFiles: extractAffectedFiles(content),
|
|
104
|
+
responseTimeMs
|
|
105
|
+
};
|
|
106
|
+
// Load existing user inputs or create new
|
|
107
|
+
let userInputs;
|
|
108
|
+
if (await FileSystemUtils.fileExists(userInputPath)) {
|
|
109
|
+
try {
|
|
110
|
+
userInputs = JSON.parse(await FileSystemUtils.readFile(userInputPath));
|
|
111
|
+
userInputs.inputs.push(inputRecord);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
userInputs = {
|
|
115
|
+
changeId,
|
|
116
|
+
sessionId: `sess-${Date.now()}`,
|
|
117
|
+
inputs: [inputRecord]
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
userInputs = {
|
|
123
|
+
changeId,
|
|
124
|
+
sessionId: `sess-${Date.now()}`,
|
|
125
|
+
inputs: [inputRecord]
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// Write user inputs file
|
|
129
|
+
await FileSystemUtils.writeFile(userInputPath, JSON.stringify(userInputs, null, 2));
|
|
130
|
+
return {
|
|
131
|
+
continue: true,
|
|
132
|
+
systemMessage: `📝 User input recorded: ${inputRecord.triggerType} (${inputRecord.summary.length} chars)`,
|
|
133
|
+
hookSpecificOutput: {
|
|
134
|
+
inputRecorded: true,
|
|
135
|
+
triggerType: inputRecord.triggerType,
|
|
136
|
+
summaryLength: inputRecord.summary.length
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* CLI entry point
|
|
142
|
+
*/
|
|
143
|
+
export async function runUserInputHook() {
|
|
144
|
+
// Read stdin for user message
|
|
145
|
+
let stdinData = {};
|
|
146
|
+
try {
|
|
147
|
+
const stdinContent = await readStdin();
|
|
148
|
+
if (stdinContent) {
|
|
149
|
+
stdinData = JSON.parse(stdinContent);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Ignore parse errors
|
|
154
|
+
}
|
|
155
|
+
const content = stdinData.user_message || '';
|
|
156
|
+
const responseTimeMs = typeof stdinData.response_time_ms === 'number'
|
|
157
|
+
? stdinData.response_time_ms
|
|
158
|
+
: undefined;
|
|
159
|
+
if (!content) {
|
|
160
|
+
console.log(JSON.stringify({ continue: true }));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const output = await userInputHook(content, responseTimeMs);
|
|
164
|
+
console.log(JSON.stringify(output));
|
|
165
|
+
}
|
|
166
|
+
async function readStdin() {
|
|
167
|
+
return new Promise((resolve) => {
|
|
168
|
+
let data = '';
|
|
169
|
+
process.stdin.setEncoding('utf8');
|
|
170
|
+
process.stdin.on('data', chunk => data += chunk);
|
|
171
|
+
process.stdin.on('end', () => resolve(data));
|
|
172
|
+
process.stdin.on('error', () => resolve(null));
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
// CLI execution
|
|
176
|
+
if (require.main === module) {
|
|
177
|
+
runUserInputHook();
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=user-input-hook.js.map
|
package/dist/core/init.d.ts
CHANGED
|
@@ -69,7 +69,15 @@ export declare class InitCommand {
|
|
|
69
69
|
private writeTemplateFiles;
|
|
70
70
|
private configureAITools;
|
|
71
71
|
private configureRootAgentsStub;
|
|
72
|
+
/**
|
|
73
|
+
* Generate Claude Code hooks configuration for ZhuanSpec workflow.
|
|
74
|
+
* Creates .claude/settings.json with hooks for SessionStart, PreToolUse, PostToolUse, and Stop.
|
|
75
|
+
*/
|
|
76
|
+
private generateClaudeSettings;
|
|
77
|
+
private readExistingSettings;
|
|
78
|
+
private mergeSettings;
|
|
72
79
|
private displaySuccessMessage;
|
|
80
|
+
private displayWorkflowDiagram;
|
|
73
81
|
private formatToolNames;
|
|
74
82
|
private renderBanner;
|
|
75
83
|
private startSpinner;
|