beeops 0.1.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/LICENSE +21 -0
- package/README.ja.md +156 -0
- package/README.md +80 -0
- package/bin/beeops.js +502 -0
- package/command/bo.md +120 -0
- package/contexts/agent-modes.json +100 -0
- package/contexts/code-reviewer.md +118 -0
- package/contexts/coder.md +247 -0
- package/contexts/default.md +1 -0
- package/contexts/en/agent-modes.json +100 -0
- package/contexts/en/code-reviewer.md +129 -0
- package/contexts/en/coder.md +247 -0
- package/contexts/en/default.md +1 -0
- package/contexts/en/fb.md +15 -0
- package/contexts/en/leader.md +158 -0
- package/contexts/en/log.md +16 -0
- package/contexts/en/queen.md +240 -0
- package/contexts/en/review-leader.md +190 -0
- package/contexts/en/reviewer-base.md +27 -0
- package/contexts/en/security-reviewer.md +200 -0
- package/contexts/en/test-auditor.md +146 -0
- package/contexts/en/tester.md +135 -0
- package/contexts/en/worker-base.md +69 -0
- package/contexts/fb.md +15 -0
- package/contexts/ja/agent-modes.json +100 -0
- package/contexts/ja/code-reviewer.md +129 -0
- package/contexts/ja/coder.md +247 -0
- package/contexts/ja/default.md +1 -0
- package/contexts/ja/fb.md +15 -0
- package/contexts/ja/leader.md +158 -0
- package/contexts/ja/log.md +17 -0
- package/contexts/ja/queen.md +240 -0
- package/contexts/ja/review-leader.md +190 -0
- package/contexts/ja/reviewer-base.md +27 -0
- package/contexts/ja/security-reviewer.md +200 -0
- package/contexts/ja/test-auditor.md +146 -0
- package/contexts/ja/tester.md +135 -0
- package/contexts/ja/worker-base.md +68 -0
- package/contexts/leader.md +158 -0
- package/contexts/log.md +16 -0
- package/contexts/queen.md +240 -0
- package/contexts/review-leader.md +190 -0
- package/contexts/reviewer-base.md +27 -0
- package/contexts/security-reviewer.md +200 -0
- package/contexts/test-auditor.md +146 -0
- package/contexts/tester.md +135 -0
- package/contexts/worker-base.md +69 -0
- package/hooks/checkpoint.py +89 -0
- package/hooks/prompt-context.py +139 -0
- package/hooks/resolve-log-path.py +93 -0
- package/hooks/run-log.py +429 -0
- package/package.json +42 -0
- package/scripts/launch-leader.sh +282 -0
- package/scripts/launch-worker.sh +184 -0
- package/skills/bo-dispatch/SKILL.md +299 -0
- package/skills/bo-issue-sync/SKILL.md +103 -0
- package/skills/bo-leader-dispatch/SKILL.md +211 -0
- package/skills/bo-log-writer/SKILL.md +101 -0
- package/skills/bo-review-backend/SKILL.md +234 -0
- package/skills/bo-review-database/SKILL.md +243 -0
- package/skills/bo-review-frontend/SKILL.md +236 -0
- package/skills/bo-review-operations/SKILL.md +268 -0
- package/skills/bo-review-process/SKILL.md +181 -0
- package/skills/bo-review-security/SKILL.md +214 -0
- package/skills/bo-review-security/references/finance-security.md +351 -0
- package/skills/bo-self-improver/SKILL.md +145 -0
- package/skills/bo-self-improver/refs/agent-manager.md +61 -0
- package/skills/bo-self-improver/refs/command-manager.md +46 -0
- package/skills/bo-self-improver/refs/skill-manager.md +59 -0
- package/skills/bo-self-improver/scripts/analyze.py +359 -0
- package/skills/bo-task-decomposer/SKILL.md +130 -0
package/bin/beeops.js
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { execSync } = require("child_process");
|
|
7
|
+
|
|
8
|
+
const PKG_DIR = path.resolve(__dirname, "..");
|
|
9
|
+
const COMMAND_SRC = path.join(PKG_DIR, "command", "bo.md");
|
|
10
|
+
const SKILLS_SRC = path.join(PKG_DIR, "skills");
|
|
11
|
+
const CONTEXTS_SRC = path.join(PKG_DIR, "contexts");
|
|
12
|
+
const HOOKS_DIR = path.join(PKG_DIR, "hooks");
|
|
13
|
+
const HOOK_SRC = path.join(HOOKS_DIR, "prompt-context.py");
|
|
14
|
+
const HOOK_STOP_SRC = path.join(HOOKS_DIR, "run-log.py");
|
|
15
|
+
const HOOK_POST_SRC = path.join(HOOKS_DIR, "checkpoint.py");
|
|
16
|
+
const HOME_DIR = process.env.HOME || process.env.USERPROFILE;
|
|
17
|
+
|
|
18
|
+
// ── Helpers ──
|
|
19
|
+
|
|
20
|
+
function getProjectRoot() {
|
|
21
|
+
try {
|
|
22
|
+
return execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
|
|
23
|
+
} catch {
|
|
24
|
+
console.error("Error: Not inside a git repository.");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ensureDir(dir) {
|
|
30
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function copyFile(src, dest) {
|
|
34
|
+
ensureDir(path.dirname(dest));
|
|
35
|
+
fs.copyFileSync(src, dest);
|
|
36
|
+
console.log(` copied: ${path.relative(process.cwd(), dest)}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function copyDir(src, dest) {
|
|
40
|
+
ensureDir(dest);
|
|
41
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
42
|
+
const srcPath = path.join(src, entry.name);
|
|
43
|
+
const destPath = path.join(dest, entry.name);
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
copyDir(srcPath, destPath);
|
|
46
|
+
} else {
|
|
47
|
+
copyFile(srcPath, destPath);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function commandExists(cmd) {
|
|
53
|
+
try {
|
|
54
|
+
execSync(`command -v ${cmd}`, { encoding: "utf8", stdio: "pipe" });
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getCommandVersion(cmd, versionFlag) {
|
|
62
|
+
try {
|
|
63
|
+
return execSync(`${cmd} ${versionFlag || "--version"}`, {
|
|
64
|
+
encoding: "utf8",
|
|
65
|
+
stdio: "pipe",
|
|
66
|
+
}).trim().split("\n")[0];
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Prerequisites check ──
|
|
73
|
+
|
|
74
|
+
function checkPrerequisites() {
|
|
75
|
+
console.log("Checking prerequisites...\n");
|
|
76
|
+
let allOk = true;
|
|
77
|
+
const warnings = [];
|
|
78
|
+
|
|
79
|
+
const checks = [
|
|
80
|
+
{ cmd: "node", required: true, label: "Node.js", versionFlag: "--version" },
|
|
81
|
+
{ cmd: "git", required: true, label: "git", versionFlag: "--version" },
|
|
82
|
+
{ cmd: "tmux", required: true, label: "tmux", versionFlag: "-V" },
|
|
83
|
+
{ cmd: "python3", required: true, label: "python3", versionFlag: "--version" },
|
|
84
|
+
{ cmd: "claude", required: false, label: "Claude CLI", versionFlag: "--version" },
|
|
85
|
+
{ cmd: "gh", required: false, label: "GitHub CLI (gh)", versionFlag: "--version" },
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
for (const { cmd, required, label, versionFlag } of checks) {
|
|
89
|
+
if (commandExists(cmd)) {
|
|
90
|
+
const ver = getCommandVersion(cmd, versionFlag) || "";
|
|
91
|
+
console.log(` [ok] ${label} (${ver})`);
|
|
92
|
+
} else if (required) {
|
|
93
|
+
console.log(` [MISSING] ${label} — required`);
|
|
94
|
+
allOk = false;
|
|
95
|
+
} else {
|
|
96
|
+
console.log(` [warn] ${label} — not found (optional)`);
|
|
97
|
+
warnings.push(label);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Node.js version check
|
|
102
|
+
const nodeVersion = parseInt(process.versions.node.split(".")[0], 10);
|
|
103
|
+
if (nodeVersion < 18) {
|
|
104
|
+
console.log(` [MISSING] Node.js >= 18 required (current: ${process.versions.node})`);
|
|
105
|
+
allOk = false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log("");
|
|
109
|
+
|
|
110
|
+
if (!allOk) {
|
|
111
|
+
console.error("Some required prerequisites are missing. Install them and try again.");
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (warnings.length > 0) {
|
|
116
|
+
console.log(` Note: ${warnings.join(", ")} not found. Some features may not work.\n`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return allOk;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Hook registration ──
|
|
123
|
+
|
|
124
|
+
function resolveSettingsFile(root, mode) {
|
|
125
|
+
switch (mode) {
|
|
126
|
+
case "global":
|
|
127
|
+
return path.join(HOME_DIR, ".claude", "settings.json");
|
|
128
|
+
case "shared":
|
|
129
|
+
return path.join(root, ".claude", "settings.json");
|
|
130
|
+
case "local":
|
|
131
|
+
default:
|
|
132
|
+
return path.join(root, ".claude", "settings.local.json");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function settingsModeLabel(mode) {
|
|
137
|
+
switch (mode) {
|
|
138
|
+
case "global": return "~/.claude/settings.json";
|
|
139
|
+
case "shared": return ".claude/settings.json";
|
|
140
|
+
case "local":
|
|
141
|
+
default: return ".claude/settings.local.json";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function upsertHook(hooks, hookType, matchStr, command, matcher) {
|
|
146
|
+
if (!hooks[hookType]) hooks[hookType] = [];
|
|
147
|
+
|
|
148
|
+
const existingIdx = hooks[hookType].findIndex((entry) => {
|
|
149
|
+
// Check new format
|
|
150
|
+
if (entry.hooks) {
|
|
151
|
+
return entry.hooks.some((h) => h.command && h.command.includes(matchStr));
|
|
152
|
+
}
|
|
153
|
+
// Check old format (for migration)
|
|
154
|
+
return entry.type === "command" && entry.command && entry.command.includes(matchStr);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const hookEntry = { hooks: [{ type: "command", command }] };
|
|
158
|
+
if (matcher) hookEntry.matcher = matcher;
|
|
159
|
+
|
|
160
|
+
if (existingIdx >= 0) {
|
|
161
|
+
hooks[hookType][existingIdx] = hookEntry;
|
|
162
|
+
return "updated";
|
|
163
|
+
} else {
|
|
164
|
+
hooks[hookType].push(hookEntry);
|
|
165
|
+
return "added";
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function updateSettingsHook(root, mode) {
|
|
170
|
+
const settingsFile = resolveSettingsFile(root, mode);
|
|
171
|
+
ensureDir(path.dirname(settingsFile));
|
|
172
|
+
|
|
173
|
+
let settings = {};
|
|
174
|
+
if (fs.existsSync(settingsFile)) {
|
|
175
|
+
try {
|
|
176
|
+
settings = JSON.parse(fs.readFileSync(settingsFile, "utf8"));
|
|
177
|
+
} catch {
|
|
178
|
+
console.warn(` warn: Could not parse ${path.basename(settingsFile)}, creating new one`);
|
|
179
|
+
settings = {};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!settings.hooks) settings.hooks = {};
|
|
184
|
+
|
|
185
|
+
const label = settingsModeLabel(mode);
|
|
186
|
+
|
|
187
|
+
// UserPromptSubmit: prompt-context.py
|
|
188
|
+
const r1 = upsertHook(settings.hooks, "UserPromptSubmit", "prompt-context.py", `python3 ${HOOK_SRC}`);
|
|
189
|
+
console.log(` ${r1}: ${label} (UserPromptSubmit hook)`);
|
|
190
|
+
|
|
191
|
+
// Stop: run-log.py
|
|
192
|
+
const r2 = upsertHook(settings.hooks, "Stop", "run-log.py", `python3 ${HOOK_STOP_SRC}`);
|
|
193
|
+
console.log(` ${r2}: ${label} (Stop hook)`);
|
|
194
|
+
|
|
195
|
+
// PostToolUse: checkpoint.py
|
|
196
|
+
const r3 = upsertHook(settings.hooks, "PostToolUse", "checkpoint.py", `python3 ${HOOK_POST_SRC}`);
|
|
197
|
+
console.log(` ${r3}: ${label} (PostToolUse hook)`);
|
|
198
|
+
|
|
199
|
+
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + "\n");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── init ──
|
|
203
|
+
|
|
204
|
+
function init(opts) {
|
|
205
|
+
checkPrerequisites();
|
|
206
|
+
|
|
207
|
+
const root = getProjectRoot();
|
|
208
|
+
const claudeDir = path.join(root, ".claude");
|
|
209
|
+
|
|
210
|
+
console.log(`Initializing beeops in ${root}...\n`);
|
|
211
|
+
|
|
212
|
+
// 1. Copy command
|
|
213
|
+
copyFile(COMMAND_SRC, path.join(claudeDir, "commands", "bo.md"));
|
|
214
|
+
|
|
215
|
+
// 2. Copy skills
|
|
216
|
+
copyDir(
|
|
217
|
+
path.join(SKILLS_SRC, "bo-dispatch"),
|
|
218
|
+
path.join(claudeDir, "skills", "bo-dispatch")
|
|
219
|
+
);
|
|
220
|
+
copyDir(
|
|
221
|
+
path.join(SKILLS_SRC, "bo-leader-dispatch"),
|
|
222
|
+
path.join(claudeDir, "skills", "bo-leader-dispatch")
|
|
223
|
+
);
|
|
224
|
+
copyDir(
|
|
225
|
+
path.join(SKILLS_SRC, "bo-task-decomposer"),
|
|
226
|
+
path.join(claudeDir, "skills", "bo-task-decomposer")
|
|
227
|
+
);
|
|
228
|
+
copyDir(
|
|
229
|
+
path.join(SKILLS_SRC, "bo-issue-sync"),
|
|
230
|
+
path.join(claudeDir, "skills", "bo-issue-sync")
|
|
231
|
+
);
|
|
232
|
+
copyDir(
|
|
233
|
+
path.join(SKILLS_SRC, "bo-log-writer"),
|
|
234
|
+
path.join(claudeDir, "skills", "bo-log-writer")
|
|
235
|
+
);
|
|
236
|
+
copyDir(
|
|
237
|
+
path.join(SKILLS_SRC, "bo-self-improver"),
|
|
238
|
+
path.join(claudeDir, "skills", "bo-self-improver")
|
|
239
|
+
);
|
|
240
|
+
copyDir(
|
|
241
|
+
path.join(SKILLS_SRC, "bo-review-backend"),
|
|
242
|
+
path.join(claudeDir, "skills", "bo-review-backend")
|
|
243
|
+
);
|
|
244
|
+
copyDir(
|
|
245
|
+
path.join(SKILLS_SRC, "bo-review-frontend"),
|
|
246
|
+
path.join(claudeDir, "skills", "bo-review-frontend")
|
|
247
|
+
);
|
|
248
|
+
copyDir(
|
|
249
|
+
path.join(SKILLS_SRC, "bo-review-database"),
|
|
250
|
+
path.join(claudeDir, "skills", "bo-review-database")
|
|
251
|
+
);
|
|
252
|
+
copyDir(
|
|
253
|
+
path.join(SKILLS_SRC, "bo-review-operations"),
|
|
254
|
+
path.join(claudeDir, "skills", "bo-review-operations")
|
|
255
|
+
);
|
|
256
|
+
copyDir(
|
|
257
|
+
path.join(SKILLS_SRC, "bo-review-process"),
|
|
258
|
+
path.join(claudeDir, "skills", "bo-review-process")
|
|
259
|
+
);
|
|
260
|
+
copyDir(
|
|
261
|
+
path.join(SKILLS_SRC, "bo-review-security"),
|
|
262
|
+
path.join(claudeDir, "skills", "bo-review-security")
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// 3. Clean up old names if exists
|
|
266
|
+
for (const old of [
|
|
267
|
+
"leader-dispatch", "ants-leader-dispatch", "ants-dispatch",
|
|
268
|
+
"meta-task-decomposer", "orch-issue-sync", "meta-log-writer", "meta-self-improver",
|
|
269
|
+
"review-backend", "review-frontend", "review-database",
|
|
270
|
+
"review-operations", "review-process", "review-security",
|
|
271
|
+
]) {
|
|
272
|
+
const oldDir = path.join(claudeDir, "skills", old);
|
|
273
|
+
if (fs.existsSync(oldDir)) {
|
|
274
|
+
fs.rmSync(oldDir, { recursive: true });
|
|
275
|
+
console.log(` removed: .claude/skills/${old}/ (migrated to bo-*)`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Old command file
|
|
279
|
+
const oldCmd = path.join(claudeDir, "commands", "ants.md");
|
|
280
|
+
if (fs.existsSync(oldCmd)) {
|
|
281
|
+
fs.unlinkSync(oldCmd);
|
|
282
|
+
console.log(" removed: .claude/commands/ants.md (migrated to bo.md)");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 4. Register hooks (UserPromptSubmit + Stop + PostToolUse)
|
|
286
|
+
updateSettingsHook(root, opts.hookMode);
|
|
287
|
+
|
|
288
|
+
// 5. Save locale preference
|
|
289
|
+
const boDir = path.join(claudeDir, "beeops");
|
|
290
|
+
ensureDir(boDir);
|
|
291
|
+
fs.writeFileSync(path.join(boDir, "locale"), opts.locale + "\n");
|
|
292
|
+
console.log(` locale: ${opts.locale} (saved to .claude/beeops/locale)`);
|
|
293
|
+
|
|
294
|
+
// 6. Copy contexts if --with-contexts
|
|
295
|
+
if (opts.withContexts) {
|
|
296
|
+
const destContexts = path.join(claudeDir, "beeops", "contexts");
|
|
297
|
+
copyDir(CONTEXTS_SRC, destContexts);
|
|
298
|
+
console.log("\n Contexts copied to .claude/beeops/contexts/ for customization.");
|
|
299
|
+
console.log(" Edit these files to customize agent behavior.");
|
|
300
|
+
console.log(" Delete a file to fall back to the package default.");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
console.log("\nbeeops initialized successfully!");
|
|
304
|
+
console.log("Run /bo in Claude Code to start the Queen.");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ── check ──
|
|
308
|
+
|
|
309
|
+
function findHookInSettings(settingsFile, hookType, matchStr) {
|
|
310
|
+
if (!fs.existsSync(settingsFile)) return null;
|
|
311
|
+
try {
|
|
312
|
+
const settings = JSON.parse(fs.readFileSync(settingsFile, "utf8"));
|
|
313
|
+
const entries = settings.hooks?.[hookType] || [];
|
|
314
|
+
const found = entries.find((entry) => {
|
|
315
|
+
// New format: { matcher, hooks: [{ type, command }] }
|
|
316
|
+
if (entry.hooks) {
|
|
317
|
+
return entry.hooks.some((h) => h.command && h.command.includes(matchStr));
|
|
318
|
+
}
|
|
319
|
+
// Old format: { type, command }
|
|
320
|
+
return entry.command && entry.command.includes(matchStr);
|
|
321
|
+
});
|
|
322
|
+
return found || null;
|
|
323
|
+
} catch {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function check() {
|
|
329
|
+
const root = getProjectRoot();
|
|
330
|
+
const claudeDir = path.join(root, ".claude");
|
|
331
|
+
let ok = true;
|
|
332
|
+
|
|
333
|
+
console.log("Checking beeops setup...\n");
|
|
334
|
+
|
|
335
|
+
// Check command
|
|
336
|
+
const cmdPath = path.join(claudeDir, "commands", "bo.md");
|
|
337
|
+
if (fs.existsSync(cmdPath)) {
|
|
338
|
+
console.log(" [ok] .claude/commands/bo.md");
|
|
339
|
+
} else {
|
|
340
|
+
console.log(" [missing] .claude/commands/bo.md");
|
|
341
|
+
ok = false;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Check skills
|
|
345
|
+
const CORE_SKILLS = [
|
|
346
|
+
"bo-dispatch", "bo-leader-dispatch", "bo-task-decomposer", "bo-issue-sync",
|
|
347
|
+
"bo-log-writer", "bo-self-improver",
|
|
348
|
+
"bo-review-backend", "bo-review-frontend", "bo-review-database",
|
|
349
|
+
"bo-review-operations", "bo-review-process", "bo-review-security",
|
|
350
|
+
];
|
|
351
|
+
for (const skill of CORE_SKILLS) {
|
|
352
|
+
const skillPath = path.join(claudeDir, "skills", skill, "SKILL.md");
|
|
353
|
+
if (fs.existsSync(skillPath)) {
|
|
354
|
+
console.log(` [ok] .claude/skills/${skill}/SKILL.md`);
|
|
355
|
+
} else {
|
|
356
|
+
console.log(` [missing] .claude/skills/${skill}/SKILL.md`);
|
|
357
|
+
ok = false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Check hook across all 3 settings locations
|
|
362
|
+
const settingsLocations = [
|
|
363
|
+
{ file: path.join(claudeDir, "settings.local.json"), label: ".claude/settings.local.json" },
|
|
364
|
+
{ file: path.join(claudeDir, "settings.json"), label: ".claude/settings.json" },
|
|
365
|
+
{ file: path.join(HOME_DIR, ".claude", "settings.json"), label: "~/.claude/settings.json" },
|
|
366
|
+
];
|
|
367
|
+
|
|
368
|
+
const HOOK_CHECKS = [
|
|
369
|
+
{ hookType: "UserPromptSubmit", matchStr: "prompt-context.py", label: "UserPromptSubmit" },
|
|
370
|
+
{ hookType: "Stop", matchStr: "run-log.py", label: "Stop" },
|
|
371
|
+
{ hookType: "PostToolUse", matchStr: "checkpoint.py", label: "PostToolUse" },
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
for (const { hookType, matchStr, label: hookLabel } of HOOK_CHECKS) {
|
|
375
|
+
let hookFound = false;
|
|
376
|
+
for (const { file, label } of settingsLocations) {
|
|
377
|
+
const hook = findHookInSettings(file, hookType, matchStr);
|
|
378
|
+
if (hook) {
|
|
379
|
+
console.log(` [ok] ${label} (${hookLabel} hook)`);
|
|
380
|
+
hookFound = true;
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (!hookFound) {
|
|
385
|
+
if (hookType === "UserPromptSubmit") {
|
|
386
|
+
console.log(` [missing] ${hookLabel} hook not found`);
|
|
387
|
+
ok = false;
|
|
388
|
+
} else {
|
|
389
|
+
console.log(` [warn] ${hookLabel} hook not found (optional)`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Check local contexts (informational)
|
|
395
|
+
const localContexts = path.join(claudeDir, "beeops", "contexts");
|
|
396
|
+
if (fs.existsSync(localContexts)) {
|
|
397
|
+
const files = fs.readdirSync(localContexts);
|
|
398
|
+
console.log(` [info] .claude/beeops/contexts/ (${files.length} custom file(s))`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Check package resolvable
|
|
402
|
+
try {
|
|
403
|
+
require.resolve("beeops/package.json");
|
|
404
|
+
console.log(" [ok] beeops package resolvable");
|
|
405
|
+
} catch {
|
|
406
|
+
console.log(" [warn] beeops package not resolvable from cwd");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
console.log(ok ? "\nAll checks passed!" : "\nSome checks failed. Run: npx beeops init");
|
|
410
|
+
process.exit(ok ? 0 : 1);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ── Help / Version ──
|
|
414
|
+
|
|
415
|
+
function printHelp() {
|
|
416
|
+
console.log("beeops — 3-layer multi-agent orchestration for Claude Code\n");
|
|
417
|
+
console.log("Usage: beeops <command> [options]\n");
|
|
418
|
+
console.log("Commands:");
|
|
419
|
+
console.log(" init Install beeops into the current project");
|
|
420
|
+
console.log(" update Update beeops files (same as init)");
|
|
421
|
+
console.log(" check Verify beeops setup\n");
|
|
422
|
+
console.log("Options for init/update:");
|
|
423
|
+
console.log(" --local Register hook in .claude/settings.local.json (default)");
|
|
424
|
+
console.log(" --shared Register hook in .claude/settings.json (team-shared)");
|
|
425
|
+
console.log(" -g, --global Register hook in ~/.claude/settings.json (all projects)");
|
|
426
|
+
console.log(" --with-contexts Copy default contexts for customization");
|
|
427
|
+
console.log(" --locale <lang> Set locale (default: en, available: en, ja)\n");
|
|
428
|
+
console.log("Examples:");
|
|
429
|
+
console.log(" npx beeops init");
|
|
430
|
+
console.log(" npx beeops init --shared --locale ja");
|
|
431
|
+
console.log(" npx beeops init --with-contexts");
|
|
432
|
+
console.log(" npx beeops check");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function printVersion() {
|
|
436
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PKG_DIR, "package.json"), "utf8"));
|
|
437
|
+
console.log(`beeops v${pkg.version}`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ── Argument parsing ──
|
|
441
|
+
|
|
442
|
+
function parseArgs(argv) {
|
|
443
|
+
const args = argv.slice(2);
|
|
444
|
+
const opts = {
|
|
445
|
+
hookMode: "local",
|
|
446
|
+
withContexts: false,
|
|
447
|
+
locale: "en",
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// Check for help/version flags first (anywhere in args)
|
|
451
|
+
if (args.includes("-h") || args.includes("--help") || args.includes("help")) {
|
|
452
|
+
printHelp();
|
|
453
|
+
process.exit(0);
|
|
454
|
+
}
|
|
455
|
+
if (args.includes("-v") || args.includes("--version") || args.includes("version")) {
|
|
456
|
+
printVersion();
|
|
457
|
+
process.exit(0);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const command = args[0];
|
|
461
|
+
|
|
462
|
+
for (let i = 1; i < args.length; i++) {
|
|
463
|
+
switch (args[i]) {
|
|
464
|
+
case "--global":
|
|
465
|
+
case "-g":
|
|
466
|
+
opts.hookMode = "global";
|
|
467
|
+
break;
|
|
468
|
+
case "--shared":
|
|
469
|
+
opts.hookMode = "shared";
|
|
470
|
+
break;
|
|
471
|
+
case "--local":
|
|
472
|
+
opts.hookMode = "local";
|
|
473
|
+
break;
|
|
474
|
+
case "--with-contexts":
|
|
475
|
+
opts.withContexts = true;
|
|
476
|
+
break;
|
|
477
|
+
case "--locale":
|
|
478
|
+
if (args[i + 1]) {
|
|
479
|
+
opts.locale = args[++i];
|
|
480
|
+
}
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return { command, opts };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ── Main ──
|
|
489
|
+
const { command, opts } = parseArgs(process.argv);
|
|
490
|
+
|
|
491
|
+
switch (command) {
|
|
492
|
+
case "init":
|
|
493
|
+
case "update":
|
|
494
|
+
init(opts);
|
|
495
|
+
break;
|
|
496
|
+
case "check":
|
|
497
|
+
check();
|
|
498
|
+
break;
|
|
499
|
+
default:
|
|
500
|
+
printHelp();
|
|
501
|
+
process.exit(command ? 1 : 0);
|
|
502
|
+
}
|
package/command/bo.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Launch a Queen session (beeops) in tmux and auto-display it.
|
|
2
|
+
queue.yaml generation and management is handled entirely by the Queen inside tmux.
|
|
3
|
+
|
|
4
|
+
## Execution steps
|
|
5
|
+
|
|
6
|
+
### Step 0: Resolve package paths
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
PKG_DIR=$(node -e "console.log(require.resolve('beeops/package.json').replace('/package.json',''))")
|
|
10
|
+
BO_SCRIPTS_DIR="$PKG_DIR/scripts"
|
|
11
|
+
BO_CONTEXTS_DIR="$PKG_DIR/contexts"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### Step 1: Check for existing session
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
SESSION="bo"
|
|
18
|
+
|
|
19
|
+
tmux has-session -t "$SESSION" 2>/dev/null && {
|
|
20
|
+
echo "An existing bo session was found."
|
|
21
|
+
echo " tmux attach -t bo # monitor"
|
|
22
|
+
echo " tmux kill-session -t bo # stop and restart"
|
|
23
|
+
# Stop here. Let the user decide.
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
If an existing session is found, display instructions and **stop**. The user must explicitly kill the session before re-running.
|
|
28
|
+
|
|
29
|
+
### Step 2: Start tmux session
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
CWD=$(pwd)
|
|
33
|
+
|
|
34
|
+
tmux new-session -d -s "$SESSION" -n queen -c "$CWD" \
|
|
35
|
+
"unset CLAUDECODE; BO_QUEEN=1 BO_SCRIPTS_DIR='$BO_SCRIPTS_DIR' BO_CONTEXTS_DIR='$BO_CONTEXTS_DIR' claude --dangerously-skip-permissions; echo '--- Done (press Enter to close) ---'; read"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Step 2.5: Configure tmux pane display
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Show role name at top of each pane
|
|
42
|
+
tmux set-option -t "$SESSION" pane-border-status top
|
|
43
|
+
|
|
44
|
+
# Format: prefer @agent_label (not overridable by Claude Code), fallback to pane_title
|
|
45
|
+
tmux set-option -t "$SESSION" pane-border-format \
|
|
46
|
+
" #{?pane_active,#[bold],}#{?@agent_label,#{@agent_label},#{pane_title}}#[default] "
|
|
47
|
+
|
|
48
|
+
# Queen pane: gold border + title
|
|
49
|
+
tmux select-pane -t "$SESSION:queen.0" -T "👑 queen"
|
|
50
|
+
tmux set-option -p -t "$SESSION:queen.0" @agent_label "👑 queen" 2>/dev/null || true
|
|
51
|
+
tmux set-option -p -t "$SESSION:queen.0" allow-rename off 2>/dev/null || true
|
|
52
|
+
tmux set-option -p -t "$SESSION:queen.0" pane-border-style "fg=yellow" 2>/dev/null || true
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Step 3: Auto-attach to tmux session
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
case "$(uname -s)" in
|
|
59
|
+
Darwin)
|
|
60
|
+
osascript -e '
|
|
61
|
+
tell application "Terminal"
|
|
62
|
+
activate
|
|
63
|
+
do script "tmux attach -t bo"
|
|
64
|
+
end tell
|
|
65
|
+
' 2>/dev/null || echo "Open a new terminal and run: tmux attach -t bo"
|
|
66
|
+
;;
|
|
67
|
+
*)
|
|
68
|
+
echo "Queen session started. Attach with: tmux attach -t bo"
|
|
69
|
+
;;
|
|
70
|
+
esac
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
On macOS, auto-opens Terminal.app and attaches to the tmux session.
|
|
74
|
+
On other platforms, prints the attach command for the user.
|
|
75
|
+
|
|
76
|
+
### Step 4: Wait for startup
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
for i in $(seq 1 60); do
|
|
80
|
+
sleep 2
|
|
81
|
+
if tmux capture-pane -t "$SESSION:queen" -p 2>/dev/null | grep -q 'Claude Code'; then
|
|
82
|
+
break
|
|
83
|
+
fi
|
|
84
|
+
done
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Polls for up to 120 seconds. Startup is complete when the `Claude Code` banner appears.
|
|
88
|
+
|
|
89
|
+
### Step 5: Send initial prompt
|
|
90
|
+
|
|
91
|
+
If `$ARGUMENTS` is non-empty, pass the user's instruction directly. Otherwise, send a default instruction to sync issues.
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
if [ -n "$ARGUMENTS" ]; then
|
|
95
|
+
INSTRUCTION="$ARGUMENTS"
|
|
96
|
+
else
|
|
97
|
+
INSTRUCTION="Sync GitHub Issues to queue.yaml and complete all tasks."
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
tmux send-keys -t "$SESSION:queen" "$INSTRUCTION"
|
|
101
|
+
sleep 0.3
|
|
102
|
+
tmux send-keys -t "$SESSION:queen" Enter
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Step 6: Display status to user
|
|
106
|
+
|
|
107
|
+
After startup, display:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
Queen started (beeops). tmux session displayed.
|
|
111
|
+
queen window: main control loop
|
|
112
|
+
issue-{N}/review-{N}: Leader/Worker windows are added automatically
|
|
113
|
+
tmux kill-session -t bo # to stop
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Notes
|
|
117
|
+
|
|
118
|
+
- `$ARGUMENTS` contains the slash command arguments
|
|
119
|
+
- This command must be run in the **target project directory**
|
|
120
|
+
- queue.yaml generation and updates are managed by the Queen inside tmux
|