skillcaddy 0.27.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +497 -0
  2. package/CONTEXT.md +149 -0
  3. package/LICENSE +21 -0
  4. package/README.md +499 -0
  5. package/README_CN.md +482 -0
  6. package/collection-metadata/github/mattpocock.json +16 -0
  7. package/lib/canonicalJson.js +11 -0
  8. package/lib/claudeStore.js +135 -0
  9. package/lib/collectionEnablement.js +115 -0
  10. package/lib/collectionSetup.js +199 -0
  11. package/lib/enablePlan.js +116 -0
  12. package/lib/enablementScope.js +8 -0
  13. package/lib/hermesStore.js +21 -0
  14. package/lib/libraryImage.js +1785 -0
  15. package/lib/managerInstall.js +109 -0
  16. package/lib/managerSourceWorkflow.js +234 -0
  17. package/lib/projectActions.js +24 -0
  18. package/lib/projectPath.js +51 -0
  19. package/lib/roots.js +99 -0
  20. package/lib/skillLinkStore.js +68 -0
  21. package/lib/skillMetadata.js +191 -0
  22. package/lib/skillStore.js +541 -0
  23. package/lib/sourceAcquisition.js +475 -0
  24. package/lib/sourceAcquisitionAdapter.js +47 -0
  25. package/lib/sourceAcquisitionError.js +18 -0
  26. package/lib/sourceAcquisitionPolicy.js +139 -0
  27. package/lib/sourceArchive.js +363 -0
  28. package/lib/sourceArchiveWorkspace.js +36 -0
  29. package/lib/sourceGit.js +95 -0
  30. package/lib/sourceGitAcquisitionAdapter.js +145 -0
  31. package/lib/sourceGitBatchUpdate.js +168 -0
  32. package/lib/sourceGitRepair.js +227 -0
  33. package/lib/sourceGitStaging.js +139 -0
  34. package/lib/sourceGitUpgradeAdapter.js +583 -0
  35. package/lib/sourceGitUrl.js +170 -0
  36. package/lib/sourceHttp.js +302 -0
  37. package/lib/sourceLocal.js +200 -0
  38. package/lib/sourceLocalAcquisitionAdapter.js +107 -0
  39. package/lib/sourceManager.js +242 -0
  40. package/lib/sourceMigration.js +172 -0
  41. package/lib/sourceMigrationIssue.js +6 -0
  42. package/lib/sourcePath.js +6 -0
  43. package/lib/sourcePolicy.js +53 -0
  44. package/lib/sourceRecord.js +295 -0
  45. package/lib/sourceRegistry.js +155 -0
  46. package/lib/sourceRemoteArchiveAcquisitionAdapter.js +147 -0
  47. package/lib/sourceRemoteFileAcquisitionAdapter.js +95 -0
  48. package/lib/sourceRemoteFilePolicy.js +62 -0
  49. package/lib/sourceRemoteFileWorkspace.js +55 -0
  50. package/lib/sourceSkillDiscovery.js +117 -0
  51. package/lib/sourceTree.js +185 -0
  52. package/lib/sourceUpgrade.js +596 -0
  53. package/lib/sourceUpgradeAdapter.js +41 -0
  54. package/lib/sourceUpgradePolicy.js +170 -0
  55. package/lib/sourceUpgradeTransaction.js +202 -0
  56. package/lib/sourceValidation.js +120 -0
  57. package/lib/sourceWorkspace.js +37 -0
  58. package/lib/specGate.js +235 -0
  59. package/lib/tuiActions.js +169 -0
  60. package/lib/tuiInstall.js +133 -0
  61. package/lib/tuiLayout.js +94 -0
  62. package/lib/version.js +17 -0
  63. package/lib/webManager.js +370 -0
  64. package/package.json +74 -0
  65. package/public/agentsUi.js +138 -0
  66. package/public/app.js +1431 -0
  67. package/public/claude.css +2 -0
  68. package/public/claudeUi.js +119 -0
  69. package/public/emptyState.js +12 -0
  70. package/public/enabled.css +303 -0
  71. package/public/favicon.svg +5 -0
  72. package/public/index.html +199 -0
  73. package/public/skillcaddy_CN.png +0 -0
  74. package/public/skillcaddy_EN.png +0 -0
  75. package/public/styles.css +1611 -0
  76. package/scripts/cli.js +647 -0
  77. package/scripts/cron-sync-update-git.sh +128 -0
  78. package/scripts/pull-github.sh +22 -0
  79. package/scripts/source.js +638 -0
  80. package/scripts/tui.js +1058 -0
  81. package/server.js +196 -0
  82. package/skills/skillcaddy-manager/SKILL.md +93 -0
  83. package/skills/skillcaddy-manager/agents/openai.yaml +7 -0
  84. package/skills/skillcaddy-manager/references/METADATA.md +75 -0
  85. package/skills/skillcaddy-manager/references/OPERATIONS.md +315 -0
  86. package/skills/skillcaddy-manager/references/RECOMMENDATION_GUIDE.md +51 -0
  87. package/skills/skillcaddy-manager/references/featured-skills.json +564 -0
  88. package/skills/skillcaddy-manager/references/skill-platforms.json +48 -0
  89. package/skills/skillcaddy-manager/scripts/check-conflicts.cjs +117 -0
  90. package/skills/skillcaddy-manager/scripts/check-global-skills.cjs +169 -0
  91. package/skills/skillcaddy-manager/scripts/translate-skill-notes.cjs +297 -0
  92. package/skills/skillcaddy-manager/scripts/version-manager.cjs +146 -0
  93. package/skills/skillcaddy-manager/scripts/view-recommendations.cjs +279 -0
  94. package/skills/skillcaddy-manager/skillcaddy.json +9 -0
  95. package/skills/spec-pipeline/spec-direction-gate/SKILL.md +119 -0
  96. package/skills/spec-pipeline/spec-direction-gate/check-approval.py +110 -0
  97. package/skills/spec-pipeline/spec-draft-intake/SKILL.md +116 -0
  98. package/skills/spec-pipeline/spec-role-contract/SKILL.md +113 -0
  99. package/skills/spec-pipeline/spec-role-contract/check-roles.py +108 -0
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Skillcaddy 版本管理脚本
4
+ * 用法:
5
+ * node version-manager.cjs check - 检查版本
6
+ * node version-manager.cjs sync - 同步版本号
7
+ * node version-manager.cjs changelog - 查看更新日志
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ // 从 skill 目录向上找到项目根目录
14
+ const skillDir = __dirname; // skills/skillcaddy-manager/scripts
15
+ const skillcaddyRoot = path.resolve(skillDir, '..', '..', '..'); // 回到 skillcaddy 根目录
16
+
17
+ const dataPath = path.join(skillDir, '..', 'references', 'featured-skills.json');
18
+ const packagePath = path.join(skillcaddyRoot, 'package.json');
19
+
20
+ console.log('');
21
+
22
+ function loadJSON(filePath) {
23
+ try {
24
+ const content = fs.readFileSync(filePath, 'utf-8');
25
+ return JSON.parse(content);
26
+ } catch (err) {
27
+ console.error(`Error loading ${filePath}:`, err.message);
28
+ return null;
29
+ }
30
+ }
31
+
32
+ function checkVersion() {
33
+ const data = loadJSON(dataPath);
34
+ const pkg = loadJSON(packagePath);
35
+
36
+ if (!data || !pkg) {
37
+ console.error('Failed to load version information');
38
+ process.exit(1);
39
+ }
40
+
41
+ console.log('## 📦 Skillcaddy 版本信息\n');
42
+ console.log(`**Skillcaddy 版本**: ${pkg.version}`);
43
+ console.log(`**推荐数据版本**: ${data.version}`);
44
+ console.log(`**最后更新**: ${data.lastUpdated}\n`);
45
+
46
+ if (data.version !== pkg.version) {
47
+ console.log('⚠️ 版本不匹配!');
48
+ console.log(` 推荐数据版本: ${data.version}`);
49
+ console.log(` 当前项目版本: ${pkg.version}`);
50
+ console.log('\n建议运行: node version-manager.cjs sync\n');
51
+ } else {
52
+ console.log('✅ 版本同步\n');
53
+ }
54
+ }
55
+
56
+ function showChangelog() {
57
+ const data = loadJSON(dataPath);
58
+
59
+ if (!data || !data.changelog) {
60
+ console.error('No changelog available');
61
+ return;
62
+ }
63
+
64
+ console.log('## 📋 更新日志\n');
65
+
66
+ data.changelog.forEach(entry => {
67
+ console.log(`### Version ${entry.version} (${entry.date})`);
68
+ entry.changes.forEach(change => {
69
+ console.log(` - ${change}`);
70
+ });
71
+ console.log('');
72
+ });
73
+ }
74
+
75
+ function syncVersion() {
76
+ const pkg = loadJSON(packagePath);
77
+ const data = loadJSON(dataPath);
78
+
79
+ if (!pkg || !data) {
80
+ console.error('Failed to load files');
81
+ process.exit(1);
82
+ }
83
+
84
+ console.log('## 🔄 同步版本\n');
85
+ console.log(`当前项目版本: ${pkg.version}`);
86
+ console.log(`推荐数据版本: ${data.version}\n`);
87
+
88
+ if (data.version === pkg.version) {
89
+ console.log('✅ 版本已同步,无需更新\n');
90
+ return;
91
+ }
92
+
93
+ const today = new Date().toISOString().split('T')[0];
94
+
95
+ // 同步版本号
96
+ data.version = pkg.version;
97
+ data.lastUpdated = today;
98
+
99
+ // 添加 changelog 条目
100
+ const newChange = {
101
+ version: pkg.version,
102
+ date: today,
103
+ changes: [
104
+ `同步至 Skillcaddy v${pkg.version}`,
105
+ "更新推荐数据和配置"
106
+ ]
107
+ };
108
+
109
+ if (!data.changelog) {
110
+ data.changelog = [];
111
+ }
112
+ data.changelog.unshift(newChange);
113
+
114
+ // 保存文件
115
+ fs.writeFileSync(dataPath, JSON.stringify(data, null, 2));
116
+
117
+ console.log('✅ 版本已同步\n');
118
+ console.log('记得提交更改:');
119
+ console.log(' git add references/featured-skills.json');
120
+ console.log(` git commit -m "chore: sync recommendations to v${pkg.version}"`);
121
+ }
122
+
123
+ // 主程序
124
+ const args = process.argv.slice(2);
125
+ const command = args[0];
126
+
127
+ switch (command) {
128
+ case 'check':
129
+ checkVersion();
130
+ break;
131
+
132
+ case 'sync':
133
+ syncVersion();
134
+ break;
135
+
136
+ case 'changelog':
137
+ showChangelog();
138
+ break;
139
+
140
+ default:
141
+ console.log('Usage:');
142
+ console.log(' node version-manager.cjs check - 检查版本同步');
143
+ console.log(' node version-manager.cjs sync - 同步版本号');
144
+ console.log(' node version-manager.cjs changelog - 查看更新日志');
145
+ break;
146
+ }
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 查看推荐 skills 列表
4
+ * 用法: node view-recommendations.js [command] [args]
5
+ *
6
+ * Commands:
7
+ * onboarding - 查看空库引导
8
+ * workflows - 查看核心工作流推荐
9
+ * scenario <name> - 查看特定场景推荐
10
+ * category <name> - 查看特定分类
11
+ * platforms - 查看平台列表
12
+ * all - 查看所有内容
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const dataPath = path.join(__dirname, '..', 'references', 'featured-skills.json');
19
+ const platformsPath = path.join(__dirname, '..', 'references', 'skill-platforms.json');
20
+
21
+ function loadJSON(filePath) {
22
+ try {
23
+ const content = fs.readFileSync(filePath, 'utf-8');
24
+ return JSON.parse(content);
25
+ } catch (err) {
26
+ console.error(`Error loading ${filePath}:`, err.message);
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function printEssentialWorkflows(workflows) {
32
+ console.log('\n## 🎯 核心工作流推荐\n');
33
+ console.log('仅在明确开发场景时使用:\n');
34
+
35
+ for (const [key, workflow] of Object.entries(workflows)) {
36
+ console.log(`### ${workflow.label}`);
37
+ console.log(`${workflow.description}\n`);
38
+
39
+ workflow.collections.forEach((col, idx) => {
40
+ const typeIcon = col.type === 'workflow-suite' ? '📦' : '🔧';
41
+ const priorityLabel = col.priority === 1 ? '【必装】' : '【必装】';
42
+
43
+ console.log(`${idx + 1}. ${typeIcon} **${col.name}** ${priorityLabel}`);
44
+
45
+ if (col.setupSkill) {
46
+ console.log(` ⚡ Setup: ${col.setupSkill} (一键配置)`);
47
+ }
48
+
49
+ console.log(` 📋 包含:`);
50
+ col.includes.forEach(item => {
51
+ console.log(` - ${item}`);
52
+ });
53
+
54
+ console.log(` 🔧 安装: ${col.installCommand}`);
55
+
56
+ if (col.installs) {
57
+ console.log(` 📊 安装量: ${col.installs}`);
58
+ }
59
+ console.log('');
60
+ });
61
+
62
+ console.log(`💡 ${workflow.whyThisCombo}\n`);
63
+ console.log('---\n');
64
+ }
65
+ }
66
+
67
+ function printScenarioRecommendation(scenarios, scenarioName) {
68
+ const scenario = scenarios[scenarioName];
69
+ if (!scenario) {
70
+ console.error(`Scenario "${scenarioName}" not found.`);
71
+ console.log('Available scenarios:', Object.keys(scenarios).join(', '));
72
+ return;
73
+ }
74
+
75
+ console.log(`\n## 🎯 ${scenario.label}\n`);
76
+ console.log(`${scenario.description}\n`);
77
+
78
+ if (scenario.recommend) {
79
+ console.log('**推荐安装:**\n');
80
+
81
+ if (scenario.recommend.primary) {
82
+ console.log('核心推荐:');
83
+ scenario.recommend.primary.forEach((id, idx) => {
84
+ console.log(` ${idx + 1}. ${id}`);
85
+ });
86
+ }
87
+
88
+ if (scenario.recommend.optional) {
89
+ console.log('\n可选推荐:');
90
+ scenario.recommend.optional.forEach((id, idx) => {
91
+ console.log(` ${idx + 1}. ${id}`);
92
+ });
93
+ }
94
+
95
+ if (scenario.recommend.why) {
96
+ console.log(`\n💡 ${scenario.recommend.why}`);
97
+ }
98
+ }
99
+
100
+ if (scenario.setupSteps) {
101
+ console.log('\n**安装步骤:**');
102
+ scenario.setupSteps.forEach(step => {
103
+ console.log(` ${step}`);
104
+ });
105
+ }
106
+
107
+ if (scenario.followUp) {
108
+ console.log('\n**后续判断:**');
109
+ scenario.followUp.forEach(step => {
110
+ console.log(` - ${step}`);
111
+ });
112
+ }
113
+ console.log('');
114
+ }
115
+
116
+ function printOnboarding(data, platforms) {
117
+ console.log('\n## 🧭 空库引导\n');
118
+ console.log('当用户库里还没有任何 skills 时,先做发现入口推荐,不要直接默认安装具体库。\n');
119
+
120
+ if (data.scenarios?.['empty-library']) {
121
+ printScenarioRecommendation(data.scenarios, 'empty-library');
122
+ }
123
+
124
+ console.log('**推荐平台:**\n');
125
+ printPlatforms(platforms);
126
+
127
+ console.log('**分流原则:**');
128
+ console.log(' - 无法判断场景: 只给平台和分类入口');
129
+ console.log(' - 明确是开发项目: 再推荐 mattpocock-workflow 或 lencx-control');
130
+ console.log(' - 已有少量库: 优先补短板,不要重复推荐同类能力');
131
+ }
132
+
133
+ function printCollections(collections, category = null, priority = null) {
134
+ const categories = category ? { [category]: collections[category] } : collections;
135
+
136
+ for (const [catName, catData] of Object.entries(categories)) {
137
+ if (!catData || !catData.collections) continue;
138
+
139
+ console.log(`\n## ${catData.label}`);
140
+ console.log(`${catData.description}\n`);
141
+
142
+ const filtered = priority
143
+ ? catData.collections.filter(c => c.priority === priority)
144
+ : catData.collections;
145
+
146
+ filtered.forEach(col => {
147
+ const priorityIcon = {
148
+ essential: '🔥',
149
+ recommended: '⭐',
150
+ optional: '📦'
151
+ }[col.priority] || '•';
152
+
153
+ const suiteBadge = col.isSuite ? ' [套件]' : '';
154
+ console.log(`${priorityIcon} **${col.name}**${suiteBadge} (${col.priority})`);
155
+
156
+ if (col.setupSkill) {
157
+ console.log(` ⚡ Setup: ${col.setupSkill}`);
158
+ }
159
+
160
+ console.log(` Repo: ${col.repo}`);
161
+ console.log(` Skills: ${col.skills.join(', ')}`);
162
+ console.log(` Reason: ${col.reason}`);
163
+
164
+ if (col.installs) {
165
+ console.log(` Installs: ${col.installs}`);
166
+ }
167
+
168
+ if (col.setupGuide) {
169
+ console.log(` 📝 Guide: ${col.setupGuide}`);
170
+ }
171
+ console.log('');
172
+ });
173
+ }
174
+ }
175
+
176
+ function printPlatforms(platforms) {
177
+ console.log('\n## 🌐 Skills 平台\n');
178
+ platforms.platforms.forEach(p => {
179
+ console.log(`**${p.name}** (${p.url})`);
180
+ console.log(` ${p.description}`);
181
+ console.log(` 适合: ${p.bestFor}`);
182
+ console.log('');
183
+ });
184
+ }
185
+
186
+ function printStrategy(strategy) {
187
+ console.log('\n## 📋 推荐策略\n');
188
+ console.log(`**原则**: ${strategy.principle}`);
189
+ console.log(`**最多推荐**: ${strategy.maxRecommendations} 个库\n`);
190
+ console.log('要点:');
191
+ strategy.notes.forEach(note => {
192
+ console.log(` - ${note}`);
193
+ });
194
+ }
195
+
196
+ // 主程序
197
+ const args = process.argv.slice(2);
198
+ const command = args[0];
199
+ const subCommand = args[1];
200
+
201
+ const data = loadJSON(dataPath);
202
+ const platforms = loadJSON(platformsPath);
203
+
204
+ if (!data) {
205
+ console.error('Failed to load featured-skills.json');
206
+ process.exit(1);
207
+ }
208
+
209
+ console.log('# Skillcaddy Recommendations');
210
+ console.log(`Version: ${data.version}`);
211
+ console.log(`Last Updated: ${data.lastUpdated}`);
212
+
213
+ // 显示推荐策略
214
+ if (data.recommendationStrategy) {
215
+ printStrategy(data.recommendationStrategy);
216
+ }
217
+
218
+ switch (command) {
219
+ case 'onboarding':
220
+ printOnboarding(data, platforms);
221
+ break;
222
+
223
+ case 'workflows':
224
+ if (data.essentialWorkflows) {
225
+ printEssentialWorkflows(data.essentialWorkflows);
226
+ }
227
+ break;
228
+
229
+ case 'scenario':
230
+ if (subCommand && data.scenarios) {
231
+ printScenarioRecommendation(data.scenarios, subCommand);
232
+ } else {
233
+ console.log('\n可用场景:');
234
+ Object.keys(data.scenarios || {}).forEach(key => {
235
+ const s = data.scenarios[key];
236
+ console.log(` - ${key}: ${s.label}`);
237
+ });
238
+ }
239
+ break;
240
+
241
+ case 'category':
242
+ if (subCommand && data.featured) {
243
+ printCollections(data.featured, subCommand);
244
+ } else {
245
+ console.log('\n可用分类:');
246
+ Object.keys(data.featured || {}).forEach(key => {
247
+ const c = data.featured[key];
248
+ console.log(` - ${key}: ${c.label}`);
249
+ });
250
+ }
251
+ break;
252
+
253
+ case 'platforms':
254
+ printPlatforms(platforms);
255
+ break;
256
+
257
+ case 'all':
258
+ if (data.essentialWorkflows) {
259
+ printEssentialWorkflows(data.essentialWorkflows);
260
+ }
261
+ if (data.featured) {
262
+ printCollections(data.featured);
263
+ }
264
+ printPlatforms(platforms);
265
+ break;
266
+
267
+ default:
268
+ printOnboarding(data, platforms);
269
+
270
+ console.log('\n---\n');
271
+ console.log('Usage:');
272
+ console.log(' node view-recommendations.cjs onboarding - 空库引导');
273
+ console.log(' node view-recommendations.cjs workflows - 核心工作流');
274
+ console.log(' node view-recommendations.cjs scenario new-project - 特定场景');
275
+ console.log(' node view-recommendations.cjs category development - 特定分类');
276
+ console.log(' node view-recommendations.cjs platforms - 平台列表');
277
+ console.log(' node view-recommendations.cjs all - 查看所有');
278
+ break;
279
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "note": "管理 Skillcaddy skill 原件库、项目启用状态、冲突建议和 Web 管理器操作流程。",
3
+ "tags": [
4
+ "Developer Tools",
5
+ "Productivity",
6
+ "Workflow"
7
+ ],
8
+ "autoEnable": true
9
+ }
@@ -0,0 +1,119 @@
1
+ ---
2
+ name: spec-direction-gate
3
+ description: 技术方案文档的方向确认关卡。在 specs/<id>/TECH.md 草稿完成、进入实施之前,先确认方向、再确认内容,两次确认分开且有序;把关卡结论写回 TECH.md 作为批准状态的显式记录。拒绝语义:没有人在场就停止并报告,绝不自动放行。用于任何"文档获批才能开始写代码"的环节之前。
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # 方向确认关卡(B)
8
+
9
+ 一条流程只有在"方向"这一格真的会挡住它的时候才算成立。这个技能就是那一格。
10
+
11
+ 输入是 `specs/<id>/TECH.md` 的草稿,输出是写回 `TECH.md` 的批准状态记录。中间没有任何自动放行的路径。
12
+
13
+ ## 何时使用
14
+
15
+ - `TECH.md` 草稿已写完,尚未开始实施
16
+ - 需要在实施前留下"谁在什么时候批准了方向"的可核验记录
17
+ - 复盘时需要确认某个实施是否真的经过方向批准,而不是默认通过
18
+
19
+ ## 何时不使用
20
+
21
+ - 纯 UI 改动、单文件修复等低复杂度变更,`TECH.md` 本身就不必产出
22
+ - 方向已经单独确认过、这次只是修订若干小节时,改用下面的"增量修订"路径
23
+
24
+ ## 硬性约束
25
+
26
+ 1. **两次确认,不合并。** 先确认方向,确认通过之后才确认内容。合并这一步会破坏关卡:把大纲和方向放在同一次确认里,人会锚定在已经看到的具体大纲上,而不再重新考虑它上位的那个问题。
27
+ 2. **拒绝语义。** 没有人在场就停止并报告这个技能需要人,不要"先按草稿往下走,回头再确认"。
28
+ 3. **批准要落盘。** 两次确认的结论写进 `TECH.md`,不只留在对话里。实施阶段的验收要能读到"批准后文档仍存活"的证据。
29
+ 4. **批准的是方向,不是逐字内容。** 方向确认通过后,作者仍可改实现细节;实施者不得改方向。
30
+
31
+ ## 流程
32
+
33
+ ### 第 0 步:确认在场
34
+
35
+ 先确认当前是有人可以回答的交互会话。
36
+
37
+ 不是交互式会话时:**停下并报告**"这个关卡需要人在场确认方向",然后结束。不要用默认值代替确认,也不要因为草稿看起来已经足够好就跳过。
38
+
39
+ ### 第 1 步:确认方向
40
+
41
+ 读 `TECH.md`,然后只向人提出**方向层面**的问题,不要展示具体大纲或章节结构:
42
+
43
+ - 这份文档要解决的问题,和仓库当前状态对得上吗
44
+ - 选定的方案,是这几个候选里最合适的那个吗
45
+ - 哪些明确不在这次范围内
46
+ - 还有什么上位问题,是这份文档答不了的
47
+
48
+ 把问题一次问完,然后等回答。
49
+
50
+ **方向被拒时的处理**:记录被拒的方向和原因,停止。不要改写成新的大纲再问第二遍——那等于把第一次确认作废。修改草稿后必须从第 1 步重来。
51
+
52
+ ### 第 2 步:确认内容
53
+
54
+ 只有在第 1 步通过后才进入这一步。展示文档的实际章节和每节承担的内容,然后等确认。
55
+
56
+ 内容被拒时:按意见修订,同样从第 1 步重新开始(内容修订可能改变方向)。
57
+
58
+ ### 第 3 步:写回批准状态
59
+
60
+ 两次都通过后,把结论追加到 `TECH.md` 末尾:
61
+
62
+ ```markdown
63
+ ## Approval
64
+
65
+ - 方向确认:通过 / 拒绝(时间:YYYY-MM-DD HH:MM CST,确认人:__)
66
+ - 内容确认:通过 / 拒绝(时间:YYYY-MM-DD HH:MM CST,确认人:__)
67
+ - 本次确认的范围:仅方向与结构。实现细节可由作者调整,方向变更须重新过本关卡。
68
+ ```
69
+
70
+ 批准状态是**显式记录**,不是推断依据。没有这段内容,验收阶段必须按"未批准"处理。
71
+
72
+ 写完用同目录的校验脚本自查:
73
+
74
+ ```bash
75
+ python3 check-approval.py specs/<id>/TECH.md
76
+ ```
77
+
78
+ 退出码 `0` = 已批准可进入实施;`1` = 挡住(缺 `## Approval`、结论仍是 `通过 / 拒绝` 模板、缺时间戳、方向被拒、结论无法判定);`2` = 用法错误。
79
+
80
+ 模板里的 `通过 / 拒绝` 必须二选一写死。**两个候选都留着会被判为未批准**——这是有意的,模板没填就等于没确认。
81
+
82
+ ## 实施前的机械检查
83
+
84
+ 上面的约束是文字。文字挡不住无人值守运行,所以本技能附带一个退出码校验:
85
+
86
+ ```bash
87
+ python3 check-approval.py specs/<id>/TECH.md
88
+ ```
89
+
90
+ 进入实施之前跑一次,退出码非 `0` 就不要开始写代码。脚本只读不写,不会改文档。
91
+
92
+ 被挡住的常见情况:
93
+
94
+ - 文件不存在
95
+ - 没有 `## Approval` 段
96
+ - 有 `## Approval` 段但里面没有方向确认结论(**摄入技能产出的就是这个状态**)
97
+ - 结论仍是 `通过 / 拒绝` 模板
98
+ - 通过但没写时间戳(批准必须可核验到"谁在什么时候批准")
99
+ - 方向被明确拒绝
100
+ - 既不是通过也不是拒绝——按未批准处理
101
+
102
+ `## Approval` 之后如果还有别的章节,脚本只读到下一个章节标题为止;把"方向确认:通过"藏在后面的章节里不算数。`## Source draft` 之类段落里出现的"通过"也不会被误读成批准。
103
+
104
+ ## 增量修订路径
105
+
106
+ 只改了措辞、补充了行级引用、调整了不影响方向的实施顺序时,不必重走两次确认。在现有的 `## Approval` 段下面追加一条修订记录,写明改了什么、为什么不需要重新确认方向。
107
+
108
+ 判断权在人手上。拿不准就走完整路径。
109
+
110
+ ## 与其他技能的衔接
111
+
112
+ - 上游:`write-tech-spec` 产出 `TECH.md`(`specs/<id>/` 目录约定)
113
+ - 下游:`implement-specs` 的前置条件只是"approved enough"这种软条件,**不构成关卡**。真正的关卡在这里,跑完本技能才算进入实施
114
+ - 实施期间:`write-tech-spec` 的 *Keep the spec current* 要求 `TECH.md` 随实施同 PR 更新,且**不得删掉 `## Approval` 段**——批准状态是验收阶段要核验的东西
115
+ - 验收阶段:新会话中对照文档验收,不对照对话
116
+
117
+ ## 验收用例
118
+
119
+ 要能构造"方向被拒"的用例,流程必须停止并报告,不得自动续跑。判定依据是对照 `write-feature-docs` 第 449–454 行的拒绝语义:不得重新加入无人值守路径,须停下并报告该技能需要人工在场。
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Direction-confirmation gate check for specs/<id>/TECH.md.
4
+
5
+ Thin shell over lib/specGate.js (decideApproval), so the decision a developer
6
+ gets locally is the same one the test suite and any CI job assert. The logic
7
+ lives in the repo because personal/ is git-ignored: a CI clone cannot see
8
+ this file at all, so it must not be where the rule is defined.
9
+
10
+ Exit codes:
11
+ 0 APPROVED - an `## Approval` section exists and direction is 通过
12
+ 1 BLOCKED - missing, malformed, or not approved. Do not start implementing.
13
+ 2 USAGE - wrong invocation (also: gate module not found)
14
+
15
+ Usage:
16
+ python3 check-approval.py specs/<id>/TECH.md
17
+ python3 check-approval.py --quiet specs/<id>/TECH.md # only the exit code
18
+
19
+ The script never edits the document. It only reports.
20
+ """
21
+
22
+ import json
23
+ import os
24
+ import subprocess
25
+ import sys
26
+ from pathlib import Path
27
+
28
+
29
+ def find_repo_root(start: Path):
30
+ """Locate the checkout that owns lib/specGate.js.
31
+
32
+ Search order: upward from the document, then the script's own location,
33
+ then the current working directory. The middle case matters because a spec
34
+ can legitimately live outside the repository; without it, every such
35
+ invocation would fail looking for /some/where/lib/specGate.js instead of
36
+ reporting a real verdict.
37
+ """
38
+ candidates = [start, *start.parents]
39
+ here = Path(__file__).resolve().parent
40
+ candidates += [here, *here.parents, Path.cwd(), *Path.cwd().parents]
41
+ for candidate in candidates:
42
+ if (candidate / "lib" / "specGate.js").is_file():
43
+ return candidate
44
+ return None
45
+
46
+
47
+ def decide(root: Path, text):
48
+ """Ask the JS module for the verdict. Returns (code, reason).
49
+
50
+ The document content is piped over stdin rather than passed as an argv
51
+ entry: paths with spaces or newlines would otherwise need escaping, and
52
+ argv offsets differ between `node -e` invocation styles.
53
+ """
54
+ module = root / "lib" / "specGate.js"
55
+ helper = (
56
+ "const { decideApproval } = await import(process.env.SPEC_GATE_MODULE);"
57
+ "const fs = await import('node:fs');"
58
+ "const t = fs.readFileSync(0, 'utf8');"
59
+ "const r = decideApproval(t);"
60
+ "process.stdout.write(JSON.stringify({ code: r.code, reason: r.reason }));"
61
+ )
62
+ env = dict(**os.environ, SPEC_GATE_MODULE=str(module))
63
+
64
+ try:
65
+ result = subprocess.run(
66
+ ["node", "--input-type=module", "-e", helper],
67
+ input=text, capture_output=True, text=True, env=env
68
+ )
69
+ except FileNotFoundError:
70
+ raise RuntimeError("找不到 node 可执行文件,无法调用 lib/specGate.js")
71
+
72
+ if result.returncode != 0:
73
+ detail = (result.stderr or "").strip().splitlines()
74
+ tail = detail[-1] if detail else "unknown error"
75
+ raise RuntimeError(f"调用 lib/specGate.js 失败:{tail}")
76
+
77
+ payload = json.loads(result.stdout)
78
+ return payload["code"], payload["reason"]
79
+
80
+
81
+ def main(argv):
82
+ args = [a for a in argv[1:] if a != "--quiet"]
83
+ quiet = "--quiet" in argv[1:]
84
+
85
+ if len(args) != 1:
86
+ print(__doc__.strip(), file=sys.stderr)
87
+ return 2
88
+
89
+ path = Path(args[0])
90
+ if not path.is_file():
91
+ code, reason = 1, "BLOCKED: TECH.md 不存在。方向尚未确认,不得进入实施。"
92
+ else:
93
+ root = find_repo_root(path.resolve().parent)
94
+ if root is None:
95
+ print("USAGE: 找不到 lib/specGate.js,无法判定批准状态。"
96
+ "请在本仓库内运行,或确认 lib/specGate.js 存在。", file=sys.stderr)
97
+ return 2
98
+ try:
99
+ code, reason = decide(root, path.read_text(encoding="utf-8"))
100
+ except RuntimeError as exc:
101
+ print(f"USAGE: {exc}", file=sys.stderr)
102
+ return 2
103
+
104
+ if not quiet or code != 0:
105
+ print(reason, file=sys.stdout if code == 0 else sys.stderr)
106
+ return code
107
+
108
+
109
+ if __name__ == "__main__":
110
+ sys.exit(main(sys.argv))