@bytesbrains/pi-telegram-bridge 1.1.1 → 1.1.3

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.
package/src/templates.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * pi-telegram-bridge — Rich Message Templates
3
3
  *
4
4
  * Well-designed notification templates for Telegram.
5
- * All use Telegram MarkdownV2 format (no tables, no triple backticks).
5
+ * Uses HTML parse mode for reliable formatting with special characters.
6
6
  * Supports: issue, PR, CI/CD, work progress, agent session, factory, alerts.
7
7
  */
8
8
 
@@ -11,779 +11,682 @@
11
11
  const BAR = "━".repeat(30);
12
12
 
13
13
  function bold(s: string) {
14
- return `*${s}*`;
14
+ return `<b>${s}</b>`;
15
15
  }
16
16
  function italic(s: string) {
17
- return `_${s}_`;
17
+ return `<i>${s}</i>`;
18
18
  }
19
19
  function code(s: string) {
20
- return `\`${s}\``;
20
+ return `<code>${s}</code>`;
21
21
  }
22
22
  function strike(s: string) {
23
- return `~${s}~`;
23
+ return `<s>${s}</s>`;
24
24
  }
25
25
  function labelValue(label: string, value: string) {
26
- return `${bold(label + ":")} ${value}`;
26
+ return `${bold(label + ":")} ${value}`;
27
+ }
28
+ function link(url: string, text: string) {
29
+ return `<a href="${url}">${text}</a>`;
27
30
  }
28
31
  function progressBar(pct: number, width = 12) {
29
- const filled = Math.round((pct / 100) * width);
30
- return `${"█".repeat(filled)}${"░".repeat(width - filled)} ${pct}%`;
32
+ const filled = Math.round((pct / 100) * width);
33
+ return `<code>${"█".repeat(filled)}${"░".repeat(width - filled)}</code> ${pct}%`;
31
34
  }
35
+
32
36
  // ── CI/CD Status Icons ─────────────────────────────────────────────
33
37
 
34
38
  function jobIcon(status: string): string {
35
- const map: Record<string, string> = {
36
- success: "✅",
37
- failure: "❌",
38
- cancelled: "⚪",
39
- skipped: "⏭️",
40
- running: "🔵",
41
- queued: "⏳",
42
- pending: "🟡",
43
- in_progress: "🔵",
44
- };
45
- return map[status] ?? "❓";
39
+ const map: Record<string, string> = {
40
+ success: "✅",
41
+ failure: "❌",
42
+ cancelled: "⚪",
43
+ skipped: "⏭️",
44
+ running: "🔵",
45
+ queued: "⏳",
46
+ pending: "🟡",
47
+ in_progress: "🔵",
48
+ };
49
+ return map[status] ?? "❓";
46
50
  }
47
51
 
48
52
  // ── Issue Tray (compact, swipe-friendly) ───────────────────────────
49
53
 
50
54
  export interface IssueCard {
51
- number: number;
52
- title: string;
53
- state: "open" | "closed";
54
- status?: string; // "in progress", "blocked", etc.
55
- assignee?: string;
56
- labels?: string[];
57
- milestone?: string;
58
- repo: { owner: string; name: string };
59
- branch?: string;
60
- pr?: { number: number; status: string };
61
- url?: string;
55
+ number: number;
56
+ title: string;
57
+ state: "open" | "closed";
58
+ status?: string;
59
+ assignee?: string;
60
+ labels?: string[];
61
+ milestone?: string;
62
+ repo: { owner: string; name: string };
63
+ branch?: string;
64
+ pr?: { number: number; status: string };
65
+ url?: string;
62
66
  }
63
67
 
