@heihei0299/matt-skills 3.0.14 → 3.0.16

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.
@@ -14,7 +14,7 @@ disable-model-invocation: true
14
14
  - **多 issue**:存在多个 `Type: task` 时,读取 [orchestration.md](references/orchestration.md) 后按依赖顺序逐个完成。
15
15
  - `research`、`prototype`、`grilling` 类型任务分流到对应技能。
16
16
 
17
- 每个 issue 开始时记录 `issue_base = HEAD`。一个 issue 可以包含一个或多个 commits;不要求固定 commit 数量,也不为满足数量约束强制 amend、squash 或重写历史。
17
+ 每个 issue 开始时记录 `issue_base = HEAD`。Commit 是交付 artifact,不是流程日志:Red-Green / Verify 期间不因 Behavior、阶段切换或验证动作创建 commit。Verify 通过后形成 1 个 Review Point commit;仅当 Full Review 产生 blocking findings 时,最多再形成 1 个 finding-fix commit。
18
18
 
19
19
  生命周期:
20
20
 
@@ -50,7 +50,7 @@ disable-model-invocation: true
50
50
 
51
51
  Verify 通过后读取 [review.md](references/review.md)。
52
52
 
53
- 先将当前 issue 交付所需的代码、测试、文档和配置形成 committed Review Point,再按 `review.md` 完成一次完整 Review 与必要的增量 Review。
53
+ 先将当前 issue 交付所需的代码、测试、文档和配置形成唯一的 committed Review Point commit,再按 `review.md` 完成一次完整 Review 与必要的增量 Review。
54
54
 
55
55
  完整 Review 的审查维度、reviewer 数量、提示词和输出格式仍以 [code-review](.agents/skills/code-review/SKILL.md) 为唯一事实源。
56
56
 
@@ -65,15 +65,17 @@ Verify 通过后读取 [review.md](references/review.md)。
65
65
 
66
66
  Review 通过后读取 [finalize.md](references/finalize.md),只做 tracker/progress/status 收尾并记录 `issue_head`。
67
67
 
68
- Finalize 不新增产品 Behavior,也不修改已经 Review 的实现内容。若收尾时发现实现、测试、文档/配置或验证遗漏,停止当前 issue,不标记 `resolved`,并按 `finalize.md` 报告遗漏请求决策;不得在 Finalize 中补改或重新进入 Red-Green、Verify 或 Review。
68
+ Finalize 不新增产品 Behavior,也不修改已经 Review 的实现内容,不为单个 issue 创建收尾 commit。若收尾时发现实现、测试、文档/配置或验证遗漏,停止当前 issue,不标记 `resolved`,并按 `finalize.md` 报告遗漏请求决策;不得在 Finalize 中补改或重新进入 Red-Green、Verify 或 Review。
69
+
70
+ 本次执行批次结束后,如仓库内 tracker/progress/status 存在待同步状态,统一写入并最多形成 1 个 batch state-sync commit;该 commit 不属于任何单个 issue 的实现/Review commit range。
69
71
 
70
72
  ## 运行纪律
71
73
 
72
74
  - Red-Green 必须覆盖当前 issue 的全部待实现 Behavior,不能只对第一个改动执行 TDD。
73
75
  - 一个 Behavior 完成后继续下一个 Behavior,直到 Step ① 出口满足。
74
76
  - Verify 只做当前 issue 必要的最终验证;已通过的等价验证不机械重复。
75
- - 完整 Review 前必须形成 committed Review Point;不得用未提交 working tree 代替 `code-review` 所需的 committed diff。
76
- - 每个 issue 只有一次逻辑上的完整双轴 Review;完整 Review 之后只处理增量 Review。技术失败或中断的恢复规则以 `review.md` 为准。
77
+ - Red-Green / Verify 期间不按 Behavior、阶段或验证动作拆 commit;完整 Review 前只形成 1 个 committed Review Point。
78
+ - 每个 issue 只有一次逻辑上的完整双轴 Review;若有 blocking findings,只允许 1 个 finding-fix commit 与最多 1 次逻辑增量 Review。技术失败或中断的恢复规则以 `review.md` 为准。
77
79
  - 当前 issue Review 与 Finalize 完成后,才能进入下一个 issue;下一个 issue 以当时的 `HEAD` 作为新的 `issue_base`。
78
80
  - 当前 Step 达到出口后继续进入下一 Step;仅在需要用户决策或存在外部阻塞时暂停。
79
81
 
@@ -1,24 +1,28 @@
1
1
  # Finalize
2
2
 
3
- 仅在 Verify 与 Review 通过后执行。Finalize 只负责 tracker/progress/status 收尾,不新增产品 Behavior,也不修改已经 Review 的代码、测试、交付文档或配置。
3
+ 仅在 Verify 与 Review 通过后执行。Finalize 只负责 tracker/progress/status 收尾,不新增产品 Behavior,不修改已经 Review 的代码、测试、交付文档或配置,也不为单个 issue 创建收尾 commit。
4
4
 
5
5
  ## 步骤
6
6
 
7
7
  1. 确认 `review_head` 已记录,且 `issue_base...review_head` 对应的实现范围已经完成 Review。
