taskops-cli 0.2.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 (193) hide show
  1. taskops/__init__.py +39 -0
  2. taskops/_clock.py +32 -0
  3. taskops/_errors.py +160 -0
  4. taskops/_ids.py +63 -0
  5. taskops/_types.py +106 -0
  6. taskops/_version.py +7 -0
  7. taskops/assets/GUIDE.md +220 -0
  8. taskops/contracts/__init__.py +96 -0
  9. taskops/contracts/_fields.py +113 -0
  10. taskops/contracts/actor.py +30 -0
  11. taskops/contracts/board.py +142 -0
  12. taskops/contracts/commit.py +36 -0
  13. taskops/contracts/day.py +150 -0
  14. taskops/contracts/dep.py +23 -0
  15. taskops/contracts/event.py +58 -0
  16. taskops/contracts/gitstate.py +45 -0
  17. taskops/contracts/index.py +37 -0
  18. taskops/contracts/lease.py +43 -0
  19. taskops/contracts/log.py +59 -0
  20. taskops/contracts/remote.py +48 -0
  21. taskops/contracts/results.py +84 -0
  22. taskops/contracts/task.py +94 -0
  23. taskops/contracts/tools.py +110 -0
  24. taskops/contracts/wire.py +60 -0
  25. taskops/engine/__init__.py +34 -0
  26. taskops/engine/_blocks.py +48 -0
  27. taskops/engine/_briefs.py +92 -0
  28. taskops/engine/_chunks.py +66 -0
  29. taskops/engine/_closed.py +65 -0
  30. taskops/engine/_entries.py +126 -0
  31. taskops/engine/_events.py +55 -0
  32. taskops/engine/_opened.py +51 -0
  33. taskops/engine/_process.py +80 -0
  34. taskops/engine/_prompts.py +98 -0
  35. taskops/engine/_stream.py +129 -0
  36. taskops/engine/activity.py +94 -0
  37. taskops/engine/bus.py +42 -0
  38. taskops/engine/commitline.py +110 -0
  39. taskops/engine/day.py +142 -0
  40. taskops/engine/diffstat.py +63 -0
  41. taskops/engine/gitio.py +108 -0
  42. taskops/engine/gitstate.py +96 -0
  43. taskops/engine/history.py +76 -0
  44. taskops/engine/identity.py +84 -0
  45. taskops/engine/log.py +68 -0
  46. taskops/engine/machine.py +160 -0
  47. taskops/engine/narrate.py +91 -0
  48. taskops/engine/project.py +57 -0
  49. taskops/engine/replay.py +142 -0
  50. taskops/engine/reports.py +67 -0
  51. taskops/engine/scheduler.py +135 -0
  52. taskops/engine/transcript.py +127 -0
  53. taskops/engine/wire.py +92 -0
  54. taskops/engine/worker.py +115 -0
  55. taskops/py.typed +0 -0
  56. taskops/render/__init__.py +40 -0
  57. taskops/render/_closed_days.py +64 -0
  58. taskops/render/_dossier.py +68 -0
  59. taskops/render/_opened.py +64 -0
  60. taskops/render/_sections.py +51 -0
  61. taskops/render/_tasklist.py +72 -0
  62. taskops/render/_text.py +74 -0
  63. taskops/render/_verbatim.py +65 -0
  64. taskops/render/ansi.py +92 -0
  65. taskops/render/board.py +55 -0
  66. taskops/render/day.py +102 -0
  67. taskops/render/dispatch.py +104 -0
  68. taskops/render/inbox.py +27 -0
  69. taskops/render/log.py +47 -0
  70. taskops/render/recover.py +64 -0
  71. taskops/render/report.py +51 -0
  72. taskops/render/reports.py +57 -0
  73. taskops/render/results.py +96 -0
  74. taskops/render/session.py +75 -0
  75. taskops/render/task.py +88 -0
  76. taskops/render/tasklist.py +65 -0
  77. taskops/storage/__init__.py +36 -0
  78. taskops/storage/_ddl.py +78 -0
  79. taskops/storage/_delivered.py +51 -0
  80. taskops/storage/_deps.py +69 -0
  81. taskops/storage/_events.py +127 -0
  82. taskops/storage/_leases.py +108 -0
  83. taskops/storage/_rows.py +90 -0
  84. taskops/storage/_tasks.py +124 -0
  85. taskops/storage/locate.py +76 -0
  86. taskops/storage/schema.py +66 -0
  87. taskops/storage/store.py +120 -0
  88. taskops/storage/sync.py +139 -0
  89. taskops/transports/__init__.py +6 -0
  90. taskops/transports/cli/__init__.py +0 -0
  91. taskops/transports/cli/commands/__init__.py +3 -0
  92. taskops/transports/cli/commands/_digest.py +64 -0
  93. taskops/transports/cli/commands/_serve_init.py +65 -0
  94. taskops/transports/cli/commands/_shared.py +50 -0
  95. taskops/transports/cli/commands/_tasks_args.py +112 -0
  96. taskops/transports/cli/commands/_window.py +45 -0
  97. taskops/transports/cli/commands/ask.py +27 -0
  98. taskops/transports/cli/commands/dispatch.py +43 -0
  99. taskops/transports/cli/commands/init.py +48 -0
  100. taskops/transports/cli/commands/log.py +22 -0
  101. taskops/transports/cli/commands/plan.py +43 -0
  102. taskops/transports/cli/commands/pushpull.py +53 -0
  103. taskops/transports/cli/commands/recover.py +30 -0
  104. taskops/transports/cli/commands/remote.py +56 -0
  105. taskops/transports/cli/commands/report.py +82 -0
  106. taskops/transports/cli/commands/run_.py +60 -0
  107. taskops/transports/cli/commands/serve.py +74 -0
  108. taskops/transports/cli/commands/sync.py +22 -0
  109. taskops/transports/cli/commands/tasks.py +79 -0
  110. taskops/transports/cli/commands/ui.py +72 -0
  111. taskops/transports/cli/commands/update.py +25 -0
  112. taskops/transports/cli/main.py +85 -0
  113. taskops/transports/hooks/__init__.py +15 -0
  114. taskops/transports/hooks/__main__.py +63 -0
  115. taskops/transports/hooks/_args.py +28 -0
  116. taskops/transports/hooks/claude.py +78 -0
  117. taskops/transports/hooks/commit.py +61 -0
  118. taskops/transports/hooks/events.py +107 -0
  119. taskops/transports/hooks/record.py +55 -0
  120. taskops/transports/http/__init__.py +21 -0
  121. taskops/transports/http/_handler.py +132 -0
  122. taskops/transports/http/_wire.py +79 -0
  123. taskops/transports/http/_wsframes.py +116 -0
  124. taskops/transports/http/agentapi.py +69 -0
  125. taskops/transports/http/api.py +122 -0
  126. taskops/transports/http/exchange.py +80 -0
  127. taskops/transports/http/live.py +160 -0
  128. taskops/transports/http/policy.py +110 -0
  129. taskops/transports/http/projects.py +116 -0
  130. taskops/transports/http/reports.py +75 -0
  131. taskops/transports/http/router.py +90 -0
  132. taskops/transports/http/server.py +50 -0
  133. taskops/transports/http/static.py +86 -0
  134. taskops/transports/http/ui/app.js +59 -0
  135. taskops/transports/http/ui/index.html +23 -0
  136. taskops/transports/http/ui/style.css +1 -0
  137. taskops/transports/http/websocket.py +60 -0
  138. taskops/transports/mcp/__init__.py +19 -0
  139. taskops/transports/mcp/__main__.py +7 -0
  140. taskops/transports/mcp/_descriptions.py +102 -0
  141. taskops/transports/mcp/_reads.py +82 -0
  142. taskops/transports/mcp/_writes.py +72 -0
  143. taskops/transports/mcp/answers.py +44 -0
  144. taskops/transports/mcp/arguments.py +104 -0
  145. taskops/transports/mcp/dispatch.py +47 -0
  146. taskops/transports/mcp/protocol.py +83 -0
  147. taskops/transports/mcp/schema.py +50 -0
  148. taskops/transports/mcp/server.py +49 -0
  149. taskops/transports/mcp/tools.py +66 -0
  150. taskops/usecases/__init__.py +56 -0
  151. taskops/usecases/_entry.py +84 -0
  152. taskops/usecases/_facts.py +49 -0
  153. taskops/usecases/_freeing.py +93 -0
  154. taskops/usecases/_gitignore.py +94 -0
  155. taskops/usecases/_mirroring.py +57 -0
  156. taskops/usecases/_narrating.py +77 -0
  157. taskops/usecases/_project.py +67 -0
  158. taskops/usecases/_range.py +102 -0
  159. taskops/usecases/_reasons.py +54 -0
  160. taskops/usecases/_remotefile.py +62 -0
  161. taskops/usecases/_reportsync.py +108 -0
  162. taskops/usecases/_routing.py +91 -0
  163. taskops/usecases/_wireclient.py +141 -0
  164. taskops/usecases/ask.py +41 -0
  165. taskops/usecases/claim.py +104 -0
  166. taskops/usecases/dispatch.py +160 -0
  167. taskops/usecases/dossier.py +155 -0
  168. taskops/usecases/edit.py +71 -0
  169. taskops/usecases/exchange.py +86 -0
  170. taskops/usecases/feed.py +138 -0
  171. taskops/usecases/guard.py +122 -0
  172. taskops/usecases/hooks.py +127 -0
  173. taskops/usecases/index.py +62 -0
  174. taskops/usecases/ingest.py +60 -0
  175. taskops/usecases/log.py +134 -0
  176. taskops/usecases/narration.py +91 -0
  177. taskops/usecases/plan.py +128 -0
  178. taskops/usecases/pushpull.py +119 -0
  179. taskops/usecases/recover.py +84 -0
  180. taskops/usecases/remote.py +78 -0
  181. taskops/usecases/report.py +87 -0
  182. taskops/usecases/reportfile.py +106 -0
  183. taskops/usecases/session.py +134 -0
  184. taskops/usecases/setup.py +92 -0
  185. taskops/usecases/sync.py +78 -0
  186. taskops/usecases/update.py +123 -0
  187. taskops/usecases/view.py +112 -0
  188. taskops_cli-0.2.0.dist-info/METADATA +291 -0
  189. taskops_cli-0.2.0.dist-info/RECORD +193 -0
  190. taskops_cli-0.2.0.dist-info/WHEEL +5 -0
  191. taskops_cli-0.2.0.dist-info/entry_points.txt +2 -0
  192. taskops_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
  193. taskops_cli-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,96 @@
