69 Commits

Author SHA1 Message Date
ce273d14db feat(voice): improve Romanian STT — hallucination gate + finetuned model
Gemma 4 cloud audio was infeasible (31b-cloud has no audio; E4B broken
upstream, no deploy host), so improve faster-whisper instead.

- Pin temperature=0.0 to disable the fallback ladder that re-decoded unclear
  audio up to 6x (source of the 16-24s latency outliers); reject hallucinated
  segments via avg_logprob/compression_ratio in the new pure _filter_segments.
- Adopt mikr/whisper-small-ro-cv11 (CT2 int8) via configurable voice.stt_model:
  spike showed WER 24%->10%, numbers fixed at source, +0.33s p50 (in budget).
- Add tools/voice_stt_mine.py (log mining) + tools/voice_stt_spike.py (model
  eval with diacritic scoring) + tests for the gate and miner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-27 18:16:16 +00:00
5c9748ffb4 feat(memory): hybrid retrieval — navigation index.md + RAG hardening
Expose a navigation layer to the agent and harden RAG, after analyzing the
OKF note and testing on the real KB.

- memory_search.search(): dedupe best-chunk-per-file (a relevant note can no
  longer be buried by another file's chunks) + keyword fallback tagged
  degraded:True when Ollama is unreachable (no more hard crash).
- update_notes_index.py: emit per-folder index.md + root router; prune empty
  folders; fix latent subcategory->project bug.
- Exclude generated index.md from RAG rglob (reindex/incremental) + indexer
  scans + heartbeat freshness check (prevents self-pollution / reindex thrash).
- CLAUDE.md: reframe memory as hybrid (navigation first, RAG for fuzzy recall).
- Delete stale orphan kb/youtube/index.json; correct the OKF source note.
- Tests: dedup, keyword fallback, index.md exclusion. Plan + review in docs/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 17:52:27 +00:00
a8d024944d chore: auto-commit from dashboard 2026-06-09 09:13:35 +00:00
735b282179 automatic 2026-05-29 13:35:15 +00:00
0ce8a5a04d Update cron, dashboard, root +3 more (+1 ~11) 2026-05-28 20:21:28 +00:00
e79bed7afe feat(voice): unify Discord voice↔text session (squash of voice/text-unify)
Voice utterances and text messages on the same Discord channel now share
one Claude session, and Echo's voice replies are mirrored back into the
text channel. Replaces the old voice:<id> session-key split.

Changes:
- src/adapters/_text_chunks.py: new leaf module for split_message
  (used by both discord_bot and voice pipeline)
- src/router.py: drop voice: prefix from session_key; add [voice] marker;
  strip leading [speaker:/[voice] tokens from user input (anti-jailbreak);
  remove dead double-clear of voice: key
- src/claude_session.py: include personality/VOICE_MODE.md unconditionally
  (rules become per-turn-aware via [speaker:] prefix instead of session flag)
- src/voice/pipeline.py: VoiceSession splits text_channel_id +
  voice_channel_id; resolve text channel per-send (no stale refs); mirror
  Echo's reply text into the text channel after route_message returns
- src/adapters/discord_voice.py: /voice join passes both channel ids
- src/adapters/discord_bot.py: import split_message from leaf module
- personality/VOICE_MODE.md: rewrite as per-turn dynamic rules;
  add synthesis instructions for text turns after voice turns

Tests:
- tests/test_router.py: 4 new cases (plain channel_id, anti-jailbreak,
  text-adapter regression, no-double-clear)
- tests/test_pipeline_mirror.py: new — Echo reply mirror chunking,
  empty guard, mirror_enabled=False, send-raises resilience
- tests/test_voice_session_channel_ids.py: new — split-attr contract
  + metrics payload schema
- tests/test_voice_session_cleanup.py: update for new kwargs

Plan: /home/moltbot/.claude/plans/vreau-ca-tot-textul-greedy-rivest.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:24:15 +00:00
4be70440e8 feat(voice): DAVE E2E + full voice UX (squash of voice/dave-recv)
Squashed branch: voice/dave-recv → master. Closes Pas 12 (DAVE E2E) and lands
voice-mode UX polish + verbal voice control on top of the Pas 1-10 scaffolding
already on master.

## DAVE E2E receive-side decrypt (e4f3177)

Vendored fork: discord-ext-voice-recv 0.5.3a+echo.dave1. Patches the receive
pipeline to handle Discord's mandatory DAVE encryption on voice gateway v=8.
- `_maybe_dave_decrypt`: uses davey.can_passthrough(user_id) as primary gate,
  falls through to dave.decrypt for DAVE-epoch peers, drops on decrypt failure
  without killing the reader thread.
- VAD fix: silero-vad v5+ requires exactly 512 samples; our 100ms window
  (1600 samples) was silently raising ValueError → STT never fired. Now slice
  into 512-sample chunks.
- Whisper: bumped beam_size 1→5 and added RO initial_prompt.
- Tests: 11 DAVE unit tests + 2 callback integration tests + contract test
  with fork-version guard.

## Voice UX polish (d1bc77e)

- Killed the 3s "mă gândesc" filler (always collided with Claude p50 4-7s).
- Barge-in via `ttsq.clear()` at top of `on_segment_done`.
- DTX silence-flush poller (200ms tick) — Discord stops sending RTP packets
  when silent, so the inline silence-check in sink.write() never fired for
  trailing audio; background thread handles it.
- `EchoStreamingAudioSource.read()` non-blocking — old `get_frame(timeout=0.1)`
  wrecked Discord's 20ms cadence and the client interpreted bursts as
  stuttering (Marius heard "4 de minute" instead of full sentence).
- RO time expansion: 23:09 → "douăzeci și trei și nouă minute".
- Supertonic Unicode sanitize centralized in tools/tts.py.
- Whisper local_files_only=True — no HF metadata GET on each startup.
- Diagnostic logging through sink → VAD → Claude stream → TTS chain.

## Voice mode iteration (e589e48)

- `personality/VOICE_MODE.md` — voice-tailored system prompt (short, no
  markdown, no abbreviations, time without seconds, distances in
  "mii"/"milioane"); plumbed via build_system_prompt(voice_mode=True).
- Isolated voice session key `voice:<channel_id>` — voice doesn't share
  context with text adapter on the same channel; auto-applied without
  /clear ceremony. /clear drops both keys.
- Metric units + Romanian thousands (normalize.py): "384.000 km" →
  "trei sute optzeci și patru de mii de kilometri" with feminine-correct
  pluralization and "de" particle for ≥20.
- `/voice setvoice <M1-F5>` slash command with native autocomplete; swaps
  live + persists voice.default_voice to config.json.
- Verbal voice change (src/voice/voice_commands.py + 29 tests) — "schimbă
  vocea pe M5", "voce em cinci", with permissive substring fallback for
  Whisper-mangled forms like "Mâcinci"=M5 and "unul cinci"=M5. Whisper
  initial_prompt now lists voice vocabulary to bias STT toward clean
  outputs.
- Fast barge-in: VAD ≥2 consecutive windows (~200ms) on Marius's user
  while Echo has pending TTS frames → cut him off mid-sentence so user
  doesn't wait the full silence + STT cycle. Acoustic echo bleed-through
  still requires headphones (no AEC).

## Test suite

130 voice + router tests pass (test_voice_recv_dave, test_voice_session_cleanup,
test_voice_adapter_contract, test_voice_normalize, test_voice_commands,
test_router).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 21:00:27 +00:00
13931db953 feat(voice): Pas 7 — discord_voice.py slash group + discord_bot wiring (CONVERGENCE)
src/adapters/discord_voice.py (NEW, ~280 linii):
- /voice slash group cu subcommands: join, leave, doctor, mirror on|off,
  record on|off
- warmup_models() async — eager faster-whisper + silero-vad load la
  on_ready pe background task
- _voice_load_error guard — /voice join responds ephemeral graceful
  dacă models load fail
- _voice_sessions: dict[int, VoiceSession] keyed pe guild_id
- _get_whitelist() re-reads config la fiecare apel — runtime edits la
  voice.allowed_user_ids fără bot restart
- Double-join guard, try/except graceful pe connect/listen/play/presence
- /voice doctor surfaces _voice_load_error + libopus state ephemeral
- await interaction.response.defer(ephemeral=True) în orice voice
  command (Discord 3s timeout pattern din CLAUDE.md)

src/adapters/discord_bot.py — 3 surgical edits:
- Linia 115: intents.voice_states = True (după intents.message_content)
- Liniile 963-966: import + register_voice(tree, client) +
  tree.add_command(voice_group), după /audio body
- Liniile 1126-1130: discord_voice._models_warmup_future =
  asyncio.create_task(discord_voice.warmup_models()) la end of on_ready

Adapted la pipeline.py API actual (channel_id int nu str, kw-only args
după *, EchoVoiceSink(session, bot_user_id) signature, loop kwarg
mandatory pentru cross-thread bot.change_presence).

Smoke import OK. test_discord.py 61 pass / 4 fail (pre-existing pe
master, verificat via git stash). test_voice_session_cleanup 5/5 +
test_voice_adapter_contract 22/22.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:55:57 +00:00
23666f7910 feat(voice): Pas 5 — voice/pipeline.py VoiceSession + EchoVoiceSink + cleanup
Central voice pipeline (~250 LOC + docstrings = ~430 lines):

VoiceSession (context manager + idempotent cleanup pe 5 căi):
- __enter__: acquire _lock, open JSONL (record=on)
- __exit__: calls cleanup("exit"), nu suprimă exceptions
- cleanup(reason): IDEMPOTENT, side effects o singură dată — JSONL
  flush+close (record=on) sau delete (record=off), bot presence cleared,
  voice_client.cleanup(), ttsq.stop(), cancel filler task, lock release,
  structured log la logs/voice_metrics.jsonl
- on_segment_done(speaker_id, text, no_speech_prob): mirror text channel,
  append JSONL, arm 3s filler timer, route_message cu on_text callback
  + cancel filler la first block
- last_activity_ts: time.monotonic() — caller-driven 5min auto-leave

EchoVoiceSink(session, bot_user_id):
- wants_opus() False (PCM)
- write() runs în voice_recv reader thread (threading primitives only):
  - GUARD 1: user None/id==0/id==bot_user_id → return (load-bearing
    echo prevention)
  - GUARD 2: whitelist filter (empty = allow all)
  - Buffer 20ms packets per-user → batch 100ms (5×20ms = 19200 bytes)
    → silero-vad threshold 0.5 → 800ms cumulative silence flush
  - _flush_to_stt: faster-whisper small int8 cpu_threads=4 lang=ro
    beam_size=1, no_speech_prob > 0.6 drop, schedule on_segment_done
    via run_coroutine_threadsafe pe session.loop

Module helpers (lazy thread-safe singletons): _get_whisper_model,
_get_silero_vad. Constants: FILLER_DELAY_S=3.0, SILENCE_FLUSH_MS=800,
VAD_THRESHOLD=0.5, VAD_WINDOW_MS=100, NO_SPEECH_DROP_THRESHOLD=0.6.

Decisions:
- STT runs in audio thread — acceptable la 2.25s p50 (user just stopped
  talking, no batching contention). Wrap în ThreadPoolExecutor.submit
  if perf bites later.
- Downsample 48k→16k via 3-sample averaging (no scipy dep). Whisper
  robust la mild aliasing.
- Energy-RMS VAD fallback dacă torch import fail — graceful degrade.
- router_route_message injection seam ca kwarg pentru testabilitate.
- bot.change_presence handling cross-thread via run_coroutine_threadsafe.

tests/test_voice_session_cleanup.py — 6 tests:
- voice_leave / disconnect / crash via __exit__ / auto_leave /
  user_left_channel (5 cleanup paths each verified for: JSONL state,
  presence cleared, voice_client.cleanup, ttsq.stop, lock release,
  idempotency)
- 1 robustness cross-cut (double-cleanup safety)

6/6 PASS. Regression suite 63/63 PASS (normalize + adapter + mutex).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:55:57 +00:00
217da65417 feat(voice): Pas 6 — voice/tts_stream.py streaming TTS pipeline
src/voice/tts_stream.py (~280 lines):
- clause_segments(text, min_words=8): yield Romanian-aware clause chunks.
  Split la punct (./!/?;:,) cu accumulation până min_words satisfied;
  edge case text < min_words → single chunk. NU split mid-word/number/
  currency. Romanian intonație de frază se rupe la sentence break — 8+
  words minimizează seams.
- TTSQueue worker thread: text queue in → PCM frames out. Methods:
  start/stop/push_text/push_filler/clear/is_empty. normalize_for_tts()
  apply first, then clause_segments(), then Supertonic synth per chunk.
- EchoStreamingAudioSource(discord.AudioSource): read() pull from PCM
  queue, 20ms frames (3840 bytes 48kHz s16le stereo). Eliminates RTP
  gap between play() calls — single play() per session, source pulls.
- load_thinking_wav(): one-shot cache → 140 × 20ms frames (~2.8s)
  pentru filler "Stai puțin să-mi adun gândurile".
- wav_to_pcm_20ms_frames(): WAV parser + ffmpeg subprocess resample
  la 48kHz s16le stereo dacă nevoie.

Smoke test (în session): clause_segments behaviour OK, thinking.wav
loads, TTSQueue + EchoStreamingAudioSource construct clean. Integration
testing deferred la convergență (Pas 7 + Pas 11).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:44:13 +00:00
3af6bcaea4 feat(voice): Pas 8 — threading.Lock per channel_id mutex + voice augment
Fix arhitectural general (beneficiu și pentru text adapters), nu doar voice.

src/claude_session.py:
- _session_locks: dict[str, threading.Lock] cu bootstrap lock pentru
  lazy creation thread-safe.
- _get_session_lock(channel_id) helper.
- send_message() body wrapped în with _get_session_lock(channel_id).
- threading.Lock (NU asyncio.Lock) — send_message e sync subprocess.run
  blocking; asyncio.Lock nu protejează cod sync rulat via to_thread.
- Per-channel granularity preserved — different channels run în paralel.
- send_message() public signature unchanged.

src/router.py:
- route_message(): dacă adapter_name == "discord-voice", prepend
  [speaker:<user_name>] prefix (Config.get("voice.user_name", "user")).
- Original text variable left untouched for downstream paths.
- Text adapters: zero behavior change.
- route_message() public signature unchanged.

tests/test_claude_session_mutex.py — 6 tests REGRESSION-CRITICAL:
- same channel serializes (concurrent → mutex serializes, no overlap)
- same channel lock identity (same dict entry per channel_id)
- different channels run in parallel (overlap MUST fire)
- 3 channels all overlap
- contested acquire blocks then proceeds (policy: blocking, not fail-fast)
- lock released on subprocess exception (no deadlock on crash)

Acquisition policy: BLOCKING acquire bound by claude --timeout (5min default)
nu fail-fast — adapters already serialize via asyncio.to_thread queue, un
non-blocking acquire ar surface transient busy errors.

Test results: 82 passed (51 existing + 31 new). 2 PRE-EXISTING failures în
TestPromptInjectionProtection (stale assertion vs current prompt text) —
out of scope, recomand ticket separat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:43:05 +00:00
a3eefbc799 feat(voice): Pas 4 — _discord_voice_adapter.py thin layer + contract test
Adapter layer peste vendored discord-ext-voice-recv. Re-exports:
VoiceReceiveClient, AudioSink, VoiceData, plus async helper
connect_voice(channel). Discord voice protocol e fragil, upstream e
hobby fork — dacă pică, swap la py-cord = doar acest fișier rescris.

Contract test (22 assertions) prinde drift la upgrade vendor:
- VoiceReceiveClient methods: connect/disconnect/listen/stop_listening/
  is_listening/stop/cleanup
- listen(sink, *, after=None) signature
- sink property (read/write)
- AudioSink methods: write/cleanup/wants_opus + write(self, user, data) arity
- VoiceData slots (packet/source/pcm) + .opus property

Critical pentru Lane PIPE downstream: write() e called from audio thread
(NOT asyncio loop) — threading primitives mandatory pentru EchoVoiceSink.

22/22 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:42:50 +00:00
a48562b2f5 feat(voice): Pas 3 — voice/normalize.py + 35 RO test cases
Pure functions pentru TTS text normalization (RO):
- strip_markdown: regex bold/italic/code/link/heading/list
- expand_numbers_ro: num2words pentru cardinals + decimal handling
  ("3.14" → "trei virgulă paisprezece", "3.05" → "trei virgulă zero
  cinci" digit-by-digit la leading zero)
- expand_currency: formă naturală RO ("12.50 RON" → "doisprezece lei
  și cincizeci de bani", "$25.99" → "douăzeci și cinci de dolari și
  nouăzeci și nouă de cenți")
- expand_symbols: %/&/@/° + whitespace collapse
- expand_abbreviations: etc./dl./dna./nr./ş.a./ş.a.m.d.
- normalize_for_tts: full pipeline + hard truncate 200 cuvinte cu
  "Restul l-am scris în chat."

Pipeline order: markdown → abbreviations → currency → numbers →
symbols → truncate. Currency BEFORE numbers — altfel "12.50 RON" se
degradează la "doisprezece virgulă cincizeci RON". Romanian "de"
particle rule: n>=20 AND (n%100 not in 1..19) → "o sută de lei",
"o sută cinci lei" (no "de"). n=1 with currency → "un dolar" /
"un leu" (article, nu cardinal).

35/35 tests pass: markdown(5), cardinals(6), decimals(4), currency
RON/USD/EUR/GBP mix(8), symbols(4), abbreviations(4), truncation(2),
edge cases empty/whitespace(2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:42:41 +00:00
af5af8133f feat(voice): Pas 2 — install voice deps, vendor discord-ext-voice-recv, setup assets
Foundation pentru Discord voice-to-voice pipeline.

- requirements.txt: faster-whisper, silero-vad, num2words, numpy, PyNaCl
- vendor/discord-ext-voice-recv/: vendored la commit ac04ea7b09 (bump version
  0.5.3a) — Discord voice protocol fragil, upstream hobby fork. Adapter layer
  in src/voice/_discord_voice_adapter.py izolează churn (swap la py-cord =
  doar acel fișier rescris). VENDOR_INFO.md documentează update procedure.
- tools/voice_setup.py: idempotent setup script — libopus check, ffmpeg
  check, Supertonic reachable, faster-whisper/silero-vad warm, assets
  generation. Exit 0 = green, 1 = needs human (currently libopus missing
  needs `sudo apt install -y libopus0`).
- assets/voice/: thinking.wav (filler "Stai puțin să-mi adun gândurile",
  ~2.8s), mhm.wav (listener noise), beep_200ms.wav (wake-up tone 880Hz).
- src/voice/__init__.py: package stub.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:42:27 +00:00
44cf0001bb chore: auto-commit from dashboard 2026-05-27 06:12:13 +00:00
574f9be5ea feat(discord): add /audio slash command with voce + text_sau_url params
Adds missing /audio slash command on Discord with:
- voce: optional choices M1-M5 / F1-F5 with descriptions
- text_sau_url: optional text or URL input
- handles __AUDIO__: response by sending WAV as file attachment

Telegram already had /audio fully implemented.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 06:00:54 +00:00
0d2d5b860d chore(tts): schimbă vocea default din M1 în M2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 05:56:55 +00:00
8fe39adc01 fix(tts): trimite lang=ro explicit la Supertonic API
Parametrul `lang` era definit (DEFAULT_LANG = "ro") dar nu era inclus
in request-ul HTTP catre /v1/audio/speech. Adaugat "lang": lang in
body-ul JSON si lang="ro" explicit in _tts_synthesize().

OpenAPI-ul Supertonic confirma ca /v1/audio/speech accepta `lang`
ca parametru optional (OpenAISpeechRequest schema).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 05:47:39 +00:00
3dd2ddbd6a chore: auto-commit from dashboard 2026-05-27 05:40:22 +00:00
3570d9a625 chore(kb): notițe youtube mai, fix email tools, update newsletter/anaf-monitor
Adaugă 4 notițe YouTube (llama.cpp, Mario Zechner, bonificatie impozit,
AI scaffolding) + notă coaching grok. Actualizează index KB.
Fix email_digest și email_forward. Update newsletter cercetasi + cron jobs.
ANAF monitor hashes/snapshots/versions la zi.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 21:02:55 +00:00
2bcefe1ab4 feat(projects): approval guard + worktree-aware ralph execution
Two structural fixes that together let users manage feature-branch
work without manual intervention:

Approval guard — `/plan/start` returns 409 `already_committed` if the
project status is approved/running/complete, unless the body opts in
with `force=true`. Frontend now renders "Re-planifică" instead of
"Planifică" on approved cards and gates it behind a confirm dialog
that threads `force=true` through. Prevents an accidental click from
wiping `status=approved` and burning a fresh planning subprocess.

Worktree awareness — projects can now declare that they target a
feature branch on an existing Gitea repo, not a repo-per-slug clone.
Three optional fields added to approved-tasks.json: `repo` (default
= slug), `branch` (feature branch to create), `base_branch` (default
main). Wired through `/p` flag parser in router.py, the dashboard
Propose modal's new "Avansat" section, and the night-execute prompt
which clones {repo} and creates {branch} from {base_branch} before
running ralph.

CLAUDE.md updated with both flows + the new schema fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:27:14 +00:00
8432fe3150 feat(planning): full chat history + auto-advance phases
Three fixes that together restore the planning UX:

- Dashboard reopen showed only a 500-char truncated excerpt of the last
  assistant message. Backend now reads the Claude session JSONL directly
  and returns full per-turn history; frontend iterates and renders all
  bubbles, falling back to last_text_excerpt when the JSONL is missing.
- Phases never advanced because the agent ran /plan-* skills inline as
  tool calls and the marker protocol was loose. Tightened the planning
  prompt (mandatory PHASE_STATUS marker on the last line of every turn,
  ban on inline phase invocation), and the frontend now auto-calls
  /plan/advance when phase_ready=true.
- The phase strip never showed visual state because data-phase values
  ("office-hours") didn't match orchestrator phase names ("/office-hours").
  Added normalizePhase + cleanup of PHASE_STATUS markers from rendered
  bubbles.

Also bumps eco.py session-content truncation from 2k to 20k so /eco
session views aren't cut mid-response either.

Bumps last_text_excerpt fallback in planning_session.py from 500 to
50_000 so even when the JSONL is unavailable, the bubble isn't sliced
mid-word.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 07:47:10 +00:00
5e930ade02 feat(dashboard): unified workspace hub — cookie auth, 9-state projects, planning chat
Merges workspace.html + ralph.html into a single unified project hub with:
- Cookie-based auth (DASHBOARD_TOKEN, HttpOnly, SameSite=Strict)
- 9-state project badge system (running-ralph/manual, planning, approved,
  pending, blocked, failed, complete, idle) with BUTTONS_FOR_STATE matrix
- SSE realtime + polling fallback, version-based optimistic concurrency (If-Match)
- Planning chat modal (phase stepper, markdown bubbles, 50s+ wait state, auto-resume)
- Propose modal (Variant B: inline Plan-with-Echo checkbox)
- 5-type toast taxonomy (success/info/warning/busy/error, 3px colored left-bar)
- Inter font self-hosted + shared tokens.css design system + DESIGN.md
- src/jsonlock.py (flock helper, sidecar .lock for stable inode)
- src/approved_tasks_cli.py (shell-safe wrapper for cron/ralph.sh)
- 55 new tests (T#1–T#30) + real jsonlock bug fix caught by T#16/T#28
- No emoji anywhere (enforced by test_dashboard_no_emoji.py)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 07:26:19 +00:00
3e7818286b feat(ralph): rate limit budget tracking + whatsapp text-keywords
Task #1 — Rate limit budget tracking MVP:
- tools/ralph_usage.py: pure functions (extract_usage_entry, parse_usage_jsonl,
  aggregate_by_day/_project, filter_by_days, summarize) + CLI append/summarize
  subcommands. Atomic write via temp+rename.
- tools/ralph/ralph.sh: după fiecare claude -p, append usage entry
  derivat din JSON envelope la <project>/scripts/ralph/usage.jsonl. Best-effort,
  niciodată blochează rularea (|| true).
- dashboard/handlers/ralph.py: GET /api/ralph/usage[?days=N] aggregează cross-
  project și returnează {today_cost, today_runs, by_project, by_day, ...}.

Task #2 — WhatsApp text-keyword commands:
- src/router.py: helper _translate_whatsapp_text mapează "aprob"/"stop <slug>"/
  "stare [<slug>]" → /a, /k, /l. Apelat DOAR pe adapter whatsapp în
  _try_ralph_dispatch (Discord/TG nu sunt afectate). NU acoperim propose
  intentionat — descrierea liberă e prea fragilă pentru parsing text-only.

Tests: 49 noi (test_ralph_usage 28 + test_whatsapp_keywords 21) + 4 noi în
test_dashboard_ralph_endpoint pentru /api/ralph/usage. Toate trec; regression
suite (test_router, test_router_planning, test_dashboard_ralph_endpoint,
test_whatsapp) — 90/90 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:05:50 +00:00
dedeedf024 fix(ralph): "Planifică" deschide modal/ForceReply când descrierea lipsește
Înainte: click pe 🧠 Planifică (Discord/Telegram) sau /plan <slug> fără descriere
pe un proiect din workspace fără entry în approved-tasks.json → mesaj eroare
"Adaugă mai întâi cu /p <slug> <descriere>" și user-ul trebuia să facă două
operații.

Acum:
- Discord button "Planifică" cu descriere goală → deschide RalphPlanModal cu
  TextInput pentru descriere; on_submit pornește direct start_planning_session
- Discord /plan <slug> fără description param și fără entry → același modal
  (response.send_modal ÎNAINTE de defer — Discord constraint)
- Telegram callback "Planifică" cu descriere goală → set state
  STEP_INPUT_DESCRIPTION_THEN_PLAN + ForceReply; handle_message detectează
  step și pornește planning cu textul user-ului
- ralph_flow.py: nou STEP_INPUT_DESCRIPTION_THEN_PLAN (alături de cel existent
  pentru propose-only)

start_planning_session deja auto-creează entry în approved-tasks.json dacă
proiectul lipsește, deci flow-ul e end-to-end: workspace → click → descriere
→ planning agent activ.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:51:09 +00:00
51e56af557 feat(ralph): conversational planning agent (W2)
Echo Core devine planning agent: poartă o conversație multi-fază cu Marius
folosind skill-urile gstack (/office-hours → /plan-ceo-review →
/plan-eng-review → /plan-design-review opt) și produce final-plan.md în
~/workspace/<slug>/scripts/ralph/, gata să fie consumat de Ralph PRD
generator (W3) noaptea.

Decizii arhitecturale (din eng review + spike findings):
- PlanningSession ca clasă SEPARATĂ de chat-ul main (NU mode=string param)
  — separation explicit. claude_session.py rămâne strict pentru chat;
  planning trăiește în src/planning_session.py + src/planning_orchestrator.py.
  Inheritance literală nu se aplică (claude_session.py expune funcții
  module-level, nu o clasă) — separation e satisfacută prin module distinct.
- Fresh subprocess PER skill phase, NU single resumed session — phase-urile
  coordinează via disk artifacts (gstack convention în
  ~/.gstack/projects/<slug>/). Avoids context window growth.
- --max-turns 20 default + retry pe error_max_turns la --max-turns 30.
  Spike a arătat că prompt-uri complexe pot exploda turn budget-ul.
- approved-tasks.json schema extins cu planning_session_id + final_plan_path
  (Status flow: pending → planning → approved → running → complete).
- State separat în sessions/planning.json (NU active.json), keyed pe
  (adapter, channel_id) pentru re-resume la restart echo-core.

Trigger-e:
- Discord: slash command /plan <slug> [descriere] cu autocomplete pe pending,
  buton "🧠 Planifică" în RalphProjectView, și /cancel slash command.
- Telegram: /plan + /cancel commands, plus buton "🧠 Planifică" în
  ralph project keyboard.
- Router: state-aware routing — dacă chat-ul e în planning, mesajele plain
  trec la PlanningOrchestrator.respond() prin --resume; /cancel revine la
  status pending; /advance / "Continuă faza" advance fază nouă (fresh
  subprocess); /finalize sau "Dau drumul" promote la status approved.

Discord defer pattern: toate butoanele noi (PlanningActiveView,
PlanningFinalView, "🧠 Planifică") apelează await
interaction.response.defer(ephemeral=True) ÎNAINTE de orice IO — evită
"Interaction failed" pe IO >3s.

UX strings warm + colaborativ (per design review): "🧠 Pornesc planning
pentru ...", "Răspunde aici", "Continuă faza", "Dau drumul tonight",
"Anulează" — niciun "Submit/Approve/Cancel" generic.

Tests: 23 noi (test_planning_session, test_planning_orchestrator,
test_router_planning) — toate pass. Mock pe _run_claude pentru a evita
subprocess Claude real în CI.

Files new:
  prompts/planning_agent.md
  src/planning_session.py
  src/planning_orchestrator.py
  tests/test_planning_session.py
  tests/test_planning_orchestrator.py
  tests/test_router_planning.py

Files modified:
  src/claude_session.py        — _run_claude(cwd=...) optional + surface subtype/is_error
  src/router.py                — state-aware routing, start_planning_session, planning_advance/approve/cancel, _ralph_propose schema cu planning_session_id + final_plan_path
  src/adapters/discord_bot.py  — /plan + /cancel slash commands; planning views imported
  src/adapters/discord_views.py — PlanningActiveView, PlanningFinalView, "Planifică" button în RalphProjectView, _split_chunks helper
  src/adapters/telegram_bot.py — /plan + /cancel handlers, callback_ralph extins cu plan/planadvance/plancancel/planapprove, planning keyboards

Status testelor pe modulele atinse: 75 passed, 0 failed
(test_claude_session security_section preexistent — neatins).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:38:51 +00:00
86384b38e3 feat(ralph): interactive UX layer pe Discord și Telegram (W1)
Adaugă straturile interactive peste slash commands flat:

**Discord (`src/adapters/discord_views.py`):**
- `RalphRootView` — listă proiecte workspace cu emoji status + Refresh + Close
- `RalphProjectView` — Propose / Vezi PRD / Aprobă tonight / Status / Stop / Înapoi
- `RalphProposeModal` — TextInput pentru descriere feature
- Critical pattern: `await interaction.response.defer(ephemeral=True)` în orice button
  callback cu I/O (eng review concern #2 — "Discord 3s timeout")
- `/p slug` autocomplete din `~/workspace/`
- `/l` afișează `RalphRootView` ephemeral

**Telegram (`src/adapters/telegram_bot.py`):**
- `cmd_ralph_l` (fără arg) trimite `InlineKeyboardMarkup` cu workspace + active
- `callback_ralph` cu pattern `^ralph:` rutează: project, menu, refresh, close,
  propose, prd, status, approve, stop
- Pentru "Propose feature" → set ralph_flow state cu step=input_description
  + `ForceReply()`; `handle_message` detectează state și rutează la `_ralph_propose`
- Pasează `adapter_name="telegram"` la `route_message`

**State management (`src/ralph_flow.py`):**
- Atomic JSON peste `sessions/ralph_flow.json` (pattern reusat din claude_session)
- Schema per (adapter, chat, user): `{step, project?, expires_at, ...}`
- TTL 10 min default; `cleanup_expired()` și auto-drop la `get_state` pe expirate

**Router (`src/router.py`):**
- `route_message` primește `adapter_name` keyword arg
- `_maybe_whatsapp_redirect` adaugă "💡 Pentru meniu interactiv folosește
  Discord sau Telegram" la mesajele de usage când adapter_name="whatsapp"
- WhatsApp `_handle_chat` pasează `adapter_name="whatsapp"`

**Tests:**
- `test_ralph_flow.py` — 10 teste (round-trip, isolation, expiry, atomic write)
- `test_router.py::TestRalphDispatch` — 3 teste (whatsapp redirect, discord
  no-redirect, usage message)

Foundation pentru W2 (planning agent — STEP_IN_PLANNING reservat).

Spike Step 0 PASS: skill subprocess + AskUserQuestion→text serialization
confirmat empiric (vezi tasks/spike-planning-findings.md).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:14:24 +00:00
094c6be5a9 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>
2026-04-26 17:46:52 +00:00
90c2a90b5e feat(ralph): add autonomous project execution system
- router.py: add !approve, !status, !stop, !propose commands for project lifecycle management
- approved-tasks.json: coordination schema for evening→night→morning pipeline
- tools/ralph/: ralph.sh loop, prompt.md, prd-template.json
- cron/jobs.json: enable morning-report, evening-report, night-execute (23:00 opus)

Evening-report proposes features to approved-tasks.json as 'pending'; Marius
approves via !approve; night-execute launches ralph.sh per project.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 15:20:52 +00:00
0bfa652b31 fix(heartbeat): suppress git-only alerts when rest is ok
Uncommitted files alone are not an actionable heartbeat alert.
Only send a message if there are other findings besides git status.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 08:04:34 +00:00
1b2b37a6bb chore: auto-commit from dashboard 2026-04-23 21:24:43 +00:00
537bab465c refactor(main): remove unused Python heartbeat in favor of cron job
Heartbeat is now handled exclusively by the Claude-based cron job
(heartbeat-2h in jobs.json), which is more flexible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 20:02:23 +00:00
0c02f0de50 fix(scheduler): suppress channel send when result is HEARTBEAT_OK
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 19:59:11 +00:00
b9a5f733c2 chore: auto-commit from dashboard 2026-04-23 09:44:20 +00:00
e747491b85 feat(scheduler): add kind:"shell" jobs with Bucharest tz and GSTACK-CRON marker
- AsyncIOScheduler now runs in Europe/Bucharest so cron strings in jobs.json
  match local wall-clock time.
- New add_shell_job() validates name, cron, command list, channel, report_on
  (always|changes|never), and optional timeout (1..3600s). Existing add_job()
  stays untouched for the Claude path.
- _execute_job dispatches on job['kind'] (default 'claude'); legacy jobs
  without the field still route to the Claude executor. Refactored the
  Claude path into _execute_claude_job; new _execute_shell_job runs
  subprocess with _safe_env + PROJECT_ROOT cwd.
- Shell semantics: non-zero exit always forwards stderr (trimmed to 500 ch)
  as '[cron:NAME] exit CODE: STDERR' regardless of report_on. On exit 0,
  'always' forwards stdout (trimmed to 1500 ch), 'never' stays silent, and
  'changes' parses the GSTACK-CRON marker (^GSTACK-CRON: changes=\d+$) and
  forwards stdout only when N>0; missing/malformed marker logs a warning
  and stays silent.
- Timeout honoured per-job (falls back to JOB_TIMEOUT=300s).
2026-04-21 07:05:19 +00:00
9314d63aa0 feat(email): add digest and forward commands to WhatsApp
Digest summarizes unread emails via Claude CLI; forward sends raw
content (split to 4096 chars). Wired as /email digest and
/email forward slash commands, plus instant per-guild sync on ready.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 05:45:02 +00:00
9d447b9ff1 fix(newsletter): use follow_redirects=False to avoid false positive on 404 redirect
Beehiiv redirects non-existent newsletters to /?404=... with HTTP 302.
With follow_redirects=True, the final 200 was misread as "newsletter exists".
Fix: disable redirect following so only a direct HTTP 200 = newsletter real.
Also reset state back to last_sent=13 (real).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 17:06:00 +00:00
5fafc29dc1 chore: auto-commit from dashboard 2026-04-02 19:43:01 +00:00
d9450ce70d chore: auto-commit from dashboard 2026-04-02 17:43:13 +00:00
006123a63b chore: auto-commit from dashboard 2026-03-03 20:17:07 +00:00
19e253ec43 feat(heartbeat): save emails to KB + fix memory symlink access
- heartbeat saves unread whitelisted emails via email_process --save --json
- fix: add --add-dir so Claude CLI subprocess can access memory/ symlink
- email_check/process: use BODY.PEEK[] to avoid marking emails as read
- email_process: simplify credential loading via credential_store only
- config: heartbeat interval 30→120min, quiet hours end 08→07

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 12:10:53 +00:00
08c330a371 corectii 2026-02-19 14:09:12 +00:00
95bd651377 openrouter 2026-02-17 09:30:35 +00:00
MoltBot Service
fd9d962ad2 chore: auto-commit from dashboard 2026-02-15 23:23:29 +00:00
MoltBot Service
8ce7ea3bd6 refactor(heartbeat): move kb/embeddings reindex messages to log only
KB and embeddings reindex status messages were being sent to Discord
as heartbeat results. These are internal housekeeping — now logged
instead of surfaced to the user.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-15 22:52:28 +00:00
MoltBot Service
207b39f957 chore: update allowed_tools syntax + gitignore memory.bak
- Switch Bash permission patterns from space to colon separator
- Add memory.bak/ to .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 21:14:53 +00:00
MoltBot Service
b3c06c0238 refactor(session,pdf): simplify git perms + rich text PDF
- claude_session: replace 10 individual git command patterns with single Bash(git *) wildcard
- generate_pdf: add italic/bold-oblique font loading and render_rich_text() for inline bold/italic

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 21:05:14 +00:00
MoltBot Service
c8ce94611b feat: add 19 fast commands (no-LLM) + incremental embeddings indexing
Fast commands for git, email, calendar, notes, search, reminders, and
diagnostics — all execute instantly without Claude CLI. Incremental
embeddings indexing in heartbeat (1h cooldown) + inline indexing after
/note, /jurnal, /email save. Fix Ollama URL (localhost → 10.0.20.161),
fix email_process.py KB path (kb/ → memory/kb/).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 15:10:44 +00:00
MoltBot Service
f8ff971627 refactor(heartbeat): smart calendar with daily summary and dedup reminders
Calendar no longer bypasses quiet hours. First run after quiet hours
sends full daily summary, subsequent runs only remind for next event
within 45 min with deduplication. Calendar cooldown set to 30 min.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 11:09:59 +00:00
MoltBot Service
9c1f9f94e7 refactor(heartbeat): config-driven checks, channel delivery, remove hardcoded values
Heartbeat system overhaul:
- Fix email/calendar checks to parse JSON output correctly
- Add per-check cooldowns and quiet hours config
- Send findings to Discord channel instead of just logging
- Auto-reindex KB when stale files detected
- Claude CLI called only if HEARTBEAT.md has extra instructions
- All settings configurable via config.json heartbeat section

Move hardcoded values to config.json:
- allowed_tools list (claude_session.py)
- Ollama URL/model (memory_search.py now reads ollama.url from config)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 23:46:04 +00:00