8
- 2. 更新 Acceptance Criteria 与 progress/tracker,记录已 Review 的实现范围、Review、验证与运行结果;此时不得提前标记 `resolved` 或解除 blockers。
8
+ 2. 准备 Acceptance Criteria 与 progress/tracker 的最终状态,记录已 Review 的实现范围、Review、验证与运行结果。外部 tracker 可在此同步;仓库内 tracker/progress/status 只记录为批次待同步状态,不在当前 issue Finalize 中写入或提交。
9
9
  3. 若 Finalize 过程中发现任何实现、测试、交付文档、配置或验证遗漏,立即停止当前 issue:保持未完成,不标记 `resolved`,不解除 blockers,不设置成功的 `issue_head`,并报告遗漏请求决策。不得在 Finalize 中补改,也不得自动重新进入 Red-Green、Verify 或 Review。
10
- 4. 仅在未发现上述遗漏且所有必要状态同步准备完成后,才同步 `resolved` / blockers 状态。
11
- 5. 若状态同步修改了仓库内的 tracker/progress/status 文件,将这些状态修改提交;不得在该提交中混入产品实现或其它未 Review 的交付修改。
12
- 6. 确认所有必要状态同步成功后,设置 `issue_head = HEAD`。
10
+ 4. 仅在未发现上述遗漏后,将当前 issue 的完成状态视为 `resolved` 并解除已满足的 blockers;仓库内对应状态仍留待批次 state-sync。
11
+ 5. Finalize 不创建 commit;设置 `issue_head = review_head`。此时当前 `HEAD` 应与 `review_head` 一致。
13
12
 
14
- 无需为了固定 commit 数量而 amend、squash 或重写当前 issue 的历史。
13
+ ## 批次状态同步
14
+
15
+ 本次执行批次结束后,如仓库内 tracker/progress/status 存在待同步状态,统一写入全部待同步内容并最多创建 1 个 batch state-sync commit。该 commit:
16
+
17
+ - 不混入产品实现、测试、交付文档或配置修改;
18
+ - 不属于任何单个 issue 的 `issue_base...issue_head` 范围;
19
+ - 不因 issue 数量增加而拆成多个 status commits。
15
20
 
16
21
  ## 出口
17
22
 
18
23
  - Acceptance Criteria 全部通过;
19
24
  - `issue_base...review_head` 的实现范围已经完成 Review;
20
25
  - Finalize 未发现实现、测试、交付文档、配置或验证遗漏;
21
- - tracker/progress/status 与实际 Review、验证和完成状态一致;
22
- - `review_head` 之后若存在 commits,只包含当前 issue 的状态收尾修改;
23
- - `issue_head = HEAD` 已记录;
26
+ - tracker/progress/status 已同步,或已进入本批次唯一的待同步集合;
27
+ - `issue_head = review_head`;
24
28
  - issue 已 `resolved`,已满足的 blockers 已解除。
@@ -22,12 +22,12 @@ for each layer:
22
22
  Verify
23
23
  Review
24
24
  Finalize
