Skip to content

Commit d35c3bd

Browse files
authored
feat(gastown): add bug reporting — issue template, UI link, and Mayor tool (#1298)
* feat(gastown): add bug reporting — issue template, UI link, and Mayor gt_report_bug tool Closes #1248 - Add .github/ISSUE_TEMPLATE/gastown-bug.yml with structured fields - Add 'Report a Bug' link in the terminal bar (visible in both user and org layouts) - Add gt_report_bug Mayor tool that searches for duplicates via GitHub API before filing - Add bug reporting guidance to Mayor system prompt - Add tests for gt_report_bug (no token, duplicates, create, failure) * fix(gastown): remove bead dependency tools that call nonexistent client methods gt_bead_add_dependency and gt_bead_remove_dependency called addBeadDependency/removeBeadDependency on MayorGastownClient, but those methods were never implemented. Remove the tools to avoid runtime failures — they can be re-added when the backend support is built. * fix(gastown): use existing labels and handle label permission errors in gt_report_bug The GH_TOKEN in containers may lack permission to create labels. Changed to use existing repo labels (bug, gt:mayor) instead of non-existent ones, and added a fallback that retries without labels on 422 errors. * fix(gastown): align duplicate search query with actual issue labels The search used label:gastown which doesn't match Mayor-filed issues (labeled bug + gt:mayor). Broadened to just label:bug so duplicate detection catches both Mayor-filed and user-filed bug reports. * feat(gastown): add Discord channel to bug report dropdown and Mayor prompt Convert the 'Report a Bug' link into a dropdown with two options: - New GitHub Issue (existing template link) - Discord Channel (Gastown bugs channel) Update the Mayor system prompt to know about the Discord channel so it can direct users there for discussion-style bug reports.
1 parent 67bbe7b commit d35c3bd

5 files changed

Lines changed: 464 additions & 5 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: 'Gastown Bug Report'
2+
description: 'Report a bug with Gastown by Kilo'
3+
labels: ['gastown', 'bug']
4+
body:
5+
- type: textarea
6+
id: description
7+
attributes:
8+
label: What happened?
9+
description: Describe the bug. What did you expect to happen vs. what actually happened?
10+
validations:
11+
required: true
12+
- type: dropdown
13+
id: area
14+
attributes:
15+
label: Area
16+
options:
17+
- Mayor / Chat
18+
- Terminal UI
19+
- Bead Board / Dashboard
20+
- Convoys
21+
- Merge Queue / Refinery
22+
- Agent Dispatch / Scheduling
23+
- Container / Git
24+
- Other
25+
validations:
26+
required: true
27+
- type: input
28+
id: town-id
29+
attributes:
30+
label: Town ID (if known)
31+
description: Found in the URL or town settings
32+
validations:
33+
required: false
34+
- type: textarea
35+
id: reproduction
36+
attributes:
37+
label: Steps to reproduce
38+
description: How can we reproduce this?
39+
validations:
40+
required: false
41+
- type: textarea
42+
id: logs
43+
attributes:
44+
label: Relevant logs or screenshots
45+
description: Paste any error messages, activity log entries, or screenshots
46+
validations:
47+
required: false

cloudflare-gastown/container/plugin/mayor-tools.test.ts

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect, vi, beforeEach } from 'vitest';
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
22
import type { MayorGastownClient } from './client';
33
import type {
44
Agent,
@@ -411,6 +411,154 @@ describe('mayor tools', () => {
411411
});
412412
});
413413

