daily-focus 0.2.0-dev.14 → 1.1.0-dev.1

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.
@@ -0,0 +1,859 @@
1
+ import {
2
+ QUICK_ACTIONS
3
+ } from "./chunk-F3EHZWRS.js";
4
+ import {
5
+ dateFromNow,
6
+ sampleBrief,
7
+ sampleGeneratedAt
8
+ } from "./chunk-Q5OKXJXT.js";
9
+ import {
10
+ PROFILES,
11
+ localDateKey
12
+ } from "./chunk-OPASVYLR.js";
13
+ import "./chunk-BC2W7NND.js";
14
+
15
+ // src/demo.ts
16
+ import { mkdir, writeFile } from "node:fs/promises";
17
+ import { join } from "node:path";
18
+ var LOGIN = "mara-l";
19
+ var JIRA_SITE = "acme.atlassian.net";
20
+ var JIRA_ACCOUNT = "mara@acme.example";
21
+ var MINUTE = 6e4;
22
+ var hoursAgo = (now, hours) => new Date(now.getTime() - hours * 60 * MINUTE).toISOString();
23
+ var daysAgo = (now, days) => hoursAgo(now, days * 24);
24
+ function minutesAgo(now, minutes) {
25
+ const start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 1).getTime();
26
+ return new Date(Math.max(start, now.getTime() - minutes * MINUTE)).toISOString();
27
+ }
28
+ function dayBack(now, days) {
29
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate() - days, now.getHours(), now.getMinutes());
30
+ }
31
+ function workingDayBack(now, n) {
32
+ let date = dayBack(now, 0);
33
+ let left = n;
34
+ while (left > 0) {
35
+ date = dayBack(date, 1);
36
+ const weekday = date.getDay();
37
+ if (weekday !== 0 && weekday !== 6) left--;
38
+ }
39
+ return date;
40
+ }
41
+ function timeOn(date, hour, minute = 0) {
42
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate(), hour, minute).toISOString();
43
+ }
44
+ function demoProfile(raw) {
45
+ const chosen = (raw ?? "").trim().toLowerCase() || "work";
46
+ if (!PROFILES.includes(chosen)) {
47
+ throw new Error(`--profile must be one of ${PROFILES.join(", ")}, got ${JSON.stringify(raw)}`);
48
+ }
49
+ return chosen;
50
+ }
51
+ function demoEnv(base, store, profile) {
52
+ const work = profile === "work";
53
+ return {
54
+ ...base,
55
+ DAILY_FOCUS_DATA: store,
56
+ // Any free port unless one was asked for, so the demo never collides with a
57
+ // dashboard already running on 4321.
58
+ DAILY_FOCUS_PORT: base.DAILY_FOCUS_PORT ?? "0",
59
+ DAILY_FOCUS_PROFILE: profile,
60
+ DAILY_FOCUS_AGENT: "off",
61
+ DAILY_FOCUS_AGENT_AT: "",
62
+ DAILY_FOCUS_AGENT_DAYS: "",
63
+ DAILY_FOCUS_ASSISTANT: "off",
64
+ DAILY_FOCUS_CALENDAR: "off",
65
+ DAILY_FOCUS_GITHUB: "on",
66
+ DAILY_FOCUS_GITHUB_ACCOUNTS: "",
67
+ DAILY_FOCUS_GITHUB_SCOPE: work ? "acme" : "",
68
+ DAILY_FOCUS_GITHUB_MERGE_GATE_CHECKS: work ? "merge-policy" : "",
69
+ DAILY_FOCUS_GITHUB_POLL_MINUTES: "",
70
+ DAILY_FOCUS_JIRA: work ? "on" : "off",
71
+ DAILY_FOCUS_JIRA_PROJECTS: work ? "PROJ, PLAT" : "",
72
+ DAILY_FOCUS_JIRA_HOLD_STATUSES: work ? "Blocked" : "",
73
+ DAILY_FOCUS_JIRA_IN_PROGRESS_STATUSES: work ? "In Progress, In Review" : "",
74
+ DAILY_FOCUS_JIRA_SITE: "",
75
+ DAILY_FOCUS_JIRA_POLL_MINUTES: "",
76
+ DAILY_FOCUS_FREE_WINDOWS: "",
77
+ DAILY_FOCUS_WORK_START: "",
78
+ DAILY_FOCUS_WORK_END: "",
79
+ DAILY_FOCUS_MIN_FREE_WINDOW: "",
80
+ DAILY_FOCUS_SESSION_MINUTES: "",
81
+ DAILY_FOCUS_STALE_AFTER_HOURS: ""
82
+ };
83
+ }
84
+ function pull(seed) {
85
+ const { repo, number, title, createdAt } = seed;
86
+ return {
87
+ id: `github:pr:${repo}#${number}`,
88
+ account: LOGIN,
89
+ url: `https://github.com/${repo}/pull/${number}`,
90
+ isDraft: false,
91
+ readyAt: createdAt,
92
+ updatedAt: seed.lastActivityByOthers?.at ?? seed.lastActivityByYou ?? createdAt,
93
+ headRef: `${LOGIN}/${title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40)}`,
94
+ baseRef: "main",
95
+ reviewDecision: null,
96
+ checks: "success",
97
+ failingChecks: [],
98
+ mergeable: "MERGEABLE",
99
+ mergeStateStatus: "CLEAN",
100
+ pendingChecks: [],
101
+ cancelledChecks: [],
102
+ autoMerge: false,
103
+ requestedReviewers: [],
104
+ reviews: [],
105
+ lastActivityByYou: createdAt,
106
+ lastActivityByOthers: null,
107
+ ...seed
108
+ };
109
+ }
110
+ function demoPulls(profile, now = /* @__PURE__ */ new Date()) {
111
+ if (profile === "personal") return personalPulls(now);
112
+ return [
113
+ // Waiting on you: red CI, a comment since your push, and a conflict.
114
+ pull({
115
+ repo: "acme/webapp",
116
+ number: 3402,
117
+ title: "Add trace context to fetch instrumentation",
118
+ createdAt: daysAgo(now, 1),
119
+ reviewDecision: "REVIEW_REQUIRED",
120
+ checks: "failure",
121
+ failingChecks: ["unit-tests (node-20)"],
122
+ mergeStateStatus: "UNSTABLE",
123
+ lastActivityByYou: hoursAgo(now, 3),
124
+ requestedReviewers: ["eli"]
125
+ }),
126
+ pull({
127
+ repo: "acme/webapp",
128
+ number: 3398,
129
+ title: "Retry failed beacon sends with backoff",
130
+ createdAt: daysAgo(now, 4),
131
+ reviewDecision: "REVIEW_REQUIRED",
132
+ mergeStateStatus: "BLOCKED",
133
+ lastActivityByYou: daysAgo(now, 1),
134
+ lastActivityByOthers: { at: hoursAgo(now, 3), login: "dana", kind: "comment" }
135
+ }),
136
+ pull({
137
+ repo: "acme/infra",
138
+ number: 512,
139
+ title: "Pin the Node 22 base image",
140
+ createdAt: daysAgo(now, 6),
141
+ reviewDecision: "APPROVED",
142
+ reviews: [{ login: "eli", state: "APPROVED", at: daysAgo(now, 2) }],
143
+ mergeable: "CONFLICTING",
144
+ mergeStateStatus: "DIRTY",
145
+ lastActivityByYou: daysAgo(now, 3),
146
+ lastActivityByOthers: { at: daysAgo(now, 2), login: "eli", kind: "review" }
147
+ }),
148
+ // Ready to merge.
149
+ pull({
150
+ repo: "acme/webapp",
151
+ number: 3410,
152
+ title: "Drop the IE11 shims from the v6 bundles",
153
+ createdAt: daysAgo(now, 2),
154
+ reviewDecision: "APPROVED",
155
+ reviews: [{ login: "chris", state: "APPROVED", at: hoursAgo(now, 2) }],
156
+ lastActivityByYou: daysAgo(now, 1),
157
+ lastActivityByOthers: { at: hoursAgo(now, 2), login: "chris", kind: "review" }
158
+ }),
159
+ // Waiting on reviewers: one long enough to ask, one fresh.
160
+ pull({
161
+ repo: "acme/webapp",
162
+ number: 3395,
163
+ title: "Bump chrome-launcher to 1.2.0",
164
+ createdAt: daysAgo(now, 3),
165
+ reviewDecision: "REVIEW_REQUIRED",
166
+ requestedReviewers: ["bob"],
167
+ mergeStateStatus: "BLOCKED"
168
+ }),
169
+ pull({
170
+ repo: "acme/docs",
171
+ number: 77,
172
+ title: "Document the sampling knobs",
173
+ createdAt: hoursAgo(now, 5),
174
+ reviewDecision: "REVIEW_REQUIRED",
175
+ requestedReviewers: ["dana", "eli"],
176
+ mergeStateStatus: "BLOCKED"
177
+ }),
178
+ // Waiting on the merge gate: approved, and the repository's own policy check still pending.
179
+ pull({
180
+ repo: "acme/webapp",
181
+ number: 3418,
182
+ title: "Instrument long tasks on the main thread",
183
+ createdAt: daysAgo(now, 2),
184
+ reviewDecision: "APPROVED",
185
+ reviews: [{ login: "alice", state: "APPROVED", at: hoursAgo(now, 22) }],
186
+ checks: "pending",
187
+ pendingChecks: [{ name: "merge-policy", kind: "status-context", detailsUrl: "https://github.com/acme/webapp/pull/3418/checks" }],
188
+ mergeStateStatus: "BLOCKED",
189
+ lastActivityByYou: hoursAgo(now, 20),
190
+ lastActivityByOthers: { at: hoursAgo(now, 22), login: "alice", kind: "review" }
191
+ }),
192
+ // Merge blocked by GitHub, with nothing pending to blame.
193
+ pull({
194
+ repo: "acme/infra",
195
+ number: 509,
196
+ title: "Rotate the staging deploy key",
197
+ createdAt: daysAgo(now, 2),
198
+ reviews: [{ login: "eli", state: "APPROVED", at: daysAgo(now, 1) }],
199
+ mergeStateStatus: "BLOCKED",
200
+ lastActivityByYou: daysAgo(now, 1),
201
+ lastActivityByOthers: { at: daysAgo(now, 1), login: "eli", kind: "review" }
202
+ }),
203
+ // Waiting on checks.
204
+ pull({
205
+ repo: "acme/webapp",
206
+ number: 3425,
207
+ title: "Stream session replay chunks instead of buffering them",
208
+ createdAt: hoursAgo(now, 26),
209
+ checks: "pending",
210
+ pendingChecks: [
211
+ { name: "e2e (chromium)", kind: "check-run", detailsUrl: "https://github.com/acme/webapp/actions/runs/88120451", checkRunId: 88120451 },
212
+ { name: "e2e (webkit)", kind: "check-run", detailsUrl: "https://github.com/acme/webapp/actions/runs/88120452", checkRunId: 88120452 }
213
+ ],
214
+ cancelledChecks: [{ name: "visual-regression", kind: "check-run", detailsUrl: "https://github.com/acme/webapp/actions/runs/88120449" }],
215
+ mergeStateStatus: "BLOCKED",
216
+ lastActivityByYou: hoursAgo(now, 1)
217
+ }),
218
+ // Drafts: one from last night, one forgotten for weeks, one parked on purpose.
219
+ pull({
220
+ repo: "acme/webapp",
221
+ number: 3430,
222
+ title: "Add a per-tab replay buffer cap behind a flag",
223
+ createdAt: hoursAgo(now, 14),
224
+ isDraft: true
225
+ }),
226
+ pull({
227
+ repo: "acme/webapp",
228
+ number: 3350,
229
+ title: "Spike: speculative prefetch for the checkout bundle",
230
+ createdAt: daysAgo(now, 24),
231
+ isDraft: true,
232
+ lastActivityByYou: daysAgo(now, 22)
233
+ }),
234
+ pull({
235
+ repo: "acme/webapp",
236
+ number: 3360,
237
+ title: "Experiment: WASM decoder for replay payloads",
238
+ createdAt: daysAgo(now, 12),
239
+ isDraft: true,
240
+ lastActivityByYou: daysAgo(now, 9)
241
+ })
242
+ ];
243
+ }
244
+ function personalPulls(now) {
245
+ return [
246
+ pull({
247
+ repo: "mara-l/pedalboard",
248
+ number: 51,
249
+ title: "Fix clipping on the looper\u2019s overdub path",
250
+ createdAt: daysAgo(now, 2),
251
+ checks: "failure",
252
+ failingChecks: ["build (macos)"],
253
+ mergeStateStatus: "UNSTABLE",
254
+ lastActivityByYou: hoursAgo(now, 15)
255
+ }),
256
+ pull({
257
+ repo: "mara-l/birdfeeder-cam",
258
+ number: 12,
259
+ title: "Move the firmware to ESP-IDF 5.2",
260
+ createdAt: daysAgo(now, 9),
261
+ mergeable: "CONFLICTING",
262
+ mergeStateStatus: "DIRTY",
263
+ lastActivityByYou: daysAgo(now, 5)
264
+ }),
265
+ pull({
266
+ repo: "openbird/feeder-firmware",
267
+ number: 204,
268
+ title: "Add the Danish species list",
269
+ createdAt: daysAgo(now, 5),
270
+ reviewDecision: "REVIEW_REQUIRED",
271
+ requestedReviewers: ["kasper-ob"],
272
+ mergeStateStatus: "BLOCKED"
273
+ }),
274
+ pull({
275
+ repo: "mara-l/pedalboard",
276
+ number: 49,
277
+ title: "Tuner: move pitch detection to an AudioWorklet",
278
+ createdAt: hoursAgo(now, 40),
279
+ checks: "pending",
280
+ pendingChecks: [{ name: "build (linux)", kind: "check-run", detailsUrl: "https://github.com/mara-l/pedalboard/actions/runs/4410233" }],
281
+ mergeStateStatus: "BLOCKED",
282
+ lastActivityByYou: hoursAgo(now, 2)
283
+ }),
284
+ pull({
285
+ repo: "mara-l/pedalboard",
286
+ number: 50,
287
+ title: "Add a metronome click track",
288
+ createdAt: hoursAgo(now, 30),
289
+ isDraft: true
290
+ }),
291
+ pull({
292
+ repo: "mara-l/dotfiles",
293
+ number: 9,
294
+ title: "Try the new terminal config",
295
+ createdAt: daysAgo(now, 6),
296
+ isDraft: true
297
+ })
298
+ ];
299
+ }
300
+ var TICKET_STATUSES = {
301
+ PROJ: ["To Do", "In Progress", "In Review", "Blocked", "Done"],
302
+ PLAT: ["Backlog", "Committed", "In Progress", "In Review", "Done"]
303
+ };
304
+ function categoryOf(status) {
305
+ const name = status.trim().toLowerCase();
306
+ if (name === "done") return "done";
307
+ if (["to do", "backlog", "committed"].includes(name)) return "new";
308
+ return "indeterminate";
309
+ }
310
+ function ticket(key, summary, workflowStatus, issueType, prs) {
311
+ return {
312
+ id: `jira:${key}`,
313
+ key,
314
+ summary,
315
+ workflowStatus,
316
+ statusCategory: categoryOf(workflowStatus),
317
+ issueType,
318
+ url: `https://${JIRA_SITE}/browse/${key}`,
319
+ hasAnyPr: prs !== "none",
320
+ hasOpenPr: prs === "open",
321
+ allPrsClosed: prs === "closed"
322
+ };
323
+ }
324
+ function demoTickets() {
325
+ return {
326
+ tickets: [
327
+ // Every pull request closed, and the ticket not Done.
328
+ ticket("PROJ-8842", "Add trace context to outgoing fetches", "In Review", "Task", "closed"),
329
+ ticket("PLAT-311", "Remove the legacy beacon endpoint", "In Progress", "Task", "closed"),
330
+ // A pull request open while the status says the work has not begun.
331
+ ticket("PROJ-8871", "Instrument long tasks on the main thread", "To Do", "Story", "open"),
332
+ ticket("PROJ-8877", "Document the sampling knobs", "To Do", "Task", "open"),
333
+ // In progress with no code linked to it.
334
+ ticket("PROJ-8865", "Write the SDK dogfooding guide", "In Progress", "Task", "none"),
335
+ ticket("PLAT-298", "Audit third-party script load order", "In Progress", "Bug", "none"),
336
+ ticket("PLAT-305", "Deprecate the v5 snippet", "In Progress", "Task", "none"),
337
+ // A deliberate hold: in the configured hold status, so not flagged.
338
+ ticket("PROJ-8850", "Migrate to the new consent API", "Blocked", "Story", "none"),
339
+ // Going as they should.
340
+ ticket("PROJ-8858", "Cap the session replay buffer on long-lived tabs", "In Progress", "Bug", "open"),
341
+ ticket("PROJ-8869", "Update the replay fixtures for the new chunk format", "In Progress", "Sub-task", "open"),
342
+ ticket("PROJ-8860", "Drop the IE11 shims from the v6 bundles", "In Review", "Task", "open"),
343
+ ticket("PLAT-320", "Evaluate the OpenTelemetry browser SDK", "Backlog", "Story", "none")
344
+ ],
345
+ statuses: Object.fromEntries(Object.entries(TICKET_STATUSES).map(([project, names]) => [project, [...names]]))
346
+ };
347
+ }
348
+ function demoIntegrations(profile) {
349
+ const world = demoTickets();
350
+ return {
351
+ github: {
352
+ async listAccounts() {
353
+ return [{ login: LOGIN, active: true }];
354
+ },
355
+ async token() {
356
+ return "demo";
357
+ },
358
+ async fetch() {
359
+ return { login: LOGIN, pulls: demoPulls(profile, /* @__PURE__ */ new Date()), warnings: [], orgs: {}, rateLimitRemaining: null };
360
+ }
361
+ },
362
+ jira: {
363
+ async identity() {
364
+ return { site: JIRA_SITE, account: JIRA_ACCOUNT };
365
+ },
366
+ async fetch() {
367
+ return { tickets: world.tickets.map((entry) => ({ ...entry })), statuses: world.statuses, warnings: [] };
368
+ },
369
+ async transition(_acli, key, status) {
370
+ const found = world.tickets.find((entry) => entry.key === key);
371
+ if (!found) throw new Error(`no work item ${key} in the sample data`);
372
+ const project = key.split("-")[0] ?? "";
373
+ if (!(world.statuses[project] ?? []).includes(status)) {
374
+ throw new Error(`${project} has no status called ${status}`);
375
+ }
376
+ found.workflowStatus = status;
377
+ found.statusCategory = categoryOf(status);
378
+ }
379
+ }
380
+ };
381
+ }
382
+ var FOCUS = {
383
+ work: `---
384
+ objective: Get the staging checkout path working again, so the browser SDK can be dogfooded
385
+ blocker: The session replay fix in webapp#3421 is still waiting on review
386
+ ---
387
+
388
+ Restore the staging path first; everything downstream of it is blocked on dogfooding.
389
+
390
+ <!-- agent-only -->
391
+ The SDK launch is on the Q4 roadmap review on the 24th, and Dana presents it. If
392
+ staging isn't usable by then the date slips a quarter. Weigh the day against that,
393
+ but don't put it on screen.
394
+ `,
395
+ personal: `---
396
+ objective: Get the house ready for winter \u2014 gutters cleared, boiler serviced, loft insulation booked
397
+ blocker: The loft insulation quote from Nordvarme still hasn't arrived
398
+ ---
399
+
400
+ Boiler is done. Gutters are booked for the 18th. The insulation is the one thing still open.
401
+
402
+ <!-- agent-only -->
403
+ The insulation comes out of what's left of the renovation savings, about 40,000 kr. If
404
+ the quote lands above that the job waits until spring. Rank accordingly; keep the
405
+ amount off the board.
406
+ `
407
+ };
408
+ var SOURCES = {
409
+ work: `# Sources
410
+
411
+ The personal half of your morning brief. The prompt next to this file
412
+ (\`prompt.md\`) says *what* to gather and how to judge it; this file says *who* and
413
+ *where*. The briefing agent reads it; the dashboard shows it only in its editor.
414
+
415
+ ## Identity
416
+
417
+ - Name, as meeting notes and Doc assignments write it: \`Mara Lindqvist\`
418
+ - Work email: \`mara@acme.example\`
419
+ - GitHub login: \`mara-l\` \u2014 review requests and authorship are judged against it
420
+
421
+ ## Calendars
422
+
423
+ Query:
424
+
425
+ - the primary work calendar
426
+ - \`Platform team\` \u2014 the shared team calendar; its events block time too
427
+
428
+ Never query or include: \`Lunch roulette\`.
429
+
430
+ ## Holiday calendars
431
+
432
+ - \`Holidays in United States\` \u2014 most of the webapp team is there; a holiday there
433
+ means slow replies, and it is worth saying so.
434
+ - \`Holidays in Denmark\` \u2014 call one out today, or within the next 14 days.
435
+
436
+ ## Recurring meeting notes
437
+
438
+ - https://docs.google.com/document/d/1xK9sampleP1atf0rmTeamN0tes/edit?tab=t.0 \u2014 Platform
439
+ team weekly; only the newest dated section is current
440
+ - https://docs.google.com/document/d/1xK9sampleSdkLaunchP1an/edit?tab=t.3 \u2014 SDK launch
441
+ plan, the "Open questions" tab
442
+
443
+ ## Jira and GitHub
444
+
445
+ - Jira projects: \`PROJ\` (webapp) and \`PLAT\` (platform). \`Blocked\` is a deliberate hold.
446
+ - GitHub organisation: \`acme\`. The board already shows every open pull request;
447
+ raise only what needs judgement today.
448
+ `,
449
+ personal: `# Sources
450
+
451
+ The private half of your morning brief. The prompt next to this file
452
+ (\`prompt.md\`) says *what* to gather and how to judge it; this file says *who* and
453
+ *where*. The briefing agent reads it; the dashboard shows it only in its editor.
454
+
455
+ ## Identity
456
+
457
+ - Name: \`Mara Lindqvist\`
458
+ - Personal email: \`mara@lindqvist.example\`
459
+ - GitHub login: \`mara-l\` \u2014 review requests and authorship are judged against it
460
+
461
+ ## My weeks
462
+
463
+ - Workdays end at 16:00; personal time is from then until 22:00.
464
+ - Daycare pickup by 16:45 on Mondays, Tuesdays and Thursdays. Sam does the other two.
465
+ - Every other week Noa has football on Wednesday; the \`Family\` calendar says which.
466
+
467
+ ## Calendars
468
+
469
+ - \`Personal\`
470
+ - \`Family\` \u2014 shared with Sam; its events block time too
471
+
472
+ Never query or include: \`Sam \u2014 work\`.
473
+
474
+ ## Holiday calendars
475
+
476
+ - \`Holidays in Denmark\` \u2014 including school holidays; a school week off changes who
477
+ needs looking after.
478
+
479
+ ## Email
480
+
481
+ The Gmail connector reaches \`mara@lindqvist.example\`. Always raise mail from Noa's
482
+ school and from the kommune. Skip newsletters and the running club's digest.
483
+
484
+ ## e-Boks
485
+
486
+ The \`eboks\` CLI, logged in as me. Read letters from the kommune, Skattestyrelsen and
487
+ the pension fund first.
488
+
489
+ ## Reminders
490
+
491
+ The Apple Reminders connector; read the \`Home\` list only.
492
+
493
+ ## Messages
494
+
495
+ The Apple Messages connector. Skip the "Lindqvist family \u{1F4F8}" group, which is photos.
496
+
497
+ ## GitHub
498
+
499
+ My own projects: \`mara-l/pedalboard\`, \`mara-l/dotfiles\`, \`mara-l/birdfeeder-cam\`.
500
+ Anything I have open elsewhere is a contribution, and ranks below those.
501
+ `
502
+ };
503
+ function finishedObjectiveItem(profile, now) {
504
+ return profile === "work" ? {
505
+ id: "github:pr:acme/webapp#3388",
506
+ source: "github",
507
+ kind: "task",
508
+ title: "Land the checkout fix behind a feature flag",
509
+ url: "https://github.com/acme/webapp/pull/3388",
510
+ firstSeen: dateFromNow(now, -7),
511
+ advancesObjective: true
512
+ } : {
513
+ id: "email:thread:19a07a1b22",
514
+ source: "email",
515
+ kind: "task",
516
+ title: "Book the boiler service before the heating season",
517
+ url: "https://mail.google.com/mail/u/0/#inbox/19a07a1b22",
518
+ firstSeen: dateFromNow(now, -8),
519
+ advancesObjective: true
520
+ };
521
+ }
522
+ function objectiveDoneAt(profile, now) {
523
+ return profile === "work" ? timeOn(workingDayBack(now, 2), 15, 40) : timeOn(workingDayBack(now, 1), 19, 10);
524
+ }
525
+ function demoArchive(brief, profile, now) {
526
+ const extra = finishedObjectiveItem(profile, now);
527
+ const doneAt = objectiveDoneAt(profile, now);
528
+ const briefs = [];
529
+ for (const back of [3, 2, 1]) {
530
+ const day = workingDayBack(now, back);
531
+ const key = localDateKey(day);
532
+ const items = brief.items.filter((item) => item.firstSeen !== void 0 && item.firstSeen <= key);
533
+ if (extra.firstSeen !== void 0 && extra.firstSeen <= key && timeOn(day, 6, 36) <= doneAt) items.unshift(extra);
534
+ briefs.push({
535
+ version: 1,
536
+ generatedAt: timeOn(day, 6, 36),
537
+ generatedBy: brief.generatedBy,
538
+ date: key,
539
+ items
540
+ });
541
+ }
542
+ return briefs;
543
+ }
544
+ function demoActions(profile, now) {
545
+ const until = (days) => dateFromNow(now, days);
546
+ const actions = profile === "work" ? [
547
+ { id: "github:pr:acme/webapp#3388", action: "done", at: objectiveDoneAt(profile, now) },
548
+ { id: "jira:PLAT-305", action: "note", text: "Waiting on the comms plan from marketing", at: daysAgo(now, 3) },
549
+ { id: "jira:PLAT-305", action: "snooze", until: until(30), at: daysAgo(now, 3) },
550
+ { id: "github:pr:acme/webapp#3395", action: "note", text: "Asked Bob in #webapp-dev", at: daysAgo(now, 2) },
551
+ { id: "github:pr:acme/webapp#3360", action: "note", text: "Revisit after the v6 cut", at: daysAgo(now, 1) },
552
+ { id: "github:pr:acme/webapp#3360", action: "snooze", until: until(30), at: daysAgo(now, 1) },
553
+ { id: "jira:PROJ-8865", action: "note", text: "Docs only \u2014 no pull request expected", at: daysAgo(now, 1) },
554
+ { id: "github:pr:acme/webapp#3430", action: "done", at: minutesAgo(now, 140) },
555
+ { id: "email:thread:18f31c0d9e", action: "note", text: "Registration runs to the 28th; no rush", at: minutesAgo(now, 131) },
556
+ { id: "email:thread:18f31c0d9e", action: "snooze", until: until(7), at: minutesAgo(now, 130) },
557
+ { id: "slack:msg:C04ABCDE/1757480412.118", action: "done", at: minutesAgo(now, 55) },
558
+ {
559
+ id: "email:thread:18f2a9c4b7",
560
+ action: "note",
561
+ text: "Told Bob on Slack I\u2019ll send the split after my 1:1 with Alice",
562
+ at: minutesAgo(now, 50)
563
+ }
564
+ ] : [
565
+ { id: "email:thread:19a07a1b22", action: "done", at: objectiveDoneAt(profile, now) },
566
+ { id: "email:thread:19a0c4e2f1", action: "note", text: "Called on Tuesday, left a voicemail", at: daysAgo(now, 2) },
567
+ { id: "github:pr:openbird/feeder-firmware#204", action: "note", text: "Pinged Kasper on the project Discord", at: daysAgo(now, 1) },
568
+ { id: "github:pr:mara-l/dotfiles#9", action: "snooze", until: until(14), at: daysAgo(now, 1) },
569
+ { id: "email:thread:19a0b9f0a2", action: "done", at: minutesAgo(now, 70) },
570
+ {
571
+ id: "eboks:message:4417911204",
572
+ action: "note",
573
+ text: "Deductions look right; check again once the holiday pay is in",
574
+ at: minutesAgo(now, 61)
575
+ },
576
+ { id: "eboks:message:4417911204", action: "snooze", until: until(7), at: minutesAgo(now, 60) },
577
+ { id: "messages:thread:chat7731/9A3D4B6C-1E22", action: "done", at: minutesAgo(now, 25) }
578
+ ];
579
+ return actions.sort((a, b) => a.at.localeCompare(b.at));
580
+ }
581
+ function demoSessions(profile, now) {
582
+ const session = (id, title, startMinutesAgo, actualMinutes, advancesObjective, endedBy) => ({
583
+ id,
584
+ title,
585
+ startedAt: minutesAgo(now, startMinutesAgo),
586
+ endedAt: minutesAgo(now, startMinutesAgo - actualMinutes),
587
+ plannedMinutes: 25,
588
+ actualMinutes,
589
+ reachedTarget: actualMinutes >= 25,
590
+ advancesObjective,
591
+ endedBy
592
+ });
593
+ return profile === "work" ? [
594
+ session("github:pr:acme/webapp#3402", "CI failing on your PR: Add trace context to fetch instrumentation", 190, 22, false, "away"),
595
+ session("gtasks:task:MTU3NDkyODkwMjE2NDcx", "Write up the staging repro steps for the checkout breakage", 150, 42, true, "user")
596
+ ] : [session("email:thread:19a0c4e2f1", "Chase Nordvarme for the loft insulation quote", 80, 18, true, "user")];
597
+ }
598
+ function runLines(seed) {
599
+ const endedAt = new Date(new Date(seed.startedAt).getTime() + seed.seconds * 1e3).toISOString();
600
+ const lines2 = [{ event: "started", run: seed.id, cli: "codex", trigger: seed.trigger, at: seed.startedAt }];
601
+ if (seed.error) {
602
+ lines2.push({ event: "failed", run: seed.id, sessionId: null, error: seed.error, report: "", messages: seed.messages, at: endedAt });
603
+ return lines2;
604
+ }
605
+ const report = seed.messages.at(-1) ?? "";
606
+ lines2.push({ event: "finished", run: seed.id, sessionId: `sample-${seed.id}`, report, messages: seed.messages, at: endedAt });
607
+ if (seed.question) {
608
+ const turn = `${seed.id}-q1`;
609
+ const replied = new Date(new Date(seed.question.at).getTime() + 48e3).toISOString();
610
+ lines2.push({ event: "asked", run: seed.id, turn, question: seed.question.text, at: seed.question.at });
611
+ lines2.push({ event: "replied", run: seed.id, turn, reply: seed.question.reply, at: replied });
612
+ }
613
+ return lines2;
614
+ }
615
+ function demoAgentLog(profile, now) {
616
+ const todaySeconds = profile === "work" ? 388 : 251;
617
+ const today = new Date(new Date(sampleGeneratedAt(now)).getTime() - todaySeconds * 1e3).toISOString();
618
+ const runs = profile === "work" ? [
619
+ {
620
+ id: "run-3",
621
+ startedAt: timeOn(workingDayBack(now, 3), 6, 30),
622
+ seconds: 71,
623
+ trigger: "schedule",
624
+ messages: ["Reading `focus.md`, the action log and the previous brief."],
625
+ error: "codex exited with code 1: network is unreachable"
626
+ },
627
+ {
628
+ id: "run-2",
629
+ startedAt: timeOn(workingDayBack(now, 2), 11, 20),
630
+ seconds: 318,
631
+ trigger: "hand",
632
+ messages: [
633
+ "A rerun: the previous brief is from 06:30 today. Keeping every `firstSeen` from it.",
634
+ "Calendar: the all-hands moved to tomorrow, which frees 14:00\u201317:00 today. Rewriting the headline around that.",
635
+ "Wrote **16 items** for today, 3 with a priority. The afternoon is now free from 14:00; the headline says so, and the checkout repro write-up is the thing to spend it on. Dropped 2 as handled since this morning. Could not reach: nothing."
636
+ ]
637
+ },
638
+ {
639
+ id: "run-1",
640
+ startedAt: timeOn(workingDayBack(now, 1), 6, 30),
641
+ seconds: 402,
642
+ trigger: "schedule",
643
+ messages: [
644
+ "Reading `focus.md`, the action log and the previous brief. Not a first run.",
645
+ "Calendar: 3 events, no clashes. Day ends at 17:00.",
646
+ "Wrote **17 items** for today, 3 with a priority: the compliance training, the review request on `webapp#3421`, and the Dublin travel approval. Dropped 2 as handled. Could not reach: nothing."
647
+ ]
648
+ },
649
+ {
650
+ id: "run-0",
651
+ startedAt: today,
652
+ seconds: todaySeconds,
653
+ trigger: "schedule",
654
+ messages: [
655
+ "Reading `focus.md`, the action log and the previous brief. The objective is the staging checkout path; the log has 4 done, 1 dismissed and a snooze that has not arrived yet.",
656
+ "Calendar: 5 events today, one clash at 11:00 \u2014 your 1:1 with Alice overlaps the design review. The lunch & learn is marked free. Day ends at 17:00.",
657
+ "Gmail: 31 threads in the window, 4 need something from you. GitHub: a review request from Alice and a red check on `#3402` are worth raising; the rest is on the board. Jira: PROJ-8842 is still open with both pull requests merged.",
658
+ "Slack: one direct question from Chris in #webapp-dev. Workday: the compliance deadline is today and the Berlin expenses are still in draft. Writing `items.json`.",
659
+ [
660
+ "Wrote **20 items** for today, 3 with a priority. Not a first run.",
661
+ "",
662
+ "- **#1** the compliance training in Workday, due tonight \u2014 the third reminder, and it escalates after today.",
663
+ "- **#2** Alice\u2019s review request on `webapp#3421`; merging it unblocks the staging checkout path, so it carries `advancesObjective`.",
664
+ "- **#3** the Q4 headcount thread, where Bob followed up again yesterday.",
665
+ "",
666
+ "Dropped as handled: 3 \u2014 the Dublin hotel thread and the on-call swap you marked done, and the vendor security questionnaire you dismissed. Carried forward with their original `firstSeen`: 11, the Berlin expense report now at 8 days.",
667
+ "",
668
+ "Left out on purpose: the 12 open pull requests already on the board, and PROJ-8871\u2019s status, which the ticket board flags itself.",
669
+ "",
670
+ "Could not reach: the **Platform team meeting notes** \u2014 the document returned 403 to the connector. If Tuesday\u2019s meeting left you action items, they are not here."
671
+ ].join("\n")
672
+ ],
673
+ question: {
674
+ at: minutesAgo(now, 95),
675
+ text: "Why isn\u2019t the Berlin expense report one of the priorities?",
676
+ reply: "It has a deadline, but five days out, and nothing escalates before then. The three priorities each have a deadline today or a person waiting on you now. It is on the list with its age showing \u2014 8 days \u2014 which is the nag it has earned. If the finance cut-off is earlier than Workday says, leave a note on the item and I will rank it higher tomorrow."
677
+ }
678
+ }
679
+ ] : [
680
+ {
681
+ id: "run-2",
682
+ startedAt: timeOn(workingDayBack(now, 2), 6, 30),
683
+ seconds: 240,
684
+ trigger: "schedule",
685
+ messages: [
686
+ "Reading `focus.md`, the action log and the previous brief.",
687
+ "Wrote **14 items**, 2 with a priority: the boiler service, which moves the objective, and the parents\u2019 evening reply. e-Boks had nothing new. Could not reach: nothing."
688
+ ]
689
+ },
690
+ {
691
+ id: "run-1",
692
+ startedAt: timeOn(workingDayBack(now, 1), 6, 30),
693
+ seconds: 265,
694
+ trigger: "schedule",
695
+ messages: [
696
+ "Reading `focus.md`, the action log and the previous brief. The boiler service was marked done last night \u2014 the objective moved.",
697
+ "Wrote **15 items**, 3 with a priority: the daycare invoice, the insulation quote, and Ida\u2019s dentist. Dropped 1 as handled. Could not reach: nothing."
698
+ ]
699
+ },
700
+ {
701
+ id: "run-0",
702
+ startedAt: today,
703
+ seconds: todaySeconds,
704
+ trigger: "schedule",
705
+ messages: [
706
+ "Reading `focus.md`, the action log and yesterday\u2019s brief. Objective: the house ready for winter; blocker: the insulation quote.",
707
+ "Calendars: Personal and Family read; 6 events, one clash at 16:30 \u2014 the insurance call overlaps the daycare pickup. Sam is away all day. No `dayStart` or `dayEnd`, as the profile asks.",
708
+ "e-Boks: 5 letters in the window. The daycare invoice and the tax assessment ask for something; three are confirmations. Gmail: 3 threads need you. Messages: Jonas\u2019s question and the photos you promised Priya.",
709
+ [
710
+ "Wrote **17 items** for today, 3 with a priority. Not a first run.",
711
+ "",
712
+ "- **#1** the daycare invoice from the kommune, due Friday \u2014 a late fee follows the deadline.",
713
+ "- **#2** chasing Nordvarme for the insulation quote: the only thing today that moves the objective, and quiet for nine days.",
714
+ "- **#3** Jonas\u2019s question about Saturday, unanswered since Tuesday.",
715
+ "",
716
+ "e-Boks: 2 letters raised, 3 skipped as confirmations. Messages: 2 obligations, both yours. Reminders: the Home list has not been touched since May, so it is one `info` item rather than seven.",
717
+ "",
718
+ "Dropped as handled: 2 \u2014 the boiler service and the parents\u2019 evening thread. Could not reach: nothing; the Family calendar was read through Sam\u2019s share."
719
+ ].join("\n")
720
+ ],
721
+ question: {
722
+ at: minutesAgo(now, 75),
723
+ text: "Is the tax assessment really not urgent?",
724
+ reply: "The correction deadline is 20 days out and the letter asks for a check, not a payment. It is on the list and will age. If you would rather deal with it on a particular day, snooze it to that day and it comes back then."
725
+ }
726
+ }
727
+ ];
728
+ return runs.flatMap(runLines);
729
+ }
730
+ function turnLines(seed) {
731
+ const quick = seed.action ? QUICK_ACTIONS.find((candidate) => candidate.id === seed.action) : void 0;
732
+ const request = quick ? seed.text ? `${quick.request}
733
+
734
+ ${seed.text}` : quick.request : seed.text ?? "";
735
+ const endedAt = new Date(new Date(seed.at).getTime() + seed.seconds * 1e3).toISOString();
736
+ return [
737
+ { event: "started", turn: seed.id, item: seed.item, agent: "claude", request, action: quick?.id ?? null, at: seed.at },
738
+ { event: "finished", turn: seed.id, sessionId: `sample-${seed.item}`, reply: seed.reply, at: endedAt }
739
+ ];
740
+ }
741
+ function demoAssistantLog(profile, now) {
742
+ const fence = "```";
743
+ const turns = profile === "work" ? [
744
+ {
745
+ id: "turn-a1",
746
+ item: "github:pr:acme/webapp#3421",
747
+ action: "summarise-pr",
748
+ at: minutesAgo(now, 46),
749
+ seconds: 52,
750
+ reply: [
751
+ "**What it changes.** Session replay keeps every recorded mutation batch in an array on the recorder for the life of the tab, and only ever appends; on a tab left open for a day that is hundreds of megabytes. The change caps the buffer per tab, drops batches once the ingest endpoint has acknowledged them, and holds DOM node references through a `WeakRef` so a detached subtree can be collected.",
752
+ "",
753
+ "**Why.** The leak is the top crash signature on long-lived checkout tabs in staging, and it is what blocks dogfooding the SDK there.",
754
+ "",
755
+ "**Likely pushback**",
756
+ "",
757
+ "1. The flush-on-acknowledge path has no test for a lost acknowledgement: a batch the network dropped is now gone rather than retried.",
758
+ "2. `WeakRef` is fine in every supported browser, but the fallback branch for older Safari is untested and dead in CI.",
759
+ "3. The cap is a constant, `MAX_BATCHES = 200`. A reviewer will ask for it to be configurable, or at least justified in a comment.",
760
+ "",
761
+ "The first two paragraphs are paste-ready as a description; the three points are what I would raise if I were reviewing."
762
+ ].join("\n")
763
+ },
764
+ {
765
+ id: "turn-a2",
766
+ item: "github:pr:acme/webapp#3421",
767
+ action: null,
768
+ text: "Is the Safari fallback actually reachable?",
769
+ at: minutesAgo(now, 44),
770
+ seconds: 31,
771
+ reply: "Only on Safari 14.0 and below, which the SDK\u2019s support matrix dropped in v5. Under the current matrix the branch is dead code. I would ask Alice to remove it rather than test it."
772
+ },
773
+ {
774
+ id: "turn-b1",
775
+ item: "email:thread:18f2a9c4b7",
776
+ action: "draft-reply",
777
+ at: minutesAgo(now, 38),
778
+ seconds: 64,
779
+ reply: [
780
+ "I read the thread \u2014 four messages, Bob twice. He wants the Q4 team split: how many of the six open roles go to Platform and how many to Webapp, with a line of justification each, by the end of the week so it lands in the planning deck.",
781
+ "",
782
+ "I have created a Gmail draft on the thread, from you, proposing four Platform and two Webapp with the reasons from the planning doc, and asking whether the deck can carry a footnote that the SDK hire depends on the Q3 numbers. It is **not sent**; it is waiting on the thread when you open it."
783
+ ].join("\n")
784
+ },
785
+ {
786
+ id: "turn-c1",
787
+ item: "github:pr:acme/webapp#3402",
788
+ action: "explain-ci",
789
+ at: minutesAgo(now, 20),
790
+ seconds: 58,
791
+ reply: [
792
+ "The failing check is `unit-tests (node-20)`, in `tracing/fetch.spec.ts`:",
793
+ "",
794
+ fence,
795
+ "expected traceparent header on the retried request",
796
+ ' Expected: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"',
797
+ " Received: undefined",
798
+ fence,
799
+ "",
800
+ "Your change attaches the trace context when a fetch is first issued, but the retry path in `fetch.ts` builds a fresh `Request` from the original URL and loses the headers. The smallest fix is to copy the original request\u2019s headers onto the retry \u2014 one line in `retryFetch`. The other three jobs are green, so this is yours rather than flaky infrastructure."
801
+ ].join("\n")
802
+ }
803
+ ] : [
804
+ {
805
+ id: "turn-p1",
806
+ item: "github:pr:mara-l/pedalboard#47",
807
+ action: "summarise-pr",
808
+ at: minutesAgo(now, 52),
809
+ seconds: 47,
810
+ reply: [
811
+ "Tomas adds MIDI clock sync to the looper: the transport listens for MIDI clock at 24 pulses per quarter note and for start and stop, derives the tempo, and quantises the loop start to the next beat. +212/\u221218 across three files, with a test for the tempo derivation.",
812
+ "",
813
+ "Worth raising before merging:",
814
+ "",
815
+ "1. The clock listener is never removed when the looper is destroyed, so switching patches leaks a listener per switch.",
816
+ "2. Tempo is derived from the last 24 ticks only, which will jitter on a flaky USB connection; a short rolling average would settle it.",
817
+ "3. Nothing says what happens when the clock stops mid-loop.",
818
+ "",
819
+ "A kind review that asks for the first two and lets the third go is probably right for a first contribution."
820
+ ].join("\n")
821
+ },
822
+ {
823
+ id: "turn-p2",
824
+ item: "email:thread:19a0c4e2f1",
825
+ action: "draft-reply",
826
+ at: minutesAgo(now, 30),
827
+ seconds: 41,
828
+ reply: "The thread is three messages: your request on the 28th, their \u201Cwithin the week\u201D on the 29th, and nothing since. I have created a Gmail draft on it that asks, briefly and politely, whether the quote is still coming this week, and says the other quotes are in and you would like to decide by Friday. It is **not sent**; open the thread to read it."
829
+ }
830
+ ];
831
+ return turns.flatMap(turnLines);
832
+ }
833
+ var lines = (records) => records.map((record) => `${JSON.stringify(record)}
834
+ `).join("");
835
+ async function writeDemoStore(store, profile, now = /* @__PURE__ */ new Date()) {
836
+ const brief = sampleBrief(now, profile);
837
+ await mkdir(join(store, "archive"), { recursive: true });
838
+ await writeFile(join(store, "items.json"), `${JSON.stringify(brief, null, 2)}
839
+ `, { encoding: "utf8", flag: "wx" });
840
+ for (const past of demoArchive(brief, profile, now)) {
841
+ await writeFile(join(store, "archive", `items-${past.date}.json`), `${JSON.stringify(past, null, 2)}
842
+ `, "utf8");
843
+ }
844
+ await writeFile(join(store, "focus.md"), FOCUS[profile], "utf8");
845
+ await writeFile(join(store, "sources.md"), SOURCES[profile], "utf8");
846
+ await writeFile(join(store, "actions.jsonl"), lines(demoActions(profile, now)), "utf8");
847
+ await writeFile(join(store, "sessions.jsonl"), lines(demoSessions(profile, now)), "utf8");
848
+ await writeFile(join(store, "agent.jsonl"), lines(demoAgentLog(profile, now)), "utf8");
849
+ await writeFile(join(store, "assistant.jsonl"), lines(demoAssistantLog(profile, now)), "utf8");
850
+ }
851
+ export {
852
+ demoEnv,
853
+ demoIntegrations,
854
+ demoProfile,
855
+ demoPulls,
856
+ demoTickets,
857
+ workingDayBack,
858
+ writeDemoStore
859
+ };