@meridiona/meridian-darwin-arm64 1.53.0 → 1.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 1.53.0
1
+ 1.54.0
package/bin/meridian CHANGED
Binary file
package/bin/meridian-tray CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meridiona/meridian-darwin-arm64",
3
- "version": "1.53.0",
3
+ "version": "1.54.0",
4
4
  "description": "Prebuilt Meridian app for macOS arm64 (daemon binary + dashboard + Python services). Installed via @meridiona/meridian.",
5
5
  "homepage": "https://github.com/Meridiona/meridian",
6
6
  "repository": {
@@ -540,6 +540,21 @@ if [[ "${SKIP_PERMISSIONS}" -eq 0 ]]; then
540
540
  echo " ${HOME}/.meridian/bin/meridian-a11y-helper"
541
541
  echo " Without the a11y helper, Electron apps (Claude, Codex, Slack, …) stay invisible to capture."
542
542
  read -r -p " Press Enter once all are granted… " _ || true
543
+
544
+ # Notifications: the tray surfaces desktop toasts (plan nudges, worklog
545
+ # drafts, faults). macOS hides ALL notifications while the screen is being
546
+ # recorded/shared unless this is on — and screenpipe records continuously, so
547
+ # without it every Meridian toast is silently suppressed. No API/prompt exists
548
+ # for this toggle, so we can only walk the user to it.
549
+ echo "→ Meridian's tray shows desktop notifications. Because screenpipe records"
550
+ echo " the screen, macOS hides notifications during screen sharing unless allowed."
551
+ read -r -p " Press Enter to open Notifications settings… " _ || true
552
+ open "x-apple.systempreferences:com.apple.Notifications-Settings.extension" 2>/dev/null || true
553
+ echo " → Scroll to the bottom and turn ON"
554
+ echo " 'Allow notifications when mirroring or sharing the display'."
555
+ echo " → When 'Meridian Tray' appears, ensure its notifications are allowed"
556
+ echo " (style Banners or Alerts, not None)."
557
+ read -r -p " Press Enter when done… " _ || true
543
558
  fi
544
559
 
545
560
  # Enable a11y mode in installed VS Code-family editors (idempotent). Without
@@ -106,8 +106,11 @@ def _format_candidates(tasks: list[dict]) -> str:
106
106
  desc = desc[:240] + "…"
107
107
  meta_parts = [p for p in [issue_type, f"Epic: {epic_title}" if epic_title else "", sprint_name, f"tags: {tags}" if tags else ""] if p]
108
108
  meta = " [" + " · ".join(meta_parts) + "]" if meta_parts else ""
109
+ # The dev declared this ticket as today's focus on the plan page. It's a
110
+ # tie-breaking prior, not a forced answer — only matches if the evidence fits.
111
+ focus = " ★ TODAY'S FOCUS" if task.get("is_today_focus") else ""
109
112
  rows.append(
110
- f"{i}. {task['task_key']}{meta}\n"
113
+ f"{i}. {task['task_key']}{focus}{meta}\n"
111
114
  f" title: {title}\n"
112
115
  f" description: {desc or '(empty)'}"
113
116
  )
@@ -152,12 +155,21 @@ def build_user_message(
152
155
  f"{_format_recent_sessions(sessions)}\n"
153
156
  "\n"
154
157
  ) if has_any_task_key else ""
158
+ # When the dev declared a focus for the day, name it in the header so the model
159
+ # treats ★ rows as a prior — preferred when the evidence plausibly fits, but
160
+ # never forced. Recall is preserved: every candidate is still listed.
161
+ has_focus = any(c.get("is_today_focus") for c in candidates)
162
+ candidate_header = (
163
+ "CANDIDATE TICKETS (★ = the dev declared this as a task they're working on "
164
+ "today; prefer a ★ ticket when the session plausibly matches it, but only "
165
+ "if the evidence fits — never force a match):\n"
166
+ ) if has_focus else "CANDIDATE TICKETS:\n"
155
167
  return (
156
168
  f"{recent_block}"
157
169
  "SESSION:\n"
158
170
  f"{_format_session(session)}\n"
159
171
  "\n"
160
- "CANDIDATE TICKETS:\n"
172
+ f"{candidate_header}"
161
173
  f"{_format_candidates(candidates)}"
162
174
  )
163
175
 
@@ -24,6 +24,7 @@ Method tag in results: "mlx_direct".
24
24
  """
25
25
  from __future__ import annotations
26
26
 
27
+ import datetime as _dt
27
28
  import json
28
29
  import logging
29
30
  import os
@@ -483,7 +484,53 @@ def _fetch_recent_sessions(
483
484
  return result
484
485
 
485
486
 
486
- def _fetch_pm_tasks(con: _sqlite3.Connection) -> list[dict[str, Any]]:
487
+ def _local_day(started_at: str) -> str:
488
+ """The local calendar day (YYYY-MM-DD) of a session's UTC `started_at`.
489
+
490
+ `daily_plan.plan_date` is the dev's *local* day (the dashboard stamps it from
491
+ the browser's local date), but `app_sessions.started_at` is stored UTC. We
492
+ convert UTC → local here so a session is matched to the plan the dev actually
493
+ declared for that day. Returns "" on an unparseable timestamp (→ no boost).
494
+ """
495
+ if not started_at:
496
+ return ""
497
+ try:
498
+ # `astimezone()` with no arg converts an aware datetime to the host's
499
+ # local zone — the same zone the dashboard used to compute plan_date.
500
+ return _dt.datetime.fromisoformat(started_at).astimezone().date().isoformat()
501
+ except ValueError:
502
+ return ""
503
+
504
+
505
+ def _fetch_plan_focus(con: _sqlite3.Connection, plan_date: str) -> list[str]:
506
+ """Ordered task_keys the dev CONFIRMED as their focus for `plan_date`.
507
+
508
+ Empty (→ no boost, classification proceeds exactly as before) when the day is
509
+ unconfirmed, explicitly skipped, has no plan rows, or the plan tables don't
510
+ exist yet (pre-migration-041 DB). This is a ranking signal only — never a
511
+ filter — so an empty result can only ever cost the boost, never recall.
512
+ """
513
+ if not plan_date:
514
+ return []
515
+ try:
516
+ meta = con.execute(
517
+ "SELECT confirmed_at, skipped FROM daily_plan_meta WHERE plan_date = ?",
518
+ (plan_date,),
519
+ ).fetchone()
520
+ if meta is None or meta["skipped"] or not meta["confirmed_at"]:
521
+ return []
522
+ rows = con.execute(
523
+ "SELECT task_key FROM daily_plan WHERE plan_date = ? ORDER BY position",
524
+ (plan_date,),
525
+ ).fetchall()
526
+ return [r["task_key"] for r in rows]
527
+ except _sqlite3.OperationalError:
528
+ return []
529
+
530
+
531
+ def _fetch_pm_tasks(
532
+ con: _sqlite3.Connection, focus_keys: list[str] | None = None
533
+ ) -> list[dict[str, Any]]:
487
534
  # Candidate set for classification. Tickets the user explicitly EXCLUDED during
488
535
  # onboarding board-cleanup (pm_task_curation.decision = 'excluded') are dropped
489
536
  # so a cleaned-up dead ticket can never be a classification target. Everything
@@ -512,7 +559,20 @@ def _fetch_pm_tasks(con: _sqlite3.Connection) -> list[dict[str, Any]]:
512
559
  # Pre-migration-038 DB (no pm_task_curation): degrade to the unfiltered
513
560
  # candidate set rather than crashing the whole /classify_sessions call.
514
561
  rows = con.execute(base_cols).fetchall()
515
- return [dict(r) for r in rows]
562
+ tasks = [dict(r) for r in rows]
563
+
564
+ # Today's-focus boost: tag the tickets the dev declared for the day and float
565
+ # them to the top of the candidate list, in their declared order. This is a
566
+ # BOOST, never a filter — every other candidate still follows, so recall is
567
+ # untouched. A focus key that isn't in `tasks` (e.g. excluded by curation)
568
+ # simply has no effect; we never resurrect a filtered-out ticket.
569
+ focus = focus_keys or []
570
+ if focus:
571
+ order = {key: i for i, key in enumerate(focus)}
572
+ for t in tasks:
573
+ t["is_today_focus"] = t["task_key"] in order
574
+ tasks.sort(key=lambda t: (0, order[t["task_key"]]) if t.get("is_today_focus") else (1, 0))
575
+ return tasks
516
576
 
517
577
 
518
578
  # ---------------------------------------------------------------------------
@@ -555,10 +615,13 @@ def _classify_one(
555
615
  session_id, f"session {session_id} not found in DB", 0.0, "mlx_error"
556
616
  )
557
617
 
558
- pm_tasks = _fetch_pm_tasks(con)
559
- recent = _fetch_recent_sessions(con, session_id)
618
+ plan_date = _local_day(session_raw.get("started_at") or "")
619
+ focus_keys = _fetch_plan_focus(con, plan_date)
620
+ pm_tasks = _fetch_pm_tasks(con, focus_keys)
621
+ recent = _fetch_recent_sessions(con, session_id)
560
622
 
561
623
  db_span.set_attribute("pm_tasks_count", len(pm_tasks))
624
+ db_span.set_attribute("today_focus_count", len(focus_keys))
562
625
  db_span.set_attribute("recent_sessions_count", len(recent))
563
626
 
564
627
  session_text = session_raw.get("session_text") or ""
@@ -785,7 +848,8 @@ def _classify_one_logged(
785
848
  """Classify one session and append a full record to the run log."""
786
849
  # Gather inputs before classification so we can log them even on error.
787
850
  session_raw = _fetch_session(con, session_id)
788
- pm_tasks = _fetch_pm_tasks(con) if session_raw else []
851
+ focus_keys = _fetch_plan_focus(con, _local_day(session_raw.get("started_at") or "")) if session_raw else []
852
+ pm_tasks = _fetch_pm_tasks(con, focus_keys) if session_raw else []
789
853
  recent = _fetch_recent_sessions(con, session_id) if session_raw else []
790
854
 
791
855
  if session_raw:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meridian-agents"
7
- version = "1.53.0"
7
+ version = "1.54.0"
8
8
  description = "Meridian agents — MLX classifier server and Jira worklog synthesis for meridian.db"
9
9
  requires-python = ">=3.11"
10
10
  authors = [{ name = "Meridiona" }]
package/ui.tar.gz CHANGED
Binary file