daily-focus 1.2.0-dev.5 → 1.2.0-dev.8

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/README.md CHANGED
@@ -64,13 +64,6 @@ since you completed something that advanced your objective.
64
64
  - Mark tasks **Done** when you've handled them.
65
65
  - **Snooze** an item until a date, or **Dismiss** it if it doesn't need attention.
66
66
  - Leave a **Note** for the next brief, such as "already replied on Slack".
67
- - Press **f** for focus mode, which shows your top item and agenda.
68
- - Press **p** on an item to start a focus timer. Its countdown appears in the tab
69
- title, and the session becomes part of the next brief's context.
70
-
71
- The timer keeps counting after its target time. On macOS, it ends a session when
72
- you've been away from the machine, excluding the time you were away. On systems
73
- without idle detection, a two-hour limit caps the recorded session.
74
67
 
75
68
  You can undo item actions except notes. The page updates as new data arrives, and
76
69
  the theme button switches between system, light, and dark mode.
@@ -90,8 +83,10 @@ The second tab shows your open pull requests in seven buckets:
90
83
  | Drafts | Pull requests you haven't marked ready for review. |
91
84
 
92
85
  Park a pull request until a date, leave a note, or click **Nudged** after asking
93
- for a review. Nudged records a note; it doesn't send a message. Completing a task
94
- about a pull request in the brief doesn't remove that pull request from the board.
86
+ for a review. Nudged records that you asked; it doesn't send a message. The pull
87
+ request moves to a Nudged section for a day, then goes back to Waiting on reviewers
88
+ with "time to ask" if nobody has reviewed it. Completing a task about a pull
89
+ request in the brief doesn't remove that pull request from the board.
95
90
 
96
91
  Cancelled checks appear as "cancelled, no verdict" rather than failures.
97
92
  When a check is rerun, the board uses its newest workflow run or attempt.
@@ -111,6 +106,14 @@ Draft pull requests count as open. Closed pull requests don't prove that a ticke
111
106
  is finished, so the board leaves that decision to you. Park a reminder or leave a
112
107
  note when there's more work planned.
113
108
 
109
+ Jira can take days to hear that a pull request merged, so the board also checks
110
+ GitHub. It searches your open pull requests and the ones closed in the last 60
111
+ days, and matches them to tickets by the key in the title, branch, description
112
+ or commit messages. An open pull request on GitHub always counts. GitHub can
113
+ also mark a ticket's pull requests as all closed, but only when it found at least
114
+ as many as Jira has linked. A row that GitHub changed lists those pull requests
115
+ under **GitHub, ahead of Jira**.
116
+
114
117
  Switch to **Working on** to see your in-progress tickets grouped by status,
115
118
  including ones that also need a status check. You can narrow this view in
