@punk6529/playbook 0.2.0 → 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 +75 -45
- 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
|
|
|
@@ -14,65 +46,44 @@ npm install -g @punk6529/playbook
|
|
|
14
46
|
npx @punk6529/playbook --help
|
|
15
47
|
```
|
|
16
48
|
|
|
17
|
-
##
|
|
49
|
+
## 用户命令
|
|
18
50
|
|
|
19
|
-
|
|
20
|
-
playbook init [path] [--tools all|none|codex,claude] [--force]
|
|
21
|
-
playbook global init [--force]
|
|
22
|
-
playbook update [path] [--tools all|none|codex,claude] [--force]
|
|
23
|
-
playbook query [tags...] [--limit N] [--scope project|global|both] [--json] [--tags-only]
|
|
24
|
-
```
|
|
51
|
+
日常使用的命令:
|
|
25
52
|
|
|
26
|
-
|
|
53
|
+
### `playbook init`
|
|
27
54
|
|
|
28
|
-
|
|
29
|
-
- 创建 `docs/playbook/{cases,patterns,checklists}` 和 `docs/playbook/INDEX.json`
|
|
30
|
-
- 根据 `--tools` 参数写入 AI skill 模板:
|
|
31
|
-
- Codex: `.codex/skills/{playbook-query,playbook-case,playbook-advisor}/SKILL.md`
|
|
32
|
-
- Claude: `.claude/skills/{playbook-query,playbook-case,playbook-advisor}/SKILL.md`
|
|
33
|
-
- `playbook global init` 全局级:
|
|
34
|
-
- 创建 `~/.playbook/repo/{cases,patterns,checklists}`
|
|
35
|
-
- 创建 `~/.playbook/repo/INDEX.json` 和 `~/.playbook/repo/tags.yml`
|
|
36
|
-
- 不支持 `--tools`
|
|
37
|
-
- `playbook update` 项目级:
|
|
38
|
-
- 仅更新托管模板文件
|
|
39
|
-
- 不修改用户业务内容(如 case markdown 文件)
|
|
40
|
-
- V1 暂不支持 `playbook global update`
|
|
55
|
+
在项目中初始化知识库。新项目首次使用时运行一次。
|
|
41
56
|
|
|
42
|
-
|
|
57
|
+
```bash
|
|
58
|
+
playbook init # 当前目录,所有 AI 工具
|
|
59
|
+
playbook init ./my-project # 指定目录
|
|
60
|
+
playbook init --tools claude # 仅安装 Claude skill
|
|
61
|
+
playbook init --tools none # 不安装任何 AI skill
|
|
62
|
+
```
|
|
43
63
|
|
|
44
|
-
|
|
64
|
+
创建 `docs/playbook/{cases,patterns,checklists}`、`INDEX.json`,以及 AI skill 模板。
|
|
45
65
|
|
|
46
|
-
|
|
47
|
-
|-------|------|
|
|
48
|
-
| `playbook-query` | 手动查询知识库 |
|
|
49
|
-
| `playbook-case` | 引导式 case 创建 |
|
|
50
|
-
| `playbook-advisor` | 主动知识应用(自动感知 tag,AI 自主查询) |
|
|
66
|
+
### `playbook global init`
|
|
51
67
|
|
|
52
|
-
|
|
68
|
+
初始化全局知识库(跨项目共享的经验)。只需运行一次。
|
|
53
69
|
|
|
54
|
-
|
|
70
|
+
```bash
|
|
71
|
+
playbook global init
|
|
72
|
+
```
|
|
55
73
|
|
|
56
|
-
|
|
57
|
-
- 有冲突的托管文件会跳过并报告为 `conflict`
|
|
58
|
-
- `playbook update` 采用分级策略:
|
|
59
|
-
- skill 模板:内容不同时自动刷新
|
|
60
|
-
- `docs/playbook/INDEX.json`:保护不覆盖,除非使用 `--force`
|
|
61
|
-
- 使用 `--force` 可强制覆盖受保护的托管文件
|
|
74
|
+
创建 `~/.playbook/repo/{cases,patterns,checklists}`、`INDEX.json` 和 `tags.yml`。
|
|
62
75
|
|
|
63
|
-
|
|
76
|
+
### `playbook update`
|
|
64
77
|
|
|
65
|
-
|
|
78
|
+
升级 playbook CLI 版本后,更新项目中的 skill 模板到最新版。
|
|
66
79
|
|
|
67
80
|
```bash
|
|
68
|
-
playbook
|
|
69
|
-
playbook
|
|
70
|
-
playbook query --tags-only # 仅输出 tag 概览:docker(3) env(5) ci(2)
|
|
71
|
-
playbook query --scope project # 仅项目范围
|
|
72
|
-
playbook query --limit 0 # 不限数量
|
|
73
|
-
playbook query docker --json # JSON 格式输出
|
|
81
|
+
playbook update # 更新当前项目
|
|
82
|
+
playbook update --force # 强制覆盖(含 INDEX.json)
|
|
74
83
|
```
|
|
75
84
|
|
|
85
|
+
仅刷新 skill 模板,不修改用户业务内容(case/pattern/checklist 文件)。`INDEX.json` 默认受保护,需 `--force` 才会覆盖。
|
|
86
|
+
|
|
76
87
|
## 在 AI 中使用
|
|
77
88
|
|
|
78
89
|
`playbook init` 之后,AI 工具中有三个 skill 可用:
|
|
@@ -123,6 +134,25 @@ AI:(引导你填写 问题/原因/解决/教训 四个部分)
|
|
|
123
134
|
AI:(生成 case 文件,更新 INDEX.json)
|
|
124
135
|
```
|
|
125
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
|
+
|
|
126
156
|
## 错误处理
|
|
127
157
|
|
|
128
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.
|