25
- issue_head = HEAD
25
+ issue_head = review_head
26
26
  ```
27
27
 
28
28
  `Review` 包含当前 issue committed Review Point 的形成以及完整/增量 Review;具体以 `SKILL.md` 与 `review.md` 为准。
29
29
 
30
- 一个 issue Finalize 完成后立即进入下一个可调度 issue。下一个 issue 以当前 `issue_head` 作为新的 `issue_base`。前置 issue 未完成时,其依赖项保持 `blocked`。
30
+ 一个 issue Finalize 完成后立即进入下一个可调度 issue。Finalize 不为单个 issue 创建 commit,因此 `issue_head = review_head`;下一个 issue 以当前 `issue_head` 作为新的 `issue_base`。前置 issue 未完成时,其依赖项保持 `blocked`。
31
31
 
32
32
  验证与 Finalize 分别以 `verify.md`、`finalize.md` 为准,本文件不重复定义其内部规则。
33
33
 
@@ -45,10 +45,10 @@ for each layer:
45
45
  其中:
46
46
 
47
47
  - `issue_base...review_head` 是已完成 Review 的实现范围;
48
- - `issue_base...issue_head` 是当前 issue 的完整提交范围;
48
+ - `issue_head = review_head`,因此 `issue_base...issue_head` 只包含当前 issue 的 Review Point commit,以及可选的唯一 finding-fix commit;
49
49
  - `issue_head` 是下一个 issue 的 `issue_base`。
50
50
 
51
- 当前层所有 issue 完成后进入下一层。全部层完成后,确认 issue 与 progress 状态一致即可结束;不额外扩大验证范围,也不再次执行完整 Review。
51
+ 当前层所有 issue 完成后进入下一层。全部层完成后,如仓库内 tracker/progress/status 存在待同步状态,统一写入并最多创建 1 个 batch state-sync commit;该 commit 不属于任何单个 issue 的提交范围。随后确认 issue 与 progress 状态一致即可结束;不额外扩大验证范围,也不再次执行完整 Review。
52
52
 
53
53
  ## 冲突与失败
54
54
 
@@ -61,5 +61,6 @@ for each layer:
61
61
 
62
62
  - 所有可执行 issue 均按依赖顺序完成;
63
63
  - issue、依赖状态与 progress 一致;
64
- - 每个完成 issue 的 `issue_base`、`review_head` 与 `issue_head` 边界明确;
64
+ - 每个完成 issue 的 `issue_base`、`review_head` 与 `issue_head` 边界明确,且每个 issue 最多包含 2 个由本技能产生的实现/Review commits;
65
+ - 仓库内状态同步如有需要,只形成最多 1 个批次 state-sync commit;
65
66
  - 不存在被误当作已完成的 blocked issue。
@@ -19,10 +19,10 @@
19
19
 
20
20
  1. 确认当前 issue 交付所需的代码、测试、文档和配置均已完成;
21
21
  2. 若最后的文档/配置修改影响已验证证据,重新验证受影响范围;
22
- 3. 将当前 issue 已完成并验证的交付修改提交到当前 branch;
22
+ 3. 将当前 issue 已完成并验证的交付修改合并形成当前 issue 唯一的 Review Point commit;不得按 Behavior、阶段或验证动作拆分 commit;
23
23
  4. 确认不存在属于当前 issue 交付内容的未提交修改。
24
24
 
25
- 一个 issue 可以在这里已有一个或多个 commits。Commit 是 Review artifact,不代表 issue 已完成。
25
+ 完整 Review 前每个 issue 只形成 1 个 Review Point commit。Commit 是交付 artifact,不是流程日志。
26
26
 
27
27
  完整 Review 使用:
28
28
 
@@ -51,32 +51,32 @@
51
51
 
52
52
  ## 3. 增量 Review
53
53
 
54
- 存在 `open_findings` 时,只处理已有 finding,不扩大当前 issue 范围。可将由同一改动共同解决的相关 findings 一起处理,不要求“一 finding 一 commit”。
54
+ 存在 `open_findings` 时,只处理已有 finding,不扩大当前 issue 范围。一次性处理当前全部 open findings,不按 finding 拆分 commit。
55
55
 
56
- 每个 issue 最多执行 2 个逻辑增量 Review 轮次。只有正常形成增量 Review 结论的轮次才计数;工具错误、stream interruption、sub-agent failure 或其它未形成完整结论的技术失败不消耗轮次,只重试当前逻辑轮次。
56
+ 每个 issue 最多执行 1 个逻辑增量 Review 轮次。只有正常形成增量 Review 结论的轮次才计数;工具错误、stream interruption、sub-agent failure 或其它未形成完整结论的技术失败不消耗轮次,只重试当前逻辑轮次。
57
57
 
58
- 每轮修复前,若 `incremental_review_rounds >= 2` 且 `open_findings` 仍非空,则停止当前 issue:不得再次启动增量 Review,不设置 `review_head`,不得进入 Finalize,并报告剩余 findings 请求决策。
58
+ 若 `incremental_review_rounds >= 1` 且 `open_findings` 仍非空,则停止当前 issue:不得再次启动增量 Review,不设置 `review_head`,不得进入 Finalize,并报告剩余 findings 请求决策。
59
59
 
60
- 每轮修复:
60
+ 唯一一轮 finding 修复:
61
61
 
62
- 1. 修复选定的 open findings;
62
+ 1. 一次性修复当前全部 open findings;
63
63
  2. 若修复产生新的 Behavior,返回 Red-Green 对该 Behavior 执行 TDD;否则直接进入受影响证据的重新验证;
64
- 3. 重新验证修复直接影响的证据;
65
- 4. 将本轮修复提交到当前 issue 的 commit range,并确认不存在属于本轮修复的未提交修改;
66
- 5. 只针对 `last_reviewed_head...HEAD` 与本轮目标 findings 做增量 Review;增量 Review 不调用完整 `code-review`。
64
+ 3. 重新验证全部修复直接影响的证据;
65
+ 4. 将本轮全部修复合并形成最多 1 个 finding-fix commit,并确认不存在属于本轮修复的未提交修改;不得按 finding 拆分 commit;
66
+ 5. 只针对 `last_reviewed_head...HEAD` 与现有 findings 做增量 Review;增量 Review 不调用完整 `code-review`。
67
67
 
68
68
  若增量 Review 正常形成结论,先设置 `incremental_review_rounds += 1`。
69
69
 
70
70
  增量 Review 通过时:
71
71
 
72
- - 仅关闭本轮已由证据确认解决的 findings;
72
+ - 仅关闭已由证据确认解决的 findings;
73
73
  - 设置 `last_reviewed_head = HEAD`。
74
74
 
75
- 增量 Review 正常完成但未通过时:
75
+ 增量 Review 正常完成但仍有 open findings 时:
76
76
 
77
77
  - 保留未关闭 findings;
78
78
  - 不推进 `last_reviewed_head`;
79
- - 若尚未达到 2 轮上限,下一轮继续从上一次成功的 `last_reviewed_head` 审查累计的未 Review 修复。
79
+ - 停止当前 issue,不设置 `review_head`,不进入 Finalize,并报告剩余 findings 请求决策。
80
80
 
81
81
  增量 Review 发生技术失败时:
82
82
 
@@ -87,13 +87,12 @@
87
87
 
88
88
  当 `open_findings` 为空时,设置 `review_head = last_reviewed_head`,Review 通过。
89
89
 
90
- 当 `incremental_review_rounds = 2` 且 `open_findings` 仍非空时,Review 不通过:保持 issue 未完成,不设置 `review_head`,不进入 Finalize,并报告剩余 findings 请求决策。
91
-
92
90
  ## 出口
93
91
 
94
92
  - `full_review_done = true`;
95
93
  - `open_findings` 为空;
96
94
  - `review_head` 非空;
97
- - `incremental_review_rounds <= 2`;
98
- - `issue_base...review_head` 是已经完成 Review 的当前 issue 实现范围;
95
+ - `incremental_review_rounds <= 1`;
96
+ - `review_head` 指向 Review Point commit,或唯一的 finding-fix commit;
97
+ - `issue_base...review_head` 是已经完成 Review 的当前 issue 实现范围,最多包含 2 个由本技能产生的 issue commits;
99
98
  - 不存在属于该实现范围的未提交交付修改。
package/README.md CHANGED
@@ -62,7 +62,7 @@ npx @heihei0299/matt-skills sync --all # 同步全部可分发 skills
62
62
  npx @heihei0299/matt-skills sync --dry-run --json
63
63
  ```
