@zhuan-ai/zhuanspec 2.15.7 → 2.16.2

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.
Files changed (43) hide show
  1. package/dist/cli/hooks.d.ts +13 -1
  2. package/dist/cli/hooks.js +75 -2
  3. package/dist/cli/index.js +111 -4
  4. package/dist/commands/accuracy.js +5 -1
  5. package/dist/commands/knowledge.d.ts +21 -0
  6. package/dist/commands/knowledge.js +80 -0
  7. package/dist/commands/progress.d.ts +78 -0
  8. package/dist/commands/progress.js +349 -3
  9. package/dist/core/archive.js +28 -0
  10. package/dist/core/configurators/codex.d.ts +55 -1
  11. package/dist/core/configurators/codex.js +234 -2
  12. package/dist/core/corrections/select-candidates.d.ts +58 -0
  13. package/dist/core/corrections/select-candidates.js +357 -0
  14. package/dist/core/hooks/collect-knowledge.js +80 -5
  15. package/dist/core/hooks/deviation-check.d.ts +26 -1
  16. package/dist/core/hooks/deviation-check.js +32 -184
  17. package/dist/core/hooks/knowledge-index.d.ts +84 -0
  18. package/dist/core/hooks/knowledge-index.js +270 -0
  19. package/dist/core/hooks/post-archive.js +25 -13
  20. package/dist/core/hooks/record-progress.d.ts +64 -1
  21. package/dist/core/hooks/record-progress.js +224 -54
  22. package/dist/core/hooks/summarize.js +107 -20
  23. package/dist/core/hooks/user-input-hook.d.ts +38 -2
  24. package/dist/core/hooks/user-input-hook.js +346 -21
  25. package/dist/core/init.d.ts +16 -0
  26. package/dist/core/init.js +208 -11
  27. package/dist/core/metrics/code-accuracy.d.ts +152 -3
  28. package/dist/core/metrics/code-accuracy.js +323 -19
  29. package/dist/core/templates/agents-root-stub.d.ts +1 -1
  30. package/dist/core/templates/agents-root-stub.js +1 -0
  31. package/dist/core/templates/agents-template.d.ts +1 -1
  32. package/dist/core/templates/agents-template.js +27 -25
  33. package/dist/core/templates/codex-hooks-template.d.ts +37 -0
  34. package/dist/core/templates/codex-hooks-template.js +33 -2
  35. package/dist/core/templates/slash-command-templates.js +208 -66
  36. package/dist/core/templates/tasks-template.js +66 -14
  37. package/dist/core/update.js +17 -0
  38. package/dist/core/validation/strict-rules.d.ts +28 -0
  39. package/dist/core/validation/strict-rules.js +284 -0
  40. package/dist/utils/hook-merge.d.ts +6 -0
  41. package/dist/utils/hook-merge.js +33 -2
  42. package/dist/utils/phase-utils.js +5 -0
  43. package/package.json +22 -20
@@ -159,6 +159,34 @@ export declare function checkTaskIdFormat(tasksContent: string): StrictCheckResu
159
159
  * This ensures automated progress tracking (parseTasks, record-progress, review gate) works correctly.
160
160
  */
161
161
  export declare function checkTaskCheckboxFormat(tasksContent: string): StrictCheckResult;
162
+ /**
163
+ * Rule: task-layer-tag (WARN level)
164
+ *
165
+ * Each task MUST carry an @layer:xxx annotation whose value is in VALID_LAYER_TAGS.
166
+ * Currently WARN-level rollout; will be upgraded to ERROR in a future release.
167
+ */
168
+ export declare function checkTaskLayerTag(parsedTasks: ParsedTask[]): StrictCheckResult;
169
+ /**
170
+ * Rule: task-layer-wave-consistency (WARN level)
171
+ *
172
+ * Wave 分层一致性:
173
+ * Wave 1 仅允许 dao / assemble / ddl / config
174
+ * Wave 2 仅允许 domain / application
175
+ * Wave 3 仅允许 entry-* / fe
176
+ */
177
+ export declare function checkTaskLayerWaveConsistency(parsedTasks: ParsedTask[]): StrictCheckResult;
178
+ /**
179
+ * Rule: external-dependency-matrix-complete (WARN level)
180
+ *
181
+ * 校验 design.md 含 「外部依赖可用性矩阵」 或类似章节,且表格无 TBD / 空 / ⚠️ 未解。
182
+ */
183
+ export declare function checkExternalDependencyMatrix(changeDir: string): StrictCheckResult;
184
+ /**
185
+ * Rule: data-storage-strategy-required (WARN level)
186
+ *
187
+ * 当 tech-spec 含 CREATE TABLE / t_xxx 新增表关键词时,design.md 必须含「数据存储策略」章节且无 TBD。
188
+ */
189
+ export declare function checkDataStorageStrategy(changeDir: string): StrictCheckResult;
162
190
  /**
163
191
  * Main entry: Run all strict validation rules
164
192
  */
