@punk6529/playbook 0.2.5 → 0.2.9
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/CHANGELOG.md +56 -0
- package/README.md +12 -1
- package/package.json +3 -2
- package/src/cli.js +69 -2
- package/src/commands/query.js +28 -0
- package/src/commands/reindex.js +98 -0
- package/src/frontmatter.js +66 -0
- package/src/options.js +38 -0
- package/templates/project/skills/playbook-case/SKILL.md +56 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.8
|
|
4
|
+
|
|
5
|
+
- 新增 `playbook root [--scope project|global]` 命令,输出 playbook 目录绝对路径
|
|
6
|
+
- playbook-case skill 写入文件前通过 `playbook root` 获取路径,避免相对路径问题
|
|
7
|
+
- playbook-case skill 新增 Write Procedure 段,明确写入步骤
|
|
8
|
+
|
|
9
|
+
## 0.2.7
|
|
10
|
+
|
|
11
|
+
- `playbook query --tags-only` 合并 tags.yml 注册表,即使 INDEX 为空也能显示完整 tag 列表
|
|
12
|
+
- playbook-case skill 改用 `playbook query --tags-only` 获取 tag 建议
|
|
13
|
+
|
|
14
|
+
## 0.2.6
|
|
15
|
+
|
|
16
|
+
- 内容文件新增 YAML front matter(title, tags, created),作为元数据主真相源
|
|
17
|
+
- 新增 `playbook reindex [--scope project|global]` 命令,从文件 front matter 重建 INDEX.json
|
|
18
|
+
- reindex 保留已有 hits 统计,跳过无效 front matter 的文件
|
|
19
|
+
- playbook-case skill 模板更新:生成的 case 文件包含 front matter
|
|
20
|
+
|
|
21
|
+
## 0.2.5
|
|
22
|
+
|
|
23
|
+
- 新增 `playbook show <entry-id>` 命令,输出案例文件完整内容
|
|
24
|
+
- playbook-advisor 和 playbook-query skill 的 L2 改为调用 `playbook show`,不再要求 AI 猜测文件路径
|
|
25
|
+
|
|
26
|
+
## 0.2.4
|
|
27
|
+
|
|
28
|
+
- 移除 init 时创建的 example 文件,INDEX.json 初始为空对象
|
|
29
|
+
|
|
30
|
+
## 0.2.3
|
|
31
|
+
|
|
32
|
+
- 新增 `playbook hit <entry-id>` 命令,记录案例阅读次数
|
|
33
|
+
- `playbook query` 输出增加 `hits:N` 字段
|
|
34
|
+
- playbook-advisor skill 阅读案例后自动调用 hit,并优先推荐高 hits 案例
|
|
35
|
+
|
|
36
|
+
## 0.2.2
|
|
37
|
+
|
|
38
|
+
- 新增 `playbook promote` 命令,将项目案例移动到全局知识库
|
|
39
|
+
- 支持单个、多个、`--all` 批量提升,`--force` 覆盖冲突
|
|
40
|
+
- 新增 playbook-promote skill 模板
|
|
41
|
+
|
|
42
|
+
## 0.2.1
|
|
43
|
+
|
|
44
|
+
- README,补充命令使用场景说明
|
|
45
|
+
- 区分用户命令与内部命令
|
|
46
|
+
|
|
47
|
+
## 0.2.0
|
|
48
|
+
|
|
49
|
+
- 新增 `playbook query` 命令(按 tag 检索、`--json`、`--tags-only`)
|
|
50
|
+
- 新增 playbook-advisor skill(主动顾问模式)
|
|
51
|
+
- 新增 playbook-query、playbook-case skill 模板
|
|
52
|
+
|
|
53
|
+
## 0.1.0
|
|
54
|
+
|
|
55
|
+
- 初始版本
|
|
56
|
+
- `playbook init`、`playbook global init`、`playbook update`
|
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
|
|
|
@@ -186,6 +186,17 @@ playbook hit case-001-docker-bind # 记录一次命中
|
|
|
186
186
|
|
|
187
187
|
AI advisor 每次阅读案例详情后自动调用。命中次数在 `playbook query` 输出中以 `hits:N` 显示,帮助识别高价值案例。
|
|
188
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
|
+
|
|
189
200
|
## 错误处理
|
|
190
201
|
|
|
191
202
|
- 未知命令或无效选项会给出可操作的提示信息
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@punk6529/playbook",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "CLI for initializing and updating Playbook & Case workflows.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"files": [
|
|
22
22
|
"bin",
|
|
23
23
|
"src",
|
|
24
|
-
"templates"
|
|
24
|
+
"templates",
|
|
25
|
+
"CHANGELOG.md"
|
|
25
26
|
]
|
|
26
27
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
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, parseShowArgs } = 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");
|
|
@@ -7,6 +10,7 @@ const { queryIndex, aggregateTags, formatPlainText, formatJson, formatTagsOnly }
|
|
|
7
10
|
const { promoteEntries } = require("./commands/promote");
|
|
8
11
|
const { hitEntry } = require("./commands/hit");
|
|
9
12
|
const { showEntry } = require("./commands/show");
|
|
13
|
+
const { reindexPlaybook } = require("./commands/reindex");
|
|
10
14
|
const { printSummary } = require("./reporting");
|
|
11
15
|
|
|
12
16
|
function usageText() {
|
|
@@ -20,6 +24,8 @@ function usageText() {
|
|
|
20
24
|
" playbook promote --all [--force]",
|
|
21
25
|
" playbook hit <entry-id>",
|
|
22
26
|
" playbook show <entry-id>",
|
|
27
|
+
" playbook reindex [--scope project|global]",
|
|
28
|
+
" playbook root [--scope project|global]",
|
|
23
29
|
"",
|
|
24
30
|
"Notes:",
|
|
25
31
|
" - `init` and `update` are project-scoped commands.",
|
|
@@ -204,6 +210,59 @@ function runShow(args, io) {
|
|
|
204
210
|
return 0;
|
|
205
211
|
}
|
|
206
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
|
+
|
|
207
266
|
function runGlobal(args, io) {
|
|
208
267
|
const subcommand = args[0];
|
|
209
268
|
if (!subcommand) {
|
|
@@ -265,11 +324,19 @@ async function runCli(argv, io = process) {
|
|
|
265
324
|
return runShow(argv.slice(1), io);
|
|
266
325
|
}
|
|
267
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
|
+
|
|
268
335
|
if (command === "global") {
|
|
269
336
|
return runGlobal(argv.slice(1), io);
|
|
270
337
|
}
|
|
271
338
|
|
|
272
|
-
throw new CliError(`Unknown command: ${command}. Supported: init, global, update, query, promote, hit, show`);
|
|
339
|
+
throw new CliError(`Unknown command: ${command}. Supported: init, global, update, query, promote, hit, show, reindex, root`);
|
|
273
340
|
} catch (error) {
|
|
274
341
|
if (error instanceof CliError) {
|
|
275
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,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
|
@@ -259,6 +259,43 @@ function parseShowArgs(args) {
|
|
|
259
259
|
return { help: false, entryId };
|
|
260
260
|
}
|
|
261
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
|
+
|
|
262
299
|
module.exports = {
|
|
263
300
|
parseProjectCommandArgs,
|
|
264
301
|
parseGlobalInitArgs,
|
|
@@ -266,5 +303,6 @@ module.exports = {
|
|
|
266
303
|
parsePromoteArgs,
|
|
267
304
|
parseHitArgs,
|
|
268
305
|
parseShowArgs,
|
|
306
|
+
parseReindexArgs,
|
|
269
307
|
parseToolsValue,
|
|
270
308
|
};
|
|
@@ -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
|