@punk6529/playbook 0.2.4 → 0.2.8
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 +22 -1
- package/package.json +1 -1
- package/src/cli.js +99 -2
- package/src/commands/query.js +28 -0
- package/src/commands/reindex.js +98 -0
- package/src/commands/show.js +57 -0
- package/src/frontmatter.js +66 -0
- package/src/options.js +58 -0
- package/templates/project/skills/playbook-advisor/SKILL.md +5 -4
- package/templates/project/skills/playbook-case/SKILL.md +56 -5
- package/templates/project/skills/playbook-query/SKILL.md +5 -4
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
- `/playbook-advisor`:会话内主动顾问,自动按需查询知识库
|
|
25
25
|
- `/playbook-query`:手动按标签检索经验
|
|
26
26
|
- `/playbook-case`:引导式记录新 case,并更新索引
|
|
27
|
-
- 一套稳定的 CLI 工作流(init / global init / update / query / promote / hit),便于持续演进而不破坏现有内容
|
|
27
|
+
- 一套稳定的 CLI 工作流(init / global init / update / query / promote / show / hit / reindex),便于持续演进而不破坏现有内容
|
|
28
28
|
|
|
29
29
|
## 典型工作流
|
|
30
30
|
|
|
@@ -166,6 +166,16 @@ playbook query docker --json # JSON 格式输出
|
|
|
166
166
|
|
|
167
167
|
这个命令是 AI skill 的底层工具。`/playbook-advisor` 和 `/playbook-query` skill 在内部调用它来搜索知识库,节省 token 开销。
|
|
168
168
|
|
|
169
|
+
### `playbook show`
|
|
170
|
+
|
|
171
|
+
输出指定案例的完整内容。AI skill 用它查看案例详情(L2),无需知道文件路径。
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
playbook show case-001-docker-bind # 输出案例文件内容
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
先查项目 INDEX,再查全局 INDEX,找到后输出对应文件内容到 stdout。
|
|
178
|
+
|
|
169
179
|
### `playbook hit`
|
|
170
180
|
|
|
171
181
|
记录一次案例阅读命中,用于追踪案例的实际使用频率。
|
|
@@ -176,6 +186,17 @@ playbook hit case-001-docker-bind # 记录一次命中
|
|
|
176
186
|
|
|
177
187
|
AI advisor 每次阅读案例详情后自动调用。命中次数在 `playbook query` 输出中以 `hits:N` 显示,帮助识别高价值案例。
|
|
178
188
|
|
|
189
|
+
### `playbook reindex`
|
|
190
|
+
|
|
191
|
+
从文件 front matter 重建 INDEX.json。当 INDEX 被清空、损坏或需要同步时使用。
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
playbook reindex # 重建项目 INDEX
|
|
195
|
+
playbook reindex --scope global # 重建全局 INDEX
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
扫描 `cases/`、`patterns/`、`checklists/` 下的 `.md` 文件,解析 YAML front matter(title, tags, created),重建 INDEX.json。保留已有的 hits 统计。
|
|
199
|
+
|
|
179
200
|
## 错误处理
|
|
180
201
|
|
|
181
202
|
- 未知命令或无效选项会给出可操作的提示信息
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
1
4
|
const { CliError } = require("./errors");
|
|
2
|
-
const { parseProjectCommandArgs, parseGlobalInitArgs, parseQueryArgs, parsePromoteArgs, parseHitArgs } = require("./options");
|
|
5
|
+
const { parseProjectCommandArgs, parseGlobalInitArgs, parseQueryArgs, parsePromoteArgs, parseHitArgs, parseShowArgs, parseReindexArgs } = require("./options");
|
|
3
6
|
const { initProject } = require("./commands/init-project");
|
|
4
7
|
const { initGlobal } = require("./commands/global-init");
|
|
5
8
|
const { updateProject } = require("./commands/update-project");
|
|
6
9
|
const { queryIndex, aggregateTags, formatPlainText, formatJson, formatTagsOnly } = require("./commands/query");
|
|
7
10
|
const { promoteEntries } = require("./commands/promote");
|
|
8
11
|
const { hitEntry } = require("./commands/hit");
|
|
12
|
+
const { showEntry } = require("./commands/show");
|
|
13
|
+
const { reindexPlaybook } = require("./commands/reindex");
|
|
9
14
|
const { printSummary } = require("./reporting");
|
|
10
15
|
|
|
11
16
|
function usageText() {
|
|
@@ -18,6 +23,9 @@ function usageText() {
|
|
|
18
23
|
" playbook promote <entry-id...> [--force]",
|
|
19
24
|
" playbook promote --all [--force]",
|
|
20
25
|
" playbook hit <entry-id>",
|
|
26
|
+
" playbook show <entry-id>",
|
|
27
|
+
" playbook reindex [--scope project|global]",
|
|
28
|
+
" playbook root [--scope project|global]",
|
|
21
29
|
"",
|
|
22
30
|
"Notes:",
|
|
23
31
|
" - `init` and `update` are project-scoped commands.",
|
|
@@ -178,6 +186,83 @@ function runHit(args, io) {
|
|
|
178
186
|
return 0;
|
|
179
187
|
}
|
|
180
188
|
|
|
189
|
+
function showHelpText() {
|
|
190
|
+
return [
|
|
191
|
+
"Usage: playbook show <entry-id>",
|
|
192
|
+
"",
|
|
193
|
+
"Display the full content of a playbook entry.",
|
|
194
|
+
"Searches project INDEX first, then global INDEX.",
|
|
195
|
+
"",
|
|
196
|
+
"Options:",
|
|
197
|
+
" -h, --help Show this help",
|
|
198
|
+
].join("\n");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function runShow(args, io) {
|
|
202
|
+
const options = parseShowArgs(args);
|
|
203
|
+
if (options.help) {
|
|
204
|
+
writeStdout(io, showHelpText());
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const result = showEntry(options);
|
|
209
|
+
writeStdout(io, result.content);
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function reindexHelpText() {
|
|
214
|
+
return [
|
|
215
|
+
"Usage: playbook reindex [options]",
|
|
216
|
+
"",
|
|
217
|
+
"Rebuild INDEX.json by scanning playbook files and reading front matter.",
|
|
218
|
+
"",
|
|
219
|
+
"Options:",
|
|
220
|
+
" --scope X Scope: project or global (default: project)",
|
|
221
|
+
" -h, --help Show this help",
|
|
222
|
+
].join("\n");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function runReindex(args, io) {
|
|
226
|
+
const options = parseReindexArgs(args);
|
|
227
|
+
if (options.help) {
|
|
228
|
+
writeStdout(io, reindexHelpText());
|
|
229
|
+
return 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const result = reindexPlaybook(options);
|
|
233
|
+
writeStdout(io, `Reindexed ${result.scope}: ${result.count} ${result.count === 1 ? "entry" : "entries"} found.`);
|
|
234
|
+
return 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function runRoot(args, io) {
|
|
238
|
+
let scope = "project";
|
|
239
|
+
for (const arg of args) {
|
|
240
|
+
if (arg === "-h" || arg === "--help") {
|
|
241
|
+
writeStdout(io, "Usage: playbook root [--scope project|global]\n\nOutput the absolute path of the playbook directory.");
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
if (arg === "--scope" || arg.startsWith("--scope=")) {
|
|
245
|
+
const value = arg === "--scope" ? args[args.indexOf(arg) + 1] : arg.slice("--scope=".length);
|
|
246
|
+
if (value === "global" || value === "project") scope = value;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (scope === "global") {
|
|
251
|
+
const globalRoot = path.join(os.homedir(), ".playbook/repo");
|
|
252
|
+
if (!fs.existsSync(globalRoot)) {
|
|
253
|
+
throw new CliError("Global playbook not found. Run 'playbook global init' first.");
|
|
254
|
+
}
|
|
255
|
+
writeStdout(io, globalRoot);
|
|
256
|
+
} else {
|
|
257
|
+
const projectRoot = path.resolve("docs/playbook");
|
|
258
|
+
if (!fs.existsSync(projectRoot)) {
|
|
259
|
+
throw new CliError("Project playbook not found. Run 'playbook init' first.");
|
|
260
|
+
}
|
|
261
|
+
writeStdout(io, projectRoot);
|
|
262
|
+
}
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
181
266
|
function runGlobal(args, io) {
|
|
182
267
|
const subcommand = args[0];
|
|
183
268
|
if (!subcommand) {
|
|
@@ -235,11 +320,23 @@ async function runCli(argv, io = process) {
|
|
|
235
320
|
return runHit(argv.slice(1), io);
|
|
236
321
|
}
|
|
237
322
|
|
|
323
|
+
if (command === "show") {
|
|
324
|
+
return runShow(argv.slice(1), io);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (command === "reindex") {
|
|
328
|
+
return runReindex(argv.slice(1), io);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (command === "root") {
|
|
332
|
+
return runRoot(argv.slice(1), io);
|
|
333
|
+
}
|
|
334
|
+
|
|
238
335
|
if (command === "global") {
|
|
239
336
|
return runGlobal(argv.slice(1), io);
|
|
240
337
|
}
|
|
241
338
|
|
|
242
|
-
throw new CliError(`Unknown command: ${command}. Supported: init, global, update, query, promote, hit`);
|
|
339
|
+
throw new CliError(`Unknown command: ${command}. Supported: init, global, update, query, promote, hit, show, reindex, root`);
|
|
243
340
|
} catch (error) {
|
|
244
341
|
if (error instanceof CliError) {
|
|
245
342
|
writeStderr(io, `Error: ${error.message}`);
|
package/src/commands/query.js
CHANGED
|
@@ -92,11 +92,28 @@ function formatPlainText(result) {
|
|
|
92
92
|
return lines.join("\n");
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
function readTagsRegistry(tagsYmlPath) {
|
|
96
|
+
try {
|
|
97
|
+
const content = fs.readFileSync(tagsYmlPath, "utf8");
|
|
98
|
+
const ids = [];
|
|
99
|
+
for (const line of content.split("\n")) {
|
|
100
|
+
const match = line.match(/^\s*-\s*id:\s*(.+)/);
|
|
101
|
+
if (match) {
|
|
102
|
+
ids.push(match[1].trim());
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return ids;
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
95
111
|
function aggregateTags(options) {
|
|
96
112
|
const { scope = "both", targetPath = "." } = options;
|
|
97
113
|
|
|
98
114
|
const projectIndexPath = path.resolve(targetPath, "docs/playbook/INDEX.json");
|
|
99
115
|
const globalIndexPath = path.join(os.homedir(), ".playbook/repo/INDEX.json");
|
|
116
|
+
const tagsYmlPath = path.join(os.homedir(), ".playbook/repo/tags.yml");
|
|
100
117
|
|
|
101
118
|
const tagCounts = {};
|
|
102
119
|
let anyIndexFound = false;
|
|
@@ -125,6 +142,17 @@ function aggregateTags(options) {
|
|
|
125
142
|
}
|
|
126
143
|
}
|
|
127
144
|
|
|
145
|
+
// Merge tags.yml registry — ensures registered tags appear even with count 0
|
|
146
|
+
const registryTags = readTagsRegistry(tagsYmlPath);
|
|
147
|
+
if (registryTags) {
|
|
148
|
+
anyIndexFound = true;
|
|
149
|
+
for (const tag of registryTags) {
|
|
150
|
+
if (!(tag in tagCounts)) {
|
|
151
|
+
tagCounts[tag] = 0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
128
156
|
if (!anyIndexFound) {
|
|
129
157
|
return { tags: {}, found: false };
|
|
130
158
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const { CliError } = require("../errors");
|
|
5
|
+
const { parseFrontMatter } = require("../frontmatter");
|
|
6
|
+
|
|
7
|
+
const TYPE_DIRS = {
|
|
8
|
+
cases: "case",
|
|
9
|
+
patterns: "pattern",
|
|
10
|
+
checklists: "checklist",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function readIndex(indexPath) {
|
|
14
|
+
try {
|
|
15
|
+
const content = fs.readFileSync(indexPath, "utf8");
|
|
16
|
+
return JSON.parse(content);
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function scanDir(baseDir) {
|
|
23
|
+
const entries = {};
|
|
24
|
+
|
|
25
|
+
for (const [dirName, type] of Object.entries(TYPE_DIRS)) {
|
|
26
|
+
const dirPath = path.join(baseDir, dirName);
|
|
27
|
+
if (!fs.existsSync(dirPath)) continue;
|
|
28
|
+
|
|
29
|
+
const files = fs.readdirSync(dirPath).filter((f) => f.endsWith(".md"));
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
const filePath = path.join(dirPath, file);
|
|
32
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
33
|
+
const fm = parseFrontMatter(content);
|
|
34
|
+
|
|
35
|
+
if (!fm.title) continue; // skip files without valid front matter
|
|
36
|
+
|
|
37
|
+
const id = file.replace(/\.md$/, "");
|
|
38
|
+
const relativePath = `${dirName}/${file}`;
|
|
39
|
+
|
|
40
|
+
entries[id] = {
|
|
41
|
+
type,
|
|
42
|
+
title: fm.title,
|
|
43
|
+
tags: fm.tags || [],
|
|
44
|
+
path: relativePath,
|
|
45
|
+
created: fm.created || new Date().toISOString().slice(0, 10),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return entries;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function reindexPlaybook(options) {
|
|
54
|
+
const { scope = "project", targetPath = "." } = options;
|
|
55
|
+
|
|
56
|
+
let baseDir;
|
|
57
|
+
let indexPath;
|
|
58
|
+
|
|
59
|
+
if (scope === "global") {
|
|
60
|
+
baseDir = path.join(os.homedir(), ".playbook/repo");
|
|
61
|
+
indexPath = path.join(baseDir, "INDEX.json");
|
|
62
|
+
} else {
|
|
63
|
+
baseDir = path.resolve(targetPath, "docs/playbook");
|
|
64
|
+
indexPath = path.join(baseDir, "INDEX.json");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!fs.existsSync(baseDir)) {
|
|
68
|
+
throw new CliError(
|
|
69
|
+
scope === "global"
|
|
70
|
+
? "Global playbook not found. Run 'playbook global init' first."
|
|
71
|
+
: "Project playbook not found. Run 'playbook init' first."
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Read existing index to preserve hits
|
|
76
|
+
const oldIndex = readIndex(indexPath) || {};
|
|
77
|
+
|
|
78
|
+
// Scan files and build new index
|
|
79
|
+
const newEntries = scanDir(baseDir);
|
|
80
|
+
|
|
81
|
+
// Preserve hits from old index
|
|
82
|
+
for (const [id, entry] of Object.entries(newEntries)) {
|
|
83
|
+
if (oldIndex[id] && oldIndex[id].hits) {
|
|
84
|
+
entry.hits = oldIndex[id].hits;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Write new index
|
|
89
|
+
fs.writeFileSync(indexPath, JSON.stringify(newEntries, null, 2) + "\n", "utf8");
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
scope,
|
|
93
|
+
count: Object.keys(newEntries).length,
|
|
94
|
+
entries: newEntries,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { reindexPlaybook, scanDir };
|
|
@@ -0,0 +1,57 @@
|
|
|
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 showEntry(options) {
|
|
16
|
+
const { entryId, targetPath = "." } = options;
|
|
17
|
+
|
|
18
|
+
const projectIndexPath = path.resolve(targetPath, "docs/playbook/INDEX.json");
|
|
19
|
+
const globalIndexPath = path.join(os.homedir(), ".playbook/repo/INDEX.json");
|
|
20
|
+
|
|
21
|
+
// Try project first
|
|
22
|
+
const projectIndex = readIndex(projectIndexPath);
|
|
23
|
+
if (projectIndex && projectIndex[entryId]) {
|
|
24
|
+
const entryPath = projectIndex[entryId].path;
|
|
25
|
+
const fullPath = path.resolve(targetPath, "docs/playbook", entryPath);
|
|
26
|
+
const content = readFile(fullPath, entryId);
|
|
27
|
+
return { entryId, scope: "project", content };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Fall back to global
|
|
31
|
+
const globalIndex = readIndex(globalIndexPath);
|
|
32
|
+
if (globalIndex && globalIndex[entryId]) {
|
|
33
|
+
const entryPath = globalIndex[entryId].path;
|
|
34
|
+
const fullPath = path.join(os.homedir(), ".playbook/repo", entryPath);
|
|
35
|
+
const content = readFile(fullPath, entryId);
|
|
36
|
+
return { entryId, scope: "global", content };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Not found
|
|
40
|
+
if (!projectIndex && !globalIndex) {
|
|
41
|
+
throw new CliError("No INDEX.json found. Run 'playbook init' or 'playbook global init' first.");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
throw new CliError(`Entry '${entryId}' not found in project or global INDEX.`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readFile(fullPath, entryId) {
|
|
48
|
+
try {
|
|
49
|
+
return fs.readFileSync(fullPath, "utf8");
|
|
50
|
+
} catch {
|
|
51
|
+
throw new CliError(`File not found for entry '${entryId}': ${fullPath}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
showEntry,
|
|
57
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse YAML front matter from markdown content.
|
|
3
|
+
* Expects format:
|
|
4
|
+
* ---
|
|
5
|
+
* title: Some title
|
|
6
|
+
* tags: [tag1, tag2]
|
|
7
|
+
* created: 2026-02-01
|
|
8
|
+
* ---
|
|
9
|
+
* body content here
|
|
10
|
+
*/
|
|
11
|
+
function parseFrontMatter(content) {
|
|
12
|
+
const trimmed = content.trimStart();
|
|
13
|
+
if (!trimmed.startsWith("---")) {
|
|
14
|
+
return { title: null, tags: null, created: null, body: content };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const endIndex = trimmed.indexOf("\n---", 3);
|
|
18
|
+
if (endIndex === -1) {
|
|
19
|
+
return { title: null, tags: null, created: null, body: content };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const fmBlock = trimmed.slice(4, endIndex);
|
|
23
|
+
const body = trimmed.slice(endIndex + 4).replace(/^\n/, "");
|
|
24
|
+
|
|
25
|
+
let title = null;
|
|
26
|
+
let tags = null;
|
|
27
|
+
let created = null;
|
|
28
|
+
|
|
29
|
+
for (const line of fmBlock.split("\n")) {
|
|
30
|
+
const stripped = line.trim();
|
|
31
|
+
if (!stripped || stripped.startsWith("#")) continue;
|
|
32
|
+
|
|
33
|
+
const colonIndex = stripped.indexOf(":");
|
|
34
|
+
if (colonIndex === -1) continue;
|
|
35
|
+
|
|
36
|
+
const key = stripped.slice(0, colonIndex).trim();
|
|
37
|
+
const value = stripped.slice(colonIndex + 1).trim();
|
|
38
|
+
|
|
39
|
+
if (key === "title") {
|
|
40
|
+
title = value;
|
|
41
|
+
} else if (key === "tags") {
|
|
42
|
+
tags = parseTagsValue(value);
|
|
43
|
+
} else if (key === "created") {
|
|
44
|
+
created = value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { title, tags, created, body };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseTagsValue(value) {
|
|
52
|
+
// Handle [tag1, tag2] format
|
|
53
|
+
const match = value.match(/^\[(.*)?\]$/);
|
|
54
|
+
if (match) {
|
|
55
|
+
if (!match[1] || !match[1].trim()) return [];
|
|
56
|
+
return match[1]
|
|
57
|
+
.split(",")
|
|
58
|
+
.map((t) => t.trim())
|
|
59
|
+
.filter(Boolean);
|
|
60
|
+
}
|
|
61
|
+
// Handle bare value as single tag
|
|
62
|
+
if (value) return [value];
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { parseFrontMatter };
|
package/src/options.js
CHANGED
|
@@ -240,11 +240,69 @@ function parseHitArgs(args) {
|
|
|
240
240
|
return { help: false, entryId };
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
function parseShowArgs(args) {
|
|
244
|
+
for (const arg of args) {
|
|
245
|
+
if (arg === "-h" || arg === "--help") {
|
|
246
|
+
return { help: true };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const entryId = args[0];
|
|
251
|
+
if (!entryId || entryId.startsWith("-")) {
|
|
252
|
+
throw new CliError("playbook show requires an entry ID");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (args.length > 1) {
|
|
256
|
+
throw new CliError("playbook show accepts exactly one entry ID");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return { help: false, entryId };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function parseReindexArgs(args) {
|
|
263
|
+
let scope = "project";
|
|
264
|
+
|
|
265
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
266
|
+
const arg = args[i];
|
|
267
|
+
|
|
268
|
+
if (arg === "-h" || arg === "--help") {
|
|
269
|
+
return { help: true };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (arg === "--scope") {
|
|
273
|
+
const next = args[i + 1];
|
|
274
|
+
if (!next || !["project", "global"].includes(next)) {
|
|
275
|
+
throw new CliError("--scope must be one of: project, global");
|
|
276
|
+
}
|
|
277
|
+
scope = next;
|
|
278
|
+
i += 1;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (arg.startsWith("--scope=")) {
|
|
283
|
+
const value = arg.slice("--scope=".length);
|
|
284
|
+
if (!["project", "global"].includes(value)) {
|
|
285
|
+
throw new CliError("--scope must be one of: project, global");
|
|
286
|
+
}
|
|
287
|
+
scope = value;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (arg.startsWith("-")) {
|
|
292
|
+
throw new CliError(`Unknown option for playbook reindex: ${arg}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return { help: false, scope };
|
|
297
|
+
}
|
|
298
|
+
|
|
243
299
|
module.exports = {
|
|
244
300
|
parseProjectCommandArgs,
|
|
245
301
|
parseGlobalInitArgs,
|
|
246
302
|
parseQueryArgs,
|
|
247
303
|
parsePromoteArgs,
|
|
248
304
|
parseHitArgs,
|
|
305
|
+
parseShowArgs,
|
|
306
|
+
parseReindexArgs,
|
|
249
307
|
parseToolsValue,
|
|
250
308
|
};
|
|
@@ -43,11 +43,12 @@ Review the titles in the output. Only proceed to Step 2 for entries whose titles
|
|
|
43
43
|
|
|
44
44
|
### Step 2 — Read specific cases (L2)
|
|
45
45
|
|
|
46
|
-
For relevant entries only:
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
For relevant entries only, run:
|
|
47
|
+
```bash
|
|
48
|
+
playbook show <entry-id>
|
|
49
|
+
```
|
|
49
50
|
|
|
50
|
-
Do NOT read all matched entries. Only
|
|
51
|
+
This outputs the full content of the case file. Do NOT read all matched entries. Only show the ones that look relevant based on title.
|
|
51
52
|
|
|
52
53
|
When multiple entries have similar titles, prefer entries with higher `hits` count — they have been used more often and are likely more effective.
|
|
53
54
|
|
|
@@ -13,10 +13,11 @@ Use this skill when the user asks to create, review, or inspect case entries.
|
|
|
13
13
|
- For new cases, enforce this guided flow:
|
|
14
14
|
1. If the user has not provided a concrete problem statement, ask first:
|
|
15
15
|
"你想把之前遇到的哪个问题沉淀为 case?请一句话描述。"
|
|
16
|
-
2. After problem statement is available,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
2. After problem statement is available, get available tags by running:
|
|
17
|
+
```bash
|
|
18
|
+
playbook query --tags-only
|
|
19
|
+
```
|
|
20
|
+
Suggest relevant existing tags from the output. Ask user to choose existing tags or explicitly confirm a new tag.
|
|
20
21
|
3. If scope is not confirmed, ask for scope confirmation:
|
|
21
22
|
- default is `project`
|
|
22
23
|
- use `global` only when user explicitly confirms
|
|
@@ -65,9 +66,51 @@ Format: `{type}-{NNN}-{slug}`
|
|
|
65
66
|
- `NNN`: zero-padded 3-digit sequence number (check existing entries to determine next number)
|
|
66
67
|
- `slug`: lowercase hyphenated descriptor derived from the title
|
|
67
68
|
|
|
69
|
+
### Case File Format
|
|
70
|
+
|
|
71
|
+
Case files MUST include YAML front matter with title, tags, and created:
|
|
72
|
+
|
|
73
|
+
```markdown
|
|
74
|
+
---
|
|
75
|
+
title: OAuth token refresh race condition under concurrency
|
|
76
|
+
tags: [auth, api]
|
|
77
|
+
created: 2026-02-10
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Problem
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
## Context
|
|
84
|
+
...
|
|
85
|
+
|
|
86
|
+
## Solution
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
## Takeaway
|
|
90
|
+
...
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The front matter is the source of truth for metadata. INDEX.json is rebuilt from front matter via `playbook reindex`.
|
|
94
|
+
|
|
95
|
+
### File Paths
|
|
96
|
+
|
|
97
|
+
Write files to the correct location based on scope:
|
|
98
|
+
|
|
99
|
+
- **Project scope:** case file → `<project-root>/docs/playbook/<path>`, INDEX → `<project-root>/docs/playbook/INDEX.json`
|
|
100
|
+
- **Global scope:** case file → `~/.playbook/repo/<path>`, INDEX → `~/.playbook/repo/INDEX.json`
|
|
101
|
+
|
|
102
|
+
**IMPORTANT:** Always use absolute paths when writing files. Do NOT use relative paths — the current working directory may not be the project root.
|
|
103
|
+
|
|
104
|
+
Get the playbook directory first:
|
|
105
|
+
```bash
|
|
106
|
+
playbook root # project scope
|
|
107
|
+
playbook root --scope global # global scope
|
|
108
|
+
```
|
|
109
|
+
Then use the output to construct absolute paths, e.g. `<root>/cases/case-001-xxx.md` and `<root>/INDEX.json`.
|
|
110
|
+
|
|
68
111
|
### Draft Package Output
|
|
69
112
|
|
|
70
|
-
When producing the draft package, output
|
|
113
|
+
When producing the draft package, output:
|
|
71
114
|
|
|
72
115
|
```
|
|
73
116
|
**Entry ID:** case-001-oauth-token-race
|
|
@@ -84,3 +127,11 @@ Add this entry to INDEX.json:
|
|
|
84
127
|
}
|
|
85
128
|
}
|
|
86
129
|
```
|
|
130
|
+
|
|
131
|
+
### Write Procedure
|
|
132
|
+
|
|
133
|
+
After user confirms the draft:
|
|
134
|
+
|
|
135
|
+
1. Write the case file to the scope-specific path
|
|
136
|
+
2. Read the current INDEX.json, merge the new entry, write back
|
|
137
|
+
3. Run `playbook hit <entry-id>` is NOT needed — hit is only for reads
|
|
@@ -30,14 +30,15 @@ Output format (one line per match):
|
|
|
30
30
|
(N matches: X project, Y global)
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
### Expansion (L2):
|
|
33
|
+
### Expansion (L2): Show entry content
|
|
34
34
|
|
|
35
35
|
When the user asks to see details of a specific entry, or when you need full context for a task:
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
```bash
|
|
38
|
+
playbook show <entry-id>
|
|
39
|
+
```
|
|
39
40
|
|
|
40
|
-
Only expand entries that are actually needed. Do not
|
|
41
|
+
This outputs the full content of the entry file. Only expand entries that are actually needed. Do not show all matched entries at once.
|
|
41
42
|
|
|
42
43
|
### Edge Cases
|
|
43
44
|
|