diff --git a/cloudflare-gastown/container/plugin/mayor-tools.ts b/cloudflare-gastown/container/plugin/mayor-tools.ts index 48ad586449..d581941265 100644 --- a/cloudflare-gastown/container/plugin/mayor-tools.ts +++ b/cloudflare-gastown/container/plugin/mayor-tools.ts @@ -27,21 +27,6 @@ function parseUiAction(value: unknown): UiActionInput { return value as UiActionInput; } -function parseJsonObject(value: string, label: string): Record { - let parsed: unknown; - try { - parsed = JSON.parse(value); - } catch { - throw new Error(`Invalid JSON in "${label}"`); - } - if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { - throw new Error( - `"${label}" must be a JSON object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}` - ); - } - return parsed as Record; -} - /** * Mayor-specific tools for cross-rig delegation. * These are only registered when `GASTOWN_AGENT_ROLE=mayor`. @@ -64,8 +49,8 @@ export function createMayorTools(client: MayorGastownClient) { ) .optional(), metadata: tool.schema - .string() - .describe('JSON-encoded metadata object for additional context') + .record(tool.schema.string(), tool.schema.unknown()) + .describe('Metadata object for additional context (e.g. { pr_url, branch, target_branch })') .optional(), labels: tool.schema .array(tool.schema.string()) @@ -73,12 +58,11 @@ export function createMayorTools(client: MayorGastownClient) { .optional(), }, async execute(args) { - const metadata = args.metadata ? parseJsonObject(args.metadata, 'metadata') : undefined; const result = await client.sling({ rig_id: args.rig_id, title: args.title, body: args.body, - metadata, + metadata: args.metadata, labels: args.labels, }); return [ diff --git a/cloudflare-gastown/container/plugin/tools.test.ts b/cloudflare-gastown/container/plugin/tools.test.ts index 4e8780013f..2c55de041c 100644 --- a/cloudflare-gastown/container/plugin/tools.test.ts +++ b/cloudflare-gastown/container/plugin/tools.test.ts @@ -232,33 +232,18 @@ describe('tools', () => { expect(result).toContain('priority: high'); }); - it('parses metadata JSON string', async () => { - await tools.gt_escalate.execute({ title: 'Test', metadata: '{"key": "value"}' }, CTX); + it('passes metadata object through to createEscalation', async () => { + await tools.gt_escalate.execute( + { title: 'Test', metadata: { key: 'value', nested: 123 } }, + CTX + ); expect(client.createEscalation).toHaveBeenCalledWith({ title: 'Test', body: undefined, priority: undefined, - metadata: { key: 'value' }, + metadata: { key: 'value', nested: 123 }, }); }); - - it('throws on invalid metadata JSON', async () => { - await expect( - tools.gt_escalate.execute({ title: 'Test', metadata: 'not json' }, CTX) - ).rejects.toThrow('Invalid JSON in "metadata"'); - }); - - it('throws when metadata is a JSON array instead of object', async () => { - await expect( - tools.gt_escalate.execute({ title: 'Test', metadata: '[1, 2]' }, CTX) - ).rejects.toThrow('"metadata" must be a JSON object, got array'); - }); - - it('throws when metadata is a JSON string instead of object', async () => { - await expect( - tools.gt_escalate.execute({ title: 'Test', metadata: '"hello"' }, CTX) - ).rejects.toThrow('"metadata" must be a JSON object, got string'); - }); }); describe('gt_checkpoint', () => { diff --git a/cloudflare-gastown/container/plugin/tools.ts b/cloudflare-gastown/container/plugin/tools.ts index 1c360c0f20..770115a6d8 100644 --- a/cloudflare-gastown/container/plugin/tools.ts +++ b/cloudflare-gastown/container/plugin/tools.ts @@ -9,16 +9,6 @@ function parseJsonArg(value: string, label: string): unknown { } } -function parseJsonObject(value: string, label: string): Record { - const parsed = parseJsonArg(value, label); - if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { - throw new Error( - `"${label}" must be a JSON object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}` - ); - } - return Object.fromEntries(Object.entries(parsed as object)); -} - export function createTools(client: GastownClient) { return { gt_prime: tool({ @@ -155,17 +145,16 @@ export function createTools(client: GastownClient) { .describe('Severity level (defaults to medium)') .optional(), metadata: tool.schema - .string() - .describe('JSON-encoded metadata object for additional context') + .record(tool.schema.string(), tool.schema.unknown()) + .describe('Metadata object for additional context') .optional(), }, async execute(args) { - const metadata = args.metadata ? parseJsonObject(args.metadata, 'metadata') : undefined; const bead = await client.createEscalation({ title: args.title, body: args.body, priority: args.priority, - metadata, + metadata: args.metadata, }); return `Escalation created: ${bead.bead_id} (priority: ${bead.priority})`; }, diff --git a/cloudflare-gastown/src/prompts/mayor-system.prompt.ts b/cloudflare-gastown/src/prompts/mayor-system.prompt.ts index 10ce483c4e..78c3b6d347 100644 --- a/cloudflare-gastown/src/prompts/mayor-system.prompt.ts +++ b/cloudflare-gastown/src/prompts/mayor-system.prompt.ts @@ -284,6 +284,26 @@ For large convoys (>5 beads) where the decomposition is non-obvious, consider using staged=true by default to give the user a chance to review before agents start spending compute. +## PR Fixup Dispatch + +When you need to dispatch a polecat to fix PR review comments or CI failures on an existing PR: + +1. Use \`gt_sling\` with the \`labels\` parameter set to \`["gt:pr-fixup"]\` +2. Include the PR URL, branch name, and target branch in the bead metadata: + \`\`\` + metadata: { + pr_url: "https://github.com/org/repo/pull/123", + branch: "gt/toast/abc123", + target_branch: "main" + } + \`\`\` +3. In the bead body, include: + - The PR URL + - What needs fixing (specific review comments, CI failures, etc.) + - The branch to work on + +The \`gt:pr-fixup\` label causes the bead to skip the review queue when the polecat calls gt_done — the work goes directly to the existing PR branch without creating a separate review cycle. + ## Bug Reporting If a user reports a bug or you encounter a repeating error, you can file a bug report diff --git a/cloudflare-gastown/src/prompts/polecat-system.prompt.ts b/cloudflare-gastown/src/prompts/polecat-system.prompt.ts index 28eca8ecfd..8815dea0e0 100644 --- a/cloudflare-gastown/src/prompts/polecat-system.prompt.ts +++ b/cloudflare-gastown/src/prompts/polecat-system.prompt.ts @@ -82,12 +82,26 @@ After all gates pass and your work is complete, create a pull request before cal ` : '' } +## PR Fixup Workflow + +When your hooked bead has the \`gt:pr-fixup\` label, you are fixing an existing PR rather than creating new work. **This is the ONE exception to the "do not switch branches" rule.** You MUST check out the PR branch from your bead metadata instead of using the default worktree branch. + +1. Check out the PR branch specified in your bead metadata (e.g. \`git fetch origin && git checkout \`). This overrides the default worktree branch for this bead. +2. Look at ALL comments on the PR using \`gh pr view --comments\` and the GitHub API. +3. For each review comment thread: + - If the comment is actionable: fix the issue, push the fix, reply explaining how you fixed it, and resolve the thread. + - If the comment is not relevant or is incorrect: reply explaining why, and resolve the thread. +4. **Important**: Resolve the entire thread, not just the individual comment. Use \`gh api\` to resolve review threads. +5. After addressing all comments, push your changes and call gt_done. + +Do NOT create a new PR. Push to the existing branch. + ## Commit & Push Hygiene - Commit after every meaningful unit of work (new function, passing test, config change). - Push after every commit. Do not batch pushes. - Use descriptive commit messages referencing the bead if applicable. -- Branch naming: your branch is pre-configured in your worktree. Do not switch branches. +- Branch naming: your branch is pre-configured in your worktree. Do not switch branches — **unless** your bead has the \`gt:pr-fixup\` label (see PR Fixup Workflow above). ## Escalation