daily-focus 1.2.0-dev.12 → 1.2.0-dev.13
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 +6 -1
- package/SETUP.md +9 -2
- package/dist/{chunk-FPBY3TAW.js → chunk-6COKG3VL.js} +17 -2
- package/dist/cli.js +2 -2
- package/dist/{demo-UKILSZYF.js → demo-AJWHY6KY.js} +69 -2
- package/dist/{server-RKEB6FDK.js → server-VPSSCDBW.js} +296 -27
- package/package.json +1 -1
- package/prompts/README.md +7 -0
- package/prompts/morning-brief-personal.md +27 -0
- package/prompts/morning-brief-work.md +27 -0
- package/public/app.js +89 -1
- package/public/style.css +58 -0
package/README.md
CHANGED
|
@@ -163,8 +163,13 @@ in `DAILY_FOCUS_AGENT_AT`, and from the refresh icon beside the brief's age when
|
|
|
163
163
|
morning's brief is out of date by lunch. Each run keeps what you have marked done,
|
|
164
164
|
snoozed or noted.
|
|
165
165
|
|
|
166
|
+
The agent reads your sources in parallel, handing each to a subagent of its own.
|
|
167
|
+
While it runs, its panel shows what it is doing: what it says, each command it runs
|
|
168
|
+
and each thread or page it reads, and each subagent under the step that started it,
|
|
169
|
+
open while it works.
|
|
170
|
+
|
|
166
171
|
Every run is recorded. Click "updated … ago" to open the latest: its report says what
|
|
167
|
-
it wrote and what it could not reach,
|
|
172
|
+
it wrote and what it could not reach, how it got there is folded away beneath,
|
|
168
173
|
and you can ask it why it did what it did, or to look again, in the same session. Earlier runs are folded away below. See
|
|
169
174
|
[SETUP.md](SETUP.md#connect-your-morning-agent).
|
|
170
175
|
|
package/SETUP.md
CHANGED
|
@@ -414,10 +414,17 @@ in the archive. While it runs, a banner says so and offers Stop; click the banne
|
|
|
414
414
|
to watch the run. When it finishes the banner goes, since the brief it wrote is the
|
|
415
415
|
page. A run that fails stays as a warning until you dismiss it.
|
|
416
416
|
|
|
417
|
+
The agent reads your sources in parallel, each handed to a subagent of its own, and
|
|
418
|
+
the panel shows what it is doing as it goes: what it says, each command it runs and
|
|
419
|
+
each thing it reads, and each subagent under the step that started it, open while
|
|
420
|
+
it works. Claude Code reports its subagents itself. Codex doesn't, so for a Codex
|
|
421
|
+
run the dashboard reads them from Codex's own session files, under `~/.codex/sessions`
|
|
422
|
+
or `$CODEX_HOME`. It only ever reads them.
|
|
423
|
+
|
|
417
424
|
Click "updated … ago" to open the latest run in the panel. It holds the agent's
|
|
418
425
|
report — what it wrote, what it dropped as handled, and any source it could not
|
|
419
|
-
reach — with everything it said on the way there folded away beneath, and a
|
|
420
|
-
to ask it about the run. The question goes to the agent in the
|
|
426
|
+
reach — with everything it said and did on the way there folded away beneath, and a
|
|
427
|
+
field to ask it about the run. The question goes to the agent in the
|
|
421
428
|
same session, so it answers from what it actually did: why it dropped an item, what
|
|
422
429
|
it could not find, whether it can find it now. A follow-up is given nothing to
|
|
423
430
|
write; if something in the brief should change, it says what, and you run it again.
|
|
@@ -299,6 +299,10 @@ var claudeAdapter = {
|
|
|
299
299
|
const event = parseLine(line);
|
|
300
300
|
if (!event) return null;
|
|
301
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
|
+
}
|
|
302
306
|
if (type === "system" && typeof event.session_id === "string") {
|
|
303
307
|
return { sessionId: event.session_id };
|
|
304
308
|
}
|
|
@@ -310,7 +314,9 @@ var claudeAdapter = {
|
|
|
310
314
|
if (text) parsed.text = text;
|
|
311
315
|
const steps = content.flatMap((block) => claudeStep(block) ?? []);
|
|
312
316
|
if (steps.length) parsed.steps = steps;
|
|
313
|
-
|
|
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;
|
|
314
320
|
}
|
|
315
321
|
if (type === "result") {
|
|
316
322
|
const ok = event.is_error !== true && event.subtype === "success";
|
|
@@ -369,6 +375,8 @@ var ADAPTERS = {
|
|
|
369
375
|
codex: codexAdapter
|
|
370
376
|
};
|
|
371
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";
|
|
372
380
|
function claudeStep(block) {
|
|
373
381
|
if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
|
|
374
382
|
return { id: block.tool_use_id, state: block.is_error === true ? "failed" : "done" };
|
|
@@ -393,7 +401,7 @@ function claudeStep(block) {
|
|
|
393
401
|
return step("List files", said("pattern"));
|
|
394
402
|
case "Task":
|
|
395
403
|
case "Agent":
|
|
396
|
-
return step(
|
|
404
|
+
return { ...step(HANDOFF, said("description")), handoff: true };
|
|
397
405
|
}
|
|
398
406
|
const mcp = /^mcp__(.+?)__(.+)$/.exec(block.name);
|
|
399
407
|
if (mcp) return step(connectorLabel(mcp[1], mcp[2]), compactArgs(input));
|
|
@@ -415,6 +423,7 @@ function codexStep(item, completed) {
|
|
|
415
423
|
if (item.type === "mcp_tool_call" && typeof item.server === "string" && typeof item.tool === "string") {
|
|
416
424
|
return { id: item.id, text: connectorLabel(item.server, item.tool), detail: compactArgs(item.arguments), state };
|
|
417
425
|
}
|
|
426
|
+
if (item.type === "collab_tool_call" && item.tool === "wait") return { id: item.id, text: WAIT, detail: null, state };
|
|
418
427
|
if (item.type === "web_search") {
|
|
419
428
|
const action = item.action ?? {};
|
|
420
429
|
if (action.type === "open_page") return { id: item.id, text: "Open a page", detail: shorten(action.url ?? null), state };
|
|
@@ -1010,6 +1019,12 @@ export {
|
|
|
1010
1019
|
codexLatestModel,
|
|
1011
1020
|
QUICK_ACTIONS,
|
|
1012
1021
|
ADAPTERS,
|
|
1022
|
+
HANDOFF,
|
|
1023
|
+
WAIT,
|
|
1024
|
+
unwrapShell,
|
|
1025
|
+
connectorLabel,
|
|
1026
|
+
compactArgs,
|
|
1027
|
+
shorten,
|
|
1013
1028
|
Assistant,
|
|
1014
1029
|
autoKeys,
|
|
1015
1030
|
actionOf,
|
package/dist/cli.js
CHANGED
|
@@ -49,7 +49,7 @@ function shouldOpen(flag) {
|
|
|
49
49
|
return flag && process.stdout.isTTY === true && !process.env.CI;
|
|
50
50
|
}
|
|
51
51
|
async function demo(open, profile) {
|
|
52
|
-
const { demoEnv, demoIntegrations, demoProfile, writeDemoStore } = await import("./demo-
|
|
52
|
+
const { demoEnv, demoIntegrations, demoProfile, writeDemoStore } = await import("./demo-AJWHY6KY.js");
|
|
53
53
|
const chosen = demoProfile(profile);
|
|
54
54
|
const store = await mkdtemp(join(tmpdir(), "daily-focus-demo-"));
|
|
55
55
|
process.on("exit", () => rmSync(store, { recursive: true, force: true }));
|
|
@@ -66,7 +66,7 @@ async function demo(open, profile) {
|
|
|
66
66
|
);
|
|
67
67
|
}
|
|
68
68
|
async function start(open, onStarted, options = {}) {
|
|
69
|
-
const { runServer } = await import("./server-
|
|
69
|
+
const { runServer } = await import("./server-VPSSCDBW.js");
|
|
70
70
|
await runServer({
|
|
71
71
|
...options,
|
|
72
72
|
onStarted: (url) => {
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
actionOf,
|
|
4
4
|
autoKeys,
|
|
5
5
|
judge
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-6COKG3VL.js";
|
|
7
7
|
import {
|
|
8
8
|
dateFromNow,
|
|
9
9
|
sampleBrief,
|
|
@@ -619,7 +619,7 @@ function runLines(seed) {
|
|
|
619
619
|
return lines2;
|
|
620
620
|
}
|
|
621
621
|
const report = seed.messages.at(-1) ?? "";
|
|
622
|
-
lines2.push({ event: "finished", run: seed.id, sessionId: `sample-${seed.id}`, report, messages: seed.messages, at: endedAt });
|
|
622
|
+
lines2.push({ event: "finished", run: seed.id, sessionId: `sample-${seed.id}`, report, messages: seed.messages, steps: seed.steps ?? [], at: endedAt });
|
|
623
623
|
if (seed.question) {
|
|
624
624
|
const turn = `${seed.id}-q1`;
|
|
625
625
|
const replied = new Date(new Date(seed.question.at).getTime() + 48e3).toISOString();
|
|
@@ -741,8 +741,75 @@ function demoAgentLog(profile, now) {
|
|
|
741
741
|
}
|
|
742
742
|
}
|
|
743
743
|
];
|
|
744
|
+
const newest = runs.find((run) => run.id === "run-0");
|
|
745
|
+
newest.steps = demoRunSteps(profile, newest.messages);
|
|
744
746
|
return runs.flatMap(runLines);
|
|
745
747
|
}
|
|
748
|
+
function demoRunSteps(profile, messages) {
|
|
749
|
+
const said2 = (text, by = null) => ({ kind: "message", text, detail: null, state: "done", by });
|
|
750
|
+
const call2 = (text, detail, by = null) => ({ kind: "tool", text, detail, state: "done", by });
|
|
751
|
+
const handoff = (name) => ({ ...call2("Hand part of it to a subagent", name), subagent: name });
|
|
752
|
+
const wait = call2("Wait for its subagents", null);
|
|
753
|
+
const start = [
|
|
754
|
+
said2(messages[0]),
|
|
755
|
+
call2("Run a command", "cat prompt.md focus.md sources.md items.json"),
|
|
756
|
+
call2("Run a command", "tail -n 400 actions.jsonl")
|
|
757
|
+
];
|
|
758
|
+
const finish = [call2("Run a command", "mv items.json.tmp items.json"), said2(messages.at(-1))];
|
|
759
|
+
if (profile === "work") {
|
|
760
|
+
return [
|
|
761
|
+
...start,
|
|
762
|
+
handoff("calendar"),
|
|
763
|
+
handoff("gmail"),
|
|
764
|
+
handoff("github"),
|
|
765
|
+
call2("Google calendar list events", '{"calendar_id":"primary","time_min":"today 00:00"}', "calendar"),
|
|
766
|
+
said2("5 events. The 1:1 with Alice and the design review both sit at 11:00; the lunch & learn is marked free.", "calendar"),
|
|
767
|
+
call2("Gmail search emails", '{"query":"newer_than:3d -category:promotions"}', "gmail"),
|
|
768
|
+
call2("Gmail batch read email", '{"message_ids":["19a2f\u2026","19a31\u2026","19a33\u2026","19a35\u2026"]}', "gmail"),
|
|
769
|
+
said2("31 threads in the window. 4 ask something of you; Bob followed up on Q4 headcount yesterday.", "gmail"),
|
|
770
|
+
call2("Run a command", "gh search prs --review-requested=@me --state=open --json url,title,updatedAt", "github"),
|
|
771
|
+
call2("Run a command", "gh pr checks 3402 -R acme/webapp", "github"),
|
|
772
|
+
said2("A review request from Alice on acme/webapp#3421, and a red check on #3402. The rest is already on the board.", "github"),
|
|
773
|
+
wait,
|
|
774
|
+
said2(messages[1]),
|
|
775
|
+
said2(messages[2]),
|
|
776
|
+
handoff("jira"),
|
|
777
|
+
handoff("slack"),
|
|
778
|
+
handoff("workday"),
|
|
779
|
+
call2("Run a command", 'acli jira workitem search --jql "assignee = currentUser() AND statusCategory != Done"', "jira"),
|
|
780
|
+
said2("PROJ-8842 is still open, with both of its pull requests merged.", "jira"),
|
|
781
|
+
call2("Slack search messages", '{"query":"to:me after:yesterday"}', "slack"),
|
|
782
|
+
said2("One direct question, from Chris in #webapp-dev.", "slack"),
|
|
783
|
+
call2("Workday list inbox tasks", null, "workday"),
|
|
784
|
+
said2("The compliance training is due tonight. The Berlin expense report is still a draft.", "workday"),
|
|
785
|
+
{ ...wait },
|
|
786
|
+
said2(messages[3]),
|
|
787
|
+
...finish
|
|
788
|
+
];
|
|
789
|
+
}
|
|
790
|
+
return [
|
|
791
|
+
...start,
|
|
792
|
+
handoff("calendar"),
|
|
793
|
+
handoff("eboks"),
|
|
794
|
+
handoff("email"),
|
|
795
|
+
call2("Google calendar list events", '{"calendar_id":"personal","time_min":"today 00:00"}', "calendar"),
|
|
796
|
+
call2("Google calendar list events", '{"calendar_id":"family","time_min":"today 00:00"}', "calendar"),
|
|
797
|
+
said2("6 events across Personal and Family. The insurance call at 16:30 overlaps the daycare pickup.", "calendar"),
|
|
798
|
+
call2("Run a command", "eboks list --since 3d --json", "eboks"),
|
|
799
|
+
call2("Run a command", "eboks text --json 4417911204", "eboks"),
|
|
800
|
+
said2("5 letters. The daycare invoice and the tax assessment ask for something; three are confirmations.", "eboks"),
|
|
801
|
+
call2("Gmail search emails", '{"query":"newer_than:3d -category:promotions"}', "email"),
|
|
802
|
+
said2("3 threads need you, the insulation quote among them, quiet for nine days.", "email"),
|
|
803
|
+
wait,
|
|
804
|
+
said2(messages[1]),
|
|
805
|
+
handoff("messages"),
|
|
806
|
+
call2("Run a command", "messages recent --since 3d --json", "messages"),
|
|
807
|
+
said2("Jonas asked about Saturday on Tuesday, and Priya is still waiting for the photos.", "messages"),
|
|
808
|
+
{ ...wait },
|
|
809
|
+
said2(messages[2]),
|
|
810
|
+
...finish
|
|
811
|
+
];
|
|
812
|
+
}
|
|
746
813
|
var call = (text, detail, state = "done") => ({ kind: "tool", text, detail, state });
|
|
747
814
|
var said = (text) => ({ kind: "message", text, detail: null, state: "done" });
|
|
748
815
|
function turnLines(seed) {
|
|
@@ -3,13 +3,19 @@ import {
|
|
|
3
3
|
Assistant,
|
|
4
4
|
AutoAsk,
|
|
5
5
|
CodexGate,
|
|
6
|
+
HANDOFF,
|
|
7
|
+
WAIT,
|
|
6
8
|
codexLatestModel,
|
|
7
9
|
codexVersionWarning,
|
|
10
|
+
compactArgs,
|
|
8
11
|
computerUseOff,
|
|
12
|
+
connectorLabel,
|
|
9
13
|
countBoard,
|
|
10
14
|
refusalFor,
|
|
11
|
-
resolveBoard
|
|
12
|
-
|
|
15
|
+
resolveBoard,
|
|
16
|
+
shorten,
|
|
17
|
+
unwrapShell
|
|
18
|
+
} from "./chunk-6COKG3VL.js";
|
|
13
19
|
import {
|
|
14
20
|
FOCUS_TEMPLATE,
|
|
15
21
|
sourcesTemplate
|
|
@@ -58,7 +64,7 @@ import {
|
|
|
58
64
|
import { createServer } from "node:http";
|
|
59
65
|
import { createReadStream } from "node:fs";
|
|
60
66
|
import { stat as stat4 } from "node:fs/promises";
|
|
61
|
-
import { basename, extname, join as
|
|
67
|
+
import { basename, extname, join as join7, normalize, resolve, sep } from "node:path";
|
|
62
68
|
import { fileURLToPath } from "node:url";
|
|
63
69
|
|
|
64
70
|
// src/clis.ts
|
|
@@ -2729,11 +2735,170 @@ async function readTicketsFile(path) {
|
|
|
2729
2735
|
import { spawn } from "node:child_process";
|
|
2730
2736
|
import { randomUUID } from "node:crypto";
|
|
2731
2737
|
import { appendFile, mkdir, readFile as readFile7 } from "node:fs/promises";
|
|
2732
|
-
import { homedir as
|
|
2738
|
+
import { homedir as homedir3 } from "node:os";
|
|
2733
2739
|
import { createInterface } from "node:readline";
|
|
2740
|
+
|
|
2741
|
+
// src/codexsessions.ts
|
|
2742
|
+
import { open, readdir as readdir2 } from "node:fs/promises";
|
|
2743
|
+
import { homedir as homedir2 } from "node:os";
|
|
2744
|
+
import { join as join4 } from "node:path";
|
|
2745
|
+
function codexHome(env = process.env) {
|
|
2746
|
+
return env.CODEX_HOME?.trim() || join4(homedir2(), ".codex");
|
|
2747
|
+
}
|
|
2748
|
+
var DAY_MS = 24 * 60 * 6e4;
|
|
2749
|
+
var CHUNK = 4 * 1024 * 1024;
|
|
2750
|
+
var CodexSessions = class {
|
|
2751
|
+
threads = /* @__PURE__ */ new Map();
|
|
2752
|
+
since;
|
|
2753
|
+
home;
|
|
2754
|
+
/** `threadId` is the run's, as the stream's `thread.started` gives it; `since`, when the run started. */
|
|
2755
|
+
constructor(threadId, since, home = codexHome()) {
|
|
2756
|
+
this.since = since;
|
|
2757
|
+
this.home = home;
|
|
2758
|
+
this.threads.set(threadId, { id: threadId, root: true, path: null, offset: 0, partial: Buffer.alloc(0) });
|
|
2759
|
+
}
|
|
2760
|
+
/**
|
|
2761
|
+
* Whatever the files have added since the last read, oldest first within each
|
|
2762
|
+
* thread. A subagent found on the way is read in the same pass.
|
|
2763
|
+
*/
|
|
2764
|
+
async read(now = /* @__PURE__ */ new Date()) {
|
|
2765
|
+
const news = [];
|
|
2766
|
+
for (const thread of this.threads.values()) {
|
|
2767
|
+
thread.path ??= await this.find(thread.id, now);
|
|
2768
|
+
if (!thread.path) continue;
|
|
2769
|
+
for (const line of await this.tail(thread)) this.take(thread, line, news);
|
|
2770
|
+
}
|
|
2771
|
+
return news;
|
|
2772
|
+
}
|
|
2773
|
+
/**
|
|
2774
|
+
* The file a thread is written to. Codex files a session under the local date
|
|
2775
|
+
* it started, so the days the run spans are searched, a day either side for a
|
|
2776
|
+
* clock that disagrees.
|
|
2777
|
+
*/
|
|
2778
|
+
async find(id, now) {
|
|
2779
|
+
const suffix = `-${id}.jsonl`;
|
|
2780
|
+
for (let at = this.since.getTime() - DAY_MS; at <= now.getTime() + DAY_MS; at += DAY_MS) {
|
|
2781
|
+
const day = new Date(at);
|
|
2782
|
+
const dir = join4(
|
|
2783
|
+
this.home,
|
|
2784
|
+
"sessions",
|
|
2785
|
+
String(day.getFullYear()),
|
|
2786
|
+
String(day.getMonth() + 1).padStart(2, "0"),
|
|
2787
|
+
String(day.getDate()).padStart(2, "0")
|
|
2788
|
+
);
|
|
2789
|
+
let names;
|
|
2790
|
+
try {
|
|
2791
|
+
names = await readdir2(dir);
|
|
2792
|
+
} catch {
|
|
2793
|
+
continue;
|
|
2794
|
+
}
|
|
2795
|
+
const name = names.find((candidate) => candidate.endsWith(suffix));
|
|
2796
|
+
if (name) return join4(dir, name);
|
|
2797
|
+
}
|
|
2798
|
+
return null;
|
|
2799
|
+
}
|
|
2800
|
+
/** The whole lines added to a thread's file since the last read. */
|
|
2801
|
+
async tail(thread) {
|
|
2802
|
+
let handle;
|
|
2803
|
+
try {
|
|
2804
|
+
handle = await open(thread.path, "r");
|
|
2805
|
+
} catch {
|
|
2806
|
+
return [];
|
|
2807
|
+
}
|
|
2808
|
+
try {
|
|
2809
|
+
const { size } = await handle.stat();
|
|
2810
|
+
if (size <= thread.offset) return [];
|
|
2811
|
+
const buffer = Buffer.alloc(Math.min(size - thread.offset, CHUNK));
|
|
2812
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, thread.offset);
|
|
2813
|
+
thread.offset += bytesRead;
|
|
2814
|
+
const bytes = Buffer.concat([thread.partial, buffer.subarray(0, bytesRead)]);
|
|
2815
|
+
const end = bytes.lastIndexOf(10);
|
|
2816
|
+
thread.partial = end === -1 ? bytes : bytes.subarray(end + 1);
|
|
2817
|
+
return end === -1 ? [] : bytes.subarray(0, end).toString("utf8").split("\n");
|
|
2818
|
+
} catch {
|
|
2819
|
+
return [];
|
|
2820
|
+
} finally {
|
|
2821
|
+
await handle.close();
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
take(thread, line, news) {
|
|
2825
|
+
let record;
|
|
2826
|
+
try {
|
|
2827
|
+
record = JSON.parse(line);
|
|
2828
|
+
} catch {
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
if (record?.type !== "event_msg" || record.payload?.type !== "item_completed") return;
|
|
2832
|
+
const item = record.payload.item;
|
|
2833
|
+
if (typeof item !== "object" || item === null) return;
|
|
2834
|
+
const by = thread.root ? null : thread.id;
|
|
2835
|
+
if (item.type === "SubAgentActivity" && typeof item.agent_thread_id === "string") {
|
|
2836
|
+
const child = item.agent_thread_id;
|
|
2837
|
+
if (item.kind === "started") {
|
|
2838
|
+
if (!this.threads.has(child)) {
|
|
2839
|
+
this.threads.set(child, { id: child, root: false, path: null, offset: 0, partial: Buffer.alloc(0) });
|
|
2840
|
+
}
|
|
2841
|
+
news.push({ by, step: { id: child, text: HANDOFF, detail: subagentName(item.agent_path), state: "running", handoff: true } });
|
|
2842
|
+
} else if (item.kind === "completed") {
|
|
2843
|
+
news.push({ by, step: { id: child, state: "done", final: true } });
|
|
2844
|
+
} else if (typeof item.kind === "string" && /fail|error|abort|interrupt|cancel|kill/i.test(item.kind)) {
|
|
2845
|
+
news.push({ by, step: { id: child, state: "failed", final: true } });
|
|
2846
|
+
}
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
if (thread.root) return;
|
|
2850
|
+
if (item.type === "AgentMessage") {
|
|
2851
|
+
const text = Array.isArray(item.content) ? item.content.map((part) => typeof part?.text === "string" ? part.text : "").join("").trim() : "";
|
|
2852
|
+
if (text) news.push({ by: thread.id, message: text });
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
2855
|
+
const step = rolloutStep(item);
|
|
2856
|
+
if (step) news.push({ by: thread.id, step: { ...step, id: `${thread.id}:${step.id}` } });
|
|
2857
|
+
}
|
|
2858
|
+
};
|
|
2859
|
+
function subagentName(path) {
|
|
2860
|
+
if (typeof path !== "string") return null;
|
|
2861
|
+
const name = path.split("/").filter(Boolean).at(-1);
|
|
2862
|
+
return name ? name.replace(/[_-]+/g, " ") : null;
|
|
2863
|
+
}
|
|
2864
|
+
function rolloutStep(item) {
|
|
2865
|
+
if (typeof item.id !== "string") return null;
|
|
2866
|
+
const failed = item.status === "failed" || item.error !== void 0 && item.error !== null;
|
|
2867
|
+
const state = failed ? "failed" : "done";
|
|
2868
|
+
switch (item.type) {
|
|
2869
|
+
case "CommandExecution": {
|
|
2870
|
+
const command3 = Array.isArray(item.command) ? item.command.length === 3 && /^-l?c$/.test(item.command[1] ?? "") ? item.command[2] : item.command.join(" ") : unwrapShell(item.command ?? "");
|
|
2871
|
+
const exited = typeof item.exit_code === "number" && item.exit_code !== 0;
|
|
2872
|
+
return { id: item.id, text: "Run a command", detail: shorten(command3), state: exited ? "failed" : state };
|
|
2873
|
+
}
|
|
2874
|
+
case "McpToolCall": {
|
|
2875
|
+
const app = item.server === "codex_apps" && typeof item.appName === "string" && typeof item.actionName === "string";
|
|
2876
|
+
const text = connectorLabel(item.server ?? "tool", app ? `${item.appName}_${item.actionName}` : item.tool ?? "call");
|
|
2877
|
+
return { id: item.id, text, detail: compactArgs(item.arguments), state };
|
|
2878
|
+
}
|
|
2879
|
+
case "Extension": {
|
|
2880
|
+
if (item.kind !== "web.search") return null;
|
|
2881
|
+
const action = item.action ?? {};
|
|
2882
|
+
if (action.type === "openPage") return { id: item.id, text: "Open a page", detail: shorten(action.url ?? null), state };
|
|
2883
|
+
if (action.type === "findInPage") return { id: item.id, text: "Find in a page", detail: shorten(action.pattern ?? null), state };
|
|
2884
|
+
return { id: item.id, text: "Search the web", detail: shorten(action.query || item.query || null), state };
|
|
2885
|
+
}
|
|
2886
|
+
case "FileChange":
|
|
2887
|
+
return { id: item.id, text: "Write a file", detail: shorten(Object.keys(item.changes ?? {}).join(", ") || null), state };
|
|
2888
|
+
case "ImageView":
|
|
2889
|
+
return { id: item.id, text: "Look at an image", detail: shorten(item.path?.replace(/^file:\/\//, "") ?? null), state };
|
|
2890
|
+
case "CollabAgentToolCall":
|
|
2891
|
+
return item.tool === "wait" ? { id: item.id, text: WAIT, detail: null, state } : null;
|
|
2892
|
+
}
|
|
2893
|
+
return null;
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
// src/agent.ts
|
|
2734
2897
|
var RUN_TIMEOUT_MS = 60 * 6e4;
|
|
2735
2898
|
var TURN_TIMEOUT_MS = 10 * 6e4;
|
|
2736
2899
|
var RUNS_SHOWN = 30;
|
|
2900
|
+
var STEPS_KEPT = 500;
|
|
2901
|
+
var SESSIONS_EVERY_MS = 2e3;
|
|
2737
2902
|
function agentPrompt(promptPath) {
|
|
2738
2903
|
return [
|
|
2739
2904
|
"Run my morning brief for today.",
|
|
@@ -2764,7 +2929,7 @@ function followUpPrompt(question) {
|
|
|
2764
2929
|
question.trim()
|
|
2765
2930
|
].join("\n");
|
|
2766
2931
|
}
|
|
2767
|
-
function displayPath(path, home =
|
|
2932
|
+
function displayPath(path, home = homedir3()) {
|
|
2768
2933
|
return path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
|
|
2769
2934
|
}
|
|
2770
2935
|
function foldAgentLog(text) {
|
|
@@ -2790,6 +2955,7 @@ function foldAgentLog(text) {
|
|
|
2790
2955
|
status: "running",
|
|
2791
2956
|
report: "",
|
|
2792
2957
|
messages: [],
|
|
2958
|
+
steps: [],
|
|
2793
2959
|
error: null,
|
|
2794
2960
|
turns: [],
|
|
2795
2961
|
resumable: false
|
|
@@ -2830,6 +2996,7 @@ function foldAgentLog(text) {
|
|
|
2830
2996
|
run5.sessionId = typeof record.sessionId === "string" ? record.sessionId : run5.sessionId;
|
|
2831
2997
|
run5.report = typeof record.report === "string" ? record.report : run5.report;
|
|
2832
2998
|
run5.messages = Array.isArray(record.messages) ? record.messages.filter((message) => typeof message === "string") : run5.report ? [run5.report] : [];
|
|
2999
|
+
run5.steps = readSteps(record.steps);
|
|
2833
3000
|
run5.endedAt = record.at;
|
|
2834
3001
|
if (record.event === "finished") {
|
|
2835
3002
|
run5.status = "done";
|
|
@@ -2843,6 +3010,57 @@ function foldAgentLog(text) {
|
|
|
2843
3010
|
}
|
|
2844
3011
|
return [...runs.values()].sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
2845
3012
|
}
|
|
3013
|
+
function readSteps(value) {
|
|
3014
|
+
if (!Array.isArray(value)) return [];
|
|
3015
|
+
const steps = [];
|
|
3016
|
+
for (const candidate of value) {
|
|
3017
|
+
if (typeof candidate !== "object" || candidate === null || typeof candidate.text !== "string") continue;
|
|
3018
|
+
steps.push({
|
|
3019
|
+
kind: candidate.kind === "message" ? "message" : "tool",
|
|
3020
|
+
text: candidate.text,
|
|
3021
|
+
detail: typeof candidate.detail === "string" ? candidate.detail : null,
|
|
3022
|
+
state: candidate.state === "failed" ? "failed" : "done",
|
|
3023
|
+
by: typeof candidate.by === "string" ? candidate.by : null,
|
|
3024
|
+
...typeof candidate.subagent === "string" ? { subagent: candidate.subagent } : {}
|
|
3025
|
+
});
|
|
3026
|
+
}
|
|
3027
|
+
return steps;
|
|
3028
|
+
}
|
|
3029
|
+
var StepTrail = class {
|
|
3030
|
+
calls = /* @__PURE__ */ new Map();
|
|
3031
|
+
run;
|
|
3032
|
+
constructor(run5) {
|
|
3033
|
+
this.run = run5;
|
|
3034
|
+
}
|
|
3035
|
+
said(by, text) {
|
|
3036
|
+
this.add({ kind: "message", text, detail: null, state: "done", by });
|
|
3037
|
+
}
|
|
3038
|
+
call(by, parsed) {
|
|
3039
|
+
const known = this.calls.get(parsed.id);
|
|
3040
|
+
if (known) {
|
|
3041
|
+
if (parsed.text) known.text = parsed.text;
|
|
3042
|
+
if (parsed.detail) known.detail = parsed.detail;
|
|
3043
|
+
if (!known.subagent || parsed.final || parsed.state === "failed") known.state = parsed.state;
|
|
3044
|
+
return;
|
|
3045
|
+
}
|
|
3046
|
+
if (!parsed.text) return;
|
|
3047
|
+
const step = { kind: "tool", text: parsed.text, detail: parsed.detail ?? null, state: parsed.state, by };
|
|
3048
|
+
if (parsed.handoff) step.subagent = parsed.id;
|
|
3049
|
+
this.calls.set(parsed.id, step);
|
|
3050
|
+
this.add(step);
|
|
3051
|
+
}
|
|
3052
|
+
/** A call nobody reported back on went the way the run did. */
|
|
3053
|
+
settle(ok) {
|
|
3054
|
+
for (const step of this.run.steps) if (step.state === "running") step.state = ok ? "done" : "failed";
|
|
3055
|
+
}
|
|
3056
|
+
add(step) {
|
|
3057
|
+
const steps = this.run.steps;
|
|
3058
|
+
steps.push(step);
|
|
3059
|
+
if (steps.length <= STEPS_KEPT) return;
|
|
3060
|
+
const oldest = steps.findIndex((candidate) => !candidate.subagent);
|
|
3061
|
+
steps.splice(oldest === -1 ? 0 : oldest, 1);
|
|
3062
|
+
}
|
|
3063
|
+
};
|
|
2846
3064
|
var AgentRunner = class {
|
|
2847
3065
|
/** Every run in the log, oldest first. */
|
|
2848
3066
|
runs = [];
|
|
@@ -2853,11 +3071,14 @@ var AgentRunner = class {
|
|
|
2853
3071
|
spawnFn;
|
|
2854
3072
|
/** Refuses a Codex too old to hold to the connector rules; see `codex.ts`. */
|
|
2855
3073
|
codexGate;
|
|
2856
|
-
|
|
3074
|
+
/** Where Codex writes its session files, which say what a Codex run's subagents do. */
|
|
3075
|
+
codexHome;
|
|
3076
|
+
constructor(config, hooks, spawnFn = spawn, execFn, home = codexHome()) {
|
|
2857
3077
|
this.config = config;
|
|
2858
3078
|
this.hooks = hooks;
|
|
2859
3079
|
this.spawnFn = spawnFn;
|
|
2860
3080
|
this.codexGate = new CodexGate(config.agent.binPath, execFn);
|
|
3081
|
+
this.codexHome = home;
|
|
2861
3082
|
}
|
|
2862
3083
|
get enabled() {
|
|
2863
3084
|
return this.config.agent.cli !== null;
|
|
@@ -2883,7 +3104,7 @@ var AgentRunner = class {
|
|
|
2883
3104
|
run5.status = "aborted";
|
|
2884
3105
|
run5.error = reason;
|
|
2885
3106
|
run5.endedAt = at;
|
|
2886
|
-
await this.append({ event: "aborted", run: run5.id, sessionId: run5.sessionId, reason, report: run5.report, messages: run5.messages, at });
|
|
3107
|
+
await this.append({ event: "aborted", run: run5.id, sessionId: run5.sessionId, reason, report: run5.report, messages: run5.messages, steps: run5.steps, at });
|
|
2887
3108
|
}
|
|
2888
3109
|
for (const turn of run5.turns) {
|
|
2889
3110
|
if (turn.status !== "running") continue;
|
|
@@ -2903,7 +3124,7 @@ var AgentRunner = class {
|
|
|
2903
3124
|
return this.runs.at(-1) ?? null;
|
|
2904
3125
|
}
|
|
2905
3126
|
view(now = /* @__PURE__ */ new Date()) {
|
|
2906
|
-
const runs = this.runs.slice(-RUNS_SHOWN).reverse().map((run5) => ({ ...run5, resumable: this.resumable(run5) }));
|
|
3127
|
+
const runs = this.runs.slice(-RUNS_SHOWN).reverse().map((run5, index) => ({ ...run5, steps: index === 0 ? run5.steps : [], resumable: this.resumable(run5) }));
|
|
2907
3128
|
return {
|
|
2908
3129
|
enabled: this.enabled,
|
|
2909
3130
|
cli: this.config.agent.cli,
|
|
@@ -2967,6 +3188,7 @@ var AgentRunner = class {
|
|
|
2967
3188
|
status: "running",
|
|
2968
3189
|
report: "",
|
|
2969
3190
|
messages: [],
|
|
3191
|
+
steps: [],
|
|
2970
3192
|
error: null,
|
|
2971
3193
|
turns: [],
|
|
2972
3194
|
resumable: false
|
|
@@ -3078,23 +3300,31 @@ var AgentRunner = class {
|
|
|
3078
3300
|
stderr.push(chunk.toString("utf8"));
|
|
3079
3301
|
if (stderr.length > 50) stderr.shift();
|
|
3080
3302
|
});
|
|
3303
|
+
const trail = turn ? null : new StepTrail(run5);
|
|
3304
|
+
let reader = null;
|
|
3081
3305
|
if (child.stdout) {
|
|
3082
3306
|
const lines = createInterface({ input: child.stdout });
|
|
3083
3307
|
lines.on("line", (line) => {
|
|
3084
3308
|
const event = adapter.parse(line);
|
|
3085
3309
|
if (!event) return;
|
|
3086
3310
|
if (event.sessionId && !turn) run5.sessionId = event.sessionId;
|
|
3311
|
+
if (trail && cli2 === "codex" && run5.sessionId && !reader) reader = this.readSessions(run5, trail);
|
|
3312
|
+
const by = event.by ?? null;
|
|
3087
3313
|
if (event.text) {
|
|
3088
|
-
if (
|
|
3314
|
+
if (by !== null) {
|
|
3315
|
+
trail?.said(by, event.text);
|
|
3316
|
+
} else if (turn) {
|
|
3089
3317
|
turn.reply = turn.reply ? `${turn.reply}
|
|
3090
3318
|
|
|
3091
3319
|
${event.text}` : event.text;
|
|
3092
3320
|
} else {
|
|
3093
3321
|
run5.messages.push(event.text);
|
|
3094
3322
|
run5.report = event.text;
|
|
3323
|
+
trail?.said(null, event.text);
|
|
3095
3324
|
}
|
|
3096
|
-
this.hooks.onChange();
|
|
3097
3325
|
}
|
|
3326
|
+
for (const parsed of event.steps ?? []) trail?.call(by, parsed);
|
|
3327
|
+
if (event.text || trail && event.steps) this.hooks.onChange();
|
|
3098
3328
|
if (event.done) done = event.done;
|
|
3099
3329
|
});
|
|
3100
3330
|
}
|
|
@@ -3103,14 +3333,53 @@ ${event.text}` : event.text;
|
|
|
3103
3333
|
});
|
|
3104
3334
|
child.on("close", (code) => {
|
|
3105
3335
|
clearTimeout(timer);
|
|
3106
|
-
this.running = null;
|
|
3107
3336
|
const outcome = running.abortReason ? { ok: false, error: running.abortReason } : done ?? {
|
|
3108
3337
|
ok: false,
|
|
3109
3338
|
error: `${cli2} exited with code ${code}${stderr.length ? `: ${stderr.join("").trim().split("\n").slice(-3).join(" ")}` : ""}`
|
|
3110
3339
|
};
|
|
3111
|
-
void
|
|
3340
|
+
void (async () => {
|
|
3341
|
+
if (reader) await this.stopReading(reader, trail);
|
|
3342
|
+
this.running = null;
|
|
3343
|
+
trail?.settle(outcome.ok);
|
|
3344
|
+
await this.finish(run5, turn, outcome, running.abortReason !== null);
|
|
3345
|
+
})();
|
|
3112
3346
|
});
|
|
3113
3347
|
}
|
|
3348
|
+
/**
|
|
3349
|
+
* Start reading a Codex run's session files for its subagents, now that the
|
|
3350
|
+
* stream has named the run's thread.
|
|
3351
|
+
*/
|
|
3352
|
+
readSessions(run5, trail) {
|
|
3353
|
+
const reader = {
|
|
3354
|
+
sessions: new CodexSessions(run5.sessionId, new Date(run5.startedAt), this.codexHome),
|
|
3355
|
+
timer: setInterval(() => void this.readOnce(reader, trail), SESSIONS_EVERY_MS),
|
|
3356
|
+
reading: null
|
|
3357
|
+
};
|
|
3358
|
+
reader.timer.unref();
|
|
3359
|
+
return reader;
|
|
3360
|
+
}
|
|
3361
|
+
readOnce(reader, trail) {
|
|
3362
|
+
reader.reading ??= (async () => {
|
|
3363
|
+
try {
|
|
3364
|
+
const news = await reader.sessions.read();
|
|
3365
|
+
for (const entry of news) {
|
|
3366
|
+
if ("message" in entry) trail.said(entry.by, entry.message);
|
|
3367
|
+
else trail.call(entry.by, entry.step);
|
|
3368
|
+
}
|
|
3369
|
+
if (news.length) this.hooks.onChange();
|
|
3370
|
+
} catch (err) {
|
|
3371
|
+
console.warn(`[daily-focus] could not read Codex's session files: ${err.message}`);
|
|
3372
|
+
} finally {
|
|
3373
|
+
reader.reading = null;
|
|
3374
|
+
}
|
|
3375
|
+
})();
|
|
3376
|
+
return reader.reading;
|
|
3377
|
+
}
|
|
3378
|
+
async stopReading(reader, trail) {
|
|
3379
|
+
clearInterval(reader.timer);
|
|
3380
|
+
await reader.reading;
|
|
3381
|
+
await this.readOnce(reader, trail);
|
|
3382
|
+
}
|
|
3114
3383
|
/**
|
|
3115
3384
|
* Kill whatever is going, run or follow-up. What a run already renamed into
|
|
3116
3385
|
* place stays: the brief it replaced is gone either way, and the archive has it.
|
|
@@ -3152,17 +3421,17 @@ ${event.text}` : event.text;
|
|
|
3152
3421
|
const report = outcome.reply ?? run5.report;
|
|
3153
3422
|
run5.report = report;
|
|
3154
3423
|
run5.endedAt = at;
|
|
3155
|
-
const messages = run5
|
|
3424
|
+
const { messages, steps } = run5;
|
|
3156
3425
|
if (outcome.ok) {
|
|
3157
|
-
await this.append({ event: "finished", run: run5.id, sessionId: run5.sessionId, report, messages, at });
|
|
3426
|
+
await this.append({ event: "finished", run: run5.id, sessionId: run5.sessionId, report, messages, steps, at });
|
|
3158
3427
|
run5.status = "done";
|
|
3159
3428
|
} else if (aborted) {
|
|
3160
3429
|
run5.error = outcome.error ?? "stopped";
|
|
3161
|
-
await this.append({ event: "aborted", run: run5.id, sessionId: run5.sessionId, reason: run5.error, report, messages, at });
|
|
3430
|
+
await this.append({ event: "aborted", run: run5.id, sessionId: run5.sessionId, reason: run5.error, report, messages, steps, at });
|
|
3162
3431
|
run5.status = "aborted";
|
|
3163
3432
|
} else {
|
|
3164
3433
|
run5.error = outcome.error ?? "failed";
|
|
3165
|
-
await this.append({ event: "failed", run: run5.id, sessionId: run5.sessionId, error: run5.error, report, messages, at });
|
|
3434
|
+
await this.append({ event: "failed", run: run5.id, sessionId: run5.sessionId, error: run5.error, report, messages, steps, at });
|
|
3166
3435
|
run5.status = "failed";
|
|
3167
3436
|
}
|
|
3168
3437
|
this.hooks.onChange();
|
|
@@ -3209,19 +3478,19 @@ function watchDataDir(dir, onChange, { debounceMs = 120, ignore = [] } = {}) {
|
|
|
3209
3478
|
}
|
|
3210
3479
|
|
|
3211
3480
|
// src/assets.ts
|
|
3212
|
-
import { readdir as
|
|
3213
|
-
import { join as
|
|
3481
|
+
import { readdir as readdir3, stat as stat3 } from "node:fs/promises";
|
|
3482
|
+
import { join as join5 } from "node:path";
|
|
3214
3483
|
async function computeAssetVersion(dir) {
|
|
3215
3484
|
const parts = [];
|
|
3216
3485
|
async function walk(current) {
|
|
3217
3486
|
let entries;
|
|
3218
3487
|
try {
|
|
3219
|
-
entries = await
|
|
3488
|
+
entries = await readdir3(current, { withFileTypes: true });
|
|
3220
3489
|
} catch {
|
|
3221
3490
|
return;
|
|
3222
3491
|
}
|
|
3223
3492
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
3224
|
-
const path =
|
|
3493
|
+
const path = join5(current, entry.name);
|
|
3225
3494
|
if (entry.isDirectory()) {
|
|
3226
3495
|
await walk(path);
|
|
3227
3496
|
} else {
|
|
@@ -3281,7 +3550,7 @@ function refusal(req, listenHost) {
|
|
|
3281
3550
|
import { spawn as spawn2 } from "node:child_process";
|
|
3282
3551
|
import { mkdir as mkdir2, mkdtemp, readFile as readFile8, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
|
|
3283
3552
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
3284
|
-
import { join as
|
|
3553
|
+
import { join as join6 } from "node:path";
|
|
3285
3554
|
import { createInterface as createInterface2 } from "node:readline";
|
|
3286
3555
|
|
|
3287
3556
|
// src/questions.ts
|
|
@@ -4233,7 +4502,7 @@ async function saveSettings(path, settings) {
|
|
|
4233
4502
|
// src/onboarding.ts
|
|
4234
4503
|
var TURN_TIMEOUT_MS2 = 4 * 6e4;
|
|
4235
4504
|
var KILL_AFTER_MS = 5e3;
|
|
4236
|
-
var
|
|
4505
|
+
var STEPS_KEPT2 = 40;
|
|
4237
4506
|
var SESSION_MODEL = {
|
|
4238
4507
|
claude: { model: "opus", effort: "low" },
|
|
4239
4508
|
codex: { model: null, effort: "low" }
|
|
@@ -4687,8 +4956,8 @@ ${request}`, resume, said);
|
|
|
4687
4956
|
return this.settle(question.id, record, { ok: false, error: `Codex could not say which plugins to switch off: ${err.message}` });
|
|
4688
4957
|
}
|
|
4689
4958
|
}
|
|
4690
|
-
const schemaDir = await mkdtemp(
|
|
4691
|
-
const schemaFile =
|
|
4959
|
+
const schemaDir = await mkdtemp(join6(tmpdir2(), "daily-focus-setup-"));
|
|
4960
|
+
const schemaFile = join6(schemaDir, "schema.json");
|
|
4692
4961
|
const json = JSON.stringify(question.agent.schema);
|
|
4693
4962
|
await writeFile2(schemaFile, json, "utf8");
|
|
4694
4963
|
await mkdir2(this.config.onboardingDir, { recursive: true });
|
|
@@ -4729,7 +4998,7 @@ ${request}`, resume, said);
|
|
|
4729
4998
|
const steps = this.steps.get(question.id);
|
|
4730
4999
|
const add = (step) => {
|
|
4731
5000
|
steps.push(step);
|
|
4732
|
-
if (steps.length >
|
|
5001
|
+
if (steps.length > STEPS_KEPT2) steps.shift();
|
|
4733
5002
|
};
|
|
4734
5003
|
if (child.stdout) {
|
|
4735
5004
|
createInterface2({ input: child.stdout }).on("line", (line) => {
|
|
@@ -5116,7 +5385,7 @@ async function readBody(req) {
|
|
|
5116
5385
|
}
|
|
5117
5386
|
async function serveStatic(res, urlPath) {
|
|
5118
5387
|
const relative = normalize(decodeURIComponent(urlPath === "/" ? "/index.html" : urlPath));
|
|
5119
|
-
const filePath =
|
|
5388
|
+
const filePath = join7(PUBLIC_DIR, relative);
|
|
5120
5389
|
if (filePath !== PUBLIC_DIR && !filePath.startsWith(PUBLIC_DIR + sep)) {
|
|
5121
5390
|
res.writeHead(403).end("Forbidden");
|
|
5122
5391
|
return;
|
|
@@ -5205,7 +5474,7 @@ async function startServer(env, options = {}) {
|
|
|
5205
5474
|
let restart = null;
|
|
5206
5475
|
let restarting = false;
|
|
5207
5476
|
let settingsWrites = Promise.resolve();
|
|
5208
|
-
await stat4(
|
|
5477
|
+
await stat4(join7(PUBLIC_DIR, "app.js")).catch(() => {
|
|
5209
5478
|
console.warn(
|
|
5210
5479
|
PACKAGED ? "[daily-focus] public/app.js is missing from the package: reinstall it" : "[daily-focus] public/app.js is missing: run `npm run build` (npm start does so on its own)"
|
|
5211
5480
|
);
|
package/package.json
CHANGED
package/prompts/README.md
CHANGED
|
@@ -78,6 +78,13 @@ writes `agent-settings.json` with the values it is running with, whether they we
|
|
|
78
78
|
saved on the settings page or set in `.env`, and both prompts say what each key means.
|
|
79
79
|
The wrapper stays the same whatever the settings are.
|
|
80
80
|
|
|
81
|
+
The prompt has the agent hand the reading of its sources to subagents, which share
|
|
82
|
+
its working directory and what its CLI allows it, so the prompt also tells it to
|
|
83
|
+
tell them that they only read. The judging and the writing stay with the agent. The
|
|
84
|
+
run's panel shows each subagent under the step that started it: Claude Code reports
|
|
85
|
+
them in its output, and for Codex the dashboard reads Codex's own session files
|
|
86
|
+
(`src/codexsessions.ts`).
|
|
87
|
+
|
|
81
88
|
Which days it runs is `DAILY_FOCUS_AGENT_DAYS` — cron-numbered, so `1-5` is Mon–Fri
|
|
82
89
|
and `0-4` is the Sun–Thu week — and Mon–Fri when unset. The same schedule drives the
|
|
83
90
|
staleness banner, which turns red once the brief is more than 24 *working* hours old,
|
|
@@ -104,6 +104,33 @@ Otherwise, each source's window starts three days before the previous brief's
|
|
|
104
104
|
|
|
105
105
|
### Step 2 — Gather
|
|
106
106
|
|
|
107
|
+
#### Hand the reading to subagents
|
|
108
|
+
|
|
109
|
+
Read the sources in parallel rather than one after another: hand each source to a
|
|
110
|
+
subagent of its own, or a few small ones to one subagent, so the slowest source sets
|
|
111
|
+
the pace rather than the sum of them. Run up to three at a time, starting the next as
|
|
112
|
+
one finishes. The reading is all you hand out. Step 1, the judging in step 3, the
|
|
113
|
+
writing in step 4 and the checks in step 5 stay with you, because they need
|
|
114
|
+
everything in one place.
|
|
115
|
+
|
|
116
|
+
A subagent may not see this file, so give it what it needs in its task: its source's
|
|
117
|
+
section of this step in full, with the value of any setting from `agent-settings.json`
|
|
118
|
+
that the section names; the rules under "Across every source" below; the window from
|
|
119
|
+
step 1; and the ids from the previous brief that came from its source and are still
|
|
120
|
+
open, so it looks at each again. Name each after its source — `calendar`, `email`, `eboks` — since
|
|
121
|
+
the dashboard shows the user which is which while you work.
|
|
122
|
+
|
|
123
|
+
Ask for evidence, not decisions. For each thing that might be an item: its upstream id
|
|
124
|
+
or key, its link, who and what it is about, its dates, and what the source shows about
|
|
125
|
+
whether it is still open. Then what it could not read, why, and the link. Merging
|
|
126
|
+
duplicates, ranking and wording are yours. When an answer is missing something its
|
|
127
|
+
source should have had, ask again or read it yourself.
|
|
128
|
+
|
|
129
|
+
A subagent only reads, and tell it so: it writes no file, `items.json` least of all,
|
|
130
|
+
and changes nothing in any source.
|
|
131
|
+
|
|
132
|
+
If you cannot start subagents, read the sources yourself, one after another.
|
|
133
|
+
|
|
107
134
|
#### Across every source
|
|
108
135
|
|
|
109
136
|
**Use a connector, API, MCP server or CLI as the evidence source.** `sources.md` says
|
|
@@ -111,6 +111,33 @@ a recent query alone make unfinished work disappear.
|
|
|
111
111
|
|
|
112
112
|
### Step 2 — Gather
|
|
113
113
|
|
|
114
|
+
#### Hand the reading to subagents
|
|
115
|
+
|
|
116
|
+
Read the sources in parallel rather than one after another: hand each source to a
|
|
117
|
+
subagent of its own, or a few small ones to one subagent, so the slowest source sets
|
|
118
|
+
the pace rather than the sum of them. Run up to three at a time, starting the next as
|
|
119
|
+
one finishes. The reading is all you hand out. Step 1, the judging in step 3, the
|
|
120
|
+
writing in step 4 and the checks in step 5 stay with you, because they need
|
|
121
|
+
everything in one place.
|
|
122
|
+
|
|
123
|
+
A subagent may not see this file, so give it what it needs in its task: its source's
|
|
124
|
+
section of this step in full, with the value of any setting from `agent-settings.json`
|
|
125
|
+
that the section names; the rules under "Across every source" below; the window from
|
|
126
|
+
step 1; and the ids from the previous brief that came from its source and are still
|
|
127
|
+
open, so it looks at each again. Name each after its source — `calendar`, `gmail`, `github` — since
|
|
128
|
+
the dashboard shows the user which is which while you work.
|
|
129
|
+
|
|
130
|
+
Ask for evidence, not decisions. For each thing that might be an item: its upstream id
|
|
131
|
+
or key, its link, who and what it is about, its dates, and what the source shows about
|
|
132
|
+
whether it is still open. Then what it could not read, why, and the link. Merging
|
|
133
|
+
duplicates, ranking and wording are yours. When an answer is missing something its
|
|
134
|
+
source should have had, ask again or read it yourself.
|
|
135
|
+
|
|
136
|
+
A subagent only reads, and tell it so: it writes no file, `items.json` least of all,
|
|
137
|
+
and changes nothing in any source.
|
|
138
|
+
|
|
139
|
+
If you cannot start subagents, read the sources yourself, one after another.
|
|
140
|
+
|
|
114
141
|
#### Across every source
|
|
115
142
|
|
|
116
143
|
**Use a connector, API or CLI as the evidence source.** A browser may only help you
|
package/public/app.js
CHANGED
|
@@ -2479,10 +2479,11 @@ function agentRunBanner(run, state2, handlers2) {
|
|
|
2479
2479
|
}
|
|
2480
2480
|
};
|
|
2481
2481
|
if (run.status === "running") {
|
|
2482
|
+
const helping = (run.steps ?? []).filter((step) => step.subagent && step.state === "running").length;
|
|
2482
2483
|
return bannerNode(
|
|
2483
2484
|
"info",
|
|
2484
2485
|
"i",
|
|
2485
|
-
`The morning agent is writing a new brief, started ${relativeTime(run.startedAt, now)}
|
|
2486
|
+
`The morning agent is writing a new brief, started ${relativeTime(run.startedAt, now)}` + (helping ? `, with ${helping} ${helping === 1 ? "subagent" : "subagents"} at work.` : "."),
|
|
2486
2487
|
[el("button", { type: "button", class: "button banner__action", onClick: () => handlers2.stopAgent() }, "Stop")],
|
|
2487
2488
|
strip
|
|
2488
2489
|
);
|
|
@@ -3935,6 +3936,7 @@ function describeWhen(value, now) {
|
|
|
3935
3936
|
return `${named ? day : date && date.getFullYear() === now.getFullYear() ? formatShortDay(value) : formatDay(value)} at ${formatTime(value)}`;
|
|
3936
3937
|
}
|
|
3937
3938
|
function runReport(run, ui2, handlers2) {
|
|
3939
|
+
if (run.steps?.length) return runWork(run, ui2, handlers2);
|
|
3938
3940
|
const messages = run.messages ?? (run.report ? [run.report] : []);
|
|
3939
3941
|
const running = run.status === "running";
|
|
3940
3942
|
const earlier = !running && messages.length && messages.at(-1) === run.report ? messages.slice(0, -1) : messages;
|
|
@@ -3993,6 +3995,92 @@ function runReport(run, ui2, handlers2) {
|
|
|
3993
3995
|
) : null
|
|
3994
3996
|
);
|
|
3995
3997
|
}
|
|
3998
|
+
function runWork(run, ui2, handlers2) {
|
|
3999
|
+
const running = run.status === "running";
|
|
4000
|
+
if (running) {
|
|
4001
|
+
return el(
|
|
4002
|
+
"section",
|
|
4003
|
+
{ class: "flyout__section" },
|
|
4004
|
+
el("h3", { class: "flyout__label" }, "What it is doing"),
|
|
4005
|
+
runSteps(run, run.steps, null, ui2, handlers2)
|
|
4006
|
+
);
|
|
4007
|
+
}
|
|
4008
|
+
const last = run.steps.findLast((step) => step.by === null && step.kind === "message");
|
|
4009
|
+
const steps2 = last && sameWords(last.text, run.report) ? run.steps.filter((step) => step !== last) : run.steps;
|
|
4010
|
+
const handoffs = steps2.filter((step) => step.subagent).length;
|
|
4011
|
+
return el(
|
|
4012
|
+
"section",
|
|
4013
|
+
{ class: "flyout__section" },
|
|
4014
|
+
el("h3", { class: "flyout__label" }, run.status === "done" ? "Its report" : "What it said last"),
|
|
4015
|
+
run.report ? el("div", { class: "assistant__reply" }, renderMarkdownBlocks(run.report)) : el("p", { class: "flyout__empty" }, "It said nothing."),
|
|
4016
|
+
steps2.length ? el(
|
|
4017
|
+
"details",
|
|
4018
|
+
{ class: "agent-run__list" },
|
|
4019
|
+
el(
|
|
4020
|
+
"summary",
|
|
4021
|
+
null,
|
|
4022
|
+
`How it got there, in ${steps2.length} ${steps2.length === 1 ? "step" : "steps"}` + (handoffs ? `, with ${handoffs} ${handoffs === 1 ? "subagent" : "subagents"}` : "")
|
|
4023
|
+
),
|
|
4024
|
+
runSteps(run, steps2, null, ui2, handlers2)
|
|
4025
|
+
) : null
|
|
4026
|
+
);
|
|
4027
|
+
}
|
|
4028
|
+
var NESTING = 4;
|
|
4029
|
+
function runSteps(run, steps2, by, ui2, handlers2, depth = 0) {
|
|
4030
|
+
const own = steps2.filter((step) => step.by === by);
|
|
4031
|
+
return el(
|
|
4032
|
+
"ol",
|
|
4033
|
+
{ class: "assistant__steps agent-run__steps" },
|
|
4034
|
+
own.length ? null : el("li", { class: "assistant__step assistant__step--message" }, "Nothing yet."),
|
|
4035
|
+
own.map((step, index) => {
|
|
4036
|
+
if (step.kind === "message") {
|
|
4037
|
+
return el("li", { key: index, class: "agent-run__said" }, renderMarkdownBlocks(step.text));
|
|
4038
|
+
}
|
|
4039
|
+
if (step.subagent && depth < NESTING) return runHandoff(run, steps2, step, ui2, handlers2, depth, index);
|
|
4040
|
+
return el(
|
|
4041
|
+
"li",
|
|
4042
|
+
{ key: index, class: "assistant__step", "data-state": step.state },
|
|
4043
|
+
el("span", { class: "assistant__step-text" }, step.text),
|
|
4044
|
+
step.detail ? el("code", { class: "assistant__step-detail", title: step.detail }, step.detail) : null
|
|
4045
|
+
);
|
|
4046
|
+
})
|
|
4047
|
+
);
|
|
4048
|
+
}
|
|
4049
|
+
function runHandoff(run, steps2, step, ui2, handlers2, depth, index) {
|
|
4050
|
+
const id = step.subagent;
|
|
4051
|
+
const own = steps2.filter((candidate) => candidate.by === id);
|
|
4052
|
+
const latest = own.at(-1);
|
|
4053
|
+
const calls = own.filter((candidate) => candidate.kind === "tool").length;
|
|
4054
|
+
const doing = step.state === "running" ? latest ? latest.kind === "message" ? firstLine(latest.text) : latest.text : "starting" : `${calls} ${calls === 1 ? "step" : "steps"}`;
|
|
4055
|
+
const key = `${run.id}:${id}`;
|
|
4056
|
+
const fallback = step.state === "running";
|
|
4057
|
+
const open = ui2.messageOpen.value.has(key) ? ui2.messageOpen.value.get(key) : fallback;
|
|
4058
|
+
return el(
|
|
4059
|
+
"li",
|
|
4060
|
+
{ key: index, class: "assistant__step agent-run__handoff", "data-state": step.state },
|
|
4061
|
+
el(
|
|
4062
|
+
"details",
|
|
4063
|
+
{
|
|
4064
|
+
open,
|
|
4065
|
+
// As the message folds do: a toggle back to the default is forgotten.
|
|
4066
|
+
onToggle: (event) => {
|
|
4067
|
+
const isOpen = event.currentTarget.open;
|
|
4068
|
+
handlers2.toggleMessage(key, isOpen === fallback ? null : isOpen);
|
|
4069
|
+
}
|
|
4070
|
+
},
|
|
4071
|
+
el(
|
|
4072
|
+
"summary",
|
|
4073
|
+
null,
|
|
4074
|
+
el("span", { class: "assistant__step-text" }, `Subagent: ${step.detail ?? "unnamed"}`),
|
|
4075
|
+
el("span", { class: "assistant__step-detail" }, doing)
|
|
4076
|
+
),
|
|
4077
|
+
runSteps(run, steps2, id, ui2, handlers2, depth + 1)
|
|
4078
|
+
)
|
|
4079
|
+
);
|
|
4080
|
+
}
|
|
4081
|
+
function sameWords(a4, b4) {
|
|
4082
|
+
return a4.replace(/\s+/g, " ").trim() === b4.replace(/\s+/g, " ").trim();
|
|
4083
|
+
}
|
|
3996
4084
|
function runStatus(run, now, handlers2) {
|
|
3997
4085
|
if (run.status === "running") {
|
|
3998
4086
|
return el(
|
package/public/style.css
CHANGED
|
@@ -541,6 +541,64 @@ body[data-view='tickets'] #brief-refresh {
|
|
|
541
541
|
.agent-run__message .assistant__reply {
|
|
542
542
|
padding: 0 10px 8px;
|
|
543
543
|
}
|
|
544
|
+
/*
|
|
545
|
+
* A run's steps, the assistant's look: what it said in its own words between
|
|
546
|
+
* the calls, and each subagent as a fold under the step that handed it work.
|
|
547
|
+
*/
|
|
548
|
+
.agent-run__list > .agent-run__steps {
|
|
549
|
+
margin-top: 8px;
|
|
550
|
+
}
|
|
551
|
+
.agent-run__said {
|
|
552
|
+
margin: 6px 0;
|
|
553
|
+
font-size: 12.5px;
|
|
554
|
+
line-height: 1.45;
|
|
555
|
+
color: var(--ink-1);
|
|
556
|
+
overflow-wrap: anywhere;
|
|
557
|
+
}
|
|
558
|
+
.agent-run__said:first-child {
|
|
559
|
+
margin-top: 0;
|
|
560
|
+
}
|
|
561
|
+
.agent-run__said > * {
|
|
562
|
+
margin: 0;
|
|
563
|
+
}
|
|
564
|
+
.agent-run__said > * + * {
|
|
565
|
+
margin-top: 5px;
|
|
566
|
+
}
|
|
567
|
+
.agent-run__said ul,
|
|
568
|
+
.agent-run__said ol {
|
|
569
|
+
padding-left: 18px;
|
|
570
|
+
}
|
|
571
|
+
.agent-run__said pre {
|
|
572
|
+
white-space: pre-wrap;
|
|
573
|
+
}
|
|
574
|
+
.agent-run__handoff > details {
|
|
575
|
+
flex: 1 1 auto;
|
|
576
|
+
min-width: 0;
|
|
577
|
+
}
|
|
578
|
+
/* The spinner stays on the subagent's own line, not in the middle of its open steps. */
|
|
579
|
+
.agent-run__steps > .agent-run__handoff[data-state='running']::before {
|
|
580
|
+
align-self: flex-start;
|
|
581
|
+
margin-top: 4px;
|
|
582
|
+
}
|
|
583
|
+
.agent-run__handoff summary {
|
|
584
|
+
cursor: pointer;
|
|
585
|
+
overflow: hidden;
|
|
586
|
+
text-overflow: ellipsis;
|
|
587
|
+
white-space: nowrap;
|
|
588
|
+
}
|
|
589
|
+
.agent-run__handoff summary:hover {
|
|
590
|
+
color: var(--ink-1);
|
|
591
|
+
}
|
|
592
|
+
.agent-run__handoff summary .assistant__step-text {
|
|
593
|
+
font-weight: 600;
|
|
594
|
+
}
|
|
595
|
+
.agent-run__handoff summary .assistant__step-detail {
|
|
596
|
+
margin-left: 6px;
|
|
597
|
+
font-size: 12px;
|
|
598
|
+
}
|
|
599
|
+
.agent-run__handoff .agent-run__steps {
|
|
600
|
+
margin-top: 4px;
|
|
601
|
+
}
|
|
544
602
|
.agent-run__runs {
|
|
545
603
|
margin: 8px 0 0;
|
|
546
604
|
padding: 0;
|