116
119
  [SETUP.md](SETUP.md#jira).
@@ -167,8 +170,6 @@ Press **?** in the dashboard to see the shortcuts.
167
170
  | `a` | Open the panel, ready to ask the assistant |
168
171
  | `o` | Open the source link |
169
172
  | `u` | Undo the last action |
170
- | `f` | Toggle focus mode |
171
- | `p` | Start / stop a focus session |
172
173
  | `r` | Refresh the current board |
173
174
  | `w` | Switch Jira views |
174
175
  | `,` | Settings |
package/SETUP.md CHANGED
@@ -148,8 +148,8 @@ Set `DAILY_FOCUS_AGENT_MODEL` and `DAILY_FOCUS_AGENT_EFFORT` if the CLI's own
148
148
  defaults are not what you want for a brief; a brief is worth a high effort. If the
149
149
  server can't find the CLI, set `DAILY_FOCUS_AGENT_BIN`.
150
150
 
151
- The agent writes `items.json`. The dashboard records your actions and focus sessions
152
- for the next run to read.
151
+ The agent writes `items.json`. The dashboard records your actions for the next run
152
+ to read.
153
153
 
154
154
  After the first run, read its report (click "updated … ago"): it says which sources
155
155
  it couldn't reach. To check the brief against the payload contract, run:
@@ -237,6 +237,11 @@ Clicking a status in the dashboard writes one transition to Jira. All other Jira
237
237
  access is read-only. See the [ticket board guide](README.md#the-jira-ticket-board)
238
238
  for workflow limits and undo behavior.
239
239
 
240
+ The board also asks GitHub about your pull requests, because Jira can learn of
241
+ them days late. It uses the pull request board's accounts and scope, and nothing
242
+ else needs to be configured. With `DAILY_FOCUS_GITHUB=off`, the ticket board uses
243
+ Jira alone.
244
+
240
245
  Set `DAILY_FOCUS_JIRA=off` to disable the board.
241
246
 
242
247
  ### Live calendar on macOS
@@ -456,7 +461,7 @@ DAILY_FOCUS_PROFILE=personal
456
461
  The dashboard then links the personal prompt, `prompts/morning-brief-personal.md`,
457
462
  into the store as `prompt.md`, and the source list starts from a template with
458
463
  sections for those sources. The profile also switches off
459
- the Jira board and away detection, and gives the tab a green house icon instead of the blue briefcase. The pull request
464
+ the Jira board, and gives the tab a green house icon instead of the blue briefcase. The pull request
460
465
  board stays on for side projects; point it at your personal account with
461
466
  `DAILY_FOCUS_GITHUB_ACCOUNTS`, or set `DAILY_FOCUS_GITHUB=off`.
462
467
 
@@ -517,9 +522,7 @@ matters when `DAILY_FOCUS_FREE_WINDOWS` is off.
517
522
  | `DAILY_FOCUS_MIN_FREE_WINDOW` | `45` | Minutes before a gap counts as a focus window |
518
523
  | `DAILY_FOCUS_STALE_AFTER_HOURS` | `24` | When to expect a refresh. Shows an informational message for 45 minutes before warning that the agent may not have run. Counted only in hours a run was due, so days off never trip it |
519
524
  | `DAILY_FOCUS_AGENT_DAYS` | `1-5` | Weekdays the dashboard runs the agent on, cron-style and cron-numbered: `1-5` for Monday to Friday, `0-4` for Sunday to Thursday, `0,6` for a weekend-only run |
520
- | `DAILY_FOCUS_SESSION_MINUTES` | `25` | Default focus session length |
521
- | `DAILY_FOCUS_AWAY_AFTER` | `10`; `0` for personal | Minutes of an untouched machine before a session is closed at the last sign of life. `0` turns it off |
522
- | `DAILY_FOCUS_GITHUB` | `on` | `off` disables the pull request board; nothing is polled |
525
+ | `DAILY_FOCUS_GITHUB` | `on` | `off` disables the pull request board, and the ticket board stops checking GitHub; nothing is polled |
523
526
  | `DAILY_FOCUS_GITHUB_ACCOUNTS` | *gh's active account* | Logins to poll as, comma-separated, each resolved with `gh auth token --user` |
524
527
  | `DAILY_FOCUS_GITHUB_SCOPE` | *everything* | Organisations or `owner/repo` entries to limit the board to, comma-separated |
525
528
  | `DAILY_FOCUS_GITHUB_MERGE_GATE_CHECKS` | *none* | Check names that stand for a repository's whole merge policy, comma-separated and matched exactly. Their rows get the *Waiting on merge gate* bucket |
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  readSystemZone,
3
3
  startingZone
4
- } from "./chunk-AIQO2YAI.js";
4
+ } from "./chunk-6SMUSPZG.js";
5
5
  import {
6
6
  linkState
7
7
  } from "./chunk-QKT2FAB5.js";
@@ -11,8 +11,9 @@ import {
11
11
  parseBrief,
12
12
  readArchiveIndex,
13
13
  resolveItems,
14
- runContractChecks
15
- } from "./chunk-26JG3D5C.js";
14
+ runContractChecks,
15
+ setsStatus
16
+ } from "./chunk-GQ3NCWRT.js";
16
17
  import {
17
18
  BRIEF_REFRESH_GRACE_HOURS,
18
19
  describeSchedule,
@@ -22,7 +23,7 @@ import {
22
23
  resolveSchedule,
23
24
  runsOn,
24
25
  workingMsBetween
25
- } from "./chunk-RNE27FQY.js";
26
+ } from "./chunk-KZNY45XW.js";
26
27
  import {
27
28
  command
28
29
  } from "./chunk-BC2W7NND.js";
