@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.
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
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,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { BugherdClient } from '../client.js';
3
+ export declare function registerAddComment(server: McpServer, client: BugherdClient): void;
@@ -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,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { BugherdClient } from '../client.js';
3
+ export declare function registerGetTask(server: McpServer, client: BugherdClient): void;
@@ -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,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { BugherdClient } from '../client.js';
3
+ export declare function registerListProjects(server: McpServer, client: BugherdClient): void;
@@ -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,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { BugherdClient } from '../client.js';
3
+ export declare function registerListTasks(server: McpServer, client: BugherdClient): void;
@@ -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,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { BugherdClient } from '../client.js';
3
+ export declare function registerUpdateTask(server: McpServer, client: BugherdClient): void;
@@ -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
+ }
@@ -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