Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
228dd86
docs(gastown): add local debug testing guide and drain test script
jrf0110 Apr 3, 2026
bf7a035
fix(gastown): refresh container token when mayor is alive but waiting…
jrf0110 Apr 3, 2026
7fd0e5e
feat(gastown): skip review queue for gt:pr-fixup beads (#1985)
jrf0110 Apr 3, 2026
0349fcc
feat(container): expand apt-get block with build tools, ripgrep, and …
jrf0110 Apr 3, 2026
ca6e7fb
feat(gastown): polecat creates PRs, refinery reviews via GitHub, code…
jrf0110 Apr 4, 2026
f31a378
fix(gastown): check commit statuses, propagate hasUncheckedRuns, gate…
jrf0110 Apr 4, 2026
2460b2c
style: auto-format files for CI
jrf0110 Apr 4, 2026
3ae19f2
fix(gastown): bug fixes — org billing (#1756), reconciler spam (#1364…
jrf0110 Apr 4, 2026
3684d76
feat(gastown): add PR-fixup workflow prompts, accept metadata as obje…
jrf0110 Apr 4, 2026
5cdac98
feat(gastown): add debug/beads/:beadId endpoint for bead inspection
jrf0110 Apr 4, 2026
7e4e318
docs(gastown): add debug endpoint docs, code_review toggle test, cont…
jrf0110 Apr 4, 2026
0c1964c
fix(gastown): clear stale org ID, improve poll failure reason, unify …
jrf0110 Apr 4, 2026
e31fe9c
style(settings): align toggle switches to the right side of cards
jrf0110 Apr 4, 2026
80d02e4
fix(gastown): extract resolveGitHubToken helper, unify token fallback…
jrf0110 Apr 4, 2026
d36364d
fix(gastown): handle NULL rig_id in MR beads, guard source bead closu…
jrf0110 Apr 4, 2026
f74ba86
fix(gastown): remove dead hasExistingFeedbackBead and add now() helpe…
Apr 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ supabase/.temp

.kilo/plans

# ESLint cache
.eslintcache

# @kilocode/trpc build output (rebuilt by: pnpm --filter @kilocode/trpc run build)
packages/trpc/dist/
.plan/
68 changes: 56 additions & 12 deletions cloudflare-gastown/container/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,18 +1,62 @@
FROM oven/bun:1-slim

# Install git, gh CLI, and Node.js (required by @kilocode/cli which uses #!/usr/bin/env node)
# Install dev toolchain, search tools, build deps, gh CLI, and Node.js
# Package categories:
# version control: git, git-lfs
# network/download: curl, wget, ca-certificates, gnupg, unzip
# build toolchain: build-essential, autoconf
# search tools: ripgrep, jq
# compression: bzip2, zstd
# SSL/crypto: libssl-dev, libffi-dev
# database client libs: libdb-dev, libgdbm-dev, libgdbm6
# Python build deps: libbz2-dev, liblzma-dev, libncurses5-dev, libreadline-dev, zlib1g-dev
# Ruby build deps: libyaml-dev
# image processing: libvips-dev
# browser/rendering: libgbm1
# C++ stdlib: libc++1
# math: libgmp-dev
# timezone data: tzdata
RUN apt-get update && \
apt-get install -y --no-install-recommends git git-lfs curl ca-certificates && \
curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && \
apt-get install -y --no-install-recommends nodejs && \
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
-o /usr/share/keyrings/githubcli-archive-keyring.gpg && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list && \
apt-get update && \
apt-get install -y --no-install-recommends gh && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
apt-get install -y --no-install-recommends \
git \
git-lfs \
curl \
wget \
ca-certificates \
gnupg \
unzip \
build-essential \
autoconf \
ripgrep \
jq \
bzip2 \
zstd \
libssl-dev \
libffi-dev \
libdb-dev \
libgdbm-dev \
libgdbm6 \
libbz2-dev \
liblzma-dev \
libncurses5-dev \
libreadline-dev \
zlib1g-dev \
libyaml-dev \
libvips-dev \
libgbm1 \
libc++1 \
libgmp-dev \
tzdata \
&& curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
-o /usr/share/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends gh \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

RUN git lfs install --system

Expand Down
68 changes: 56 additions & 12 deletions cloudflare-gastown/container/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -1,18 +1,62 @@
FROM --platform=linux/arm64 oven/bun:1-slim

# Install git, gh CLI, and Node.js (required by @kilocode/cli which uses #!/usr/bin/env node)
# Install dev toolchain, search tools, build deps, gh CLI, and Node.js
# Package categories:
# version control: git, git-lfs
# network/download: curl, wget, ca-certificates, gnupg, unzip
# build toolchain: build-essential, autoconf
# search tools: ripgrep, jq
# compression: bzip2, zstd
# SSL/crypto: libssl-dev, libffi-dev
# database client libs: libdb-dev, libgdbm-dev, libgdbm6
# Python build deps: libbz2-dev, liblzma-dev, libncurses5-dev, libreadline-dev, zlib1g-dev
# Ruby build deps: libyaml-dev
# image processing: libvips-dev
# browser/rendering: libgbm1
# C++ stdlib: libc++1
# math: libgmp-dev
# timezone data: tzdata
RUN apt-get update && \
apt-get install -y --no-install-recommends git git-lfs curl ca-certificates && \
curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && \
apt-get install -y --no-install-recommends nodejs && \
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
-o /usr/share/keyrings/githubcli-archive-keyring.gpg && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list && \
apt-get update && \
apt-get install -y --no-install-recommends gh && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
apt-get install -y --no-install-recommends \
git \
git-lfs \
curl \
wget \
ca-certificates \
gnupg \
unzip \
build-essential \
autoconf \
ripgrep \
jq \
bzip2 \
zstd \
libssl-dev \
libffi-dev \
libdb-dev \
libgdbm-dev \
libgdbm6 \
libbz2-dev \
liblzma-dev \
libncurses5-dev \
libreadline-dev \
zlib1g-dev \
libyaml-dev \
libvips-dev \
libgbm1 \
libc++1 \
libgmp-dev \
tzdata \
&& curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
-o /usr/share/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends gh \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

RUN git lfs install --system

Expand Down
1 change: 1 addition & 0 deletions cloudflare-gastown/container/plugin/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ export class MayorGastownClient {
title: string;
body?: string;
metadata?: Record<string, unknown>;
labels?: string[];
}): Promise<SlingResult> {
return this.request<SlingResult>(this.mayorPath('/sling'), {
method: 'POST',
Expand Down
29 changes: 10 additions & 19 deletions cloudflare-gastown/container/plugin/mayor-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,6 @@ function parseUiAction(value: unknown): UiActionInput {
return value as UiActionInput;
}

function parseJsonObject(value: string, label: string): Record<string, unknown> {
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<string, unknown>;
}

/**
* Mayor-specific tools for cross-rig delegation.
* These are only registered when `GASTOWN_AGENT_ROLE=mayor`.
Expand All @@ -64,17 +49,23 @@ 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())
.describe('Labels to attach to the bead (e.g. ["gt:pr-fixup"])')
.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 [
`Task slung successfully.`,
Expand Down
27 changes: 6 additions & 21 deletions cloudflare-gastown/container/plugin/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
17 changes: 3 additions & 14 deletions cloudflare-gastown/container/plugin/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,6 @@ function parseJsonArg(value: string, label: string): unknown {
}
}

function parseJsonObject(value: string, label: string): Record<string, unknown> {
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({
Expand Down Expand Up @@ -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})`;
},
Expand Down
19 changes: 19 additions & 0 deletions cloudflare-gastown/container/src/control-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ function syncTownConfigToProcessEnv(): void {
} else {
delete process.env.GASTOWN_DISABLE_AI_COAUTHOR;
}

// Keep the standalone env var in sync with the town config so org
// billing context is never lost across model changes.
const orgId = cfg.organization_id;
if (typeof orgId === 'string' && orgId) {
process.env.GASTOWN_ORGANIZATION_ID = orgId;
} else {
delete process.env.GASTOWN_ORGANIZATION_ID;
}
}

export const app = new Hono();
Expand Down Expand Up @@ -216,6 +225,11 @@ app.post('/agents/start', async c => {
return c.json({ error: 'Invalid request body', issues: parsed.error.issues }, 400);
}

// Persist the organization ID as a standalone env var so it survives
// config rebuilds (e.g. model hot-swap). The env var is the primary
// source of truth; KILO_CONFIG_CONTENT extraction is the fallback.
process.env.GASTOWN_ORGANIZATION_ID = parsed.data.organizationId ?? '';

console.log(
`[control-server] /agents/start: role=${parsed.data.role} name=${parsed.data.name} rigId=${parsed.data.rigId} agentId=${parsed.data.agentId}`
);
Expand Down Expand Up @@ -285,6 +299,11 @@ app.patch('/agents/:agentId/model', async c => {
return c.json({ error: 'Invalid request body', issues: parsed.error.issues }, 400);
}

// Update org billing context from the request body if provided.
if (parsed.data.organizationId) {
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: Hot-swap fallback cannot clear a stale organization context

This truthy check means the model-update request can only set GASTOWN_ORGANIZATION_ID, never clear it. On an org -> personal transition, if X-Town-Config parsing fails (the exact case this request-body fallback is meant to cover), the container keeps the previous org ID and later model calls keep billing against the old organization. Mirroring /agents/start here with parsed.data.organizationId ?? '' would preserve the fallback behavior.

process.env.GASTOWN_ORGANIZATION_ID = parsed.data.organizationId;
}

// Sync config-derived env vars from X-Town-Config into process.env so
// the SDK server restart picks up fresh tokens and git identity.
// The middleware already parsed the header into lastKnownTownConfig.
Expand Down
Loading