@meridiona/meridian-darwin-arm64 1.67.0 → 1.68.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.67.0
1
+ 1.68.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.67.0",
3
+ "version": "1.68.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": {
@@ -267,13 +267,15 @@ st, body = call("GET", f"/api/{org}/dashboards")
267
267
  if st != 200:
268
268
  print(f" ⚠ could not list dashboards ({st}) — skipping dashboard import")
269
269
  sys.exit(0)
270
- existing = set()
270
+ id_map = {} # title -> dashboard_id
271
271
  for d in json.loads(body).get("dashboards", []):
272
- for v in ("v1", "v2", "v3", "v4", "v5", "v6"):
272
+ did = d.get("dashboard_id", "")
273
+ for v in ("v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8"):
273
274
  if d.get(v) and d[v].get("title"):
274
- existing.add(d[v]["title"])
275
+ id_map[d[v]["title"]] = did
276
+ break
275
277
 
276
- created = skipped = failed = 0
278
+ created = updated = failed = 0
277
279
  for path in sorted(glob.glob(os.path.join(dash_dir, "*.json"))):
278
280
  try:
279
281
  dash = json.load(open(path))
@@ -282,18 +284,32 @@ for path in sorted(glob.glob(os.path.join(dash_dir, "*.json"))):
282
284
  failed += 1
283
285
  continue
284
286
  title = dash.get("title", "")
285
- if title in existing:
286
- skipped += 1
287
- continue
288
- st, resp = call("POST", f"/api/{org}/dashboards?folder=default", dash)
289
- if st in (200, 201):
290
- created += 1
291
- print(f" ✓ imported dashboard: {title}")
287
+ if title in id_map:
288
+ # OO PUT requires an internal hash that can't be round-tripped reliably.
289
+ # Delete + re-create is the safe upsert path.
290
+ did = id_map[title]
291
+ st_d, _ = call("DELETE", f"/api/{org}/dashboards/{did}")
292
+ if st_d not in (200, 204):
293
+ failed += 1
294
+ print(f" ⚠ delete failed {title} ({st_d}) — skipping")
295
+ continue
296
+ st, resp = call("POST", f"/api/{org}/dashboards?folder=default", dash)
297
+ if st in (200, 201):
298
+ updated += 1
299
+ print(f" ✓ updated dashboard: {title}")
300
+ else:
301
+ failed += 1
302
+ print(f" ⚠ failed to re-create {os.path.basename(path)} ({st}): {resp[:200].decode(errors='replace')}")
292
303
  else:
293
- failed += 1
294
- print(f" ⚠ failed to import {os.path.basename(path)} ({st}): {resp[:200].decode(errors='replace')}")
304
+ st, resp = call("POST", f"/api/{org}/dashboards?folder=default", dash)
305
+ if st in (200, 201):
306
+ created += 1
307
+ print(f" ✓ imported dashboard: {title}")
308
+ else:
309
+ failed += 1
310
+ print(f" ⚠ failed to import {os.path.basename(path)} ({st}): {resp[:200].decode(errors='replace')}")
295
311
 
296
- print(f"→ dashboards: {created} imported, {skipped} already present, {failed} failed")
312
+ print(f"→ dashboards: {created} imported, {updated} updated, {failed} failed")
297
313
  PYEOF
298
314
  return 0
299
315
  }
@@ -18,7 +18,7 @@ from typing import Any, Iterator
18
18
  from agents import model_registry, observability
19
19
 
20
20
  log = logging.getLogger("agents.mlx_classifier")
21
- tracer = observability.setup("meridian-mlx-classifier")
21
+ tracer = observability.setup("meridian-mlx-server")
22
22
 
23
23
  # Generative/classifier checkpoint — resolved from the model registry (the single
24
24
  # source of truth for all three pipeline models), env-overridable via MERIDIAN_LLM_ID.
@@ -3,39 +3,30 @@
3
3
  A single `setup(agent_name)` call wires up:
4
4
 
5
5
  * an OTel `TracerProvider` with `service.name=agent_name`
