/**
* BeadsGuardPlugin
*
* Enforces beads usage when a .beads directory exists in the project.
* - Injects context on session creation reminding to use beads
* - Intercepts todowrite tool calls and suggests using beads instead
* - Shows toast notifications for guidance
*/
import type { Plugin } from "@opencode-ai/plugin";
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
/**
* Context message injected when beads is detected
*/
const BEADS_CONTEXT = `
## Issue Tracking with Beads
This project uses **beads** for issue tracking. The \`.beads\` directory contains the issue database.
### Required Behavior:
1. **Use bd_* tools** for all task management instead of the todowrite tool
2. **Check existing issues** with \`bd_list\` before creating new work
3. **Update issue status** as you work (open → in_progress → closed)
4. **Link related issues** with dependencies when appropriate
### Quick Reference:
- \`bd_list\` - List issues (filter by status, priority, label)
- \`bd_ready\` - Show issues ready to work on (no blockers)
- \`bd_create\` - Create new issue
- \`bd_update\` - Update issue (status, priority, assignee)
- \`bd_close\` - Close completed issues
- \`bd_show\` - View issue details
- \`bd_epics\` - List epics
- \`bd_search\` - Search issues by text
### Workflow:
1. Start session → \`bd_ready\` to see what's actionable
2. Pick an issue → \`bd_update\` status to in_progress
3. Complete work → \`bd_close\` the issue
4. New task needed? → \`bd_create\` (not todowrite!)
**Do NOT use the todowrite tool** - use beads (bd_*) tools instead.
`.trim();
/**
* Message shown when todowrite is intercepted
*/
const TODOWRITE_WARNING =
"This project uses beads for issue tracking. Consider using bd_create instead of todowrite for persistent task management.";
/**
* Check if beads is initialized in the directory
*/
const hasBeadsDirectory = (directory: unknown): boolean => {
if (typeof directory !== "string") {
console.error("[BeadsGuard] directory is not a string:", typeof directory, directory);
return false;
}
const beadsPath = join(directory, ".beads");
return existsSync(beadsPath);
};
/**
* Check if beads has any issues (not just initialized)
*/
const hasBeadsIssues = (directory: unknown): boolean => {
if (typeof directory !== "string") {
return false;
}
const issuesPath = join(directory, ".beads", "issues.jsonl");
if (!existsSync(issuesPath)) return false;
try {
const content = readFileSync(issuesPath, "utf-8");
return content.trim().length > 0;
} catch {
return false;
}
};
/**
* Type guard for session.created event
*/
type SessionCreatedEvent = {
type: "session.created";
properties: {
info: {
id: string;
};
};
};
const isSessionCreatedEvent = (
event: unknown,
): event is SessionCreatedEvent => {
if (typeof event !== "object" || event === null) return false;
const e = event as Record;
if (e.type !== "session.created") return false;
if (typeof e.properties !== "object" || e.properties === null) return false;
const props = e.properties as Record;
if (typeof props.info !== "object" || props.info === null) return false;
const info = props.info as Record;
return typeof info.id === "string";
};
/**
* BeadsGuardPlugin - Enforces beads usage in projects with .beads directory
*/
export const BeadsGuardPlugin: Plugin = async ({ client, directory }) => {
const beadsExists = hasBeadsDirectory(directory);
if (!beadsExists) {
// No beads directory, plugin does nothing
return {};
}
// Track sessions we've already injected context into
const injectedSessions = new Set();
return {
event: async ({ event }) => {
// Inject context when a new session is created
if (isSessionCreatedEvent(event)) {
const sessionId = event.properties.info.id;
// Avoid duplicate injections
if (injectedSessions.has(sessionId)) return;
injectedSessions.add(sessionId);
try {
// Inject beads context without triggering AI response
await client.session.prompt({
path: { id: sessionId },
body: {
noReply: true,
parts: [{ type: "text", text: BEADS_CONTEXT }],
},
});
// Show a toast to the user
await client.tui.showToast({
body: {
message: "Beads detected - use bd_* tools for task management",
variant: "info",
},
});
} catch (error) {
// Silently fail - don't break the session
console.error("[BeadsGuard] Failed to inject context:", error);
}
}
},
"tool.execute.before": async (input) => {
// Intercept todowrite calls and warn about beads
if (input.tool === "todowrite" && beadsExists) {
try {
await client.tui.showToast({
body: {
message: TODOWRITE_WARNING,
variant: "warning",
},
});
} catch {
// Silently fail
}
}
},
"tool.execute.after": async (input, output) => {
// After todowrite, remind about beads
if (input.tool === "todowrite" && beadsExists) {
// Modify output to include reminder
if (typeof output.output === "string") {
output.output = `${output.output}\n\n💡 Tip: This project uses beads for issue tracking. Consider using bd_create for persistent task management.`;
}
}
},
};
};
// Re-export for convenience
export { hasBeadsDirectory, hasBeadsIssues };