64
64
 
65
- `init` 默认保护已有 `AGENTS.md`;需要刷新完整模板时使用 `init --all`。默认 `sync` 保留已有 `AGENTS.md` 和项目规则,`sync --all` 才将其整体刷新为当前分发模板。`sync` 不删除目标项目的额外文件或自定义 skills。
65
+ `init` 对已有 `AGENTS.md` 始终跳过;已有项目使用 `sync`。默认 `sync` 保留已有 `AGENTS.md` 和项目规则,`--all` 只扩大技能范围。需要显式刷新 `AGENTS.md` 时使用 `sync --refresh-agents`;无受管区块时会先备份为 `AGENTS.md.bak`。`sync` 不删除目标项目的额外文件或自定义 skills。
66
66
 
67
67
  默认 programming 范围中的 4 个独有 skills 是 `tdd-implement`、`diagnose-fix`、`grill-to-spec`、`show-me`。
68
68
 
@@ -72,17 +72,19 @@ npx @heihei0299/matt-skills sync --dry-run --json
72
72
  npx @heihei0299/matt-skills list [--all] [--json]
73
73
  npx @heihei0299/matt-skills install [--all] [--tools <list>] [--global] [--dest <dir>]
74
74
  npx @heihei0299/matt-skills init [--all] [--dest <dir>]
75
- npx @heihei0299/matt-skills sync [--all] [--dry-run] [--json] [--dest <dir>]
75
+ npx @heihei0299/matt-skills sync [--all] [--dry-run] [--refresh-agents] [--dest <dir>]
76
76
  npx @heihei0299/matt-skills check [--all] [--json] [--upstream <url>] [--ref <ref>]
77
77
  ```
78
78
 
79
79
  常用选项:
80
80
 
81
- - `--all`:包含全部可分发 skills,默认范围只包含 programming skills。
81
+ - `--all`:包含全部可分发 skills,默认范围只包含 programming skills;不会隐式刷新 `AGENTS.md`。
82
+ - `--refresh-agents`:显式刷新目标 `AGENTS.md`;与 `--all`、`--dry-run` 可组合。
82
83
  - `--dest <dir>`:指定目标目录。
83
84
  - `--tools <list>`:选择 `codex`、`pi`、`opencode` 或 `claude`;项目级 skills 统一写入 `.agents/skills/`,全局安装仍使用各工具目录。
84
85
  - `--global`:写入用户级 skills 目录。
85
- - `--dry-run`:只检查差异,不写入;`--json` 输出机器可读结果。
86
+ - `--dry-run`:预演目标项目差异,不写入;`sync --dry-run --json` 输出机器可读结果。
87
+ - `check --json`:输出上游比较的机器可读结果。普通 `sync --json` 不支持。
86
88
 
87
89
  ## Codex CLI 支持
88
90
 
@@ -100,9 +102,11 @@ CODEX_E2E=1 npm run codex:smoke
100
102
  非独有 skills 来自 [mattpocock/skills](https://github.com/mattpocock/skills)。
101
103
 
102
104
  ```sh
103
- npx @heihei0299/matt-skills sync --dry-run --json # 检查上游差异
104
- npx @heihei0299/matt-skills sync # 同步默认范围
105
- npx @heihei0299/matt-skills sync --all # 同步全部可分发范围
105
+ npx @heihei0299/matt-skills check --json # 检查上游差异
106
+ npx @heihei0299/matt-skills sync --dry-run --json # 预演目标项目变化
107
+ npx @heihei0299/matt-skills sync # 同步默认范围
108
+ npx @heihei0299/matt-skills sync --all # 同步全部可分发范围
109
+ npx @heihei0299/matt-skills sync --refresh-agents # 同步并刷新 AGENTS.md
106
110
  ```
107
111
 
108
112
  ## 发布
package/bin/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { readdir, readFile, writeFile, cp, stat, lstat, rm, mkdir } from 'node:fs/promises';
2
+ import { readdir, readFile, writeFile, cp, stat, lstat, rm, mkdir, mkdtemp } from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
@@ -39,7 +39,7 @@ const HELP_GLOBAL = `matt-skills — install and manage this skill collection
39
39
 
40
40
  Usage:
41
41
  matt-skills init [options] Initialize a project: template + skills (${PROJECT_SKILL_DIRS})