@@ -180,7 +181,7 @@ if (!previous) {
180
181
  const reproduced = [...previousIds].filter((id) => currentIds.has(id));
181
182
  const dropped = [...previousIds].filter((id) => !currentIds.has(id));
182
183
  const added = [...currentIds].filter((id) => !previousIds.has(id));
183
- const actedOn = new Set(actions.filter((a) => a.action !== "note").map((a) => a.id));
184
+ const actedOn = new Set(actions.filter(setsStatus).map((a) => a.id));
184
185
  const addedFingerprints = new Map(added.map((id) => [fingerprintId(id), id]));
185
186
  const handled = dropped.filter((id) => actedOn.has(id));
186
187
  const drifted = dropped.filter((id) => !actedOn.has(id) && addedFingerprints.has(fingerprintId(id)));
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  writeJsonAtomic
3
- } from "./chunk-26JG3D5C.js";
3
+ } from "./chunk-GQ3NCWRT.js";
4
4
  import {
5
5
  localTimeZone
6
- } from "./chunk-RNE27FQY.js";
6
+ } from "./chunk-KZNY45XW.js";
7
7
 
8
8
  // src/zone.ts
9
9
  import { readFile, readlink } from "node:fs/promises";
@@ -14,7 +14,7 @@ import {
14
14
  startOfLocalDay,
15
15
  workingDaysSince,
16
16
  workingMsBetween
17
- } from "./chunk-RNE27FQY.js";
17
+ } from "./chunk-KZNY45XW.js";
18
18
 
19
19
  // src/fs.ts
20
20
  import { mkdir, rename, writeFile } from "node:fs/promises";
@@ -38,101 +38,6 @@ function fingerprintId(id) {
38
38
  return id.toLowerCase().replace(/[^a-z0-9]/g, "");
39
39
  }
40
40
 
41
- // src/archive.ts
42
- import { readFile, readdir } from "node:fs/promises";
43
- import { resolve } from "node:path";
44
- var ARCHIVE_FILE = /^items-(\d{4}-\d{2}-\d{2})\.json$/;
45
- var MAX_ARCHIVE_FILES = 130;
46
- var RECENT_WINDOW_DAYS = 14;
47
- function archivePath(config, dateKey) {
48
- return resolve(config.archiveDir, `items-${dateKey}.json`);
49
- }
50
- function briefDateKey(brief) {
51
- if (brief.date && /^\d{4}-\d{2}-\d{2}$/.test(brief.date)) return brief.date;
52
- const generated = parseISO(brief.generatedAt);
53
- return localDateKey(generated ?? /* @__PURE__ */ new Date());
54
- }
55
- async function archiveBrief(config, brief) {
56
- const dateKey = briefDateKey(brief);
57
- const target = archivePath(config, dateKey);
58
- try {
59
- const existing = JSON.parse(await readFile(target, "utf8"));
60
- const existingAt = parseISO(existing.generatedAt)?.getTime() ?? 0;
61
- const incomingAt = parseISO(brief.generatedAt)?.getTime() ?? 0;
62
- if (existingAt >= incomingAt) return false;
63
- } catch {
64
- }
65
- await writeJsonAtomic(target, brief);
66
- return true;
67
- }
68
- async function listArchivedDates(config) {
69
- let names;
70
- try {
71
- names = await readdir(config.archiveDir);
72
- } catch {
73
- return [];
74
- }
75
- return names.map((name) => ARCHIVE_FILE.exec(name)?.[1]).filter((date) => date !== void 0).sort();
76
- }
77
- async function readArchiveIndex(config) {
78
- const index = /* @__PURE__ */ new Map();
79
- const dated = (await listArchivedDates(config)).reverse().slice(0, MAX_ARCHIVE_FILES);
80
- for (const date of dated) {
81
- let brief;
82
- try {
83
- brief = JSON.parse(await readFile(archivePath(config, date), "utf8"));
84
- } catch {
85
- continue;
86
- }
87
- for (const item of brief.items ?? []) {
88
- if (!item?.id) continue;
89
- const existing = index.get(item.id);
90
- if (!existing) {
91
- index.set(item.id, {
92
- title: item.title ?? item.id,
93
- advancesObjective: item.advancesObjective === true,
94
- firstSeen: item.firstSeen
95
- });
96
- continue;
97
- }
98
- if (item.firstSeen && (!existing.firstSeen || item.firstSeen < existing.firstSeen)) {
99
- existing.firstSeen = item.firstSeen;
100
- }
101
- }
102
- }
103
- return index;
104
- }
105
- function computeObjectiveProgress(index, actions, now, counts = isWorkingDay) {
106
- const lastAction = /* @__PURE__ */ new Map();
107
- for (const action of actions) {
108
- if (action.action === "note") continue;
109
- lastAction.set(canonicalId(action.id), action);
110
- }
111
- const byCanonical = /* @__PURE__ */ new Map();
112
- for (const [id, entry] of index) byCanonical.set(canonicalId(id), entry);
113
- let lastAt = null;
114
- let lastTitle = null;
115
- let recentCount = 0;
116
- for (const [id, action] of lastAction) {
117
- if (action.action !== "done") continue;
118
- if (!byCanonical.get(id)?.advancesObjective) continue;
119
- const at = parseISO(action.at);
120
- if (!at) continue;
121
- if (workingDaysSince(at, now, counts) <= RECENT_WINDOW_DAYS) recentCount++;
122
- if (!lastAt || at.getTime() > lastAt.getTime()) {
123
- lastAt = at;
124
- lastTitle = byCanonical.get(id)?.title ?? null;
125
- }
126
- }
127
- return {
128
- workingDaysSince: lastAt ? workingDaysSince(lastAt, now, counts) : null,
129
- lastAt: lastAt ? lastAt.toISOString() : null,
130
- lastTitle,
131
- recentCount,
132
- windowDays: RECENT_WINDOW_DAYS
133
- };
134
- }
135
-
136
41
  // src/validate.ts
