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 +12 -11
- package/SETUP.md +9 -6
- package/dist/{audit-ITRXIX6T.js → audit-DNTFZOHH.js} +6 -5
- package/dist/{chunk-AIQO2YAI.js → chunk-6SMUSPZG.js} +2 -2
- package/dist/{chunk-26JG3D5C.js → chunk-GQ3NCWRT.js} +113 -272
- package/dist/{chunk-RNE27FQY.js → chunk-KZNY45XW.js} +0 -5
- package/dist/{chunk-UY6HQLGE.js → chunk-P3E2B2SD.js} +1 -1
- package/dist/cli.js +6 -6
- package/dist/{demo-KVFAVGO7.js → demo-6Q22AAC7.js} +33 -24
- package/dist/{init-57WHZZNG.js → init-XDMAXU2S.js} +1 -9
- package/dist/{seed-VALN2TCQ.js → seed-TWT7EDXJ.js} +3 -3
- package/dist/{server-SOCS3SZJ.js → server-PIYOIUKJ.js} +336 -251
- package/dist/{service-NAAAV67B.js → service-ZUHI3VKH.js} +1 -1
- package/package.json +1 -1
- package/prompts/README.md +2 -3
- package/prompts/morning-brief-personal.md +1 -25
- package/prompts/morning-brief-work.md +8 -33
- package/public/app.js +175 -345
- package/public/style.css +12 -170
|
@@ -11,22 +11,20 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
TimeZoneKeeper,
|
|
13
13
|
validZone
|
|
14
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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
|
|
503
|
-
const sequence =
|
|
504
|
-
if (typeof sequence === "number") return [sequence,
|
|
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,
|
|
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,
|
|
1038
|
+
const quietSince = laterISO(waitingSince, nudgedAt) ?? waitingSince;
|
|
962
1039
|
const waited = now.getTime() - (ms(quietSince) ?? now.getTime());
|
|
963
|
-
|
|
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
|
|
970
|
-
const
|
|
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
|
|
1981
|
-
|
|
1982
|
-
|
|
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 (!
|
|
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 (
|
|
2182
|
+
if (unansweredNow.length > 0) {
|
|
2009
2183
|
warnings.push(
|
|
2010
|
-
`Could not read Jira's development panel for ${listed(
|
|
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 (
|
|
2187
|
+
if (unaccountedNow.length > 0) {
|
|
2014
2188
|
warnings.push(
|
|
2015
|
-
`Jira's search says ${listed(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
2457
|
-
if (!
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
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
|
-
|
|
2710
|
+
run5.status = "done";
|
|
2493
2711
|
} else if (record.event === "failed") {
|
|
2494
|
-
|
|
2495
|
-
|
|
2712
|
+
run5.status = "failed";
|
|
2713
|
+
run5.error = record.error;
|
|
2496
2714
|
} else if (record.event === "aborted") {
|
|
2497
|
-
|
|
2498
|
-
|
|
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
|
|
2539
|
-
if (
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
await this.append({ event: "aborted", run:
|
|
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
|
|
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:
|
|
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((
|
|
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
|
|
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(
|
|
2632
|
-
await this.append({ event: "started", run:
|
|
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(
|
|
2645
|
-
return
|
|
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
|
|
2656
|
-
if (!
|
|
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(
|
|
2880
|
+
if (!this.resumable(run5)) {
|
|
2663
2881
|
throw new Error(
|
|
2664
|
-
|
|
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
|
-
|
|
2679
|
-
await this.append({ event: "asked", run:
|
|
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:
|
|
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(
|
|
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(
|
|
2701
|
-
const 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(
|
|
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(
|
|
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:
|
|
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)
|
|
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
|
-
|
|
2745
|
-
|
|
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(
|
|
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(
|
|
2780
|
-
return
|
|
2997
|
+
resumable(run5) {
|
|
2998
|
+
return run5.status !== "running" && run5.sessionId !== null && run5.cli === this.config.agent.cli;
|
|
2781
2999
|
}
|
|
2782
|
-
async finish(
|
|
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:
|
|
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:
|
|
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:
|
|
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 ??
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
const 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:
|
|
2809
|
-
|
|
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
|
-
|
|
2812
|
-
await this.append({ event: "aborted", run:
|
|
2813
|
-
|
|
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
|
-
|
|
2816
|
-
await this.append({ event: "failed", run:
|
|
2817
|
-
|
|
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
|
|
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
|
|
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
|
|
3372
|
-
var
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
3516
|
-
const fallback =
|
|
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,
|
|
3562
|
-
if (
|
|
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,
|
|
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 &&
|
|
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 =
|
|
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
|
|
3812
|
-
//
|
|
3813
|
-
//
|
|
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:
|
|
4093
|
-
if (typeof
|
|
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(
|
|
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();
|