6
- * a `BatchSpanProcessor` writing OTLP/HTTP-protobuf spans to the durable
7
- telemetry spool (the Rust daemon's shipper drains it to OpenObserve)
8
- * a `LoggerProvider` + spool log handler so every `logging.LogRecord` is also
9
- spooled (correlated to the active span), mirroring the Rust daemon's
10
- `OpenTelemetryTracingBridge`
6
+ * direct OTLP/HTTP export to OpenObserve when `otlp_enabled=true` and
7
+ credentials are set in `~/.meridian/settings.json` no Rust daemon needed
8
+ * spool fallback (`~/.meridian/telemetry/pending/`) when toggle is on but
9
+ credentials are absent; the Rust daemon drains that directory to OO
10
+ * a `LoggerProvider` + matching log handler so every `logging.LogRecord`
11
+ is correlated to the active span via trace_id/span_id
11
12
  * W3C `TraceContextTextMapPropagator` as the global propagator so each
12
13
  agent can pick up the Rust daemon's `traceparent` and continue the trace
13
14
  * `LoggingInstrumentor` so every `logging.LogRecord` carries
14
15
  `otelTraceID` / `otelSpanID` attributes for OpenObserve correlation
15
16
  * a JSON formatter (`python-json-logger`) writing daily-rotated JSONL files
16
- under `~/.meridian/logs/{agent_name}.jsonl` plus stderr — both ingestable
17
- by OpenObserve's log pipeline without further parsing.
17
+ under `~/.meridian/logs/{agent_name}.jsonl` plus stderr
18
18
 
19
- Export is gated by the SAME `~/.meridian/settings.json` the Rust daemon reads
20
- the `otlp_enabled` toggle so the dashboard Settings page is the single source
21
- of truth for both processes. Delivery (endpoint + Basic-auth credentials) is
22
- owned entirely by the Rust shipper; Python only ever writes to the spool. The
23
- legacy `MERIDIAN_OO_AUTH` env credential is deprecated and ignored.
19
+ Export gate: `~/.meridian/settings.json` `otlp_enabled` toggle + OO credentials
20
+ (`oo_email` / `oo_password`). When both are present the Python service ships
21
+ directly to OO it does NOT depend on the Rust daemon for delivery. The spool
22
+ path is a fallback for when credentials are absent (daemon delivers later).
24
23
 
25
24
  `extract_parent_context(traceparent)` is the helper agents use to continue
26
25
  a span emitted by another process — typically the Rust ETL or another
27
26
  agent stage.
28
27
 
29
28
  Idempotent: calling `setup` twice is a no-op for the second call (returns
30
- the existing tracer). This matters because both the daemon and the
31
- single-shot CLI paths funnel through the same module.
32
-
33
- Spool durability: when `otlp_enabled` is true (regardless of whether
34
- credentials are present), spans and logs are written atomically to
35
- `~/.meridian/telemetry/pending/<signal>-<unix_micros>-<seq>.otlp` via
36
- `SpoolSpanExporter` and `SpoolLogExporter`. The Rust daemon's shipper
37
- task drains that shared directory to OpenObserve — Python does NOT need
38
- its own shipper, and credential resolution lives there, not here.
29
+ the existing tracer).
39
30
  """
40
31
  from __future__ import annotations
41
32
 
@@ -55,10 +46,10 @@ from opentelemetry.context import Context
55
46
  from opentelemetry.instrumentation.logging import LoggingInstrumentor
56
47
  from opentelemetry.propagate import set_global_textmap
57
48
  from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
58
- from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, LogExportResult
49
+ from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, SimpleLogRecordProcessor, LogExportResult
59
50
  from opentelemetry.sdk.resources import Resource
60
51
  from opentelemetry.sdk.trace import TracerProvider
61
- from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExportResult
52
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor, SpanExportResult
62
53
  from opentelemetry.trace.propagation.tracecontext import (
63
54
  TraceContextTextMapPropagator,
64
55
  )
@@ -200,11 +191,8 @@ class SpoolLogExporter:
200
191
 
201
192
  # ──────────────────────── Config ───────────────────────────────────────────────
202
193
  DEFAULT_LOG_DIR = Path.home() / ".meridian" / "logs"
203
- # Single source of truth for the export TOGGLE the SAME file the Rust daemon
204
- # reads (see `src/observability.rs::resolve_otlp_target`). Delivery credentials
205
- # live there too: the dashboard Settings page writes here and the Rust shipper
206
- # picks them up. This process only reads `otlp_enabled` to decide whether to
207
- # spool — it never delivers, so it needs no endpoint/credentials of its own.
194
+ # Same settings.json the Rust daemon reads. When otlp_enabled=true and
195
+ # oo_email/oo_password are set, Python ships directly to OO (no daemon needed).
208
196
  _SETTINGS_PATH = Path(
209
197
  os.environ.get("MERIDIAN_SETTINGS_PATH")
210
198
  or (Path.home() / ".meridian" / "settings.json")
@@ -230,13 +218,7 @@ def _load_settings() -> dict[str, object]:
230
218
 
231
219
 
232
220
  def _is_otlp_enabled() -> bool:
233
- """Return True when the otlp_enabled toggle is on and tracing is not disabled.
234
-
235
- Deliberately does NOT check credentials — used to gate the spool exporters
236
- so telemetry is captured even when OO credentials are absent (the Rust
237
- shipper delivers when they are provided later, and warns once if a user
238
- leaves the toggle on with no credentials).
239
- """
221
+ """Return True when the otlp_enabled toggle is on and tracing is not disabled."""
240
222
  if os.environ.get("MERIDIAN_TRACING_DISABLED", "").lower() in ("1", "true", "yes"):
241
223
  return False
242
224
  return bool(_load_settings().get("otlp_enabled"))
@@ -280,22 +262,16 @@ def setup(agent_name: str) -> trace.Tracer:
280
262
 
281
263
 
282
264
  def shutdown() -> None:
283
- """Flush and shut down the global TracerProvider.
265
+ """Shut down the global TracerProvider and log provider.
284
266
 
285
- Must be called before a short-lived process exits — BatchSpanProcessor
286
- queues spans asynchronously and drops them on interpreter shutdown unless
287
- explicitly flushed.
267
+ BatchSpanProcessor/BatchLogRecordProcessor queue spans asynchronously;
268
+ calling shutdown() flushes the queue before releasing resources.
288
269
  """
