@punk6529/playbook 0.2.1 → 0.2.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 +52 -20
- package/package.json +1 -1
- package/src/cli.js +58 -2
- package/src/commands/promote.js +96 -0
- package/src/manifest.js +12 -0
- package/src/options.js +41 -0
- package/templates/project/skills/playbook-promote/SKILL.md +32 -0
package/README.md
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
# @punk6529/playbook
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
把团队踩过的坑,沉淀成 AI 可读、可查、可复用的工程记忆。
|
|
4
|
+
|
|
5
|
+
`@punk6529/playbook` 是一个结构化工程知识库 CLI,用于在项目中快速建立并维护 Playbook 资产(cases / patterns / checklists + INDEX)。
|
|
6
|
+
|
|
7
|
+
它不只是“记笔记工具”,而是把“个人经验”变成“团队可复用资产”的最小基础设施:
|
|
8
|
+
|
|
9
|
+
- 解决问题后可立即沉淀为 case,避免同类问题反复排查
|
|
10
|
+
- AI 可以按 tag 精准检索历史经验,而不是每次从零猜测
|
|
11
|
+
- 支持项目级和全局级知识库,单项目沉淀可逐步升级为跨项目最佳实践
|
|
12
|
+
- 通过结构化索引 + 按需读取,控制上下文体积,降低 token 浪费
|
|
13
|
+
|
|
14
|
+
## 这个工具适合谁
|
|
15
|
+
|
|
16
|
+
- 希望减少重复踩坑的个人开发者
|
|
17
|
+
- 需要把故障处置经验标准化的技术团队
|
|
18
|
+
- 在 Codex / Claude 工作流中,希望让 AI“带着历史经验工作”的团队
|
|
19
|
+
|
|
20
|
+
## 你会获得什么
|
|
21
|
+
|
|
22
|
+
- 一套开箱即用的知识库目录结构(项目级 + 全局级)
|
|
23
|
+
- 三个可直接使用的 AI skills:
|
|
24
|
+
- `/playbook-advisor`:会话内主动顾问,自动按需查询知识库
|
|
25
|
+
- `/playbook-query`:手动按标签检索经验
|
|
26
|
+
- `/playbook-case`:引导式记录新 case,并更新索引
|
|
27
|
+
- 一套稳定的 CLI 工作流(init / global init / update / query),便于持续演进而不破坏现有内容
|
|
28
|
+
|
|
29
|
+
## 典型工作流
|
|
30
|
+
|
|
31
|
+
1. `playbook init` 初始化项目知识库与 AI skills (新项目一次即可)
|
|
32
|
+
2. `playbook global init` 初始化全局知识库(一次即可)
|
|
33
|
+
3. 新会话开始先调用 `/playbook-advisor`,AI 会列出现有 tags;后续遇到问题时,会按相关 tags 查询案例 summary,并选取最相关案例查看详情
|
|
34
|
+
4. 问题解决后,用 `/playbook-case` 沉淀经验并更新索引
|
|
35
|
+
5. 在多个项目间通过全局仓库复用高价值模式
|
|
4
36
|
|
|
5
37
|
## 安装
|
|
6
38
|
|
|
@@ -52,25 +84,6 @@ playbook update --force # 强制覆盖(含 INDEX.json)
|
|
|
52
84
|
|
|
53
85
|
仅刷新 skill 模板,不修改用户业务内容(case/pattern/checklist 文件)。`INDEX.json` 默认受保护,需 `--force` 才会覆盖。
|
|
54
86
|
|
|
55
|
-
## 内部命令
|
|
56
|
-
|
|
57
|
-
以下命令主要由 AI skill 内部调用,用户一般不需要直接使用:
|
|
58
|
-
|
|
59
|
-
### `playbook query`
|
|
60
|
-
|
|
61
|
-
按 tag 搜索 INDEX.json 条目(OR 逻辑),同时搜索项目和全局范围。
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
playbook query docker env # 匹配 "docker" 或 "env" 的条目
|
|
65
|
-
playbook query # 所有条目
|
|
66
|
-
playbook query --tags-only # 仅输出 tag 概览:docker(3) env(5) ci(2)
|
|
67
|
-
playbook query --scope project # 仅项目范围
|
|
68
|
-
playbook query --limit 0 # 不限数量
|
|
69
|
-
playbook query docker --json # JSON 格式输出
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
这个命令是 AI skill 的底层工具。`/playbook-advisor` 和 `/playbook-query` skill 在内部调用它来搜索知识库,节省 token 开销。
|
|
73
|
-
|
|
74
87
|
## 在 AI 中使用
|
|
75
88
|
|
|
76
89
|
`playbook init` 之后,AI 工具中有三个 skill 可用:
|
|
@@ -121,6 +134,25 @@ AI:(引导你填写 问题/原因/解决/教训 四个部分)
|
|
|
121
134
|
AI:(生成 case 文件,更新 INDEX.json)
|
|
122
135
|
```
|
|
123
136
|
|
|
137
|
+
## 内部命令
|
|
138
|
+
|
|
139
|
+
以下命令主要由 AI skill 内部调用,用户一般不需要直接使用:
|
|
140
|
+
|
|
141
|
+
### `playbook query`
|
|
142
|
+
|
|
143
|
+
按 tag 搜索 INDEX.json 条目(OR 逻辑),同时搜索项目和全局范围。
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
playbook query docker env # 匹配 "docker" 或 "env" 的条目
|
|
147
|
+
playbook query # 所有条目
|
|
148
|
+
playbook query --tags-only # 仅输出 tag 概览:docker(3) env(5) ci(2)
|
|
149
|
+
playbook query --scope project # 仅项目范围
|
|
150
|
+
playbook query --limit 0 # 不限数量
|
|
151
|
+
playbook query docker --json # JSON 格式输出
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
这个命令是 AI skill 的底层工具。`/playbook-advisor` 和 `/playbook-query` skill 在内部调用它来搜索知识库,节省 token 开销。
|
|
155
|
+
|
|
124
156
|
## 错误处理
|
|
125
157
|
|
|
126
158
|
- 未知命令或无效选项会给出可操作的提示信息
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
const { CliError } = require("./errors");
|
|
2
|
-
const { parseProjectCommandArgs, parseGlobalInitArgs, parseQueryArgs } = require("./options");
|
|
2
|
+
const { parseProjectCommandArgs, parseGlobalInitArgs, parseQueryArgs, parsePromoteArgs } = require("./options");
|
|
3
3
|
const { initProject } = require("./commands/init-project");
|
|
4
4
|
const { initGlobal } = require("./commands/global-init");
|
|
5
5
|
const { updateProject } = require("./commands/update-project");
|
|
6
6
|
const { queryIndex, aggregateTags, formatPlainText, formatJson, formatTagsOnly } = require("./commands/query");
|
|
7
|
+
const { promoteEntries } = require("./commands/promote");
|
|
7
8
|
const { printSummary } = require("./reporting");
|
|
8
9
|
|
|
9
10
|
function usageText() {
|
|
@@ -13,6 +14,8 @@ function usageText() {
|
|
|
13
14
|
" playbook global init [--force]",
|
|
14
15
|
" playbook update [path] [--tools all|none|codex,claude] [--force]",
|
|
15
16
|
" playbook query [tags...] [--limit N] [--scope project|global|both] [--json] [--tags-only]",
|
|
17
|
+
" playbook promote <entry-id...> [--force]",
|
|
18
|
+
" playbook promote --all [--force]",
|
|
16
19
|
"",
|
|
17
20
|
"Notes:",
|
|
18
21
|
" - `init` and `update` are project-scoped commands.",
|
|
@@ -100,6 +103,55 @@ function runQuery(args, io) {
|
|
|
100
103
|
return 0;
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
function promoteHelpText() {
|
|
107
|
+
return [
|
|
108
|
+
"Usage: playbook promote <entry-id...> [options]",
|
|
109
|
+
" playbook promote --all [options]",
|
|
110
|
+
"",
|
|
111
|
+
"Move project entries to global knowledge base.",
|
|
112
|
+
"",
|
|
113
|
+
"Options:",
|
|
114
|
+
" --all Promote all project entries",
|
|
115
|
+
" --force Overwrite if entry already exists in global",
|
|
116
|
+
" -h, --help Show this help",
|
|
117
|
+
].join("\n");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function runPromote(args, io) {
|
|
121
|
+
const options = parsePromoteArgs(args);
|
|
122
|
+
if (options.help) {
|
|
123
|
+
writeStdout(io, promoteHelpText());
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const result = promoteEntries(options);
|
|
128
|
+
|
|
129
|
+
if (result.message) {
|
|
130
|
+
writeStdout(io, result.message);
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let hasError = false;
|
|
135
|
+
for (const r of result.results) {
|
|
136
|
+
if (r.action === "promoted") {
|
|
137
|
+
writeStdout(io, `[promoted] ${r.entryId}\t${r.title}`);
|
|
138
|
+
} else if (r.action === "conflict") {
|
|
139
|
+
writeStderr(io, `[conflict] ${r.entryId}\t${r.reason}`);
|
|
140
|
+
hasError = true;
|
|
141
|
+
} else if (r.action === "error") {
|
|
142
|
+
writeStderr(io, `[error] ${r.entryId}\t${r.reason}`);
|
|
143
|
+
hasError = true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const promoted = result.results.filter((r) => r.action === "promoted").length;
|
|
148
|
+
if (promoted > 0) {
|
|
149
|
+
writeStdout(io, `(${promoted} ${promoted === 1 ? "entry" : "entries"} promoted to global)`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return hasError ? 1 : 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
103
155
|
function runGlobal(args, io) {
|
|
104
156
|
const subcommand = args[0];
|
|
105
157
|
if (!subcommand) {
|
|
@@ -149,11 +201,15 @@ async function runCli(argv, io = process) {
|
|
|
149
201
|
return runQuery(argv.slice(1), io);
|
|
150
202
|
}
|
|
151
203
|
|
|
204
|
+
if (command === "promote") {
|
|
205
|
+
return runPromote(argv.slice(1), io);
|
|
206
|
+
}
|
|
207
|
+
|
|
152
208
|
if (command === "global") {
|
|
153
209
|
return runGlobal(argv.slice(1), io);
|
|
154
210
|
}
|
|
155
211
|
|
|
156
|
-
throw new CliError(`Unknown command: ${command}. Supported: init, global, update, query`);
|
|
212
|
+
throw new CliError(`Unknown command: ${command}. Supported: init, global, update, query, promote`);
|
|
157
213
|
} catch (error) {
|
|
158
214
|
if (error instanceof CliError) {
|
|
159
215
|
writeStderr(io, `Error: ${error.message}`);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const { CliError } = require("../errors");
|
|
5
|
+
|
|
6
|
+
function readIndex(indexPath) {
|
|
7
|
+
try {
|
|
8
|
+
const content = fs.readFileSync(indexPath, "utf8");
|
|
9
|
+
return JSON.parse(content);
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function writeIndex(indexPath, data) {
|
|
16
|
+
fs.writeFileSync(indexPath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function promoteEntry(entryId, projectIndex, globalIndex, projectBase, globalBase, force) {
|
|
20
|
+
const entry = projectIndex[entryId];
|
|
21
|
+
if (!entry) {
|
|
22
|
+
return { action: "error", entryId, reason: `Entry '${entryId}' not found in project INDEX` };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (globalIndex[entryId] && !force) {
|
|
26
|
+
return {
|
|
27
|
+
action: "conflict",
|
|
28
|
+
entryId,
|
|
29
|
+
reason: `Entry '${entryId}' already exists in global INDEX (use --force to overwrite)`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const srcFile = path.join(projectBase, entry.path);
|
|
34
|
+
const destFile = path.join(globalBase, entry.path);
|
|
35
|
+
|
|
36
|
+
// Write to global first (safety: write before delete)
|
|
37
|
+
try {
|
|
38
|
+
fs.mkdirSync(path.dirname(destFile), { recursive: true });
|
|
39
|
+
fs.copyFileSync(srcFile, destFile);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
return { action: "error", entryId, reason: `Failed to copy file to global: ${err.message}` };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
globalIndex[entryId] = entry;
|
|
45
|
+
|
|
46
|
+
// Now safe to remove from project
|
|
47
|
+
delete projectIndex[entryId];
|
|
48
|
+
try {
|
|
49
|
+
fs.unlinkSync(srcFile);
|
|
50
|
+
} catch {
|
|
51
|
+
// File may not exist, that's ok — INDEX entry already removed
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { action: "promoted", entryId, title: entry.title };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function promoteEntries(options) {
|
|
58
|
+
const { entryIds = [], all = false, force = false, targetPath = "." } = options;
|
|
59
|
+
|
|
60
|
+
const projectBase = path.resolve(targetPath, "docs/playbook");
|
|
61
|
+
const globalBase = path.join(os.homedir(), ".playbook/repo");
|
|
62
|
+
|
|
63
|
+
const projectIndexPath = path.join(projectBase, "INDEX.json");
|
|
64
|
+
const globalIndexPath = path.join(globalBase, "INDEX.json");
|
|
65
|
+
|
|
66
|
+
const projectIndex = readIndex(projectIndexPath);
|
|
67
|
+
if (!projectIndex) {
|
|
68
|
+
throw new CliError("Project INDEX.json not found. Run 'playbook init' first.");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const globalIndex = readIndex(globalIndexPath);
|
|
72
|
+
if (!globalIndex) {
|
|
73
|
+
throw new CliError("Global INDEX.json not found. Run 'playbook global init' first.");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const ids = all ? Object.keys(projectIndex) : entryIds;
|
|
77
|
+
|
|
78
|
+
if (all && ids.length === 0) {
|
|
79
|
+
return { results: [], message: "No entries in project INDEX to promote." };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const results = [];
|
|
83
|
+
for (const id of ids) {
|
|
84
|
+
results.push(promoteEntry(id, projectIndex, globalIndex, projectBase, globalBase, force));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Write updated indices
|
|
88
|
+
writeIndex(projectIndexPath, projectIndex);
|
|
89
|
+
writeIndex(globalIndexPath, globalIndex);
|
|
90
|
+
|
|
91
|
+
return { results };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
promoteEntries,
|
|
96
|
+
};
|
package/src/manifest.js
CHANGED
|
@@ -57,6 +57,18 @@ const PROJECT_MANAGED_FILES = [
|
|
|
57
57
|
tool: "claude",
|
|
58
58
|
overwriteOnUpdate: true,
|
|
59
59
|
},
|
|
60
|
+
{
|
|
61
|
+
relativePath: ".codex/skills/playbook-promote/SKILL.md",
|
|
62
|
+
templatePath: "project/skills/playbook-promote/SKILL.md",
|
|
63
|
+
tool: "codex",
|
|
64
|
+
overwriteOnUpdate: true,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
relativePath: ".claude/skills/playbook-promote/SKILL.md",
|
|
68
|
+
templatePath: "project/skills/playbook-promote/SKILL.md",
|
|
69
|
+
tool: "claude",
|
|
70
|
+
overwriteOnUpdate: true,
|
|
71
|
+
},
|
|
60
72
|
];
|
|
61
73
|
|
|
62
74
|
const GLOBAL_SKELETON_DIRS = ["cases", "patterns", "checklists"];
|
package/src/options.js
CHANGED
|
@@ -181,9 +181,50 @@ function parseQueryArgs(args) {
|
|
|
181
181
|
return { help: false, tags, limit, scope, json, tagsOnly };
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
function parsePromoteArgs(args) {
|
|
185
|
+
let force = false;
|
|
186
|
+
let all = false;
|
|
187
|
+
const entryIds = [];
|
|
188
|
+
|
|
189
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
190
|
+
const arg = args[i];
|
|
191
|
+
|
|
192
|
+
if (arg === "-h" || arg === "--help") {
|
|
193
|
+
return { help: true };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (arg === "--force") {
|
|
197
|
+
force = true;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (arg === "--all") {
|
|
202
|
+
all = true;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (arg.startsWith("-")) {
|
|
207
|
+
throw new CliError(`Unknown option for playbook promote: ${arg}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
entryIds.push(arg);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!all && entryIds.length === 0) {
|
|
214
|
+
throw new CliError("playbook promote requires entry IDs or --all");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (all && entryIds.length > 0) {
|
|
218
|
+
throw new CliError("Cannot use --all with specific entry IDs");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { help: false, force, all, entryIds };
|
|
222
|
+
}
|
|
223
|
+
|
|
184
224
|
module.exports = {
|
|
185
225
|
parseProjectCommandArgs,
|
|
186
226
|
parseGlobalInitArgs,
|
|
187
227
|
parseQueryArgs,
|
|
228
|
+
parsePromoteArgs,
|
|
188
229
|
parseToolsValue,
|
|
189
230
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: playbook-promote
|
|
3
|
+
description: Guide when to promote project-level playbook entries to global knowledge base.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Use this skill to suggest promoting valuable project experiences to the global knowledge base for cross-project reuse.
|
|
8
|
+
|
|
9
|
+
## When to Suggest Promote
|
|
10
|
+
|
|
11
|
+
Suggest `playbook promote <entry-id>` to the user when:
|
|
12
|
+
- A project case was successfully used to resolve a problem and the lesson is not project-specific
|
|
13
|
+
- A pattern or checklist has proven useful and would benefit other projects
|
|
14
|
+
- The user explicitly asks about sharing knowledge across projects
|
|
15
|
+
|
|
16
|
+
## When NOT to Suggest
|
|
17
|
+
|
|
18
|
+
Do not suggest promote when:
|
|
19
|
+
- The entry references project-specific configuration, paths, or dependencies
|
|
20
|
+
- The entry is still being refined or hasn't been validated
|
|
21
|
+
- The user is focused on other work (don't interrupt)
|
|
22
|
+
|
|
23
|
+
## How to Promote
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
playbook promote <entry-id> # promote a single entry
|
|
27
|
+
playbook promote <id1> <id2> # promote multiple entries
|
|
28
|
+
playbook promote --all # promote all project entries
|
|
29
|
+
playbook promote <entry-id> --force # overwrite if already exists in global
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The command moves entries from project to global: copies the file, updates both INDEX.json files, and removes the project copy.
|