daily-focus 1.1.1-dev.3 → 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 +93 -20
- package/dist/{audit-B5GKGH2P.js → audit-7ZCLKF46.js} +6 -5
- package/dist/{chunk-YWHADSKR.js → chunk-64XB7UDN.js} +1 -1
- package/dist/{chunk-NWYOCDUU.js → chunk-J7ZYVUG6.js} +4 -6
- package/dist/chunk-ODJGQCM3.js +1003 -0
- package/dist/{chunk-C6FXWHGT.js → chunk-RZNQJ6UG.js} +123 -282
- package/dist/{chunk-AUBJ2IDQ.js → chunk-Z4SFMMRQ.js} +2 -2
- package/dist/cli.js +9 -8
- package/dist/{demo-AMK3Y4JZ.js → demo-BYLLMKSQ.js} +135 -32
- package/dist/{init-YGOPPELJ.js → init-EMZDG2I7.js} +1 -9
- package/dist/{seed-57RJGU4Y.js → seed-JU4LQ33D.js} +3 -3
- package/dist/{server-5HCA62YY.js → server-WQYUOKB3.js} +416 -384
- package/dist/{service-63U4WBUH.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 +440 -441
- package/public/style.css +192 -181
- package/dist/chunk-GFCWPYEI.js +0 -456
|
@@ -1,7 +1,12 @@
|
|
|
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
|
|
@@ -9,22 +14,20 @@ import {
|
|
|
9
14
|
import {
|
|
10
15
|
TimeZoneKeeper,
|
|
11
16
|
validZone
|
|
12
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-Z4SFMMRQ.js";
|
|
13
18
|
import {
|
|
14
19
|
describeLink,
|
|
15
20
|
linkStore,
|
|
16
21
|
storeLinkWarning
|
|
17
22
|
} from "./chunk-QKT2FAB5.js";
|
|
18
23
|
import {
|
|
24
|
+
ACTION_TYPES,
|
|
19
25
|
Store,
|
|
20
26
|
canonicalId,
|
|
21
27
|
foldActionLog,
|
|
22
|
-
reconcileSession,
|
|
23
|
-
startSession,
|
|
24
|
-
stopSession,
|
|
25
28
|
tempPathFor,
|
|
26
29
|
writeJsonAtomic
|
|
27
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-RZNQJ6UG.js";
|
|
28
31
|
import {
|
|
29
32
|
CLI_NAMES,
|
|
30
33
|
LAUNCH_SETTINGS,
|
|
@@ -36,11 +39,10 @@ import {
|
|
|
36
39
|
layerEnv,
|
|
37
40
|
loadConfig,
|
|
38
41
|
nextScheduledRun,
|
|
39
|
-
parseISO,
|
|
40
42
|
readDotEnv,
|
|
41
43
|
resolveSchedule,
|
|
42
44
|
scheduledRunDue
|
|
43
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-J7ZYVUG6.js";
|
|
44
46
|
import {
|
|
45
47
|
PACKAGED,
|
|
46
48
|
command,
|
|
@@ -497,9 +499,9 @@ function checkSlot(node) {
|
|
|
497
499
|
return `check\0${node.name ?? ""}`;
|
|
498
500
|
}
|
|
499
501
|
function checkGeneration(node) {
|
|
500
|
-
const
|
|
501
|
-
const sequence =
|
|
502
|
-
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];
|
|
503
505
|
const at = node.completedAt ?? node.startedAt ?? node.createdAt ?? null;
|
|
504
506
|
const time = at ? Date.parse(at) : Number.NaN;
|
|
505
507
|
return [Number.isFinite(time) ? time : 0, 0];
|
|
@@ -815,6 +817,7 @@ async function fetchPulls(token, scope, expectedLogin, signal) {
|
|
|
815
817
|
for (const [nodeId, head] of checks.heads) {
|
|
816
818
|
const pull = found.get(nodeId);
|
|
817
819
|
if (!pull) continue;
|
|
820
|
+
pull.headSha = head.oid;
|
|
818
821
|
applyChecks(pull, head.rollup);
|
|
819
822
|
if (head.rollup && rollupTruncated(head.rollup)) {
|
|
820
823
|
partial.push({ id: nodeId, pull, oid: head.oid, rollup: head.rollup });
|
|
@@ -869,131 +872,84 @@ async function fetchPulls(token, scope, expectedLogin, signal) {
|
|
|
869
872
|
}
|
|
870
873
|
return { login, pulls, warnings, orgs, rateLimitRemaining };
|
|
871
874
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
var
|
|
875
|
-
var
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
+
];
|
|
883
904
|
}
|
|
884
|
-
function
|
|
885
|
-
if (
|
|
886
|
-
if (
|
|
887
|
-
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";
|
|
888
909
|
return null;
|
|
889
910
|
}
|
|
890
|
-
function
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
const
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
const theirMove = others !== null && (you === null || others.at > you);
|
|
916
|
-
const approval = newest(pr, "APPROVED");
|
|
917
|
-
const afterApproval = others !== null && approval !== void 0 && others.at > approval.at;
|
|
918
|
-
const approved = decision === "APPROVED" && !afterApproval;
|
|
919
|
-
if (theirMove && others && !approved) {
|
|
920
|
-
reasons.push({ kind: "activity", login: others.login, at: others.at, activity: others.kind });
|
|
921
|
-
}
|
|
922
|
-
if (reasons.length > 0) {
|
|
923
|
-
const since = theirMove && others ? others.at : pr.updatedAt;
|
|
924
|
-
return { court: "you", decision, reasons, stale: false, nudge: false, since };
|
|
925
|
-
}
|
|
926
|
-
const waitingSince = laterISO(pr.readyAt, you) ?? pr.createdAt;
|
|
927
|
-
const settled = (court, reason) => ({
|
|
928
|
-
court,
|
|
929
|
-
decision,
|
|
930
|
-
reasons: [reason],
|
|
931
|
-
stale: false,
|
|
932
|
-
nudge: false,
|
|
933
|
-
since: waitingSince
|
|
934
|
-
});
|
|
935
|
-
if (decision !== "REVIEW_REQUIRED") {
|
|
936
|
-
const gate = matchMergeGates(pr, gates);
|
|
937
|
-
if (gate.length > 0) return settled("gate", { kind: "merge-gate", checks: gate });
|
|
938
|
-
const pending = pr.pendingChecks ?? [];
|
|
939
|
-
const running = { kind: "checks-pending" };
|
|
940
|
-
if (pending.length > 0) running.checks = pending;
|
|
941
|
-
if (mergeState === "BLOCKED" && pending.length === 0) {
|
|
942
|
-
return settled("blocked", { kind: "merge-blocked" });
|
|
943
|
-
}
|
|
944
|
-
if (mergeState === "BLOCKED" || mergeState === "UNSTABLE") return settled("checks", running);
|
|
945
|
-
if (mergeState === "UNKNOWN") return settled("checks", { kind: "mergeability-unknown" });
|
|
946
|
-
if (approved) {
|
|
947
|
-
if (mergeState === "CLEAN" || mergeState === "HAS_HOOKS") {
|
|
948
|
-
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("; ")}`);
|
|
949
936
|
}
|
|
950
|
-
if (
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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;
|
|
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;
|
|
956
948
|
}
|
|
949
|
+
after = pageInfo.endCursor;
|
|
957
950
|
}
|
|
958
951
|
}
|
|
959
|
-
|
|
960
|
-
const waited = now.getTime() - (ms(quietSince) ?? now.getTime());
|
|
961
|
-
return { court: "reviewers", decision, reasons, stale: false, nudge: waited >= NUDGE_AFTER_MS, since: waitingSince };
|
|
962
|
-
}
|
|
963
|
-
function resolveBoard(pulls, actions, now, gates = []) {
|
|
964
|
-
const folded = foldActionLog(actions, now);
|
|
965
|
-
const rows = pulls.map((pr) => {
|
|
966
|
-
const log = folded.get(canonicalId(pr.id));
|
|
967
|
-
const notes = log?.notes ?? [];
|
|
968
|
-
const lastNoteAt = notes.length > 0 ? notes[notes.length - 1].at : null;
|
|
969
|
-
const verdict = judge(pr, now, lastNoteAt, gates);
|
|
970
|
-
const row = { ...pr, ...verdict, status: log?.status === "snoozed" ? "snoozed" : "open", notes };
|
|
971
|
-
if (row.status === "snoozed" && log?.snoozedUntil) row.snoozedUntil = log.snoozedUntil;
|
|
972
|
-
return row;
|
|
973
|
-
});
|
|
974
|
-
return rows.sort((a, b) => {
|
|
975
|
-
const court = courtOrder(a.court) - courtOrder(b.court);
|
|
976
|
-
if (court !== 0) return court;
|
|
977
|
-
if (a.since !== b.since) return a.since < b.since ? -1 : 1;
|
|
978
|
-
return a.id < b.id ? -1 : 1;
|
|
979
|
-
});
|
|
980
|
-
}
|
|
981
|
-
function countBoard(rows) {
|
|
982
|
-
const counts = {
|
|
983
|
-
you: 0,
|
|
984
|
-
ready: 0,
|
|
985
|
-
reviewers: 0,
|
|
986
|
-
gate: 0,
|
|
987
|
-
blocked: 0,
|
|
988
|
-
checks: 0,
|
|
989
|
-
draft: 0,
|
|
990
|
-
parked: 0
|
|
991
|
-
};
|
|
992
|
-
for (const row of rows) {
|
|
993
|
-
if (row.status === "snoozed") counts.parked++;
|
|
994
|
-
else counts[row.court]++;
|
|
995
|
-
}
|
|
996
|
-
return counts;
|
|
952
|
+
return { pulls, complete };
|
|
997
953
|
}
|
|
998
954
|
|
|
999
955
|
// src/board.ts
|
|
@@ -1357,6 +1313,9 @@ function normalizeStoredPull(raw) {
|
|
|
1357
1313
|
autoMerge: raw.autoMerge === true,
|
|
1358
1314
|
requestedReviewers: strings(raw.requestedReviewers),
|
|
1359
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,
|
|
1360
1319
|
lastActivityByYou: typeof raw.lastActivityByYou === "string" ? raw.lastActivityByYou : null,
|
|
1361
1320
|
lastActivityByOthers
|
|
1362
1321
|
};
|
|
@@ -1692,6 +1651,87 @@ import { readFile as readFile5 } from "node:fs/promises";
|
|
|
1692
1651
|
// src/jira.ts
|
|
1693
1652
|
import { execFile as execFile3 } from "node:child_process";
|
|
1694
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
|
|
1695
1735
|
var run3 = promisify3(execFile3);
|
|
1696
1736
|
var ACLI_TIMEOUT_MS = 45e3;
|
|
1697
1737
|
var PAGE_LIMIT = 200;
|
|
@@ -1975,22 +2015,23 @@ async function fetchTickets(acliPath, opts) {
|
|
|
1975
2015
|
if (unreadable > 0) {
|
|
1976
2016
|
warnings.push(`Skipped ${unreadable} work ${unreadable === 1 ? "item" : "items"} Jira described in a way this board could not read.`);
|
|
1977
2017
|
}
|
|
1978
|
-
const
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
DEV_CONCURRENCY,
|
|
1982
|
-
(ticket) => fetchDevPullRequests(acliPath, ticket.key).then(
|
|
1983
|
-
(dev) => ({ asked: true, dev }),
|
|
1984
|
-
() => ({ asked: false, dev: null })
|
|
1985
|
-
)
|
|
2018
|
+
const readPanel = (ticket) => fetchDevPullRequests(acliPath, ticket.key).then(
|
|
2019
|
+
(dev) => ({ asked: true, dev }),
|
|
2020
|
+
() => ({ asked: false, dev: null })
|
|
1986
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();
|
|
1987
2027
|
const unanswered = [];
|
|
1988
2028
|
const unaccounted = [];
|
|
1989
2029
|
candidates.forEach((ticket, i) => {
|
|
1990
|
-
const { asked, dev } = panels[i];
|
|
2030
|
+
const { asked: asked2, dev } = panels[i];
|
|
2031
|
+
if (dev) counts.set(ticket.key, dev.count);
|
|
1991
2032
|
if (allPullRequestsClosed(dev)) {
|
|
1992
2033
|
ticket.allPrsClosed = true;
|
|
1993
|
-
} else if (!
|
|
2034
|
+
} else if (!asked2) {
|
|
1994
2035
|
unanswered.push(ticket.key);
|
|
1995
2036
|
} else if (dev === null) {
|
|
1996
2037
|
unaccounted.push(ticket.key);
|
|
@@ -1998,19 +2039,30 @@ async function fetchTickets(acliPath, opts) {
|
|
|
1998
2039
|
ticket.hasOpenPr = true;
|
|
1999
2040
|
}
|
|
2000
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));
|
|
2001
2053
|
const listed = (keys2) => keys2.map((key) => {
|
|
2002
2054
|
const url = browseUrl(opts.site, key);
|
|
2003
2055
|
return url === null ? key : `[${key}](${url})`;
|
|
2004
2056
|
}).join(", ");
|
|
2005
2057
|
const heldOut = (keys2) => `${keys2.length === 1 ? "it is" : "they are"} held out of "no open pull requests left" rather than guessed at`;
|
|
2006
|
-
if (
|
|
2058
|
+
if (unansweredNow.length > 0) {
|
|
2007
2059
|
warnings.push(
|
|
2008
|
-
`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.`
|
|
2009
2061
|
);
|
|
2010
2062
|
}
|
|
2011
|
-
if (
|
|
2063
|
+
if (unaccountedNow.length > 0) {
|
|
2012
2064
|
warnings.push(
|
|
2013
|
-
`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.`
|
|
2014
2066
|
);
|
|
2015
2067
|
}
|
|
2016
2068
|
if (!opts.site) {
|
|
@@ -2027,7 +2079,7 @@ async function fetchTickets(acliPath, opts) {
|
|
|
2027
2079
|
);
|
|
2028
2080
|
}
|
|
2029
2081
|
return {
|
|
2030
|
-
tickets,
|
|
2082
|
+
tickets: reconciled,
|
|
2031
2083
|
statuses: statusesByProject(base, ...finished.map((batch) => batch ?? [])),
|
|
2032
2084
|
warnings
|
|
2033
2085
|
};
|
|
@@ -2099,16 +2151,16 @@ function readTransitionReport(stdout, key, status) {
|
|
|
2099
2151
|
}
|
|
2100
2152
|
|
|
2101
2153
|
// src/tickets.ts
|
|
2102
|
-
var
|
|
2154
|
+
var COURT_ORDER = ["settled", "started", "idle"];
|
|
2103
2155
|
function ticketCourtOrder(court) {
|
|
2104
|
-
return
|
|
2156
|
+
return COURT_ORDER.indexOf(court);
|
|
2105
2157
|
}
|
|
2106
2158
|
function inStatus(ticket, statuses) {
|
|
2107
2159
|
const status = ticket.workflowStatus.trim().toLowerCase();
|
|
2108
2160
|
if (status === "") return false;
|
|
2109
2161
|
return statuses.some((listed) => listed.trim().toLowerCase() === status);
|
|
2110
2162
|
}
|
|
2111
|
-
function
|
|
2163
|
+
function judge(ticket, holdStatuses = []) {
|
|
2112
2164
|
if (ticket.statusCategory === "done") return null;
|
|
2113
2165
|
if (ticket.hasAnyPr && !ticket.hasOpenPr && ticket.allPrsClosed) return "settled";
|
|
2114
2166
|
if (ticket.statusCategory === "new" && ticket.hasOpenPr) return "started";
|
|
@@ -2119,7 +2171,7 @@ function resolveTickets(tickets, actions, now, holdStatuses = []) {
|
|
|
2119
2171
|
const folded = foldActionLog(actions, now);
|
|
2120
2172
|
const rows = [];
|
|
2121
2173
|
for (const ticket of tickets) {
|
|
2122
|
-
const court =
|
|
2174
|
+
const court = judge(ticket, holdStatuses);
|
|
2123
2175
|
if (!court) continue;
|
|
2124
2176
|
const log = folded.get(canonicalId(ticket.id));
|
|
2125
2177
|
const row = {
|
|
@@ -2148,7 +2200,13 @@ function countTickets(rows) {
|
|
|
2148
2200
|
|
|
2149
2201
|
// src/ticketboard.ts
|
|
2150
2202
|
var MAX_BACKOFF_MS3 = 30 * 6e4;
|
|
2151
|
-
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
|
+
};
|
|
2152
2210
|
var TicketBoard = class {
|
|
2153
2211
|
#config;
|
|
2154
2212
|
#deps;
|
|
@@ -2165,7 +2223,8 @@ var TicketBoard = class {
|
|
|
2165
2223
|
#audience = 0;
|
|
2166
2224
|
#stopped = false;
|
|
2167
2225
|
#lastReadAt = 0;
|
|
2168
|
-
|
|
2226
|
+
#abort = new AbortController();
|
|
2227
|
+
constructor(config, onChange, deps = realDeps4) {
|
|
2169
2228
|
this.#config = config;
|
|
2170
2229
|
this.#deps = deps;
|
|
2171
2230
|
this.#onChange = onChange;
|
|
@@ -2179,10 +2238,12 @@ var TicketBoard = class {
|
|
|
2179
2238
|
this.#file = await readTicketsFile(this.#config.ticketsFile);
|
|
2180
2239
|
await this.refresh();
|
|
2181
2240
|
}
|
|
2241
|
+
/** Stop reading, and cancel a GitHub request in flight so nothing lands after close. */
|
|
2182
2242
|
stop() {
|
|
2183
2243
|
this.#stopped = true;
|
|
2184
2244
|
if (this.#timer) clearTimeout(this.#timer);
|
|
2185
2245
|
this.#timer = null;
|
|
2246
|
+
this.#abort.abort();
|
|
2186
2247
|
}
|
|
2187
2248
|
/** How many browsers are watching. Polling pauses at zero. */
|
|
2188
2249
|
setAudience(count) {
|
|
@@ -2297,8 +2358,24 @@ var TicketBoard = class {
|
|
|
2297
2358
|
return;
|
|
2298
2359
|
}
|
|
2299
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);
|
|
2300
2374
|
try {
|
|
2301
|
-
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];
|
|
2302
2379
|
this.#reason = null;
|
|
2303
2380
|
this.#warnings = warnings;
|
|
2304
2381
|
this.#streak = null;
|
|
@@ -2334,7 +2411,7 @@ function normalizeStoredTicket(raw) {
|
|
|
2334
2411
|
const { key, url, workflowStatus, statusCategory, issueType, summary } = raw;
|
|
2335
2412
|
if (typeof key !== "string" || key === "") return null;
|
|
2336
2413
|
const site = typeof url === "string" ? /^https:\/\/([^/]+)\/browse\//.exec(url)?.[1] ?? null : null;
|
|
2337
|
-
|
|
2414
|
+
const ticket = toTicket(
|
|
2338
2415
|
{
|
|
2339
2416
|
key,
|
|
2340
2417
|
fields: {
|
|
@@ -2351,6 +2428,25 @@ function normalizeStoredTicket(raw) {
|
|
|
2351
2428
|
// files are exactly the ones whose settled court counted drafts as merges.
|
|
2352
2429
|
raw.allPrsClosed === true
|
|
2353
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
|
+
});
|
|
2354
2450
|
}
|
|
2355
2451
|
async function readTicketsFile(path) {
|
|
2356
2452
|
let text;
|
|
@@ -2451,11 +2547,11 @@ function foldAgentLog(text) {
|
|
|
2451
2547
|
});
|
|
2452
2548
|
continue;
|
|
2453
2549
|
}
|
|
2454
|
-
const
|
|
2455
|
-
if (!
|
|
2550
|
+
const run5 = runs.get(record.run);
|
|
2551
|
+
if (!run5) continue;
|
|
2456
2552
|
if (record.event === "asked") {
|
|
2457
2553
|
if (typeof record.turn !== "string" || typeof record.question !== "string") continue;
|
|
2458
|
-
|
|
2554
|
+
run5.turns.push({
|
|
2459
2555
|
id: record.turn,
|
|
2460
2556
|
question: record.question,
|
|
2461
2557
|
startedAt: record.at,
|
|
@@ -2467,7 +2563,7 @@ function foldAgentLog(text) {
|
|
|
2467
2563
|
continue;
|
|
2468
2564
|
}
|
|
2469
2565
|
if (record.event === "replied" || record.event === "turn-failed" || record.event === "turn-aborted") {
|
|
2470
|
-
const turn =
|
|
2566
|
+
const turn = run5.turns.find((candidate) => candidate.id === record.turn);
|
|
2471
2567
|
if (!turn) continue;
|
|
2472
2568
|
turn.reply = typeof record.reply === "string" ? record.reply : turn.reply;
|
|
2473
2569
|
turn.endedAt = record.at;
|
|
@@ -2482,18 +2578,18 @@ function foldAgentLog(text) {
|
|
|
2482
2578
|
}
|
|
2483
2579
|
continue;
|
|
2484
2580
|
}
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
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;
|
|
2489
2585
|
if (record.event === "finished") {
|
|
2490
|
-
|
|
2586
|
+
run5.status = "done";
|
|
2491
2587
|
} else if (record.event === "failed") {
|
|
2492
|
-
|
|
2493
|
-
|
|
2588
|
+
run5.status = "failed";
|
|
2589
|
+
run5.error = record.error;
|
|
2494
2590
|
} else if (record.event === "aborted") {
|
|
2495
|
-
|
|
2496
|
-
|
|
2591
|
+
run5.status = "aborted";
|
|
2592
|
+
run5.error = record.reason;
|
|
2497
2593
|
}
|
|
2498
2594
|
}
|
|
2499
2595
|
return [...runs.values()].sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
@@ -2506,10 +2602,13 @@ var AgentRunner = class {
|
|
|
2506
2602
|
config;
|
|
2507
2603
|
hooks;
|
|
2508
2604
|
spawnFn;
|
|
2509
|
-
|
|
2605
|
+
/** Refuses a Codex too old to hold to the connector rules; see `codex.ts`. */
|
|
2606
|
+
codexGate;
|
|
2607
|
+
constructor(config, hooks, spawnFn = spawn, execFn) {
|
|
2510
2608
|
this.config = config;
|
|
2511
2609
|
this.hooks = hooks;
|
|
2512
2610
|
this.spawnFn = spawnFn;
|
|
2611
|
+
this.codexGate = new CodexGate(config.agent.binPath, execFn);
|
|
2513
2612
|
}
|
|
2514
2613
|
get enabled() {
|
|
2515
2614
|
return this.config.agent.cli !== null;
|
|
@@ -2530,19 +2629,19 @@ var AgentRunner = class {
|
|
|
2530
2629
|
this.runs = foldAgentLog(text);
|
|
2531
2630
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2532
2631
|
const reason = "the dashboard restarted while this was running";
|
|
2533
|
-
for (const
|
|
2534
|
-
if (
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
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 });
|
|
2539
2638
|
}
|
|
2540
|
-
for (const turn of
|
|
2639
|
+
for (const turn of run5.turns) {
|
|
2541
2640
|
if (turn.status !== "running") continue;
|
|
2542
2641
|
turn.status = "aborted";
|
|
2543
2642
|
turn.error = reason;
|
|
2544
2643
|
turn.endedAt = at;
|
|
2545
|
-
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 });
|
|
2546
2645
|
}
|
|
2547
2646
|
}
|
|
2548
2647
|
}
|
|
@@ -2555,7 +2654,7 @@ var AgentRunner = class {
|
|
|
2555
2654
|
return this.runs.at(-1) ?? null;
|
|
2556
2655
|
}
|
|
2557
2656
|
view(now = /* @__PURE__ */ new Date()) {
|
|
2558
|
-
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) }));
|
|
2559
2658
|
return {
|
|
2560
2659
|
enabled: this.enabled,
|
|
2561
2660
|
cli: this.config.agent.cli,
|
|
@@ -2609,7 +2708,7 @@ var AgentRunner = class {
|
|
|
2609
2708
|
);
|
|
2610
2709
|
}
|
|
2611
2710
|
await this.start();
|
|
2612
|
-
const
|
|
2711
|
+
const run5 = {
|
|
2613
2712
|
id: randomUUID(),
|
|
2614
2713
|
cli,
|
|
2615
2714
|
trigger,
|
|
@@ -2623,8 +2722,8 @@ var AgentRunner = class {
|
|
|
2623
2722
|
turns: [],
|
|
2624
2723
|
resumable: false
|
|
2625
2724
|
};
|
|
2626
|
-
this.runs.push(
|
|
2627
|
-
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 });
|
|
2628
2727
|
const adapter = ADAPTERS[cli];
|
|
2629
2728
|
const args = adapter.args({
|
|
2630
2729
|
prompt: agentPrompt(displayPath(this.config.promptFile)),
|
|
@@ -2632,10 +2731,12 @@ var AgentRunner = class {
|
|
|
2632
2731
|
model: this.config.agent.model,
|
|
2633
2732
|
effort: this.config.agent.effort,
|
|
2634
2733
|
tools: [...briefWritingTools(), ...this.config.agent.tools],
|
|
2635
|
-
denyEdits: false
|
|
2734
|
+
denyEdits: false,
|
|
2735
|
+
drafts: false,
|
|
2736
|
+
codexConfig: []
|
|
2636
2737
|
});
|
|
2637
|
-
await this.spawnInto(
|
|
2638
|
-
return
|
|
2738
|
+
await this.spawnInto(run5, null, args, RUN_TIMEOUT_MS, "gave up after an hour");
|
|
2739
|
+
return run5;
|
|
2639
2740
|
}
|
|
2640
2741
|
/**
|
|
2641
2742
|
* Ask a finished run a question, in its own session. Returns once the process
|
|
@@ -2645,16 +2746,16 @@ var AgentRunner = class {
|
|
|
2645
2746
|
const cli = this.config.agent.cli;
|
|
2646
2747
|
if (!cli) throw new Error("running the morning agent is off; set DAILY_FOCUS_AGENT to codex or claude");
|
|
2647
2748
|
await this.start();
|
|
2648
|
-
const
|
|
2649
|
-
if (!
|
|
2749
|
+
const run5 = this.runs.find((candidate) => candidate.id === runId);
|
|
2750
|
+
if (!run5) throw new Error("no such run");
|
|
2650
2751
|
if (this.running) {
|
|
2651
2752
|
throw new Error(
|
|
2652
2753
|
this.running.turn ? "the morning agent is still answering a question" : "the morning agent is still running"
|
|
2653
2754
|
);
|
|
2654
2755
|
}
|
|
2655
|
-
if (!this.resumable(
|
|
2756
|
+
if (!this.resumable(run5)) {
|
|
2656
2757
|
throw new Error(
|
|
2657
|
-
|
|
2758
|
+
run5.sessionId === null ? "this run left no session to continue" : `this run was made by ${run5.cli}, and the agent is ${cli} now`
|
|
2658
2759
|
);
|
|
2659
2760
|
}
|
|
2660
2761
|
const text = question.trim();
|
|
@@ -2668,19 +2769,21 @@ var AgentRunner = class {
|
|
|
2668
2769
|
reply: "",
|
|
2669
2770
|
error: null
|
|
2670
2771
|
};
|
|
2671
|
-
|
|
2672
|
-
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 });
|
|
2673
2774
|
const adapter = ADAPTERS[cli];
|
|
2674
2775
|
const args = adapter.args({
|
|
2675
2776
|
prompt: followUpPrompt(text),
|
|
2676
|
-
sessionId:
|
|
2777
|
+
sessionId: run5.sessionId,
|
|
2677
2778
|
model: this.config.agent.model,
|
|
2678
2779
|
effort: this.config.agent.effort,
|
|
2679
2780
|
// The sources, and not the brief: a chat is given nothing to write.
|
|
2680
2781
|
tools: this.config.agent.tools,
|
|
2681
|
-
denyEdits: true
|
|
2782
|
+
denyEdits: true,
|
|
2783
|
+
drafts: false,
|
|
2784
|
+
codexConfig: []
|
|
2682
2785
|
});
|
|
2683
|
-
await this.spawnInto(
|
|
2786
|
+
await this.spawnInto(run5, turn, args, TURN_TIMEOUT_MS, "gave up after ten minutes");
|
|
2684
2787
|
return turn;
|
|
2685
2788
|
}
|
|
2686
2789
|
/**
|
|
@@ -2688,10 +2791,15 @@ var AgentRunner = class {
|
|
|
2688
2791
|
* run when there is none. The store in both cases: Claude Code files a
|
|
2689
2792
|
* session under the directory it was started in, and looks for it there.
|
|
2690
2793
|
*/
|
|
2691
|
-
async spawnInto(
|
|
2692
|
-
const cli =
|
|
2794
|
+
async spawnInto(run5, turn, args, timeoutMs, timeoutReason) {
|
|
2795
|
+
const cli = run5.cli;
|
|
2693
2796
|
const adapter = ADAPTERS[cli];
|
|
2694
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
|
+
}
|
|
2695
2803
|
let child;
|
|
2696
2804
|
try {
|
|
2697
2805
|
child = this.spawnFn(this.config.agent.binPath, args, {
|
|
@@ -2701,12 +2809,12 @@ var AgentRunner = class {
|
|
|
2701
2809
|
env: process.env
|
|
2702
2810
|
});
|
|
2703
2811
|
} catch (err) {
|
|
2704
|
-
await this.finish(
|
|
2812
|
+
await this.finish(run5, turn, { ok: false, error: this.spawnError(err) });
|
|
2705
2813
|
return;
|
|
2706
2814
|
}
|
|
2707
2815
|
const timer = setTimeout(() => void this.stop(timeoutReason), timeoutMs);
|
|
2708
2816
|
timer.unref();
|
|
2709
|
-
const running = { run:
|
|
2817
|
+
const running = { run: run5, turn, child, timer, abortReason: null };
|
|
2710
2818
|
this.running = running;
|
|
2711
2819
|
this.hooks.onChange();
|
|
2712
2820
|
let done;
|
|
@@ -2720,15 +2828,15 @@ var AgentRunner = class {
|
|
|
2720
2828
|
lines.on("line", (line) => {
|
|
2721
2829
|
const event = adapter.parse(line);
|
|
2722
2830
|
if (!event) return;
|
|
2723
|
-
if (event.sessionId && !turn)
|
|
2831
|
+
if (event.sessionId && !turn) run5.sessionId = event.sessionId;
|
|
2724
2832
|
if (event.text) {
|
|
2725
2833
|
if (turn) {
|
|
2726
2834
|
turn.reply = turn.reply ? `${turn.reply}
|
|
2727
2835
|
|
|
2728
2836
|
${event.text}` : event.text;
|
|
2729
2837
|
} else {
|
|
2730
|
-
|
|
2731
|
-
|
|
2838
|
+
run5.messages.push(event.text);
|
|
2839
|
+
run5.report = event.text;
|
|
2732
2840
|
}
|
|
2733
2841
|
this.hooks.onChange();
|
|
2734
2842
|
}
|
|
@@ -2745,7 +2853,7 @@ ${event.text}` : event.text;
|
|
|
2745
2853
|
ok: false,
|
|
2746
2854
|
error: `${cli} exited with code ${code}${stderr.length ? `: ${stderr.join("").trim().split("\n").slice(-3).join(" ")}` : ""}`
|
|
2747
2855
|
};
|
|
2748
|
-
void this.finish(
|
|
2856
|
+
void this.finish(run5, turn, outcome, running.abortReason !== null);
|
|
2749
2857
|
});
|
|
2750
2858
|
}
|
|
2751
2859
|
/**
|
|
@@ -2762,45 +2870,45 @@ ${event.text}` : event.text;
|
|
|
2762
2870
|
async close() {
|
|
2763
2871
|
await this.stop("the dashboard shut down");
|
|
2764
2872
|
}
|
|
2765
|
-
resumable(
|
|
2766
|
-
return
|
|
2873
|
+
resumable(run5) {
|
|
2874
|
+
return run5.status !== "running" && run5.sessionId !== null && run5.cli === this.config.agent.cli;
|
|
2767
2875
|
}
|
|
2768
|
-
async finish(
|
|
2876
|
+
async finish(run5, turn, outcome, aborted = false) {
|
|
2769
2877
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2770
2878
|
if (turn) {
|
|
2771
2879
|
const reply = outcome.reply ?? turn.reply;
|
|
2772
2880
|
turn.reply = reply;
|
|
2773
2881
|
turn.endedAt = at;
|
|
2774
2882
|
if (outcome.ok) {
|
|
2775
|
-
await this.append({ event: "replied", run:
|
|
2883
|
+
await this.append({ event: "replied", run: run5.id, turn: turn.id, reply, at });
|
|
2776
2884
|
turn.status = "done";
|
|
2777
2885
|
} else if (aborted) {
|
|
2778
2886
|
turn.error = outcome.error ?? "stopped";
|
|
2779
|
-
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 });
|
|
2780
2888
|
turn.status = "aborted";
|
|
2781
2889
|
} else {
|
|
2782
2890
|
turn.error = outcome.error ?? "failed";
|
|
2783
|
-
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 });
|
|
2784
2892
|
turn.status = "failed";
|
|
2785
2893
|
}
|
|
2786
2894
|
this.hooks.onChange();
|
|
2787
2895
|
return;
|
|
2788
2896
|
}
|
|
2789
|
-
const report = outcome.reply ??
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
const messages =
|
|
2897
|
+
const report = outcome.reply ?? run5.report;
|
|
2898
|
+
run5.report = report;
|
|
2899
|
+
run5.endedAt = at;
|
|
2900
|
+
const messages = run5.messages;
|
|
2793
2901
|
if (outcome.ok) {
|
|
2794
|
-
await this.append({ event: "finished", run:
|
|
2795
|
-
|
|
2902
|
+
await this.append({ event: "finished", run: run5.id, sessionId: run5.sessionId, report, messages, at });
|
|
2903
|
+
run5.status = "done";
|
|
2796
2904
|
} else if (aborted) {
|
|
2797
|
-
|
|
2798
|
-
await this.append({ event: "aborted", run:
|
|
2799
|
-
|
|
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";
|
|
2800
2908
|
} else {
|
|
2801
|
-
|
|
2802
|
-
await this.append({ event: "failed", run:
|
|
2803
|
-
|
|
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";
|
|
2804
2912
|
}
|
|
2805
2913
|
this.hooks.onChange();
|
|
2806
2914
|
}
|
|
@@ -2914,33 +3022,6 @@ function refusal(req, listenHost) {
|
|
|
2914
3022
|
return null;
|
|
2915
3023
|
}
|
|
2916
3024
|
|
|
2917
|
-
// src/presence.ts
|
|
2918
|
-
import { execFile as execFile4 } from "node:child_process";
|
|
2919
|
-
import { platform } from "node:os";
|
|
2920
|
-
import { promisify as promisify4 } from "node:util";
|
|
2921
|
-
var run4 = promisify4(execFile4);
|
|
2922
|
-
var PROBE_TIMEOUT_MS = 4e3;
|
|
2923
|
-
var HID_IDLE = /"HIDIdleTime"\s*=\s*(\d+)/;
|
|
2924
|
-
var readIdleSeconds = async () => {
|
|
2925
|
-
if (platform() !== "darwin") return null;
|
|
2926
|
-
try {
|
|
2927
|
-
const { stdout } = await run4("ioreg", ["-c", "IOHIDSystem", "-d", "1", "-r"], {
|
|
2928
|
-
timeout: PROBE_TIMEOUT_MS,
|
|
2929
|
-
maxBuffer: 1024 * 1024
|
|
2930
|
-
});
|
|
2931
|
-
return parseIdleSeconds(stdout);
|
|
2932
|
-
} catch {
|
|
2933
|
-
return null;
|
|
2934
|
-
}
|
|
2935
|
-
};
|
|
2936
|
-
function parseIdleSeconds(ioregOutput) {
|
|
2937
|
-
const match = HID_IDLE.exec(ioregOutput);
|
|
2938
|
-
if (!match) return null;
|
|
2939
|
-
const nanoseconds = Number(match[1]);
|
|
2940
|
-
if (!Number.isFinite(nanoseconds) || nanoseconds < 0) return null;
|
|
2941
|
-
return nanoseconds / 1e9;
|
|
2942
|
-
}
|
|
2943
|
-
|
|
2944
3025
|
// src/settings.ts
|
|
2945
3026
|
var SETTING_GROUPS = [
|
|
2946
3027
|
{ id: "instance", title: "This dashboard" },
|
|
@@ -2949,8 +3030,7 @@ var SETTING_GROUPS = [
|
|
|
2949
3030
|
{ id: "github", title: "Pull requests" },
|
|
2950
3031
|
{ id: "jira", title: "Jira tickets" },
|
|
2951
3032
|
{ id: "calendar", title: "Calendar" },
|
|
2952
|
-
{ id: "day", title: "Working day" }
|
|
2953
|
-
{ id: "sessions", title: "Focus sessions" }
|
|
3033
|
+
{ id: "day", title: "Working day" }
|
|
2954
3034
|
];
|
|
2955
3035
|
var OFF_OR_CLI = [
|
|
2956
3036
|
{ value: "off", label: "Off" },
|
|
@@ -2964,7 +3044,7 @@ var SETTINGS = [
|
|
|
2964
3044
|
key: "DAILY_FOCUS_PROFILE",
|
|
2965
3045
|
group: "instance",
|
|
2966
3046
|
label: "What this dashboard briefs",
|
|
2967
|
-
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.",
|
|
2968
3048
|
kind: "choice",
|
|
2969
3049
|
choices: PROFILES.map((value) => ({ value, label: value === "work" ? "Work" : "Personal life" })),
|
|
2970
3050
|
fallback: always("Work")
|
|
@@ -3015,7 +3095,7 @@ var SETTINGS = [
|
|
|
3015
3095
|
key: "DAILY_FOCUS_AGENT_TOOLS",
|
|
3016
3096
|
group: "agent",
|
|
3017
3097
|
label: "Tools Claude Code may use",
|
|
3018
|
-
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.",
|
|
3019
3099
|
kind: "list",
|
|
3020
3100
|
fallback: always("Bash(gh *), Bash(acli *), WebFetch, mcp__claude_ai_Gmail"),
|
|
3021
3101
|
advanced: true
|
|
@@ -3042,7 +3122,7 @@ var SETTINGS = [
|
|
|
3042
3122
|
key: "DAILY_FOCUS_ASSISTANT",
|
|
3043
3123
|
group: "assistant",
|
|
3044
3124
|
label: "Answer questions with",
|
|
3045
|
-
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.",
|
|
3046
3126
|
kind: "choice",
|
|
3047
3127
|
choices: OFF_OR_CLI,
|
|
3048
3128
|
fallback: always("Off")
|
|
@@ -3068,11 +3148,36 @@ var SETTINGS = [
|
|
|
3068
3148
|
key: "DAILY_FOCUS_ASSISTANT_TOOLS",
|
|
3069
3149
|
group: "assistant",
|
|
3070
3150
|
label: "Tools Claude Code may use",
|
|
3071
|
-
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.",
|
|
3072
3152
|
kind: "list",
|
|
3073
3153
|
fallback: always("Bash(gh *), Bash(acli *), WebFetch, mcp__claude_ai_Gmail"),
|
|
3074
3154
|
advanced: true
|
|
3075
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
|
+
},
|
|
3076
3181
|
{
|
|
3077
3182
|
key: "DAILY_FOCUS_ASSISTANT_BIN",
|
|
3078
3183
|
group: "assistant",
|
|
@@ -3086,7 +3191,7 @@ var SETTINGS = [
|
|
|
3086
3191
|
key: "DAILY_FOCUS_GITHUB",
|
|
3087
3192
|
group: "github",
|
|
3088
3193
|
label: "Pull request board",
|
|
3089
|
-
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.",
|
|
3090
3195
|
kind: "flag",
|
|
3091
3196
|
fallback: always("On")
|
|
3092
3197
|
},
|
|
@@ -3265,22 +3370,6 @@ var SETTINGS = [
|
|
|
3265
3370
|
help: "Minutes a gap has to last to count as time for focused work.",
|
|
3266
3371
|
kind: "number",
|
|
3267
3372
|
fallback: always("45 minutes")
|
|
3268
|
-
},
|
|
3269
|
-
{
|
|
3270
|
-
key: "DAILY_FOCUS_SESSION_MINUTES",
|
|
3271
|
-
group: "sessions",
|
|
3272
|
-
label: "Session length",
|
|
3273
|
-
help: "Minutes a focus session is set for when you start one.",
|
|
3274
|
-
kind: "number",
|
|
3275
|
-
fallback: always("25 minutes")
|
|
3276
|
-
},
|
|
3277
|
-
{
|
|
3278
|
-
key: "DAILY_FOCUS_AWAY_AFTER",
|
|
3279
|
-
group: "sessions",
|
|
3280
|
-
label: "End a session when away for",
|
|
3281
|
-
help: "Minutes without touching the Mac before a session is closed where you left. 0 turns it off.",
|
|
3282
|
-
kind: "number",
|
|
3283
|
-
fallback: (profile) => profile === "personal" ? "0, off" : "10 minutes"
|
|
3284
3373
|
}
|
|
3285
3374
|
];
|
|
3286
3375
|
var BY_KEY = new Map(SETTINGS.map((def) => [def.key, def]));
|
|
@@ -3343,11 +3432,11 @@ async function saveSettings(path, settings) {
|
|
|
3343
3432
|
}
|
|
3344
3433
|
|
|
3345
3434
|
// src/setup.ts
|
|
3346
|
-
import { execFile as
|
|
3435
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
3347
3436
|
import { access, constants, readdir as readdir2, readFile as readFile7, stat as stat3 } from "node:fs/promises";
|
|
3348
3437
|
import { delimiter, isAbsolute, join as join3 } from "node:path";
|
|
3349
|
-
import { promisify as
|
|
3350
|
-
var
|
|
3438
|
+
import { promisify as promisify4 } from "node:util";
|
|
3439
|
+
var run4 = promisify4(execFile4);
|
|
3351
3440
|
var PLACEHOLDERS = [
|
|
3352
3441
|
"Your Name",
|
|
3353
3442
|
"you@example.com",
|
|
@@ -3437,11 +3526,11 @@ var identity = null;
|
|
|
3437
3526
|
function detectIdentity(config) {
|
|
3438
3527
|
identity ??= (async () => {
|
|
3439
3528
|
const [user, email] = await Promise.all([
|
|
3440
|
-
|
|
3529
|
+
run4(config.github.ghPath, ["api", "user", "--jq", '[.login, .name // ""] | join("\\n")'], { timeout: 5e3 }).then(
|
|
3441
3530
|
({ stdout }) => stdout.split("\n"),
|
|
3442
3531
|
() => []
|
|
3443
3532
|
),
|
|
3444
|
-
|
|
3533
|
+
run4("git", ["config", "--global", "user.email"], { timeout: 2e3 }).then(
|
|
3445
3534
|
({ stdout }) => stdout.trim() || null,
|
|
3446
3535
|
() => null
|
|
3447
3536
|
)
|
|
@@ -3490,8 +3579,8 @@ var defaultExec = (file, args, { timeout }) => new Promise((resolve2, reject) =>
|
|
|
3490
3579
|
child.on("error", (error) => finish(error));
|
|
3491
3580
|
child.on("close", () => finish(null));
|
|
3492
3581
|
});
|
|
3493
|
-
function shellCandidates(envShell, loginShell,
|
|
3494
|
-
const fallback =
|
|
3582
|
+
function shellCandidates(envShell, loginShell, platform) {
|
|
3583
|
+
const fallback = platform === "darwin" ? "/bin/zsh" : platform === "linux" ? "/bin/bash" : void 0;
|
|
3495
3584
|
const shells = [];
|
|
3496
3585
|
for (const shell of [envShell, loginShell, fallback]) {
|
|
3497
3586
|
const trimmed = shell?.trim();
|
|
@@ -3536,11 +3625,11 @@ function currentLoginShell() {
|
|
|
3536
3625
|
return void 0;
|
|
3537
3626
|
}
|
|
3538
3627
|
}
|
|
3539
|
-
async function hydratePath(env = process.env,
|
|
3540
|
-
if (
|
|
3628
|
+
async function hydratePath(env = process.env, platform = process.platform, exec = defaultExec, loginShell = currentLoginShell()) {
|
|
3629
|
+
if (platform === "win32") return null;
|
|
3541
3630
|
let found = null;
|
|
3542
3631
|
let from = null;
|
|
3543
|
-
for (const shell of shellCandidates(env.SHELL, loginShell,
|
|
3632
|
+
for (const shell of shellCandidates(env.SHELL, loginShell, platform)) {
|
|
3544
3633
|
try {
|
|
3545
3634
|
found = await readLoginShellPath(shell, exec);
|
|
3546
3635
|
} catch {
|
|
@@ -3550,7 +3639,7 @@ async function hydratePath(env = process.env, platform2 = process.platform, exec
|
|
|
3550
3639
|
break;
|
|
3551
3640
|
}
|
|
3552
3641
|
}
|
|
3553
|
-
if (!found &&
|
|
3642
|
+
if (!found && platform === "darwin") {
|
|
3554
3643
|
found = await readLaunchctlPath(exec);
|
|
3555
3644
|
if (found) from = "launchctl";
|
|
3556
3645
|
}
|
|
@@ -3576,15 +3665,8 @@ var TEXT_FILES = {
|
|
|
3576
3665
|
"/api/text/focus": { file: "focusFile", name: "focus.md" },
|
|
3577
3666
|
"/api/text/sources": { file: "sourcesFile", name: "sources.md" }
|
|
3578
3667
|
};
|
|
3579
|
-
var VALID_ACTIONS =
|
|
3580
|
-
"done",
|
|
3581
|
-
"snooze",
|
|
3582
|
-
"dismiss",
|
|
3583
|
-
"reopen",
|
|
3584
|
-
"note"
|
|
3585
|
-
]);
|
|
3668
|
+
var VALID_ACTIONS = new Set(ACTION_TYPES);
|
|
3586
3669
|
var MAX_BODY_BYTES = 64 * 1024;
|
|
3587
|
-
var PRESENCE_POLL_MS = 3e4;
|
|
3588
3670
|
var AGENT_CLOCK_MS = 3e4;
|
|
3589
3671
|
function sendJSON(res, status, body) {
|
|
3590
3672
|
const payload = JSON.stringify(body);
|
|
@@ -3707,21 +3789,48 @@ async function startServer(env, options = {}) {
|
|
|
3707
3789
|
});
|
|
3708
3790
|
const subscribers = /* @__PURE__ */ new Set();
|
|
3709
3791
|
let assetVersion = await computeAssetVersion(PUBLIC_DIR);
|
|
3710
|
-
|
|
3792
|
+
let autoAsk = null;
|
|
3793
|
+
const board = new Board(
|
|
3794
|
+
config,
|
|
3795
|
+
() => {
|
|
3796
|
+
void broadcast();
|
|
3797
|
+
autoAsk?.poke();
|
|
3798
|
+
},
|
|
3799
|
+
options.integrations?.github
|
|
3800
|
+
);
|
|
3711
3801
|
const calendar = new CalendarBoard(config, () => void broadcast());
|
|
3712
3802
|
const tickets = new TicketBoard(config, () => void broadcast(), options.integrations?.jira);
|
|
3713
3803
|
const assistant = new Assistant(config, {
|
|
3714
3804
|
onChange: () => {
|
|
3715
3805
|
void broadcast();
|
|
3716
3806
|
restartWhenIdle();
|
|
3807
|
+
autoAsk?.poke();
|
|
3717
3808
|
}
|
|
3718
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;
|
|
3719
3819
|
const agent = new AgentRunner(config, {
|
|
3720
3820
|
onChange: () => {
|
|
3721
3821
|
void broadcast();
|
|
3722
3822
|
restartWhenIdle();
|
|
3723
3823
|
}
|
|
3724
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
|
+
});
|
|
3725
3834
|
function requestRestart() {
|
|
3726
3835
|
restart = options.onRestart ? "waiting" : "manual";
|
|
3727
3836
|
restartWhenIdle();
|
|
@@ -3743,19 +3852,27 @@ async function startServer(env, options = {}) {
|
|
|
3743
3852
|
const setup = await setupState(config, {
|
|
3744
3853
|
profileChosen: Boolean(layerEnv(sources).DAILY_FOCUS_PROFILE?.trim())
|
|
3745
3854
|
});
|
|
3855
|
+
const pulls = board.view(actions, now);
|
|
3746
3856
|
return {
|
|
3747
3857
|
...state,
|
|
3748
|
-
board:
|
|
3858
|
+
board: pulls,
|
|
3749
3859
|
tickets: tickets.view(actions, now),
|
|
3750
|
-
assistant: assistant.view(),
|
|
3860
|
+
assistant: { ...assistant.view(), auto: ahead.view(pulls.rows) },
|
|
3751
3861
|
agentRun: agent.view(now),
|
|
3752
|
-
setupWarnings: [...configWarnings, ...linkWarning ? [linkWarning] : []],
|
|
3862
|
+
setupWarnings: [...configWarnings, ...codexWarnings, ...linkWarning ? [linkWarning] : []],
|
|
3753
3863
|
restart,
|
|
3754
3864
|
setup,
|
|
3755
3865
|
assetVersion,
|
|
3756
3866
|
storeId
|
|
3757
3867
|
};
|
|
3758
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
|
+
}
|
|
3759
3876
|
async function broadcast(prebuilt) {
|
|
3760
3877
|
if (subscribers.size === 0) return;
|
|
3761
3878
|
let payload;
|
|
@@ -3777,13 +3894,11 @@ data: ${payload}
|
|
|
3777
3894
|
() => {
|
|
3778
3895
|
void store.archiveCurrentBrief().then(() => broadcast());
|
|
3779
3896
|
},
|
|
3780
|
-
// The
|
|
3781
|
-
//
|
|
3782
|
-
//
|
|
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.
|
|
3783
3900
|
{
|
|
3784
3901
|
ignore: [
|
|
3785
|
-
basename(config.sessionFile),
|
|
3786
|
-
basename(tempPathFor(config.sessionFile)),
|
|
3787
3902
|
basename(config.pullsFile),
|
|
3788
3903
|
basename(tempPathFor(config.pullsFile)),
|
|
3789
3904
|
basename(config.ticketsFile),
|
|
@@ -3803,32 +3918,6 @@ data: ${payload}
|
|
|
3803
3918
|
void broadcast();
|
|
3804
3919
|
});
|
|
3805
3920
|
});
|
|
3806
|
-
let sessionTick = null;
|
|
3807
|
-
function scheduleSessionTick(endsAt) {
|
|
3808
|
-
if (sessionTick) clearTimeout(sessionTick);
|
|
3809
|
-
sessionTick = null;
|
|
3810
|
-
if (!endsAt) return;
|
|
3811
|
-
const delay = new Date(endsAt).getTime() - Date.now();
|
|
3812
|
-
if (delay <= 0 || delay > 36e5) return;
|
|
3813
|
-
sessionTick = setTimeout(() => void broadcast(), delay);
|
|
3814
|
-
sessionTick.unref();
|
|
3815
|
-
}
|
|
3816
|
-
async function checkPresence() {
|
|
3817
|
-
let closed;
|
|
3818
|
-
try {
|
|
3819
|
-
closed = await reconcileSession(config, readIdleSeconds);
|
|
3820
|
-
} catch (err) {
|
|
3821
|
-
console.error(`[daily-focus] presence check failed: ${err.message}`);
|
|
3822
|
-
return;
|
|
3823
|
-
}
|
|
3824
|
-
if (!closed) return;
|
|
3825
|
-
console.log(
|
|
3826
|
-
`[daily-focus] stopped "${closed.title}" at ${closed.endedAt} after ${closed.actualMinutes} min \u2014 machine untouched since then`
|
|
3827
|
-
);
|
|
3828
|
-
scheduleSessionTick(null);
|
|
3829
|
-
await broadcast();
|
|
3830
|
-
}
|
|
3831
|
-
await checkPresence();
|
|
3832
3921
|
await assistant.start();
|
|
3833
3922
|
await agent.start();
|
|
3834
3923
|
let ticks = Promise.resolve();
|
|
@@ -3853,10 +3942,10 @@ data: ${payload}
|
|
|
3853
3942
|
void clockTick();
|
|
3854
3943
|
}, AGENT_CLOCK_MS);
|
|
3855
3944
|
agentClock.unref();
|
|
3856
|
-
scheduleSessionTick((await buildState()).session.active?.endsAt ?? null);
|
|
3857
3945
|
void board.start().catch((err) => {
|
|
3858
3946
|
console.error(`[daily-focus] pull request board failed to start: ${err.message}`);
|
|
3859
3947
|
});
|
|
3948
|
+
ahead.poke();
|
|
3860
3949
|
void calendar.start().catch((err) => {
|
|
3861
3950
|
console.error(`[daily-focus] live agenda failed to start: ${err.message}`);
|
|
3862
3951
|
});
|
|
@@ -3867,10 +3956,6 @@ data: ${payload}
|
|
|
3867
3956
|
void broadcast();
|
|
3868
3957
|
}, 6e4);
|
|
3869
3958
|
heartbeat.unref();
|
|
3870
|
-
const presencePoll = setInterval(() => {
|
|
3871
|
-
void checkPresence();
|
|
3872
|
-
}, PRESENCE_POLL_MS);
|
|
3873
|
-
presencePoll.unref();
|
|
3874
3959
|
const server = createServer((req, res) => {
|
|
3875
3960
|
void handle(req, res).catch((err) => {
|
|
3876
3961
|
console.error(`[daily-focus] ${req.method} ${req.url} failed:`, err);
|
|
@@ -4017,12 +4102,7 @@ data: ${JSON.stringify(await buildState())}
|
|
|
4017
4102
|
sendJSON(res, 409, { error: "the assistant is off; set DAILY_FOCUS_ASSISTANT to claude or codex" });
|
|
4018
4103
|
return;
|
|
4019
4104
|
}
|
|
4020
|
-
const
|
|
4021
|
-
const context = {
|
|
4022
|
-
item: current.items.find((item) => item.id === id),
|
|
4023
|
-
pull: current.board.rows.find((row) => row.id === id),
|
|
4024
|
-
ticket: current.tickets.rows.find((row) => row.id === id) ?? current.tickets.inProgress.find((row) => row.id === id)
|
|
4025
|
-
};
|
|
4105
|
+
const context = contextFor(id, await buildState());
|
|
4026
4106
|
if (!context.item && !context.pull && !context.ticket) {
|
|
4027
4107
|
sendJSON(res, 404, { error: `no item with id ${id}` });
|
|
4028
4108
|
return;
|
|
@@ -4058,8 +4138,8 @@ data: ${JSON.stringify(await buildState())}
|
|
|
4058
4138
|
sendJSON(res, 400, { error: `invalid JSON body: ${err.message}` });
|
|
4059
4139
|
return;
|
|
4060
4140
|
}
|
|
4061
|
-
const { run:
|
|
4062
|
-
if (typeof
|
|
4141
|
+
const { run: run5, text } = typeof body === "object" && body !== null ? body : {};
|
|
4142
|
+
if (typeof run5 !== "string" || run5 === "") {
|
|
4063
4143
|
sendJSON(res, 400, { error: "run must be a run id" });
|
|
4064
4144
|
return;
|
|
4065
4145
|
}
|
|
@@ -4068,7 +4148,7 @@ data: ${JSON.stringify(await buildState())}
|
|
|
4068
4148
|
return;
|
|
4069
4149
|
}
|
|
4070
4150
|
try {
|
|
4071
|
-
await agent.ask(
|
|
4151
|
+
await agent.ask(run5, text);
|
|
4072
4152
|
} catch (err) {
|
|
4073
4153
|
sendJSON(res, 409, { error: err.message });
|
|
4074
4154
|
return;
|
|
@@ -4138,53 +4218,6 @@ data: ${JSON.stringify(await buildState())}
|
|
|
4138
4218
|
void broadcast(state);
|
|
4139
4219
|
return;
|
|
4140
4220
|
}
|
|
4141
|
-
if (path === "/api/session" && req.method === "POST") {
|
|
4142
|
-
let body;
|
|
4143
|
-
try {
|
|
4144
|
-
body = JSON.parse(await readBody(req));
|
|
4145
|
-
} catch (err) {
|
|
4146
|
-
sendJSON(res, 400, { error: `invalid JSON body: ${err.message}` });
|
|
4147
|
-
return;
|
|
4148
|
-
}
|
|
4149
|
-
if (typeof body !== "object" || body === null) {
|
|
4150
|
-
sendJSON(res, 400, { error: "body must be an object" });
|
|
4151
|
-
return;
|
|
4152
|
-
}
|
|
4153
|
-
const { action, id, minutes } = body;
|
|
4154
|
-
if (action === "stop") {
|
|
4155
|
-
await stopSession(config);
|
|
4156
|
-
} else if (action === "start") {
|
|
4157
|
-
if (typeof id !== "string" || id === "") {
|
|
4158
|
-
sendJSON(res, 400, { error: '"id" is required to start a session' });
|
|
4159
|
-
return;
|
|
4160
|
-
}
|
|
4161
|
-
const { brief } = await store.readBrief();
|
|
4162
|
-
const item = brief?.items.find((candidate) => candidate.id === id);
|
|
4163
|
-
if (!item) {
|
|
4164
|
-
sendJSON(res, 404, { error: `no item with id ${id}` });
|
|
4165
|
-
return;
|
|
4166
|
-
}
|
|
4167
|
-
const requested = typeof minutes === "number" && Number.isFinite(minutes) ? Math.round(minutes) : config.sessionMinutes;
|
|
4168
|
-
if (requested < 1 || requested > 120) {
|
|
4169
|
-
sendJSON(res, 400, { error: '"minutes" must be between 1 and 120' });
|
|
4170
|
-
return;
|
|
4171
|
-
}
|
|
4172
|
-
await startSession(config, {
|
|
4173
|
-
id,
|
|
4174
|
-
title: item.title,
|
|
4175
|
-
minutes: requested,
|
|
4176
|
-
advancesObjective: item.advancesObjective === true
|
|
4177
|
-
});
|
|
4178
|
-
} else {
|
|
4179
|
-
sendJSON(res, 400, { error: '"action" must be "start" or "stop"' });
|
|
4180
|
-
return;
|
|
4181
|
-
}
|
|
4182
|
-
const state = await buildState();
|
|
4183
|
-
sendState(res, state);
|
|
4184
|
-
scheduleSessionTick(state.session.active?.endsAt ?? null);
|
|
4185
|
-
void broadcast(state);
|
|
4186
|
-
return;
|
|
4187
|
-
}
|
|
4188
4221
|
if (path === "/api/settings" && req.method === "GET") {
|
|
4189
4222
|
sendJSON(res, 200, { ...describeSettings(readSources()), restart });
|
|
4190
4223
|
return;
|
|
@@ -4312,9 +4345,8 @@ ${entry.source}`)).map(({ title, source }) => ({ title, source })).sort((a, b) =
|
|
|
4312
4345
|
}
|
|
4313
4346
|
async function stopEverything() {
|
|
4314
4347
|
clearInterval(heartbeat);
|
|
4315
|
-
clearInterval(presencePoll);
|
|
4316
4348
|
clearInterval(agentClock);
|
|
4317
|
-
|
|
4349
|
+
ahead.close();
|
|
4318
4350
|
board.stop();
|
|
4319
4351
|
calendar.stop();
|
|
4320
4352
|
tickets.stop();
|