@zhuan-ai/zhuanspec 2.8.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.
@@ -531,7 +531,9 @@ export class ArchiveCommand {
531
531
  await this.copyDirectoryContentsIfExists(path.join(templateRoot, 'specs'), path.join(zhuanspecDir, 'specs'));
532
532
  console.log(chalk.gray(' → 同步 changes 目录...'));
533
533
  await this.copyDirectoryContentsIfExists(path.join(templateRoot, 'changes'), path.join(zhuanspecDir, 'changes'));
534
- console.log(chalk.green(`✅ 已拉取 ${businessDirection} 业务模板(specs/changes)。`));
534
+ console.log(chalk.gray(' → 同步 knowledge 目录...'));
535
+ await this.copyDirectoryContentsIfExists(path.join(templateRoot, 'knowledge'), path.join(zhuanspecDir, 'knowledge'));
536
+ console.log(chalk.green(`✅ 已拉取 ${businessDirection} 业务模板(specs/changes/knowledge)。`));
535
537
  }
536
538
  finally {
537
539
  rmSync(tempRepo, { recursive: true, force: true });
@@ -211,38 +211,80 @@ function extractMcpTool(toolName) {
211
211
  return parts.length >= 3 ? parts[2] : undefined;
212
212
  }
213
213
  /**
214
- * Detect active change from progress.json files
215
- * Scans changes directory metrics progress.json to find non-idle phase
216
- * 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).
217
220
  */
218
- async function detectActiveChange(cwd) {
221
+ async function detectActiveChange(cwd, filePath) {
219
222
  const changesDir = path.join(cwd, 'zhuanspec', 'changes');
220
223
  if (!await FileSystemUtils.directoryExists(changesDir)) {
221
224
  return null;
222
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
223
249
  const entries = await fs.readdir(changesDir, { withFileTypes: true });
250
+ const candidates = [];
224
251
  for (const entry of entries) {
225
252
  if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'archive')
226
253
  continue;
227
254
  const progressPath = path.join(changesDir, entry.name, 'metrics', 'progress.json');
228
- if (await FileSystemUtils.fileExists(progressPath)) {
229
- try {
230
- const progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
231
- if (progress.phase && progress.phase !== 'idle' && PHASE_ORDER.includes(progress.phase)) {
232
- return { changeId: entry.name, phase: progress.phase };
233
- }
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;
234
267
  }
235
- catch {
236
- // 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();
237
272
  }
273
+ candidates.push({ changeId: entry.name, phase: progress.phase, sortKey });
274
+ }
275
+ catch {
276
+ // Ignore parse errors
238
277
  }
239
278
  }
240
- 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 };
241
283
  }
242
284
  async function runRecordProgress(filePath, toolName, success, stdinData) {
243
285
  const cwd = process.cwd();
244
- // Priority: progress.json > environment variables
245
- const activeChange = await detectActiveChange(cwd);
286
+ // Priority: filePath-derived > progress.json scan > environment variables
287
+ const activeChange = await detectActiveChange(cwd, filePath);
246
288
  const phase = activeChange?.phase || process.env.ZHUANSPEC_PHASE || 'idle';
247
289
  const changeId = activeChange?.changeId || process.env.ZHUANSPEC_CHANGE_ID || '';
248
290
  const currentTask = process.env.ZHUANSPEC_CURRENT_TASK || '';
@@ -641,7 +683,7 @@ export async function initializeProgress(changeId, initialPhase = 'propose') {
641
683
  */
642
684
  export async function recordHookTrigger(hookName, hookPhase, success, options) {
643
685
  const cwd = process.cwd();
644
- const activeChange = await detectActiveChange(cwd);
686
+ const activeChange = await detectActiveChange(cwd, options?.filePath);
645
687
  const phase = activeChange?.phase || process.env.ZHUANSPEC_PHASE || 'idle';
646
688
  const changeId = activeChange?.changeId || process.env.ZHUANSPEC_CHANGE_ID || '';
647
689
  if (!changeId || phase === 'idle')
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 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhuan-ai/zhuanspec",
3
- "version": "2.8.0",
3
+ "version": "2.9.5",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "zhuanspec",