Skip to content

feat(gastown): configurable merge strategy (direct/PR)#701

Merged
jrf0110 merged 5 commits intomainfrom
473-configurable-merge-strategy
Mar 5, 2026
Merged

feat(gastown): configurable merge strategy (direct/PR)#701
jrf0110 merged 5 commits intomainfrom
473-configurable-merge-strategy

Conversation

@jrf0110
Copy link
Copy Markdown
Contributor

@jrf0110 jrf0110 commented Mar 1, 2026

Summary

Implements #473 — Configurable Merge Strategy, a sub-issue of #204.

Video walkthrough: https://storage.j0.hn/gastown-merge-strategy-1.mp4

  • Adds a merge_strategy config field (direct | pr) at the town and rig level, with rig-level overrides inheriting from the town default
  • When set to pr, the Refinery creates a GitHub PR or GitLab MR instead of pushing directly to the default branch, enabling human review before agent work lands
  • Polls open PRs on each alarm tick and updates MR bead status when PRs are merged or closed externally
  • PR links are displayed on merge_request beads in the dashboard drawer

Changes by area

Schema & Config

  • MergeStrategy Zod enum and merge_strategy field on TownConfigSchema (defaults to direct)
  • Per-rig merge_strategy override on RigConfig
  • resolveMergeStrategy() helper: rig override → town default → direct
  • Mirrored schema in the Next.js gastown client

PR/MR Creation (platform-pr.util.ts — new)

  • parseGitUrl() — extracts platform/owner/repo from HTTPS and SSH git URLs
  • createGitHubPR() / createGitLabMR() — REST API calls with labels
  • buildPRBody() — Markdown body with quality gate results and agent attribution

Review Queue & Town DO

  • setReviewPrUrl(), markReviewInReview(), listPendingPRReviews() on review queue
  • executeMergeStrategy() dispatches to direct merge or PR creation
  • triggerPRCreation() creates the PR and tracks the URL on the MR bead
  • pollPendingPRs() + checkPRStatus() poll GitHub/GitLab APIs for PR state
  • pr_created and pr_creation_failed bead event types

Refinery Prompt

  • Accepts mergeStrategy param; when pr, instructs the refinery to push the branch and call gt_done (TownDO creates the PR) instead of merging itself

Dashboard UI

  • Merge Strategy radio picker in town settings (direct push vs pull request)
  • PR link badge on merge_request beads in BeadDetailDrawer

Dev Infrastructure

  • Fixed container→host networking: use Docker Desktop host gateway IP (192.168.65.254) instead of host.docker.internal which doesn't work on wrangler's workerd-network
  • Bind wrangler dev to 0.0.0.0 so containers can reach the worker
  • CF Access service token passthrough for production container→worker auth
  • Fail-fast guard when JWT minting fails (prevents launching broken agents)
  • Resolve catalog: references in Dockerfiles for bun compatibility
  • Better error logging in resolveJWTSecret

Tests — 23 unit tests covering merge strategy resolution, git URL parsing, and PR body generation

How to test

  1. Set merge_strategy: "pr" in town settings
  2. Create a bead and let a polecat work on it
  3. When the Refinery runs, it should create a GitHub PR instead of merging directly
  4. The MR bead should show a "View Pull Request" link
  5. Merging/closing the PR externally should update the MR bead status on the next alarm tick

Closes #473

@kilo-code-bot
Copy link
Copy Markdown
Contributor

kilo-code-bot bot commented Mar 1, 2026

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
src/lib/gastown/trpc.ts 64 new URL(GASTOWN_URL) throws TypeError when NEXT_PUBLIC_GASTOWN_URL env var is not configured (defaults to empty string), crashing all WebSocket and tRPC client operations

SUGGESTION

File Line Issue
cloudflare-gastown/src/trpc/router.ts 354 Unused rigId input field in sendMessage procedure — accepted in schema but never passed to sendMayorMessage
Other Observations (not in diff)

