@punk6529/playbook 0.2.9 → 0.3.0
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 +14 -0
- package/README.md +31 -50
- package/package.json +1 -1
- package/src/cli.js +21 -95
- package/src/commands/hit.js +9 -20
- package/src/commands/query.js +11 -56
- package/src/commands/reindex.js +4 -19
- package/src/commands/show.js +10 -22
- package/src/core/context.js +0 -8
- package/src/manifest.js +1 -21
- package/src/options.js +3 -87
- package/templates/project/skills/playbook-advisor/SKILL.md +1 -1
- package/templates/project/skills/playbook-case/SKILL.md +9 -17
- package/templates/project/skills/playbook-query/SKILL.md +5 -6
- package/src/commands/promote.js +0 -96
- package/templates/project/docs/playbook/INDEX.json +0 -1
- package/templates/project/skills/playbook-promote/SKILL.md +0 -32
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
**BREAKING**: 移除项目级知识存储,简化为全局单一作用域
|
|
6
|
+
|
|
7
|
+
- 移除 `promote` 命令及 `playbook-promote` skill
|
|
8
|
+
- 移除 `--scope` 参数(query/show/hit/reindex 全部只操作 `~/.playbook/repo/`)
|
|
9
|
+
- `playbook init` 不再创建 `docs/playbook/` 目录,仅安装 AI skill 模板
|
|
10
|
+
- `playbook update` 仅刷新 skill 模板
|
|
11
|
+
- `playbook reindex` 直接重建全局 INDEX,无需 `--scope global`
|
|
12
|
+
- `playbook root` 直接输出全局路径,无需 `--scope`
|
|
13
|
+
- `playbook-case` skill 直接写入全局知识库,不再询问 scope
|
|
14
|
+
- Skills 从 4 个减为 3 个(advisor、query、case)
|
|
15
|
+
- query 输出不再包含 `[scope]` 前缀和 scope 字段
|
|
16
|
+
|
|
3
17
|
## 0.2.8
|
|
4
18
|
|
|
5
19
|
- 新增 `playbook root [--scope project|global]` 命令,输出 playbook 目录绝对路径
|
package/README.md
CHANGED
|
@@ -1,38 +1,37 @@
|
|
|
1
1
|
# @punk6529/playbook
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
把踩过的坑,沉淀成 AI 可读、可查、可复用的工程记忆。
|
|
4
4
|
|
|
5
|
-
`@punk6529/playbook` 是一个结构化工程知识库 CLI
|
|
5
|
+
`@punk6529/playbook` 是一个结构化工程知识库 CLI,用于建立并维护全局 Playbook 资产(cases / patterns / checklists + INDEX)。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
它不只是"记笔记工具",而是把"个人经验"变成"可复用资产"的最小基础设施:
|
|
8
8
|
|
|
9
9
|
- 解决问题后可立即沉淀为 case,避免同类问题反复排查
|
|
10
10
|
- AI 可以按 tag 精准检索历史经验,而不是每次从零猜测
|
|
11
|
-
-
|
|
11
|
+
- 全局知识库跨项目共享,所有经验统一管理
|
|
12
12
|
- 通过结构化索引 + 按需读取,控制上下文体积,降低 token 浪费
|
|
13
13
|
|
|
14
14
|
## 这个工具适合谁
|
|
15
15
|
|
|
16
16
|
- 希望减少重复踩坑的个人开发者
|
|
17
|
-
-
|
|
18
|
-
- 在 Codex / Claude 工作流中,希望让 AI“带着历史经验工作”的团队
|
|
17
|
+
- 在 Codex / Claude 工作流中,希望让 AI"带着历史经验工作"的用户
|
|
19
18
|
|
|
20
19
|
## 你会获得什么
|
|
21
20
|
|
|
22
|
-
-
|
|
21
|
+
- 一套全局知识库目录结构(`~/.playbook/repo/`)
|
|
23
22
|
- 三个可直接使用的 AI skills:
|
|
24
23
|
- `/playbook-advisor`:会话内主动顾问,自动按需查询知识库
|
|
25
24
|
- `/playbook-query`:手动按标签检索经验
|
|
26
25
|
- `/playbook-case`:引导式记录新 case,并更新索引
|
|
27
|
-
- 一套稳定的 CLI 工作流(init /
|
|
26
|
+
- 一套稳定的 CLI 工作流(global init / init / update / query / show / hit / reindex),便于持续演进而不破坏现有内容
|
|
28
27
|
|
|
29
28
|
## 典型工作流
|
|
30
29
|
|
|
31
|
-
1. `playbook init`
|
|
32
|
-
2. `playbook
|
|
33
|
-
3. 新会话开始先调用 `/playbook-advisor`,AI 会列出现有 tags;后续遇到问题时,会按相关 tags 查询案例 summary
|
|
34
|
-
4.
|
|
35
|
-
5.
|
|
30
|
+
1. `playbook global init` 初始化全局知识库(一次即可)
|
|
31
|
+
2. `playbook init` 在项目中安装 AI skill 模板(每个项目一次)
|
|
32
|
+
3. 新会话开始先调用 `/playbook-advisor`,AI 会列出现有 tags;后续遇到问题时,会按相关 tags 查询案例 summary,并选取最相关案例查看详情,获取问题解决方案
|
|
33
|
+
4. 如果遇到新问题,解决后,手动调用 `/playbook-case` 沉淀经验并更新索引
|
|
34
|
+
5. 所有经验统一存储在全局知识库,自动跨项目复用
|
|
36
35
|
|
|
37
36
|
## 安装
|
|
38
37
|
|
|
@@ -50,28 +49,28 @@ npx @punk6529/playbook --help
|
|
|
50
49
|
|
|
51
50
|
日常使用的命令:
|
|
52
51
|
|
|
53
|
-
### `playbook init`
|
|
52
|
+
### `playbook global init`
|
|
54
53
|
|
|
55
|
-
|
|
54
|
+
初始化全局知识库。只需运行一次。
|
|
56
55
|
|
|
57
56
|
```bash
|
|
58
|
-
playbook init
|
|
59
|
-
playbook init ./my-project # 指定目录
|
|
60
|
-
playbook init --tools claude # 仅安装 Claude skill
|
|
61
|
-
playbook init --tools none # 不安装任何 AI skill
|
|
57
|
+
playbook global init
|
|
62
58
|
```
|
|
63
59
|
|
|
64
|
-
创建
|
|
60
|
+
创建 `~/.playbook/repo/{cases,patterns,checklists}`、`INDEX.json` 和 `tags.yml`。
|
|
65
61
|
|
|
66
|
-
### `playbook
|
|
62
|
+
### `playbook init`
|
|
67
63
|
|
|
68
|
-
|
|
64
|
+
在项目中安装 AI skill 模板。每个新项目首次使用时运行一次。
|
|
69
65
|
|
|
70
66
|
```bash
|
|
71
|
-
playbook
|
|
67
|
+
playbook init # 当前目录,所有 AI 工具
|
|
68
|
+
playbook init ./my-project # 指定目录
|
|
69
|
+
playbook init --tools claude # 仅安装 Claude skill
|
|
70
|
+
playbook init --tools none # 不安装任何 AI skill
|
|
72
71
|
```
|
|
73
72
|
|
|
74
|
-
|
|
73
|
+
安装 AI skill 模板到 `.claude/skills/` 和 `.codex/skills/`。
|
|
75
74
|
|
|
76
75
|
### `playbook update`
|
|
77
76
|
|
|
@@ -79,10 +78,9 @@ playbook global init
|
|
|
79
78
|
|
|
80
79
|
```bash
|
|
81
80
|
playbook update # 更新当前项目
|
|
82
|
-
playbook update --force # 强制覆盖(含 INDEX.json)
|
|
83
81
|
```
|
|
84
82
|
|
|
85
|
-
仅刷新 skill
|
|
83
|
+
仅刷新 skill 模板,不修改知识库内容。
|
|
86
84
|
|
|
87
85
|
## 在 AI 中使用
|
|
88
86
|
|
|
@@ -118,8 +116,8 @@ AI:根据之前的经验,这个问题是因为 Docker 默认绑定 0.0.0.0
|
|
|
118
116
|
AI:请提供要搜索的 tag
|
|
119
117
|
你:docker
|
|
120
118
|
AI:找到以下条目:
|
|
121
|
-
|
|
122
|
-
(1 match
|
|
119
|
+
case-001-docker-bind Docker bind address issue docker,env hits:3
|
|
120
|
+
(1 match)
|
|
123
121
|
```
|
|
124
122
|
|
|
125
123
|
### 3. `/playbook-case` —— 记录新 case
|
|
@@ -131,35 +129,21 @@ AI:找到以下条目:
|
|
|
131
129
|
AI:请描述遇到的问题...
|
|
132
130
|
你:Next.js 部署到 Vercel 后 API 路由 404
|
|
133
131
|
AI:(引导你填写 问题/原因/解决/教训 四个部分)
|
|
134
|
-
AI:(生成 case
|
|
132
|
+
AI:(生成 case 文件到 ~/.playbook/repo/,更新 INDEX.json)
|
|
135
133
|
```
|
|
136
134
|
|
|
137
|
-
### `playbook promote`
|
|
138
|
-
|
|
139
|
-
将项目级案例提升到全局知识库,让经验跨项目复用。
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
playbook promote case-001-docker-bind # 提升单个条目
|
|
143
|
-
playbook promote case-001 case-002 # 提升多个条目
|
|
144
|
-
playbook promote --all # 提升所有项目条目
|
|
145
|
-
playbook promote case-001 --force # 强制覆盖已存在的全局条目
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
将项目 `docs/playbook/` 下的条目移动到全局 `~/.playbook/repo/`,同时更新两端的 INDEX.json。
|
|
149
|
-
|
|
150
135
|
## 内部命令
|
|
151
136
|
|
|
152
137
|
以下命令主要由 AI skill 内部调用,用户一般不需要直接使用:
|
|
153
138
|
|
|
154
139
|
### `playbook query`
|
|
155
140
|
|
|
156
|
-
按 tag
|
|
141
|
+
按 tag 搜索全局 INDEX.json 条目(OR 逻辑)。
|
|
157
142
|
|
|
158
143
|
```bash
|
|
159
144
|
playbook query docker env # 匹配 "docker" 或 "env" 的条目
|
|
160
145
|
playbook query # 所有条目
|
|
161
146
|
playbook query --tags-only # 仅输出 tag 概览:docker(3) env(5) ci(2)
|
|
162
|
-
playbook query --scope project # 仅项目范围
|
|
163
147
|
playbook query --limit 0 # 不限数量
|
|
164
148
|
playbook query docker --json # JSON 格式输出
|
|
165
149
|
```
|
|
@@ -174,8 +158,6 @@ playbook query docker --json # JSON 格式输出
|
|
|
174
158
|
playbook show case-001-docker-bind # 输出案例文件内容
|
|
175
159
|
```
|
|
176
160
|
|
|
177
|
-
先查项目 INDEX,再查全局 INDEX,找到后输出对应文件内容到 stdout。
|
|
178
|
-
|
|
179
161
|
### `playbook hit`
|
|
180
162
|
|
|
181
163
|
记录一次案例阅读命中,用于追踪案例的实际使用频率。
|
|
@@ -188,14 +170,13 @@ AI advisor 每次阅读案例详情后自动调用。命中次数在 `playbook q
|
|
|
188
170
|
|
|
189
171
|
### `playbook reindex`
|
|
190
172
|
|
|
191
|
-
从文件 front matter
|
|
173
|
+
从文件 front matter 重建全局 INDEX.json。当 INDEX 被清空、损坏或需要同步时使用。
|
|
192
174
|
|
|
193
175
|
```bash
|
|
194
|
-
playbook reindex #
|
|
195
|
-
playbook reindex --scope global # 重建全局 INDEX
|
|
176
|
+
playbook reindex # 重建全局 INDEX
|
|
196
177
|
```
|
|
197
178
|
|
|
198
|
-
扫描 `cases/`、`patterns/`、`checklists/`
|
|
179
|
+
扫描 `~/.playbook/repo/` 下 `cases/`、`patterns/`、`checklists/` 的 `.md` 文件,解析 YAML front matter(title, tags, created),重建 INDEX.json。保留已有的 hits 统计。
|
|
199
180
|
|
|
200
181
|
## 错误处理
|
|
201
182
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -2,12 +2,11 @@ const fs = require("fs");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const os = require("os");
|
|
4
4
|
const { CliError } = require("./errors");
|
|
5
|
-
const { parseProjectCommandArgs, parseGlobalInitArgs, parseQueryArgs,
|
|
5
|
+
const { parseProjectCommandArgs, parseGlobalInitArgs, parseQueryArgs, parseHitArgs, parseShowArgs, parseReindexArgs } = require("./options");
|
|
6
6
|
const { initProject } = require("./commands/init-project");
|
|
7
7
|
const { initGlobal } = require("./commands/global-init");
|
|
8
8
|
const { updateProject } = require("./commands/update-project");
|
|
9
9
|
const { queryIndex, aggregateTags, formatPlainText, formatJson, formatTagsOnly } = require("./commands/query");
|
|
10
|
-
const { promoteEntries } = require("./commands/promote");
|
|
11
10
|
const { hitEntry } = require("./commands/hit");
|
|
12
11
|
const { showEntry } = require("./commands/show");
|
|
13
12
|
const { reindexPlaybook } = require("./commands/reindex");
|
|
@@ -19,19 +18,16 @@ function usageText() {
|
|
|
19
18
|
" playbook init [path] [--tools all|none|codex,claude] [--force]",
|
|
20
19
|
" playbook global init [--force]",
|
|
21
20
|
" playbook update [path] [--tools all|none|codex,claude] [--force]",
|
|
22
|
-
" playbook query [tags...] [--limit N] [--
|
|
23
|
-
" playbook promote <entry-id...> [--force]",
|
|
24
|
-
" playbook promote --all [--force]",
|
|
21
|
+
" playbook query [tags...] [--limit N] [--json] [--tags-only]",
|
|
25
22
|
" playbook hit <entry-id>",
|
|
26
23
|
" playbook show <entry-id>",
|
|
27
|
-
" playbook reindex
|
|
28
|
-
" playbook root
|
|
24
|
+
" playbook reindex",
|
|
25
|
+
" playbook root",
|
|
29
26
|
"",
|
|
30
27
|
"Notes:",
|
|
31
|
-
" - `init`
|
|
32
|
-
" - `
|
|
33
|
-
" - `query` searches INDEX.json by tags and returns matching entries.",
|
|
34
|
-
" - V1 does not support `playbook global update`.",
|
|
28
|
+
" - `global init` initializes ~/.playbook/repo (knowledge storage).",
|
|
29
|
+
" - `init` installs AI skill templates into the current project.",
|
|
30
|
+
" - `query` searches global INDEX.json by tags and returns matching entries.",
|
|
35
31
|
].join("\n");
|
|
36
32
|
}
|
|
37
33
|
|
|
@@ -82,12 +78,11 @@ function queryHelpText() {
|
|
|
82
78
|
return [
|
|
83
79
|
"Usage: playbook query [tags...] [options]",
|
|
84
80
|
"",
|
|
85
|
-
"Search INDEX.json entries by tags (OR logic).",
|
|
81
|
+
"Search global INDEX.json entries by tags (OR logic).",
|
|
86
82
|
"Without tags, returns all entries.",
|
|
87
83
|
"",
|
|
88
84
|
"Options:",
|
|
89
85
|
" --limit N Max results to return (default: 7, 0 = unlimited)",
|
|
90
|
-
" --scope X Search scope: project, global, or both (default: both)",
|
|
91
86
|
" --json Output results as JSON array",
|
|
92
87
|
" --tags-only Output only tag names with case counts",
|
|
93
88
|
" -h, --help Show this help",
|
|
@@ -113,61 +108,11 @@ function runQuery(args, io) {
|
|
|
113
108
|
return 0;
|
|
114
109
|
}
|
|
115
110
|
|
|
116
|
-
function promoteHelpText() {
|
|
117
|
-
return [
|
|
118
|
-
"Usage: playbook promote <entry-id...> [options]",
|
|
119
|
-
" playbook promote --all [options]",
|
|
120
|
-
"",
|
|
121
|
-
"Move project entries to global knowledge base.",
|
|
122
|
-
"",
|
|
123
|
-
"Options:",
|
|
124
|
-
" --all Promote all project entries",
|
|
125
|
-
" --force Overwrite if entry already exists in global",
|
|
126
|
-
" -h, --help Show this help",
|
|
127
|
-
].join("\n");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function runPromote(args, io) {
|
|
131
|
-
const options = parsePromoteArgs(args);
|
|
132
|
-
if (options.help) {
|
|
133
|
-
writeStdout(io, promoteHelpText());
|
|
134
|
-
return 0;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const result = promoteEntries(options);
|
|
138
|
-
|
|
139
|
-
if (result.message) {
|
|
140
|
-
writeStdout(io, result.message);
|
|
141
|
-
return 0;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
let hasError = false;
|
|
145
|
-
for (const r of result.results) {
|
|
146
|
-
if (r.action === "promoted") {
|
|
147
|
-
writeStdout(io, `[promoted] ${r.entryId}\t${r.title}`);
|
|
148
|
-
} else if (r.action === "conflict") {
|
|
149
|
-
writeStderr(io, `[conflict] ${r.entryId}\t${r.reason}`);
|
|
150
|
-
hasError = true;
|
|
151
|
-
} else if (r.action === "error") {
|
|
152
|
-
writeStderr(io, `[error] ${r.entryId}\t${r.reason}`);
|
|
153
|
-
hasError = true;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const promoted = result.results.filter((r) => r.action === "promoted").length;
|
|
158
|
-
if (promoted > 0) {
|
|
159
|
-
writeStdout(io, `(${promoted} ${promoted === 1 ? "entry" : "entries"} promoted to global)`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return hasError ? 1 : 0;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
111
|
function hitHelpText() {
|
|
166
112
|
return [
|
|
167
113
|
"Usage: playbook hit <entry-id>",
|
|
168
114
|
"",
|
|
169
|
-
"Record a read hit on a playbook entry.",
|
|
170
|
-
"Searches project INDEX first, then global INDEX.",
|
|
115
|
+
"Record a read hit on a playbook entry in global INDEX.",
|
|
171
116
|
"",
|
|
172
117
|
"Options:",
|
|
173
118
|
" -h, --help Show this help",
|
|
@@ -182,7 +127,7 @@ function runHit(args, io) {
|
|
|
182
127
|
}
|
|
183
128
|
|
|
184
129
|
const result = hitEntry(options);
|
|
185
|
-
writeStdout(io,
|
|
130
|
+
writeStdout(io, `${result.entryId} hits:${result.hits}`);
|
|
186
131
|
return 0;
|
|
187
132
|
}
|
|
188
133
|
|
|
@@ -190,8 +135,7 @@ function showHelpText() {
|
|
|
190
135
|
return [
|
|
191
136
|
"Usage: playbook show <entry-id>",
|
|
192
137
|
"",
|
|
193
|
-
"Display the full content of a playbook entry.",
|
|
194
|
-
"Searches project INDEX first, then global INDEX.",
|
|
138
|
+
"Display the full content of a playbook entry from global INDEX.",
|
|
195
139
|
"",
|
|
196
140
|
"Options:",
|
|
197
141
|
" -h, --help Show this help",
|
|
@@ -212,12 +156,11 @@ function runShow(args, io) {
|
|
|
212
156
|
|
|
213
157
|
function reindexHelpText() {
|
|
214
158
|
return [
|
|
215
|
-
"Usage: playbook reindex
|
|
159
|
+
"Usage: playbook reindex",
|
|
216
160
|
"",
|
|
217
|
-
"Rebuild INDEX.json by scanning playbook files and reading front matter.",
|
|
161
|
+
"Rebuild global INDEX.json by scanning playbook files and reading front matter.",
|
|
218
162
|
"",
|
|
219
163
|
"Options:",
|
|
220
|
-
" --scope X Scope: project or global (default: project)",
|
|
221
164
|
" -h, --help Show this help",
|
|
222
165
|
].join("\n");
|
|
223
166
|
}
|
|
@@ -230,36 +173,23 @@ function runReindex(args, io) {
|
|
|
230
173
|
}
|
|
231
174
|
|
|
232
175
|
const result = reindexPlaybook(options);
|
|
233
|
-
writeStdout(io, `Reindexed
|
|
176
|
+
writeStdout(io, `Reindexed: ${result.count} ${result.count === 1 ? "entry" : "entries"} found.`);
|
|
234
177
|
return 0;
|
|
235
178
|
}
|
|
236
179
|
|
|
237
180
|
function runRoot(args, io) {
|
|
238
|
-
let scope = "project";
|
|
239
181
|
for (const arg of args) {
|
|
240
182
|
if (arg === "-h" || arg === "--help") {
|
|
241
|
-
writeStdout(io, "Usage: playbook root
|
|
183
|
+
writeStdout(io, "Usage: playbook root\n\nOutput the absolute path of the global playbook directory.");
|
|
242
184
|
return 0;
|
|
243
185
|
}
|
|
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
186
|
}
|
|
249
187
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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);
|
|
188
|
+
const globalRoot = path.join(os.homedir(), ".playbook/repo");
|
|
189
|
+
if (!fs.existsSync(globalRoot)) {
|
|
190
|
+
throw new CliError("Global playbook not found. Run 'playbook global init' first.");
|
|
262
191
|
}
|
|
192
|
+
writeStdout(io, globalRoot);
|
|
263
193
|
return 0;
|
|
264
194
|
}
|
|
265
195
|
|
|
@@ -288,7 +218,7 @@ function runGlobal(args, io) {
|
|
|
288
218
|
printSummary("playbook global init completed", result.globalRoot, result.results, (line) =>
|
|
289
219
|
writeStdout(io, line)
|
|
290
220
|
);
|
|
291
|
-
writeStdout(io, "Next: run `playbook init` inside a project to
|
|
221
|
+
writeStdout(io, "Next: run `playbook init` inside a project to install AI skill templates.");
|
|
292
222
|
return 0;
|
|
293
223
|
}
|
|
294
224
|
|
|
@@ -312,10 +242,6 @@ async function runCli(argv, io = process) {
|
|
|
312
242
|
return runQuery(argv.slice(1), io);
|
|
313
243
|
}
|
|
314
244
|
|
|
315
|
-
if (command === "promote") {
|
|
316
|
-
return runPromote(argv.slice(1), io);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
245
|
if (command === "hit") {
|
|
320
246
|
return runHit(argv.slice(1), io);
|
|
321
247
|
}
|
|
@@ -336,7 +262,7 @@ async function runCli(argv, io = process) {
|
|
|
336
262
|
return runGlobal(argv.slice(1), io);
|
|
337
263
|
}
|
|
338
264
|
|
|
339
|
-
throw new CliError(`Unknown command: ${command}. Supported: init, global, update, query,
|
|
265
|
+
throw new CliError(`Unknown command: ${command}. Supported: init, global, update, query, hit, show, reindex, root`);
|
|
340
266
|
} catch (error) {
|
|
341
267
|
if (error instanceof CliError) {
|
|
342
268
|
writeStderr(io, `Error: ${error.message}`);
|
package/src/commands/hit.js
CHANGED
|
@@ -17,33 +17,22 @@ function writeIndex(indexPath, data) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
function hitEntry(options) {
|
|
20
|
-
const { entryId
|
|
20
|
+
const { entryId } = options;
|
|
21
21
|
|
|
22
|
-
const projectIndexPath = path.resolve(targetPath, "docs/playbook/INDEX.json");
|
|
23
22
|
const globalIndexPath = path.join(os.homedir(), ".playbook/repo/INDEX.json");
|
|
24
|
-
|
|
25
|
-
// Try project first
|
|
26
|
-
const projectIndex = readIndex(projectIndexPath);
|
|
27
|
-
if (projectIndex && projectIndex[entryId]) {
|
|
28
|
-
projectIndex[entryId].hits = (projectIndex[entryId].hits || 0) + 1;
|
|
29
|
-
writeIndex(projectIndexPath, projectIndex);
|
|
30
|
-
return { entryId, scope: "project", hits: projectIndex[entryId].hits };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Fall back to global
|
|
34
23
|
const globalIndex = readIndex(globalIndexPath);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return { entryId, scope: "global", hits: globalIndex[entryId].hits };
|
|
24
|
+
|
|
25
|
+
if (!globalIndex) {
|
|
26
|
+
throw new CliError("No INDEX.json found. Run 'playbook global init' first.");
|
|
39
27
|
}
|
|
40
28
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
throw new CliError("No INDEX.json found. Run 'playbook init' or 'playbook global init' first.");
|
|
29
|
+
if (!globalIndex[entryId]) {
|
|
30
|
+
throw new CliError(`Entry '${entryId}' not found in INDEX.`);
|
|
44
31
|
}
|
|
45
32
|
|
|
46
|
-
|
|
33
|
+
globalIndex[entryId].hits = (globalIndex[entryId].hits || 0) + 1;
|
|
34
|
+
writeIndex(globalIndexPath, globalIndex);
|
|
35
|
+
return { entryId, hits: globalIndex[entryId].hits };
|
|
47
36
|
}
|
|
48
37
|
|
|
49
38
|
module.exports = {
|
package/src/commands/query.js
CHANGED
|
@@ -24,37 +24,16 @@ function filterByTags(index, tags) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function queryIndex(options) {
|
|
27
|
-
const { tags = [], limit = DEFAULT_LIMIT
|
|
27
|
+
const { tags = [], limit = DEFAULT_LIMIT } = options;
|
|
28
28
|
|
|
29
|
-
const projectIndexPath = path.resolve(targetPath, "docs/playbook/INDEX.json");
|
|
30
29
|
const globalIndexPath = path.join(os.homedir(), ".playbook/repo/INDEX.json");
|
|
30
|
+
const globalIndex = readIndex(globalIndexPath);
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
let globalEntries = [];
|
|
34
|
-
let anyIndexFound = false;
|
|
35
|
-
|
|
36
|
-
if (scope === "both" || scope === "project") {
|
|
37
|
-
const projectIndex = readIndex(projectIndexPath);
|
|
38
|
-
if (projectIndex) {
|
|
39
|
-
anyIndexFound = true;
|
|
40
|
-
projectEntries = filterByTags(projectIndex, tags).map((e) => ({ ...e, scope: "project" }));
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (scope === "both" || scope === "global") {
|
|
45
|
-
const globalIndex = readIndex(globalIndexPath);
|
|
46
|
-
if (globalIndex) {
|
|
47
|
-
anyIndexFound = true;
|
|
48
|
-
globalEntries = filterByTags(globalIndex, tags).map((e) => ({ ...e, scope: "global" }));
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const all = [...projectEntries, ...globalEntries];
|
|
53
|
-
|
|
54
|
-
if (!anyIndexFound) {
|
|
32
|
+
if (!globalIndex) {
|
|
55
33
|
return { entries: [], found: false };
|
|
56
34
|
}
|
|
57
35
|
|
|
36
|
+
const all = filterByTags(globalIndex, tags);
|
|
58
37
|
const limited = limit > 0 ? all.slice(0, limit) : all;
|
|
59
38
|
|
|
60
39
|
return { entries: limited, total: all.length, found: true };
|
|
@@ -70,23 +49,17 @@ function formatPlainText(result) {
|
|
|
70
49
|
}
|
|
71
50
|
|
|
72
51
|
const lines = result.entries.map(
|
|
73
|
-
(e) =>
|
|
52
|
+
(e) => `${e.id}\t${e.title}\t${e.tags.join(",")}\thits:${e.hits || 0}`
|
|
74
53
|
);
|
|
75
54
|
|
|
76
55
|
const displayed = result.entries.length;
|
|
77
56
|
const total = result.total;
|
|
78
|
-
const projectCount = result.entries.filter((e) => e.scope === "project").length;
|
|
79
|
-
const globalCount = result.entries.filter((e) => e.scope === "global").length;
|
|
80
|
-
|
|
81
|
-
const parts = [];
|
|
82
|
-
if (projectCount > 0) parts.push(`${projectCount} project`);
|
|
83
|
-
if (globalCount > 0) parts.push(`${globalCount} global`);
|
|
84
57
|
|
|
85
58
|
let summary = `(${total} match${total === 1 ? "" : "es"}`;
|
|
86
59
|
if (displayed < total) {
|
|
87
60
|
summary += `, showing ${displayed}`;
|
|
88
61
|
}
|
|
89
|
-
summary +=
|
|
62
|
+
summary += ")";
|
|
90
63
|
|
|
91
64
|
lines.push(summary);
|
|
92
65
|
return lines.join("\n");
|
|
@@ -108,40 +81,23 @@ function readTagsRegistry(tagsYmlPath) {
|
|
|
108
81
|
}
|
|
109
82
|
}
|
|
110
83
|
|
|
111
|
-
function aggregateTags(
|
|
112
|
-
const { scope = "both", targetPath = "." } = options;
|
|
113
|
-
|
|
114
|
-
const projectIndexPath = path.resolve(targetPath, "docs/playbook/INDEX.json");
|
|
84
|
+
function aggregateTags() {
|
|
115
85
|
const globalIndexPath = path.join(os.homedir(), ".playbook/repo/INDEX.json");
|
|
116
86
|
const tagsYmlPath = path.join(os.homedir(), ".playbook/repo/tags.yml");
|
|
117
87
|
|
|
118
88
|
const tagCounts = {};
|
|
119
89
|
let anyIndexFound = false;
|
|
120
90
|
|
|
121
|
-
|
|
122
|
-
|
|
91
|
+
const globalIndex = readIndex(globalIndexPath);
|
|
92
|
+
if (globalIndex) {
|
|
93
|
+
anyIndexFound = true;
|
|
94
|
+
for (const entry of Object.values(globalIndex)) {
|
|
123
95
|
for (const tag of entry.tags) {
|
|
124
96
|
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
|
|
125
97
|
}
|
|
126
98
|
}
|
|
127
99
|
}
|
|
128
100
|
|
|
129
|
-
if (scope === "both" || scope === "project") {
|
|
130
|
-
const projectIndex = readIndex(projectIndexPath);
|
|
131
|
-
if (projectIndex) {
|
|
132
|
-
anyIndexFound = true;
|
|
133
|
-
countTags(projectIndex);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (scope === "both" || scope === "global") {
|
|
138
|
-
const globalIndex = readIndex(globalIndexPath);
|
|
139
|
-
if (globalIndex) {
|
|
140
|
-
anyIndexFound = true;
|
|
141
|
-
countTags(globalIndex);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
101
|
// Merge tags.yml registry — ensures registered tags appear even with count 0
|
|
146
102
|
const registryTags = readTagsRegistry(tagsYmlPath);
|
|
147
103
|
if (registryTags) {
|
|
@@ -180,7 +136,6 @@ function formatJson(result) {
|
|
|
180
136
|
|
|
181
137
|
const output = result.entries.map((e) => ({
|
|
182
138
|
id: e.id,
|
|
183
|
-
scope: e.scope,
|
|
184
139
|
type: e.type,
|
|
185
140
|
title: e.title,
|
|
186
141
|
tags: e.tags,
|
package/src/commands/reindex.js
CHANGED
|
@@ -50,26 +50,12 @@ function scanDir(baseDir) {
|
|
|
50
50
|
return entries;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
function reindexPlaybook(
|
|
54
|
-
const
|
|
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
|
-
}
|
|
53
|
+
function reindexPlaybook() {
|
|
54
|
+
const baseDir = path.join(os.homedir(), ".playbook/repo");
|
|
55
|
+
const indexPath = path.join(baseDir, "INDEX.json");
|
|
66
56
|
|
|
67
57
|
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
|
-
);
|
|
58
|
+
throw new CliError("Global playbook not found. Run 'playbook global init' first.");
|
|
73
59
|
}
|
|
74
60
|
|
|
75
61
|
// Read existing index to preserve hits
|
|
@@ -89,7 +75,6 @@ function reindexPlaybook(options) {
|
|
|
89
75
|
fs.writeFileSync(indexPath, JSON.stringify(newEntries, null, 2) + "\n", "utf8");
|
|
90
76
|
|
|
91
77
|
return {
|
|
92
|
-
scope,
|
|
93
78
|
count: Object.keys(newEntries).length,
|
|
94
79
|
entries: newEntries,
|
|
95
80
|
};
|
package/src/commands/show.js
CHANGED
|
@@ -13,35 +13,23 @@ function readIndex(indexPath) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function showEntry(options) {
|
|
16
|
-
const { entryId
|
|
16
|
+
const { entryId } = options;
|
|
17
17
|
|
|
18
|
-
const projectIndexPath = path.resolve(targetPath, "docs/playbook/INDEX.json");
|
|
19
18
|
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
19
|
const globalIndex = readIndex(globalIndexPath);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const content = readFile(fullPath, entryId);
|
|
36
|
-
return { entryId, scope: "global", content };
|
|
20
|
+
|
|
21
|
+
if (!globalIndex) {
|
|
22
|
+
throw new CliError("No INDEX.json found. Run 'playbook global init' first.");
|
|
37
23
|
}
|
|
38
24
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
throw new CliError("No INDEX.json found. Run 'playbook init' or 'playbook global init' first.");
|
|
25
|
+
if (!globalIndex[entryId]) {
|
|
26
|
+
throw new CliError(`Entry '${entryId}' not found in INDEX.`);
|
|
42
27
|
}
|
|
43
28
|
|
|
44
|
-
|
|
29
|
+
const entryPath = globalIndex[entryId].path;
|
|
30
|
+
const fullPath = path.join(os.homedir(), ".playbook/repo", entryPath);
|
|
31
|
+
const content = readFile(fullPath, entryId);
|
|
32
|
+
return { entryId, content };
|
|
45
33
|
}
|
|
46
34
|
|
|
47
35
|
function readFile(fullPath, entryId) {
|
package/src/core/context.js
CHANGED
|
@@ -11,14 +11,6 @@ function resolveGlobalRoot() {
|
|
|
11
11
|
return path.join(home, ".playbook", "repo");
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function resolveRuntimeContext(projectRoot = ".") {
|
|
15
|
-
return {
|
|
16
|
-
projectRoot: path.resolve(projectRoot),
|
|
17
|
-
globalRoot: resolveGlobalRoot(),
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
14
|
module.exports = {
|
|
22
|
-
resolveRuntimeContext,
|
|
23
15
|
resolveGlobalRoot,
|
|
24
16
|
};
|
package/src/manifest.js
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
|
-
const PROJECT_SKELETON_DIRS = [
|
|
2
|
-
"docs/playbook/cases",
|
|
3
|
-
"docs/playbook/patterns",
|
|
4
|
-
"docs/playbook/checklists",
|
|
5
|
-
];
|
|
1
|
+
const PROJECT_SKELETON_DIRS = [];
|
|
6
2
|
|
|
7
3
|
const PROJECT_MANAGED_FILES = [
|
|
8
|
-
{
|
|
9
|
-
relativePath: "docs/playbook/INDEX.json",
|
|
10
|
-
templatePath: "project/docs/playbook/INDEX.json",
|
|
11
|
-
},
|
|
12
4
|
{
|
|
13
5
|
relativePath: ".codex/skills/playbook-query/SKILL.md",
|
|
14
6
|
templatePath: "project/skills/playbook-query/SKILL.md",
|
|
@@ -45,18 +37,6 @@ const PROJECT_MANAGED_FILES = [
|
|
|
45
37
|
tool: "claude",
|
|
46
38
|
overwriteOnUpdate: true,
|
|
47
39
|
},
|
|
48
|
-
{
|
|
49
|
-
relativePath: ".codex/skills/playbook-promote/SKILL.md",
|
|
50
|
-
templatePath: "project/skills/playbook-promote/SKILL.md",
|
|
51
|
-
tool: "codex",
|
|
52
|
-
overwriteOnUpdate: true,
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
relativePath: ".claude/skills/playbook-promote/SKILL.md",
|
|
56
|
-
templatePath: "project/skills/playbook-promote/SKILL.md",
|
|
57
|
-
tool: "claude",
|
|
58
|
-
overwriteOnUpdate: true,
|
|
59
|
-
},
|
|
60
40
|
];
|
|
61
41
|
|
|
62
42
|
const GLOBAL_SKELETON_DIRS = ["cases", "patterns", "checklists"];
|
package/src/options.js
CHANGED
|
@@ -108,7 +108,6 @@ function parseGlobalInitArgs(args) {
|
|
|
108
108
|
|
|
109
109
|
function parseQueryArgs(args) {
|
|
110
110
|
let limit = 7;
|
|
111
|
-
let scope = "both";
|
|
112
111
|
let json = false;
|
|
113
112
|
let tagsOnly = false;
|
|
114
113
|
const tags = [];
|
|
@@ -152,25 +151,6 @@ function parseQueryArgs(args) {
|
|
|
152
151
|
continue;
|
|
153
152
|
}
|
|
154
153
|
|
|
155
|
-
if (arg === "--scope") {
|
|
156
|
-
const next = args[i + 1];
|
|
157
|
-
if (!next || !["project", "global", "both"].includes(next)) {
|
|
158
|
-
throw new CliError("--scope must be one of: project, global, both");
|
|
159
|
-
}
|
|
160
|
-
scope = next;
|
|
161
|
-
i += 1;
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (arg.startsWith("--scope=")) {
|
|
166
|
-
const value = arg.slice("--scope=".length);
|
|
167
|
-
if (!["project", "global", "both"].includes(value)) {
|
|
168
|
-
throw new CliError("--scope must be one of: project, global, both");
|
|
169
|
-
}
|
|
170
|
-
scope = value;
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
154
|
if (arg.startsWith("-")) {
|
|
175
155
|
throw new CliError(`Unknown option for playbook query: ${arg}`);
|
|
176
156
|
}
|
|
@@ -178,47 +158,7 @@ function parseQueryArgs(args) {
|
|
|
178
158
|
tags.push(arg);
|
|
179
159
|
}
|
|
180
160
|
|
|
181
|
-
return { help: false, tags, limit,
|
|
182
|
-
}
|
|
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 };
|
|
161
|
+
return { help: false, tags, limit, json, tagsOnly };
|
|
222
162
|
}
|
|
223
163
|
|
|
224
164
|
function parseHitArgs(args) {
|
|
@@ -260,47 +200,23 @@ function parseShowArgs(args) {
|
|
|
260
200
|
}
|
|
261
201
|
|
|
262
202
|
function parseReindexArgs(args) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
266
|
-
const arg = args[i];
|
|
267
|
-
|
|
203
|
+
for (const arg of args) {
|
|
268
204
|
if (arg === "-h" || arg === "--help") {
|
|
269
205
|
return { help: true };
|
|
270
206
|
}
|
|
271
207
|
|
|
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
208
|
if (arg.startsWith("-")) {
|
|
292
209
|
throw new CliError(`Unknown option for playbook reindex: ${arg}`);
|
|
293
210
|
}
|
|
294
211
|
}
|
|
295
212
|
|
|
296
|
-
return { help: false
|
|
213
|
+
return { help: false };
|
|
297
214
|
}
|
|
298
215
|
|
|
299
216
|
module.exports = {
|
|
300
217
|
parseProjectCommandArgs,
|
|
301
218
|
parseGlobalInitArgs,
|
|
302
219
|
parseQueryArgs,
|
|
303
|
-
parsePromoteArgs,
|
|
304
220
|
parseHitArgs,
|
|
305
221
|
parseShowArgs,
|
|
306
222
|
parseReindexArgs,
|
|
@@ -17,7 +17,7 @@ This outputs all available tags with their case counts, e.g.: `docker(3) react(2
|
|
|
17
17
|
|
|
18
18
|
Internalize this tag list. It tells you what domains have recorded experience.
|
|
19
19
|
|
|
20
|
-
If the output is "No playbook INDEX found." — inform the user that no playbook knowledge is available and suggest running `playbook init` to get started. Stop here.
|
|
20
|
+
If the output is "No playbook INDEX found." — inform the user that no playbook knowledge is available and suggest running `playbook global init` to get started. Stop here.
|
|
21
21
|
|
|
22
22
|
## When to Query
|
|
23
23
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: playbook-case
|
|
3
|
-
description: Draft and inspect playbook case entries
|
|
3
|
+
description: Draft and inspect playbook case entries in global knowledge base.
|
|
4
4
|
license: MIT
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ Use this skill when the user asks to create, review, or inspect case entries.
|
|
|
8
8
|
|
|
9
9
|
## Behavior
|
|
10
10
|
|
|
11
|
-
-
|
|
11
|
+
- All entries are stored in the global knowledge base (`~/.playbook/repo/`).
|
|
12
12
|
- Keep draft generation non-destructive by default.
|
|
13
13
|
- For new cases, enforce this guided flow:
|
|
14
14
|
1. If the user has not provided a concrete problem statement, ask first:
|
|
@@ -18,14 +18,10 @@ Use this skill when the user asks to create, review, or inspect case entries.
|
|
|
18
18
|
playbook query --tags-only
|
|
19
19
|
```
|
|
20
20
|
Suggest relevant existing tags from the output. Ask user to choose existing tags or explicitly confirm a new tag.
|
|
21
|
-
3.
|
|
22
|
-
- default is `project`
|
|
23
|
-
- use `global` only when user explicitly confirms
|
|
24
|
-
4. Only after required inputs are complete, produce the draft package.
|
|
21
|
+
3. Only after required inputs are complete, produce the draft package.
|
|
25
22
|
- Required inputs before any draft output:
|
|
26
23
|
- problem statement
|
|
27
24
|
- tag decision (existing tags or confirmed new tag)
|
|
28
|
-
- scope confirmation
|
|
29
25
|
- Hard gate: while any required input is missing, do not output case ID, suggested path, markdown draft, or INDEX update suggestion.
|
|
30
26
|
- For new cases, once inputs are complete, produce a complete draft package:
|
|
31
27
|
- case ID (format: `{type}-{NNN}-{slug}`, e.g. `case-001-oauth-token-race`)
|
|
@@ -94,20 +90,16 @@ The front matter is the source of truth for metadata. INDEX.json is rebuilt from
|
|
|
94
90
|
|
|
95
91
|
### File Paths
|
|
96
92
|
|
|
97
|
-
|
|
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.
|
|
93
|
+
All files are stored in the global playbook directory.
|
|
103
94
|
|
|
104
95
|
Get the playbook directory first:
|
|
105
96
|
```bash
|
|
106
|
-
playbook root
|
|
107
|
-
playbook root --scope global # global scope
|
|
97
|
+
playbook root
|
|
108
98
|
```
|
|
109
99
|
Then use the output to construct absolute paths, e.g. `<root>/cases/case-001-xxx.md` and `<root>/INDEX.json`.
|
|
110
100
|
|
|
101
|
+
**IMPORTANT:** Always use absolute paths when writing files. Do NOT use relative paths — the current working directory may not be the project root.
|
|
102
|
+
|
|
111
103
|
### Draft Package Output
|
|
112
104
|
|
|
113
105
|
When producing the draft package, output:
|
|
@@ -132,6 +124,6 @@ Add this entry to INDEX.json:
|
|
|
132
124
|
|
|
133
125
|
After user confirms the draft:
|
|
134
126
|
|
|
135
|
-
1. Write the case file to
|
|
136
|
-
2. Read the current INDEX.json
|
|
127
|
+
1. Write the case file to `~/.playbook/repo/<path>`
|
|
128
|
+
2. Read the current INDEX.json at `~/.playbook/repo/INDEX.json`, merge the new entry, write back
|
|
137
129
|
3. Run `playbook hit <entry-id>` is NOT needed — hit is only for reads
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: playbook-query
|
|
3
|
-
description: Query
|
|
3
|
+
description: Query global playbook knowledge using CLI for token-efficient discovery.
|
|
4
4
|
license: MIT
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -17,17 +17,16 @@ Run:
|
|
|
17
17
|
playbook query <tag1> <tag2> ...
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
This searches
|
|
20
|
+
This searches the global INDEX.json and returns matching entries (max 7 by default).
|
|
21
21
|
|
|
22
22
|
Options:
|
|
23
23
|
- `--limit N` — change max results (0 = unlimited)
|
|
24
|
-
- `--scope project|global|both` — restrict search scope
|
|
25
24
|
- `--json` — output as JSON array
|
|
26
25
|
|
|
27
26
|
Output format (one line per match):
|
|
28
27
|
```
|
|
29
|
-
|
|
30
|
-
(N matches
|
|
28
|
+
entry-id title tags hits:N
|
|
29
|
+
(N matches)
|
|
31
30
|
```
|
|
32
31
|
|
|
33
32
|
### Expansion (L2): Show entry content
|
|
@@ -42,6 +41,6 @@ This outputs the full content of the entry file. Only expand entries that are ac
|
|
|
42
41
|
|
|
43
42
|
### Edge Cases
|
|
44
43
|
|
|
45
|
-
- If query returns "No playbook INDEX found." — tell the user: "No playbook initialized. Run `playbook init` to get started."
|
|
44
|
+
- If query returns "No playbook INDEX found." — tell the user: "No playbook initialized. Run `playbook global init` to get started."
|
|
46
45
|
- If query returns "No matching entries." — suggest trying different tags or running without tags to see all entries
|
|
47
46
|
- If user query is vague, run `playbook query` without tags to list everything
|
package/src/commands/promote.js
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{}
|
|
@@ -1,32 +0,0 @@
|
|
|
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.
|