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
2 changes: 0 additions & 2 deletions .dev.vars.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,3 @@ FACTORY_AI_GATEWAY_ID="replace-me"
# Optional: advisory triage posts to Discord. DISCORD_SECURITY_CHANNEL_ID is a
# wrangler.jsonc variable; the bot token is a secret.
# DISCORD_BOT_TOKEN="replace-me"
# Optional: the Discord assistant uses the same bot token. Its server, roles,
# repository, and public key are wrangler.jsonc variables.
67 changes: 0 additions & 67 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,6 @@ GitHub webhooks ─→ Hono ingress (signature verification)
├→ AuthorCoordinator DO (one per PR) ─→ AuthorWorkflow ─→ CodeAuthor agent (one conversation per PR)
├→ AdvisoryCoordinator DO (one per advisory) ─→ AdvisoryWorkflow ─→ AdvisoryTriager agent ─→ Discord
└→ ReleaseSecurityCoordinator DO (one per PR) ─→ ReleaseSecurityWorkflow ─→ ReleaseSecurityReviewer agent

Discord Gateway ─→ DiscordGateway DO (one WebSocket; @mentions by allowed roles)
Discord buttons ─→ /channels/discord/interactions (signature + role check)
└→ DiscordThreadCoordinator DO (one per thread) ─→ DiscordThreadWorkflow ─→ DiscordAssistant agent (one conversation per thread)
```

- **Router** (`src/router.ts`): deterministic and pure. `pull_request.labeled`
Expand Down Expand Up @@ -240,45 +236,6 @@ by the reply inline so it can be copied into the advisory. The announcement is
edited to show the verdict. Mentions are disabled on every message. The complete
result is also stored in the `PRIVATE_REPORTS` R2 bucket under `advisories/`.

### Discord assistant (`src/discord/`)

Maintainers can bring Factory into a Discord discussion by mentioning the bot
(`@Astro Factory where does this get normalized?`). The assistant reads the
thread, answers with evidence from a checkout of the repository, and, when
asked, drafts a GitHub issue or proposes a pull request for a maintainer to
approve with a button.

- **Who can use it.** Only members holding one of the roles in
`DISCORD_ALLOWED_ROLE_IDS` (for example Maintainer and Core). Everyone else's
mentions are ignored silently, and button clicks are checked again on the
interactions endpoint. `DISCORD_ASSISTANT_CHANNEL_IDS` optionally limits the
channels it answers in.
- **One thread, one conversation.** A mention inside a thread uses that thread;
a mention in a text channel starts a thread from the message. Each thread has
one durable agent conversation, so the assistant remembers the discussion,
and one sandbox holding the repository's default branch, installed and built
with the `triage` section's commands. The sandbox sleeps after an hour idle
and is rebuilt on the next mention; the conversation survives that.
- **Staying in the conversation.** The bot replies right away ("On it.", or
"Starting up a sandbox first…" when it has to rebuild), keeps the typing
indicator up while it works, says once that it's still working if something
runs long, and otherwise stays quiet until it has an answer.
- **Issues.** Asked to file an issue, the assistant posts a draft with
**Create issue** and **Dismiss** buttons. Asking for changes produces a new
draft. Factory files the issue as the GitHub App, linking the thread.
- **Pull requests.** Asked for a fix, it posts a plan with **Open PR** and
**Dismiss** buttons. Approving it queues a job: the assistant implements the
change on a fresh branch (`factory/discord-<id>`) and runs tests, then
Factory commits, pushes, opens the pull request, and hands it to the author
and reviewer personas, so the usual review loop takes over.
- **Linked issues.** GitHub issues of the repository referenced in the thread
(`#123`, URLs) are staged in the sandbox for the assistant to read.

Jobs in a thread run one at a time and in order. Mentions that arrive while
the assistant is busy are answered together in its next turn; approved pull
requests are never dropped. The agent never holds GitHub or Discord
credentials: trusted workflow code posts its replies, files issues, and pushes.

## Personas

