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/README.md +101 -11
- package/bin/adapters/claude.js +6 -1
- package/bin/adapters/codex.js +0 -1
- package/bin/install.js +153 -69
- package/bin/lib/skill-registry.js +190 -0
- package/bin/lib/style-registry.js +119 -0
- package/bin/lib/utils.js +9 -3
- package/bin/verify-skills-contract.js +27 -0
- package/config/AGENTS.md +1 -1
- package/config/CLAUDE.md +1 -1
- package/output-styles/abyss-command.md +56 -0
- package/output-styles/abyss-concise.md +89 -0
- package/output-styles/abyss-ritual.md +70 -0
- package/output-styles/index.json +36 -0
- package/package.json +3 -2
- package/skills/SKILL.md +102 -0
- package/skills/run_skill.js +35 -38
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
|
```
|
package/skills/run_skill.js
CHANGED
|
@@ -15,12 +15,11 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
const { spawn } = require('child_process');
|
|
18
|
-
const {
|
|
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
|
|
32
|
-
|
|
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
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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) {
|
|
77
|
-
|
|
78
|
-
|
|
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 =
|
|
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
|
+
});
|