daily-focus 1.2.0-dev.8 → 2.0.0-dev.14

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.
@@ -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,7 +128,11 @@ 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;
134
+ var STEPS_KEPT = 100;
135
+ var DETAIL_LENGTH = 200;
110
136
  var QUICK_ACTIONS = [
111
137
  {
112
138
  id: "assess-review",
@@ -189,10 +215,12 @@ function foldAssistantLog(text) {
189
215
  sessionId: null,
190
216
  request: record.request,
191
217
  action: typeof record.action === "string" ? record.action : null,
218
+ auto: typeof record.auto === "string" ? record.auto : null,
192
219
  startedAt: record.at,
193
220
  endedAt: null,
194
221
  status: "running",
195
222
  reply: "",
223
+ steps: [],
196
224
  error: null
197
225
  });
198
226
  continue;
@@ -201,6 +229,7 @@ function foldAssistantLog(text) {
201
229
  if (!turn) continue;
202
230
  turn.sessionId = typeof record.sessionId === "string" ? record.sessionId : turn.sessionId;
203
231
  turn.reply = typeof record.reply === "string" ? record.reply : turn.reply;
232
+ turn.steps = readSteps(record.steps);
204
233
  turn.endedAt = record.at;
205
234
  if (record.event === "finished") {
206
235
  turn.status = "done";
@@ -214,6 +243,27 @@ function foldAssistantLog(text) {
214
243
  }
215
244
  return turns;
216
245
  }
246
+ function addStep(turn, step) {
247
+ turn.steps.push(step);
248
+ if (turn.steps.length > STEPS_KEPT) turn.steps.shift();
249
+ }
250
+ function sameWords(a, b) {
251
+ return a.replace(/\s+/g, " ").trim() === b.replace(/\s+/g, " ").trim();
252
+ }
253
+ function readSteps(value) {
254
+ if (!Array.isArray(value)) return [];
255
+ const steps = [];
256
+ for (const candidate of value) {
257
+ if (typeof candidate !== "object" || candidate === null || typeof candidate.text !== "string") continue;
258
+ steps.push({
259
+ kind: candidate.kind === "message" ? "message" : "tool",
260
+ text: candidate.text,
261
+ detail: typeof candidate.detail === "string" ? candidate.detail : null,
262
+ state: candidate.state === "failed" ? "failed" : "done"
263
+ });
264
+ }
265
+ return steps;
266
+ }
217
267
  function parseLine(line) {
218
268
  const trimmed = line.trim();
219
269
  if (!trimmed.startsWith("{")) return null;
@@ -225,7 +275,7 @@ function parseLine(line) {
225
275
  }
226
276
  }
227
277
  var claudeAdapter = {
228
- args({ prompt, sessionId, model, effort, tools, denyEdits }) {
278
+ args({ prompt, sessionId, model, effort, tools, denyEdits, schema }) {
229
279
  const args = [
230
280
  "-p",
231
281
  "--output-format",
@@ -240,6 +290,7 @@ var claudeAdapter = {
240
290
  if (tools.length > 0) args.push("--allowedTools", ...tools);
241
291
  if (model) args.push("--model", model);
242
292
  if (effort) args.push("--effort", effort);
293
+ if (schema) args.push("--json-schema", schema.json);
243
294
  if (sessionId) args.push("--resume", sessionId);
244
295
  args.push("--", prompt);
245
296
  return args;
@@ -248,14 +299,24 @@ var claudeAdapter = {
248
299
  const event = parseLine(line);
249
300
  if (!event) return null;
250
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
+ }
251
306
  if (type === "system" && typeof event.session_id === "string") {
252
307
  return { sessionId: event.session_id };
253
308
  }
254
- if (type === "assistant") {
309
+ if (type === "assistant" || type === "user") {
255
310
  const message = event.message;
256
311
  const content = Array.isArray(message?.content) ? message.content : [];
257
- const text = content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n\n");
258
- return text ? { text } : null;
312
+ const parsed = {};
313
+ const text = type === "assistant" ? content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n\n") : "";
314
+ if (text) parsed.text = text;
315
+ const steps = content.flatMap((block) => claudeStep(block) ?? []);
316
+ if (steps.length) parsed.steps = steps;
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;
259
320
  }
260
321
  if (type === "result") {
261
322
  const ok = event.is_error !== true && event.subtype === "success";
@@ -268,10 +329,11 @@ var claudeAdapter = {
268
329
  }
269
330
  };
270
331
  var codexAdapter = {
271
- args({ prompt, sessionId, model, effort, drafts, codexConfig }) {
332
+ args({ prompt, sessionId, model, effort, drafts, codexConfig, schema }) {
272
333
  const args = ["exec"];
273
334
  if (sessionId) args.push("resume");
274
335
  args.push("--json", "--skip-git-repo-check");
336
+ if (schema) args.push("--output-schema", schema.file);
275
337
  const config = [
276
338
  'sandbox_mode="workspace-write"',
277
339
  "sandbox_workspace_write.network_access=true",
@@ -293,10 +355,11 @@ var codexAdapter = {
293
355
  if (type === "thread.started" && typeof event.thread_id === "string") {
294
356
  return { sessionId: event.thread_id };
295
357
  }
296
- if (type === "item.completed") {
358
+ if (type === "item.started" || type === "item.completed") {
297
359
  const item = event.item;
298
- if (item?.type === "agent_message" && typeof item.text === "string") return { text: item.text };
299
- return null;
360
+ if (type === "item.completed" && item?.type === "agent_message" && typeof item.text === "string") return { text: item.text };
361
+ const step = item ? codexStep(item, type === "item.completed") : null;
362
+ return step ? { steps: [step] } : null;
300
363
  }
301
364
  if (type === "turn.completed") return { done: { ok: true } };
302
365
  if (type === "turn.failed" || type === "error") {
@@ -311,6 +374,90 @@ var ADAPTERS = {
311
374
  claude: claudeAdapter,
312
375
  codex: codexAdapter
313
376
  };
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";
380
+ function claudeStep(block) {
381
+ if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
382
+ return { id: block.tool_use_id, state: block.is_error === true ? "failed" : "done" };
383
+ }
384
+ if (block.type !== "tool_use" || typeof block.id !== "string" || typeof block.name !== "string") return null;
385
+ if (CLAUDE_QUIET_TOOLS.has(block.name)) return null;
386
+ const input = block.input ?? {};
387
+ const said = (key) => typeof input[key] === "string" && input[key] !== "" ? input[key] : null;
388
+ const step = (text, detail) => ({ id: block.id, text, detail: shorten(detail), state: "running" });
389
+ switch (block.name) {
390
+ case "Bash":
391
+ return step(said("description") ?? "Run a command", said("command"));
392
+ case "WebFetch":
393
+ return step("Fetch a page", said("url"));
394
+ case "WebSearch":
395
+ return step("Search the web", said("query"));
396
+ case "Read":
397
+ return step("Read a file", said("file_path"));
398
+ case "Grep":
399
+ return step("Search files", said("pattern"));
400
+ case "Glob":
401
+ return step("List files", said("pattern"));
402
+ case "Task":
403
+ case "Agent":
404
+ return { ...step(HANDOFF, said("description")), handoff: true };
405
+ }
406
+ const mcp = /^mcp__(.+?)__(.+)$/.exec(block.name);
407
+ if (mcp) return step(connectorLabel(mcp[1], mcp[2]), compactArgs(input));
408
+ return step(block.name, compactArgs(input));
409
+ }
410
+ function codexStep(item, completed) {
411
+ if (typeof item.id !== "string") return null;
412
+ const failed = item.status === "failed" || item.error !== void 0 && item.error !== null;
413
+ const state = completed ? failed ? "failed" : "done" : "running";
414
+ if (item.type === "command_execution") {
415
+ const exited = typeof item.exit_code === "number" && item.exit_code !== 0;
416
+ return {
417
+ id: item.id,
418
+ text: "Run a command",
419
+ detail: shorten(unwrapShell(item.command ?? "")),
420
+ state: completed && exited ? "failed" : state
421
+ };
422
+ }
423
+ if (item.type === "mcp_tool_call" && typeof item.server === "string" && typeof item.tool === "string") {
424
+ return { id: item.id, text: connectorLabel(item.server, item.tool), detail: compactArgs(item.arguments), state };
425
+ }
426
+ if (item.type === "collab_tool_call" && item.tool === "wait") return { id: item.id, text: WAIT, detail: null, state };
427
+ if (item.type === "web_search") {
428
+ const action = item.action ?? {};
429
+ if (action.type === "open_page") return { id: item.id, text: "Open a page", detail: shorten(action.url ?? null), state };
430
+ if (action.type === "find_in_page") return { id: item.id, text: "Find in a page", detail: shorten(action.pattern ?? action.url ?? null), state };
431
+ return { id: item.id, text: "Search the web", detail: shorten(action.query || item.query || null), state };
432
+ }
433
+ return null;
434
+ }
435
+ function unwrapShell(command) {
436
+ const wrapped = /^(?:\S*\/)?(?:ba|z)?sh\s+-l?c\s+(['"])([\s\S]*)\1$/.exec(command.trim());
437
+ if (!wrapped) return command.trim();
438
+ return wrapped[1] === "'" ? wrapped[2].replaceAll(`'\\''`, `'`) : wrapped[2];
439
+ }
440
+ function connectorLabel(server, tool) {
441
+ const words = (name) => name.replace(/[_-]+/g, " ").trim();
442
+ if (server === "codex_apps") {
443
+ const said = words(tool);
444
+ return said.charAt(0).toUpperCase() + said.slice(1);
445
+ }
446
+ return `${words(server.replace(/^claude_ai_/, ""))}: ${words(tool)}`;
447
+ }
448
+ function compactArgs(input) {
449
+ if (input === null || input === void 0) return null;
450
+ if (typeof input === "object" && Object.keys(input).length === 0) return null;
451
+ return shorten(typeof input === "string" ? input : JSON.stringify(input));
452
+ }
453
+ function shorten(value) {
454
+ if (!value) return null;
455
+ const lines = value.trim().split("\n");
456
+ const first = lines[0].trim();
457
+ const more = lines.length > 1;
458
+ if (first.length > DETAIL_LENGTH) return `${first.slice(0, DETAIL_LENGTH - 1)}\u2026`;
459
+ return more ? `${first} \u2026` : first;
460
+ }
314
461
  function composePrompt(instructions, context, request, profile) {
315
462
  const now = /* @__PURE__ */ new Date();
316
463
  const facts = [`- Profile: ${profile}`, `- Today: ${now.toISOString().slice(0, 10)}`];
@@ -364,7 +511,7 @@ var Assistant = class _Assistant {
364
511
  for (const turn of this.turns.values()) {
365
512
  if (turn.status !== "running") continue;
366
513
  turn.status = "aborted";
367
- turn.error = "the dashboard restarted while this was running";
514
+ turn.error = RESTARTED;
368
515
  turn.endedAt = (/* @__PURE__ */ new Date()).toISOString();
369
516
  await this.append({
370
517
  event: "aborted",
@@ -372,10 +519,20 @@ var Assistant = class _Assistant {
372
519
  sessionId: turn.sessionId,
373
520
  reason: turn.error,
374
521
  reply: turn.reply,
522
+ steps: turn.steps,
375
523
  at: turn.endedAt
376
524
  });
377
525
  }
378
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. */
379
536
  view() {
380
537
  const items = {};
381
538
  for (const turn of this.turns.values()) {
@@ -408,9 +565,10 @@ ${typed}` : action.request : typed;
408
565
  }
409
566
  /**
410
567
  * Start a turn. Returns once the process is running, not once it has answered;
411
- * 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.
412
570
  */
413
- async ask(ask, context) {
571
+ async ask(ask, context, options = {}) {
414
572
  const agent = this.config.assistant.agent;
415
573
  if (!agent) throw new Error("the assistant is off; set DAILY_FOCUS_ASSISTANT to claude or codex");
416
574
  if (this.running.has(ask.itemId)) throw new Error("the assistant is already working on this item");
@@ -427,10 +585,12 @@ ${typed}` : action.request : typed;
427
585
  sessionId: null,
428
586
  request: resolved.request,
429
587
  action: resolved.action,
588
+ auto: options.auto ?? null,
430
589
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
431
590
  endedAt: null,
432
591
  status: "running",
433
592
  reply: "",
593
+ steps: [],
434
594
  error: null
435
595
  };
436
596
  this.turns.set(turn.id, turn);
@@ -441,6 +601,7 @@ ${typed}` : action.request : typed;
441
601
  agent,
442
602
  request: turn.request,
443
603
  action: turn.action,
604
+ ...turn.auto ? { auto: turn.auto } : {},
444
605
  at: turn.startedAt
445
606
  });
446
607
  const prompt = previous ? resolved.request : composePrompt(await this.instructions(), context, resolved.request, this.config.profile);
@@ -492,18 +653,27 @@ ${typed}` : action.request : typed;
492
653
  stderr.push(chunk.toString("utf8"));
493
654
  if (stderr.length > 50) stderr.shift();
494
655
  });
656
+ const calls = /* @__PURE__ */ new Map();
495
657
  if (child.stdout) {
496
658
  const lines = createInterface({ input: child.stdout });
497
659
  lines.on("line", (line) => {
498
660
  const event = adapter.parse(line);
499
661
  if (!event) return;
500
662
  if (event.sessionId) turn.sessionId = event.sessionId;
501
- if (event.text) {
502
- turn.reply = turn.reply ? `${turn.reply}
503
-
504
- ${event.text}` : event.text;
505
- this.hooks.onChange();
663
+ if (event.text) addStep(turn, { kind: "message", text: event.text, detail: null, state: "done" });
664
+ for (const parsed of event.steps ?? []) {
665
+ const known = calls.get(parsed.id);
666
+ if (known) {
667
+ if (parsed.text) known.text = parsed.text;
668
+ if (parsed.detail) known.detail = parsed.detail;
669
+ known.state = parsed.state;
670
+ } else if (parsed.text) {
671
+ const step = { kind: "tool", text: parsed.text, detail: parsed.detail ?? null, state: parsed.state };
672
+ calls.set(parsed.id, step);
673
+ addStep(turn, step);
674
+ }
506
675
  }
676
+ if (event.text || event.steps) this.hooks.onChange();
507
677
  if (event.done) done = event.done;
508
678
  });
509
679
  }
@@ -531,24 +701,28 @@ ${event.text}` : event.text;
531
701
  }
532
702
  /** Kill everything, for shutdown. The exit handlers write the aborted lines. */
533
703
  async close() {
534
- for (const itemId of [...this.running.keys()]) await this.stop(itemId, "the dashboard shut down");
704
+ for (const itemId of [...this.running.keys()]) await this.stop(itemId, SHUT_DOWN);
535
705
  }
536
706
  async finish(turn, outcome, aborted = false) {
537
707
  const at = (/* @__PURE__ */ new Date()).toISOString();
538
- const reply = outcome.reply ?? turn.reply;
708
+ const last = turn.steps.findLast((step) => step.kind === "message");
709
+ const reply = outcome.ok ? outcome.reply ?? last?.text ?? "" : "";
710
+ if (outcome.ok && last && sameWords(last.text, reply)) turn.steps.splice(turn.steps.lastIndexOf(last), 1);
711
+ for (const step of turn.steps) if (step.state === "running") step.state = outcome.ok ? "done" : "failed";
539
712
  turn.reply = reply;
540
713
  turn.endedAt = at;
714
+ const steps = turn.steps;
541
715
  if (outcome.ok) {
542
716
  turn.status = "done";
543
- await this.append({ event: "finished", turn: turn.id, sessionId: turn.sessionId, reply, at });
717
+ await this.append({ event: "finished", turn: turn.id, sessionId: turn.sessionId, reply, steps, at });
544
718
  } else if (aborted) {
545
719
  turn.status = "aborted";
546
720
  turn.error = outcome.error ?? "stopped";
547
- await this.append({ event: "aborted", turn: turn.id, sessionId: turn.sessionId, reason: turn.error, reply, at });
721
+ await this.append({ event: "aborted", turn: turn.id, sessionId: turn.sessionId, reason: turn.error, reply, steps, at });
548
722
  } else {
549
723
  turn.status = "failed";
550
724
  turn.error = outcome.error ?? "failed";
551
- await this.append({ event: "failed", turn: turn.id, sessionId: turn.sessionId, error: turn.error, reply, at });
725
+ await this.append({ event: "failed", turn: turn.id, sessionId: turn.sessionId, error: turn.error, reply, steps, at });
552
726
  }
553
727
  this.hooks.onChange();
554
728
  }
@@ -594,10 +768,268 @@ ${event.text}` : event.text;
594
768
  }
595
769
  };
596
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
+
597
1014
  export {
1015
+ computerUseOff,
1016
+ refusalFor,
598
1017
  CodexGate,
599
1018
  codexVersionWarning,
1019
+ codexLatestModel,
600
1020
  QUICK_ACTIONS,
601
1021
  ADAPTERS,
602
- Assistant
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
603
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
+ };