Personas are assignable GitHub identities in front of capabilities. You
Expand Down Expand Up @@ -413,12 +370,6 @@ advisories:
# skill: .agents/skills/advisory-triage # overrides the bundled default skill
# model: cloudflare-ai-gateway/claude-opus-4-6 # default: triage.model
# thinkingLevel: high # default: high

discord:
# enabled: true # the Discord assistant (needs the Worker's Discord settings)
# skill: .agents/skills/discord-assistant # overrides the bundled default skill
# model: cloudflare-ai-gateway/claude-opus-4-6 # default: triage.model
# thinkingLevel: high # default: high
```

Skills resolve as **bundled default, repository override wins**: the factory
Expand Down Expand Up @@ -633,24 +584,6 @@ and read message history in the target channel. Set the channel id as the
`DISCORD_SECURITY_CHANNEL_ID` variable in `wrangler.jsonc` and the bot token as
the `DISCORD_BOT_TOKEN` secret. Without both, reported advisories are skipped.

The Discord assistant uses the same bot. In the Discord developer portal:

1. Under **Bot**, enable the **Message Content** privileged intent. Without
it the Gateway connection is refused (close code 4014).
2. Set the **Interactions Endpoint URL** to
`https://<worker>/channels/discord/interactions`, and set
`DISCORD_PUBLIC_KEY` in `wrangler.jsonc` to the application's public key.
3. Allow the bot to view channels, read message history, send messages,
create public threads, and send messages in threads wherever it should
answer.

Then set the `wrangler.jsonc` variables: `DISCORD_GUILD_ID` (the server),
`DISCORD_ALLOWED_ROLE_IDS` (comma-separated role ids allowed to use it),
`DISCORD_REPOSITORY` (`owner/repo`; the GitHub App must be installed there),
and optionally `DISCORD_ASSISTANT_CHANNEL_IDS`. The assistant is off until the
server, roles, and repository are all set. A one-minute cron trigger keeps the
Gateway connection up.

Public and private repositories are both supported. Public repositories get
an anonymous blobless clone (the triage sandbox holds no credentials at all);
private repositories get a full single-branch clone authenticated with a
Expand Down
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,8 @@
"@cloudflare/codemode": "^0.5.1",
"@cloudflare/sandbox": "0.12.5",
"@earendil-works/pi-ai": "^0.87.1",
"@flue/discord": "^2.2.2",
"@flue/github": "^2.2.2",
"@flue/runtime": "^2.2.2",
"discord-api-types": "0.38.52",
"hono": "4.12.32",
"js-yaml": "^5.2.3",
"just-bash": "^3.2.0",
Expand Down
23 changes: 0 additions & 23 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 0 additions & 73 deletions skills/discord-assistant/skill.md

This file was deleted.

13 changes: 0 additions & 13 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { createCloudflareTracing } from '@flue/runtime/cloudflare';
import { Hono } from 'hono';
import { createFactoryAIGatewayProvider } from './ai-gateway.ts';
import { githubChannel } from './channels/github.ts';
import { discordInteractionsChannel } from './discord/interactions.ts';
import type { AppHonoEnv } from './env.ts';
import { createFlueEventLogger, type FlueEventLogger } from './flue-logging.ts';

Expand Down Expand Up @@ -31,16 +30,4 @@ const app = new Hono<AppHonoEnv>();
app.get('/health', (c) => c.json({ status: 'ok' }));
app.route('/channels/github', githubChannel.route());

// Discord button clicks. The public key comes from `env`, so the channel is
// built on first use; without it the endpoint doesn't exist.
app.all('/channels/discord/*', async (c) => {
const channel = discordInteractionsChannel(c.env.DISCORD_PUBLIC_KEY);
if (!channel) return c.notFound();
const discord = new Hono<AppHonoEnv>().route(
'/channels/discord',
channel.route(),
);
return discord.fetch(c.req.raw, c.env, c.executionCtx);
});

