@zhuan-ai/zhuanspec 2.6.0 → 2.9.5

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.
@@ -8,13 +8,27 @@
8
8
  */
9
9
  import { Phase } from '../../utils/phase-utils.js';
10
10
  /**
11
- * Get Beijing time (UTC+8) as ISO string format
12
- * Returns format: "2026-04-19T18:35:22.934+08:00"
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-19T18-35-22"
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: phaseMatch?.[1] || 'unknown',
92
- currentNode: phaseMatch?.[1] || 'unknown',
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 ISO string format
118
- * Returns format: "2026-04-19T18:35:22.934+08:00"
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 as ISO string with +08:00 timezone
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
- const ms = String(beijingTime.getMilliseconds()).padStart(3, '0');
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-19T18-35-22"
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}T${hours}-${minutes}-${seconds}`;
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
@@ -204,38 +211,80 @@ function extractMcpTool(toolName) {
204
211
  return parts.length >= 3 ? parts[2] : undefined;
205
212
  }
206
213
  /**
207
- * Detect active change from progress.json files
208
- * Scans changes directory metrics progress.json to find non-idle phase
209
- * Returns null if no active change found
214
+ * Detect active change from progress.json files.
215
+ *
216
+ * Strategy:
217
+ * 1. Primary — if filePath is under zhuanspec/changes/{changeId}/, extract changeId directly.
218
+ * 2. Fallback — scan all changes and return the most recently updated one
219
+ * (by lastUpdatedAt field; stub files without it fall back to filesystem mtime).
210
220
  */
