Skip to content
901 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.1import assert from "node:assert/strict";
2import { test } from "node:test";
3
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.4import type { ActivityDigest, ActorCount, AgentSession, ChatActivity, Deployment, InboxItem, InstallRequest, Memory, RepoDigest, WorkflowRun } from "@g1t/contracts";
The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.5
6import {
7 type AttentionRow,
8 type CodeNeed,
9 type CodePull,
10 acceptance,
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.11 actorKind,
The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.12 agentRow,
13 attention,
14 chatRow,
15 codeRow,
16 dayIn,
17 decisionsIn,
18 deploysIn,
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.19 digestOf,
The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.20 duration,
21 installRequestRow,
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.22 landed,
23 landedDeploys,
24 landedFromActivity,
The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.25 landedIn,
26 limitRow,
27 notificationRow,
28 parseWindow,
29 pullOutcome,
30 rankAttention,
31 running,
32 runningDeploys,
33 runningSessions,
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.34 runningWorkflows,
The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.35 sessionOutcome,
36 sinceWords,
37 spanFor,
38 spanMonths,
39 spanSentence,
40 spanWork,
41 spendIn,
42 startHere,
43 trendLabel,
44 waitingSentence,
45} from "./home.ts";
46
47const HOUR = 3_600_000;
48const DAY = 24 * HOUR;
49// Friday 2026-10-09, 18:00 UTC; 11:00 in Los Angeles.
50const NOW = Date.parse("2026-10-09T18:00:00Z");
51const LA = "America/Los_Angeles";
52const repo = { namespace: "acme", name: "web" };
53const iso = (ms: number) => new Date(ms).toISOString();
54
55function pull(over: Partial<CodePull> = {}): CodePull {
56 return {
57 repo,
58 number: 1,
59 title: "Fix the flaky test",
60 status: "merged",
61 mergedAt: iso(NOW - HOUR),
62 createdAt: iso(NOW - 3 * HOUR),
63 updatedAt: iso(NOW - HOUR),
64 author: { username: "g1t", kind: "agent" },
65 ...over,
66 };
67}
68
69function session(over: Partial<AgentSession> = {}): AgentSession {
70 return {
71 id: "s1",
72 workspace_id: "w",
73 agent_id: "a",
74 agent_handle: "margo",
75 agent_name: "Margo",
76 agent_avatar_seed: "margo",
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.77 agent_look: null,
The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.78 subagent: null,
79 kind: "chat",
80 parent_id: null,
81 root_id: "s1",
82 payer_agent_id: "a",
83 title: "Draft the launch notes",
84 goal: "",
85 status: "done",
86 status_note: null,
87 summary: null,
88 channel_id: "c",
89 channel_kind: "channel",
90 channel_name: "launch",
91 card_message_id: null,
92 asked_by: null,
93 asked_by_username: "bo",
94 routine_id: null,
95 steps: 3,
96 tool_calls: 2,
97 input_tokens: 0,
98 output_tokens: 0,
99 charged_micros: 400_000,
100 cap_micros: 2_000_000,
101 model: null,
102 outputs: [],
103 created_at: iso(NOW - 2 * HOUR),
104 updated_at: iso(NOW - HOUR),
105 finished_at: iso(NOW - HOUR),
106 visible: true,
107 ...over,
108 };
109}
110const need = (over: Partial<CodeNeed>): CodeNeed => ({
111 key: "pull:p1",
112 reason: "ready_to_merge",
113 repo,
114 ref: "#12",
115 title: "Add retries",
116 ask: "Waiting for you to merge it.",
117 by: { name: "g1t", agent: true },
118 for: "bo",
119 at: NOW - 2 * HOUR,
120 to: "/acme/web/pull/12",
121 facts: [{ label: "Files changed", value: "4", tone: null }],
122 ...over,
123});
124
125test("Code's needs become rows with what is at stake", () => {
126 const ready = codeRow(need({}));
127 assert.equal(ready.kind, "ready");
128 assert.equal(ready.stake?.text, "4 files to land");
129 assert.equal(ready.action.label, "Merge");
130 const deploy = codeRow(need({ key: "deploy:site", reason: "blocking", facts: [{ label: "Production", value: "Serving the build before", tone: "good" }] }));
131 assert.equal(deploy.kind, "deploy");
132 assert.equal(deploy.stake?.text, "Production on an older build");
133 assert.equal(codeRow(need({ reason: "checks_failing" })).kind, "pull_blocked");
134 assert.equal(codeRow(need({ key: "run:r1", reason: "stalled", facts: [{ label: "Cost so far", value: "$1.20", tone: null }] })).stake?.text, "$1.20 so far");
135});
136
137const row = (over: Partial<AttentionRow>): AttentionRow => ({
138 key: "k",
139 kind: "chat",
140 title: "t",
141 detail: "d",
142 stake: null,
143 action: { label: "Open", to: "/x" },
144 owner: null,
145 at: NOW,
146 from: "chat",
147 ...over,
148});
149
150test("Start here: the most pressing tier first, then what has waited longest", () => {
151 const rows = [
152 row({ key: "chat:old", kind: "chat", at: NOW - 5 * DAY, action: { label: "Reply", to: "/acme/-/chat/general" } }),
153 row({ key: "ready:new", kind: "ready", at: NOW - HOUR, action: { label: "Merge", to: "/acme/web/pull/2" } }),
154 row({ key: "ready:old", kind: "ready", at: NOW - 3 * HOUR, action: { label: "Merge", to: "/acme/web/pull/1" } }),
155 row({ key: "session:s", kind: "session_cap", at: NOW - 10 * 60_000, action: { label: "Approve more", to: "/acme/-/agents/margo/sessions/s" } }),
156 ];
157 const ranked = rankAttention(rows);
158 assert.deepEqual(ranked.map((r) => r.key), ["session:s", "ready:old", "ready:new", "chat:old"]);
159 const start = startHere(ranked);
160 assert.equal(start?.row.key, "session:s");
161 assert.match(start?.why ?? "", /spend cap/);
162 // A failed production deploy beats everything.
163 const withDeploy = rankAttention([...rows, row({ key: "deploy:site", kind: "deploy", at: NOW, action: { label: "See the build", to: "/acme/site/deployments/1" } })]);
164 assert.equal(startHere(withDeploy)?.row.key, "deploy:site");
165 // Ties in tier say so.
166 const two = startHere(rankAttention(rows.filter((r) => r.kind === "ready")));
167 assert.equal(two?.row.key, "ready:old");
168 assert.match(two?.why ?? "", /Of the 2 like it, it has waited longest/);
169 assert.equal(startHere([]), null);
170});
171
172test("the same order whatever order the rows came in", () => {
173 const rows = [row({ key: "b", kind: "review", at: NOW }), row({ key: "a", kind: "review", at: NOW, action: { label: "Review", to: "/y" } })];
174 assert.deepEqual(rankAttention(rows).map((r) => r.key), ["a", "b"]);
175 assert.deepEqual(rankAttention([...rows].reverse()).map((r) => r.key), ["a", "b"]);
176});
177
178const item = (over: Partial<InboxItem>): InboxItem => ({
179 id: "n1",
180 reason: "ci_activity",
181 severity: "error",
182 title: "Checks failed on #12",
183 body: "Add retries",
184 event: "check.failed",
185 repo: "acme/web",
186 workspace: "acme",
187 subject: "pull",
188 number: 12,
189 url: "/acme/web/pull/12",
190 actor: "g1t",
191 count: 1,
192 createdAt: iso(NOW - HOUR),
193 updatedAt: iso(NOW - HOUR),
194 readAt: null,
195 doneAt: null,
196 saved: false,
197 snoozedUntil: null,
198 ...over,
199});
200
201test("notifications: warnings and failures in this workspace; none about repositories without Code", () => {
202 assert.equal(notificationRow(item({}), "acme", true)?.kind, "notification");
203 assert.equal(notificationRow(item({}), "acme", false), null);
204 assert.equal(notificationRow(item({ repo: null, url: "/acme/-/agents/margo" }), "acme", false)?.kind, "notification");
205 assert.equal(notificationRow(item({ severity: "info" }), "acme", true), null);
206 assert.equal(notificationRow(item({ workspace: "other", repo: "other/web" }), "acme", true), null);
207 assert.equal(notificationRow(item({ readAt: iso(NOW) }), "acme", true), null);
208});
209
210test("a notification about something already listed is left out", () => {
211 const out = attention({
212 slug: "acme",
213 code: true,
214 codeNeeds: [need({})],
215 capped: [],
216 agents: [],
217 canManage: false,
218 notifications: [item({})],
219 chat: [],
220 });
221 assert.deepEqual(out.rows.map((r) => r.key), ["pull:p1"]);
222 assert.deepEqual(out.missing, []);
223});
224
225test("without Code access no Code row is asked for or shown, and a silent source is named", () => {
226 const out = attention({
227 slug: "acme",
228 code: false,
229 codeNeeds: [need({})],
230 capped: null,
231 agents: null,
232 canManage: false,
233 notifications: [item({}), item({ id: "n2", repo: null, reason: "agent", severity: "warning", url: "/acme/-/agents/margo" })],
234 chat: null,
235 });
236 assert.deepEqual(out.rows.map((r) => r.key), ["notification:n2"]);
237 assert.equal(out.rows.some((r) => r.from === "code"), false);
238 assert.deepEqual(out.missing, ["Agents", "Chat"]);
239});
240
241test("an agent out of budget is listed only for those who may raise it", () => {
242 const agent = { id: "a1", handle: "margo", display_name: "Margo", status: "out_of_budget", spent_month_micros: 52_000_000, budget: { monthly_micros: 50_000_000 }, updated_at: iso(NOW) };
243 assert.equal(agentRow(agent, "acme")?.stake?.text, "$0.00 left");
244 const base = { slug: "acme", code: true, codeNeeds: [], capped: [], notifications: [], chat: [] };
245 assert.equal(attention({ ...base, agents: [agent], canManage: false }).rows.length, 0);
246 assert.equal(attention({ ...base, agents: [agent], canManage: true }).rows[0]?.kind, "agent_budget");
247});
248
249test("chat: mentions anywhere and unread direct messages, never muted", () => {
250 const channel = { id: "c1", kind: "channel" as const, name: "general", last_message_at: iso(NOW) };
251 assert.equal(chatRow({ channel, title: "general", muted: false, unread: 4, mentions: 0 }, "acme"), null);
252 assert.equal(chatRow({ channel, title: "general", muted: false, unread: 4, mentions: 2 }, "acme")?.stake?.text, "2 mentions");
253 assert.equal(chatRow({ channel, title: "general", muted: true, unread: 4, mentions: 2 }, "acme"), null);
254 const dm = chatRow({ channel: { ...channel, kind: "dm", name: null }, title: "Bo Reed", muted: false, unread: 1, mentions: 0 }, "acme");
255 assert.equal(dm?.action.to, "/acme/-/chat/dm/c1");
256});
257
258// The span is NOW back to 3 days ago unless a test says otherwise.
259const SPAN = { from: NOW - 3 * DAY, now: NOW };
260
261test("a day is the reader's day", () => {
262 assert.equal(dayIn(NOW, LA), "2026-10-09");
263 assert.equal(dayIn(Date.parse("2026-10-10T02:00:00Z"), LA), "2026-10-09");
264 assert.equal(dayIn(Date.parse("2026-10-10T02:00:00Z"), null), "2026-10-10");
265 assert.equal(dayIn(NOW, "Not/AZone"), "2026-10-09");
266});
267
268test("since your last visit, in words, in the reader's time zone", () => {
269 // NOW is Friday 11:00 in Los Angeles.
270 assert.equal(sinceWords(NOW - 20_000, NOW, LA), "a moment ago");
271 assert.equal(sinceWords(NOW - 40 * 60_000, NOW, LA), "40 minutes ago");
272 assert.equal(sinceWords(NOW - HOUR - 60_000, NOW, LA), "an hour ago");
273 assert.equal(sinceWords(NOW - 2 * HOUR, NOW, LA), "2 hours ago");
274 // 07:00 Friday.
275 assert.equal(sinceWords(NOW - 4 * HOUR, NOW, LA), "this morning");
276 // 02:00 Friday is Thursday night.
277 assert.equal(sinceWords(NOW - 9 * HOUR, NOW, LA), "last night");
278 // 19:00 Thursday.
279 assert.equal(sinceWords(NOW - 16 * HOUR, NOW, LA), "yesterday evening");
280 // 19:00 Tuesday.
281 assert.equal(sinceWords(NOW - 2 * DAY - 16 * HOUR, NOW, LA), "Tuesday evening");
282 // 02:00 Wednesday is Tuesday night.
283 assert.equal(sinceWords(NOW - 2 * DAY - 9 * HOUR, NOW, LA), "Tuesday night");
284 assert.equal(sinceWords(NOW - 9 * DAY, NOW, LA), "Sep 30");
285});
286
287test("how long a span is", () => {
288 assert.equal(duration(30_000), "a moment");
289 assert.equal(duration(45 * 60_000), "45 minutes");
290 assert.equal(duration(HOUR), "1 hour");
291 assert.equal(duration(3 * DAY + HOUR), "3 days");
292 assert.equal(duration(15 * DAY), "2 weeks");
293});
294
295test("the span: since your last visit, capped, or a fixed window", () => {
296 assert.equal(parseWindow("24h"), "24h");
297 assert.equal(parseWindow("7d"), "7d");
298 assert.equal(parseWindow("nonsense"), "last");
299 assert.equal(parseWindow(null), "last");
300 const away = spanFor("last", NOW, NOW - 2 * DAY - 16 * HOUR, LA);
301 assert.equal(away.from, NOW - 2 * DAY - 16 * HOUR);
302 assert.equal(away.words, "Since Tuesday evening");
303 assert.equal(away.length, "2 days");
304 assert.equal(away.note, null);
305 // Never visited: the last 24 hours, and it says why.
306 const first = spanFor("last", NOW, null, LA);
307 assert.equal(first.from, NOW - DAY);
308 assert.equal(first.note, "first");
309 assert.equal(spanFor("last", NOW, undefined, LA).note, "unknown");
310 // Away a month: 14 days at most.
311 const capped = spanFor("last", NOW, NOW - 30 * DAY, LA);
312 assert.equal(capped.from, NOW - 14 * DAY);
313 assert.equal(capped.note, "capped");
314 assert.equal(capped.words, "In the last 14 days");
315 // The fixed windows ignore the last visit.
316 assert.equal(spanFor("24h", NOW, NOW - 3 * DAY, LA).from, NOW - DAY);
317 assert.deepEqual([spanFor("7d", NOW, NOW - HOUR, LA).from, spanFor("7d", NOW, NOW - HOUR, LA).words], [NOW - 7 * DAY, "In the last 7 days"]);
318});
319
320test("an agent's pull request is accepted first time when it merged in the span with no revision", () => {
321 assert.equal(pullOutcome(pull(), 0, SPAN), "accepted");
322 assert.equal(pullOutcome(pull(), 2, SPAN), "fixed");
323 assert.equal(pullOutcome(pull({ mergedAt: iso(NOW - 4 * DAY) }), 0, SPAN), null);
324 assert.equal(pullOutcome(pull({ status: "closed", mergedAt: null }), 0, SPAN), "dropped");
325 // Still open is not settled: it is under Running now.
326 assert.equal(pullOutcome(pull({ status: "open", mergedAt: null }), 1, SPAN), null);
327});
328
329test("a session counts once, at the root of its tree, when it settled in the span", () => {
330 assert.equal(sessionOutcome(session(), SPAN), "finished");
331 assert.equal(sessionOutcome(session({ parent_id: "s0" }), SPAN), null);
332 assert.equal(sessionOutcome(session({ status: "failed" }), SPAN), "dropped");
333 assert.equal(sessionOutcome(session({ status: "stopped" }), SPAN), "dropped");
334 assert.equal(sessionOutcome(session({ status: "working", finished_at: null }), SPAN), null);
335 assert.equal(sessionOutcome(session({ finished_at: iso(NOW - 4 * DAY) }), SPAN), null);
336});
337
338test("accepted first time is accepted over every agent pull request that settled", () => {
339 assert.deepEqual(acceptance(["accepted", "accepted", "fixed", "dropped", "finished"]), { value: 0.5, of: 4 });
340 assert.equal(acceptance(["finished"]), null);
341});
342
343test("the span's work: pull requests and sessions, sources, and the 7 days before", () => {
344 const work = spanWork({
345 span: SPAN,
346 slug: "acme",
347 code: {
348 pulls: [
349 pull({ number: 1 }),
350 pull({ number: 2 }),
351 pull({ number: 3, status: "open", mergedAt: null }),
352 // A person's pull request is not agent work.
353 pull({ number: 4, author: { username: "bo", kind: "user" } }),
354 // The 7 days before the span: one accepted, one fixed.
355 pull({ number: 5, mergedAt: iso(NOW - 4 * DAY), updatedAt: iso(NOW - 4 * DAY) }),
356 pull({ number: 6, mergedAt: iso(NOW - 5 * DAY), updatedAt: iso(NOW - 5 * DAY) }),
357 // Older than that: not counted.
358 pull({ number: 7, mergedAt: iso(NOW - 11 * DAY), updatedAt: iso(NOW - 11 * DAY) }),
359 ],
360 revisions: { "acme/web#2": 1, "acme/web#6": 2 },
361 complete: true,
362 },
363 sessions: { sessions: [session(), session({ id: "s2", kind: "routine", status: "working", finished_at: null }), session({ id: "s3", parent_id: "s1" })], complete: true },
364 });
365 assert.equal(work.tasks.length, 3);
366 assert.deepEqual(work.counts, { accepted: 1, fixed: 1, finished: 1, dropped: 0 });
367 assert.deepEqual(work.rate, { value: 0.5, of: 2 });
368 assert.deepEqual(work.before, { value: 0.5, of: 2 });
369 assert.deepEqual(work.sources, [
370 { source: "code", count: 2 },
371 { source: "chat", count: 1 },
372 ]);
373 assert.equal(work.code, "read");
374 assert.equal(work.partial, false);
375});
376
377test("no trend when the 7 days before could not all be read, and none without Code", () => {
378 const partial = spanWork({ span: SPAN, slug: "acme", code: { pulls: [pull()], revisions: {}, complete: false }, sessions: null });
379 assert.equal(partial.before, null);
380 assert.equal(partial.partial, true);
381 assert.equal(partial.sessions, "unavailable");
382 const member = spanWork({ span: SPAN, slug: "acme", code: "no_access", sessions: { sessions: [session()], complete: true } });
383 assert.equal(member.code, "no_access");
384 assert.equal(member.rate, null);
385 assert.equal(member.tasks.every((t) => t.source !== "code"), true);
386});
387
388test("the trend is in whole points", () => {
389 assert.equal(trendLabel({ value: 0.89 }, { value: 0.83 }), "+6 pts vs the 7 days before");
390 assert.equal(trendLabel({ value: 0.5 }, { value: 0.75 }), "−25 pts vs the 7 days before");
391 assert.equal(trendLabel({ value: 0.5 }, { value: 0.5 }), "Same as the 7 days before");
392 assert.equal(trendLabel(null, { value: 0.5 }), null);
393});
394
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.395// --- People and agents ------------------------------------------------------------
396
397const ANA = "user:usr_ana";
398const BO = "user:usr_bo";
399const CY = "user:usr_cy";
400const MARGO = "agent:agt_margo";
401const G1T = "user:usr_g1t_agent";
402const NAMES = { [ANA]: "ana", [BO]: "bo", [CY]: "cy", [MARGO]: "margo" };
403const REPOS = {
404 rep_web: { namespace: "acme", name: "web", defaultBranch: "main" },
405 rep_api: { namespace: "acme", name: "api", defaultBranch: "main" },
406 rep_docs: { namespace: "acme", name: "docs", defaultBranch: "main" },
407 rep_cli: { namespace: "acme", name: "cli", defaultBranch: "trunk" },
408};
409
410const count = (actor: string, n: number): ActorCount => ({ actor, count: n });
411const quietRepo = (repo_id: string): RepoDigest => ({
412 repo_id,
413 pushes: { count: 0, commits: 0, branches: [], by: [], default_branch: { count: 0, commits: 0, by: [], last_at: null } },
414 pulls: { opened: [], merged: [], closed: [] },
415 issues: { opened: [], closed: [] },
416 reviews: [],
417 comments: [],
418 deployments: { succeeded: [], failed: [], production: 0 },
419 releases: [],
420 packages: [],
421});
422
423/**
424 * A day on a busy workspace: 3 people pushed 41 commits to 4 projects,
425 * merged 5 pull requests and opened 2; agents (g1t and Margo) pushed 6,
426 * opened 3 pull requests and reviewed 2; 2 production deploys went out;
427 * 120 messages were sent, 15 of them Margo's; 7 docs were edited.
428 */
429function busyDay(): ActivityDigest {
430 return {
431 from: iso(SPAN.from),
432 until: iso(SPAN.now),
433 complete: true,
434 repos: [
435 {
436 ...quietRepo("rep_web"),
437 pushes: {
438 count: 14,
439 commits: 33,
440 branches: ["fix-login", "main"],
441 by: [
442 { actor: ANA, count: 8, commits: 24 },
443 { actor: BO, count: 4, commits: 5 },
444 { actor: G1T, count: 2, commits: 4 },
445 ],
446 default_branch: { count: 3, commits: 9, by: [{ actor: ANA, count: 3, commits: 9 }], last_at: iso(NOW - 2 * HOUR) },
447 },
448 pulls: { opened: [count(ANA, 1), count(G1T, 2), count(MARGO, 1)], merged: [count(ANA, 3), count(BO, 1)], closed: [] },
449 issues: { opened: [count(BO, 2)], closed: [count(ANA, 1), count(G1T, 1)] },
450 reviews: [count(BO, 2), count(G1T, 2)],
451 comments: [count(ANA, 4), count(MARGO, 3)],
452 deployments: { succeeded: [count(ANA, 2)], failed: [], production: 2 },
453 releases: [{ tag: "v1.4.0", name: "Autumn", actor: ANA, at: iso(NOW - 3 * HOUR) }],
454 packages: [],
455 },
456 {
457 ...quietRepo("rep_api"),
458 pushes: { count: 5, commits: 8, branches: ["main"], by: [{ actor: CY, count: 5, commits: 8 }], default_branch: { count: 5, commits: 8, by: [{ actor: CY, count: 5, commits: 8 }], last_at: iso(NOW - 5 * HOUR) } },
459 pulls: { opened: [count(CY, 1)], merged: [count(CY, 1)], closed: [] },
460 packages: [{ ecosystem: "container", name: "api", version: "1.4.0", actor: G1T, at: iso(NOW - HOUR) }],
461 },
462 {
463 ...quietRepo("rep_docs"),
464 pushes: { count: 2, commits: 2, branches: ["typo"], by: [{ actor: BO, count: 2, commits: 2 }], default_branch: { count: 0, commits: 0, by: [], last_at: null } },
465 },
466 {
467 ...quietRepo("rep_cli"),
468 pushes: { count: 3, commits: 4, branches: ["trunk"], by: [{ actor: ANA, count: 1, commits: 2 }, { actor: MARGO, count: 2, commits: 2 }], default_branch: { count: 3, commits: 4, by: [{ actor: ANA, count: 1, commits: 2 }, { actor: MARGO, count: 2, commits: 2 }], last_at: iso(NOW - 30 * 60_000) } },
469 },
470 ],
471 folios: {
472 created: [count(ANA, 1)],
473 edited_count: 7,
474 edited: [
475 { folio_id: "fol_1", kind: "doc", authors: [ANA, MARGO] },
476 { folio_id: "fol_2", kind: "doc", authors: [ANA] },
477 { folio_id: "fol_3", kind: "doc", authors: [BO] },
478 { folio_id: "fol_4", kind: "doc", authors: [BO] },
479 { folio_id: "fol_5", kind: "doc", authors: [CY] },
480 { folio_id: "fol_6", kind: "doc", authors: [CY] },
481 { folio_id: "fol_7", kind: "doc", authors: [MARGO] },
482 ],
483 },
484 };
485}
486
487const busyChat: ChatActivity = {
488 from: iso(SPAN.from),
489 until: iso(SPAN.now),
490 messages: 120,
491 channels: 5,
492 authors: [
493 { key: ANA, messages: 60 },
494 { key: BO, messages: 30 },
495 { key: CY, messages: 15 },
496 { key: MARGO, messages: 15 },
497 ],
498};
499
500/** Seven agent tasks that settled well, and one that did not. */
501function busyWork() {
502 return spanWork({
503 span: SPAN,
504 slug: "acme",
505 code: {
506 pulls: [1, 2, 3, 4, 5].map((n) => pull({ number: n, mergedAt: iso(NOW - n * HOUR), updatedAt: iso(NOW - n * HOUR) })).concat(pull({ number: 6, status: "closed", mergedAt: null })),
507 revisions: { "acme/web#2": 1 },
508 complete: true,
509 },
510 sessions: { sessions: [session(), session({ id: "s2", kind: "routine" })], complete: true },
511 });
512}
513
514test("an actor is an agent when it is one of the workspace's agents or g1t; everyone else is a person", () => {
515 assert.equal(actorKind(MARGO), "agent");
516 assert.equal(actorKind(G1T), "agent");
Four small things seen today. An agent can attach a file it made to any doc the person who asked can edit, whatever the space's agent mode: attaching changes nothing in the doc, so the suggest mode that stopped every PDF and spreadsheet no longer does; the ability is its own, attach, beside read, suggest and edit. Home no longer counts robots among the people: events that name an agent by its own id, or g1t's upkeep as the workspace or as g1t, read as the agent and as g1t, in the digest and on the page, so the faces say who instead of someone. Tooltips are the inverted bubble, the page's foreground behind the page's background, with no border. The Spend page's slice tabs keep their ring inside the row instead of losing its top to the scroll edge, and the Apps launcher keeps its search box and footer in place while it reads which apps there are, with skeleton tiles between.517 assert.equal(actorKind("user:agt_01abc"), "agent", "an agent named by its own id under user: is still a robot");
518 assert.equal(actorKind("user:wsp_01abc"), "agent", "the workspace's own upkeep is g1t's");
519 assert.equal(actorKind("user:g1t"), "agent");
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.520 assert.equal(actorKind(ANA), "person");
521 assert.equal(actorKind(""), "person");
522 assert.equal(actorKind("user:usr_bot", new Set(["user:usr_bot"])), "agent");
523});
524
525test("the digest: what people did, in lines with counts, and who", () => {
526 const digest = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: busyChat, names: NAMES, work: busyWork(), code: true });
527 assert.deepEqual(
528 digest.people.lines.map((line) => [line.text, line.to]),
529 [
530 ["3 people pushed 41 commits to 4 projects", "/acme/-/repositories"],
531 ["opened 2 pull requests, merged 5", "/acme/-/repositories"],
532 ["reviewed 2 pull requests", "/acme/web/pulls"],
533 ["opened 2 issues, closed 1", "/acme/web/issues"],
534 ["left 4 comments", "/acme/web/issues"],
535 ["2 deploys went out", "/acme/web/deployments"],
536 ["published 1 release", "/acme/web/releases"],
537 ["created 1 doc, edited 6 docs", "/acme/-/artifacts"],
538 ["sent 105 messages in 5 conversations", "/acme/-/chat"],
539 ],
540 );
541 // Faces: most active first, by name.
542 assert.deepEqual(
543 digest.people.actors.map((actor) => [actor.name, actor.kind]),
544 [
545 ["ana", "person"],
546 ["bo", "person"],
547 ["cy", "person"],
548 ],
549 );
550 assert.deepEqual(digest.missing, []);
551 assert.equal(digest.partial, false);
552});
553
554test("the digest: what agents did, their tasks first, and g1t and the workspace's agents as agents", () => {
555 const digest = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: busyChat, names: NAMES, work: busyWork(), code: true });
556 assert.deepEqual(
557 digest.agents.lines.map((line) => line.text),
558 [
559 "finished 7 tasks, 1 didn't finish",
560 "ran 2 sessions",
561 "pushed 6 commits to 2 projects",
562 "opened 3 pull requests",
563 "reviewed 2 pull requests",
564 "closed 1 issue",
565 "left 3 comments",
566 "published 1 package version",
567 "edited 2 docs",
568 "replied 15 times in chat",
569 ],
570 );
571 assert.deepEqual(
572 digest.agents.actors.map((actor) => [actor.name, actor.kind, actor.key]),
573 [
574 ["margo", "agent", MARGO],
575 ["g1t", "agent", G1T],
576 ],
577 );
578 assert.deepEqual(digest.summary, { people: 3, commits: 41, merged: 5, finished: 7, dropped: 1, said: 105, anything: true, peopleRead: true });
579});
580
581test("the digest says which part didn't answer, and counts what could be read", () => {
582 const noChat = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: null, names: NAMES, work: busyWork(), code: true });
583 assert.deepEqual(noChat.missing, ["Chat"]);
584 assert.equal(noChat.people.lines.some((line) => line.key === "chat"), false);
585 const noCode = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: busyChat, names: NAMES, work: null, code: true });
586 assert.deepEqual(noCode.missing, ["Code", "Agents"]);
587 assert.deepEqual(
588 noCode.people.lines.map((line) => line.text),
589 ["sent 105 messages in 5 conversations"],
590 );
591 assert.equal(noCode.summary.peopleRead, true);
592 // Without Code access, events are read for artifacts only: no repositories, nothing missing.
593 const member = digestOf({ slug: "acme", repos: {}, activity: { ...busyDay(), repos: [] }, chat: busyChat, names: NAMES, work: busyWork(), code: false });
594 assert.deepEqual(member.missing, []);
595 assert.deepEqual(
596 member.people.lines.map((line) => line.key),
597 ["docs", "chat"],
598 );
599 // A digest cut short says so.
600 const cut = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), complete: false }, chat: busyChat, names: NAMES, work: busyWork(), code: true });
601 assert.equal(cut.partial, true);
602 // Nobody named: a mirror's pushes are still commits.
603 const mirror = digestOf({
604 slug: "acme",
605 repos: REPOS,
606 activity: { ...busyDay(), folios: null, repos: [{ ...quietRepo("rep_web"), pushes: { count: 2, commits: 7, branches: ["main"], by: [{ actor: "", count: 2, commits: 7 }], default_branch: { count: 2, commits: 7, by: [{ actor: "", count: 2, commits: 7 }], last_at: iso(NOW - HOUR) } } }] },
607 chat: null,
608 names: {},
609 work: null,
610 code: true,
611 });
612 assert.deepEqual(
613 mirror.people.lines.map((line) => [line.text, line.to]),
614 [["7 commits pushed to 1 project", "/acme/web/activity"]],
615 );
616 assert.equal(mirror.summary.people, 0);
617});
618
619test("the sentence: people first, then the agents' tasks, then what landed; honest when quiet or unread", () => {
620 const digest = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: busyChat, names: NAMES, work: busyWork(), code: true });
621 assert.equal(
622 spanSentence({ words: "Since Tuesday evening" }, digest, 12),
623 "Since Tuesday evening, 3 people pushed 41 commits and merged 5 changes, agents finished 7 tasks, and 12 changes landed.",
624 );
625 const quiet = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), repos: [], folios: { created: [], edited: [], edited_count: 0 } }, chat: { ...busyChat, messages: 0, channels: 0, authors: [] }, names: {}, work: busyWork(), code: true });
626 quiet.summary.finished = 0;
627 quiet.summary.dropped = 0;
628 quiet.summary.anything = false;
629 assert.equal(spanSentence({ words: "Since Tuesday evening" }, quiet, 0), "Quiet since Tuesday evening: nothing pushed, merged, deployed or said.");
630 assert.equal(spanSentence({ words: "In the last 24 hours" }, quiet, 0), "Quiet in the last 24 hours: nothing pushed, merged, deployed or said.");
631 // Only agents' tasks and merges.
632 const agentsOnly = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), repos: [], folios: null }, chat: { ...busyChat, authors: [], messages: 0, channels: 0 }, names: {}, work: busyWork(), code: true });
633 assert.equal(spanSentence({ words: "Since this morning" }, agentsOnly, 5), "Since this morning, agents finished 7 tasks and 5 changes landed.");
634 // People only talked.
635 const talked = digestOf({ slug: "acme", repos: REPOS, activity: { ...busyDay(), repos: [], folios: null }, chat: busyChat, names: NAMES, work: null, code: true });
636 assert.equal(spanSentence({ words: "Since an hour ago" }, talked, 0), "Since an hour ago, 3 people sent 105 messages.");
637 // Neither events nor chat answered.
638 const unread = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: null, names: {}, work: busyWork(), code: true });
639 assert.equal(spanSentence({ words: "Since this morning" }, unread, 5), "Since this morning, agents finished 7 tasks and 5 changes landed; what people did couldn't be read.");
640 const nothingRead = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: null, names: {}, work: null, code: true });
641 assert.equal(spanSentence({ words: "Since this morning" }, nothingRead, 0), "Since this morning, what people did couldn't be read, and no agent task settled.");
642 assert.equal(spanSentence({ words: "Since this morning" }, null, null), "Since this morning, what happened couldn't be read.");
The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.643 assert.equal(waitingSentence(0), "Nothing needs you right now.");
644 assert.equal(waitingSentence(1), "1 thing needs you.");
645 assert.equal(waitingSentence(8), "8 things need you.");
646});
647
648test("what landed: every merge in the span, a person's or an agent's, newest first", () => {
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.649 const merged = landedIn(
The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.650 [
651 pull({ number: 1, mergedAt: iso(NOW - 2 * DAY), mergedBy: "bo" }),
652 pull({ number: 2, author: { username: "bo", kind: "user" }, mergedAt: iso(NOW - HOUR) }),
653 pull({ number: 3, mergedAt: iso(NOW - 5 * DAY) }),
654 pull({ number: 4, status: "open", mergedAt: null }),
655 ],
656 SPAN,
657 );
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.658 assert.deepEqual(merged.map((change) => [change.kind, change.title, change.detail, change.agent]), [
659 ["merge", "Fix the flaky test", "web#2 · by bo", false],
660 ["merge", "Fix the flaky test", "web#1 · by @g1t, merged by bo", true],
The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.661 ]);
662});
663
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.664test("what landed, besides merges: direct pushes, releases, packages and production builds, newest first", () => {
665 const fromActivity = landedFromActivity(busyDay(), REPOS, NAMES, "acme");
666 assert.deepEqual(
667 fromActivity.map((change) => [change.kind, change.title, change.detail, change.to, change.agent]),
668 [
669 ["push", "9 commits pushed to main", "web · 3 pushes by ana", "/acme/web/activity", false],
670 ["release", "v1.4.0 · Autumn", "web · released by ana", "/acme/web/releases/tag/v1.4.0", false],
671 ["push", "8 commits pushed to main", "api · 5 pushes by cy", "/acme/api/activity", false],
672 ["package", "api 1.4.0", "container package · published by @g1t", "/acme/-/packages", true],
673 ["push", "4 commits pushed to trunk", "cli · 3 pushes by ana and @margo", "/acme/cli/activity", false],
674 ],
675 );
676 const builds = landedDeploys([
Notifications is the bell in the top bar alone: the rail no longer carries it, and the notifications guide points at the bell; a phone keeps it in the bottom bar, which has no bell. Home names who made a deployment, by username or by an agent's handle, instead of showing the account's id, and says someone when identity doesn't know the id. The Apps launcher's skeleton tiles are drawn in the line colour so they show on its raised surface.677 { key: "deploy:site:d1", project: "site", kind: "production", outcome: "live", commit: "abcdef1", branch: null, at: NOW - HOUR, to: "/acme/site/deployments/d1", by: "@g1t", agent: true },
678 { key: "deploy:site:d2", project: "site", kind: "preview", outcome: "live", commit: "abcdef2", branch: "fix", at: NOW - HOUR, to: "/acme/site/deployments/d2", by: "bo", agent: false },
679 { key: "deploy:site:d3", project: "site", kind: "production", outcome: "failed", commit: "abcdef3", branch: null, at: NOW - HOUR, to: "/acme/site/deployments/d3", by: "bo", agent: false },
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.680 ]);
681 assert.deepEqual(builds.map((change) => [change.kind, change.title, change.agent]), [["deploy", "Production of site went live", true]]);
682 const all = landed([fromActivity, builds]);
683 assert.deepEqual(
684 all.map((change) => change.key),
685 ["push:rep_cli", "landed:deploy:site:d1", "package:rep_api:container:api:1.4.0", "push:rep_web", "release:rep_web:v1.4.0", "push:rep_api"],
686 );
687});
688
The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.689const deploy = (over: Partial<Deployment>): Deployment => ({
690 id: "d1",
691 kind: "production",
692 branch: null,
693 number: null,
694 commit: "abcdef1234",
695 status: "ready",
696 url: "https://site.g1t.page",
697 error: null,
698 warnings: [],
699 buildSeconds: 40,
700 createdBy: "bo",
701 createdAt: iso(NOW - 2 * HOUR),
702 finishedAt: iso(NOW - HOUR),
703 ...over,
704});
705
706test("builds that finished in the span: live or failed, never skipped", () => {
707 const rows = deploysIn(
708 [
709 {
710 slug: "site",
711 deployments: [
712 deploy({}),
713 deploy({ id: "d2", status: "replaced", finishedAt: iso(NOW - DAY) }),
714 deploy({ id: "d3", status: "failed", finishedAt: iso(NOW - 2 * DAY) }),
715 deploy({ id: "d4", status: "skipped", finishedAt: iso(NOW - HOUR) }),
716 deploy({ id: "d5", status: "building", finishedAt: null }),
717 deploy({ id: "d6", finishedAt: iso(NOW - 5 * DAY) }),
718 ],
719 },
720 ],
721 "acme",
722 SPAN,
Notifications is the bell in the top bar alone: the rail no longer carries it, and the notifications guide points at the bell; a phone keeps it in the bottom bar, which has no bell. Home names who made a deployment, by username or by an agent's handle, instead of showing the account's id, and says someone when identity doesn't know the id. The Apps launcher's skeleton tiles are drawn in the line colour so they show on its raised surface.723 { "user:bo": "bo" },
The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.724 );
725 assert.deepEqual(rows.map((row) => [row.key, row.outcome]), [
726 ["deploy:site:d1", "live"],
727 ["deploy:site:d2", "live"],
728 ["deploy:site:d3", "failed"],
729 ]);
730 assert.equal(rows[0]!.to, "/acme/site/deployments/d1");
731 assert.equal(rows[0]!.commit, "abcdef1");
Notifications is the bell in the top bar alone: the rail no longer carries it, and the notifications guide points at the bell; a phone keeps it in the bottom bar, which has no bell. Home names who made a deployment, by username or by an agent's handle, instead of showing the account's id, and says someone when identity doesn't know the id. The Apps launcher's skeleton tiles are drawn in the line colour so they show on its raised surface.732 // The maker is named, never shown as an id; an agent's by its handle; an id identity doesn't know as someone.
733 assert.equal(rows[0]!.by, "bo");
734 assert.equal(rows[0]!.agent, false);
735 const unknown = deploysIn([{ slug: "site", deployments: [deploy({ createdBy: "usr_zzz" })] }], "acme", SPAN);
736 assert.equal(unknown[0]!.by, "someone");
737 const robot = deploysIn([{ slug: "site", deployments: [deploy({ createdBy: "agt_01x" })] }], "acme", SPAN, { "user:agt_01x": "otto" });
738 assert.deepEqual([robot[0]!.by, robot[0]!.agent], ["@otto", true]);
The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.739});
740
741const request = (over: Partial<InstallRequest>): InstallRequest => ({
742 id: "r1",
743 listing: "integration:sentry",
744 kind: "integration",
745 name: "Sentry",
746 note: "For the error alerts",
747 requested_by: "bo",
748 requested_at: iso(NOW - 2 * DAY),
749 status: "open",
750 resolved_by: null,
751 resolved_at: null,
752 ...over,
753});
754
755const memory = (over: Partial<Memory>): Memory => ({
756 id: "m1",
757 scope: "workspace",
758 workspace: "acme",
759 repo: null,
760 text: "Use Postgres for the ledger",
761 kind: "decision",
762 source: { kind: "person" } as Memory["source"],
763 createdBy: "ada",
764 pinned: false,
765 createdAt: iso(NOW - DAY),
766 updatedAt: iso(NOW - DAY),
767 lastUsedAt: null,
768 ...over,
769});
770
771test("decisions: kept decisions in memory and requests answered, in the span", () => {
772 const rows = decisionsIn(
773 {
774 memories: [memory({}), memory({ id: "m2", kind: "fact" }), memory({ id: "m3", status: "candidate" }), memory({ id: "m4", createdAt: iso(NOW - 6 * DAY) })],
775 requests: [request({}), request({ id: "r2", status: "declined", resolved_by: "ada", resolved_at: iso(NOW - HOUR) })],
776 },
777 "acme",
778 SPAN,
779 );
780 assert.deepEqual(rows.map((row) => row.key), ["request:r2", "memory:m1"]);
781 assert.equal(rows[0]!.text, "Turned down Sentry, which bo asked for");
782});
783
784test("an open install request needs an owner; answered ones don't", () => {
785 assert.equal(installRequestRow(request({}), "acme")?.action.to, "/acme/-/marketplace/requests");
786 assert.equal(installRequestRow(request({ status: "done" }), "acme"), null);
787 const base = { slug: "acme", code: true, codeNeeds: [], capped: [], agents: [], canManage: true, notifications: [], chat: [] };
788 assert.equal(attention({ ...base, requests: [request({})] }).rows[0]?.kind, "install_request");
789 assert.equal(attention({ ...base, requests: null }).rows.length, 0);
790});
791
792test("the workspace's agent budget used up comes first, for those who may raise it", () => {
793 assert.equal(limitRow({ alert: 90, spentMicros: 1, since: NOW }, "acme"), null);
794 const limit = { alert: 100, spentMicros: 500_000_000, since: NOW - DAY };
795 assert.equal(limitRow(limit, "acme")?.action.to, "/acme/-/spend");
796 const base = { slug: "acme", code: true, codeNeeds: [need({})], capped: [], agents: [], notifications: [], chat: [], limit };
797 assert.equal(attention({ ...base, canManage: true }).start?.row.kind, "limit");
798 assert.equal(attention({ ...base, canManage: false }).rows.some((r) => r.kind === "limit"), false);
799});
800
801test("running now: live root sessions, agents' changes and builds going; a capped session is under Needs you", () => {
802 const sessions = runningSessions(
803 [
804 session({ id: "a", status: "working", finished_at: null, created_at: iso(NOW - HOUR) }),
805 session({ id: "b", status: "needs_approval", finished_at: null }),
806 session({ id: "c", status: "queued", finished_at: null, parent_id: "a" }),
807 session({ id: "d", status: "done" }),
808 session({ id: "e", status: "queued", finished_at: null, visible: false, created_at: iso(NOW - 2 * HOUR) }),
809 ],
810 "acme",
811 );
812 assert.deepEqual(sessions.map((row) => [row.key, row.status]), [
813 ["session:a", "Working"],
814 ["session:e", "Queued"],
815 ]);
816 assert.equal(sessions[1]!.title, "A session in a conversation you're not in");
817 const builds = runningDeploys(
818 [
819 { slug: "site", enabled: true, production: null, previews: 0, latest: deploy({ status: "building", finishedAt: null }) },
820 { slug: "docs", enabled: true, production: null, previews: 0, latest: deploy({}) },
821 ],
822 "acme",
823 );
824 assert.deepEqual(builds.map((row) => row.status), ["Building"]);
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.825 const run = (over: Partial<WorkflowRun>): WorkflowRun => ({
826 id: "run_1",
827 workflowId: "wf_1",
828 path: ".g1t/workflows/ci.yml",
829 name: "CI",
830 title: "Fix the flaky test",
831 number: 12,
832 attempt: 1,
833 event: "push",
834 ref: "refs/heads/main",
835 sha: "abcdef1",
836 pull: null,
837 status: "in_progress",
838 conclusion: null,
839 error: null,
840 actor: "ana",
841 createdAt: iso(NOW - 10 * 60_000),
842 startedAt: iso(NOW - 9 * 60_000),
843 finishedAt: null,
844 ...over,
845 });
846 const workflows = runningWorkflows([
847 { repo, runs: [run({}), run({ id: "run_2", status: "queued", startedAt: null, title: "" }), run({ id: "run_3", status: "completed", conclusion: "success" }), run({ id: "run_4", status: "action_required" })] },
848 ]);
849 assert.deepEqual(
850 workflows.map((row) => [row.key, row.title, row.detail, row.status, row.to]),
851 [
852 ["workflow:acme/web:run_1", "CI: Fix the flaky test", "acme/web main · by ana", "Running", "/acme/web/actions/runs/run_1"],
853 ["workflow:acme/web:run_2", "CI", "acme/web main · by ana", "Queued", "/acme/web/actions/runs/run_2"],
854 ["workflow:acme/web:run_4", "CI: Fix the flaky test", "acme/web main · by ana", "Needs approval", "/acme/web/actions/runs/run_4"],
855 ],
856 );
857 // Longest-going first within a kind: sessions, then changes, then workflow runs, then builds.
858 assert.deepEqual(running([builds, workflows.slice(0, 1), sessions]).map((row) => row.key), ["session:e", "session:a", "workflow:acme/web:run_1", "deploy:site:d1"]);
The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.859});
860
861
862test("the span's months: one, or the ones it crosses", () => {
863 assert.deepEqual(spanMonths(SPAN), ["2026-10"]);
864 assert.deepEqual(spanMonths({ from: Date.parse("2026-09-28T00:00:00Z"), now: NOW }), ["2026-09", "2026-10"]);
865 assert.deepEqual(spanMonths({ from: Date.parse("2025-12-30T00:00:00Z"), now: Date.parse("2026-01-02T00:00:00Z") }), ["2025-12", "2026-01"]);
866});
867
868test("spend in the span: the UTC days it covers across months, at price, without money in", () => {
869 const september = {
870 groups: [
871 { key: "2026-09-29", label: "", chargedMicros: 0, lines: [{ kind: "Agent runs", count: 2, chargedMicros: 1_000_000, costMicros: 0 }] },
872 { key: "2026-09-20", label: "", chargedMicros: 0, lines: [{ kind: "Agent runs", count: 9, chargedMicros: 9_000_000, costMicros: 0 }] },
873 ],
874 totals: { chargedMicros: 10_000_000, paidMicros: 0, costMicros: 0, entries: 2 },
875 };
876 const october = {
877 groups: [
878 {
879 key: "2026-10-09",
880 label: "2026-10-09",
881 chargedMicros: 0,
882 lines: [
883 { kind: "Agent runs", count: 12, chargedMicros: 3_000_000, costMicros: 2_500_000, priceMicros: 4_000_000 },
884 { kind: "Sandbox time", count: 12, chargedMicros: 600_000, costMicros: 500_000 },
885 { kind: "Payments", count: 1, chargedMicros: -50_000_000, costMicros: 0, priceMicros: 0 },
886 ],
887 },
888 ],
889 totals: { chargedMicros: 12_600_000, paidMicros: 50_000_000, costMicros: 0, entries: 4, priceMicros: 13_600_000 },
890 };
Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means.891 const spend = spendIn([september, october], { from: Date.parse("2026-09-29T15:00:00Z"), now: NOW }, 13_609_000);
The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.892 assert.deepEqual([spend.from, spend.to], ["2026-09-29", "2026-10-09"]);
893 assert.equal(spend.totalMicros, 5_600_000);
894 assert.deepEqual(spend.lines.map((l) => [l.kind, l.micros, l.count]), [
895 ["Agent runs", 5_000_000, 14],
896 ["Sandbox time", 600_000, 12],
897 ]);
Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means.898 // The month so far is billing's usage report, the top bar's figure, not the statement: it carries what is not yet closed.
899 assert.equal(spend.monthMicros, 13_609_000);
900 assert.equal(spendIn([september, october], { from: Date.parse("2026-09-29T15:00:00Z"), now: NOW }, null).monthMicros, null);
The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.901});