cursorfleet 0.1.0__py3-none-any.whl

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.
Files changed (101) hide show
  1. cursorfleet/__init__.py +9 -0
  2. cursorfleet/_templates/cursor/agents/architect.md +18 -0
  3. cursorfleet/_templates/cursor/agents/implementer.md +20 -0
  4. cursorfleet/_templates/cursor/agents/patcher.md +17 -0
  5. cursorfleet/_templates/cursor/agents/qa.md +21 -0
  6. cursorfleet/_templates/cursor/agents/reviewer.md +20 -0
  7. cursorfleet/_templates/cursor/agents/scout.md +17 -0
  8. cursorfleet/_templates/cursor/agents/tester.md +20 -0
  9. cursorfleet/_templates/cursor/agents-md/root.md +7 -0
  10. cursorfleet/_templates/cursor/agents-md/work.md +7 -0
  11. cursorfleet/_templates/cursor/config/config.toml +47 -0
  12. cursorfleet/_templates/cursor/hooks/entry.json +4 -0
  13. cursorfleet/_templates/cursor/rules/cursorfleet-core.mdc +10 -0
  14. cursorfleet/_templates/cursor/rules/cursorfleet-handoff.mdc +15 -0
  15. cursorfleet/_templates/cursor/skills/cursorfleet-artifacts/SKILL.md +57 -0
  16. cursorfleet/_templates/cursor/skills/cursorfleet-coordinator/SKILL.md +35 -0
  17. cursorfleet/adapters/__init__.py +0 -0
  18. cursorfleet/adapters/cursor/__init__.py +0 -0
  19. cursorfleet/adapters/cursor/blocks.py +104 -0
  20. cursorfleet/adapters/cursor/diagnostics.py +547 -0
  21. cursorfleet/adapters/cursor/fsutil.py +156 -0
  22. cursorfleet/adapters/cursor/hook_main.py +428 -0
  23. cursorfleet/adapters/cursor/hook_normalize.py +527 -0
  24. cursorfleet/adapters/cursor/hook_policy.py +76 -0
  25. cursorfleet/adapters/cursor/hook_sanitize.py +474 -0
  26. cursorfleet/adapters/cursor/hooksjson.py +305 -0
  27. cursorfleet/adapters/cursor/installer.py +626 -0
  28. cursorfleet/adapters/cursor/kit.py +210 -0
  29. cursorfleet/adapters/cursor/kit_probe.py +54 -0
  30. cursorfleet/adapters/cursor/lock.py +145 -0
  31. cursorfleet/adapters/cursor/templates.py +60 -0
  32. cursorfleet/adapters/cursor/validation.py +365 -0
  33. cursorfleet/adapters/cursor/workspace.py +80 -0
  34. cursorfleet/cli/__init__.py +0 -0
  35. cursorfleet/cli/commands/__init__.py +1 -0
  36. cursorfleet/cli/commands/_common.py +82 -0
  37. cursorfleet/cli/commands/_resolution.py +56 -0
  38. cursorfleet/cli/commands/doctor.py +81 -0
  39. cursorfleet/cli/commands/events.py +197 -0
  40. cursorfleet/cli/commands/index.py +55 -0
  41. cursorfleet/cli/commands/init.py +71 -0
  42. cursorfleet/cli/commands/migrate.py +43 -0
  43. cursorfleet/cli/commands/replay.py +76 -0
  44. cursorfleet/cli/commands/run.py +97 -0
  45. cursorfleet/cli/commands/status.py +210 -0
  46. cursorfleet/cli/commands/tui.py +47 -0
  47. cursorfleet/cli/commands/uninstall.py +73 -0
  48. cursorfleet/cli/commands/validate.py +69 -0
  49. cursorfleet/cli/main.py +81 -0
  50. cursorfleet/config/__init__.py +44 -0
  51. cursorfleet/config/io.py +27 -0
  52. cursorfleet/config/models.py +154 -0
  53. cursorfleet/config/roster.py +245 -0
  54. cursorfleet/events/__init__.py +39 -0
  55. cursorfleet/events/forbidden.py +45 -0
  56. cursorfleet/events/ids.py +109 -0
  57. cursorfleet/events/kinds.py +132 -0
  58. cursorfleet/events/models.py +213 -0
  59. cursorfleet/events/pathcheck.py +45 -0
  60. cursorfleet/git/__init__.py +0 -0
  61. cursorfleet/git/collector.py +226 -0
  62. cursorfleet/git/runner.py +135 -0
  63. cursorfleet/git/worktrees.py +71 -0
  64. cursorfleet/policy/__init__.py +0 -0
  65. cursorfleet/py.typed +0 -0
  66. cursorfleet/safeexe.py +70 -0
  67. cursorfleet/schema_gen.py +73 -0
  68. cursorfleet/state/__init__.py +0 -0
  69. cursorfleet/state/artifact_scan.py +309 -0
  70. cursorfleet/state/context.py +38 -0
  71. cursorfleet/state/event_store.py +227 -0
  72. cursorfleet/state/indexer.py +443 -0
  73. cursorfleet/state/lock.py +74 -0
  74. cursorfleet/state/models.py +268 -0
  75. cursorfleet/state/reducer.py +588 -0
  76. cursorfleet/state/retention.py +218 -0
  77. cursorfleet/state/run_builder.py +136 -0
  78. cursorfleet/state/run_model.py +43 -0
  79. cursorfleet/state/run_store.py +95 -0
  80. cursorfleet/state/runtime.py +645 -0
  81. cursorfleet/state/spool.py +186 -0
  82. cursorfleet/state/spool_read.py +242 -0
  83. cursorfleet/state/status_doc.py +293 -0
  84. cursorfleet/tui/__init__.py +0 -0
  85. cursorfleet/tui/app.py +581 -0
  86. cursorfleet/tui/data.py +545 -0
  87. cursorfleet/tui/filters.py +82 -0
  88. cursorfleet/tui/gates.py +255 -0
  89. cursorfleet/tui/help.py +101 -0
  90. cursorfleet/tui/layout.py +26 -0
  91. cursorfleet/tui/theme.py +176 -0
  92. cursorfleet/tui/views.py +1077 -0
  93. cursorfleet/tui/watch.py +33 -0
  94. cursorfleet/workflow/__init__.py +0 -0
  95. cursorfleet/workflow/artifacts.py +148 -0
  96. cursorfleet/workflow/frontmatter.py +232 -0
  97. cursorfleet-0.1.0.dist-info/METADATA +161 -0
  98. cursorfleet-0.1.0.dist-info/RECORD +101 -0
  99. cursorfleet-0.1.0.dist-info/WHEEL +4 -0
  100. cursorfleet-0.1.0.dist-info/entry_points.txt +3 -0
  101. cursorfleet-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1077 @@
