@zhuan-ai/zhuanspec 2.4.8 → 2.6.0
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/dist/commands/progress.d.ts +6 -0
- package/dist/commands/progress.js +92 -25
- package/dist/core/hooks/record-progress.js +142 -27
- package/dist/core/init.d.ts +4 -1
- package/dist/core/init.js +122 -36
- package/dist/core/task-graph/execution-planner.d.ts +6 -0
- package/dist/core/task-graph/execution-planner.js +88 -2
- package/dist/core/task-graph/task-parser.d.ts +13 -0
- package/dist/core/task-graph/task-parser.js +57 -0
- package/dist/core/task-graph/types.d.ts +13 -0
- package/dist/core/templates/agents-template.d.ts +1 -1
- package/dist/core/templates/agents-template.js +6 -3
- package/dist/core/templates/slash-command-templates.js +6 -6
- package/dist/core/templates/tasks-template.js +0 -10
- package/dist/core/validation/strict-rules.d.ts +7 -0
- package/dist/core/validation/strict-rules.js +61 -4
- package/package.json +1 -1
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
* - Estimated remaining time
|
|
8
8
|
*/
|
|
9
9
|
export declare class ProgressCommand {
|
|
10
|
+
/**
|
|
11
|
+
* Recover damaged progress.json using three-tier fallback strategy
|
|
12
|
+
* @param filePath - Path to progress.json file
|
|
13
|
+
* @returns Recovered ProgressData or null if unrecoverable
|
|
14
|
+
*/
|
|
15
|
+
private recoverProgressJson;
|
|
10
16
|
/**
|
|
11
17
|
* Show progress for a specific change
|
|
12
18
|
* Usage: zhuanspec progress show <change-id>
|
|
@@ -12,6 +12,69 @@ import { FileSystemUtils } from '../utils/file-system.js';
|
|
|
12
12
|
import { PHASE_ORDER } from '../utils/phase-utils.js';
|
|
13
13
|
import { initializeProgress } from '../core/hooks/record-progress.js';
|
|
14
14
|
export class ProgressCommand {
|
|
15
|
+
/**
|
|
16
|
+
* Recover damaged progress.json using three-tier fallback strategy
|
|
17
|
+
* @param filePath - Path to progress.json file
|
|
18
|
+
* @returns Recovered ProgressData or null if unrecoverable
|
|
19
|
+
*/
|
|
20
|
+
async recoverProgressJson(filePath) {
|
|
21
|
+
const content = await FileSystemUtils.readFile(filePath);
|
|
22
|
+
// Layer 1: Standard JSON parse
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(content);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Continue to Layer 2
|
|
28
|
+
}
|
|
29
|
+
// Layer 2: Extract first top-level JSON object (handle trailing corruption)
|
|
30
|
+
try {
|
|
31
|
+
// Find the first complete JSON object
|
|
32
|
+
const objectStart = content.indexOf('{');
|
|
33
|
+
if (objectStart === -1)
|
|
34
|
+
return null;
|
|
35
|
+
// Track brace depth to find object boundary
|
|
36
|
+
let depth = 0;
|
|
37
|
+
let objectEnd = -1;
|
|
38
|
+
for (let i = objectStart; i < content.length; i++) {
|
|
39
|
+
if (content[i] === '{')
|
|
40
|
+
depth++;
|
|
41
|
+
else if (content[i] === '}') {
|
|
42
|
+
depth--;
|
|
43
|
+
if (depth === 0) {
|
|
44
|
+
objectEnd = i + 1;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (objectEnd > objectStart) {
|
|
50
|
+
const objectContent = content.slice(objectStart, objectEnd);
|
|
51
|
+
return JSON.parse(objectContent);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Continue to Layer 3
|
|
56
|
+
}
|
|
57
|
+
// Layer 3: Minimal field extraction using regex
|
|
58
|
+
const phaseMatch = content.match(/"phase"\s*:\s*"([^"]+)"/);
|
|
59
|
+
const currentTaskMatch = content.match(/"currentTask"\s*:\s*"([^"]+)"/);
|
|
60
|
+
const totalTasksMatch = content.match(/"totalTasks"\s*:\s*(\d+)/);
|
|
61
|
+
const changeIdMatch = content.match(/"changeId"\s*:\s*"([^"]+)"/);
|
|
62
|
+
const sessionIdMatch = content.match(/"sessionId"\s*:\s*"([^"]+)"/);
|
|
63
|
+
if (phaseMatch || changeIdMatch) {
|
|
64
|
+
return {
|
|
65
|
+
changeId: changeIdMatch?.[1] || '',
|
|
66
|
+
sessionId: sessionIdMatch?.[1] || '',
|
|
67
|
+
startedAt: '',
|
|
68
|
+
lastUpdatedAt: '',
|
|
69
|
+
phase: phaseMatch?.[1] || 'unknown',
|
|
70
|
+
currentNode: phaseMatch?.[1] || 'unknown',
|
|
71
|
+
currentTask: currentTaskMatch?.[1] || '',
|
|
72
|
+
completedTasks: [],
|
|
73
|
+
totalTasks: totalTasksMatch ? parseInt(totalTasksMatch[1], 10) : 0,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
15
78
|
/**
|
|
16
79
|
* Show progress for a specific change
|
|
17
80
|
* Usage: zhuanspec progress show <change-id>
|
|
@@ -24,16 +87,11 @@ export class ProgressCommand {
|
|
|
24
87
|
if (!await FileSystemUtils.directoryExists(changeDir)) {
|
|
25
88
|
throw new Error(`Change '${changeId}' not found`);
|
|
26
89
|
}
|
|
27
|
-
// Read progress.json
|
|
90
|
+
// Read progress.json with recovery support
|
|
28
91
|
const progressPath = path.join(metricsDir, 'progress.json');
|
|
29
92
|
let progress = null;
|
|
30
93
|
if (await FileSystemUtils.fileExists(progressPath)) {
|
|
31
|
-
|
|
32
|
-
progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
// Ignore parse errors
|
|
36
|
-
}
|
|
94
|
+
progress = await this.recoverProgressJson(progressPath);
|
|
37
95
|
}
|
|
38
96
|
// Read tasks.md
|
|
39
97
|
const tasksPath = path.join(changeDir, 'tasks.md');
|
|
@@ -67,16 +125,11 @@ export class ProgressCommand {
|
|
|
67
125
|
}
|
|
68
126
|
const changeDir = path.join(zhuanspecDir, 'changes', changeId);
|
|
69
127
|
const metricsDir = path.join(changeDir, 'metrics');
|
|
70
|
-
// Read progress.json
|
|
128
|
+
// Read progress.json with recovery support
|
|
71
129
|
const progressPath = path.join(metricsDir, 'progress.json');
|
|
72
130
|
let progress = null;
|
|
73
131
|
if (await FileSystemUtils.fileExists(progressPath)) {
|
|
74
|
-
|
|
75
|
-
progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
// Ignore parse errors
|
|
79
|
-
}
|
|
132
|
+
progress = await this.recoverProgressJson(progressPath);
|
|
80
133
|
}
|
|
81
134
|
// Read tasks.md
|
|
82
135
|
const tasksPath = path.join(changeDir, 'tasks.md');
|
|
@@ -126,18 +179,36 @@ export class ProgressCommand {
|
|
|
126
179
|
currentWave = parseInt(waveMatch[1], 10);
|
|
127
180
|
continue;
|
|
128
181
|
}
|
|
129
|
-
// Match task lines: - [ ] X.Y description or - [x] X.Y description
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
132
|
-
const statusChar =
|
|
133
|
-
const taskId =
|
|
134
|
-
const description =
|
|
182
|
+
// Match new format task lines: - [ ] X.Y description or - [x] X.Y description
|
|
183
|
+
const newFormatMatch = line.match(/^-\s*\[([x ])\]\s*(\d+\.\d+)\s*(.+)/);
|
|
184
|
+
if (newFormatMatch) {
|
|
185
|
+
const statusChar = newFormatMatch[1];
|
|
186
|
+
const taskId = newFormatMatch[2];
|
|
187
|
+
const description = newFormatMatch[3].trim();
|
|
135
188
|
tasks.push({
|
|
136
189
|
taskId,
|
|
137
190
|
status: statusChar === 'x' ? 'completed' : 'pending',
|
|
138
191
|
wave: currentWave,
|
|
139
192
|
description: description.split('@')[0].trim(), // Remove @skill/@depends tags
|
|
140
193
|
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
// Match legacy format task headers: #### T1: description or #### T2: description
|
|
197
|
+
const legacyMatch = line.match(/^####\s*(T\d+)\s*:\s*(.+)$/);
|
|
198
|
+
if (legacyMatch) {
|
|
199
|
+
const taskId = legacyMatch[1];
|
|
200
|
+
const rawDescription = legacyMatch[2].trim();
|
|
201
|
+
// Check if description ends with ✅ (completed marker)
|
|
202
|
+
const isCompleted = rawDescription.endsWith('✅');
|
|
203
|
+
const description = isCompleted
|
|
204
|
+
? rawDescription.slice(0, -1).trim()
|
|
205
|
+
: rawDescription;
|
|
206
|
+
tasks.push({
|
|
207
|
+
taskId,
|
|
208
|
+
status: isCompleted ? 'completed' : 'pending',
|
|
209
|
+
wave: currentWave,
|
|
210
|
+
description: description.split('@')[0].trim(), // Remove @skill/@depends tags
|
|
211
|
+
});
|
|
141
212
|
}
|
|
142
213
|
}
|
|
143
214
|
return tasks;
|
|
@@ -211,11 +282,9 @@ export class ProgressCommand {
|
|
|
211
282
|
? Math.round((durationMin / completedCount) * (totalTasks - completedCount))
|
|
212
283
|
: 0;
|
|
213
284
|
console.log('');
|
|
214
|
-
console.log(`📊 变更进度: ${changeId}`);
|
|
285
|
+
console.log(`📊 变更进度: ${changeId} | Phase: ${phase} (${phaseIndex}/${phases.length}) | ${progressBar} ${percentage.toFixed(1)}% (${completedCount}/${totalTasks})`);
|
|
215
286
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
216
287
|
console.log('');
|
|
217
|
-
console.log(`Phase: ${phase} (${phaseIndex}/${phases.length})`);
|
|
218
|
-
console.log('');
|
|
219
288
|
// Wave status
|
|
220
289
|
for (const wave of waves) {
|
|
221
290
|
const waveStatusEmoji = wave.status === 'completed' ? '✅' : wave.status === 'in_progress' ? '🔄' : '⏳';
|
|
@@ -229,8 +298,6 @@ export class ProgressCommand {
|
|
|
229
298
|
}
|
|
230
299
|
console.log('');
|
|
231
300
|
}
|
|
232
|
-
// Overall progress
|
|
233
|
-
console.log(`整体: ${progressBar} ${percentage.toFixed(1)}% (${completedCount}/${totalTasks})`);
|
|
234
301
|
// Duration info
|
|
235
302
|
if (durationMin > 0) {
|
|
236
303
|
console.log(`耗时: ${durationMin}m | 预计剩余: ~${estimatedRemaining}m`);
|
|
@@ -11,15 +11,118 @@ import { promises as fsPromises } from 'fs';
|
|
|
11
11
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
12
12
|
import { PHASE_ORDER } from '../../utils/phase-utils.js';
|
|
13
13
|
const fs = fsPromises;
|
|
14
|
+
/**
|
|
15
|
+
* Atomic write JSON file: write to temp file first, then rename
|
|
16
|
+
* This prevents half-written files during crashes or concurrent writes
|
|
17
|
+
* @param filePath - Target file path
|
|
18
|
+
* @param data - Data to write (will be JSON stringified)
|
|
19
|
+
*/
|
|
20
|
+
async function atomicWriteJson(filePath, data) {
|
|
21
|
+
const tmpPath = `${filePath}.tmp`;
|
|
22
|
+
const content = JSON.stringify(data, null, 2);
|
|
23
|
+
try {
|
|
24
|
+
// Write to temp file first
|
|
25
|
+
await FileSystemUtils.writeFile(tmpPath, content);
|
|
26
|
+
// Rename to final path (atomic on most filesystems)
|
|
27
|
+
await fs.rename(tmpPath, filePath);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// On failure, try to clean up temp file
|
|
31
|
+
try {
|
|
32
|
+
await fs.unlink(tmpPath);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Ignore cleanup errors
|
|
36
|
+
}
|
|
37
|
+
// Do not throw - preserve original file and continue silently
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Recover damaged progress.json for write operations
|
|
42
|
+
* Uses same three-tier strategy as read side
|
|
43
|
+
* @param filePath - Path to progress.json
|
|
44
|
+
* @returns Recovered ProgressData or null
|
|
45
|
+
*/
|
|
46
|
+
async function recoverProgressJsonForWrite(filePath) {
|
|
47
|
+
try {
|
|
48
|
+
const content = await FileSystemUtils.readFile(filePath);
|
|
49
|
+
// Layer 1: Standard JSON parse
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(content);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Continue to Layer 2
|
|
55
|
+
}
|
|
56
|
+
// Layer 2: Extract first top-level JSON object
|
|
57
|
+
const objectStart = content.indexOf('{');
|
|
58
|
+
if (objectStart === -1)
|
|
59
|
+
return null;
|
|
60
|
+
let depth = 0;
|
|
61
|
+
let objectEnd = -1;
|
|
62
|
+
for (let i = objectStart; i < content.length; i++) {
|
|
63
|
+
if (content[i] === '{')
|
|
64
|
+
depth++;
|
|
65
|
+
else if (content[i] === '}') {
|
|
66
|
+
depth--;
|
|
67
|
+
if (depth === 0) {
|
|
68
|
+
objectEnd = i + 1;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (objectEnd > objectStart) {
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(content.slice(objectStart, objectEnd));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Continue to Layer 3
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Layer 3: Minimal field extraction
|
|
82
|
+
const phaseMatch = content.match(/"phase"\s*:\s*"([^"]+)"/);
|
|
83
|
+
const changeIdMatch = content.match(/"changeId"\s*:\s*"([^"]+)"/);
|
|
84
|
+
const sessionIdMatch = content.match(/"sessionId"\s*:\s*"([^"]+)"/);
|
|
85
|
+
if (phaseMatch || changeIdMatch) {
|
|
86
|
+
return {
|
|
87
|
+
changeId: changeIdMatch?.[1] || '',
|
|
88
|
+
sessionId: sessionIdMatch?.[1] || '',
|
|
89
|
+
startedAt: '',
|
|
90
|
+
lastUpdatedAt: '',
|
|
91
|
+
phase: phaseMatch?.[1] || 'unknown',
|
|
92
|
+
currentNode: phaseMatch?.[1] || 'unknown',
|
|
93
|
+
currentTask: '',
|
|
94
|
+
completedTasks: [],
|
|
95
|
+
totalTasks: 0,
|
|
96
|
+
toolCalls: [],
|
|
97
|
+
skillCalls: [],
|
|
98
|
+
hookTriggers: [],
|
|
99
|
+
clarifications: [],
|
|
100
|
+
filesModified: [],
|
|
101
|
+
linesAdded: 0,
|
|
102
|
+
linesRemoved: 0,
|
|
103
|
+
deviationCount: 0,
|
|
104
|
+
deviationRecords: [],
|
|
105
|
+
reviewStats: { loopCount: 0, criticalFixes: 0, testFixes: 0, consistencyFixes: 0 },
|
|
106
|
+
phaseTransitions: [],
|
|
107
|
+
stats: { tokenUsageTotal: 0, contextLoad: 0, durationMs: { propose: 0, apply: 0, review: 0, archive: 0 } },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// File read error - return null
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
14
116
|
/**
|
|
15
117
|
* Get Beijing time (UTC+8) as ISO string format
|
|
16
118
|
* Returns format: "2026-04-19T18:35:22.934+08:00"
|
|
17
119
|
*/
|
|
18
120
|
export function getBeijingTime() {
|
|
19
121
|
const now = new Date();
|
|
20
|
-
//
|
|
21
|
-
const
|
|
22
|
-
const
|
|
122
|
+
// Get UTC time and add 8 hours for Beijing
|
|
123
|
+
const utcMs = now.getTime() + now.getTimezoneOffset() * 60 * 1000;
|
|
124
|
+
const beijingMs = utcMs + 8 * 60 * 60 * 1000;
|
|
125
|
+
const beijingTime = new Date(beijingMs);
|
|
23
126
|
// Format as ISO string with +08:00 timezone
|
|
24
127
|
const year = beijingTime.getFullYear();
|
|
25
128
|
const month = String(beijingTime.getMonth() + 1).padStart(2, '0');
|
|
@@ -36,8 +139,10 @@ export function getBeijingTime() {
|
|
|
36
139
|
*/
|
|
37
140
|
export function getBeijingTimeForFilename() {
|
|
38
141
|
const now = new Date();
|
|
39
|
-
|
|
40
|
-
const
|
|
142
|
+
// Get UTC time and add 8 hours for Beijing
|
|
143
|
+
const utcMs = now.getTime() + now.getTimezoneOffset() * 60 * 1000;
|
|
144
|
+
const beijingMs = utcMs + 8 * 60 * 60 * 1000;
|
|
145
|
+
const beijingTime = new Date(beijingMs);
|
|
41
146
|
const year = beijingTime.getFullYear();
|
|
42
147
|
const month = String(beijingTime.getMonth() + 1).padStart(2, '0');
|
|
43
148
|
const day = String(beijingTime.getDate()).padStart(2, '0');
|
|
@@ -147,11 +252,12 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
|
147
252
|
await FileSystemUtils.createDirectory(metricsDir);
|
|
148
253
|
const progressPath = path.join(metricsDir, 'progress.json');
|
|
149
254
|
const toolCallsPath = path.join(metricsDir, 'tool_calls.json');
|
|
150
|
-
// Load existing progress or create new
|
|
255
|
+
// Load existing progress or create new (with recovery support)
|
|
151
256
|
let progress;
|
|
152
257
|
if (await FileSystemUtils.fileExists(progressPath)) {
|
|
153
|
-
|
|
154
|
-
|
|
258
|
+
const recovered = await recoverProgressJsonForWrite(progressPath);
|
|
259
|
+
if (recovered) {
|
|
260
|
+
progress = recovered;
|
|
155
261
|
progress.phase = progress.phase || phase;
|
|
156
262
|
progress.currentNode = progress.currentNode || phase;
|
|
157
263
|
progress.completedTasks = progress.completedTasks || [];
|
|
@@ -183,7 +289,7 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
|
183
289
|
archive: 0,
|
|
184
290
|
};
|
|
185
291
|
}
|
|
186
|
-
|
|
292
|
+
else {
|
|
187
293
|
progress = createNewProgress(changeId);
|
|
188
294
|
}
|
|
189
295
|
}
|
|
@@ -290,17 +396,26 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
|
290
396
|
const contextLoadFromStdIn = Number(stdinData.tool_result?.contextLoad || 0);
|
|
291
397
|
progress.stats.tokenUsageTotal += tokenUsageFromEnv + tokenUsageFromStdIn;
|
|
292
398
|
progress.stats.contextLoad += contextLoadFromEnv + contextLoadFromStdIn;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
399
|
+
// Calculate duration for current phase from phaseTransitions
|
|
400
|
+
// Find the last transition to current phase
|
|
401
|
+
const transitionsToCurrentPhase = progress.phaseTransitions.filter((t) => t.to === phase);
|
|
402
|
+
const currentPhaseStart = transitionsToCurrentPhase[transitionsToCurrentPhase.length - 1];
|
|
403
|
+
if (currentPhaseStart) {
|
|
404
|
+
const startTime = new Date(currentPhaseStart.timestamp).getTime();
|
|
405
|
+
const elapsedMs = Date.now() - startTime;
|
|
406
|
+
if (elapsedMs > 0) {
|
|
407
|
+
if (phase === 'propose')
|
|
408
|
+
progress.stats.durationMs.propose = elapsedMs;
|
|
409
|
+
if (phase === 'apply')
|
|
410
|
+
progress.stats.durationMs.apply = elapsedMs;
|
|
411
|
+
if (phase === 'review')
|
|
412
|
+
progress.stats.durationMs.review = elapsedMs;
|
|
413
|
+
if (phase === 'archive')
|
|
414
|
+
progress.stats.durationMs.archive = elapsedMs;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// Write progress file atomically
|
|
418
|
+
await atomicWriteJson(progressPath, progress);
|
|
304
419
|
// Also update tool_calls.json separately for detailed tracking
|
|
305
420
|
let toolCallsLog;
|
|
306
421
|
if (await FileSystemUtils.fileExists(toolCallsPath)) {
|
|
@@ -423,16 +538,16 @@ export async function initializeProgress(changeId, initialPhase = 'propose') {
|
|
|
423
538
|
// Ensure metrics directory exists
|
|
424
539
|
await FileSystemUtils.createDirectory(metricsDir);
|
|
425
540
|
const progressPath = path.join(metricsDir, 'progress.json');
|
|
426
|
-
// Create or update progress file
|
|
541
|
+
// Create or update progress file (with recovery support)
|
|
427
542
|
let progress;
|
|
428
543
|
if (await FileSystemUtils.fileExists(progressPath)) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
544
|
+
const recovered = await recoverProgressJsonForWrite(progressPath);
|
|
545
|
+
if (recovered) {
|
|
546
|
+
progress = recovered;
|
|
432
547
|
progress.phase = initialPhase;
|
|
433
548
|
progress.currentNode = initialPhase;
|
|
434
549
|
}
|
|
435
|
-
|
|
550
|
+
else {
|
|
436
551
|
progress = createNewProgress(changeId);
|
|
437
552
|
progress.phase = initialPhase;
|
|
438
553
|
progress.currentNode = initialPhase;
|
|
@@ -444,7 +559,7 @@ export async function initializeProgress(changeId, initialPhase = 'propose') {
|
|
|
444
559
|
progress.currentNode = initialPhase;
|
|
445
560
|
}
|
|
446
561
|
progress.lastUpdatedAt = getBeijingTime();
|
|
447
|
-
await
|
|
562
|
+
await atomicWriteJson(progressPath, progress);
|
|
448
563
|
}
|
|
449
564
|
/**
|
|
450
565
|
* Record a hook trigger event
|
|
@@ -485,7 +600,7 @@ export async function recordHookTrigger(hookName, hookPhase, success, options) {
|
|
|
485
600
|
};
|
|
486
601
|
progress.hookTriggers.push(hookRecord);
|
|
487
602
|
progress.lastUpdatedAt = getBeijingTime();
|
|
488
|
-
await
|
|
603
|
+
await atomicWriteJson(progressPath, progress);
|
|
489
604
|
}
|
|
490
605
|
catch {
|
|
491
606
|
// Ignore errors
|
package/dist/core/init.d.ts
CHANGED
|
@@ -58,7 +58,10 @@ export declare class InitCommand {
|
|
|
58
58
|
private resolveClaudeAssetSource;
|
|
59
59
|
private copyClaudeFileIfNeeded;
|
|
60
60
|
private copyClaudeDirectoryIfNeeded;
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Copy .claude directory from common source (unified source, no fallback).
|
|
63
|
+
*/
|
|
64
|
+
private copyClaudeDirectoryFromCommon;
|
|
62
65
|
private copyClaudeDirectoryFromSingleSource;
|
|
63
66
|
private directoryHasAtLeastOneFile;
|
|
64
67
|
private copyClaudeDirectoryByFileSystem;
|