289
270
  provider = trace.get_tracer_provider()
290
- if hasattr(provider, "force_flush"):
291
- provider.force_flush(timeout_millis=5_000)
292
271
  if hasattr(provider, "shutdown"):
293
272
  provider.shutdown()
294
273
 
295
- # Flush queued log records too — BatchLogRecordProcessor drops them on
296
- # interpreter exit otherwise, the same hazard as spans.
297
274
  if _LOGGER_PROVIDER is not None:
298
- _LOGGER_PROVIDER.force_flush(timeout_millis=5_000)
299
275
  _LOGGER_PROVIDER.shutdown()
300
276
 
301
277
 
@@ -312,41 +288,86 @@ def extract_parent_context(traceparent: Optional[str]) -> Optional[Context]:
312
288
 
313
289
 
314
290
  # ──────────────────────── Tracing setup ────────────────────────────────────────
291
+ def _oo_otlp_headers() -> dict[str, str]:
292
+ """Build the Basic-auth header for direct OO OTLP export from settings.json."""
293
+ import base64
294
+ s = _load_settings()
295
+ email = s.get("oo_email") or ""
296
+ passwd = s.get("oo_password") or ""
297
+ if not email or not passwd:
298
+ return {}
299
+ token = base64.b64encode(f"{email}:{passwd}".encode()).decode()
300
+ return {"Authorization": f"Basic {token}"}
301
+
302
+
303
+ def _oo_otlp_base_url() -> str:
304
+ """Return the OO OTLP base URL (without /v1/traces or /v1/logs suffix)."""
305
+ s = _load_settings()
306
+ endpoint = s.get("otlp_endpoint") or "http://localhost:5080/api/default/v1/traces"
307
+ # Strip the /v1/traces suffix to get the org-level base used by both signals.
308
+ for suffix in ("/v1/traces", "/v1/logs"):
309
+ if endpoint.endswith(suffix):
310
+ return endpoint[: -len(suffix)]
311
+ return endpoint.rstrip("/")
312
+
313
+
315
314
  def _configure_tracing(agent_name: str) -> None:
