issueboard-mcp.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. /**
  2. * Issueboard MCP - Manages issues and workflow during PBL generation.
  3. *
  4. * Migrated from PBL-Nano. Key changes:
  5. * - No Anthropic SDK dependency (initialize_question_agent removed)
  6. * - Question agent initialization is handled by generate-pbl.ts post-processing
  7. * - Operates directly on a shared PBLProjectConfig
  8. */
  9. import type { PBLProjectConfig, PBLIssue, PBLToolResult } from '../types';
  10. import { AgentMCP } from './agent-mcp';
  11. import { getQuestionAgentPrompt, getJudgeAgentPrompt } from './agent-templates';
  12. export class IssueboardMCP {
  13. private config: PBLProjectConfig;
  14. private agentMCP: AgentMCP;
  15. private language: string;
  16. private nextIssueId: number;
  17. constructor(config: PBLProjectConfig, agentMCP: AgentMCP, language: string = 'en-US') {
  18. this.config = config;
  19. this.agentMCP = agentMCP;
  20. this.language = language;
  21. this.nextIssueId = 1;
  22. }
  23. createIssueboard(): PBLToolResult {
  24. this.config.issueboard = {
  25. agent_ids: [],
  26. issues: [],
  27. current_issue_id: null,
  28. };
  29. this.nextIssueId = 1;
  30. return { success: true, message: 'Issueboard created successfully.' };
  31. }
  32. getIssueboard(): PBLToolResult {
  33. return {
  34. success: true,
  35. agent_ids: [...this.config.issueboard.agent_ids],
  36. issues: this.config.issueboard.issues.map((i) => ({ ...i })),
  37. };
  38. }
  39. updateIssueboardAgents(agentIds: string[]): PBLToolResult {
  40. this.config.issueboard.agent_ids = [...agentIds];
  41. return {
  42. success: true,
  43. message: 'Issueboard agents updated successfully.',
  44. };
  45. }
  46. createIssue(params: {
  47. title: string;
  48. description: string;
  49. person_in_charge: string;
  50. participants?: string[];
  51. notes?: string;
  52. parent_issue?: string | null;
  53. index?: number;
  54. }): PBLToolResult {
  55. const {
  56. title,
  57. description,
  58. person_in_charge,
  59. participants = [],
  60. notes = '',
  61. parent_issue = null,
  62. index = 0,
  63. } = params;
  64. if (!title?.trim()) {
  65. return { success: false, error: 'Title cannot be empty.' };
  66. }
  67. if (!person_in_charge?.trim()) {
  68. return { success: false, error: 'Person in charge cannot be empty.' };
  69. }
  70. if (parent_issue && !this.config.issueboard.issues.find((i) => i.id === parent_issue)) {
  71. return {
  72. success: false,
  73. error: `Parent issue "${parent_issue}" not found.`,
  74. };
  75. }
  76. const issueId = `issue_${this.nextIssueId++}`;
  77. const questionAgentName = `Question Agent - ${issueId}`;
  78. const judgeAgentName = `Judge Agent - ${issueId}`;
  79. const newIssue: PBLIssue = {
  80. id: issueId,
  81. title,
  82. description,
  83. person_in_charge,
  84. participants: [...participants],
  85. notes,
  86. parent_issue,
  87. index,
  88. is_done: false,
  89. is_active: false,
  90. generated_questions: '',
  91. question_agent_name: questionAgentName,
  92. judge_agent_name: judgeAgentName,
  93. };
  94. this.config.issueboard.issues.push(newIssue);
  95. // Auto-create question and judge agents
  96. this.agentMCP.createAgent({
  97. name: questionAgentName,
  98. system_prompt: getQuestionAgentPrompt(this.language),
  99. default_mode: 'chat',
  100. actor_role: 'Question Assistant for Issue',
  101. role_division: 'development',
  102. is_system_agent: true,
  103. });
  104. this.agentMCP.createAgent({
  105. name: judgeAgentName,
  106. system_prompt: getJudgeAgentPrompt(this.language),
  107. default_mode: 'chat',
  108. actor_role: 'Judge for Issue Completion',
  109. role_division: 'management',
  110. is_system_agent: true,
  111. });
  112. return {
  113. success: true,
  114. issue_id: issueId,
  115. message: 'Issue created with question and judge agents.',
  116. };
  117. }
  118. listIssues(): PBLToolResult {
  119. return {
  120. success: true,
  121. issues: this.config.issueboard.issues.map((i) => ({ ...i })),
  122. };
  123. }
  124. getIssue(issueId: string): PBLToolResult {
  125. const issue = this.config.issueboard.issues.find((i) => i.id === issueId);
  126. if (!issue) {
  127. return { success: false, error: `Issue "${issueId}" not found.` };
  128. }
  129. return { success: true, issues: [{ ...issue }] };
  130. }
  131. updateIssue(params: {
  132. issue_id: string;
  133. title?: string;
  134. description?: string;
  135. person_in_charge?: string;
  136. participants?: string[];
  137. notes?: string;
  138. parent_issue?: string | null;
  139. index?: number;
  140. }): PBLToolResult {
  141. const issue = this.config.issueboard.issues.find((i) => i.id === params.issue_id);
  142. if (!issue) {
  143. return { success: false, error: `Issue "${params.issue_id}" not found.` };
  144. }
  145. if (
  146. params.parent_issue !== undefined &&
  147. params.parent_issue !== null &&
  148. !this.config.issueboard.issues.find((i) => i.id === params.parent_issue)
  149. ) {
  150. return {
  151. success: false,
  152. error: `Parent issue "${params.parent_issue}" not found.`,
  153. };
  154. }
  155. if (params.title !== undefined) issue.title = params.title;
  156. if (params.description !== undefined) issue.description = params.description;
  157. if (params.person_in_charge !== undefined) issue.person_in_charge = params.person_in_charge;
  158. if (params.participants !== undefined) issue.participants = [...params.participants];
  159. if (params.notes !== undefined) issue.notes = params.notes;
  160. if (params.parent_issue !== undefined) issue.parent_issue = params.parent_issue;
  161. if (params.index !== undefined) issue.index = params.index;
  162. return { success: true, message: 'Issue updated successfully.' };
  163. }
  164. deleteIssue(issueId: string): PBLToolResult {
  165. const index = this.config.issueboard.issues.findIndex((i) => i.id === issueId);
  166. if (index === -1) {
  167. return { success: false, error: `Issue "${issueId}" not found.` };
  168. }
  169. this.config.issueboard.issues.splice(index, 1);
  170. // Remove child issues
  171. this.config.issueboard.issues = this.config.issueboard.issues.filter(
  172. (i) => i.parent_issue !== issueId,
  173. );
  174. return { success: true, message: 'Issue deleted successfully.' };
  175. }
  176. reorderIssues(issueIds: string[]): PBLToolResult {
  177. for (const id of issueIds) {
  178. if (!this.config.issueboard.issues.find((i) => i.id === id)) {
  179. return { success: false, error: `Issue "${id}" not found.` };
  180. }
  181. }
  182. const reordered: PBLIssue[] = [];
  183. for (let i = 0; i < issueIds.length; i++) {
  184. const issue = this.config.issueboard.issues.find((iss) => iss.id === issueIds[i])!;
  185. issue.index = i;
  186. reordered.push(issue);
  187. }
  188. // Append any issues not in the reorder list
  189. for (const issue of this.config.issueboard.issues) {
  190. if (!issueIds.includes(issue.id)) {
  191. reordered.push(issue);
  192. }
  193. }
  194. this.config.issueboard.issues = reordered;
  195. return { success: true, message: 'Issues reordered successfully.' };
  196. }
  197. activateNextIssue(): PBLToolResult {
  198. // Deactivate current
  199. const current = this.config.issueboard.issues.find((i) => i.is_active);
  200. if (current) {
  201. current.is_active = false;
  202. this.config.issueboard.current_issue_id = null;
  203. }
  204. // Find next incomplete issue
  205. const next = this.config.issueboard.issues
  206. .filter((i) => !i.is_done)
  207. .sort((a, b) => a.index - b.index)[0];
  208. if (!next) {
  209. return { success: false, error: 'No more issues to activate.' };
  210. }
  211. next.is_active = true;
  212. this.config.issueboard.current_issue_id = next.id;
  213. return {
  214. success: true,
  215. issue_id: next.id,
  216. message: `Activated issue: ${next.title}`,
  217. };
  218. }
  219. completeCurrentIssue(): PBLToolResult {
  220. const current = this.config.issueboard.issues.find((i) => i.is_active);
  221. if (!current) {
  222. return { success: false, error: 'No active issue to complete.' };
  223. }
  224. current.is_done = true;
  225. current.is_active = false;
  226. this.config.issueboard.current_issue_id = null;
  227. return {
  228. success: true,
  229. message: `Issue "${current.id}" marked as complete.`,
  230. };
  231. }
  232. }