Originally created by: adityaharishch
What ships in this release
This is a production release merging the pre-main accumulation branch into main. It delivers several major feature milestones plus code-quality cleanup.
Hour-level worklog pipeline (Python + Rust)
- New
worklog_pipeline Rust module drives per-hour worklog drafting via /worklog_hour
- Python agno pipeline: distil → activity report → reranker hint → tiered task matcher → worklog draft per matched task (or propose new ticket)
- Time budget fix: multiple matched tasks now divide the hour evenly instead of each claiming the full 3600s
- Ledger-backed settle/aging prevents drafts on hours still receiving ETL data
- New FastAPI routes:
/worklog_hour, /activity_report, /distill_hour, /rerank, /v1/chat/completions, /v1/models, /prefetch_model, /prefetch_status
Dashboard → Tauri fold (no more Node server)
- Next.js dashboard now ships as a static export embedded in the Tauri binary
- All
/api/* routes replaced by Tauri invoke commands reading meridian-core directly
meridian-core crate introduced as the shared DB layer (daemon re-exports; tray depends directly)
- Frontend calls Rust via
ui/lib/bridge.ts — HTTP fetch and SSE removed
In-process capture (Bucket 2)
- Screen/accessibility capture runs in the tray process (no screenpipe launchd agent)
meridian-cli.sh updated: screenpipe removed from managed LABELS
OAuth PKCE flow (meridian-oauth)
- In-process browser-based OAuth 2.0 for Jira, Linear, Trello
- CodeQL HIGH severity fix:
build_authorize_url now takes individual fields (authorize_url, scopes, extra_params) instead of the full ProviderSpec, so client_secret provably cannot reach eprintln!
Setup wizard + onboarding
- New
ui/app/setup/ wizard with MLX runtime detection, model prefetch, and integration connect
~/.meridian/onboarded flag controls first-run auto-open
Staging release pipeline
- GitHub Actions:
release-staging.yml, build-mlx-runtime.yml, new CI workflow
- Semantic-release configured for
pre-main staging channel
Other fixes in this release
worklog_pipeline.rs: replace unwrap() on and_hms_opt, add .context() to all ledger DB calls, reject invalid --day with explicit error
observability.py: remove duplicate instrument_agno definition
mlx_classifier.py: remove shebang, remove unused _MAX_TOKENS/_TEMPERATURE constants
pm_worklog_update/config.py: align MLX_SERVER_MODEL default with actual deployed model
routes/chat.py: return actual MODEL_ID in OpenAI /v1/chat/completions response
server.py: self_url built from actual bind host (not hardcoded 127.0.0.1)
worklog.py: remove redundant WORKLOG_SYSTEM = WORKLOG_SYSTEM self-assignment
match.py: remove unused MATCH_SYSTEM import
prefetch.py: remove unused HTTPException import; document bare except OSError
session_distiller.py: document bare except in MPS cache flush
activity_report.py: replace en dash with ASCII hyphen (Ruff RUF001)
match_tasks.py: remove stray bare 2 in prompt text
Deferred (not in this release)
- SSRF validation for pipeline internal HTTP calls
- Per-route span instrumentation across new FastAPI routes
- Request size caps on new endpoints
- Reranker eviction guard
- Apple Developer signing/notarization (enrollment submitted, pending approval)
Test plan
- [ ]
cargo test passes (Rust integration tests)
- [ ]
cargo clippy -- -D warnings clean
- [ ] CodeQL HIGH severity alert (
meridian-oauth/src/flow.rs:72) cleared by CI re-run
- [ ] Hour-level worklog drafts appear in
pm_worklogs after a pipeline run
- [ ] Multiple-task match: sum of
time_spent_seconds across tasks = 3600
- [ ] Setup wizard opens on fresh install (
~/.meridian/onboarded absent)
- [ ] Dashboard loads in Tauri webview (no Node server required)
- [ ] OAuth connect flow opens browser and stores tokens
🤖 Generated with Claude Code
Summary by CodeRabbit
- New Features
- Added an hour-level worklog pipeline that distills activity, matches tasks, reranks candidates, and drafts proposed worklogs.
- Added new MLX-backed HTTP endpoints (activity reports, hourly distillation, summarisation, OpenAI-compatible chat/model routes, reranking, and worklog-hour processing) plus model prefetch and service info/health endpoints.
- Bug Fixes
- Hygiene items marked “overdue” are now treated as must-fix and can be rescheduled.
- Documentation
- Updated setup/development, tray, and service/agent docs to reflect the new workflow and endpoint set.
Originally posted by: coderabbitai[bot]
📝 Walkthrough
## Walkthrough The PR replaces classifier-driven session handling with an hourly worklog pipeline, adds MLX-backed FastAPI routes and shared runtime state, moves Jira OAuth into a shared crate, and updates dev tooling, docs, and eval defaults for the tray-based workflow and 2B model. ## Changes **Worklog migration** |Layer / File(s)|Summary| |---|---| |**Core task-state contracts**`meridian-core/src/settings.rs`, `meridian-core/src/util/hygiene.rs`, `src/coding_agent_session_ingest/summariser/*`, `src/health/daemon.rs`, `src/pm_worklog/ledger.rs`, `src/migrations/049_pm_proposed_tasks.sql`|`RuntimeSettings` drops `llm_model_preference`, `overdue` becomes a must-fix hygiene reason with a fix, summariser rows now settle into `summarised`, `pending_classifier` is removed from hour-blocking checks, and `pm_proposed_tasks` is added.| |**Shared OAuth crate**
`Cargo.toml`, `meridian-oauth/*`, `src/intelligence/oauth/*`|The workspace adds a shared OAuth crate, moves Jira OAuth logic into it, and re-exports the shared modules from the daemon-side OAuth surface.| |**Agent runtime and model services**
`services/agents/{_state.py,config.py,mlx_classifier.py,reranker.py,observability.py,agno_viewer.py,pm_worklog_update/config.py}`|Shared runtime state, model lifecycle helpers, reranking and distillation services, observability helpers, worklog config, and the Agno viewer are added or updated.| |**Workflow routes and wiring**
`services/agents/{routes/__init__.py,routes/health.py,routes/prefetch.py,routes/chat.py,routes/summarise.py,routes/inference.py,routes/activity.py,routes/distill.py,routes/rerank.py,routes/worklog.py,server.py}`|Health, prefetch, chat, summarise, activity, distill, rerank, and worklog handlers call the new runtime helpers, and `server.py` registers the routers through shared app state and lifespan setup.| |**Worklog pipeline contracts**
`services/agents/worklog_pipeline/{__init__.py,agent_io.py,db.py,match.py,models.py,prompts/*}`|The worklog pipeline package adds agent factories, SQLite helpers, structured matching schemas, and the match, propose-ticket, and worklog prompt text.| |**Worklog orchestration**
`services/agents/worklog_pipeline/{pipeline.py,workflow.py,worklog.py}`, `src/lib.rs`, `src/main.rs`, `src/worklog_pipeline.rs`|The hour pipeline distills sessions, builds reports, reranks candidates, matches tasks in tiers, drafts worklogs or proposals, and runs those stages through an Agno workflow wrapper; the daemon exports and drives the new hourly runner.| |**Dev scripts and tray guards**
`.githooks/post-push`, `.github/workflows/ci.yml`, `dev-start.sh`, `install-dev.sh`, `install.sh`, `scripts/{meridian-cli.sh,setup-hooks.sh,sync-oo-dashboards.py}`, `services/scripts/*`, `tests/tray_assets.rs`, `tray/src-tauri/src/commands/health.rs`|Git hooks, install scripts, dashboard sync, CLI helpers, tray health checks, and tray dev-setup tests are updated for the tray-based workflow.| |**Docs and eval references**
`CLAUDE.md`, `CONTRIBUTING.md`, `README.md`, `SETUP.md`, `services/{README.md,agents/README.md,tests/evals/README.md,tests/evals/configs/*}`, `tray/README.md`|Repository docs, setup notes, service descriptions, tray docs, and eval configs are rewritten for the tray-based runtime and the 2B model defaults.| ## Estimated code review effort 🎯 5 (Critical) | ⏱️ ~120 minutes ## Possibly related PRs - [[Meridiona/meridian#334](https://github.com/Meridiona/meridian/issues/334)](https://github.com/Meridiona/meridian/pull/334): Updates the same tray/dev/install scripts, guard tests, and runtime docs that this PR revises. - [[Meridiona/meridian#338](https://github.com/Meridiona/meridian/issues/338)](https://github.com/Meridiona/meridian/pull/338): Overlaps on the shared OAuth crate split and daemon-side Jira OAuth re-export path. ## Poem > I hopped through logs and launched the tray, > Where worklog hours now find their way. > Two-bits hum softly in MLX light, > Dashboards sync after pushes at night. > Hop, hop—our new paths feel just right.
🚥 Pre-merge checks | ✅ 4 | ❌ 1
### ❌ Failed checks (1 warning) | Check name | Status | Explanation | Resolution | | :----------------: | :--------- | :------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------- | | Docstring Coverage | ⚠️ Warning | Docstring coverage is 64.58% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |✅ Passed checks (4 passed)
| Check name | Status | Explanation | | :------------------------: | :------- | :------------------------------------------------------------------------------------------------------------------------------------- | | Title check | ✅ Passed | The title is concise and accurately summarizes the main release themes in the changeset. | | Description check | ✅ Passed | The description covers the release purpose, major changes, and testing plan, though some template sections are abbreviated or missing. | | Linked Issues check | ✅ Passed | Check skipped because no linked issues were found for this pull request. | | Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |✨ Finishing Touches
🧪 Generate unit tests (beta)
- [ ] Create PR with unit tests - [ ] Commit unit tests in branch `pre-main`Comment
@coderabbitai helpto get the list of available commands.Originally posted by: Akarsh-Hegde
Review fixes applied (commit c50107a6)
CodeQL HIGH severity (blocking CI) — fixed
meridian-oauth/src/flow.rs:72:build_authorize_url()now takesauthorize_url: &str,scopes: &str,extra_params: &[...]instead of the fullProviderSpec. This breaks the data-flow path CodeQL was tracking fromspec.client_secretthrough the function and intoeprintln!. The authorize URL has never contained the secret — this makes it provably taint-free.Real data bug — fixed
services/agents/worklog_pipeline/pipeline.py: When an hour matches multiple tasks, each task was being logged with the full 3600s — a 3-task match would log 3h for a 1h window. Now divides evenly:time_per_task = 3600 // max(1, len(bindings)).Rust quality — fixed
src/worklog_pipeline.rs: replacedand_hms_opt(...).unwrap()with.context(), added.with_context()to all four ledger DB calls (ensure_hour,hour_is_done,upstream_settled,mark_hour_done), reject invalid--daystrings with an explicit error message instead of silently using today, truncate HTTP error body to 200 chars inbail!.Python code quality — fixed
observability.py: removed the stale firstinstrument_agnodefinition (usedopeninference); kept the current one (usesopentelemetry)mlx_classifier.py: removed#!/usr/bin/env python3shebang; removed unused_MAX_TOKENS = 1024and_TEMPERATURE = 0.0constantsworklog_pipeline/worklog.py: removed self-assignmentWORKLOG_SYSTEM = WORKLOG_SYSTEMworklog_pipeline/match.py: removed unusedfrom agents.worklog_pipeline.prompts.match_tasks import SYSTEM as MATCH_SYSTEMroutes/prefetch.py: removed unusedHTTPExceptionimport; added# file may be deleted mid-walk; skip silentlyto bareexcept OSErrorsession_distiller.py: added# noqa: BLE001 — MPS cache flush is best-effort; non-fatal if torch absentto bare exceptpm_worklog_update/config.py:MLX_SERVER_MODELdefault changed from"qwen3.5-2b-instruct"→"mlx-community/Qwen3.5-2B-OptiQ-4bit"(matches the actual deployed model id)prompts/activity_report.py: replaced en dash–with ASCII hyphen-(Ruff RUF001)worklog_pipeline/prompts/match_tasks.py: removed stray bare2between paragraphsroutes/chat.py:/v1/chat/completionsresponse now returnsMODEL_ID(the actual loaded model) instead of echoingreq.model or "qwen3.5-2b-instruct"server.py:self_urlnow built fromargs.host(not hardcoded"127.0.0.1") — fixes orchestration when--hostis non-loopbackScripts — fixed
scripts/meridian-cli.sh: removed${LABEL_SCREENPIPE}fromLABELS— screenpipe is no longer a managed launchd agent (capture is in-process since Bucket-2)Docs — fixed
services/agents/README.md: added blank lines before\``bash` code blocks (MD031)Deferred (noted for follow-up):
/worklog_hour//activity_reportactions/missing-workflow-permissionsCI annotation (low severity, no attack surface in the affected jobs)Originally posted by: Akarsh-Hegde
Second round of review fixes (commits 1c9a5410, 4ccd42f1)
CodeQL HIGH severity — second attempt
meridian-oauth/src/flow.rs: The first fix (decomposingProviderSpecinto individual fields) wasn't enough — CodeQL's Rust analysis is field-insensitive at the struct level, sospec.extra_authorize_paramswas still tainted becausespeccarriesclient_secret. Removed theeprintln!("If it doesn't open, paste this URL…")line entirely — the authorize URL is still opened in the browser viaopen_browser(&authorize); the print fallback is gone. This definitively breaks the taint chain since no value derived fromspecreaches any log/print sink.Session distiller
session_distiller.py: Wrapped thesqlite3.connect(db)call in atry/finallyblock socon.close()is guaranteed even whenexecute()orfetchall()raises (was missing, could leak connections on query errors)session_distiller.py: Added_HOUR_RE = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}$')validation before_load_rows(..., "started_at LIKE ?", (hour + "%", ...))— prevents SQLite wildcard injection ifhourcontains%or_Documentation
CLAUDE.md: Replaced stale "it serves/summarise+/classify_sessions" with the current route list (classification has run inside the Rust daemon since the pipeline port —/classify_sessionsis no longer exposed)Code quality
agno_viewer.py: Replacedprint()calls in__main__withlog.info()per the module-logger convention in services/agents/scripts/sync-oo-dashboards.py: Guarded.get('message', result)withisinstance(result, dict)— OO can return a JSON array or string on error, which would have raisedAttributeErrorDeferred (explicit disposition)
These are tracked here so every review thread is accounted for:
reranker.py:118— add spans toscore_candidates()reranker.py:109— guard against eviction failure before loading rerankerroutes/chat.py:119— instrument/v1/chat/completionswith OTel spansroutes/health.py:41— instrument/healthand/inforoutes/prefetch.py:88— prevent stale worker from overwriting model statusroutes/prefetch.py:138— stabilitymlx_classifier.py:79— wrap model load/eviction in OTel spansconfig.py:63— validate and bound env overrides at import timesync-oo-dashboards.py:57— SSRF (URL not restricted to loopback)--base-urlis only ever set by the developer running itOriginally posted by: Akarsh-Hegde
Round 3 fixes — remaining CodeRabbit findings
Commit
49a56c6caddresses the open items from rounds 1–2:services/agents/worklog_pipeline/worklog.pyWORKLOG_SYSTEMimportmeridian-oauth/src/flow.rseprintln!should use structured tracingeprintln!calls withtracing::info!services/agents/session_distiller.py:63datetime.strptime(hour, '%Y-%m-%dT%H')after the regex checksrc/main.rs:591worklog_notify.notify_one()afteretl_notify.notify_one()scripts/sync-oo-dashboards.py:106services/agents/routes/inference.pyroutes/chat.py+routes/summarise.pyare the canonical implementationsDeferred items (non-blocking; tracked for follow-up):
mlx_classifier.py:79,reranker.py:118,routes/chat.py:119,routes/health.py:41,routes/summarise.py:113— per-route OpenObserve span instrumentation; deferred to a dedicated observability passreranker.py:109— guard against failed eviction before loading reranker; deferred as a stability follow-uproutes/prefetch.py:88— stale worker / status-overwrite race guard; deferred to a stability follow-uproutes/prefetch.py:138/worklog_pipeline/workflow.py:172— worker lifecycle stability; deferredroutes/activity.py:31,routes/rerank.py:33— request-size / token-budget caps; deferred to a security hardening passroutes/activity.py:115,routes/summarise.py:105— strip incomplete<think>blocks; deferred as a polish itemroutes/distill.py:29,routes/worklog.py:31—db_pathrestriction to~/.meridian/; deferred to a security passroutes/worklog.py:95— omit free-form worklog content from span attributes; deferred to observability clean-upworklog_pipeline/db.py:179— atomic commit/rollback across writes; deferred as a data-integrity improvementworklog_pipeline/match.py:25—_MIN_CONFIDENCEalignment with prompt contract; tracked for next classifier iterationworklog_pipeline/models.py:87— enum member name normalisation; tracked for schema refactorworklog_pipeline/workflow.py:75—hour/cycle_indexvalidation at workflow layer; deferred as belt-and-suspenders clean-upconfig.py:63— env-override validation; deferred; module-import abort is acceptable failure mode for nowsync-oo-dashboards.py:57— SSRF; accepted risk for this internal ops script run only by developersworklog_pipeline/pipeline.py:43— loopback-URL SSRF restriction; deferred as hardeningservices/README.md:7— classifier ownership wording; will update when Python classifier is fully retiredmeridian-oauth/src/jira.rs:38— cross-process refresh-token race; tracked in project decision log alongside other PR [#338] deferred findings (fd-lockfix in backlog)False positive:
server.py:50—_MODEL_ALLOW_PATTERNSis lazy-imported byroutes/prefetch.py(lines 54 and 66) to avoid a circular import. No change needed.Related
Tickets:
#338Ticket changed by: Akarsh-Hegde