opencode-swarm-plugin 0.62.0 → 0.62.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/bin/swarm.ts +1166 -1
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/dist/index.js +245 -2
- package/claude-plugin/hooks/hooks.json +60 -0
- package/dist/bin/swarm.js +47102 -44774
- package/dist/marketplace/index.js +245 -2
- package/package.json +1 -1
package/bin/swarm.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* swarm setup - Interactive installer for all dependencies
|
|
9
9
|
* swarm doctor - Check dependency health with detailed status
|
|
10
10
|
* swarm init - Initialize swarm in current project
|
|
11
|
+
* swarm claude - Claude Code integration commands
|
|
12
|
+
* swarm mcp-serve - Debug-only MCP server (Claude auto-launches)
|
|
11
13
|
* swarm version - Show version info
|
|
12
14
|
* swarm - Interactive mode (same as setup)
|
|
13
15
|
*/
|
|
@@ -17,17 +19,20 @@ import {
|
|
|
17
19
|
chmodSync,
|
|
18
20
|
copyFileSync,
|
|
19
21
|
existsSync,
|
|
22
|
+
lstatSync,
|
|
20
23
|
mkdirSync,
|
|
21
24
|
readFileSync,
|
|
25
|
+
readlinkSync,
|
|
22
26
|
readdirSync,
|
|
23
27
|
renameSync,
|
|
24
28
|
rmdirSync,
|
|
25
29
|
rmSync,
|
|
26
30
|
statSync,
|
|
31
|
+
symlinkSync,
|
|
27
32
|
writeFileSync,
|
|
28
33
|
} from "fs";
|
|
29
34
|
import { homedir } from "os";
|
|
30
|
-
import { basename, dirname, join } from "path";
|
|
35
|
+
import { basename, dirname, join, resolve } from "path";
|
|
31
36
|
import { fileURLToPath } from "url";
|
|
32
37
|
import {
|
|
33
38
|
checkBeadsMigrationNeeded,
|
|
@@ -50,13 +55,17 @@ import {
|
|
|
50
55
|
resolvePartialId,
|
|
51
56
|
createDurableStreamAdapter,
|
|
52
57
|
createDurableStreamServer,
|
|
58
|
+
consolidateDatabases,
|
|
59
|
+
getGlobalDbPath,
|
|
53
60
|
} from "swarm-mail";
|
|
61
|
+
import { createMemoryAdapter } from "../src/memory";
|
|
54
62
|
import { execSync, spawn } from "child_process";
|
|
55
63
|
import { tmpdir } from "os";
|
|
56
64
|
|
|
57
65
|
// Query & observability tools
|
|
58
66
|
import {
|
|
59
67
|
executeQuery,
|
|
68
|
+
executeQueryCLI,
|
|
60
69
|
executePreset,
|
|
61
70
|
formatAsTable,
|
|
62
71
|
formatAsCSV,
|
|
@@ -101,11 +110,16 @@ import { detectRegressions } from "../src/regression-detection.js";
|
|
|
101
110
|
// All tools (for tool command)
|
|
102
111
|
import { allTools } from "../src/index.js";
|
|
103
112
|
|
|
113
|
+
// Skills (for skill-reload command)
|
|
114
|
+
import { invalidateSkillsCache, discoverSkills } from "../src/skills.js";
|
|
115
|
+
|
|
104
116
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
105
117
|
// When bundled to dist/bin/swarm.js, need to go up two levels to find package.json
|
|
106
118
|
const pkgPath = join(__dirname, "..", "..", "package.json");
|
|
107
119
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
108
120
|
const VERSION: string = pkg.version;
|
|
121
|
+
const PACKAGE_ROOT = dirname(pkgPath);
|
|
122
|
+
const CLAUDE_PLUGIN_NAME = "swarm";
|
|
109
123
|
|
|
110
124
|
// ============================================================================
|
|
111
125
|
// ASCII Art & Branding
|
|
@@ -202,6 +216,156 @@ function rmWithStatus(path: string, label: string): void {
|
|
|
202
216
|
}
|
|
203
217
|
}
|
|
204
218
|
|
|
219
|
+
// ============================================================================
|
|
220
|
+
// Claude Code Integration Types & Helpers
|
|
221
|
+
// ============================================================================
|
|
222
|
+
|
|
223
|
+
interface ClaudeHookInput {
|
|
224
|
+
project?: { path?: string };
|
|
225
|
+
cwd?: string;
|
|
226
|
+
session?: { id?: string };
|
|
227
|
+
metadata?: { cwd?: string };
|
|
228
|
+
prompt?: string; // UserPromptSubmit includes the user's prompt text
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Resolve the Claude Code plugin root bundled with the package.
|
|
233
|
+
*/
|
|
234
|
+
function getClaudePluginRoot(): string {
|
|
235
|
+
return join(PACKAGE_ROOT, "claude-plugin");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Resolve the Claude Code config directory.
|
|
240
|
+
*/
|
|
241
|
+
function getClaudeConfigDir(): string {
|
|
242
|
+
return join(homedir(), ".claude");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Read JSON input from stdin for Claude Code hooks.
|
|
247
|
+
*/
|
|
248
|
+
async function readHookInput<T>(): Promise<T | null> {
|
|
249
|
+
if (process.stdin.isTTY) return null;
|
|
250
|
+
const chunks: string[] = [];
|
|
251
|
+
for await (const chunk of process.stdin) {
|
|
252
|
+
chunks.push(chunk.toString());
|
|
253
|
+
}
|
|
254
|
+
const raw = chunks.join("").trim();
|
|
255
|
+
if (!raw) return null;
|
|
256
|
+
try {
|
|
257
|
+
return JSON.parse(raw) as T;
|
|
258
|
+
} catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Resolve the project path for Claude Code hook executions.
|
|
265
|
+
*/
|
|
266
|
+
function resolveClaudeProjectPath(input: ClaudeHookInput | null): string {
|
|
267
|
+
return (
|
|
268
|
+
input?.project?.path ||
|
|
269
|
+
input?.cwd ||
|
|
270
|
+
input?.metadata?.cwd ||
|
|
271
|
+
process.env.CLAUDE_PROJECT_DIR ||
|
|
272
|
+
process.env.PWD ||
|
|
273
|
+
process.cwd()
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Format hook output for Claude Code context injection.
|
|
279
|
+
* Uses the proper JSON format with hookSpecificOutput for structured feedback.
|
|
280
|
+
*/
|
|
281
|
+
function writeClaudeHookOutput(
|
|
282
|
+
hookEventName: string,
|
|
283
|
+
additionalContext: string,
|
|
284
|
+
options?: { suppressOutput?: boolean }
|
|
285
|
+
): void {
|
|
286
|
+
if (!additionalContext.trim()) return;
|
|
287
|
+
process.stdout.write(
|
|
288
|
+
`${JSON.stringify({
|
|
289
|
+
suppressOutput: options?.suppressOutput,
|
|
290
|
+
hookSpecificOutput: {
|
|
291
|
+
hookEventName,
|
|
292
|
+
additionalContext,
|
|
293
|
+
},
|
|
294
|
+
})}\n`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* @deprecated Use writeClaudeHookOutput for proper hook-specific JSON format
|
|
300
|
+
*/
|
|
301
|
+
function writeClaudeHookContext(additionalContext: string): void {
|
|
302
|
+
if (!additionalContext.trim()) return;
|
|
303
|
+
process.stdout.write(
|
|
304
|
+
`${JSON.stringify({
|
|
305
|
+
additionalContext,
|
|
306
|
+
})}\n`,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
interface ClaudeInstallStatus {
|
|
311
|
+
pluginRoot: string;
|
|
312
|
+
globalPluginPath: string;
|
|
313
|
+
globalPluginTarget?: string;
|
|
314
|
+
globalPluginExists: boolean;
|
|
315
|
+
globalPluginLinked: boolean;
|
|
316
|
+
projectClaudeDir: string;
|
|
317
|
+
projectConfigExists: boolean;
|
|
318
|
+
projectConfigPaths: string[];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Inspect Claude Code install state for global and project scopes.
|
|
323
|
+
*/
|
|
324
|
+
function getClaudeInstallStatus(projectPath: string): ClaudeInstallStatus {
|
|
325
|
+
const pluginRoot = getClaudePluginRoot();
|
|
326
|
+
const claudeConfigDir = getClaudeConfigDir();
|
|
327
|
+
const globalPluginPath = join(claudeConfigDir, "plugins", CLAUDE_PLUGIN_NAME);
|
|
328
|
+
|
|
329
|
+
let globalPluginExists = false;
|
|
330
|
+
let globalPluginLinked = false;
|
|
331
|
+
let globalPluginTarget: string | undefined;
|
|
332
|
+
|
|
333
|
+
if (existsSync(globalPluginPath)) {
|
|
334
|
+
globalPluginExists = true;
|
|
335
|
+
try {
|
|
336
|
+
const stat = lstatSync(globalPluginPath);
|
|
337
|
+
if (stat.isSymbolicLink()) {
|
|
338
|
+
globalPluginLinked = true;
|
|
339
|
+
const target = readlinkSync(globalPluginPath);
|
|
340
|
+
globalPluginTarget = resolve(dirname(globalPluginPath), target);
|
|
341
|
+
}
|
|
342
|
+
} catch {
|
|
343
|
+
// Ignore errors
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const projectClaudeDir = join(projectPath, ".claude");
|
|
348
|
+
const projectConfigPaths = [
|
|
349
|
+
join(projectClaudeDir, "commands"),
|
|
350
|
+
join(projectClaudeDir, "agents"),
|
|
351
|
+
join(projectClaudeDir, "skills"),
|
|
352
|
+
join(projectClaudeDir, "hooks"),
|
|
353
|
+
join(projectClaudeDir, ".mcp.json"),
|
|
354
|
+
];
|
|
355
|
+
const projectConfigExists = projectConfigPaths.some((path) => existsSync(path));
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
pluginRoot,
|
|
359
|
+
globalPluginPath,
|
|
360
|
+
globalPluginTarget,
|
|
361
|
+
globalPluginExists,
|
|
362
|
+
globalPluginLinked,
|
|
363
|
+
projectClaudeDir,
|
|
364
|
+
projectConfigExists,
|
|
365
|
+
projectConfigPaths,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
205
369
|
// ============================================================================
|
|
206
370
|
// Seasonal Messages (inspired by Astro's Houston)
|
|
207
371
|
// ============================================================================
|
|
@@ -6024,6 +6188,1004 @@ async function memory() {
|
|
|
6024
6188
|
}
|
|
6025
6189
|
}
|
|
6026
6190
|
|
|
6191
|
+
// ============================================================================
|
|
6192
|
+
// Claude Code Integration Commands
|
|
6193
|
+
// ============================================================================
|
|
6194
|
+
|
|
6195
|
+
/**
|
|
6196
|
+
* Claude subcommand dispatcher.
|
|
6197
|
+
*/
|
|
6198
|
+
async function claudeCommand() {
|
|
6199
|
+
const args = process.argv.slice(3);
|
|
6200
|
+
const subcommand = args[0];
|
|
6201
|
+
|
|
6202
|
+
if (!subcommand || ["help", "--help", "-h"].includes(subcommand)) {
|
|
6203
|
+
showClaudeHelp();
|
|
6204
|
+
return;
|
|
6205
|
+
}
|
|
6206
|
+
|
|
6207
|
+
switch (subcommand) {
|
|
6208
|
+
case "path":
|
|
6209
|
+
claudePath();
|
|
6210
|
+
break;
|
|
6211
|
+
case "install":
|
|
6212
|
+
await claudeInstall();
|
|
6213
|
+
break;
|
|
6214
|
+
case "uninstall":
|
|
6215
|
+
await claudeUninstall();
|
|
6216
|
+
break;
|
|
6217
|
+
case "init":
|
|
6218
|
+
await claudeInit();
|
|
6219
|
+
break;
|
|
6220
|
+
case "session-start":
|
|
6221
|
+
await claudeSessionStart();
|
|
6222
|
+
break;
|
|
6223
|
+
case "user-prompt":
|
|
6224
|
+
await claudeUserPrompt();
|
|
6225
|
+
break;
|
|
6226
|
+
case "pre-edit":
|
|
6227
|
+
await claudePreEdit();
|
|
6228
|
+
break;
|
|
6229
|
+
case "pre-complete":
|
|
6230
|
+
await claudePreComplete();
|
|
6231
|
+
break;
|
|
6232
|
+
case "post-complete":
|
|
6233
|
+
await claudePostComplete();
|
|
6234
|
+
break;
|
|
6235
|
+
case "pre-compact":
|
|
6236
|
+
await claudePreCompact();
|
|
6237
|
+
break;
|
|
6238
|
+
case "session-end":
|
|
6239
|
+
await claudeSessionEnd();
|
|
6240
|
+
break;
|
|
6241
|
+
case "track-tool":
|
|
6242
|
+
await claudeTrackTool(Bun.argv[4]); // tool name is 4th arg
|
|
6243
|
+
break;
|
|
6244
|
+
case "compliance":
|
|
6245
|
+
await claudeCompliance();
|
|
6246
|
+
break;
|
|
6247
|
+
case "skill-reload":
|
|
6248
|
+
await claudeSkillReload();
|
|
6249
|
+
break;
|
|
6250
|
+
case "coordinator-start":
|
|
6251
|
+
await claudeCoordinatorStart();
|
|
6252
|
+
break;
|
|
6253
|
+
case "worker-start":
|
|
6254
|
+
await claudeWorkerStart();
|
|
6255
|
+
break;
|
|
6256
|
+
case "subagent-stop":
|
|
6257
|
+
await claudeSubagentStop();
|
|
6258
|
+
break;
|
|
6259
|
+
case "agent-stop":
|
|
6260
|
+
await claudeAgentStop();
|
|
6261
|
+
break;
|
|
6262
|
+
case "track-task":
|
|
6263
|
+
await claudeTrackTask();
|
|
6264
|
+
break;
|
|
6265
|
+
case "post-task-update":
|
|
6266
|
+
await claudePostTaskUpdate();
|
|
6267
|
+
break;
|
|
6268
|
+
default:
|
|
6269
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
6270
|
+
showClaudeHelp();
|
|
6271
|
+
process.exit(1);
|
|
6272
|
+
}
|
|
6273
|
+
}
|
|
6274
|
+
|
|
6275
|
+
function showClaudeHelp() {
|
|
6276
|
+
console.log(`
|
|
6277
|
+
Usage: swarm claude <command>
|
|
6278
|
+
|
|
6279
|
+
Commands:
|
|
6280
|
+
path Print Claude plugin path (for --plugin-dir)
|
|
6281
|
+
install Symlink plugin into ~/.claude/plugins/${CLAUDE_PLUGIN_NAME}
|
|
6282
|
+
uninstall Remove Claude plugin symlink
|
|
6283
|
+
init Create project-local .claude/ config
|
|
6284
|
+
session-start Hook: session start context (JSON output)
|
|
6285
|
+
user-prompt Hook: prompt submit context (JSON output)
|
|
6286
|
+
pre-edit Hook: pre-Edit/Write reminder (hivemind check)
|
|
6287
|
+
pre-complete Hook: pre-swarm_complete checklist
|
|
6288
|
+
post-complete Hook: post-swarm_complete learnings reminder
|
|
6289
|
+
pre-compact Hook: pre-compaction handler
|
|
6290
|
+
session-end Hook: session cleanup
|
|
6291
|
+
track-tool Hook: track mandatory tool usage
|
|
6292
|
+
compliance Hook: check worker compliance
|
|
6293
|
+
skill-reload Hook: hot-reload skills after modification
|
|
6294
|
+
coordinator-start Hook: coordinator subagent start
|
|
6295
|
+
worker-start Hook: worker subagent start
|
|
6296
|
+
subagent-stop Hook: subagent stop cleanup
|
|
6297
|
+
agent-stop Hook: agent stop cleanup
|
|
6298
|
+
track-task Hook: track task events
|
|
6299
|
+
post-task-update Hook: post task update tracking
|
|
6300
|
+
`);
|
|
6301
|
+
}
|
|
6302
|
+
|
|
6303
|
+
/**
|
|
6304
|
+
* Print the bundled Claude plugin path.
|
|
6305
|
+
*/
|
|
6306
|
+
function claudePath() {
|
|
6307
|
+
const pluginRoot = getClaudePluginRoot();
|
|
6308
|
+
if (!existsSync(pluginRoot)) {
|
|
6309
|
+
console.error(`Claude plugin not found at ${pluginRoot}`);
|
|
6310
|
+
process.exit(1);
|
|
6311
|
+
}
|
|
6312
|
+
console.log(pluginRoot);
|
|
6313
|
+
}
|
|
6314
|
+
|
|
6315
|
+
/**
|
|
6316
|
+
* Install the Claude plugin symlink for local development.
|
|
6317
|
+
*/
|
|
6318
|
+
async function claudeInstall() {
|
|
6319
|
+
p.intro("swarm claude install");
|
|
6320
|
+
const pluginRoot = getClaudePluginRoot();
|
|
6321
|
+
if (!existsSync(pluginRoot)) {
|
|
6322
|
+
p.log.error(`Claude plugin not found at ${pluginRoot}`);
|
|
6323
|
+
p.outro("Aborted");
|
|
6324
|
+
process.exit(1);
|
|
6325
|
+
}
|
|
6326
|
+
|
|
6327
|
+
const claudeConfigDir = getClaudeConfigDir();
|
|
6328
|
+
const pluginsDir = join(claudeConfigDir, "plugins");
|
|
6329
|
+
const pluginPath = join(pluginsDir, CLAUDE_PLUGIN_NAME);
|
|
6330
|
+
|
|
6331
|
+
mkdirWithStatus(pluginsDir);
|
|
6332
|
+
|
|
6333
|
+
if (existsSync(pluginPath)) {
|
|
6334
|
+
const stat = lstatSync(pluginPath);
|
|
6335
|
+
if (!stat.isSymbolicLink()) {
|
|
6336
|
+
p.log.error(`Existing path is not a symlink: ${pluginPath}`);
|
|
6337
|
+
p.outro("Aborted");
|
|
6338
|
+
process.exit(1);
|
|
6339
|
+
}
|
|
6340
|
+
|
|
6341
|
+
const target = readlinkSync(pluginPath);
|
|
6342
|
+
const resolved = resolve(dirname(pluginPath), target);
|
|
6343
|
+
if (resolved === pluginRoot) {
|
|
6344
|
+
p.log.success("Claude plugin already linked");
|
|
6345
|
+
p.outro("Done");
|
|
6346
|
+
return;
|
|
6347
|
+
}
|
|
6348
|
+
|
|
6349
|
+
rmSync(pluginPath, { force: true });
|
|
6350
|
+
}
|
|
6351
|
+
|
|
6352
|
+
symlinkSync(pluginRoot, pluginPath);
|
|
6353
|
+
p.log.success(`Linked ${CLAUDE_PLUGIN_NAME} → ${pluginRoot}`);
|
|
6354
|
+
p.log.message(dim(" Claude Code will auto-launch MCP from .mcp.json"));
|
|
6355
|
+
p.outro("Claude plugin installed");
|
|
6356
|
+
}
|
|
6357
|
+
|
|
6358
|
+
/**
|
|
6359
|
+
* Remove the Claude plugin symlink.
|
|
6360
|
+
*/
|
|
6361
|
+
async function claudeUninstall() {
|
|
6362
|
+
p.intro("swarm claude uninstall");
|
|
6363
|
+
const pluginPath = join(getClaudeConfigDir(), "plugins", CLAUDE_PLUGIN_NAME);
|
|
6364
|
+
|
|
6365
|
+
if (!existsSync(pluginPath)) {
|
|
6366
|
+
p.log.warn("Claude plugin symlink not found");
|
|
6367
|
+
p.outro("Nothing to remove");
|
|
6368
|
+
return;
|
|
6369
|
+
}
|
|
6370
|
+
|
|
6371
|
+
rmSync(pluginPath, { recursive: true, force: true });
|
|
6372
|
+
p.log.success(`Removed ${pluginPath}`);
|
|
6373
|
+
p.outro("Claude plugin uninstalled");
|
|
6374
|
+
}
|
|
6375
|
+
|
|
6376
|
+
/**
|
|
6377
|
+
* Create project-local Claude Code config from bundled plugin assets.
|
|
6378
|
+
*/
|
|
6379
|
+
async function claudeInit() {
|
|
6380
|
+
p.intro("swarm claude init");
|
|
6381
|
+
const projectPath = process.cwd();
|
|
6382
|
+
const pluginRoot = getClaudePluginRoot();
|
|
6383
|
+
|
|
6384
|
+
if (!existsSync(pluginRoot)) {
|
|
6385
|
+
p.log.error(`Claude plugin not found at ${pluginRoot}`);
|
|
6386
|
+
p.outro("Aborted");
|
|
6387
|
+
process.exit(1);
|
|
6388
|
+
}
|
|
6389
|
+
|
|
6390
|
+
const projectClaudeDir = join(projectPath, ".claude");
|
|
6391
|
+
const commandDir = join(projectClaudeDir, "commands");
|
|
6392
|
+
const agentDir = join(projectClaudeDir, "agents");
|
|
6393
|
+
const skillsDir = join(projectClaudeDir, "skills");
|
|
6394
|
+
const hooksDir = join(projectClaudeDir, "hooks");
|
|
6395
|
+
|
|
6396
|
+
for (const dir of [projectClaudeDir, commandDir, agentDir, skillsDir, hooksDir]) {
|
|
6397
|
+
mkdirWithStatus(dir);
|
|
6398
|
+
}
|
|
6399
|
+
|
|
6400
|
+
const copyMap = [
|
|
6401
|
+
{ src: join(pluginRoot, "commands"), dest: commandDir, label: "Commands" },
|
|
6402
|
+
{ src: join(pluginRoot, "agents"), dest: agentDir, label: "Agents" },
|
|
6403
|
+
{ src: join(pluginRoot, "skills"), dest: skillsDir, label: "Skills" },
|
|
6404
|
+
{ src: join(pluginRoot, "hooks"), dest: hooksDir, label: "Hooks" },
|
|
6405
|
+
];
|
|
6406
|
+
|
|
6407
|
+
for (const { src, dest, label } of copyMap) {
|
|
6408
|
+
if (existsSync(src)) {
|
|
6409
|
+
copyDirRecursiveSync(src, dest);
|
|
6410
|
+
p.log.success(`${label}: ${dest}`);
|
|
6411
|
+
}
|
|
6412
|
+
}
|
|
6413
|
+
|
|
6414
|
+
const mcpSourcePath = join(pluginRoot, ".mcp.json");
|
|
6415
|
+
if (existsSync(mcpSourcePath)) {
|
|
6416
|
+
const mcpDestPath = join(projectClaudeDir, ".mcp.json");
|
|
6417
|
+
const content = readFileSync(mcpSourcePath, "utf-8");
|
|
6418
|
+
writeFileWithStatus(mcpDestPath, content, "MCP config");
|
|
6419
|
+
}
|
|
6420
|
+
|
|
6421
|
+
const lspSourcePath = join(pluginRoot, ".lsp.json");
|
|
6422
|
+
if (existsSync(lspSourcePath)) {
|
|
6423
|
+
const lspDestPath = join(projectClaudeDir, ".lsp.json");
|
|
6424
|
+
const content = readFileSync(lspSourcePath, "utf-8");
|
|
6425
|
+
writeFileWithStatus(lspDestPath, content, "LSP config");
|
|
6426
|
+
}
|
|
6427
|
+
|
|
6428
|
+
p.log.message(dim(" Uses ${CLAUDE_PLUGIN_ROOT} in MCP config"));
|
|
6429
|
+
p.outro("Claude project config ready");
|
|
6430
|
+
}
|
|
6431
|
+
|
|
6432
|
+
/**
|
|
6433
|
+
* Claude hook: start a session and emit comprehensive context for Claude Code.
|
|
6434
|
+
*
|
|
6435
|
+
* Gathers:
|
|
6436
|
+
* - Session info + previous handoff notes
|
|
6437
|
+
* - In-progress cells (work that was mid-flight)
|
|
6438
|
+
* - Open epics with their children
|
|
6439
|
+
* - Recent activity summary
|
|
6440
|
+
*/
|
|
6441
|
+
async function claudeSessionStart() {
|
|
6442
|
+
try {
|
|
6443
|
+
const input = await readHookInput<ClaudeHookInput>();
|
|
6444
|
+
const projectPath = resolveClaudeProjectPath(input);
|
|
6445
|
+
const swarmMail = await getSwarmMailLibSQL(projectPath);
|
|
6446
|
+
const db = await swarmMail.getDatabase(projectPath);
|
|
6447
|
+
const adapter = createHiveAdapter(db, projectPath);
|
|
6448
|
+
|
|
6449
|
+
await adapter.runMigrations();
|
|
6450
|
+
|
|
6451
|
+
const session = await adapter.startSession(projectPath, {});
|
|
6452
|
+
const contextLines: string[] = [];
|
|
6453
|
+
|
|
6454
|
+
// Session basics
|
|
6455
|
+
contextLines.push(`## Swarm Session: ${session.id}`);
|
|
6456
|
+
contextLines.push(`Source: ${(input as { source?: string }).source || "startup"}`);
|
|
6457
|
+
contextLines.push("");
|
|
6458
|
+
|
|
6459
|
+
// Previous handoff notes (critical for continuation)
|
|
6460
|
+
if (session.previous_handoff_notes) {
|
|
6461
|
+
contextLines.push("## Previous Handoff Notes");
|
|
6462
|
+
contextLines.push(session.previous_handoff_notes);
|
|
6463
|
+
contextLines.push("");
|
|
6464
|
+
}
|
|
6465
|
+
|
|
6466
|
+
// Active cell from previous session
|
|
6467
|
+
if (session.active_cell_id) {
|
|
6468
|
+
const activeCell = await adapter.getCell(projectPath, session.active_cell_id);
|
|
6469
|
+
if (activeCell) {
|
|
6470
|
+
contextLines.push("## Active Cell (from previous session)");
|
|
6471
|
+
contextLines.push(`- **${activeCell.id}**: ${activeCell.title}`);
|
|
6472
|
+
contextLines.push(` Status: ${activeCell.status}, Priority: ${activeCell.priority}`);
|
|
6473
|
+
if (activeCell.description) {
|
|
6474
|
+
contextLines.push(` ${activeCell.description.slice(0, 200)}...`);
|
|
6475
|
+
}
|
|
6476
|
+
contextLines.push("");
|
|
6477
|
+
}
|
|
6478
|
+
}
|
|
6479
|
+
|
|
6480
|
+
// In-progress cells (work that was mid-flight)
|
|
6481
|
+
const inProgressCells = await adapter.getInProgressCells(projectPath);
|
|
6482
|
+
if (inProgressCells.length > 0) {
|
|
6483
|
+
contextLines.push("## In-Progress Work");
|
|
6484
|
+
for (const cell of inProgressCells.slice(0, 5)) {
|
|
6485
|
+
contextLines.push(`- **${cell.id}**: ${cell.title} (${cell.type}, P${cell.priority})`);
|
|
6486
|
+
if (cell.description) {
|
|
6487
|
+
contextLines.push(` ${cell.description.slice(0, 150)}`);
|
|
6488
|
+
}
|
|
6489
|
+
}
|
|
6490
|
+
if (inProgressCells.length > 5) {
|
|
6491
|
+
contextLines.push(` ... and ${inProgressCells.length - 5} more`);
|
|
6492
|
+
}
|
|
6493
|
+
contextLines.push("");
|
|
6494
|
+
}
|
|
6495
|
+
|
|
6496
|
+
// Open epics (high-level context)
|
|
6497
|
+
const openEpics = await adapter.queryCells(projectPath, {
|
|
6498
|
+
status: "open",
|
|
6499
|
+
type: "epic",
|
|
6500
|
+
limit: 3
|
|
6501
|
+
});
|
|
6502
|
+
if (openEpics.length > 0) {
|
|
6503
|
+
contextLines.push("## Open Epics");
|
|
6504
|
+
for (const epic of openEpics) {
|
|
6505
|
+
const children = await adapter.getEpicChildren(projectPath, epic.id);
|
|
6506
|
+
const openChildren = children.filter(c => c.status !== "closed");
|
|
6507
|
+
contextLines.push(`- **${epic.id}**: ${epic.title}`);
|
|
6508
|
+
contextLines.push(` ${openChildren.length}/${children.length} subtasks remaining`);
|
|
6509
|
+
}
|
|
6510
|
+
contextLines.push("");
|
|
6511
|
+
}
|
|
6512
|
+
|
|
6513
|
+
// Stats summary
|
|
6514
|
+
const stats = await adapter.getCellsStats(projectPath);
|
|
6515
|
+
contextLines.push("## Hive Stats");
|
|
6516
|
+
contextLines.push(`Open: ${stats.open} | In Progress: ${stats.in_progress} | Blocked: ${stats.blocked} | Closed: ${stats.closed}`);
|
|
6517
|
+
|
|
6518
|
+
writeClaudeHookOutput("SessionStart", contextLines.join("\n"));
|
|
6519
|
+
} catch (error) {
|
|
6520
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
6521
|
+
}
|
|
6522
|
+
}
|
|
6523
|
+
|
|
6524
|
+
/**
|
|
6525
|
+
* Claude hook: provide active context on prompt submission.
|
|
6526
|
+
*
|
|
6527
|
+
* Lightweight hook that runs on every prompt. Provides:
|
|
6528
|
+
* - Active session info
|
|
6529
|
+
* - Current in-progress cell (if any)
|
|
6530
|
+
* - Quick stats for awareness
|
|
6531
|
+
*
|
|
6532
|
+
* Output is suppressed from verbose mode to avoid noise.
|
|
6533
|
+
*/
|
|
6534
|
+
async function claudeUserPrompt() {
|
|
6535
|
+
try {
|
|
6536
|
+
const input = await readHookInput<ClaudeHookInput>();
|
|
6537
|
+
const projectPath = resolveClaudeProjectPath(input);
|
|
6538
|
+
const swarmMail = await getSwarmMailLibSQL(projectPath);
|
|
6539
|
+
const db = await swarmMail.getDatabase(projectPath);
|
|
6540
|
+
const adapter = createHiveAdapter(db, projectPath);
|
|
6541
|
+
|
|
6542
|
+
await adapter.runMigrations();
|
|
6543
|
+
|
|
6544
|
+
const session = await adapter.getCurrentSession(projectPath);
|
|
6545
|
+
if (!session) return;
|
|
6546
|
+
|
|
6547
|
+
const contextLines: string[] = [];
|
|
6548
|
+
|
|
6549
|
+
// Always inject current timestamp for temporal awareness
|
|
6550
|
+
const now = new Date();
|
|
6551
|
+
const timestamp = now.toLocaleString("en-US", {
|
|
6552
|
+
weekday: "short",
|
|
6553
|
+
year: "numeric",
|
|
6554
|
+
month: "short",
|
|
6555
|
+
day: "numeric",
|
|
6556
|
+
hour: "2-digit",
|
|
6557
|
+
minute: "2-digit",
|
|
6558
|
+
timeZoneName: "short",
|
|
6559
|
+
});
|
|
6560
|
+
contextLines.push(`**Now**: ${timestamp}`);
|
|
6561
|
+
|
|
6562
|
+
// Semantic memory recall - search hivemind for relevant context
|
|
6563
|
+
if (input.prompt && input.prompt.trim().length > 10) {
|
|
6564
|
+
try {
|
|
6565
|
+
const memoryAdapter = await createMemoryAdapter(db);
|
|
6566
|
+
const findResult = await memoryAdapter.find({ query: input.prompt, limit: 3 });
|
|
6567
|
+
const memories = findResult.results;
|
|
6568
|
+
|
|
6569
|
+
if (memories && memories.length > 0) {
|
|
6570
|
+
// Only include high-confidence matches (score > 0.5)
|
|
6571
|
+
const relevant = memories.filter((m: { score?: number }) => (m.score ?? 0) > 0.5);
|
|
6572
|
+
if (relevant.length > 0) {
|
|
6573
|
+
const memorySnippets = relevant
|
|
6574
|
+
.slice(0, 2) // Max 2 memories to keep context light
|
|
6575
|
+
.map((m: { content?: string; information?: string }) => {
|
|
6576
|
+
const content = m.content || m.information || "";
|
|
6577
|
+
return content.length > 200 ? content.slice(0, 200) + "..." : content;
|
|
6578
|
+
})
|
|
6579
|
+
.join(" | ");
|
|
6580
|
+
contextLines.push(`**Recall**: ${memorySnippets}`);
|
|
6581
|
+
}
|
|
6582
|
+
}
|
|
6583
|
+
} catch {
|
|
6584
|
+
// Memory recall is optional - don't fail the hook
|
|
6585
|
+
}
|
|
6586
|
+
}
|
|
6587
|
+
|
|
6588
|
+
// Current active cell (most relevant context)
|
|
6589
|
+
if (session.active_cell_id) {
|
|
6590
|
+
const activeCell = await adapter.getCell(projectPath, session.active_cell_id);
|
|
6591
|
+
if (activeCell && activeCell.status !== "closed") {
|
|
6592
|
+
contextLines.push(`**Active**: ${activeCell.id} - ${activeCell.title}`);
|
|
6593
|
+
}
|
|
6594
|
+
}
|
|
6595
|
+
|
|
6596
|
+
// Quick in-progress count for awareness
|
|
6597
|
+
const inProgress = await adapter.getInProgressCells(projectPath);
|
|
6598
|
+
if (inProgress.length > 0) {
|
|
6599
|
+
const titles = inProgress.slice(0, 3).map(c => c.title.slice(0, 40)).join(", ");
|
|
6600
|
+
contextLines.push(`**WIP (${inProgress.length})**: ${titles}${inProgress.length > 3 ? "..." : ""}`);
|
|
6601
|
+
}
|
|
6602
|
+
|
|
6603
|
+
// Only output if there's meaningful context
|
|
6604
|
+
if (contextLines.length > 0) {
|
|
6605
|
+
writeClaudeHookOutput("UserPromptSubmit", contextLines.join(" | "), { suppressOutput: true });
|
|
6606
|
+
}
|
|
6607
|
+
} catch (error) {
|
|
6608
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
6609
|
+
}
|
|
6610
|
+
}
|
|
6611
|
+
|
|
6612
|
+
/**
|
|
6613
|
+
* Claude hook: capture comprehensive state before compaction.
|
|
6614
|
+
*
|
|
6615
|
+
* This is the CRITICAL hook for continuation. It captures:
|
|
6616
|
+
* - All in-progress work with details
|
|
6617
|
+
* - Active epic state and progress
|
|
6618
|
+
* - Any pending/blocked cells
|
|
6619
|
+
* - Reserved files
|
|
6620
|
+
* - Session context
|
|
6621
|
+
*
|
|
6622
|
+
* The output becomes part of the compacted summary, enabling
|
|
6623
|
+
* Claude to continue work seamlessly after context window fills.
|
|
6624
|
+
*/
|
|
6625
|
+
async function claudePreCompact() {
|
|
6626
|
+
try {
|
|
6627
|
+
const input = await readHookInput<ClaudeHookInput & { trigger?: string; custom_instructions?: string }>();
|
|
6628
|
+
const projectPath = resolveClaudeProjectPath(input);
|
|
6629
|
+
const swarmMail = await getSwarmMailLibSQL(projectPath);
|
|
6630
|
+
const db = await swarmMail.getDatabase(projectPath);
|
|
6631
|
+
const adapter = createHiveAdapter(db, projectPath);
|
|
6632
|
+
|
|
6633
|
+
await adapter.runMigrations();
|
|
6634
|
+
|
|
6635
|
+
const session = await adapter.getCurrentSession(projectPath);
|
|
6636
|
+
const contextLines: string[] = [];
|
|
6637
|
+
|
|
6638
|
+
contextLines.push("# Swarm State Snapshot (Pre-Compaction)");
|
|
6639
|
+
contextLines.push(`Trigger: ${input.trigger || "auto"}`);
|
|
6640
|
+
if (input.custom_instructions) {
|
|
6641
|
+
contextLines.push(`Instructions: ${input.custom_instructions}`);
|
|
6642
|
+
}
|
|
6643
|
+
contextLines.push("");
|
|
6644
|
+
|
|
6645
|
+
// Session info
|
|
6646
|
+
if (session) {
|
|
6647
|
+
contextLines.push("## Session");
|
|
6648
|
+
contextLines.push(`ID: ${session.id}`);
|
|
6649
|
+
if (session.active_cell_id) {
|
|
6650
|
+
contextLines.push(`Active cell: ${session.active_cell_id}`);
|
|
6651
|
+
}
|
|
6652
|
+
}
|
|
6653
|
+
contextLines.push("");
|
|
6654
|
+
|
|
6655
|
+
// In-progress work (CRITICAL for continuation)
|
|
6656
|
+
const inProgressCells = await adapter.getInProgressCells(projectPath);
|
|
6657
|
+
if (inProgressCells.length > 0) {
|
|
6658
|
+
contextLines.push("## In-Progress Work (CONTINUE THESE)");
|
|
6659
|
+
for (const cell of inProgressCells) {
|
|
6660
|
+
contextLines.push(`### ${cell.id}: ${cell.title}`);
|
|
6661
|
+
contextLines.push(`Type: ${cell.type} | Priority: ${cell.priority} | Status: ${cell.status}`);
|
|
6662
|
+
if (cell.parent_id) {
|
|
6663
|
+
contextLines.push(`Parent: ${cell.parent_id}`);
|
|
6664
|
+
}
|
|
6665
|
+
if (cell.description) {
|
|
6666
|
+
contextLines.push(`Description: ${cell.description}`);
|
|
6667
|
+
}
|
|
6668
|
+
// Get comments for context on progress
|
|
6669
|
+
const comments = await adapter.getComments(projectPath, cell.id);
|
|
6670
|
+
if (comments.length > 0) {
|
|
6671
|
+
const recent = comments.slice(-3);
|
|
6672
|
+
contextLines.push("Recent notes:");
|
|
6673
|
+
for (const comment of recent) {
|
|
6674
|
+
contextLines.push(`- ${comment.body.slice(0, 200)}`);
|
|
6675
|
+
}
|
|
6676
|
+
}
|
|
6677
|
+
contextLines.push("");
|
|
6678
|
+
}
|
|
6679
|
+
}
|
|
6680
|
+
|
|
6681
|
+
// Open epics with children status
|
|
6682
|
+
const openEpics = await adapter.queryCells(projectPath, {
|
|
6683
|
+
status: ["open", "in_progress"],
|
|
6684
|
+
type: "epic",
|
|
6685
|
+
limit: 5
|
|
6686
|
+
});
|
|
6687
|
+
if (openEpics.length > 0) {
|
|
6688
|
+
contextLines.push("## Active Epics");
|
|
6689
|
+
for (const epic of openEpics) {
|
|
6690
|
+
const children = await adapter.getEpicChildren(projectPath, epic.id);
|
|
6691
|
+
const completed = children.filter(c => c.status === "closed").length;
|
|
6692
|
+
const inProgress = children.filter(c => c.status === "in_progress");
|
|
6693
|
+
const open = children.filter(c => c.status === "open");
|
|
6694
|
+
|
|
6695
|
+
contextLines.push(`### ${epic.id}: ${epic.title}`);
|
|
6696
|
+
contextLines.push(`Progress: ${completed}/${children.length} completed`);
|
|
6697
|
+
|
|
6698
|
+
if (inProgress.length > 0) {
|
|
6699
|
+
contextLines.push("In progress:");
|
|
6700
|
+
for (const c of inProgress) {
|
|
6701
|
+
contextLines.push(`- ${c.id}: ${c.title}`);
|
|
6702
|
+
}
|
|
6703
|
+
}
|
|
6704
|
+
if (open.length > 0 && open.length <= 5) {
|
|
6705
|
+
contextLines.push("Remaining:");
|
|
6706
|
+
for (const c of open) {
|
|
6707
|
+
contextLines.push(`- ${c.id}: ${c.title}`);
|
|
6708
|
+
}
|
|
6709
|
+
} else if (open.length > 5) {
|
|
6710
|
+
contextLines.push(`Remaining: ${open.length} tasks`);
|
|
6711
|
+
}
|
|
6712
|
+
contextLines.push("");
|
|
6713
|
+
}
|
|
6714
|
+
}
|
|
6715
|
+
|
|
6716
|
+
// Blocked cells (so Claude knows what's waiting)
|
|
6717
|
+
const blockedCells = await adapter.getBlockedCells(projectPath);
|
|
6718
|
+
if (blockedCells.length > 0) {
|
|
6719
|
+
contextLines.push("## Blocked Work");
|
|
6720
|
+
for (const { cell, blockers } of blockedCells.slice(0, 5)) {
|
|
6721
|
+
contextLines.push(`- ${cell.id}: ${cell.title}`);
|
|
6722
|
+
contextLines.push(` Blocked by: ${blockers.join(", ")}`);
|
|
6723
|
+
}
|
|
6724
|
+
contextLines.push("");
|
|
6725
|
+
}
|
|
6726
|
+
|
|
6727
|
+
// Ready cells (next work available)
|
|
6728
|
+
const readyCell = await adapter.getNextReadyCell(projectPath);
|
|
6729
|
+
if (readyCell) {
|
|
6730
|
+
contextLines.push("## Next Ready Task");
|
|
6731
|
+
contextLines.push(`${readyCell.id}: ${readyCell.title} (P${readyCell.priority})`);
|
|
6732
|
+
contextLines.push("");
|
|
6733
|
+
}
|
|
6734
|
+
|
|
6735
|
+
// Stats
|
|
6736
|
+
const stats = await adapter.getCellsStats(projectPath);
|
|
6737
|
+
contextLines.push("## Hive Stats");
|
|
6738
|
+
contextLines.push(`Open: ${stats.open} | In Progress: ${stats.in_progress} | Blocked: ${stats.blocked} | Closed: ${stats.closed}`);
|
|
6739
|
+
|
|
6740
|
+
writeClaudeHookOutput("PreCompact", contextLines.join("\n"));
|
|
6741
|
+
} catch (error) {
|
|
6742
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
6743
|
+
}
|
|
6744
|
+
}
|
|
6745
|
+
|
|
6746
|
+
/**
|
|
6747
|
+
* Claude hook: end the active session.
|
|
6748
|
+
*/
|
|
6749
|
+
async function claudeSessionEnd() {
|
|
6750
|
+
try {
|
|
6751
|
+
const input = await readHookInput<ClaudeHookInput>();
|
|
6752
|
+
const projectPath = resolveClaudeProjectPath(input);
|
|
6753
|
+
const swarmMail = await getSwarmMailLibSQL(projectPath);
|
|
6754
|
+
const db = await swarmMail.getDatabase(projectPath);
|
|
6755
|
+
const adapter = createHiveAdapter(db, projectPath);
|
|
6756
|
+
|
|
6757
|
+
await adapter.runMigrations();
|
|
6758
|
+
|
|
6759
|
+
const currentSession = await adapter.getCurrentSession(projectPath);
|
|
6760
|
+
if (!currentSession) return;
|
|
6761
|
+
|
|
6762
|
+
await adapter.endSession(projectPath, currentSession.id, {});
|
|
6763
|
+
} catch (error) {
|
|
6764
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
6765
|
+
}
|
|
6766
|
+
}
|
|
6767
|
+
|
|
6768
|
+
/**
|
|
6769
|
+
* Claude hook: pre-edit reminder for workers
|
|
6770
|
+
*
|
|
6771
|
+
* Runs BEFORE Edit/Write tool calls. Reminds worker to query hivemind first.
|
|
6772
|
+
* This is a gentle nudge, not a blocker.
|
|
6773
|
+
*/
|
|
6774
|
+
async function claudePreEdit() {
|
|
6775
|
+
try {
|
|
6776
|
+
const input = await readHookInput<ClaudeHookInput & { tool_name?: string; tool_input?: Record<string, unknown> }>();
|
|
6777
|
+
|
|
6778
|
+
// Only inject reminder if this looks like a worker context
|
|
6779
|
+
// (workers have initialized via swarmmail_init)
|
|
6780
|
+
const projectPath = resolveClaudeProjectPath(input);
|
|
6781
|
+
|
|
6782
|
+
// Check if we've seen hivemind_find in this session
|
|
6783
|
+
// For now, we always remind - tracking will come later
|
|
6784
|
+
const contextLines: string[] = [];
|
|
6785
|
+
|
|
6786
|
+
contextLines.push("**Before this edit**: Did you run `hivemind_find` to check for existing solutions?");
|
|
6787
|
+
contextLines.push("If you haven't queried hivemind yet, consider doing so to avoid re-solving problems.");
|
|
6788
|
+
|
|
6789
|
+
writeClaudeHookOutput("PreToolUse:Edit", contextLines.join("\n"), { suppressOutput: true });
|
|
6790
|
+
} catch (error) {
|
|
6791
|
+
// Non-fatal - don't block the edit
|
|
6792
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
6793
|
+
}
|
|
6794
|
+
}
|
|
6795
|
+
|
|
6796
|
+
/**
|
|
6797
|
+
* Claude hook: pre-complete check for workers
|
|
6798
|
+
*
|
|
6799
|
+
* Runs BEFORE swarm_complete. Checks compliance with mandatory steps
|
|
6800
|
+
* and warns if any were skipped.
|
|
6801
|
+
*/
|
|
6802
|
+
async function claudePreComplete() {
|
|
6803
|
+
try {
|
|
6804
|
+
const input = await readHookInput<ClaudeHookInput & { tool_input?: Record<string, unknown> }>();
|
|
6805
|
+
const projectPath = resolveClaudeProjectPath(input);
|
|
6806
|
+
const contextLines: string[] = [];
|
|
6807
|
+
|
|
6808
|
+
// Check session tracking markers
|
|
6809
|
+
const trackingDir = join(projectPath, ".claude", ".worker-tracking");
|
|
6810
|
+
const sessionId = (input as { session_id?: string }).session_id || "";
|
|
6811
|
+
const sessionDir = join(trackingDir, sessionId.slice(0, 8));
|
|
6812
|
+
|
|
6813
|
+
const mandatoryTools = [
|
|
6814
|
+
{ name: "swarmmail_init", label: "Initialize coordination" },
|
|
6815
|
+
{ name: "hivemind_find", label: "Query past learnings" },
|
|
6816
|
+
];
|
|
6817
|
+
const recommendedTools = [
|
|
6818
|
+
{ name: "skills_use", label: "Load relevant skills" },
|
|
6819
|
+
{ name: "hivemind_store", label: "Store new learnings" },
|
|
6820
|
+
];
|
|
6821
|
+
|
|
6822
|
+
const missing: string[] = [];
|
|
6823
|
+
const skippedRecommended: string[] = [];
|
|
6824
|
+
|
|
6825
|
+
for (const tool of mandatoryTools) {
|
|
6826
|
+
const markerPath = join(sessionDir, `${tool.name}.marker`);
|
|
6827
|
+
if (!existsSync(markerPath)) {
|
|
6828
|
+
missing.push(tool.label);
|
|
6829
|
+
}
|
|
6830
|
+
}
|
|
6831
|
+
|
|
6832
|
+
for (const tool of recommendedTools) {
|
|
6833
|
+
const markerPath = join(sessionDir, `${tool.name}.marker`);
|
|
6834
|
+
if (!existsSync(markerPath)) {
|
|
6835
|
+
skippedRecommended.push(tool.label);
|
|
6836
|
+
}
|
|
6837
|
+
}
|
|
6838
|
+
|
|
6839
|
+
if (missing.length > 0) {
|
|
6840
|
+
contextLines.push("**MANDATORY STEPS SKIPPED:**");
|
|
6841
|
+
for (const step of missing) {
|
|
6842
|
+
contextLines.push(` - ${step}`);
|
|
6843
|
+
}
|
|
6844
|
+
contextLines.push("");
|
|
6845
|
+
contextLines.push("These steps are critical for swarm coordination.");
|
|
6846
|
+
contextLines.push("Consider running `hivemind_find` before future completions.");
|
|
6847
|
+
}
|
|
6848
|
+
|
|
6849
|
+
if (skippedRecommended.length > 0 && missing.length === 0) {
|
|
6850
|
+
contextLines.push("**Recommended steps not observed:**");
|
|
6851
|
+
for (const step of skippedRecommended) {
|
|
6852
|
+
contextLines.push(` - ${step}`);
|
|
6853
|
+
}
|
|
6854
|
+
}
|
|
6855
|
+
|
|
6856
|
+
if (missing.length === 0 && skippedRecommended.length === 0) {
|
|
6857
|
+
contextLines.push("**All mandatory and recommended steps completed!**");
|
|
6858
|
+
}
|
|
6859
|
+
|
|
6860
|
+
// Emit compliance event for analytics
|
|
6861
|
+
try {
|
|
6862
|
+
const swarmMail = await getSwarmMailLibSQL(projectPath);
|
|
6863
|
+
const db = await swarmMail.getDatabase(projectPath);
|
|
6864
|
+
|
|
6865
|
+
await db.execute({
|
|
6866
|
+
sql: `INSERT INTO swarm_events (event_type, project_path, agent_name, epic_id, bead_id, payload, created_at)
|
|
6867
|
+
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))`,
|
|
6868
|
+
args: [
|
|
6869
|
+
"worker_compliance",
|
|
6870
|
+
projectPath,
|
|
6871
|
+
"worker",
|
|
6872
|
+
"",
|
|
6873
|
+
"",
|
|
6874
|
+
JSON.stringify({
|
|
6875
|
+
session_id: sessionId,
|
|
6876
|
+
mandatory_skipped: missing.length,
|
|
6877
|
+
recommended_skipped: skippedRecommended.length,
|
|
6878
|
+
score: Math.round(((mandatoryTools.length - missing.length) / mandatoryTools.length) * 100),
|
|
6879
|
+
}),
|
|
6880
|
+
],
|
|
6881
|
+
});
|
|
6882
|
+
} catch {
|
|
6883
|
+
// Non-fatal
|
|
6884
|
+
}
|
|
6885
|
+
|
|
6886
|
+
if (contextLines.length > 0) {
|
|
6887
|
+
writeClaudeHookOutput("PreToolUse:swarm_complete", contextLines.join("\n"), { suppressOutput: missing.length === 0 });
|
|
6888
|
+
}
|
|
6889
|
+
} catch (error) {
|
|
6890
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
6891
|
+
}
|
|
6892
|
+
}
|
|
6893
|
+
|
|
6894
|
+
/**
|
|
6895
|
+
* Claude hook: post-complete reminder for workers
|
|
6896
|
+
*
|
|
6897
|
+
* Runs AFTER swarm_complete. Reminds to store learnings if any were discovered.
|
|
6898
|
+
*/
|
|
6899
|
+
async function claudePostComplete() {
|
|
6900
|
+
try {
|
|
6901
|
+
const input = await readHookInput<ClaudeHookInput & { tool_output?: string }>();
|
|
6902
|
+
const contextLines: string[] = [];
|
|
6903
|
+
|
|
6904
|
+
contextLines.push("Task completed. If you discovered anything valuable during this work:");
|
|
6905
|
+
contextLines.push("```");
|
|
6906
|
+
contextLines.push("hivemind_store(");
|
|
6907
|
+
contextLines.push(' information="<what you learned, WHY it matters>",');
|
|
6908
|
+
contextLines.push(' tags="<domain, pattern-type>"');
|
|
6909
|
+
contextLines.push(")");
|
|
6910
|
+
contextLines.push("```");
|
|
6911
|
+
contextLines.push("**The swarm's collective intelligence grows when agents share learnings.**");
|
|
6912
|
+
|
|
6913
|
+
writeClaudeHookOutput("PostToolUse:swarm_complete", contextLines.join("\n"), { suppressOutput: true });
|
|
6914
|
+
} catch (error) {
|
|
6915
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
6916
|
+
}
|
|
6917
|
+
}
|
|
6918
|
+
|
|
6919
|
+
/**
|
|
6920
|
+
* Track when a mandatory tool is called.
|
|
6921
|
+
*
|
|
6922
|
+
* Creates a session-specific marker file that records tool usage.
|
|
6923
|
+
* These markers are checked at swarm_complete to calculate compliance.
|
|
6924
|
+
*/
|
|
6925
|
+
async function claudeTrackTool(toolName: string) {
|
|
6926
|
+
if (!toolName) return;
|
|
6927
|
+
|
|
6928
|
+
try {
|
|
6929
|
+
const input = await readHookInput<ClaudeHookInput>();
|
|
6930
|
+
const projectPath = resolveClaudeProjectPath(input);
|
|
6931
|
+
|
|
6932
|
+
// Get or create session tracking directory
|
|
6933
|
+
const trackingDir = join(projectPath, ".claude", ".worker-tracking");
|
|
6934
|
+
const sessionId = (input as { session_id?: string }).session_id || `unknown-${Date.now()}`;
|
|
6935
|
+
const sessionDir = join(trackingDir, sessionId.slice(0, 8)); // Use first 8 chars of session ID
|
|
6936
|
+
|
|
6937
|
+
if (!existsSync(sessionDir)) {
|
|
6938
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
6939
|
+
}
|
|
6940
|
+
|
|
6941
|
+
// Write marker file for this tool
|
|
6942
|
+
const markerPath = join(sessionDir, `${toolName}.marker`);
|
|
6943
|
+
writeFileSync(markerPath, new Date().toISOString());
|
|
6944
|
+
|
|
6945
|
+
// Also emit an event for long-term analytics (non-blocking)
|
|
6946
|
+
try {
|
|
6947
|
+
const swarmMail = await getSwarmMailLibSQL(projectPath);
|
|
6948
|
+
const db = await swarmMail.getDatabase(projectPath);
|
|
6949
|
+
|
|
6950
|
+
await db.execute({
|
|
6951
|
+
sql: `INSERT INTO swarm_events (event_type, project_path, agent_name, epic_id, bead_id, payload, created_at)
|
|
6952
|
+
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))`,
|
|
6953
|
+
args: [
|
|
6954
|
+
"worker_tool_call",
|
|
6955
|
+
projectPath,
|
|
6956
|
+
"worker", // We don't have agent name in hook context
|
|
6957
|
+
"",
|
|
6958
|
+
"",
|
|
6959
|
+
JSON.stringify({ tool: toolName, session_id: sessionId }),
|
|
6960
|
+
],
|
|
6961
|
+
});
|
|
6962
|
+
} catch {
|
|
6963
|
+
// Non-fatal - tracking is best-effort
|
|
6964
|
+
}
|
|
6965
|
+
} catch (error) {
|
|
6966
|
+
// Silent failure - don't interrupt the tool call
|
|
6967
|
+
console.error(`[track-tool] ${error instanceof Error ? error.message : String(error)}`);
|
|
6968
|
+
}
|
|
6969
|
+
}
|
|
6970
|
+
|
|
6971
|
+
/**
|
|
6972
|
+
* Check worker compliance - which mandatory tools were called.
|
|
6973
|
+
*
|
|
6974
|
+
* Returns compliance data based on session tracking markers.
|
|
6975
|
+
*/
|
|
6976
|
+
async function claudeCompliance() {
|
|
6977
|
+
try {
|
|
6978
|
+
const input = await readHookInput<ClaudeHookInput>();
|
|
6979
|
+
const projectPath = resolveClaudeProjectPath(input);
|
|
6980
|
+
|
|
6981
|
+
const trackingDir = join(projectPath, ".claude", ".worker-tracking");
|
|
6982
|
+
const sessionId = (input as { session_id?: string }).session_id || "";
|
|
6983
|
+
const sessionDir = join(trackingDir, sessionId.slice(0, 8));
|
|
6984
|
+
|
|
6985
|
+
const mandatoryTools = ["swarmmail_init", "hivemind_find", "skills_use"];
|
|
6986
|
+
const recommendedTools = ["hivemind_store"];
|
|
6987
|
+
|
|
6988
|
+
const compliance: Record<string, boolean> = {};
|
|
6989
|
+
|
|
6990
|
+
for (const tool of [...mandatoryTools, ...recommendedTools]) {
|
|
6991
|
+
const markerPath = join(sessionDir, `${tool}.marker`);
|
|
6992
|
+
compliance[tool] = existsSync(markerPath);
|
|
6993
|
+
}
|
|
6994
|
+
|
|
6995
|
+
const mandatoryCount = mandatoryTools.filter(t => compliance[t]).length;
|
|
6996
|
+
const score = Math.round((mandatoryCount / mandatoryTools.length) * 100);
|
|
6997
|
+
|
|
6998
|
+
console.log(JSON.stringify({
|
|
6999
|
+
session_id: sessionId,
|
|
7000
|
+
compliance,
|
|
7001
|
+
mandatory_score: score,
|
|
7002
|
+
mandatory_met: mandatoryCount,
|
|
7003
|
+
mandatory_total: mandatoryTools.length,
|
|
7004
|
+
stored_learnings: compliance.hivemind_store,
|
|
7005
|
+
}));
|
|
7006
|
+
} catch (error) {
|
|
7007
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
7008
|
+
}
|
|
7009
|
+
}
|
|
7010
|
+
|
|
7011
|
+
/**
|
|
7012
|
+
* Hot-reload skills by clearing cache and re-scanning directories.
|
|
7013
|
+
* Triggered by Claude Code hook when skill files are modified.
|
|
7014
|
+
*/
|
|
7015
|
+
async function claudeSkillReload() {
|
|
7016
|
+
try {
|
|
7017
|
+
// Clear the skills cache
|
|
7018
|
+
invalidateSkillsCache();
|
|
7019
|
+
|
|
7020
|
+
// Re-scan skill directories to get updated list
|
|
7021
|
+
const skills = await discoverSkills();
|
|
7022
|
+
|
|
7023
|
+
// Return success with count
|
|
7024
|
+
console.log(JSON.stringify({
|
|
7025
|
+
success: true,
|
|
7026
|
+
reloaded: skills.size,
|
|
7027
|
+
message: `Reloaded ${skills.size} skill(s)`,
|
|
7028
|
+
}));
|
|
7029
|
+
} catch (error) {
|
|
7030
|
+
console.error(JSON.stringify({
|
|
7031
|
+
success: false,
|
|
7032
|
+
error: error instanceof Error ? error.message : String(error),
|
|
7033
|
+
}));
|
|
7034
|
+
}
|
|
7035
|
+
}
|
|
7036
|
+
|
|
7037
|
+
/**
|
|
7038
|
+
* Query worker compliance stats across all sessions.
|
|
7039
|
+
*/
|
|
7040
|
+
async function showWorkerCompliance() {
|
|
7041
|
+
const projectPath = process.cwd();
|
|
7042
|
+
|
|
7043
|
+
try {
|
|
7044
|
+
// Use shared executeQuery (handles DB connection internally)
|
|
7045
|
+
const toolUsageSql = `SELECT
|
|
7046
|
+
json_extract(payload, '$.tool') as tool,
|
|
7047
|
+
COUNT(*) as count,
|
|
7048
|
+
COUNT(DISTINCT json_extract(payload, '$.session_id')) as sessions
|
|
7049
|
+
FROM swarm_events
|
|
7050
|
+
WHERE event_type = 'worker_tool_call'
|
|
7051
|
+
AND created_at > datetime('now', '-7 days')
|
|
7052
|
+
GROUP BY tool
|
|
7053
|
+
ORDER BY count DESC`;
|
|
7054
|
+
|
|
7055
|
+
const toolResult = await executeQueryCLI(projectPath, toolUsageSql);
|
|
7056
|
+
const rows = toolResult.rows;
|
|
7057
|
+
|
|
7058
|
+
console.log(yellow(BANNER));
|
|
7059
|
+
console.log(cyan("\n Worker Tool Usage (Last 7 Days)\n"));
|
|
7060
|
+
|
|
7061
|
+
if (rows.length === 0) {
|
|
7062
|
+
console.log(dim("No worker tool usage data found."));
|
|
7063
|
+
console.log(dim("Data is collected when workers run with the Claude Code plugin."));
|
|
7064
|
+
return;
|
|
7065
|
+
}
|
|
7066
|
+
|
|
7067
|
+
console.log(dim("Tool Calls Sessions"));
|
|
7068
|
+
console.log(dim("\u2500".repeat(45)));
|
|
7069
|
+
|
|
7070
|
+
for (const row of rows) {
|
|
7071
|
+
const tool = String(row.tool).padEnd(22);
|
|
7072
|
+
const count = String(row.count).padStart(6);
|
|
7073
|
+
const sessions = String(row.sessions).padStart(8);
|
|
7074
|
+
console.log(`${tool} ${count} ${sessions}`);
|
|
7075
|
+
}
|
|
7076
|
+
|
|
7077
|
+
// Calculate compliance rate
|
|
7078
|
+
const hivemindFinds = rows.find(r => r.tool === "hivemind_find");
|
|
7079
|
+
const completesSql = `SELECT COUNT(*) as count FROM swarm_events
|
|
7080
|
+
WHERE event_type = 'worker_completed'
|
|
7081
|
+
AND created_at > datetime('now', '-7 days')`;
|
|
7082
|
+
const completesResult = await executeQueryCLI(projectPath, completesSql);
|
|
7083
|
+
|
|
7084
|
+
const completes = Number(completesResult.rows[0]?.count || 0);
|
|
7085
|
+
const finds = Number(hivemindFinds?.count || 0);
|
|
7086
|
+
|
|
7087
|
+
if (completes > 0) {
|
|
7088
|
+
const rate = Math.round((Math.min(finds, completes) / completes) * 100);
|
|
7089
|
+
console.log(dim("\n\u2500".repeat(45)));
|
|
7090
|
+
console.log(`\n${green("Hivemind compliance rate:")} ${rate}% (${finds} queries / ${completes} completions)`);
|
|
7091
|
+
}
|
|
7092
|
+
|
|
7093
|
+
} catch (error) {
|
|
7094
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
7095
|
+
if (msg.includes("no such table")) {
|
|
7096
|
+
console.log(yellow(BANNER));
|
|
7097
|
+
console.log(cyan("\n Worker Tool Usage\n"));
|
|
7098
|
+
console.log(dim("No tracking data yet."));
|
|
7099
|
+
console.log(dim("Compliance tracking starts when workers run with the Claude Code plugin hooks."));
|
|
7100
|
+
console.log(dim("\nThe swarm_events table will be created on first tracked tool call."));
|
|
7101
|
+
} else {
|
|
7102
|
+
console.error("Error fetching compliance data:", msg);
|
|
7103
|
+
}
|
|
7104
|
+
}
|
|
7105
|
+
}
|
|
7106
|
+
|
|
7107
|
+
// ============================================================================
|
|
7108
|
+
// Claude Code Integration - New Stub Handlers
|
|
7109
|
+
// ============================================================================
|
|
7110
|
+
|
|
7111
|
+
/**
|
|
7112
|
+
* Claude hook: coordinator subagent start.
|
|
7113
|
+
* Initializes coordinator context for subagent spawning.
|
|
7114
|
+
*/
|
|
7115
|
+
async function claudeCoordinatorStart() {
|
|
7116
|
+
try {
|
|
7117
|
+
const input = await readHookInput<ClaudeHookInput>();
|
|
7118
|
+
const projectPath = resolveClaudeProjectPath(input);
|
|
7119
|
+
writeClaudeHookOutput("SubagentStart:coordinator",
|
|
7120
|
+
`Coordinator initialized for project: ${projectPath}`);
|
|
7121
|
+
} catch (e) {
|
|
7122
|
+
// Non-fatal
|
|
7123
|
+
}
|
|
7124
|
+
}
|
|
7125
|
+
|
|
7126
|
+
/**
|
|
7127
|
+
* Claude hook: worker subagent start.
|
|
7128
|
+
* Initializes worker context for subagent execution.
|
|
7129
|
+
*/
|
|
7130
|
+
async function claudeWorkerStart() {
|
|
7131
|
+
try {
|
|
7132
|
+
const input = await readHookInput<ClaudeHookInput>();
|
|
7133
|
+
const projectPath = resolveClaudeProjectPath(input);
|
|
7134
|
+
writeClaudeHookOutput("SubagentStart:worker",
|
|
7135
|
+
`Worker initialized for project: ${projectPath}`);
|
|
7136
|
+
} catch (e) {
|
|
7137
|
+
// Non-fatal
|
|
7138
|
+
}
|
|
7139
|
+
}
|
|
7140
|
+
|
|
7141
|
+
/**
|
|
7142
|
+
* Claude hook: subagent stop cleanup.
|
|
7143
|
+
*/
|
|
7144
|
+
async function claudeSubagentStop() {
|
|
7145
|
+
try {
|
|
7146
|
+
await readHookInput<ClaudeHookInput>();
|
|
7147
|
+
// Cleanup - non-fatal
|
|
7148
|
+
} catch (e) {
|
|
7149
|
+
// Silent
|
|
7150
|
+
}
|
|
7151
|
+
}
|
|
7152
|
+
|
|
7153
|
+
/**
|
|
7154
|
+
* Claude hook: agent stop cleanup.
|
|
7155
|
+
*/
|
|
7156
|
+
async function claudeAgentStop() {
|
|
7157
|
+
try {
|
|
7158
|
+
await readHookInput<ClaudeHookInput>();
|
|
7159
|
+
// Cleanup - non-fatal
|
|
7160
|
+
} catch (e) {
|
|
7161
|
+
// Silent
|
|
7162
|
+
}
|
|
7163
|
+
}
|
|
7164
|
+
|
|
7165
|
+
/**
|
|
7166
|
+
* Claude hook: track task events.
|
|
7167
|
+
*/
|
|
7168
|
+
async function claudeTrackTask() {
|
|
7169
|
+
try {
|
|
7170
|
+
await readHookInput<ClaudeHookInput>();
|
|
7171
|
+
// Track task event - non-fatal
|
|
7172
|
+
} catch (e) {
|
|
7173
|
+
// Silent
|
|
7174
|
+
}
|
|
7175
|
+
}
|
|
7176
|
+
|
|
7177
|
+
/**
|
|
7178
|
+
* Claude hook: post task update tracking.
|
|
7179
|
+
*/
|
|
7180
|
+
async function claudePostTaskUpdate() {
|
|
7181
|
+
try {
|
|
7182
|
+
await readHookInput<ClaudeHookInput>();
|
|
7183
|
+
// Post-update tracking - non-fatal
|
|
7184
|
+
} catch (e) {
|
|
7185
|
+
// Silent
|
|
7186
|
+
}
|
|
7187
|
+
}
|
|
7188
|
+
|
|
6027
7189
|
// ============================================================================
|
|
6028
7190
|
// Main
|
|
6029
7191
|
// ============================================================================
|
|
@@ -6048,6 +7210,9 @@ switch (command) {
|
|
|
6048
7210
|
case "config":
|
|
6049
7211
|
config();
|
|
6050
7212
|
break;
|
|
7213
|
+
case "claude":
|
|
7214
|
+
await claudeCommand();
|
|
7215
|
+
break;
|
|
6051
7216
|
case "serve":
|
|
6052
7217
|
await serve();
|
|
6053
7218
|
break;
|