211
- async function detectActiveChange(cwd) {
221
+ async function detectActiveChange(cwd, filePath) {
212
222
  const changesDir = path.join(cwd, 'zhuanspec', 'changes');
213
223
  if (!await FileSystemUtils.directoryExists(changesDir)) {
214
224
  return null;
215
225
  }
226
+ // Primary: derive changeId from the file being modified
227
+ if (filePath) {
228
+ const absFilePath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
229
+ const changesDirAbs = path.resolve(changesDir);
230
+ if (absFilePath.startsWith(changesDirAbs + path.sep)) {
231
+ const changeId = path.relative(changesDirAbs, absFilePath).split(path.sep)[0];
232
+ if (changeId && changeId !== 'archive') {
233
+ const progressPath = path.join(changesDirAbs, changeId, 'metrics', 'progress.json');
234
+ if (await FileSystemUtils.fileExists(progressPath)) {
235
+ try {
236
+ const progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
237
+ if (progress.phase && progress.phase !== 'idle' && PHASE_ORDER.includes(progress.phase)) {
238
+ return { changeId, phase: progress.phase };
239
+ }
240
+ }
241
+ catch {
242
+ // Fall through to fallback
243
+ }
244
+ }
245
+ }
246
+ }
247
+ }
248
+ // Fallback: most recently updated active change
216
249
  const entries = await fs.readdir(changesDir, { withFileTypes: true });
250
+ const candidates = [];
217
251
  for (const entry of entries) {
218
252
  if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'archive')
219
253
  continue;
220
254
  const progressPath = path.join(changesDir, entry.name, 'metrics', 'progress.json');
221
- if (await FileSystemUtils.fileExists(progressPath)) {
222
- try {
223
- const progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
224
- if (progress.phase && progress.phase !== 'idle' && PHASE_ORDER.includes(progress.phase)) {
225
- return { changeId: entry.name, phase: progress.phase };
226
- }
255
+ if (!await FileSystemUtils.fileExists(progressPath))
256
+ continue;
257
+ try {
258
+ const progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
259
+ if (!progress.phase || progress.phase === 'idle' || !PHASE_ORDER.includes(progress.phase))
260
+ continue;
261
+ let sortKey;
262
+ if (progress.lastUpdatedAt) {
263
+ // "2026-04-22 11:39:59" (Beijing time) — replace space with T for Date parsing
264
+ sortKey = new Date(progress.lastUpdatedAt.replace(' ', 'T')).getTime();
265
+ if (isNaN(sortKey))
266
+ sortKey = 0;
227
267
  }
228
- catch {
229
- // Ignore parse errors
268
+ else {
269
+ // Stub files (e.g. techDesign) lack lastUpdatedAt — use filesystem mtime
270
+ const stat = await fs.stat(progressPath);
271
+ sortKey = stat.mtime.getTime();
230
272
  }
273
+ candidates.push({ changeId: entry.name, phase: progress.phase, sortKey });
274
+ }
275
+ catch {
276
+ // Ignore parse errors
231
277
  }
232
278
  }
233
- return null;
279
+ if (candidates.length === 0)
280
+ return null;
281
+ candidates.sort((a, b) => b.sortKey - a.sortKey);
282
+ return { changeId: candidates[0].changeId, phase: candidates[0].phase };
234
283
  }
235
284
  async function runRecordProgress(filePath, toolName, success, stdinData) {
236
285
  const cwd = process.cwd();
237
- // Priority: progress.json > environment variables
238
- const activeChange = await detectActiveChange(cwd);
286
+ // Priority: filePath-derived > progress.json scan > environment variables
287
+ const activeChange = await detectActiveChange(cwd, filePath);
239
288
  const phase = activeChange?.phase || process.env.ZHUANSPEC_PHASE || 'idle';
240
289
  const changeId = activeChange?.changeId || process.env.ZHUANSPEC_CHANGE_ID || '';
241
290
  const currentTask = process.env.ZHUANSPEC_CURRENT_TASK || '';
@@ -277,6 +326,7 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
277
326
  consistencyFixes: 0,
278
327
  };
279
328
  progress.phaseTransitions = progress.phaseTransitions || [];
329
+ progress.phaseDurations = progress.phaseDurations || [];
280
330
  progress.stats = progress.stats || {
281
331
  tokenUsageTotal: 0,
282
332
  contextLoad: 0,
@@ -375,8 +425,14 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
375
425
  progress.currentNode = process.env.ZHUANSPEC_CURRENT_NODE || phase;
376
426
  progress.currentTask = currentTask || progress.currentTask;
377
427
  progress.toolCalls.push(toolCall);
378
- if (filePath && !progress.filesModified.includes(filePath)) {
379
- progress.filesModified.push(filePath);
428
+ const WRITE_TOOLS = new Set(['Write', 'Edit', 'NotebookEdit']);
429
+ if (filePath && WRITE_TOOLS.has(toolName) && !progress.filesModified.includes(filePath)) {
430
+ // Exclude ZhuanSpec internal files (spec/metrics/doc files inside zhuanspec/ dir)
431
+ const absFilePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
432
+ const zhuanspecDirPrefix = path.resolve(process.cwd(), 'zhuanspec') + path.sep;
433
+ if (!absFilePath.startsWith(zhuanspecDirPrefix)) {
434
+ progress.filesModified.push(filePath);
435
+ }
380
436
  }
381
437
  // Try to estimate lines from content
382
438
  const content = stdinData.tool_input?.content;
@@ -486,13 +542,14 @@ function generateProgressBar(percentage, blocks) {
486
542
  }
487
543
  function createNewProgress(changeId) {
488
544
  const sessionId = `sess-${getBeijingTimeForFilename()}`;
545
+ const initialPhase = process.env.ZHUANSPEC_PHASE || 'idle';
489
546
  return {
490
547
  changeId,
491
548
  sessionId,
492
549
  startedAt: getBeijingTime(),
493
550
  lastUpdatedAt: getBeijingTime(),
494
- phase: process.env.ZHUANSPEC_PHASE || 'idle',
495
- currentNode: process.env.ZHUANSPEC_CURRENT_NODE || process.env.ZHUANSPEC_PHASE || 'idle',
551
+ phase: initialPhase,
552
+ currentNode: process.env.ZHUANSPEC_CURRENT_NODE || initialPhase,
496
553
  currentTask: '',
497
554
  completedTasks: [],
498
555
  totalTasks: 0,
@@ -512,6 +569,13 @@ function createNewProgress(changeId) {
512
569
  consistencyFixes: 0,
513
570
  },
514
571
  phaseTransitions: [],
572
+ phaseDurations: [{
573
+ phase: initialPhase,
574
+ startedAt: getBeijingTime(),
575
+ durationMs: 0,
576
+ taskCount: 0,
577
+ completedTaskCount: 0,
578
+ }],
515
579
  stats: {
516
580
  tokenUsageTotal: 0,
517
581
  contextLoad: 0,
@@ -538,12 +602,60 @@ export async function initializeProgress(changeId, initialPhase = 'propose') {
538
602
  // Ensure metrics directory exists
539
603
  await FileSystemUtils.createDirectory(metricsDir);
540
604
  const progressPath = path.join(metricsDir, 'progress.json');
605
+ const timestamp = getBeijingTime();
541
606
  // Create or update progress file (with recovery support)
542
607
  let progress;
543
608
  if (await FileSystemUtils.fileExists(progressPath)) {
544
609
  const recovered = await recoverProgressJsonForWrite(progressPath);
545
610
  if (recovered) {
546
611
  progress = recovered;
612
+ // Fill missing fields with defaults
613
+ progress.toolCalls = progress.toolCalls || [];
614
+ progress.skillCalls = progress.skillCalls || [];
615
+ progress.hookTriggers = progress.hookTriggers || [];
616
+ progress.clarifications = progress.clarifications || [];
617
+ progress.filesModified = progress.filesModified || [];
618
+ progress.completedTasks = progress.completedTasks || [];
619
+ progress.deviationRecords = progress.deviationRecords || [];
620
+ progress.phaseTransitions = progress.phaseTransitions || [];
621
+ progress.phaseDurations = progress.phaseDurations || [];
622
+ progress.reviewStats = progress.reviewStats || {
623
+ loopCount: 0,
624
+ criticalFixes: 0,
625
+ testFixes: 0,
626
+ consistencyFixes: 0,
627
+ };
628
+ progress.stats = progress.stats || {
629
+ tokenUsageTotal: 0,
630
+ contextLoad: 0,
631
+ durationMs: { propose: 0, apply: 0, review: 0, archive: 0 },
632
+ };
633
+ const previousPhase = progress.phase;
634
+ if (previousPhase !== initialPhase) {
635
+ // Record phase transition
636
+ progress.phaseTransitions.push({
637
+ from: previousPhase,
638
+ to: initialPhase,
639
+ timestamp,
640
+ triggeredBy: 'initializeProgress',
641
+ });
642
+ // Update previous phase duration
643
+ const prevPhaseDuration = progress.phaseDurations.find(pd => pd.phase === previousPhase && !pd.endedAt);
644
+ if (prevPhaseDuration) {
645
+ prevPhaseDuration.endedAt = timestamp;
646
+ const startTime = new Date(prevPhaseDuration.startedAt).getTime();
647
+ const endTime = new Date(timestamp).getTime();
648
+ prevPhaseDuration.durationMs = endTime > startTime ? endTime - startTime : 0;
649
+ }
650
+ // Add new phase duration
651
+ progress.phaseDurations.push({
652
+ phase: initialPhase,
653
+ startedAt: timestamp,
654
+ durationMs: 0,
655
+ taskCount: 0,
656
+ completedTaskCount: 0,
657
+ });
658
+ }
547
659
  progress.phase = initialPhase;
548
660
  progress.currentNode = initialPhase;
549
661
  }
@@ -558,7 +670,7 @@ export async function initializeProgress(changeId, initialPhase = 'propose') {
558
670
  progress.phase = initialPhase;
559
671
  progress.currentNode = initialPhase;
560
672
  }
561
- progress.lastUpdatedAt = getBeijingTime();
673
+ progress.lastUpdatedAt = timestamp;
562
674
  await atomicWriteJson(progressPath, progress);
563
675
  }
564
676
  /**
@@ -571,7 +683,7 @@ export async function initializeProgress(changeId, initialPhase = 'propose') {
571
683
  */
572
684
  export async function recordHookTrigger(hookName, hookPhase, success, options) {
573
685
  const cwd = process.cwd();
574
- const activeChange = await detectActiveChange(cwd);
686
+ const activeChange = await detectActiveChange(cwd, options?.filePath);
575
687
  const phase = activeChange?.phase || process.env.ZHUANSPEC_PHASE || 'idle';
576
688
  const changeId = activeChange?.changeId || process.env.ZHUANSPEC_CHANGE_ID || '';
577
689
  if (!changeId || phase === 'idle')
@@ -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 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
- });
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
  }
package/dist/core/init.js CHANGED
@@ -314,7 +314,7 @@ export class InitCommand {
314
314
  }
315
315
  const specTemplateSpinner = this.startSpinner(`正在同步 ${this.businessDirection} 业务规范模板...`);
316
316
  const syncResult = this.syncBusinessSpecTemplate(zhuanspecPath);
317
- if (syncResult.syncedSpecs || syncResult.syncedChanges) {
317
+ if (syncResult.syncedSpecs || syncResult.syncedChanges || syncResult.syncedKnowledge) {
318
318
  const syncedParts = [];
319
319
  if (syncResult.syncedSpecs) {
320
320
  syncedParts.push('specs');
@@ -322,6 +322,9 @@ export class InitCommand {
322
322
  if (syncResult.syncedChanges) {
323
323
  syncedParts.push('changes');
324
324
  }
325
+ if (syncResult.syncedKnowledge) {
326
+ syncedParts.push('knowledge');
327
+ }
325
328
  specTemplateSpinner.stopAndPersist({
326
329
  symbol: PALETTE.white('▌'),
327
330
  text: PALETTE.white(`已从远端模板同步 ${syncedParts.join('、')} 目录`),
@@ -791,28 +794,29 @@ export class InitCommand {
791
794
  }
792
795
  syncBusinessSpecTemplate(zhuanspecPath) {
793
796
  if (!this.businessDirection) {
794
- return { syncedSpecs: false, syncedChanges: false };
797
+ return { syncedSpecs: false, syncedChanges: false, syncedKnowledge: false };
795
798
  }
796
799
  const tempDir = this.cloneRemoteArchitectureRepoToTemp();
797
800
  if (!tempDir) {
798
- return { syncedSpecs: false, syncedChanges: false };
801
+ return { syncedSpecs: false, syncedChanges: false, syncedKnowledge: false };
799
802
  }
800
803
  try {
801
804
  const specsRoot = path.join(tempDir, 'specs');
802
805
  const businessRoot = this.resolveBusinessDirectionRoot(specsRoot);
803
806
  if (!businessRoot) {
804
- return { syncedSpecs: false, syncedChanges: false };
807
+ return { syncedSpecs: false, syncedChanges: false, syncedKnowledge: false };
805
808
  }
806
809
  const templateRoot = this.findFirstTemplateSourceRoot(businessRoot);
807
810
  if (!templateRoot) {
808
- return { syncedSpecs: false, syncedChanges: false };
811
+ return { syncedSpecs: false, syncedChanges: false, syncedKnowledge: false };
809
812
  }
810
813
  const syncedSpecs = this.copyDirectoryContentsIfExists(path.join(templateRoot, 'specs'), path.join(zhuanspecPath, 'specs'));
811
814
  const syncedChanges = this.copyDirectoryContentsIfExists(path.join(templateRoot, 'changes'), path.join(zhuanspecPath, 'changes'));
812
- return { syncedSpecs, syncedChanges };
815
+ const syncedKnowledge = this.copyDirectoryContentsIfExists(path.join(templateRoot, 'knowledge'), path.join(zhuanspecPath, 'knowledge'));
816
+ return { syncedSpecs, syncedChanges, syncedKnowledge };
813
817
  }
814
818
  catch {
815
- return { syncedSpecs: false, syncedChanges: false };
819
+ return { syncedSpecs: false, syncedChanges: false, syncedKnowledge: false };
816
820
  }
817
821
  finally {
818
822
  rmSync(tempDir, { recursive: true, force: true });