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,293 @@
1
+ """The versioned ``status --json`` document (``cursorfleet.status/1``).
2
+
3
+ Combines the reducer's fleet view (hook telemetry) with the read-only git snapshot. Shape
4
+ is stable and documented in ``docs/status-json.md``; a snapshot-style test pins it. Where
5
+ no telemetry exists the fields say so explicitly (``"unknown"``, ``telemetry: "none"``)
6
+ instead of guessing: Cursor hooks expose no token budget, and an unlinked worktree has no
7
+ known owner or lane.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ from datetime import datetime
14
+ from typing import Any
15
+
16
+ from pydantic import BaseModel, ConfigDict
17
+
18
+ from cursorfleet.adapters.cursor.kit_probe import HooksKitState
19
+ from cursorfleet.git.collector import GitSnapshot, WorktreeSnapshot
20
+ from cursorfleet.state.models import (
21
+ BUDGET_UNKNOWN,
22
+ FleetView,
23
+ Lane,
24
+ SessionAcc,
25
+ SessionView,
26
+ TaskView,
27
+ utc,
28
+ )
29
+ from cursorfleet.state.run_model import RunSnapshot
30
+ from cursorfleet.state.spool_read import Corruption
31
+
32
+ STATUS_SCHEMA = "cursorfleet.status/1"
33
+ STATUS_SCHEMA_WITH_RUNS = "cursorfleet.status/0.2"
34
+
35
+
36
+ def json_abs_path(path: str | None) -> str | None:
37
+ """Absolute path with POSIX separators for the status JSON contract.
38
+
39
+ Windows ``os.path`` and ``git rev-parse`` emit backslashes; the document is
40
+ compared and consumed across OSes, so stored absolute paths use ``/``.
41
+ """
42
+ if path is None:
43
+ return None
44
+ return os.path.normpath(path).replace("\\", "/")
45
+
46
+
47
+ class _Doc(BaseModel):
48
+ model_config = ConfigDict(extra="forbid")
49
+
50
+
51
+ class RepoInfo(_Doc):
52
+ common_dir: str | None
53
+ runtime_dir: str | None
54
+
55
+
56
+ class TelemetryInfo(_Doc):
57
+ state: str # "hooks" when any hook event was indexed, else "none"
58
+ sessions: int
59
+ events: int
60
+ spool_files: int
61
+ stale_after_s: int
62
+ source: str # "projection" | "replay" | "none"
63
+ corruption: dict[str, int]
64
+ notes: list[str]
65
+
66
+
67
+ class Limits(_Doc):
68
+ """Constant honesty statements (docs/product-contract.md)."""
69
+
70
+ token_budget: str = BUDGET_UNKNOWN
71
+ cloud_agents: str = "not_visible"
72
+ enforcement: str = "none"
73
+
74
+
75
+ class GitInfo(_Doc):
76
+ available: bool
77
+ error: str | None
78
+
79
+
80
+ class Owner(_Doc):
81
+ session_id: str
82
+ agent_key: str
83
+ role: str | None
84
+ lane: str
85
+ attribution: str
86
+
87
+
88
+ class RunGroupDoc(_Doc):
89
+ role: str
90
+ summary_lane: str
91
+ confidence: str
92
+ instance_count: int
93
+ conflict: bool
94
+
95
+
96
+ class RunDoc(_Doc):
97
+ run_id: str
98
+ task_slug: str
99
+ confidence: str
100
+ session_ids: list[str]
101
+ groups: list[RunGroupDoc]
102
+
103
+
104
+ class WorktreeDoc(_Doc):
105
+ path: str
106
+ worktree_id: str | None
107
+ is_main: bool
108
+ exists: bool
109
+ branch: str | None
110
+ head: str | None
111
+ detached: bool
112
+ bare: bool
113
+ locked: bool
114
+ prunable: bool
115
+ dirty_count: int | None
116
+ upstream: bool
117
+ ahead: int | None
118
+ behind: int | None
119
+ last_commit_ts: datetime | None
120
+ last_event_ts: datetime | None
121
+ stale: bool
122
+ stale_reasons: list[str]
123
+ telemetry: str # "hooks" | "none"
124
+ lane: str # a lane value, or "unknown" when there is no telemetry
125
+ owners: list[Owner]
126
+ tasks: list[str]
127
+ error: str | None
128
+
129
+
130
+ class StatusDoc(_Doc):
131
+ schema_: str
132
+ generated_at: datetime
133
+ repo: RepoInfo
134
+ telemetry: TelemetryInfo
135
+ limits: Limits
136
+ git: GitInfo
137
+ sessions: list[SessionView]
138
+ tasks: list[TaskView]
139
+ worktrees: list[WorktreeDoc]
140
+ runs: list[RunDoc] = []
141
+
142
+ def to_json_dict(self) -> dict[str, Any]:
143
+ data: dict[str, Any] = self.model_dump(mode="json")
144
+ schema = data.pop("schema_")
145
+ runs = data.pop("runs", [])
146
+ ordered: dict[str, Any] = {"schema": schema}
147
+ ordered.update(data)
148
+ if runs:
149
+ ordered["runs"] = runs
150
+ return ordered
151
+
152
+
153
+ def worktree_activity(sessions: dict[str, SessionAcc]) -> dict[str, datetime]:
154
+ """Last hook activity per ``worktree_id`` (for the collector's ``inactive`` check)."""
155
+ activity: dict[str, datetime] = {}
156
+ for session in sessions.values():
157
+ stamp = session.last_live_ts
158
+ if stamp is None:
159
+ continue
160
+ for wt in session.worktree_ids:
161
+ if wt not in activity or stamp > activity[wt]:
162
+ activity[wt] = stamp
163
+ return activity
164
+
165
+
166
+ def _worktree_doc(snap: WorktreeSnapshot, views: list[SessionView]) -> WorktreeDoc:
167
+ linked = [v for v in views if snap.worktree_id and snap.worktree_id in v.worktree_ids]
168
+ owners: list[Owner] = []
169
+ tasks: set[str] = set()
170
+ for view in linked:
171
+ for agent in view.agents:
172
+ if agent.worktree_id == snap.worktree_id:
173
+ owners.append(
174
+ Owner(
175
+ session_id=view.session_id,
176
+ agent_key=agent.key,
177
+ role=agent.role,
178
+ lane=agent.lane,
179
+ attribution=agent.attribution,
180
+ )
181
+ )
182
+ tasks.update(agent.issue_refs)
183
+ latest = max(linked, key=lambda v: (v.last_ts, v.session_id), default=None)
184
+ return WorktreeDoc(
185
+ path=json_abs_path(snap.path) or snap.path,
186
+ worktree_id=snap.worktree_id,
187
+ is_main=snap.is_main,
188
+ exists=snap.exists,
189
+ branch=snap.branch,
190
+ head=snap.head,
191
+ detached=snap.detached,
192
+ bare=snap.bare,
193
+ locked=snap.locked,
194
+ prunable=snap.prunable,
195
+ dirty_count=snap.dirty_count,
196
+ upstream=snap.upstream,
197
+ ahead=snap.ahead,
198
+ behind=snap.behind,
199
+ last_commit_ts=snap.last_commit_ts,
200
+ last_event_ts=latest.last_ts if latest else None,
201
+ stale=snap.stale,
202
+ stale_reasons=list(snap.stale_reasons),
203
+ telemetry="hooks" if linked else "none",
204
+ lane=latest.lane if latest else Lane.UNKNOWN.value,
205
+ owners=sorted(owners, key=lambda o: (o.session_id, o.agent_key)),
206
+ tasks=sorted(tasks),
207
+ error=snap.error,
208
+ )
209
+
210
+
211
+ def run_docs_from_snapshots(snapshots: list[RunSnapshot]) -> list[RunDoc]:
212
+ out: list[RunDoc] = []
213
+ for snap in snapshots:
214
+ groups = [
215
+ RunGroupDoc(
216
+ role=g.role,
217
+ summary_lane=g.summary_lane,
218
+ confidence=g.confidence,
219
+ instance_count=len(g.instances),
220
+ conflict=g.conflict,
221
+ )
222
+ for g in snap.groups
223
+ ]
224
+ session_ids = sorted({inst.session_id for g in snap.groups for inst in g.instances})
225
+ out.append(
226
+ RunDoc(
227
+ run_id=snap.run_id,
228
+ task_slug=snap.task_slug,
229
+ confidence=snap.confidence,
230
+ session_ids=session_ids,
231
+ groups=groups,
232
+ )
233
+ )
234
+ return out
235
+
236
+
237
+ def build_status( # noqa: PLR0913
238
+ *,
239
+ now: datetime,
240
+ fleet: FleetView,
241
+ git: GitSnapshot | None,
242
+ common_dir: str | None,
243
+ runtime_dir: str | None,
244
+ corruption: Corruption,
245
+ spool_files: int,
246
+ source: str,
247
+ hooks_kit: HooksKitState | None = None,
248
+ runs: list[RunDoc] | None = None,
249
+ ) -> StatusDoc:
250
+ """Assemble the document. Pure given its inputs (``now`` is injected)."""
251
+ events = sum(s.event_count for s in fleet.sessions)
252
+ notes: list[str] = []
253
+ if not fleet.sessions:
254
+ kit = hooks_kit if hooks_kit is not None else HooksKitState.MISSING
255
+ if kit == HooksKitState.INSTALLED:
256
+ notes.append(
257
+ "hooks installed; open this repo in Cursor and start a session to record "
258
+ "telemetry (git-only view until the first hook run)"
259
+ )
260
+ else:
261
+ notes.append("no hook telemetry indexed: git-only view; agent lanes are unknown")
262
+ if corruption.total:
263
+ notes.append(
264
+ f"{corruption.total} corrupt or skipped spool line(s); see telemetry.corruption"
265
+ )
266
+ if git is None:
267
+ notes.append("git collection was disabled")
268
+ elif not git.available:
269
+ notes.append(f"git unavailable: {git.error}")
270
+ worktrees = [_worktree_doc(w, fleet.sessions) for w in git.worktrees] if git is not None else []
271
+ run_docs = runs or []
272
+ schema = STATUS_SCHEMA_WITH_RUNS if run_docs else STATUS_SCHEMA
273
+ return StatusDoc(
274
+ schema_=schema,
275
+ generated_at=utc(now),
276
+ repo=RepoInfo(common_dir=json_abs_path(common_dir), runtime_dir=json_abs_path(runtime_dir)),
277
+ telemetry=TelemetryInfo(
278
+ state="hooks" if fleet.sessions else "none",
279
+ sessions=len(fleet.sessions),
280
+ events=events,
281
+ spool_files=spool_files,
282
+ stale_after_s=fleet.stale_after_s,
283
+ source=source,
284
+ corruption=corruption.to_dict(),
285
+ notes=notes,
286
+ ),
287
+ limits=Limits(),
288
+ git=GitInfo(available=bool(git and git.available), error=git.error if git else "disabled"),
289
+ sessions=fleet.sessions,
290
+ tasks=list(fleet.tasks),
291
+ worktrees=worktrees,
292
+ runs=run_docs,
293
+ )
File without changes