@@ -33,6 +33,30 @@ const TABLE_DATA_ROW_PATTERN = /^\|(?![-:|\s]+\|$)[^\n]+\|/gm;
33
33
  const SKILL_TAG_PATTERN = /@skill:([^\s@]+)/g;
34
34
  // Wave header pattern
35
35
  const WAVE_HEADER_PATTERN = /^###\s+Wave\s+(\d+)/im;
36
+ // @layer tag whitelist (WARN-level rollout; upgrade to ERROR in next release)
37
+ const VALID_LAYER_TAGS = [
38
+ 'dao', 'assemble', 'domain', 'application',
39
+ 'entry-scf', 'entry-mq-producer', 'entry-mq-consumer', 'entry-job',
40
+ 'fe', 'config', 'ddl',
41
+ ];
42
+ // Wave-layer consistency: which @layer values are allowed in each wave
43
+ const WAVE_ALLOWED_LAYERS = {
44
+ 1: ['dao', 'assemble', 'ddl', 'config'],
45
+ 2: ['domain', 'application'],
46
+ 3: ['entry-scf', 'entry-mq-producer', 'entry-mq-consumer', 'entry-job', 'fe'],
47
+ };
48
+ // @layer:xxx extraction pattern
49
+ const LAYER_TAG_PATTERN = /@layer:([a-z][a-z-]*)/i;
50
+ const MIDDLEWARE_SKILL_RULES = [
51
+ { keyword: /\bzzmq\b|\bRocketMQ\b|@MessageConsumer|@MessageProducer|\bTopic\s*[:=\uff1a]/i, requiredSkill: 'java-middleware-usage-skill', description: 'ZZMQ / RocketMQ 消息中间件' },
52
+ { keyword: /\bxxl-?job\b|@XxlJob|@JobHandler/i, requiredSkill: 'java-middleware-usage-skill', description: 'XXL-Job 定时任务' },
53
+ { keyword: /\bRedisson\b|\bRedisTemplate\b|\bZZLock\b|@RedissonLock/i, requiredSkill: 'java-middleware-usage-skill', description: 'Redisson / ZZLock 分布式锁' },
54
+ { keyword: /@Cacheable\b|\bLettuce\b|\b缓存\b/i, requiredSkill: 'java-middleware-usage-skill', description: 'Redis 缓存' },
55
+ { keyword: /@ApolloJsonValue|\bApollo\s*(?:配置|config|配置中心)/i, requiredSkill: 'java-middleware-usage-skill', description: 'Apollo 配置中心' },
56
+ { keyword: /@ServiceContract|@SCFInterface|\bscf-client\b|\bSCF\s*(?:接口|服务|RPC)/i, requiredSkill: 'java-scf-rpc-usage-skill', description: 'SCF 远程调用' },
57
+ { keyword: /\bMapper\.xml\b|\bMyBatis\b|@MyBatis\b|\bselectBy[A-Z]\w*/i, requiredSkill: 'java-db-schema-standards', description: 'MyBatis DAO' },
58
+ { keyword: /\bElasticsearch\b|\bES\s*异构索引\b|\bES\s*索引\b/i, requiredSkill: 'es-integration', description: 'Elasticsearch / ES 异构索引' },
59
+ ];
36
60
  /**
37
61
  * Rule 0: test-case-inquiry-logged
38
62
  *
@@ -225,6 +249,20 @@ export function checkSkillTagsValid(tasksContent, parsedTasks, knownSkillNames)
225
249
  result.errors.push(`任务 ${task.id}: @skill:${skill} 不是已知的 skill。可用 skills: ${availableSkills}`);
226
250
  }
227
251
  }
252
+ // f. Middleware keyword reverse lookup (WARN level)
253
+ // If task description mentions middleware keywords (zzmq / @XxlJob / Redisson / Apollo / SCF / MyBatis),
254
+ // then the corresponding skill MUST be declared in task.skills.
255
+ // Skip rules whose requiredSkill is not registered in knownSkillNames, to avoid cascading
256
+ // "unknown skill" errors when the user follows the WARN hint.
257
+ const taskSkillsLower = new Set(task.skills.map(s => s.toLowerCase()));
258
+ for (const rule of MIDDLEWARE_SKILL_RULES) {
259
+ if (knownSkillNames.length > 0 && !knownSkillSet.has(rule.requiredSkill.toLowerCase())) {
260
+ continue;
261
+ }
262
+ if (rule.keyword.test(task.description) && !taskSkillsLower.has(rule.requiredSkill.toLowerCase())) {
263
+ result.warnings.push(`任务 ${task.id}: 描述中出现中间件关键词(${rule.description}),但未引用 @skill:${rule.requiredSkill}`);
264
+ }
265
+ }
228
266
  }
229
267
  // Passed if no errors
230
268
  result.passed = result.errors.length === 0;
@@ -1339,6 +1377,245 @@ export function checkTaskCheckboxFormat(tasksContent) {
1339
1377
  result.passed = result.errors.length === 0;
1340
1378
  return result;
1341
1379
  }
1380
+ /**
1381
+ * Rule: task-layer-tag (WARN level)
1382
+ *
1383
+ * Each task MUST carry an @layer:xxx annotation whose value is in VALID_LAYER_TAGS.
1384
+ * Currently WARN-level rollout; will be upgraded to ERROR in a future release.
1385
+ */
1386
+ export function checkTaskLayerTag(parsedTasks) {
1387
+ const result = {
1388
+ ruleId: 'task-layer-tag',
1389
+ ruleName: '任务分层标签校验',
1390
+ passed: false,
1391
+ errors: [],
1392
+ warnings: [],
1393
+ };
1394
+ if (parsedTasks.length === 0) {
1395
+ result.passed = true;
1396
+ return result;
1397
+ }
1398
+ const validLayerSet = new Set(VALID_LAYER_TAGS);
1399
+ for (const task of parsedTasks) {
1400
+ const match = task.description.match(LAYER_TAG_PATTERN);
1401
+ if (!match) {
1402
+ result.warnings.push(`任务 ${task.id}: 缺少 @layer 标签。白名单:${VALID_LAYER_TAGS.join(' / ')}`);
1403
+ continue;
1404
+ }
1405
+ const layer = match[1].toLowerCase();
1406
+ if (!validLayerSet.has(layer)) {
1407
+ result.warnings.push(`任务 ${task.id}: @layer:${layer} 不在白名单内。可选值:${VALID_LAYER_TAGS.join(' / ')}`);
1408
+ }
1409
+ }
1410
+ // WARN-level rule: always pass, surface issues via warnings only
1411
+ result.passed = true;
1412
+ return result;
1413
+ }
1414
+ /**
1415
+ * Rule: task-layer-wave-consistency (WARN level)
1416
+ *
1417
+ * Wave 分层一致性:
1418
+ * Wave 1 仅允许 dao / assemble / ddl / config
1419
+ * Wave 2 仅允许 domain / application
1420
+ * Wave 3 仅允许 entry-* / fe
1421
+ */
1422
+ export function checkTaskLayerWaveConsistency(parsedTasks) {
1423
+ const result = {
1424
+ ruleId: 'task-layer-wave-consistency',
1425
+ ruleName: 'Wave 分层一致性',
1426
+ passed: false,
1427
+ errors: [],
1428
+ warnings: [],
1429
+ };
1430
+ if (parsedTasks.length === 0) {
1431
+ result.passed = true;
1432
+ return result;
1433
+ }
1434
+ let executionPlan;
1435
+ try {
1436
+ executionPlan = generateExecutionPlan(parsedTasks);
1437
+ }
1438
+ catch {
1439
+ result.passed = true;
1440
+ return result;
1441
+ }
1442
+ if (executionPlan.hasCycle) {
1443
+ result.passed = true;
1444
+ return result;
1445
+ }
1446
+ for (const wave of executionPlan.waves) {
1447
+ const allowed = WAVE_ALLOWED_LAYERS[wave.wave];
1448
+ if (!allowed)
1449
+ continue; // no rule for this wave
1450
+ for (const task of wave.tasks) {
1451
+ const match = task.description.match(LAYER_TAG_PATTERN);
1452
+ if (!match)
1453
+ continue; // missing @layer already warned by task-layer-tag
1454
+ const layer = match[1].toLowerCase();
1455
+ if (!allowed.includes(layer)) {
1456
+ result.warnings.push(`任务 ${task.id}(Wave ${wave.wave})@layer:${layer} 与 Wave 分层建议不一致。Wave ${wave.wave} 建议:${allowed.join(' / ')}`);
1457
+ }
1458
+ }
1459
+ }
1460
+ result.passed = true;
1461
+ return result;
1462
+ }
1463
+ /**
1464
+ * Rule: external-dependency-matrix-complete (WARN level)
1465
+ *
1466
+ * 校验 design.md 含 「外部依赖可用性矩阵」 或类似章节,且表格无 TBD / 空 / ⚠️ 未解。
1467
+ */
1468
+ export function checkExternalDependencyMatrix(changeDir) {
1469
+ const result = {
1470
+ ruleId: 'external-dependency-matrix-complete',
1471
+ ruleName: '外部依赖矩阵完整性',
1472
+ passed: false,
1473
+ errors: [],
1474
+ warnings: [],
1475
+ };
1476
+ const designPath = join(changeDir, 'design.md');
1477
+ if (!existsSync(designPath)) {
1478
+ result.passed = true;
1479
+ return result;
1480
+ }
1481
+ let designContent;
1482
+ try {
1483
+ designContent = readFileSync(designPath, 'utf-8');
1484
+ }
1485
+ catch {
1486
+ result.passed = true;
1487
+ return result;
1488
+ }
1489
+ const sectionRegex = /^##+\s+(?:外部依赖(?:可用性)?矩阵|External\s+Dependency\s+Matrix)[^\n]*$/im;
1490
+ const sectionMatch = designContent.match(sectionRegex);
1491
+ if (!sectionMatch) {
1492
+ result.warnings.push('design.md 未找到「外部依赖可用性矩阵」章节——若本变更不涉及外部依赖可忽略;否则请按 6 列(依赖名称 / 类型 / 状态 / 版本环境 / 兜底阻塞策略 / 负责方)补齐');
1493
+ result.passed = true;
1494
+ return result;
1495
+ }
1496
+ const startIdx = sectionMatch.index + sectionMatch[0].length;
1497
+ const rest = designContent.slice(startIdx);
1498
+ const nextSectionMatch = rest.match(/^##+\s+/m);
1499
+ const sectionBody = nextSectionMatch ? rest.slice(0, nextSectionMatch.index) : rest;
1500
+ const tableRows = sectionBody.match(/^\|[^\n]+\|\s*$/gm) || [];
1501
+ const dataRows = tableRows.filter(r => !/^\|[\s\-:|]+\|$/.test(r.trim()));
1502
+ if (dataRows.length < 2) {
1503
+ result.warnings.push('「外部依赖可用性矩阵」表格数据行 < 1,请至少填写一行或明确标注无外部依赖');
1504
+ }
1505
+ // Validate header has at least 6 columns
1506
+ const headerRow = dataRows[0];
1507
+ if (headerRow) {
1508
+ const cols = headerRow.split('|').map(s => s.trim()).filter(s => s.length > 0);
1509
+ if (cols.length < 6) {
1510
+ result.warnings.push(`「外部依赖可用性矩阵」表头列数不足(当前 ${cols.length},期望 6):依赖名称 / 类型 / 状态 / 版本环境 / 兜底阻塞策略 / 负责方`);
1511
+ }
1512
+ }
1513
+ // Scan data rows (skip header) for TBD / unresolved markers
1514
+ for (let i = 1; i < dataRows.length; i++) {
1515
+ const row = dataRows[i];
1516
+ if (/\bTBD\b|\bTODO\b|\bPENDING\b|⚠️|待确认|待填/i.test(row)) {
1517
+ result.warnings.push(`外部依赖矩阵第 ${i} 行含未解标记(TBD/TODO/⚠️):${row.trim().substring(0, 80)}`);
1518
+ }
1519
+ }
1520
+ result.passed = true;
1521
+ return result;
1522
+ }
1523
+ /**
1524
+ * Rule: data-storage-strategy-required (WARN level)
1525
+ *
1526
+ * 当 tech-spec 含 CREATE TABLE / t_xxx 新增表关键词时,design.md 必须含「数据存储策略」章节且无 TBD。
1527
+ */
1528
+ export function checkDataStorageStrategy(changeDir) {
1529
+ const result = {
1530
+ ruleId: 'data-storage-strategy-required',
1531
+ ruleName: '新增表数据存储策略',
1532
+ passed: false,
1533
+ errors: [],
1534
+ warnings: [],
1535
+ };
1536
+ // 1. Scan tech-spec candidates to detect new-table keywords
1537
+ const techSpecCandidates = [];
1538
+ const techDesignDir = join(changeDir, 'techDesign');
1539
+ if (existsSync(techDesignDir)) {
1540
+ try {
1541
+ for (const entry of readdirSync(techDesignDir)) {
1542
+ if (entry.endsWith('.md'))
1543
+ techSpecCandidates.push(join(techDesignDir, entry));
1544
+ }
1545
+ }
1546
+ catch { /* ignore */ }
1547
+ }
1548
+ try {
1549
+ for (const entry of readdirSync(changeDir)) {
1550
+ if (entry.endsWith('.md') && entry !== 'design.md' && entry !== 'tasks.md' && entry !== 'proposal.md') {
1551
+ techSpecCandidates.push(join(changeDir, entry));
1552
+ }
1553
+ }
1554
+ }
1555
+ catch { /* ignore */ }
1556
+ const NEW_TABLE_PATTERN = /CREATE\s+TABLE|\bt_[a-z][a-z0-9_]*\b|新增表|新建表/i;
1557
+ let hasNewTable = false;
1558
+ for (const p of techSpecCandidates) {
1559
+ try {
1560
+ const content = readFileSync(p, 'utf-8');
1561
+ if (NEW_TABLE_PATTERN.test(content)) {
1562
+ hasNewTable = true;
1563
+ break;
1564
+ }
1565
+ }
1566
+ catch { /* ignore */ }
1567
+ }
1568
+ if (!hasNewTable) {
1569
+ result.passed = true;
1570
+ return result;
1571
+ }
1572
+ // 2. Verify design.md has 「数据存储策略」 section
1573
+ const designPath = join(changeDir, 'design.md');
1574
+ if (!existsSync(designPath)) {
1575
+ result.warnings.push('检测到新增表(CREATE TABLE / t_xxx),但 design.md 不存在,无法校验数据存储策略');
1576
+ result.passed = true;
1577
+ return result;
1578
+ }
1579
+ let designContent;
1580
+ try {
1581
+ designContent = readFileSync(designPath, 'utf-8');
1582
+ }
1583
+ catch {
1584
+ result.passed = true;
1585
+ return result;
1586
+ }
1587
+ const storageSectionRegex = /^##+\s+数据存储策略[^\n]*$/im;
1588
+ const storageMatch = designContent.match(storageSectionRegex);
1589
+ if (!storageMatch) {
1590
+ result.warnings.push('检测到新增表关键词,但 design.md 缺少「## 数据存储策略」章节——请按 techDesign 步骤 6.5 补全(数量级 / 查询场景 / QPS / 时效性 / 推荐存储)');
1591
+ result.passed = true;
1592
+ return result;
1593
+ }
1594
+ const startIdx = storageMatch.index + storageMatch[0].length;
1595
+ const rest = designContent.slice(startIdx);
1596
+ const nextSectionMatch = rest.match(/^##+\s+/m);
1597
+ const sectionBody = nextSectionMatch ? rest.slice(0, nextSectionMatch.index) : rest;
1598
+ if (/\bTBD\b|\bTODO\b|\bPENDING\b|待确认|待填/i.test(sectionBody)) {
1599
+ result.warnings.push('「数据存储策略」章节含未解标记(TBD / TODO / 待确认)');
1600
+ }
1601
+ const tableRows = sectionBody.match(/^\|[^\n]+\|\s*$/gm) || [];
1602
+ const dataRows = tableRows.filter(r => !/^\|[\s\-:|]+\|$/.test(r.trim()));
1603
+ if (dataRows.length < 2) {
1604
+ result.warnings.push('「数据存储策略」表格数据行 < 1,请逐表填写(表名 / 数量级 / 查询场景 / QPS / 时效性 / 一致性 / 推荐存储 / 说明)');
1605
+ }
1606
+ // Column-count check: header row should carry 8 columns to include 一致性(含跨表事务)
1607
+ // 表名 | 预估数量级 | 查询场景 | 读/写 QPS | 时效性 | 一致性(含跨表事务) | 存储策略 | 备注
1608
+ // Keep WARN-level and backward-compatible: 7-column legacy tables only trigger a soft warn.
1609
+ if (tableRows.length > 0) {
1610
+ const headerRow = tableRows[0].trim();
1611
+ const headerCols = headerRow.split('|').slice(1, -1).map(c => c.trim()).filter(c => c.length > 0);
1612
+ if (headerCols.length > 0 && headerCols.length < 8) {
1613
+ result.warnings.push(`「数据存储策略」表头只有 ${headerCols.length} 列,建议补齐到 8 列(增加 "一致性(含跨表事务)" 列,参见 techDesign 步骤 6.5)`);
1614
+ }
1615
+ }
1616
+ result.passed = true;
1617
+ return result;
1618
+ }
1342
1619
  /**
1343
1620
  * Main entry: Run all strict validation rules
1344
1621
  */
@@ -1367,6 +1644,8 @@ export async function runStrictValidation(tasksContent, parsedTasks, knownSkillN
1367
1644
  checkTestCaseCoverage(tasksContent, hasTestCase, testCasesContent),
1368
1645
  checkTaskIdFormat(tasksContent),
1369
1646
  checkTaskCheckboxFormat(tasksContent),
1647
+ checkTaskLayerTag(parsedTasks),
1648
+ checkTaskLayerWaveConsistency(parsedTasks),
1370
1649
  ];
1371
1650
  // Add new rules if parameters are provided
1372
1651
  if (options?.proposalContent) {
@@ -1400,6 +1679,11 @@ export async function runStrictValidation(tasksContent, parsedTasks, knownSkillN
1400
1679
  checks.push(checkTechSpecCoverage(options.changeDir, options.techSpecPath));
1401
1680
  }
1402
1681
  }
1682
+ // design.md-based rules: only run if changeDir is provided
1683
+ if (options?.changeDir) {
1684
+ checks.push(checkExternalDependencyMatrix(options.changeDir));
1685
+ checks.push(checkDataStorageStrategy(options.changeDir));
1686
+ }
1403
1687
  // Detect current phase for spec-code consistency check
1404
1688
  if (options?.specsDir && options?.repoRoot) {
1405
1689
  const phase = options?.changeDir ? detectPhase(options.changeDir) : 'apply';
@@ -10,6 +10,12 @@ export interface HookSyncSummary {
10
10
  totalAddedHooks: number;
11
11
  /** Number of hook groups skipped because an identical entry already existed. */
12
12
  duplicateHooksSkipped: number;
13
+ /**
14
+ * v2.15.16:被清理的 ZhuanSpec 管辖老 group 数量。
15
+ * 例如旧版本 settings.json 挂着 `deviation-check --trigger post-prompt`,
16
+ * Sprint 3 新模板不再挂同名条目,要在 merge 前先调手带掉。
17
+ */
18
+ stalehooksRemoved: number;
13
19
  }
14
20
  export interface HookSettings {
15
21
  hooks?: Record<string, unknown>;
@@ -5,6 +5,24 @@
5
5
  * Both hosts share the same `hooks.<Event>[].hooks[].type/command` schema, so
6
6
  * we can centralize the signature-based de-duplication logic here.
7
7
  */
8
+ /**
9
+ * v2.15.16:判定一个 hook group 是否完全由 ZhuanSpec 管辖。
10
+ * 条件:group.hooks 里所有 entry 的 command 都以 `zhuanspec-hook` 开头。
11
+ * 任何一个非 ZhuanSpec 命令(用户自己挂的)都会让整组保留,避免误伤。
12
+ */
13
+ function isZhuanspecManagedGroup(group) {
14
+ if (!group || typeof group !== 'object')
15
+ return false;
16
+ const hooks = group.hooks;
17
+ if (!Array.isArray(hooks) || hooks.length === 0)
18
+ return false;
19
+ return hooks.every((h) => {
20
+ if (!h || typeof h !== 'object')
21
+ return false;
22
+ const command = h.command;
23
+ return typeof command === 'string' && command.trim().startsWith('zhuanspec-hook');
24
+ });
25
+ }
8
26
  /**
9
27
  * Merge ZhuanSpec-managed hook configuration into an existing settings object.
10
28
  *
@@ -15,6 +33,7 @@ export function mergeHookSettings(existing, zhuanspecConfig) {
15
33
  const summary = {
16
34
  totalAddedHooks: 0,
17
35
  duplicateHooksSkipped: 0,
36
+ stalehooksRemoved: 0,
18
37
  };
19
38
  if (!existing) {
20
39
  const zhuanspecHooks = (zhuanspecConfig.hooks ?? {});
@@ -31,7 +50,19 @@ export function mergeHookSettings(existing, zhuanspecConfig) {
31
50
  const existingValue = existingHooks[hookType];
32
51
  if (Array.isArray(existingValue)) {
33
52
  const zhuanspecArray = zhuanspecHookConfig ?? [];
34
- const existingSignatures = new Set(existingValue.map((item) => JSON.stringify(item)));
53
+ // v2.15.16:先剔除所有纯 ZhuanSpec 管辖的老 group,再 append 新模板。
54
+ // 这样新版本模板里删除的 entry(如 Sprint 3 下线的 deviation-check --trigger post-prompt)
55
+ // 不会因为老 settings.json 里残留而永久活下来;非 ZhuanSpec entry(用户自己加的)则全量保留。
56
+ const cleanedExisting = [];
57
+ for (const item of existingValue) {
58
+ if (isZhuanspecManagedGroup(item)) {
59
+ summary.stalehooksRemoved += 1;
60
+ }
61
+ else {
62
+ cleanedExisting.push(item);
63
+ }
64
+ }
65
+ const existingSignatures = new Set(cleanedExisting.map((item) => JSON.stringify(item)));
35
66
  const uniqueToAppend = [];
36
67
  for (const item of zhuanspecArray) {
37
68
  const signature = JSON.stringify(item);
@@ -43,7 +74,7 @@ export function mergeHookSettings(existing, zhuanspecConfig) {
43
74
  uniqueToAppend.push(item);
44
75
  }
45
76
  summary.totalAddedHooks += uniqueToAppend.length;
46
- existingHooks[hookType] = [...existingValue, ...uniqueToAppend];
77
+ existingHooks[hookType] = [...cleanedExisting, ...uniqueToAppend];
47
78
  }
48
79
  else {
49
80
  const zhuanspecArray = zhuanspecHookConfig;
@@ -79,6 +79,7 @@ export async function setPhase(changeDir, phase) {
79
79
  phaseDurations: [{
80
80
  phase,
81
81
  startedAt: timestamp,
82
+ lastUpdatedAt: timestamp,
82
83
  durationMs: 0,
83
84
  taskCount: 0,
84
85
  completedTaskCount: 0,
@@ -114,6 +115,7 @@ export async function setPhase(changeDir, phase) {
114
115
  phaseDurations: [{
115
116
  phase,
116
117
  startedAt: timestamp,
118
+ lastUpdatedAt: timestamp,
117
119
  durationMs: 0,
118
120
  taskCount: 0,
119
121
  completedTaskCount: 0,
@@ -129,6 +131,7 @@ export async function setPhase(changeDir, phase) {
129
131
  progress.phaseDurations.push({
130
132
  phase,
131
133
  startedAt: timestamp,
134
+ lastUpdatedAt: timestamp,
132
135
  durationMs: 0,
133
136
  taskCount: 0,
134
137
  completedTaskCount: 0,
@@ -145,6 +148,7 @@ export async function setPhase(changeDir, phase) {
145
148
  const prevPhaseDuration = progress.phaseDurations.find(pd => pd.phase === previousPhase && !pd.endedAt);
146
149
  if (prevPhaseDuration) {
147
150
  prevPhaseDuration.endedAt = timestamp;
151
+ prevPhaseDuration.lastUpdatedAt = timestamp;
148
152
  const startTime = new Date(prevPhaseDuration.startedAt).getTime();
149
153
  const endTime = new Date(timestamp).getTime();
150
154
  prevPhaseDuration.durationMs = endTime > startTime ? endTime - startTime : 0;
@@ -153,6 +157,7 @@ export async function setPhase(changeDir, phase) {
153
157
  progress.phaseDurations.push({
154
158
  phase,
155
159
  startedAt: timestamp,
160
+ lastUpdatedAt: timestamp,
156
161
  durationMs: 0,
157
162
  taskCount: 0,
158
163
  completedTaskCount: 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhuan-ai/zhuanspec",
3
- "version": "2.15.7",
3
+ "version": "2.16.2",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "zhuanspec",
@@ -39,6 +39,26 @@
39
39
  "!dist/**/__tests__",
40
40
  "!dist/**/*.map"
41
41
  ],
42
+ "scripts": {
43
+ "lint": "eslint src/",
44
+ "build": "node build.js",
45
+ "dev": "tsc --watch",
46
+ "dev:cli": "pnpm build && node bin/zhuanspec.js",
47
+ "test": "vitest run",
48
+ "test:watch": "vitest",
49
+ "test:ui": "vitest --ui",
50
+ "test:coverage": "vitest --coverage",
51
+ "test:postinstall": "node scripts/postinstall.js",
52
+ "prepare": "npm run build",
53
+ "prepublishOnly": "npm run build",
54
+ "postinstall": "node scripts/postinstall.js",
55
+ "check:pack-version": "node scripts/pack-version-check.mjs",
56
+ "diagnose:cursor": "node scripts/diagnose-cursor-commands.js",
57
+ "release": "pnpm run release:ci",
58
+ "release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
59
+ "release:local": "pnpm exec changeset version && pnpm run check:pack-version && pnpm exec changeset publish",
60
+ "changeset": "changeset"
61
+ },
42
62
  "engines": {
43
63
  "node": ">=20.19.0"
44
64
  },
@@ -60,23 +80,5 @@
60
80
  "ora": "^8.2.0",
61
81
  "yaml": "^2.8.2",
62
82
  "zod": "^4.0.17"
63
- },
64
- "scripts": {
65
- "lint": "eslint src/",
66
- "build": "node build.js",
67
- "dev": "tsc --watch",
68
- "dev:cli": "pnpm build && node bin/zhuanspec.js",
69
- "test": "vitest run",
70
- "test:watch": "vitest",
71
- "test:ui": "vitest --ui",
72
- "test:coverage": "vitest --coverage",
73
- "test:postinstall": "node scripts/postinstall.js",
74
- "postinstall": "node scripts/postinstall.js",
75
- "check:pack-version": "node scripts/pack-version-check.mjs",
76
- "diagnose:cursor": "node scripts/diagnose-cursor-commands.js",
77
- "release": "pnpm run release:ci",
78
- "release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
79
- "release:local": "pnpm exec changeset version && pnpm run check:pack-version && pnpm exec changeset publish",
80
- "changeset": "changeset"
81
83
  }
82
- }
84
+ }