1
+ """Pure view builders: ``FleetData`` + ``UiState`` -> rows and detail text.
2
+
3
+ No Textual here, so every screen can be asserted as plain text. All untrusted strings go
4
+ through :func:`~cursorfleet.tui.theme.safe` and end up in ``rich.text.Text`` (never markup).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from collections.abc import Sequence
11
+ from dataclasses import dataclass, field
12
+
13
+ from rich.text import Text
14
+
15
+ from cursorfleet.adapters.cursor.kit_probe import HooksKitState
16
+ from cursorfleet.events.models import Event
17
+ from cursorfleet.state.artifact_scan import ArtifactRecord
18
+ from cursorfleet.state.event_store import EventPage
19
+ from cursorfleet.state.run_model import ParticipantGroup
20
+ from cursorfleet.tui import gates as gate_logic
21
+ from cursorfleet.tui.data import AgentBundle, Card, FleetData
22
+ from cursorfleet.tui.filters import matches_text
23
+ from cursorfleet.tui.theme import (
24
+ BASIS_TAG,
25
+ LANE_MARK,
26
+ LANE_ORDER,
27
+ LANE_TAG,
28
+ SELF_REPORTED_NOTE,
29
+ UNKNOWN_BUDGET,
30
+ Theme,
31
+ fmt_age,
32
+ fmt_duration,
33
+ fmt_ts,
34
+ plural,
35
+ safe,
36
+ short_sha,
37
+ )
38
+
39
+ VIEWS: tuple[str, ...] = (
40
+ "active_run",
41
+ "active_runs",
42
+ "overview",
43
+ "timeline",
44
+ "worktrees",
45
+ "gates",
46
+ "evidence",
47
+ )
48
+ VIEW_TITLE = {
49
+ "active_run": "Active run",
50
+ "active_runs": "Active runs",
51
+ "overview": "Sessions (archive)",
52
+ "timeline": "Timeline",
53
+ "worktrees": "Worktrees",
54
+ "gates": "Observations",
55
+ "evidence": "Evidence",
56
+ }
57
+
58
+ CONFIDENCE_LABEL = {
59
+ "explicit": "Explicit",
60
+ "declared": "Declared",
61
+ "inferred": "Inferred",
62
+ }
63
+
64
+
65
+ def default_view() -> str:
66
+ """Primary screen for M2.5; legacy overview via ``CURSORFLEET_PROTOTYPE=overview``."""
67
+ mode = os.environ.get("CURSORFLEET_PROTOTYPE", "").strip().lower()
68
+ if mode in {"overview", "legacy", "sessions"}:
69
+ return "overview"
70
+ return "active_run"
71
+
72
+
73
+ MAX_CARDS_PER_LANE = 100
74
+ MAX_LIST = 40
75
+
76
+ INIT_HINT = "cursorfleet init --cursor"
77
+ DOCTOR_HINT = "cursorfleet doctor"
78
+ GUIDANCE_NO_KIT = (
79
+ f"No hook telemetry yet. Run `{INIT_HINT}` to install the observe-only hooks, then use "
80
+ f"Cursor in this repo. `{DOCTOR_HINT}` diagnoses the setup."
81
+ )
82
+ GUIDANCE_AWAITING_CURSOR = (
83
+ "Hooks are installed (`cursorfleet doctor` should be ok). Open this folder in **Cursor**, "
84
+ "start a chat or agent session, then press `r` here — the first hook run creates telemetry."
85
+ )
86
+ GUIDANCE_KIT_PARTIAL = (
87
+ f"The CursorFleet kit looks incomplete (lock file or `.cursor/hooks.json` mismatch). "
88
+ f"Run `{INIT_HINT}` or `{DOCTOR_HINT}`."
89
+ )
90
+ HONEST_UNKNOWN = "Without telemetry the state is UNKNOWN; CursorFleet never infers inactivity."
91
+
92
+
93
+ def _timeline_empty_message(data: FleetData) -> str:
94
+ lines = overview_guidance_lines(data)
95
+ return lines[0] if lines else GUIDANCE_NO_KIT
96
+
97
+
98
+ def overview_guidance_lines(data: FleetData) -> list[str]:
99
+ """Primary setup hint when lanes are unknown (no hook sessions indexed yet)."""
100
+ if data.repo_root is None:
101
+ return [
102
+ "Not inside a git repository. Run from a repo or pass --path.",
103
+ f"Setup: `{INIT_HINT}`, check with `{DOCTOR_HINT}`.",
104
+ ]
105
+ if data.telemetry != "none":
106
+ return []
107
+ if data.hooks_kit == HooksKitState.INSTALLED:
108
+ return [GUIDANCE_AWAITING_CURSOR, HONEST_UNKNOWN]
109
+ if data.hooks_kit == HooksKitState.PARTIAL:
110
+ return [GUIDANCE_KIT_PARTIAL, HONEST_UNKNOWN]
111
+ return [GUIDANCE_NO_KIT, HONEST_UNKNOWN]
112
+
113
+
114
+ @dataclass(frozen=True)
115
+ class Row:
116
+ key: str
117
+ label: Text
118
+ search: str = ""
119
+ header: bool = False # headers are shown but cannot be selected
120
+
121
+
122
+ @dataclass
123
+ class UiState:
124
+ theme: Theme = field(default_factory=Theme)
125
+ pinned: set[str] = field(default_factory=set)
126
+ filter_text: str = ""
127
+ timeline_limit: int = 200
128
+ selected: dict[str, str] = field(default_factory=dict) # remembered key per view
129
+
130
+
131
+ def _t(*parts: str | tuple[str, str]) -> Text:
132
+ return Text.assemble(*parts)
133
+
134
+
135
+ def _header(key: str, text: str, style: str = "bold") -> Row:
136
+ return Row(key, Text(text, style=style), header=True)
137
+
138
+
139
+ def _note(key: str, text: str) -> Row:
140
+ return Row(key, Text(text, style="dim"), header=True)
141
+
142
+
143
+ def _visible(rows: Sequence[Row], ui: UiState) -> list[Row]:
144
+ if not ui.filter_text.strip():
145
+ return list(rows)
146
+ keep: list[Row] = []
147
+ for row in rows:
148
+ if row.header or matches_text(row.search, ui.filter_text):
149
+ keep.append(row)
150
+ return keep
151
+
152
+
153
+ # ---------------------------------------------------------------------------- overview
154
+
155
+
156
+ def _card_sort(card: Card, ui: UiState) -> tuple[int, int, float]:
157
+ stamp = card.last_ts.timestamp() if card.last_ts else 0.0
158
+ # pinned first, then blockers (most first), then recency (newest first); never raw activity
159
+ return (0 if card.key in ui.pinned else 1, -card.blockers, -stamp)
160
+
161
+
162
+ def card_label(card: Card, data: FleetData, ui: UiState) -> Text:
163
+ mark = LANE_MARK.get(card.lane, "[?]")
164
+ pin = "* " if card.key in ui.pinned else ""
165
+ basis = BASIS_TAG.get(card.basis, card.basis)
166
+ bundle = data.bundles.get(card.key)
167
+ tail: list[str] = [basis]
168
+ if card.kind == "agent" and card.lane == "stale_offline":
169
+ last = bundle.view.last_lane if bundle and bundle.view else None
170
+ tail.append(f"silent, was {last or '?'} (not idle)")
171
+ if card.kind != "worktree" or card.last_ts:
172
+ tail.append(
173
+ ("declared " if card.kind == "declared" else "") + fmt_age(data.now, card.last_ts)
174
+ )
175
+ if card.blockers:
176
+ tail.append(
177
+ f"BLOCKERS:{card.blockers}" + (" (self-reported)" if card.declared_blockers else "")
178
+ )
179
+ return _t(
180
+ pin,
181
+ (mark, ui.theme.lane(card.lane)),
182
+ " ",
183
+ (safe(card.title, 40), "bold"),
184
+ " ",
185
+ (safe(card.detail, 60), "dim"),
186
+ " ",
187
+ " ".join(tail),
188
+ )
189
+
190
+
191
+ def _group_label(group: ParticipantGroup, data: FleetData, ui: UiState) -> Text:
192
+ mark = LANE_MARK.get(group.summary_lane, "[?]")
193
+ conf = CONFIDENCE_LABEL.get(group.confidence, group.confidence)
194
+ tail = [conf]
195
+ if group.conflict:
196
+ tail.append("Conflict")
197
+ if group.declared_blockers:
198
+ tail.append(f"declared blockers:{group.declared_blockers}")
199
+ tail.append(f"{len(group.instances)} instance(s)")
200
+ return _t(
201
+ (mark, ui.theme.lane(group.summary_lane)),
202
+ " ",
203
+ (safe(group.role, 40), "bold"),
204
+ " ",
205
+ " ".join(tail),
206
+ )
207
+
208
+
209
+ def active_run_rows(data: FleetData, ui: UiState) -> list[Row]:
210
+ run = data.primary_run
211
+ rows: list[Row] = []
212
+ if run is None:
213
+ rows.extend(_guidance_rows(data))
214
+ rows.append(
215
+ _note(
216
+ "ar:empty",
217
+ "No active run yet. Declare a task under .cursorfleet/work/ or "
218
+ "`cursorfleet run start --task <slug>`.",
219
+ )
220
+ )
221
+ return rows
222
+ conf = CONFIDENCE_LABEL.get(run.confidence, run.confidence)
223
+ rows.append(
224
+ Row(
225
+ "ar:h",
226
+ _t(
227
+ ("ACTIVE RUN", "bold"),
228
+ f" task={safe(run.task_slug, 40)} run={safe(run.run_id, 24)} {conf}",
229
+ ),
230
+ header=True,
231
+ )
232
+ )
233
+ if run.branch or run.commit:
234
+ commit_label = short_sha(run.commit) if run.commit else "-"
235
+ rows.append(_note("ar:git", f"branch={safe(run.branch or '-', 40)} commit={commit_label}"))
236
+ for group in run.groups:
237
+ key = f"grp:{group.role}"
238
+ search = f"{group.role} {group.summary_lane} {group.confidence}"
239
+ rows.append(Row(key, _group_label(group, data, ui), search.lower()))
240
+ if run.suggested_session_ids:
241
+ rows.append(Row("ar:sug:h", _t(("Suggested sessions (not attached)", "bold")), header=True))
242
+ for sid in run.suggested_session_ids[:MAX_LIST]:
243
+ hint = f" attach: cursorfleet run attach <run> --session {sid}"
244
+ rows.append(_note(f"sug:{sid}", hint))
245
+ if run.unassociated_session_ids:
246
+ rows.append(Row("ar:un:h", _t(("Other live sessions (unscoped)", "bold")), header=True))
247
+ for sid in run.unassociated_session_ids[:MAX_LIST]:
248
+ rows.append(_note(f"un:{sid}", f" session {safe(sid, 48)}"))
249
+ rows.append(_note("ar:nav", "Press u for all runs; o for full session archive."))
250
+ return rows
251
+
252
+
253
+ def active_runs_rows(data: FleetData, ui: UiState) -> list[Row]:
254
+ rows: list[Row] = [Row("runs:h", _t(("ACTIVE RUNS", "bold")), header=True)]
255
+ if not data.run_snapshots:
256
+ rows.append(_note("runs:0", "No persisted runs. `cursorfleet run start --task <slug>`"))
257
+ if data.primary_run is not None:
258
+ run = data.primary_run
259
+ conf = CONFIDENCE_LABEL.get(run.confidence, run.confidence)
260
+ rows.append(
261
+ Row(
262
+ f"run:{run.run_id}",
263
+ _t(("inferred", "dim"), f" {safe(run.task_slug, 40)} ({conf})"),
264
+ run.task_slug,
265
+ )
266
+ )
267
+ return rows
268
+ for run in data.run_snapshots:
269
+ conf = CONFIDENCE_LABEL.get(run.confidence, run.confidence)
270
+ rows.append(
271
+ Row(
272
+ f"run:{run.run_id}",
273
+ _t(
274
+ (safe(run.task_slug, 40), "bold"),
275
+ f" {safe(run.run_id, 24)} {conf} groups={len(run.groups)}",
276
+ ),
277
+ run.task_slug,
278
+ )
279
+ )
280
+ archived = sum(1 for r in data.run_records if r.archived)
281
+ if archived:
282
+ rows.append(_note("runs:arch", f"{archived} archived run(s); see `cursorfleet run list`"))
283
+ return rows
284
+
285
+
286
+ def active_run_detail(data: FleetData, ui: UiState, key: str) -> Text:
287
+ run = data.primary_run
288
+ if run is None or not key.startswith("grp:"):
289
+ return Text("Select a role group to see instances.", style="dim")
290
+ role = key[4:]
291
+ group = next((g for g in run.groups if g.role == role), None)
292
+ if group is None:
293
+ return Text("Unknown group.", style="dim")
294
+ doc = _Doc(ui.theme)
295
+ doc.title(f"Role: {safe(role, 40)}", "bold")
296
+ doc.line("Summary lane", LANE_TAG.get(group.summary_lane, group.summary_lane))
297
+ doc.line("Confidence", CONFIDENCE_LABEL.get(group.confidence, group.confidence))
298
+ if group.conflict:
299
+ doc.raw("Conflict: instances disagree on lane or state.", "bold")
300
+ doc.raw("")
301
+ for inst in group.instances:
302
+ lane = LANE_TAG.get(inst.lane, inst.lane)
303
+ basis = BASIS_TAG.get(inst.lane_basis, inst.lane_basis)
304
+ doc.line(f"{inst.session_id}/{inst.agent_key}", f"{lane} ({basis})")
305
+ doc.line(" last", fmt_ts(inst.last_ts) if inst.last_ts else "-")
306
+ return doc.text
307
+
308
+
309
+ def overview_rows(data: FleetData, ui: UiState) -> list[Row]:
310
+ by_lane: dict[str, list[Card]] = {lane: [] for lane in LANE_ORDER}
311
+ for card in data.cards:
312
+ by_lane.setdefault(card.lane if card.lane in by_lane else "unknown", []).append(card)
313
+ rows: list[Row] = []
314
+ for lane in LANE_ORDER:
315
+ cards = sorted(by_lane[lane], key=lambda c: _card_sort(c, ui))
316
+ shown: list[Row] = []
317
+ for card in cards[:MAX_CARDS_PER_LANE]:
318
+ search = f"{card.title} {card.detail} {LANE_TAG[lane]} {card.basis} {card.kind}"
319
+ shown.append(Row(card.key, card_label(card, data, ui), search.lower()))
320
+ visible = _visible(shown, ui)
321
+ count = f"{len(visible)}" if not ui.filter_text.strip() else f"{len(visible)}/{len(cards)}"
322
+ rows.append(
323
+ Row(
324
+ f"lane:{lane}",
325
+ _t(
326
+ (
327
+ f"{LANE_MARK[lane]} {LANE_TAG[lane]} ({count})",
328
+ ui.theme.lane(lane) or "bold",
329
+ ),
330
+ ),
331
+ header=True,
332
+ )
333
+ )
334
+ rows.extend(visible)
335
+ if len(cards) > MAX_CARDS_PER_LANE:
336
+ rows.append(
337
+ _note(f"more:{lane}", f" +{len(cards) - MAX_CARDS_PER_LANE} older (filter with /)")
338
+ )
339
+ if lane == "unknown":
340
+ rows.extend(_guidance_rows(data))
341
+ return rows
342
+
343
+
344
+ def _guidance_rows(data: FleetData) -> list[Row]:
345
+ lines = overview_guidance_lines(data)
346
+ return [_note(f"g:{index}", line) for index, line in enumerate(lines)]
347
+
348
+
349
+ def _declared_lines(records: Sequence[ArtifactRecord]) -> dict[str, list[ArtifactRecord]]:
350
+ out: dict[str, list[ArtifactRecord]] = {}
351
+ for record in sorted(records, key=lambda r: (r.created, r.path)):
352
+ out.setdefault(record.kind, []).append(record)
353
+ return out
354
+
355
+
356
+ class _Doc:
357
+ """Tiny builder for the detail pane."""
358
+
359
+ def __init__(self, theme: Theme) -> None:
360
+ self.theme = theme
361
+ self.text = Text()
362
+
363
+ def title(self, text: str, style: str = "") -> None:
364
+ self.text.append(text + "\n", style=style or self.theme.accent)
365
+
366
+ def line(self, label: str, value: str, style: str = "") -> None:
367
+ self.text.append(f"{label:<13}", style="dim")
368
+ self.text.append(value + "\n", style=style)
369
+
370
+ def raw(self, value: str, style: str = "") -> None:
371
+ self.text.append(value + "\n", style=style)
372
+
373
+ def blank(self) -> None:
374
+ self.text.append("\n")
375
+
376
+
377
+ def agent_detail(
378
+ data: FleetData, ui: UiState, key: str, tools: Sequence[Event] | None = None
379
+ ) -> Text:
380
+ bundle = data.bundles.get(key)
381
+ if bundle is None:
382
+ return Text("Select an agent to see its detail.", style="dim")
383
+ doc = _Doc(ui.theme)
384
+ acc, view = bundle.acc, bundle.view
385
+ declared_only = view is None
386
+ lane = view.lane if view else acc.lane.value
387
+ title = safe(acc.role or ("main agent" if acc.key == "main" else acc.key), 60)
388
+ doc.title(f"AGENT {title}")
389
+ doc.line(
390
+ "Lane",
391
+ f"{LANE_MARK.get(lane, '[?]')} {LANE_TAG.get(lane, lane)}",
392
+ ui.theme.lane(lane),
393
+ )
394
+ if view is not None:
395
+ basis = BASIS_TAG.get(view.lane_basis, view.lane_basis)
396
+ doc.line("Lane basis", f"{basis} (heuristic over events; {view.attribution} attribution)")
397
+ if view.stale:
398
+ doc.line(
399
+ "Silent",
400
+ f"no live event for {fmt_age(data.now, bundle.acc.last_live_ts)}; "
401
+ f"last lane was {view.last_lane or '?'}. Silence is not idleness.",
402
+ "bold",
403
+ )
404
+ else:
405
+ doc.line("Lane basis", "SELF-REPORTED by artifact only; no hook telemetry for this role")
406
+ doc.line("Role", safe(acc.role or "-"))
407
+ if bundle.session is not None and not declared_only:
408
+ doc.line("Session", safe(bundle.session.session_id, 80))
409
+ doc.line("Agent key", safe(acc.key, 80))
410
+ if acc.instance_id:
411
+ doc.line("Instance", safe(acc.instance_id, 80))
412
+ _task_lines(doc, bundle)
413
+ _worktree_lines(doc, data, bundle)
414
+ if view is not None:
415
+ _activity_lines(doc, data, bundle, tools)
416
+ _gate_lines(doc, data, bundle)
417
+ doc.line("Token/cost", UNKNOWN_BUDGET)
418
+ _declared_lines_into(doc, bundle)
419
+ return doc.text
420
+
421
+
422
+ def _task_lines(doc: _Doc, bundle: AgentBundle) -> None:
423
+ records = bundle.records
424
+ tasks = sorted({r.task for r in records})
425
+ if tasks:
426
+ doc.line(
427
+ "Task", f"{', '.join(safe(t, 40) for t in tasks[:4])} [SELF-REPORTED, by role name]"
428
+ )
429
+ else:
430
+ doc.line("Task", "unknown (no artifact declares one for this role)")
431
+ observed = list(bundle.acc.issue_refs)
432
+ declared = sorted({r.issue_ref for r in records if r.issue_ref})
433
+ refs = [f"{safe(r, 40)} (observed)" for r in observed]
434
+ refs += [f"{safe(r, 40)} (SELF-REPORTED)" for r in declared if r not in observed]
435
+ doc.line("Issues", ", ".join(refs[:6]) if refs else "none known")
436
+
437
+
438
+ def _worktree_lines(doc: _Doc, data: FleetData, bundle: AgentBundle) -> None:
439
+ acc = bundle.acc
440
+ wt = data.worktree_by_id(acc.worktree_id)
441
+ if wt is not None:
442
+ branch = wt.branch or ("detached" if wt.detached else "?")
443
+ dirty = "?" if wt.dirty_count is None else str(wt.dirty_count)
444
+ doc.line("Worktree", safe(wt.path, 100))
445
+ doc.line("Branch", f"{safe(branch, 60)} HEAD {short_sha(wt.head)} dirty files: {dirty}")
446
+ elif acc.branch or acc.commit or acc.worktree_id:
447
+ doc.line("Worktree", f"{safe(acc.worktree_id or '?')} (not listed by git now)")
448
+ doc.line(
449
+ "Branch",
450
+ f"{safe(acc.branch or '?', 60)} commit {short_sha(acc.commit)} (as last seen)",
451
+ )
452
+ else:
453
+ doc.line("Worktree", "unknown")
454
+
455
+
456
+ def _activity_lines(
457
+ doc: _Doc, data: FleetData, bundle: AgentBundle, tools: Sequence[Event] | None
458
+ ) -> None:
459
+ acc, view = bundle.acc, bundle.view
460
+ assert view is not None # noqa: S101
461
+ more = "+" if acc.files_overflow else ""
462
+ doc.line("Files", f"{len(acc.files)}{more} changed (observed)")
463
+ if tools:
464
+ names = [safe(e.tool_name or e.kind.value, 24) for e in tools]
465
+ doc.line("Last tools", ", ".join(names))
466
+ else:
467
+ doc.line("Last tools", safe(acc.last_tool or "none observed", 40))
468
+ if acc.last_test is not None:
469
+ doc.line(
470
+ "Tests",
471
+ f"last run {acc.last_test.outcome.value} "
472
+ f"{fmt_age(data.now, acc.last_test.ts)} (observed)",
473
+ )
474
+ else:
475
+ doc.line("Tests", "no test run observed")
476
+ fails = f" ({view.tool_failures} failed)" if view.tool_failures else ""
477
+ doc.line("Elapsed", fmt_duration(view.elapsed_s))
478
+ doc.line("Tool calls", f"{view.tool_call_count}{fails}")
479
+ ctx = (
480
+ f", context {view.last_context_usage_percent:.0f}%"
481
+ if view.last_context_usage_percent is not None
482
+ else ""
483
+ )
484
+ doc.line("Compactions", f"{view.compactions}{ctx}")
485
+ if view.stop_report is not None:
486
+ sr = view.stop_report
487
+ calls = sr.tool_call_count if sr.tool_call_count is not None else "?"
488
+ doc.line(
489
+ "Stop report",
490
+ f"outcome {sr.outcome or '?'}, {calls} tool calls (reported by Cursor)",
491
+ )
492
+
493
+
494
+ def _gate_lines(doc: _Doc, data: FleetData, bundle: AgentBundle) -> None:
495
+ rows = data.gates.by_worktree.get(bundle.acc.worktree_id)
496
+ if rows is None:
497
+ doc.line("Gates", "unknown (no evidence for this worktree), display-only")
498
+ return
499
+ doc.line("Gates", "this worktree, display-only, independent signals:")
500
+ for row in rows:
501
+ doc.raw(f" {row.label}: {row.state.upper()}", doc.theme.gate(row.state))
502
+
503
+
504
+ def _declared_lines_into(doc: _Doc, bundle: AgentBundle) -> None:
505
+ records = bundle.records
506
+ if not records:
507
+ doc.line("Declared", "no artifacts for this role")
508
+ return
509
+ doc.blank()
510
+ doc.title(f"DECLARED BY THE AGENT - {SELF_REPORTED_NOTE}", "bold")
511
+ groups = _declared_lines(records)
512
+ for kind, label in (
513
+ ("context.loaded", "Context sources"),
514
+ ("plan.created", "Plans"),
515
+ ("handoff.created", "Handoffs"),
516
+ ("blocker.raised", "Blockers"),
517
+ ):
518
+ items = groups.get(kind, [])
519
+ if not items:
520
+ continue
521
+ if kind == "context.loaded":
522
+ refs = sorted({ref for r in items for ref in r.context_refs})
523
+ shown = ", ".join(safe(r, 50) for r in refs[:8]) + (
524
+ f" (+{len(refs) - 8} more)" if len(refs) > 8 else ""
525
+ )
526
+ doc.line(label, f"{len(refs)} path(s): {shown or '-'}")
527
+ elif kind == "handoff.created":
528
+ last = items[-1]
529
+ dest = safe(last.to_role or "unspecified", 40)
530
+ doc.line(label, f"{len(items)}; latest destination: {dest}")
531
+ else:
532
+ latest = items[-1]
533
+ doc.line(
534
+ label,
535
+ f"{len(items)}; latest {fmt_ts(latest.created)} UTC task {safe(latest.task, 40)}",
536
+ )
537
+
538
+
539
+ # ---------------------------------------------------------------------------- timeline
540
+
541
+ _SRC = {"observed": "obs", "self_reported": "SELF", "derived": "drv"}
542
+ _ATTR = {"exact": "exact", "inferred": "infer", "unknown": "unk"}
543
+
544
+
545
+ def event_summary(event: Event) -> str:
546
+ bits: list[str] = []
547
+ if event.tool_name:
548
+ bits.append(safe(event.tool_name, 30))
549
+ if event.command is not None:
550
+ shown = event.command.display or event.command.argv0
551
+ bits.append(safe(shown, 70))
552
+ if event.command.exit_code is not None:
553
+ bits.append(f"exit {event.command.exit_code}")
554
+ if event.paths:
555
+ bits.append(
556
+ safe(event.paths[0].path, 60)
557
+ + (f" +{len(event.paths) - 1}" if len(event.paths) > 1 else "")
558
+ )
559
+ if event.outcome is not None:
560
+ bits.append(event.outcome.value)
561
+ if event.gate is not None:
562
+ bits.append(f"{event.gate.name}={event.gate.state.value}")
563
+ if event.issue_ref:
564
+ bits.append(safe(event.issue_ref, 30))
565
+ return " ".join(bits)
566
+
567
+
568
+ def event_label(event: Event, ui: UiState) -> Text:
569
+ risk = f" RISK:{event.risk.value}" if event.risk.value != "none" else ""
570
+ src = _SRC.get(event.source.value, event.source.value)
571
+ who = safe(event.agent_id or "main/unknown", 28)
572
+ return _t(
573
+ (fmt_ts(event.ts), "dim"),
574
+ " ",
575
+ (f"{event.kind.value:<17}", "bold"),
576
+ f" {src}/{_ATTR.get(event.attribution.value, '?')} ",
577
+ (who, ""),
578
+ " ",
579
+ event_summary(event),
580
+ (risk, "bold" if risk else ""),
581
+ )
582
+
583
+
584
+ def timeline_rows(data: FleetData, ui: UiState, page: EventPage | None = None) -> list[Row]:
585
+ page = page or data.timeline
586
+ rows: list[Row] = []
587
+ shown = len(page.events)
588
+ note = f"Showing {shown} of {page.total} matching event(s), newest first. Times are UTC."
589
+ rows.append(_header("tl:head", note))
590
+ if page.total > shown:
591
+ rows.append(_note("tl:more", "Press m to load more."))
592
+ for err in data.timeline_errors:
593
+ rows.append(_note(f"tl:err:{err}", f"filter problem: {err}"))
594
+ if page.error:
595
+ rows.append(_note("tl:error", page.error))
596
+ if page.skipped:
597
+ rows.append(
598
+ _note(
599
+ "tl:skipped", f"{page.skipped} stored event(s) no longer validate and were skipped"
600
+ )
601
+ )
602
+ if not page.events:
603
+ if page.total == 0 and ui.filter_text:
604
+ empty_msg = "No events match."
605
+ else:
606
+ empty_msg = _timeline_empty_message(data)
607
+ rows.append(_note("tl:empty", empty_msg))
608
+ for event in page.events:
609
+ rows.append(Row(f"ev:{event.session_id}:{event.event_id}", event_label(event, ui)))
610
+ return rows
611
+
612
+
613
+ def _attr_explain(event: Event) -> str:
614
+ return {
615
+ "exact": "identity came in the Cursor payload itself",
616
+ "inferred": "linked by tool_use_id or worktree (PROVISIONAL)",
617
+ "unknown": "Cursor did not say which agent (PROVISIONAL, ADR 0001 Q1)",
618
+ }.get(event.attribution.value, "")
619
+
620
+
621
+ def event_detail(event: Event | None, ui: UiState) -> Text:
622
+ if event is None:
623
+ return Text("Select an event to see its sanitized fields.", style="dim")
624
+ doc = _Doc(ui.theme)
625
+ doc.title(f"EVENT {event.kind.value}")
626
+ src = event.source.value
627
+ note = " [SELF-REPORTED: a claim, not evidence]" if src == "self_reported" else ""
628
+ doc.line("Source", f"{src}{note}")
629
+ doc.line("Attribution", f"{event.attribution.value}: {_attr_explain(event)}")
630
+ doc.line("Time", f"{fmt_ts(event.ts)} UTC")
631
+ doc.line("Producer", f"{event.producer.value} {safe(event.producer_version, 30)}")
632
+ doc.line("Risk", f"{event.risk.value} (informational only; v0.1 never acts on it)")
633
+ doc.line("Session", safe(event.session_id, 100))
634
+ doc.line("Agent", safe(event.agent_id or "none (main agent or unattributed)", 100))
635
+ for label, value in (
636
+ ("Worktree", event.worktree_id),
637
+ ("Branch", event.branch),
638
+ ("Commit", event.commit),
639
+ ("Issue", event.issue_ref),
640
+ ("Tool", event.tool_name),
641
+ ("Hook", event.hook),
642
+ ("Outcome", event.outcome.value if event.outcome else None),
643
+ ("Status", event.status.value if event.status else None),
644
+ ):
645
+ if value:
646
+ doc.line(label, safe(value, 100))
647
+ if event.gate is not None:
648
+ doc.line("Gate", f"{event.gate.name} = {event.gate.state.value}")
649
+ if event.command is not None:
650
+ cmd = event.command
651
+ doc.line(
652
+ "Command",
653
+ safe(cmd.display or cmd.argv0, 200)
654
+ + (" (truncated)" if cmd.display_truncated else ""),
655
+ )
656
+ extra = [f"argv0 {safe(cmd.argv0, 40)}"]
657
+ if cmd.subcommand:
658
+ extra.append(f"sub {safe(cmd.subcommand, 40)}")
659
+ if cmd.exit_code is not None:
660
+ extra.append(f"exit {cmd.exit_code}")
661
+ if cmd.duration_ms is not None:
662
+ extra.append(f"{cmd.duration_ms} ms")
663
+ doc.line("", " ".join(extra))
664
+ if event.paths:
665
+ doc.line("Paths", plural(len(event.paths), "path") + " (workspace-relative):")
666
+ for item in event.paths[:MAX_LIST]:
667
+ doc.raw(f" {safe(item.path, 120)} ({item.op.value})")
668
+ if len(event.paths) > MAX_LIST:
669
+ doc.raw(f" ... +{len(event.paths) - MAX_LIST} more")
670
+ if event.metrics is not None:
671
+ parts = [f"{k}={v}" for k, v in event.metrics.model_dump(exclude_none=True).items()]
672
+ doc.line("Metrics", ", ".join(parts))
673
+ return doc.text
674
+
675
+
676
+ # ---------------------------------------------------------------------------- worktrees
677
+
678
+
679
+ def worktree_rows(data: FleetData, ui: UiState) -> list[Row]:
680
+ rows: list[Row] = []
681
+ if data.git is None:
682
+ reason = "git collection is disabled (--no-git)" if not data.git_enabled else "no data yet"
683
+ return [_header("wt:none", f"No worktree data: {reason}.")]
684
+ if not data.git.available:
685
+ return [
686
+ _header("wt:down", f"git data unavailable: {safe(data.git.error, 100)}"),
687
+ _note("wt:hint", f"Run from inside a git repository, then `{DOCTOR_HINT}`."),
688
+ ]
689
+ rows.append(
690
+ _header(
691
+ "wt:head",
692
+ f"{plural(len(data.git.worktrees), 'worktree')} (read-only; r re-reads, "
693
+ "CursorFleet never fetches, so ahead/behind can lag the remote).",
694
+ )
695
+ )
696
+ for index, wt in enumerate(data.git.worktrees):
697
+ branch = wt.branch or ("detached" if wt.detached else "?")
698
+ dirty = "dirty ?" if wt.dirty_count is None else f"dirty {wt.dirty_count}"
699
+ sync = "no upstream" if wt.ahead is None else f"ahead {wt.ahead}/behind {wt.behind}"
700
+ marks = [f"STALE({','.join(wt.stale_reasons)})"] if wt.stale else []
701
+ owners = _owner_names(data, wt.worktree_id)
702
+ label = _t(
703
+ ("[main] " if wt.is_main else "[wt] ", "bold"),
704
+ (safe(branch, 40), "bold"),
705
+ f" HEAD {short_sha(wt.head)} {dirty} {sync} ",
706
+ (" ".join(marks), "bold" if marks else ""),
707
+ f" owner: {owners}" if owners else " owner: unknown (no telemetry)",
708
+ f" last commit {fmt_age(data.now, wt.last_commit_ts)}",
709
+ )
710
+ search = f"{wt.path} {branch} {' '.join(marks)} {owners}".lower()
711
+ rows.append(Row(f"wt:{index}:{wt.path}", label, search))
712
+ return [rows[0], *_visible(rows[1:], ui)]
713
+
714
+
715
+ def _owner_names(data: FleetData, worktree_id: str | None) -> str:
716
+ if worktree_id is None:
717
+ return ""
718
+ names: list[str] = []
719
+ for bundle in data.bundles.values():
720
+ if bundle.acc.worktree_id == worktree_id and bundle.view is not None:
721
+ names.append(safe(bundle.acc.role or bundle.acc.key, 30))
722
+ return ", ".join(sorted(set(names)))
723
+
724
+
725
+ def worktree_detail(data: FleetData, ui: UiState, key: str) -> Text:
726
+ if data.git is None:
727
+ return Text("No worktree data.", style="dim")
728
+ wt = next((w for i, w in enumerate(data.git.worktrees) if key == f"wt:{i}:{w.path}"), None)
729
+ if wt is None:
730
+ return Text("Select a worktree.", style="dim")
731
+ doc = _Doc(ui.theme)
732
+ doc.title(f"WORKTREE {'(main) ' if wt.is_main else ''}{safe(_base_name(wt.path), 60)}")
733
+ doc.line("Path", safe(wt.path, 200))
734
+ doc.line("Branch", safe(wt.branch or ("detached HEAD" if wt.detached else "?"), 100))
735
+ doc.line("HEAD", wt.head or "-")
736
+ dirty = "unknown" if wt.dirty_count is None else f"{wt.dirty_count} file(s)"
737
+ doc.line("Dirty", dirty)
738
+ if wt.ahead is None:
739
+ doc.line("Ahead/behind", "unknown (no upstream, detached HEAD or unreadable ref)")
740
+ else:
741
+ doc.line(
742
+ "Ahead/behind", f"+{wt.ahead} / -{wt.behind} vs the existing upstream ref (no fetch)"
743
+ )
744
+ doc.line(
745
+ "State",
746
+ ", ".join(
747
+ s
748
+ for s, on in (
749
+ ("locked", wt.locked),
750
+ ("prunable", wt.prunable),
751
+ ("bare", wt.bare),
752
+ ("missing", not wt.exists),
753
+ )
754
+ if on
755
+ )
756
+ or "normal",
757
+ )
758
+ doc.line("Stale", ", ".join(wt.stale_reasons) if wt.stale else "no")
759
+ owners = _owner_names(data, wt.worktree_id)
760
+ doc.line("Owner", owners or "unknown (no hook telemetry links an agent to this worktree)")
761
+ doc.line(
762
+ "Last commit", f"{fmt_ts(wt.last_commit_ts)} UTC ({fmt_age(data.now, wt.last_commit_ts)})"
763
+ )
764
+ if wt.error:
765
+ doc.line("Problem", safe(wt.error, 100))
766
+ doc.blank()
767
+ doc.raw("Cursor creates and deletes worktrees itself; this list is a snapshot.", "dim")
768
+ return doc.text
769
+
770
+
771
+ def _base_name(path: str) -> str:
772
+ return path.rstrip("/\\").rsplit("/", 1)[-1].rsplit("\\", 1)[-1] or path
773
+
774
+
775
+ # ---------------------------------------------------------------------------- gates
776
+
777
+
778
+ def _gate_board(data: FleetData) -> list[tuple[str | None, str, list[gate_logic.GateStatus]]]:
779
+ """``(worktree_id, title, statuses)`` for each worktree, plus unlinked evidence."""
780
+ boards: list[tuple[str | None, str, list[gate_logic.GateStatus]]] = []
781
+ seen: set[str | None] = set()
782
+ if data.git is not None:
783
+ for wt in data.git.worktrees:
784
+ statuses = data.gates.by_worktree.get(wt.worktree_id)
785
+ if statuses is None:
786
+ statuses = gate_logic.evaluate([], worktree_id=wt.worktree_id, head=wt.head)
787
+ branch = wt.branch or ("detached" if wt.detached else "?")
788
+ boards.append((wt.worktree_id, f"{safe(branch, 40)} @ {short_sha(wt.head)}", statuses))
789
+ seen.add(wt.worktree_id)
790
+ for wt_id, statuses in data.gates.by_worktree.items():
791
+ if wt_id not in seen:
792
+ title = f"worktree {safe(wt_id or 'unlinked', 30)} (not listed by git; HEAD unknown)"
793
+ boards.append((wt_id, title, statuses))
794
+ if not boards:
795
+ boards.append(
796
+ (
797
+ None,
798
+ "no git data (gates cannot be bound to a commit)",
799
+ gate_logic.evaluate([], worktree_id=None, head=None),
800
+ )
801
+ )
802
+ return boards
803
+
804
+
805
+ def gate_rows(data: FleetData, ui: UiState) -> list[Row]:
806
+ rows: list[Row] = [
807
+ _header(
808
+ "gt:head", f"GATES - {gate_logic.DISPLAY_ONLY}. Independent signals; no overall score."
809
+ ),
810
+ _note(
811
+ "gt:rule",
812
+ "Evidence = observed test/lint/type runs bound to a commit. "
813
+ "Self-reported claims never count.",
814
+ ),
815
+ ]
816
+ for wt_id, title, statuses in _gate_board(data):
817
+ rows.append(_header(f"gt:wt:{wt_id}", f"== {title}"))
818
+ for status in statuses:
819
+ label = _t(
820
+ (f"[{status.state.upper():<7}]", ui.theme.gate(status.state)),
821
+ f" {status.label:<19} ",
822
+ (safe(status.detail, 80), "dim"),
823
+ )
824
+ rows.append(
825
+ Row(
826
+ f"gt:{wt_id}:{status.gate}",
827
+ label,
828
+ f"{status.label} {status.state} {title}".lower(),
829
+ )
830
+ )
831
+ return [r for r in _visible(rows, ui)]
832
+
833
+
834
+ def gate_detail(data: FleetData, ui: UiState, key: str) -> Text:
835
+ for wt_id, title, statuses in _gate_board(data):
836
+ for status in statuses:
837
+ if key != f"gt:{wt_id}:{status.gate}":
838
+ continue
839
+ doc = _Doc(ui.theme)
840
+ doc.title(f"GATE {status.label}")
841
+ doc.line("State", status.state.upper(), ui.theme.gate(status.state))
842
+ doc.line("Detail", status.detail)
843
+ doc.line("Worktree", title)
844
+ doc.line("HEAD", status.head or "unknown")
845
+ item = status.evidence
846
+ if item is None:
847
+ doc.line(
848
+ "Evidence",
849
+ "none. In v0.1 only observed test/lint/type runs and "
850
+ "gate.changed events count.",
851
+ )
852
+ else:
853
+ doc.line("Evidence", f"{item.origin} (observed/derived from Cursor hooks)")
854
+ doc.line("Result", item.state)
855
+ doc.line("Commit", item.commit or "not recorded")
856
+ doc.line("When", f"{fmt_ts(item.ts)} UTC ({fmt_age(data.now, item.ts)})")
857
+ doc.line("Agent", safe(item.agent or "unattributed", 60))
858
+ if item.command:
859
+ doc.line("Command", safe(item.command, 160))
860
+ if item.note:
861
+ doc.line("Note", item.note)
862
+ doc.blank()
863
+ doc.raw(gate_logic.DISPLAY_ONLY + ".", "bold")
864
+ return doc.text
865
+ return Text("Select a gate.", style="dim")
866
+
867
+
868
+ # ---------------------------------------------------------------------------- evidence
869
+
870
+
871
+ def evidence_rows(data: FleetData, ui: UiState) -> list[Row]:
872
+ rows: list[Row] = [
873
+ _header("ev:h1", "OBSERVED / DERIVED EVIDENCE (test, lint and type runs; gate events)")
874
+ ]
875
+ latest = sorted(data.gates.evidence, key=lambda e: (e.ts, e.session_id), reverse=True)
876
+ if not latest:
877
+ rows.append(_note("ev:none", "none yet: no verify-style command has been observed."))
878
+ for index, item in enumerate(latest[:MAX_LIST]):
879
+ label = _t(
880
+ (f"[{item.state.upper():<7}]", ui.theme.gate(item.state)),
881
+ f" {gate_logic.GATE_LABEL[item.gate]:<19} @{short_sha(item.commit)} ",
882
+ (fmt_age(data.now, item.ts), "dim"),
883
+ f" {safe(item.command or item.origin, 60)}",
884
+ )
885
+ rows.append(
886
+ Row(f"evi:{index}", label, f"{item.gate} {item.state} {item.command or ''}".lower())
887
+ )
888
+ rows.append(_header("ev:h2", f"DECLARED BY AGENTS - {SELF_REPORTED_NOTE}"))
889
+ records = sorted(data.artifacts.records, key=lambda r: (r.created, r.path), reverse=True)
890
+ if not records:
891
+ rows.append(_note("ev:noart", "no valid artifacts under the work directory."))
892
+ for index, rec in enumerate(records[:MAX_LIST]):
893
+ label = _t(
894
+ ("[SELF]", "bold"),
895
+ f" {rec.kind:<16} {safe(rec.author_role, 24)}",
896
+ f" -> {safe(rec.to_role, 20)}" if rec.to_role else "",
897
+ f" task {safe(rec.task, 30)} ",
898
+ (fmt_age(data.now, rec.created), "dim"),
899
+ )
900
+ rows.append(
901
+ Row(
902
+ f"art:{index}",
903
+ label,
904
+ f"{rec.kind} {rec.author_role} {rec.task} {rec.to_role or ''}".lower(),
905
+ )
906
+ )
907
+ if data.artifacts.issues:
908
+ rows.append(
909
+ _header("ev:h3", "INVALID ARTIFACTS (validation problems; not shown as declarations)")
910
+ )
911
+ for index, issue in enumerate(data.artifacts.issues[:MAX_LIST]):
912
+ rows.append(
913
+ Row(
914
+ f"iss:{index}",
915
+ _t(("[INVALID]", "bold"), f" {safe(issue.path, 60)}: {issue.message}"),
916
+ f"{issue.path} {issue.code}".lower(),
917
+ )
918
+ )
919
+ return _visible(rows, ui)
920
+
921
+
922
+ def evidence_detail(data: FleetData, ui: UiState, key: str) -> Text:
923
+ doc = _Doc(ui.theme)
924
+ if key.startswith("evi:"):
925
+ latest = sorted(data.gates.evidence, key=lambda e: (e.ts, e.session_id), reverse=True)
926
+ index = int(key[4:])
927
+ if index < len(latest):
928
+ item = latest[index]
929
+ doc.title(f"EVIDENCE {gate_logic.GATE_LABEL[item.gate]}")
930
+ doc.line("Result", item.state.upper(), ui.theme.gate(item.state))
931
+ doc.line("Origin", f"{item.origin} (observed/derived from Cursor hooks)")
932
+ doc.line("Commit", item.commit or "not recorded (evidence cannot be bound)")
933
+ doc.line("Session", safe(item.session_id, 80))
934
+ doc.line("Agent", safe(item.agent or "unattributed", 60))
935
+ doc.line("When", f"{fmt_ts(item.ts)} UTC")
936
+ if item.command:
937
+ doc.line("Command", safe(item.command, 160))
938
+ if item.note:
939
+ doc.line("Note", item.note)
940
+ return doc.text
941
+ if key.startswith("art:"):
942
+ records = sorted(data.artifacts.records, key=lambda r: (r.created, r.path), reverse=True)
943
+ index = int(key[4:])
944
+ if index < len(records):
945
+ rec = records[index]
946
+ doc.title(f"DECLARATION {rec.kind}", "bold")
947
+ doc.raw(SELF_REPORTED_NOTE, "bold")
948
+ doc.line("Task", safe(rec.task, 60))
949
+ doc.line("Author role", f"{safe(rec.author_role, 40)} (claimed; can be forged)")
950
+ doc.line("To role", safe(rec.to_role or "-", 40))
951
+ doc.line("Issue", safe(rec.issue_ref or "-", 60))
952
+ doc.line("Created", f"{fmt_ts(rec.created)} UTC")
953
+ doc.line("File", safe(rec.path, 100))
954
+ doc.line("Context", f"{len(rec.context_refs)} path(s)")
955
+ for ref in rec.context_refs[:MAX_LIST]:
956
+ doc.raw(f" {safe(ref, 100)}")
957
+ return doc.text
958
+ if key.startswith("iss:"):
959
+ index = int(key[4:])
960
+ if index < len(data.artifacts.issues):
961
+ issue = data.artifacts.issues[index]
962
+ doc.title("INVALID ARTIFACT", "bold")
963
+ doc.line("File", safe(issue.path, 100))
964
+ doc.line("Problem", issue.message)
965
+ doc.line("Code", issue.code)
966
+ doc.raw(
967
+ "Fix the file and run `cursorfleet validate`. The file's content is never shown.",
968
+ "dim",
969
+ )
970
+ return doc.text
971
+ return Text("Select an item to see its detail.", style="dim")
972
+
973
+
974
+ # ---------------------------------------------------------------------------- dispatch
975
+
976
+
977
+ def rows_for(view: str, data: FleetData, ui: UiState) -> list[Row]: # noqa: PLR0911
978
+ if view == "active_run":
979
+ return active_run_rows(data, ui)
980
+ if view == "active_runs":
981
+ return active_runs_rows(data, ui)
982
+ if view == "timeline":
983
+ return timeline_rows(data, ui)
984
+ if view == "worktrees":
985
+ return worktree_rows(data, ui)
986
+ if view == "gates":
987
+ return gate_rows(data, ui)
988
+ if view == "evidence":
989
+ return evidence_rows(data, ui)
990
+ return overview_rows(data, ui)
991
+
992
+
993
+ def detail_for( # noqa: PLR0911 - one return per view, clearer than a table
994
+ view: str,
995
+ data: FleetData,
996
+ ui: UiState,
997
+ key: str | None,
998
+ tools: Sequence[Event] | None = None,
999
+ ) -> Text:
1000
+ if key is None:
1001
+ return Text("Nothing selected.", style="dim")
1002
+ if view == "active_run":
1003
+ return active_run_detail(data, ui, key)
1004
+ if view == "active_runs" and key.startswith("run:"):
1005
+ run_id = key[4:]
1006
+ run = next(
1007
+ (r for r in data.run_snapshots if r.run_id == run_id),
1008
+ data.primary_run if data.primary_run and data.primary_run.run_id == run_id else None,
1009
+ )
1010
+ if run is None:
1011
+ return Text("Select a run.", style="dim")
1012
+ doc = _Doc(ui.theme)
1013
+ doc.title(f"Run {safe(run.run_id, 40)}", "bold")
1014
+ doc.line("Task", safe(run.task_slug, 60))
1015
+ doc.line("Confidence", CONFIDENCE_LABEL.get(run.confidence, run.confidence))
1016
+ doc.line("Groups", str(len(run.groups)))
1017
+ return doc.text
1018
+ if view == "timeline":
1019
+ found = next(
1020
+ (e for e in data.timeline.events if f"ev:{e.session_id}:{e.event_id}" == key), None
1021
+ )
1022
+ return event_detail(found, ui)
1023
+ if view == "worktrees":
1024
+ return worktree_detail(data, ui, key)
1025
+ if view == "gates":
1026
+ return gate_detail(data, ui, key)
1027
+ if view == "evidence":
1028
+ return evidence_detail(data, ui, key)
1029
+ return (
1030
+ agent_detail(data, ui, key, tools)
1031
+ if not key.startswith("wt:")
1032
+ else _wt_card_detail(data, ui, key)
1033
+ )
1034
+
1035
+
1036
+ def _wt_card_detail(data: FleetData, ui: UiState, key: str) -> Text:
1037
+ """Detail for a git-only worktree card in the overview (no telemetry)."""
1038
+ if data.git is None:
1039
+ return Text("No data.", style="dim")
1040
+ for index, wt in enumerate(data.git.worktrees):
1041
+ if key == f"wt:{wt.path}":
1042
+ text = worktree_detail(data, ui, f"wt:{index}:{wt.path}")
1043
+ text.append("\nNo hook telemetry: lane is UNKNOWN, never idle.\n", style="bold")
1044
+ for line in overview_guidance_lines(data):
1045
+ text.append(line + "\n", style="dim")
1046
+ return text
1047
+ return Text("Select an item.", style="dim")
1048
+
1049
+
1050
+ def recent_events_text(data: FleetData, ui: UiState, count: int = 12) -> Text:
1051
+ """Tile content: the newest events, no selection."""
1052
+ text = Text("RECENT EVENTS\n", style=ui.theme.accent)
1053
+ if not data.recent.events:
1054
+ text.append("none yet\n", style="dim")
1055
+ for event in data.recent.events[:count]:
1056
+ text.append_text(event_label(event, ui))
1057
+ text.append("\n")
1058
+ return text
1059
+
1060
+
1061
+ def side_summary_text(data: FleetData, ui: UiState) -> Text:
1062
+ """Tile content: gate state per worktree (no aggregate) and worktree facts."""
1063
+ text = Text("GATES + WORKTREES (display-only)\n", style=ui.theme.accent)
1064
+ for _wt, title, statuses in _gate_board(data)[:4]:
1065
+ text.append(f"{title}\n", style="bold")
1066
+ text.append(
1067
+ " " + " ".join(f"{s.label.split()[0]}:{s.state.upper()}" for s in statuses) + "\n"
1068
+ )
1069
+ return text
1070
+
1071
+
1072
+ def card_for_key(data: FleetData, key: str) -> Card | None:
1073
+ return next((c for c in data.cards if c.key == key), None)
1074
+
1075
+
1076
+ def first_selectable(rows: Sequence[Row]) -> str | None:
1077
+ return next((r.key for r in rows if not r.header), None)