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.
- package/README.md +25 -7
- 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-SX5FPS3A.js → chunk-JTMBBUMD.js} +454 -22
- 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-6Q22AAC7.js → demo-KE6WDWMA.js} +194 -40
- package/dist/{init-XDMAXU2S.js → init-5KJVHWJU.js} +41 -6
- package/dist/{seed-TWT7EDXJ.js → seed-T4VEY6DT.js} +4 -3
- package/dist/{server-PIYOIUKJ.js → server-FFRNYEAO.js} +2000 -743
- package/dist/{service-ZUHI3VKH.js → service-LQ46YRCZ.js} +7 -6
- package/package.json +2 -2
- package/prompts/README.md +20 -4
- package/prompts/assistant.md +14 -3
- 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 +976 -381
- package/public/style.css +821 -100
- 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,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
|
|
258
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
+
};
|