e7a14c9 · Ship broadcast cards and an agent skill · 27 days ago
1
"""Room messages. Four kinds, one shape. No labeled dumps."""2
3
from __future__ import annotations4
5
from dataclasses import dataclass6
7
from mapping import Mapping, Work8
9
KINDS = ("kickoff", "nudge", "blocked", "done")10
STATES = ("active", "blocked", "closed")11
EXPECT = {12
"kickoff": "active",13
"nudge": "active",14
"blocked": "blocked",15
"done": "closed",16
}17
CTA = {18
"kickoff": "Reply in this thread. One status is enough.",19
"nudge": "Still open. Reply in this thread.",20
"blocked": "Blocked. Reply here only if that changed.",21
"done": "Closed.",22
}23
24
25
class BroadcastError(ValueError):26
pass27
28
29
@dataclass(frozen=True)30
class Broadcast:31
kind: str32
work_id: str33
title: str34
state: str35
owner: str = ""36
assignee: str = ""37
quote: str = ""38
quote_from: str = ""39
platform: str = "slack"40
41
42
def render(b: Broadcast) -> str:43
if b.kind not in KINDS:44
raise BroadcastError(f"unknown kind: {b.kind}")45
if b.state not in STATES:46
raise BroadcastError(f"unknown state: {b.state}")47
if b.state != EXPECT[b.kind]:48
raise BroadcastError(f"{b.kind} requires state {EXPECT[b.kind]}")49
title = _line(b.title)50
work_id = _line(b.work_id)51
if not title or not work_id:52
raise BroadcastError("title and work id are required")53
lines = [_bold(title, b.platform), f"`{work_id}` · {b.state}"]54
who = _who(_line(b.owner), _line(b.assignee))55
if who:56
lines.append("")57
lines.extend(who)58
quote = _line(b.quote)59
if quote:60
lines.append("")61
lines.append("> " + quote)62
frm = _line(b.quote_from)63
if frm:64
lines.append("> — " + frm)65
lines.append("")66
lines.append(CTA[b.kind])67
text = "\n".join(lines) + "\n"68
for banned in ("broadcast:", "state:", "owner:", "in flight:", "context:"):69
if banned in text:70
raise BroadcastError(f"labeled dump leaked: {banned}")71
return text72
73
74
def kickoff_for(75
work: Work,76
*,77
assignee: str = "",78
quote: str = "",79
quote_from: str = "",80
platform: str = "slack"81
) -> str:82
return render(83
Broadcast(84
kind="kickoff",85
work_id=work.id,86
title=work.name or work.id,87
state=work.state,88
owner=work.owner,89
assignee=assignee,90
quote=quote,91
quote_from=quote_from,92
platform=platform,93
)94
)95
96
97
def preview_posts(mapping: Mapping, platform: str = "slack") -> str:98
"""The kickoff each `post` row would drop in its room. Empty if nothing posts."""99
chunks = [100
kickoff_for(w, platform=platform)101
for w, action in mapping.plan()102
if action == "post"103
]104
if not chunks:105
return ""106
return "# would post\n\n" + "\n".join(chunks)107
108
109
def _who(owner: str, assignee: str) -> list[str]:110
lines: list[str] = []111
if owner:112
lines.append(f"{owner} owns it")113
if assignee:114
lines.append(f"{assignee} is on it")115
return lines116
117
118
def _bold(text: str, platform: str) -> str:119
if platform == "discord":120
return f"**{text}**"121
return f"*{text}*"122
123
124
def _line(text: str) -> str:125
return " ".join(text.split())126