42
- matt-skills sync [--all|--dry-run] [--dest <path>] Sync existing project to latest template + skills
42
+ matt-skills sync [--all|--dry-run|--refresh-agents] [--dest <path>] Sync existing project to latest template + skills
43
43
  matt-skills list [--all] [--json] List available skills and their descriptions
44
44
  matt-skills install [options] Install skills (interactive by default)
45
45
  matt-skills check [--all] [--json] [--upstream <url>] [--ref <ref>]
@@ -57,7 +57,7 @@ Init options:
57
57
  --dest <path> Target directory (default: current directory)
58
58
  --all Include all distributable skills; default only default programming skills
59
59
  --help, -h Show this help
60
- 提示:已有 AGENTS.md 时普通 init 跳过;显式 init --all 刷新模板并覆盖全量可分发 skills。
60
+ 提示:已有 AGENTS.md 时 init 始终跳过;需要更新已有项目请使用 sync。
61
61
 
62
62
  提示:matt-skills --help 查看全量
63
63
  `;
@@ -65,16 +65,19 @@ Init options:
65
65
  const HELP_SYNC = `matt-skills sync — Sync existing project to latest template + skills
66
66
 
67
67
  Usage:
68
- matt-skills sync [--all|--dry-run] [--dest <path>]
68
+ matt-skills sync [--all|--dry-run|--refresh-agents] [--dest <path>]
69
69
 
70
70
  Sync options:
71
- --all 仅更新同名技能内容(存在则覆盖,不存在则新增)并更新 AGENTS.md
72
- --dry-run 预演:只比对不写盘
73
- --dest <path> Target directory (default: current directory)
74
- --help, -h Show this help
71
+ --all 同步全部可分发技能;不改变 AGENTS.md 刷新策略
72
+ --refresh-agents 显式刷新 AGENTS.md;无受管区块时先备份为 AGENTS.md.bak
73
+ --dry-run 预演目标项目变化,只比对不写盘
74
+ --json 仅与 --dry-run 一起使用,输出机器可读结果
75
+ --dest <path> Target directory (default: current directory)
76
+ --help, -h Show this help
75
77
  项目 skills:${PROJECT_SKILL_DIRS}
76
78
 
77
- 说明:默认不带 --all 增量同步默认 programming skill,并保留现有 AGENTS.md;--all 时同步全部可分发 skill 并强制刷新 AGENTS.md。
79
+ 说明:默认同步默认编程 skill 并保留现有 AGENTS.md;--all 只扩大技能范围。
80
+ --refresh-agents 与 --all、--dry-run 可组合;上游检查请使用 check。
78
81
 
79
82
  提示:matt-skills --help 查看全量
