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.
@@ -11,22 +11,20 @@ import {
11
11
  import {
12
12
  TimeZoneKeeper,
13
13
  validZone
14
- } from "./chunk-AIQO2YAI.js";
14
+ } from "./chunk-6SMUSPZG.js";
15
15
  import {
16
16
  describeLink,
17
17
  linkStore,
18
18
  storeLinkWarning
19
19
  } from "./chunk-QKT2FAB5.js";
20
20
  import {
21
+ ACTION_TYPES,
21
22
  Store,
22
23
  canonicalId,
23
24
  foldActionLog,
24
- reconcileSession,
25
- startSession,
26
- stopSession,
27
25
  tempPathFor,
28
26
  writeJsonAtomic
29
- } from "./chunk-26JG3D5C.js";
27
+ } from "./chunk-GQ3NCWRT.js";
30
28
  import {
31
29
  CLI_NAMES,
32
30
  LAUNCH_SETTINGS,
@@ -42,7 +40,7 @@ import {
42
40
  readDotEnv,
43
41
  resolveSchedule,
44
42
  scheduledRunDue
45
- } from "./chunk-RNE27FQY.js";
43
+ } from "./chunk-KZNY45XW.js";
46
44
  import {
47
45
  PACKAGED,
48
46
  command,
@@ -499,9 +497,9 @@ function checkSlot(node) {
499
497
  return `check\0${node.name ?? ""}`;
500
498
  }
501
499
  function checkGeneration(node) {
502
- const run6 = node.checkSuite?.workflowRun;
503
- const sequence = run6?.runNumber ?? run6?.databaseId;
504
- if (typeof sequence === "number") return [sequence, run6?.runAttempt ?? 0];
500
+ const run5 = node.checkSuite?.workflowRun;
501
+ const sequence = run5?.runNumber ?? run5?.databaseId;
502
+ if (typeof sequence === "number") return [sequence, run5?.runAttempt ?? 0];
505
503
  const at = node.completedAt ?? node.startedAt ?? node.createdAt ?? null;
506
504
  const time = at ? Date.parse(at) : Number.NaN;
507
505
  return [Number.isFinite(time) ? time : 0, 0];
@@ -871,6 +869,85 @@ async function fetchPulls(token, scope, expectedLogin, signal) {
871
869
  }
872
870
  return { login, pulls, warnings, orgs, rateLimitRemaining };
873
871
  }
872
+ var TICKET_PAGE_SIZE = 25;
873
+ var TICKET_MAX_PAGES = 12;
874
+ var TICKET_COMMITS = 100;
875
+ var TICKET_SEARCH_QUERY = `
876
+ query($q: String!, $first: Int!, $after: String) {
877
+ search(query: $q, type: ISSUE, first: $first, after: $after) {
878
+ pageInfo { hasNextPage endCursor }
879
+ nodes {
880
+ ... on PullRequest {
881
+ number
882
+ title
883
+ url
884
+ state
885
+ isDraft
886
+ closedAt
887
+ mergedAt
888
+ headRefName
889
+ body
890
+ repository { nameWithOwner }
891
+ commits(first: ${TICKET_COMMITS}) { nodes { commit { message } } }
892
+ }
893
+ }
894
+ }
895
+ }`;
896
+ function buildTicketSearchQueries(scope, closedSince) {
897
+ return [
898
+ ["is:pr", "author:@me", "is:open", ...scope].join(" "),
899
+ ["is:pr", "author:@me", "is:closed", `closed:>=${closedSince}`, ...scope].join(" ")
900
+ ];
901
+ }
902
+ function toLinkedState(state, isDraft) {
903
+ if (state === "MERGED") return "merged";
904
+ if (state === "CLOSED") return "closed";
905
+ if (state === "OPEN") return isDraft === true ? "draft" : "open";
906
+ return null;
907
+ }
908
+ function normalizeTicketPull(raw) {
909
+ const repo = raw.repository?.nameWithOwner;
910
+ const url = safeUrl(raw.url);
911
+ const state = toLinkedState(raw.state, raw.isDraft);
912
+ if (typeof raw.number !== "number" || typeof repo !== "string" || typeof raw.title !== "string" || !url || !state) {
913
+ return null;
914
+ }
915
+ const closedAt = state === "merged" ? raw.mergedAt ?? raw.closedAt ?? null : state === "closed" ? raw.closedAt ?? null : null;
916
+ const text = [raw.title, raw.headRefName ?? "", raw.body ?? ""];
917
+ for (const node of raw.commits?.nodes ?? []) {
918
+ const message = node?.commit?.message;
919
+ if (typeof message === "string") text.push(message);
920
+ }
921
+ return { pull: { repo, number: raw.number, title: raw.title, url, state, closedAt }, text };
922
+ }
923
+ async function fetchTicketPulls(token, scope, closedSince, signal) {
924
+ const pulls = [];
925
+ let complete = true;
926
+ for (const q of buildTicketSearchQueries(scope, closedSince)) {
927
+ let after = null;
928
+ for (let page = 0; ; page++) {
929
+ const result = await graphql(token, TICKET_SEARCH_QUERY, { q, first: TICKET_PAGE_SIZE, after }, signal);
930
+ const fatal = result.errors.filter((error) => !isSamlError(error));
931
+ if (result.data?.search === void 0 && fatal.length > 0) {
932
+ throw new GitHubRequestError(`GitHub search failed: ${fatal.map((error) => error.message).join("; ")}`);
933
+ }
934
+ if (result.errors.length > 0) complete = false;
935
+ for (const node of result.data?.search?.nodes ?? []) {
936
+ const found = node ? normalizeTicketPull(node) : null;
937
+ if (found) pulls.push(found);
938
+ else complete = false;
939
+ }
940
+ const pageInfo = result.data?.search?.pageInfo;
941
+ if (!pageInfo?.hasNextPage || !pageInfo.endCursor) break;
942
+ if (page === TICKET_MAX_PAGES - 1) {
943
+ complete = false;
944
+ break;
945
+ }
946
+ after = pageInfo.endCursor;
947
+ }
948
+ }
949
+ return { pulls, complete };
950
+ }
874
951
 
875
952
  // src/prs.ts
876
953
  var NUDGE_AFTER_MS = 24 * 36e5;
@@ -896,7 +973,7 @@ function matchMergeGates(pr, gates) {
896
973
  function newest(pr, state) {
897
974
  return pr.reviews.filter((review) => review.state === state).sort((a, b) => a.at < b.at ? 1 : -1)[0];
898
975
  }
899
- function judge(pr, now, lastNoteAt, gates = []) {
976
+ function judge(pr, now, nudgedAt, gates = []) {
900
977
  const decision = deriveDecision(pr);
901
978
  const you = pr.lastActivityByYou ?? null;
902
979
  const others = pr.lastActivityByOthers ?? null;
@@ -958,18 +1035,22 @@ function judge(pr, now, lastNoteAt, gates = []) {
958
1035
  }
959
1036
  }
960
1037
  }
961
- const quietSince = laterISO(waitingSince, lastNoteAt) ?? waitingSince;
1038
+ const quietSince = laterISO(waitingSince, nudgedAt) ?? waitingSince;
962
1039
  const waited = now.getTime() - (ms(quietSince) ?? now.getTime());
963
- return { court: "reviewers", decision, reasons, stale: false, nudge: waited >= NUDGE_AFTER_MS, since: waitingSince };
1040
+ const verdict = { court: "reviewers", decision, reasons, stale: false, nudge: waited >= NUDGE_AFTER_MS, since: waitingSince };
1041
+ const nudged = ms(nudgedAt);
1042
+ if (nudgedAt && nudged !== null && quietSince === nudgedAt && !verdict.nudge) {
1043
+ verdict.nudgedAt = nudgedAt;
1044
+ verdict.nudgedUntil = new Date(nudged + NUDGE_AFTER_MS).toISOString();
1045
+ }
1046
+ return verdict;
964
1047
  }
965
1048
  function resolveBoard(pulls, actions, now, gates = []) {
966
1049
  const folded = foldActionLog(actions, now);
967
1050
  const rows = pulls.map((pr) => {
968
1051
  const log = folded.get(canonicalId(pr.id));
969
- const notes = log?.notes ?? [];
970
- const lastNoteAt = notes.length > 0 ? notes[notes.length - 1].at : null;
971
- const verdict = judge(pr, now, lastNoteAt, gates);
972
- const row = { ...pr, ...verdict, status: log?.status === "snoozed" ? "snoozed" : "open", notes };
1052
+ const verdict = judge(pr, now, log?.nudgedAt ?? null, gates);
1053
+ const row = { ...pr, ...verdict, status: log?.status === "snoozed" ? "snoozed" : "open", notes: log?.notes ?? [] };
973
1054
  if (row.status === "snoozed" && log?.snoozedUntil) row.snoozedUntil = log.snoozedUntil;
974
1055
  return row;
975
1056
  });
@@ -1694,6 +1775,87 @@ import { readFile as readFile5 } from "node:fs/promises";
1694
1775
  // src/jira.ts
1695
1776
  import { execFile as execFile3 } from "node:child_process";
1696
1777
  import { promisify as promisify3 } from "node:util";
1778
+
1779
+ // src/linked.ts
1780
+ var CLOSED_LOOKBACK_DAYS = 60;
1781
+ var KEYISH = /(?<![A-Za-z0-9])([A-Za-z][A-Za-z0-9_]*)-(\d+)/g;
1782
+ function issueKeysIn(text) {
1783
+ const keys = /* @__PURE__ */ new Set();
1784
+ for (const match of text.matchAll(KEYISH)) {
1785
+ const parts = match[1].split("_");
1786
+ for (let i = 0; i < parts.length; i++) {
1787
+ const project = parts.slice(i).join("_");
1788
+ if (/^[A-Za-z]/.test(project)) keys.add(`${project.toUpperCase()}-${match[2]}`);
1789
+ }
1790
+ }
1791
+ return keys;
1792
+ }
1793
+ var realDeps3 = { token: ghToken, search: fetchTicketPulls };
1794
+ var CONSEQUENCE = "so a merged pull request counts only once Jira has it";
1795
+ async function readLinkedPulls(github, now, signal, deps = realDeps3) {
1796
+ const closedSince = new Date(now.getTime() - CLOSED_LOOKBACK_DAYS * 864e5).toISOString().slice(0, 10);
1797
+ const logins = github.accounts.length > 0 ? [...github.accounts] : [null];
1798
+ const results = await Promise.allSettled(
1799
+ logins.map(async (login) => deps.search(await deps.token(github.ghPath, login), github.scope, closedSince, signal))
1800
+ );
1801
+ if (results.every((result) => result.status === "rejected")) {
1802
+ throw results[0].reason;
1803
+ }
1804
+ const warnings = [];
1805
+ let complete = true;
1806
+ const byKey = /* @__PURE__ */ new Map();
1807
+ const seen = /* @__PURE__ */ new Set();
1808
+ results.forEach((result, i) => {
1809
+ const who = logins[i] ? ` as ${logins[i]}` : "";
1810
+ if (result.status === "rejected") {
1811
+ complete = false;
1812
+ warnings.push(`Could not ask GitHub${who} about these tickets' pull requests (${result.reason.message}), ${CONSEQUENCE}.`);
1813
+ return;
1814
+ }
1815
+ if (!result.value.complete) {
1816
+ complete = false;
1817
+ warnings.push(`GitHub did not list every pull request${who}, ${CONSEQUENCE}.`);
1818
+ }
1819
+ for (const { pull, text } of result.value.pulls) {
1820
+ if (seen.has(pull.url)) continue;
1821
+ seen.add(pull.url);
1822
+ const keys = /* @__PURE__ */ new Set();
1823
+ for (const piece of text) for (const key of issueKeysIn(piece)) keys.add(key);
1824
+ for (const key of keys) byKey.set(key, [...byKey.get(key) ?? [], pull]);
1825
+ }
1826
+ });
1827
+ return { byKey, complete, warnings };
1828
+ }
1829
+ function isOpen(pull) {
1830
+ return pull.state === "open" || pull.state === "draft";
1831
+ }
1832
+ function byRecency(a, b) {
1833
+ if (isOpen(a) !== isOpen(b)) return isOpen(a) ? -1 : 1;
1834
+ return (b.closedAt ?? "").localeCompare(a.closedAt ?? "");
1835
+ }
1836
+ function linkedTo(ticket, linked) {
1837
+ return linked?.byKey.get(ticket.key.toUpperCase()) ?? [];
1838
+ }
1839
+ function needsJiraCount(ticket, linked) {
1840
+ const found = linkedTo(ticket, linked);
1841
+ return Boolean(linked?.complete) && found.length > 0 && !found.some(isOpen) && ticket.hasAnyPr && !ticket.allPrsClosed;
1842
+ }
1843
+ function reconcile(ticket, linked, jiraCount) {
1844
+ const found = [...linkedTo(ticket, linked)].sort(byRecency);
1845
+ if (found.length === 0) return ticket;
1846
+ const jira = { hasAnyPr: ticket.hasAnyPr, hasOpenPr: ticket.hasOpenPr, allPrsClosed: ticket.allPrsClosed };
1847
+ let reading = jira;
1848
+ if (found.some(isOpen)) {
1849
+ reading = { hasAnyPr: true, hasOpenPr: true, allPrsClosed: false };
1850
+ } else if (linked?.complete && !ticket.allPrsClosed) {
1851
+ const known = ticket.hasAnyPr ? jiraCount : 0;
1852
+ if (known !== null && found.length >= known) reading = { hasAnyPr: true, hasOpenPr: false, allPrsClosed: true };
1853
+ }
1854
+ const changed = reading.hasAnyPr !== jira.hasAnyPr || reading.hasOpenPr !== jira.hasOpenPr || reading.allPrsClosed !== jira.allPrsClosed;
1855
+ return { ...ticket, ...reading, pulls: found, ...changed ? { fromGitHub: true } : {} };
1856
+ }
1857
+
1858
+ // src/jira.ts
1697
1859
  var run3 = promisify3(execFile3);
1698
1860
  var ACLI_TIMEOUT_MS = 45e3;
1699
1861
  var PAGE_LIMIT = 200;
@@ -1977,22 +2139,23 @@ async function fetchTickets(acliPath, opts) {
1977
2139
  if (unreadable > 0) {
1978
2140
  warnings.push(`Skipped ${unreadable} work ${unreadable === 1 ? "item" : "items"} Jira described in a way this board could not read.`);
1979
2141
  }
1980
- const candidates = tickets.filter((ticket) => ticket.hasAnyPr && !ticket.hasOpenPr);
1981
- const panels = await mapLimit(
1982
- candidates,
1983
- DEV_CONCURRENCY,
1984
- (ticket) => fetchDevPullRequests(acliPath, ticket.key).then(
1985
- (dev) => ({ asked: true, dev }),
1986
- () => ({ asked: false, dev: null })
1987
- )
2142
+ const readPanel = (ticket) => fetchDevPullRequests(acliPath, ticket.key).then(
2143
+ (dev) => ({ asked: true, dev }),
2144
+ () => ({ asked: false, dev: null })
1988
2145
  );
2146
+ const candidates = tickets.filter((ticket) => ticket.hasAnyPr && !ticket.hasOpenPr);
2147
+ const reading = mapLimit(candidates, DEV_CONCURRENCY, readPanel);
2148
+ const linked = await opts.linked ?? null;
2149
+ const panels = await reading;
2150
+ const counts = /* @__PURE__ */ new Map();
1989
2151
  const unanswered = [];
1990
2152
  const unaccounted = [];
1991
2153
  candidates.forEach((ticket, i) => {
1992
- const { asked, dev } = panels[i];
2154
+ const { asked: asked2, dev } = panels[i];
2155
+ if (dev) counts.set(ticket.key, dev.count);
1993
2156
  if (allPullRequestsClosed(dev)) {
1994
2157
  ticket.allPrsClosed = true;
1995
- } else if (!asked) {
2158
+ } else if (!asked2) {
1996
2159
  unanswered.push(ticket.key);
1997
2160
  } else if (dev === null) {
1998
2161
  unaccounted.push(ticket.key);
@@ -2000,19 +2163,30 @@ async function fetchTickets(acliPath, opts) {
2000
2163
  ticket.hasOpenPr = true;
2001
2164
  }
2002
2165
  });
2166
+ const asked = new Set(candidates);
2167
+ const behind = tickets.filter((ticket) => !asked.has(ticket) && needsJiraCount(ticket, linked));
2168
+ const extra = await mapLimit(behind, DEV_CONCURRENCY, readPanel);
2169
+ behind.forEach((ticket, i) => {
2170
+ const dev = extra[i].dev;
2171
+ if (dev) counts.set(ticket.key, dev.count);
2172
+ });
2173
+ const reconciled = tickets.map((ticket) => reconcile(ticket, linked, counts.get(ticket.key) ?? null));
2174
+ const undecided = new Set(reconciled.filter((ticket) => !ticket.hasOpenPr && !ticket.allPrsClosed).map((ticket) => ticket.key));
2175
+ const unansweredNow = unanswered.filter((key) => undecided.has(key));
2176
+ const unaccountedNow = unaccounted.filter((key) => undecided.has(key));
2003
2177
  const listed = (keys2) => keys2.map((key) => {
2004
2178
  const url = browseUrl(opts.site, key);
2005
2179
  return url === null ? key : `[${key}](${url})`;
2006
2180
  }).join(", ");
2007
2181
  const heldOut = (keys2) => `${keys2.length === 1 ? "it is" : "they are"} held out of "no open pull requests left" rather than guessed at`;
2008
- if (unanswered.length > 0) {
2182
+ if (unansweredNow.length > 0) {
2009
2183
  warnings.push(
2010
- `Could not read Jira's development panel for ${listed(unanswered)}, so ${heldOut(unanswered)}. The next read may well manage it.`
2184
+ `Could not read Jira's development panel for ${listed(unansweredNow)}, so ${heldOut(unansweredNow)}. The next read may well manage it.`
2011
2185
  );
2012
2186
  }
2013
- if (unaccounted.length > 0) {
2187
+ if (unaccountedNow.length > 0) {
2014
2188
  warnings.push(
2015
- `Jira's search says ${listed(unaccounted)} ${unaccounted.length === 1 ? "has" : "have"} pull requests, but its own development panel names none, so ${heldOut(unaccounted)}. Jira is disagreeing with itself; the row comes back when one of its caches catches up.`
2189
+ `Jira's search says ${listed(unaccountedNow)} ${unaccountedNow.length === 1 ? "has" : "have"} pull requests, but its own development panel names none, so ${heldOut(unaccountedNow)}. Jira is disagreeing with itself; the row comes back when one of its caches catches up.`
2016
2190
  );
2017
2191
  }
2018
2192
  if (!opts.site) {
@@ -2029,7 +2203,7 @@ async function fetchTickets(acliPath, opts) {
2029
2203
  );
2030
2204
  }
2031
2205
  return {
2032
- tickets,
2206
+ tickets: reconciled,
2033
2207
  statuses: statusesByProject(base, ...finished.map((batch) => batch ?? [])),
2034
2208
  warnings
2035
2209
  };
@@ -2150,7 +2324,13 @@ function countTickets(rows) {
2150
2324
 
2151
2325
  // src/ticketboard.ts
2152
2326
  var MAX_BACKOFF_MS3 = 30 * 6e4;
2153
- var realDeps3 = { identity: acliIdentity, fetch: fetchTickets, transition: transitionTicket };
2327
+ var GITHUB_DEADLINE_MS = 9e4;
2328
+ var realDeps4 = {
2329
+ identity: acliIdentity,
2330
+ fetch: fetchTickets,
2331
+ transition: transitionTicket,
2332
+ linked: (github, now, signal) => readLinkedPulls(github, now, signal)
2333
+ };
2154
2334
  var TicketBoard = class {
2155
2335
  #config;
2156
2336
  #deps;
@@ -2167,7 +2347,8 @@ var TicketBoard = class {
2167
2347
  #audience = 0;
2168
2348
  #stopped = false;
2169
2349
  #lastReadAt = 0;
2170
- constructor(config, onChange, deps = realDeps3) {
2350
+ #abort = new AbortController();
2351
+ constructor(config, onChange, deps = realDeps4) {
2171
2352
  this.#config = config;
2172
2353
  this.#deps = deps;
2173
2354
  this.#onChange = onChange;
@@ -2181,10 +2362,12 @@ var TicketBoard = class {
2181
2362
  this.#file = await readTicketsFile(this.#config.ticketsFile);
2182
2363
  await this.refresh();
2183
2364
  }
2365
+ /** Stop reading, and cancel a GitHub request in flight so nothing lands after close. */
2184
2366
  stop() {
2185
2367
  this.#stopped = true;
2186
2368
  if (this.#timer) clearTimeout(this.#timer);
2187
2369
  this.#timer = null;
2370
+ this.#abort.abort();
2188
2371
  }
2189
2372
  /** How many browsers are watching. Polling pauses at zero. */
2190
2373
  setAudience(count) {
@@ -2299,8 +2482,24 @@ var TicketBoard = class {
2299
2482
  return;
2300
2483
  }
2301
2484
  const site = configuredSite ?? identity2.site;
2485
+ const githubWarnings = [];
2486
+ const linked = this.#config.github.enabled ? this.#deps.linked(this.#config.github, /* @__PURE__ */ new Date(), AbortSignal.any([this.#abort.signal, AbortSignal.timeout(GITHUB_DEADLINE_MS)])).then(
2487
+ (read) => {
2488
+ githubWarnings.push(...read.warnings);
2489
+ return read;
2490
+ },
2491
+ (err) => {
2492
+ githubWarnings.push(
2493
+ `Could not ask GitHub about these tickets' pull requests (${err.message}), so they are as Jira has them, which can be days behind.`
2494
+ );
2495
+ return null;
2496
+ }
2497
+ ) : Promise.resolve(null);
2302
2498
  try {
2303
- const { tickets, statuses, warnings } = await this.#deps.fetch(acliPath, { projects, site });
2499
+ const fetched = await this.#deps.fetch(acliPath, { projects, site, linked });
2500
+ await linked;
2501
+ const { tickets, statuses } = fetched;
2502
+ const warnings = [...fetched.warnings, ...githubWarnings];
2304
2503
  this.#reason = null;
2305
2504
  this.#warnings = warnings;
2306
2505
  this.#streak = null;
@@ -2336,7 +2535,7 @@ function normalizeStoredTicket(raw) {
2336
2535
  const { key, url, workflowStatus, statusCategory, issueType, summary } = raw;
2337
2536
  if (typeof key !== "string" || key === "") return null;
2338
2537
  const site = typeof url === "string" ? /^https:\/\/([^/]+)\/browse\//.exec(url)?.[1] ?? null : null;
2339
- return toTicket(
2538
+ const ticket = toTicket(
2340
2539
  {
2341
2540
  key,
2342
2541
  fields: {
@@ -2353,6 +2552,25 @@ function normalizeStoredTicket(raw) {
2353
2552
  // files are exactly the ones whose settled court counted drafts as merges.
2354
2553
  raw.allPrsClosed === true
2355
2554
  );
2555
+ if (!ticket) return null;
2556
+ const pulls = storedPulls(raw.pulls);
2557
+ if (pulls.length > 0) {
2558
+ ticket.pulls = pulls;
2559
+ if (raw.fromGitHub === true) ticket.fromGitHub = true;
2560
+ }
2561
+ return ticket;
2562
+ }
2563
+ var PULL_STATES = ["open", "draft", "merged", "closed"];
2564
+ function storedPulls(value) {
2565
+ if (!Array.isArray(value)) return [];
2566
+ return value.flatMap((entry) => {
2567
+ if (!isRecord3(entry)) return [];
2568
+ const { repo, number, title, url, state, closedAt } = entry;
2569
+ if (typeof repo !== "string" || typeof number !== "number" || typeof title !== "string") return [];
2570
+ if (typeof url !== "string" || !/^https?:\/\//i.test(url)) return [];
2571
+ if (typeof state !== "string" || !PULL_STATES.includes(state)) return [];
2572
+ return [{ repo, number, title, url, state, closedAt: typeof closedAt === "string" ? closedAt : null }];
2573
+ });
2356
2574
  }
2357
2575
  async function readTicketsFile(path) {
2358
2576
  let text;
@@ -2453,11 +2671,11 @@ function foldAgentLog(text) {
2453
2671
  });
2454
2672
  continue;
2455
2673
  }
2456
- const run6 = runs.get(record.run);
2457
- if (!run6) continue;
2674
+ const run5 = runs.get(record.run);
2675
+ if (!run5) continue;
2458
2676
  if (record.event === "asked") {
2459
2677
  if (typeof record.turn !== "string" || typeof record.question !== "string") continue;
2460
- run6.turns.push({
2678
+ run5.turns.push({
2461
2679
  id: record.turn,
2462
2680
  question: record.question,
2463
2681
  startedAt: record.at,
@@ -2469,7 +2687,7 @@ function foldAgentLog(text) {
2469
2687
  continue;
2470
2688
  }
2471
2689
  if (record.event === "replied" || record.event === "turn-failed" || record.event === "turn-aborted") {
2472
- const turn = run6.turns.find((candidate) => candidate.id === record.turn);
2690
+ const turn = run5.turns.find((candidate) => candidate.id === record.turn);
2473
2691
  if (!turn) continue;
2474
2692
  turn.reply = typeof record.reply === "string" ? record.reply : turn.reply;
2475
2693
  turn.endedAt = record.at;
@@ -2484,18 +2702,18 @@ function foldAgentLog(text) {
2484
2702
  }
2485
2703
  continue;
2486
2704
  }
2487
- run6.sessionId = typeof record.sessionId === "string" ? record.sessionId : run6.sessionId;
2488
- run6.report = typeof record.report === "string" ? record.report : run6.report;
2489
- run6.messages = Array.isArray(record.messages) ? record.messages.filter((message) => typeof message === "string") : run6.report ? [run6.report] : [];
2490
- run6.endedAt = record.at;
2705
+ run5.sessionId = typeof record.sessionId === "string" ? record.sessionId : run5.sessionId;
2706
+ run5.report = typeof record.report === "string" ? record.report : run5.report;
2707
+ run5.messages = Array.isArray(record.messages) ? record.messages.filter((message) => typeof message === "string") : run5.report ? [run5.report] : [];
2708
+ run5.endedAt = record.at;
2491
2709
  if (record.event === "finished") {
2492
- run6.status = "done";
2710
+ run5.status = "done";
2493
2711
  } else if (record.event === "failed") {
2494
- run6.status = "failed";
2495
- run6.error = record.error;
2712
+ run5.status = "failed";
2713
+ run5.error = record.error;
2496
2714
  } else if (record.event === "aborted") {
2497
- run6.status = "aborted";
2498
- run6.error = record.reason;
2715
+ run5.status = "aborted";
2716
+ run5.error = record.reason;
2499
2717
  }
2500
2718
  }
2501
2719
  return [...runs.values()].sort((a, b) => a.startedAt.localeCompare(b.startedAt));
@@ -2535,19 +2753,19 @@ var AgentRunner = class {
2535
2753
  this.runs = foldAgentLog(text);
2536
2754
  const at = (/* @__PURE__ */ new Date()).toISOString();
2537
2755
  const reason = "the dashboard restarted while this was running";
2538
- for (const run6 of this.runs) {
2539
- if (run6.status === "running") {
2540
- run6.status = "aborted";
2541
- run6.error = reason;
2542
- run6.endedAt = at;
2543
- await this.append({ event: "aborted", run: run6.id, sessionId: run6.sessionId, reason, report: run6.report, messages: run6.messages, at });
2756
+ for (const run5 of this.runs) {
2757
+ if (run5.status === "running") {
2758
+ run5.status = "aborted";
2759
+ run5.error = reason;
2760
+ run5.endedAt = at;
2761
+ await this.append({ event: "aborted", run: run5.id, sessionId: run5.sessionId, reason, report: run5.report, messages: run5.messages, at });
2544
2762
  }
2545
- for (const turn of run6.turns) {
2763
+ for (const turn of run5.turns) {
2546
2764
  if (turn.status !== "running") continue;
2547
2765
  turn.status = "aborted";
2548
2766
  turn.error = reason;
2549
2767
  turn.endedAt = at;
2550
- await this.append({ event: "turn-aborted", run: run6.id, turn: turn.id, reason, reply: turn.reply, at });
2768
+ await this.append({ event: "turn-aborted", run: run5.id, turn: turn.id, reason, reply: turn.reply, at });
2551
2769
  }
2552
2770
  }
2553
2771
  }
@@ -2560,7 +2778,7 @@ var AgentRunner = class {
2560
2778
  return this.runs.at(-1) ?? null;
2561
2779
  }
2562
2780
  view(now = /* @__PURE__ */ new Date()) {
2563
- const runs = this.runs.slice(-RUNS_SHOWN).reverse().map((run6) => ({ ...run6, resumable: this.resumable(run6) }));
2781
+ const runs = this.runs.slice(-RUNS_SHOWN).reverse().map((run5) => ({ ...run5, resumable: this.resumable(run5) }));
2564
2782
  return {
2565
2783
  enabled: this.enabled,
2566
2784
  cli: this.config.agent.cli,
@@ -2614,7 +2832,7 @@ var AgentRunner = class {
2614
2832
  );
2615
2833
  }
2616
2834
  await this.start();
2617
- const run6 = {
2835
+ const run5 = {
2618
2836
  id: randomUUID(),
2619
2837
  cli,
2620
2838
  trigger,
@@ -2628,8 +2846,8 @@ var AgentRunner = class {
2628
2846
  turns: [],
2629
2847
  resumable: false
2630
2848
  };
2631
- this.runs.push(run6);
2632
- await this.append({ event: "started", run: run6.id, cli, trigger, at: run6.startedAt });
2849
+ this.runs.push(run5);
2850
+ await this.append({ event: "started", run: run5.id, cli, trigger, at: run5.startedAt });
2633
2851
  const adapter = ADAPTERS[cli];
2634
2852
  const args = adapter.args({
2635
2853
  prompt: agentPrompt(displayPath(this.config.promptFile)),
@@ -2641,8 +2859,8 @@ var AgentRunner = class {
2641
2859
  drafts: false,
2642
2860
  codexConfig: []
2643
2861
  });
2644
- await this.spawnInto(run6, null, args, RUN_TIMEOUT_MS, "gave up after an hour");
2645
- return run6;
2862
+ await this.spawnInto(run5, null, args, RUN_TIMEOUT_MS, "gave up after an hour");
2863
+ return run5;
2646
2864
  }
2647
2865
  /**
2648
2866
  * Ask a finished run a question, in its own session. Returns once the process
@@ -2652,16 +2870,16 @@ var AgentRunner = class {
2652
2870
  const cli = this.config.agent.cli;
2653
2871
  if (!cli) throw new Error("running the morning agent is off; set DAILY_FOCUS_AGENT to codex or claude");
2654
2872
  await this.start();
2655
- const run6 = this.runs.find((candidate) => candidate.id === runId);
2656
- if (!run6) throw new Error("no such run");
2873
+ const run5 = this.runs.find((candidate) => candidate.id === runId);
2874
+ if (!run5) throw new Error("no such run");
2657
2875
  if (this.running) {
2658
2876
  throw new Error(
2659
2877
  this.running.turn ? "the morning agent is still answering a question" : "the morning agent is still running"
2660
2878
  );
2661
2879
  }
2662
- if (!this.resumable(run6)) {
2880
+ if (!this.resumable(run5)) {
2663
2881
  throw new Error(
2664
- run6.sessionId === null ? "this run left no session to continue" : `this run was made by ${run6.cli}, and the agent is ${cli} now`
2882
+ run5.sessionId === null ? "this run left no session to continue" : `this run was made by ${run5.cli}, and the agent is ${cli} now`
2665
2883
  );
2666
2884
  }
2667
2885
  const text = question.trim();
@@ -2675,12 +2893,12 @@ var AgentRunner = class {
2675
2893
  reply: "",
2676
2894
  error: null
2677
2895
  };
2678
- run6.turns.push(turn);
2679
- await this.append({ event: "asked", run: run6.id, turn: turn.id, question: text, at: turn.startedAt });
2896
+ run5.turns.push(turn);
2897
+ await this.append({ event: "asked", run: run5.id, turn: turn.id, question: text, at: turn.startedAt });
2680
2898
  const adapter = ADAPTERS[cli];
2681
2899
  const args = adapter.args({
2682
2900
  prompt: followUpPrompt(text),
2683
- sessionId: run6.sessionId,
2901
+ sessionId: run5.sessionId,
2684
2902
  model: this.config.agent.model,
2685
2903
  effort: this.config.agent.effort,
2686
2904
  // The sources, and not the brief: a chat is given nothing to write.
@@ -2689,7 +2907,7 @@ var AgentRunner = class {
2689
2907
  drafts: false,
2690
2908
  codexConfig: []
2691
2909
  });
2692
- await this.spawnInto(run6, turn, args, TURN_TIMEOUT_MS, "gave up after ten minutes");
2910
+ await this.spawnInto(run5, turn, args, TURN_TIMEOUT_MS, "gave up after ten minutes");
2693
2911
  return turn;
2694
2912
  }
2695
2913
  /**
@@ -2697,13 +2915,13 @@ var AgentRunner = class {
2697
2915
  * run when there is none. The store in both cases: Claude Code files a
2698
2916
  * session under the directory it was started in, and looks for it there.
2699
2917
  */
2700
- async spawnInto(run6, turn, args, timeoutMs, timeoutReason) {
2701
- const cli = run6.cli;
2918
+ async spawnInto(run5, turn, args, timeoutMs, timeoutReason) {
2919
+ const cli = run5.cli;
2702
2920
  const adapter = ADAPTERS[cli];
2703
2921
  await mkdir(this.config.dataDir, { recursive: true });
2704
2922
  const refusal2 = cli === "codex" ? await this.codexGate.refusal() : null;
2705
2923
  if (refusal2) {
2706
- await this.finish(run6, turn, { ok: false, error: refusal2 });
2924
+ await this.finish(run5, turn, { ok: false, error: refusal2 });
2707
2925
  return;
2708
2926
  }
2709
2927
  let child;
@@ -2715,12 +2933,12 @@ var AgentRunner = class {
2715
2933
  env: process.env
2716
2934
  });
2717
2935
  } catch (err) {
2718
- await this.finish(run6, turn, { ok: false, error: this.spawnError(err) });
2936
+ await this.finish(run5, turn, { ok: false, error: this.spawnError(err) });
2719
2937
  return;
2720
2938
  }
2721
2939
  const timer = setTimeout(() => void this.stop(timeoutReason), timeoutMs);
2722
2940
  timer.unref();
2723
- const running = { run: run6, turn, child, timer, abortReason: null };
2941
+ const running = { run: run5, turn, child, timer, abortReason: null };
2724
2942
  this.running = running;
2725
2943
  this.hooks.onChange();
2726
2944
  let done;
@@ -2734,15 +2952,15 @@ var AgentRunner = class {
2734
2952
  lines.on("line", (line) => {
2735
2953
  const event = adapter.parse(line);
2736
2954
  if (!event) return;
2737
- if (event.sessionId && !turn) run6.sessionId = event.sessionId;
2955
+ if (event.sessionId && !turn) run5.sessionId = event.sessionId;
2738
2956
  if (event.text) {
2739
2957
  if (turn) {
2740
2958
  turn.reply = turn.reply ? `${turn.reply}
2741
2959
 
2742
2960
  ${event.text}` : event.text;
2743
2961
  } else {
2744
- run6.messages.push(event.text);
2745
- run6.report = event.text;
2962
+ run5.messages.push(event.text);
2963
+ run5.report = event.text;
2746
2964
  }
2747
2965
  this.hooks.onChange();
2748
2966
  }
@@ -2759,7 +2977,7 @@ ${event.text}` : event.text;
2759
2977
  ok: false,
2760
2978
  error: `${cli} exited with code ${code}${stderr.length ? `: ${stderr.join("").trim().split("\n").slice(-3).join(" ")}` : ""}`
2761
2979
  };
2762
- void this.finish(run6, turn, outcome, running.abortReason !== null);
2980
+ void this.finish(run5, turn, outcome, running.abortReason !== null);
2763
2981
  });
2764
2982
  }
2765
2983
  /**
@@ -2776,45 +2994,45 @@ ${event.text}` : event.text;
2776
2994
  async close() {
2777
2995
  await this.stop("the dashboard shut down");
2778
2996
  }
2779
- resumable(run6) {
2780
- return run6.status !== "running" && run6.sessionId !== null && run6.cli === this.config.agent.cli;
2997
+ resumable(run5) {
2998
+ return run5.status !== "running" && run5.sessionId !== null && run5.cli === this.config.agent.cli;
2781
2999
  }
2782
- async finish(run6, turn, outcome, aborted = false) {
3000
+ async finish(run5, turn, outcome, aborted = false) {
2783
3001
  const at = (/* @__PURE__ */ new Date()).toISOString();
2784
3002
  if (turn) {
2785
3003
  const reply = outcome.reply ?? turn.reply;
2786
3004
  turn.reply = reply;
2787
3005
  turn.endedAt = at;
2788
3006
  if (outcome.ok) {
2789
- await this.append({ event: "replied", run: run6.id, turn: turn.id, reply, at });
3007
+ await this.append({ event: "replied", run: run5.id, turn: turn.id, reply, at });
2790
3008
  turn.status = "done";
2791
3009
  } else if (aborted) {
2792
3010
  turn.error = outcome.error ?? "stopped";
2793
- await this.append({ event: "turn-aborted", run: run6.id, turn: turn.id, reason: turn.error, reply, at });
3011
+ await this.append({ event: "turn-aborted", run: run5.id, turn: turn.id, reason: turn.error, reply, at });
2794
3012
  turn.status = "aborted";
2795
3013
  } else {
2796
3014
  turn.error = outcome.error ?? "failed";
2797
- await this.append({ event: "turn-failed", run: run6.id, turn: turn.id, error: turn.error, reply, at });
3015
+ await this.append({ event: "turn-failed", run: run5.id, turn: turn.id, error: turn.error, reply, at });
2798
3016
  turn.status = "failed";
2799
3017
  }
2800
3018
  this.hooks.onChange();
2801
3019
  return;
2802
3020
  }
2803
- const report = outcome.reply ?? run6.report;
2804
- run6.report = report;
2805
- run6.endedAt = at;
2806
- const messages = run6.messages;
3021
+ const report = outcome.reply ?? run5.report;
3022
+ run5.report = report;
3023
+ run5.endedAt = at;
3024
+ const messages = run5.messages;
2807
3025
  if (outcome.ok) {
2808
- await this.append({ event: "finished", run: run6.id, sessionId: run6.sessionId, report, messages, at });
2809
- run6.status = "done";
3026
+ await this.append({ event: "finished", run: run5.id, sessionId: run5.sessionId, report, messages, at });
3027
+ run5.status = "done";
2810
3028
  } else if (aborted) {
2811
- run6.error = outcome.error ?? "stopped";
2812
- await this.append({ event: "aborted", run: run6.id, sessionId: run6.sessionId, reason: run6.error, report, messages, at });
2813
- run6.status = "aborted";
3029
+ run5.error = outcome.error ?? "stopped";
3030
+ await this.append({ event: "aborted", run: run5.id, sessionId: run5.sessionId, reason: run5.error, report, messages, at });
3031
+ run5.status = "aborted";
2814
3032
  } else {
2815
- run6.error = outcome.error ?? "failed";
2816
- await this.append({ event: "failed", run: run6.id, sessionId: run6.sessionId, error: run6.error, report, messages, at });
2817
- run6.status = "failed";
3033
+ run5.error = outcome.error ?? "failed";
3034
+ await this.append({ event: "failed", run: run5.id, sessionId: run5.sessionId, error: run5.error, report, messages, at });
3035
+ run5.status = "failed";
2818
3036
  }
2819
3037
  this.hooks.onChange();
2820
3038
  }
@@ -2928,33 +3146,6 @@ function refusal(req, listenHost) {
2928
3146
  return null;
2929
3147
  }
2930
3148
 
2931
- // src/presence.ts
2932
- import { execFile as execFile4 } from "node:child_process";
2933
- import { platform } from "node:os";
2934
- import { promisify as promisify4 } from "node:util";
2935
- var run4 = promisify4(execFile4);
2936
- var PROBE_TIMEOUT_MS = 4e3;
2937
- var HID_IDLE = /"HIDIdleTime"\s*=\s*(\d+)/;
2938
- var readIdleSeconds = async () => {
2939
- if (platform() !== "darwin") return null;
2940
- try {
2941
- const { stdout } = await run4("ioreg", ["-c", "IOHIDSystem", "-d", "1", "-r"], {
2942
- timeout: PROBE_TIMEOUT_MS,
2943
- maxBuffer: 1024 * 1024
2944
- });
2945
- return parseIdleSeconds(stdout);
2946
- } catch {
2947
- return null;
2948
- }
2949
- };
2950
- function parseIdleSeconds(ioregOutput) {
2951
- const match = HID_IDLE.exec(ioregOutput);
2952
- if (!match) return null;
2953
- const nanoseconds = Number(match[1]);
2954
- if (!Number.isFinite(nanoseconds) || nanoseconds < 0) return null;
2955
- return nanoseconds / 1e9;
2956
- }
2957
-
2958
3149
  // src/settings.ts
2959
3150
  var SETTING_GROUPS = [
2960
3151
  { id: "instance", title: "This dashboard" },
@@ -2963,8 +3154,7 @@ var SETTING_GROUPS = [
2963
3154
  { id: "github", title: "Pull requests" },
2964
3155
  { id: "jira", title: "Jira tickets" },
2965
3156
  { id: "calendar", title: "Calendar" },
2966
- { id: "day", title: "Working day" },
2967
- { id: "sessions", title: "Focus sessions" }
3157
+ { id: "day", title: "Working day" }
2968
3158
  ];
2969
3159
  var OFF_OR_CLI = [
2970
3160
  { value: "off", label: "Off" },
@@ -2978,7 +3168,7 @@ var SETTINGS = [
2978
3168
  key: "DAILY_FOCUS_PROFILE",
2979
3169
  group: "instance",
2980
3170
  label: "What this dashboard briefs",
2981
- help: "Work or home. Picks the morning prompt and the defaults below: the personal profile switches off Jira, free time and away detection.",
3171
+ help: "Work or home. Picks the morning prompt and the defaults below: the personal profile switches off Jira and free time.",
2982
3172
  kind: "choice",
2983
3173
  choices: PROFILES.map((value) => ({ value, label: value === "work" ? "Work" : "Personal life" })),
2984
3174
  fallback: always("Work")
@@ -3108,7 +3298,7 @@ var SETTINGS = [
3108
3298
  key: "DAILY_FOCUS_GITHUB",
3109
3299
  group: "github",
3110
3300
  label: "Pull request board",
3111
- help: "Reads your open pull requests through the GitHub CLI. Sign in with gh auth login.",
3301
+ help: "Reads your open pull requests through the GitHub CLI. Sign in with gh auth login. The Jira ticket board uses it too, to catch merges Jira hasn't heard about yet.",
3112
3302
  kind: "flag",
3113
3303
  fallback: always("On")
3114
3304
  },
@@ -3287,22 +3477,6 @@ var SETTINGS = [
3287
3477
  help: "Minutes a gap has to last to count as time for focused work.",
3288
3478
  kind: "number",
3289
3479
  fallback: always("45 minutes")
3290
- },
3291
- {
3292
- key: "DAILY_FOCUS_SESSION_MINUTES",
3293
- group: "sessions",
3294
- label: "Session length",
3295
- help: "Minutes a focus session is set for when you start one.",
3296
- kind: "number",
3297
- fallback: always("25 minutes")
3298
- },
3299
- {
3300
- key: "DAILY_FOCUS_AWAY_AFTER",
3301
- group: "sessions",
3302
- label: "End a session when away for",
3303
- help: "Minutes without touching the Mac before a session is closed where you left. 0 turns it off.",
3304
- kind: "number",
3305
- fallback: (profile) => profile === "personal" ? "0, off" : "10 minutes"
3306
3480
  }
3307
3481
  ];
3308
3482
  var BY_KEY = new Map(SETTINGS.map((def) => [def.key, def]));
@@ -3365,11 +3539,11 @@ async function saveSettings(path, settings) {
3365
3539
  }
3366
3540
 
3367
3541
  // src/setup.ts
3368
- import { execFile as execFile5 } from "node:child_process";
3542
+ import { execFile as execFile4 } from "node:child_process";
3369
3543
  import { access, constants, readdir as readdir2, readFile as readFile7, stat as stat3 } from "node:fs/promises";
3370
3544
  import { delimiter, isAbsolute, join as join3 } from "node:path";
3371
- import { promisify as promisify5 } from "node:util";
3372
- var run5 = promisify5(execFile5);
3545
+ import { promisify as promisify4 } from "node:util";
3546
+ var run4 = promisify4(execFile4);
3373
3547
  var PLACEHOLDERS = [
3374
3548
  "Your Name",
3375
3549
  "you@example.com",
@@ -3459,11 +3633,11 @@ var identity = null;
3459
3633
  function detectIdentity(config) {
3460
3634
  identity ??= (async () => {
3461
3635
  const [user, email] = await Promise.all([
3462
- run5(config.github.ghPath, ["api", "user", "--jq", '[.login, .name // ""] | join("\\n")'], { timeout: 5e3 }).then(
3636
+ run4(config.github.ghPath, ["api", "user", "--jq", '[.login, .name // ""] | join("\\n")'], { timeout: 5e3 }).then(
3463
3637
  ({ stdout }) => stdout.split("\n"),
3464
3638
  () => []
3465
3639
  ),
3466
- run5("git", ["config", "--global", "user.email"], { timeout: 2e3 }).then(
3640
+ run4("git", ["config", "--global", "user.email"], { timeout: 2e3 }).then(
3467
3641
  ({ stdout }) => stdout.trim() || null,
3468
3642
  () => null
3469
3643
  )
@@ -3512,8 +3686,8 @@ var defaultExec = (file, args, { timeout }) => new Promise((resolve2, reject) =>
3512
3686
  child.on("error", (error) => finish(error));
3513
3687
  child.on("close", () => finish(null));
3514
3688
  });
3515
- function shellCandidates(envShell, loginShell, platform2) {
3516
- const fallback = platform2 === "darwin" ? "/bin/zsh" : platform2 === "linux" ? "/bin/bash" : void 0;
3689
+ function shellCandidates(envShell, loginShell, platform) {
3690
+ const fallback = platform === "darwin" ? "/bin/zsh" : platform === "linux" ? "/bin/bash" : void 0;
3517
3691
  const shells = [];
3518
3692
  for (const shell of [envShell, loginShell, fallback]) {
3519
3693
  const trimmed = shell?.trim();
@@ -3558,11 +3732,11 @@ function currentLoginShell() {
3558
3732
  return void 0;
3559
3733
  }
3560
3734
  }
3561
- async function hydratePath(env = process.env, platform2 = process.platform, exec = defaultExec, loginShell = currentLoginShell()) {
3562
- if (platform2 === "win32") return null;
3735
+ async function hydratePath(env = process.env, platform = process.platform, exec = defaultExec, loginShell = currentLoginShell()) {
3736
+ if (platform === "win32") return null;
3563
3737
  let found = null;
3564
3738
  let from = null;
3565
- for (const shell of shellCandidates(env.SHELL, loginShell, platform2)) {
3739
+ for (const shell of shellCandidates(env.SHELL, loginShell, platform)) {
3566
3740
  try {
3567
3741
  found = await readLoginShellPath(shell, exec);
3568
3742
  } catch {
@@ -3572,7 +3746,7 @@ async function hydratePath(env = process.env, platform2 = process.platform, exec
3572
3746
  break;
3573
3747
  }
3574
3748
  }
3575
- if (!found && platform2 === "darwin") {
3749
+ if (!found && platform === "darwin") {
3576
3750
  found = await readLaunchctlPath(exec);
3577
3751
  if (found) from = "launchctl";
3578
3752
  }
@@ -3598,15 +3772,8 @@ var TEXT_FILES = {
3598
3772
  "/api/text/focus": { file: "focusFile", name: "focus.md" },
3599
3773
  "/api/text/sources": { file: "sourcesFile", name: "sources.md" }
3600
3774
  };
3601
- var VALID_ACTIONS = /* @__PURE__ */ new Set([
3602
- "done",
3603
- "snooze",
3604
- "dismiss",
3605
- "reopen",
3606
- "note"
3607
- ]);
3775
+ var VALID_ACTIONS = new Set(ACTION_TYPES);
3608
3776
  var MAX_BODY_BYTES = 64 * 1024;
3609
- var PRESENCE_POLL_MS = 3e4;
3610
3777
  var AGENT_CLOCK_MS = 3e4;
3611
3778
  function sendJSON(res, status, body) {
3612
3779
  const payload = JSON.stringify(body);
@@ -3808,13 +3975,11 @@ data: ${payload}
3808
3975
  () => {
3809
3976
  void store.archiveCurrentBrief().then(() => broadcast());
3810
3977
  },
3811
- // The timer re-stamps its own file every poll and the two boards rewrite
3812
- // prs.json and tickets.json every fetch; that's this process talking to
3813
- // itself, and all three already broadcast when something actually changed.
3978
+ // The two boards rewrite prs.json and tickets.json every fetch; that's this
3979
+ // process talking to itself, and both already broadcast when something
3980
+ // actually changed.
3814
3981
  {
3815
3982
  ignore: [
3816
- basename(config.sessionFile),
3817
- basename(tempPathFor(config.sessionFile)),
3818
3983
  basename(config.pullsFile),
3819
3984
  basename(tempPathFor(config.pullsFile)),
3820
3985
  basename(config.ticketsFile),
@@ -3834,32 +3999,6 @@ data: ${payload}
3834
3999
  void broadcast();
3835
4000
  });
3836
4001
  });
3837
- let sessionTick = null;
3838
- function scheduleSessionTick(endsAt) {
3839
- if (sessionTick) clearTimeout(sessionTick);
3840
- sessionTick = null;
3841
- if (!endsAt) return;
3842
- const delay = new Date(endsAt).getTime() - Date.now();
3843
- if (delay <= 0 || delay > 36e5) return;
3844
- sessionTick = setTimeout(() => void broadcast(), delay);
3845
- sessionTick.unref();
3846
- }
3847
- async function checkPresence() {
3848
- let closed;
3849
- try {
3850
- closed = await reconcileSession(config, readIdleSeconds);
3851
- } catch (err) {
3852
- console.error(`[daily-focus] presence check failed: ${err.message}`);
3853
- return;
3854
- }
3855
- if (!closed) return;
3856
- console.log(
3857
- `[daily-focus] stopped "${closed.title}" at ${closed.endedAt} after ${closed.actualMinutes} min \u2014 machine untouched since then`
3858
- );
3859
- scheduleSessionTick(null);
3860
- await broadcast();
3861
- }
3862
- await checkPresence();
3863
4002
  await assistant.start();
3864
4003
  await agent.start();
3865
4004
  let ticks = Promise.resolve();
@@ -3884,7 +4023,6 @@ data: ${payload}
3884
4023
  void clockTick();
3885
4024
  }, AGENT_CLOCK_MS);
3886
4025
  agentClock.unref();
3887
- scheduleSessionTick((await buildState()).session.active?.endsAt ?? null);
3888
4026
  void board.start().catch((err) => {
3889
4027
  console.error(`[daily-focus] pull request board failed to start: ${err.message}`);
3890
4028
  });
@@ -3898,10 +4036,6 @@ data: ${payload}
3898
4036
  void broadcast();
3899
4037
  }, 6e4);
3900
4038
  heartbeat.unref();
3901
- const presencePoll = setInterval(() => {
3902
- void checkPresence();
3903
- }, PRESENCE_POLL_MS);
3904
- presencePoll.unref();
3905
4039
  const server = createServer((req, res) => {
3906
4040
  void handle(req, res).catch((err) => {
3907
4041
  console.error(`[daily-focus] ${req.method} ${req.url} failed:`, err);
@@ -4089,8 +4223,8 @@ data: ${JSON.stringify(await buildState())}
4089
4223
  sendJSON(res, 400, { error: `invalid JSON body: ${err.message}` });
4090
4224
  return;
4091
4225
  }
4092
- const { run: run6, text } = typeof body === "object" && body !== null ? body : {};
4093
- if (typeof run6 !== "string" || run6 === "") {
4226
+ const { run: run5, text } = typeof body === "object" && body !== null ? body : {};
4227
+ if (typeof run5 !== "string" || run5 === "") {
4094
4228
  sendJSON(res, 400, { error: "run must be a run id" });
4095
4229
  return;
4096
4230
  }
@@ -4099,7 +4233,7 @@ data: ${JSON.stringify(await buildState())}
4099
4233
  return;
4100
4234
  }
4101
4235
  try {
4102
- await agent.ask(run6, text);
4236
+ await agent.ask(run5, text);
4103
4237
  } catch (err) {
4104
4238
  sendJSON(res, 409, { error: err.message });
4105
4239
  return;
@@ -4169,53 +4303,6 @@ data: ${JSON.stringify(await buildState())}
4169
4303
  void broadcast(state);
4170
4304
  return;
4171
4305
  }
4172
- if (path === "/api/session" && req.method === "POST") {
4173
- let body;
4174
- try {
4175
- body = JSON.parse(await readBody(req));
4176
- } catch (err) {
4177
- sendJSON(res, 400, { error: `invalid JSON body: ${err.message}` });
4178
- return;
4179
- }
4180
- if (typeof body !== "object" || body === null) {
4181
- sendJSON(res, 400, { error: "body must be an object" });
4182
- return;
4183
- }
4184
- const { action, id, minutes } = body;
4185
- if (action === "stop") {
4186
- await stopSession(config);
4187
- } else if (action === "start") {
4188
- if (typeof id !== "string" || id === "") {
4189
- sendJSON(res, 400, { error: '"id" is required to start a session' });
4190
- return;
4191
- }
4192
- const { brief } = await store.readBrief();
4193
- const item = brief?.items.find((candidate) => candidate.id === id);
4194
- if (!item) {
4195
- sendJSON(res, 404, { error: `no item with id ${id}` });
4196
- return;
4197
- }
4198
- const requested = typeof minutes === "number" && Number.isFinite(minutes) ? Math.round(minutes) : config.sessionMinutes;
4199
- if (requested < 1 || requested > 120) {
4200
- sendJSON(res, 400, { error: '"minutes" must be between 1 and 120' });
4201
- return;
4202
- }
4203
- await startSession(config, {
4204
- id,
4205
- title: item.title,
4206
- minutes: requested,
4207
- advancesObjective: item.advancesObjective === true
4208
- });
4209
- } else {
4210
- sendJSON(res, 400, { error: '"action" must be "start" or "stop"' });
4211
- return;
4212
- }
4213
- const state = await buildState();
4214
- sendState(res, state);
4215
- scheduleSessionTick(state.session.active?.endsAt ?? null);
4216
- void broadcast(state);
4217
- return;
4218
- }
4219
4306
  if (path === "/api/settings" && req.method === "GET") {
4220
4307
  sendJSON(res, 200, { ...describeSettings(readSources()), restart });
4221
4308
  return;
@@ -4343,9 +4430,7 @@ ${entry.source}`)).map(({ title, source }) => ({ title, source })).sort((a, b) =
4343
4430
  }
4344
4431
  async function stopEverything() {
4345
4432
  clearInterval(heartbeat);
4346
- clearInterval(presencePoll);
4347
4433
  clearInterval(agentClock);
4348
- if (sessionTick) clearTimeout(sessionTick);
4349
4434
  board.stop();
4350
4435
  calendar.stop();
4351
4436
  tickets.stop();