@heihei0299/matt-skills 1.5.4 → 1.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.
@@ -17,6 +17,7 @@ description: "Run the pre-commit gate before any commit: verify docs match the i
17
17
  - 发现不一致 → 先修文档(或更新实现),再进入下一步
18
18
  - 改动涉及项目结构、分发文件、技能/命令清单时,检查 README 中对应的结构说明、映射表、清单是否同步
19
19
  - 改动涉及用法/CLI/配置/示例时,检查 README 对应描述与实际一致
20
+ - **重点聚焦(README + matt-skills 流程)**:本次门禁优先对齐 `README.md` 与 matt-skills 流程相关文件——`AGENTS.md`(路由)、`CONTEXT.md`(术语)、`docs/agents/*`(`skill-design.md`/`runtime-discipline.md`/`issue-tracker.md`/`triage-labels.md`/`domain.md`)、`template/` 镜像(含 `.agents/skills` 全量、`AGENTS.md`、`CONTEXT.md`、`docs/agents`)、`.agents/skills/*`(技能正文与 `references/`)、`config/*` 与 `scripts/build-template.js`;改动触及上述任一文件时,逐项核对 README 的结构说明/清单/映射表与模板镜像是否同步,未同步先修复再 commit
20
21
  - 存在模板镜像/分发副本时,确认源文件与副本同步(如有守护测试,跑一遍确认)
21
22
  - **特例**:`AGENTS.md` 的 `tdd-implement ↔ implement` 路由行 + 技能文件 + `.gitignore` 的 `.pi/` 忽略,且存在 `AGENTS.md.bak` 时,视为模板同步预期增量,不回滚
22
23
  ### ② 保持目录卫生
package/bin/cli.js CHANGED
@@ -32,37 +32,83 @@ process.stdout.on('error', (err) => {
32
32
  throw err;
33
33
  });
34
34
 
35
- const HELP = `matt-skills — install and manage this skill collection
35
+ const HELP_GLOBAL = `matt-skills — install and manage this skill collection
36
36
 
37
37
  Usage:
38
38
  matt-skills init [options] Initialize a project: template + skills (.agents/skills)
39
- matt-skills sync [--apply|--force] [--all] [--dest <path>] [--upstream <url>] [--ref <ref>] [--json] Sync existing project to latest template + skills
39
+ matt-skills sync [--all|--force|--dry-run] [--dest <path>] Sync existing project to latest template + skills
40
40
  matt-skills list [--all] [--json] List available skills and their descriptions
41
41
  matt-skills install [options] Install skills (interactive by default)
42
42
  matt-skills check [--all] [--json] [--upstream <url>] [--ref <ref>]
43
43
  Check if upstream skills are up to date (read-only)
44
- matt-skills --help Show this help
44
+ matt-skills --help | -h Show this help
45
+ matt-skills --version | -v Show version
46
+ `;
47
+
48
+ const HELP_INIT = `matt-skills init [options] — Initialize a project: template + skills (.agents/skills)
49
+
50
+ Usage:
51
+ matt-skills init [options]
45
52
 
46
53
  Init options:
47
54
  --dest <path> Target directory (default: current directory)
48
55
  --force Overwrite existing files
49
56
  --all Include non-programming skills (productivity) and optional proprietary; default only core programming (engineering 18 + default proprietary 4 → 22)
57
+ --help, -h Show this help
58
+
59
+ 提示:matt-skills --help 查看全量
60
+ `;
61
+
62
+ const HELP_SYNC = `matt-skills sync — Sync existing project to latest template + skills
63
+
64
+ Usage:
65
+ matt-skills sync [--all|--force|--dry-run] [--dest <path>]
66
+
50
67
  Sync options:
51
- --apply Apply changes (safe incremental, respects custom AGENTS.md)
52
- --force Hard overwrite (backup .bak + full sync)
53
- --all Include non-programming and optional proprietary; default only core programming (22)
68
+ --all 范围:含非编程与可选独有(默认仅编程 22)
69
+ --force 力度:硬盖(备份 .bak + 删多余,全量 add/update/remove)
70
+ --dry-run 预演:只比对不写盘
54
71
  --dest <path> Target directory (default: current directory)