315
+ # The W3C propagator is always installed so traceparent round-trips work.
316
+ set_global_textmap(TraceContextTextMapPropagator())
317
+
318
+ if not _is_otlp_enabled():
319
+ return
320
+
316
321
  resource = Resource.create({"service.name": agent_name})
317
322
  provider = TracerProvider(resource=resource)
318
323
 
319
- # Wire the spool exporter when otlp_enabled is true (even without creds).
320
- # The Rust shipper drains the shared spool dir when a target is available.
321
- if _is_otlp_enabled():
324
+ headers = _oo_otlp_headers()
325
+ if headers:
326
+ # Credentials present — ship directly to OO, no Rust daemon dependency.
327
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
328
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
329
+ exporter = OTLPSpanExporter(
330
+ endpoint=f"{_oo_otlp_base_url()}/v1/traces",
331
+ headers=headers,
332
+ )
333
+ provider.add_span_processor(BatchSpanProcessor(exporter))
334
+ else:
335
+ # Credentials absent — spool for the Rust daemon to deliver later.
336
+ # BatchSpanProcessor avoids blocking inference threads on each span end.
322
337
  provider.add_span_processor(BatchSpanProcessor(SpoolSpanExporter()))
323
338
 
324
- # Set as the global provider. OTel's `set_tracer_provider` warns if
325
- # someone already configured a provider in-process; we accept that and
326
- # overwrite — the agent process is the authority on its own telemetry.
327
339
  trace.set_tracer_provider(provider)
328
- set_global_textmap(TraceContextTextMapPropagator())
329
340
 
330
341
 
331
342
  def _configure_log_export(agent_name: str) -> Optional[logging.Handler]:
332
- """Build a spool-logs handler so every `log.*` record reaches OpenObserve,
333
- correlated to the active span by trace_id/span_id — the Python counterpart
334
- of the Rust daemon's `OpenTelemetryTracingBridge`.
335
-
336
- Returns the handler (caller attaches it to root) or `None` when export is
337
- disabled, in which case logs still go to the JSONL file + stdout/stderr.
343
+ """Build an OTel log handler so every ``log.*`` record reaches OpenObserve,
344
+ correlated to the active span by trace_id/span_id.
338
345
 
