Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 31 additions & 0 deletions cloudflare-gastown/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,37 @@
## Durable Objects

- Each DO module must export a `get{ClassName}Stub` helper function (e.g. `getRigDOStub`) that centralizes how that DO namespace creates instances. Callers should use this helper instead of accessing the namespace binding directly.
- **Sub-modules for large DOs**: When a Durable Object grows beyond a few hundred lines, extract domain logic into sub-modules under a `<do-name>/` directory alongside the DO file. For example, `Town.do.ts` delegates to modules in `town/`:

```
dos/
Town.do.ts # Class definition, RPC methods, alarm loop
town/
agents.ts # Agent CRUD, hook management
beads.ts # Bead CRUD, convoy progress
scheduling.ts # Agent dispatch, pending work scheduling
review-queue.ts # Review lifecycle, recovery
patrol.ts # Zombie detection, stale hook recovery
config.ts # Town configuration
rigs.ts # Rig registry
mail.ts # Inter-agent mail
container-dispatch.ts # Container start/stop/status
```

Each sub-module exports plain functions (not classes) that accept `SqlStorage` and any other required context as arguments. The DO imports them with the `import * as X` pattern:

```ts
import * as beadOps from './town/beads';
import * as agents from './town/agents';
import * as scheduling from './town/scheduling';

// In the DO class:
beadOps.updateBeadStatus(this.sql, beadId, 'closed', agentId);
agents.getOrCreateAgent(this.sql, 'polecat', rigId, this.townId);
await scheduling.schedulePendingWork(this.schedulingCtx);
```

This keeps the DO class thin (RPC surface + orchestration) while sub-modules own the business logic. The `import * as X` pattern makes call sites self-documenting — you can always tell which domain a function belongs to.

## IO boundaries

Expand Down
8 changes: 8 additions & 0 deletions cloudflare-gastown/container/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ RUN cd /opt/gastown-plugin && npm install --omit=dev && \
ln -s /opt/gastown-plugin/index.ts /home/agent/.config/kilo/plugins/gastown.ts && \
chown -R agent:agent /home/agent/.config

# ── Git config for agent user ───────────────────────────────────────
# Skip LFS smudge filter: agents don't need binary assets and LFS
# downloads can fail when credentials don't cover the batch endpoint.
# Also disable LFS fetch entirely so clone/worktree never stalls.
RUN printf '[filter "lfs"]\n\tsmudge = git-lfs smudge --skip -- %%f\n\tprocess = git-lfs filter-process --skip\n\tclean = git-lfs clean -- %%f\n\trequired = true\n[lfs]\n\tfetchexclude = *\n' \
> /home/agent/.gitconfig && \
chown agent:agent /home/agent/.gitconfig

WORKDIR /app

# ── Install production deps via pnpm ────────────────────────────────
Expand Down
9 changes: 3 additions & 6 deletions cloudflare-gastown/container/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
"start": "bun run src/main.ts",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsgo --noEmit --incremental false",
"lint": "pnpm run lint:oxlint && pnpm run lint:eslint:fallback",
"lint:oxlint": "pnpm -w exec oxlint --config .oxlintrc.json cloudflare-gastown/container/src",
"lint:eslint:fallback": "eslint --config eslint.config.mjs --cache 'src/**/*.ts'"
"typecheck": "tsc --noEmit",
"lint": "eslint --config eslint.config.mjs --cache 'src/**/*.ts'"
},
"dependencies": {
"@kilocode/plugin": "7.0.37",
Expand All @@ -21,8 +19,7 @@
},
"devDependencies": {
"@kilocode/eslint-config": "workspace:*",
"@types/bun": "^1.3.10",
"@typescript/native-preview": "catalog:",
"@types/bun": "^1.2.17",
"eslint": "catalog:",
"typescript": "catalog:",
"vitest": "^3.2.4"
Expand Down
42 changes: 10 additions & 32 deletions cloudflare-gastown/container/plugin/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ export class GastownClient {
});
}

async requestChanges(input: {
feedback: string;
files?: string[];
}): Promise<{ rework_bead_id: string }> {
return this.request<{ rework_bead_id: string }>(this.agentPath('/request-changes'), {
method: 'POST',
body: JSON.stringify(input),
});
}

