Files
hanarang-rails/docs/discord-setup.md
이랑이 2cadb3e0df feat(sprint-007): 마이그레이션 도구 + 운영 문서 + v0.1.0 릴리즈 준비
Sprint 007 전체 구현 — 마지막 스프린트. 프로젝트 완성:

CLI:
- rails doctor — 환경 헬스체크 (Node/pnpm/git/env/프로젝트 파일)
- rails scaffold [dir] — 신규 프로젝트 .plans/ 구조 생성
- rails migrate from-hanarang-harness <path> — 레거시 아카이브 스캐너
  agents/scripts/workflows 분류 (portable vs deprecated)
  xhigh 참조 경고 등 위험 패턴 감지

Docs (신규 3종):
- docs/migration-guide.md — 레거시 하네스 → rails 단계별 이전 가이드
- docs/operations.md — PM2, health check, 트러블슈팅, DB 유지보수
- docs/discord-setup.md — 봇 생성, DiscordPoster 구현 예시,
  marker 프로토콜 완전 명세

README 대폭 업데이트:
- v0.1.0 상태 선언
- 빠른 시작 가이드
- CLI 13 서브커맨드 목록
- 문서 링크

Tests (4 신규, 105 total pass):
- 마이그레이션 스캐너 (agents/scripts/workflows 감지)
- node_modules/.git 제외
- 빈 아카이브 처리
- scaffold 디렉토리 구조 검증

검증: tsc --noEmit ✓ | vitest 105/105 ✓ | build ✓
       rails doctor → 정상 출력 ✓
       rails --help → 13 subcommands ✓

마감 상태:
- F1~F6 모든 실패 모드 코어에서 해결
- 7 스프린트 완료 (000: 계획, 001~006: 코어, 007: 릴리즈)
- 105 테스트, 19 문서 (.plans/) + 3 운영 문서 (docs/)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:53:54 +09:00

199 lines
5.9 KiB
Markdown

