@winspan/claude-forge 8.51.0 → 8.53.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 (85) hide show
  1. package/CLAUDE.md +6 -6
  2. package/dist/cli/commands/skills.d.ts.map +1 -1
  3. package/dist/cli/commands/skills.js +115 -0
  4. package/dist/cli/commands/skills.js.map +1 -1
  5. package/dist/core/constants.d.ts +2 -0
  6. package/dist/core/constants.d.ts.map +1 -1
  7. package/dist/core/constants.js +4 -0
  8. package/dist/core/constants.js.map +1 -1
  9. package/dist/daemon/hook-sync.d.ts +17 -0
  10. package/dist/daemon/hook-sync.d.ts.map +1 -0
  11. package/dist/daemon/hook-sync.js +74 -0
  12. package/dist/daemon/hook-sync.js.map +1 -0
  13. package/dist/daemon/index.d.ts.map +1 -1
  14. package/dist/daemon/index.js +21 -1
  15. package/dist/daemon/index.js.map +1 -1
  16. package/dist/daemon/skill-sync.d.ts +21 -0
  17. package/dist/daemon/skill-sync.d.ts.map +1 -0
  18. package/dist/daemon/skill-sync.js +75 -0
  19. package/dist/daemon/skill-sync.js.map +1 -0
  20. package/dist/hooks/notification.sh +1 -1
  21. package/dist/hooks/post-tool-use.sh +1 -1
  22. package/dist/hooks/pre-tool-use.sh +1 -1
  23. package/dist/hooks/stop.sh +1 -1
  24. package/dist/hooks/user-prompt-submit.sh +1 -1
  25. package/dist/skills/official/code-simplifier.md +37 -1
  26. package/dist/skills/official/find-skills.md +120 -1
  27. package/dist/skills/official/official-api-design.md +14 -1
  28. package/dist/skills/official/official-architecture-decision.md +22 -1
  29. package/dist/skills/official/official-db-schema-design.md +19 -1
  30. package/dist/skills/official/official-debug.md +9 -1
  31. package/dist/skills/official/official-pr-review.md +1 -1
  32. package/dist/skills/official/official-security-hardening.md +7 -1
  33. package/dist/skills/official/planning-with-files.md +206 -2
  34. package/dist/skills/official/ui-ux-pro-max.md +88 -1
  35. package/dist/skills/official/webapp-testing.md +85 -1
  36. package/dist/skills/registry.d.ts +1 -1
  37. package/dist/skills/registry.d.ts.map +1 -1
  38. package/dist/skills/registry.js +2 -2
  39. package/dist/skills/registry.js.map +1 -1
  40. package/dist/skills/semantic-matcher.d.ts +2 -1
  41. package/dist/skills/semantic-matcher.d.ts.map +1 -1
  42. package/dist/skills/semantic-matcher.js +6 -3
  43. package/dist/skills/semantic-matcher.js.map +1 -1
  44. package/dist/skills/upgrade-engine.d.ts +91 -0
  45. package/dist/skills/upgrade-engine.d.ts.map +1 -0
  46. package/dist/skills/upgrade-engine.js +436 -0
  47. package/dist/skills/upgrade-engine.js.map +1 -0
  48. package/dist/skills/upgrade-prompt.d.ts +20 -0
  49. package/dist/skills/upgrade-prompt.d.ts.map +1 -0
  50. package/dist/skills/upgrade-prompt.js +75 -0
  51. package/dist/skills/upgrade-prompt.js.map +1 -0
  52. package/docs/design/skill-ai-upgrade-spec-20260518-1930.md +297 -0
  53. package/docs/implementation/daemon-skill-sync-changelog-20260518-2000.md +22 -0
  54. package/docs/implementation/skill-ai-upgrade-changelog-20260518-1930.md +49 -0
  55. package/package.json +1 -1
  56. package/src/cli/commands/skills.ts +143 -0
  57. package/src/core/constants.ts +5 -0
  58. package/src/daemon/hook-sync.ts +91 -0
  59. package/src/daemon/index.ts +21 -1
  60. package/src/daemon/skill-sync.ts +88 -0
  61. package/src/hooks/notification.sh +1 -1
  62. package/src/hooks/post-tool-use.sh +1 -1
  63. package/src/hooks/pre-tool-use.sh +1 -1
  64. package/src/hooks/stop.sh +1 -1
  65. package/src/hooks/user-prompt-submit.sh +1 -1
  66. package/src/skills/official/code-simplifier.md +37 -1
  67. package/src/skills/official/find-skills.md +120 -1
  68. package/src/skills/official/official-api-design.md +14 -1
  69. package/src/skills/official/official-architecture-decision.md +22 -1
  70. package/src/skills/official/official-db-schema-design.md +19 -1
  71. package/src/skills/official/official-debug.md +9 -1
  72. package/src/skills/official/official-pr-review.md +1 -1
  73. package/src/skills/official/official-security-hardening.md +7 -1
  74. package/src/skills/official/planning-with-files.md +206 -2
  75. package/src/skills/official/ui-ux-pro-max.md +88 -1
  76. package/src/skills/official/webapp-testing.md +85 -1
  77. package/src/skills/registry.ts +2 -2
  78. package/src/skills/semantic-matcher.ts +6 -3
  79. package/src/skills/upgrade-engine.ts +541 -0
  80. package/src/skills/upgrade-prompt.ts +84 -0
  81. package/tests/unit/daemon/hook-sync.test.ts +71 -0
  82. package/tests/unit/daemon/skill-sync.test.ts +75 -0
  83. package/tests/unit/skills/upgrade-engine-parse.test.ts +138 -0
  84. package/tests/unit/skills/upgrade-engine.test.ts +401 -0
  85. package/tests/unit/skills/upgrade-prompt.test.ts +89 -0
