From c84d61561c446d5b0e4b66adccb07234ec0e81b4 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 22 Apr 2026 21:11:42 -0300 Subject: [PATCH 1/9] refactor(telegram): centralize notifications in routines, remove from skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move Telegram reply() out of skill SKILL.md files into the routine .py callers via notify_telegram=True on run_skill(). This guarantees exactly one send per execution — the instruction is appended at the end of the prompt after all skill steps, so the agent cannot send it early. - runner.py: add notify_telegram param to run_skill() — reads chat_id from TELEGRAM_CHAT_ID env, appends explicit one-shot instruction - Skills cleaned: prod-end-of-day, prod-good-morning, pulse-faq-sync, pulse-daily (custom files gitignored, updated locally) - Routines updated: end_of_day, good_morning (custom routines gitignored) Co-Authored-By: Claude Sonnet 4.6 --- .claude/skills/prod-end-of-day/SKILL.md | 7 --- .claude/skills/prod-good-morning/SKILL.md | 7 --- .claude/skills/pulse-daily/SKILL.md | 7 --- .claude/skills/pulse-faq-sync/SKILL.md | 8 --- ADWs/routines/end_of_day.py | 2 +- ADWs/routines/good_morning.py | 2 +- ADWs/runner.py | 64 ++++++++++++++++++++++- 7 files changed, 64 insertions(+), 33 deletions(-) diff --git a/.claude/skills/prod-end-of-day/SKILL.md b/.claude/skills/prod-end-of-day/SKILL.md index 88059d11..a20b80d5 100644 --- a/.claude/skills/prod-end-of-day/SKILL.md +++ b/.claude/skills/prod-end-of-day/SKILL.md @@ -93,10 +93,3 @@ Present a short summary: **Tomorrow:** {sentence about where to resume} ``` - -### Notify via Telegram - -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + routine name + main result (1-3 lines) -- If the routine had no updates, send anyway with "no updates" diff --git a/.claude/skills/prod-good-morning/SKILL.md b/.claude/skills/prod-good-morning/SKILL.md index e75bc47e..915390ac 100644 --- a/.claude/skills/prod-good-morning/SKILL.md +++ b/.claude/skills/prod-good-morning/SKILL.md @@ -64,10 +64,3 @@ Create the `workspace/daily-logs/` directory if it does not exist. ## Tone Keep the morning briefing conversational and brief. The user is starting their day — they don't need a wall of text. Punchy bullets, one clear recommendation, then move into action. - -### Notify via Telegram - -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + routine name + main result (1-3 lines) -- If the routine had no updates, send anyway with "no updates" diff --git a/.claude/skills/pulse-daily/SKILL.md b/.claude/skills/pulse-daily/SKILL.md index 4457a45c..0d461c40 100644 --- a/.claude/skills/pulse-daily/SKILL.md +++ b/.claude/skills/pulse-daily/SKILL.md @@ -93,10 +93,3 @@ Report saved to workspace/community/reports/daily/ - **Compare with average** — if previous reports exist in the directory, compare metrics - **Empty channels = OK** — if a channel had no activity, do not report as a problem - -### Notify via Telegram - -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + routine name + main result (1-3 lines) -- If the routine had no updates, send anyway with "no updates" diff --git a/.claude/skills/pulse-faq-sync/SKILL.md b/.claude/skills/pulse-faq-sync/SKILL.md index f1aefdb0..c484103c 100644 --- a/.claude/skills/pulse-faq-sync/SKILL.md +++ b/.claude/skills/pulse-faq-sync/SKILL.md @@ -203,11 +203,3 @@ The "Skipped" block is mandatory — it gives visibility on questions the commun - **Tags in comments** — keep HTML comment tags for easy searching - **Keep organized** — categories in logical order (installation -> config -> integrations -> product -> billing -> errors) - -### Notify via Telegram - -Upon completion, send **exactly ONE** Telegram message with the full summary: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + routine name + main result (totals + alerts combined in one message) -- Do NOT split into multiple messages — combine summary and alerts into a single call -- If the routine had no updates, send anyway with "no updates" diff --git a/ADWs/routines/end_of_day.py b/ADWs/routines/end_of_day.py index 2fabe95e..4314c2e5 100644 --- a/ADWs/routines/end_of_day.py +++ b/ADWs/routines/end_of_day.py @@ -8,7 +8,7 @@ def main(): banner("🌙 End of Day", "Memória • Logs • Tarefas • Aprendizados | @clawdia") results = [] - results.append(run_skill("prod-end-of-day", log_name="end-of-day", timeout=600, agent="clawdia-assistant")) + results.append(run_skill("prod-end-of-day", log_name="end-of-day", timeout=600, agent="clawdia-assistant", notify_telegram=True)) summary(results, "End of Day") if __name__ == "__main__": diff --git a/ADWs/routines/good_morning.py b/ADWs/routines/good_morning.py index 5c97c088..e6dc28b4 100644 --- a/ADWs/routines/good_morning.py +++ b/ADWs/routines/good_morning.py @@ -8,7 +8,7 @@ def main(): banner("☀️ Good Morning", "Agenda • Emails • Tarefas | @clawdia") results = [] - results.append(run_skill("prod-good-morning", log_name="good-morning", timeout=600, agent="clawdia-assistant")) + results.append(run_skill("prod-good-morning", log_name="good-morning", timeout=600, agent="clawdia-assistant", notify_telegram=True)) summary(results, "Good Morning") if __name__ == "__main__": diff --git a/ADWs/runner.py b/ADWs/runner.py index 6d1533a7..c7b75fbe 100644 --- a/ADWs/runner.py +++ b/ADWs/runner.py @@ -291,9 +291,38 @@ def run_claude(prompt: str, log_name: str = "unnamed", timeout: int = 600, agent return {"success": False, "stdout": "", "stderr": str(e), "returncode": -3, "duration": duration} -def run_skill(skill_name: str, args: str = "", log_name: str = None, timeout: int = 600, agent: str = None) -> dict: - """Execute a skill via CLI, optionally with an agent.""" +def run_skill( + skill_name: str, + args: str = "", + log_name: str = None, + timeout: int = 600, + agent: str = None, + notify_telegram: bool | str = False, +) -> dict: + """Execute a skill via CLI, optionally with an agent. + + Args: + notify_telegram: Controls post-skill Telegram notification. + False (default) — no notification (skill must NOT call reply() either). + True — appends notification instruction; reads chat_id from + TELEGRAM_CHAT_ID env var. + "" — same as True but overrides the chat_id. + """ prompt = f"Execute the skill /{skill_name} {args}".strip() + if notify_telegram: + chat_id = ( + notify_telegram + if isinstance(notify_telegram, str) + else os.environ.get("TELEGRAM_CHAT_ID", "") + ) + if chat_id: + prompt += ( + f"\n\nAo concluir TODOS os passos acima, envie UMA única mensagem Telegram via:" + f'\nreply(chat_id="{chat_id}", text="...")' + f"\nFormato: emoji + nome da rotina + principais resultados em 2-3 linhas." + f"\nCRÍTICO: chame reply() EXATAMENTE UMA VEZ, somente aqui no final." + f" Não envie mensagens intermediárias nem de progresso." + ) return run_claude(prompt, log_name or skill_name, timeout, agent=agent) @@ -393,3 +422,34 @@ def summary(results: list, title: str = "Completed"): border_style="green" if failed == 0 else "yellow", padding=(0, 2) )) + + +def send_telegram(text: str, chat_id: str = None) -> bool: + """Send a Telegram message via bot API (no MCP dependency). + + Reads TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID from environment. + Returns True if sent successfully, False otherwise. + """ + import urllib.request + import urllib.parse + + token = os.environ.get("TELEGRAM_BOT_TOKEN", "") + cid = chat_id or os.environ.get("TELEGRAM_CHAT_ID", "") + if not token or not cid: + console.print(" [warning]⚠ Telegram not configured (missing BOT_TOKEN or CHAT_ID)[/warning]") + return False + + try: + payload = urllib.parse.urlencode({"chat_id": cid, "text": text, "parse_mode": "HTML"}).encode() + url = f"https://api.telegram.org/bot{token}/sendMessage" + req = urllib.request.Request(url, data=payload, method="POST") + with urllib.request.urlopen(req, timeout=10) as resp: + ok = resp.status == 200 + if ok: + console.print(" [success]✓[/success] Telegram enviado") + else: + console.print(f" [warning]⚠ Telegram status {resp.status}[/warning]") + return ok + except Exception as e: + console.print(f" [warning]⚠ Telegram error: {e}[/warning]") + return False From 277adb0a900910e2919448a625efbc6d549626de Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Sat, 25 Apr 2026 07:53:56 -0300 Subject: [PATCH 2/9] fix(telegram): remove reply() from prod-review-todoist skill Telegram notification was still in the skill file, causing the agent to call reply() multiple times per run (3x observed on 25/04 at 06:51). Notification now controlled by notify_telegram=True in review_todoist.py (gitignored custom routine, already updated on disk). Co-Authored-By: Claude Sonnet 4.6 --- .claude/skills/prod-review-todoist/SKILL.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.claude/skills/prod-review-todoist/SKILL.md b/.claude/skills/prod-review-todoist/SKILL.md index 2180aab2..34a78c1d 100644 --- a/.claude/skills/prod-review-todoist/SKILL.md +++ b/.claude/skills/prod-review-todoist/SKILL.md @@ -124,10 +124,3 @@ If the user wants to see details of what changed, they ask. - **If unsure about the category**, use `[Operations]` as fallback - **Execute first, report after** — no intermediate report - -### Notify via Telegram - -Upon completion, send **exactly ONE** Telegram message — do NOT call `reply()` more than once per run: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + routine name + main result (1-3 lines, all combined in a single message) -- If the routine had no updates, send anyway with "no updates" From cab896653a38d6546e5315f7d00ef1dec37c4441 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Sat, 25 Apr 2026 08:40:42 -0300 Subject: [PATCH 3/9] fix(chat-bridge): resolve agent file from WORKSPACE_ROOT, not session cwd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ticket threads pass workspace_path (e.g. workspace/personal/) as the session cwd. loadAgentFile was building the path relative to that cwd, so it looked for .claude/agents/ inside the ticket's folder instead of the workspace root — always failing for every agent. Fix: try WORKSPACE_ROOT/.claude/agents/{name}.md first, fall back to cwd for custom per-directory agents. Affected: kai-personal-assistant, flux-finance, and any ticket with a workspace_path set. Co-Authored-By: Claude Sonnet 4.6 --- dashboard/terminal-server/src/chat-bridge.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dashboard/terminal-server/src/chat-bridge.js b/dashboard/terminal-server/src/chat-bridge.js index 64827bf6..caf138b1 100644 --- a/dashboard/terminal-server/src/chat-bridge.js +++ b/dashboard/terminal-server/src/chat-bridge.js @@ -48,7 +48,10 @@ const NEEDS_APPROVAL = new Set([ * Extracts YAML frontmatter for metadata and the body as the prompt. */ function loadAgentFile(agentName, cwd) { - const agentPath = path.join(cwd, '.claude', 'agents', `${agentName}.md`); + // Agent definitions live at the workspace root — cwd varies per ticket session. + const rootPath = path.join(WORKSPACE_ROOT, '.claude', 'agents', `${agentName}.md`); + const cwdPath = path.join(cwd, '.claude', 'agents', `${agentName}.md`); + const agentPath = fs.existsSync(rootPath) ? rootPath : cwdPath; if (!fs.existsSync(agentPath)) { console.warn(`[chat-bridge] Agent file not found: ${agentPath}`); return null; From cc27a4abe2a47441e27feb6c6b71292a50c0e67a Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Sun, 26 Apr 2026 20:14:05 -0300 Subject: [PATCH 4/9] fix(telegram): eliminate duplicate notifications from inline reply() template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: notify_telegram instruction showed reply(chat_id=..., text="...") as an inline code snippet, causing the agent to execute it immediately on reading the instruction AND again at the end — 2x per instruction. Financial Pulse was getting 4x because the skill's Step 8 also contained a reply() call instruction (2 instructions × 2 executions = 4 messages). Fixes: - runner.py: rewrite notify_telegram prompt to describe the action in plain text without an inline function-call template - fin-daily-pulse SKILL.md: remove Step 8 Telegram section — notification is handled by the routine caller via notify_telegram=True Co-Authored-By: Claude Sonnet 4.6 --- .claude/skills/fin-daily-pulse/SKILL.md | 8 +++----- ADWs/runner.py | 12 +++++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.claude/skills/fin-daily-pulse/SKILL.md b/.claude/skills/fin-daily-pulse/SKILL.md index 2f2a74af..1c98eba8 100644 --- a/.claude/skills/fin-daily-pulse/SKILL.md +++ b/.claude/skills/fin-daily-pulse/SKILL.md @@ -157,9 +157,9 @@ workspace/finance/reports/daily/[C] YYYY-MM-DD-financial-pulse.html Create the directory `workspace/finance/reports/daily/` if it does not exist. -## Step 8 — Confirm and notify (ONE Telegram message only) +## Step 8 — Confirm -Output the completion summary, then send **exactly one** Telegram message. Do NOT call `reply` more than once per run. +Output the completion summary in the terminal: ``` ## Financial Pulse generated @@ -171,6 +171,4 @@ Output the completion summary, then send **exactly one** Telegram message. Do NO **Alerts:** {N} attention points ``` -Call `reply` **once** with a short summary (do not send the full markdown above — send a compact version): - -- Format: `[emoji] Financial Pulse [date] | MRR: R$ X,XXX | Receita: R$ X,XXX | Churn: X% | [N] alertas` +Do NOT send a Telegram message here — the caller handles notifications. diff --git a/ADWs/runner.py b/ADWs/runner.py index c7b75fbe..30c06a45 100644 --- a/ADWs/runner.py +++ b/ADWs/runner.py @@ -317,11 +317,13 @@ def run_skill( ) if chat_id: prompt += ( - f"\n\nAo concluir TODOS os passos acima, envie UMA única mensagem Telegram via:" - f'\nreply(chat_id="{chat_id}", text="...")' - f"\nFormato: emoji + nome da rotina + principais resultados em 2-3 linhas." - f"\nCRÍTICO: chame reply() EXATAMENTE UMA VEZ, somente aqui no final." - f" Não envie mensagens intermediárias nem de progresso." + f"\n\n---\n" + f"NOTIFICAÇÃO TELEGRAM — executar SOMENTE após concluir TODOS os passos acima.\n" + f"Use a ferramenta Telegram reply com chat_id={chat_id} e um texto compacto:\n" + f" emoji + nome da rotina + data + principais resultados em 2-3 linhas.\n" + f"REGRA ABSOLUTA: chame a ferramenta reply UMA ÚNICA VEZ, no final de tudo.\n" + f"Nunca chame reply para progresso, confirmação intermediária ou teste.\n" + f"---" ) return run_claude(prompt, log_name or skill_name, timeout, agent=agent) From 7400aa1aecb99f9612031522d6a7039469156cd8 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Sun, 26 Apr 2026 20:18:38 -0300 Subject: [PATCH 5/9] fix(telegram): remove all inline reply() calls from skill SKILL.md files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comprehensive sweep of all 175+ skills. Root cause of all duplicate Telegram notifications: skills contained inline reply(chat_id=...) code examples that Claude executed immediately on reading, then called again at the actual end-of-skill step — causing 2x (or 4x when combined with a second instruction from notify_telegram=True in the routine). Changes: - Removed "Notify via Telegram" sections entirely from 10 skills that have a corresponding routine using notify_telegram=True: fin-weekly-report, gog-email-triage, prod-dashboard, prod-trends, pulse-monthly, pulse-weekly, sage-strategy-digest, fin-monthly-close-kickoff, social-analytics-report, social-youtube-report - Rewrote Telegram instructions without inline reply() code in 4 skills that are called without notify_telegram (heartbeat or conditional): int-sync-meetings, int-github-review, int-linear-review (plain text), custom-getfy-sync, custom-omc-sync (plain text, local/gitignored) The corresponding routines (gitignored) were updated locally to add notify_telegram=True where the section was removed. Co-Authored-By: Claude Sonnet 4.6 --- .../skills/fin-monthly-close-kickoff/SKILL.md | 6 ------ .claude/skills/fin-weekly-report/SKILL.md | 6 ------ .claude/skills/gog-email-triage/SKILL.md | 8 -------- .claude/skills/int-github-review/SKILL.md | 7 +++---- .claude/skills/int-linear-review/SKILL.md | 7 +++---- .claude/skills/int-sync-meetings/SKILL.md | 11 ++++++----- .claude/skills/prod-dashboard/SKILL.md | 7 ------- .claude/skills/prod-trends/SKILL.md | 8 -------- .claude/skills/pulse-monthly/SKILL.md | 6 ------ .claude/skills/pulse-weekly/SKILL.md | 8 -------- .claude/skills/sage-strategy-digest/SKILL.md | 8 -------- .claude/skills/social-analytics-report/SKILL.md | 13 ------------- .claude/skills/social-youtube-report/SKILL.md | 5 ----- dashboard/terminal-server/package-lock.json | 3 --- uv.lock | 16 ++-------------- 15 files changed, 14 insertions(+), 105 deletions(-) diff --git a/.claude/skills/fin-monthly-close-kickoff/SKILL.md b/.claude/skills/fin-monthly-close-kickoff/SKILL.md index a62548f3..83a97202 100644 --- a/.claude/skills/fin-monthly-close-kickoff/SKILL.md +++ b/.claude/skills/fin-monthly-close-kickoff/SKILL.md @@ -182,9 +182,3 @@ Create the directory `workspace/finance/reports/monthly/` if it does not exist. **Checklist:** X/10 completed **Finance team pending items:** {N} items ``` - -### Notify via Telegram - -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + "Monthly Close" + month's result + pending items (2-3 lines) diff --git a/.claude/skills/fin-weekly-report/SKILL.md b/.claude/skills/fin-weekly-report/SKILL.md index ef3c85f2..4c344aa2 100644 --- a/.claude/skills/fin-weekly-report/SKILL.md +++ b/.claude/skills/fin-weekly-report/SKILL.md @@ -164,9 +164,3 @@ Create the directory `workspace/finance/reports/weekly/` if it does not exist. **MRR total:** R$ X,XXX (Stripe: R$ X,XXX | Evo Academy: R$ X,XXX) | **Projected 30d balance:** R$ XX,XXX **Alerts:** {N} overdue accounts | {N} pending invoices ``` - -### Notify via Telegram - -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + "Financial Weekly" + revenue vs expenses + MRR + alerts (2-3 lines) diff --git a/.claude/skills/gog-email-triage/SKILL.md b/.claude/skills/gog-email-triage/SKILL.md index 44cc3d45..3bda6b0f 100644 --- a/.claude/skills/gog-email-triage/SKILL.md +++ b/.claude/skills/gog-email-triage/SKILL.md @@ -354,11 +354,3 @@ See `skills/gog/_shared/references/testing.md` for complete test plan. - If email subject/sender contains obvious credentials or secrets, redact in output - For recurring newsletters, suggest creating a filter/rule rather than manual archiving - - -### Notify via Telegram - -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + routine name + main result (1-3 lines) -- If the routine had no updates, send anyway with "no updates" diff --git a/.claude/skills/int-github-review/SKILL.md b/.claude/skills/int-github-review/SKILL.md index d9ac780a..5cf8edcc 100644 --- a/.claude/skills/int-github-review/SKILL.md +++ b/.claude/skills/int-github-review/SKILL.md @@ -118,7 +118,6 @@ Create directory if it does not exist. ### Notify via Telegram -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + routine name + main result (1-3 lines) -- If the routine had no updates, send anyway with "no updates" +Upon completion, use the Telegram reply tool to send a short summary to the user. +Format: emoji + routine name + main result (1-3 lines). +If the routine had no updates, send anyway with "no updates". diff --git a/.claude/skills/int-linear-review/SKILL.md b/.claude/skills/int-linear-review/SKILL.md index 1deb55e7..e0ce493d 100644 --- a/.claude/skills/int-linear-review/SKILL.md +++ b/.claude/skills/int-linear-review/SKILL.md @@ -97,7 +97,6 @@ Create the directory `workspace/projects/linear-reviews/` if it does not exist. ### Notify via Telegram -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + routine name + main result (1-3 lines) -- If the routine had no updates, send anyway with "no updates" +Upon completion, use the Telegram reply tool to send a short summary to the user. +Format: emoji + routine name + main result (1-3 lines). +If the routine had no updates, send anyway with "no updates". diff --git a/.claude/skills/int-sync-meetings/SKILL.md b/.claude/skills/int-sync-meetings/SKILL.md index c4b22f22..2302f85e 100644 --- a/.claude/skills/int-sync-meetings/SKILL.md +++ b/.claude/skills/int-sync-meetings/SKILL.md @@ -186,12 +186,13 @@ Without listing tasks one by one — just counts. If the user wants details, the ### Step 9 — Notify via Telegram -Send the Step 8 summary via Telegram to the user using the `/int-telegram` skill: -- Chat ID: `YOUR_CHAT_ID` -- Use `reply(chat_id="YOUR_CHAT_ID", text="...")` via MCP -- Short format: emoji + title + meeting and task count +Only send if at least one new meeting was processed (i.e., you did NOT stop at Step 2). -If there are no new meetings (stopped at Step 2), do **NOT** send any Telegram message — stay silent. Only notify when at least one new meeting was processed. +Use the Telegram reply tool with the user's chat_id and a short summary: +- Format: emoji + title + meeting count + task count +- One line only + +If there are no new meetings (stopped at Step 2), do **NOT** send any Telegram message — stay completely silent. ## Notes diff --git a/.claude/skills/prod-dashboard/SKILL.md b/.claude/skills/prod-dashboard/SKILL.md index 56c3c84a..56da10f4 100644 --- a/.claude/skills/prod-dashboard/SKILL.md +++ b/.claude/skills/prod-dashboard/SKILL.md @@ -142,10 +142,3 @@ Present a short summary: **Health:** Product {status} | Community {status} | Financial {status} | Routines {status} **Alerts:** {N} attention points ``` - -### Notify via Telegram - -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + routine name + health status of each area (1-3 lines) -- If there were no updates, send anyway with "no updates" diff --git a/.claude/skills/prod-trends/SKILL.md b/.claude/skills/prod-trends/SKILL.md index 051ce18d..062f5270 100644 --- a/.claude/skills/prod-trends/SKILL.md +++ b/.claude/skills/prod-trends/SKILL.md @@ -163,11 +163,3 @@ Create `memory/trends/` if it does not exist. - **If a source has no data, skip** — do not block due to a missing report - **Focus on action** — each insight should lead to a concrete recommendation - **Do not alarm without evidence** — red only when the metric truly indicates risk - - -### Notify via Telegram - -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + routine name + main result (1-3 lines) -- If the routine had no updates, send anyway with "no updates" diff --git a/.claude/skills/pulse-monthly/SKILL.md b/.claude/skills/pulse-monthly/SKILL.md index c84a2439..dbe8cfb1 100644 --- a/.claude/skills/pulse-monthly/SKILL.md +++ b/.claude/skills/pulse-monthly/SKILL.md @@ -178,9 +178,3 @@ Create the directory `workspace/community/reports/monthly/` if it does not exist **Sentiment:** {trend} | **Resolution:** {X}% **Highlights:** {N} features, {N} bugs, {N} docs gaps ``` - -### Notify via Telegram - -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + "Community Monthly" + MAM + sentiment + highlights (2-3 lines) diff --git a/.claude/skills/pulse-weekly/SKILL.md b/.claude/skills/pulse-weekly/SKILL.md index ff41cc33..10d584fb 100644 --- a/.claude/skills/pulse-weekly/SKILL.md +++ b/.claude/skills/pulse-weekly/SKILL.md @@ -111,11 +111,3 @@ Report saved to workspace/community/reports/weekly/ - **Docs gap is gold** — each question without docs becomes a backlog item - **Comparison is fundamental** — always show trend vs previous week - **Product insights** — the most valuable section, handle with care - - -### Notify via Telegram - -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + routine name + main result (1-3 lines) -- If the routine had no updates, send anyway with "no updates" diff --git a/.claude/skills/sage-strategy-digest/SKILL.md b/.claude/skills/sage-strategy-digest/SKILL.md index ff47d1c3..f4f8135e 100644 --- a/.claude/skills/sage-strategy-digest/SKILL.md +++ b/.claude/skills/sage-strategy-digest/SKILL.md @@ -96,11 +96,3 @@ Present a short and direct version. - **Opinions flagged** — when it is opinion vs data, make it clear - **One recommendation** — do not give 10 suggestions, give 1 clear one - **Connect the dots** — the value of the digest is crossing areas, not repeating individual reports - - -### Notify via Telegram - -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + routine name + main result (1-3 lines) -- If the routine had no updates, send anyway with "no updates" diff --git a/.claude/skills/social-analytics-report/SKILL.md b/.claude/skills/social-analytics-report/SKILL.md index ce8aec24..fdc2c2c9 100644 --- a/.claude/skills/social-analytics-report/SKILL.md +++ b/.claude/skills/social-analytics-report/SKILL.md @@ -137,16 +137,3 @@ workspace/social/reports/consolidated/[C] YYYY-MM-DD-social-analytics.html ``` Create directory if it does not exist. - -### Step 9 — Telegram - -Notify: `reply(chat_id="YOUR_CHAT_ID", text="...")` -Format: -``` -📊 Social Analytics — {period} -👥 Total followers: {N} ({delta}) -📹 YouTube: {subs} sub | {eng}% eng -📸 Instagram: {followers} fol | {eng}% eng -💼 LinkedIn: profile connected -🏆 Top: "{best content}" ({platform}, {eng}%) -``` diff --git a/.claude/skills/social-youtube-report/SKILL.md b/.claude/skills/social-youtube-report/SKILL.md index 63d2ace6..8d5c42bf 100644 --- a/.claude/skills/social-youtube-report/SKILL.md +++ b/.claude/skills/social-youtube-report/SKILL.md @@ -64,8 +64,3 @@ workspace/social/reports/youtube/[C] YYYY-MM-DD-youtube-{period}.html ``` Criar diretório if it does not exist. - -### Step 7 — Telegram - -Notify: `reply(chat_id="946857210", text="...")` -Format: emoji + canal + inscritos + delta + melhor vídeo diff --git a/dashboard/terminal-server/package-lock.json b/dashboard/terminal-server/package-lock.json index 1a4af597..0611b4de 100644 --- a/dashboard/terminal-server/package-lock.json +++ b/dashboard/terminal-server/package-lock.json @@ -858,7 +858,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -1063,7 +1062,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.15.tgz", "integrity": "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -1765,7 +1763,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/uv.lock b/uv.lock index 6d6116d7..69360e19 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'darwin'", @@ -589,7 +589,7 @@ wheels = [ [[package]] name = "evo-nexus" -version = "0.32.3" +version = "0.32.2" source = { virtual = "." } dependencies = [ { name = "alembic" }, @@ -826,18 +826,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/bc/e30e1e3d5e8860b0e0ce4d2b16b2681b77fd13542fc0d72f7e3c22d16eff/greenlet-3.4.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:d18eae9a7fb0f499efcd146b8c9750a2e1f6e0e93b5a382b3481875354a430e6", size = 284315, upload-time = "2026-04-08T17:02:52.322Z" }, { url = "https://files.pythonhosted.org/packages/5b/cc/e023ae1967d2a26737387cac083e99e47f65f58868bd155c4c80c01ec4e0/greenlet-3.4.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:636d2f95c309e35f650e421c23297d5011716be15d966e6328b367c9fc513a82", size = 601916, upload-time = "2026-04-08T16:24:35.533Z" }, { url = "https://files.pythonhosted.org/packages/67/32/5be1677954b6d8810b33abe94e3eb88726311c58fa777dc97e390f7caf5a/greenlet-3.4.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:234582c20af9742583c3b2ddfbdbb58a756cfff803763ffaae1ac7990a9fac31", size = 616399, upload-time = "2026-04-08T16:30:54.536Z" }, - { url = "https://files.pythonhosted.org/packages/82/0a/3a4af092b09ea02bcda30f33fd7db397619132fe52c6ece24b9363130d34/greenlet-3.4.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ac6a5f618be581e1e0713aecec8e54093c235e5fa17d6d8eb7ffc487e2300508", size = 621077, upload-time = "2026-04-08T16:40:34.946Z" }, { url = "https://files.pythonhosted.org/packages/74/bf/2d58d5ea515704f83e34699128c9072a34bea27d2b6a556e102105fe62a5/greenlet-3.4.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:523677e69cd4711b5a014e37bc1fb3a29947c3e3a5bb6a527e1cc50312e5a398", size = 611978, upload-time = "2026-04-08T15:56:31.335Z" }, - { url = "https://files.pythonhosted.org/packages/8c/39/3786520a7d5e33ee87b3da2531f589a3882abf686a42a3773183a41ef010/greenlet-3.4.0-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:d336d46878e486de7d9458653c722875547ac8d36a1cff9ffaf4a74a3c1f62eb", size = 416893, upload-time = "2026-04-08T16:43:02.392Z" }, { url = "https://files.pythonhosted.org/packages/bd/69/6525049b6c179d8a923256304d8387b8bdd4acab1acf0407852463c6d514/greenlet-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b45e45fe47a19051a396abb22e19e7836a59ee6c5a90f3be427343c37908d65b", size = 1571957, upload-time = "2026-04-08T16:26:17.041Z" }, { url = "https://files.pythonhosted.org/packages/4e/6c/bbfb798b05fec736a0d24dc23e81b45bcee87f45a83cfb39db031853bddc/greenlet-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5434271357be07f3ad0936c312645853b7e689e679e29310e2de09a9ea6c3adf", size = 1637223, upload-time = "2026-04-08T15:57:27.556Z" }, { url = "https://files.pythonhosted.org/packages/b7/7d/981fe0e7c07bd9d5e7eb18decb8590a11e3955878291f7a7de2e9c668eb7/greenlet-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:a19093fbad824ed7c0f355b5ff4214bffda5f1a7f35f29b31fcaa240cc0135ab", size = 237902, upload-time = "2026-04-08T17:03:14.16Z" }, { url = "https://files.pythonhosted.org/packages/fb/c6/dba32cab7e3a625b011aa5647486e2d28423a48845a2998c126dd69c85e1/greenlet-3.4.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:805bebb4945094acbab757d34d6e1098be6de8966009ab9ca54f06ff492def58", size = 285504, upload-time = "2026-04-08T15:52:14.071Z" }, { url = "https://files.pythonhosted.org/packages/54/f4/7cb5c2b1feb9a1f50e038be79980dfa969aa91979e5e3a18fdbcfad2c517/greenlet-3.4.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:439fc2f12b9b512d9dfa681c5afe5f6b3232c708d13e6f02c845e0d9f4c2d8c6", size = 605476, upload-time = "2026-04-08T16:24:37.064Z" }, { url = "https://files.pythonhosted.org/packages/d6/af/b66ab0b2f9a4c5a867c136bf66d9599f34f21a1bcca26a2884a29c450bd9/greenlet-3.4.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a70ed1cb0295bee1df57b63bf7f46b4e56a5c93709eea769c1fec1bb23a95875", size = 618336, upload-time = "2026-04-08T16:30:56.59Z" }, - { url = "https://files.pythonhosted.org/packages/6d/31/56c43d2b5de476f77d36ceeec436328533bff960a4cba9a07616e93063ab/greenlet-3.4.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c5696c42e6bb5cfb7c6ff4453789081c66b9b91f061e5e9367fa15792644e76", size = 625045, upload-time = "2026-04-08T16:40:37.111Z" }, { url = "https://files.pythonhosted.org/packages/e5/5c/8c5633ece6ba611d64bf2770219a98dd439921d6424e4e8cf16b0ac74ea5/greenlet-3.4.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c660bce1940a1acae5f51f0a064f1bc785d07ea16efcb4bc708090afc4d69e83", size = 613515, upload-time = "2026-04-08T15:56:32.478Z" }, - { url = "https://files.pythonhosted.org/packages/80/ca/704d4e2c90acb8bdf7ae593f5cbc95f58e82de95cc540fb75631c1054533/greenlet-3.4.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:89995ce5ddcd2896d89615116dd39b9703bfa0c07b583b85b89bf1b5d6eddf81", size = 419745, upload-time = "2026-04-08T16:43:04.022Z" }, { url = "https://files.pythonhosted.org/packages/a9/df/950d15bca0d90a0e7395eb777903060504cdb509b7b705631e8fb69ff415/greenlet-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee407d4d1ca9dc632265aee1c8732c4a2d60adff848057cdebfe5fe94eb2c8a2", size = 1574623, upload-time = "2026-04-08T16:26:18.596Z" }, { url = "https://files.pythonhosted.org/packages/1a/e7/0839afab829fcb7333c9ff6d80c040949510055d2d4d63251f0d1c7c804e/greenlet-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:956215d5e355fffa7c021d168728321fd4d31fd730ac609b1653b450f6a4bc71", size = 1639579, upload-time = "2026-04-08T15:57:29.231Z" }, { url = "https://files.pythonhosted.org/packages/d9/2b/b4482401e9bcaf9f5c97f67ead38db89c19520ff6d0d6699979c6efcc200/greenlet-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:5cb614ace7c27571270354e9c9f696554d073f8aa9319079dcba466bbdead711", size = 238233, upload-time = "2026-04-08T17:02:54.286Z" }, @@ -845,9 +841,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/65/8b/3669ad3b3f247a791b2b4aceb3aa5a31f5f6817bf547e4e1ff712338145a/greenlet-3.4.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1a54a921561dd9518d31d2d3db4d7f80e589083063ab4d3e2e950756ef809e1a", size = 286902, upload-time = "2026-04-08T15:52:12.138Z" }, { url = "https://files.pythonhosted.org/packages/38/3e/3c0e19b82900873e2d8469b590a6c4b3dfd2b316d0591f1c26b38a4879a5/greenlet-3.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16dec271460a9a2b154e3b1c2fa1050ce6280878430320e85e08c166772e3f97", size = 606099, upload-time = "2026-04-08T16:24:38.408Z" }, { url = "https://files.pythonhosted.org/packages/b5/33/99fef65e7754fc76a4ed14794074c38c9ed3394a5bd129d7f61b705f3168/greenlet-3.4.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90036ce224ed6fe75508c1907a77e4540176dcf0744473627785dd519c6f9996", size = 618837, upload-time = "2026-04-08T16:30:58.298Z" }, - { url = "https://files.pythonhosted.org/packages/44/57/eae2cac10421feae6c0987e3dc106c6d86262b1cb379e171b017aba893a6/greenlet-3.4.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6f0def07ec9a71d72315cf26c061aceee53b306c36ed38c35caba952ea1b319d", size = 624901, upload-time = "2026-04-08T16:40:38.981Z" }, { url = "https://files.pythonhosted.org/packages/36/f7/229f3aed6948faa20e0616a0b8568da22e365ede6a54d7d369058b128afd/greenlet-3.4.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1c4f6b453006efb8310affb2d132832e9bbb4fc01ce6df6b70d810d38f1f6dc", size = 615062, upload-time = "2026-04-08T15:56:33.766Z" }, - { url = "https://files.pythonhosted.org/packages/6a/8a/0e73c9b94f31d1cc257fe79a0eff621674141cdae7d6d00f40de378a1e42/greenlet-3.4.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:0e1254cf0cbaa17b04320c3a78575f29f3c161ef38f59c977108f19ffddaf077", size = 423927, upload-time = "2026-04-08T16:43:05.293Z" }, { url = "https://files.pythonhosted.org/packages/08/97/d988180011aa40135c46cd0d0cf01dd97f7162bae14139b4a3ef54889ba5/greenlet-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b2d9a138ffa0e306d0e2b72976d2fb10b97e690d40ab36a472acaab0838e2de", size = 1573511, upload-time = "2026-04-08T16:26:20.058Z" }, { url = "https://files.pythonhosted.org/packages/d4/0f/a5a26fe152fb3d12e6a474181f6e9848283504d0afd095f353d85726374b/greenlet-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8424683caf46eb0eb6f626cb95e008e8cc30d0cb675bdfa48200925c79b38a08", size = 1640396, upload-time = "2026-04-08T15:57:30.88Z" }, { url = "https://files.pythonhosted.org/packages/42/cf/bb2c32d9a100e36ee9f6e38fad6b1e082b8184010cb06259b49e1266ca01/greenlet-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0a53fb071531d003b075c444014ff8f8b1a9898d36bb88abd9ac7b3524648a2", size = 238892, upload-time = "2026-04-08T17:03:10.094Z" }, @@ -855,9 +849,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7a/75/7e9cd1126a1e1f0cd67b0eda02e5221b28488d352684704a78ed505bd719/greenlet-3.4.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:43748988b097f9c6f09364f260741aa73c80747f63389824435c7a50bfdfd5c1", size = 285856, upload-time = "2026-04-08T15:52:45.82Z" }, { url = "https://files.pythonhosted.org/packages/9d/c4/3e2df392e5cb199527c4d9dbcaa75c14edcc394b45040f0189f649631e3c/greenlet-3.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5566e4e2cd7a880e8c27618e3eab20f3494452d12fd5129edef7b2f7aa9a36d1", size = 610208, upload-time = "2026-04-08T16:24:39.674Z" }, { url = "https://files.pythonhosted.org/packages/da/af/750cdfda1d1bd30a6c28080245be8d0346e669a98fdbae7f4102aa95fff3/greenlet-3.4.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1054c5a3c78e2ab599d452f23f7adafef55062a783a8e241d24f3b633ba6ff82", size = 621269, upload-time = "2026-04-08T16:30:59.767Z" }, - { url = "https://files.pythonhosted.org/packages/e0/93/c8c508d68ba93232784bbc1b5474d92371f2897dfc6bc281b419f2e0d492/greenlet-3.4.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98eedd1803353daf1cd9ef23eef23eda5a4d22f99b1f998d273a8b78b70dd47f", size = 628455, upload-time = "2026-04-08T16:40:40.698Z" }, { url = "https://files.pythonhosted.org/packages/54/78/0cbc693622cd54ebe25207efbb3a0eb07c2639cb8594f6e3aaaa0bb077a8/greenlet-3.4.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f82cb6cddc27dd81c96b1506f4aa7def15070c3b2a67d4e46fd19016aacce6cf", size = 617549, upload-time = "2026-04-08T15:56:34.893Z" }, - { url = "https://files.pythonhosted.org/packages/7f/46/cfaaa0ade435a60550fd83d07dfd5c41f873a01da17ede5c4cade0b9bab8/greenlet-3.4.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:b7857e2202aae67bc5725e0c1f6403c20a8ff46094ece015e7d474f5f7020b55", size = 426238, upload-time = "2026-04-08T16:43:06.865Z" }, { url = "https://files.pythonhosted.org/packages/ba/c0/8966767de01343c1ff47e8b855dc78e7d1a8ed2b7b9c83576a57e289f81d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:227a46251ecba4ff46ae742bc5ce95c91d5aceb4b02f885487aff269c127a729", size = 1575310, upload-time = "2026-04-08T16:26:21.671Z" }, { url = "https://files.pythonhosted.org/packages/b8/38/bcdc71ba05e9a5fda87f63ffc2abcd1f15693b659346df994a48c968003d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5b99e87be7eba788dd5b75ba1cde5639edffdec5f91fe0d734a249535ec3408c", size = 1640435, upload-time = "2026-04-08T15:57:32.572Z" }, { url = "https://files.pythonhosted.org/packages/a1/c2/19b664b7173b9e4ef5f77e8cef9f14c20ec7fce7920dc1ccd7afd955d093/greenlet-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:849f8bc17acd6295fcb5de8e46d55cc0e52381c56eaf50a2afd258e97bc65940", size = 238760, upload-time = "2026-04-08T17:04:03.878Z" }, @@ -865,9 +857,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/02/bde66806e8f169cf90b14d02c500c44cdbe02c8e224c9c67bafd1b8cadd1/greenlet-3.4.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:10a07aca6babdd18c16a3f4f8880acfffc2b88dfe431ad6aa5f5740759d7d75e", size = 286291, upload-time = "2026-04-08T17:09:34.307Z" }, { url = "https://files.pythonhosted.org/packages/05/1f/39da1c336a87d47c58352fb8a78541ce63d63ae57c5b9dae1fe02801bbc2/greenlet-3.4.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:076e21040b3a917d3ce4ad68fb5c3c6b32f1405616c4a57aa83120979649bd3d", size = 656749, upload-time = "2026-04-08T16:24:41.721Z" }, { url = "https://files.pythonhosted.org/packages/d3/6c/90ee29a4ee27af7aa2e2ec408799eeb69ee3fcc5abcecac6ddd07a5cd0f2/greenlet-3.4.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e82689eea4a237e530bb5cb41b180ef81fa2160e1f89422a67be7d90da67f615", size = 669084, upload-time = "2026-04-08T16:31:01.372Z" }, - { url = "https://files.pythonhosted.org/packages/d2/4a/74078d3936712cff6d3c91a930016f476ce4198d84e224fe6d81d3e02880/greenlet-3.4.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:06c2d3b89e0c62ba50bd7adf491b14f39da9e7e701647cb7b9ff4c99bee04b19", size = 673405, upload-time = "2026-04-08T16:40:42.527Z" }, { url = "https://files.pythonhosted.org/packages/07/49/d4cad6e5381a50947bb973d2f6cf6592621451b09368b8c20d9b8af49c5b/greenlet-3.4.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df3b0b2289ec686d3c821a5fee44259c05cfe824dd5e6e12c8e5f5df23085cf", size = 665621, upload-time = "2026-04-08T15:56:35.995Z" }, - { url = "https://files.pythonhosted.org/packages/79/3e/df8a83ab894751bc31e1106fdfaa80ca9753222f106b04de93faaa55feb7/greenlet-3.4.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:070b8bac2ff3b4d9e0ff36a0d19e42103331d9737e8504747cd1e659f76297bd", size = 471670, upload-time = "2026-04-08T16:43:08.512Z" }, { url = "https://files.pythonhosted.org/packages/37/31/d1edd54f424761b5d47718822f506b435b6aab2f3f93b465441143ea5119/greenlet-3.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8bff29d586ea415688f4cec96a591fcc3bf762d046a796cdadc1fdb6e7f2d5bf", size = 1622259, upload-time = "2026-04-08T16:26:23.201Z" }, { url = "https://files.pythonhosted.org/packages/b0/c6/6d3f9cdcb21c4e12a79cb332579f1c6aa1af78eb68059c5a957c7812d95e/greenlet-3.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a569c2fb840c53c13a2b8967c63621fafbd1a0e015b9c82f408c33d626a2fda", size = 1686916, upload-time = "2026-04-08T15:57:34.282Z" }, { url = "https://files.pythonhosted.org/packages/63/45/c1ca4a1ad975de4727e52d3ffe641ae23e1d7a8ffaa8ff7a0477e1827b92/greenlet-3.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:207ba5b97ea8b0b60eb43ffcacf26969dd83726095161d676aac03ff913ee50d", size = 239821, upload-time = "2026-04-08T17:03:48.423Z" }, @@ -875,9 +865,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/8f/18d72b629783f5e8d045a76f5325c1e938e659a9e4da79c7dcd10169a48d/greenlet-3.4.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d70012e51df2dbbccfaf63a40aaf9b40c8bed37c3e3a38751c926301ce538ece", size = 294681, upload-time = "2026-04-08T15:52:35.778Z" }, { url = "https://files.pythonhosted.org/packages/9e/ad/5fa86ec46769c4153820d58a04062285b3b9e10ba3d461ee257b68dcbf53/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a58bec0751f43068cd40cff31bb3ca02ad6000b3a51ca81367af4eb5abc480c8", size = 658899, upload-time = "2026-04-08T16:24:43.32Z" }, { url = "https://files.pythonhosted.org/packages/43/f0/4e8174ca0e87ae748c409f055a1ba161038c43cc0a5a6f1433a26ac2e5bf/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05fa0803561028f4b2e3b490ee41216a842eaee11aed004cc343a996d9523aa2", size = 665284, upload-time = "2026-04-08T16:31:02.833Z" }, - { url = "https://files.pythonhosted.org/packages/ef/92/466b0d9afd44b8af623139a3599d651c7564fa4152f25f117e1ee5949ffb/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c4cd56a9eb7a6444edbc19062f7b6fbc8f287c663b946e3171d899693b1c19fa", size = 665872, upload-time = "2026-04-08T16:40:43.912Z" }, { url = "https://files.pythonhosted.org/packages/19/da/991cf7cd33662e2df92a1274b7eb4d61769294d38a1bba8a45f31364845e/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e60d38719cb80b3ab5e85f9f1aed4960acfde09868af6762ccb27b260d68f4ed", size = 661861, upload-time = "2026-04-08T15:56:37.269Z" }, - { url = "https://files.pythonhosted.org/packages/0d/14/3395a7ef3e260de0325152ddfe19dffb3e49fe10873b94654352b53ad48e/greenlet-3.4.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:1f85f204c4d54134ae850d401fa435c89cd667d5ce9dc567571776b45941af72", size = 489237, upload-time = "2026-04-08T16:43:09.993Z" }, { url = "https://files.pythonhosted.org/packages/36/c5/6c2c708e14db3d9caea4b459d8464f58c32047451142fe2cfd90e7458f41/greenlet-3.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f50c804733b43eded05ae694691c9aa68bca7d0a867d67d4a3f514742a2d53f", size = 1622182, upload-time = "2026-04-08T16:26:24.777Z" }, { url = "https://files.pythonhosted.org/packages/7a/4c/50c5fed19378e11a29fabab1f6be39ea95358f4a0a07e115a51ca93385d8/greenlet-3.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2d4f0635dc4aa638cda4b2f5a07ae9a2cff9280327b581a3fcb6f317b4fbc38a", size = 1685050, upload-time = "2026-04-08T15:57:36.453Z" }, { url = "https://files.pythonhosted.org/packages/db/72/85ae954d734703ab48e622c59d4ce35d77ce840c265814af9c078cacc7aa/greenlet-3.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1a4a48f24681300c640f143ba7c404270e1ebbbcf34331d7104a4ff40f8ea705", size = 245554, upload-time = "2026-04-08T17:03:50.044Z" }, From dc2f7ba53ac7243a1b913cec3818902f7d6a16c5 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 28 Apr 2026 17:17:32 -0300 Subject: [PATCH 6/9] fix(telegram): move notification from Claude MCP to Python runner (definitive fix) Root cause: prompt instructions cannot reliably prevent Claude from calling reply() multiple times. No matter how explicit the instruction, the agent may call the tool on reading it, on output, and again at the end. Architectural change: Claude never calls Telegram anymore. 1. runner.py run_skill(): Claude is asked to output a TELEGRAM_MSG: line in stdout. After the skill returns, Python reads that line and calls send_telegram() exactly once via HTTP bot API. Guaranteed 1 message. 2. int-sync-meetings, int-github-review, int-linear-review: replaced "use the Telegram tool" with "write TELEGRAM_MSG: as the last line". 3. sync_meetings.py (local): added notify_telegram=True so the runner handles the notification. This makes duplicate Telegram notifications structurally impossible regardless of how many times Claude mentions or tries to call reply(). --- .claude/skills/int-github-review/SKILL.md | 7 ++-- .claude/skills/int-linear-review/SKILL.md | 7 ++-- .claude/skills/int-sync-meetings/SKILL.md | 10 ++--- ADWs/runner.py | 48 ++++++++++++++++------- 4 files changed, 43 insertions(+), 29 deletions(-) diff --git a/.claude/skills/int-github-review/SKILL.md b/.claude/skills/int-github-review/SKILL.md index 5cf8edcc..f997e810 100644 --- a/.claude/skills/int-github-review/SKILL.md +++ b/.claude/skills/int-github-review/SKILL.md @@ -116,8 +116,7 @@ Create directory if it does not exist. - **Focus on action** — what needs the responsible person's attention, not just numbers -### Notify via Telegram +### Notification line -Upon completion, use the Telegram reply tool to send a short summary to the user. -Format: emoji + routine name + main result (1-3 lines). -If the routine had no updates, send anyway with "no updates". +Write as the last line of your output: +TELEGRAM_MSG: 🐙 GitHub Review [date] | [main result in 1 line] diff --git a/.claude/skills/int-linear-review/SKILL.md b/.claude/skills/int-linear-review/SKILL.md index e0ce493d..94b14f70 100644 --- a/.claude/skills/int-linear-review/SKILL.md +++ b/.claude/skills/int-linear-review/SKILL.md @@ -95,8 +95,7 @@ Create the directory `workspace/projects/linear-reviews/` if it does not exist. - **Be direct** — numbers, not narrative -### Notify via Telegram +### Notification line -Upon completion, use the Telegram reply tool to send a short summary to the user. -Format: emoji + routine name + main result (1-3 lines). -If the routine had no updates, send anyway with "no updates". +Write as the last line of your output: +TELEGRAM_MSG: 📋 Linear Review [date] | [main result in 1 line] diff --git a/.claude/skills/int-sync-meetings/SKILL.md b/.claude/skills/int-sync-meetings/SKILL.md index 2302f85e..b4e3f284 100644 --- a/.claude/skills/int-sync-meetings/SKILL.md +++ b/.claude/skills/int-sync-meetings/SKILL.md @@ -184,15 +184,13 @@ When finished, present a short summary: Without listing tasks one by one — just counts. If the user wants details, they ask. -### Step 9 — Notify via Telegram +### Step 9 — Notification line -Only send if at least one new meeting was processed (i.e., you did NOT stop at Step 2). +Only if at least one new meeting was processed, write this as the last line of your output: -Use the Telegram reply tool with the user's chat_id and a short summary: -- Format: emoji + title + meeting count + task count -- One line only +TELEGRAM_MSG: 🎙️ Sync Fathom — N reunião(ões) processada(s) | N tarefas criadas -If there are no new meetings (stopped at Step 2), do **NOT** send any Telegram message — stay completely silent. +If no new meetings were processed (stopped at Step 2), do NOT write a TELEGRAM_MSG line. ## Notes diff --git a/ADWs/runner.py b/ADWs/runner.py index 30c06a45..5a2cda4b 100644 --- a/ADWs/runner.py +++ b/ADWs/runner.py @@ -303,29 +303,47 @@ def run_skill( Args: notify_telegram: Controls post-skill Telegram notification. - False (default) — no notification (skill must NOT call reply() either). - True — appends notification instruction; reads chat_id from - TELEGRAM_CHAT_ID env var. + False (default) — no notification. + True — Python sends ONE Telegram after the skill; reads + chat_id from TELEGRAM_CHAT_ID env var. "" — same as True but overrides the chat_id. + + The agent is asked to output a line "TELEGRAM_MSG: " in its stdout. + Python reads that line and calls send_telegram() exactly once. + The agent NEVER calls the Telegram MCP tool directly. """ - prompt = f"Execute the skill /{skill_name} {args}".strip() + chat_id = None if notify_telegram: chat_id = ( notify_telegram if isinstance(notify_telegram, str) else os.environ.get("TELEGRAM_CHAT_ID", "") + ) or None + + prompt = f"Execute the skill /{skill_name} {args}".strip() + if chat_id: + prompt += ( + f"\n\n---\n" + f"Ao finalizar, escreva na última linha do output:\n" + f"TELEGRAM_MSG: [emoji] [nome da rotina] [data] | [resultado 1] | [resultado 2]\n" + f"Apenas UMA linha TELEGRAM_MSG:. NÃO use a ferramenta Telegram/reply — " + f"o sistema Python lê essa linha e envia a notificação automaticamente.\n" + f"---" ) - if chat_id: - prompt += ( - f"\n\n---\n" - f"NOTIFICAÇÃO TELEGRAM — executar SOMENTE após concluir TODOS os passos acima.\n" - f"Use a ferramenta Telegram reply com chat_id={chat_id} e um texto compacto:\n" - f" emoji + nome da rotina + data + principais resultados em 2-3 linhas.\n" - f"REGRA ABSOLUTA: chame a ferramenta reply UMA ÚNICA VEZ, no final de tudo.\n" - f"Nunca chame reply para progresso, confirmação intermediária ou teste.\n" - f"---" - ) - return run_claude(prompt, log_name or skill_name, timeout, agent=agent) + + result = run_claude(prompt, log_name or skill_name, timeout, agent=agent) + + if chat_id and result.get("returncode", -1) == 0: + stdout = result.get("stdout", "") + for line in reversed(stdout.splitlines()): + line = line.strip() + if line.startswith("TELEGRAM_MSG:"): + msg = line[len("TELEGRAM_MSG:"):].strip() + if msg: + send_telegram(msg, chat_id=chat_id) + break # only ever send one message + + return result def run_script(func, log_name: str = "unnamed", timeout: int = 120) -> dict: From 11629a41a0ad0a3883a00ac2c6605046720ec694 Mon Sep 17 00:00:00 2001 From: dbadaniel Date: Wed, 29 Apr 2026 22:36:00 -0300 Subject: [PATCH 7/9] Fix Brain Repo restoration flow: accept token in query string, validate repo info, and improve SSE events --- dashboard/backend/brain_repo/restore.py | 24 ++++++++++++--- dashboard/backend/routes/brain_repo.py | 5 +++- .../pages/onboarding/restore/RestoreFlow.tsx | 5 +++- .../onboarding/restore/RestoreSelectRepo.tsx | 4 +-- .../restore/RestoreSelectSnapshot.tsx | 29 +++++++++++++++---- 5 files changed, 53 insertions(+), 14 deletions(-) diff --git a/dashboard/backend/brain_repo/restore.py b/dashboard/backend/brain_repo/restore.py index dc328483..9ca596cb 100644 --- a/dashboard/backend/brain_repo/restore.py +++ b/dashboard/backend/brain_repo/restore.py @@ -16,7 +16,14 @@ def _event(step: str, progress: int, message: str, error: bool = False) -> dict: - return {"step": step, "progress": progress, "message": message, "error": error} + event_type = "error" if error else "progress" + return { + "type": event_type, + "step": step, + "progress": progress, + "message": message, + "error": error + } def _cleanup_staging(staging: Path) -> None: @@ -47,6 +54,9 @@ def execute_restore( """ from brain_repo import git_ops, manifest as manifest_mod, migrations, secrets_scanner # type: ignore[import] + # ✅ Cria o diretório pai primeiro + STAGING_DIR.mkdir(parents=True, exist_ok=True) + staging = STAGING_DIR / f"restore-{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}" staging.mkdir(parents=True, exist_ok=True) @@ -57,10 +67,16 @@ def execute_restore( # If ref is not HEAD/default branch, checkout that specific ref if ref and ref not in ("HEAD", "main", "master"): ref_staging = staging / "_ref_extract" + ref_staging.mkdir(parents=True, exist_ok=True) # ✅ CREATE THE DIRECTORY FIRST git_ops.checkout_ref(staging, ref, ref_staging) # Use extracted ref content instead of full clone - shutil.rmtree(staging, ignore_errors=True) - ref_staging.rename(staging) + # ✅ Move o conteúdo de ref_staging para staging + for item in ref_staging.iterdir(): + dest = staging / item.name + if dest.exists(): + shutil.rmtree(dest, ignore_errors=True) + shutil.move(str(item), str(dest)) + shutil.rmtree(ref_staging, ignore_errors=True) except Exception as exc: yield _event("clone", 5, f"Clone failed: {exc}", error=True) _cleanup_staging(staging) @@ -194,4 +210,4 @@ def execute_restore( # ---------------------------------------------------------------- 10. complete _cleanup_staging(staging) - yield _event("complete", 100, "Restore complete") + yield {"type": "complete", "step": "complete", "progress": 100, "message": "Restore complete", "error": False} diff --git a/dashboard/backend/routes/brain_repo.py b/dashboard/backend/routes/brain_repo.py index 5bd8dd03..32a1f4ca 100644 --- a/dashboard/backend/routes/brain_repo.py +++ b/dashboard/backend/routes/brain_repo.py @@ -325,7 +325,7 @@ def connect(): create_repo - Name of a new private repo to create (mutually exclusive with repo_url) """ data = request.get_json() or {} - token = data.get("token", "").strip() + token = data.get("token", "").strip() or request.args.get("token", "").strip() repo_url = data.get("repo_url", "").strip() create_repo = data.get("create_repo", "").strip() @@ -527,6 +527,9 @@ def snapshots(): if not token: abort(400, description="Could not decrypt stored token") + if not config.repo_owner or not config.repo_name: + abort(400, description="Repository info not fully configured — please reconnect the brain repo") + try: from brain_repo.github_api import list_snapshots result = list_snapshots(token, config.repo_owner, config.repo_name) diff --git a/dashboard/frontend/src/pages/onboarding/restore/RestoreFlow.tsx b/dashboard/frontend/src/pages/onboarding/restore/RestoreFlow.tsx index 7f9d5f6f..cd816ef8 100644 --- a/dashboard/frontend/src/pages/onboarding/restore/RestoreFlow.tsx +++ b/dashboard/frontend/src/pages/onboarding/restore/RestoreFlow.tsx @@ -21,12 +21,14 @@ export default function RestoreFlow({ onComplete, onBack }: RestoreFlowProps) { const [step, setStep] = useState('select-repo') const [repoUrl, setRepoUrl] = useState('') const [snapshot, setSnapshot] = useState(null) + const [token, setToken] = useState('') if (step === 'select-repo') { return ( { + onNext={(url: string, pat: string) => { setRepoUrl(url) + setToken(pat) setStep('select-snapshot') }} onBack={onBack} @@ -38,6 +40,7 @@ export default function RestoreFlow({ onComplete, onBack }: RestoreFlowProps) { return ( { setSnapshot(s) setStep('confirm') diff --git a/dashboard/frontend/src/pages/onboarding/restore/RestoreSelectRepo.tsx b/dashboard/frontend/src/pages/onboarding/restore/RestoreSelectRepo.tsx index a7dfa790..4b4feac4 100644 --- a/dashboard/frontend/src/pages/onboarding/restore/RestoreSelectRepo.tsx +++ b/dashboard/frontend/src/pages/onboarding/restore/RestoreSelectRepo.tsx @@ -12,7 +12,7 @@ interface Repo { } interface RestoreSelectRepoProps { - onNext: (repoUrl: string) => void + onNext: (repoUrl: string, token: string) => void onBack: () => void } @@ -65,7 +65,7 @@ export default function RestoreSelectRepo({ onNext, onBack }: RestoreSelectRepoP setError(t('restore.selectRepo.selectRepo')) return } - onNext(selectedRepo.html_url) + onNext(selectedRepo.html_url, token.trim()) } return ( diff --git a/dashboard/frontend/src/pages/onboarding/restore/RestoreSelectSnapshot.tsx b/dashboard/frontend/src/pages/onboarding/restore/RestoreSelectSnapshot.tsx index 4822c17b..d5bae4b1 100644 --- a/dashboard/frontend/src/pages/onboarding/restore/RestoreSelectSnapshot.tsx +++ b/dashboard/frontend/src/pages/onboarding/restore/RestoreSelectSnapshot.tsx @@ -25,6 +25,7 @@ interface SelectedSnapshot { interface RestoreSelectSnapshotProps { repoUrl: string + token: string onNext: (snapshot: SelectedSnapshot) => void onBack: () => void } @@ -62,7 +63,7 @@ function SnapshotItem({ ) } -export default function RestoreSelectSnapshot({ repoUrl, onNext, onBack }: RestoreSelectSnapshotProps) { +export default function RestoreSelectSnapshot({ repoUrl, token, onNext, onBack }: RestoreSelectSnapshotProps) { const { t } = useTranslation() const [data, setData] = useState(null) const [loading, setLoading] = useState(true) @@ -71,11 +72,27 @@ export default function RestoreSelectSnapshot({ repoUrl, onNext, onBack }: Resto const [error, setError] = useState('') useEffect(() => { - api.get('/brain-repo/snapshots') - .then((d: SnapshotData) => setData(d)) - .catch(() => setError(t('restore.selectSnapshot.failed'))) - .finally(() => setLoading(false)) - }, [repoUrl, t]) + const loadSnapshots = async () => { + try { + setLoading(true) + setError('') + // ✅ Primeiro: conectar o repositório + await api.post('/brain-repo/connect', { token, repo_url: repoUrl }) + + // ✅ Depois: carregar os snapshots + const d = await api.get('/brain-repo/snapshots') as SnapshotData + setData(d) + } catch (err) { + setError(t('restore.selectSnapshot.failed')) + } finally { + setLoading(false) + } + } + + if (token && repoUrl) { + loadSnapshots() + } + }, [repoUrl, token, t]) const handleNext = () => { if (!selected) { From 31d4f7a6322dde5d8af5a1cd566bd72e72dcb5ae Mon Sep 17 00:00:00 2001 From: dbadaniel Date: Wed, 29 Apr 2026 23:00:31 -0300 Subject: [PATCH 8/9] fix(dashboard): add websocket-client to fix terminal proxy WS error --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 802fcdc0..66185c15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ "watchdog>=4.0", "sqlparse>=0.4,<1.0", "jsonschema>=4.21", + "websocket-client>=1.7", ] [project.scripts] From 8cf6afa2349cdf85113358c5bac9f55330c2afa9 Mon Sep 17 00:00:00 2001 From: dbadaniel Date: Wed, 29 Apr 2026 23:45:20 -0300 Subject: [PATCH 9/9] fix(dashboard): resolve DB migration crash (plugin_id vs slug) --- dashboard/backend/app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dashboard/backend/app.py b/dashboard/backend/app.py index 2dccb597..7b28a5e1 100644 --- a/dashboard/backend/app.py +++ b/dashboard/backend/app.py @@ -433,7 +433,7 @@ def _cors_allowed_origins(): ); CREATE TABLE IF NOT EXISTS plugin_audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, - plugin_id TEXT NOT NULL, + slug TEXT NOT NULL, action TEXT NOT NULL, payload TEXT, success INTEGER NOT NULL DEFAULT 1, @@ -450,8 +450,9 @@ def _cors_allowed_origins(): PRIMARY KEY (plugin_slug, handler_path) ); CREATE INDEX IF NOT EXISTS idx_plugins_status ON plugins_installed(status); - CREATE INDEX IF NOT EXISTS idx_plugin_audit_plugin ON plugin_audit_log(plugin_id, created_at); + CREATE INDEX IF NOT EXISTS idx_plugin_audit_slug ON plugin_audit_log(slug); CREATE INDEX IF NOT EXISTS idx_hook_cb_disabled ON plugin_hook_circuit_state(disabled_until); + """) _conn.commit() # --- End plugins migration ---