414+
describe('gt_report_bug', () => {
415+
const originalEnv = process.env;
416+
417+
beforeEach(() => {
418+
process.env = {
419+
...originalEnv,
420+
GH_TOKEN: 'fake-gh-token',
421+
GASTOWN_TOWN_ID: 'town-123',
422+
GASTOWN_AGENT_ID: 'mayor-agent-1',
423+
};
424+
});
425+
426+
afterEach(() => {
427+
process.env = originalEnv;
428+
vi.restoreAllMocks();
429+
});
430+
431+
it('returns fallback message when GH_TOKEN is not set', async () => {
432+
delete process.env.GH_TOKEN;
433+
const result = await tools.gt_report_bug.execute(
434+
{
435+
title: 'Test bug',
436+
description: 'Something broke',
437+
area: 'Mayor / Chat' as const,
438+
},
439+
CTX
440+
);
441+
expect(result).toContain('GH_TOKEN is not available');
442+
expect(result).toContain('github.com/Kilo-Org/cloud/issues/new');
443+
});
444+
445+
it('reports potential duplicates when search finds matches', async () => {
446+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
447+
new Response(
448+
JSON.stringify({
449+
items: [
450+
{
451+
number: 42,
452+
title: 'Similar bug',
453+
html_url: 'https://github.com/Kilo-Org/cloud/issues/42',
454+
},
455+
],
456+
}),
457+
{ status: 200 }
458+
)
459+
);
460+
461+
const result = await tools.gt_report_bug.execute(
462+
{
463+
title: 'Similar bug report',
464+
description: 'Something broke',
465+
area: 'Mayor / Chat' as const,
466+
},
467+
CTX
468+
);
469+
470+
expect(result).toContain('potentially related');
471+
expect(result).toContain('#42');
472+
expect(fetchSpy).toHaveBeenCalledOnce();
473+
});
474+
475+
it('creates issue when no duplicates found', async () => {
476+
const fetchSpy = vi
477+
.spyOn(globalThis, 'fetch')
478+
.mockResolvedValueOnce(new Response(JSON.stringify({ items: [] }), { status: 200 }))
479+
.mockResolvedValueOnce(
480+
new Response(
481+
JSON.stringify({ number: 99, html_url: 'https://github.com/Kilo-Org/cloud/issues/99' }),
482+
{ status: 201 }
483+
)
484+
);
485+
486+
const result = await tools.gt_report_bug.execute(
487+
{
488+
title: 'New bug',
489+
description: 'Something broke',
490+
area: 'Container / Git' as const,
491+
rig_id: 'rig-5',
492+
recent_errors: 'Error: connection refused',
493+
},
494+
CTX
495+
);
496+
497+
expect(result).toContain('#99');
498+
expect(result).toContain('Bug report filed');
499+
expect(fetchSpy).toHaveBeenCalledTimes(2);
500+
501+
// Verify the create call body
502+
const createCall = fetchSpy.mock.calls[1];
503+
const body = JSON.parse(createCall[1]?.body as string);
504+
expect(body.title).toBe('[Gastown] New bug');
505+
expect(body.labels).toEqual(['bug', 'gt:mayor']);
506+
expect(body.body).toContain('town-123');
507+
expect(body.body).toContain('rig-5');
508+
expect(body.body).toContain('connection refused');
509+
});
510+
511+
it('retries without labels on 422 (label permission error)', async () => {
512+
const fetchSpy = vi
513+
.spyOn(globalThis, 'fetch')
514+
.mockResolvedValueOnce(new Response(JSON.stringify({ items: [] }), { status: 200 }))
515+
.mockResolvedValueOnce(
516+
new Response('Validation Failed: label permissions', { status: 422 })
517+
)
518+
.mockResolvedValueOnce(
519+
new Response(
520+
JSON.stringify({
521+
number: 100,
522+
html_url: 'https://github.com/Kilo-Org/cloud/issues/100',
523+
}),
524+
{ status: 201 }
525+
)
526+
);
527+
528+
const result = await tools.gt_report_bug.execute(
529+
{ title: 'Label bug', description: 'Labels fail', area: 'Other' as const },
530+
CTX
531+
);
532+
533+
expect(result).toContain('#100');
534+
expect(result).toContain('Bug report filed');
535+
expect(fetchSpy).toHaveBeenCalledTimes(3);
536+
537+
// Retry call should omit labels
538+
const retryCall = fetchSpy.mock.calls[2];
539+
const retryBody = JSON.parse(retryCall[1]?.body as string);
540+
expect(retryBody.labels).toBeUndefined();
541+
});
542+
543+
it('handles create failure gracefully', async () => {
544+
vi.spyOn(globalThis, 'fetch')
545+
.mockResolvedValueOnce(new Response(JSON.stringify({ items: [] }), { status: 200 }))
546+
.mockResolvedValueOnce(new Response('Forbidden', { status: 403 }));
547+
548+
const result = await tools.gt_report_bug.execute(
549+
{
550+
title: 'Bug',
551+
description: 'Broken',
552+
area: 'Other' as const,
553+
},
554+
CTX
555+
);
556+
557+
expect(result).toContain('Failed to create issue');
558+
expect(result).toContain('403');
559+
});
560+
});
561+
414562
describe('gt_nudge', () => {
415563
it('sends a nudge and returns the nudge_id', async () => {
416564
const result = await tools.gt_nudge.execute(

cloudflare-gastown/container/plugin/mayor-tools.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,5 +472,134 @@ export function createMayorTools(client: MayorGastownClient) {
472472
return `Nudge queued: ${result.nudge_id} (mode: ${args.mode ?? 'wait-idle'})`;
473473
},
474474
}),
475+
476+
gt_report_bug: tool({
477+
description:
478+
'File a bug report on the Kilo-Org/cloud GitHub repo. ' +
479+
'Searches existing issues first to avoid duplicates. ' +
480+
'Use this when a user reports a bug or you encounter a repeating system error. ' +
481+
'Do NOT file bugs for user errors, expected behavior, or issues you can resolve yourself ' +
482+
'(e.g. re-slinging a failed bead). Do NOT file bugs about yourself being unable to start.',
483+
args: {
484+
title: tool.schema.string().describe('Concise bug title'),
485+
description: tool.schema
486+
.string()
487+
.describe('What happened vs. what was expected. Include error messages if available.'),
488+
area: tool.schema
489+
.enum([
490+
'Mayor / Chat',
491+
'Terminal UI',
492+
'Bead Board / Dashboard',
493+
'Convoys',
494+
'Merge Queue / Refinery',
495+
'Agent Dispatch / Scheduling',
496+
'Container / Git',
497+
'Other',
498+
])
499+
.describe('Which area of Gastown is affected'),
500+
rig_id: tool.schema
501+
.string()
502+
.describe('The rig ID where the bug was observed, if applicable')
503+
.optional(),
504+
recent_errors: tool.schema
505+
.string()
506+
.describe('Recent error messages or log snippets for context')
507+
.optional(),
508+
},
509+
async execute(args) {
510+
const ghToken = process.env.GH_TOKEN;
511+
if (!ghToken) {
512+
return 'Cannot file bug report: GH_TOKEN is not available in this container. Ask the user to file manually at https://github.com/Kilo-Org/cloud/issues/new?template=gastown-bug.yml';
513+
}
514+
515+
const repo = 'Kilo-Org/cloud';
516+
const headers = {
517+
Authorization: `Bearer ${ghToken}`,
518+
Accept: 'application/vnd.github+json',
519+
'X-GitHub-Api-Version': '2022-11-28',
520+
'Content-Type': 'application/json',
521+
};
522+
523+
// Search for potential duplicates (match both Mayor-filed and user-filed bug issues)
524+
const searchKeywords = args.title.split(/\s+/).slice(0, 5).join(' ');
525+
const searchQuery = encodeURIComponent(
526+
`repo:${repo} is:issue is:open label:bug ${searchKeywords}`
527+
);
528+
529+
let duplicates: Array<{ number: number; title: string; html_url: string }> = [];
530+
try {
531+
const searchRes = await fetch(
532+
`https://api.github.com/search/issues?q=${searchQuery}&per_page=5`,
533+
{ headers }
534+
);
535+
if (searchRes.ok) {
536+
const searchData = (await searchRes.json()) as {
537+
items: Array<{ number: number; title: string; html_url: string }>;
538+
};
539+
duplicates = searchData.items;
540+
}
541+
} catch {
542+
// Search failure is non-fatal — proceed to create
543+
}
544+
545+
if (duplicates.length > 0) {
546+
const list = duplicates
547+
.map(d => ` - #${d.number}: ${d.title} (${d.html_url})`)
548+
.join('\n');
549+
return [
550+
`Found ${duplicates.length} potentially related open issue(s):`,
551+
list,
552+
'',
553+
'Review these before filing a new issue. If none match, call gt_report_bug again with a more specific title.',
554+
].join('\n');
555+
}
556+
557+
// Build issue body with structured context
558+
const townId = process.env.GASTOWN_TOWN_ID ?? 'unknown';
559+
const agentId = process.env.GASTOWN_AGENT_ID ?? 'unknown';
560+
const bodyParts = [
561+
`## What happened?\n\n${args.description}`,
562+
`## Area\n\n${args.area}`,
563+
`## Context\n\n- **Town ID:** ${townId}\n- **Agent:** Mayor (${agentId})`,
564+
];
565+
if (args.rig_id) {
566+
bodyParts[bodyParts.length - 1] += `\n- **Rig ID:** ${args.rig_id}`;
567+
}
568+
if (args.recent_errors) {
569+
bodyParts.push(`## Recent Errors\n\n\`\`\`\n${args.recent_errors}\n\`\`\``);
570+
}
571+
bodyParts.push('*Filed automatically by the Mayor via `gt_report_bug`.*');
572+
const body = bodyParts.join('\n\n');
573+
574+
const issuePayload = {
575+
title: `[Gastown] ${args.title}`,
576+
body,
577+
labels: ['bug', 'gt:mayor'],
578+
};
579+
580+
let createRes = await fetch(`https://api.github.com/repos/${repo}/issues`, {
581+
method: 'POST',
582+
headers,
583+
body: JSON.stringify(issuePayload),
584+
});
585+
586+
// If labeling failed (e.g. token lacks label permissions), retry without labels
587+
if (!createRes.ok && createRes.status === 422) {
588+
createRes = await fetch(`https://api.github.com/repos/${repo}/issues`, {
589+
method: 'POST',
590+
headers,
591+
body: JSON.stringify({ title: issuePayload.title, body: issuePayload.body }),
592+
});
593+
}
594+
595+
if (!createRes.ok) {
596+
const errText = await createRes.text();
597+
return `Failed to create issue (HTTP ${createRes.status}): ${errText}`;
598+
}
599+
600+
const issue = (await createRes.json()) as { number: number; html_url: string };
601+
return `Bug report filed: #${issue.number}${issue.html_url}`;
602+
},
603+
}),
475604
};
476605
}

cloudflare-gastown/src/prompts/mayor-system.prompt.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,5 +282,25 @@ convoy, call gt_convoy_start with the convoy_id.
282282
283283
For large convoys (>5 beads) where the decomposition is non-obvious, consider
284284
using staged=true by default to give the user a chance to review before agents
285-
start spending compute.`;
285+
start spending compute.
286+
287+
## Bug Reporting
288+
289+
If a user reports a bug or you encounter a repeating error, you can file a bug report
290+
using gt_report_bug. Before filing:
291+
1. Search existing issues to avoid duplicates (the tool does this automatically)
292+
2. Include the town ID, what went wrong, and any error context
293+
3. Share the issue URL with the user
294+
295+
Only file bugs for genuine system problems — not for user errors or expected behavior.
296+
Don't file bugs for issues you can resolve yourself (e.g. re-slinging a failed bead).
297+
Don't file bugs about yourself being unable to start — that's a chicken-and-egg problem.
298+
299+
The UI has a "Report a Bug" dropdown in the terminal bar with two options:
300+
- **New GitHub Issue** — opens the structured bug report template
301+
- **Discord Channel** — links to the Gastown bugs channel at https://discord.com/channels/1349288496988160052/1485796776635142174
302+
303+
If a user prefers to discuss a problem rather than file a formal issue, point them to the
304+
Discord channel. For reproducible bugs with clear steps, prefer filing a GitHub issue via
305+
gt_report_bug so it's tracked.`;
286306
}

0 commit comments

Comments
 (0)