@@ -0,0 +1,297 @@
1
+ # AI 辅助 official skill 升级 Spec
2
+
3
+ ## 目标
4
+
5
+ 为 `claude-forge` 新增 `cf skills upgrade` 命令,通过 AI 评估将外部开源 skill 仓库(addyosmani/agent-skills、obra/superpowers)的优质内容合并进 `src/skills/official/*.md`(永久内置 skill),而不是让它们在用户目录独立存在。整个流程默认 dry-run,用户审核 markdown 报告后执行 `--apply`。
6
+
7
+ ## 用户决策(已通过对话确认)
8
+
9
+ | 决策点 | 确认值 |
10
+ |--------|--------|
11
+ | 候选源目录 | `~/.claude-forge/skill-candidates/<source-name>/` |
12
+ | AI 评估使用 | `config.ai.model` + `config.ai.base_url` |
13
+ | 覆盖目标 | `src/skills/official/*.md`(编译进 dist 的永久内置) |
14
+ | 报告路径 | `~/.claude-forge/skill-upgrade-report.md` |
15
+ | 默认行为 | dry-run(只生成报告) |
16
+ | `--apply` 行为 | 读取报告中 action,备份后覆写源码 |
17
+ | 备份路径 | `~/.claude-forge/backups/skills/<timestamp>/`(已有 FORGE_PATHS.backups('skills')) |
18
+
19
+ ## 数据流图
20
+
21
+ ```
22
+ [cf skills upgrade]
23
+
24
+
25
+ [1] pullCandidates()
26
+ ├─ git clone/pull ~/.claude-forge/skill-candidates/agent-skills/
27
+ └─ git clone/pull ~/.claude-forge/skill-candidates/superpowers/
28
+
29
+
30
+ [2] scanCandidateFiles()
31
+ └─ 遍历 .md 文件,解析 frontmatter → CandidateSkill[]
32
+
33
+
34
+ [3] matchToOfficial() (per candidate)
35
+ └─ tag/keyword 模糊匹配 → MatchResult (official id | null)
36
+
37
+
38
+ [4] evaluateWithAI() (有 official 匹配时才调用)
39
+ └─ ClaudeProvider.complete() → UpgradeDecision (JSON)
40
+
41
+
42
+ [5] generateReport()
43
+ └─ ~/.claude-forge/skill-upgrade-report.md
44
+
45
+ ▼ (用户审核)
46
+
47
+ [cf skills upgrade --apply]
48
+
49
+
50
+ [6] applyDecisions()
51
+ ├─ git status 工作树检查
52
+ ├─ backup src/skills/official/ → backups/skills/<ts>/
53
+ └─ 覆写 src/skills/official/<id>.md
54
+ ```
55
+
56
+ ## 模块设计
57
+
58
+ ### Module 1: `src/skills/upgrade-engine.ts`
59
+
60
+ **职责**:编排全流程,不含 AI prompt 字符串逻辑。
61
+
62
+ ```ts
63
+ export interface CandidateSkill {
64
+ id: string;
65
+ source: string; // 'agent-skills' | 'superpowers'
66
+ filePath: string;
67
+ name: string;
68
+ description: string;
69
+ keywords: string[];
70
+ content: string;
71
+ }
72
+
73
+ export interface MatchResult {
74
+ officialId: string;
75
+ score: number; // 0-100,关键词重叠率
76
+ }
77
+
78
+ export type UpgradeAction = 'upgrade' | 'merge' | 'skip';
79
+ export interface UpgradeDecision {
80
+ action: UpgradeAction;
81
+ confidence: number; // 0-100
82
+ reasoning: string;
83
+ merged_content: string | null;
84
+ }
85
+
86
+ export interface ReportEntry {
87
+ officialId: string;
88
+ candidateId: string;
89
+ candidateSource: string;
90
+ action: UpgradeAction | 'error' | 'needs_review';
91
+ confidence: number;
92
+ reasoning: string;
93
+ candidateFilePath: string;
94
+ merged_content: string | null;
95
+ }
96
+
97
+ export async function pullCandidates(
98
+ sources: Array<{ name: string; url: string }>,
99
+ candidatesDir: string,
100
+ ): Promise<CandidateSkill[]>
101
+
102
+ export function matchToOfficial(
103
+ candidate: CandidateSkill,
104
+ officialSkills: OfficialSkill[],
105
+ ): MatchResult | null // null = 无匹配(score < 30)
106
+
107
+ export async function evaluateWithAI(
108
+ candidate: CandidateSkill,
109
+ official: OfficialSkill,
110
+ aiProvider: ClaudeProvider,
111
+ ): Promise<UpgradeDecision>
112
+
113
+ export async function generateReport(
114
+ entries: ReportEntry[],
115
+ outputPath: string,
116
+ stats: { total: number; matched: number; unmatched: number },
117
+ ): Promise<void>
118
+
119
+ export async function applyDecisions(
120
+ reportPath: string,
121
+ officialDir: string,
122
+ backupBaseDir: string,
123
+ ): Promise<{ applied: number; skipped: number; backupPath: string }>
124
+ ```
125
+
126
+ **关键逻辑**:
127
+ - `pullCandidates`:单源失败 try/catch 跳过
128
+ - `matchToOfficial`:keywords + name token overlap,阈值 30 分(满分 100)
129
+ - `evaluateWithAI`:`maxTokens: 1200, timeoutMs: 45000`;JSON 解析失败返回 skip + needs_review
130
+ - `applyDecisions`:apply 前 `git status --porcelain` 检查工作树是否干净,脏的话 abort
131
+
132
+ ### Module 2: `src/skills/upgrade-prompt.ts`
133
+
134
+ ```ts
135
+ export function buildEvaluationPrompt(
136
+ candidate: CandidateSkill,
137
+ official: OfficialSkill,
138
+ ): { system: string; user: string }
139
+ ```
140
+
141
+ **System prompt 要点**:
142
+ ```
143
+ 你是 Claude Code skill 内容评估专家。
144
+ 评估维度:主题重叠度 / 内容质量 / 互补性
145
+ 返回严格 JSON(不含其他文本):
146
+ {"action":"upgrade|merge|skip","confidence":0-100,"reasoning":"...","merged_content":null或合并后完整md}
147
+
148
+ 决策标准:
149
+ - skip:主题不重叠,或 official 已完整覆盖
150
+ - upgrade:候选覆盖 official 全部要点且质量明显更高(+30% depth/breadth)
151
+ - merge:主题相同但各有独特章节,合并后价值更高
152
+ ```
153
+
154
+ **User prompt**:双 skill 内容各截 2000 字 + JSON 要求。
155
+
156
+ ### Module 3: 候选源拉取
157
+
158
+ ```ts
159
+ const DEFAULT_SOURCES = [
160
+ { name: 'agent-skills', url: 'https://github.com/addyosmani/agent-skills.git' },
161
+ { name: 'superpowers', url: 'https://github.com/obra/superpowers.git' },
162
+ ];
163
+ ```
164
+
165
+ **扫描兼容**:
166
+ - agent-skills 格式:`<name>/SKILL.md`(目录格式)
167
+ - superpowers 格式:顶层 `*.md`(平铺)
168
+ - 复用 `SkillRegistry.scan()` 的格式识别逻辑
169
+
170
+ ### Module 4: CLI 子命令
171
+
172
+ ```ts
173
+ skills
174
+ .command('upgrade')
175
+ .description('AI-assisted upgrade of official skills (dry-run by default)')
176
+ .option('--apply', 'Apply decisions from the last report')
177
+ .option('--rollback <timestamp>', 'Rollback to a backup')
178
+ .option('--report <path>', 'Custom report path')
179
+ .action(async (options) => { await upgradeSkills(options); });
180
+ ```
181
+
182
+ ## AI 输出契约
183
+
184
+ ```ts
185
+ type UpgradeDecision = {
186
+ action: 'upgrade' | 'merge' | 'skip';
187
+ confidence: number; // 0-100 整数
188
+ reasoning: string; // 简短中文 < 200 字
189
+ merged_content: string | null;
190
+ // merged_content 规则:
191
+ // merge → 完整合并后 .md(含 frontmatter)
192
+ // upgrade → null(直接用候选原文)
193
+ // skip → null
194
+ };
195
+ ```
196
+
197
+ 容错:`JSON.parse` 失败 → `action: skip, needs_review: true`,不阻断。
198
+
199
+ ## 报告格式
200
+
201
+ ```markdown
202
+ # Skill Upgrade Report
203
+
204
+ Generated: 2026-05-18 19:30 | Model: claude-opus-4-7
205
+
206
+ ## Summary
207
+
208
+ | Metric | Count |
209
+ |--------|-------|
210
+ | Candidates scanned | 37 (23 agent-skills + 14 superpowers) |
211
+ | Matched to official | 12 |
212
+ | Action: upgrade | 5 |
213
+ | Action: merge | 4 |
214
+ | Action: skip | 3 |
215
+ | Unmatched | 25 |
216
+
217
+ ## Per-Skill Decisions
218
+
219
+ ### official-db-schema-design
220
+ - **Action**: upgrade
221
+ - **Confidence**: 85%
222
+ - **Candidate**: `agent-skills/database-design`
223
+ - **Reasoning**: 候选增加"分库分表策略"和"迁移工具对比"...
224
+
225
+ <!-- upgrade-entry: official-db-schema-design | <candidatePath> | upgrade -->
226
+ ```
227
+
228
+ **机器可读标记**:HTML 注释 `<!-- upgrade-entry: <id> | <path> | <action> -->`;merge 用 `<!-- merged-content-begin -->...<!-- merged-content-end -->` 包裹合并内容。`--apply` 解析这些注释。
229
+
230
+ ## 备份机制
231
+
232
+ ```
233
+ ~/.claude-forge/backups/skills/
234
+ └── 20260518-1930/
235
+ ├── official-tdd.md
236
+ └── ... (所有 official .md 快照)
237
+ ```
238
+
239
+ - 只在 `--apply` 触发备份
240
+ - `--rollback <ts>` 还原
241
+ - 复用 `FORGE_PATHS.backups('skills')`
242
+ - 同 ts 已存在则追加 `-2`、`-3`
243
+
244
+ ## 错误处理
245
+
246
+ | 场景 | 处理 |
247
+ |------|------|
248
+ | git clone/pull 失败 | warn + 跳过该 source |
249
+ | AI JSON 解析失败 | needs_review,继续 |
250
+ | AI API 超时/5xx | retry 2 次失败 → error 标记 |
251
+ | `--apply` 工作树脏 | abort,提示先 commit |
252
+ | 候选无 frontmatter | 用文件名作 id,keywords=[] |
253
+ | 同 ts 备份冲突 | 后缀 -2 -3 |
254
+
255
+ ## 改造范围
256
+
257
+ | 文件 | 操作 | 行数估计 |
258
+ |------|------|---------|
259
+ | `src/skills/upgrade-engine.ts` | 新增 | ~200 |
260
+ | `src/skills/upgrade-prompt.ts` | 新增 | ~60 |
261
+ | `src/cli/commands/skills.ts` | 加 upgrade 子命令 + 函数 | +80 |
262
+ | `src/core/constants.ts` | 加 `skillCandidates` 路径(可选)| +2 |
263
+
264
+ **不改动**:dist/、daemon、web 路由、hook、SkillRegistry、official-skills.ts。
265
+
266
+ ## 测试策略
267
+
268
+ | 测试文件 | 内容 |
269
+ |---------|------|
270
+ | `tests/unit/skills/upgrade-engine.test.ts` | mock ClaudeProvider.complete → 三条路径;applyDecisions dry-run 不写;备份目录创建 |
271
+ | `tests/unit/skills/upgrade-prompt.test.ts` | system/user prompt 含必要字段;JSON 格式要求 |
272
+ | `tests/unit/skills/upgrade-engine-parse.test.ts` | AI 非 JSON → needs_review;合法 JSON 正确解析 |
273
+
274
+ 集成测试可选:本地 mock .md 跑 dry-run 全流程。
275
+
276
+ ## 风险与回滚
277
+
278
+ | 风险 | 缓解 |
279
+ |------|------|
280
+ | AI 误判覆盖好 skill | 默认 dry-run;apply 强制 backup;--rollback 还原 |
281
+ | 覆写后 build 错 | apply 后提示 `npm run build` 验证 |
282
+ | API 成本 | 仅 score>=30 调 AI;截 2000 字;预估 ~18K tokens / 12 matched |
283
+ | 候选仓库结构变化 | 单源失败不阻断 |
284
+
285
+ ## 实施顺序
286
+
287
+ 1. **upgrade-prompt.ts**(纯函数,无依赖)
288
+ 2. **upgrade-engine.ts** — matchToOfficial → evaluateWithAI → pullCandidates → generateReport → applyDecisions
289
+ 3. **cli/commands/skills.ts** — 注册 upgrade 子命令
290
+ 4. (可选)`FORGE_PATHS.skillCandidates`
291
+ 5. 测试:upgrade-prompt → upgrade-engine(mock)→ CLI e2e
292
+
293
+ ## 命名遵循
294
+
295
+ - 文件:kebab-case
296
+ - 函数:动词开头 camelCase(`pullCandidates` / `matchToOfficial`)
297
+ - 类型:PascalCase(`CandidateSkill` / `UpgradeDecision`)
@@ -0,0 +1,22 @@
1
+ # daemon skill 自动同步 Changelog
2
+
3
+ **Date**: 2026-05-18 20:00
4
+ **Status**: 完成
5
+
6
+ ## 背景
7
+ 类比 v8.51.1 hook-sync:npm 升级 dist/skills/official/ 但不会自动同步
8
+ ~/.claude/skills/ 本地副本,SkillRegistry 优先级 user > official 加载
9
+ 旧副本,导致 skill 残缺/功能错乱。
10
+
11
+ ## 改动
12
+ - 新建 src/daemon/skill-sync.ts: syncSkills() sha256 比对自动同步
13
+ - daemon/index.ts 启动时调用(紧挨 syncHooks)
14
+ - 失败 logger.warn 不阻断
15
+
16
+ ## 测试
17
+ - 5 case 单测全过
18
+ - 现有测试不退化
19
+ - tsc 0 errors
20
+
21
+ ## 已知问题
22
+ - linkEventToTask pre-existing(sqlite-refactor-harness.test.ts)
@@ -0,0 +1,49 @@
1
+ # AI 辅助 skill 升级 Changelog
2
+
3
+ **Date**: 2026-05-18 19:30
4
+ **Spec**: docs/design/skill-ai-upgrade-spec-20260518-1930.md
5
+ **Status**: 完成
6
+
7
+ ## 完成清单
8
+
9
+ - [x] `src/skills/upgrade-prompt.ts` — buildEvaluationPrompt 纯函数
10
+ - [x] `src/skills/upgrade-engine.ts` — 5 个 pipeline 函数 + 全部类型定义
11
+ - [x] `src/cli/commands/skills.ts` — upgrade 子命令 + upgradeSkills() 函数
12
+ - [x] `src/core/constants.ts` — 新增 skillCandidates / skillUpgradeReport 路径
13
+ - [x] 单测 3 文件(44 个新测试用例全过)
14
+
15
+ ## 关键代码定位
16
+
17
+ - `src/skills/upgrade-prompt.ts:14` — `buildEvaluationPrompt()` system/user prompt 构造
18
+ - `src/skills/upgrade-engine.ts:105` — `pullCandidates()` git clone/pull + .md 扫描
19
+ - `src/skills/upgrade-engine.ts:141` — `matchToOfficial()` Jaccard 关键词重叠评分(阈值 30/100)
20
+ - `src/skills/upgrade-engine.ts:183` — `evaluateWithAI()` AI 调用 + JSON 容错解析
21
+ - `src/skills/upgrade-engine.ts:237` — `generateReport()` markdown + HTML 注释机器标记
22
+ - `src/skills/upgrade-engine.ts:284` — `applyDecisions()` git status 检查 + 备份 + 写文件
23
+ - `src/cli/commands/skills.ts:126` — `upgradeSkills()` 全流程协调(dry-run / apply / rollback)
24
+
25
+ ## 测试结果
26
+
27
+ - 新增测试: 44 passed(upgrade-prompt: 11, upgrade-engine: 22, upgrade-engine-parse: 11)
28
+ - tsc: 0 errors
29
+ - npm test: 614 passed / 615 total(1 pre-existing failure: linkEventToTask)
30
+ - build: 成功
31
+
32
+ ## 实施决策
33
+
34
+ 1. **ClaudeProvider.complete() 接口**: 使用 `complete(prompt, options)` 形式,其中 `options.system` 传递 system prompt。经查 `src/core/ai/provider.ts` 确认签名。
35
+
36
+ 2. **require('node:fs') 用于同步读文件**: `parseCandidateFile` 使用同步 `readFileSync`,通过 `require('node:fs')` 调用(ESM 模块中已有 `import fs` 但需要同步读取)。
37
+
38
+ 3. **matchToOfficial 评分策略**: 采用 keyword overlap(权重 60%)+ name/description token overlap(权重 40%)的加权 Jaccard 相似度,而非简单字符串匹配。
39
+
40
+ 4. **evaluateWithAI markdown fence 剥离**: AI 模型有时会将 JSON 包裹在 `\`\`\`json` 代码块中,在 JSON.parse 前统一剥离。
41
+
42
+ 5. **applyDecisions git status**: 使用 `git status --porcelain <officialDir>` 检查工作树。若目标目录不在 git 仓库中(如测试用 tmp 目录),warn 并继续(不 abort)。
43
+
44
+ 6. **备份冲突后缀**: 对 `<YYYYMMDD-HHMM>` 时间戳已存在时,自动追加 `-2`、`-3` 等后缀,通过循环 `fs.access` 找到可用路径。
45
+
46
+ ## 已知问题
47
+
48
+ - `linkEventToTask` 测试失败:pre-existing,与本次改动无关
49
+ - 手测 deferred:网络环境未验证 git clone 实际可达性;dry-run 流程逻辑已通过 mock 测试覆盖
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@winspan/claude-forge",
3
- "version": "8.51.0",
3
+ "version": "8.53.2",
4
4
  "description": "SDLC intelligent orchestration engine for Claude Code",