No additional issues found outside the diff.

Files Reviewed (75 files)

Infrastructure & Build:

  • cloudflare-gastown/.gitignore - 0 issues
  • cloudflare-gastown/container/.dockerignore - 0 issues (new)
  • cloudflare-gastown/container/.gitignore - 0 issues (new)
  • cloudflare-gastown/container/Dockerfile - 0 issues (bun → pnpm migration)
  • cloudflare-gastown/container/Dockerfile.dev - 0 issues (bun → pnpm migration)
  • cloudflare-gastown/container/bun.lock - 0 issues (deleted)
  • cloudflare-gastown/container/package.json - 0 issues (SDK version bump)
  • cloudflare-gastown/package.json - 0 issues (new deps, scripts)
  • cloudflare-gastown/scripts/prepare-container.mjs - 0 issues (new)
  • cloudflare-gastown/tsconfig.types.json - 0 issues (new)
  • cloudflare-gastown/wrangler.jsonc - 0 issues (new bindings)
  • cloudflare-gastown/worker-configuration.d.ts - 0 issues (regenerated)
  • pnpm-lock.yaml - 0 issues (lockfile update)
  • pnpm-workspace.yaml - 0 issues

Core Backend (Worker):

  • cloudflare-gastown/src/dos/Town.do.ts - 0 new issues (alarm tuning, pollPendingPRs, checkPRStatus)
  • cloudflare-gastown/src/dos/town/config.ts - 0 issues (resolveSmallModel, resolveMergeStrategy)
  • cloudflare-gastown/src/dos/town/review-queue.ts - 0 issues (PR URL storage, agentDone refactor)
  • cloudflare-gastown/src/dos/town/container-dispatch.ts - 0 issues (JWT abort, smallModel param)
  • cloudflare-gastown/src/types.ts - 0 issues (MergeStrategy enum, TownConfigUpdateSchema)
  • cloudflare-gastown/src/db/tables/bead-events.table.ts - 0 issues (new event types)
  • cloudflare-gastown/src/prompts/polecat-system.prompt.ts - 0 issues (no-PR instructions)
  • cloudflare-gastown/src/prompts/refinery-system.prompt.ts - 0 issues (merge strategy prompts)

tRPC Migration:

  • cloudflare-gastown/src/trpc/init.ts - 0 issues (new)
  • cloudflare-gastown/src/trpc/router.ts - 1 issue (new, 577 lines)
  • cloudflare-gastown/src/trpc/schemas.ts - 0 issues (new)
  • cloudflare-gastown/src/gastown.worker.ts - 0 issues (CORS, kiloAuth, tRPC mount)

Auth & Security:

  • cloudflare-gastown/src/middleware/auth.middleware.ts - 0 issues
  • cloudflare-gastown/src/middleware/kilo-auth.middleware.ts - 0 issues (new)
  • cloudflare-gastown/src/util/kilo-token.util.ts - 0 issues (new)
  • cloudflare-gastown/src/util/user-db.util.ts - 0 issues (new)
  • packages/worker-utils/src/kilo-token.ts - 0 issues (isAdmin field)
  • src/lib/tokens.ts - 0 issues (isAdmin payload)
  • src/app/api/gastown/token/route.ts - 0 issues (new JWT endpoint)

Platform PR/MR:

  • cloudflare-gastown/src/util/platform-pr.util.ts - 0 new issues (new, 358 lines)

Container Agent:

  • cloudflare-gastown/container/plugin/client.ts - 0 issues (whitespace)
  • cloudflare-gastown/container/src/agent-runner.ts - 0 issues (kiloModel, GH_TOKEN)
  • cloudflare-gastown/container/src/process-manager.ts - 0 issues (SDK rename)
  • cloudflare-gastown/container/src/types.ts - 0 issues (smallModel field)

