@taktdev/coda 1.0.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/dist/client.d.ts +20 -0
- package/dist/client.js +66 -0
- package/dist/formatters.d.ts +5 -0
- package/dist/formatters.js +60 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +29 -0
- package/dist/tools/add-comment.d.ts +3 -0
- package/dist/tools/add-comment.js +11 -0
- package/dist/tools/get-task.d.ts +3 -0
- package/dist/tools/get-task.js +21 -0
- package/dist/tools/list-projects.d.ts +3 -0
- package/dist/tools/list-projects.js +7 -0
- package/dist/tools/list-tasks.d.ts +3 -0
- package/dist/tools/list-tasks.js +12 -0
- package/dist/tools/update-task.d.ts +3 -0
- package/dist/tools/update-task.js +24 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +1 -0
- package/package.json +41 -0
- package/scripts/postinstall.js +52 -0
- package/skills/qa-approve/SKILL.md +110 -0
- package/skills/qa-remediate/SKILL.md +211 -0
- package/skills/qa-resume/SKILL.md +84 -0
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TasksResponse, TaskResponse, CommentsResponse, ProjectsResponse } from './types.js';
|
|
2
|
+
export declare class BugherdClient {
|
|
3
|
+
private baseUrl;
|
|
4
|
+
private authHeader;
|
|
5
|
+
private defaultProjectId?;
|
|
6
|
+
private userEmail?;
|
|
7
|
+
constructor(apiKey: string, defaultProjectId?: string, userEmail?: string);
|
|
8
|
+
private request;
|
|
9
|
+
private projectPath;
|
|
10
|
+
listTasks(params: {
|
|
11
|
+
projectId?: string;
|
|
12
|
+
status?: string;
|
|
13
|
+
page?: number;
|
|
14
|
+
}): Promise<TasksResponse>;
|
|
15
|
+
getTask(taskId: number, projectId?: string): Promise<TaskResponse>;
|
|
16
|
+
getComments(taskId: number, projectId?: string): Promise<CommentsResponse>;
|
|
17
|
+
updateTask(taskId: number, data: Record<string, unknown>, projectId?: string): Promise<TaskResponse>;
|
|
18
|
+
addComment(taskId: number, text: string, projectId?: string): Promise<void>;
|
|
19
|
+
listProjects(): Promise<ProjectsResponse>;
|
|
20
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export class BugherdClient {
|
|
2
|
+
baseUrl = 'https://www.bugherd.com/api_v2';
|
|
3
|
+
authHeader;
|
|
4
|
+
defaultProjectId;
|
|
5
|
+
userEmail;
|
|
6
|
+
constructor(apiKey, defaultProjectId, userEmail) {
|
|
7
|
+
this.authHeader = `Basic ${Buffer.from(`${apiKey}:x`).toString('base64')}`;
|
|
8
|
+
this.defaultProjectId = defaultProjectId;
|
|
9
|
+
this.userEmail = userEmail;
|
|
10
|
+
}
|
|
11
|
+
async request(path, options) {
|
|
12
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
13
|
+
...options,
|
|
14
|
+
headers: {
|
|
15
|
+
'Authorization': this.authHeader,
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
...options?.headers,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
const body = await response.text();
|
|
22
|
+
throw new Error(`Bugherd API ${response.status}: ${body}`);
|
|
23
|
+
}
|
|
24
|
+
return response.json();
|
|
25
|
+
}
|
|
26
|
+
projectPath(projectId) {
|
|
27
|
+
const id = projectId ?? this.defaultProjectId;
|
|
28
|
+
if (!id)
|
|
29
|
+
throw new Error('No project_id provided and BUGHERD_PROJECT_ID not set');
|
|
30
|
+
return `/projects/${id}`;
|
|
31
|
+
}
|
|
32
|
+
async listTasks(params) {
|
|
33
|
+
const query = new URLSearchParams();
|
|
34
|
+
if (params.status)
|
|
35
|
+
query.set('status', params.status);
|
|
36
|
+
if (params.page)
|
|
37
|
+
query.set('page', String(params.page));
|
|
38
|
+
const qs = query.toString();
|
|
39
|
+
return this.request(`${this.projectPath(params.projectId)}/tasks${qs ? `?${qs}` : ''}`);
|
|
40
|
+
}
|
|
41
|
+
async getTask(taskId, projectId) {
|
|
42
|
+
return this.request(`${this.projectPath(projectId)}/tasks/${taskId}`);
|
|
43
|
+
}
|
|
44
|
+
async getComments(taskId, projectId) {
|
|
45
|
+
return this.request(`${this.projectPath(projectId)}/tasks/${taskId}/comments`);
|
|
46
|
+
}
|
|
47
|
+
async updateTask(taskId, data, projectId) {
|
|
48
|
+
const task = this.userEmail ? { ...data, updater_email: this.userEmail } : data;
|
|
49
|
+
return this.request(`${this.projectPath(projectId)}/tasks/${taskId}`, {
|
|
50
|
+
method: 'PUT',
|
|
51
|
+
body: JSON.stringify({ task }),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async addComment(taskId, text, projectId) {
|
|
55
|
+
const comment = { text };
|
|
56
|
+
if (this.userEmail)
|
|
57
|
+
comment.email = this.userEmail;
|
|
58
|
+
await this.request(`${this.projectPath(projectId)}/tasks/${taskId}/comments`, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
body: JSON.stringify({ comment }),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async listProjects() {
|
|
64
|
+
return this.request('/projects');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { BugherdTask, BugherdComment, BugherdProject, BugherdMeta } from './types.js';
|
|
2
|
+
export declare function formatTask(task: BugherdTask): string;
|
|
3
|
+
export declare function formatTaskList(tasks: BugherdTask[], meta: BugherdMeta): string;
|
|
4
|
+
export declare function formatComments(comments: BugherdComment[]): string;
|
|
5
|
+
export declare function formatProjects(projects: BugherdProject[]): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export function formatTask(task) {
|
|
2
|
+
const lines = [
|
|
3
|
+
`# BH-${task.local_task_id}: Task #${task.id}`,
|
|
4
|
+
'',
|
|
5
|
+
`- **Status:** ${task.status}`,
|
|
6
|
+
`- **Priority:** ${task.priority}`,
|
|
7
|
+
`- **Admin link:** ${task.admin_link}`,
|
|
8
|
+
];
|
|
9
|
+
if (task.url) {
|
|
10
|
+
lines.push(`- **Page URL:** ${task.url}`);
|
|
11
|
+
}
|
|
12
|
+
if (task.screenshot_url) {
|
|
13
|
+
lines.push(`- **Screenshot:** ${task.screenshot_url}`);
|
|
14
|
+
}
|
|
15
|
+
if (task.tag_names.length > 0) {
|
|
16
|
+
lines.push(`- **Tags:** ${task.tag_names.join(', ')}`);
|
|
17
|
+
}
|
|
18
|
+
if (task.assigned_to_id) {
|
|
19
|
+
lines.push(`- **Assigned to:** user #${task.assigned_to_id}`);
|
|
20
|
+
}
|
|
21
|
+
lines.push('', '## Description', '', task.description || '_No description_');
|
|
22
|
+
return lines.join('\n');
|
|
23
|
+
}
|
|
24
|
+
export function formatTaskList(tasks, meta) {
|
|
25
|
+
if (tasks.length === 0)
|
|
26
|
+
return '_No tasks found._';
|
|
27
|
+
const lines = [
|
|
28
|
+
`**${meta.total_count} tasks** (page ${meta.current_page}/${meta.total_pages})`,
|
|
29
|
+
'',
|
|
30
|
+
'| # | ID | Priority | Description |',
|
|
31
|
+
'|---|-----|----------|-------------|',
|
|
32
|
+
];
|
|
33
|
+
for (const task of tasks) {
|
|
34
|
+
const desc = task.description
|
|
35
|
+
? task.description.slice(0, 80).replace(/\n/g, ' ') + (task.description.length > 80 ? '...' : '')
|
|
36
|
+
: '_No description_';
|
|
37
|
+
lines.push(`| BH-${task.local_task_id} | ${task.id} | ${task.priority} | ${desc} |`);
|
|
38
|
+
}
|
|
39
|
+
return lines.join('\n');
|
|
40
|
+
}
|
|
41
|
+
export function formatComments(comments) {
|
|
42
|
+
if (comments.length === 0)
|
|
43
|
+
return '_No comments._';
|
|
44
|
+
return comments.map(c => {
|
|
45
|
+
const date = new Date(c.created_at).toLocaleDateString();
|
|
46
|
+
return `**User #${c.user_id}** (${date}):\n${c.text}`;
|
|
47
|
+
}).join('\n\n---\n\n');
|
|
48
|
+
}
|
|
49
|
+
export function formatProjects(projects) {
|
|
50
|
+
if (projects.length === 0)
|
|
51
|
+
return '_No projects found._';
|
|
52
|
+
const lines = [
|
|
53
|
+
'| ID | Name | URL | Active |',
|
|
54
|
+
'|----|------|-----|--------|',
|
|
55
|
+
];
|
|
56
|
+
for (const p of projects) {
|
|
57
|
+
lines.push(`| ${p.id} | ${p.name} | ${p.devurl || '_none_'} | ${p.is_active ? 'Yes' : 'No'} |`);
|
|
58
|
+
}
|
|
59
|
+
return lines.join('\n');
|
|
60
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import { BugherdClient } from './client.js';
|
|
6
|
+
import { registerListTasks } from './tools/list-tasks.js';
|
|
7
|
+
import { registerGetTask } from './tools/get-task.js';
|
|
8
|
+
import { registerUpdateTask } from './tools/update-task.js';
|
|
9
|
+
import { registerAddComment } from './tools/add-comment.js';
|
|
10
|
+
import { registerListProjects } from './tools/list-projects.js';
|
|
11
|
+
const apiKey = process.env.BUGHERD_API_KEY;
|
|
12
|
+
if (!apiKey) {
|
|
13
|
+
console.error('BUGHERD_API_KEY environment variable is required');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const projectId = process.env.BUGHERD_PROJECT_ID;
|
|
17
|
+
const userEmail = process.env.BUGHERD_USER_EMAIL;
|
|
18
|
+
const client = new BugherdClient(apiKey, projectId, userEmail);
|
|
19
|
+
const server = new McpServer({
|
|
20
|
+
name: 'bugherd',
|
|
21
|
+
version: '0.1.0',
|
|
22
|
+
});
|
|
23
|
+
registerListTasks(server, client);
|
|
24
|
+
registerGetTask(server, client);
|
|
25
|
+
registerUpdateTask(server, client);
|
|
26
|
+
registerAddComment(server, client);
|
|
27
|
+
registerListProjects(server, client);
|
|
28
|
+
const transport = new StdioServerTransport();
|
|
29
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function registerAddComment(server, client) {
|
|
3
|
+
server.tool('add_comment', 'Add a comment to a Bugherd task.', {
|
|
4
|
+
task_id: z.number().int().positive().describe('The task ID'),
|
|
5
|
+
text: z.string().describe('Comment text'),
|
|
6
|
+
project_id: z.string().optional().describe('Bugherd project ID (uses default if omitted)'),
|
|
7
|
+
}, async ({ task_id, text, project_id }) => {
|
|
8
|
+
await client.addComment(task_id, text, project_id);
|
|
9
|
+
return { content: [{ type: 'text', text: `Comment added to task #${task_id}.` }] };
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { formatTask, formatComments } from '../formatters.js';
|
|
3
|
+
export function registerGetTask(server, client) {
|
|
4
|
+
server.tool('get_task', 'Get a Bugherd task with full details and comments.', {
|
|
5
|
+
task_id: z.number().int().positive().describe('The task ID'),
|
|
6
|
+
project_id: z.string().optional().describe('Bugherd project ID (uses default if omitted)'),
|
|
7
|
+
}, async ({ task_id, project_id }) => {
|
|
8
|
+
const [taskResult, commentsResult] = await Promise.all([
|
|
9
|
+
client.getTask(task_id, project_id),
|
|
10
|
+
client.getComments(task_id, project_id),
|
|
11
|
+
]);
|
|
12
|
+
const text = [
|
|
13
|
+
formatTask(taskResult.task),
|
|
14
|
+
'',
|
|
15
|
+
'## Comments',
|
|
16
|
+
'',
|
|
17
|
+
formatComments(commentsResult.comments),
|
|
18
|
+
].join('\n');
|
|
19
|
+
return { content: [{ type: 'text', text }] };
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { formatProjects } from '../formatters.js';
|
|
2
|
+
export function registerListProjects(server, client) {
|
|
3
|
+
server.tool('list_projects', 'List all Bugherd projects accessible with the current API key.', {}, async () => {
|
|
4
|
+
const result = await client.listProjects();
|
|
5
|
+
return { content: [{ type: 'text', text: formatProjects(result.projects) }] };
|
|
6
|
+
});
|
|
7
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { formatTaskList } from '../formatters.js';
|
|
3
|
+
export function registerListTasks(server, client) {
|
|
4
|
+
server.tool('list_tasks', 'List Bugherd tasks filtered by status. Defaults to "development" column.', {
|
|
5
|
+
project_id: z.string().optional().describe('Bugherd project ID (uses default if omitted)'),
|
|
6
|
+
status: z.string().default('development').describe('Task status filter: backlog, todo, doing, done, closed, or custom status name'),
|
|
7
|
+
page: z.number().int().positive().default(1).describe('Page number for pagination'),
|
|
8
|
+
}, async ({ project_id, status, page }) => {
|
|
9
|
+
const result = await client.listTasks({ projectId: project_id, status, page });
|
|
10
|
+
return { content: [{ type: 'text', text: formatTaskList(result.tasks, result.meta) }] };
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { formatTask } from '../formatters.js';
|
|
3
|
+
export function registerUpdateTask(server, client) {
|
|
4
|
+
server.tool('update_task', 'Update a Bugherd task (status, priority, or assignment).', {
|
|
5
|
+
task_id: z.number().int().positive().describe('The task ID'),
|
|
6
|
+
project_id: z.string().optional().describe('Bugherd project ID (uses default if omitted)'),
|
|
7
|
+
status: z.string().optional().describe('New status'),
|
|
8
|
+
priority: z.string().optional().describe('New priority: critical, important, normal, minor'),
|
|
9
|
+
assigned_to_id: z.number().int().optional().describe('User ID to assign to'),
|
|
10
|
+
}, async ({ task_id, project_id, status, priority, assigned_to_id }) => {
|
|
11
|
+
const data = {};
|
|
12
|
+
if (status !== undefined)
|
|
13
|
+
data.status = status;
|
|
14
|
+
if (priority !== undefined)
|
|
15
|
+
data.priority = priority;
|
|
16
|
+
if (assigned_to_id !== undefined)
|
|
17
|
+
data.assigned_to_id = assigned_to_id;
|
|
18
|
+
if (Object.keys(data).length === 0) {
|
|
19
|
+
return { content: [{ type: 'text', text: 'Error: At least one field (status, priority, assigned_to_id) must be provided.' }] };
|
|
20
|
+
}
|
|
21
|
+
const result = await client.updateTask(task_id, data, project_id);
|
|
22
|
+
return { content: [{ type: 'text', text: `Task updated.\n\n${formatTask(result.task)}` }] };
|
|
23
|
+
});
|
|
24
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface BugherdTask {
|
|
2
|
+
id: number;
|
|
3
|
+
local_task_id: number;
|
|
4
|
+
status: string;
|
|
5
|
+
priority: string;
|
|
6
|
+
description: string;
|
|
7
|
+
tag_names: string[];
|
|
8
|
+
external_id: string | null;
|
|
9
|
+
requester_id: number;
|
|
10
|
+
assigned_to_id: number | null;
|
|
11
|
+
created_at: string;
|
|
12
|
+
updated_at: string;
|
|
13
|
+
admin_link: string;
|
|
14
|
+
screenshot_url: string | null;
|
|
15
|
+
url: string | null;
|
|
16
|
+
}
|
|
17
|
+
export interface BugherdComment {
|
|
18
|
+
id: number;
|
|
19
|
+
user_id: number;
|
|
20
|
+
text: string;
|
|
21
|
+
created_at: string;
|
|
22
|
+
updated_at: string;
|
|
23
|
+
}
|
|
24
|
+
export interface BugherdProject {
|
|
25
|
+
id: number;
|
|
26
|
+
name: string;
|
|
27
|
+
devurl: string;
|
|
28
|
+
is_active: boolean;
|
|
29
|
+
is_public: boolean;
|
|
30
|
+
members: {
|
|
31
|
+
id: number;
|
|
32
|
+
display_name: string;
|
|
33
|
+
email: string;
|
|
34
|
+
avatar_url: string;
|
|
35
|
+
}[];
|
|
36
|
+
}
|
|
37
|
+
export interface BugherdMeta {
|
|
38
|
+
count: number;
|
|
39
|
+
total_count: number;
|
|
40
|
+
total_pages: number;
|
|
41
|
+
current_page: number;
|
|
42
|
+
}
|
|
43
|
+
export interface TasksResponse {
|
|
44
|
+
tasks: BugherdTask[];
|
|
45
|
+
meta: BugherdMeta;
|
|
46
|
+
}
|
|
47
|
+
export interface TaskResponse {
|
|
48
|
+
task: BugherdTask;
|
|
49
|
+
}
|
|
50
|
+
export interface CommentsResponse {
|
|
51
|
+
comments: BugherdComment[];
|
|
52
|
+
}
|
|
53
|
+
export interface ProjectsResponse {
|
|
54
|
+
projects: BugherdProject[];
|
|
55
|
+
}
|
|
56
|
+
export interface ProjectResponse {
|
|
57
|
+
project: BugherdProject;
|
|
58
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@taktdev/coda",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Bugherd QA automation MCP server",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"coda": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"skills",
|
|
13
|
+
"scripts"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"dev": "tsc --watch",
|
|
18
|
+
"postinstall": "node scripts/postinstall.js",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"author": {
|
|
25
|
+
"name": "Takt",
|
|
26
|
+
"email": "developers@takt.inc",
|
|
27
|
+
"url": "https://takt.inc"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
31
|
+
"dotenv": "^17.3.1",
|
|
32
|
+
"zod": "^3.24.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.0.0",
|
|
36
|
+
"typescript": "^5.7.0"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { cpSync, mkdirSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join, dirname } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const projectRoot = process.env.INIT_CWD;
|
|
9
|
+
|
|
10
|
+
if (!projectRoot) {
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// --- Skills ---
|
|
15
|
+
|
|
16
|
+
const skillsSource = join(__dirname, '..', 'skills');
|
|
17
|
+
const skillsDest = join(projectRoot, '.claude', 'skills');
|
|
18
|
+
|
|
19
|
+
const skills = ['qa-remediate', 'qa-resume', 'qa-approve'];
|
|
20
|
+
|
|
21
|
+
for (const skill of skills) {
|
|
22
|
+
const src = join(skillsSource, skill);
|
|
23
|
+
const dest = join(skillsDest, skill);
|
|
24
|
+
|
|
25
|
+
if (!existsSync(src)) continue;
|
|
26
|
+
|
|
27
|
+
mkdirSync(dest, { recursive: true });
|
|
28
|
+
cpSync(src, dest, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log('@taktdev/coda: installed skills → .claude/skills/qa-{remediate,resume,approve}');
|
|
32
|
+
|
|
33
|
+
// --- MCP config ---
|
|
34
|
+
|
|
35
|
+
const mcpPath = join(projectRoot, '.mcp.json');
|
|
36
|
+
const bugherdServer = {
|
|
37
|
+
command: 'npx',
|
|
38
|
+
args: ['-y', '@taktdev/coda'],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (!existsSync(mcpPath)) {
|
|
42
|
+
writeFileSync(mcpPath, JSON.stringify({ mcpServers: { bugherd: bugherdServer } }, null, 2) + '\n');
|
|
43
|
+
console.log('@taktdev/coda: created .mcp.json with bugherd server');
|
|
44
|
+
} else {
|
|
45
|
+
const config = JSON.parse(readFileSync(mcpPath, 'utf-8'));
|
|
46
|
+
if (!config.mcpServers?.bugherd) {
|
|
47
|
+
config.mcpServers = config.mcpServers || {};
|
|
48
|
+
config.mcpServers.bugherd = bugherdServer;
|
|
49
|
+
writeFileSync(mcpPath, JSON.stringify(config, null, 2) + '\n');
|
|
50
|
+
console.log('@taktdev/coda: added bugherd server to existing .mcp.json');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: qa-approve
|
|
3
|
+
description: "Commit and tag fixes for Bugherd tickets. Triggers Ready for Review in Bugherd."
|
|
4
|
+
argument-hint: "<ticket-id> [ticket-id...]"
|
|
5
|
+
user-invocable: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# /qa-approve
|
|
9
|
+
|
|
10
|
+
Merge fix branches into main/current branch with tagged commits that trigger Bugherd's GitHub integration, then shut down the responsible agent to free a team slot.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
/qa-approve 42
|
|
16
|
+
/qa-approve 42 43 44
|
|
17
|
+
/qa-approve all
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Argument Parsing
|
|
21
|
+
|
|
22
|
+
- **Single ticket:** `/qa-approve 42`
|
|
23
|
+
- **Multiple tickets:** `/qa-approve 42 43 44`
|
|
24
|
+
- **All pending:** `/qa-approve all` — finds all `.takty/qa/BH-*.md` files that have NOT been renamed to `-fixed` (i.e., excludes `BH-*-fixed.md`)
|
|
25
|
+
|
|
26
|
+
## Flow
|
|
27
|
+
|
|
28
|
+
For each ticket:
|
|
29
|
+
|
|
30
|
+
### 1. Read Remediation File
|
|
31
|
+
|
|
32
|
+
Read `.takty/qa/BH-{id}.md` to get:
|
|
33
|
+
- The branch name (from the "Worktree" section)
|
|
34
|
+
- The short description (from the `# BH-{id}: {title}` heading)
|
|
35
|
+
- The list of changes made
|
|
36
|
+
- The **agent name** (from the "Agent" section)
|
|
37
|
+
|
|
38
|
+
### 2. Verify Branch Exists
|
|
39
|
+
|
|
40
|
+
Check that the branch exists:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
git branch --list {branch-name}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
If the branch doesn't exist, report which tickets couldn't be found and skip them. Continue with remaining tickets.
|
|
47
|
+
|
|
48
|
+
### 3. Merge the Branch
|
|
49
|
+
|
|
50
|
+
Merge the ticket's branch into the current branch (usually `main`):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git merge {branch-name} --no-ff -m "{short description} [Ready for Review BH-{id}]"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This creates a merge commit with the Bugherd tag in a single step.
|
|
57
|
+
|
|
58
|
+
If there are merge conflicts:
|
|
59
|
+
- For simple conflicts (whitespace, non-overlapping changes): resolve automatically
|
|
60
|
+
- For complex conflicts: ask the user to resolve manually
|
|
61
|
+
|
|
62
|
+
### 4. Verify Tagged Commit
|
|
63
|
+
|
|
64
|
+
Confirm the merge commit was created with the correct tag format:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
git log -1 --oneline
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Tag format rules:**
|
|
71
|
+
- The Bugherd tag **must be in the commit subject line** (first line), not in the body/description
|
|
72
|
+
- Do NOT add a `Co-Authored-By` line — it can interfere with the tag parsing
|
|
73
|
+
- **Case-sensitive** — must be `Ready for Review` (capital R on Review), not `Ready for review`
|
|
74
|
+
- Multiple tickets in one commit: `{short description} [Ready for Review BH-1,2,3]`
|
|
75
|
+
|
|
76
|
+
### 5. Mark Remediation File as Fixed
|
|
77
|
+
|
|
78
|
+
Rename the remediation file to append `-fixed`:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
mv .takty/qa/BH-{id}.md .takty/qa/BH-{id}-fixed.md
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 6. Shut Down the Agent
|
|
85
|
+
|
|
86
|
+
If the remediation file has an **Agent** name and that agent is still part of the team:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
SendMessage type: "shutdown_request" to the agent
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This frees a team slot. If more tickets are waiting in the Bugherd development column, the team lead can spawn a new agent.
|
|
93
|
+
|
|
94
|
+
### 7. Report Result
|
|
95
|
+
|
|
96
|
+
Report for each ticket:
|
|
97
|
+
- Commit hash
|
|
98
|
+
- Branch merged
|
|
99
|
+
- Agent shut down (or already idle/gone)
|
|
100
|
+
- Remaining team slots available (out of 10)
|
|
101
|
+
|
|
102
|
+
## Important Notes
|
|
103
|
+
|
|
104
|
+
- **One merge commit per ticket** so each ticket moves independently in Bugherd's GitHub integration
|
|
105
|
+
- **Do NOT push** after merging. Report the commits created and tell the user to push when ready.
|
|
106
|
+
- Process tickets sequentially to avoid merge conflicts between branches
|
|
107
|
+
- If using `/qa-approve all`, first list which tickets will be approved and their descriptions before proceeding
|
|
108
|
+
- The commit message format `{description} [Ready for Review BH-{id}]` is required by Bugherd's GitHub integration — tag must be in the subject line, case-sensitive
|
|
109
|
+
- **Do NOT delete the team** after approving. The team persists until the user explicitly requests cleanup. Other agents may still be working or idle.
|
|
110
|
+
- After shutting down an agent, check if there are unassigned tickets that could fill the freed slot
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: qa-remediate
|
|
3
|
+
description: "Fetch Bugherd tickets and fix bugs. Spawns parallel agents for batch mode."
|
|
4
|
+
argument-hint: "[ticket-id] [context/hints]"
|
|
5
|
+
user-invocable: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# /qa-remediate
|
|
9
|
+
|
|
10
|
+
Fetch Bugherd tickets, group by similarity, and spawn team members to fix them. Up to 10 agents work in parallel, each on its own branch.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
/qa-remediate
|
|
16
|
+
/qa-remediate 42
|
|
17
|
+
/qa-remediate 42 The button is misaligned on mobile
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Argument Parsing
|
|
21
|
+
|
|
22
|
+
- **No arguments** → Batch mode: fetch all tickets from the "development" column
|
|
23
|
+
- **Number only** → Single ticket mode: spawn one agent for that ticket
|
|
24
|
+
- **Number + text** → Single ticket mode with context hints to guide the fix
|
|
25
|
+
|
|
26
|
+
## Team Lead Role
|
|
27
|
+
|
|
28
|
+
The team lead (you, the main session) is **only** responsible for:
|
|
29
|
+
|
|
30
|
+
1. **Fetching** tickets from Bugherd
|
|
31
|
+
2. **Grouping** tickets by similarity
|
|
32
|
+
3. **Spawning and assigning** team members
|
|
33
|
+
4. **Relaying** agent reports and status to the user
|
|
34
|
+
5. **Managing slots** — spawn new agents as slots free up (max 10 concurrent)
|
|
35
|
+
|
|
36
|
+
The team lead does NOT fix tickets itself. All remediation work is done by spawned agents.
|
|
37
|
+
|
|
38
|
+
## Batch Mode Flow
|
|
39
|
+
|
|
40
|
+
1. Call `list_tasks` MCP tool with `status="development"` to get tickets in the development column. **Only take the first 10 tickets** to avoid overloading context. If more exist, note the count and fetch the next batch after the current tickets are processed.
|
|
41
|
+
2. Create a team: `TeamCreate` with name `qa-remediate` (if not already active)
|
|
42
|
+
3. **Group tickets by similarity:**
|
|
43
|
+
- Same block/module/component
|
|
44
|
+
- Similar issue type across different components (e.g., "arrow icon wrong" on multiple blocks)
|
|
45
|
+
- Related CSS/styling changes
|
|
46
|
+
- Present the grouping to the user before spawning agents
|
|
47
|
+
4. Create a `TaskCreate` for each ticket
|
|
48
|
+
5. Spawn up to 10 remediator agents. Each agent MUST use:
|
|
49
|
+
- `isolation: "worktree"` — each agent works in its own isolated worktree
|
|
50
|
+
- `model: "opus"` — Opus 4.6 for complex reasoning
|
|
51
|
+
- `subagent_type: "general-purpose"`
|
|
52
|
+
- `team_name: "qa-remediate"`
|
|
53
|
+
- `name:` a descriptive name based on the ticket group (e.g., `remediator-hero`, `remediator-arrows`)
|
|
54
|
+
- Grouped tickets can go to the same agent if closely related
|
|
55
|
+
6. Each remediator agent follows the **Remediator Steps** below
|
|
56
|
+
7. As agents finish and report back, relay their summaries to the user
|
|
57
|
+
8. **Do NOT shut down the team or agents.** Agents wait idle until the user reviews and approves via `/qa-approve`.
|
|
58
|
+
|
|
59
|
+
## Single Ticket Mode Flow
|
|
60
|
+
|
|
61
|
+
Same as batch but for one ticket. Still spawns an agent — the team lead does not fix it directly.
|
|
62
|
+
|
|
63
|
+
1. Create a team: `TeamCreate` with name `qa-remediate` (if not already active)
|
|
64
|
+
2. Create a `TaskCreate` for the ticket
|
|
65
|
+
3. Spawn one remediator agent (same settings as batch mode)
|
|
66
|
+
4. Agent follows the **Remediator Steps** below
|
|
67
|
+
5. Relay the agent's report to the user when done
|
|
68
|
+
|
|
69
|
+
## Slot Management
|
|
70
|
+
|
|
71
|
+
- Maximum 10 concurrent team members
|
|
72
|
+
- When a ticket is approved via `/qa-approve`, that agent is shut down, freeing a slot
|
|
73
|
+
- If more tickets remain in the current batch, the team lead can spawn a new agent for the next ticket
|
|
74
|
+
- When all tickets from the current batch are assigned or completed, fetch the next 10 from Bugherd via `list_tasks`
|
|
75
|
+
- Check `TaskList` and team config to track active vs idle agents
|
|
76
|
+
|
|
77
|
+
## Remediator Steps
|
|
78
|
+
|
|
79
|
+
For each ticket:
|
|
80
|
+
|
|
81
|
+
### 1. Gather Context
|
|
82
|
+
- Call `get_task` MCP tool with the ticket ID to get full ticket details and comments
|
|
83
|
+
- If a screenshot URL is present, call `WebFetch` on it to view the screenshot
|
|
84
|
+
- Note the priority, description, and any comments from the team
|
|
85
|
+
|
|
86
|
+
### 2. Find Relevant Code
|
|
87
|
+
- Use `Grep`, `Glob`, and `Read` to search the codebase for relevant code
|
|
88
|
+
- Look at block templates, Blade views, CSS, and JS files related to the reported issue
|
|
89
|
+
- Read the files to understand the current implementation
|
|
90
|
+
|
|
91
|
+
### 3. Assess Risk
|
|
92
|
+
|
|
93
|
+
Categorize the fix as **safe** or **risky**:
|
|
94
|
+
|
|
95
|
+
| Safe (implement directly) | Risky (flag for manual review) |
|
|
96
|
+
|---|---|
|
|
97
|
+
| CSS/Tailwind class changes | PHP logic changes |
|
|
98
|
+
| Copy/text updates | JavaScript state management |
|
|
99
|
+
| Template markup adjustments | Data model / attribute changes |
|
|
100
|
+
| Spacing, colors, typography | Database queries or post meta |
|
|
101
|
+
| Visibility/responsive tweaks | Authentication / permissions |
|
|
102
|
+
| Changes scoped to a single block | **Changes to shared components** (`block-wrapper`, `block-header`, `button`, `eyebrow`) — these affect every block that uses them |
|
|
103
|
+
|
|
104
|
+
### 4. Implement or Flag
|
|
105
|
+
|
|
106
|
+
**Safe fixes:** Implement the fix directly in the worktree.
|
|
107
|
+
|
|
108
|
+
**Risky fixes:** Do NOT implement. Instead:
|
|
109
|
+
- Document the recommended fix in the remediation file
|
|
110
|
+
- Explain why it's flagged in the Analysis section
|
|
111
|
+
- Mark the status as "Flagged for manual review"
|
|
112
|
+
|
|
113
|
+
### 5. Create Test Page
|
|
114
|
+
|
|
115
|
+
Always attempt to create a draft page for QA verification. If `ddev exec` fails, note the command in the remediation file so it can be run later.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
ddev exec wp post create --post_type=page --post_title="QA: BH-{id}" --post_status=draft
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
When the test page needs block content, assign the block markup to a shell variable first to avoid escaping issues with `!` in `<!-- wp:` markup:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
CONTENT='<!-- wp:paragraph --><p>Test content for BH-{id}</p><!-- /wp:paragraph -->'
|
|
125
|
+
ddev exec wp post create --post_type=page --post_title="QA: BH-{id}" --post_status=draft --post_content="$CONTENT"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 6. Write Remediation File
|
|
129
|
+
|
|
130
|
+
Create `.takty/qa/BH-{id}.md` using this template:
|
|
131
|
+
|
|
132
|
+
```markdown
|
|
133
|
+
# BH-{id}: {title/short description}
|
|
134
|
+
|
|
135
|
+
## Ticket
|
|
136
|
+
- **Bugherd ID:** {id}
|
|
137
|
+
- **Priority:** {priority}
|
|
138
|
+
- **Admin link:** {admin_link}
|
|
139
|
+
- **Screenshot:** {screenshot_url}
|
|
140
|
+
|
|
141
|
+
## Agent
|
|
142
|
+
- **Name:** {agent name, e.g. remediator-hero}
|
|
143
|
+
- **Group:** {ticket group description, e.g. "Hero block styling"}
|
|
144
|
+
|
|
145
|
+
## Description
|
|
146
|
+
{from Bugherd}
|
|
147
|
+
|
|
148
|
+
## Analysis
|
|
149
|
+
{root cause, affected files}
|
|
150
|
+
|
|
151
|
+
## Changes Made
|
|
152
|
+
| File | Change |
|
|
153
|
+
|------|--------|
|
|
154
|
+
| `path/to/file.php` | Fixed XYZ |
|
|
155
|
+
|
|
156
|
+
## Test Page
|
|
157
|
+
- **URL:** {site_url}/qa-bh-{id}/
|
|
158
|
+
- **What to verify:** {instructions}
|
|
159
|
+
|
|
160
|
+
## Worktree
|
|
161
|
+
- **Branch:** {branch name from worktree}
|
|
162
|
+
- **Path:** {worktree path}
|
|
163
|
+
|
|
164
|
+
## Status
|
|
165
|
+
- [x] Analyzed
|
|
166
|
+
- [x] Fixed
|
|
167
|
+
- [ ] Approved
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
For risky/flagged tickets, replace "Fixed" with "Flagged for manual review" and explain why in the Analysis section.
|
|
171
|
+
|
|
172
|
+
### 7. Commit Changes
|
|
173
|
+
|
|
174
|
+
Create a branch and commit all changes:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
git checkout -b qa/bh-{id}
|
|
178
|
+
git add -A
|
|
179
|
+
git commit -m "fix: BH-{id} {short description}"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Do NOT add a `Co-Authored-By` line.** Commits from QA workflows must not include co-author trailers.
|
|
183
|
+
|
|
184
|
+
### 8. Report and Wait
|
|
185
|
+
|
|
186
|
+
After committing:
|
|
187
|
+
1. Send a message to the team lead with a summary (ticket ID, what was fixed, branch name, any concerns)
|
|
188
|
+
2. Mark the task as completed
|
|
189
|
+
3. **Wait idle** — do NOT shut down. The user will review the report and either approve or request changes via `/qa-resume`.
|
|
190
|
+
|
|
191
|
+
## Code Style Rules
|
|
192
|
+
|
|
193
|
+
When writing fixes, follow these project conventions:
|
|
194
|
+
|
|
195
|
+
- **Tailwind over custom CSS** — use utility classes, not BEM-style class names or `style.css` files (unless the rule truly can't be expressed in Tailwind)
|
|
196
|
+
- **Tailwind scale over arbitrary values** — use the nearest scale value (`pr-16`, `gap-4`, `mt-6`) instead of arbitrary pixel values (`pr-[68px]`, `gap-[14px]`). Pixel-perfect fidelity is not required; round to the closest Tailwind step.
|
|
197
|
+
- **`cn()` takes a single object** — `cn({ 'base': true, 'conditional': flag })`, NOT `cn('base', { 'conditional': flag })`
|
|
198
|
+
- **Blade: use `@class` directive** for conditional classes and `$attributes->class()` for mergeable component classes
|
|
199
|
+
- **Use existing components** — check for shared Blade components (`x-button`, `x-block-header`, `x-eyebrow`) and editor components (`BlockHeader`, `BlockWrapper`) before building inline
|
|
200
|
+
|
|
201
|
+
For the full checklist, see `.takty/code-standards-checklist.md`.
|
|
202
|
+
|
|
203
|
+
## Important Notes
|
|
204
|
+
|
|
205
|
+
- The `.takty/qa/` directory is gitignored — remediation files are local working docs only, they do not get committed
|
|
206
|
+
- ALWAYS use `isolation: "worktree"` when spawning remediator agents via the Task tool. Each agent's worktree gets its own branch automatically.
|
|
207
|
+
- Remediator agents MUST commit their changes before reporting back
|
|
208
|
+
- For risky fixes: still create the `.takty/qa/BH-{id}.md` file but mark it as "Flagged for manual review"
|
|
209
|
+
- When creating test pages with `wp post create`, assign block content to a shell variable first to avoid escaping issues with `!` in `<!-- wp:` markup
|
|
210
|
+
- **Do NOT delete the team** until the user explicitly tells you to. The team persists across `/qa-approve` and `/qa-resume` calls.
|
|
211
|
+
- Each remediation file MUST include the **Agent** section with the team member's name so `/qa-resume` can delegate back to it
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: qa-resume
|
|
3
|
+
description: "Resume work on a Bugherd ticket from where it left off."
|
|
4
|
+
argument-hint: "<ticket-number> [context/hints]"
|
|
5
|
+
user-invocable: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# /qa-resume
|
|
9
|
+
|
|
10
|
+
Resume work on a Bugherd ticket by delegating to the original idle agent, or falling back to a new agent if the original is unavailable.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
/qa-resume 42
|
|
16
|
+
/qa-resume 42 The hover state still looks wrong on mobile
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
A ticket number is **required**. Error if missing.
|
|
20
|
+
|
|
21
|
+
## Flow
|
|
22
|
+
|
|
23
|
+
### 1. Parse Argument
|
|
24
|
+
|
|
25
|
+
Extract the ticket number and any additional context/hints from the argument. If no number is provided, display an error:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
Error: Ticket number required. Usage: /qa-resume <ticket-number>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 2. Read Remediation File
|
|
32
|
+
|
|
33
|
+
Read `.takty/qa/BH-{id}.md` for context:
|
|
34
|
+
- Previous analysis and root cause
|
|
35
|
+
- Changes already made (file paths and descriptions)
|
|
36
|
+
- Branch name and worktree path
|
|
37
|
+
- Current status (what's done, what's pending)
|
|
38
|
+
- **Agent name** (from the "Agent" section)
|
|
39
|
+
|
|
40
|
+
If the file doesn't exist, report that no previous work was found for this ticket and suggest using `/qa-remediate {id}` instead.
|
|
41
|
+
|
|
42
|
+
### 3. Fetch Latest Ticket State
|
|
43
|
+
|
|
44
|
+
Call `get_task` MCP tool with the ticket ID to:
|
|
45
|
+
- Check for new comments since the last session
|
|
46
|
+
- See if priority or status has changed
|
|
47
|
+
- Get any new screenshots or feedback
|
|
48
|
+
|
|
49
|
+
### 4. Delegate to the Original Agent
|
|
50
|
+
|
|
51
|
+
If the remediation file has an **Agent** name (e.g., `remediator-hero`):
|
|
52
|
+
|
|
53
|
+
1. Read the team config at `~/.claude/teams/qa-remediate/config.json` to check if the agent is still a team member
|
|
54
|
+
2. If the agent exists, send it a message via `SendMessage`:
|
|
55
|
+
- Include the user's context/instructions from the `/qa-resume` arguments
|
|
56
|
+
- Include any new ticket comments or feedback from step 3
|
|
57
|
+
- The agent already has the worktree and branch context from its previous work
|
|
58
|
+
3. Relay the agent's response back to the user when it reports back
|
|
59
|
+
|
|
60
|
+
**The team lead does NOT do the fix itself.** Always delegate.
|
|
61
|
+
|
|
62
|
+
### 5. Fallback: Spawn a New Agent
|
|
63
|
+
|
|
64
|
+
If the original agent is not found (shut down, crashed, etc.):
|
|
65
|
+
|
|
66
|
+
1. Spawn a new remediator agent with the same settings as `/qa-remediate`:
|
|
67
|
+
- `isolation: "worktree"`
|
|
68
|
+
- `model: "opus"`
|
|
69
|
+
- `subagent_type: "general-purpose"`
|
|
70
|
+
- `team_name: "qa-remediate"`
|
|
71
|
+
- `name:` reuse the original agent name from the remediation file
|
|
72
|
+
2. Include in the agent prompt:
|
|
73
|
+
- The full remediation file content (previous analysis, changes, branch)
|
|
74
|
+
- The latest ticket state and new comments
|
|
75
|
+
- The user's context/instructions
|
|
76
|
+
- Instructions to check out the existing branch if it still exists, or start fresh if not
|
|
77
|
+
3. The agent follows the same remediation steps: fix, commit, update remediation file, report back, wait idle
|
|
78
|
+
|
|
79
|
+
### 6. Update Remediation File
|
|
80
|
+
|
|
81
|
+
After the agent reports back, ensure the remediation file is updated with:
|
|
82
|
+
- The new agent name (if a fallback agent was spawned)
|
|
83
|
+
- Any new changes in the "Changes Made" table
|
|
84
|
+
- Updated analysis if the approach changed
|