5
5
  "main": "dist/cli/index.js",
6
6
  "type": "module",
@@ -2,11 +2,24 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { homedir } from 'os';
4
4
  import { fileURLToPath } from 'url';
5
+ import { promises as fsPromises } from 'node:fs';
5
6
  import type { Command } from 'commander';
6
7
  import { loadOfficialSkills } from '../../skills/official-skills.js';
7
8
  import { SkillRegistry } from '../../skills/registry.js';
8
9
  import { skillInvoke } from '../../skills/tools/skill-invoke.js';
9
10
  import { skillList } from '../../skills/tools/skill-list.js';
11
+ import {
12
+ pullCandidates,
13
+ matchToOfficial,
14
+ evaluateWithAI,
15
+ generateReport,
16
+ applyDecisions,
17
+ DEFAULT_SOURCES,
18
+ } from '../../skills/upgrade-engine.js';
19
+ import type { ReportEntry } from '../../skills/upgrade-engine.js';
20
+ import { ConfigManager } from '../../core/config.js';
21
+ import { ClaudeProvider } from '../../core/ai/provider.js';
22
+ import { FORGE_PATHS } from '../../core/constants.js';
10
23
 
11
24
  const SKILLS_DIR = path.join(homedir(), '.claude', 'skills');
12
25
 