Frontend (tRPC migration + merge strategy UI):

  • src/lib/gastown/trpc.ts - 1 issue (new)
  • src/lib/gastown/types/README.md - 0 issues (new)
  • src/lib/gastown/types/init.d.ts - 0 issues (new)
  • src/lib/gastown/types/router.d.ts - 0 issues (generated)
  • src/lib/gastown/types/schemas.d.ts - 0 issues (generated)
  • src/lib/constants.ts - 0 issues (GASTOWN_URL)
  • src/lib/providers/index.ts - 0 issues (comment only)
  • src/components/Providers.tsx - 0 issues
  • src/components/gastown/ActivityFeed.tsx - 0 issues (extractPrUrl)
  • src/components/gastown/AgentDetailDrawer.tsx - 0 issues
  • src/components/gastown/AgentStream.tsx - 0 issues
  • src/components/gastown/BeadDetailDrawer.tsx - 0 issues (PR link display)
  • src/components/gastown/ConvoyTimeline.tsx - 0 issues
  • src/components/gastown/CreateRigDialog.tsx - 0 issues
  • src/components/gastown/CreateTownDialog.tsx - 0 issues
  • src/components/gastown/GastownBeadDetailSheet.tsx - 0 issues
  • src/components/gastown/GastownTownSidebar.tsx - 0 issues
  • src/components/gastown/MayorChat.tsx - 0 issues
  • src/components/gastown/SlingDialog.tsx - 0 issues
  • src/components/gastown/SystemTopology.tsx - 0 issues
  • src/components/gastown/TerminalBar.tsx - 0 issues
  • src/components/gastown/drawer-panels/AgentPanel.tsx - 0 issues
  • src/components/gastown/drawer-panels/BeadPanel.tsx - 0 issues
  • src/components/gastown/useXtermPty.ts - 0 issues
  • src/app/(app)/gastown/TownListPageClient.tsx - 0 issues
  • src/app/(app)/gastown/[townId]/TownOverviewPageClient.tsx - 0 issues
  • src/app/(app)/gastown/[townId]/agents/AgentsPageClient.tsx - 0 issues
  • src/app/(app)/gastown/[townId]/beads/BeadsPageClient.tsx - 0 issues
  • src/app/(app)/gastown/[townId]/mail/MailPageClient.tsx - 0 issues
  • src/app/(app)/gastown/[townId]/merges/MergesPageClient.tsx - 0 issues
  • src/app/(app)/gastown/[townId]/observability/ObservabilityPageClient.tsx - 0 issues
  • src/app/(app)/gastown/[townId]/rigs/[rigId]/RigDetailPageClient.tsx - 0 issues
  • src/app/(app)/gastown/[townId]/settings/TownSettingsPageClient.tsx - 0 issues (merge strategy UI)

Deleted:

  • src/lib/gastown/gastown-client.ts - 0 issues (deleted, replaced by tRPC)
  • src/routers/gastown-router.ts - 0 issues (deleted, replaced by worker tRPC)
  • src/routers/root-router.ts - 0 issues (gastown sub-router removed)

Tests:

  • cloudflare-gastown/test/unit/merge-strategy.test.ts - 0 issues (new)

Fix these issues in Kilo Cloud

