Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

Commit

Signals fold in session cost, read the work every five minutes, and see practices spread

Spend per agent merge and spend threaded to the work now add each pull request's share of its harness sessions' cost (estimated at list price) to g1t's metered usage. A new finding and a Spreading list say when a harness catches on in a team. Stalled is three days with nothing happening, as the work service now counts it. Each person's reading of the work is kept for five minutes in the isolate and the data centre, so Home's refreshes don't recompute ten weeks each time. Part of #157

G1t-Session:
ses_01m57128wre9bvvj1y4z4b8dk0
syntaqxcommitted Parent0ba9169Browse files
6 files+269−330/6 viewed
+7−4
4949 | Figure | What it counts |
5050 | --- | --- |
5151 | **Agent-authored merges** | Pull requests that merged in the week and were an [agent's](#whose-work-is-an-agents). |
52−| **Spend per agent merge** | The week's agent [usage](/guides/usage-and-billing/#the-usage-page) at price, divided by the agent pull requests that merged in it: an average, not what any one cost. A dash when usage can't be read or nothing an agent made merged. |
53−| **Stalled changes** | Pull requests open more than seven days at the end of each week (now, for this one), neither merged nor closed. Drafts count. |
52+| **Spend per agent merge** | The week's agent [usage](/guides/usage-and-billing/#the-usage-page) at price, plus what the [sessions](/guides/sessions/) started in it cost (estimated at list price from their token usage), divided by the agent pull requests that merged in it: an average, not what any one cost. A dash when usage can't be read or nothing an agent made merged. |
53+| **Stalled changes** | Pull requests open at the end of each week (now, for this one), opened three days or more before, with nothing in the three days before: no push, comment, review, timeline event, check or session. Drafts count. |
5454 | **Agent rework rate** | Of the agent pull requests that merged in the week, the share a person asked for changes on before they merged. |
5555
5656 ### Whose work is an agent's
7474 | **Work items waiting on one unreviewed change** | An open pull request no one has reviewed is for a work item that other open work items are blocked by. The one holding up the most leads. |
7575 | **Agent drafts waiting for review** | This week's first reviews waited at least four times as long as agents took to open the pull requests (from their session starting), and at least four hours. Without that gap: first reviews took half as long again as the weeks before. |
7676 | **A milestone shipped** | A milestone closed in the last seven days, against its due date, with its weeks in build, its merged pull requests and its projects. |
77−| **Where agent spend is going** | More than a third of the quarter's agent usage by pull request went into one milestone (else one work item, else one project). |
77+| **Where agent spend is going** | More than a third of the quarter's agent spend by pull request went into one milestone (else one work item, else one project). Spend is g1t's usage on each pull request and its share of its sessions' cost. |
7878 | **Stalled changes rising** | At least three more stalled changes than last week, and a quarter more. |
7979 | **Agent rework moving** | People asked for changes on agents' merged work at least 8 points more, or less, often than over the four weeks before (with five agent merges or more). |
80+| **A practice catching on** | Two or more people started using a harness (Claude Code, Codex, Cursor and so on) this month, at least two weeks after the first person did. |
8081 | **Who works where** | Someone started merging to a project one person has carried (three quarters of its last 90 days' merges), or merged to a project for the first time this week after work elsewhere. |
8182
8283 Findings are ordered by how much they hold up: blocked work first, then
8384 waits, then what shipped and where spend went. Up to five show, and the
84−rest are one click away. The same reads, at more length, are on the
85+rest are one click away. What the work says is read again at most every
86+five minutes for each person, so a figure can be up to five minutes
87+behind. The same reads, at more length, are on the
8588 [workspace overview](/guides/workspaces/#the-workspace-overview).
8689
8790 ## Ask from Home
+6−2
683683 2. **Spend, threaded to the work**: the quarter's work in four columns:
684684 each milestone, its work items, each work item's change set in each
685685 project, and that change set's pull requests. Each box is sized by
686− what the agents' usage on its pull requests came to, and shaded by how
687− many were an agent's (**Agent use**, less to more). Choose a box to
686+ what agents cost on its pull requests (g1t's metered usage, and each
687+ pull request's share of its sessions' cost estimated at list price),
688+ and shaded by how many were an agent's (**Agent use**, less to more). Choose a box to
688689 open what is under it. Work with no milestone or work item has its own
689690 box. When usage can't be read, boxes are sized by pull requests.
690691 3. **What's moving and what's waiting**: each team's last two weeks, day
701702 started there this month.
702703 - **New ground**: people who merged to a project for the first time in
703704 the last two weeks, after three or more merges elsewhere.
705+ - **Spreading**: a harness two or more people in a team started using
706+ this month, at least two weeks after the first person did (across
707+ the workspace, when it has no teams).
704708 - **New habits**: people whose first agent-made change merged in the
705709 last two weeks, after three or more of their own.
706710
+70−4
3535 return result?.ok ? result.value : null;
3636 }
3737
38+/** How long one reading of the work is used again: Home refreshes itself often, and the weeks barely move in minutes. */
39+const KEEP_MS = 5 * 60_000;
40+/** Readings kept in the isolate at once. */
41+const KEEP_MAX = 50;
42+/** Where readings are kept in the data centre's cache: a name nothing outside can ask for. */
43+const KEEP_ORIGIN = "https://signals.g1t.internal/";
44+
45+const kept = new Map<string, { at: number; data: SignalData }>();
46+
3847 /**
39− * What the workspace's work says, for a page drawn at `now`. Null when
40− * the work service answered none of the batches: the page then says so.
48+ * What the workspace's work says, for a page drawn at `now`, read again
49+ * at most every five minutes per person (what each person can read
50+ * differs, so a reading is never shared between people). Kept in the
51+ * isolate and in the data centre's cache; only a reading every batch of
52+ * which answered is kept. Null when the work service answered none of
53+ * the batches: the page then says so.
4154 */
4255 export async function loadSignalData(viewer: User, slug: string, now: number, owned: Repo[] | null): Promise<SignalData | null> {
4356 if (!owned) return null;
57+ const key = `${viewer.id}/${slug}/${await digest(
58+ owned
59+ .map((repo) => repo.id)
60+ .sort()
61+ .join(","),
62+ )}`;
63+ const memory = kept.get(key);
64+ if (memory && now - memory.at < KEEP_MS) return memory.data;
65+ const shared = sharedCache();
66+ const url = `${KEEP_ORIGIN}${encodeURIComponent(key)}`;
67+ const found = shared ? await shared.match(url).catch(() => undefined) : undefined;
68+ if (found) {
69+ const data = (await found.json().catch(() => null)) as SignalData | null;
70+ if (data && now - data.now < KEEP_MS) {
71+ remember(key, data);
72+ return data;
73+ }
74+ }
75+ const data = await readSignalData(viewer, slug, now, owned);
76+ if (data?.complete) {
77+ remember(key, data);
78+ const response = new Response(JSON.stringify(data), {
79+ headers: { "content-type": "application/json", "cache-control": `private, max-age=${KEEP_MS / 1000}` },
80+ });
81+ // Kept for the next page; the answer never waits for it.
82+ void shared?.put(url, response).catch(() => undefined);
83+ }
84+ return data;
85+}
86+
87+function remember(key: string, data: SignalData) {
88+ kept.delete(key);
89+ kept.set(key, { at: data.now, data });
90+ while (kept.size > KEEP_MAX) kept.delete(kept.keys().next().value!);
91+}
92+
93+/** The data centre's cache, or null where there is none (tests, local development). */
94+function sharedCache(): Cache | null {
95+ try {
96+ return (globalThis as { caches?: { default?: Cache } }).caches?.default ?? null;
97+ } catch {
98+ return null;
99+ }
100+}
101+
102+async function digest(text: string): Promise<string> {
103+ const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
104+ return [...new Uint8Array(bytes).slice(0, 12)].map((b) => b.toString(16).padStart(2, "0")).join("");
105+}
106+
107+async function readSignalData(viewer: User, slug: string, now: number, owned: Repo[]): Promise<SignalData | null> {
44108 const range = signalsRange(now);
45109 const starts = weekStarts(range.until, range.weeks);
46110 const ids = owned.map((repo) => repo.id);
62126 for (const batch of batches) if (batch.status === "rejected") console.warn("g1t: workspace signals could not be read:", batch.reason);
63127 if (ids.length > 0 && answered.length === 0) return null;
64128 const signals = mergeSignals(answered);
129+ const weeks = weekTotals(signals.weeks, starts);
65130 return {
66131 slug,
67132 now,
70135 repos,
71136 signals,
72137 complete: answered.length === batches.length,
73− weeks: weekTotals(signals.weeks, starts),
74− spend: weeklySpend(weekly, starts),
138+ weeks,
139+ // g1t's metered usage each week, and what the harness sessions started in it cost.
140+ spend: weeklySpend(weekly, starts)?.map((micros, i) => micros + Math.round((weeks[i]?.sessionCostUsd ?? 0) * 1_000_000)) ?? null,
75141 quarterUsage,
76142 };
77143 }
+51−1
1414 mergeSignals,
1515 newGround,
1616 newHabits,
17+ namesWords,
1718 pulse,
19+ quarterSpend,
1820 quarterStart,
1921 quietHeadline,
2022 reviewSignal,
2729 spendFlow,
2830 spendSignal,
2931 spoken,
32+ spreadSignal,
33+ spreading,
3034 stalledSignal,
3135 weekStarts,
3236 weekTotals,
256260 test("stalled changes and rework are findings only when they moved", () => {
257261 assert.equal(
258262 stalledSignal(ctx({ weeks: [week("a", { stalled: 16 }), week("b", { stalled: 23 })] }))!.headline,
259− "23 changes have been open over a week, 7 more than last week.",
263+ "23 open changes have gone three days with nothing happening, 7 more than last week.",
260264 );
261265 assert.equal(stalledSignal(ctx({ weeks: [week("a", { stalled: 16 }), week("b", { stalled: 17 })] })), null);
262266 const steady = [1, 2, 3, 4].map((i) => week(`w${i}`, { agentMerged: 10, agentReworked: 1 }));
380384 ],
381385 );
382386 });
387+
388+test("quarter spend adds each pull request's share of its sessions to g1t's usage", () => {
389+ const signals = {
390+ ...emptySignals(),
391+ pulls: [
392+ pull({ number: 1, sessionCostUsd: 2.5 }),
393+ pull({ number: 2, sessionCostUsd: 1, createdAt: "2026-05-01T00:00:00Z", mergedAt: "2026-05-02T00:00:00Z" }),
394+ ],
395+ };
396+ const quarterUsage = usage({
397+ byPull: [
398+ { key: "acme/web#1", micros: 1_000_000, runs: 1 },
399+ { key: "acme/other#9", micros: 500_000, runs: 1 },
400+ ],
401+ });
402+ const spend = quarterSpend({ repos, signals, quarterUsage, quarter: "2026-10-01" })!;
403+ assert.equal(spend.byPull.get("acme/web#1"), 3_500_000);
404+ assert.equal(spend.byPull.get("acme/web#2"), undefined, "last quarter's sessions are left out");
405+ assert.equal(spend.total, 4_000_000, "usage on pull requests not listed still counts in the total");
406+ assert.equal(spend.sessions, 2_500_000);
407+ assert.equal(quarterSpend({ repos, signals, quarterUsage: null, quarter: "2026-10-01" }), null);
408+});
409+
410+test("a harness catching on: two or more people started this month, two weeks or more after the first", () => {
411+ const at = (days: number) => new Date(NOW - days * DAY).toISOString();
412+ const harnesses = [
413+ { repoId: "r1", ownerId: "u1", ownerName: "ana", harness: "codex", firstAt: at(60), sessions: 30 },
414+ { repoId: "r2", ownerId: "u1", ownerName: "ana", harness: "codex", firstAt: at(20), sessions: 3 },
415+ { repoId: "r1", ownerId: "u2", ownerName: "bo", harness: "codex", firstAt: at(10), sessions: 4 },
416+ { repoId: "r2", ownerId: "u3", ownerName: "cy", harness: "codex", firstAt: at(3), sessions: 2 },
417+ { repoId: "r1", ownerId: "u2", ownerName: "bo", harness: "cursor", firstAt: at(5), sessions: 1 },
418+ ];
419+ const [spread] = spreading(harnesses, NOW);
420+ assert.equal(spread!.harness, "codex");
421+ assert.equal(spread!.first.name, "ana", "a person's first session anywhere counts");
422+ assert.deepEqual(
423+ spread!.recent.map((p) => p.name),
424+ ["bo", "cy"],
425+ );
426+ assert.equal(spreading(harnesses, NOW, { only: new Set(["r1"]) }).length, 0, "within one project only bo is new");
427+ assert.equal(
428+ spreadSignal(ctx({ signals: { ...emptySignals(), harnesses } }))!.headline,
429+ "Codex is catching on: bo and cy started using it this month, after ana.",
430+ );
431+ assert.equal(namesWords(["a", "b", "c", "d"]), "a, b and 2 others");
432+});
+117−16
1515 RepoPath,
1616 SignalAuthor,
1717 SignalDay,
18+ SignalHarness,
1819 SignalItem,
1920 SignalMilestone,
2021 SignalPull,
310311 delta: deltaOf(perMerge[at] ?? null, before ? (perMerge[at - 1] ?? null) : null, (abs) => dollars(abs), false),
311312 series: perMerge,
312313 points: perMerge.map((micros, i) => `${labels[i]}: ${micros == null ? "no agent merges" : dollars(micros)}`),
313− hint: "The week's agent usage at price, over the agent pull requests that merged",
314+ hint: "g1t's agent usage at price and harness sessions' cost at list price, over the agent merges",
314315 to: `/${slug}/-/usage`,
315316 },
316317 {
319320 value: String(now.stalled),
320321 delta: deltaOf(now.stalled, before?.stalled ?? null, count, false),
321322 series: weeks.map((week) => week.stalled),
322− points: weeks.map((week, i) => `${labels[i]}: ${week.stalled} open over a week`),
323− hint: "Pull requests open more than seven days, neither merged nor closed",
323+ points: weeks.map((week, i) => `${labels[i]}: ${week.stalled} with nothing for three days`),
324+ hint: "Open pull requests with no push, comment, review, check or session for three days",
324325 to: `/${slug}/-/overview?tab=needs#work`,
325326 },
326327 {
452453 return null;
453454 }
454455
455−/** What each of the quarter's pull requests cost, by `namespace/name#number`. */
456−function pullSpend(usage: Usage | null): Map<string, number> {
457− const spend = new Map<string, number>();
458− for (const slice of usage?.byPull ?? []) spend.set(slice.key.toLowerCase(), (spend.get(slice.key.toLowerCase()) ?? 0) + slice.micros);
459− return spend;
456+const MICROS = 1_000_000;
457+
458+/** Whether a pull request's work falls in the quarter: opened in it, or merged in it. */
459+function inQuarter(pull: SignalPull, quarter: string): boolean {
460+ return pull.createdAt.slice(0, 10) >= quarter || (pull.mergedAt != null && pull.mergedAt.slice(0, 10) >= quarter);
460461 }
461462
463+/**
464+ * What the quarter's agent work cost, by pull request (`namespace/name#number`)
465+ * and in all: g1t's metered usage on each pull request (usage's `byPull`), and
466+ * each pull request's share of what its harness sessions cost, estimated at
467+ * list price from their token usage. The total also holds g1t usage on pull
468+ * requests the reading doesn't list. Null when usage did not answer.
469+ */
470+export function quarterSpend(
471+ ctx: Pick<SignalContext, "repos" | "signals" | "quarterUsage" | "quarter">,
472+): { byPull: Map<string, number>; total: number; sessions: number } | null {
473+ if (!ctx.quarterUsage) return null;
474+ const byPull = new Map<string, number>();
475+ let total = 0;
476+ for (const slice of ctx.quarterUsage.byPull) {
477+ const key = slice.key.toLowerCase();
478+ byPull.set(key, (byPull.get(key) ?? 0) + slice.micros);
479+ total += slice.micros;
480+ }
481+ let sessions = 0;
482+ for (const pull of ctx.signals.pulls) {
483+ const repo = ctx.repos[pull.repoId];
484+ const micros = Math.round((pull.sessionCostUsd ?? 0) * MICROS);
485+ if (!repo || micros <= 0 || !inQuarter(pull, ctx.quarter)) continue;
486+ const key = pullKey(repo, pull.number);
487+ byPull.set(key, (byPull.get(key) ?? 0) + micros);
488+ total += micros;
489+ sessions += micros;
490+ }
491+ return { byPull, total, sessions };
492+}
493+
462494 function pullKey(repo: RepoPath, number: number): string {
463495 return `${repo.namespace}/${repo.name}#${number}`.toLowerCase();
464496 }
465497
466498 /** Where the quarter's agent usage went, by milestone (else work item, else project): the biggest share. */
467499 export function spendSignal(ctx: SignalContext): Signal | null {
468− const usage = ctx.quarterUsage;
469− if (!usage) return null;
470− const spend = pullSpend(usage);
471− const total = usage.byPull.reduce((sum, slice) => sum + slice.micros, 0);
472− if (total <= 0) return null;
500+ const quarter = quarterSpend(ctx);
501+ if (!quarter || quarter.total <= 0) return null;
502+ const { byPull: spend, total } = quarter;
473503 const items = new Map(ctx.signals.items.map((item) => [item.number, item]));
474504 const milestones = new Map(ctx.signals.milestones.map((milestone) => [milestone.number, milestone]));
475505 const groups = new Map<string, { label: string; to: string; micros: number }>();
552582 if (more < 3 || now.stalled < 1.25 * before.stalled) return null;
553583 return {
554584 key: "stalled",
555− headline: `${countWord(now.stalled)} changes have been open over a week, ${more} more than last week.`,
585+ headline: `${countWord(now.stalled)} open changes have gone three days with nothing happening, ${more} more than last week.`,
556586 facts: [`${before.stalled} last week`, `${now.stalled} now`],
557587 link: { label: "See what's stalled", to: `/${ctx.slug}/-/overview?tab=needs#work` },
558588 trend: {
662692 return found.sort((a, b) => b.firstAgentAt.localeCompare(a.firstAgentAt));
663693 }
664694
695+/**
696+ * A way of working catching on: a harness that someone took up first,
697+ * then at least two more people started using in the last `days`, at
698+ * least two weeks after them. Over the repositories in `only` (a team's),
699+ * or all of them; the newest spread first.
700+ */
701+export type Spread = {
702+ harness: string;
703+ first: { ownerId: string; name: string; at: string };
704+ recent: { ownerId: string; name: string; at: string }[];
705+ people: number;
706+};
707+
708+export function spreading(harnesses: SignalHarness[], now: number, { days = 30, only }: { days?: number; only?: ReadonlySet<string> } = {}): Spread[] {
709+ // Each person's first session with each harness, over the repositories asked about.
710+ const firsts = new Map<string, Map<string, { ownerId: string; name: string; at: string }>>();
711+ for (const row of harnesses) {
712+ if (only && !only.has(row.repoId)) continue;
713+ const people = firsts.get(row.harness) ?? new Map();
714+ const known = people.get(row.ownerId);
715+ if (!known || row.firstAt < known.at) people.set(row.ownerId, { ownerId: row.ownerId, name: row.ownerName, at: row.firstAt });
716+ firsts.set(row.harness, people);
717+ }
718+ const found: Spread[] = [];
719+ for (const [harness, people] of firsts) {
720+ const order = [...people.values()].sort((a, b) => a.at.localeCompare(b.at));
721+ const first = order[0]!;
722+ const recent = order.filter((person) => now - Date.parse(person.at) <= days * DAY_MS && Date.parse(person.at) - Date.parse(first.at) >= 14 * DAY_MS);
723+ if (recent.length >= 2) found.push({ harness, first, recent, people: order.length });
724+ }
725+ return found.sort((a, b) => b.recent[b.recent.length - 1]!.at.localeCompare(a.recent[a.recent.length - 1]!.at));
726+}
727+
728+const HARNESS_WORDS: Record<string, string> = {
729+ claude_code: "Claude Code",
730+ claude_managed_agents: "Claude Managed Agents",
731+ codex: "Codex",
732+ cursor: "Cursor",
733+ copilot: "Copilot",
734+};
735+
736+/** A harness as a sentence names it. */
737+export function harnessWords(harness: string): string {
738+ return HARNESS_WORDS[harness] ?? "another harness";
739+}
740+
741+/** Names in a sentence: "ana", "ana and bo", "ana, bo and 2 others". */
742+export function namesWords(names: string[], max = 3): string {
743+ if (names.length <= 1) return names[0] ?? "";
744+ if (names.length <= max) return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
745+ return `${names.slice(0, max - 1).join(", ")} and ${names.length - max + 1} others`;
746+}
747+
748+/** A practice catching on, as a finding. */
749+export function spreadSignal(ctx: SignalContext): Signal | null {
750+ const spread = spreading(ctx.signals.harnesses ?? [], ctx.now)[0];
751+ if (!spread) return null;
752+ const tool = harnessWords(spread.harness);
753+ return {
754+ key: "spread",
755+ headline: `${tool} is catching on: ${namesWords(spread.recent.map((person) => person.name))} started using it this month, after ${spread.first.name}.`,
756+ facts: [
757+ `${spread.people} people use it now`,
758+ `${spread.first.name} first, ${spoken(Math.max(0, (Date.parse(spread.recent[0]!.at) - Date.parse(spread.first.at)) / 1000))} before the next`,
759+ ],
760+ link: { label: "See the sessions", to: `/${ctx.slug}/-/work/sessions` },
761+ weight: 22 + Math.min(12, spread.recent.length * 3),
762+ ask: `How have ${namesWords(spread.recent.map((person) => person.name))} been using ${tool}, and what did ${spread.first.name} do with it first?`,
763+ };
764+}
765+
665766 /** The quiet changes as one finding: someone moving into a project one person has carried, else anyone new to one. */
666767 export function groundSignal(ctx: SignalContext): Signal | null {
667768 const spreading = silos(ctx.signals.authors, ctx.now).find((silo) => silo.joining.length > 0 && ctx.repos[silo.repoId]);
695796 };
696797 }
697798
698−const FINDINGS = [blockedSignal, reviewSignal, shippedSignal, spendSignal, stalledSignal, reworkSignal, groundSignal];
799+const FINDINGS = [blockedSignal, reviewSignal, shippedSignal, spendSignal, stalledSignal, reworkSignal, groundSignal, spreadSignal];
699800
700801 /** Every finding worth a look today, the one that matters most first. A finding that cannot be worked out is left out. */
701802 export function findings(ctx: SignalContext): Signal[] {
813914 * no work item has a node of its own.
814915 */
815916 export function spendFlow(ctx: Pick<SignalContext, "slug" | "repos" | "signals" | "quarterUsage" | "quarter">): FlowNode[] {
816− const spend = pullSpend(ctx.quarterUsage);
917+ const spend = quarterSpend(ctx)?.byPull ?? new Map<string, number>();
817918 const items = new Map(ctx.signals.items.map((item) => [item.number, item]));
818919 const milestones = new Map(ctx.signals.milestones.map((milestone) => [milestone.number, milestone]));
819920 type Build = FlowNode & { index: Map<string, Build> };
+18−6
2424 import { SpendFlow } from "../../components/spend-flow";
2525 import { isFailed, part } from "../../lib/partial";
2626 import { loadRepos } from "../../lib/home.server";
27−import { dailySignal, dayList, dollars, newGround, newHabits, rhythm, silos, spendFlow, spoken, waitingRows } from "../../lib/signals";
27+import { dailySignal, dayList, dollars, harnessWords, namesWords, newGround, newHabits, quarterSpend, rhythm, silos, spendFlow, spoken, spreading, waitingRows } from "../../lib/signals";
2828 import { type SignalData, loadGroups, loadSignalData } from "../../lib/signals.server";
2929
3030 export function meta({ params, ...args }: Route.MetaArgs) {
8383 function shapeGround(found: SignalData, groups: Awaited<ReturnType<typeof loadGroups>>, now: number) {
8484 const name = (id: string) => found.repos[id]?.name ?? "a project";
8585 const path = (id: string) => (found.repos[id] ? `/${found.repos[id]!.namespace}/${found.repos[id]!.name}` : null);
86− const quarterSpend = found.quarterUsage ? found.quarterUsage.byPull.reduce((sum, slice) => sum + slice.micros, 0) : null;
86+ const quarter = quarterSpend({ repos: found.repos, signals: found.signals, quarterUsage: found.quarterUsage, quarter: found.range.quarter });
87+ // Practices catching on in each team (else across the workspace), the newest first.
88+ const scopes = groups?.by === "team" ? groups.groups.map((group) => ({ key: group.key, label: group.label, only: new Set(group.repoIds) as ReadonlySet<string> | undefined })) : [{ key: "all", label: "the workspace", only: undefined }];
89+ const spreads = scopes.flatMap((scope) => spreading(found.signals.harnesses, now, { only: scope.only }).map((spread) => ({ scope, spread })));
8790 return {
8891 tiles: dailySignal(found).tiles,
8992 complete: found.complete,
9093 capped: found.signals.pullsCapped,
9194 flow: spendFlow({ slug: found.slug, repos: found.repos, signals: found.signals, quarterUsage: found.quarterUsage, quarter: found.range.quarter }),
92− sized: (quarterSpend ?? 0) > 0 ? ("spend" as const) : ("pulls" as const),
93− quarterSpend,
95+ sized: (quarter?.total ?? 0) > 0 ? ("spend" as const) : ("pulls" as const),
96+ quarterSpend: quarter?.total ?? null,
97+ sessionSpend: quarter?.sessions ?? 0,
9498 rhythm: groups ? { by: groups.by, rows: rhythm(found.signals.days, dayList(found.range.until, found.range.days), groups.groups) } : null,
9599 waiting: waitingRows(found.signals.waiting, found.repos).map((pull) => ({ ...pull, age: spoken(Math.max(0, (now - Date.parse(pull.createdAt)) / 1000)) })),
96100 silos: silos(found.signals.authors, now)
105109 newGround: newGround(found.signals.authors, now)
106110 .slice(0, QUIET_ROWS)
107111 .map((row) => ({ key: `${row.ownerId}:${row.repoId}`, to: path(row.repoId), where: name(row.repoId), line: `${row.name} merged a first change here`, more: `${row.elsewhere} merges elsewhere first` })),
112+ spreading: spreads.slice(0, QUIET_ROWS).map(({ scope, spread }) => ({
113+ key: `${scope.key}:${spread.harness}`,
114+ to: null,
115+ where: `${harnessWords(spread.harness)} in ${scope.label}`,
116+ line: `${namesWords(spread.recent.map((person) => person.name))} started using it this month, after ${spread.first.name}`,
117+ more: `${spread.people} people use it now`,
118+ })),
108119 habits: newHabits(found.signals.pulls, now)
109120 .slice(0, QUIET_ROWS)
110121 .map((row) => ({ key: row.ownerId, to: path(row.repoId), where: name(row.repoId), line: `${row.name} merged a first agent-made change`, more: `after ${row.byHand} by hand` })),
287298 title={ground.quarterSpend ? `${dollars(ground.quarterSpend)} of agent spend this quarter, and what it went into` : "This quarter's work, and how much of it agents did"}
288299 hint={
289300 ground.sized === "spend"
290− ? "Each milestone, its work items, their change set in each project and its pull requests, sized by what the agents' usage on them came to and shaded by how much was an agent's. Choose a box to open what is under it."
301+ ? `Each milestone, its work items, their change set in each project and its pull requests, sized by what agents cost on them${ground.sessionSpend > 0 ? ` (g1t's metered usage, and ${dollars(ground.sessionSpend)} of harness sessions estimated at list price)` : ""} and shaded by how much was an agent's. Choose a box to open what is under it.`
291302 : "Usage isn't known for this quarter, so each box is sized by its pull requests and shaded by how many were an agent's. Choose a box to open what is under it."
292303 }
293304 >
340351 title="Who works where, and what is spreading"
341352 hint="What no roll-up shows: knowledge resting with one person, people moving into new ground, and new ways of working taking hold. Each is a place to look, not a verdict."
342353 >
343− <div className="grid gap-8 md:grid-cols-3">
354+ <div className="grid gap-8 md:grid-cols-2 xl:grid-cols-4">
344355 <QuietList title="Knowledge silos" hint="Projects where one person has done most of the last 90 days' work" rows={ground.silos} empty="No project rests on one person right now." />
345356 <QuietList title="New ground" hint="People who merged to a project for the first time in the last two weeks" rows={ground.newGround} empty="No one has moved into a new project in the last two weeks." />
357+ <QuietList title="Spreading" hint="A harness more people took up this month, after someone started with it" rows={ground.spreading} empty="No harness has caught on in a team this month." />
346358 <QuietList title="New habits" hint="People whose first agent-made change merged in the last two weeks" rows={ground.habits} empty="No one new started merging agents' work in the last two weeks." />
347359 </div>
348360 </Section>

Comments

Sign in to comment.