@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,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
|