@jrf0110 jrf0110 force-pushed the 473-configurable-merge-strategy branch 5 times, most recently from 3846180 to b3274c2 Compare March 2, 2026 03:11
@jrf0110 jrf0110 force-pushed the 473-configurable-merge-strategy branch 2 times, most recently from 091068c to b01e42a Compare March 3, 2026 20:12
@jrf0110 jrf0110 force-pushed the 473-configurable-merge-strategy branch from b01e42a to 8d28245 Compare March 3, 2026 20:29
@jrf0110 jrf0110 force-pushed the 473-configurable-merge-strategy branch from 8d28245 to 69680fe Compare March 3, 2026 20:40
@jrf0110 jrf0110 force-pushed the 473-configurable-merge-strategy branch 4 times, most recently from f5a6c64 to de8771d Compare March 3, 2026 23:10
@jrf0110 jrf0110 force-pushed the 473-configurable-merge-strategy branch 2 times, most recently from 7324fca to 58324af Compare March 4, 2026 20:29
@jrf0110 jrf0110 force-pushed the 473-configurable-merge-strategy branch from 58324af to 3fdb05f Compare March 4, 2026 21:13
@jrf0110 jrf0110 force-pushed the 473-configurable-merge-strategy branch 2 times, most recently from facacdc to 7bc9749 Compare March 4, 2026 22:20
@jrf0110 jrf0110 force-pushed the 473-configurable-merge-strategy branch from 7bc9749 to ce705d3 Compare March 4, 2026 22:36
@jrf0110 jrf0110 force-pushed the 473-configurable-merge-strategy branch from ce705d3 to d10c0e8 Compare March 4, 2026 23:49
@jrf0110 jrf0110 force-pushed the 473-configurable-merge-strategy branch from d10c0e8 to b899c21 Compare March 5, 2026 00:43
@jrf0110 jrf0110 force-pushed the 473-configurable-merge-strategy branch from b899c21 to b80f29f Compare March 5, 2026 00:53
@jrf0110 jrf0110 force-pushed the 473-configurable-merge-strategy branch 3 times, most recently from db8deff to 5dd6a1a Compare March 5, 2026 18:48
jrf0110 added 5 commits March 5, 2026 13:23
… review bead fixes

- Add configurable merge strategy (direct/PR) for rigs and towns
- Refinery agents now handle merging (direct) or PR creation (gh/glab CLI) themselves
- Strategy-aware refinery system prompt with explicit merge/PR instructions
- Fix review beads missing PR link: write pr_url to beads.metadata on submission
- Fix PR beads not closing: validate pr_url is a real PR, add polling diagnostics
- Add small_model to TownConfig for configurable lightweight model
- Use resolveModel()/resolveSmallModel() throughout instead of hardcoded model strings
- All kilo config sub-agent roles derive model from town config
- Add PR link display to BeadPanel.tsx drawer
- Polecat prompt: clarify not to pass git push convenience URLs as pr_url
- Add kiloAuthMiddleware: validates Kilo user JWTs (NEXTAUTH_SECRET) via
  verifyKiloToken from @kilocode/worker-utils