137
42
  var SOURCES = /* @__PURE__ */ new Set([
138
43
  "github",
@@ -300,6 +205,10 @@ function parseBrief(text) {
300
205
  if (raw.dayEnd !== void 0 && !dayEnd) warnings.push('items.json has an unreadable "dayEnd"; using the default');
301
206
  return { brief, error: null, warnings };
302
207
  }
208
+ var ACTION_TYPES = ["done", "snooze", "dismiss", "reopen", "note", "nudge"];
209
+ function setsStatus(action) {
210
+ return action.action !== "note" && action.action !== "nudge";
211
+ }
303
212
  function parseActionLine(line) {
304
213
  const trimmed = line.trim();
305
214
  if (trimmed === "") return null;
@@ -313,7 +222,7 @@ function parseActionLine(line) {
313
222
  const id = optionalString(raw.id);
314
223
  const action = optionalString(raw.action);
315
224
  if (!id || !action) return null;
316
- if (!["done", "snooze", "dismiss", "reopen", "note"].includes(action)) return null;
225
+ if (!ACTION_TYPES.includes(action)) return null;
317
226
  const result = {
318
227
  id,
319
228
  action,
@@ -326,6 +235,101 @@ function parseActionLine(line) {
326
235
  return result;
327
236
  }
328
237
 
238
+ // src/archive.ts
239
+ import { readFile, readdir } from "node:fs/promises";
240
+ import { resolve } from "node:path";
241
+ var ARCHIVE_FILE = /^items-(\d{4}-\d{2}-\d{2})\.json$/;
242
+ var MAX_ARCHIVE_FILES = 130;
243
+ var RECENT_WINDOW_DAYS = 14;
244
+ function archivePath(config, dateKey) {
245
+ return resolve(config.archiveDir, `items-${dateKey}.json`);
246
+ }
247
+ function briefDateKey(brief) {
248
+ if (brief.date && /^\d{4}-\d{2}-\d{2}$/.test(brief.date)) return brief.date;
249
+ const generated = parseISO(brief.generatedAt);
250
+ return localDateKey(generated ?? /* @__PURE__ */ new Date());
251
+ }
252
+ async function archiveBrief(config, brief) {
253
+ const dateKey = briefDateKey(brief);
254
+ const target = archivePath(config, dateKey);
255
+ try {
256
+ const existing = JSON.parse(await readFile(target, "utf8"));
257
+ const existingAt = parseISO(existing.generatedAt)?.getTime() ?? 0;
258
+ const incomingAt = parseISO(brief.generatedAt)?.getTime() ?? 0;
259
+ if (existingAt >= incomingAt) return false;
260
+ } catch {
261
+ }
262
+ await writeJsonAtomic(target, brief);
263
+ return true;
264
+ }
265
+ async function listArchivedDates(config) {
266
+ let names;
267
+ try {
268
+ names = await readdir(config.archiveDir);
269
+ } catch {
270
+ return [];
271
+ }
272
+ return names.map((name) => ARCHIVE_FILE.exec(name)?.[1]).filter((date) => date !== void 0).sort();
273
+ }
274
+ async function readArchiveIndex(config) {
275
+ const index = /* @__PURE__ */ new Map();
276
+ const dated = (await listArchivedDates(config)).reverse().slice(0, MAX_ARCHIVE_FILES);
277
+ for (const date of dated) {
278
+ let brief;
279
+ try {
280
+ brief = JSON.parse(await readFile(archivePath(config, date), "utf8"));
281
+ } catch {
282
+ continue;
283
+ }
284
+ for (const item of brief.items ?? []) {
285
+ if (!item?.id) continue;
286
+ const existing = index.get(item.id);
287
+ if (!existing) {
288
+ index.set(item.id, {
289
+ title: item.title ?? item.id,
290
+ advancesObjective: item.advancesObjective === true,
291
+ firstSeen: item.firstSeen
292
+ });
293
+ continue;
294
+ }
295
+ if (item.firstSeen && (!existing.firstSeen || item.firstSeen < existing.firstSeen)) {
296
+ existing.firstSeen = item.firstSeen;
297
+ }
298
+ }
299
+ }
300
+ return index;
301
+ }
302
+ function computeObjectiveProgress(index, actions, now, counts = isWorkingDay) {
303
+ const lastAction = /* @__PURE__ */ new Map();
304
+ for (const action of actions) {
305
+ if (!setsStatus(action)) continue;
306
+ lastAction.set(canonicalId(action.id), action);
307
+ }
308
+ const byCanonical = /* @__PURE__ */ new Map();
309
+ for (const [id, entry] of index) byCanonical.set(canonicalId(id), entry);
310
+ let lastAt = null;
311
+ let lastTitle = null;
312
+ let recentCount = 0;
313
+ for (const [id, action] of lastAction) {
314
+ if (action.action !== "done") continue;
315
+ if (!byCanonical.get(id)?.advancesObjective) continue;
316
+ const at = parseISO(action.at);
317
+ if (!at) continue;
318
+ if (workingDaysSince(at, now, counts) <= RECENT_WINDOW_DAYS) recentCount++;
319
+ if (!lastAt || at.getTime() > lastAt.getTime()) {
320
+ lastAt = at;
321
+ lastTitle = byCanonical.get(id)?.title ?? null;
322
+ }
323
+ }
324
+ return {
325
+ workingDaysSince: lastAt ? workingDaysSince(lastAt, now, counts) : null,
326
+ lastAt: lastAt ? lastAt.toISOString() : null,
327
+ lastTitle,
328
+ recentCount,
329
+ windowDays: RECENT_WINDOW_DAYS
330
+ };
331
+ }
332
+
329
333
  // src/checks.ts
330
334
  var GENEROUS_FLAG_THRESHOLD = 5;
331
335
  function runContractChecks({ brief, items, focus, history }) {
@@ -386,172 +390,9 @@ function findLeakedPhrase(brief, focus) {
386
390
  return null;
387
391
  }
388
392
 
389
- // src/sessions.ts
390
- import { appendFile, mkdir as mkdir2, readFile as readFile2, unlink } from "node:fs/promises";
391
- var MAX_SESSION_MINUTES = 120;
392
- var MIN_LOGGABLE_MINUTES = 1;
393
- async function readActive(config) {
394
- try {
395
- return JSON.parse(await readFile2(config.sessionFile, "utf8"));
396
- } catch {
397
- return null;
398
- }
399
- }
400
- async function writeActive(config, session) {
401
- await writeJsonAtomic(config.sessionFile, session);
402
- }
403
- async function clearActive(config) {
404
- try {
405
- await unlink(config.sessionFile);
406
- } catch {
407
- }
408
- }
409
- async function logSession(config, session, endedAt, endedBy) {
410
- const startedAt = parseISO(session.startedAt);
411
- if (!startedAt) return null;
412
- const actualMinutes = Math.round((endedAt.getTime() - startedAt.getTime()) / 6e4);
413
- if (actualMinutes < MIN_LOGGABLE_MINUTES) return null;
414
- const entry = {
415
- id: session.id,
416
- title: session.title,
417
- startedAt: session.startedAt,
418
- endedAt: endedAt.toISOString(),
419
- plannedMinutes: session.minutes,
420
- actualMinutes,
421
- reachedTarget: actualMinutes >= session.minutes,
422
- advancesObjective: session.advancesObjective,
423
- endedBy
424
- };
425
- await mkdir2(config.dataDir, { recursive: true });
426
- await appendFile(config.sessionsLogFile, `${JSON.stringify(entry)}
427
- `, "utf8");
428
- return entry;
429
- }
430
- async function startSession(config, opts) {
431
- const now = opts.now ?? /* @__PURE__ */ new Date();
432
- await stopSession(config, now);
433
- await writeActive(config, {
434
- id: opts.id,
435
- title: opts.title,
436
- startedAt: now.toISOString(),
437
- minutes: opts.minutes,
438
- advancesObjective: opts.advancesObjective,
439
- // The click that got here is itself the first evidence of a person.
440
- lastActiveAt: now.toISOString()
441
- });
442
- }
443
- async function stopSession(config, now = /* @__PURE__ */ new Date(), endedBy = "user") {
444
- const active = await readActive(config);
445
- if (!active) return null;
446
- await logSession(config, active, now, endedBy);
447
- await clearActive(config);
448
- return active;
449
- }
450
- async function reconcileSession(config, probe, now = /* @__PURE__ */ new Date()) {
451
- if (config.awayAfterMinutes <= 0) return null;
452
- const active = await readActive(config);
453
- if (!active) return null;
454
- const startedAt = parseISO(active.startedAt);
455
- if (!startedAt) {
456
- await clearActive(config);
457
- return null;
458
- }
459
- const awayAfterMs = config.awayAfterMinutes * 6e4;
460
- const previous = parseISO(active.lastActiveAt) ?? startedAt;
461
- if (now.getTime() - previous.getTime() > awayAfterMs) {
462
- const logged = await logSession(config, active, previous, "away");
463
- await clearActive(config);
464
- return logged;
465
- }
466
- const idleSeconds = await probe();
467
- const lastInput = idleSeconds === null ? now : new Date(now.getTime() - Math.max(0, idleSeconds) * 1e3);
468
- const confirmed = lastInput.getTime() > previous.getTime() ? lastInput : previous;
469
- if (confirmed.toISOString() !== active.lastActiveAt) {
470
- await writeActive(config, { ...active, lastActiveAt: confirmed.toISOString() });
471
- }
472
- return null;
473
- }
474
- async function closeIfPastBackstop(config, active, now) {
475
- if (!active) return null;
476
- const startedAt = parseISO(active.startedAt);
477
- const elapsed = startedAt ? (now.getTime() - startedAt.getTime()) / 6e4 : Infinity;
478
- if (elapsed <= MAX_SESSION_MINUTES) return active;
479
- const ceiling = new Date((startedAt?.getTime() ?? now.getTime()) + MAX_SESSION_MINUTES * 6e4);
480
- await stopSession(config, ceiling, "limit");
481
- return null;
482
- }
483
- function lastUnattendedClose(todayLog) {
484
- const last = todayLog.at(-1);
485
- if (!last || last.endedBy === void 0 || last.endedBy === "user") return null;
486
- return {
487
- id: last.id,
488
- title: last.title,
489
- endedAt: last.endedAt,
490
- actualMinutes: last.actualMinutes,
491
- reason: last.endedBy
492
- };
493
- }
494
- async function readSessionState(config, agenda, now = /* @__PURE__ */ new Date()) {
495
- const active = await closeIfPastBackstop(config, await readActive(config), now);
496
- const todayLog = await readTodaysSessions(config, now);
497
- const completedToday = todayLog.length;
498
- const minutesToday = todayLog.reduce((sum, entry) => sum + entry.actualMinutes, 0);
499
- const unattendedClose = active ? null : lastUnattendedClose(todayLog);
500
- if (!active) {
501
- return { active: null, completedToday, minutesToday, unattendedClose };
502
- }
503
- const startedAt = parseISO(active.startedAt);
504
- const endsAt = new Date(startedAt.getTime() + active.minutes * 6e4);
505
- const remainingSeconds = Math.round((endsAt.getTime() - now.getTime()) / 1e3);
506
- const nextEvent = agenda.events.map((event) => parseISO(event.start)).filter((start) => start !== null && start.getTime() > now.getTime()).sort((a, b) => a.getTime() - b.getTime())[0];
507
- return {
508
- active: {
509
- id: active.id,
510
- title: active.title,
511
- startedAt: active.startedAt,
512
- advancesObjective: active.advancesObjective,
513
- lastActiveAt: active.lastActiveAt,
514
- endsAt: endsAt.toISOString(),
515
- minutes: active.minutes,
516
- remainingSeconds,
517
- overrun: remainingSeconds < 0,
518
- collidesWithNextEvent: nextEvent ? nextEvent.getTime() < endsAt.getTime() : false,
519
- nextEventAt: nextEvent ? nextEvent.toISOString() : null
520
- },
521
- completedToday,
522
- minutesToday,
523
- unattendedClose
524
- };
525
- }
526
- async function readSessions(config) {
527
- let text;
528
- try {
529
- text = await readFile2(config.sessionsLogFile, "utf8");
530
- } catch {
531
- return [];
532
- }
533
- const sessions = [];
534
- for (const line of text.split("\n")) {
535
- if (line.trim() === "") continue;
536
- try {
537
- const entry = JSON.parse(line);
538
- if (entry?.id && entry.startedAt) sessions.push(entry);
539
- } catch {
540
- }
541
- }
542
- return sessions;
543
- }
544
- async function readTodaysSessions(config, now) {
545
- const all = await readSessions(config);
546
- return all.filter((entry) => {
547
- const started = parseISO(entry.startedAt);
548
- return started !== null && started.getFullYear() === now.getFullYear() && started.getMonth() === now.getMonth() && started.getDate() === now.getDate();
549
- });
550
- }
551
-
552
393
  // src/store.ts
553
394
  import { createHash } from "node:crypto";
554
- import { appendFile as appendFile2, mkdir as mkdir3, readFile as readFile3, stat } from "node:fs/promises";
395
+ import { appendFile, mkdir as mkdir2, readFile as readFile2, stat } from "node:fs/promises";
555
396
  import { setTimeout as delay } from "node:timers/promises";
556
397
 
557
398
  // src/agenda.ts
@@ -708,7 +549,7 @@ var Store = class {
708
549
  this.config = config;
709
550
  }
710
551
  async ensureDataDir() {
711
- await mkdir3(this.config.dataDir, { recursive: true });
552
+ await mkdir2(this.config.dataDir, { recursive: true });
712
553
  }
713
554
  /**
714
555
  * Which store this is, as an opaque fingerprint: its path, and the directory's
@@ -736,7 +577,7 @@ var Store = class {
736
577
  for (let attempt = 0; attempt < READ_RETRIES; attempt++) {
737
578
  let text;
738
579
  try {
739
- text = await readFile3(this.config.itemsFile, "utf8");
580
+ text = await readFile2(this.config.itemsFile, "utf8");
740
581
  } catch (err) {
741
582
  if (err.code === "ENOENT") {
742
583
  return {
@@ -757,7 +598,7 @@ var Store = class {
757
598
  async readActions() {
758
599
  let text;
759
600
  try {
760
- text = await readFile3(this.config.actionsFile, "utf8");
601
+ text = await readFile2(this.config.actionsFile, "utf8");
761
602
  } catch (err) {
762
603
  if (err.code === "ENOENT") return [];
763
604
  throw err;
@@ -789,7 +630,7 @@ var Store = class {
789
630
  /** Read the standing objective. Returns null when focus.md hasn't been written. */
790
631
  async readFocus() {
791
632
  try {
792
- return parseFocus(await readFile3(this.config.focusFile, "utf8"));
633
+ return parseFocus(await readFile2(this.config.focusFile, "utf8"));
793
634
  } catch (err) {
794
635
  if (err.code === "ENOENT") return null;
795
636
  console.warn(`[daily-focus] could not read focus.md: ${err.message}`);
@@ -802,7 +643,7 @@ var Store = class {
802
643
  `;
803
644
  const write = this.#writeQueue.then(async () => {
804
645
  await this.ensureDataDir();
805
- await appendFile2(this.config.actionsFile, line, "utf8");
646
+ await appendFile(this.config.actionsFile, line, "utf8");
806
647
  });
807
648
  this.#writeQueue = write.catch(() => {
808
649
  });
@@ -870,12 +711,10 @@ var Store = class {
870
711
  }
871
712
  objectiveProgress = computeObjectiveProgress(withToday, actions, now, isRunDay);
872
713
  }
873
- const session = await readSessionState(this.config, agenda, now);
874
714
  return {
875
715
  // toPublicFocus, not focus — the agent-only section must never be serialised.
876
716
  focus: focus ? toPublicFocus(focus) : null,
877
717
  objectiveProgress,
878
- session,
879
718
  schedule: {
880
719
  days: [...schedule.days],
881
720
  source: schedule.source,
@@ -931,6 +770,9 @@ function foldActionLog(actions, now) {
931
770
  case "note":
932
771
  if (action.text) entry.notes.push({ text: action.text, at: action.at });
933
772
  break;
773
+ case "nudge":
774
+ entry.nudgedAt = action.at;
775
+ break;
934
776
  case "done":
935
777
  entry.status = "done";
936
778
  delete entry.snoozedUntil;
@@ -986,12 +828,11 @@ export {
986
828
  tempPathFor,
987
829
  canonicalId,
988
830
  fingerprintId,
989
- readArchiveIndex,
990
831
  parseBrief,
832
+ ACTION_TYPES,
833
+ setsStatus,
834
+ readArchiveIndex,
991
835
  runContractChecks,
992
- startSession,
993
- stopSession,
994
- reconcileSession,
995
836
  Store,
996
837
  foldActionLog,
997
838
  resolveItems
@@ -440,15 +440,10 @@ function loadConfig(env = loadEnv()) {
440
440
  schemaFile: resolve2(dataDir, "items.schema.json"),
441
441
  schemaSource: resolve2(import.meta.dirname, "..", "schema", "items.schema.json"),
442
442
  archiveDir: resolve2(dataDir, "archive"),
443
- sessionFile: resolve2(dataDir, "session.json"),
444
- sessionsLogFile: resolve2(dataDir, "sessions.jsonl"),
445
443
  pullsFile: resolve2(dataDir, "prs.json"),
446
444
  calendarFile: resolve2(dataDir, "calendar.json"),
447
445
  ticketsFile: resolve2(dataDir, "tickets.json"),
448
446
  timeZoneFile: resolve2(dataDir, "timezone.json"),
449
- // The same 1 to 120 the session endpoint holds a start to.
450
- sessionMinutes: envBounded("DAILY_FOCUS_SESSION_MINUTES", 25, 1, 120, env),
451
- awayAfterMinutes: envBounded("DAILY_FOCUS_AWAY_AFTER", profile === "personal" ? 0 : 10, 0, 24 * 60, env),
452
447
  port: envBounded("DAILY_FOCUS_PORT", 4321, 0, 65535, env),
453
448
  host: envString("DAILY_FOCUS_HOST", "127.0.0.1", env),
454
449
  freeWindows: envFlag("DAILY_FOCUS_FREE_WINDOWS", profile !== "personal", env),
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  localDateKey
3
- } from "./chunk-RNE27FQY.js";
3
+ } from "./chunk-KZNY45XW.js";
4
4
 
5
5
  // src/sample.ts
6
6
  function timeOn(now, hour, minute = 0) {