async checkMail(): Promise<Mail[]> {
return this.request<Mail[]>(this.agentPath('/mail'));
}
Expand Down Expand Up @@ -300,8 +310,6 @@ export class MayorGastownClient {
title: string;
body?: string;
metadata?: Record<string, unknown>;
depends_on?: string[];
convoy_id?: string;
}): Promise<SlingResult> {
return this.request<SlingResult>(this.mayorPath('/sling'), {
method: 'POST',
Expand Down Expand Up @@ -383,35 +391,6 @@ export class MayorGastownClient {
);
}

async addBeadDependency(input: {
rig_id: string;
bead_id: string;
depends_on_bead_id: string;
dependency_type?: 'blocks' | 'tracks' | 'parent-child';
}): Promise<void> {
await this.request<{ ok: true }>(
`${this.baseUrl}/api/towns/${this.townId}/rigs/${input.rig_id}/beads/${input.bead_id}/dependencies`,
{
method: 'POST',
body: JSON.stringify({
depends_on_bead_id: input.depends_on_bead_id,
dependency_type: input.dependency_type,
}),
}
);
}

async removeBeadDependency(input: {
rig_id: string;
bead_id: string;
depends_on_bead_id: string;
}): Promise<void> {
await this.request<{ ok: true; deleted: boolean }>(
`${this.baseUrl}/api/towns/${this.townId}/rigs/${input.rig_id}/beads/${input.bead_id}/dependencies/${input.depends_on_bead_id}`,
{ method: 'DELETE' }
);
}

async listConvoys(): Promise<Convoy[]> {
return this.request<Convoy[]>(this.mayorPath('/convoys'));
}
Expand All @@ -429,7 +408,6 @@ export class MayorGastownClient {
status?: 'open' | 'in_progress' | 'in_review' | 'closed' | 'failed';
priority?: 'low' | 'medium' | 'high' | 'critical';
labels?: string[];
convoy_id?: string | null;
}
): Promise<Bead> {
return this.request<Bead>(this.mayorPath(`/rigs/${rigId}/beads/${beadId}`), {
Expand Down
89 changes: 4 additions & 85 deletions cloudflare-gastown/container/plugin/mayor-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,46 +67,22 @@ export function createMayorTools(client: MayorGastownClient) {
.string()
.describe('JSON-encoded metadata object for additional context')
.optional(),
depends_on: tool.schema
.array(tool.schema.string())
.describe(
'Optional list of bead IDs this task depends on. The new bead will not be dispatched until all listed beads are closed.'
)
.optional(),
convoy_id: tool.schema
.string()
.describe(
'Optional convoy ID to add this bead to. The bead will be tracked by the convoy and included in its progress.'
)
.optional(),
},
async execute(args) {
const metadata = args.metadata ? parseJsonObject(args.metadata, 'metadata') : undefined;
// Pass depends_on directly to client.sling() so TownDO.slingBead()
// inserts the dependency rows atomically before arming dispatch.
const result = await client.sling({
rig_id: args.rig_id,
title: args.title,
body: args.body,
metadata,
depends_on: args.depends_on,
convoy_id: args.convoy_id,
});

const lines = [
return [
`Task slung successfully.`,
`Bead: ${result.bead.bead_id} — "${result.bead.title}"`,
`Assigned to: ${result.agent.name} (${result.agent.role}, id: ${result.agent.id})`,
`Status: ${result.bead.status}`,
];
if (args.depends_on && args.depends_on.length > 0) {
lines.push(`Dependencies: blocked by ${args.depends_on.length} bead(s)`);
}
if (args.convoy_id) {
lines.push(`Convoy: added to ${args.convoy_id}`);
}
lines.push(`The polecat will be dispatched automatically by the alarm scheduler.`);
return lines.join('\n');
`The polecat will be dispatched automatically by the alarm scheduler.`,
].join('\n');
},
}),

Expand Down Expand Up @@ -325,9 +301,7 @@ export function createMayorTools(client: MayorGastownClient) {
}),

gt_bead_update: tool({
description:
"Edit a bead's status, title, body, priority, labels, or convoy membership. " +
'Set convoy_id to add the bead to a convoy, or set it to null/empty to remove it.',
description: "Edit a bead's status, title, body, priority, or labels.",
args: {
rig_id: tool.schema.string().describe('The UUID of the rig the bead belongs to'),
bead_id: tool.schema.string().describe('The UUID of the bead to update'),
Expand All @@ -345,13 +319,6 @@ export function createMayorTools(client: MayorGastownClient) {
.array(tool.schema.string())
.describe('Replacement labels array for the bead')
.optional(),
convoy_id: tool.schema
.string()
.describe(
'Set to a convoy UUID to add this bead to that convoy. ' +
'Set to an empty string to remove the bead from its current convoy.'
)
.optional(),
},
async execute(args) {
const bead = await client.updateBead(args.rig_id, args.bead_id, {
Expand All @@ -360,7 +327,6 @@ export function createMayorTools(client: MayorGastownClient) {
status: args.status,
priority: args.priority,
labels: args.labels,
convoy_id: args.convoy_id === '' ? null : args.convoy_id,
});
return `Bead ${bead.bead_id} updated. Status: ${bead.status}, Priority: ${bead.priority}, Title: "${bead.title}".`;
},
Expand Down Expand Up @@ -506,52 +472,5 @@ export function createMayorTools(client: MayorGastownClient) {
return `Nudge queued: ${result.nudge_id} (mode: ${args.mode ?? 'wait-idle'})`;
},
}),

gt_bead_add_dependency: tool({
description:
'Add a dependency between two beads. The bead at bead_id will be blocked by depends_on_bead_id — ' +
'it will not be dispatched until the dependency is closed.',
args: {
rig_id: tool.schema.string().describe('The UUID of the rig the beads belong to'),
bead_id: tool.schema.string().describe('The UUID of the bead that should be blocked'),
depends_on_bead_id: tool.schema
.string()
.describe('The UUID of the bead that must close first'),
dependency_type: tool.schema
.enum(['blocks', 'parent-child'])
.describe('Type of dependency (default: blocks)')
.optional(),
},
async execute(args) {
await client.addBeadDependency({
rig_id: args.rig_id,
bead_id: args.bead_id,
depends_on_bead_id: args.depends_on_bead_id,
dependency_type: args.dependency_type ?? 'blocks',
});
return `Dependency added: bead ${args.bead_id} now depends on ${args.depends_on_bead_id} (type: ${args.dependency_type ?? 'blocks'}).`;
},
}),

gt_bead_remove_dependency: tool({
description:
'Remove a dependency between two beads. If removing the dependency unblocks the bead, ' +
'it will be dispatched automatically.',
args: {
rig_id: tool.schema.string().describe('The UUID of the rig the beads belong to'),
bead_id: tool.schema.string().describe('The UUID of the dependent bead'),
depends_on_bead_id: tool.schema
.string()
.describe('The UUID of the bead it currently depends on'),
},
async execute(args) {
await client.removeBeadDependency({
rig_id: args.rig_id,
bead_id: args.bead_id,
depends_on_bead_id: args.depends_on_bead_id,
});
return `Dependency removed: bead ${args.bead_id} no longer depends on ${args.depends_on_bead_id}. If this was the last blocker, the bead will be dispatched automatically.`;
},
}),
};
}
32 changes: 32 additions & 0 deletions cloudflare-gastown/container/plugin/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,38 @@ export function createTools(client: GastownClient) {
},
}),

gt_request_changes: tool({
description:
'Request changes on the code you are reviewing. This creates a rework task ' +
'for a polecat to address your feedback. After calling this, call gt_done to ' +
'release your session. The polecat will push fixes to the same branch, and ' +
'you will be re-dispatched to re-review once the rework is complete. ' +
'Only available to refinery agents.',
args: {
feedback: tool.schema
.string()
.describe(
'Detailed description of what needs to change. Be specific: ' +
'reference file names, function names, and the exact issues found.'
),
files: tool.schema
.array(tool.schema.string())
.describe('Optional list of specific file paths that need changes')
.optional(),
},
async execute(args) {
const result = await client.requestChanges({
feedback: args.feedback,
files: args.files,
});
return (
`Rework request created (bead ${result.rework_bead_id}). ` +
'A polecat will be assigned to address your feedback. ' +
'Call gt_done now to release your session. You will be re-dispatched to re-review once the rework is complete.'
);
},
}),

gt_mail_send: tool({
description:
'Send a typed message to another agent in the rig. ' +
Expand Down
Loading
Loading