@@ -98,6 +111,126 @@ async function invokeSkill(skillId: string, options: InvokeOptions): Promise<voi
98
111
  }
99
112
  }
100
113
 
114
+ interface UpgradeOptions {
115
+ apply?: boolean;
116
+ rollback?: string;
117
+ report?: string;
118
+ }
119
+
120
+ /**
121
+ * Coordinate the full skill upgrade pipeline.
122
+ * Default: dry-run — generates a report without writing to src/.
123
+ * --apply: read report and apply decisions.
124
+ * --rollback <ts>: restore official skills from a backup timestamp.
125
+ */
126
+ async function upgradeSkills(options: UpgradeOptions): Promise<void> {
127
+ const reportPath = options.report ?? FORGE_PATHS.skillUpgradeReport();
128
+ const officialDir = resolveBuiltinDir();
129
+ const backupBaseDir = FORGE_PATHS.backups('skills');
130
+ const candidatesDir = FORGE_PATHS.skillCandidates();
131
+
132
+ // ── Rollback mode ────────────────────────────────────────────────────────
133
+ if (options.rollback) {
134
+ const backupPath = path.join(backupBaseDir, options.rollback);
135
+ try {
136
+ await fsPromises.access(backupPath);
137
+ } catch {
138
+ console.error(`Backup not found: ${backupPath}`);
139
+ process.exit(1);
140
+ }
141
+ const files = await fsPromises.readdir(backupPath);
142
+ for (const file of files) {
143
+ if (!file.endsWith('.md')) continue;
144
+ await fsPromises.copyFile(path.join(backupPath, file), path.join(officialDir, file));
145
+ }
146
+ console.log(`Rolled back ${files.filter(f => f.endsWith('.md')).length} files from ${backupPath}`);
147
+ console.log('Run `npm run build` to rebuild with restored skills.');
148
+ return;
149
+ }
150
+
151
+ // ── Apply mode ───────────────────────────────────────────────────────────
152
+ if (options.apply) {
153
+ try {
154
+ await fsPromises.access(reportPath);
155
+ } catch {
156
+ console.error(`Report not found: ${reportPath}`);
157
+ console.error('Run `cf skills upgrade` first to generate a report.');
158
+ process.exit(1);
159
+ }
160
+ console.log(`Applying decisions from ${reportPath}...`);
161
+ const result = await applyDecisions(reportPath, officialDir, backupBaseDir);
162
+ console.log(`\nApply complete:`);
163
+ console.log(` Applied : ${result.applied}`);
164
+ console.log(` Skipped : ${result.skipped}`);
165
+ console.log(` Backup : ${result.backupPath}`);
166
+ console.log('\nRun `npm run build` to rebuild with updated skills.');
167
+ return;
168
+ }
169
+
170
+ // ── Dry-run (report generation) mode ────────────────────────────────────
171
+ console.log('Fetching candidate skills...');
172
+ const candidates = await pullCandidates(DEFAULT_SOURCES, candidatesDir);
173
+ console.log(` Found ${candidates.length} candidate skills`);
174
+
175
+ const officialSkills = loadOfficialSkills(officialDir);
176
+ console.log(` Loaded ${officialSkills.length} official skills`);
177
+
178
+ // Set up AI provider
179
+ const config = new ConfigManager().get();
180
+ if (!config.ai.api_key) {
181
+ console.error('No AI API key configured. Run `cf menu` to set it up.');
182
+ process.exit(1);
183
+ }
184
+ const aiProvider = new ClaudeProvider(config.ai.api_key, config.ai.model, config.ai.base_url);
185
+
186
+ let matched = 0;
187
+ let unmatched = 0;
188
+ const entries: ReportEntry[] = [];
189
+
190
+ console.log('\nEvaluating candidates...');
191
+
192
+ for (const candidate of candidates) {
193
+ const matchResult = matchToOfficial(candidate, officialSkills);
194
+ if (!matchResult) {
195
+ unmatched++;
196
+ continue;
197
+ }
198
+
199
+ matched++;
200
+ const official = officialSkills.find((o) => o.name === matchResult.officialId);
201
+ if (!official) continue;
202
+
203
+ process.stdout.write(` ${candidate.id} → ${official.name} (score ${matchResult.score})... `);
204
+
205
+ const decision = await evaluateWithAI(candidate, official, aiProvider);
206
+ const action = (decision as { needs_review?: boolean }).needs_review
207
+ ? 'needs_review'
208
+ : decision.action;
209
+
210
+ process.stdout.write(`${action}\n`);
211
+
212
+ entries.push({
213
+ officialId: official.name,
214
+ candidateId: candidate.id,
215
+ candidateSource: candidate.source,
216
+ action,
217
+ confidence: decision.confidence,
218
+ reasoning: decision.reasoning,
219
+ candidateFilePath: candidate.filePath,
220
+ merged_content: decision.merged_content,
221
+ });
222
+ }
223
+
224
+ const stats = { total: candidates.length, matched, unmatched };
225
+ await generateReport(entries, reportPath, stats);
226
+
227
+ console.log(`\nReport generated: ${reportPath}`);
228
+ console.log(`Summary: ${matched} matched, ${unmatched} unmatched (of ${candidates.length} total)`);
229
+ console.log('\nReview the report, then run:');
230
+ console.log(' cf skills upgrade --apply (to apply decisions)');
231
+ }
232
+
233
+
101
234
  function registerSkillSubcommands(skills: Command): void {
102
235
  skills
103
236
  .command('sync')
@@ -124,6 +257,16 @@ function registerSkillSubcommands(skills: Command): void {
124
257
  .action(async (skillId: string, options: InvokeOptions) => {
125
258
  await invokeSkill(skillId, options);
126
259
  });
260
+
261
+ skills
262
+ .command('upgrade')
263
+ .description('AI-assisted upgrade of official skills (dry-run by default)')
264
+ .option('--apply', 'Apply decisions from the last report')
265
+ .option('--rollback <timestamp>', 'Rollback to a backup (e.g. 20260518-1930)')
266
+ .option('--report <path>', 'Custom report path')
267
+ .action(async (options: UpgradeOptions) => {
268
+ await upgradeSkills(options);
269
+ });
127
270
  }
128
271
 
129
272
  export function register(program: Command): void {
@@ -28,6 +28,11 @@ export const FORGE_PATHS = {
28
28
  // ── Patchable targets ───────────────────────────────────
29
29
  routingYaml: () => join(FORGE_HOME, 'routing.yaml'),
30
30
  backups: (kind: 'skills' | 'routing') => join(FORGE_HOME, 'backups', kind),
31
+ skillCandidates: (source?: string) =>
32
+ source
33
+ ? join(FORGE_HOME, 'skill-candidates', source)
34
+ : join(FORGE_HOME, 'skill-candidates'),
35
+ skillUpgradeReport: () => join(FORGE_HOME, 'skill-upgrade-report.md'),
31
36
  } as const;
32
37
 
33
38
  /**
@@ -0,0 +1,91 @@
1
+ import { existsSync, readFileSync, copyFileSync, chmodSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { join, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { FORGE_PATHS } from '../core/constants.js';
6
+ import { logger } from '../core/utils/logger.js';
7
+
8
+ // All hook files to sync (5 hooks + shared lib)
9
+ const HOOK_FILES = [
10
+ 'pre-tool-use.sh',
11
+ 'post-tool-use.sh',
12
+ 'user-prompt-submit.sh',
13
+ 'notification.sh',
14
+ 'stop.sh',
15
+ 'hook-lib.sh',
16
+ ];
17
+
18
+ function sha256(content: Buffer): string {
19
+ return createHash('sha256').update(content).digest('hex');
20
+ }
21
+
22
+ function getSourceHooksDir(): string {
23
+ // Compiled: dist/daemon/hook-sync.js → dist/hooks
24
+ // Dev (vitest): src/daemon/hook-sync.ts → src/hooks
25
+ return join(dirname(fileURLToPath(import.meta.url)), '..', 'hooks');
26
+ }
27
+
28
+ export interface SyncResult {
29
+ copied: number;
30
+ checked: number;
31
+ skipped: number;
32
+ }
33
+
34
+ /**
35
+ * Sync hooks from the package source (dist/hooks) to ~/.claude-forge/hooks/.
36
+ * Uses SHA-256 comparison to skip identical files.
37
+ *
38
+ * Accepts optional overrides for source/target dirs — used in unit tests.
39
+ * Any failure logs a warning and continues; never throws.
40
+ */
41
+ export function syncHooks(opts?: { sourceDir?: string; targetDir?: string }): SyncResult {
42
+ const result: SyncResult = { copied: 0, checked: 0, skipped: 0 };
43
+ const sourceDir = opts?.sourceDir ?? getSourceHooksDir();
44
+ const targetDir = opts?.targetDir ?? FORGE_PATHS.hooks();
45
+
46
+ if (!existsSync(sourceDir)) {
47
+ logger.warn(`[HookSync] source hooks dir not found at ${sourceDir}, skipping`);
48
+ return result;
49
+ }
50
+
51
+ if (!existsSync(targetDir)) {
52
+ // User has not run `claude-forge init` yet — skip silently
53
+ logger.debug(`[HookSync] target dir not found: ${targetDir} (user may not have run \`claude-forge init\`)`);
54
+ return result;
55
+ }
56
+
57
+ for (const file of HOOK_FILES) {
58
+ const src = join(sourceDir, file);
59
+ const dest = join(targetDir, file);
60
+
61
+ if (!existsSync(src)) {
62
+ result.skipped++;
63
+ continue;
64
+ }
65
+
66
+ result.checked++;
67
+
68
+ try {
69
+ const srcContent = readFileSync(src);
70
+ if (existsSync(dest)) {
71
+ const destContent = readFileSync(dest);
72
+ if (sha256(srcContent) === sha256(destContent)) {
73
+ continue; // identical — no copy needed
74
+ }
75
+ }
76
+
77
+ copyFileSync(src, dest);
78
+ chmodSync(dest, 0o755);
79
+ result.copied++;
80
+ logger.info(`[HookSync] updated ${file}`);
81
+ } catch (err) {
82
+ logger.warn(`[HookSync] failed to sync ${file}: ${err instanceof Error ? err.message : String(err)}`);
83
+ }
84
+ }
85
+
86
+ if (result.copied > 0) {
87
+ logger.info(`[HookSync] synced ${result.copied}/${result.checked} hook files`);
88
+ }
89
+
90
+ return result;
91
+ }
@@ -34,6 +34,8 @@ import { ConventionExtractor } from '../claudemd/convention-extractor.js';
34
34
  import { UserPromptHandler } from './handlers/user-prompt.js';
35
35
  import { PostToolUseHandler } from './handlers/post-tool-use.js';
36
36
  import { StopHandler } from './handlers/stop.js';
37
+ import { syncHooks } from './hook-sync.js';
38
+ import { syncSkills } from './skill-sync.js';
37
39
  import { replayQueue } from '../core/queue/index.js';
38
40
  import { DEFAULTS } from '../core/constants.js';
39
41
  import type { ForgeEvent } from '../core/types.js';
@@ -74,6 +76,24 @@ export async function startDaemon(foreground: boolean = false, options: DaemonOp
74
76
  const storage = new SQLiteStorage(dbPath);
75
77
  logger.info(`Storage initialized: ${dbPath}`);
76
78
 
79
+ // ── 3.5. Auto-sync hooks ────────────────────────────────────────────────
80
+ // npm upgrade 不会自动更新 ~/.claude-forge/hooks/,每次 daemon 启动
81
+ // 用 SHA-256 比对源 dist/hooks 与本地副本,不一致则覆盖。
82
+ try {
83
+ syncHooks();
84
+ } catch (err) {
85
+ logger.warn(`[HookSync] unexpected error: ${err}`);
86
+ }
87
+
88
+ // ── 3.6. Auto-sync official skills ─────────────────────────────────────
89
+ // npm upgrade 不会自动更新 ~/.claude/skills/,每次 daemon 启动
90
+ // 用 SHA-256 比对 dist/skills/official 与本地副本,不一致则覆盖。
91
+ try {
92
+ syncSkills();
93
+ } catch (err) {
94
+ logger.warn(`[SkillSync] unexpected error: ${err}`);
95
+ }
96
+
77
97
  // ── 4. AI Provider ─────────────────────────────────────────────────────────
78
98
  const apiKey = config.ai.api_key || process.env.ANTHROPIC_API_KEY || '';
79
99
  if (!apiKey) {
@@ -86,7 +106,7 @@ export async function startDaemon(foreground: boolean = false, options: DaemonOp
86
106
 
87
107
  // ── 5. Initialize services ─────────────────────────────────────────────────
88
108
  const skillApiKey = config.skill_matching?.api_key || apiKey;
89
- const skillRegistry = new SkillRegistry(skillApiKey);
109
+ const skillRegistry = new SkillRegistry(skillApiKey, config.ai.model, config.ai.base_url);
90
110
  const invocationGuard = new InvocationGuard();
91
111
  if (skillApiKey) {
92
112
  logger.info('[Skills] Registry loaded with semantic matching support');