code-abyss 2.0.0 → 2.0.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.
package/skills/SKILL.md CHANGED
@@ -8,6 +8,108 @@ disable-model-invocation: false
8
8
 
9
9
  # 神通秘典 · 总纲
10
10
 
11
+ ## Skill Authoring Contract
12
+
13
+ 以下规则是 `skills/**/SKILL.md` 的正式 authoring contract;共享 registry、`run_skill.js`、Claude commands、Codex prompts、CI gate 全部以此为准。
14
+
15
+ ### 必填 frontmatter
16
+
17
+ ```yaml
18
+ ---
19
+ name: verify-quality
20
+ description: 代码质量校验关卡。
21
+ user-invocable: true
22
+ allowed-tools: Bash, Read, Glob # 可选,省略时默认 Read
23
+ argument-hint: <扫描路径> # 可选
24
+ ---
25
+ ```
26
+
27
+ 必填字段:
28
+
29
+ - `name`:唯一标识,必须是 kebab-case slug,仅允许小写字母、数字、连字符
30
+ - `description`:会进入 Claude command frontmatter 与 Codex prompt 文本,不能为空
31
+ - `user-invocable`:`true/false`,决定是否进入生成集合
32
+
33
+ 可选字段:
34
+
35
+ - `allowed-tools`:逗号分隔工具名列表;省略时默认 `Read`
36
+ - `argument-hint`:生成命令/提示词时展示参数说明
37
+ - 其他 frontmatter 可保留在 `meta` 中,但不会自动进入生成物
38
+
39
+ ### 分类与运行时推断
40
+
41
+ - `category` 不是手写字段,而是由目录前缀自动推断:
42
+ - `skills/tools/*` → `tool`
43
+ - `skills/domains/*` → `domain`
44
+ - `skills/orchestration/*` → `orchestration`
45
+ - 其他位置 → `root`
46
+ - `runtimeType` 同样自动推断:
47
+ - `scripts/` 下存在且仅存在一个 `.js` 文件 → `scripted`
48
+ - 没有脚本入口 → `knowledge`
49
+
50
+ ### 脚本入口规则
51
+
52
+ - 脚本型 skill 必须把唯一入口放在 `scripts/*.js`
53
+ - `scripts/` 下若出现多个 `.js` 文件,registry 会 fail-fast 报错
54
+ - `runtimeType=scripted` 时,Claude / Codex 产物都会调用各自的 `run_skill.js`
55
+ - `runtimeType=knowledge` 时,产物只读取对应 `SKILL.md`,不会尝试执行脚本
56
+ - `kind` 与 kebab-case compatibility 镜像字段已从 registry 返回面移除;对外只暴露 normalized fields,raw frontmatter 仅保留在 `meta`
57
+
58
+ ### Fail-fast 校验
59
+
60
+ 以下情况会让 `collectSkills()` / `npm run verify:skills` / CI 立即失败:
61
+
62
+ - `SKILL.md` 没有可解析 frontmatter
63
+ - frontmatter 缺少 `name`、`description` 或 `user-invocable`
64
+ - `name` 不是合法 kebab-case slug
65
+ - `allowed-tools` 含非法工具名
66
+ - skill name 重复,导致生成文件名冲突
67
+ - `scripts/` 下出现多个 `.js` 入口
68
+
69
+ ### 生成链
70
+
71
+ 1. registry 扫描并标准化 `skills/**/SKILL.md`
72
+ 2. 仅 `userInvocable=true` 的 skill 进入 invocable 集合
73
+ 3. Claude 生成 `~/.claude/commands/*.md`
74
+ 4. Codex 生成 `~/.codex/prompts/*.md`
75
+ 5. `run_skill.js` 仅负责 `runtimeType=scripted` 的执行编排
76
+
77
+ ### 作者清单
78
+
79
+ 新增或修改 skill 时,至少完成以下检查:
80
+
81
+ - 运行 `npm run verify:skills`
82
+ - 运行 `npm test -- --runInBand test/install-utils.test.js test/install-registry.test.js test/install-generation.test.js test/install-smoke.test.js test/run-skill.test.js`
83
+ - 确认命令名不会与现有 skill 冲突
84
+ - 若新增脚本型 skill,确认 `scripts/` 下仅一个 `.js` 入口
85
+
86
+ ## 能力地图 / 调用规则 / 生成规则
87
+
88
+ ### 能力地图
89
+
90
+ - `domains/`:知识型秘典,负责场景路由、原则、模板与执行纪律
91
+ - `tools/`:可执行校验/生成关卡,既可被 slash command / custom prompt 直接调用,也可在流程中自动触发
92
+ - `orchestration/`:协同规范与多 Agent 编排
93
+ - `run_skill.js`:脚本型 skill 执行器,不负责知识路由
94
+
95
+ ### 调用规则
96
+
97
+ 1. 每个 skill 的权威元数据来自对应 `SKILL.md` frontmatter
98
+ 2. `user-invocable: true` 表示该 skill 进入可调用集合
99
+ 3. 若 skill 目录下存在唯一 `scripts/*.js`,则视为脚本型 skill(`runtimeType=scripted`)
100
+ 4. 若不存在脚本入口,则视为知识型 skill(`runtimeType=knowledge`),只读取 `SKILL.md` 执行
101
+ 5. `run_skill.js` 只负责脚本型 skill:解析 skill、校验 `runtimeType`、加锁、执行脚本、透传退出码
102
+
103
+ ### 自动生成规则
104
+
105
+ 1. 安装器通过共享 registry 递归扫描 `skills/**/SKILL.md`
106
+ 2. Claude 与 Codex 使用同一 invocable skill 集合
107
+ 3. Claude 生成 `~/.claude/commands/*.md`
108
+ 4. Codex 生成 `~/.codex/prompts/*.md`
109
+ 5. `runtimeType=scripted` 时,双端都调用各自的 `~/.claude/skills/run_skill.js` / `~/.codex/skills/run_skill.js`
110
+ 6. `runtimeType=knowledge` 时,双端都退化为“先读 `SKILL.md`,再据秘典执行”
111
+ 7. `npm run verify:skills` 与 CI 会在生成前先校验整个 contract,任何无效 skill 都会阻断后续流程
112
+
11
113
  ## 目录结构
