The daily signal counts agents' direct pushes, and stops flagging silos in one-person workspaces
Read against flagon-io's own numbers: most of its work lands as commits agent sessions push straight to main, so agent merges alone read 2 for a week of real work. workspace_signals now counts the commits sessions push outside any pull request (agent_commits), and the pulse, spend per agent change, the quiet headline and each team's rhythm count them. With one person merging, every project rests on them, so knowledge silos only show where three people or more merged lately. With no milestones, the spend tree starts at the work items instead of one No milestone box. Closes #162
- G1t-Session:
- ses_01m57128wre9bvvj1y4z4b8dk0
11 files+183−710/11 viewed
| 48 | 48 | ||
| 49 | 49 | | Figure | What it counts | | |
| 50 | 50 | | --- | --- | | |
| 51 | − | | **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, 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. | | |
| 51 | + | | **Agent-authored changes** | Pull requests that merged in the week and were an [agent's](#whose-work-is-an-agents), and the commits agent sessions pushed outside any pull request (to the default branch, most of all), each once. A workspace that pushes straight to main lands most of its work the second way. | | |
| 52 | + | | **Spend per agent change** | 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 changes that landed in it: an average, not what any one cost. A dash when usage can't be read or no agent change landed. | | |
| 53 | 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. | | |
| 54 | − | | **Agent rework rate** | Of the agent pull requests that merged in the week, the share a person asked for changes on before they merged. | | |
| 54 | + | | **Agent rework rate** | Of the agent pull requests that merged in the week (direct commits get no review, so they're not counted here), the share a person asked for changes on before they merged. | | |
| 55 | 55 | ||
| 56 | 56 | ### Whose work is an agent's | |
| 57 | 57 | ||
| ⋯ | |||
| 78 | 78 | | **Stalled changes rising** | At least three more stalled changes than last week, and a quarter more. | | |
| 79 | 79 | | **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 | 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. | | |
| 81 | − | | **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. | | |
| 81 | + | | **Who works where** | In a workspace where three people or more merged lately, 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. | | |
| 82 | 82 | ||
| 83 | 83 | Findings are ordered by how much they hold up: blocked work first, then | |
| 84 | 84 | waits, then what shipped and where spend went. Up to five show, and the | |
| 682 | 682 | figures as Home. | |
| 683 | 683 | 2. **Spend, threaded to the work**: the quarter's work in four columns: | |
| 684 | 684 | each milestone, its work items, each work item's change set in each | |
| 685 | − | project, and that change set's pull requests. Each box is sized by | |
| 685 | + | project, and that change set's pull requests (with no milestones in | |
| 686 | + | the workspace, it starts at the work items). Each box is sized by | |
| 686 | 687 | what agents cost on its pull requests (g1t's metered usage, and each | |
| 687 | 688 | pull request's share of its sessions' cost estimated at list price), | |
| 688 | 689 | and shaded by how many were an agent's (**Agent use**, less to more). Choose a box to | |
| ⋯ | |||
| 694 | 695 | organization ran, so it is never added to g1t's. | |
| 695 | 696 | 3. **What's moving and what's waiting**: each team's last two weeks, day | |
| 696 | 697 | by day, by the projects the team works on (each project, when the | |
| 697 | − | workspace has no teams). Above the line are pull requests opened and | |
| 698 | − | merged, and agent sessions started; below it, reviews and comments and | |
| 698 | + | workspace has no teams). Above the line are changes (pull requests | |
| 699 | + | opened and merged, and commits agents pushed outside one) and agent | |
| 700 | + | sessions started; below it, reviews and comments and | |
| 699 | 701 | requests for changes. Turn each layer on or off above the rows, point | |
| 700 | 702 | at a day for its numbers, or open **The numbers, as a table**. Each | |
| 701 | 703 | row is drawn to its own scale. Beside it, **Waiting on a first | |
| ⋯ | |||
| 703 | 705 | 4. **Quiet changes**: places to look, not verdicts. | |
| 704 | 706 | - **Knowledge silos**: projects where one person merged three quarters | |
| 705 | 707 | or more of the last 90 days' changes (six or more), and whoever has | |
| 706 | − | started there this month. | |
| 708 | + | started there this month. Only in a workspace where three people or | |
| 709 | + | more merged lately: with fewer, every project rests on someone. | |
| 707 | 710 | - **New ground**: people who merged to a project for the first time in | |
| 708 | 711 | the last two weeks, after three or more merges elsewhere. | |
| 709 | 712 | - **Spreading**: a harness two or more people in a team started using | |
| 1 | 1 | /** | |
| 2 | 2 | * What is moving and what is waiting (lib/signals.ts rhythm): each team's | |
| 3 | 3 | * last two weeks, day by day. Above the line, pull requests opened and | |
| 4 | − | * merged (bars) and agent sessions started (a wash); below it, reviews and | |
| 4 | + | * merged and commits agents pushed outside one (bars), and agent sessions | |
| 5 | + | * started (a wash); below it, reviews and | |
| 5 | 6 | * comments (a wash) and requests for changes (bars). Each row is drawn to | |
| 6 | 7 | * its own scale, so a small team's week is as legible as a big one's; the | |
| 7 | 8 | * numbers are in the readout and the table. Layers turn on and off above | |
| ⋯ | |||
| 16 | 17 | type Layer = "prs" | "rework" | "agent" | "reviews"; | |
| 17 | 18 | ||
| 18 | 19 | const LAYERS: { key: Layer; label: string; swatch: string }[] = [ | |
| 19 | − | { key: "prs", label: "PR activity", swatch: "bg-fg-soft" }, | |
| 20 | + | { key: "prs", label: "Changes", swatch: "bg-fg-soft" }, | |
| 20 | 21 | { key: "rework", label: "Rework", swatch: "bg-faint" }, | |
| 21 | 22 | { key: "agent", label: "Agent activity", swatch: "bg-series-1" }, | |
| 22 | 23 | { key: "reviews", label: "Reviews & comments", swatch: "bg-series-4" }, | |
| ⋯ | |||
| 54 | 55 | <ul className="mt-2 space-y-1 text-fg"> | |
| 55 | 56 | <li className="flex items-center gap-2"> | |
| 56 | 57 | <span className="size-2 rounded-full bg-fg-soft" /> | |
| 57 | − | {sum.opened} opened · {sum.merged} merged | |
| 58 | + | {sum.opened} opened · {sum.merged} merged{sum.commits > 0 && ` · ${sum.commits} pushed by agents`} | |
| 58 | 59 | </li> | |
| 59 | 60 | <li className="flex items-center gap-2"> | |
| 60 | 61 | <span className="size-2 rounded-full bg-faint" /> | |
| ⋯ | |||
| 89 | 90 | const n = row.days.length; | |
| 90 | 91 | const slot = WIDTH / n; | |
| 91 | 92 | const x = (i: number) => slot * i + slot / 2; | |
| 92 | − | const up = Math.max(1, ...row.days.map((day) => Math.max(on.has("prs") ? day.opened + day.merged : 0, on.has("agent") ? day.sessions : 0))); | |
| 93 | + | const up = Math.max(1, ...row.days.map((day) => Math.max(on.has("prs") ? day.opened + day.merged + day.commits : 0, on.has("agent") ? day.sessions : 0))); | |
| 93 | 94 | const down = Math.max(1, ...row.days.map((day) => Math.max(on.has("rework") ? day.reworked : 0, on.has("reviews") ? day.reviews : 0))); | |
| 94 | 95 | const above = (value: number) => MID - (value / up) * (MID - 6); | |
| 95 | 96 | const below = (value: number) => MID + (value / down) * (MID - 6); | |
| ⋯ | |||
| 133 | 134 | <line x1={0} x2={WIDTH} y1={MID} y2={MID} className="stroke-line-strong" strokeWidth={1} vectorEffect="non-scaling-stroke" /> | |
| 134 | 135 | {on.has("prs") && | |
| 135 | 136 | row.days.map((d, i) => | |
| 136 | − | d.opened + d.merged > 0 ? ( | |
| 137 | + | d.opened + d.merged + d.commits > 0 ? ( | |
| 137 | 138 | <rect | |
| 138 | 139 | key={`p${i}`} | |
| 139 | 140 | x={x(i) - BAR / 2} | |
| 140 | − | y={above(d.opened + d.merged)} | |
| 141 | + | y={above(d.opened + d.merged + d.commits)} | |
| 141 | 142 | width={BAR} | |
| 142 | − | height={MID - above(d.opened + d.merged)} | |
| 143 | + | height={MID - above(d.opened + d.merged + d.commits)} | |
| 143 | 144 | rx={1.5} | |
| 144 | 145 | className="fill-fg-soft" | |
| 145 | 146 | /> | |
| ⋯ | |||
| 224 | 225 | <th className="py-1 font-normal">{by === "team" ? "Team" : "Project"}</th> | |
| 225 | 226 | <th className="py-1 font-normal">Opened</th> | |
| 226 | 227 | <th className="py-1 font-normal">Merged</th> | |
| 228 | + | <th className="py-1 font-normal">Pushed by agents</th> | |
| 227 | 229 | <th className="py-1 font-normal">Rework</th> | |
| 228 | 230 | <th className="py-1 font-normal">Reviews & comments</th> | |
| 229 | 231 | <th className="py-1 font-normal">Agent sessions</th> | |
| ⋯ | |||
| 240 | 242 | <td className="py-1.5 text-fg">{row.label}</td> | |
| 241 | 243 | <td>{cell(last.opened, now.opened)}</td> | |
| 242 | 244 | <td>{cell(last.merged, now.merged)}</td> | |
| 245 | + | <td>{cell(last.commits, now.commits)}</td> | |
| 243 | 246 | <td>{cell(last.reworked, now.reworked)}</td> | |
| 244 | 247 | <td>{cell(last.reviews, now.reviews)}</td> | |
| 245 | 248 | <td>{cell(last.sessions, now.sessions)}</td> | |
| 141 | 141 | export function SpendFlow({ flow, sized }: { flow: FlowNode[]; sized: "spend" | "pulls" }) { | |
| 142 | 142 | const [path, setPath] = useState<string[]>([]); | |
| 143 | 143 | const weight = weightOf(sized); | |
| 144 | + | // A workspace with no milestones starts at its work items: one "No milestone" box tells nothing. | |
| 145 | + | const skip = flow.length === 1 && flow[0]!.key === "m-"; | |
| 146 | + | const depth = skip ? 2 : 3; | |
| 147 | + | const heads = HEADS.slice(3 - depth); | |
| 144 | 148 | // The chosen node at each level: the one asked for, else the biggest. | |
| 145 | − | const columns: FlowNode[][] = [flow]; | |
| 149 | + | const columns: FlowNode[][] = [skip ? flow[0]!.children : flow]; | |
| 146 | 150 | const chosen: FlowNode[] = []; | |
| 147 | − | for (let level = 0; level < 3; level++) { | |
| 151 | + | for (let level = 0; level < depth; level++) { | |
| 148 | 152 | const nodes = columns[level] ?? []; | |
| 149 | 153 | const pick = nodes.find((node) => node.key === path[level]) ?? nodes[0]; | |
| 150 | 154 | if (!pick) break; | |
| 151 | 155 | chosen.push(pick); | |
| 152 | − | if (level < 2) columns.push(pick.children); | |
| 156 | + | if (level < depth - 1) columns.push(pick.children); | |
| 153 | 157 | } | |
| 154 | − | const pulls = chosen[2]?.list ?? []; | |
| 158 | + | const pulls = chosen[depth - 1]?.list ?? []; | |
| 155 | 159 | const choose = (level: number, key: string) => setPath([...chosen.slice(0, level).map((node) => node.key), key]); | |
| 156 | 160 | const boxes = columns.map((nodes) => stack(nodes.map(weight))); | |
| 157 | 161 | const rowHeight = pulls.length > 0 ? HEIGHT / Math.max(pulls.length, 5) : 0; | |
| ⋯ | |||
| 162 | 166 | return ( | |
| 163 | 167 | <div className="space-y-4"> | |
| 164 | 168 | <Legend /> | |
| 165 | − | {/* Wide: the four columns, joined. */} | |
| 169 | + | {/* Wide: the columns, joined. */} | |
| 166 | 170 | <div className="hidden md:block"> | |
| 167 | 171 | <div className="relative h-7"> | |
| 168 | − | {HEADS.map((head, column) => ( | |
| 172 | + | {heads.map((head, column) => ( | |
| 169 | 173 | <p key={head} className="absolute top-0 font-mono text-[0.6875rem] tracking-[0.14em] text-muted uppercase" style={{ left: `${left(column)}%` }}> | |
| 170 | 174 | {head} | |
| 171 | 175 | </p> | |
| ⋯ | |||
| 176 | 180 | {chosen.map((node, level) => { | |
| 177 | 181 | const index = columns[level]!.indexOf(node); | |
| 178 | 182 | const from = boxes[level]![index]!; | |
| 179 | − | if (level < 2) { | |
| 183 | + | if (level < depth - 1) { | |
| 180 | 184 | const children = columns[level + 1]!; | |
| 181 | 185 | return slices(from, children.map(weight)).map((slice, i) => ( | |
| 182 | 186 | <path key={`${level}-${i}`} d={band(level, slice, boxes[level + 1]![i]!)} className="fill-line-strong" opacity={0.45} /> | |
| ⋯ | |||
| 209 | 213 | )} | |
| 210 | 214 | <div | |
| 211 | 215 | className="absolute rounded-xl border border-line bg-surface" | |
| 212 | − | style={{ left: `${left(3)}%`, width: `${100 - left(3)}%`, top: 0, height: pulls.length * rowHeight || MIN_BOX }} | |
| 216 | + | style={{ left: `${left(depth)}%`, width: `${100 - left(depth)}%`, top: 0, height: pulls.length * rowHeight || MIN_BOX }} | |
| 213 | 217 | > | |
| 214 | 218 | {pulls.length === 0 ? ( | |
| 215 | 219 | <p className="px-3 py-4 text-sm text-muted">No pull requests here.</p> | |
| ⋯ | |||
| 223 | 227 | <div className="space-y-5 md:hidden"> | |
| 224 | 228 | {columns.map((nodes, level) => ( | |
| 225 | 229 | <div key={level} className="space-y-2"> | |
| 226 | − | <p className="font-mono text-[0.6875rem] tracking-[0.14em] text-muted uppercase">{HEADS[level]}</p> | |
| 230 | + | <p className="font-mono text-[0.6875rem] tracking-[0.14em] text-muted uppercase">{heads[level]}</p> | |
| 227 | 231 | <div className="grid gap-2"> | |
| 228 | 232 | {nodes.map((node) => ( | |
| 229 | 233 | <NodeBox key={node.key} node={node} sized={sized} chosen={chosen[level]?.key === node.key} onChoose={() => choose(level, node.key)} /> | |
| ⋯ | |||
| 232 | 236 | </div> | |
| 233 | 237 | ))} | |
| 234 | 238 | <div className="space-y-2"> | |
| 235 | − | <p className="font-mono text-[0.6875rem] tracking-[0.14em] text-muted uppercase">{HEADS[3]}</p> | |
| 239 | + | <p className="font-mono text-[0.6875rem] tracking-[0.14em] text-muted uppercase">{heads[depth]}</p> | |
| 236 | 240 | <div className="rounded-xl border border-line bg-surface"> | |
| 237 | 241 | {pulls.map((pull) => ( | |
| 238 | 242 | <PullRow key={pull.key} pull={pull} style={{ height: 48 }} /> | |
| 277 | 277 | author({ ownerId: "u1", ownerName: "ana", repoId: "r1", recent: 9, merged: 40 }), | |
| 278 | 278 | author({ ownerId: "u2", ownerName: "bo", repoId: "r1", firstMergedAt: recent, recent: 1, merged: 1 }), | |
| 279 | 279 | author({ ownerId: "u2", ownerName: "bo", repoId: "r2", firstMergedAt: "2026-02-01T00:00:00Z", merged: 12, recent: 2 }), | |
| 280 | + | author({ ownerId: "u3", ownerName: "cy", repoId: "r2", firstMergedAt: "2026-03-01T00:00:00Z", merged: 5, recent: 1 }), | |
| 280 | 281 | ]; | |
| 281 | 282 | assert.deepEqual( | |
| 282 | 283 | newGround(authors, NOW).map((row) => [row.name, row.repoId, row.elsewhere]), | |
| 283 | 284 | [["bo", "r1", 12]], | |
| 284 | 285 | ); | |
| 286 | + | assert.deepEqual(silos(authors.slice(0, 3), NOW), [], "two people: every project rests on one of them, which says nothing"); | |
| 285 | 287 | const [silo] = silos(authors, NOW); | |
| 286 | 288 | assert.equal(silo!.name, "ana"); | |
| 287 | 289 | assert.equal(silo!.share, 0.9); | |
| ⋯ | |||
| 306 | 308 | found.map((s) => s.key), | |
| 307 | 309 | ["blocked", "stalled"], | |
| 308 | 310 | ); | |
| 309 | − | assert.equal(quietHeadline(week("x")), "A quiet week: nothing has merged yet, and nothing is stuck."); | |
| 311 | + | assert.equal(quietHeadline(week("x")), "A quiet week: nothing has landed yet, and nothing is stuck."); | |
| 310 | 312 | assert.equal( | |
| 311 | 313 | quietHeadline(week("x", { merged: 4, agentMerged: 2 })), | |
| 312 | − | "Four changes merged this week, 50% of them an agent's, and nothing needs a closer look.", | |
| 314 | + | "Four changes landed this week, 50% of them an agent's, and nothing needs a closer look.", | |
| 315 | + | ); | |
| 316 | + | // A workspace that pushes straight to main: agents' direct commits are what landed. | |
| 317 | + | assert.equal( | |
| 318 | + | quietHeadline(week("x", { merged: 1, agentMerged: 0, agentCommits: 9 })), | |
| 319 | + | "Ten changes landed this week, 90% of them an agent's, and nothing needs a closer look.", | |
| 313 | 320 | ); | |
| 314 | 321 | }); | |
| 315 | 322 | ||
| ⋯ | |||
| 430 | 437 | ); | |
| 431 | 438 | assert.equal(namesWords(["a", "b", "c", "d"]), "a, b and 2 others"); | |
| 432 | 439 | }); | |
| 440 | + | ||
| 441 | + | test("agents' direct commits count as what they landed, and as what spend is spread over", () => { | |
| 442 | + | const weeks = [week("2026-10-05", { merged: 2, agentMerged: 1, agentCommits: 3 }), week("2026-10-12", { merged: 1, agentMerged: 1, agentCommits: 9 })]; | |
| 443 | + | const [changes, spend] = pulse({ slug: "acme", weeks, spend: [4_000_000, 20_000_000] }); | |
| 444 | + | assert.equal(changes!.label, "Agent-authored changes"); | |
| 445 | + | assert.equal(changes!.value, "10"); | |
| 446 | + | assert.deepEqual(changes!.delta, { text: "+6", tone: "good" }); | |
| 447 | + | assert.equal(changes!.points[1], "Week of Oct 12: 1 merged, 9 pushed directly, of 10 changes"); | |
| 448 | + | assert.equal(spend!.value, "$2.00", "$20 over ten agent changes"); | |
| 449 | + | }); | |
| 114 | 114 | reviewSeconds: 0, | |
| 115 | 115 | stalled: 0, | |
| 116 | 116 | sessionCostUsd: 0, | |
| 117 | + | agentCommits: 0, | |
| 117 | 118 | }); | |
| 118 | 119 | ||
| 119 | 120 | /** Every week in `starts`, quiet ones too, for the repositories in `only` (every one when not given). */ | |
| ⋯ | |||
| 131 | 132 | week.reviewSeconds += row.reviewSeconds; | |
| 132 | 133 | week.stalled += row.stalled; | |
| 133 | 134 | week.sessionCostUsd += row.sessionCostUsd ?? 0; | |
| 135 | + | week.agentCommits += row.agentCommits ?? 0; | |
| 134 | 136 | } | |
| 135 | 137 | return starts.map((start) => byStart.get(start)!); | |
| 136 | 138 | } | |
| ⋯ | |||
| 172 | 174 | return week.agentMerged > 0 ? week.agentReworked / week.agentMerged : null; | |
| 173 | 175 | } | |
| 174 | 176 | ||
| 175 | − | /** The share of merges that were an agent's, 0 to 1. */ | |
| 177 | + | /** | |
| 178 | + | * What agents landed in a week: their merged pull requests, and the | |
| 179 | + | * commits their sessions pushed outside any pull request (to the default | |
| 180 | + | * branch, most of all). A workspace that pushes straight to main lands | |
| 181 | + | * most of its work the second way. | |
| 182 | + | */ | |
| 183 | + | export function agentChanges(week: WeekTotals): number { | |
| 184 | + | return week.agentMerged + (week.agentCommits ?? 0); | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | /** Everything that landed in a week: merged pull requests, and agents' direct commits. */ | |
| 188 | + | export function allChanges(week: WeekTotals): number { | |
| 189 | + | return week.merged + (week.agentCommits ?? 0); | |
| 190 | + | } | |
| 191 | + | ||
| 192 | + | /** The share of what landed that was an agent's, 0 to 1. */ | |
| 176 | 193 | export function agentShare(week: WeekTotals): number | null { | |
| 177 | − | return week.merged > 0 ? week.agentMerged / week.merged : null; | |
| 194 | + | return allChanges(week) > 0 ? agentChanges(week) / allChanges(week) : null; | |
| 178 | 195 | } | |
| 179 | 196 | ||
| 180 | − | /** Usage per agent merge, in micros. */ | |
| 197 | + | /** Usage per agent change (merge or direct commit), in micros. */ | |
| 181 | 198 | export function spendPerMerge(micros: number | null, week: WeekTotals): number | null { | |
| 182 | − | return micros != null && week.agentMerged > 0 ? micros / week.agentMerged : null; | |
| 199 | + | return micros != null && agentChanges(week) > 0 ? micros / agentChanges(week) : null; | |
| 183 | 200 | } | |
| 184 | 201 | ||
| 185 | 202 | // --- Words ------------------------------------------------------------------------ | |
| ⋯ | |||
| 282 | 299 | ||
| 283 | 300 | /** | |
| 284 | 301 | * The past week in four figures, each against the week before and drawn | |
| 285 | − | * over the last ten: agent merges, usage per agent merge, stalled changes, | |
| 286 | − | * and how often people asked for changes on an agent's work. | |
| 302 | + | * over the last ten: what agents landed (merges and direct commits), usage | |
| 303 | + | * per agent change, stalled changes, and how often people asked for | |
| 304 | + | * changes on an agent's merged work. | |
| 287 | 305 | */ | |
| 288 | 306 | export function pulse({ slug, weeks, spend }: PulseInput): PulseTile[] { | |
| 289 | 307 | const at = weeks.length - 1; | |
| ⋯ | |||
| 297 | 315 | return [ | |
| 298 | 316 | { | |
| 299 | 317 | key: "merges", | |
| 300 | − | label: "Agent-authored merges", | |
| 301 | − | value: String(now.agentMerged), | |
| 302 | − | delta: deltaOf(now.agentMerged, before?.agentMerged ?? null, count, true), | |
| 303 | − | series: weeks.map((week) => week.agentMerged), | |
| 304 | − | points: weeks.map((week, i) => `${labels[i]}: ${week.agentMerged} of ${week.merged} merges`), | |
| 305 | − | hint: "Merged pull requests an agent made: g1t's agent, or a harness session tied to it", | |
| 318 | + | label: "Agent-authored changes", | |
| 319 | + | value: String(agentChanges(now)), | |
| 320 | + | delta: deltaOf(agentChanges(now), before ? agentChanges(before) : null, count, true), | |
| 321 | + | series: weeks.map(agentChanges), | |
| 322 | + | points: weeks.map((week, i) => `${labels[i]}: ${week.agentMerged} merged, ${week.agentCommits ?? 0} pushed directly, of ${allChanges(week)} changes`), | |
| 323 | + | hint: "Agents' merged pull requests, and the commits their sessions pushed outside one", | |
| 306 | 324 | to: `/${slug}/-/overview?tab=landed`, | |
| 307 | 325 | }, | |
| 308 | 326 | { | |
| 309 | 327 | key: "spend", | |
| 310 | − | label: "Spend per agent merge", | |
| 328 | + | label: "Spend per agent change", | |
| 311 | 329 | value: dollars(perMerge[at] ?? null), | |
| 312 | 330 | delta: deltaOf(perMerge[at] ?? null, before ? (perMerge[at - 1] ?? null) : null, (abs) => dollars(abs), false), | |
| 313 | 331 | series: perMerge, | |
| 314 | − | points: perMerge.map((micros, i) => `${labels[i]}: ${micros == null ? "no agent merges" : dollars(micros)}`), | |
| 315 | − | hint: "g1t's agent usage at price and harness sessions' cost at list price, over the agent merges", | |
| 332 | + | points: perMerge.map((micros, i) => `${labels[i]}: ${micros == null ? "no agent changes" : dollars(micros)}`), | |
| 333 | + | hint: "g1t's agent usage at price and harness sessions' cost at list price, over what agents landed", | |
| 316 | 334 | to: `/${slug}/-/usage`, | |
| 317 | 335 | }, | |
| 318 | 336 | { | |
| ⋯ | |||
| 646 | 664 | /** | |
| 647 | 665 | * A project where one person has done most of the recent work: their | |
| 648 | 666 | * share of its merges in the last 90 days, and whoever else has started | |
| 649 | − | * merging there in the last 30 (knowledge spreading, or not yet). | |
| 667 | + | * merging there in the last 30 (knowledge spreading, or not yet). Only in | |
| 668 | + | * a workspace where three people or more merged lately. | |
| 650 | 669 | */ | |
| 651 | 670 | export type Silo = { | |
| 652 | 671 | repoId: string; | |
| ⋯ | |||
| 658 | 677 | joining: { ownerId: string; name: string }[]; | |
| 659 | 678 | }; | |
| 660 | 679 | ||
| 661 | − | export function silos(authors: SignalAuthor[], now: number, { minRecent = 6, minShare = 0.75 } = {}): Silo[] { | |
| 680 | + | export function silos(authors: SignalAuthor[], now: number, { minRecent = 6, minShare = 0.75, minPeople = 3 } = {}): Silo[] { | |
| 681 | + | // Fewer than `minPeople` people merging anywhere lately: every project rests on someone, and saying so tells nothing. | |
| 682 | + | const people = new Set(authors.filter((row) => row.recent > 0).map((row) => row.ownerId)); | |
| 683 | + | if (people.size < minPeople) return []; | |
| 662 | 684 | const byRepo = new Map<string, SignalAuthor[]>(); | |
| 663 | 685 | for (const row of authors) byRepo.set(row.repoId, [...(byRepo.get(row.repoId) ?? []), row]); | |
| 664 | 686 | const found: Silo[] = []; | |
| ⋯ | |||
| 815 | 837 | ||
| 816 | 838 | /** The headline when nothing stands out: still a sentence about the week. */ | |
| 817 | 839 | export function quietHeadline(week: WeekTotals | undefined): string { | |
| 818 | − | if (!week || week.merged === 0) return "A quiet week: nothing has merged yet, and nothing is stuck."; | |
| 840 | + | const landed = week ? allChanges(week) : 0; | |
| 841 | + | if (!week || landed === 0) return "A quiet week: nothing has landed yet, and nothing is stuck."; | |
| 819 | 842 | const share = agentShare(week); | |
| 820 | − | return `${countWord(week.merged)} ${week.merged === 1 ? "change" : "changes"} merged this week${share ? `, ${percent(share)} of them an agent's` : ""}, and nothing needs a closer look.`; | |
| 843 | + | return `${countWord(landed)} ${landed === 1 ? "change" : "changes"} landed this week${share ? `, ${percent(share)} of them an agent's` : ""}, and nothing needs a closer look.`; | |
| 821 | 844 | } | |
| 822 | 845 | ||
| 823 | 846 | /** What Home shows: the pulse, and the findings with the one that leads. */ | |
| ⋯ | |||
| 976 | 999 | ||
| 977 | 1000 | // --- The rhythm of the work ------------------------------------------------------- | |
| 978 | 1001 | ||
| 979 | − | export type RhythmDay = { day: string; opened: number; merged: number; reworked: number; reviews: number; sessions: number }; | |
| 1002 | + | export type RhythmDay = { day: string; opened: number; merged: number; reworked: number; reviews: number; sessions: number; commits: number }; | |
| 980 | 1003 | ||
| 981 | 1004 | export type RhythmRow = { | |
| 982 | 1005 | key: string; | |
| ⋯ | |||
| 995 | 1018 | export function rhythm(days: SignalDay[], list: string[], groups: Group[], max = 6): RhythmRow[] { | |
| 996 | 1019 | const rows = groups.map((group) => { | |
| 997 | 1020 | const ids = new Set(group.repoIds); | |
| 998 | − | const byDay = new Map(list.map((day) => [day, { day, opened: 0, merged: 0, reworked: 0, reviews: 0, sessions: 0 }])); | |
| 1021 | + | const byDay = new Map(list.map((day) => [day, { day, opened: 0, merged: 0, reworked: 0, reviews: 0, sessions: 0, commits: 0 }])); | |
| 999 | 1022 | for (const row of days) { | |
| 1000 | 1023 | const day = byDay.get(row.day); | |
| 1001 | 1024 | if (!day || !ids.has(row.repoId)) continue; | |
| ⋯ | |||
| 1004 | 1027 | day.reworked += row.reworked; | |
| 1005 | 1028 | day.reviews += row.reviews; | |
| 1006 | 1029 | day.sessions += row.sessions; | |
| 1030 | + | day.commits += row.agentCommits ?? 0; | |
| 1007 | 1031 | } | |
| 1008 | 1032 | const out = list.map((day) => byDay.get(day)!); | |
| 1009 | − | const total = out.reduce((sum, day) => sum + day.opened + day.merged + day.reworked + day.reviews + day.sessions, 0); | |
| 1033 | + | const total = out.reduce((sum, day) => sum + day.opened + day.merged + day.reworked + day.reviews + day.sessions + day.commits, 0); | |
| 1010 | 1034 | return { key: group.key, label: group.label, note: group.note, to: group.to, days: out, total }; | |
| 1011 | 1035 | }); | |
| 1012 | 1036 | return rows | |
| ⋯ | |||
| 1024 | 1048 | reworked: sum.reworked + day.reworked, | |
| 1025 | 1049 | reviews: sum.reviews + day.reviews, | |
| 1026 | 1050 | sessions: sum.sessions + day.sessions, | |
| 1051 | + | commits: sum.commits + day.commits, | |
| 1027 | 1052 | }), | |
| 1028 | − | { opened: 0, merged: 0, reworked: 0, reviews: 0, sessions: 0 }, | |
| 1053 | + | { opened: 0, merged: 0, reworked: 0, reviews: 0, sessions: 0, commits: 0 }, | |
| 1029 | 1054 | ); | |
| 1030 | 1055 | } | |
| 1031 | 1056 | ||
| ⋯ | |||
| 1034 | 1059 | const half = Math.floor(row.days.length / 2); | |
| 1035 | 1060 | const last = rhythmSum(row.days.slice(0, half)); | |
| 1036 | 1061 | const now = rhythmSum(row.days.slice(half)); | |
| 1037 | − | const prs = (sum: ReturnType<typeof rhythmSum>) => sum.opened + sum.merged; | |
| 1062 | + | const prs = (sum: ReturnType<typeof rhythmSum>) => sum.opened + sum.merged + sum.commits; | |
| 1038 | 1063 | if (prs(now) === 0 && prs(last) > 0) return "Quiet this week after a busy last week"; | |
| 1039 | 1064 | if (now.reviews === 0 && now.opened > 0) return `${now.opened} opened this week, no reviews yet`; | |
| 1040 | 1065 | if (prs(now) > 1.5 * prs(last) && prs(now) >= 4) return "Busier than last week"; | |
| 1041 | 1066 | if (now.reworked > Math.max(2, last.reworked * 2)) return "More rework than last week"; | |
| 1042 | − | return `${now.merged} merged this week, ${last.merged} last week`; | |
| 1067 | + | return `${now.merged + now.commits} landed this week, ${last.merged + last.commits} last week`; | |
| 1043 | 1068 | } | |
| 1044 | 1069 | ||
| 1045 | 1070 | export type WaitingRow = WaitingPull & { repo: RepoPath; to: string }; | |
| 58 | 58 | /// What the harness sessions started in the week cost, as they | |
| 59 | 59 | /// reported it (estimated at list price from their token usage). | |
| 60 | 60 | pub session_cost_usd: f64, | |
| 61 | + | /// Commits agent sessions pushed outside any pull request in the week | |
| 62 | + | /// (to the default branch, most of all), each once. | |
| 63 | + | pub agent_commits: u32, | |
| 61 | 64 | } | |
| 62 | 65 | ||
| 63 | 66 | /// One repository's UTC day. Days with nothing in them are left out. | |
| ⋯ | |||
| 76 | 79 | pub reviews: u32, | |
| 77 | 80 | /// Harness sessions started. | |
| 78 | 81 | pub sessions: u32, | |
| 82 | + | /// Commits agent sessions pushed outside any pull request. | |
| 83 | + | pub agent_commits: u32, | |
| 79 | 84 | } | |
| 80 | 85 | ||
| 81 | 86 | /// An open pull request no one has reviewed yet, oldest first. | |
| 1908 | 1908 | reviewSeconds: number; | |
| 1909 | 1909 | stalled: number; | |
| 1910 | 1910 | sessionCostUsd: number; | |
| 1911 | + | /** Commits agent sessions pushed outside any pull request (to the default branch, most of all), each once. */ | |
| 1912 | + | agentCommits: number; | |
| 1911 | 1913 | }; | |
| 1912 | − | export type SignalDay = { repoId: string; day: string; opened: number; merged: number; agentMerged: number; reworked: number; reviews: number; sessions: number }; | |
| 1914 | + | export type SignalDay = { repoId: string; day: string; opened: number; merged: number; agentMerged: number; reworked: number; reviews: number; sessions: number; agentCommits: number }; | |
| 1913 | 1915 | /** An open pull request no one has reviewed, oldest first; `blocks`: the open work items waiting on its work item. */ | |
| 1914 | 1916 | export type WaitingPull = { repoId: string; number: number; title: string; createdAt: string; agent: boolean; draftSeconds: number | null; item: number | null; blocks: number[] }; | |
| 1915 | 1917 | export type SignalPull = { |
| 2 | 2 | -- `YYYY-MM-DDT00:00:00`. One row per repository and UTC day that had | |
| 3 | 3 | -- anything in it. Reviews are comments of any kind on a pull request by a | |
| 4 | 4 | -- person other than its owner (not g1t's agent, a workspace agent or g1t | |
| 5 | − | -- itself); reworked, the ones of them that ask for changes. | |
| 5 | + | -- itself); reworked, the ones of them that ask for changes. Agent commits | |
| 6 | + | -- are those agent sessions pushed outside any pull request (weeks.sql). | |
| 6 | 7 | , | |
| 7 | − | ev(repo_id, day, opened, merged, agent_merged, reworked, reviews, sessions) AS ( | |
| 8 | − | SELECT repo_id, substr(created_at, 1, 10), 1, 0, 0, 0, 0, 0 FROM pr | |
| 8 | + | ev(repo_id, day, opened, merged, agent_merged, reworked, reviews, sessions, agent_commits) AS ( | |
| 9 | + | SELECT repo_id, substr(created_at, 1, 10), 1, 0, 0, 0, 0, 0, 0 FROM pr | |
| 9 | 10 | WHERE created_at >= ?2 AND created_at < ?3 | |
| 10 | − | UNION ALL SELECT repo_id, substr(merged_at, 1, 10), 0, 1, agent, 0, 0, 0 FROM pr | |
| 11 | + | UNION ALL SELECT repo_id, substr(merged_at, 1, 10), 0, 1, agent, 0, 0, 0, 0 FROM pr | |
| 11 | 12 | WHERE status = 'merged' AND merged_at >= ?2 AND merged_at < ?3 | |
| 12 | − | UNION ALL SELECT c.repo_id, substr(c.created_at, 1, 10), 0, 0, 0, c.verdict IS 'request_changes', 1, 0 | |
| 13 | + | UNION ALL SELECT c.repo_id, substr(c.created_at, 1, 10), 0, 0, 0, c.verdict IS 'request_changes', 1, 0, 0 | |
| 13 | 14 | FROM comments c JOIN pr ON pr.repo_id = c.repo_id AND pr.number = c.number | |
| 14 | 15 | WHERE c.repo_id IN (SELECT value FROM json_each(?1)) AND c.created_at >= ?2 AND c.created_at < ?3 | |
| 15 | 16 | AND c.author_id != pr.owner_id AND c.agent_id IS NULL | |
| 16 | 17 | AND c.author_id NOT IN ('usr_g1t_agent', 'g1t', 'g1t_policy', 'svc_runner', 'g1t_runner') | |
| 17 | − | UNION ALL SELECT repo_id, substr(started_at, 1, 10), 0, 0, 0, 0, 0, 1 FROM sessions | |
| 18 | + | UNION ALL SELECT repo_id, substr(started_at, 1, 10), 0, 0, 0, 0, 0, 1, 0 FROM sessions | |
| 18 | 19 | WHERE repo_id IN (SELECT value FROM json_each(?1)) AND started_at >= ?2 AND started_at < ?3 | |
| 20 | + | UNION ALL SELECT t.repo_id, substr(t.at, 1, 10), 0, 0, 0, 0, 0, 0, COUNT(DISTINCT t.value) FROM session_touches t | |
| 21 | + | WHERE t.repo_id IN (SELECT value FROM json_each(?1)) AND t.kind = 'commit' AND t.at >= ?2 AND t.at < ?3 | |
| 22 | + | AND t.session_id NOT IN (SELECT session_id FROM tied) | |
| 23 | + | GROUP BY t.repo_id, substr(t.at, 1, 10) | |
| 19 | 24 | ) | |
| 20 | 25 | SELECT repo_id, day, SUM(opened) AS opened, SUM(merged) AS merged, SUM(agent_merged) AS agent_merged, | |
| 21 | − | SUM(reworked) AS reworked, SUM(reviews) AS reviews, SUM(sessions) AS sessions | |
| 26 | + | SUM(reworked) AS reworked, SUM(reviews) AS reviews, SUM(sessions) AS sessions, | |
| 27 | + | SUM(agent_commits) AS agent_commits | |
| 22 | 28 | FROM ev GROUP BY repo_id, day ORDER BY day, repo_id |
| 27 | 27 | CREATE TABLE commit_statuses (repo_id TEXT, sha TEXT, context TEXT, updated_at TEXT); | |
| 28 | 28 | CREATE TABLE commit_check_runs (id TEXT, repo_id TEXT, head_sha TEXT, created_at TEXT, updated_at TEXT); | |
| 29 | 29 | CREATE TABLE session_links (session_id TEXT, repo_id TEXT, kind TEXT, number INTEGER); | |
| 30 | − | CREATE TABLE session_touches (session_id TEXT, repo_id TEXT, kind TEXT, value TEXT); | |
| 30 | + | CREATE TABLE session_touches (session_id TEXT, repo_id TEXT, kind TEXT, value TEXT, at TEXT); | |
| 31 | 31 | CREATE TABLE work_items (id TEXT, workspace TEXT, number INTEGER, title TEXT, state TEXT, | |
| 32 | 32 | blocked_by TEXT DEFAULT '[]', milestone INTEGER); | |
| 33 | 33 | CREATE TABLE milestones (workspace TEXT, number INTEGER, title TEXT, state TEXT, due_on TEXT, | |
| ⋯ | |||
| 195 | 195 | INSERT INTO sessions (id, repo_id, pull, started_at) VALUES ('ses_4', 'a', 4, '2026-10-06T13:00:00Z'); | |
| 196 | 196 | INSERT INTO sessions (id, repo_id, pull, started_at) VALUES ('ses_5', 'a', NULL, '2026-10-06T11:30:00Z'); | |
| 197 | 197 | INSERT INTO sessions (id, repo_id, pull, started_at) VALUES ('ses_5b', 'a', NULL, '2026-10-06T10:00:00.500Z'); | |
| 198 | − | INSERT INTO session_touches VALUES ('ses_5', 'a', 'branch', 'branch-5'); | |
| 199 | − | INSERT INTO session_touches VALUES ('ses_5b', 'a', 'branch', 'branch-5'); | |
| 198 | + | INSERT INTO session_touches (session_id, repo_id, kind, value) VALUES ('ses_5', 'a', 'branch', 'branch-5'); | |
| 199 | + | INSERT INTO session_touches (session_id, repo_id, kind, value) VALUES ('ses_5b', 'a', 'branch', 'branch-5'); | |
| 200 | 200 | -- 6's branch is on a fork: a push to the same name is not its. | |
| 201 | 201 | UPDATE pulls SET fork_repo_id = 'fork' WHERE number = 6; | |
| 202 | 202 | INSERT INTO sessions (id, repo_id, pull, started_at) VALUES ('ses_6', 'a', NULL, '2026-10-06T11:00:00Z'); | |
| 203 | − | INSERT INTO session_touches VALUES ('ses_6', 'a', 'branch', 'branch-6'); | |
| 203 | + | INSERT INTO session_touches (session_id, repo_id, kind, value) VALUES ('ses_6', 'a', 'branch', 'branch-6'); | |
| 204 | 204 | -- A session of another repository with the same number is not its. | |
| 205 | 205 | INSERT INTO sessions (id, repo_id, pull, started_at) VALUES ('ses_x', 'b', 6, '2026-10-06T11:00:00Z');", | |
| 206 | 206 | ) | |
| ⋯ | |||
| 735 | 735 | -- A subagent tied itself, by a push to 3's branch, counts with that. | |
| 736 | 736 | INSERT INTO sessions (id, repo_id, parent_id, started_at, cost_usd) | |
| 737 | 737 | VALUES ('ses_b', 'a', 'ses_r', '2026-10-06T00:00:00Z', 3); | |
| 738 | − | INSERT INTO session_touches VALUES ('ses_b', 'a', 'branch', 'branch-3'); | |
| 738 | + | INSERT INTO session_touches (session_id, repo_id, kind, value) VALUES ('ses_b', 'a', 'branch', 'branch-3'); | |
| 739 | 739 | -- A session whose cost is not known. | |
| 740 | 740 | INSERT INTO sessions (id, repo_id, pull, started_at, cost_usd) VALUES ('ses_n', 'a', 3, '2026-10-06T00:00:00Z', NULL); | |
| 741 | 741 | -- A session tied to nothing counts with nothing. | |
| ⋯ | |||
| 793 | 793 | ] | |
| 794 | 794 | ); | |
| 795 | 795 | } | |
| 796 | + | ||
| 797 | + | #[test] | |
| 798 | + | fn agent_commits_are_a_sessions_own_outside_any_pull_request() { | |
| 799 | + | let db = schema(); | |
| 800 | + | pull(&db, 1, "open", "2026-10-06T00:00:00Z", None, "2026-10-06T00:00:00Z"); | |
| 801 | + | db.execute_batch( | |
| 802 | + | "UPDATE pulls SET source_branch = 'feature' WHERE number = 1; | |
| 803 | + | INSERT INTO sessions (id, repo_id, started_at) VALUES ('ses_main', 'a', '2026-10-06T00:00:00Z'); | |
| 804 | + | INSERT INTO sessions (id, repo_id, started_at) VALUES ('ses_pr', 'a', '2026-10-06T00:00:00Z'); | |
| 805 | + | INSERT INTO session_touches VALUES ('ses_main', 'a', 'commit', 'c1', '2026-10-06T10:00:00Z'); | |
| 806 | + | INSERT INTO session_touches VALUES ('ses_main', 'a', 'commit', 'c1', '2026-10-06T10:00:01Z'); | |
| 807 | + | INSERT INTO session_touches VALUES ('ses_main', 'a', 'commit', 'c2', '2026-10-01T10:00:00Z'); | |
| 808 | + | INSERT INTO session_touches VALUES ('ses_main', 'b', 'commit', 'c9', '2026-10-06T10:00:00Z'); | |
| 809 | + | INSERT INTO session_touches VALUES ('ses_pr', 'a', 'branch', 'feature', '2026-10-06T09:00:00Z'); | |
| 810 | + | INSERT INTO session_touches VALUES ('ses_pr', 'a', 'commit', 'c3', '2026-10-06T10:00:00Z');", | |
| 811 | + | ) | |
| 812 | + | .unwrap(); | |
| 813 | + | let found = weeks(&db, "2026-10-18T00:00:00Z"); | |
| 814 | + | // c1 once, in its week; c2 in the week before; c3 belongs to a pull request's session; c9 to another repository. | |
| 815 | + | assert_eq!(in_week(&found, "2026-10-05").agent_commits, 1); | |
| 816 | + | assert_eq!(in_week(&found, "2026-09-28").agent_commits, 1); | |
| 817 | + | let window = window("2026-10-18T00:00:00Z"); | |
| 818 | + | let days: Vec<SignalDay> = rows(&db, DAYS, &[&IDS, &window.days_from, &window.end]); | |
| 819 | + | let commits: Vec<(&str, u32)> = days | |
| 820 | + | .iter() | |
| 821 | + | .filter(|d| d.agent_commits > 0) | |
| 822 | + | .map(|d| (d.day.as_str(), d.agent_commits)) | |
| 823 | + | .collect(); | |
| 824 | + | assert_eq!( | |
| 825 | + | commits, | |
| 826 | + | [("2026-10-06", 1)], | |
| 827 | + | "the days read are the last two weeks; c2 is before them" | |
| 828 | + | ); | |
| 829 | + | } | |
| 84 | 84 | TOTAL(CASE WHEN x.agent AND x.c >= w.lo AND x.c < w.hi THEN x.draft_seconds END) AS agent_draft_seconds, | |
| 85 | 85 | COUNT(CASE WHEN x.r >= w.lo AND x.r < w.hi THEN 1 END) AS reviewed, | |
| 86 | 86 | TOTAL(CASE WHEN x.r >= w.lo AND x.r < w.hi THEN MAX(0, (x.r - x.c) * 86400) END) AS review_seconds, | |
| 87 | − | 0 AS stalled, 0.0 AS session_cost_usd | |
| 87 | + | 0 AS stalled, 0.0 AS session_cost_usd, 0 AS agent_commits | |
| 88 | 88 | FROM x CROSS JOIN w | |
| 89 | 89 | GROUP BY x.repo_id, w.start | |
| 90 | 90 | ), | |
| ⋯ | |||
| 92 | 92 | -- least three days before, and nothing in the three days before it. | |
| 93 | 93 | sx AS ( | |
| 94 | 94 | SELECT k.repo_id, w.start, 0 AS merged, 0 AS agent_merged, 0 AS agent_reworked, 0 AS agent_drafted, | |
| 95 | − | 0.0 AS agent_draft_seconds, 0 AS reviewed, 0.0 AS review_seconds, COUNT(*) AS stalled, 0.0 AS session_cost_usd | |
| 95 | + | 0.0 AS agent_draft_seconds, 0 AS reviewed, 0.0 AS review_seconds, COUNT(*) AS stalled, 0.0 AS session_cost_usd, 0 AS agent_commits | |
| 96 | 96 | FROM cand k CROSS JOIN w LEFT JOIN busy b ON b.pull_id = k.id AND b.start = w.start | |
| 97 | 97 | WHERE b.pull_id IS NULL AND k.c <= w.cut - 3 | |
| 98 | 98 | AND NOT COALESCE(k.m < w.cut, 0) | |
| ⋯ | |||
| 104 | 104 | sw AS ( | |
| 105 | 105 | SELECT s.repo_id, w.start, 0 AS merged, 0 AS agent_merged, 0 AS agent_reworked, 0 AS agent_drafted, | |
| 106 | 106 | 0.0 AS agent_draft_seconds, 0 AS reviewed, 0.0 AS review_seconds, 0 AS stalled, | |
| 107 | − | TOTAL(s.cost_usd) AS session_cost_usd | |
| 107 | + | TOTAL(s.cost_usd) AS session_cost_usd, 0 AS agent_commits | |
| 108 | 108 | FROM sessions s JOIN w ON s.started_at >= w.lo_at AND s.started_at < w.hi_at | |
| 109 | 109 | WHERE s.repo_id IN (SELECT value FROM json_each(?1)) | |
| 110 | 110 | AND s.started_at >= (SELECT MIN(lo_at) FROM w) AND s.started_at < (SELECT MAX(hi_at) FROM w) | |
| 111 | 111 | GROUP BY s.repo_id, w.start | |
| 112 | + | ), | |
| 113 | + | -- The commits agent sessions pushed outside any pull request (to a | |
| 114 | + | -- branch no pull request is from, the default branch most of all): a | |
| 115 | + | -- session's own commits, by when it recorded them, each once. | |
| 116 | + | cw AS ( | |
| 117 | + | SELECT t.repo_id, w.start, 0 AS merged, 0 AS agent_merged, 0 AS agent_reworked, 0 AS agent_drafted, | |
| 118 | + | 0.0 AS agent_draft_seconds, 0 AS reviewed, 0.0 AS review_seconds, 0 AS stalled, 0.0 AS session_cost_usd, | |
| 119 | + | COUNT(DISTINCT t.value) AS agent_commits | |
| 120 | + | FROM session_touches t JOIN w ON t.at >= w.lo_at AND t.at < w.hi_at | |
| 121 | + | WHERE t.repo_id IN (SELECT value FROM json_each(?1)) AND t.kind = 'commit' | |
| 122 | + | AND t.session_id NOT IN (SELECT session_id FROM tied) | |
| 123 | + | GROUP BY t.repo_id, w.start | |
| 112 | 124 | ) | |
| 113 | 125 | SELECT repo_id, start, SUM(merged) AS merged, SUM(agent_merged) AS agent_merged, | |
| 114 | 126 | SUM(agent_reworked) AS agent_reworked, SUM(agent_drafted) AS agent_drafted, | |
| 115 | 127 | TOTAL(agent_draft_seconds) AS agent_draft_seconds, SUM(reviewed) AS reviewed, | |
| 116 | − | TOTAL(review_seconds) AS review_seconds, SUM(stalled) AS stalled, TOTAL(session_cost_usd) AS session_cost_usd | |
| 117 | − | FROM (SELECT * FROM pw UNION ALL SELECT * FROM sx UNION ALL SELECT * FROM sw) | |
| 128 | + | TOTAL(review_seconds) AS review_seconds, SUM(stalled) AS stalled, TOTAL(session_cost_usd) AS session_cost_usd, | |
| 129 | + | SUM(agent_commits) AS agent_commits | |
| 130 | + | FROM (SELECT * FROM pw UNION ALL SELECT * FROM sx UNION ALL SELECT * FROM sw UNION ALL SELECT * FROM cw) | |
| 118 | 131 | GROUP BY repo_id, start | |
| 119 | − | HAVING SUM(merged) + SUM(agent_drafted) + SUM(reviewed) + SUM(stalled) > 0 OR TOTAL(session_cost_usd) > 0 | |
| 132 | + | HAVING SUM(merged) + SUM(agent_drafted) + SUM(reviewed) + SUM(stalled) + SUM(agent_commits) > 0 OR TOTAL(session_cost_usd) > 0 | |
| 120 | 133 | ORDER BY start, repo_id | |
Comments
Sign in to comment.