feat(ralph): unified slash commands /p /a /l /k cu legacy aliases

Restructurează comenzile Ralph într-un dispatcher unificat (_try_ralph_dispatch)
care suportă atât comenzile noi scurte (/p /a /l /k) cât și aliasurile legacy
(!propose !approve !status !stop). Pe Discord adaugă slash commands native cu
autocomplete dinamic pentru pending (/a) și running (/k). Pe Telegram apar în
meniul /. WhatsApp le parsează ca text plain.

Activează cron jobs morning-report (08:30) și evening-report (21:00) și adaugă
night-execute (23:00) pentru execuția autonomă a proiectelor aprobate.

Foundation pentru W1 din planul "Echo Core conversational planning agent".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 17:46:52 +00:00
parent 479fcc4356
commit 094c6be5a9
5 changed files with 296 additions and 106 deletions

View File

@@ -16,7 +16,14 @@ from src.claude_session import (
VALID_MODELS,
)
from src.fast_commands import dispatch as fast_dispatch
from src.router import route_message
from src.router import (
route_message,
_ralph_propose,
_ralph_approve,
_ralph_status,
_ralph_stop,
_load_approved_tasks,
)
logger = logging.getLogger("echo-core.discord")
_security_log = logging.getLogger("echo-core.security")
@@ -150,6 +157,12 @@ def create_bot(config: Config) -> discord.Client:
"`/heartbeat` — Health checks",
"`/restart` — Restart bot (owner)",
"",
"**Ralph (autonomous projects)**",
"`/p <slug> <description>` — Propose new project",
"`/a [slug]` — Approve for tonight (autocomplete)",
"`/l` — List projects status",
"`/k <slug>` — Stop a running project (autocomplete)",
"",
"**Admin**",
"`/setup` — Claim ownership",
"`/channel add <alias>` — Register channel",
@@ -886,6 +899,68 @@ def create_bot(config: Config) -> discord.Client:
f"Error reading logs: {e}", ephemeral=True
)
# --- Ralph commands (autonomous project execution) ---
async def _autocomplete_by_status(
interaction: discord.Interaction, current: str, statuses: tuple[str, ...]
) -> list[app_commands.Choice[str]]:
try:
data = _load_approved_tasks()
except Exception:
return []
current_low = (current or "").lower()
choices: list[app_commands.Choice[str]] = []
for p in data.get("projects", []):
if p.get("status") not in statuses:
continue
name = p.get("name", "")
if current_low and current_low not in name.lower():
continue
desc = (p.get("description") or "").strip()
label = f"{name}{desc}"[:100] if desc else name
choices.append(app_commands.Choice(name=label, value=name))
if len(choices) >= 25:
break
return choices
async def _ralph_autocomplete_pending(
interaction: discord.Interaction, current: str
) -> list[app_commands.Choice[str]]:
return await _autocomplete_by_status(interaction, current, ("pending",))
async def _ralph_autocomplete_running(
interaction: discord.Interaction, current: str
) -> list[app_commands.Choice[str]]:
return await _autocomplete_by_status(interaction, current, ("running", "approved"))
@tree.command(name="p", description="Propose new Ralph project")
@app_commands.describe(slug="Project slug (e.g. game-library)", description="Short description of what to do")
async def ralph_p(
interaction: discord.Interaction, slug: str, description: str
) -> None:
await interaction.response.send_message(_ralph_propose(slug, description))
@tree.command(name="a", description="Approve Ralph project for tonight (no slug = list pending)")
@app_commands.describe(slug="Project slug to approve (leave empty to list pending)")
@app_commands.autocomplete(slug=_ralph_autocomplete_pending)
async def ralph_a(
interaction: discord.Interaction, slug: str | None = None
) -> None:
slugs = [slug] if slug else []
await interaction.response.send_message(_ralph_approve(slugs))
@tree.command(name="l", description="List Ralph projects status")
async def ralph_l(interaction: discord.Interaction) -> None:
await interaction.response.send_message(_ralph_status())
@tree.command(name="k", description="Stop a running Ralph project")
@app_commands.describe(slug="Project slug to stop")
@app_commands.autocomplete(slug=_ralph_autocomplete_running)
async def ralph_k(
interaction: discord.Interaction, slug: str
) -> None:
await interaction.response.send_message(_ralph_stop(slug))
# --- Events ---
@client.event