64
68
  export function issueCard(i: IssueCard): string {
65
- const icon =
66
- i.state === "closed" ? "✅" : i.status === "blocked" ? "🚫" : "📋";
67
- const closed = i.state === "closed";
68
-
69
- let msg = `${icon} ${bold(`Issue #${i.number}`)}${closed ? strike("") : ""}: ${closed ? strike(i.title) : bold(i.title)}\n`;
70
- msg += `${BAR}\n`;
71
-
72
- if (i.status) {
73
- const statusIcon =
74
- i.status === "in progress" ? "🔄" : i.status === "blocked" ? "🚫" : "📌";
75
- msg +=
76
- labelValue(
77
- "Status",
78
- `${closed ? strike(`${statusIcon} ${i.status}`) : `${statusIcon} ${i.status}`}`,
79
- ) + "\n";
80
- }
81
- if (i.assignee) msg += labelValue("Assignee", code(i.assignee)) + "\n";
82
- if (i.labels && i.labels.length > 0) {
83
- msg += labelValue("Labels", i.labels.map((l) => code(l)).join(" ")) + "\n";
84
- }
85
- if (i.milestone) msg += labelValue("Milestone", italic(i.milestone)) + "\n";
86
- msg += labelValue("Repo", italic(`${i.repo.owner}/${i.repo.name}`)) + "\n";
87
- msg += `${BAR}\n`;
88
- if (i.branch) msg += labelValue("Branch", code(i.branch)) + "\n";
89
- if (i.pr) {
90
- msg +=
91
- labelValue("PR", `[#${i.pr.number}](${i.url ?? ""}) — ${i.pr.status}`) +
92
- "\n";
93
- }
94
- if (i.url) msg += `\n[View on GitHub](${i.url})`;
95
- return msg;
69
+ const icon = i.state === "closed" ? "✅" : i.status === "blocked" ? "🚫" : "📋";
70
+ const closed = i.state === "closed";
71
+
72
+ let msg = `${icon} ${bold(`Issue #${i.number}`)}${closed ? strike("") : ""}: ${closed ? strike(i.title) : bold(i.title)}\n`;
73
+ msg += `${BAR}\n`;
74
+
75
+ if (i.status) {
76
+ const statusIcon = i.status === "in progress" ? "🔄" : i.status === "blocked" ? "🚫" : "📌";
77
+ msg += labelValue("Status", `${closed ? strike(`${statusIcon} ${i.status}`) : `${statusIcon} ${i.status}`}`) + "\n";
78
+ }
79
+ if (i.assignee) msg += labelValue("Assignee", code(i.assignee)) + "\n";
80
+ if (i.labels && i.labels.length > 0) {
81
+ msg += labelValue("Labels", i.labels.map((l) => code(l)).join(" ")) + "\n";
82
+ }
83
+ if (i.milestone) msg += labelValue("Milestone", italic(i.milestone)) + "\n";
84
+ msg += labelValue("Repo", italic(`${i.repo.owner}/${i.repo.name}`)) + "\n";
85
+ msg += `${BAR}\n`;
86
+ if (i.branch) msg += labelValue("Branch", code(i.branch)) + "\n";
87
+ if (i.pr && i.url) {
88
+ msg += labelValue("PR", `${link(i.url, `#${i.pr.number}`)} — ${i.pr.status}`) + "\n";
89
+ } else if (i.pr) {
90
+ msg += labelValue("PR", `#${i.pr.number} — ${i.pr.status}`) + "\n";
91
+ }
92
+ if (i.url) msg += `\n${link(i.url, "View on GitHub")}`;
93
+ return msg;
96
94
  }
97
95
 
98
96
  // ── PR Status Card ──────────────────────────────────────────────────
99
97
 
100
98
  export interface PRCard {
101
- number: number;
102
- title: string;
103
- state: "open" | "closed" | "merged" | "draft";
104
- author: string;
105
- base: string;
106
- head: string;
107
- repo: { owner: string; name: string };
108
- ci?: { passed: number; failed: number; total: number; status?: string };
109
- reviews?: { approved: number; requested: number; changesRequested: number };
110
- files?: { added: number; removed: number; changed: number };
111
- breaking?: boolean;
112
- draft?: boolean;
113
- url?: string;
99
+ number: number;
100
+ title: string;
101
+ state: "open" | "closed" | "merged" | "draft";
102
+ author: string;
103
+ base: string;
104
+ head: string;
105
+ repo: { owner: string; name: string };
106
+ ci?: { passed: number; failed: number; total: number; status?: string };
107
+ reviews?: { approved: number; requested: number; changesRequested: number };
108
+ files?: { added: number; removed: number; changed: number };
109
+ breaking?: boolean;
110
+ draft?: boolean;
111
+ url?: string;
114
112
  }
115
113
 
116
114
  export function prCard(pr: PRCard): string {
117
- const stateIcon =
118
- pr.state === "merged"
119
- ? "💜"
120
- : pr.state === "closed"
121
- ? ""
122
- : pr.draft
123
- ? "📝"
124
- : "🔀";
125
- const stateBadge =
126
- pr.state === "merged"
127
- ? "💜 Merged"
128
- : pr.state === "closed"
129
- ? "⚫ Closed"
130
- : pr.draft
131
- ? "📝 Draft"
132
- : "🟡 Open";
133
-
134
- let msg = `${stateIcon} ${bold(`PR #${pr.number}`)}: ${italic(pr.title)}\n`;
135
- msg += `${BAR}\n`;
136
- msg += labelValue("Status", stateBadge) + "\n";
137
- msg += labelValue("Author", code(pr.author)) + "\n";
138
- msg += labelValue("Branch", `${code(pr.base)} ← ${code(pr.head)}`) + "\n";
139
-
140
- // CI status
141
- if (pr.ci) {
142
- const icon = pr.ci.status
143
- ? jobIcon(pr.ci.status)
144
- : pr.ci.failed === 0
145
- ? ""
146
- : "❌";
147
- msg +=
148
- labelValue(
149
- "CI",
150
- `${icon} ${pr.ci.passed}/${pr.ci.total} checks passed` +
151
- (pr.ci.failed > 0 ? ` · ${pr.ci.failed} failed` : ""),
152
- ) + "\n";
153
- }
154
-
155
- // Reviews
156
- if (pr.reviews) {
157
- const parts: string[] = [];
158
- if (pr.reviews.approved > 0)
159
- parts.push(`✅ ${pr.reviews.approved} approved`);
160
- if (pr.reviews.changesRequested > 0)
161
- parts.push(`🔴 ${pr.reviews.changesRequested} changes req`);
162
- if (pr.reviews.requested > 0)
163
- parts.push(`⏳ ${pr.reviews.requested} pending`);
164
- msg += labelValue("Reviews", parts.join(" · ")) + "\n";
165
- }
166
-
167
- if (pr.files) {
168
- msg +=
169
- labelValue(
170
- "Files",
171
- `${pr.files.changed} files (+${pr.files.added} −${pr.files.removed})`,
172
- ) + "\n";
173
- }
174
- if (pr.breaking) msg += labelValue("Breaking", "⚠️ Yes") + "\n";
175
-
176
- msg += `${BAR}\n`;
177
- if (pr.url) msg += `[View on GitHub](${pr.url})`;
178
- return msg;
115
+ const stateIcon =
116
+ pr.state === "merged" ? "💜" : pr.state === "closed" ? "⚫" : pr.draft ? "📝" : "🔀";
117
+ const stateBadge =
118
+ pr.state === "merged"
119
+ ? "💜 Merged"
120
+ : pr.state === "closed"
121
+ ? "⚫ Closed"
122
+ : pr.draft
123
+ ? "📝 Draft"
124
+ : "🟡 Open";
125
+
126
+ let msg = `${stateIcon} ${bold(`PR #${pr.number}`)}: ${italic(pr.title)}\n`;
127
+ msg += `${BAR}\n`;
128
+ msg += labelValue("Status", stateBadge) + "\n";
129
+ msg += labelValue("Author", code(pr.author)) + "\n";
130
+ msg += labelValue("Branch", `${code(pr.base)} ← ${code(pr.head)}`) + "\n";
131
+
132
+ if (pr.ci) {
133
+ const icon = pr.ci.status
134
+ ? jobIcon(pr.ci.status)
135
+ : pr.ci.failed === 0
136
+ ? ""
137
+ : "❌";
138
+ msg +=
139
+ labelValue(
140
+ "CI",
141
+ `${icon} ${pr.ci.passed}/${pr.ci.total} checks passed` +
142
+ (pr.ci.failed > 0 ? ` · ${pr.ci.failed} failed` : ""),
143
+ ) + "\n";
144
+ }
145
+
146
+ if (pr.reviews) {
147
+ const parts: string[] = [];
148
+ if (pr.reviews.approved > 0) parts.push(`✅ ${pr.reviews.approved} approved`);
149
+ if (pr.reviews.changesRequested > 0)
150
+ parts.push(`🔴 ${pr.reviews.changesRequested} changes req`);
151
+ if (pr.reviews.requested > 0) parts.push(`⏳ ${pr.reviews.requested} pending`);
152
+ msg += labelValue("Reviews", parts.join(" · ")) + "\n";
153
+ }
154
+
155
+ if (pr.files) {
156
+ msg +=
157
+ labelValue(
158
+ "Files",
159
+ `${pr.files.changed} files (+${pr.files.added} −${pr.files.removed})`,
160
+ ) + "\n";
161
+ }
162
+ if (pr.breaking) msg += labelValue("Breaking", "⚠️ Yes") + "\n";
163
+
164
+ msg += `${BAR}\n`;
165
+ if (pr.url) msg += link(pr.url, "View on GitHub");
166
+ return msg;
179
167
  }
180
168
 
181
169
  // ── Work / Task Progress ────────────────────────────────────────────
182
170
 
183
171
  export interface TaskProgress {
184
- title: string;
185
- emoji?: string;
186
- status: "started" | "in-progress" | "paused" | "blocked" | "complete";
187
- progress?: number; // 0–100
188
- elapsed?: string; // "3m 22s"
189
- eta?: string;
190
- details?: string[];
191
- metrics?: Record<string, string>;
172
+ title: string;
173
+ emoji?: string;
174
+ status: "started" | "in-progress" | "paused" | "blocked" | "complete";
175
+ progress?: number;
176
+ elapsed?: string;
177
+ eta?: string;
178
+ details?: string[];
179
+ metrics?: Record<string, string>;
192
180
  }
193
181
 
194
182
  export function taskProgress(t: TaskProgress): string {
195
- const icons: Record<string, string> = {
196
- started: "🟢",
197
- "in-progress": "🔵",
198
- paused: "🟡",
199
- blocked: "🚫",
200
- complete: "✅",
201
- };
202
- const emoji = t.emoji ?? "🛠️";
203
- const icon = icons[t.status] ?? "🔵";
204
-
205
- let msg = `${emoji} ${bold(t.title)}\n`;
206
- msg += `${BAR}\n`;
207
- msg += labelValue("Status", `${icon} ${t.status}`) + "\n";
208
-
209
- if (t.progress !== undefined) {
210
- msg += labelValue("Progress", progressBar(t.progress)) + "\n";
211
- }
212
- if (t.elapsed) msg += labelValue("Elapsed", code(t.elapsed)) + "\n";
213
- if (t.eta) msg += labelValue("ETA", code(t.eta)) + "\n";
214
-
215
- if (t.details && t.details.length > 0) {
216
- msg += `${BAR}\n`;
217
- for (const d of t.details) {
218
- msg += ` ${d}\n`;
219
- }
220
- }
221
-
222
- if (t.metrics && Object.keys(t.metrics).length > 0) {
223
- msg += `${BAR}\n`;
224
- for (const [k, v] of Object.entries(t.metrics)) {
225
- msg += labelValue(k, code(v)) + "\n";
226
- }
227
- }
228
- return msg;
183
+ const icons: Record<string, string> = {
184
+ started: "🟢",
185
+ "in-progress": "🔵",
186
+ paused: "🟡",
187
+ blocked: "🚫",
188
+ complete: "✅",
189
+ };
190
+ const emoji = t.emoji ?? "🛠️";
191
+ const icon = icons[t.status] ?? "🔵";
192
+
193
+ let msg = `${emoji} ${bold(t.title)}\n`;
194
+ msg += `${BAR}\n`;
195
+ msg += labelValue("Status", `${icon} ${t.status}`) + "\n";
196
+
197
+ if (t.progress !== undefined) {
198
+ msg += labelValue("Progress", progressBar(t.progress)) + "\n";
199
+ }
200
+ if (t.elapsed) msg += labelValue("Elapsed", code(t.elapsed)) + "\n";
201
+ if (t.eta) msg += labelValue("ETA", code(t.eta)) + "\n";
202
+
203
+ if (t.details && t.details.length > 0) {
204
+ msg += `${BAR}\n`;
205
+ for (const d of t.details) {
206
+ msg += ` ${d}\n`;
207
+ }
208
+ }
209
+
210
+ if (t.metrics && Object.keys(t.metrics).length > 0) {
211
+ msg += `${BAR}\n`;
212
+ for (const [k, v] of Object.entries(t.metrics)) {
213
+ msg += labelValue(k, code(v)) + "\n";
214
+ }
215
+ }
216
+ return msg;
229
217
  }
230
218
 
231
219
  // ── CI Pipeline ─────────────────────────────────────────────────────
232
220
 
233
221
  export interface CIPipeline {
234
- workflow: string;
235
- runNumber: number;
236
- branch: string;
237
- event: string; // "push", "pull_request"
238
- jobs: Array<{ name: string; status: string; duration?: string }>;
239
- triggeredBy?: string;
240
- url?: string;
222
+ workflow: string;
223
+ runNumber: number;
224
+ branch: string;
225
+ event: string;
226
+ jobs: Array<{ name: string; status: string; duration?: string }>;
227
+ triggeredBy?: string;
228
+ url?: string;
241
229
  }
242
230
 
243
231
  export function ciPipeline(p: CIPipeline): string {
244
- let msg = `🔄 ${bold(p.workflow)}\n`;
245
- msg += `${BAR}\n`;
246
- msg += labelValue("Run", `#${p.runNumber}`) + "\n";
247
- msg += labelValue("Branch", code(p.branch)) + "\n";
248
- msg += labelValue("Trigger", p.event) + "\n";
249
- if (p.triggeredBy) msg += labelValue("By", code(p.triggeredBy)) + "\n";
250
- msg += `${BAR}\n`;
232
+ let msg = `🔄 ${bold(p.workflow)}\n`;
233
+ msg += `${BAR}\n`;
234
+ msg += labelValue("Run", `#${p.runNumber}`) + "\n";
235
+ msg += labelValue("Branch", code(p.branch)) + "\n";
236
+ msg += labelValue("Trigger", p.event) + "\n";
237
+ if (p.triggeredBy) msg += labelValue("By", code(p.triggeredBy)) + "\n";
238
+ msg += `${BAR}\n`;
251
239
 
252
- for (const j of p.jobs) {
253
- const dur = j.duration ? ` (${j.duration})` : "";
254
- msg += `${jobIcon(j.status)} ${code(j.name)}${dur}\n`;
255
- }
240
+ for (const j of p.jobs) {
241
+ const dur = j.duration ? ` (${j.duration})` : "";
242
+ msg += `${jobIcon(j.status)} ${code(j.name)}${dur}\n`;
243
+ }
256
244
 
257
- if (p.url) msg += `\n[View Pipeline](${p.url})`;
258
- return msg;
245
+ if (p.url) msg += `\n${link(p.url, "View Pipeline")}`;
246
+ return msg;
259
247
  }
260
248
 
261
249
  // ── Agent Session Card ──────────────────────────────────────────────
262
250
 
263
251
  export interface SessionCard {
264
- session: string;
265
- branch?: string;
266
- model?: string;
267
- turns?: number;
268
- tokens?: { used: number; total: number };
269
- duration?: string;
270
- status: "idle" | "streaming" | "tool-exec" | "waiting";
252
+ session: string;
253
+ branch?: string;
254
+ model?: string;
255
+ turns?: number;
256
+ tokens?: { used: number; total: number };
257
+ duration?: string;
258
+ status: "idle" | "streaming" | "tool-exec" | "waiting";
271
259
  }
272
260
 
273
261
  export function sessionCard(s: SessionCard): string {
274
- const statusIcons: Record<string, string> = {
275
- idle: "⏸️",
276
- streaming: "💬",
277
- "tool-exec": "🔧",
278
- waiting: "⏳",
279
- };
280
- const icon = statusIcons[s.status] ?? "🤖";
281
-
282
- let msg = `${icon} ${bold("Agent Session")}\n`;
283
- msg += `${BAR}\n`;
284
- msg += labelValue("Session", code(s.session)) + "\n";
285
- if (s.branch) msg += labelValue("Branch", code(s.branch)) + "\n";
286
- if (s.model) msg += labelValue("Model", italic(s.model)) + "\n";
287
- if (s.turns !== undefined) msg += labelValue("Turns", `${s.turns}`) + "\n";
288
- if (s.tokens) {
289
- const pct = Math.round((s.tokens.used / s.tokens.total) * 100);
290
- msg +=
291
- labelValue(
292
- "Tokens",
293
- `${s.tokens.used.toLocaleString()} / ${s.tokens.total.toLocaleString()} (${pct}%)`,
294
- ) + "\n";
295
- }
296
- if (s.duration) msg += labelValue("Duration", code(s.duration)) + "\n";
297
- return msg;
262
+ const statusIcons: Record<string, string> = {
263
+ idle: "⏸️",
264
+ streaming: "💬",
265
+ "tool-exec": "🔧",
266
+ waiting: "⏳",
267
+ };
268
+ const icon = statusIcons[s.status] ?? "🤖";
269
+
270
+ let msg = `${icon} ${bold("Agent Session")}\n`;
271
+ msg += `${BAR}\n`;
272
+ msg += labelValue("Session", code(s.session)) + "\n";
273
+ if (s.branch) msg += labelValue("Branch", code(s.branch)) + "\n";
274
+ if (s.model) msg += labelValue("Model", italic(s.model)) + "\n";
275
+ if (s.turns !== undefined) msg += labelValue("Turns", `${s.turns}`) + "\n";
276
+ if (s.tokens) {
277
+ const pct = Math.round((s.tokens.used / s.tokens.total) * 100);
278
+ msg += labelValue("Tokens", `${s.tokens.used.toLocaleString()} / ${s.tokens.total.toLocaleString()} (${pct}%)`) + "\n";
279
+ }
280
+ if (s.duration) msg += labelValue("Duration", code(s.duration)) + "\n";
281
+ return msg;
298
282
  }
299
283
 
300
284
  // ── Alert / Error ───────────────────────────────────────────────────
301
285
 
302
286
  export interface AlertCard {
303
- level: "info" | "warning" | "error" | "critical";
304
- title: string;
305
- source?: string;
306
- detail?: string;
307
- action?: string;
308
- stack?: string;
287
+ level: "info" | "warning" | "error" | "critical";
288
+ title: string;
289
+ source?: string;
290
+ detail?: string;
291
+ action?: string;
292
+ stack?: string;
309
293
  }
310
294
 
311
295
  export function alertCard(a: AlertCard): string {
312
- const icons: Record<string, string> = {
313
- info: "ℹ️",
314
- warning: "⚠️",
315
- error: "🚨",
316
- critical: "🛑",
317
- };
318
- const icon = icons[a.level] ?? "📢";
319
-
320
- let msg = `${icon} ${bold(a.title)}\n`;
321
- msg += `${BAR}\n`;
322
- if (a.source) msg += labelValue("Source", code(a.source)) + "\n";
323
- if (a.detail) msg += labelValue("Detail", italic(a.detail)) + "\n";
324
- if (a.action) msg += labelValue("Action", bold(a.action)) + "\n";
325
- if (a.stack) {
326
- msg += `\n${code(a.stack.slice(0, 300))}\n`;
327
- }
328
- return msg;
296
+ const icons: Record<string, string> = {
297
+ info: "ℹ️",
298
+ warning: "⚠️",
299
+ error: "🚨",
300
+ critical: "🛑",
301
+ };
302
+ const icon = icons[a.level] ?? "📢";
303
+
304
+ let msg = `${icon} ${bold(a.title)}\n`;
305
+ msg += `${BAR}\n`;
306
+ if (a.source) msg += labelValue("Source", code(a.source)) + "\n";
307
+ if (a.detail) msg += labelValue("Detail", italic(a.detail)) + "\n";
308
+ if (a.action) msg += labelValue("Action", bold(a.action)) + "\n";
309
+ if (a.stack) {
310
+ msg += `\n${code(a.stack.slice(0, 300))}\n`;
311
+ }
312
+ return msg;
329
313
  }
330
314
 
331
315
  // ── Factory / Orchestrator ──────────────────────────────────────────
332
316
 
333
317
  export interface FactoryJobCard {
334
- jobId: string;
335
- mode: "single" | "chain" | "parallel";
336
- status: "pending" | "running" | "success" | "failure" | "cancelled";
337
- agent?: string;
338
- task?: string;
339
- environment?: string;
340
- duration?: string;
341
- worker?: string;
318
+ jobId: string;
319
+ mode: "single" | "chain" | "parallel";
320
+ status: "pending" | "running" | "success" | "failure" | "cancelled";
321
+ agent?: string;
322
+ task?: string;
323
+ environment?: string;
324
+ duration?: string;
325
+ worker?: string;
342
326
  }
343
327
 
344
328
  export function factoryJobCard(j: FactoryJobCard): string {
345
- const icons: Record<string, string> = {
346
- pending: "⏳",
347
- running: "🔵",
348
- success: "✅",
349
- failure: "❌",
350
- cancelled: "⚪",
351
- };
352
- const icon = icons[j.status] ?? "❓";
353
-
354
- let msg = `${icon} ${bold("Factory Job")} ${code(j.jobId)}\n`;
355
- msg += `${BAR}\n`;
356
- msg += labelValue("Status", `${icon} ${j.status}`) + "\n";
357
- msg += labelValue("Mode", code(j.mode)) + "\n";
358
- if (j.agent) msg += labelValue("Agent", code(j.agent)) + "\n";
359
- if (j.task) msg += labelValue("Task", italic(j.task.slice(0, 120))) + "\n";
360
- if (j.environment) msg += labelValue("Env", code(j.environment)) + "\n";
361
- if (j.worker) msg += labelValue("Worker", code(j.worker)) + "\n";
362
- if (j.duration) msg += labelValue("Duration", code(j.duration)) + "\n";
363
- return msg;
329
+ const icons: Record<string, string> = {
330
+ pending: "⏳",
331
+ running: "🔵",
332
+ success: "✅",
333
+ failure: "❌",
334
+ cancelled: "⚪",
335
+ };
336
+ const icon = icons[j.status] ?? "❓";
337
+
338
+ let msg = `${icon} ${bold("Factory Job")} ${code(j.jobId)}\n`;
339
+ msg += `${BAR}\n`;
340
+ msg += labelValue("Status", `${icon} ${j.status}`) + "\n";
341
+ msg += labelValue("Mode", code(j.mode)) + "\n";
342
+ if (j.agent) msg += labelValue("Agent", code(j.agent)) + "\n";
343
+ if (j.task) msg += labelValue("Task", italic(j.task.slice(0, 120))) + "\n";
344
+ if (j.environment) msg += labelValue("Env", code(j.environment)) + "\n";
345
+ if (j.worker) msg += labelValue("Worker", code(j.worker)) + "\n";
346
+ if (j.duration) msg += labelValue("Duration", code(j.duration)) + "\n";
347
+ return msg;
364
348
  }
365
349
 
366
350
  // ── Multi-Item Summaries ────────────────────────────────────────────
367
351
 
368
352
  export interface SummaryHeader {
369
- title: string;
370
- subtitle?: string;
371
- items?: Array<{ icon: string; label: string; value: string }>;
353
+ title: string;
354
+ subtitle?: string;
355
+ items?: Array<{ icon: string; label: string; value: string }>;
372
356
  }
373
357
 
374
358
  export function summaryHeader(s: SummaryHeader): string {
375
- let msg = `${bold(s.title)}\n`;
376
- if (s.subtitle) msg += `${italic(s.subtitle)}\n`;
377
- msg += `${BAR}\n`;
378
- if (s.items) {
379
- for (const item of s.items) {
380
- msg += `${item.icon} ${labelValue(item.label, code(item.value))}\n`;
381
- }
382
- }
383
- return msg;
359
+ let msg = `${bold(s.title)}\n`;
360
+ if (s.subtitle) msg += `${italic(s.subtitle)}\n`;
361
+ msg += `${BAR}\n`;
362
+ if (s.items) {
363
+ for (const item of s.items) {
364
+ msg += `${item.icon} ${labelValue(item.label, code(item.value))}\n`;
365
+ }
366
+ }
367
+ return msg;
384
368
  }
385
369
 
386
370
  // ── Quick Acknowledgment ────────────────────────────────────────────
387
371
 
388
372
  export function ack(text: string, emoji = "✅"): string {
389
- return `${emoji} ${italic(text)}`;
373
+ return `${emoji} ${italic(text)}`;
390
374
  }
391
375
 
392
- // ── Decision Card (for override/approval context) ───────────────────
376
+ // ── Decision Card ───────────────────────────────────────────────────
393
377
 
394
378
  export interface DecisionCard {
395
- question: string;
396
- context: string;
397
- options: string[];
398
- risk: "low" | "medium" | "high" | "critical";
399
- command?: string;
400
- reason?: string;
379
+ question: string;
380
+ context: string;
381
+ options: string[];
382
+ risk: "low" | "medium" | "high" | "critical";
383
+ command?: string;
384
+ reason?: string;
401
385
  }
402
386
 
403
387
  export function decisionCard(d: DecisionCard): string {
404
- const riskIcons: Record<string, string> = {
405
- low: "🟢",
406
- medium: "🟡",
407
- high: "🟠",
408
- critical: "🔴",
409
- };
410
-
411
- let msg = `🛑 ${bold("Decision Required")}\n`;
412
- msg += `${BAR}\n`;
413
- msg += labelValue("Risk", `${riskIcons[d.risk]} ${d.risk}`) + "\n";
414
- msg += labelValue("Context", italic(d.context)) + "\n";
415
- if (d.command) msg += labelValue("Command", code(d.command)) + "\n";
416
- if (d.reason) msg += labelValue("Reason", d.reason) + "\n";
417
- msg += `\n${bold("❓ " + d.question)}\n`;
418
- msg += `\n${d.options.map((o, i) => `${i + 1}\\. ${o}`).join("\n")}`;
419
- return msg;
388
+ const riskIcons: Record<string, string> = {
389
+ low: "🟢",
390
+ medium: "🟡",
391
+ high: "🟠",
392
+ critical: "🔴",
393
+ };
394
+
395
+ let msg = `🛑 ${bold("Decision Required")}\n`;
396
+ msg += `${BAR}\n`;
397
+ msg += labelValue("Risk", `${riskIcons[d.risk]} ${d.risk}`) + "\n";
398
+ msg += labelValue("Context", italic(d.context)) + "\n";
399
+ if (d.command) msg += labelValue("Command", code(d.command)) + "\n";
400
+ if (d.reason) msg += labelValue("Reason", d.reason) + "\n";
401
+ msg += `\n${bold("❓ " + d.question)}\n`;
402
+ msg += `\n${d.options.map((o, i) => `${i + 1}. ${o}`).join("\n")}`;
403
+ return msg;
420
404
  }
421
405
 
422
406
  // ── Composite: Daily Standup / Status Roundup ───────────────────────
423
407
 
424
408
  export interface StandupCard {
425
- date: string;
426
- agent: string;
427
- workedOn: string[];
428
- nextUp: string[];
429
- blockers: string[];
430
- metrics?: Record<string, string>;
409
+ date: string;
410
+ agent: string;
411
+ workedOn: string[];
412
+ nextUp: string[];
413
+ blockers: string[];
414
+ metrics?: Record<string, string>;
431
415
  }
432
416
 
433
417
  export function standupCard(s: StandupCard): string {
434
- let msg = `📊 ${bold("Status Roundup")}\n`;
435
- msg += `${BAR}\n`;
436
- msg += labelValue("Date", code(s.date)) + "\n";
437
- msg += labelValue("Agent", code(s.agent)) + "\n";
438
- msg += `${BAR}\n`;
439
-
440
- msg += `${bold("✅ Completed")}\n`;
441
- for (const item of s.workedOn) {
442
- msg += ` • ${item}\n`;
443
- }
444
- msg += `\n${bold("🔄 Next")}\n`;
445
- for (const item of s.nextUp) {
446
- msg += ` • ${item}\n`;
447
- }
448
- if (s.blockers.length > 0) {
449
- msg += `\n${bold("🚫 Blockers")}\n`;
450
- for (const item of s.blockers) {
451
- msg += ` • ${item}\n`;
452
- }
453
- }
454
- if (s.metrics && Object.keys(s.metrics).length > 0) {
455
- msg += `${BAR}\n`;
456
- for (const [k, v] of Object.entries(s.metrics)) {
457
- msg += labelValue(k, code(v)) + "\n";
458
- }
459
- }
460
- return msg;
418
+ let msg = `📊 ${bold("Status Roundup")}\n`;
419
+ msg += `${BAR}\n`;
420
+ msg += labelValue("Date", code(s.date)) + "\n";
421
+ msg += labelValue("Agent", code(s.agent)) + "\n";
422
+ msg += `${BAR}\n`;
423
+
424
+ msg += `${bold("✅ Completed")}\n`;
425
+ for (const item of s.workedOn) {
426
+ msg += ` • ${item}\n`;
427
+ }
428
+ msg += `\n${bold("🔄 Next")}\n`;
429
+ for (const item of s.nextUp) {
430
+ msg += ` • ${item}\n`;
431
+ }
432
+ if (s.blockers.length > 0) {
433
+ msg += `\n${bold("🚫 Blockers")}\n`;
434
+ for (const item of s.blockers) {
435
+ msg += ` • ${item}\n`;
436
+ }
437
+ }
438
+ if (s.metrics && Object.keys(s.metrics).length > 0) {
439
+ msg += `${BAR}\n`;
440
+ for (const [k, v] of Object.entries(s.metrics)) {
441
+ msg += labelValue(k, code(v)) + "\n";
442
+ }
443
+ }
444
+ return msg;
461
445
  }
462
446
 
463
447
  // ── Diff / Change Summary ───────────────────────────────────────────
464
448
 
465
449
  export interface DiffSummary {
466
- title: string;
467
- stats: { added: number; removed: number; files: number };
468
- highlights: string[];
469
- url?: string;
450
+ title: string;
451
+ stats: { added: number; removed: number; files: number };
452
+ highlights: string[];
453
+ url?: string;
470
454
  }
471
455
 
472
456
  export function diffSummary(d: DiffSummary): string {
473
- let msg = `📝 ${bold(d.title)}\n`;
474
- msg += `${BAR}\n`;
475
- msg +=
476
- labelValue(
477
- "Changes",
478
- `+${d.stats.added} −${d.stats.removed} in ${d.stats.files} files`,
479
- ) + "\n";
480
- if (d.highlights.length > 0) {
481
- msg += `${BAR}\n`;
482
- for (const h of d.highlights) {
483
- msg += ` • ${h}\n`;
484
- }
485
- }
486
- if (d.url) msg += `\n[View Diff](${d.url})`;
487
- return msg;
457
+ let msg = `📝 ${bold(d.title)}\n`;
458
+ msg += `${BAR}\n`;
459
+ msg +=
460
+ labelValue(
461
+ "Changes",
462
+ `+${d.stats.added} −${d.stats.removed} in ${d.stats.files} files`,
463
+ ) + "\n";
464
+ if (d.highlights.length > 0) {
465
+ msg += `${BAR}\n`;
466
+ for (const h of d.highlights) {
467
+ msg += ` • ${h}\n`;
468
+ }
469
+ }
470
+ if (d.url) msg += `\n${link(d.url, "View Diff")}`;
471
+ return msg;
488
472
  }
489
473
 
490
474
  // ── Quick Pickers (for telegram_ask buttons) ────────────────────────
491
475
 
492
476
  export function quickActions(label: string, actions: string[]): string {
493
- return `${bold(label)}\n${actions.map((a, i) => `${i + 1}\\. ${code(a)}`).join(" \n")}`;
477
+ return `${bold(label)}\n${actions.map((a, i) => `${i + 1}. ${code(a)}`).join(" \n")}`;
494
478
  }
495
479
 
496
480
  export function yesNoExplain(prompt: string): string {
497
- return `❓ ${bold(prompt)}\n\nPick an option below:`;
481
+ return `❓ ${bold(prompt)}\n\nPick an option below:`;
498
482
  }
499
483
 
500
484
  // ── Build Notification (main dispatch for telegram_notify tool) ────
501
485
 
502
- // Flattened input from the tool; only the relevant subset is used per kind.
503
486
  export interface NotifyInput {
504
- kind:
505
- | "issue"
506
- | "pr"
507
- | "task"
508
- | "pipeline"
509
- | "session"
510
- | "alert"
511
- | "factory-job"
512
- | "standup"
513
- | "diff"
514
- | "decision"
515
- | "ack";
516
- // issue
517
- issueNumber?: number;
518
- issueTitle?: string;
519
- issueState?: "open" | "closed";
520
- issueStatus?: string;
521
- issueAssignee?: string;
522
- issueLabels?: string[];
523
- issueMilestone?: string;
524
- issueRepoOwner?: string;
525
- issueRepoName?: string;
526
- issueBranch?: string;
527
- issuePRNumber?: number;
528
- issuePRStatus?: string;
529
- issueUrl?: string;
530
- // pr
531
- prNumber?: number;
532
- prTitle?: string;
533
- prState?: "open" | "closed" | "merged" | "draft";
534
- prAuthor?: string;
535
- prBase?: string;
536
- prHead?: string;
537
- prRepoOwner?: string;
538
- prRepoName?: string;
539
- prCIPassed?: number;
540
- prCIFailed?: number;
541
- prCITotal?: number;
542
- prCIStatus?: string;
543
- prReviewsApproved?: number;
544
- prReviewsRequested?: number;
545
- prReviewsChanges?: number;
546
- prFilesAdded?: number;
547
- prFilesRemoved?: number;
548
- prFilesChanged?: number;
549
- prBreaking?: boolean;
550
- prDraft?: boolean;
551
- prUrl?: string;
552
- // task
553
- taskTitle?: string;
554
- taskEmoji?: string;
555
- taskStatus?: "started" | "in-progress" | "paused" | "blocked" | "complete";
556
- taskProgress?: number;
557
- taskElapsed?: string;
558
- taskEta?: string;
559
- taskDetails?: string[];
560
- taskMetrics?: Record<string, string>;
561
- // pipeline
562
- pipelineWorkflow?: string;
563
- pipelineRunNumber?: number;
564
- pipelineBranch?: string;
565
- pipelineEvent?: string;
566
- pipelineJobs?: Array<{ name: string; status: string; duration?: string }>;
567
- pipelineTriggeredBy?: string;
568
- pipelineUrl?: string;
569
- // session
570
- sessionName?: string;
571
- sessionBranch?: string;
572
- sessionModel?: string;
573
- sessionTurns?: number;
574
- sessionTokensUsed?: number;
575
- sessionTokensTotal?: number;
576
- sessionDuration?: string;
577
- sessionStatus?: "idle" | "streaming" | "tool-exec" | "waiting";
578
- // alert
579
- alertLevel?: "info" | "warning" | "error" | "critical";
580
- alertTitle?: string;
581
- alertSource?: string;
582
- alertDetail?: string;
583
- alertAction?: string;
584
- alertStack?: string;
585
- // factory-job
586
- jobId?: string;
587
- jobMode?: "single" | "chain" | "parallel";
588
- jobStatus?: "pending" | "running" | "success" | "failure" | "cancelled";
589
- jobAgent?: string;
590
- jobTask?: string;
591
- jobEnvironment?: string;
592
- jobDuration?: string;
593
- jobWorker?: string;
594
- // standup
595
- standupDate?: string;
596
- standupAgent?: string;
597
- standupWorkedOn?: string[];
598
- standupNextUp?: string[];
599
- standupBlockers?: string[];
600
- standupMetrics?: Record<string, string>;
601
- // diff
602
- diffTitle?: string;
603
- diffAdded?: number;
604
- diffRemoved?: number;
605
- diffFiles?: number;
606
- diffHighlights?: string[];
607
- diffUrl?: string;
608
- // decision
609
- decisionQuestion?: string;
610
- decisionContext?: string;
611
- decisionOptions?: string[];
612
- decisionRisk?: "low" | "medium" | "high" | "critical";
613
- decisionCommand?: string;
614
- decisionReason?: string;
615
- // ack
616
- ackText?: string;
617
- ackEmoji?: string;
487
+ kind: "issue" | "pr" | "task" | "pipeline" | "session" | "alert" | "factory-job" | "standup" | "diff" | "decision" | "ack";
488
+ issueNumber?: number; issueTitle?: string; issueState?: "open" | "closed";
489
+ issueStatus?: string; issueAssignee?: string; issueLabels?: string[];
490
+ issueMilestone?: string; issueRepoOwner?: string; issueRepoName?: string;
491
+ issueBranch?: string; issuePRNumber?: number; issuePRStatus?: string;
492
+ issueUrl?: string;
493
+ prNumber?: number; prTitle?: string; prState?: "open" | "closed" | "merged" | "draft";
494
+ prAuthor?: string; prBase?: string; prHead?: string;
495
+ prRepoOwner?: string; prRepoName?: string;
496
+ prCIPassed?: number; prCIFailed?: number; prCITotal?: number; prCIStatus?: string;
497
+ prReviewsApproved?: number; prReviewsRequested?: number; prReviewsChanges?: number;
498
+ prFilesAdded?: number; prFilesRemoved?: number; prFilesChanged?: number;
499
+ prBreaking?: boolean; prDraft?: boolean; prUrl?: string;
500
+ taskTitle?: string; taskEmoji?: string;
501
+ taskStatus?: "started" | "in-progress" | "paused" | "blocked" | "complete";
502
+ taskProgress?: number; taskElapsed?: string; taskEta?: string;
503
+ taskDetails?: string[]; taskMetrics?: Record<string, string>;
504
+ pipelineWorkflow?: string; pipelineRunNumber?: number;
505
+ pipelineBranch?: string; pipelineEvent?: string;
506
+ pipelineJobs?: Array<{ name: string; status: string; duration?: string }>;
507
+ pipelineTriggeredBy?: string; pipelineUrl?: string;
508
+ sessionName?: string; sessionBranch?: string; sessionModel?: string;
509
+ sessionTurns?: number; sessionTokensUsed?: number; sessionTokensTotal?: number;
510
+ sessionDuration?: string; sessionStatus?: "idle" | "streaming" | "tool-exec" | "waiting";
511
+ alertLevel?: "info" | "warning" | "error" | "critical";
512
+ alertTitle?: string; alertSource?: string; alertDetail?: string;
513
+ alertAction?: string; alertStack?: string;
514
+ jobId?: string; jobMode?: "single" | "chain" | "parallel";
515
+ jobStatus?: "pending" | "running" | "success" | "failure" | "cancelled";
516
+ jobAgent?: string; jobTask?: string; jobEnvironment?: string;
517
+ jobDuration?: string; jobWorker?: string;
518
+ standupDate?: string; standupAgent?: string;
519
+ standupWorkedOn?: string[]; standupNextUp?: string[];
520
+ standupBlockers?: string[]; standupMetrics?: Record<string, string>;
521
+ diffTitle?: string; diffAdded?: number; diffRemoved?: number;
522
+ diffFiles?: number; diffHighlights?: string[]; diffUrl?: string;
523
+ decisionQuestion?: string; decisionContext?: string;
524
+ decisionOptions?: string[]; decisionRisk?: "low" | "medium" | "high" | "critical";
525
+ decisionCommand?: string; decisionReason?: string;
526
+ ackText?: string; ackEmoji?: string;
618
527
  }
619
528
 
620
529
  export function buildNotification(input: NotifyInput): string {
621
- switch (input.kind) {
622
- case "issue":
623
- return issueCard({
624
- number: input.issueNumber ?? 0,
625
- title: input.issueTitle ?? "Unknown",
626
- state: input.issueState ?? "open",
627
- status: input.issueStatus,
628
- assignee: input.issueAssignee,
629
- labels: input.issueLabels,
630
- milestone: input.issueMilestone,
631
- repo: {
632
- owner: input.issueRepoOwner ?? "unknown",
633
- name: input.issueRepoName ?? "unknown",
634
- },
635
- branch: input.issueBranch,
636
- pr: input.issuePRNumber
637
- ? {
638
- number: input.issuePRNumber,
639
- status: input.issuePRStatus ?? "unknown",
640
- }
641
- : undefined,
642
- url: input.issueUrl,
643
- });
644
-
645
- case "pr":
646
- return prCard({
647
- number: input.prNumber ?? 0,
648
- title: input.prTitle ?? "Unknown",
649
- state: input.prState ?? "open",
650
- author: input.prAuthor ?? "unknown",
651
- base: input.prBase ?? "main",
652
- head: input.prHead ?? "unknown",
653
- repo: {
654
- owner: input.prRepoOwner ?? "unknown",
655
- name: input.prRepoName ?? "unknown",
656
- },
657
- ci:
658
- input.prCITotal !== undefined
659
- ? {
660
- passed: input.prCIPassed ?? 0,
661
- failed: input.prCIFailed ?? 0,
662
- total: input.prCITotal,
663
- status: input.prCIStatus,
664
- }
665
- : undefined,
666
- reviews:
667
- input.prReviewsApproved !== undefined ||
668
- input.prReviewsRequested !== undefined ||
669
- input.prReviewsChanges !== undefined
670
- ? {
671
- approved: input.prReviewsApproved ?? 0,
672
- requested: input.prReviewsRequested ?? 0,
673
- changesRequested: input.prReviewsChanges ?? 0,
674
- }
675
- : undefined,
676
- files:
677
- input.prFilesChanged !== undefined
678
- ? {
679
- added: input.prFilesAdded ?? 0,
680
- removed: input.prFilesRemoved ?? 0,
681
- changed: input.prFilesChanged,
682
- }
683
- : undefined,
684
- breaking: input.prBreaking,
685
- draft: input.prDraft,
686
- url: input.prUrl,
687
- });
688
-
689
- case "task":
690
- return taskProgress({
691
- title: input.taskTitle ?? "Task",
692
- emoji: input.taskEmoji,
693
- status: input.taskStatus ?? "started",
694
- progress: input.taskProgress,
695
- elapsed: input.taskElapsed,
696
- eta: input.taskEta,
697
- details: input.taskDetails,
698
- metrics: input.taskMetrics,
699
- });
700
-
701
- case "pipeline":
702
- return ciPipeline({
703
- workflow: input.pipelineWorkflow ?? "CI",
704
- runNumber: input.pipelineRunNumber ?? 0,
705
- branch: input.pipelineBranch ?? "unknown",
706
- event: input.pipelineEvent ?? "push",
707
- jobs: input.pipelineJobs ?? [],
708
- triggeredBy: input.pipelineTriggeredBy,
709
- url: input.pipelineUrl,
710
- });
711
-
712
- case "session":
713
- return sessionCard({
714
- session: input.sessionName ?? "unknown",
715
- branch: input.sessionBranch,
716
- model: input.sessionModel,
717
- turns: input.sessionTurns,
718
- tokens:
719
- input.sessionTokensTotal !== undefined
720
- ? {
721
- used: input.sessionTokensUsed ?? 0,
722
- total: input.sessionTokensTotal,
723
- }
724
- : undefined,
725
- duration: input.sessionDuration,
726
- status: input.sessionStatus ?? "idle",
727
- });
728
-
729
- case "alert":
730
- return alertCard({
731
- level: input.alertLevel ?? "info",
732
- title: input.alertTitle ?? "Alert",
733
- source: input.alertSource,
734
- detail: input.alertDetail,
735
- action: input.alertAction,
736
- stack: input.alertStack,
737
- });
738
-
739
- case "factory-job":
740
- return factoryJobCard({
741
- jobId: input.jobId ?? "unknown",
742
- mode: input.jobMode ?? "single",
743
- status: input.jobStatus ?? "pending",
744
- agent: input.jobAgent,
745
- task: input.jobTask,
746
- environment: input.jobEnvironment,
747
- duration: input.jobDuration,
748
- worker: input.jobWorker,
749
- });
750
-
751
- case "standup":
752
- return standupCard({
753
- date: input.standupDate ?? "unknown",
754
- agent: input.standupAgent ?? "pi",
755
- workedOn: input.standupWorkedOn ?? [],
756
- nextUp: input.standupNextUp ?? [],
757
- blockers: input.standupBlockers ?? [],
758
- metrics: input.standupMetrics,
759
- });
760
-
761
- case "diff":
762
- return diffSummary({
763
- title: input.diffTitle ?? "Changes",
764
- stats: {
765
- added: input.diffAdded ?? 0,
766
- removed: input.diffRemoved ?? 0,
767
- files: input.diffFiles ?? 0,
768
- },
769
- highlights: input.diffHighlights ?? [],
770
- url: input.diffUrl,
771
- });
772
-
773
- case "decision":
774
- return decisionCard({
775
- question: input.decisionQuestion ?? "Proceed?",
776
- context: input.decisionContext ?? "",
777
- options: input.decisionOptions ?? ["Yes", "No"],
778
- risk: input.decisionRisk ?? "medium",
779
- command: input.decisionCommand,
780
- reason: input.decisionReason,
781
- });
782
-
783
- case "ack":
784
- return ack(input.ackText ?? "Done", input.ackEmoji);
785
-
786
- default:
787
- return "Unknown notification kind";
788
- }
530
+ switch (input.kind) {
531
+ case "issue":
532
+ return issueCard({
533
+ number: input.issueNumber ?? 0,
534
+ title: input.issueTitle ?? "Unknown",
535
+ state: input.issueState ?? "open",
536
+ status: input.issueStatus,
537
+ assignee: input.issueAssignee,
538
+ labels: input.issueLabels,
539
+ milestone: input.issueMilestone,
540
+ repo: {
541
+ owner: input.issueRepoOwner ?? "unknown",
542
+ name: input.issueRepoName ?? "unknown",
543
+ },
544
+ branch: input.issueBranch,
545
+ pr: input.issuePRNumber
546
+ ? { number: input.issuePRNumber, status: input.issuePRStatus ?? "unknown" }
547
+ : undefined,
548
+ url: input.issueUrl,
549
+ });
550
+
551
+ case "pr":
552
+ return prCard({
553
+ number: input.prNumber ?? 0,
554
+ title: input.prTitle ?? "Unknown",
555
+ state: input.prState ?? "open",
556
+ author: input.prAuthor ?? "unknown",
557
+ base: input.prBase ?? "main",
558
+ head: input.prHead ?? "unknown",
559
+ repo: {
560
+ owner: input.prRepoOwner ?? "unknown",
561
+ name: input.prRepoName ?? "unknown",
562
+ },
563
+ ci:
564
+ input.prCITotal !== undefined
565
+ ? {
566
+ passed: input.prCIPassed ?? 0,
567
+ failed: input.prCIFailed ?? 0,
568
+ total: input.prCITotal,
569
+ status: input.prCIStatus,
570
+ }
571
+ : undefined,
572
+ reviews:
573
+ input.prReviewsApproved !== undefined ||
574
+ input.prReviewsRequested !== undefined ||
575
+ input.prReviewsChanges !== undefined
576
+ ? {
577
+ approved: input.prReviewsApproved ?? 0,
578
+ requested: input.prReviewsRequested ?? 0,
579
+ changesRequested: input.prReviewsChanges ?? 0,
580
+ }
581
+ : undefined,
582
+ files:
583
+ input.prFilesChanged !== undefined
584
+ ? {
585
+ added: input.prFilesAdded ?? 0,
586
+ removed: input.prFilesRemoved ?? 0,
587
+ changed: input.prFilesChanged,
588
+ }
589
+ : undefined,
590
+ breaking: input.prBreaking,
591
+ draft: input.prDraft,
592
+ url: input.prUrl,
593
+ });
594
+
595
+ case "task":
596
+ return taskProgress({
597
+ title: input.taskTitle ?? "Task",
598
+ emoji: input.taskEmoji,
599
+ status: input.taskStatus ?? "started",
600
+ progress: input.taskProgress,
601
+ elapsed: input.taskElapsed,
602
+ eta: input.taskEta,
603
+ details: input.taskDetails,
604
+ metrics: input.taskMetrics,
605
+ });
606
+
607
+ case "pipeline":
608
+ return ciPipeline({
609
+ workflow: input.pipelineWorkflow ?? "CI",
610
+ runNumber: input.pipelineRunNumber ?? 0,
611
+ branch: input.pipelineBranch ?? "unknown",
612
+ event: input.pipelineEvent ?? "push",
613
+ jobs: input.pipelineJobs ?? [],
614
+ triggeredBy: input.pipelineTriggeredBy,
615
+ url: input.pipelineUrl,
616
+ });
617
+
618
+ case "session":
619
+ return sessionCard({
620
+ session: input.sessionName ?? "unknown",
621
+ branch: input.sessionBranch,
622
+ model: input.sessionModel,
623
+ turns: input.sessionTurns,
624
+ tokens:
625
+ input.sessionTokensTotal !== undefined
626
+ ? { used: input.sessionTokensUsed ?? 0, total: input.sessionTokensTotal }
627
+ : undefined,
628
+ duration: input.sessionDuration,
629
+ status: input.sessionStatus ?? "idle",
630
+ });
631
+
632
+ case "alert":
633
+ return alertCard({
634
+ level: input.alertLevel ?? "info",
635
+ title: input.alertTitle ?? "Alert",
636
+ source: input.alertSource,
637
+ detail: input.alertDetail,
638
+ action: input.alertAction,
639
+ stack: input.alertStack,
640
+ });
641
+
642
+ case "factory-job":
643
+ return factoryJobCard({
644
+ jobId: input.jobId ?? "unknown",
645
+ mode: input.jobMode ?? "single",
646
+ status: input.jobStatus ?? "pending",
647
+ agent: input.jobAgent,
648
+ task: input.jobTask,
649
+ environment: input.jobEnvironment,
650
+ duration: input.jobDuration,
651
+ worker: input.jobWorker,
652
+ });
653
+
654
+ case "standup":
655
+ return standupCard({
656
+ date: input.standupDate ?? "unknown",
657
+ agent: input.standupAgent ?? "pi",
658
+ workedOn: input.standupWorkedOn ?? [],
659
+ nextUp: input.standupNextUp ?? [],
660
+ blockers: input.standupBlockers ?? [],
661
+ metrics: input.standupMetrics,
662
+ });
663
+
664
+ case "diff":
665
+ return diffSummary({
666
+ title: input.diffTitle ?? "Changes",
667
+ stats: {
668
+ added: input.diffAdded ?? 0,
669
+ removed: input.diffRemoved ?? 0,
670
+ files: input.diffFiles ?? 0,
671
+ },
672
+ highlights: input.diffHighlights ?? [],
673
+ url: input.diffUrl,
674
+ });
675
+
676
+ case "decision":
677
+ return decisionCard({
678
+ question: input.decisionQuestion ?? "Proceed?",
679
+ context: input.decisionContext ?? "",
680
+ options: input.decisionOptions ?? ["Yes", "No"],
681
+ risk: input.decisionRisk ?? "medium",
682
+ command: input.decisionCommand,
683
+ reason: input.decisionReason,
684
+ });
685
+
686
+ case "ack":
687
+ return ack(input.ackText ?? "Done", input.ackEmoji);
688
+
689
+ default:
690
+ return "Unknown notification kind";
691
+ }
789
692
  }