339
- When `otlp_enabled` is true the spool log exporter is always wired (even
340
- without credentials) — the Rust shipper handles delivery.
346
+ Returns the handler (caller attaches it to root) or ``None`` when export is
347
+ disabledlogs still go to the JSONL file + stdout/stderr regardless.
341
348
  """
342
349
  global _LOGGER_PROVIDER
343
350
 
344
351
  if not _is_otlp_enabled():
345
352
  return None
346
353
 
354
+ headers = _oo_otlp_headers()
347
355
  resource = Resource.create({"service.name": agent_name})
348
356
  provider = LoggerProvider(resource=resource)
349
- provider.add_log_record_processor(BatchLogRecordProcessor(SpoolLogExporter()))
357
+
358
+ if headers:
359
+ # Credentials present — ship directly to OO, no Rust daemon dependency.
360
+ from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
361
+ log_exporter = OTLPLogExporter(
362
+ endpoint=f"{_oo_otlp_base_url()}/v1/logs",
363
+ headers=headers,
364
+ )
365
+ else:
366
+ # Credentials absent — spool for the Rust daemon to deliver later.
367
+ log_exporter = SpoolLogExporter() # type: ignore[assignment]
368
+
369
+ # BatchLogRecordProcessor for both paths so log export never blocks callers.
370
+ provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter))
350
371
  set_logger_provider(provider)
351
372
  _LOGGER_PROVIDER = provider
352
373
  return LoggingHandler(level=logging.NOTSET, logger_provider=provider)
@@ -471,6 +492,108 @@ def instrument_agno() -> None:
471
492
  )
472
493
 
473
494
 
495
+ # Process-level handle so the agno TracerProvider isn't garbage-collected.
496
+ _AGNO_TRACER_PROVIDER = None
497
+
498
+
499
+ def _build_agno_db_exporter(db, workflow_id: str, agent_id: str):
500
+ """agno ``DatabaseSpanExporter`` subclass that stamps the pipeline identity
501
+ onto each trace's ROOT span.
502
+
503
+ AgentOS hides any trace whose ``workflow_id`` AND ``agent_id`` are both
504
+ null. With OpenObserve off, the meridian wrapper span (``worklog.hour``) is
505
+ non-recording, so agno's worklog Workflow runs as its OWN root trace; its
506
+ root ``*.run`` span is parentless here and gets the ids, surfacing the trace
507
+ in the dashboard. Child spans are written unchanged.
508
+ """
509
+ from collections import defaultdict
510
+
511
+ from agno.tracing.exporter import DatabaseSpanExporter
512
+ from agno.tracing.schemas import Span
513
+ from opentelemetry.sdk.trace.export import SpanExportResult
514
+
515
+ class _AgnoDbExporter(DatabaseSpanExporter):
516
+ def export(self, spans): # type: ignore[override]
517
+ if self._shutdown:
518
+ return SpanExportResult.FAILURE
519
+ if not spans:
520
+ return SpanExportResult.SUCCESS
521
+ converted = []
522
+ for s in spans:
523
+ try:
524
+ cs = Span.from_otel_span(s)
525
+ except Exception: # noqa: BLE001 — skip a span we can't convert
526
+ continue
527
+ if not cs.parent_span_id:
528
+ attrs = dict(cs.attributes or {})
529
+ attrs.setdefault("workflow_id", workflow_id)
530
+ attrs.setdefault("agent_id", agent_id)
531
+ # Group hour-runs into one AgentOS session. agno normally
532
+ # stamps session_id when run(session_id=...) is passed; this
533
+ # is a fallback that derives the day-level session from the
534
+ # run_id ("wl-<day>T<hh>" → "wl-<day>") so the trace always
535
+ # surfaces under a session in the dashboard.
536
+ if not (attrs.get("session_id") or attrs.get("agno.session.id")):
537
+ rid = attrs.get("run_id") or attrs.get("agno.run.id") or ""
538
+ if isinstance(rid, str) and "T" in rid:
539
+ attrs["session_id"] = rid.rsplit("T", 1)[0]
540
+ cs.attributes = attrs
541
+ converted.append(cs)
542
+ if not converted:
543
+ return SpanExportResult.SUCCESS
544
+ by_trace: dict = defaultdict(list)
545
+ for cs in converted:
546
+ by_trace[cs.trace_id].append(cs)
547
+ try:
548
+ self._export_sync(by_trace) # SqliteDb is synchronous
549
+ except Exception as e: # noqa: BLE001
550
+ logging.getLogger(__name__).warning("agno trace export failed: %s", e)
551
+ return SpanExportResult.FAILURE
552
+ return SpanExportResult.SUCCESS
553
+
554
+ return _AgnoDbExporter(db=db)
555
+
556
+
557
+ def setup_agno_tracing(
558
+ db_file: Optional[str] = None,
559
+ workflow_id: str = "worklog-hour",
560
+ agent_id: str = "meridian-worklog-pipeline",
561
+ ):
562
+ """Route agno's native (openinference) spans — and ONLY those — to a SqliteDb
563
+ the AgentOS viewer reads.
564
+
565
+ The TracerProvider here is EXPLICIT, not global: meridian's own manual spans
566
+ live on the no-op global provider and never reach this exporter, so the
567
+ dashboard shows agno's ``tracing=True`` output alone. Idempotent per process.
568
+ Returns the provider (or ``None`` if agno/openinference aren't installed).
569
+ """
570
+ global _AGNO_TRACER_PROVIDER
571
+ if _AGNO_TRACER_PROVIDER is not None:
572
+ return _AGNO_TRACER_PROVIDER
573
+ try:
574
+ from agno.db.sqlite import SqliteDb
575
+ from openinference.instrumentation.agno import AgnoInstrumentor
576
+ from opentelemetry.sdk.trace import TracerProvider as _TP
577
+ from opentelemetry.sdk.trace.export import SimpleSpanProcessor
578
+ except ImportError as e:
579
+ logging.getLogger(__name__).warning(
580
+ "setup_agno_tracing: dependencies missing (%s); agno tracing disabled", e
581
+ )
582
+ return None
583
+
584
+ path = db_file or os.environ.get("AGNO_TRACE_DB") or str(
585
+ Path("~/.meridian/agno_traces.db").expanduser()
586
+ )
587
+ path = str(Path(path).expanduser())
588
+ exporter = _build_agno_db_exporter(SqliteDb(db_file=path), workflow_id, agent_id)
589
+ provider = _TP()
590
+ provider.add_span_processor(SimpleSpanProcessor(exporter))
591
+ AgnoInstrumentor().instrument(tracer_provider=provider)
592
+ _AGNO_TRACER_PROVIDER = provider
593
+ logging.getLogger(__name__).info("setup_agno_tracing: agno spans -> %s", path)
594
+ return provider
595
+
596
+
474
597
  def preview(text: Optional[str], max_chars: int = 200) -> str:
475
598
  """Truncate text to `max_chars` for use as a span attribute value."""
476
599
  if not text:
@@ -478,4 +601,11 @@ def preview(text: Optional[str], max_chars: int = 200) -> str:
478
601
  return text[:max_chars] + ("…" if len(text) > max_chars else "")
479
602
 
480
603
 
481
- __all__ = ["setup", "extract_parent_context", "instrument_agno", "current_traceparent", "preview"]
604
+ __all__ = [
605
+ "setup",
606
+ "extract_parent_context",
607
+ "instrument_agno",
608
+ "setup_agno_tracing",
609
+ "current_traceparent",
610
+ "preview",
611
+ ]
@@ -1,71 +1,52 @@
1
1
  """System prompt for the activity reporter.
2
2
 
3
- Produces a human-readable worklog entry from distilled session text.
4
- Audience: entire team engineers, PMs, stakeholders.
3
+ Produces a structured activity report from distilled screen-capture data.
4
+ Format: TLDR + Core Tasks + Decisions + Resources.
5
5
  """
6
6
  from __future__ import annotations
7
7
 
8
8
  SYSTEM = """\
9
- You are writing a daily worklog update for a software development team.
10
- Your audience is the entire team engineers, designers, product managers, and
11
- stakeholders. Most readers are not familiar with the codebase internals.
9
+ You have been given a compressed snapshot of a software developer's screen activity over the last hour.
10
+ The data comes from OCR and accessibility capture: editor content, browser tabs and URLs, terminal output, UI text, video titles, and other on-screen text. It is noisy and incomplete — piece together the story from the fragments.
12
11
 
13
- You receive a compressed log of what a developer did during a session
14
- (captured from their screen: editor, terminal, browser, and other tools).
12
+ Your job: infer what the developer was actually trying to accomplish and write a structured activity report that a PM, a teammate, or a downstream task-matcher can use to answer "which project areas did this person work on and why?"
15
13
 
16
- TASK
17
- Write a detailed, human-readable account of the developer's session.
18
- Cover everything that happened — building features, fixing bugs, researching
19
- topics, reading documentation, watching talks or tutorials, investigating issues,
20
- running experiments, reviewing code, and any other activity.
21
- Nothing should be left out just because it seems minor.
14
+ ---
22
15
 
23
- Focus on WHAT was accomplished or explored and WHY it matters.
24
- Do not focus on internal variable names, function signatures, or file paths —
25
- those are noise for most readers. Write as if explaining to a smart colleague
26
- who was not in the room.
16
+ OUTPUT FORMAT write all sections that have content, skip sections that are empty:
27
17
 
28
- OUTPUT FORMAT
29
- Write the following sections in order. Skip a section only if there is truly
30
- nothing to report — do not write placeholder text.
18
+ ### TLDR
19
+ One short paragraph. What was the developer focused on this hour and why — what problem were they solving or what goal were they advancing? Name the main work areas explicitly. Avoid generic descriptions like "the developer was doing development work."
31
20
 
32
- ## Session Summary
33
- 2-4 sentences: what was the developer focused on, what got done, and what was
34
- the character of the session (exploration, deep build, debugging, review)?
21
+ ### Core Tasks & Projects
22
+ One section per distinct work thread. Bold the topic name as the header.
35
23
 
36
- ## What Was Worked On
37
- One paragraph per distinct activity thread. A thread is any continuous block
38
- of related work, regardless of which tool was used.
39
- For each thread:
40
- - Start with the goal (what the developer was trying to achieve).
41
- - Describe what happened: progress made, problems hit, things learned.
42
- - End with status: completed / in progress / blocked.
43
- - If the work directly benefits users or the team, say so plainly.
44
- Include ALL activity types: coding, research, reading docs, watching videos,
45
- testing, debugging, reviewing, discussing, planning, experimenting.
24
+ For each thread, write it as the developer's story — not as a list of actions:
25
+ - WHY: what problem or goal drove this work
26
+ - WHAT: what the developer accomplished or decided — the outcome, not the steps
27
+ - HOW: the significant technical context (which system, which file, which tool) that gives the outcome meaning
46
28
 
47
- ## Research & Learning
48
- Anything the developer looked up, read, or watched:
49
- - What question or problem triggered the research?
50
- - What sources were consulted (docs, articles, videos, colleagues)?
51
- - What was concluded or learned?
29
+ Write in the developer's voice — "the developer fixed…", "the developer investigated…" — not passive constructions. Where you can estimate from the volume of captured activity, note the approximate time proportion: "(most of the hour)", "(~15 min)", "(brief)".
52
30
 
53
- ## Decisions Made
54
- Any meaningful choice that shapes the product or how the team works:
55
- - What was the decision?
56
- - What alternatives were considered?
57
- - Why was this direction chosen?
31
+ Include all work areas — coding, debugging, research, planning, reading docs, leisure. Do not filter anything out.
58
32
 
59
- ## Tickets & Tasks
60
- Include ONLY if specific ticket keys (KAN-NNN, JIRA-NNN, etc.) appear in the
61
- input. For each:
62
- - Plain-English goal of the ticket
63
- - What progress was made this session
64
- - What still remains
33
+ ### Key Decisions
34
+ One bullet per meaningful choice or conclusion reached. Bold the decision. Explain what was decided and why — what problem it solves or what alternative was rejected. Only include if clearly evidenced.
35
+
36
+ ### Resources Consulted
37
+ List documentation pages, repos, articles, videos, dashboards, or other materials the developer looked at, with brief context for why.
38
+
39
+ ---
65
40
 
66
41
  RULES
67
- - Write for someone who does not know the codebase. No jargon, no variable
68
- names, no file paths as headlines.
69
- - Do not make up facts not present in the input.
70
- - Do not truncate. If there is more to cover, cover it.\
42
+ - Infer the PURPOSE, not just the activity. If the screen shows edits to a prompt file + model test runs, say what the developer was trying to improve and why — not just "edited prompt file and ran tests."
43
+ - Extract identifiable specifics: system names, service names, model names, tool names — anything that helps a matcher connect this to a ticket.
44
+ - Do not make up facts, numbers, or names not present in the input.
45
+ - Leisure, browsing, and breaks are valid report them honestly.
46
+ - If a section has nothing to report, omit it entirely.
47
+ - DO NOT infer active work from PM/ticket dashboards. If the screen shows Jira, Linear, GitHub Issues, or Trello — the developer was reviewing tickets, not doing the work in them. Report what was visible (e.g. "reviewed ticket board"), not ticket content as if it were work in progress.
48
+ - DO NOT use git branch names as signals for what was worked on. A branch name only tells you a branch existed — report only what editor content, terminal output, or browser activity actually shows.
49
+
50
+ LENGTH
51
+ Keep the total response under 400 words. TLDR: 2–3 sentences. Each Core Task thread: 3–4 sentences. Key Decisions: one bullet per decision, one sentence each. Resources: one line per item.\
71
52
  """