daily-focus 1.1.1-dev.2 → 1.2.0-dev.10
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 +34 -20
- package/SETUP.md +103 -21
- package/dist/{audit-W3O6CLOF.js → audit-7ZCLKF46.js} +10 -4
- package/dist/{chunk-Q5OKXJXT.js → chunk-64XB7UDN.js} +1 -1
- package/dist/{chunk-OPASVYLR.js → chunk-J7ZYVUG6.js} +5 -6
- package/dist/chunk-ODJGQCM3.js +1003 -0
- package/dist/{chunk-UM2EPNWR.js → chunk-RZNQJ6UG.js} +123 -282
- package/dist/chunk-Z4SFMMRQ.js +107 -0
- package/dist/cli.js +9 -8
- package/dist/{demo-OQLT3QWP.js → demo-BYLLMKSQ.js} +135 -32
- package/dist/{init-DEVCAQZZ.js → init-EMZDG2I7.js} +1 -9
- package/dist/{seed-SMKNFYTC.js → seed-JU4LQ33D.js} +3 -3
- package/dist/{server-5O3LQVEY.js → server-WQYUOKB3.js} +459 -385
- package/dist/{service-6ZFOHPWN.js → service-PUJ3YDH5.js} +1 -1
- package/package.json +2 -2
- package/prompts/README.md +2 -3
- package/prompts/assistant.md +14 -3
- package/prompts/morning-brief-personal.md +1 -25
- package/prompts/morning-brief-work.md +8 -33
- package/public/app.js +506 -431
- package/public/style.css +198 -181
- package/dist/chunk-GFCWPYEI.js +0 -456
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ADAPTERS,
|
|
3
|
-
Assistant
|
|
4
|
-
|
|
3
|
+
Assistant,
|
|
4
|
+
AutoAsk,
|
|
5
|
+
CodexGate,
|
|
6
|
+
codexVersionWarning,
|
|
7
|
+
countBoard,
|
|
8
|
+
resolveBoard
|
|
9
|
+
} from "./chunk-ODJGQCM3.js";
|
|
5
10
|
import {
|
|
6
11
|
FOCUS_TEMPLATE,
|
|
7
12
|
sourcesTemplate
|
|
8
13
|
} from "./chunk-V2UN3JYL.js";
|
|
14
|
+
import {
|
|
15
|
+
TimeZoneKeeper,
|
|
16
|
+
validZone
|
|
17
|
+
} from "./chunk-Z4SFMMRQ.js";
|
|
9
18
|
import {
|
|
10
19
|
describeLink,
|
|
11
20
|
linkStore,
|
|
12
21
|
storeLinkWarning
|
|
13
22
|
} from "./chunk-QKT2FAB5.js";
|
|
14
23
|
import {
|
|
24
|
+
ACTION_TYPES,
|
|
15
25
|
Store,
|
|
16
26
|
canonicalId,
|
|
17
27
|
foldActionLog,
|
|
18
|
-
reconcileSession,
|
|
19
|
-
startSession,
|
|
20
|
-
stopSession,
|
|
21
28
|
tempPathFor,
|
|
22
29
|
writeJsonAtomic
|
|
23
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-RZNQJ6UG.js";
|
|
24
31
|
import {
|
|
25
32
|
CLI_NAMES,
|
|
26
33
|
LAUNCH_SETTINGS,
|
|
@@ -32,11 +39,10 @@ import {
|
|
|
32
39
|
layerEnv,
|
|
33
40
|
loadConfig,
|
|
34
41
|
nextScheduledRun,
|
|
35
|
-
parseISO,
|
|
36
42
|
readDotEnv,
|
|
37
43
|
resolveSchedule,
|
|
38
44
|
scheduledRunDue
|
|
39
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-J7ZYVUG6.js";
|
|
40
46
|
import {
|
|
41
47
|
PACKAGED,
|
|
42
48
|
command,
|
|
@@ -493,9 +499,9 @@ function checkSlot(node) {
|
|
|
493
499
|
return `check\0${node.name ?? ""}`;
|
|
494
500
|
}
|
|
495
501
|
function checkGeneration(node) {
|
|
496
|
-
const
|
|
497
|
-
const sequence =
|
|
498
|
-
if (typeof sequence === "number") return [sequence,
|
|
502
|
+
const run5 = node.checkSuite?.workflowRun;
|
|
503
|
+
const sequence = run5?.runNumber ?? run5?.databaseId;
|
|
504
|
+
if (typeof sequence === "number") return [sequence, run5?.runAttempt ?? 0];
|
|
499
505
|
const at = node.completedAt ?? node.startedAt ?? node.createdAt ?? null;
|
|
500
506
|
const time = at ? Date.parse(at) : Number.NaN;
|
|
501
507
|
return [Number.isFinite(time) ? time : 0, 0];
|
|
@@ -811,6 +817,7 @@ async function fetchPulls(token, scope, expectedLogin, signal) {
|
|
|
811
817
|
for (const [nodeId, head] of checks.heads) {
|
|
812
818
|
const pull = found.get(nodeId);
|
|
813
819
|
if (!pull) continue;
|
|
820
|
+
pull.headSha = head.oid;
|
|
814
821
|
applyChecks(pull, head.rollup);
|
|
815
822
|
if (head.rollup && rollupTruncated(head.rollup)) {
|
|
816
823
|
partial.push({ id: nodeId, pull, oid: head.oid, rollup: head.rollup });
|
|
@@ -865,131 +872,84 @@ async function fetchPulls(token, scope, expectedLogin, signal) {
|
|
|
865
872
|
}
|
|
866
873
|
return { login, pulls, warnings, orgs, rateLimitRemaining };
|
|
867
874
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
var
|
|
871
|
-
var
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
875
|
+
var TICKET_PAGE_SIZE = 25;
|
|
876
|
+
var TICKET_MAX_PAGES = 12;
|
|
877
|
+
var TICKET_COMMITS = 100;
|
|
878
|
+
var TICKET_SEARCH_QUERY = `
|
|
879
|
+
query($q: String!, $first: Int!, $after: String) {
|
|
880
|
+
search(query: $q, type: ISSUE, first: $first, after: $after) {
|
|
881
|
+
pageInfo { hasNextPage endCursor }
|
|
882
|
+
nodes {
|
|
883
|
+
... on PullRequest {
|
|
884
|
+
number
|
|
885
|
+
title
|
|
886
|
+
url
|
|
887
|
+
state
|
|
888
|
+
isDraft
|
|
889
|
+
closedAt
|
|
890
|
+
mergedAt
|
|
891
|
+
headRefName
|
|
892
|
+
body
|
|
893
|
+
repository { nameWithOwner }
|
|
894
|
+
commits(first: ${TICKET_COMMITS}) { nodes { commit { message } } }
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}`;
|
|
899
|
+
function buildTicketSearchQueries(scope, closedSince) {
|
|
900
|
+
return [
|
|
901
|
+
["is:pr", "author:@me", "is:open", ...scope].join(" "),
|
|
902
|
+
["is:pr", "author:@me", "is:closed", `closed:>=${closedSince}`, ...scope].join(" ")
|
|
903
|
+
];
|
|
879
904
|
}
|
|
880
|
-
function
|
|
881
|
-
if (
|
|
882
|
-
if (
|
|
883
|
-
if (
|
|
905
|
+
function toLinkedState(state, isDraft) {
|
|
906
|
+
if (state === "MERGED") return "merged";
|
|
907
|
+
if (state === "CLOSED") return "closed";
|
|
908
|
+
if (state === "OPEN") return isDraft === true ? "draft" : "open";
|
|
884
909
|
return null;
|
|
885
910
|
}
|
|
886
|
-
function
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
const
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
const theirMove = others !== null && (you === null || others.at > you);
|
|
912
|
-
const approval = newest(pr, "APPROVED");
|
|
913
|
-
const afterApproval = others !== null && approval !== void 0 && others.at > approval.at;
|
|
914
|
-
const approved = decision === "APPROVED" && !afterApproval;
|
|
915
|
-
if (theirMove && others && !approved) {
|
|
916
|
-
reasons.push({ kind: "activity", login: others.login, at: others.at, activity: others.kind });
|
|
917
|
-
}
|
|
918
|
-
if (reasons.length > 0) {
|
|
919
|
-
const since = theirMove && others ? others.at : pr.updatedAt;
|
|
920
|
-
return { court: "you", decision, reasons, stale: false, nudge: false, since };
|
|
921
|
-
}
|
|
922
|
-
const waitingSince = laterISO(pr.readyAt, you) ?? pr.createdAt;
|
|
923
|
-
const settled = (court, reason) => ({
|
|
924
|
-
court,
|
|
925
|
-
decision,
|
|
926
|
-
reasons: [reason],
|
|
927
|
-
stale: false,
|
|
928
|
-
nudge: false,
|
|
929
|
-
since: waitingSince
|
|
930
|
-
});
|
|
931
|
-
if (decision !== "REVIEW_REQUIRED") {
|
|
932
|
-
const gate = matchMergeGates(pr, gates);
|
|
933
|
-
if (gate.length > 0) return settled("gate", { kind: "merge-gate", checks: gate });
|
|
934
|
-
const pending = pr.pendingChecks ?? [];
|
|
935
|
-
const running = { kind: "checks-pending" };
|
|
936
|
-
if (pending.length > 0) running.checks = pending;
|
|
937
|
-
if (mergeState === "BLOCKED" && pending.length === 0) {
|
|
938
|
-
return settled("blocked", { kind: "merge-blocked" });
|
|
939
|
-
}
|
|
940
|
-
if (mergeState === "BLOCKED" || mergeState === "UNSTABLE") return settled("checks", running);
|
|
941
|
-
if (mergeState === "UNKNOWN") return settled("checks", { kind: "mergeability-unknown" });
|
|
942
|
-
if (approved) {
|
|
943
|
-
if (mergeState === "CLEAN" || mergeState === "HAS_HOOKS") {
|
|
944
|
-
return { court: "ready", decision, reasons: [], stale: false, nudge: false, since: approval?.at ?? pr.updatedAt };
|
|
911
|
+
function normalizeTicketPull(raw) {
|
|
912
|
+
const repo = raw.repository?.nameWithOwner;
|
|
913
|
+
const url = safeUrl(raw.url);
|
|
914
|
+
const state = toLinkedState(raw.state, raw.isDraft);
|
|
915
|
+
if (typeof raw.number !== "number" || typeof repo !== "string" || typeof raw.title !== "string" || !url || !state) {
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
const closedAt = state === "merged" ? raw.mergedAt ?? raw.closedAt ?? null : state === "closed" ? raw.closedAt ?? null : null;
|
|
919
|
+
const text = [raw.title, raw.headRefName ?? "", raw.body ?? ""];
|
|
920
|
+
for (const node of raw.commits?.nodes ?? []) {
|
|
921
|
+
const message = node?.commit?.message;
|
|
922
|
+
if (typeof message === "string") text.push(message);
|
|
923
|
+
}
|
|
924
|
+
return { pull: { repo, number: raw.number, title: raw.title, url, state, closedAt }, text };
|
|
925
|
+
}
|
|
926
|
+
async function fetchTicketPulls(token, scope, closedSince, signal) {
|
|
927
|
+
const pulls = [];
|
|
928
|
+
let complete = true;
|
|
929
|
+
for (const q of buildTicketSearchQueries(scope, closedSince)) {
|
|
930
|
+
let after = null;
|
|
931
|
+
for (let page = 0; ; page++) {
|
|
932
|
+
const result = await graphql(token, TICKET_SEARCH_QUERY, { q, first: TICKET_PAGE_SIZE, after }, signal);
|
|
933
|
+
const fatal = result.errors.filter((error) => !isSamlError(error));
|
|
934
|
+
if (result.data?.search === void 0 && fatal.length > 0) {
|
|
935
|
+
throw new GitHubRequestError(`GitHub search failed: ${fatal.map((error) => error.message).join("; ")}`);
|
|
945
936
|
}
|
|
946
|
-
if (
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
return settled("checks", running);
|
|
937
|
+
if (result.errors.length > 0) complete = false;
|
|
938
|
+
for (const node of result.data?.search?.nodes ?? []) {
|
|
939
|
+
const found = node ? normalizeTicketPull(node) : null;
|
|
940
|
+
if (found) pulls.push(found);
|
|
941
|
+
else complete = false;
|
|
952
942
|
}
|
|
943
|
+
const pageInfo = result.data?.search?.pageInfo;
|
|
944
|
+
if (!pageInfo?.hasNextPage || !pageInfo.endCursor) break;
|
|
945
|
+
if (page === TICKET_MAX_PAGES - 1) {
|
|
946
|
+
complete = false;
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
after = pageInfo.endCursor;
|
|
953
950
|
}
|
|
954
951
|
}
|
|
955
|
-
|
|
956
|
-
const waited = now.getTime() - (ms(quietSince) ?? now.getTime());
|
|
957
|
-
return { court: "reviewers", decision, reasons, stale: false, nudge: waited >= NUDGE_AFTER_MS, since: waitingSince };
|
|
958
|
-
}
|
|
959
|
-
function resolveBoard(pulls, actions, now, gates = []) {
|
|
960
|
-
const folded = foldActionLog(actions, now);
|
|
961
|
-
const rows = pulls.map((pr) => {
|
|
962
|
-
const log = folded.get(canonicalId(pr.id));
|
|
963
|
-
const notes = log?.notes ?? [];
|
|
964
|
-
const lastNoteAt = notes.length > 0 ? notes[notes.length - 1].at : null;
|
|
965
|
-
const verdict = judge(pr, now, lastNoteAt, gates);
|
|
966
|
-
const row = { ...pr, ...verdict, status: log?.status === "snoozed" ? "snoozed" : "open", notes };
|
|
967
|
-
if (row.status === "snoozed" && log?.snoozedUntil) row.snoozedUntil = log.snoozedUntil;
|
|
968
|
-
return row;
|
|
969
|
-
});
|
|
970
|
-
return rows.sort((a, b) => {
|
|
971
|
-
const court = courtOrder(a.court) - courtOrder(b.court);
|
|
972
|
-
if (court !== 0) return court;
|
|
973
|
-
if (a.since !== b.since) return a.since < b.since ? -1 : 1;
|
|
974
|
-
return a.id < b.id ? -1 : 1;
|
|
975
|
-
});
|
|
976
|
-
}
|
|
977
|
-
function countBoard(rows) {
|
|
978
|
-
const counts = {
|
|
979
|
-
you: 0,
|
|
980
|
-
ready: 0,
|
|
981
|
-
reviewers: 0,
|
|
982
|
-
gate: 0,
|
|
983
|
-
blocked: 0,
|
|
984
|
-
checks: 0,
|
|
985
|
-
draft: 0,
|
|
986
|
-
parked: 0
|
|
987
|
-
};
|
|
988
|
-
for (const row of rows) {
|
|
989
|
-
if (row.status === "snoozed") counts.parked++;
|
|
990
|
-
else counts[row.court]++;
|
|
991
|
-
}
|
|
992
|
-
return counts;
|
|
952
|
+
return { pulls, complete };
|
|
993
953
|
}
|
|
994
954
|
|
|
995
955
|
// src/board.ts
|
|
@@ -1353,6 +1313,9 @@ function normalizeStoredPull(raw) {
|
|
|
1353
1313
|
autoMerge: raw.autoMerge === true,
|
|
1354
1314
|
requestedReviewers: strings(raw.requestedReviewers),
|
|
1355
1315
|
reviews,
|
|
1316
|
+
// Kept, or a restart would hand the assistant's answers ahead about failing
|
|
1317
|
+
// checks a key the live read doesn't, and they would be asked again.
|
|
1318
|
+
headSha: typeof raw.headSha === "string" ? raw.headSha : null,
|
|
1356
1319
|
lastActivityByYou: typeof raw.lastActivityByYou === "string" ? raw.lastActivityByYou : null,
|
|
1357
1320
|
lastActivityByOthers
|
|
1358
1321
|
};
|
|
@@ -1488,7 +1451,8 @@ async function runHelper(appPath, selfAddresses) {
|
|
|
1488
1451
|
const out = join(tmpdir(), `daily-focus-calendar-${process.pid}-${Date.now()}.json`);
|
|
1489
1452
|
try {
|
|
1490
1453
|
try {
|
|
1491
|
-
|
|
1454
|
+
const zone = process.env.TZ ? ["--env", `TZ=${process.env.TZ}`] : [];
|
|
1455
|
+
await run2("/usr/bin/open", ["-n", "-a", appPath, ...zone, "--args", out, selfAddresses.join(",")], {
|
|
1492
1456
|
timeout: HELPER_TIMEOUT_MS
|
|
1493
1457
|
});
|
|
1494
1458
|
} catch (err) {
|
|
@@ -1687,6 +1651,87 @@ import { readFile as readFile5 } from "node:fs/promises";
|
|
|
1687
1651
|
// src/jira.ts
|
|
1688
1652
|
import { execFile as execFile3 } from "node:child_process";
|
|
1689
1653
|
import { promisify as promisify3 } from "node:util";
|
|
1654
|
+
|
|
1655
|
+
// src/linked.ts
|
|
1656
|
+
var CLOSED_LOOKBACK_DAYS = 60;
|
|
1657
|
+
var KEYISH = /(?<![A-Za-z0-9])([A-Za-z][A-Za-z0-9_]*)-(\d+)/g;
|
|
1658
|
+
function issueKeysIn(text) {
|
|
1659
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1660
|
+
for (const match of text.matchAll(KEYISH)) {
|
|
1661
|
+
const parts = match[1].split("_");
|
|
1662
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1663
|
+
const project = parts.slice(i).join("_");
|
|
1664
|
+
if (/^[A-Za-z]/.test(project)) keys.add(`${project.toUpperCase()}-${match[2]}`);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
return keys;
|
|
1668
|
+
}
|
|
1669
|
+
var realDeps3 = { token: ghToken, search: fetchTicketPulls };
|
|
1670
|
+
var CONSEQUENCE = "so a merged pull request counts only once Jira has it";
|
|
1671
|
+
async function readLinkedPulls(github, now, signal, deps = realDeps3) {
|
|
1672
|
+
const closedSince = new Date(now.getTime() - CLOSED_LOOKBACK_DAYS * 864e5).toISOString().slice(0, 10);
|
|
1673
|
+
const logins = github.accounts.length > 0 ? [...github.accounts] : [null];
|
|
1674
|
+
const results = await Promise.allSettled(
|
|
1675
|
+
logins.map(async (login) => deps.search(await deps.token(github.ghPath, login), github.scope, closedSince, signal))
|
|
1676
|
+
);
|
|
1677
|
+
if (results.every((result) => result.status === "rejected")) {
|
|
1678
|
+
throw results[0].reason;
|
|
1679
|
+
}
|
|
1680
|
+
const warnings = [];
|
|
1681
|
+
let complete = true;
|
|
1682
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1683
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1684
|
+
results.forEach((result, i) => {
|
|
1685
|
+
const who = logins[i] ? ` as ${logins[i]}` : "";
|
|
1686
|
+
if (result.status === "rejected") {
|
|
1687
|
+
complete = false;
|
|
1688
|
+
warnings.push(`Could not ask GitHub${who} about these tickets' pull requests (${result.reason.message}), ${CONSEQUENCE}.`);
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
if (!result.value.complete) {
|
|
1692
|
+
complete = false;
|
|
1693
|
+
warnings.push(`GitHub did not list every pull request${who}, ${CONSEQUENCE}.`);
|
|
1694
|
+
}
|
|
1695
|
+
for (const { pull, text } of result.value.pulls) {
|
|
1696
|
+
if (seen.has(pull.url)) continue;
|
|
1697
|
+
seen.add(pull.url);
|
|
1698
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1699
|
+
for (const piece of text) for (const key of issueKeysIn(piece)) keys.add(key);
|
|
1700
|
+
for (const key of keys) byKey.set(key, [...byKey.get(key) ?? [], pull]);
|
|
1701
|
+
}
|
|
1702
|
+
});
|
|
1703
|
+
return { byKey, complete, warnings };
|
|
1704
|
+
}
|
|
1705
|
+
function isOpen(pull) {
|
|
1706
|
+
return pull.state === "open" || pull.state === "draft";
|
|
1707
|
+
}
|
|
1708
|
+
function byRecency(a, b) {
|
|
1709
|
+
if (isOpen(a) !== isOpen(b)) return isOpen(a) ? -1 : 1;
|
|
1710
|
+
return (b.closedAt ?? "").localeCompare(a.closedAt ?? "");
|
|
1711
|
+
}
|
|
1712
|
+
function linkedTo(ticket, linked) {
|
|
1713
|
+
return linked?.byKey.get(ticket.key.toUpperCase()) ?? [];
|
|
1714
|
+
}
|
|
1715
|
+
function needsJiraCount(ticket, linked) {
|
|
1716
|
+
const found = linkedTo(ticket, linked);
|
|
1717
|
+
return Boolean(linked?.complete) && found.length > 0 && !found.some(isOpen) && ticket.hasAnyPr && !ticket.allPrsClosed;
|
|
1718
|
+
}
|
|
1719
|
+
function reconcile(ticket, linked, jiraCount) {
|
|
1720
|
+
const found = [...linkedTo(ticket, linked)].sort(byRecency);
|
|
1721
|
+
if (found.length === 0) return ticket;
|
|
1722
|
+
const jira = { hasAnyPr: ticket.hasAnyPr, hasOpenPr: ticket.hasOpenPr, allPrsClosed: ticket.allPrsClosed };
|
|
1723
|
+
let reading = jira;
|
|
1724
|
+
if (found.some(isOpen)) {
|
|
1725
|
+
reading = { hasAnyPr: true, hasOpenPr: true, allPrsClosed: false };
|
|
1726
|
+
} else if (linked?.complete && !ticket.allPrsClosed) {
|
|
1727
|
+
const known = ticket.hasAnyPr ? jiraCount : 0;
|
|
1728
|
+
if (known !== null && found.length >= known) reading = { hasAnyPr: true, hasOpenPr: false, allPrsClosed: true };
|
|
1729
|
+
}
|
|
1730
|
+
const changed = reading.hasAnyPr !== jira.hasAnyPr || reading.hasOpenPr !== jira.hasOpenPr || reading.allPrsClosed !== jira.allPrsClosed;
|
|
1731
|
+
return { ...ticket, ...reading, pulls: found, ...changed ? { fromGitHub: true } : {} };
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// src/jira.ts
|
|
1690
1735
|
var run3 = promisify3(execFile3);
|
|
1691
1736
|
var ACLI_TIMEOUT_MS = 45e3;
|
|
1692
1737
|
var PAGE_LIMIT = 200;
|
|
@@ -1970,22 +2015,23 @@ async function fetchTickets(acliPath, opts) {
|
|
|
1970
2015
|
if (unreadable > 0) {
|
|
1971
2016
|
warnings.push(`Skipped ${unreadable} work ${unreadable === 1 ? "item" : "items"} Jira described in a way this board could not read.`);
|
|
1972
2017
|
}
|
|
1973
|
-
const
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
DEV_CONCURRENCY,
|
|
1977
|
-
(ticket) => fetchDevPullRequests(acliPath, ticket.key).then(
|
|
1978
|
-
(dev) => ({ asked: true, dev }),
|
|
1979
|
-
() => ({ asked: false, dev: null })
|
|
1980
|
-
)
|
|
2018
|
+
const readPanel = (ticket) => fetchDevPullRequests(acliPath, ticket.key).then(
|
|
2019
|
+
(dev) => ({ asked: true, dev }),
|
|
2020
|
+
() => ({ asked: false, dev: null })
|
|
1981
2021
|
);
|
|
2022
|
+
const candidates = tickets.filter((ticket) => ticket.hasAnyPr && !ticket.hasOpenPr);
|
|
2023
|
+
const reading = mapLimit(candidates, DEV_CONCURRENCY, readPanel);
|
|
2024
|
+
const linked = await opts.linked ?? null;
|
|
2025
|
+
const panels = await reading;
|
|
2026
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1982
2027
|
const unanswered = [];
|
|
1983
2028
|
const unaccounted = [];
|
|
1984
2029
|
candidates.forEach((ticket, i) => {
|
|
1985
|
-
const { asked, dev } = panels[i];
|
|
2030
|
+
const { asked: asked2, dev } = panels[i];
|
|
2031
|
+
if (dev) counts.set(ticket.key, dev.count);
|
|
1986
2032
|
if (allPullRequestsClosed(dev)) {
|
|
1987
2033
|
ticket.allPrsClosed = true;
|
|
1988
|
-
} else if (!
|
|
2034
|
+
} else if (!asked2) {
|
|
1989
2035
|
unanswered.push(ticket.key);
|
|
1990
2036
|
} else if (dev === null) {
|
|
1991
2037
|
unaccounted.push(ticket.key);
|
|
@@ -1993,19 +2039,30 @@ async function fetchTickets(acliPath, opts) {
|
|
|
1993
2039
|
ticket.hasOpenPr = true;
|
|
1994
2040
|
}
|
|
1995
2041
|
});
|
|
2042
|
+
const asked = new Set(candidates);
|
|
2043
|
+
const behind = tickets.filter((ticket) => !asked.has(ticket) && needsJiraCount(ticket, linked));
|
|
2044
|
+
const extra = await mapLimit(behind, DEV_CONCURRENCY, readPanel);
|
|
2045
|
+
behind.forEach((ticket, i) => {
|
|
2046
|
+
const dev = extra[i].dev;
|
|
2047
|
+
if (dev) counts.set(ticket.key, dev.count);
|
|
2048
|
+
});
|
|
2049
|
+
const reconciled = tickets.map((ticket) => reconcile(ticket, linked, counts.get(ticket.key) ?? null));
|
|
2050
|
+
const undecided = new Set(reconciled.filter((ticket) => !ticket.hasOpenPr && !ticket.allPrsClosed).map((ticket) => ticket.key));
|
|
2051
|
+
const unansweredNow = unanswered.filter((key) => undecided.has(key));
|
|
2052
|
+
const unaccountedNow = unaccounted.filter((key) => undecided.has(key));
|
|
1996
2053
|
const listed = (keys2) => keys2.map((key) => {
|
|
1997
2054
|
const url = browseUrl(opts.site, key);
|
|
1998
2055
|
return url === null ? key : `[${key}](${url})`;
|
|
1999
2056
|
}).join(", ");
|
|
2000
2057
|
const heldOut = (keys2) => `${keys2.length === 1 ? "it is" : "they are"} held out of "no open pull requests left" rather than guessed at`;
|
|
2001
|
-
if (
|
|
2058
|
+
if (unansweredNow.length > 0) {
|
|
2002
2059
|
warnings.push(
|
|
2003
|
-
`Could not read Jira's development panel for ${listed(
|
|
2060
|
+
`Could not read Jira's development panel for ${listed(unansweredNow)}, so ${heldOut(unansweredNow)}. The next read may well manage it.`
|
|
2004
2061
|
);
|
|
2005
2062
|
}
|
|
2006
|
-
if (
|
|
2063
|
+
if (unaccountedNow.length > 0) {
|
|
2007
2064
|
warnings.push(
|
|
2008
|
-
`Jira's search says ${listed(
|
|
2065
|
+
`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.`
|
|
2009
2066
|
);
|
|
2010
2067
|
}
|
|
2011
2068
|
if (!opts.site) {
|
|
@@ -2022,7 +2079,7 @@ async function fetchTickets(acliPath, opts) {
|
|
|
2022
2079
|
);
|
|
2023
2080
|
}
|
|
2024
2081
|
return {
|
|
2025
|
-
tickets,
|
|
2082
|
+
tickets: reconciled,
|
|
2026
2083
|
statuses: statusesByProject(base, ...finished.map((batch) => batch ?? [])),
|
|
2027
2084
|
warnings
|
|
2028
2085
|
};
|
|
@@ -2094,16 +2151,16 @@ function readTransitionReport(stdout, key, status) {
|
|
|
2094
2151
|
}
|
|
2095
2152
|
|
|
2096
2153
|
// src/tickets.ts
|
|
2097
|
-
var
|
|
2154
|
+
var COURT_ORDER = ["settled", "started", "idle"];
|
|
2098
2155
|
function ticketCourtOrder(court) {
|
|
2099
|
-
return
|
|
2156
|
+
return COURT_ORDER.indexOf(court);
|
|
2100
2157
|
}
|
|
2101
2158
|
function inStatus(ticket, statuses) {
|
|
2102
2159
|
const status = ticket.workflowStatus.trim().toLowerCase();
|
|
2103
2160
|
if (status === "") return false;
|
|
2104
2161
|
return statuses.some((listed) => listed.trim().toLowerCase() === status);
|
|
2105
2162
|
}
|
|
2106
|
-
function
|
|
2163
|
+
function judge(ticket, holdStatuses = []) {
|
|
2107
2164
|
if (ticket.statusCategory === "done") return null;
|
|
2108
2165
|
if (ticket.hasAnyPr && !ticket.hasOpenPr && ticket.allPrsClosed) return "settled";
|
|
2109
2166
|
if (ticket.statusCategory === "new" && ticket.hasOpenPr) return "started";
|
|
@@ -2114,7 +2171,7 @@ function resolveTickets(tickets, actions, now, holdStatuses = []) {
|
|
|
2114
2171
|
const folded = foldActionLog(actions, now);
|
|
2115
2172
|
const rows = [];
|
|
2116
2173
|
for (const ticket of tickets) {
|
|
2117
|
-
const court =
|
|
2174
|
+
const court = judge(ticket, holdStatuses);
|
|
2118
2175
|
if (!court) continue;
|
|
2119
2176
|
const log = folded.get(canonicalId(ticket.id));
|
|
2120
2177
|
const row = {
|
|
@@ -2143,7 +2200,13 @@ function countTickets(rows) {
|
|
|
2143
2200
|
|
|
2144
2201
|
// src/ticketboard.ts
|
|
2145
2202
|
var MAX_BACKOFF_MS3 = 30 * 6e4;
|
|
2146
|
-
var
|
|
2203
|
+
var GITHUB_DEADLINE_MS = 9e4;
|
|
2204
|
+
var realDeps4 = {
|
|
2205
|
+
identity: acliIdentity,
|
|
2206
|
+
fetch: fetchTickets,
|
|
2207
|
+
transition: transitionTicket,
|
|
2208
|
+
linked: (github, now, signal) => readLinkedPulls(github, now, signal)
|
|
2209
|
+
};
|
|
2147
2210
|
var TicketBoard = class {
|
|
2148
2211
|
#config;
|
|
2149
2212
|
#deps;
|
|
@@ -2160,7 +2223,8 @@ var TicketBoard = class {
|
|
|
2160
2223
|
#audience = 0;
|
|
2161
2224
|
#stopped = false;
|
|
2162
2225
|
#lastReadAt = 0;
|
|
2163
|
-
|
|
2226
|
+
#abort = new AbortController();
|
|
2227
|
+
constructor(config, onChange, deps = realDeps4) {
|
|
2164
2228
|
this.#config = config;
|
|
2165
2229
|
this.#deps = deps;
|
|
2166
2230
|
this.#onChange = onChange;
|
|
@@ -2174,10 +2238,12 @@ var TicketBoard = class {
|
|
|
2174
2238
|
this.#file = await readTicketsFile(this.#config.ticketsFile);
|
|
2175
2239
|
await this.refresh();
|
|
2176
2240
|
}
|
|
2241
|
+
/** Stop reading, and cancel a GitHub request in flight so nothing lands after close. */
|
|
2177
2242
|
stop() {
|
|
2178
2243
|
this.#stopped = true;
|
|
2179
2244
|
if (this.#timer) clearTimeout(this.#timer);
|
|
2180
2245
|
this.#timer = null;
|
|
2246
|
+
this.#abort.abort();
|
|
2181
2247
|
}
|
|
2182
2248
|
/** How many browsers are watching. Polling pauses at zero. */
|
|
2183
2249
|
setAudience(count) {
|
|
@@ -2292,8 +2358,24 @@ var TicketBoard = class {
|
|
|
2292
2358
|
return;
|
|
2293
2359
|
}
|
|
2294
2360
|
const site = configuredSite ?? identity2.site;
|
|
2361
|
+
const githubWarnings = [];
|
|
2362
|
+
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(
|
|
2363
|
+
(read) => {
|
|
2364
|
+
githubWarnings.push(...read.warnings);
|
|
2365
|
+
return read;
|
|
2366
|
+
},
|
|
2367
|
+
(err) => {
|
|
2368
|
+
githubWarnings.push(
|
|
2369
|
+
`Could not ask GitHub about these tickets' pull requests (${err.message}), so they are as Jira has them, which can be days behind.`
|
|
2370
|
+
);
|
|
2371
|
+
return null;
|
|
2372
|
+
}
|
|
2373
|
+
) : Promise.resolve(null);
|
|
2295
2374
|
try {
|
|
2296
|
-
const
|
|
2375
|
+
const fetched = await this.#deps.fetch(acliPath, { projects, site, linked });
|
|
2376
|
+
await linked;
|
|
2377
|
+
const { tickets, statuses } = fetched;
|
|
2378
|
+
const warnings = [...fetched.warnings, ...githubWarnings];
|
|
2297
2379
|
this.#reason = null;
|
|
2298
2380
|
this.#warnings = warnings;
|
|
2299
2381
|
this.#streak = null;
|
|
@@ -2329,7 +2411,7 @@ function normalizeStoredTicket(raw) {
|
|
|
2329
2411
|
const { key, url, workflowStatus, statusCategory, issueType, summary } = raw;
|
|
2330
2412
|
if (typeof key !== "string" || key === "") return null;
|
|
2331
2413
|
const site = typeof url === "string" ? /^https:\/\/([^/]+)\/browse\//.exec(url)?.[1] ?? null : null;
|
|
2332
|
-
|
|
2414
|
+
const ticket = toTicket(
|
|
2333
2415
|
{
|
|
2334
2416
|
key,
|
|
2335
2417
|
fields: {
|
|
@@ -2346,6 +2428,25 @@ function normalizeStoredTicket(raw) {
|
|
|
2346
2428
|
// files are exactly the ones whose settled court counted drafts as merges.
|
|
2347
2429
|
raw.allPrsClosed === true
|
|
2348
2430
|
);
|
|
2431
|
+
if (!ticket) return null;
|
|
2432
|
+
const pulls = storedPulls(raw.pulls);
|
|
2433
|
+
if (pulls.length > 0) {
|
|
2434
|
+
ticket.pulls = pulls;
|
|
2435
|
+
if (raw.fromGitHub === true) ticket.fromGitHub = true;
|
|
2436
|
+
}
|
|
2437
|
+
return ticket;
|
|
2438
|
+
}
|
|
2439
|
+
var PULL_STATES = ["open", "draft", "merged", "closed"];
|
|
2440
|
+
function storedPulls(value) {
|
|
2441
|
+
if (!Array.isArray(value)) return [];
|
|
2442
|
+
return value.flatMap((entry) => {
|
|
2443
|
+
if (!isRecord3(entry)) return [];
|
|
2444
|
+
const { repo, number, title, url, state, closedAt } = entry;
|
|
2445
|
+
if (typeof repo !== "string" || typeof number !== "number" || typeof title !== "string") return [];
|
|
2446
|
+
if (typeof url !== "string" || !/^https?:\/\//i.test(url)) return [];
|
|
2447
|
+
if (typeof state !== "string" || !PULL_STATES.includes(state)) return [];
|
|
2448
|
+
return [{ repo, number, title, url, state, closedAt: typeof closedAt === "string" ? closedAt : null }];
|
|
2449
|
+
});
|
|
2349
2450
|
}
|
|
2350
2451
|
async function readTicketsFile(path) {
|
|
2351
2452
|
let text;
|
|
@@ -2446,11 +2547,11 @@ function foldAgentLog(text) {
|
|
|
2446
2547
|
});
|
|
2447
2548
|
continue;
|
|
2448
2549
|
}
|
|
2449
|
-
const
|
|
2450
|
-
if (!
|
|
2550
|
+
const run5 = runs.get(record.run);
|
|
2551
|
+
if (!run5) continue;
|
|
2451
2552
|
if (record.event === "asked") {
|
|
2452
2553
|
if (typeof record.turn !== "string" || typeof record.question !== "string") continue;
|
|
2453
|
-
|
|
2554
|
+
run5.turns.push({
|
|
2454
2555
|
id: record.turn,
|
|
2455
2556
|
question: record.question,
|
|
2456
2557
|
startedAt: record.at,
|
|
@@ -2462,7 +2563,7 @@ function foldAgentLog(text) {
|
|
|
2462
2563
|
continue;
|
|
2463
2564
|
}
|
|
2464
2565
|
if (record.event === "replied" || record.event === "turn-failed" || record.event === "turn-aborted") {
|
|
2465
|
-
const turn =
|
|
2566
|
+
const turn = run5.turns.find((candidate) => candidate.id === record.turn);
|
|
2466
2567
|
if (!turn) continue;
|
|
2467
2568
|
turn.reply = typeof record.reply === "string" ? record.reply : turn.reply;
|
|
2468
2569
|
turn.endedAt = record.at;
|
|
@@ -2477,18 +2578,18 @@ function foldAgentLog(text) {
|
|
|
2477
2578
|
}
|
|
2478
2579
|
continue;
|
|
2479
2580
|
}
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2581
|
+
run5.sessionId = typeof record.sessionId === "string" ? record.sessionId : run5.sessionId;
|
|
2582
|
+
run5.report = typeof record.report === "string" ? record.report : run5.report;
|
|
2583
|
+
run5.messages = Array.isArray(record.messages) ? record.messages.filter((message) => typeof message === "string") : run5.report ? [run5.report] : [];
|
|
2584
|
+
run5.endedAt = record.at;
|
|
2484
2585
|
if (record.event === "finished") {
|
|
2485
|
-
|
|
2586
|
+
run5.status = "done";
|
|
2486
2587
|
} else if (record.event === "failed") {
|
|
2487
|
-
|
|
2488
|
-
|
|
2588
|
+
run5.status = "failed";
|
|
2589
|
+
run5.error = record.error;
|
|
2489
2590
|
} else if (record.event === "aborted") {
|
|
2490
|
-
|
|
2491
|
-
|
|
2591
|
+
run5.status = "aborted";
|
|
2592
|
+
run5.error = record.reason;
|
|
2492
2593
|
}
|
|
2493
2594
|
}
|
|
2494
2595
|
return [...runs.values()].sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
@@ -2501,10 +2602,13 @@ var AgentRunner = class {
|
|
|
2501
2602
|
config;
|
|
2502
2603
|
hooks;
|
|
2503
2604
|
spawnFn;
|
|
2504
|
-
|
|
2605
|
+
/** Refuses a Codex too old to hold to the connector rules; see `codex.ts`. */
|
|
2606
|
+
codexGate;
|
|
2607
|
+
constructor(config, hooks, spawnFn = spawn, execFn) {
|
|
2505
2608
|
this.config = config;
|
|
2506
2609
|
this.hooks = hooks;
|
|
2507
2610
|
this.spawnFn = spawnFn;
|
|
2611
|
+
this.codexGate = new CodexGate(config.agent.binPath, execFn);
|
|
2508
2612
|
}
|
|
2509
2613
|
get enabled() {
|
|
2510
2614
|
return this.config.agent.cli !== null;
|
|
@@ -2525,19 +2629,19 @@ var AgentRunner = class {
|
|
|
2525
2629
|
this.runs = foldAgentLog(text);
|
|
2526
2630
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2527
2631
|
const reason = "the dashboard restarted while this was running";
|
|
2528
|
-
for (const
|
|
2529
|
-
if (
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
await this.append({ event: "aborted", run:
|
|
2632
|
+
for (const run5 of this.runs) {
|
|
2633
|
+
if (run5.status === "running") {
|
|
2634
|
+
run5.status = "aborted";
|
|
2635
|
+
run5.error = reason;
|
|
2636
|
+
run5.endedAt = at;
|
|
2637
|
+
await this.append({ event: "aborted", run: run5.id, sessionId: run5.sessionId, reason, report: run5.report, messages: run5.messages, at });
|
|
2534
2638
|
}
|
|
2535
|
-
for (const turn of
|
|
2639
|
+
for (const turn of run5.turns) {
|
|
2536
2640
|
if (turn.status !== "running") continue;
|
|
2537
2641
|
turn.status = "aborted";
|
|
2538
2642
|
turn.error = reason;
|
|
2539
2643
|
turn.endedAt = at;
|
|
2540
|
-
await this.append({ event: "turn-aborted", run:
|
|
2644
|
+
await this.append({ event: "turn-aborted", run: run5.id, turn: turn.id, reason, reply: turn.reply, at });
|
|
2541
2645
|
}
|
|
2542
2646
|
}
|
|
2543
2647
|
}
|
|
@@ -2550,7 +2654,7 @@ var AgentRunner = class {
|
|
|
2550
2654
|
return this.runs.at(-1) ?? null;
|
|
2551
2655
|
}
|
|
2552
2656
|
view(now = /* @__PURE__ */ new Date()) {
|
|
2553
|
-
const runs = this.runs.slice(-RUNS_SHOWN).reverse().map((
|
|
2657
|
+
const runs = this.runs.slice(-RUNS_SHOWN).reverse().map((run5) => ({ ...run5, resumable: this.resumable(run5) }));
|
|
2554
2658
|
return {
|
|
2555
2659
|
enabled: this.enabled,
|
|
2556
2660
|
cli: this.config.agent.cli,
|
|
@@ -2604,7 +2708,7 @@ var AgentRunner = class {
|
|
|
2604
2708
|
);
|
|
2605
2709
|
}
|
|
2606
2710
|
await this.start();
|
|
2607
|
-
const
|
|
2711
|
+
const run5 = {
|
|
2608
2712
|
id: randomUUID(),
|
|
2609
2713
|
cli,
|
|
2610
2714
|
trigger,
|
|
@@ -2618,8 +2722,8 @@ var AgentRunner = class {
|
|
|
2618
2722
|
turns: [],
|
|
2619
2723
|
resumable: false
|
|
2620
2724
|
};
|
|
2621
|
-
this.runs.push(
|
|
2622
|
-
await this.append({ event: "started", run:
|
|
2725
|
+
this.runs.push(run5);
|
|
2726
|
+
await this.append({ event: "started", run: run5.id, cli, trigger, at: run5.startedAt });
|
|
2623
2727
|
const adapter = ADAPTERS[cli];
|
|
2624
2728
|
const args = adapter.args({
|
|
2625
2729
|
prompt: agentPrompt(displayPath(this.config.promptFile)),
|
|
@@ -2627,10 +2731,12 @@ var AgentRunner = class {
|
|
|
2627
2731
|
model: this.config.agent.model,
|
|
2628
2732
|
effort: this.config.agent.effort,
|
|
2629
2733
|
tools: [...briefWritingTools(), ...this.config.agent.tools],
|
|
2630
|
-
denyEdits: false
|
|
2734
|
+
denyEdits: false,
|
|
2735
|
+
drafts: false,
|
|
2736
|
+
codexConfig: []
|
|
2631
2737
|
});
|
|
2632
|
-
await this.spawnInto(
|
|
2633
|
-
return
|
|
2738
|
+
await this.spawnInto(run5, null, args, RUN_TIMEOUT_MS, "gave up after an hour");
|
|
2739
|
+
return run5;
|
|
2634
2740
|
}
|
|
2635
2741
|
/**
|
|
2636
2742
|
* Ask a finished run a question, in its own session. Returns once the process
|
|
@@ -2640,16 +2746,16 @@ var AgentRunner = class {
|
|
|
2640
2746
|
const cli = this.config.agent.cli;
|
|
2641
2747
|
if (!cli) throw new Error("running the morning agent is off; set DAILY_FOCUS_AGENT to codex or claude");
|
|
2642
2748
|
await this.start();
|
|
2643
|
-
const
|
|
2644
|
-
if (!
|
|
2749
|
+
const run5 = this.runs.find((candidate) => candidate.id === runId);
|
|
2750
|
+
if (!run5) throw new Error("no such run");
|
|
2645
2751
|
if (this.running) {
|
|
2646
2752
|
throw new Error(
|
|
2647
2753
|
this.running.turn ? "the morning agent is still answering a question" : "the morning agent is still running"
|
|
2648
2754
|
);
|
|
2649
2755
|
}
|
|
2650
|
-
if (!this.resumable(
|
|
2756
|
+
if (!this.resumable(run5)) {
|
|
2651
2757
|
throw new Error(
|
|
2652
|
-
|
|
2758
|
+
run5.sessionId === null ? "this run left no session to continue" : `this run was made by ${run5.cli}, and the agent is ${cli} now`
|
|
2653
2759
|
);
|
|
2654
2760
|
}
|
|
2655
2761
|
const text = question.trim();
|
|
@@ -2663,19 +2769,21 @@ var AgentRunner = class {
|
|
|
2663
2769
|
reply: "",
|
|
2664
2770
|
error: null
|
|
2665
2771
|
};
|
|
2666
|
-
|
|
2667
|
-
await this.append({ event: "asked", run:
|
|
2772
|
+
run5.turns.push(turn);
|
|
2773
|
+
await this.append({ event: "asked", run: run5.id, turn: turn.id, question: text, at: turn.startedAt });
|
|
2668
2774
|
const adapter = ADAPTERS[cli];
|
|
2669
2775
|
const args = adapter.args({
|
|
2670
2776
|
prompt: followUpPrompt(text),
|
|
2671
|
-
sessionId:
|
|
2777
|
+
sessionId: run5.sessionId,
|
|
2672
2778
|
model: this.config.agent.model,
|
|
2673
2779
|
effort: this.config.agent.effort,
|
|
2674
2780
|
// The sources, and not the brief: a chat is given nothing to write.
|
|
2675
2781
|
tools: this.config.agent.tools,
|
|
2676
|
-
denyEdits: true
|
|
2782
|
+
denyEdits: true,
|
|
2783
|
+
drafts: false,
|
|
2784
|
+
codexConfig: []
|
|
2677
2785
|
});
|
|
2678
|
-
await this.spawnInto(
|
|
2786
|
+
await this.spawnInto(run5, turn, args, TURN_TIMEOUT_MS, "gave up after ten minutes");
|
|
2679
2787
|
return turn;
|
|
2680
2788
|
}
|
|
2681
2789
|
/**
|
|
@@ -2683,10 +2791,15 @@ var AgentRunner = class {
|
|
|
2683
2791
|
* run when there is none. The store in both cases: Claude Code files a
|
|
2684
2792
|
* session under the directory it was started in, and looks for it there.
|
|
2685
2793
|
*/
|
|
2686
|
-
async spawnInto(
|
|
2687
|
-
const cli =
|
|
2794
|
+
async spawnInto(run5, turn, args, timeoutMs, timeoutReason) {
|
|
2795
|
+
const cli = run5.cli;
|
|
2688
2796
|
const adapter = ADAPTERS[cli];
|
|
2689
2797
|
await mkdir(this.config.dataDir, { recursive: true });
|
|
2798
|
+
const refusal2 = cli === "codex" ? await this.codexGate.refusal() : null;
|
|
2799
|
+
if (refusal2) {
|
|
2800
|
+
await this.finish(run5, turn, { ok: false, error: refusal2 });
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2690
2803
|
let child;
|
|
2691
2804
|
try {
|
|
2692
2805
|
child = this.spawnFn(this.config.agent.binPath, args, {
|
|
@@ -2696,12 +2809,12 @@ var AgentRunner = class {
|
|
|
2696
2809
|
env: process.env
|
|
2697
2810
|
});
|
|
2698
2811
|
} catch (err) {
|
|
2699
|
-
await this.finish(
|
|
2812
|
+
await this.finish(run5, turn, { ok: false, error: this.spawnError(err) });
|
|
2700
2813
|
return;
|
|
2701
2814
|
}
|
|
2702
2815
|
const timer = setTimeout(() => void this.stop(timeoutReason), timeoutMs);
|
|
2703
2816
|
timer.unref();
|
|
2704
|
-
const running = { run:
|
|
2817
|
+
const running = { run: run5, turn, child, timer, abortReason: null };
|
|
2705
2818
|
this.running = running;
|
|
2706
2819
|
this.hooks.onChange();
|
|
2707
2820
|
let done;
|
|
@@ -2715,15 +2828,15 @@ var AgentRunner = class {
|
|
|
2715
2828
|
lines.on("line", (line) => {
|
|
2716
2829
|
const event = adapter.parse(line);
|
|
2717
2830
|
if (!event) return;
|
|
2718
|
-
if (event.sessionId && !turn)
|
|
2831
|
+
if (event.sessionId && !turn) run5.sessionId = event.sessionId;
|
|
2719
2832
|
if (event.text) {
|
|
2720
2833
|
if (turn) {
|
|
2721
2834
|
turn.reply = turn.reply ? `${turn.reply}
|
|
2722
2835
|
|
|
2723
2836
|
${event.text}` : event.text;
|
|
2724
2837
|
} else {
|
|
2725
|
-
|
|
2726
|
-
|
|
2838
|
+
run5.messages.push(event.text);
|
|
2839
|
+
run5.report = event.text;
|
|
2727
2840
|
}
|
|
2728
2841
|
this.hooks.onChange();
|
|
2729
2842
|
}
|
|
@@ -2740,7 +2853,7 @@ ${event.text}` : event.text;
|
|
|
2740
2853
|
ok: false,
|
|
2741
2854
|
error: `${cli} exited with code ${code}${stderr.length ? `: ${stderr.join("").trim().split("\n").slice(-3).join(" ")}` : ""}`
|
|
2742
2855
|
};
|
|
2743
|
-
void this.finish(
|
|
2856
|
+
void this.finish(run5, turn, outcome, running.abortReason !== null);
|
|
2744
2857
|
});
|
|
2745
2858
|
}
|
|
2746
2859
|
/**
|
|
@@ -2757,45 +2870,45 @@ ${event.text}` : event.text;
|
|
|
2757
2870
|
async close() {
|
|
2758
2871
|
await this.stop("the dashboard shut down");
|
|
2759
2872
|
}
|
|
2760
|
-
resumable(
|
|
2761
|
-
return
|
|
2873
|
+
resumable(run5) {
|
|
2874
|
+
return run5.status !== "running" && run5.sessionId !== null && run5.cli === this.config.agent.cli;
|
|
2762
2875
|
}
|
|
2763
|
-
async finish(
|
|
2876
|
+
async finish(run5, turn, outcome, aborted = false) {
|
|
2764
2877
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2765
2878
|
if (turn) {
|
|
2766
2879
|
const reply = outcome.reply ?? turn.reply;
|
|
2767
2880
|
turn.reply = reply;
|
|
2768
2881
|
turn.endedAt = at;
|
|
2769
2882
|
if (outcome.ok) {
|
|
2770
|
-
await this.append({ event: "replied", run:
|
|
2883
|
+
await this.append({ event: "replied", run: run5.id, turn: turn.id, reply, at });
|
|
2771
2884
|
turn.status = "done";
|
|
2772
2885
|
} else if (aborted) {
|
|
2773
2886
|
turn.error = outcome.error ?? "stopped";
|
|
2774
|
-
await this.append({ event: "turn-aborted", run:
|
|
2887
|
+
await this.append({ event: "turn-aborted", run: run5.id, turn: turn.id, reason: turn.error, reply, at });
|
|
2775
2888
|
turn.status = "aborted";
|
|
2776
2889
|
} else {
|
|
2777
2890
|
turn.error = outcome.error ?? "failed";
|
|
2778
|
-
await this.append({ event: "turn-failed", run:
|
|
2891
|
+
await this.append({ event: "turn-failed", run: run5.id, turn: turn.id, error: turn.error, reply, at });
|
|
2779
2892
|
turn.status = "failed";
|
|
2780
2893
|
}
|
|
2781
2894
|
this.hooks.onChange();
|
|
2782
2895
|
return;
|
|
2783
2896
|
}
|
|
2784
|
-
const report = outcome.reply ??
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
const messages =
|
|
2897
|
+
const report = outcome.reply ?? run5.report;
|
|
2898
|
+
run5.report = report;
|
|
2899
|
+
run5.endedAt = at;
|
|
2900
|
+
const messages = run5.messages;
|
|
2788
2901
|
if (outcome.ok) {
|
|
2789
|
-
await this.append({ event: "finished", run:
|
|
2790
|
-
|
|
2902
|
+
await this.append({ event: "finished", run: run5.id, sessionId: run5.sessionId, report, messages, at });
|
|
2903
|
+
run5.status = "done";
|
|
2791
2904
|
} else if (aborted) {
|
|
2792
|
-
|
|
2793
|
-
await this.append({ event: "aborted", run:
|
|
2794
|
-
|
|
2905
|
+
run5.error = outcome.error ?? "stopped";
|
|
2906
|
+
await this.append({ event: "aborted", run: run5.id, sessionId: run5.sessionId, reason: run5.error, report, messages, at });
|
|
2907
|
+
run5.status = "aborted";
|
|
2795
2908
|
} else {
|
|
2796
|
-
|
|
2797
|
-
await this.append({ event: "failed", run:
|
|
2798
|
-
|
|
2909
|
+
run5.error = outcome.error ?? "failed";
|
|
2910
|
+
await this.append({ event: "failed", run: run5.id, sessionId: run5.sessionId, error: run5.error, report, messages, at });
|
|
2911
|
+
run5.status = "failed";
|
|
2799
2912
|
}
|
|
2800
2913
|
this.hooks.onChange();
|
|
2801
2914
|
}
|
|
@@ -2909,33 +3022,6 @@ function refusal(req, listenHost) {
|
|
|
2909
3022
|
return null;
|
|
2910
3023
|
}
|
|
2911
3024
|
|
|
2912
|
-
// src/presence.ts
|
|
2913
|
-
import { execFile as execFile4 } from "node:child_process";
|
|
2914
|
-
import { platform } from "node:os";
|
|
2915
|
-
import { promisify as promisify4 } from "node:util";
|
|
2916
|
-
var run4 = promisify4(execFile4);
|
|
2917
|
-
var PROBE_TIMEOUT_MS = 4e3;
|
|
2918
|
-
var HID_IDLE = /"HIDIdleTime"\s*=\s*(\d+)/;
|
|
2919
|
-
var readIdleSeconds = async () => {
|
|
2920
|
-
if (platform() !== "darwin") return null;
|
|
2921
|
-
try {
|
|
2922
|
-
const { stdout } = await run4("ioreg", ["-c", "IOHIDSystem", "-d", "1", "-r"], {
|
|
2923
|
-
timeout: PROBE_TIMEOUT_MS,
|
|
2924
|
-
maxBuffer: 1024 * 1024
|
|
2925
|
-
});
|
|
2926
|
-
return parseIdleSeconds(stdout);
|
|
2927
|
-
} catch {
|
|
2928
|
-
return null;
|
|
2929
|
-
}
|
|
2930
|
-
};
|
|
2931
|
-
function parseIdleSeconds(ioregOutput) {
|
|
2932
|
-
const match = HID_IDLE.exec(ioregOutput);
|
|
2933
|
-
if (!match) return null;
|
|
2934
|
-
const nanoseconds = Number(match[1]);
|
|
2935
|
-
if (!Number.isFinite(nanoseconds) || nanoseconds < 0) return null;
|
|
2936
|
-
return nanoseconds / 1e9;
|
|
2937
|
-
}
|
|
2938
|
-
|
|
2939
3025
|
// src/settings.ts
|
|
2940
3026
|
var SETTING_GROUPS = [
|
|
2941
3027
|
{ id: "instance", title: "This dashboard" },
|
|
@@ -2944,8 +3030,7 @@ var SETTING_GROUPS = [
|
|
|
2944
3030
|
{ id: "github", title: "Pull requests" },
|
|
2945
3031
|
{ id: "jira", title: "Jira tickets" },
|
|
2946
3032
|
{ id: "calendar", title: "Calendar" },
|
|
2947
|
-
{ id: "day", title: "Working day" }
|
|
2948
|
-
{ id: "sessions", title: "Focus sessions" }
|
|
3033
|
+
{ id: "day", title: "Working day" }
|
|
2949
3034
|
];
|
|
2950
3035
|
var OFF_OR_CLI = [
|
|
2951
3036
|
{ value: "off", label: "Off" },
|
|
@@ -2959,7 +3044,7 @@ var SETTINGS = [
|
|
|
2959
3044
|
key: "DAILY_FOCUS_PROFILE",
|
|
2960
3045
|
group: "instance",
|
|
2961
3046
|
label: "What this dashboard briefs",
|
|
2962
|
-
help: "Work or home. Picks the morning prompt and the defaults below: the personal profile switches off Jira
|
|
3047
|
+
help: "Work or home. Picks the morning prompt and the defaults below: the personal profile switches off Jira and free time.",
|
|
2963
3048
|
kind: "choice",
|
|
2964
3049
|
choices: PROFILES.map((value) => ({ value, label: value === "work" ? "Work" : "Personal life" })),
|
|
2965
3050
|
fallback: always("Work")
|
|
@@ -3010,7 +3095,7 @@ var SETTINGS = [
|
|
|
3010
3095
|
key: "DAILY_FOCUS_AGENT_TOOLS",
|
|
3011
3096
|
group: "agent",
|
|
3012
3097
|
label: "Tools Claude Code may use",
|
|
3013
|
-
help: "In Claude Code's permission syntax, comma-separated. Add your calendar, chat and document connectors by name, or the brief reports them as unreachable. Ignored by Codex.",
|
|
3098
|
+
help: "In Claude Code's permission syntax, comma-separated. Add your calendar, chat and document connectors by name, or the brief reports them as unreachable. The default allows every Gmail tool, sending included; name the tools you want instead to narrow it. Ignored by Codex.",
|
|
3014
3099
|
kind: "list",
|
|
3015
3100
|
fallback: always("Bash(gh *), Bash(acli *), WebFetch, mcp__claude_ai_Gmail"),
|
|
3016
3101
|
advanced: true
|
|
@@ -3037,7 +3122,7 @@ var SETTINGS = [
|
|
|
3037
3122
|
key: "DAILY_FOCUS_ASSISTANT",
|
|
3038
3123
|
group: "assistant",
|
|
3039
3124
|
label: "Answer questions with",
|
|
3040
|
-
help: "Puts an assistant in every item's panel
|
|
3125
|
+
help: "Puts an assistant in every item's panel, told to read and draft and never to send, post or edit. Under Codex, every connector write but a Gmail draft is refused.",
|
|
3041
3126
|
kind: "choice",
|
|
3042
3127
|
choices: OFF_OR_CLI,
|
|
3043
3128
|
fallback: always("Off")
|
|
@@ -3063,11 +3148,36 @@ var SETTINGS = [
|
|
|
3063
3148
|
key: "DAILY_FOCUS_ASSISTANT_TOOLS",
|
|
3064
3149
|
group: "assistant",
|
|
3065
3150
|
label: "Tools Claude Code may use",
|
|
3066
|
-
help: "In Claude Code's permission syntax, comma-separated. Editing tools are denied whatever this says. Ignored by Codex.",
|
|
3151
|
+
help: "In Claude Code's permission syntax, comma-separated. Editing tools are denied whatever this says. The default allows every Gmail tool, sending included; name the tools you want instead to narrow it. Ignored by Codex.",
|
|
3067
3152
|
kind: "list",
|
|
3068
3153
|
fallback: always("Bash(gh *), Bash(acli *), WebFetch, mcp__claude_ai_Gmail"),
|
|
3069
3154
|
advanced: true
|
|
3070
3155
|
},
|
|
3156
|
+
{
|
|
3157
|
+
key: "DAILY_FOCUS_ASSISTANT_COMPUTER_USE",
|
|
3158
|
+
group: "assistant",
|
|
3159
|
+
label: "Computer use",
|
|
3160
|
+
help: "Codex only. On lets the assistant use Codex's computer-use and browser plugins, which act in your apps and browser with your sign-ins. Off switches them off for its runs. Claude Code uses only the tools in its list.",
|
|
3161
|
+
kind: "flag",
|
|
3162
|
+
fallback: always("Off")
|
|
3163
|
+
},
|
|
3164
|
+
{
|
|
3165
|
+
key: "DAILY_FOCUS_ASSISTANT_AUTO",
|
|
3166
|
+
group: "assistant",
|
|
3167
|
+
label: "Answer ahead on pull requests",
|
|
3168
|
+
help: "When a pull request starts waiting on you because of a review or a failing check, asks the assistant about it before you do, so the answer is on the row when you look. Only reads; it never drafts. It spends the CLI\u2019s usage without you pressing anything.",
|
|
3169
|
+
kind: "flag",
|
|
3170
|
+
fallback: always("Off")
|
|
3171
|
+
},
|
|
3172
|
+
{
|
|
3173
|
+
key: "DAILY_FOCUS_ASSISTANT_AUTO_LIMIT",
|
|
3174
|
+
group: "assistant",
|
|
3175
|
+
label: "Answer ahead at most",
|
|
3176
|
+
help: "Times a day the assistant may start on its own, counted on the dashboard\u2019s calendar.",
|
|
3177
|
+
kind: "number",
|
|
3178
|
+
fallback: always("10 a day"),
|
|
3179
|
+
advanced: true
|
|
3180
|
+
},
|
|
3071
3181
|
{
|
|
3072
3182
|
key: "DAILY_FOCUS_ASSISTANT_BIN",
|
|
3073
3183
|
group: "assistant",
|
|
@@ -3081,7 +3191,7 @@ var SETTINGS = [
|
|
|
3081
3191
|
key: "DAILY_FOCUS_GITHUB",
|
|
3082
3192
|
group: "github",
|
|
3083
3193
|
label: "Pull request board",
|
|
3084
|
-
help: "Reads your open pull requests through the GitHub CLI. Sign in with gh auth login.",
|
|
3194
|
+
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.",
|
|
3085
3195
|
kind: "flag",
|
|
3086
3196
|
fallback: always("On")
|
|
3087
3197
|
},
|
|
@@ -3260,22 +3370,6 @@ var SETTINGS = [
|
|
|
3260
3370
|
help: "Minutes a gap has to last to count as time for focused work.",
|
|
3261
3371
|
kind: "number",
|
|
3262
3372
|
fallback: always("45 minutes")
|
|
3263
|
-
},
|
|
3264
|
-
{
|
|
3265
|
-
key: "DAILY_FOCUS_SESSION_MINUTES",
|
|
3266
|
-
group: "sessions",
|
|
3267
|
-
label: "Session length",
|
|
3268
|
-
help: "Minutes a focus session is set for when you start one.",
|
|
3269
|
-
kind: "number",
|
|
3270
|
-
fallback: always("25 minutes")
|
|
3271
|
-
},
|
|
3272
|
-
{
|
|
3273
|
-
key: "DAILY_FOCUS_AWAY_AFTER",
|
|
3274
|
-
group: "sessions",
|
|
3275
|
-
label: "End a session when away for",
|
|
3276
|
-
help: "Minutes without touching the Mac before a session is closed where you left. 0 turns it off.",
|
|
3277
|
-
kind: "number",
|
|
3278
|
-
fallback: (profile) => profile === "personal" ? "0, off" : "10 minutes"
|
|
3279
3373
|
}
|
|
3280
3374
|
];
|
|
3281
3375
|
var BY_KEY = new Map(SETTINGS.map((def) => [def.key, def]));
|
|
@@ -3338,11 +3432,11 @@ async function saveSettings(path, settings) {
|
|
|
3338
3432
|
}
|
|
3339
3433
|
|
|
3340
3434
|
// src/setup.ts
|
|
3341
|
-
import { execFile as
|
|
3435
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
3342
3436
|
import { access, constants, readdir as readdir2, readFile as readFile7, stat as stat3 } from "node:fs/promises";
|
|
3343
3437
|
import { delimiter, isAbsolute, join as join3 } from "node:path";
|
|
3344
|
-
import { promisify as
|
|
3345
|
-
var
|
|
3438
|
+
import { promisify as promisify4 } from "node:util";
|
|
3439
|
+
var run4 = promisify4(execFile4);
|
|
3346
3440
|
var PLACEHOLDERS = [
|
|
3347
3441
|
"Your Name",
|
|
3348
3442
|
"you@example.com",
|
|
@@ -3432,11 +3526,11 @@ var identity = null;
|
|
|
3432
3526
|
function detectIdentity(config) {
|
|
3433
3527
|
identity ??= (async () => {
|
|
3434
3528
|
const [user, email] = await Promise.all([
|
|
3435
|
-
|
|
3529
|
+
run4(config.github.ghPath, ["api", "user", "--jq", '[.login, .name // ""] | join("\\n")'], { timeout: 5e3 }).then(
|
|
3436
3530
|
({ stdout }) => stdout.split("\n"),
|
|
3437
3531
|
() => []
|
|
3438
3532
|
),
|
|
3439
|
-
|
|
3533
|
+
run4("git", ["config", "--global", "user.email"], { timeout: 2e3 }).then(
|
|
3440
3534
|
({ stdout }) => stdout.trim() || null,
|
|
3441
3535
|
() => null
|
|
3442
3536
|
)
|
|
@@ -3485,8 +3579,8 @@ var defaultExec = (file, args, { timeout }) => new Promise((resolve2, reject) =>
|
|
|
3485
3579
|
child.on("error", (error) => finish(error));
|
|
3486
3580
|
child.on("close", () => finish(null));
|
|
3487
3581
|
});
|
|
3488
|
-
function shellCandidates(envShell, loginShell,
|
|
3489
|
-
const fallback =
|
|
3582
|
+
function shellCandidates(envShell, loginShell, platform) {
|
|
3583
|
+
const fallback = platform === "darwin" ? "/bin/zsh" : platform === "linux" ? "/bin/bash" : void 0;
|
|
3490
3584
|
const shells = [];
|
|
3491
3585
|
for (const shell of [envShell, loginShell, fallback]) {
|
|
3492
3586
|
const trimmed = shell?.trim();
|
|
@@ -3531,11 +3625,11 @@ function currentLoginShell() {
|
|
|
3531
3625
|
return void 0;
|
|
3532
3626
|
}
|
|
3533
3627
|
}
|
|
3534
|
-
async function hydratePath(env = process.env,
|
|
3535
|
-
if (
|
|
3628
|
+
async function hydratePath(env = process.env, platform = process.platform, exec = defaultExec, loginShell = currentLoginShell()) {
|
|
3629
|
+
if (platform === "win32") return null;
|
|
3536
3630
|
let found = null;
|
|
3537
3631
|
let from = null;
|
|
3538
|
-
for (const shell of shellCandidates(env.SHELL, loginShell,
|
|
3632
|
+
for (const shell of shellCandidates(env.SHELL, loginShell, platform)) {
|
|
3539
3633
|
try {
|
|
3540
3634
|
found = await readLoginShellPath(shell, exec);
|
|
3541
3635
|
} catch {
|
|
@@ -3545,7 +3639,7 @@ async function hydratePath(env = process.env, platform2 = process.platform, exec
|
|
|
3545
3639
|
break;
|
|
3546
3640
|
}
|
|
3547
3641
|
}
|
|
3548
|
-
if (!found &&
|
|
3642
|
+
if (!found && platform === "darwin") {
|
|
3549
3643
|
found = await readLaunchctlPath(exec);
|
|
3550
3644
|
if (found) from = "launchctl";
|
|
3551
3645
|
}
|
|
@@ -3571,15 +3665,8 @@ var TEXT_FILES = {
|
|
|
3571
3665
|
"/api/text/focus": { file: "focusFile", name: "focus.md" },
|
|
3572
3666
|
"/api/text/sources": { file: "sourcesFile", name: "sources.md" }
|
|
3573
3667
|
};
|
|
3574
|
-
var VALID_ACTIONS =
|
|
3575
|
-
"done",
|
|
3576
|
-
"snooze",
|
|
3577
|
-
"dismiss",
|
|
3578
|
-
"reopen",
|
|
3579
|
-
"note"
|
|
3580
|
-
]);
|
|
3668
|
+
var VALID_ACTIONS = new Set(ACTION_TYPES);
|
|
3581
3669
|
var MAX_BODY_BYTES = 64 * 1024;
|
|
3582
|
-
var PRESENCE_POLL_MS = 3e4;
|
|
3583
3670
|
var AGENT_CLOCK_MS = 3e4;
|
|
3584
3671
|
function sendJSON(res, status, body) {
|
|
3585
3672
|
const payload = JSON.stringify(body);
|
|
@@ -3686,6 +3773,8 @@ async function startServer(env, options = {}) {
|
|
|
3686
3773
|
const store = new Store(config);
|
|
3687
3774
|
await store.ensureDataDir();
|
|
3688
3775
|
const storeId = await store.fingerprint();
|
|
3776
|
+
const zone = new TimeZoneKeeper(config.timeZoneFile, options.readSystemZone);
|
|
3777
|
+
await zone.start();
|
|
3689
3778
|
for (const outcome of await linkStore(config)) {
|
|
3690
3779
|
if (outcome.result === "linked") console.log(`[daily-focus] linked ${describeLink(config, outcome)}`);
|
|
3691
3780
|
if (outcome.result === "failed") console.warn(`[daily-focus] could not link ${outcome.name}: ${outcome.detail}`);
|
|
@@ -3700,21 +3789,48 @@ async function startServer(env, options = {}) {
|
|
|
3700
3789
|
});
|
|
3701
3790
|
const subscribers = /* @__PURE__ */ new Set();
|
|
3702
3791
|
let assetVersion = await computeAssetVersion(PUBLIC_DIR);
|
|
3703
|
-
|
|
3792
|
+
let autoAsk = null;
|
|
3793
|
+
const board = new Board(
|
|
3794
|
+
config,
|
|
3795
|
+
() => {
|
|
3796
|
+
void broadcast();
|
|
3797
|
+
autoAsk?.poke();
|
|
3798
|
+
},
|
|
3799
|
+
options.integrations?.github
|
|
3800
|
+
);
|
|
3704
3801
|
const calendar = new CalendarBoard(config, () => void broadcast());
|
|
3705
3802
|
const tickets = new TicketBoard(config, () => void broadcast(), options.integrations?.jira);
|
|
3706
3803
|
const assistant = new Assistant(config, {
|
|
3707
3804
|
onChange: () => {
|
|
3708
3805
|
void broadcast();
|
|
3709
3806
|
restartWhenIdle();
|
|
3807
|
+
autoAsk?.poke();
|
|
3710
3808
|
}
|
|
3711
3809
|
});
|
|
3810
|
+
autoAsk = new AutoAsk({
|
|
3811
|
+
config,
|
|
3812
|
+
assistant,
|
|
3813
|
+
rows: async () => board.view(await store.readActions(), /* @__PURE__ */ new Date()).rows,
|
|
3814
|
+
context: async (id) => contextFor(id, await buildState()),
|
|
3815
|
+
// Nothing new starts while saved settings wait on the restart that applies them.
|
|
3816
|
+
canStart: () => restart !== "waiting"
|
|
3817
|
+
});
|
|
3818
|
+
const ahead = autoAsk;
|
|
3712
3819
|
const agent = new AgentRunner(config, {
|
|
3713
3820
|
onChange: () => {
|
|
3714
3821
|
void broadcast();
|
|
3715
3822
|
restartWhenIdle();
|
|
3716
3823
|
}
|
|
3717
3824
|
});
|
|
3825
|
+
let codexWarnings = [];
|
|
3826
|
+
const codexBins = /* @__PURE__ */ new Set();
|
|
3827
|
+
if (config.agent.cli === "codex") codexBins.add(config.agent.binPath);
|
|
3828
|
+
if (config.assistant.agent === "codex") codexBins.add(config.assistant.binPath);
|
|
3829
|
+
void Promise.all([...codexBins].map((bin) => codexVersionWarning(bin))).then((found) => {
|
|
3830
|
+
codexWarnings = [...new Set(found.filter((warning) => warning !== null))];
|
|
3831
|
+
for (const warning of codexWarnings) console.warn(`[daily-focus] ${warning}`);
|
|
3832
|
+
if (codexWarnings.length > 0) void broadcast();
|
|
3833
|
+
});
|
|
3718
3834
|
function requestRestart() {
|
|
3719
3835
|
restart = options.onRestart ? "waiting" : "manual";
|
|
3720
3836
|
restartWhenIdle();
|
|
@@ -3736,19 +3852,27 @@ async function startServer(env, options = {}) {
|
|
|
3736
3852
|
const setup = await setupState(config, {
|
|
3737
3853
|
profileChosen: Boolean(layerEnv(sources).DAILY_FOCUS_PROFILE?.trim())
|
|
3738
3854
|
});
|
|
3855
|
+
const pulls = board.view(actions, now);
|
|
3739
3856
|
return {
|
|
3740
3857
|
...state,
|
|
3741
|
-
board:
|
|
3858
|
+
board: pulls,
|
|
3742
3859
|
tickets: tickets.view(actions, now),
|
|
3743
|
-
assistant: assistant.view(),
|
|
3860
|
+
assistant: { ...assistant.view(), auto: ahead.view(pulls.rows) },
|
|
3744
3861
|
agentRun: agent.view(now),
|
|
3745
|
-
setupWarnings: [...configWarnings, ...linkWarning ? [linkWarning] : []],
|
|
3862
|
+
setupWarnings: [...configWarnings, ...codexWarnings, ...linkWarning ? [linkWarning] : []],
|
|
3746
3863
|
restart,
|
|
3747
3864
|
setup,
|
|
3748
3865
|
assetVersion,
|
|
3749
3866
|
storeId
|
|
3750
3867
|
};
|
|
3751
3868
|
}
|
|
3869
|
+
function contextFor(id, current) {
|
|
3870
|
+
return {
|
|
3871
|
+
item: current.items.find((item) => item.id === id),
|
|
3872
|
+
pull: current.board.rows.find((row) => row.id === id),
|
|
3873
|
+
ticket: current.tickets.rows.find((row) => row.id === id) ?? current.tickets.inProgress.find((row) => row.id === id)
|
|
3874
|
+
};
|
|
3875
|
+
}
|
|
3752
3876
|
async function broadcast(prebuilt) {
|
|
3753
3877
|
if (subscribers.size === 0) return;
|
|
3754
3878
|
let payload;
|
|
@@ -3770,17 +3894,17 @@ data: ${payload}
|
|
|
3770
3894
|
() => {
|
|
3771
3895
|
void store.archiveCurrentBrief().then(() => broadcast());
|
|
3772
3896
|
},
|
|
3773
|
-
// The
|
|
3774
|
-
//
|
|
3775
|
-
//
|
|
3897
|
+
// The two boards rewrite prs.json and tickets.json every fetch; that's this
|
|
3898
|
+
// process talking to itself, and both already broadcast when something
|
|
3899
|
+
// actually changed.
|
|
3776
3900
|
{
|
|
3777
3901
|
ignore: [
|
|
3778
|
-
basename(config.sessionFile),
|
|
3779
|
-
basename(tempPathFor(config.sessionFile)),
|
|
3780
3902
|
basename(config.pullsFile),
|
|
3781
3903
|
basename(tempPathFor(config.pullsFile)),
|
|
3782
3904
|
basename(config.ticketsFile),
|
|
3783
3905
|
basename(tempPathFor(config.ticketsFile)),
|
|
3906
|
+
basename(config.timeZoneFile),
|
|
3907
|
+
basename(tempPathFor(config.timeZoneFile)),
|
|
3784
3908
|
// Appended by this process, which broadcasts on its own.
|
|
3785
3909
|
basename(config.assistantLogFile),
|
|
3786
3910
|
basename(config.agentLogFile)
|
|
@@ -3794,47 +3918,34 @@ data: ${payload}
|
|
|
3794
3918
|
void broadcast();
|
|
3795
3919
|
});
|
|
3796
3920
|
});
|
|
3797
|
-
let sessionTick = null;
|
|
3798
|
-
function scheduleSessionTick(endsAt) {
|
|
3799
|
-
if (sessionTick) clearTimeout(sessionTick);
|
|
3800
|
-
sessionTick = null;
|
|
3801
|
-
if (!endsAt) return;
|
|
3802
|
-
const delay = new Date(endsAt).getTime() - Date.now();
|
|
3803
|
-
if (delay <= 0 || delay > 36e5) return;
|
|
3804
|
-
sessionTick = setTimeout(() => void broadcast(), delay);
|
|
3805
|
-
sessionTick.unref();
|
|
3806
|
-
}
|
|
3807
|
-
async function checkPresence() {
|
|
3808
|
-
let closed;
|
|
3809
|
-
try {
|
|
3810
|
-
closed = await reconcileSession(config, readIdleSeconds);
|
|
3811
|
-
} catch (err) {
|
|
3812
|
-
console.error(`[daily-focus] presence check failed: ${err.message}`);
|
|
3813
|
-
return;
|
|
3814
|
-
}
|
|
3815
|
-
if (!closed) return;
|
|
3816
|
-
console.log(
|
|
3817
|
-
`[daily-focus] stopped "${closed.title}" at ${closed.endedAt} after ${closed.actualMinutes} min \u2014 machine untouched since then`
|
|
3818
|
-
);
|
|
3819
|
-
scheduleSessionTick(null);
|
|
3820
|
-
await broadcast();
|
|
3821
|
-
}
|
|
3822
|
-
await checkPresence();
|
|
3823
3921
|
await assistant.start();
|
|
3824
3922
|
await agent.start();
|
|
3825
|
-
|
|
3923
|
+
let ticks = Promise.resolve();
|
|
3924
|
+
function clockTick() {
|
|
3925
|
+
ticks = ticks.then(tickOnce).catch((err) => {
|
|
3926
|
+
console.error(`[daily-focus] the morning clock failed: ${err.message}`);
|
|
3927
|
+
});
|
|
3928
|
+
return ticks;
|
|
3929
|
+
}
|
|
3930
|
+
async function tickOnce() {
|
|
3931
|
+
if (await zone.checkSystem()) zoneMoved();
|
|
3826
3932
|
if (restart === "waiting" || await firstBriefPending(config)) return;
|
|
3827
3933
|
await agent.tick();
|
|
3828
3934
|
}
|
|
3935
|
+
function zoneMoved() {
|
|
3936
|
+
console.log(`[daily-focus] time zone is now ${zone.current}`);
|
|
3937
|
+
void calendar.refresh();
|
|
3938
|
+
void broadcast();
|
|
3939
|
+
}
|
|
3829
3940
|
void clockTick();
|
|
3830
3941
|
const agentClock = setInterval(() => {
|
|
3831
3942
|
void clockTick();
|
|
3832
3943
|
}, AGENT_CLOCK_MS);
|
|
3833
3944
|
agentClock.unref();
|
|
3834
|
-
scheduleSessionTick((await buildState()).session.active?.endsAt ?? null);
|
|
3835
3945
|
void board.start().catch((err) => {
|
|
3836
3946
|
console.error(`[daily-focus] pull request board failed to start: ${err.message}`);
|
|
3837
3947
|
});
|
|
3948
|
+
ahead.poke();
|
|
3838
3949
|
void calendar.start().catch((err) => {
|
|
3839
3950
|
console.error(`[daily-focus] live agenda failed to start: ${err.message}`);
|
|
3840
3951
|
});
|
|
@@ -3845,10 +3956,6 @@ data: ${payload}
|
|
|
3845
3956
|
void broadcast();
|
|
3846
3957
|
}, 6e4);
|
|
3847
3958
|
heartbeat.unref();
|
|
3848
|
-
const presencePoll = setInterval(() => {
|
|
3849
|
-
void checkPresence();
|
|
3850
|
-
}, PRESENCE_POLL_MS);
|
|
3851
|
-
presencePoll.unref();
|
|
3852
3959
|
const server = createServer((req, res) => {
|
|
3853
3960
|
void handle(req, res).catch((err) => {
|
|
3854
3961
|
console.error(`[daily-focus] ${req.method} ${req.url} failed:`, err);
|
|
@@ -3909,6 +4016,26 @@ data: ${JSON.stringify(await buildState())}
|
|
|
3909
4016
|
sendState(res, await buildState());
|
|
3910
4017
|
return;
|
|
3911
4018
|
}
|
|
4019
|
+
if (path === "/api/timezone" && req.method === "POST") {
|
|
4020
|
+
let body;
|
|
4021
|
+
try {
|
|
4022
|
+
body = JSON.parse(await readBody(req));
|
|
4023
|
+
} catch (err) {
|
|
4024
|
+
sendJSON(res, 400, { error: `invalid JSON body: ${err.message}` });
|
|
4025
|
+
return;
|
|
4026
|
+
}
|
|
4027
|
+
const timeZone = validZone(body?.timeZone);
|
|
4028
|
+
if (!timeZone) {
|
|
4029
|
+
sendJSON(res, 400, { error: '"timeZone" must be an IANA time zone, such as Europe/Copenhagen' });
|
|
4030
|
+
return;
|
|
4031
|
+
}
|
|
4032
|
+
if (await zone.report(timeZone)) {
|
|
4033
|
+
zoneMoved();
|
|
4034
|
+
await clockTick();
|
|
4035
|
+
}
|
|
4036
|
+
sendState(res, await buildState());
|
|
4037
|
+
return;
|
|
4038
|
+
}
|
|
3912
4039
|
if (path === "/api/calendar/refresh" && req.method === "POST") {
|
|
3913
4040
|
await calendar.refresh();
|
|
3914
4041
|
sendState(res, await buildState());
|
|
@@ -3975,12 +4102,7 @@ data: ${JSON.stringify(await buildState())}
|
|
|
3975
4102
|
sendJSON(res, 409, { error: "the assistant is off; set DAILY_FOCUS_ASSISTANT to claude or codex" });
|
|
3976
4103
|
return;
|
|
3977
4104
|
}
|
|
3978
|
-
const
|
|
3979
|
-
const context = {
|
|
3980
|
-
item: current.items.find((item) => item.id === id),
|
|
3981
|
-
pull: current.board.rows.find((row) => row.id === id),
|
|
3982
|
-
ticket: current.tickets.rows.find((row) => row.id === id) ?? current.tickets.inProgress.find((row) => row.id === id)
|
|
3983
|
-
};
|
|
4105
|
+
const context = contextFor(id, await buildState());
|
|
3984
4106
|
if (!context.item && !context.pull && !context.ticket) {
|
|
3985
4107
|
sendJSON(res, 404, { error: `no item with id ${id}` });
|
|
3986
4108
|
return;
|
|
@@ -4016,8 +4138,8 @@ data: ${JSON.stringify(await buildState())}
|
|
|
4016
4138
|
sendJSON(res, 400, { error: `invalid JSON body: ${err.message}` });
|
|
4017
4139
|
return;
|
|
4018
4140
|
}
|
|
4019
|
-
const { run:
|
|
4020
|
-
if (typeof
|
|
4141
|
+
const { run: run5, text } = typeof body === "object" && body !== null ? body : {};
|
|
4142
|
+
if (typeof run5 !== "string" || run5 === "") {
|
|
4021
4143
|
sendJSON(res, 400, { error: "run must be a run id" });
|
|
4022
4144
|
return;
|
|
4023
4145
|
}
|
|
@@ -4026,7 +4148,7 @@ data: ${JSON.stringify(await buildState())}
|
|
|
4026
4148
|
return;
|
|
4027
4149
|
}
|
|
4028
4150
|
try {
|
|
4029
|
-
await agent.ask(
|
|
4151
|
+
await agent.ask(run5, text);
|
|
4030
4152
|
} catch (err) {
|
|
4031
4153
|
sendJSON(res, 409, { error: err.message });
|
|
4032
4154
|
return;
|
|
@@ -4096,53 +4218,6 @@ data: ${JSON.stringify(await buildState())}
|
|
|
4096
4218
|
void broadcast(state);
|
|
4097
4219
|
return;
|
|
4098
4220
|
}
|
|
4099
|
-
if (path === "/api/session" && req.method === "POST") {
|
|
4100
|
-
let body;
|
|
4101
|
-
try {
|
|
4102
|
-
body = JSON.parse(await readBody(req));
|
|
4103
|
-
} catch (err) {
|
|
4104
|
-
sendJSON(res, 400, { error: `invalid JSON body: ${err.message}` });
|
|
4105
|
-
return;
|
|
4106
|
-
}
|
|
4107
|
-
if (typeof body !== "object" || body === null) {
|
|
4108
|
-
sendJSON(res, 400, { error: "body must be an object" });
|
|
4109
|
-
return;
|
|
4110
|
-
}
|
|
4111
|
-
const { action, id, minutes } = body;
|
|
4112
|
-
if (action === "stop") {
|
|
4113
|
-
await stopSession(config);
|
|
4114
|
-
} else if (action === "start") {
|
|
4115
|
-
if (typeof id !== "string" || id === "") {
|
|
4116
|
-
sendJSON(res, 400, { error: '"id" is required to start a session' });
|
|
4117
|
-
return;
|
|
4118
|
-
}
|
|
4119
|
-
const { brief } = await store.readBrief();
|
|
4120
|
-
const item = brief?.items.find((candidate) => candidate.id === id);
|
|
4121
|
-
if (!item) {
|
|
4122
|
-
sendJSON(res, 404, { error: `no item with id ${id}` });
|
|
4123
|
-
return;
|
|
4124
|
-
}
|
|
4125
|
-
const requested = typeof minutes === "number" && Number.isFinite(minutes) ? Math.round(minutes) : config.sessionMinutes;
|
|
4126
|
-
if (requested < 1 || requested > 120) {
|
|
4127
|
-
sendJSON(res, 400, { error: '"minutes" must be between 1 and 120' });
|
|
4128
|
-
return;
|
|
4129
|
-
}
|
|
4130
|
-
await startSession(config, {
|
|
4131
|
-
id,
|
|
4132
|
-
title: item.title,
|
|
4133
|
-
minutes: requested,
|
|
4134
|
-
advancesObjective: item.advancesObjective === true
|
|
4135
|
-
});
|
|
4136
|
-
} else {
|
|
4137
|
-
sendJSON(res, 400, { error: '"action" must be "start" or "stop"' });
|
|
4138
|
-
return;
|
|
4139
|
-
}
|
|
4140
|
-
const state = await buildState();
|
|
4141
|
-
sendState(res, state);
|
|
4142
|
-
scheduleSessionTick(state.session.active?.endsAt ?? null);
|
|
4143
|
-
void broadcast(state);
|
|
4144
|
-
return;
|
|
4145
|
-
}
|
|
4146
4221
|
if (path === "/api/settings" && req.method === "GET") {
|
|
4147
4222
|
sendJSON(res, 200, { ...describeSettings(readSources()), restart });
|
|
4148
4223
|
return;
|
|
@@ -4270,9 +4345,8 @@ ${entry.source}`)).map(({ title, source }) => ({ title, source })).sort((a, b) =
|
|
|
4270
4345
|
}
|
|
4271
4346
|
async function stopEverything() {
|
|
4272
4347
|
clearInterval(heartbeat);
|
|
4273
|
-
clearInterval(presencePoll);
|
|
4274
4348
|
clearInterval(agentClock);
|
|
4275
|
-
|
|
4349
|
+
ahead.close();
|
|
4276
4350
|
board.stop();
|
|
4277
4351
|
calendar.stop();
|
|
4278
4352
|
tickets.stop();
|