daily-focus 1.2.0-dev.9 → 2.0.0-dev.15
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 +18 -4
- package/SETUP.md +156 -105
- package/dist/{audit-DNTFZOHH.js → audit-AQUBVWPA.js} +4 -3
- package/dist/{chunk-KZNY45XW.js → chunk-72NUDSOV.js} +126 -119
- package/dist/{chunk-GQ3NCWRT.js → chunk-ITUK7XKS.js} +6 -19
- package/dist/{chunk-KL6RU4XT.js → chunk-JTMBBUMD.js} +316 -10
- package/dist/chunk-QG4XXKBP.js +18 -0
- package/dist/chunk-U6EPOXEW.js +570 -0
- package/dist/{chunk-P3E2B2SD.js → chunk-WPXIQPYX.js} +1 -1
- package/dist/{chunk-6SMUSPZG.js → chunk-YPGKXTBX.js} +2 -2
- package/dist/cli.js +19 -12
- package/dist/{demo-TIIXODJG.js → demo-KE6WDWMA.js} +159 -37
- package/dist/{init-XDMAXU2S.js → init-5KJVHWJU.js} +41 -6
- package/dist/{seed-TWT7EDXJ.js → seed-T4VEY6DT.js} +4 -3
- package/dist/{server-WT4CQ3VX.js → server-FFRNYEAO.js} +2000 -743
- package/dist/{service-ZUHI3VKH.js → service-LQ46YRCZ.js} +7 -6
- package/package.json +6 -2
- package/prompts/README.md +20 -4
- package/prompts/assistant.md +7 -1
- package/prompts/morning-brief-personal.md +37 -0
- package/prompts/morning-brief-work.md +37 -0
- package/prompts/onboarding.md +41 -0
- package/public/app.js +806 -287
- package/public/fonts/IBMPlexSans-Variable.woff2 +0 -0
- package/public/fonts/IBMPlexSerif-Italic.woff2 +0 -0
- package/public/fonts/IBMPlexSerif-Medium.woff2 +0 -0
- package/public/fonts/IBMPlexSerif-Regular.woff2 +0 -0
- package/public/fonts/IBMPlexSerif-SemiBold.woff2 +0 -0
- package/public/fonts/OFL.txt +93 -0
- package/public/style.css +1686 -481
- package/dist/chunk-V2UN3JYL.js +0 -140
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalId,
|
|
3
|
+
foldActionLog
|
|
4
|
+
} from "./chunk-ITUK7XKS.js";
|
|
5
|
+
import {
|
|
6
|
+
laterISO,
|
|
7
|
+
localDateKey,
|
|
8
|
+
parseISO
|
|
9
|
+
} from "./chunk-72NUDSOV.js";
|
|
10
|
+
|
|
1
11
|
// src/codex.ts
|
|
2
12
|
import { execFile } from "node:child_process";
|
|
3
13
|
import { promisify } from "node:util";
|
|
@@ -99,6 +109,18 @@ async function codexVersionWarning(bin, run = defaultExec) {
|
|
|
99
109
|
const consequence = olderThan(version, CODEX_CONNECTOR_POLICY) ? `Before ${CODEX_CONNECTOR_POLICY} Codex ignores the dashboard's refusal of connector writes without saying so, so the dashboard won't start the agent or the assistant on it.` : `Before ${CODEX_MINIMUM} Codex can't switch computer use off, so the assistant won't answer while computer use is off in its settings.`;
|
|
100
110
|
return `Codex ${version} is older than ${CODEX_MINIMUM}, the oldest the dashboard's limits on Codex work with. ${consequence} Update Codex.`;
|
|
101
111
|
}
|
|
112
|
+
async function codexLatestModel(bin, run = defaultExec) {
|
|
113
|
+
try {
|
|
114
|
+
const catalog = JSON.parse(await run(bin, ["debug", "models"], { timeout: LIST_TIMEOUT_MS }));
|
|
115
|
+
const listed = (Array.isArray(catalog.models) ? catalog.models : []).filter(
|
|
116
|
+
(model) => typeof model === "object" && model !== null && typeof model.slug === "string" && typeof model.priority === "number" && model.visibility === "list"
|
|
117
|
+
);
|
|
118
|
+
listed.sort((a, b) => a.priority - b.priority);
|
|
119
|
+
return listed[0]?.slug ?? null;
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
102
124
|
|
|
103
125
|
// src/assistant.ts
|
|
104
126
|
import { spawn } from "node:child_process";
|
|
@@ -106,6 +128,8 @@ import { randomUUID } from "node:crypto";
|
|
|
106
128
|
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
107
129
|
import { createInterface } from "node:readline";
|
|
108
130
|
var TURN_TIMEOUT_MS = 10 * 6e4;
|
|
131
|
+
var RESTARTED = "the dashboard restarted while this was running";
|
|
132
|
+
var SHUT_DOWN = "the dashboard shut down";
|
|
109
133
|
var TURNS_SHOWN = 20;
|
|
110
134
|
var STEPS_KEPT = 100;
|
|
111
135
|
var DETAIL_LENGTH = 200;
|
|
@@ -191,6 +215,7 @@ function foldAssistantLog(text) {
|
|
|
191
215
|
sessionId: null,
|
|
192
216
|
request: record.request,
|
|
193
217
|
action: typeof record.action === "string" ? record.action : null,
|
|
218
|
+
auto: typeof record.auto === "string" ? record.auto : null,
|
|
194
219
|
startedAt: record.at,
|
|
195
220
|
endedAt: null,
|
|
196
221
|
status: "running",
|
|
@@ -250,7 +275,7 @@ function parseLine(line) {
|
|
|
250
275
|
}
|
|
251
276
|
}
|
|
252
277
|
var claudeAdapter = {
|
|
253
|
-
args({ prompt, sessionId, model, effort, tools, denyEdits }) {
|
|
278
|
+
args({ prompt, sessionId, model, effort, tools, denyEdits, schema }) {
|
|
254
279
|
const args = [
|
|
255
280
|
"-p",
|
|
256
281
|
"--output-format",
|
|
@@ -265,6 +290,7 @@ var claudeAdapter = {
|
|
|
265
290
|
if (tools.length > 0) args.push("--allowedTools", ...tools);
|
|
266
291
|
if (model) args.push("--model", model);
|
|
267
292
|
if (effort) args.push("--effort", effort);
|
|
293
|
+
if (schema) args.push("--json-schema", schema.json);
|
|
268
294
|
if (sessionId) args.push("--resume", sessionId);
|
|
269
295
|
args.push("--", prompt);
|
|
270
296
|
return args;
|
|
@@ -273,6 +299,10 @@ var claudeAdapter = {
|
|
|
273
299
|
const event = parseLine(line);
|
|
274
300
|
if (!event) return null;
|
|
275
301
|
const type = event.type;
|
|
302
|
+
if (type === "system" && event.subtype === "task_notification" && typeof event.tool_use_id === "string") {
|
|
303
|
+
const step = { id: event.tool_use_id, state: event.status === "completed" ? "done" : "failed", final: true };
|
|
304
|
+
return { steps: [step] };
|
|
305
|
+
}
|
|
276
306
|
if (type === "system" && typeof event.session_id === "string") {
|
|
277
307
|
return { sessionId: event.session_id };
|
|
278
308
|
}
|
|
@@ -284,7 +314,9 @@ var claudeAdapter = {
|
|
|
284
314
|
if (text) parsed.text = text;
|
|
285
315
|
const steps = content.flatMap((block) => claudeStep(block) ?? []);
|
|
286
316
|
if (steps.length) parsed.steps = steps;
|
|
287
|
-
|
|
317
|
+
if (!text && !steps.length) return null;
|
|
318
|
+
if (typeof event.parent_tool_use_id === "string") parsed.by = event.parent_tool_use_id;
|
|
319
|
+
return parsed;
|
|
288
320
|
}
|
|
289
321
|
if (type === "result") {
|
|
290
322
|
const ok = event.is_error !== true && event.subtype === "success";
|
|
@@ -297,10 +329,11 @@ var claudeAdapter = {
|
|
|
297
329
|
}
|
|
298
330
|
};
|
|
299
331
|
var codexAdapter = {
|
|
300
|
-
args({ prompt, sessionId, model, effort, drafts, codexConfig }) {
|
|
332
|
+
args({ prompt, sessionId, model, effort, drafts, codexConfig, schema }) {
|
|
301
333
|
const args = ["exec"];
|
|
302
334
|
if (sessionId) args.push("resume");
|
|
303
335
|
args.push("--json", "--skip-git-repo-check");
|
|
336
|
+
if (schema) args.push("--output-schema", schema.file);
|
|
304
337
|
const config = [
|
|
305
338
|
'sandbox_mode="workspace-write"',
|
|
306
339
|
"sandbox_workspace_write.network_access=true",
|
|
@@ -341,7 +374,9 @@ var ADAPTERS = {
|
|
|
341
374
|
claude: claudeAdapter,
|
|
342
375
|
codex: codexAdapter
|
|
343
376
|
};
|
|
344
|
-
var CLAUDE_QUIET_TOOLS = /* @__PURE__ */ new Set(["ToolSearch", "TodoWrite"]);
|
|
377
|
+
var CLAUDE_QUIET_TOOLS = /* @__PURE__ */ new Set(["ToolSearch", "TodoWrite", "StructuredOutput"]);
|
|
378
|
+
var HANDOFF = "Hand part of it to a subagent";
|
|
379
|
+
var WAIT = "Wait for its subagents";
|
|
345
380
|
function claudeStep(block) {
|
|
346
381
|
if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
|
|
347
382
|
return { id: block.tool_use_id, state: block.is_error === true ? "failed" : "done" };
|
|
@@ -366,7 +401,7 @@ function claudeStep(block) {
|
|
|
366
401
|
return step("List files", said("pattern"));
|
|
367
402
|
case "Task":
|
|
368
403
|
case "Agent":
|
|
369
|
-
return step(
|
|
404
|
+
return { ...step(HANDOFF, said("description")), handoff: true };
|
|
370
405
|
}
|
|
371
406
|
const mcp = /^mcp__(.+?)__(.+)$/.exec(block.name);
|
|
372
407
|
if (mcp) return step(connectorLabel(mcp[1], mcp[2]), compactArgs(input));
|
|
@@ -388,6 +423,7 @@ function codexStep(item, completed) {
|
|
|
388
423
|
if (item.type === "mcp_tool_call" && typeof item.server === "string" && typeof item.tool === "string") {
|
|
389
424
|
return { id: item.id, text: connectorLabel(item.server, item.tool), detail: compactArgs(item.arguments), state };
|
|
390
425
|
}
|
|
426
|
+
if (item.type === "collab_tool_call" && item.tool === "wait") return { id: item.id, text: WAIT, detail: null, state };
|
|
391
427
|
if (item.type === "web_search") {
|
|
392
428
|
const action = item.action ?? {};
|
|
393
429
|
if (action.type === "open_page") return { id: item.id, text: "Open a page", detail: shorten(action.url ?? null), state };
|
|
@@ -475,7 +511,7 @@ var Assistant = class _Assistant {
|
|
|
475
511
|
for (const turn of this.turns.values()) {
|
|
476
512
|
if (turn.status !== "running") continue;
|
|
477
513
|
turn.status = "aborted";
|
|
478
|
-
turn.error =
|
|
514
|
+
turn.error = RESTARTED;
|
|
479
515
|
turn.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
480
516
|
await this.append({
|
|
481
517
|
event: "aborted",
|
|
@@ -488,6 +524,15 @@ var Assistant = class _Assistant {
|
|
|
488
524
|
});
|
|
489
525
|
}
|
|
490
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* Every turn the dashboard started on its own, however old: what has been
|
|
529
|
+
* asked ahead is never asked again, and the panel's window of recent turns
|
|
530
|
+
* is no record of that.
|
|
531
|
+
*/
|
|
532
|
+
autoTurns() {
|
|
533
|
+
return [...this.turns.values()].filter((turn) => turn.auto !== null);
|
|
534
|
+
}
|
|
535
|
+
/** The assistant's half of the state. Asking ahead is `autoask.ts`'s to add, since it needs the board. */
|
|
491
536
|
view() {
|
|
492
537
|
const items = {};
|
|
493
538
|
for (const turn of this.turns.values()) {
|
|
@@ -520,9 +565,10 @@ ${typed}` : action.request : typed;
|
|
|
520
565
|
}
|
|
521
566
|
/**
|
|
522
567
|
* Start a turn. Returns once the process is running, not once it has answered;
|
|
523
|
-
* the answer arrives through `onChange` and the next state.
|
|
568
|
+
* the answer arrives through `onChange` and the next state. `auto` marks a
|
|
569
|
+
* turn the dashboard started on its own, with the key of what it answers.
|
|
524
570
|
*/
|
|
525
|
-
async ask(ask, context) {
|
|
571
|
+
async ask(ask, context, options = {}) {
|
|
526
572
|
const agent = this.config.assistant.agent;
|
|
527
573
|
if (!agent) throw new Error("the assistant is off; set DAILY_FOCUS_ASSISTANT to claude or codex");
|
|
528
574
|
if (this.running.has(ask.itemId)) throw new Error("the assistant is already working on this item");
|
|
@@ -539,6 +585,7 @@ ${typed}` : action.request : typed;
|
|
|
539
585
|
sessionId: null,
|
|
540
586
|
request: resolved.request,
|
|
541
587
|
action: resolved.action,
|
|
588
|
+
auto: options.auto ?? null,
|
|
542
589
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
543
590
|
endedAt: null,
|
|
544
591
|
status: "running",
|
|
@@ -554,6 +601,7 @@ ${typed}` : action.request : typed;
|
|
|
554
601
|
agent,
|
|
555
602
|
request: turn.request,
|
|
556
603
|
action: turn.action,
|
|
604
|
+
...turn.auto ? { auto: turn.auto } : {},
|
|
557
605
|
at: turn.startedAt
|
|
558
606
|
});
|
|
559
607
|
const prompt = previous ? resolved.request : composePrompt(await this.instructions(), context, resolved.request, this.config.profile);
|
|
@@ -653,7 +701,7 @@ ${typed}` : action.request : typed;
|
|
|
653
701
|
}
|
|
654
702
|
/** Kill everything, for shutdown. The exit handlers write the aborted lines. */
|
|
655
703
|
async close() {
|
|
656
|
-
for (const itemId of [...this.running.keys()]) await this.stop(itemId,
|
|
704
|
+
for (const itemId of [...this.running.keys()]) await this.stop(itemId, SHUT_DOWN);
|
|
657
705
|
}
|
|
658
706
|
async finish(turn, outcome, aborted = false) {
|
|
659
707
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -720,10 +768,268 @@ ${typed}` : action.request : typed;
|
|
|
720
768
|
}
|
|
721
769
|
};
|
|
722
770
|
|
|
771
|
+
// src/autoask.ts
|
|
772
|
+
var AUTO_ACTIONS = ["assess-review", "explain-ci"];
|
|
773
|
+
function autoKeys(row) {
|
|
774
|
+
if (row.court !== "you" || row.status !== "open") return [];
|
|
775
|
+
const keys = [];
|
|
776
|
+
const heard = row.reasons.filter((reason) => reason.kind === "changes-requested" || reason.kind === "activity").map((reason) => reason.at ?? "");
|
|
777
|
+
if (heard.length > 0) keys.push(`assess-review@${heard.sort().at(-1) || row.updatedAt}`);
|
|
778
|
+
if (row.reasons.some((reason) => reason.kind === "ci-failing")) {
|
|
779
|
+
const commit = row.headSha ?? row.lastActivityByYou ?? row.updatedAt;
|
|
780
|
+
keys.push(`explain-ci@${commit}:${[...row.failingChecks].sort().join(",")}`);
|
|
781
|
+
}
|
|
782
|
+
return keys;
|
|
783
|
+
}
|
|
784
|
+
function actionOf(key) {
|
|
785
|
+
const action = key.slice(0, key.indexOf("@"));
|
|
786
|
+
return AUTO_ACTIONS.includes(action) ? action : null;
|
|
787
|
+
}
|
|
788
|
+
var SETTLE_MS = 1e3;
|
|
789
|
+
var AutoAsk = class {
|
|
790
|
+
deps;
|
|
791
|
+
timer = null;
|
|
792
|
+
/** A look is under way: one at a time, so two can't both start a turn. */
|
|
793
|
+
looking = false;
|
|
794
|
+
/** Something changed while a look was under way, so look again after it. */
|
|
795
|
+
again = false;
|
|
796
|
+
constructor(deps) {
|
|
797
|
+
this.deps = deps;
|
|
798
|
+
}
|
|
799
|
+
get enabled() {
|
|
800
|
+
return this.deps.config.assistant.auto && this.deps.assistant.enabled;
|
|
801
|
+
}
|
|
802
|
+
/** Something changed — the board, or a turn — so look again shortly. Coalesced. */
|
|
803
|
+
poke() {
|
|
804
|
+
if (!this.enabled || this.timer) return;
|
|
805
|
+
this.timer = setTimeout(() => {
|
|
806
|
+
this.timer = null;
|
|
807
|
+
void this.consider().catch((err) => {
|
|
808
|
+
console.error(`[daily-focus] could not ask ahead: ${err.message}`);
|
|
809
|
+
});
|
|
810
|
+
}, SETTLE_MS);
|
|
811
|
+
this.timer.unref();
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Start the next question the board calls for, if one may start now. Returns
|
|
815
|
+
* its key, or null when nothing was started. Each finished turn pokes, which
|
|
816
|
+
* is what takes the queue on to the next.
|
|
817
|
+
*/
|
|
818
|
+
async consider() {
|
|
819
|
+
if (!this.enabled || !this.deps.canStart()) return null;
|
|
820
|
+
if (this.looking) {
|
|
821
|
+
this.again = true;
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
this.looking = true;
|
|
825
|
+
try {
|
|
826
|
+
return await this.look();
|
|
827
|
+
} finally {
|
|
828
|
+
this.looking = false;
|
|
829
|
+
if (this.again) {
|
|
830
|
+
this.again = false;
|
|
831
|
+
this.poke();
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
async look() {
|
|
836
|
+
const { assistant } = this.deps;
|
|
837
|
+
await assistant.start();
|
|
838
|
+
const asked = assistant.autoTurns();
|
|
839
|
+
if (asked.some((turn) => turn.status === "running")) return null;
|
|
840
|
+
if (this.usedToday(asked) >= this.deps.config.assistant.autoLimit) return null;
|
|
841
|
+
const items = assistant.view().items;
|
|
842
|
+
const cutShort = (turn) => turn.status === "aborted" && (turn.error === RESTARTED || turn.error === SHUT_DOWN);
|
|
843
|
+
const done = new Set(asked.filter((turn) => !cutShort(turn)).map((turn) => `${turn.itemId} ${turn.auto}`));
|
|
844
|
+
for (const row of await this.deps.rows()) {
|
|
845
|
+
if (items[row.id]?.running) continue;
|
|
846
|
+
for (const key of autoKeys(row)) {
|
|
847
|
+
const action = actionOf(key);
|
|
848
|
+
if (!action || done.has(`${row.id} ${key}`)) continue;
|
|
849
|
+
try {
|
|
850
|
+
await assistant.ask({ itemId: row.id, action }, await this.deps.context(row.id), { auto: key });
|
|
851
|
+
return key;
|
|
852
|
+
} catch {
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
/** Asking ahead as the page shows it, for the board's rows as they are now. */
|
|
860
|
+
view(rows) {
|
|
861
|
+
const keyed = {};
|
|
862
|
+
for (const row of rows) {
|
|
863
|
+
const keys = autoKeys(row);
|
|
864
|
+
if (keys.length > 0) keyed[row.id] = keys;
|
|
865
|
+
}
|
|
866
|
+
return {
|
|
867
|
+
enabled: this.enabled,
|
|
868
|
+
limit: this.deps.config.assistant.autoLimit,
|
|
869
|
+
usedToday: this.usedToday(this.deps.assistant.autoTurns()),
|
|
870
|
+
rows: keyed
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
close() {
|
|
874
|
+
if (this.timer) clearTimeout(this.timer);
|
|
875
|
+
this.timer = null;
|
|
876
|
+
}
|
|
877
|
+
/** Turns started on its own today, on the server's calendar, however they went. */
|
|
878
|
+
usedToday(asked) {
|
|
879
|
+
const today = localDateKey(this.deps.now?.() ?? /* @__PURE__ */ new Date());
|
|
880
|
+
return asked.filter((turn) => localDateKey(new Date(turn.startedAt)) === today).length;
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
// src/prs.ts
|
|
885
|
+
var NUDGE_AFTER_MS = 24 * 36e5;
|
|
886
|
+
var STALE_DRAFT_AFTER_MS = 14 * 864e5;
|
|
887
|
+
var COURT_ORDER = ["you", "ready", "reviewers", "gate", "blocked", "checks", "draft"];
|
|
888
|
+
function courtOrder(court) {
|
|
889
|
+
return COURT_ORDER.indexOf(court);
|
|
890
|
+
}
|
|
891
|
+
function ms(iso) {
|
|
892
|
+
const parsed = parseISO(iso);
|
|
893
|
+
return parsed ? parsed.getTime() : null;
|
|
894
|
+
}
|
|
895
|
+
function deriveDecision(pr) {
|
|
896
|
+
if (pr.reviewDecision) return pr.reviewDecision;
|
|
897
|
+
if (pr.reviews.some((review) => review.state === "CHANGES_REQUESTED")) return "CHANGES_REQUESTED";
|
|
898
|
+
if (pr.reviews.some((review) => review.state === "APPROVED")) return "APPROVED";
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
function matchMergeGates(pr, gates) {
|
|
902
|
+
if (gates.length === 0) return [];
|
|
903
|
+
return (pr.pendingChecks ?? []).filter((check) => gates.includes(check.name));
|
|
904
|
+
}
|
|
905
|
+
function newest(pr, state) {
|
|
906
|
+
return pr.reviews.filter((review) => review.state === state).sort((a, b) => a.at < b.at ? 1 : -1)[0];
|
|
907
|
+
}
|
|
908
|
+
function judge(pr, now, nudgedAt, gates = []) {
|
|
909
|
+
const decision = deriveDecision(pr);
|
|
910
|
+
const you = pr.lastActivityByYou ?? null;
|
|
911
|
+
const others = pr.lastActivityByOthers ?? null;
|
|
912
|
+
const mergeState = pr.mergeStateStatus ?? null;
|
|
913
|
+
if (pr.isDraft) {
|
|
914
|
+
const touched = you ?? pr.createdAt;
|
|
915
|
+
const idle = now.getTime() - (ms(touched) ?? now.getTime());
|
|
916
|
+
return { court: "draft", decision, reasons: [], stale: idle >= STALE_DRAFT_AFTER_MS, nudge: false, since: touched };
|
|
917
|
+
}
|
|
918
|
+
const reasons = [];
|
|
919
|
+
if (decision === "CHANGES_REQUESTED") {
|
|
920
|
+
const request = newest(pr, "CHANGES_REQUESTED");
|
|
921
|
+
reasons.push({ kind: "changes-requested", login: request?.login, at: request?.at });
|
|
922
|
+
}
|
|
923
|
+
if (pr.checks === "failure") reasons.push({ kind: "ci-failing" });
|
|
924
|
+
if (pr.mergeable === "CONFLICTING" || mergeState === "DIRTY") reasons.push({ kind: "conflicts" });
|
|
925
|
+
if (mergeState === "BEHIND") reasons.push({ kind: "behind" });
|
|
926
|
+
const theirMove = others !== null && (you === null || others.at > you);
|
|
927
|
+
const approval = newest(pr, "APPROVED");
|
|
928
|
+
const afterApproval = others !== null && approval !== void 0 && others.at > approval.at;
|
|
929
|
+
const approved = decision === "APPROVED" && !afterApproval;
|
|
930
|
+
if (theirMove && others && !approved) {
|
|
931
|
+
reasons.push({ kind: "activity", login: others.login, at: others.at, activity: others.kind });
|
|
932
|
+
}
|
|
933
|
+
if (reasons.length > 0) {
|
|
934
|
+
const since = theirMove && others ? others.at : pr.updatedAt;
|
|
935
|
+
return { court: "you", decision, reasons, stale: false, nudge: false, since };
|
|
936
|
+
}
|
|
937
|
+
const waitingSince = laterISO(pr.readyAt, you) ?? pr.createdAt;
|
|
938
|
+
const settled = (court, reason) => ({
|
|
939
|
+
court,
|
|
940
|
+
decision,
|
|
941
|
+
reasons: [reason],
|
|
942
|
+
stale: false,
|
|
943
|
+
nudge: false,
|
|
944
|
+
since: waitingSince
|
|
945
|
+
});
|
|
946
|
+
if (decision !== "REVIEW_REQUIRED") {
|
|
947
|
+
const gate = matchMergeGates(pr, gates);
|
|
948
|
+
if (gate.length > 0) return settled("gate", { kind: "merge-gate", checks: gate });
|
|
949
|
+
const pending = pr.pendingChecks ?? [];
|
|
950
|
+
const running = { kind: "checks-pending" };
|
|
951
|
+
if (pending.length > 0) running.checks = pending;
|
|
952
|
+
if (mergeState === "BLOCKED" && pending.length === 0) {
|
|
953
|
+
return settled("blocked", { kind: "merge-blocked" });
|
|
954
|
+
}
|
|
955
|
+
if (mergeState === "BLOCKED" || mergeState === "UNSTABLE") return settled("checks", running);
|
|
956
|
+
if (mergeState === "UNKNOWN") return settled("checks", { kind: "mergeability-unknown" });
|
|
957
|
+
if (approved) {
|
|
958
|
+
if (mergeState === "CLEAN" || mergeState === "HAS_HOOKS") {
|
|
959
|
+
return { court: "ready", decision, reasons: [], stale: false, nudge: false, since: approval?.at ?? pr.updatedAt };
|
|
960
|
+
}
|
|
961
|
+
if (mergeState === null) {
|
|
962
|
+
const unfinished = pr.checks === "pending" || pending.length > 0;
|
|
963
|
+
if (!unfinished) {
|
|
964
|
+
return { court: "ready", decision, reasons: [], stale: false, nudge: false, since: approval?.at ?? pr.updatedAt };
|
|
965
|
+
}
|
|
966
|
+
return settled("checks", running);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
const quietSince = laterISO(waitingSince, nudgedAt) ?? waitingSince;
|
|
971
|
+
const waited = now.getTime() - (ms(quietSince) ?? now.getTime());
|
|
972
|
+
const verdict = { court: "reviewers", decision, reasons, stale: false, nudge: waited >= NUDGE_AFTER_MS, since: waitingSince };
|
|
973
|
+
const nudged = ms(nudgedAt);
|
|
974
|
+
if (nudgedAt && nudged !== null && quietSince === nudgedAt && !verdict.nudge) {
|
|
975
|
+
verdict.nudgedAt = nudgedAt;
|
|
976
|
+
verdict.nudgedUntil = new Date(nudged + NUDGE_AFTER_MS).toISOString();
|
|
977
|
+
}
|
|
978
|
+
return verdict;
|
|
979
|
+
}
|
|
980
|
+
function resolveBoard(pulls, actions, now, gates = []) {
|
|
981
|
+
const folded = foldActionLog(actions, now);
|
|
982
|
+
const rows = pulls.map((pr) => {
|
|
983
|
+
const log = folded.get(canonicalId(pr.id));
|
|
984
|
+
const verdict = judge(pr, now, log?.nudgedAt ?? null, gates);
|
|
985
|
+
const row = { ...pr, ...verdict, status: log?.status === "snoozed" ? "snoozed" : "open", notes: log?.notes ?? [] };
|
|
986
|
+
if (row.status === "snoozed" && log?.snoozedUntil) row.snoozedUntil = log.snoozedUntil;
|
|
987
|
+
return row;
|
|
988
|
+
});
|
|
989
|
+
return rows.sort((a, b) => {
|
|
990
|
+
const court = courtOrder(a.court) - courtOrder(b.court);
|
|
991
|
+
if (court !== 0) return court;
|
|
992
|
+
if (a.since !== b.since) return a.since < b.since ? -1 : 1;
|
|
993
|
+
return a.id < b.id ? -1 : 1;
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
function countBoard(rows) {
|
|
997
|
+
const counts = {
|
|
998
|
+
you: 0,
|
|
999
|
+
ready: 0,
|
|
1000
|
+
reviewers: 0,
|
|
1001
|
+
gate: 0,
|
|
1002
|
+
blocked: 0,
|
|
1003
|
+
checks: 0,
|
|
1004
|
+
draft: 0,
|
|
1005
|
+
parked: 0
|
|
1006
|
+
};
|
|
1007
|
+
for (const row of rows) {
|
|
1008
|
+
if (row.status === "snoozed") counts.parked++;
|
|
1009
|
+
else counts[row.court]++;
|
|
1010
|
+
}
|
|
1011
|
+
return counts;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
723
1014
|
export {
|
|
1015
|
+
computerUseOff,
|
|
1016
|
+
refusalFor,
|
|
724
1017
|
CodexGate,
|
|
725
1018
|
codexVersionWarning,
|
|
1019
|
+
codexLatestModel,
|
|
726
1020
|
QUICK_ACTIONS,
|
|
727
1021
|
ADAPTERS,
|
|
728
|
-
|
|
1022
|
+
HANDOFF,
|
|
1023
|
+
WAIT,
|
|
1024
|
+
unwrapShell,
|
|
1025
|
+
connectorLabel,
|
|
1026
|
+
compactArgs,
|
|
1027
|
+
shorten,
|
|
1028
|
+
Assistant,
|
|
1029
|
+
autoKeys,
|
|
1030
|
+
actionOf,
|
|
1031
|
+
AutoAsk,
|
|
1032
|
+
judge,
|
|
1033
|
+
resolveBoard,
|
|
1034
|
+
countBoard
|
|
729
1035
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// src/fs.ts
|
|
2
|
+
import { mkdir, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
async function writeJsonAtomic(path, value) {
|
|
5
|
+
await mkdir(dirname(path), { recursive: true });
|
|
6
|
+
const temp = tempPathFor(path);
|
|
7
|
+
await writeFile(temp, `${JSON.stringify(value, null, 2)}
|
|
8
|
+
`, "utf8");
|
|
9
|
+
await rename(temp, path);
|
|
10
|
+
}
|
|
11
|
+
function tempPathFor(path) {
|
|
12
|
+
return `${path}.tmp`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
writeJsonAtomic,
|
|
17
|
+
tempPathFor
|
|
18
|
+
};
|