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.
- cursorfleet/__init__.py +9 -0
- cursorfleet/_templates/cursor/agents/architect.md +18 -0
- cursorfleet/_templates/cursor/agents/implementer.md +20 -0
- cursorfleet/_templates/cursor/agents/patcher.md +17 -0
- cursorfleet/_templates/cursor/agents/qa.md +21 -0
- cursorfleet/_templates/cursor/agents/reviewer.md +20 -0
- cursorfleet/_templates/cursor/agents/scout.md +17 -0
- cursorfleet/_templates/cursor/agents/tester.md +20 -0
- cursorfleet/_templates/cursor/agents-md/root.md +7 -0
- cursorfleet/_templates/cursor/agents-md/work.md +7 -0
- cursorfleet/_templates/cursor/config/config.toml +47 -0
- cursorfleet/_templates/cursor/hooks/entry.json +4 -0
- cursorfleet/_templates/cursor/rules/cursorfleet-core.mdc +10 -0
- cursorfleet/_templates/cursor/rules/cursorfleet-handoff.mdc +15 -0
- cursorfleet/_templates/cursor/skills/cursorfleet-artifacts/SKILL.md +57 -0
- cursorfleet/_templates/cursor/skills/cursorfleet-coordinator/SKILL.md +35 -0
- cursorfleet/adapters/__init__.py +0 -0
- cursorfleet/adapters/cursor/__init__.py +0 -0
- cursorfleet/adapters/cursor/blocks.py +104 -0
- cursorfleet/adapters/cursor/diagnostics.py +547 -0
- cursorfleet/adapters/cursor/fsutil.py +156 -0
- cursorfleet/adapters/cursor/hook_main.py +428 -0
- cursorfleet/adapters/cursor/hook_normalize.py +527 -0
- cursorfleet/adapters/cursor/hook_policy.py +76 -0
- cursorfleet/adapters/cursor/hook_sanitize.py +474 -0
- cursorfleet/adapters/cursor/hooksjson.py +305 -0
- cursorfleet/adapters/cursor/installer.py +626 -0
- cursorfleet/adapters/cursor/kit.py +210 -0
- cursorfleet/adapters/cursor/kit_probe.py +54 -0
- cursorfleet/adapters/cursor/lock.py +145 -0
- cursorfleet/adapters/cursor/templates.py +60 -0
- cursorfleet/adapters/cursor/validation.py +365 -0
- cursorfleet/adapters/cursor/workspace.py +80 -0
- cursorfleet/cli/__init__.py +0 -0
- cursorfleet/cli/commands/__init__.py +1 -0
- cursorfleet/cli/commands/_common.py +82 -0
- cursorfleet/cli/commands/_resolution.py +56 -0
- cursorfleet/cli/commands/doctor.py +81 -0
- cursorfleet/cli/commands/events.py +197 -0
- cursorfleet/cli/commands/index.py +55 -0
- cursorfleet/cli/commands/init.py +71 -0
- cursorfleet/cli/commands/migrate.py +43 -0
- cursorfleet/cli/commands/replay.py +76 -0
- cursorfleet/cli/commands/run.py +97 -0
- cursorfleet/cli/commands/status.py +210 -0
- cursorfleet/cli/commands/tui.py +47 -0
- cursorfleet/cli/commands/uninstall.py +73 -0
- cursorfleet/cli/commands/validate.py +69 -0
- cursorfleet/cli/main.py +81 -0
- cursorfleet/config/__init__.py +44 -0
- cursorfleet/config/io.py +27 -0
- cursorfleet/config/models.py +154 -0
- cursorfleet/config/roster.py +245 -0
- cursorfleet/events/__init__.py +39 -0
- cursorfleet/events/forbidden.py +45 -0
- cursorfleet/events/ids.py +109 -0
- cursorfleet/events/kinds.py +132 -0
- cursorfleet/events/models.py +213 -0
- cursorfleet/events/pathcheck.py +45 -0
- cursorfleet/git/__init__.py +0 -0
- cursorfleet/git/collector.py +226 -0
- cursorfleet/git/runner.py +135 -0
- cursorfleet/git/worktrees.py +71 -0
- cursorfleet/policy/__init__.py +0 -0
- cursorfleet/py.typed +0 -0
- cursorfleet/safeexe.py +70 -0
- cursorfleet/schema_gen.py +73 -0
- cursorfleet/state/__init__.py +0 -0
- cursorfleet/state/artifact_scan.py +309 -0
- cursorfleet/state/context.py +38 -0
- cursorfleet/state/event_store.py +227 -0
- cursorfleet/state/indexer.py +443 -0
- cursorfleet/state/lock.py +74 -0
- cursorfleet/state/models.py +268 -0
- cursorfleet/state/reducer.py +588 -0
- cursorfleet/state/retention.py +218 -0
- cursorfleet/state/run_builder.py +136 -0
- cursorfleet/state/run_model.py +43 -0
- cursorfleet/state/run_store.py +95 -0
- cursorfleet/state/runtime.py +645 -0
- cursorfleet/state/spool.py +186 -0
- cursorfleet/state/spool_read.py +242 -0
- cursorfleet/state/status_doc.py +293 -0
- cursorfleet/tui/__init__.py +0 -0
- cursorfleet/tui/app.py +581 -0
- cursorfleet/tui/data.py +545 -0
- cursorfleet/tui/filters.py +82 -0
- cursorfleet/tui/gates.py +255 -0
- cursorfleet/tui/help.py +101 -0
- cursorfleet/tui/layout.py +26 -0
- cursorfleet/tui/theme.py +176 -0
- cursorfleet/tui/views.py +1077 -0
- cursorfleet/tui/watch.py +33 -0
- cursorfleet/workflow/__init__.py +0 -0
- cursorfleet/workflow/artifacts.py +148 -0
- cursorfleet/workflow/frontmatter.py +232 -0
- cursorfleet-0.1.0.dist-info/METADATA +161 -0
- cursorfleet-0.1.0.dist-info/RECORD +101 -0
- cursorfleet-0.1.0.dist-info/WHEEL +4 -0
- cursorfleet-0.1.0.dist-info/entry_points.txt +3 -0
- 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
|