Skip to content

Commit d00a12a

Browse files
adamclaude
andcommitted
Replace prefix-based feature request detection with Claude-driven intent detection
Chat system prompt now instructs Claude to append [FEATURE] or [IMPROVEMENT] markers when it detects a feature request intent. The marker is stripped before display/history and routes to the cog's new start_from_intent() method. No extra API call — piggybacks on the existing chat call. Explicit prefixes ("feature request:", "bot improvement:") still work as before. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ce2e66f commit d00a12a

File tree

8 files changed

+489
-66
lines changed

8 files changed

+489
-66
lines changed

CLAUDE.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ data/ — Plugin storage (created automatically, per-plugin isolat
4848
## Key Behaviors
4949

5050
- **Chat**: @mention the bot → Claude responds with per-channel conversation memory (last 20 messages)
51-
- **Feature requests**: @mention with "feature request: <description>" → role check → creates Discord thread → multi-turn planning conversation with Claude → user confirms → code gen → AST scan → opens PR
52-
- **Bot improvements**: @mention with "bot improvement: <description>" → role check → creates Discord thread → planning conversation → user confirms → code gen → PR flagged as CORE CHANGE
51+
- **Intent detection**: During chat, the system prompt instructs Claude to append `[FEATURE]` or `[IMPROVEMENT]` markers when it detects the user wants a feature. The marker is stripped before display/history and routes to the feature request flow. No extra API call — piggybacks on the existing chat call.
52+
- **Feature requests**: Detected naturally via chat intent, or explicitly with "feature request: <description>" → role check → creates Discord thread → multi-turn planning conversation with Claude → user confirms → code gen → AST scan → opens PR
53+
- **Bot improvements**: Detected naturally via chat intent, or explicitly with "bot improvement: <description>" → role check → creates Discord thread → planning conversation → user confirms → code gen → PR flagged as CORE CHANGE
5354
- **Deploy**: GitHub webhook on PR merge → bot writes `.deploy` + exits → supervisor pulls + restarts
5455
- **Rollback**: If bot crashes within 30s of deploy, supervisor reverts to last known good commit
5556
- **Admin channel**: `LOG_CHANNEL_ID` — bot posts deploy status, errors, feature request activity, rollback alerts
@@ -58,8 +59,8 @@ data/ — Plugin storage (created automatically, per-plugin isolat
5859

5960
Feature requests use a multi-turn thread conversation instead of one-shot code generation:
6061

61-
1. User @mentions the bot with a feature request in a regular channel
62-
2. Bot creates a Discord thread from the message
62+
1. User @mentions the bot with a feature request (natural language or explicit prefix) in a regular channel
63+
2. Bot detects intent (via chat marker or prefix) and creates a Discord thread from the message
6364
3. Bot calls Claude (using `PLANNING_SYSTEM_PROMPT`) to evaluate the request and ask clarifying questions
6465
4. User and Claude go back and forth in the thread until the plan is clear
6566
5. When Claude is satisfied, it includes a `---PLAN_READY---` marker in its response

PRIVACY_POLICY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ When you @mention the Bot, the text of your message (with the mention stripped)
1717

1818
### 1.2 Feature Request Content
1919

20-
When you submit a feature request or bot improvement, the Bot creates a **Discord thread** for a collaborative planning conversation. During this process:
20+
The Bot detects feature request intent during normal chat — no additional API call is made for intent detection. When a feature request is identified (either through natural language detection or an explicit prefix), the Bot creates a **Discord thread** for a collaborative planning conversation. During this process:
2121

2222
- Your request description and all follow-up messages in the thread are sent to the Anthropic Claude API for planning and code generation
2323
- The conversation history for the thread is stored in application memory (RAM) for the duration of the session (up to 30 minutes of inactivity) and is lost on Bot restart

README.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,24 +67,38 @@ The supervisor manages the bot lifecycle, handles deploys on PR merge, and rolls
6767

6868
### Feature Requests (Plugin)
6969

70+
Just describe what you want in natural language:
71+
7072
```
71-
@Turbot feature request: add a command that tells jokes
73+
@Turbot can you add a command that tells jokes?
7274
```
7375

74-
The bot creates a **Discord thread** and starts a collaborative planning conversation. It will ask clarifying questions, propose an implementation plan, and wait for your confirmation before generating code. Once you reply **go**, it generates a sandboxed plugin in `plugins/`, scans it for security policy violations, and opens a PR. Reply **cancel** to abort at any time. Requires the `BotAdmin` role (configurable).
76+
Turbot detects the intent automatically during normal chat. When it recognizes a feature request, it creates a **Discord thread** and starts a collaborative planning conversation. It will ask clarifying questions, propose an implementation plan, and wait for your confirmation before generating code. Once you reply **go**, it generates a sandboxed plugin in `plugins/`, scans it for security policy violations, and opens a PR. Reply **cancel** to abort at any time. Requires the `BotAdmin` role (configurable).
77+
78+
You can also use the explicit prefix if you prefer:
79+
```
80+
@Turbot feature request: add a command that tells jokes
81+
```
7582

7683
### Bot Improvements (Core)
7784

85+
If Turbot detects you want to change its core behavior, it routes through the improvement flow:
86+
7887
```
79-
@Turbot bot improvement: add rate limiting to chat responses
88+
@Turbot can you make the error messages friendlier?
8089
```
8190

8291
Same conversational flow as feature requests, but this can modify any file in the project. The PR is flagged with "CORE CHANGE" and triggers an admin channel warning.
8392

93+
Explicit prefix also works:
94+
```
95+
@Turbot bot improvement: add rate limiting to chat responses
96+
```
97+
8498
### Feature Request Flow
8599

86-
1. User submits a request in any channel
87-
2. Bot creates a thread and evaluates the request
100+
1. User @mentions the bot with a request (natural language or explicit prefix)
101+
2. Bot detects the intent and creates a thread
88102
3. Bot asks clarifying questions (1-3 at a time)
89103
4. User and bot refine the plan collaboratively
90104
5. Bot proposes a plan and waits for confirmation

TERMS_OF_SERVICE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ You agree not to:
2424

2525
## 4. Feature Requests and Generated Code
2626

27-
- Feature requests and bot improvements are handled through a **conversational planning process** in Discord threads. The Bot will ask clarifying questions and propose a plan before generating code
27+
- Feature requests and bot improvements can be triggered through natural language or explicit prefixes, and are handled through a **conversational planning process** in Discord threads. The Bot will ask clarifying questions and propose a plan before generating code
2828
- You may confirm the plan to proceed with code generation, or cancel the request at any time
2929
- Feature request sessions time out after 30 minutes of inactivity
3030
- All generated code is subject to review before merge; the Bot does not auto-apply changes

bot.py

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,26 @@ def _split_reply(text: str, limit: int = DISCORD_MSG_LIMIT) -> list[str]:
6666
return chunks
6767

6868

69+
def _extract_intent(text: str) -> tuple[str, str | None]:
70+
"""Strip intent marker from response, return (clean_text, request_type)."""
71+
stripped = text.rstrip()
72+
if stripped.endswith("[FEATURE]"):
73+
return stripped[: -len("[FEATURE]")].rstrip(), "plugin"
74+
if stripped.endswith("[IMPROVEMENT]"):
75+
return stripped[: -len("[IMPROVEMENT]")].rstrip(), "core"
76+
return text, None
77+
78+
79+
async def _start_feature_request(
80+
message: discord.Message, description: str, request_type: str,
81+
) -> None:
82+
"""Bridge: hand off detected intent to the FeatureRequestCog."""
83+
cog = bot.get_cog("FeatureRequestCog")
84+
if cog is None:
85+
return
86+
await cog.start_from_intent(message, description, request_type)
87+
88+
6989
@bot.event
7090
async def on_ready() -> None:
7191
print(f"Turbot is online as {bot.user} — feeling Turbotastic!")
@@ -123,8 +143,9 @@ async def on_message(message: discord.Message) -> None:
123143
if not bot.user or bot.user not in message.mentions:
124144
return
125145

126-
# Skip feature requests — handled by the cog
127146
text = re.sub(r"<@!?\d+>", "", message.content).strip()
147+
148+
# Skip explicit feature request prefixes — handled directly by the cog
128149
if text.lower().startswith(("feature request:", "bot improvement:")):
129150
return
130151

@@ -150,29 +171,40 @@ async def on_message(message: discord.Message) -> None:
150171
was_recovering = claude_health.state == "half_open"
151172

152173
try:
153-
response = await claude.messages.create(
154-
model=config.CLAUDE_MODEL,
155-
max_tokens=1024,
156-
system=(
157-
"You are Turbot, a friendly and helpful Discord bot. "
158-
"You are Turbotastic — cheerful, concise, and occasionally "
159-
"make fish puns. Keep replies under a few paragraphs."
160-
),
161-
messages=list(history),
162-
)
163-
reply = response.content[0].text
174+
async with message.channel.typing():
175+
response = await claude.messages.create(
176+
model=config.CLAUDE_MODEL,
177+
max_tokens=1024,
178+
system=(
179+
"You are Turbot, a friendly and helpful Discord bot. "
180+
"You are Turbotastic — cheerful, concise, and occasionally "
181+
"make fish puns. Keep replies under a few paragraphs.\n"
182+
"If the user is asking you to add a new feature or command, "
183+
"end your reply with [FEATURE].\n"
184+
"If they want a change to your core behavior, "
185+
"end with [IMPROVEMENT].\n"
186+
"Only add a marker when the intent is clear."
187+
),
188+
messages=list(history),
189+
)
190+
raw_reply = response.content[0].text
191+
192+
claude_health.record_success()
193+
if was_recovering:
194+
await log_to_admin("**Claude API recovered** — circuit breaker reset.")
195+
196+
reply, intent = _extract_intent(raw_reply)
164197

165-
claude_health.record_success()
166-
if was_recovering:
167-
await log_to_admin("**Claude API recovered** — circuit breaker reset.")
198+
history.append({"role": "assistant", "content": reply})
199+
if len(history) > MAX_HISTORY:
200+
history[:] = history[-MAX_HISTORY:]
168201

169-
history.append({"role": "assistant", "content": reply})
170-
if len(history) > MAX_HISTORY:
171-
history[:] = history[-MAX_HISTORY:]
202+
# Split long replies to respect Discord's 2000-char limit
203+
for chunk in _split_reply(reply):
204+
await message.reply(chunk)
172205

173-
# Split long replies to respect Discord's 2000-char limit
174-
for chunk in _split_reply(reply):
175-
await message.reply(chunk)
206+
if intent is not None:
207+
await _start_feature_request(message, text, intent)
176208

177209
except Exception as e:
178210
if is_transient(e):

cog_feature.py

Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -270,9 +270,10 @@ async def _handle_thread_message(
270270
"Generating code changes... this may take a moment."
271271
)
272272
try:
273-
pr_url = await self._handle_request(
274-
description, session.request_type,
275-
)
273+
async with message.channel.typing():
274+
pr_url = await self._handle_request(
275+
description, session.request_type,
276+
)
276277
session.state = "done"
277278
await message.channel.send(
278279
f"Turbotastic! PR created: {pr_url}"
@@ -324,7 +325,8 @@ async def _handle_thread_message(
324325
session.messages.append({"role": "user", "content": user_text})
325326

326327
try:
327-
reply_text = await self._call_planning_claude(session)
328+
async with message.channel.typing():
329+
reply_text = await self._call_planning_claude(session)
328330
except Exception as e:
329331
if is_transient(e):
330332
claude_health.record_failure()
@@ -358,31 +360,21 @@ async def _handle_thread_message(
358360
session.messages.append({"role": "assistant", "content": reply_text})
359361
await message.channel.send(display_text)
360362

361-
@commands.Cog.listener()
362-
async def on_message(self, message: discord.Message) -> None:
363-
if message.author.bot:
364-
return
365-
366-
# Check if this is a message in a tracked thread
367-
session = _sessions.get(message.channel.id)
368-
if session is not None:
369-
await self._handle_thread_message(message, session)
370-
return
371-
372-
if not self.bot.user or self.bot.user not in message.mentions:
373-
return
374-
375-
text = message.content
376-
# Strip the mention itself
377-
text = re.sub(r"<@!?\d+>", "", text).strip()
378-
379-
request_type = _detect_request_type(text)
380-
if request_type is None:
381-
return # Not a feature request — let the chat handler deal with it
363+
async def start_from_intent(
364+
self,
365+
message: discord.Message,
366+
description: str,
367+
request_type: str,
368+
) -> None:
369+
"""Start a feature request flow from an intent detected during chat.
382370
371+
Also used internally by the prefix-based on_message path. Performs role
372+
check, cooldown, circuit breaker check, thread creation, and first
373+
planning call.
374+
"""
383375
# Role check
384376
required_role = config.FEATURE_REQUEST_ROLE
385-
roles = getattr(message.author, 'roles', ())
377+
roles = getattr(message.author, "roles", ())
386378
has_role = any(r.name == required_role for r in roles)
387379

388380
if not has_role:
@@ -391,9 +383,8 @@ async def on_message(self, message: discord.Message) -> None:
391383
)
392384
return
393385

394-
feature_desc = _extract_description(text, request_type)
395-
if not feature_desc:
396-
await message.reply("Please describe the feature after the request prefix.")
386+
if not description:
387+
await message.reply("Please describe the feature you'd like.")
397388
return
398389

399390
# Per-user cooldown
@@ -420,30 +411,31 @@ async def on_message(self, message: discord.Message) -> None:
420411

421412
await _log(
422413
f"**{label}** from {message.author} "
423-
f"in <#{message.channel.id}>: {feature_desc}"
414+
f"in <#{message.channel.id}>: {description}"
424415
)
425416

426417
# Create a thread for the conversation
427418
thread = await message.create_thread(
428-
name=f"Feature: {feature_desc[:80]}",
419+
name=f"Feature: {description[:80]}",
429420
)
430421

431422
session = ThreadSession(
432423
thread_id=thread.id,
433424
user_id=message.author.id,
434425
request_type=request_type,
435-
original_description=feature_desc,
426+
original_description=description,
436427
)
437428
_sessions[thread.id] = session
438429

439430
# First planning call
440431
session.messages.append({
441432
"role": "user",
442-
"content": f"{label}: {feature_desc}",
433+
"content": f"{label}: {description}",
443434
})
444435

445436
try:
446-
reply_text = await self._call_planning_claude(session)
437+
async with thread.typing():
438+
reply_text = await self._call_planning_claude(session)
447439
except Exception as e:
448440
if is_transient(e):
449441
tripped = claude_health.record_failure()
@@ -480,6 +472,31 @@ async def on_message(self, message: discord.Message) -> None:
480472
session.messages.append({"role": "assistant", "content": reply_text})
481473
await thread.send(display_text)
482474

475+
@commands.Cog.listener()
476+
async def on_message(self, message: discord.Message) -> None:
477+
if message.author.bot:
478+
return
479+
480+
# Check if this is a message in a tracked thread
481+
session = _sessions.get(message.channel.id)
482+
if session is not None:
483+
await self._handle_thread_message(message, session)
484+
return
485+
486+
if not self.bot.user or self.bot.user not in message.mentions:
487+
return
488+
489+
text = message.content
490+
# Strip the mention itself
491+
text = re.sub(r"<@!?\d+>", "", text).strip()
492+
493+
request_type = _detect_request_type(text)
494+
if request_type is None:
495+
return # Not a feature request — let the chat handler deal with it
496+
497+
feature_desc = _extract_description(text, request_type)
498+
await self.start_from_intent(message, feature_desc, request_type)
499+
483500
async def _handle_request(self, description: str, request_type: str) -> str:
484501
"""Generate code changes and create a PR."""
485502
security_policy = _load_security_policy()

0 commit comments

Comments
 (0)