- Apply kiloAuthMiddleware to user-facing routes (/api/users/*, /api/towns/*/config,
  /api/towns/*/container/*, /api/towns/*/convoys/*, /api/towns/*/escalations/*,
  /api/towns/*/mayor/*)
- Remove global CF Access middleware from all routes
- Replace CF Access validation in WebSocket upgrade handler with Kilo JWT validation
- Update gastown-client.ts to send Bearer token (via generateInternalServiceToken)
  instead of CF-Access-Client-Id/Secret headers
- Add NEXTAUTH_SECRET binding to wrangler.jsonc secrets store
- Add NEXTAUTH_SECRET to Env type in worker-configuration.d.ts
- Keep existing agent JWT auth (GASTOWN_SESSION_TOKEN) for container→worker routes
- Health and dashboard endpoints remain unauthenticated
…C proxy

The gastown frontend now calls the Cloudflare Worker's tRPC endpoint
directly instead of proxying through the Next.js backend. This removes
a network hop and lets the worker handle auth end-to-end.

Key changes:
- Add /api/gastown/token endpoint that mints short-lived JWTs with
  isAdmin and apiTokenPepper claims for browser→worker auth
- Create standalone gastown tRPC client (src/lib/gastown/trpc.ts) with
  token management, GastownTRPCProvider, and gastownWsUrl helper
- Add .output() schemas to all worker tRPC procedures for type-safe
  generated declarations
- Add GIT_TOKEN_SERVICE binding to worker for git credential refresh
  (replaces server-side refreshTownGitCredentials)
- Add configureRig/addRig calls to worker's createRig procedure
- Add setTownId calls to ensure TownDO uses correct UUID for containers
- Add CORS for /trpc/* routes and always-on kiloAuthMiddleware
- Worker requireAdmin now checks JWT claims instead of DB lookup
- WebSocket URLs returned as relative paths (frontend constructs full URL)
- Update ~25 frontend files: useTRPC → useGastownTRPC
- Remove gastown-router.ts, gastown-client.ts, and root-router registration
…robustness

- Remove console.log(secret) that leaked NEXTAUTH_SECRET to stdout
- Replace raw error logging with sanitized message in kilo-auth middleware
- Add verifyRigOwnership check to deleteRig tRPC procedure
- Use TownConfigUpdateSchema instead of z.record for updateTownConfig input
- Validate GitLab host in checkPRStatus before sending PRIVATE-TOKEN
- Handle empty model strings in kiloModel with fallback to kilo/auto
- Replace 'as' cast with Zod validation in gastown trpc.ts fetchToken
- Add warning log for unrecognized PR URL formats in checkPRStatus
- Handle HTTP 422 (duplicate PR) in createGitHubPR by fetching existing PR
- Add console.warn for label application failures instead of silent catch
- Emit pr_creation_failed event when refinery provides invalid pr_url
- Add git status instruction to refinery prompt per reviewer feedback
- Use MergeStrategy type import in refinery prompt instead of inline union
- Strip workspace: references from dependencies (not just devDependencies)
- Remove unused TOKEN_EXPIRY import to fix lint error
- Add SSRF protection to createGitLabMR: validate instanceUrl host against
  gitlab.com or configured instance URL before sending PRIVATE-TOKEN
- Log HTTP error status codes in checkPRStatus for both GitHub and GitLab
  API responses instead of silently returning null
- Add TownConfigUpdateSchema tests verifying no phantom defaults are
  injected on empty input
@jrf0110 jrf0110 force-pushed the 473-configurable-merge-strategy branch from 4440eb8 to 0bae000 Compare March 5, 2026 19:23
@jrf0110 jrf0110 merged commit 329dae6 into main Mar 5, 2026
13 checks passed
@jrf0110 jrf0110 deleted the 473-configurable-merge-strategy branch March 5, 2026 19:29
// The browser constructs the full ws(s):// URL using the known GASTOWN_URL.

export function gastownWsUrl(relativePath: string): string {
const base = new URL(GASTOWN_URL);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: new URL(GASTOWN_URL) throws TypeError when NEXT_PUBLIC_GASTOWN_URL is not configured

GASTOWN_URL defaults to '' (empty string) in src/lib/constants.ts:48 when the env var is unset. new URL('') throws TypeError: Invalid URL, crashing every gastownWsUrl() call.

Similarly, gastownTrpcUrl on line 71 would evaluate to /trpc (a relative path), which may not work with httpBatchLink that typically expects an absolute URL.

Consider guarding against an empty GASTOWN_URL:

export function gastownWsUrl(relativePath: string): string {
  if (!GASTOWN_URL) throw new Error('NEXT_PUBLIC_GASTOWN_URL is not configured');
  const base = new URL(GASTOWN_URL);
  ...
}

Or validate early at module scope so the error surfaces at startup rather than on the first WS connection attempt.

townId: z.string().uuid(),
message: z.string().min(1),
model: z.string().default('anthropic/claude-sonnet-4.6'),
rigId: z.string().uuid().optional(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: Unused input field — rigId is declared but never read

rigId is accepted in the sendMessage input schema but is not passed to townStub.sendMayorMessage(input.message, input.model). If it's intended for future use, consider removing it now and adding it when the mayor actually uses it. Dead schema fields create confusion about what the procedure actually depends on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Configurable merge strategy: direct push vs pull request

3 participants