# Discord Setup
> How to wire `hanarang-rails` to a Discord guild for the real DiscordTransport.
## Overview
Rails splits transport and observation:
- **Transport (deterministic)**: Rails posts marker blocks with structured invoke data. Agent bots parse the markers directly (not through LLM).
- **Observation (natural language)**: Agent bots keep posting free-form messages for human readers. Rails ignores the free-form text.
## Bot accounts
Two kinds of discord bots are involved:
1. **Rails bot** — posts invoke markers, state transitions, escalations.
2. **Agent bots (one per role, optional)** — each agent/sister has its own bot persona that responds with result markers and natural-language commentary.
If you don't need per-role personas, you can run a single bot for both rails and all agents.
## Rails bot setup
1. Go to https://discord.com/developers/applications
2. Create a new application → bot user
3. Enable privileged intents: **Message Content Intent** must be on.
4. OAuth2 URL generator → scopes: `bot`, permissions: `Send Messages`, `Read Message History`, `Create Public Threads`, `Manage Messages` (for marker cleanup, optional).
5. Invite the bot to your guild.
6. Copy the token.
Set in `.env`:
```
RAILS_DISCORD_TOKEN=<token>
DISCORD_GUILD_ID=<guild-id>
DISCORD_PIPELINE_CHANNEL_ID=<channel-id>
```
## Agent bot integration
Each agent host needs a minimal message handler that recognizes rails markers and routes them out of the LLM path:
```ts
import { DiscordPoster } from "hanarang-rails";
client.on("messageCreate", async (msg) => {
const invokeMarker = "<!-- rails:invoke v1 -->";
if (msg.content.includes(invokeMarker)) {
// Structured mode — do NOT send to the LLM
const req = extractJsonBlock(msg.content, "rails:invoke");
const result = await runRailsTask(req); // your agent's task runner
const resultMarker =
"<!-- rails:result v1 -->\n```json\n" +
JSON.stringify(result) +
"\n```\n<!-- /rails:result -->";
await msg.channel.send(
resultMarker +
"\n\n(Agent natural-language commentary here, optional)"
);
return;
}
// Otherwise: existing free-form conversation path
await runFreeFormLlm(msg);
});
```
### Marker format
**Invoke** (rails → agent):
```
<!-- rails:invoke v1 -->
```json
{
"pipelineId": "01HW0...",
"contractId": "01HW1...",
"stage": "implement",
"role": "implement",
"sprintId": "SPRINT-007",
"task": {
"title": "Add feature X",
"description": "...",
"workdir": "/path/to/workdir"
},
"timeoutMs": 30000,
"structuredOutput": true
}
```
<!-- /rails:invoke -->
```
**Result** (agent → rails):
```
<!-- rails:result v1 -->
```json
{
"stage": "implement",
"verdict": "IMPL_DONE",
"payload": {
"branch": "feature/sprint-007",
"commits": ["abc1234"],
"workdir": "...",
"selfTestReport": {"typecheck": "pass"}
},
"errorReason": ""
}
```
<!-- /rails:result -->
구현 완료했어요! 테스트 전부 통과했습니다 ❤️
```
The natural-language tail after `/rails:result` is free-form — rails ignores it, but the human user sees it.
## DiscordPoster interface
To wire rails to a real discord.js client, implement `DiscordPoster` and pass it when constructing `DiscordTransport`:
```ts
import { Client, GatewayIntentBits, TextChannel } from "discord.js";
import { DiscordTransport, type DiscordPoster } from "hanarang-rails";
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
await client.login(process.env.RAILS_DISCORD_TOKEN);
const poster: DiscordPoster = {
async postMessage(channelId, content) {
const channel = await client.channels.fetch(channelId);
if (!channel?.isTextBased()) throw new Error("Not a text channel");
const msg = await (channel as TextChannel).send(content);
return msg.id;
},
async waitForResult({ channelId, pipelineId, stage, timeoutMs, signal }) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("timeout")), timeoutMs);
signal?.addEventListener("abort", () => {
clearTimeout(timer);
reject(new Error("aborted"));
});
const handler = (msg: any) => {
if (msg.channel.id !== channelId) return;
const body = msg.content as string;
if (!body.includes("<!-- rails:result v1 -->")) return;
if (!body.includes(pipelineId)) return;
if (!body.includes(`"stage":"${stage}"`)) return;
clearTimeout(timer);
client.off("messageCreate", handler);
resolve(body);
};
client.on("messageCreate", handler);
});
},
async close() {
await client.destroy();
},
};
const transport = new DiscordTransport({
token: process.env.RAILS_DISCORD_TOKEN!,
guildId: process.env.DISCORD_GUILD_ID!,
channelId: process.env.DISCORD_PIPELINE_CHANNEL_ID!,
poster,
});
```
## Per-pipeline threads
For cleanness, create a forum thread per pipeline:
```ts
// On state transition, create a thread under the pipeline channel
const thread = await (channel as TextChannel).threads.create({
name: `[SPRINT-007] ${projectName}`,
autoArchiveDuration: 1440,
});
```
Pass `thread.id` as `channelId` when invoking agents. Rails stores the pipeline → thread mapping in SQLite (`pipelines.contextJson`).
## Testing without a live bot
For development, use `rails run --mock` — the `MockTransport` doesn't touch discord and returns deterministic success messages. All tests ship with a fake poster; no real tokens needed.
## Security
- **Never commit tokens.** `.env` is gitignored. Use secrets manager for production.
- **Validate HMAC** on any inbound webhooks (Gitea). See `GITEA_WEBHOOK_SECRET`.
- **Rate limit guard**: rails retries on 429 with exponential backoff (Sprint 005).
## Related
- `operations.md` — day-to-day ops
- `migration-guide.md` — porting from legacy bridges
- `.plans/design/transports.md` — transport abstraction design