1
+ """Layer 1 — every payload that crosses a boundary, defined once.
2
+
3
+ ZERO logic lives here: only types. `storage` reads and writes them, `engine`
4
+ decides with them, `render` turns them into text, the three transports serialize
5
+ them, and the studio mirrors them in TypeScript. One definition, six readers.
6
+
7
+ TypedDict on purpose, not dataclasses and not pydantic: at runtime these ARE
8
+ dicts, so a consumer writes `task["status"]`, the wire format is the same object
9
+ serialized, and the package keeps its zero dependencies.
10
+
11
+ ⚠️ Optionality uses the `total=False` SPLIT, never `NotRequired`. Under
12
+ `from __future__ import annotations` every annotation is a string, so TypedDict
13
+ cannot see a `NotRequired[...]` marker at class-creation time: `__optional_keys__`
14
+ comes back EMPTY and every field reads as required to anything that introspects
15
+ the class — including the MCP schema generator, which would then advertise every
16
+ optional parameter as mandatory. Totality is class-level, so the split is immune.
17
+ `tests/contracts/test_optionality.py` pins this.
18
+
19
+ This package imports nothing but layer 0, so it can never introduce a cycle —
20
+ which is what lets every layer above depend on it freely.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from .actor import Actor
26
+ from .board import (
27
+ Activity,
28
+ ActorRoll,
29
+ Board,
30
+ Burndown,
31
+ Card,
32
+ Column,
33
+ Fleet,
34
+ FleetMember,
35
+ Standup,
36
+ )
37
+ from .commit import CommitRef
38
+ from .day import ClosedCard, CommitStat, DayReport, OpenedCard, PeriodReport, ReportFile
39
+ from .dep import Dep
40
+ from .event import Event, Inbox
41
+ from .gitstate import BranchState
42
+ from .index import ReportEntry
43
+ from .lease import Lease
44
+ from .log import EntryKind, LogEntry, SessionLog
45
+ from .remote import Remote
46
+ from .results import Claim, EditResult, NextResult, PlanResult, UpdateResult
47
+ from .task import Task, TaskView
48
+ from .tools import AskParams, NextParams, PlanParams, ReportParams, UpdateParams
49
+ from .wire import WireMessage
50
+
51
+ __all__ = [
52
+ # the entities
53
+ "Task",
54
+ "TaskView",
55
+ "Dep",
56
+ "Lease",
57
+ "Actor",
58
+ "Event",
59
+ "WireMessage",
60
+ "Inbox",
61
+ "BranchState",
62
+ "CommitRef",
63
+ "LogEntry",
64
+ "SessionLog",
65
+ "EntryKind",
66
+ "Remote",
67
+ # what the use cases return
68
+ "PlanResult",
69
+ "Claim",
70
+ "NextResult",
71
+ "UpdateResult",
72
+ "EditResult",
73
+ # the projections
74
+ "Board",
75
+ "Column",
76
+ "Card",
77
+ "Standup",
78
+ "Activity",
79
+ "ActorRoll",
80
+ "Burndown",
81
+ "DayReport",
82
+ "PeriodReport",
83
+ "ClosedCard",
84
+ "OpenedCard",
85
+ "CommitStat",
86
+ "ReportFile",
87
+ "ReportEntry",
88
+ "Fleet",
89
+ "FleetMember",
90
+ # what a tool call carries in
91
+ "PlanParams",
92
+ "NextParams",
93
+ "UpdateParams",
94
+ "AskParams",
95
+ "ReportParams",
96
+ ]
@@ -0,0 +1,113 @@
1
+ """The field descriptions, and the aliases shared across tool contracts.
2
+
3
+ Prose in a data structure, so it is split out for the same reason `transports/mcp/_descriptions` is:
4
+ reviewing a change to which fields a tool takes should not mean re-reading sixty lines of unchanged
5
+ text. The strings themselves are the contract — they are the only thing a calling agent reads before
6
+ it chooses arguments.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Annotated
12
+
13
+ __all__ = ["Repo", "Actor", "Session", "TaskId", "TASKS", "DRY_RUN", "STATUS", "COMMENT",
14
+ "MENTIONS", "BLOCKED_ON", "NO_CODE", "LABELS", "CLAIM_ONE", "ASK_TASK",
15
+ "ASK_QUERY", "DISPATCH_TASKS", "DISPATCH_COUNT", "PREFIX", "MODEL",
16
+ "RECOVER_FORCE", "RECOVER_GRACE", "REPORT_KIND", "REPORT_ACTOR", "SINCE", "DATE"]
17
+
18
+ Repo = Annotated[str, "path to the repository root; a path INSIDE it also works — "
19
+ "the root is found from .taskops"]
20
+
21
+ Actor = Annotated[str, "who is calling: `agent:<dev>/<name>` or `dev:<name>`. Omit "
22
+ "and it resolves from $TASKOPS_ACTOR or the git identity"]
23
+
24
+ Session = Annotated[str, "your Claude Code session id. It links this work to your "
25
+ "transcript on the live board"]
26
+
27
+ TaskId = Annotated[str, "a task id, e.g. tk-4f2a9c"]
28
+
29
+
30
+
31
+ TASKS = ("the tasks to create: a list of {title, spec, priority?, labels?, files?, parent?, "
32
+ "after?, blocks?, assignee?}. `spec` is the brief a FRESH agent reads to do the work "
33
+ "with no other context — what done looks like, what must not change, where to start. "
34
+ "`after` lists what must finish BEFORE this card, each an existing id or the 0-based "
35
+ "INDEX of an earlier entry in this same list. `blocks` is the inverse: existing cards "
36
+ "that must wait for this one — that is how an agent mid-task creates the prerequisite it "
37
+ "just discovered and makes its own task wait, in one call. `files` is the edit surface: "
38
+ "name it and no two agents get the same file")
39
+
40
+ STATUS = ("the new status. `released` returns the task to the queue with your progress in "
41
+ "`comment` — the honest move when out of context. `done` needs a commit bound to "
42
+ "this task")
43
+
44
+ COMMENT = ("what happened, for the task's thread. Write the decision and the surprise; the next "
45
+ "agent and the human reviewing at 9am read this and nothing else")
46
+
47
+ MENTIONS = ("comma-separated actor ids to notify, e.g. 'agent:ana/api-1,dev:ana'. It reaches "
48
+ "their inbox and the live board — this is how you raise a shared file with another "
49
+ "developer's agent")
50
+
51
+ BLOCKED_ON = ("a task id that must finish first. Adds the dependency AND sets you blocked, so a "
52
+ "discovery lands in the graph instead of a comment")
53
+
54
+ NO_CODE = ("declare that this task produces no commit (research, a decision, docs elsewhere). "
55
+ "Required to close one, and recorded with your comment as the justification")
56
+
57
+ LABELS = "comma-separated labels to restrict the pick to"
58
+
59
+ CLAIM_ONE = ("claim THIS task rather than letting the scheduler choose. Use it when a human named "
60
+ "one — otherwise the scheduler also avoids files another live agent is in")
61
+
62
+ ASK_TASK = ("the task to read in full: spec, conversation, commits, what blocks it, what it "
63
+ "blocks, and which other tasks touch the same files")
64
+
65
+ ASK_QUERY = "free text over titles, specs and comments, when you have no id. Search once, then ask by id"
66
+
67
+ DISPATCH_TASKS = ("comma-separated task ids to dispatch, one worker each. Omit to let the "
68
+ "scheduler pick the best `count` ready cards")
69
+
70
+ DISPATCH_COUNT = ("how many workers to launch when you did not name tasks (default 3, ceiling "
71
+ "12). Every worker is a real Claude Code process, so ask for what you need "
72
+ "rather than for everything")
73
+
74
+ PREFIX = ("names the workers `agent:<you>/<prefix>1..n` (default 'w'), so a fleet view reads as "
75
+ "api1, api2 instead of three hashes")
76
+
77
+ MODEL = ("model for the workers, e.g. claude-sonnet-5. Omit for their default — a cheap model is "
78
+ "often right for mechanical cards")
79
+
80
+ REPORT_KIND = ("board (default) every column; standup what changed in a window, per actor; "
81
+ "day the full dossier of ONE calendar day — every card closed with its "
82
+ "commits and their diff sizes, what is still in flight, and the conversation; "
83
+ "range the same dossier over MANY days, grouped by day (pass `last`, or "
84
+ "`from`/`to`, and omit both for the WHOLE project)")
85
+
86
+ REPORT_ACTOR = "restrict a standup to one actor"
87
+
88
+ SINCE = "how far back a standup looks: '24h', '7d', '30m'"
89
+
90
+ LAST = ("with kind=range: how far back from `to`, e.g. '7d', '2w', '1m'. Inclusive of both "
91
+ "ends — '7d' is seven days of work. Leave `last`, `from` and `to` all empty and a "
92
+ "range covers the project from its first event, which is what to ask for when "
93
+ "somebody wants everything evaluated and not just one day")
94
+
95
+ FROM_DATE = ("with kind=range: the first day, 'YYYY-MM-DD'. Use it instead of `last` when the "
96
+ "window is a specific span rather than a distance back from today")
97
+
98
+ TO_DATE = ("with kind=range: the last day, INCLUSIVE, 'YYYY-MM-DD'. Defaults to today")
99
+
100
+ DATE = ("which day a `day` report covers: 'today' (default), 'yesterday', or a date like "
101
+ "2026-07-28. It is a CALENDAR day in local time, not a rolling window — ask for the "
102
+ "same date tomorrow and you get the same report")
103
+
104
+ DRY_RUN = ("show which cards WOULD get a worker and stop — nothing assigned, nothing launched. "
105
+ "Worth doing first when you are about to spend several models on a plan you just wrote")
106
+
107
+ RECOVER_FORCE = ("release cards even from workers that are STILL reporting. For a fleet that is "
108
+ "alive and WRONG — chasing a bad spec, say. Without it, only workers that have "
109
+ "gone silent are touched")
110
+
111
+ RECOVER_GRACE = ("seconds of silence before a worker counts as gone. Defaults to the same grace "
112
+ "the fleet view uses to print SILENT, so what you see on the board is exactly "
113
+ "what this acts on")
@@ -0,0 +1,30 @@
1
+ """Who did it — the identity that outlives a session.
2
+
3
+ The "50 First Dates" problem in one type: a Claude Code session is anonymous and
4
+ gone tomorrow, so anything it writes has to be attributed to something that is
5
+ neither. An actor id is `dev:<name>` or `agent:<dev>/<name>`, and the second half
6
+ of the agent form is what makes a hundred agents belonging to four developers
7
+ legible on one board — every agent answers to a human by construction.
8
+
9
+ Format handling lives in `engine.identity`, not here: this layer is types only.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import TypedDict
15
+
16
+ from .._types import ActorKind
17
+
18
+ __all__ = ["Actor"]
19
+
20
+
21
+ class Actor(TypedDict):
22
+ """A developer or one of their agents."""
23
+
24
+ id: str
25
+ """`dev:berna` or `agent:berna/polecat-1`. The wire form, and what the event
26
+ log stores — a nested object would make every event three lines of JSON."""
27
+
28
+ kind: ActorKind
29
+ dev: str
30
+ """The human accountable for this actor. For a dev, itself."""
@@ -0,0 +1,142 @@
1
+ """The projections — what a human or an agent gets to LOOK at.
2
+
3
+ Every one of these is derived from tasks plus the event log, never stored. That is
4
+ the property that makes them safe to change: a new column or a different standup
5
+ window is a rendering decision, not a migration.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TypedDict
11
+
12
+ from .._types import Status
13
+ from .event import Event
14
+ from .gitstate import BranchState
15
+ from .lease import Lease
16
+ from .task import Task
17
+
18
+ __all__ = ["Card", "Column", "Board", "Standup", "Burndown", "FleetMember", "Fleet",
19
+ "ActorRoll", "Activity"]
20
+
21
+
22
+ class Card(TypedDict):
23
+ """One task as the board shows it: the row, plus who is on it right now."""
24
+
25
+ task: Task
26
+ lease: Lease | None
27
+ blocked_by: int
28
+ blocks: int
29
+ commits: int
30
+
31
+
32
+ class Column(TypedDict):
33
+ status: Status
34
+ cards: list[Card]
35
+
36
+
37
+ class Board(TypedDict):
38
+ """The whole project, column by column, in declaration order of `STATUSES`."""
39
+
40
+ repo: str
41
+ columns: list[Column]
42
+ ready: int
43
+ """How many tasks could be picked up this second. The one number that says
44
+ whether adding another agent would help or just add contention."""
45
+
46
+ total: int
47
+
48
+
49
+ class Standup(TypedDict):
50
+ """What happened in a window, per actor — generated, never written by hand."""
51
+
52
+ repo: str
53
+ since: float
54
+ actors: list[str]
55
+ events: list[Event]
56
+ done: list[Task]
57
+ in_flight: list[Task]
58
+ blocked: list[Task]
59
+
60
+
61
+ class ActorRoll(TypedDict):
62
+ """One actor's whole record in the window — what they touched, not whether they are free.
63
+
64
+ Availability is not a question worth answering when agents are created on demand: there is no
65
+ pool to allocate from. What survives an agent's session is what it DID, and that is this.
66
+ """
67
+
68
+ actor: str
69
+ tasks: int
70
+ """Distinct tasks touched. Deliberately not "events": an actor that commented forty times on
71
+ one card has done less than one that closed four."""
72
+
73
+ commits: int
74
+ comments: int
75
+ done: int
76
+ first_seen: float
77
+ last_seen: float
78
+
79
+
80
+ class Activity(TypedDict):
81
+ """The event log as something a person can read: a timeline, plus who did what.
82
+
83
+ A projection like every other one here — nothing is stored for it. The log already holds every
84
+ fact it shows, which is why this could be added without a migration and why it cannot drift.
85
+ """
86
+
87
+ repo: str
88
+ since: float
89
+ events: list[Event]
90
+ """Newest FIRST, unlike the log's own order. A timeline is read from the top, and a reader who
91
+ has to scroll to the bottom to find out what just happened will stop opening it."""
92
+
93
+ titles: dict[str, str]
94
+ """Task id -> title, for the tasks these events name. Sent with the timeline rather than fetched
95
+ per row: a hundred events would otherwise be a hundred requests to render one screen."""
96
+
97
+ actors: list[ActorRoll]
98
+ kinds: list[str]
99
+ """The kinds actually present, so the filter offers what exists instead of a hardcoded list that
100
+ goes stale the day a new kind is written."""
101
+
102
+ truncated: bool
103
+
104
+
105
+ class Burndown(TypedDict):
106
+ """Open versus closed over time. Deliberately coarse: a day-level series is
107
+ what a burndown IS, and anything finer is a live board's job."""
108
+
109
+ repo: str
110
+ days: list[str]
111
+ open_counts: list[int]
112
+ done_counts: list[int]
113
+
114
+
115
+ class FleetMember(TypedDict):
116
+ """One live session, as the fleet view sees it."""
117
+
118
+ actor: str
119
+ session: str
120
+ task: str
121
+ branch: str
122
+ alive: bool
123
+ """False once the lease has gone quiet past its grace — the lease is still on
124
+ the books, so the board must be able to show a claim it no longer believes."""
125
+
126
+ last_seen: float
127
+ doing: str
128
+ """The most recent `activity` event's summary — what tool touched what file.
129
+ Empty when the agent has not reported any, which a session without the plugin
130
+ installed never will."""
131
+
132
+ git: BranchState
133
+ """Whether this agent's work is reachable by anybody else yet.
134
+
135
+ The question a standup actually asks. An agent can be busy, alive, and holding three unpushed
136
+ commits that exist on one laptop — which looks identical to progress on a board that only shows
137
+ activity."""
138
+
139
+
140
+ class Fleet(TypedDict):
141
+ repo: str
142
+ members: list[FleetMember]
@@ -0,0 +1,36 @@
1
+ """A commit, as the card knows it.
2
+
3
+ `TaskView.commits` used to be `list[str]` — bare shas — while the `commit` event underneath already
4
+ carried the subject and the files it touched. So the data was recorded, stored, replicated, and then
5
+ thrown away one layer before anybody could read it: a finished card showed `4c13dbd45823` and nothing
6
+ else, and the only substance on the page was whatever the agent happened to write in a comment.
7
+
8
+ Nothing here is new information. It is the event body, given a shape so that a renderer and the studio
9
+ can use what was always there.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import TypedDict
15
+
16
+ __all__ = ["CommitRef"]
17
+
18
+
19
+ class CommitRef(TypedDict):
20
+ """One commit bound to a task."""
21
+
22
+ sha: str
23
+ subject: str
24
+ """The commit's first line. What makes a list of commits readable at a glance instead of a
25
+ column of hashes — and the reason this contract exists."""
26
+
27
+ files: list[str]
28
+ """Paths the commit touched, as `git diff-tree` reported them.
29
+
30
+ Empty for a repository's very first commit, which has no parent to diff against. Also the answer
31
+ to "did this task do what its `files` said it would", which is a question a review asks and a
32
+ board could not previously support.
33
+ """
34
+
35
+ actor: str
36
+ ts: float
@@ -0,0 +1,150 @@
1
+ """ONE calendar day, in full — what a person reads at the end of it.
2
+
3
+ A projection like every other one here: nothing is stored for it, every fact comes from
4
+ `events.jsonl` plus what git already knows about the commits those events name. That is what
5
+ makes it safe to change and what makes it impossible to flatter anybody — a dossier nobody
6
+ typed cannot claim a card was closed that the log does not show closed.
7
+
8
+ The unit is a CALENDAR day and not a rolling window, because that is the question being asked.
9
+ "What happened yesterday" and "what happened in the last 24 hours" are different reports, and
10
+ the second one silently moves every time it is run.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import TypedDict
16
+
17
+ from .board import ActorRoll
18
+ from .commit import CommitRef
19
+ from .event import Event
20
+ from .task import Task
21
+
22
+ __all__ = ["CommitStat", "ClosedCard", "OpenedCard", "PeriodReport", "DayReport", "ReportFile"]
23
+
24
+
25
+ class CommitStat(CommitRef):
26
+ """A commit with its SIZE — the one thing the event log does not carry.
27
+
28
+ `additions`/`deletions` come from git at read time rather than from the `commit` event,
29
+ because the event is written by a hook that must never be slow and because a diff can be
30
+ recomputed forever while a bad number recorded once is permanent.
31
+
32
+ Zeros when git could not answer (no repository, the sha is on another machine, a shallow
33
+ clone). Honest and useless beats raising inside a report.
34
+ """
35
+
36
+ additions: int
37
+ deletions: int
38
+
39
+
40
+ class ClosedCard(TypedDict):
41
+ """A card that reached `done` on this day, with everything it took to get there."""
42
+
43
+ task: Task
44
+ actor: str
45
+ """Who closed it. The task row keeps the state and never who moved it there — this is the
46
+ only place that fact lives, and it comes from the `done` event."""
47
+
48
+ claimed_ts: float
49
+ """When its last claim before the close was taken, or the close itself when the card was
50
+ never claimed (a human closing something by hand). The pair with `done_ts` is the only
51
+ honest duration available: an earlier claim that was released is not time spent."""
52
+
53
+ done_ts: float
54
+ commits: list[CommitStat]
55
+ """Every commit bound to the task, not only the ones written today. A card closed at 9am
56
+ after three days of work shipped all of it, and showing one commit would understate it."""
57
+
58
+
59
+ class OpenedCard(TypedDict):
60
+ """A card CREATED in this window and not yet closed — planned work, with its edges.
61
+
62
+ The section that was missing entirely: a window whose whole content is planning reported
63
+ `0 closed · 0 in flight · 0 blocked` and printed nothing, because `backlog` and `ready`
64
+ belonged to no section. A day spent writing four specs is a day something happened on.
65
+
66
+ The edges travel with the card because they ARE the content of a planning day: which of
67
+ these can be started right now, and which is waiting on which. A list of titles without
68
+ the DAG is the same silence in a longer form.
69
+ """
70
+
71
+ task: Task
72
+ waiting_on: list[str]
73
+ """Ids of the OPEN cards this one waits for. Empty means it can be started now."""
74
+
75
+ blocking: list[str]
76
+ """Ids waiting on this one — the argument for doing it first."""
77
+
78
+
79
+ class PeriodReport(TypedDict):
80
+ """A WINDOW of calendar days, as one object. One day is the case where both ends match.
81
+
82
+ ONE contract rather than a day report and a range report side by side: they carry exactly
83
+ the same facts over a wider window, and two shapes would drift the moment a field was
84
+ added to the one somebody happened to be reading.
85
+ """
86
+
87
+ repo: str
88
+ from_date: str
89
+ """`YYYY-MM-DD`, in the reader's LOCAL calendar. The window opens at its 00:00."""
90
+
91
+ to_date: str
92
+ """`YYYY-MM-DD`, INCLUSIVE — the window closes at the midnight AFTER it."""
93
+
94
+ label: str
95
+ """What a human calls this window, and what names its file: `2026-07-28` for one day,
96
+ `2026-07-22..2026-07-28` for a range, `all` for the whole project. Derived and never
97
+ typed, so the heading of a report and the name of the file on disk cannot disagree."""
98
+
99
+ closed: list[ClosedCard]
100
+ dropped: int
101
+ """Closed cards the window held and this report does NOT carry, because the cap cut them.
102
+
103
+ Reported rather than silent, exactly as the activity view reports truncation: a month
104
+ that closed 400 cards and shows 200 of them is a fine report and a terrible lie."""
105
+
106
+ opened: list[OpenedCard]
107
+ """Cards created in the window and still open. A card created AND closed inside it is not
108
+ here — `closed` already tells that story in full, and listing it twice would invite a
109
+ reader to count it twice."""
110
+
111
+ in_flight: list[Task]
112
+ blocked: list[Task]
113
+ waiting: list[Task]
114
+ """Open cards touched in the window that nobody has started — `ready` and `backlog`.
115
+
116
+ They belonged to no section at all until this field existed, which is how a freshly
117
+ planned project reported that nothing had happened. Cards created in this window are
118
+ excluded: those are in `opened`, where they carry their dependencies too.
119
+ """
120
+
121
+ conversations: list[Event]
122
+ actors: list[ActorRoll]
123
+ commits_total: int
124
+
125
+
126
+ DayReport = PeriodReport
127
+ """The one-day case, under the name every caller already used. An alias and not a second
128
+ TypedDict — a subclass would be a second shape to keep in step for no gain."""
129
+
130
+
131
+ class ReportFile(TypedDict):
132
+ """A day's dossier as it exists ON DISK — or as it would be if it were written.
133
+
134
+ `exists` and `stale` are two different answers and both are needed: a day nobody wrote up
135
+ is not the same as one written before half of it happened, and a reader who cannot tell
136
+ them apart will either regenerate a report somebody narrated or cite one that is short.
137
+ """
138
+
139
+ date: str
140
+ path: str
141
+ dossier_md: str
142
+ """The written file when there is one, INCLUDING any narration; otherwise the dossier the
143
+ generator would produce right now. A caller always gets something to read."""
144
+
145
+ exists: bool
146
+ stale: bool
147
+ missing_events: int
148
+ """How many of the day's events landed after the file was generated. `stale` is this
149
+ being non-zero; the number is here so a UI can say how far behind rather than just that
150
+ it is."""
@@ -0,0 +1,23 @@
1
+ """The dependency edge. Two ids and a direction — that is genuinely all of it.
2
+
3
+ Direction is fixed and worth stating once, because every reader gets it backwards
4
+ otherwise: `task` must finish BEFORE `blocks` can start. So `deps` rows are read
5
+ one way to ask "what am I waiting for" (`WHERE blocks = me`) and the other way to
6
+ ask "who is waiting on me" (`WHERE task = me`), and both are one indexed lookup.
7
+
8
+ Kept as a separate edge table rather than a `blocked_by` list on the task because
9
+ the graph is queried from both ends, and a JSON list can only be read from one.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import TypedDict
15
+
16
+ __all__ = ["Dep"]
17
+
18
+
19
+ class Dep(TypedDict):
20
+ """`task` blocks `blocks`."""
21
+
22
+ task: str
23
+ blocks: str
@@ -0,0 +1,58 @@
1
+ """The event — the only thing taskops actually stores.
2
+
3
+ The board, a standup, an inbox and a burndown are all PROJECTIONS of this table.
4
+ That is what makes "who decided this and when" answerable months later, and it is
5
+ why adding a fact about a task is a new `kind` rather than a column somewhere.
6
+
7
+ The id is the content, hashed (`_ids.event_id`), so the same event arriving twice
8
+ — once through `git pull`, once through the relay — is a primary-key no-op rather
9
+ than a duplicate comment in somebody's inbox.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any, TypedDict
15
+
16
+ from .._types import EventKind
17
+
18
+ __all__ = ["Event", "Inbox"]
19
+
20
+
21
+ class Event(TypedDict):
22
+ """One thing that happened, attributed and timestamped."""
23
+
24
+ id: str
25
+ task: str
26
+ """The task it is about. Never empty: an event with no task cannot be found
27
+ again by anyone looking at the work, and project-wide facts belong in `meta`."""
28
+
29
+ actor: str
30
+ kind: EventKind
31
+
32
+ body: dict[str, Any]
33
+ """The payload, shaped by `kind` — `{sha, message, files}` for a commit,
34
+ `{text, mentions}` for a comment, `{from, to}` for a status change.
35
+
36
+ Deliberately open rather than a tagged union of thirteen TypedDicts: a reader
37
+ that does not know a kind must be able to store and forward it untouched,
38
+ because a newer taskops on a teammate's machine WILL write kinds this one has
39
+ never heard of into the shared log. Renderers read the keys they know.
40
+ """
41
+
42
+ ts: float
43
+
44
+
45
+ class Inbox(TypedDict):
46
+ """What an actor has not been shown yet.
47
+
48
+ Delivery is tracked per (actor, event) rather than by a timestamp cursor: an
49
+ agent's hooks fire in an order nobody controls, and a cursor would silently
50
+ skip anything that arrived out of order.
51
+ """
52
+
53
+ actor: str
54
+ messages: list[Event]
55
+ """Directed at this actor — mentions and handoffs, oldest first."""
56
+
57
+ tasks: list[str]
58
+ """The tasks those messages are about, deduplicated, for the render's header."""