55
- --upstream <url> Upstream repo URL (default: https://github.com/mattpocock/skills.git)
56
- --ref <ref> Upstream ref (default: HEAD)
72
+ --help, -h Show this help
73
+
74
+ 说明:默认不带参即安全增量同步默认技能(22);--all 与 --force 互斥。
75
+
76
+ 提示:matt-skills --help 查看全量
77
+ `;
78
+
79
+ const HELP_LIST = `matt-skills list — List available skills
80
+
81
+ Usage:
82
+ matt-skills list [--all] [--json]
83
+
84
+ List options:
85
+ --all List all skills (default only core programming 22)
57
86
  --json Output as JSON
87
+ --help, -h Show this help
88
+
89
+ 提示:matt-skills --help 查看全量
90
+ `;
91
+
92
+ const HELP_CHECK = `matt-skills check — Check if upstream skills are up to date (read-only)
93
+
94
+ Usage:
95
+ matt-skills check [--all] [--json] [--upstream <url>] [--ref <ref>]
96
+
58
97
  Check options:
59
98
  --all Include non-programming and optional proprietary; default only core programming (22)
60
99
  --json Output as JSON
61
100
  --upstream <url> Upstream repo URL (default: https://github.com/mattpocock/skills.git)
62
101
  --ref <ref> Upstream ref (default: HEAD)
63
- List options:
64
- --all List all skills (default only core programming 22)
65
- --json Output as JSON
102
+ --help, -h Show this help
103
+
104
+ 提示:matt-skills --help 查看全量
105
+ `;
106
+
107
+ const HELP_INSTALL = `matt-skills install — Install skills (interactive by default)
108
+
109
+ Usage:
110
+ matt-skills install [options]
111
+
66
112
  Install options:
67
113
  --tools <a,b> Install for the given tools (codex, pi, opencode, claude); skips tool selection — 共享技能统一指向 .agents/skills,.pi/skills/.opencode/skills 仅用于项目自定义
68
114
  --all Install all skills (default only core programming 22); skips skill selection
@@ -70,8 +116,13 @@ Install options:
70
116
  --global Install to the user's global skill directories
71
117
  --project Install to project skill directories (default)
72
118
  --dest <path> Install everything into a single custom directory (overrides --tools)
119
+ --help, -h Show this help
120
+
121
+ 提示:matt-skills --help 查看全量
73
122
  `;
74
123
 
124
+ const HELP = HELP_GLOBAL;
125
+
75
126
  function parseFrontmatter(text) {
76
127
  const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
77
128
  if (!match) return {};
@@ -280,25 +331,22 @@ async function initCommand({ dest, force, all }) {
280
331
  }
281
332
  process.stdout.write(`目标路径:${target}\n`);
282
333
  }
283
- async function syncCommand({ dest, force, apply, upstreamUrl, ref, json, all }) {
334
+ async function syncCommand({ dest, force, all, dryRun, json, upstreamUrl, ref }) {
284
335
  const onlyProgramming = !all;
285
- // 默认:仅对比不写盘 (check 模式),--apply / --force 显式写盘
286
- const doApply = apply || force;
287
- if (!doApply) {
336
+ if (dryRun) {
288
337
  const { compare } = await import('../scripts/sync-upstream.js');
289
338
  const cmp = await compare({ upstreamUrl, ref, onlyProgramming });
290
339
  if (json) {
291
340
  process.stdout.write(JSON.stringify({ head: cmp.head, counts: cmp.counts, result: cmp.result, onlyProgramming }, null, 2) + '\n');
292
341
  } else {
342
+ const modeHint = onlyProgramming ? '(仅编程)' : '(全量)';
293
343
  const lines = [];
294
344
  lines.push(`上游 HEAD: ${cmp.head}`);
295
- const modeHint = onlyProgramming ? '(仅编程)' : '(全量)';
296
345
  lines.push(`本地非独有: ${cmp.counts.local} 上游: ${cmp.counts.upstream} ${modeHint}`);
297
346
  lines.push('');
298
347
  const totalDiff = cmp.result.added.length + cmp.result.updated.length + cmp.result.removed.length + cmp.result.renamed.length;
299
- if (totalDiff === 0) {
300
- lines.push('✅ 已是最新,无差异');
301
- } else {
348
+ if (totalDiff === 0) lines.push('✅ 已是最新,无差异');
349
+ else {
302
350
  if (cmp.result.added.length) lines.push(`新增 (${cmp.result.added.length}): ${cmp.result.added.join(', ')}`);
303
351
  if (cmp.result.updated.length) lines.push(`更新 (${cmp.result.updated.length}): ${cmp.result.updated.join(', ')}`);
304
352
  if (cmp.result.renamed.length) lines.push(`重命名 (${cmp.result.renamed.length}): ${cmp.result.renamed.map((r) => `${r.from}→${r.to}`).join(', ')}`);
@@ -309,8 +357,8 @@ async function syncCommand({ dest, force, apply, upstreamUrl, ref, json, all })
309
357
  }
310
358
  const { rm } = await import('node:fs/promises');
311
359
  await rm(cmp.dest, { recursive: true, force: true });
312
- const hasDiff = cmp.result.added.length + cmp.result.updated.length + cmp.result.removed.length + cmp.result.renamed.length > 0;
313
- if (hasDiff) process.exitCode = 1;
360
+ const totalDiff = cmp.result.added.length + cmp.result.updated.length + cmp.result.removed.length + cmp.result.renamed.length;
361
+ if (totalDiff > 0) process.exitCode = 1;
314
362
  return;
315
363
  }
316
364
 
@@ -341,22 +389,16 @@ async function syncCommand({ dest, force, apply, upstreamUrl, ref, json, all })
341
389
  }
342
390
  if (!(await pathExists(marker))) {
343
391
  process.stdout.write('未检测到现有项目(AGENTS.md 不存在),将执行全新初始化\n');
344
- if (onlyProgramming) {
345
- await copyTemplateFiltered();
346
- } else {
347
- await cp(TEMPLATE_DIR, target, { recursive: true, force: true });
348
- }
392
+ if (onlyProgramming) await copyTemplateFiltered();
393
+ else await cp(TEMPLATE_DIR, target, { recursive: true, force: true });
349
394
  process.stdout.write('模板:已复制(AGENTS.md、.agents/skills、.opencode/、.pi/)\n');
350
395
  } else if (force) {
351
396
  process.stdout.write('同步:检测到现有项目,将增量更新\n');
352
397
  await backupIfExists(path.join(target, 'AGENTS.md'));
353
- if (onlyProgramming) {
354
- await copyTemplateFiltered();
355
- } else {
356
- await cp(TEMPLATE_DIR, target, { recursive: true, force: true });
357
- }
398
+ if (onlyProgramming) await copyTemplateFiltered();
399
+ else await cp(TEMPLATE_DIR, target, { recursive: true, force: true });
358
400
  process.stdout.write('模板:已覆盖(AGENTS.md、.agents/skills、.opencode/、.pi/)\n');
359
- } else if (apply) {
401
+ } else {
360
402
  process.stdout.write('同步:检测到现有项目,将增量更新\n');
361
403
  let skipAgents = false;
362
404
  try {
@@ -382,21 +424,10 @@ async function syncCommand({ dest, force, apply, upstreamUrl, ref, json, all })
382
424
  }
383
425
  process.stdout.write('模板:已同步(AGENTS.md 跳过,已含定制)\n');
384
426
  } else {
385
- if (onlyProgramming) {
386
- await copyTemplateFiltered();
387
- } else {
388
- await cp(TEMPLATE_DIR, target, { recursive: true, force: true });
389
- }
427
+ if (onlyProgramming) await copyTemplateFiltered();
428
+ else await cp(TEMPLATE_DIR, target, { recursive: true, force: true });
390
429
  process.stdout.write('模板:已同步(AGENTS.md、.agents/skills、.opencode/、.pi/)\n');
391
430
  }
392
- } else {
393
- process.stdout.write('同步:检测到现有项目,将增量更新\n');
394
- if (onlyProgramming) {
395
- await copyTemplateFiltered();
396
- } else {
397
- await cp(TEMPLATE_DIR, target, { recursive: true, force: true });
398
- }
399
- process.stdout.write('模板:已同步(AGENTS.md、.agents/skills、.opencode/、.pi/)\n');
400
431
  }
401
432
  // 技能同步:按编程过滤(默认仅编程,--all 全量)
402
433
  const entries = await readdir(SKILLS_DIR, { withFileTypes: true });
@@ -428,7 +459,7 @@ async function syncCommand({ dest, force, apply, upstreamUrl, ref, json, all })
428
459
  }
429
460
  }
430
461
  // --force 时删除多余;仅编程模式下多余指不在编程集合中的,--all 模式下多余指不在全量中的
431
- // 默认 --apply 保留多余(不删除),符合“默认保留、--force 删除”
462
+ // 默认安全增量保留多余(不删除),符合“默认保留、--force 删除”
432
463
  if (force) {
433
464
  let localEntries = [];
434
465
  try {
@@ -478,25 +509,55 @@ async function syncCommand({ dest, force, apply, upstreamUrl, ref, json, all })
478
509
  function parseInitArgs(args) {
479
510
  let dest;
480
511
  let force = false;
481
- let apply = false;
482
- let json = false;
483
512
  let all = false;
513
+ for (let i = 0; i < args.length; i++) {
514
+ const arg = args[i];
515
+ if (arg === '--dest') {
516
+ if (i + 1 >= args.length || args[i + 1].startsWith('-')) throw new Error(`unknown option '--dest' requires a value`);
517
+ dest = args[++i];
518
+ } else if (arg.startsWith('--dest=')) dest = arg.slice('--dest='.length);
519
+ else if (arg === '--force') force = true;
520
+ else if (arg === '--all') all = true;
521
+ else if (arg === '--help' || arg === '-h') {} // handled at main level, ignore here
522
+ else if (arg.startsWith('-')) throw new Error(`unknown option '${arg}' for command 'init'`);
523
+ else throw new Error(`unknown argument '${arg}' for command 'init'`);
524
+ }
525
+ return { dest, force, all };
526
+ }
527
+
528
+ function parseSyncArgs(args) {
529
+ let dest;
530
+ let force = false;
531
+ let all = false;
532
+ let dryRun = false;
533
+ let json = false;
484
534
  let upstreamUrl;
485
535
  let ref;
486
536
  for (let i = 0; i < args.length; i++) {
487
537
  const arg = args[i];
488
- if (arg === '--dest') dest = args[++i];
489
- else if (arg.startsWith('--dest=')) dest = arg.slice('--dest='.length);
538
+ if (arg === '--dest') {
539
+ if (i + 1 >= args.length || args[i + 1].startsWith('-')) throw new Error(`unknown option '--dest' requires a value`);
540
+ dest = args[++i];
541
+ } else if (arg.startsWith('--dest=')) dest = arg.slice('--dest='.length);
490
542
  else if (arg === '--force') force = true;
491
- else if (arg === '--apply') apply = true;
492
543
  else if (arg === '--all') all = true;
544
+ else if (arg === '--dry-run') dryRun = true;
493
545
  else if (arg === '--json') json = true;
494
- else if (arg === '--upstream') upstreamUrl = args[++i];
495
- else if (arg.startsWith('--upstream=')) upstreamUrl = arg.slice('--upstream='.length);
496
- else if (arg === '--ref') ref = args[++i];
497
- else if (arg.startsWith('--ref=')) ref = arg.slice('--ref='.length);
546
+ else if (arg === '--upstream') {
547
+ if (i + 1 >= args.length || args[i + 1].startsWith('-')) throw new Error(`unknown option '--upstream' requires a value`);
548
+ upstreamUrl = args[++i];
549
+ } else if (arg.startsWith('--upstream=')) upstreamUrl = arg.slice('--upstream='.length);
550
+ else if (arg === '--ref') {
551
+ if (i + 1 >= args.length || args[i + 1].startsWith('-')) throw new Error(`unknown option '--ref' requires a value`);
552
+ ref = args[++i];
553
+ } else if (arg.startsWith('--ref=')) ref = arg.slice('--ref='.length);
554
+ else if (arg === '--help' || arg === '-h') {} // handled at main
555
+ else if (arg === '--apply') {} // deprecated alias, same as default safe incremental
556
+ else if (arg.startsWith('-')) throw new Error(`unknown option '${arg}' for command 'sync'`);
557
+ else throw new Error(`unknown argument '${arg}' for command 'sync'`);
498
558
  }
499
- return { dest, force, apply, json, all, upstreamUrl, ref };
559
+ if (all && force) throw new Error(`--all and --force are mutually exclusive, choose one`);
560
+ return { dest, force, all, dryRun, json, upstreamUrl, ref };
500
561
  }
501
562
 
502
563
  function parseInstallArgs(args) {
@@ -507,14 +568,21 @@ function parseInstallArgs(args) {
507
568
  let toolsArg;
508
569
  for (let i = 0; i < args.length; i++) {
509
570
  const arg = args[i];
510
- if (arg === '--dest') dest = args[++i];
511
- else if (arg.startsWith('--dest=')) dest = arg.slice('--dest='.length);
571
+ if (arg === '--dest') {
572
+ if (i + 1 >= args.length || args[i + 1].startsWith('-')) throw new Error(`unknown option '--dest' requires a value`);
573
+ dest = args[++i];
574
+ } else if (arg.startsWith('--dest=')) dest = arg.slice('--dest='.length);
512
575
  else if (arg === '--all') all = true;
513
576
  else if (arg === '--force') force = true;
514
- else if (arg === '--tools') toolsArg = args[++i];
515
- else if (arg.startsWith('--tools=')) toolsArg = arg.slice('--tools='.length);
577
+ else if (arg === '--tools') {
578
+ if (i + 1 >= args.length || args[i + 1].startsWith('-')) throw new Error(`unknown option '--tools' requires a value`);
579
+ toolsArg = args[++i];
580
+ } else if (arg.startsWith('--tools=')) toolsArg = arg.slice('--tools='.length);
516
581
  else if (arg === '--global') global = true;
517
582
  else if (arg === '--project') global = false;
583
+ else if (arg === '--help' || arg === '-h') {} // handled at main
584
+ else if (arg.startsWith('-')) throw new Error(`unknown option '${arg}' for command 'install'`);
585
+ else throw new Error(`unknown argument '${arg}' for command 'install'`);
518
586
  }
519
587
  const tools = toolsArg
520
588
  ? toolsArg.split(',').map((t) => t.trim()).filter(Boolean)
@@ -559,12 +627,49 @@ async function checkCommand(args) {
559
627
 
560
628
  async function main() {
561
629
  const args = process.argv.slice(2);
562
- if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
563
- process.stdout.write(HELP);
630
+ // -v/--version highest priority, anywhere
631
+ if (args.includes('-v') || args.includes('--version')) {
632
+ try {
633
+ const pkgRaw = await readFile(path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8');
634
+ const pkg = JSON.parse(pkgRaw);
635
+ process.stdout.write(`${pkg.version}\n`);
636
+ } catch {
637
+ process.stdout.write('unknown\n');
638
+ }
639
+ return;
640
+ }
641
+ // -h/--help suffix handling (global vs per-command)
642
+ if (args.includes('-h') || args.includes('--help')) {
643
+ const known = ['init', 'sync', 'list', 'install', 'check'];
644
+ const first = args[0];
645
+ const cmd = known.includes(first) ? first : null;
646
+ if (cmd === 'init') { process.stdout.write(HELP_INIT); return; }
647
+ if (cmd === 'sync') { process.stdout.write(HELP_SYNC); return; }
648
+ if (cmd === 'list') { process.stdout.write(HELP_LIST); return; }
649
+ if (cmd === 'check') { process.stdout.write(HELP_CHECK); return; }
650
+ if (cmd === 'install') { process.stdout.write(HELP_INSTALL); return; }
651
+ process.stdout.write(HELP_GLOBAL);
652
+ return;
653
+ }
654
+ if (args.length === 0) {
655
+ process.stdout.write(HELP_GLOBAL);
564
656
  return;
565
657
  }
566
658
  const [command, ...rest] = args;
659
+ const knownCommands = new Set(['list', 'init', 'sync', 'install', 'check', 'update']);
660
+ if (!knownCommands.has(command)) {
661
+ process.stderr.write(`error: unknown command '${command}'\n`);
662
+ process.stderr.write(`Run 'matt-skills --help' for usage.\n`);
663
+ process.exitCode = 1;
664
+ return;
665
+ }
567
666
  if (command === 'list') {
667
+ // list strict: only --all/--json/--help allowed, rest handled via parse but we keep simple
668
+ for (const a of rest) {
669
+ if (a === '--all' || a === '--json' || a === '--help' || a === '-h') continue;
670
+ if (a.startsWith('-')) { process.stderr.write(`error: unknown option '${a}' for command 'list'\n`); process.stderr.write(`Run 'matt-skills list --help' for usage.\n`); process.exitCode = 1; return; }
671
+ process.stderr.write(`error: unknown argument '${a}' for command 'list'\n`); process.stderr.write(`Run 'matt-skills list --help' for usage.\n`); process.exitCode = 1; return;
672
+ }
568
673
  const onlyProgramming = !rest.includes('--all');
569
674
  const skills = await listSkills({ onlyProgramming });
570
675
  if (rest.includes('--json')) {
@@ -581,7 +686,7 @@ async function main() {
581
686
  return;
582
687
  }
583
688
  if (command === 'sync') {
584
- await syncCommand(parseInitArgs(rest));
689
+ await syncCommand(parseSyncArgs(rest));
585
690
  return;
586
691
  }
587
692
  if (command === 'install') {
@@ -589,16 +694,22 @@ async function main() {
589
694
  return;
590
695
  }
591
696
  if (command === 'check') {
697
+ // check strict: allow --all/--json/--upstream/--ref/--help
698
+ for (const a of rest) {
699
+ if (a === '--all' || a === '--json' || a === '--help' || a === '-h') continue;
700
+ if (a === '--upstream' || a.startsWith('--upstream=')) continue;
701
+ if (a === '--ref' || a.startsWith('--ref=')) continue;
702
+ if (a.startsWith('-')) { process.stderr.write(`error: unknown option '${a}' for command 'check'\n`); process.stderr.write(`Run 'matt-skills check --help' for usage.\n`); process.exitCode = 1; return; }
703
+ process.stderr.write(`error: unknown argument '${a}' for command 'check'\n`); process.stderr.write(`Run 'matt-skills check --help' for usage.\n`); process.exitCode = 1; return;
704
+ }
592
705
  await checkCommand(rest);
593
706
  return;
594
707
  }
595
708
  if (command === 'update') {
596
- process.stderr.write('update 已合并到 sync --apply\n');
709
+ process.stderr.write('update 已合并到 sync(默认即增量同步)\n');
597
710
  process.exitCode = 1;
598
711
  return;
599
712
  }
600
- process.stderr.write(HELP);
601
- process.exitCode = 1;
602
713
  }
603
714
  main().catch((error) => {
604
715
  process.stderr.write(`error: ${error.message}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heihei0299/matt-skills",
3
- "version": "1.5.4",
3
+ "version": "1.6.0",
4
4
  "description": "Agent skills + 项目配置模板:一条命令初始化 opencode / pi-agent 项目(含 mattpocock/skills 上游技能)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,6 +17,7 @@ description: "Run the pre-commit gate before any commit: verify docs match the i
17
17
  - 发现不一致 → 先修文档(或更新实现),再进入下一步
18
18
  - 改动涉及项目结构、分发文件、技能/命令清单时,检查 README 中对应的结构说明、映射表、清单是否同步
19
19
  - 改动涉及用法/CLI/配置/示例时,检查 README 对应描述与实际一致
20
+ - **重点聚焦(README + matt-skills 流程)**:本次门禁优先对齐 `README.md` 与 matt-skills 流程相关文件——`AGENTS.md`(路由)、`CONTEXT.md`(术语)、`docs/agents/*`(`skill-design.md`/`runtime-discipline.md`/`issue-tracker.md`/`triage-labels.md`/`domain.md`)、`template/` 镜像(含 `.agents/skills` 全量、`AGENTS.md`、`CONTEXT.md`、`docs/agents`)、`.agents/skills/*`(技能正文与 `references/`)、`config/*` 与 `scripts/build-template.js`;改动触及上述任一文件时,逐项核对 README 的结构说明/清单/映射表与模板镜像是否同步,未同步先修复再 commit
20
21
  - 存在模板镜像/分发副本时,确认源文件与副本同步(如有守护测试,跑一遍确认)
21
22
  - **特例**:`AGENTS.md` 的 `tdd-implement ↔ implement` 路由行 + 技能文件 + `.gitignore` 的 `.pi/` 忽略,且存在 `AGENTS.md.bak` 时,视为模板同步预期增量,不回滚
22
23
  ### ② 保持目录卫生