export default app;
17 changes: 0 additions & 17 deletions src/cloudflare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,9 @@ export { AdvisoryCoordinator } from './advisory/coordinator.ts';
export { AdvisoryWorkflow } from './advisory/workflow.ts';
export { AuthorCoordinator } from './author/coordinator.ts';
export { AuthorWorkflow } from './author/workflow.ts';
export { DiscordGateway } from './discord/gateway.ts';
export { DiscordThreadCoordinator } from './discord/thread-coordinator.ts';
export { DiscordThreadWorkflow } from './discord/workflow.ts';
export { ReleaseSecurityCoordinator } from './release-security/coordinator.ts';
export { ReleaseSecurityWorkflow } from './release-security/workflow.ts';
export { ReviewCoordinator } from './review/coordinator.ts';
export { ReviewWorkflow } from './review/workflow.ts';
export { TriageCoordinator } from './triage/coordinator.ts';
export { TriageWorkflow } from './triage/workflow.ts';

/**
* Worker-level handlers. The cron trigger keeps the Discord Gateway
* connection up: it wakes the gateway Durable Object every minute, which
* reconnects if a deploy or an eviction dropped the socket.
*/
export default {
async scheduled(
_controller: ScheduledController,
env: import('./env.ts').WorkerEnv,
): Promise<void> {
await env.DISCORD_GATEWAY.getByName('default').ensureConnected();
},
};
39 changes: 0 additions & 39 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,14 +328,6 @@ const factoryConfigSchema = v.object({
thinkingLevel: v.optional(thinkingLevelSchema),
}),
),
discord: v.optional(
v.object({
enabled: v.optional(v.boolean()),
skill: v.optional(v.pipe(v.string(), v.trim(), v.minLength(1))),
model: v.optional(modelSchema),
thinkingLevel: v.optional(thinkingLevelSchema),
}),
),
});

function labelConfigShape() {
Expand Down Expand Up @@ -401,22 +393,6 @@ export interface AdvisoriesConfig {
thinkingLevel: ThinkingLevel;
}

/**
* The Discord assistant: maintainers mention Factory's bot in a Discord
* thread to ask about the codebase, then have it file an issue or open a
* pull request. On by default; it also needs the Worker's Discord settings,
* which pick the repository the assistant works on. The checkout is prepared
* with the triage section's install and build commands.
*/
export interface DiscordAssistantConfig {
enabled: boolean;
/** Repository skill override; the bundled default skill is used when absent. */
skill: string | undefined;
/** `<provider>/<model>`; defaults to the triage pipeline's model. */
model: string;
thinkingLevel: ThinkingLevel;
}

export interface TriageConfig {
enabled: boolean;
autoPrOnFix: boolean;
Expand Down Expand Up @@ -504,7 +480,6 @@ export interface FactoryConfig {
triage: TriageConfig;
releaseSecurity: { thinkingLevel: ThinkingLevel };
advisories: AdvisoriesConfig;
discord: DiscordAssistantConfig;
}

/** Configuration used when the repository has no factory.yml at all. */
Expand Down Expand Up @@ -534,12 +509,6 @@ export function defaultFactoryConfig(): FactoryConfig {
model: CODE_MODEL,
thinkingLevel: DEFAULT_THINKING_LEVEL,
},
discord: {
enabled: true,
skill: undefined,
model: CODE_MODEL,
thinkingLevel: DEFAULT_THINKING_LEVEL,
},
};
}

Expand Down Expand Up @@ -635,14 +604,6 @@ export function parseFactoryConfig(source: string): FactoryConfig {
model: config.advisories?.model ?? config.triage?.model ?? CODE_MODEL,
thinkingLevel: config.advisories?.thinkingLevel ?? DEFAULT_THINKING_LEVEL,
},
discord: {
enabled: config.discord?.enabled ?? true,
skill: config.discord?.skill
? validateSkillDirectory(config.discord.skill)
: undefined,
model: config.discord?.model ?? config.triage?.model ?? CODE_MODEL,
thinkingLevel: config.discord?.thinkingLevel ?? DEFAULT_THINKING_LEVEL,
},
};
}

Expand Down
Loading
Loading