12
114
 
13
115
  ```
@@ -15,12 +15,11 @@
15
15
  */
16
16
 
17
17
  const { spawn } = require('child_process');
18
- const { readdirSync, statSync, writeFileSync, unlinkSync, closeSync, openSync } = require('fs');
18
+ const { unlinkSync, closeSync, openSync } = require('fs');
19
19
  const { join, resolve } = require('path');
20
20
  const { createHash } = require('crypto');
21
21
  const { tmpdir } = require('os');
22
-
23
- const IS_WIN = process.platform === 'win32';
22
+ const { resolveExecutableSkillScript } = require('../bin/lib/skill-registry');
24
23
 
25
24
  function getSkillsDir() {
26
25
  const override = process.env.SAGE_SKILLS_DIR;
@@ -28,39 +27,29 @@ function getSkillsDir() {
28
27
  return __dirname;
29
28
  }
30
29
 
31
- function discoverSkills(skillsDir) {
32
- const found = {};
33
- const toolsDir = join(skillsDir, 'tools');
34
- let entries;
35
- try { entries = readdirSync(toolsDir).sort(); } catch { return found; }
36
-
37
- for (const name of entries) {
38
- const scriptsDir = join(toolsDir, name, 'scripts');
39
- let scripts;
40
- try { scripts = readdirSync(scriptsDir); } catch { continue; }
41
- const jsFile = scripts.find(f => f.endsWith('.js'));
42
- if (jsFile) found[name] = join(scriptsDir, jsFile);
43
- }
44
- return found;
30
+ function sleep(ms) {
31
+ return new Promise(resolveSleep => setTimeout(resolveSleep, ms));
45
32
  }
46
33
 
47
- function getScriptPath(skillName) {
48
- const available = discoverSkills(getSkillsDir());
49
- if (!(skillName in available)) {
50
- const names = Object.keys(available).join(', ') || '(无)';
34
+ function getScriptEntry(skillName) {
35
+ const skillsDir = getSkillsDir();
36
+ const { skill, scriptPath, reason } = resolveExecutableSkillScript(skillsDir, skillName);
37
+
38
+ if (reason === 'missing') {
51
39
  console.error(`错误: 未知的 skill '${skillName}'`);
52
- console.error(`可用的 skills: ${names}`);
53
40
  process.exit(1);
54
41
  }
55
- return available[skillName];
56
- }
57
42
 
58
- function sleepMs(ms) {
59
- const end = Date.now() + ms;
60
- while (Date.now() < end) { /* busy wait */ }
43
+ if (reason === 'no-script') {
44
+ console.error(`错误: skill '${skillName}' runtimeType 不是 scripted`);
45
+ console.error(`请先阅读: ${skill.skillPath}`);
46
+ process.exit(1);
47
+ }
48
+
49
+ return { skill, scriptPath };
61
50
  }
62
51
 
63
- function acquireTargetLock(args) {
52
+ async function acquireTargetLock(args) {
64
53
  const target = args.find(a => !a.startsWith('-')) || process.cwd();
65
54
  const hash = createHash('md5').update(resolve(target)).digest('hex').slice(0, 12);
66
55
  const lockPath = join(tmpdir(), `sage_skill_${hash}.lock`);
@@ -70,12 +59,18 @@ function acquireTargetLock(args) {
70
59
  while (true) {
71
60
  try {
72
61
  const fd = openSync(lockPath, 'wx');
73
- return { fd, lockPath };
62
+ return { fd, lockPath, target };
74
63
  } catch (e) {
75
- if (e.code !== 'EEXIST') return { fd: null, lockPath: null };
76
- if (first) { console.log(`⏳ 等待锁释放: ${target}`); first = false; }
77
- if (Date.now() >= deadline) { console.error(`⏳ 等待锁超时: ${target}`); process.exit(1); }
78
- sleepMs(200);
64
+ if (e.code !== 'EEXIST') return { fd: null, lockPath: null, target };
65
+ if (first) {
66
+ console.log(`⏳ 等待锁释放: ${target}`);
67
+ first = false;
68
+ }
69
+ if (Date.now() >= deadline) {
70
+ console.error(`⏳ 等待锁超时: ${target}`);
71
+ process.exit(1);
72
+ }
73
+ await sleep(200);
79
74
  }
80
75
  }
81
76
  }
@@ -89,7 +84,7 @@ function releaseLock({ fd, lockPath }) {
89
84
  }
90
85
  }
91
86
 
92
- function main() {
87
+ async function main() {
93
88
  const args = process.argv.slice(2);
94
89
 
95
90
  if (args.length === 0 || args[0] === '-h' || args[0] === '--help') {
@@ -98,10 +93,9 @@ function main() {
98
93
  }
99
94
 
100
95
  const skillName = args[0];
101
- const scriptPath = getScriptPath(skillName);
96
+ const { scriptPath } = getScriptEntry(skillName);
102
97
  const scriptArgs = args.slice(1);
103
-
104
- const lock = acquireTargetLock(scriptArgs);
98
+ const lock = await acquireTargetLock(scriptArgs);
105
99
 
106
100
  const child = spawn(process.execPath, [scriptPath, ...scriptArgs], {
107
101
  stdio: 'inherit',
@@ -126,4 +120,7 @@ function main() {
126
120
  });
127
121
  }
128
122
 
129
- main();
123
+ main().catch((err) => {
124
+ console.error(`执行错误: ${err.message}`);
125
+ process.exit(1);
126
+ });