@zhuan-ai/zhuanspec 2.6.0 → 2.8.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/review.d.ts +1 -1
- package/dist/commands/review.js +24 -24
- package/dist/commands/validate.d.ts +0 -4
- package/dist/commands/validate.js +0 -22
- package/dist/core/archive.js +19 -12
- package/dist/core/hooks/deviation-check.js +2 -30
- package/dist/core/hooks/deviation-handler.d.ts +1 -0
- package/dist/core/hooks/deviation-handler.js +68 -15
- package/dist/core/hooks/post-apply.js +102 -39
- package/dist/core/hooks/record-progress.d.ts +167 -3
- package/dist/core/hooks/record-progress.js +86 -16
- package/dist/core/hooks/summarize.js +29 -10
- package/dist/core/templates/agents-template.d.ts +1 -1
- package/dist/core/templates/agents-template.js +1 -1
- package/dist/core/templates/slash-command-templates.js +1 -1
- package/dist/core/validation/strict-rules.js +10 -42
- package/dist/utils/git-repo-detector.d.ts +130 -0
- package/dist/utils/git-repo-detector.js +275 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/phase-utils.d.ts +3 -3
- package/dist/utils/phase-utils.js +133 -10
- package/package.json +1 -1
|
@@ -8,13 +8,27 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Phase } from '../../utils/phase-utils.js';
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Atomic write JSON file: write to temp file first, then rename
|
|
12
|
+
* This prevents half-written files during crashes or concurrent writes
|
|
13
|
+
* @param filePath - Target file path
|
|
14
|
+
* @param data - Data to write (will be JSON stringified)
|
|
15
|
+
*/
|
|
16
|
+
export declare function atomicWriteJson(filePath: string, data: unknown): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Recover damaged progress.json for write operations
|
|
19
|
+
* Uses same three-tier strategy as read side
|
|
20
|
+
* @param filePath - Path to progress.json
|
|
21
|
+
* @returns Recovered ProgressData or null
|
|
22
|
+
*/
|
|
23
|
+
export declare function recoverProgressJsonForWrite(filePath: string): Promise<ProgressData | null>;
|
|
24
|
+
/**
|
|
25
|
+
* Get Beijing time (UTC+8) as display format
|
|
26
|
+
* Returns format: "2026-04-21 18:35:22" (北京时间)
|
|
13
27
|
*/
|
|
14
28
|
export declare function getBeijingTime(): string;
|
|
15
29
|
/**
|
|
16
30
|
* Get Beijing time date string for session ID and filenames
|
|
17
|
-
* Returns format: "2026-04-
|
|
31
|
+
* Returns format: "2026-04-21 18-35-22" (文件名安全格式)
|
|
18
32
|
*/
|
|
19
33
|
export declare function getBeijingTimeForFilename(): string;
|
|
20
34
|
interface RecordProgressOptions {
|
|
@@ -23,6 +37,156 @@ interface RecordProgressOptions {
|
|
|
23
37
|
tool?: string;
|
|
24
38
|
success?: string;
|
|
25
39
|
}
|
|
40
|
+
interface ToolCallRecord {
|
|
41
|
+
callId: string;
|
|
42
|
+
tool: string;
|
|
43
|
+
input: {
|
|
44
|
+
file_path?: string;
|
|
45
|
+
content?: string;
|
|
46
|
+
pattern?: string;
|
|
47
|
+
path?: string;
|
|
48
|
+
command?: string;
|
|
49
|
+
skill?: string;
|
|
50
|
+
args?: string;
|
|
51
|
+
agent_type?: string;
|
|
52
|
+
mcp_server?: string;
|
|
53
|
+
mcp_tool?: string;
|
|
54
|
+
};
|
|
55
|
+
output: {
|
|
56
|
+
success: boolean;
|
|
57
|
+
lines?: number;
|
|
58
|
+
matches?: number;
|
|
59
|
+
exit_code?: number;
|
|
60
|
+
result?: string;
|
|
61
|
+
};
|
|
62
|
+
timestamp: string;
|
|
63
|
+
durationMs?: number;
|
|
64
|
+
phase: string;
|
|
65
|
+
taskId?: string;
|
|
66
|
+
}
|
|
67
|
+
interface DeviationRecord {
|
|
68
|
+
deviationId: string;
|
|
69
|
+
detectedAt: string;
|
|
70
|
+
triggerType: 'task-complete' | 'phase-complete' | 'user-input';
|
|
71
|
+
userInput?: string;
|
|
72
|
+
specChanged: string[];
|
|
73
|
+
codeChanged: string[];
|
|
74
|
+
resolvedAt?: string;
|
|
75
|
+
}
|
|
76
|
+
interface ReviewStats {
|
|
77
|
+
loopCount: number;
|
|
78
|
+
criticalFixes: number;
|
|
79
|
+
testFixes: number;
|
|
80
|
+
consistencyFixes: number;
|
|
81
|
+
}
|
|
82
|
+
interface PhaseTransition {
|
|
83
|
+
from: string;
|
|
84
|
+
to: string;
|
|
85
|
+
timestamp: string;
|
|
86
|
+
triggeredBy: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Skill 调用记录
|
|
90
|
+
*/
|
|
91
|
+
interface SkillCallRecord {
|
|
92
|
+
callId: string;
|
|
93
|
+
skill: string;
|
|
94
|
+
args?: string;
|
|
95
|
+
description?: string;
|
|
96
|
+
triggeredAt: string;
|
|
97
|
+
phase: string;
|
|
98
|
+
durationMs?: number;
|
|
99
|
+
success: boolean;
|
|
100
|
+
outputSummary?: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Hook 触发记录
|
|
104
|
+
*/
|
|
105
|
+
interface HookTriggerRecord {
|
|
106
|
+
hookId: string;
|
|
107
|
+
hookName: string;
|
|
108
|
+
hookPhase: string;
|
|
109
|
+
triggeredAt: string;
|
|
110
|
+
phase: string;
|
|
111
|
+
toolName?: string;
|
|
112
|
+
filePath?: string;
|
|
113
|
+
success: boolean;
|
|
114
|
+
durationMs?: number;
|
|
115
|
+
message?: string;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 咨询问题记录 (AskQuestion)
|
|
119
|
+
*/
|
|
120
|
+
interface ClarificationRecord {
|
|
121
|
+
clarificationId: string;
|
|
122
|
+
questions: Array<{
|
|
123
|
+
question: string;
|
|
124
|
+
header?: string;
|
|
125
|
+
options: Array<{
|
|
126
|
+
label: string;
|
|
127
|
+
description?: string;
|
|
128
|
+
}>;
|
|
129
|
+
multiSelect: boolean;
|
|
130
|
+
}>;
|
|
131
|
+
answers: Record<string, string>;
|
|
132
|
+
annotations?: Record<string, {
|
|
133
|
+
notes?: string;
|
|
134
|
+
preview?: string;
|
|
135
|
+
}>;
|
|
136
|
+
askedAt: string;
|
|
137
|
+
answeredAt?: string;
|
|
138
|
+
phase: string;
|
|
139
|
+
context?: string;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Phase duration record for tracking time spent in each phase
|
|
143
|
+
*/
|
|
144
|
+
export interface PhaseDurationRecord {
|
|
145
|
+
phase: Phase;
|
|
146
|
+
startedAt: string;
|
|
147
|
+
endedAt?: string;
|
|
148
|
+
durationMs: number;
|
|
149
|
+
taskCount: number;
|
|
150
|
+
completedTaskCount: number;
|
|
151
|
+
}
|
|
152
|
+
export interface ProgressData {
|
|
153
|
+
changeId: string;
|
|
154
|
+
sessionId: string;
|
|
155
|
+
startedAt: string;
|
|
156
|
+
lastUpdatedAt: string;
|
|
157
|
+
phase: string;
|
|
158
|
+
currentNode: string;
|
|
159
|
+
currentTask: string;
|
|
160
|
+
completedTasks: string[];
|
|
161
|
+
totalTasks: number;
|
|
162
|
+
toolCalls: ToolCallRecord[];
|
|
163
|
+
skillCalls: SkillCallRecord[];
|
|
164
|
+
hookTriggers: HookTriggerRecord[];
|
|
165
|
+
clarifications: ClarificationRecord[];
|
|
166
|
+
filesModified: string[];
|
|
167
|
+
linesAdded: number;
|
|
168
|
+
linesRemoved: number;
|
|
169
|
+
deviationCount: number;
|
|
170
|
+
deviationRecords: DeviationRecord[];
|
|
171
|
+
reviewStats: ReviewStats;
|
|
172
|
+
phaseTransitions: PhaseTransition[];
|
|
173
|
+
phaseDurations: PhaseDurationRecord[];
|
|
174
|
+
stats: {
|
|
175
|
+
tokenUsageTotal: number;
|
|
176
|
+
contextLoad: number;
|
|
177
|
+
durationMs: {
|
|
178
|
+
propose: number;
|
|
179
|
+
apply: number;
|
|
180
|
+
review: number;
|
|
181
|
+
archive: number;
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
events?: Array<{
|
|
185
|
+
event: string;
|
|
186
|
+
timestamp: string;
|
|
187
|
+
}>;
|
|
188
|
+
lastEvent?: string;
|
|
189
|
+
}
|
|
26
190
|
export declare function recordProgressHook(options: RecordProgressOptions): Promise<void>;
|
|
27
191
|
/**
|
|
28
192
|
* Initialize progress.json at phase start
|
|
@@ -17,7 +17,7 @@ const fs = fsPromises;
|
|
|
17
17
|
* @param filePath - Target file path
|
|
18
18
|
* @param data - Data to write (will be JSON stringified)
|
|
19
19
|
*/
|
|
20
|
-
async function atomicWriteJson(filePath, data) {
|
|
20
|
+
export async function atomicWriteJson(filePath, data) {
|
|
21
21
|
const tmpPath = `${filePath}.tmp`;
|
|
22
22
|
const content = JSON.stringify(data, null, 2);
|
|
23
23
|
try {
|
|
@@ -43,7 +43,7 @@ async function atomicWriteJson(filePath, data) {
|
|
|
43
43
|
* @param filePath - Path to progress.json
|
|
44
44
|
* @returns Recovered ProgressData or null
|
|
45
45
|
*/
|
|
46
|
-
async function recoverProgressJsonForWrite(filePath) {
|
|
46
|
+
export async function recoverProgressJsonForWrite(filePath) {
|
|
47
47
|
try {
|
|
48
48
|
const content = await FileSystemUtils.readFile(filePath);
|
|
49
49
|
// Layer 1: Standard JSON parse
|
|
@@ -83,13 +83,14 @@ async function recoverProgressJsonForWrite(filePath) {
|
|
|
83
83
|
const changeIdMatch = content.match(/"changeId"\s*:\s*"([^"]+)"/);
|
|
84
84
|
const sessionIdMatch = content.match(/"sessionId"\s*:\s*"([^"]+)"/);
|
|
85
85
|
if (phaseMatch || changeIdMatch) {
|
|
86
|
+
const recoveredPhase = phaseMatch?.[1] || 'unknown';
|
|
86
87
|
return {
|
|
87
88
|
changeId: changeIdMatch?.[1] || '',
|
|
88
89
|
sessionId: sessionIdMatch?.[1] || '',
|
|
89
90
|
startedAt: '',
|
|
90
91
|
lastUpdatedAt: '',
|
|
91
|
-
phase:
|
|
92
|
-
currentNode:
|
|
92
|
+
phase: recoveredPhase,
|
|
93
|
+
currentNode: recoveredPhase,
|
|
93
94
|
currentTask: '',
|
|
94
95
|
completedTasks: [],
|
|
95
96
|
totalTasks: 0,
|
|
@@ -104,6 +105,13 @@ async function recoverProgressJsonForWrite(filePath) {
|
|
|
104
105
|
deviationRecords: [],
|
|
105
106
|
reviewStats: { loopCount: 0, criticalFixes: 0, testFixes: 0, consistencyFixes: 0 },
|
|
106
107
|
phaseTransitions: [],
|
|
108
|
+
phaseDurations: [{
|
|
109
|
+
phase: recoveredPhase,
|
|
110
|
+
startedAt: '',
|
|
111
|
+
durationMs: 0,
|
|
112
|
+
taskCount: 0,
|
|
113
|
+
completedTaskCount: 0,
|
|
114
|
+
}],
|
|
107
115
|
stats: { tokenUsageTotal: 0, contextLoad: 0, durationMs: { propose: 0, apply: 0, review: 0, archive: 0 } },
|
|
108
116
|
};
|
|
109
117
|
}
|
|
@@ -114,8 +122,8 @@ async function recoverProgressJsonForWrite(filePath) {
|
|
|
114
122
|
return null;
|
|
115
123
|
}
|
|
116
124
|
/**
|
|
117
|
-
* Get Beijing time (UTC+8) as
|
|
118
|
-
* Returns format: "2026-04-
|
|
125
|
+
* Get Beijing time (UTC+8) as display format
|
|
126
|
+
* Returns format: "2026-04-21 18:35:22" (北京时间)
|
|
119
127
|
*/
|
|
120
128
|
export function getBeijingTime() {
|
|
121
129
|
const now = new Date();
|
|
@@ -123,19 +131,18 @@ export function getBeijingTime() {
|
|
|
123
131
|
const utcMs = now.getTime() + now.getTimezoneOffset() * 60 * 1000;
|
|
124
132
|
const beijingMs = utcMs + 8 * 60 * 60 * 1000;
|
|
125
133
|
const beijingTime = new Date(beijingMs);
|
|
126
|
-
// Format
|
|
134
|
+
// Format: yyyy-MM-dd hh:mm:ss
|
|
127
135
|
const year = beijingTime.getFullYear();
|
|
128
136
|
const month = String(beijingTime.getMonth() + 1).padStart(2, '0');
|
|
129
137
|
const day = String(beijingTime.getDate()).padStart(2, '0');
|
|
130
138
|
const hours = String(beijingTime.getHours()).padStart(2, '0');
|
|
131
139
|
const minutes = String(beijingTime.getMinutes()).padStart(2, '0');
|
|
132
140
|
const seconds = String(beijingTime.getSeconds()).padStart(2, '0');
|
|
133
|
-
|
|
134
|
-
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${ms}+08:00`;
|
|
141
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
135
142
|
}
|
|
136
143
|
/**
|
|
137
144
|
* Get Beijing time date string for session ID and filenames
|
|
138
|
-
* Returns format: "2026-04-
|
|
145
|
+
* Returns format: "2026-04-21 18-35-22" (文件名安全格式)
|
|
139
146
|
*/
|
|
140
147
|
export function getBeijingTimeForFilename() {
|
|
141
148
|
const now = new Date();
|
|
@@ -149,7 +156,7 @@ export function getBeijingTimeForFilename() {
|
|
|
149
156
|
const hours = String(beijingTime.getHours()).padStart(2, '0');
|
|
150
157
|
const minutes = String(beijingTime.getMinutes()).padStart(2, '0');
|
|
151
158
|
const seconds = String(beijingTime.getSeconds()).padStart(2, '0');
|
|
152
|
-
return `${year}-${month}-${day}
|
|
159
|
+
return `${year}-${month}-${day} ${hours}-${minutes}-${seconds}`;
|
|
153
160
|
}
|
|
154
161
|
export async function recordProgressHook(options) {
|
|
155
162
|
// Read stdin for Claude Code hook input
|
|
@@ -277,6 +284,7 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
|
277
284
|
consistencyFixes: 0,
|
|
278
285
|
};
|
|
279
286
|
progress.phaseTransitions = progress.phaseTransitions || [];
|
|
287
|
+
progress.phaseDurations = progress.phaseDurations || [];
|
|
280
288
|
progress.stats = progress.stats || {
|
|
281
289
|
tokenUsageTotal: 0,
|
|
282
290
|
contextLoad: 0,
|
|
@@ -375,8 +383,14 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
|
375
383
|
progress.currentNode = process.env.ZHUANSPEC_CURRENT_NODE || phase;
|
|
376
384
|
progress.currentTask = currentTask || progress.currentTask;
|
|
377
385
|
progress.toolCalls.push(toolCall);
|
|
378
|
-
|
|
379
|
-
|
|
386
|
+
const WRITE_TOOLS = new Set(['Write', 'Edit', 'NotebookEdit']);
|
|
387
|
+
if (filePath && WRITE_TOOLS.has(toolName) && !progress.filesModified.includes(filePath)) {
|
|
388
|
+
// Exclude ZhuanSpec internal files (spec/metrics/doc files inside zhuanspec/ dir)
|
|
389
|
+
const absFilePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
|
|
390
|
+
const zhuanspecDirPrefix = path.resolve(process.cwd(), 'zhuanspec') + path.sep;
|
|
391
|
+
if (!absFilePath.startsWith(zhuanspecDirPrefix)) {
|
|
392
|
+
progress.filesModified.push(filePath);
|
|
393
|
+
}
|
|
380
394
|
}
|
|
381
395
|
// Try to estimate lines from content
|
|
382
396
|
const content = stdinData.tool_input?.content;
|
|
@@ -486,13 +500,14 @@ function generateProgressBar(percentage, blocks) {
|
|
|
486
500
|
}
|
|
487
501
|
function createNewProgress(changeId) {
|
|
488
502
|
const sessionId = `sess-${getBeijingTimeForFilename()}`;
|
|
503
|
+
const initialPhase = process.env.ZHUANSPEC_PHASE || 'idle';
|
|
489
504
|
return {
|
|
490
505
|
changeId,
|
|
491
506
|
sessionId,
|
|
492
507
|
startedAt: getBeijingTime(),
|
|
493
508
|
lastUpdatedAt: getBeijingTime(),
|
|
494
|
-
phase:
|
|
495
|
-
currentNode: process.env.ZHUANSPEC_CURRENT_NODE ||
|
|
509
|
+
phase: initialPhase,
|
|
510
|
+
currentNode: process.env.ZHUANSPEC_CURRENT_NODE || initialPhase,
|
|
496
511
|
currentTask: '',
|
|
497
512
|
completedTasks: [],
|
|
498
513
|
totalTasks: 0,
|
|
@@ -512,6 +527,13 @@ function createNewProgress(changeId) {
|
|
|
512
527
|
consistencyFixes: 0,
|
|
513
528
|
},
|
|
514
529
|
phaseTransitions: [],
|
|
530
|
+
phaseDurations: [{
|
|
531
|
+
phase: initialPhase,
|
|
532
|
+
startedAt: getBeijingTime(),
|
|
533
|
+
durationMs: 0,
|
|
534
|
+
taskCount: 0,
|
|
535
|
+
completedTaskCount: 0,
|
|
536
|
+
}],
|
|
515
537
|
stats: {
|
|
516
538
|
tokenUsageTotal: 0,
|
|
517
539
|
contextLoad: 0,
|
|
@@ -538,12 +560,60 @@ export async function initializeProgress(changeId, initialPhase = 'propose') {
|
|
|
538
560
|
// Ensure metrics directory exists
|
|
539
561
|
await FileSystemUtils.createDirectory(metricsDir);
|
|
540
562
|
const progressPath = path.join(metricsDir, 'progress.json');
|
|
563
|
+
const timestamp = getBeijingTime();
|
|
541
564
|
// Create or update progress file (with recovery support)
|
|
542
565
|
let progress;
|
|
543
566
|
if (await FileSystemUtils.fileExists(progressPath)) {
|
|
544
567
|
const recovered = await recoverProgressJsonForWrite(progressPath);
|
|
545
568
|
if (recovered) {
|
|
546
569
|
progress = recovered;
|
|
570
|
+
// Fill missing fields with defaults
|
|
571
|
+
progress.toolCalls = progress.toolCalls || [];
|
|
572
|
+
progress.skillCalls = progress.skillCalls || [];
|
|
573
|
+
progress.hookTriggers = progress.hookTriggers || [];
|
|
574
|
+
progress.clarifications = progress.clarifications || [];
|
|
575
|
+
progress.filesModified = progress.filesModified || [];
|
|
576
|
+
progress.completedTasks = progress.completedTasks || [];
|
|
577
|
+
progress.deviationRecords = progress.deviationRecords || [];
|
|
578
|
+
progress.phaseTransitions = progress.phaseTransitions || [];
|
|
579
|
+
progress.phaseDurations = progress.phaseDurations || [];
|
|
580
|
+
progress.reviewStats = progress.reviewStats || {
|
|
581
|
+
loopCount: 0,
|
|
582
|
+
criticalFixes: 0,
|
|
583
|
+
testFixes: 0,
|
|
584
|
+
consistencyFixes: 0,
|
|
585
|
+
};
|
|
586
|
+
progress.stats = progress.stats || {
|
|
587
|
+
tokenUsageTotal: 0,
|
|
588
|
+
contextLoad: 0,
|
|
589
|
+
durationMs: { propose: 0, apply: 0, review: 0, archive: 0 },
|
|
590
|
+
};
|
|
591
|
+
const previousPhase = progress.phase;
|
|
592
|
+
if (previousPhase !== initialPhase) {
|
|
593
|
+
// Record phase transition
|
|
594
|
+
progress.phaseTransitions.push({
|
|
595
|
+
from: previousPhase,
|
|
596
|
+
to: initialPhase,
|
|
597
|
+
timestamp,
|
|
598
|
+
triggeredBy: 'initializeProgress',
|
|
599
|
+
});
|
|
600
|
+
// Update previous phase duration
|
|
601
|
+
const prevPhaseDuration = progress.phaseDurations.find(pd => pd.phase === previousPhase && !pd.endedAt);
|
|
602
|
+
if (prevPhaseDuration) {
|
|
603
|
+
prevPhaseDuration.endedAt = timestamp;
|
|
604
|
+
const startTime = new Date(prevPhaseDuration.startedAt).getTime();
|
|
605
|
+
const endTime = new Date(timestamp).getTime();
|
|
606
|
+
prevPhaseDuration.durationMs = endTime > startTime ? endTime - startTime : 0;
|
|
607
|
+
}
|
|
608
|
+
// Add new phase duration
|
|
609
|
+
progress.phaseDurations.push({
|
|
610
|
+
phase: initialPhase,
|
|
611
|
+
startedAt: timestamp,
|
|
612
|
+
durationMs: 0,
|
|
613
|
+
taskCount: 0,
|
|
614
|
+
completedTaskCount: 0,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
547
617
|
progress.phase = initialPhase;
|
|
548
618
|
progress.currentNode = initialPhase;
|
|
549
619
|
}
|
|
@@ -558,7 +628,7 @@ export async function initializeProgress(changeId, initialPhase = 'propose') {
|
|
|
558
628
|
progress.phase = initialPhase;
|
|
559
629
|
progress.currentNode = initialPhase;
|
|
560
630
|
}
|
|
561
|
-
progress.lastUpdatedAt =
|
|
631
|
+
progress.lastUpdatedAt = timestamp;
|
|
562
632
|
await atomicWriteJson(progressPath, progress);
|
|
563
633
|
}
|
|
564
634
|
/**
|
|
@@ -227,6 +227,7 @@ function parseTasks(content) {
|
|
|
227
227
|
const tasks = [];
|
|
228
228
|
const lines = content.split('\n');
|
|
229
229
|
let currentWave = 0;
|
|
230
|
+
let autoId = 0;
|
|
230
231
|
for (const line of lines) {
|
|
231
232
|
// Match wave headers: ### Wave 1, ### Wave 2, etc.
|
|
232
233
|
const waveMatch = line.match(/^###\s*Wave\s*(\d+)/i);
|
|
@@ -234,17 +235,35 @@ function parseTasks(content) {
|
|
|
234
235
|
currentWave = parseInt(waveMatch[1], 10);
|
|
235
236
|
continue;
|
|
236
237
|
}
|
|
237
|
-
// Match
|
|
238
|
-
const
|
|
239
|
-
if (
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
238
|
+
// Match any checkbox line: - [ ] ... or - [x] ...
|
|
239
|
+
const checkboxMatch = line.match(/^-\s*\[([x ])\]\s*(.+)/);
|
|
240
|
+
if (!checkboxMatch)
|
|
241
|
+
continue;
|
|
242
|
+
const statusChar = checkboxMatch[1];
|
|
243
|
+
const rest = checkboxMatch[2].trim();
|
|
244
|
+
// Skip review gate lines (legacy tasks.md files)
|
|
245
|
+
if (rest.includes('zhuanspec review') || rest.includes('审查门禁'))
|
|
246
|
+
continue;
|
|
247
|
+
// Extract task ID from multiple formats:
|
|
248
|
+
// Format 1: "1.1 description" or "1.1: description"
|
|
249
|
+
const numericIdMatch = rest.match(/^(\d+\.\d+)[:\s]/);
|
|
250
|
+
// Format 2: "T1: description" or "T1 description"
|
|
251
|
+
const tPrefixIdMatch = rest.match(/^(T\d+)[:\s]/i);
|
|
252
|
+
let taskId;
|
|
253
|
+
if (numericIdMatch) {
|
|
254
|
+
taskId = numericIdMatch[1];
|
|
255
|
+
}
|
|
256
|
+
else if (tPrefixIdMatch) {
|
|
257
|
+
taskId = tPrefixIdMatch[1].toUpperCase();
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
taskId = `task-${++autoId}`;
|
|
247
261
|
}
|
|
262
|
+
tasks.push({
|
|
263
|
+
taskId,
|
|
264
|
+
status: statusChar === 'x' ? 'completed' : 'pending',
|
|
265
|
+
wave: currentWave,
|
|
266
|
+
});
|
|
248
267
|
}
|
|
249
268
|
return tasks;
|
|
250
269
|
}
|