80
83
  `;
@@ -126,6 +129,8 @@ Install options:
126
129
 
127
130
  const HELP = HELP_GLOBAL;
128
131
 
132
+ const DRY_RUN_PATHS = ['AGENTS.md', 'AGENTS.md.bak', '.opencode', '.pi', '.agents/skills', '.claude/skills'];
133
+
129
134
  function parseFrontmatter(text) {
130
135
  const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
131
136
  if (!match) return {};
@@ -377,7 +382,7 @@ async function initCommand({ dest, all }) {
377
382
  const target = dest ? path.resolve(process.cwd(), dest) : process.cwd();
378
383
  const marker = path.join(target, 'AGENTS.md');
379
384
  const onlyProgramming = !all;
380
- if (await pathExists(marker) && !all) {
385
+ if (await pathExists(marker)) {
381
386
  process.stdout.write('模板已存在(AGENTS.md),跳过\n');
382
387
  } else {
383
388
  await cp(TEMPLATE_DIR, target, {
@@ -422,73 +427,124 @@ async function initCommand({ dest, all }) {
422
427
  }
423
428
  process.stdout.write(`目标路径:${target}\n`);
424
429
  }
425
- async function syncCommand({ dest, all, dryRun, json, upstreamUrl, ref }) {
430
+ async function copyDryRunInputs(target, stage) {
431
+ for (const relative of DRY_RUN_PATHS) {
432
+ const source = path.join(target, relative);
433
+ if (!await pathExists(source)) continue;
434
+ const destination = path.join(stage, relative);
435
+ await mkdir(path.dirname(destination), { recursive: true });
436
+ await cp(source, destination, { recursive: true, force: true });
437
+ }
438
+ }
439
+
440
+ async function compareDryRunTrees(target, stage) {
441
+ const result = { added: [], updated: [], removed: [] };
442
+ for (const relative of DRY_RUN_PATHS) {
443
+ const before = path.join(target, relative);
444
+ const after = path.join(stage, relative);
445
+ const beforeExists = await pathExists(before);
446
+ const afterExists = await pathExists(after);
447
+ if (!beforeExists && afterExists) result.added.push(relative);
448
+ else if (beforeExists && !afterExists) result.removed.push(relative);
449
+ else if (beforeExists && afterExists && !await sameTree(before, after)) result.updated.push(relative);
450
+ }
451
+ return result;
452
+ }
453
+
454
+ function formatTargetComparison({ target, result, onlyProgramming, refreshAgents }) {
455
+ const lines = [
456
+ `目标: ${target}`,
457
+ `范围: ${onlyProgramming ? '默认编程' : '全部可分发'}${refreshAgents ? ';刷新 AGENTS.md' : ''}`,
458
+ '',
459
+ ];
460
+ const totalDiff = result.added.length + result.updated.length + result.removed.length;
461
+ if (totalDiff === 0) {
462
+ lines.push('✅ 无需更新');
463
+ return lines.join('\n');
464
+ }
465
+ if (result.added.length) lines.push(`新增 (${result.added.length}): ${result.added.join(', ')}`);
466
+ if (result.updated.length) lines.push(`更新 (${result.updated.length}): ${result.updated.join(', ')}`);
467
+ if (result.removed.length) lines.push(`删除 (${result.removed.length}): ${result.removed.join(', ')}`);
468
+ return lines.join('\n');
469
+ }
470
+
471
+ async function refreshAgentsFile(targetFile) {
472
+ const templateFile = path.join(TEMPLATE_DIR, 'AGENTS.md');
473
+ const current = await readFile(targetFile, 'utf8');
474
+ const template = await readFile(templateFile, 'utf8');
475
+ const merged = mergeManagedAgents(current, template);
476
+ if (merged !== null) {
477
+ if (merged !== current) await writeFile(targetFile, merged);
478
+ return 'managed';
479
+ }
480
+ await cp(targetFile, `${targetFile}.bak`, { force: true });
481
+ await cp(templateFile, targetFile, { force: true });
482
+ return 'full';
483
+ }
484
+
485
+ async function syncCommand({ dest, all, dryRun, json, refreshAgents, quiet = false }) {
426
486
  const onlyProgramming = !all;
487
+ const output = (text) => {
488
+ if (!quiet) process.stdout.write(text);
489
+ };
490
+ if (!dryRun && json) throw new Error('--json 仅支持 sync --dry-run');
427
491
  if (dryRun) {
428
- const { compare, formatComparison } = await import('../scripts/sync-upstream.js');
429
- const cmp = await compare({ upstreamUrl, ref, onlyProgramming });
430
- if (json) {
431
- process.stdout.write(JSON.stringify({ head: cmp.head, counts: cmp.counts, result: cmp.result, onlyProgramming }, null, 2) + '\n');
432
- } else {
433
- process.stdout.write(formatComparison(cmp) + '\n');
492
+ const target = dest ? path.resolve(process.cwd(), dest) : process.cwd();
493
+ const stage = await mkdtemp(path.join(os.tmpdir(), 'matt-skills-sync-dry-run-'));
494
+ try {
495
+ await copyDryRunInputs(target, stage);
496
+ await syncCommand({ dest: stage, all, dryRun: false, json: false, refreshAgents, quiet: true });
497
+ const result = await compareDryRunTrees(target, stage);
498
+ const payload = { target, result, onlyProgramming, refreshAgents };
499
+ if (json) output(`${JSON.stringify(payload, null, 2)}\n`);
500
+ else output(`${formatTargetComparison(payload)}\n`);
501
+ const totalDiff = result.added.length + result.updated.length + result.removed.length;
502
+ if (totalDiff > 0) process.exitCode = 1;
503
+ } finally {
504
+ await rm(stage, { recursive: true, force: true });
434
505
  }
435
- const { rm } = await import('node:fs/promises');
436
- await rm(cmp.dest, { recursive: true, force: true });
437
- const totalDiff = cmp.result.added.length + cmp.result.updated.length + cmp.result.removed.length + cmp.result.renamed.length;
438
- if (totalDiff > 0) process.exitCode = 1;
439
506
  return;
440
507
  }
441
508
 
442
509
  const target = dest ? path.resolve(process.cwd(), dest) : process.cwd();
443
510
  const marker = path.join(target, 'AGENTS.md');
444
- // 模板同步:默认模式下过滤 skills,仅同步默认子集
445
511
  async function copyTemplateFiltered() {
446
- if (!onlyProgramming) {
447
- await cp(TEMPLATE_DIR, target, {
448
- recursive: true,
449
- force: true,
450
- filter: shouldCopyTemplatePath,
451
- });
452
- return;
453
- }
454
- // Skeleton only; shared Skills are copied from the canonical source below.
455
- await cp(path.join(TEMPLATE_DIR, 'AGENTS.md'), path.join(target, 'AGENTS.md'), { force: true });
456
- await cp(path.join(TEMPLATE_DIR, '.opencode'), path.join(target, '.opencode'), { recursive: true, force: true });
457
- await cp(path.join(TEMPLATE_DIR, '.pi'), path.join(target, '.pi'), { recursive: true, force: true });
458
- }
459
- if (!(await pathExists(marker))) {
460
- process.stdout.write('未检测到现有项目(AGENTS.md 不存在),将执行全新初始化\n');
461
- if (onlyProgramming) await copyTemplateFiltered();
462
- else await cp(TEMPLATE_DIR, target, {
512
+ await cp(TEMPLATE_DIR, target, {
463
513
  recursive: true,
464
514
  force: true,
465
515
  filter: shouldCopyTemplatePath,
466
516
  });
467
- process.stdout.write(`模板:已复制(AGENTS.md、skills:${PROJECT_SKILL_DIRS})\n`);
517
+ }
518
+ if (!(await pathExists(marker))) {
519
+ output('未检测到现有项目(AGENTS.md 不存在),将执行全新初始化\n');
520
+ await copyTemplateFiltered();
521
+ output(`模板:已复制(AGENTS.md、skills:${PROJECT_SKILL_DIRS})\n`);
468
522
  } else {
469
- process.stdout.write('同步:检测到现有项目,将增量更新\n');
470
- if (all) {
471
- await cp(TEMPLATE_DIR, target, {
472
- recursive: true,
473
- force: true,
474
- filter: shouldCopyTemplatePath,
475
- });
476
- process.stdout.write(`模板:已同步(AGENTS.md 整体刷新、skills:${PROJECT_SKILL_DIRS})\n`);
523
+ output('同步:检测到现有项目,将增量更新\n');
524
+ let agentsManaged = false;
525
+ let agentsRefreshed = false;
526
+ let agentsRefreshMode = null;
527
+ if (refreshAgents) {
528
+ agentsRefreshMode = await refreshAgentsFile(marker);
529
+ agentsRefreshed = true;
477
530
  } else {
478
- let agentsManaged = false;
479
531
  try {
480
532
  agentsManaged = await syncManagedAgents(marker);
481
533
  } catch {}
482
- await cp(path.join(TEMPLATE_DIR, '.opencode'), path.join(target, '.opencode'), { recursive: true, force: true });
483
- await cp(path.join(TEMPLATE_DIR, '.pi'), path.join(target, '.pi'), { recursive: true, force: true });
484
- if (agentsManaged) {
485
- process.stdout.write(`模板:已同步(AGENTS.md 受管区块已更新、skills:${PROJECT_SKILL_DIRS},项目自定义内容已保留)\n`);
486
- } else {
487
- process.stdout.write(`模板:已同步(AGENTS.md 未受管、skills:${PROJECT_SKILL_DIRS},已原样保留)\n`);
488
- }
534
+ }
535
+ await cp(path.join(TEMPLATE_DIR, '.opencode'), path.join(target, '.opencode'), { recursive: true, force: true });
536
+ await cp(path.join(TEMPLATE_DIR, '.pi'), path.join(target, '.pi'), { recursive: true, force: true });
537
+ if (agentsRefreshed && agentsRefreshMode === 'managed') {
538
+ output(`模板:已同步(AGENTS.md 受管区块已刷新、skills:${PROJECT_SKILL_DIRS},项目自定义内容已保留)\n`);
539
+ } else if (agentsRefreshed) {
540
+ output(`模板:已同步(AGENTS.md 已刷新,旧文件备份为 AGENTS.md.bak、skills:${PROJECT_SKILL_DIRS})\n`);
541
+ } else if (agentsManaged) {
542
+ output(`模板:已同步(AGENTS.md 受管区块已更新、skills:${PROJECT_SKILL_DIRS},项目自定义内容已保留)\n`);
543
+ } else {
544
+ output(`模板:已同步(AGENTS.md 未受管、skills:${PROJECT_SKILL_DIRS},已原样保留)\n`);
489
545
  }
490
546
  }
491
- // 技能同步:--all 仅更新同名可分发技能内容,存在则覆盖,不存在则新增,并更新 AGENTS.md(由上一步已处理);默认范围为默认 programming,不删多余
547
+ // --all 只扩大技能范围;同名覆盖、不存在新增,不删除目标中的额外技能。
492
548
  const allSkills = await listSkillNames({ onlyProgramming });
493
549
  const skillTargets = projectSkillTargets(target);
494
550
  const skillsDir = path.resolve(target, PROJECT_DIRS.codex);
@@ -569,20 +625,19 @@ async function syncCommand({ dest, all, dryRun, json, upstreamUrl, ref }) {
569
625
  if (await pathExists(piSettings)) {
570
626
  const txt = await readFile(piSettings, 'utf8');
571
627
  if (txt.includes('.opencode/skills') || txt.includes('../.opencode')) {
572
- const { writeFile } = await import('node:fs/promises');
573
628
  await writeFile(piSettings, '{}\n');
574
629
  }
575
630
  }
576
631
  } catch {}
577
632
  if (cleanedLegacy.length) {
578
- process.stdout.write(`迁移清理:${cleanedLegacy.join(', ')} 旧共享副本已移除\n`);
633
+ output(`迁移清理:${cleanedLegacy.join(', ')} 旧共享副本已移除\n`);
579
634
  }
580
635
  if (preservedRepoLocal.length) {
581
- process.stdout.write(`迁移提示:${preservedRepoLocal.join(', ')} 已不再分发,现有副本已保留\n`);
636
+ output(`迁移提示:${preservedRepoLocal.join(', ')} 已不再分发,现有副本已保留\n`);
582
637
  }
583
638
  const modeLabel = onlyProgramming ? '默认编程' : '全部可分发';
584
- process.stdout.write(`技能:新增 ${installed}、更新 ${updated}(${modeLabel} ${allSkills.length})\n`);
585
- process.stdout.write(`目标路径:${target}\n`);
639
+ output(`技能:新增 ${installed}、更新 ${updated}(${modeLabel} ${allSkills.length})\n`);
640
+ output(`目标路径:${target}\n`);
586
641
  }
587
642
 
588
643
  function parseInitArgs(args) {
@@ -607,8 +662,7 @@ function parseSyncArgs(args) {
607
662
  let all = false;
608
663
  let dryRun = false;
609
664
  let json = false;
610
- let upstreamUrl;
611
- let ref;
665
+ let refreshAgents = false;
612
666
  for (let i = 0; i < args.length; i++) {
613
667
  const arg = args[i];
614
668
  if (arg === '--dest') {
@@ -618,20 +672,13 @@ function parseSyncArgs(args) {
618
672
  else if (arg === '--all') all = true;
619
673
  else if (arg === '--dry-run') dryRun = true;
620
674
  else if (arg === '--json') json = true;
621
- else if (arg === '--upstream') {
622
- if (i + 1 >= args.length || args[i + 1].startsWith('-')) throw new Error(`unknown option '--upstream' requires a value`);
623
- upstreamUrl = args[++i];
624
- } else if (arg.startsWith('--upstream=')) upstreamUrl = arg.slice('--upstream='.length);
625
- else if (arg === '--ref') {
626
- if (i + 1 >= args.length || args[i + 1].startsWith('-')) throw new Error(`unknown option '--ref' requires a value`);
627
- ref = args[++i];
628
- } else if (arg.startsWith('--ref=')) ref = arg.slice('--ref='.length);
675
+ else if (arg === '--refresh-agents') refreshAgents = true;
629
676
  else if (arg === '--help' || arg === '-h') {} // handled at main
630
677
  else if (arg === '--apply') {} // deprecated alias, same as default safe incremental
631
678
  else if (arg.startsWith('-')) throw new Error(`unknown option '${arg}' for command 'sync'`);
632
679
  else throw new Error(`unknown argument '${arg}' for command 'sync'`);
633
680
  }
634
- return { dest, all, dryRun, json, upstreamUrl, ref };
681
+ return { dest, all, dryRun, json, refreshAgents };
635
682
  }
636
683
 
637
684
  function parseInstallArgs(args) {
@@ -721,15 +768,15 @@ async function main() {
721
768
  if (!knownCommands.has(command)) {
722
769
  process.stderr.write(`error: unknown command '${command}'\n`);
723
770
  process.stderr.write(`Run 'matt-skills --help' for usage.\n`);
724
- process.exitCode = 1;
771
+ process.exitCode = 2;
725
772
  return;
726
773
  }
727
774
  if (command === 'list') {
728
775
  // list strict: only --all/--json/--help allowed, rest handled via parse but we keep simple
729
776
  for (const a of rest) {
730
777
  if (a === '--all' || a === '--json' || a === '--help' || a === '-h') continue;
731
- 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; }
732
- 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;
778
+ 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 = 2; return; }
779
+ process.stderr.write(`error: unknown argument '${a}' for command 'list'\n`); process.stderr.write(`Run 'matt-skills list --help' for usage.\n`); process.exitCode = 2; return;
733
780
  }
734
781
  const onlyProgramming = !rest.includes('--all');
735
782
  const skills = await listSkills({ onlyProgramming });
@@ -759,21 +806,30 @@ async function main() {
759
806
  for (let i = 0; i < rest.length; i++) {
760
807
  const a = rest[i];
761
808
  if (a === '--all' || a === '--json' || a === '--help' || a === '-h') continue;
762
- if (a === '--upstream' || a === '--ref') { i++; continue; }
809
+ if (a === '--upstream' || a === '--ref') {
810
+ if (i + 1 >= rest.length || rest[i + 1].startsWith('-')) {
811
+ process.stderr.write(`error: option '${a}' for command 'check' requires a value\n`);
812
+ process.stderr.write(`Run 'matt-skills check --help' for usage.\n`);
813
+ process.exitCode = 2;
814
+ return;
815
+ }
816
+ i++;
817
+ continue;
818
+ }
763
819
  if (a.startsWith('--upstream=') || a.startsWith('--ref=')) continue;
764
- 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; }
765
- 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;
820
+ 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 = 2; return; }
821
+ process.stderr.write(`error: unknown argument '${a}' for command 'check'\n`); process.stderr.write(`Run 'matt-skills check --help' for usage.\n`); process.exitCode = 2; return;
766
822
  }
767
823
  await checkCommand(rest);
768
824
  return;
769
825
  }
770
826
  if (command === 'update') {
771
827
  process.stderr.write('update 已合并到 sync(默认即增量同步)\n');
772
- process.exitCode = 1;
828
+ process.exitCode = 2;
773
829
  return;
774
830
  }
775
831
  }
776
832
  main().catch((error) => {
777
833
  process.stderr.write(`error: ${error.message}\n`);
778
- process.exitCode = 1;
834
+ process.exitCode = 2;
779
835
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heihei0299/matt-skills",
3
- "version": "3.0.14",
3
+ "version": "3.0.16",
4
4
  "description": "Agent skills + 项目配置模板:一条命令初始化 opencode / pi-agent 项目(含 mattpocock/skills 上游技能)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,3 +1,4 @@
1
+ <!-- matt-skills:managed:start -->
1
2
  # AGENTS.md
2
3
 
3
4
  ## Workflow
@@ -75,3 +76,4 @@ codegraph node "<符号>"
75
76
  ## Completion
76
77
 
77
78
  完成时说明改动或审查结论、已执行验证、未执行验证及原因、剩余风险;如有 commit,报告 commit hash。
79
+ <!-- matt-skills:managed:end -->