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.
- taskops/__init__.py +39 -0
- taskops/_clock.py +32 -0
- taskops/_errors.py +160 -0
- taskops/_ids.py +63 -0
- taskops/_types.py +106 -0
- taskops/_version.py +7 -0
- taskops/assets/GUIDE.md +220 -0
- taskops/contracts/__init__.py +96 -0
- taskops/contracts/_fields.py +113 -0
- taskops/contracts/actor.py +30 -0
- taskops/contracts/board.py +142 -0
- taskops/contracts/commit.py +36 -0
- taskops/contracts/day.py +150 -0
- taskops/contracts/dep.py +23 -0
- taskops/contracts/event.py +58 -0
- taskops/contracts/gitstate.py +45 -0
- taskops/contracts/index.py +37 -0
- taskops/contracts/lease.py +43 -0
- taskops/contracts/log.py +59 -0
- taskops/contracts/remote.py +48 -0
- taskops/contracts/results.py +84 -0
- taskops/contracts/task.py +94 -0
- taskops/contracts/tools.py +110 -0
- taskops/contracts/wire.py +60 -0
- taskops/engine/__init__.py +34 -0
- taskops/engine/_blocks.py +48 -0
- taskops/engine/_briefs.py +92 -0
- taskops/engine/_chunks.py +66 -0
- taskops/engine/_closed.py +65 -0
- taskops/engine/_entries.py +126 -0
- taskops/engine/_events.py +55 -0
- taskops/engine/_opened.py +51 -0
- taskops/engine/_process.py +80 -0
- taskops/engine/_prompts.py +98 -0
- taskops/engine/_stream.py +129 -0
- taskops/engine/activity.py +94 -0
- taskops/engine/bus.py +42 -0
- taskops/engine/commitline.py +110 -0
- taskops/engine/day.py +142 -0
- taskops/engine/diffstat.py +63 -0
- taskops/engine/gitio.py +108 -0
- taskops/engine/gitstate.py +96 -0
- taskops/engine/history.py +76 -0
- taskops/engine/identity.py +84 -0
- taskops/engine/log.py +68 -0
- taskops/engine/machine.py +160 -0
- taskops/engine/narrate.py +91 -0
- taskops/engine/project.py +57 -0
- taskops/engine/replay.py +142 -0
- taskops/engine/reports.py +67 -0
- taskops/engine/scheduler.py +135 -0
- taskops/engine/transcript.py +127 -0
- taskops/engine/wire.py +92 -0
- taskops/engine/worker.py +115 -0
- taskops/py.typed +0 -0
- taskops/render/__init__.py +40 -0
- taskops/render/_closed_days.py +64 -0
- taskops/render/_dossier.py +68 -0
- taskops/render/_opened.py +64 -0
- taskops/render/_sections.py +51 -0
- taskops/render/_tasklist.py +72 -0
- taskops/render/_text.py +74 -0
- taskops/render/_verbatim.py +65 -0
- taskops/render/ansi.py +92 -0
- taskops/render/board.py +55 -0
- taskops/render/day.py +102 -0
- taskops/render/dispatch.py +104 -0
- taskops/render/inbox.py +27 -0
- taskops/render/log.py +47 -0
- taskops/render/recover.py +64 -0
- taskops/render/report.py +51 -0
- taskops/render/reports.py +57 -0
- taskops/render/results.py +96 -0
- taskops/render/session.py +75 -0
- taskops/render/task.py +88 -0
- taskops/render/tasklist.py +65 -0
- taskops/storage/__init__.py +36 -0
- taskops/storage/_ddl.py +78 -0
- taskops/storage/_delivered.py +51 -0
- taskops/storage/_deps.py +69 -0
- taskops/storage/_events.py +127 -0
- taskops/storage/_leases.py +108 -0
- taskops/storage/_rows.py +90 -0
- taskops/storage/_tasks.py +124 -0
- taskops/storage/locate.py +76 -0
- taskops/storage/schema.py +66 -0
- taskops/storage/store.py +120 -0
- taskops/storage/sync.py +139 -0
- taskops/transports/__init__.py +6 -0
- taskops/transports/cli/__init__.py +0 -0
- taskops/transports/cli/commands/__init__.py +3 -0
- taskops/transports/cli/commands/_digest.py +64 -0
- taskops/transports/cli/commands/_serve_init.py +65 -0
- taskops/transports/cli/commands/_shared.py +50 -0
- taskops/transports/cli/commands/_tasks_args.py +112 -0
- taskops/transports/cli/commands/_window.py +45 -0
- taskops/transports/cli/commands/ask.py +27 -0
- taskops/transports/cli/commands/dispatch.py +43 -0
- taskops/transports/cli/commands/init.py +48 -0
- taskops/transports/cli/commands/log.py +22 -0
- taskops/transports/cli/commands/plan.py +43 -0
- taskops/transports/cli/commands/pushpull.py +53 -0
- taskops/transports/cli/commands/recover.py +30 -0
- taskops/transports/cli/commands/remote.py +56 -0
- taskops/transports/cli/commands/report.py +82 -0
- taskops/transports/cli/commands/run_.py +60 -0
- taskops/transports/cli/commands/serve.py +74 -0
- taskops/transports/cli/commands/sync.py +22 -0
- taskops/transports/cli/commands/tasks.py +79 -0
- taskops/transports/cli/commands/ui.py +72 -0
- taskops/transports/cli/commands/update.py +25 -0
- taskops/transports/cli/main.py +85 -0
- taskops/transports/hooks/__init__.py +15 -0
- taskops/transports/hooks/__main__.py +63 -0
- taskops/transports/hooks/_args.py +28 -0
- taskops/transports/hooks/claude.py +78 -0
- taskops/transports/hooks/commit.py +61 -0
- taskops/transports/hooks/events.py +107 -0
- taskops/transports/hooks/record.py +55 -0
- taskops/transports/http/__init__.py +21 -0
- taskops/transports/http/_handler.py +132 -0
- taskops/transports/http/_wire.py +79 -0
- taskops/transports/http/_wsframes.py +116 -0
- taskops/transports/http/agentapi.py +69 -0
- taskops/transports/http/api.py +122 -0
- taskops/transports/http/exchange.py +80 -0
- taskops/transports/http/live.py +160 -0
- taskops/transports/http/policy.py +110 -0
- taskops/transports/http/projects.py +116 -0
- taskops/transports/http/reports.py +75 -0
- taskops/transports/http/router.py +90 -0
- taskops/transports/http/server.py +50 -0
- taskops/transports/http/static.py +86 -0
- taskops/transports/http/ui/app.js +59 -0
- taskops/transports/http/ui/index.html +23 -0
- taskops/transports/http/ui/style.css +1 -0
- taskops/transports/http/websocket.py +60 -0
- taskops/transports/mcp/__init__.py +19 -0
- taskops/transports/mcp/__main__.py +7 -0
- taskops/transports/mcp/_descriptions.py +102 -0
- taskops/transports/mcp/_reads.py +82 -0
- taskops/transports/mcp/_writes.py +72 -0
- taskops/transports/mcp/answers.py +44 -0
- taskops/transports/mcp/arguments.py +104 -0
- taskops/transports/mcp/dispatch.py +47 -0
- taskops/transports/mcp/protocol.py +83 -0
- taskops/transports/mcp/schema.py +50 -0
- taskops/transports/mcp/server.py +49 -0
- taskops/transports/mcp/tools.py +66 -0
- taskops/usecases/__init__.py +56 -0
- taskops/usecases/_entry.py +84 -0
- taskops/usecases/_facts.py +49 -0
- taskops/usecases/_freeing.py +93 -0
- taskops/usecases/_gitignore.py +94 -0
- taskops/usecases/_mirroring.py +57 -0
- taskops/usecases/_narrating.py +77 -0
- taskops/usecases/_project.py +67 -0
- taskops/usecases/_range.py +102 -0
- taskops/usecases/_reasons.py +54 -0
- taskops/usecases/_remotefile.py +62 -0
- taskops/usecases/_reportsync.py +108 -0
- taskops/usecases/_routing.py +91 -0
- taskops/usecases/_wireclient.py +141 -0
- taskops/usecases/ask.py +41 -0
- taskops/usecases/claim.py +104 -0
- taskops/usecases/dispatch.py +160 -0
- taskops/usecases/dossier.py +155 -0
- taskops/usecases/edit.py +71 -0
- taskops/usecases/exchange.py +86 -0
- taskops/usecases/feed.py +138 -0
- taskops/usecases/guard.py +122 -0
- taskops/usecases/hooks.py +127 -0
- taskops/usecases/index.py +62 -0
- taskops/usecases/ingest.py +60 -0
- taskops/usecases/log.py +134 -0
- taskops/usecases/narration.py +91 -0
- taskops/usecases/plan.py +128 -0
- taskops/usecases/pushpull.py +119 -0
- taskops/usecases/recover.py +84 -0
- taskops/usecases/remote.py +78 -0
- taskops/usecases/report.py +87 -0
- taskops/usecases/reportfile.py +106 -0
- taskops/usecases/session.py +134 -0
- taskops/usecases/setup.py +92 -0
- taskops/usecases/sync.py +78 -0
- taskops/usecases/update.py +123 -0
- taskops/usecases/view.py +112 -0
- taskops_cli-0.2.0.dist-info/METADATA +291 -0
- taskops_cli-0.2.0.dist-info/RECORD +193 -0
- taskops_cli-0.2.0.dist-info/WHEEL +5 -0
- taskops_cli-0.2.0.dist-info/entry_points.txt +2 -0
- taskops_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
- 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
|
taskops/contracts/day.py
ADDED
|
@@ -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."""
|
taskops/contracts/dep.py
ADDED
|
@@ -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."""
|