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.

ubuntu-latest follows GitHub: 4 vCPU in a public repository, 2 in a private one #170

Mergedsyntaqx merged runners-like-github into main
26 files+576−9090/26 viewed
+5−4
266266 name: The desktop app's Linux build
267267 needs: look
268268 if: ${{ needs.look.outputs.desktop-linux == 'true' }}
269− # The larger machine: linking Tauri on the small one dies with a bus error.
270− runs-on: g1t-4core
269+ # ubuntu-latest is 4 vCPU in this public repository, as on GitHub; linking
270+ # Tauri needs it (on 2 vCPU it dies with a bus error).
271+ runs-on: ubuntu-latest
271272 timeout-minutes: 60
272273 steps:
273274 - uses: actions/checkout@v5
310311 name: The desktop app's Windows build
311312 needs: look
312313 if: ${{ needs.look.outputs.desktop-windows == 'true' && vars.DESKTOP_WINDOWS_FROM_LINUX == 'true' && vars.DESKTOP_WINDOWS_RUNNER != 'true' }}
313− runs-on: g1t-4core
314+ runs-on: ubuntu-latest
314315 timeout-minutes: 60
315316 steps:
316317 - uses: actions/checkout@v5
374375 needs: look
375376 if: ${{ needs.look.outputs.cli == 'true' }}
376377 # The builder runs in Docker, which g1t's own machines give each job.
377− runs-on: g1t-4core
378+ runs-on: ubuntu-latest
378379 timeout-minutes: 60
379380 steps:
380381 - uses: actions/checkout@v5
+4−4
3838 jobs:
3939 rust:
4040 name: Rust
41− runs-on: g1t-4core
41+ runs-on: ubuntu-latest
4242 timeout-minutes: 45
4343 env:
4444 # Line tables, not full debug info: a panic still names its file and
158158
159159 desktop:
160160 name: Desktop app
161− # The larger machine, as the Rust job: linking Tauri on the small one
162− # ran out of room and the linker died with a bus error.
163− runs-on: g1t-4core
161+ # ubuntu-latest is 4 vCPU in this public repository, as on GitHub; linking
162+ # Tauri needs it (on 2 vCPU it ran out of room and died with a bus error).
163+ runs-on: ubuntu-latest
164164 timeout-minutes: 30
165165 steps:
166166 - uses: actions/checkout@v5
+2−2
1111 # Every platform builds in one job, in the rust-cross/cargo-zigbuild image,
1212 # pinned by digest in scripts/cli-release.mjs: the macOS builds link Apple's
1313 # Security framework (the keychain), and that image carries its SDK. It
14−# runs on g1t's own machines (g1t-4core), whose jobs have a Docker Engine of
14+# runs on g1t's own machines (ubuntu-latest, 4 vCPU here), whose jobs have a Docker Engine of
1515 # their own.
1616 name: CLI release
1717
3434 jobs:
3535 build:
3636 name: Build every platform
37− runs-on: g1t-4core
37+ runs-on: ubuntu-latest
3838 timeout-minutes: 60
3939 steps:
4040 - uses: actions/checkout@v5
+4−5
156156 # Runs when nothing before it failed: a migrate job skipped for having
157157 # nothing to apply is not a failure.
158158 if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_core == 'true' && inputs.dry_run != true }}
159− # Rust builds and the runner's image get 4 vCPUs; everything else the
160− # standard machine.
161− runs-on: ${{ (matrix.rust || matrix.image) && 'g1t-4core' || 'ubuntu-latest' }}
159+ # ubuntu-latest: 4 vCPU in this public repository, as on GitHub.
160+ runs-on: ubuntu-latest
162161 environment:
163162 name: production
164163 url: https://g1t.sh
306305 name: edge (${{ matrix.group }})
307306 needs: [check, plan, migrate, core]
308307 if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_edge == 'true' && inputs.dry_run != true }}
309− runs-on: ${{ (matrix.rust || matrix.image) && 'g1t-4core' || 'ubuntu-latest' }}
308+ runs-on: ubuntu-latest
310309 environment:
311310 name: production
312311 url: https://g1t.sh
321320 name: front (${{ matrix.group }})
322321 needs: [check, plan, migrate, core, edge]
323322 if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_front == 'true' && inputs.dry_run != true }}
324− runs-on: ${{ (matrix.rust || matrix.image) && 'g1t-4core' || 'ubuntu-latest' }}
323+ runs-on: ubuntu-latest
325324 environment:
326325 name: production
327326 url: https://g1t.sh
+4−3
6363 linux:
6464 name: Linux
6565 needs: version
66− # The larger machine: linking Tauri on the small one dies with a bus error.
67− runs-on: g1t-4core
66+ # ubuntu-latest is 4 vCPU in this public repository, as on GitHub; linking
67+ # Tauri needs it (on 2 vCPU it dies with a bus error).
68+ runs-on: ubuntu-latest
6869 # Production's secrets, without a deployment: a release is not a deploy of
6970 # g1t.sh, and would otherwise show on its card as one in progress.
7071 environment:
168169 name: Windows, built on Linux
169170 needs: version
170171 if: vars.DESKTOP_WINDOWS_FROM_LINUX == 'true' && vars.DESKTOP_WINDOWS_RUNNER != 'true'
171− runs-on: g1t-4core
172+ runs-on: ubuntu-latest
172173 # Production's secrets, without a deployment: a release is not a deploy of
173174 # g1t.sh, and would otherwise show on its card as one in progress.
174175 environment:
+3−3
1111 # to make the release key the first time; CONTRIBUTING.md ("Deploying")
1212 # says how a release is made.
1313 #
14−# The image jobs run on g1t's own machines (g1t-4core), whose jobs have a
14+# The image jobs run on g1t's own machines (ubuntu-latest, 4 vCPU here), whose jobs have a
1515 # Docker Engine of their own (crates/runner/src/docker/engine.rs). arm64
1616 # images need QEMU's emulators registered with the kernel; each image job
1717 # tries that, and when the machine does not allow it, builds amd64 alone
126126 image:
127127 name: Container image
128128 needs: binaries
129− runs-on: g1t-4core
129+ runs-on: ubuntu-latest
130130 # Production's secrets, without a deployment: a release is not a deploy of
131131 # g1t.sh, and would otherwise show on its card as one in progress.
132132 environment:
183183 sandbox:
184184 name: Sandbox images
185185 needs: [binaries, image]
186− runs-on: g1t-4core
186+ runs-on: ubuntu-latest
187187 timeout-minutes: 60
188188 steps:
189189 - uses: actions/checkout@v5
+9−9
351351
352352 ### Machine sizes, time and storage
353353
354−| Limit | Value |
355−| ------------------------- | ----------------------------------------------------------------------------------------------------- |
356−| Largest machine | 4 vCPUs, 12 GiB of memory, 20 GB of disk (`g1t-4core`). No GPUs. |
357−| Standard machine | 2 vCPUs, 8 GiB of memory, 16 GB of disk (`ubuntu-latest`, every other hosted label, and `g1t-2core`). |
358−| One job on g1t's machines | 60 minutes. On a self-hosted runner, 24 hours. |
359−| One cache entry | 2 GiB, compressed. A larger one is not saved. |
360−| A repository's caches | 10 GiB together. Past it, the entries restored longest ago are removed. |
361−| One artifact | 5 GiB, zipped. Kept 14 days unless the repository says otherwise, at most 90. |
362−| A run's artifacts | 10 GiB together. |
354+| Limit | Value |
355+| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
356+| Largest machine | 4 vCPUs, 12 GiB of memory, 20 GB of disk (`g1t-4core`, and `ubuntu-latest` in a public repository). No GPUs. GitHub's machine for public repositories has 16 GB of memory. |
357+| Standard machine | 2 vCPUs, 8 GiB of memory, 16 GB of disk (`ubuntu-latest` and every other hosted label in a private repository, and `g1t-2core`). |
358+| One job on g1t's machines | 60 minutes. On a self-hosted runner, 24 hours. |
359+| One cache entry | 2 GiB, compressed. A larger one is not saved. |
360+| A repository's caches | 10 GiB together. Past it, the entries restored longest ago are removed. |
361+| One artifact | 5 GiB, zipped. Kept 14 days unless the repository says otherwise, at most 90. |
362+| A run's artifacts | 10 GiB together. |
363363
364364 The machine sizes are Cloudflare Containers' instance sizes. For more, use a
365365 [self-hosted runner](/guides/self-hosted-runners/). See
+8−5
7474 `runner` defines three Container classes, picked by `runs-on`, each a
7575 named Cloudflare instance type:
7676
77−| `runs-on` | Class | Size |
78−| ---------------------------------------------------- | ---------------- | ----------------------------------------------- |
79−| Any other hosted label, and every other kind of work | `AttemptSandbox` | `standard-3`: 2 vCPU, 8 GiB memory, 16 GB disk |
80−| `g1t-2core` | `Sandbox2Core` | `standard-3`: 2 vCPU, 8 GiB memory, 16 GB disk |
81−| `g1t-4core` | `Sandbox4Core` | `standard-4`: 4 vCPU, 12 GiB memory, 20 GB disk |
77+| `runs-on` | Class | Size |
78+| ---------------------------------------------------------------------------- | ---------------- | ----------------------------------------------- |
79+| Any other hosted label in a private repository, and every other kind of work | `AttemptSandbox` | `standard-3`: 2 vCPU, 8 GiB memory, 16 GB disk |
80+| `g1t-2core` | `Sandbox2Core` | `standard-3`: 2 vCPU, 8 GiB memory, 16 GB disk |
81+| `g1t-4core`, and any other hosted label in a public repository | `Sandbox4Core` | `standard-4`: 4 vCPU, 12 GiB memory, 20 GB disk |
8282
83+`ubuntu-latest` follows GitHub's hosted runners: the actions service
84+resolves it to `g1t-4core` when the run's repository is public (its
85+event's `repository.private`), and the job is billed as that machine.
8386 `g1t-2core` is kept as a name of the standard machine, so workflows that
8487 ask for it keep working; it keeps its class and its own `max_instances`
8588 in `services/runner/wrangler.jsonc`. The contracts' instance table
+17−10
554554
555555 ### Machine sizes
556556
557−A job runs on the standard machine unless its `runs-on` is `g1t-4core`:
557+`ubuntu-latest` gets what it gets on GitHub: 4 vCPUs in a public
558+repository, 2 in a private one. Any other Linux label is the same.
558559
559−| `runs-on` | vCPUs | Memory | Disk | A minute |
560−| -------------------------------------------- | ----- | ------ | ----- | -------- |
561−| `ubuntu-latest`, or any other Linux label | 2 | 8 GiB | 16 GB | $0.004 |
562−| `g1t-2core` (a name of the standard machine) | 2 | 8 GiB | 16 GB | $0.004 |
563−| `g1t-4core` | 4 | 12 GiB | 20 GB | $0.007 |
560+| `runs-on` | vCPUs | Memory | Disk | A minute |
561+| ----------------------------------------------------------------- | ----- | ------ | ----- | -------- |
562+| `ubuntu-latest`, or any other Linux label, in a public repository | 4 | 12 GiB | 20 GB | $0.007 |
563+| `ubuntu-latest`, or any other Linux label, in a private one | 2 | 8 GiB | 16 GB | $0.004 |
564+| `g1t-4core`, in any repository | 4 | 12 GiB | 20 GB | $0.007 |
565+| `g1t-2core`, in any repository | 2 | 8 GiB | 16 GB | $0.004 |
564566
567+GitHub's machine for public repositories has 16 GB of memory; g1t's has
568+12 GiB, the most its machines have.
569+
570+To choose a machine whatever the repository's visibility, name it:
571+
565572 ```yaml
566573 jobs:
567574 build:
568575 runs-on: g1t-4core
569576 ```
570577
571−`g1t-4core` is for heavier jobs: builds that compile, such as Rust or a
572−large TypeScript project, image builds and databases. `g1t-2core` is kept
573−as a name of the standard machine, so workflows that ask for it keep
574−working. The label can come from the matrix or the run's inputs
578+`g1t-4core` suits heavier jobs in a private repository: builds that
579+compile, such as Rust or a large TypeScript project, image builds and
580+databases. `g1t-2core` keeps a public repository's light jobs on the
581+smaller machine. The label can come from the matrix or the run's inputs
575582 (`runs-on: ${{ matrix.big && 'g1t-4core' || 'ubuntu-latest' }}`).
576583
577584 Each job is charged its time rounded up to the whole minute (61 seconds
+2−1
340340
341341 The standard sandbox is the instance type `standard-3` (2 vCPU, 8 GiB,
342342 16 GB), and so is `runs-on: g1t-2core`, in a class of its own; jobs that
343−ask for `runs-on: g1t-4core` get `standard-4` (4 vCPU, 12 GiB, 20 GB). At
343+ask for `runs-on: g1t-4core`, and `ubuntu-latest` jobs in a public
344+repository, get `standard-4` (4 vCPU, 12 GiB, 20 GB). At
344345 the `max_instances` in `services/runner/wrangler.jsonc` (20, 10 and 10),
345346 40 sandboxes at once hold 100 vCPU, 360 GiB of memory and 680 GB of disk,
346347 which your account's Containers limits for live instances must allow.
+71−78
11 ---
22 title: Home
3−description: Your workspace's front page. A box that asks g1t, your move (everything waiting on you, and the one item to start with), the agents, runs and builds going now, this week's work day by day with what is worth a look, and what happened since you were last here in plain English. How every number is worked out.
3+description: Your workspace's front page. The past week in four figures, the daily signal (what your work says today, with the evidence), where you're needed now, what happened since you were last here in plain English, what is running, and the one item to start with. How every number is worked out.
44 ---
55
66 **Home** is the first page of your workspace, at `/<workspace>/-/home`.
7−It reads top to bottom, from what you can do now to what already happened:
7+Across the top is [the past week](#the-past-week) in four figures, each
8+against the week before. Under it, the main column is read top to
9+bottom:
810
9−1. **What are we building today?**: a box that [asks g1t](#ask-from-home)
10− anything about your work, with questions to start with.
11−2. **[Your move](#your-move)**: everything waiting on you, however long ago
11+1. **[The daily signal](#the-daily-signal)**: what the workspace's work
12+ says today. Its headline is the finding that matters most, and the
13+ others follow, each a sentence with its evidence and where to see it.
14+ Under them is a box that [asks g1t](#ask-from-home) about any of it.
15+2. **Where you're needed**: everything waiting on you, however long ago
1216 it started, with the action beside each item. Its first row is
13− [Start here](#start-here), the one item to do first, and why. Beside it,
14− **[Live](#live)**: the agents at work, workflow runs and builds going
15− now, each agent session with what it has cost so far.
16−3. **[This week](#this-week)**: the week in a sentence and four figures,
17− the last two weeks day by day (what agents and people landed), and what
18− is [worth a look](#worth-a-look), each finding with its evidence and
19− pinned to the day it happened where it happened on one.
20−4. **Since you were last here**: one card for the span. It shows what people
17+ [Start here](#start-here), the one item to do first, and why.
18+3. **Since you were last here**: one card for the span. It shows what people
2119 did, what changed in plain English when you ask for it, and what landed,
22− each change with whether it is live yet. Beside it, kept in view as you
23− scroll: **[Spent](#spent)**, what the span cost and the month so far,
24− and **[Settled for you](#settled-for-you)**, what finished by itself.
20+ each change with whether it is live yet.
2521
26−On a phone it is one column, in the same order.
22+The side column stays in view as you scroll:
23+
24+- **[Running now](#running-now)**: workflow runs and builds still going.
25+- **[Spent](#spent)**: what the span cost, and the month so far.
26+- **[Settled for you](#settled-for-you)**: what finished by itself.
2727
28+On a phone it is one column: the past week, the daily signal, Where
29+you're needed, Running now, the span, then Spent and Settled for you.
30+
2831 For a workspace's owners, Home also opens
2932 [Getting started](/guides/workspaces/#getting-started), the setup guide,
3033 until they finish or close it. The **Getting started** button at the top
3639 so, and the rest of the page still shows. Only what you ask g1t is written
3740 by a model: [What changed](#what-changed) and the answers in Ask.
3841
39−## This week
40−
41−A sentence for what landed in the last seven UTC days and how much of it
42−was agents', then four figures. Each shows how it moved since the seven
43−days before (green when that is good news, red when it is not); point at
44−one for what it counts, and it opens the page behind it.
42+## The past week
4543
46−Under them are the last fourteen UTC days as columns: what agents landed
47−(their merged pull requests and the commits their sessions pushed
48−directly) in the accent, what people merged above it, last week's days
49−faded. Point at a day, or focus it, to read it in full: what landed, by
50−whom, and the sessions started. A finding that happened on one day, such
51−as a milestone that closed, is pinned over it with the number it has in
52−[Worth a look](#worth-a-look).
44+Four figures for the last seven UTC days. Each shows how it moved since the
45+seven days before (green when that is good news, red when it is not) and a
46+line over the last ten weeks. Point at the line, or focus it and use the
47+arrow keys, to read any week. Each figure opens the page behind it.
5348
54−| Figure | What it counts |
55−| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
56−| **Landed by agents** | 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. |
57−| **Cost 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. |
58−| **Gone quiet** | 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. |
59−| **Sent back** | 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. |
49+| Figure | What it counts |
50+| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
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+| **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 (direct commits get no review, so they're not counted here), the share a person asked for changes on before they merged. |
6055
6156 ### Whose work is an agent's
6257
6661 for it, else its author, so an agent's work counts toward the person it
6762 was for.
6863
69−### Worth a look
64+## The daily signal
7065
71−Under the days, what the work says is worth a look, most important first.
72−Each finding is a sentence with its evidence after it, a link to where you
73−can see it, and **Ask g1t**, which asks [Ask](/guides/ask/) to look into
74−it. A finding with a trend has its line beside it. When nothing stands
75−out against the workspace's last weeks, it says so. None of it is about
76−what needs you: that is [Your move](#your-move).
66+First the day, then one sentence for the finding that matters most today,
67+then the others. Each finding names its evidence in a line of figures,
68+links to where you can see it, and has **Ask g1t**, which asks
69+[Ask](/guides/ask/) to look into it. A finding with a trend has its line
70+beside it. When nothing stands out, the headline says what the week did.
7771
78−| Finding | When it shows |
79−| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
80−| **One unreviewed change holding up work** | 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. |
81−| **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. |
82−| **A milestone closed** | A milestone closed in the last seven days, against its due date, with its weeks in build, its merged pull requests and its projects. |
83−| **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. |
84−| **More changes gone quiet** | At least three more stalled changes than last week, and a quarter more. |
85−| **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). |
86−| **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. |
87−| **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. |
72+| Finding | When it shows |
73+| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
74+| **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. |
75+| **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. |
76+| **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 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. |
78+| **Stalled changes rising** | At least three more stalled changes than last week, and a quarter more. |
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+| **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** | 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. |
8882
8983 Findings are ordered by how much they hold up: blocked work first, then
90−waits, then what closed and where spend went. Up to four show, and the
84+waits, then what shipped and where spend went. Up to five show, and the
9185 rest are one click away. What the work says is read again at most every
9286 five minutes for each person, so a figure can be up to five minutes
9387 behind. The same reads, at more length, are on the
9589
9690 ## Ask from Home
9791
98−The box at the top of Home asks g1t anything about your work. Type a question and
92+The box under the daily signal asks g1t anything about your work. Type a question and
9993 press **Enter** (**Shift+Enter** starts a new line): the top bar's
10094 [Ask](/guides/ask/) panel opens and sends it, and the answer streams in
10195 there. Under the box are questions to start with:
115109 still answering an earlier question, the panel opens with your question
116110 in its box rather than sending it, so nothing you typed is lost.
117111
118−## Your move
112+## Where you're needed
119113
120114 This list has no time window: something that started waiting a week ago
121115 is here until you deal with it. Each row says what it is, what is at
151145 6. Mentions and direct messages.
152146
153147 The list shows the first eight rows; **Show more** opens the rest. The
154−line under its heading says how many things need you.
148+sentence under the page's heading starts with how many things need you.
155149
156150 ## Start here
157151
158−**Start here** is the first row of Your move, set apart at the
152+**Start here** is the first row of Where you're needed, set apart at the
159153 top: the most pressing tier, and within it the row that has waited
160154 longest. g1t picks it with that rule, so the same rows always give the
161155 same pick. It says why, such as "Everything it needs is met; it lands when
198192 - With no visit on record, Home shows the last 24 hours and says so. After
199193 more than 14 days away, it shows the last 14 days.
200194
201−The sentence under the card's heading sums it up, for example:
195+The sentence under the heading sums it up, for example:
202196
203−> Since Tuesday evening, 3 people pushed 41 commits and merged 5
204−> changes, and 12 changes landed.
197+> 8 things need you. Since Tuesday evening, 3 people pushed 41 commits and
198+> merged 5 changes, and 12 changes landed.
205199
206200 It says what people did first (the commits they pushed and the changes
207201 they merged; if neither, the messages they sent), then everything that
333327 requests, pushes and releases, deployments) didn't answer, the card says
334328 so under the list.
335329
336−## Live
330+## Running now
337331
338−Beside Your move, what is going right now. First the agents at work: each
339−live [session](/guides/sessions/) of a harness (Claude Code, Codex and so
340−on) or a hosted agent, with what it is on, who runs it, how long it has
341−gone, its subagents, and what it has cost so far, estimated at list price
342−from its token usage. Then workflow runs and builds, longest-going first,
343−each with its state and how long it has gone:
332+At the top of the side column, what is going right now, longest-going
333+first, each with its state and how long it has gone:
344334
345335 | Kind | What it is |
346336 | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
349339
350340 ## Spent
351341
352−Beside the span: what the workspace spent over it, from its
342+In the side column: what the workspace spent over the span, from its
353343 [statement](/guides/usage-and-billing/), with each line at its price: what
354344 was charged plus what your plan's included usage, a trial, a pool or a
355345 discount paid of it. The lines are the statement's kinds of charge, such
366356
367357 ## Settled for you
368358
369−Under Spent: your notifications in this workspace
359+At the bottom of the side column: your notifications in this workspace
370360 that [settled themselves](/guides/notifications/#threads-that-settle-themselves)
371361 in the span, newest first, each with why, such as **CI passed in run 330**
372362 or **Merged by @ana**: a later run passed, the pull request merged, the
378368 Home keeps up with the workspace without a reload. What lands, starts
379369 running, finishes or comes to need you while you are on the page joins its
380370 list, sliding in at its place and lit for a moment; counts and the
381−lines update with it. Sessions that end and runs that finish leave Live.
371+sentence under the heading update with it. Runs that finish leave Running
372+now.
382373
383374 | Home updates when | From |
384375 | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
403394 A member whose membership does not include Code sees Home without anything
404395 from Code:
405396
406−- This week is not shown.
397+- The past week and the daily signal are not shown. The page opens with
398+ **What are we building today?** and the box that asks g1t.
407399 - People shows what was said and the artifacts edited; nothing from
408400 repositories.
409−- Your move has no rows from Code, and no notifications about a
401+- Where you're needed has no rows from Code, and no notifications about a
410402 repository.
411−- Landed is not shown, and Live has no sessions, runs or builds.
403+- Landed and Running now are not shown.
412404 - Spent is the same for every member.
413405
414406 ## When something doesn't answer
416408 Each section reads its own part of g1t and shows as soon as it has it.
417409 When one does not answer:
418410
419−- Your move and Live name the parts that didn't answer,
411+- Where you're needed and Running now name the parts that didn't answer,
420412 so an empty list never reads as nothing waiting.
421413 - People counts what could be read, and names the part that is missing
422414 (Code or Chat). If nothing could be read, the card says so, and the
423− sentence under the card's heading says what people did couldn't be read.
415+ sentence under the heading says what people did couldn't be read.
424416 - Landed and Spent each say their part didn't answer. When
425417 deployments or sessions can't be read, Landed shows its rows without
426418 their live labels or agent markers.
427419 - Settled for you says when your notifications can't be read.
428−- This week says when the work couldn't be read. When some projects
429− didn't answer, it says the week leaves them out.
420+- The daily signal says when the work couldn't be read, and the past week
421+ is left out. When some projects didn't answer, it says its findings
422+ leave them out.
430423 - What changed says when Ask can't be reached or isn't set up, or when
431424 the question could not be sent.
432425 - If your last visit can't be read, the span is the last 24 hours and the
+2−2
9999 A thread that leaves you nothing to do moves to Done by itself, read, the
100100 moment what it told you of is over: a later run passes, the pull request
101101 merges, the review is no longer asked of you. It leaves your unread count,
102−the bell, [Home](/guides/home/#your-move)'s **Your
103−move** and `g1t status`, and
102+the bell, [Home](/guides/home/#where-youre-needed)'s **Where you're
103+needed** and `g1t status`, and
104104 stays under **Done** with its history. New activity brings it back, as with
105105 any thread.
106106
+15−9
102102 - **$10 of usage each month**, used first: everything g1t runs, at the
103103 [prices below](#what-it-costs) (runner minutes at their flat rate per
104104 machine, everything else at cost plus 20%). That is 2,500 runner minutes on the standard machine, or
105− about 1,428 on `g1t-4core`.
105+ about 1,428 on `g1t-4core` (which `ubuntu-latest` gets in a public
106+ repository).
106107 - **Everyone in the workspace** at one price, never per person.
107108 - **Unlimited** projects, previews and repositories.
108109 - Workflows, the merge queue,
244245
245246 Once its card is checked, a workspace gets **$5 of trial credit**, once. It
246247 pays for usage at the plan's prices: runner minutes for checks, workflows
247−and the merge queue (1,250 on the standard machine), and semantic search. It never pays for deployments.
248+and the merge queue (1,250 on the standard machine, about 714 on the
249+4-core one), and semantic search. It never pays for deployments.
248250
249251 - Each card gets one trial, whichever workspace it is checked in. Prepaid
250252 cards start none.
261263 ## The open-source pool
262264
263265 g1t sets aside **$25 a month** for public repositories, at most **$2 a
264−month** for any one repository: 6,250 and 500 runner minutes on the
265−standard machine. It pays for **checks, workflows and the merge queue on
266+month** for any one repository: about 3,571 and 285 minutes of
267+workflow jobs, which run on the 4-core machine in a public repository,
268+or 6,250 and 500 minutes of checks and merge queue runs on the standard
269+machine. It pays for **checks, workflows and the merge queue on
266270 public repositories**, after a card check. It does not pay
267271 for deployments, storage or embeddings.
268272
362366 queue run) is charged by the minute, at a fixed rate for the machine it
363367 ran on:
364368
365−| `runs-on` | Machine | vCPUs | Memory | Disk | A minute |
366−| ------------------------------------------------------ | ------------------- | ----- | ------ | ----- | -------- |
367−| `ubuntu-latest`, any other Linux label, or `g1t-2core` | Standard (`2-core`) | 2 | 8 GiB | 16 GB | $0.004 |
368−| `g1t-4core` | `4-core` | 4 | 12 GiB | 20 GB | $0.007 |
369+| `runs-on` | Machine | vCPUs | Memory | Disk | A minute |
370+| -------------------------------------------------------------------------------- | ------------------- | ----- | ------ | ----- | -------- |
371+| `ubuntu-latest` or any other Linux label in a private repository, or `g1t-2core` | Standard (`2-core`) | 2 | 8 GiB | 16 GB | $0.004 |
372+| `ubuntu-latest` or any other Linux label in a public repository, or `g1t-4core` | `4-core` | 4 | 12 GiB | 20 GB | $0.007 |
369373
370−Checks and the merge queue run on the standard machine. `g1t-2core` is
374+`ubuntu-latest` gets what it gets on GitHub: 4 vCPUs in a public
375+repository, 2 in a private one. A job is charged as the machine it ran
376+on. Checks and the merge queue run on the standard machine. `g1t-2core` is
371377 kept as a name of the standard machine, so workflows that ask for it keep
372378 working, at its rate.
373379
+2−2
675675
676676 The workspace's **Overview**, `g1t.sh/<workspace>/-/workspace`, opens with
677677 what its work says, for members who can see Code. It is worked out the
678−same way as [Home's week](/guides/home/#this-week), and
678+same way as [Home's daily signal](/guides/home/#the-daily-signal), and
679679 each person sees only the projects they can read.
680680
681−1. **[The past week](/guides/home/#this-week)**: the same four
681+1. **[The past week](/guides/home/#the-past-week)**: the same four
682682 figures as Home.
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
+18−9
3434 }
3535
3636 /**
37− * Home's box that asks g1t, with a few things to ask first. It leads the
38− * page, above everything it can be asked about, with what are we building
39− * today.
37+ * Home's box that asks g1t, with a few things to ask first: under the
38+ * daily signal, `compact`, it says to ask about any of it; on its own, it
39+ * leads the page with what are we building today.
4040 */
41−export function HomeAsk({ span }: { span: AskSpan }) {
41+export function HomeAsk({ span, compact = false }: { span: AskSpan; compact?: boolean }) {
4242 const [draft, setDraft] = useState("");
4343 const send = () => {
4444 if (!draft.trim()) return;
5656 }
5757 };
5858 return (
59− <section aria-labelledby="home-ask" className="mx-auto flex w-full max-w-5xl flex-col items-center gap-5 pt-4 pb-3 md:pt-8 md:pb-5">
60− <h1 id="home-ask" className="text-center text-[1.75rem] leading-tight font-semibold tracking-tight md:text-[2rem]">
61− What are we building today?
62− </h1>
59+ <section
60+ aria-labelledby="home-ask"
61+ className={compact ? "flex w-full flex-col gap-3" : "mx-auto flex w-full max-w-3xl flex-col items-center gap-5 pt-4 pb-2 md:pt-8"}
62+ >
63+ {compact ? (
64+ <h2 id="home-ask" className="text-sm font-semibold text-fg">
65+ Ask about any of this
66+ </h2>
67+ ) : (
68+ <h1 id="home-ask" className="text-center text-[1.75rem] leading-tight font-semibold tracking-tight md:text-[2rem]">
69+ What are we building today?
70+ </h1>
71+ )}
6372 <form onSubmit={submit} className="w-full">
6473 <div className="flex items-end gap-2 rounded-2xl border border-line bg-surface p-2 pl-4 shadow-sm transition-shadow focus-within:border-accent-dim focus-within:ring-2 focus-within:ring-accent/25">
6574 <MarkGlyph className="mb-2.5 h-4 shrink-0 max-sm:hidden" />
8392 </Hint>
8493 </div>
8594 </form>
86− <Suggestions className="justify-center">
95+ <Suggestions className={compact ? "" : "justify-center"}>
8796 <Suggestion
8897 suggestion={`Summarize what changed ${spanLabel(span)}`}
8998 onPick={() => window.dispatchEvent(new CustomEvent(SUMMARIZE_EVENT))}
+0−278
1−/**
2− * Home's week (lib/signals.ts): the week in a sentence and four figures,
3− * the last two weeks day by day (what agents and people landed), and what
4− * is worth a look, each pinned to the day it happened where it happened
5− * on one. The page leads with what needs you; this is the work's own
6− * account, so it never says whether anything needs anyone.
7− */
8−import { ChevronRight, MessageSquare, TriangleAlert } from "lucide-react";
9−import { useState } from "react";
10−import { Link } from "react-router";
11−
12−import { askG1t } from "../lib/ask";
13−import { cn } from "../lib/cn";
14−import type { DailySignal, DayBar, PulseTile, Signal } from "../lib/signals";
15−import { TrendLine } from "./signals";
16−import { Hint } from "./ui/hint";
17−import { Skeleton } from "./ui/skeleton";
18−
19−const DELTA_TONE: Record<"good" | "bad" | "flat", string> = { good: "text-success", bad: "text-danger", flat: "text-faint" };
20−
21−/** One figure: what it counts, this week's value, and the change on last week. */
22−function Figure({ tile }: { tile: PulseTile }) {
23− return (
24− <Hint label={tile.hint}>
25− <Link to={tile.to} className="group min-w-0 rounded-lg px-3 py-2 transition-colors hover:bg-raised">
26− <p className="truncate text-xs text-muted group-hover:text-fg">{tile.label}</p>
27− <p className="mt-0.5 flex items-baseline gap-2">
28− <span className="text-xl font-semibold tracking-tight text-fg tabular-nums">{tile.value}</span>
29− {tile.delta && (
30− <span className={cn("text-xs tabular-nums", DELTA_TONE[tile.delta.tone])}>
31− {tile.delta.text}
32− <span className="text-faint"> on last week</span>
33− </span>
34− )}
35− </p>
36− </Link>
37− </Hint>
38− );
39−}
40−
41−function dayName(day: string, style: "short" | "long"): string {
42− const date = new Date(`${day}T00:00:00Z`);
43− return style === "short"
44− ? date.toLocaleDateString("en-US", { weekday: "narrow", timeZone: "UTC" })
45− : date.toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric", timeZone: "UTC" });
46−}
47−
48−function dayWords(bar: DayBar): string {
49− const landed = bar.agent + bar.people;
50− if (landed === 0 && bar.sessions === 0) return `${dayName(bar.day, "long")}: nothing landed`;
51− const parts = [`${landed} landed`, `${bar.agent} by agents`, `${bar.people} by people`];
52− if (bar.sessions > 0) parts.push(`${bar.sessions} ${bar.sessions === 1 ? "session" : "sessions"} started`);
53− return `${dayName(bar.day, "long")}: ${parts.join(", ")}`;
54−}
55−
56−/**
57− * Two weeks of days as columns: agents' changes in the accent, people's
58− * above them, last week's faded. A finding that happened on a day is
59− * pinned over it with its number in the list below.
60− */
61−function Days({ days, pins }: { days: DayBar[]; pins: Map<string, number[]> }) {
62− const [active, setActive] = useState<number | null>(null);
63− const top = Math.max(1, ...days.map((bar) => bar.agent + bar.people));
64− const thisWeek = days.length - 7;
65− const shown = active ?? days.length - 1;
66− return (
67− <figure className="px-5">
68− <div
69− className="grid h-36 items-end gap-1.5 sm:gap-2"
70− style={{ gridTemplateColumns: `repeat(${days.length}, minmax(0, 1fr))` }}
71− onPointerLeave={() => setActive(null)}
72− >
73− {days.map((bar, i) => {
74− const landed = bar.agent + bar.people;
75− const pinned = pins.get(bar.day);
76− return (
77− <button
78− key={bar.day}
79− type="button"
80− aria-label={dayWords(bar)}
81− onPointerEnter={() => setActive(i)}
82− onFocus={() => setActive(i)}
83− onBlur={() => setActive(null)}
84− className={cn(
85− "group relative flex h-full flex-col items-stretch justify-end rounded-md outline-none focus-visible:ring-2 focus-visible:ring-accent/40",
86− i < thisWeek && "opacity-45",
87− active === i && "opacity-100",
88− )}
89− >
90− {pinned && (
91− <span className="absolute inset-x-0 -top-1 flex justify-center gap-0.5">
92− {pinned.map((n) => (
93− <span key={n} className="flex size-4 items-center justify-center rounded-full bg-warn text-[0.625rem] font-semibold text-bg">
94− {n}
95− </span>
96− ))}
97− </span>
98− )}
99− <span
100− className="mx-auto flex w-full max-w-11 flex-col justify-end overflow-hidden rounded-[5px]"
101− style={{ height: `${Math.max(landed > 0 ? 6 : 2, (landed / top) * 80)}%` }}
102− >
103− {landed === 0 ? (
104− <span className="h-full bg-line" />
105− ) : (
106− <>
107− <span className="bg-line-strong transition-colors group-hover:bg-fg-soft/50" style={{ flexGrow: bar.people }} />
108− <span className="bg-accent transition-colors group-hover:bg-accent-hover" style={{ flexGrow: bar.agent }} />
109− </>
110− )}
111− </span>
112− </button>
113− );
114− })}
115− </div>
116− <div className="mt-1.5 grid gap-1.5 sm:gap-2" style={{ gridTemplateColumns: `repeat(${days.length}, minmax(0, 1fr))` }} aria-hidden>
117− {days.map((bar, i) => (
118− <span key={bar.day} className={cn("text-center font-mono text-[0.625rem] text-faint", i === days.length - 1 && "text-fg-soft")}>
119− {dayName(bar.day, "short")}
120− </span>
121− ))}
122− </div>
123− <figcaption className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted">
124− <span className="text-fg-soft" aria-live="polite">
125− {days[shown] ? dayWords(days[shown]) : ""}
126− </span>
127− <span className="ml-auto flex items-center gap-3">
128− <span className="flex items-center gap-1.5">
129− <span className="size-2 rounded-sm bg-accent" />
130− Agents
131− </span>
132− <span className="flex items-center gap-1.5">
133− <span className="size-2 rounded-sm bg-line-strong" />
134− People
135− </span>
136− <span className="text-faint">last week faded</span>
137− </span>
138− </figcaption>
139− </figure>
140− );
141−}
142−
143−/** A finding: what the work says, the evidence after it, where to see it, and Ask. */
144−function Finding({ signal, n }: { signal: Signal; n: number | null }) {
145− return (
146− <li className={cn("grid gap-3 border-t border-line px-5 py-4", signal.trend && "md:grid-cols-[minmax(0,1fr)_9rem] md:gap-6")}>
147− <div className="flex min-w-0 gap-3">
148− <span
149− className={cn(
150− "mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full text-[0.6875rem] font-semibold",
151− n != null ? "bg-warn text-bg" : "border border-line-strong text-faint",
152− )}
153− aria-hidden
154− >
155− {n ?? "·"}
156− </span>
157− <div className="min-w-0 space-y-1">
158− <p className="text-sm leading-snug font-medium text-fg">{signal.headline}</p>
159− <p className="text-xs leading-relaxed text-muted">{signal.facts.join(" · ")}</p>
160− <div className="flex flex-wrap items-center gap-x-4 gap-y-1 pt-0.5">
161− {signal.link && (
162− <Link to={signal.link.to} className="inline-flex items-center gap-0.5 text-xs font-medium text-accent hover:underline">
163− {signal.link.label}
164− <ChevronRight size={12} />
165− </Link>
166− )}
167− <button type="button" onClick={() => askG1t(signal.ask)} className="inline-flex items-center gap-1 text-xs text-muted hover:text-fg">
168− <MessageSquare size={11} />
169− Ask g1t
170− </button>
171− </div>
172− </div>
173− </div>
174− {signal.trend && (
175− <div className="hidden md:block">
176− <TrendLine
177− values={signal.trend.values}
178− points={signal.trend.points}
179− label={`${signal.trend.caption}, ${signal.trend.range}`}
180− width={144}
181− height={32}
182− />
183− <p className="mt-1 font-mono text-[0.625rem] text-faint">{signal.trend.range}</p>
184− </div>
185− )}
186− </li>
187− );
188−}
189−
190−/** Findings shown before "N more". */
191−const SHOWN = 4;
192−
193−/** Home's week: its figures, its days with findings pinned, and what is worth a look. */
194−export function HomeWeek({ view }: { view: DailySignal | null }) {
195− const [more, setMore] = useState(false);
196− if (view == null) {
197− return (
198− <section aria-labelledby="week" className="rounded-2xl border border-line bg-surface px-5 py-4">
199− <h2 id="week" className="text-base font-semibold tracking-tight text-fg">
200− This week
201− </h2>
202− <p className="mt-2 flex items-center gap-1.5 text-sm text-muted">
203− <TriangleAlert size={13} className="shrink-0 text-warn" />
204− The week&apos;s work couldn&apos;t be read just now.
205− </p>
206− </section>
207− );
208− }
209− // A number for each finding pinned to a day in the chart, in the list's order.
210− const days = new Set(view.days.map((bar) => bar.day));
211− const numbers = new Map<string, number>();
212− const pins = new Map<string, number[]>();
213− for (const signal of view.findings) {
214− if (!signal.day || !days.has(signal.day)) continue;
215− const n = numbers.size + 1;
216− numbers.set(signal.key, n);
217− pins.set(signal.day, [...(pins.get(signal.day) ?? []), n]);
218− }
219− const shown = more ? view.findings : view.findings.slice(0, SHOWN);
220− return (
221− <section aria-labelledby="week" className="rounded-2xl border border-line bg-surface">
222− <header className="flex flex-wrap items-baseline gap-x-3 gap-y-1 px-5 pt-4">
223− <h2 id="week" className="text-base font-semibold tracking-tight text-fg">
224− This week
225− </h2>
226− <p className="text-sm text-muted">{view.summary}</p>
227− </header>
228− {view.tiles.length > 0 && (
229− <div className="mt-2 grid grid-cols-2 gap-1 px-2 lg:grid-cols-4">
230− {view.tiles.map((tile) => (
231− <Figure key={tile.key} tile={tile} />
232− ))}
233− </div>
234− )}
235− <div className="mt-4">
236− <Days days={view.days} pins={pins} />
237− </div>
238− <div className="mt-5">
239− <h3 className="px-5 pb-2 text-xs font-medium text-muted">Worth a look</h3>
240− {view.findings.length === 0 ? (
241− <p className="border-t border-line px-5 py-4 text-sm text-muted">Nothing in the work stands out against its last weeks.</p>
242− ) : (
243− <ul>
244− {shown.map((signal) => (
245− <Finding key={signal.key} signal={signal} n={numbers.get(signal.key) ?? null} />
246− ))}
247− </ul>
248− )}
249− {view.findings.length > SHOWN && (
250− <button type="button" onClick={() => setMore(!more)} className="border-t border-line px-5 py-3 text-left text-xs text-muted hover:text-fg w-full">
251− {more ? "Show fewer" : `${view.findings.length - SHOWN} more worth a look`}
252− </button>
253− )}
254− </div>
255− {!view.complete && (
256− <p className="flex items-center gap-1.5 border-t border-line px-5 py-2.5 text-xs text-faint">
257− <TriangleAlert size={12} className="shrink-0" />
258− Some projects didn&apos;t answer, so the week leaves them out.
259− </p>
260− )}
261− <div className="h-2" />
262− </section>
263− );
264−}
265−
266−export function HomeWeekSkeleton() {
267− return (
268− <section className="space-y-4 rounded-2xl border border-line bg-surface px-5 py-4" aria-busy="true">
269− <Skeleton className="h-5 w-72" />
270− <div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
271− {[0, 1, 2, 3].map((i) => (
272− <Skeleton key={i} className="h-10" />
273− ))}
274− </div>
275− <Skeleton className="h-36 w-full" />
276− </section>
277− );
278−}
+7−62
11 /**
2− * Home's parts (routes/workspace/home.tsx): Your move, Start here, what
3− * happened in the span (what people did, what landed, builds, spend) and
4− * Live (sessions, runs and builds going now). Each takes what
2+ * Home's parts (routes/workspace/home.tsx): Where you're needed, Start
3+ * here, what happened in the span (what people did, what landed, builds,
4+ * spend) and Running now. Each takes what
55 * lib/home.ts worked out and only draws it; a row that arrived while the
66 * page was open comes in (`animate-arrive`, lib/use-home-live.ts).
77 */
2727 } from "lucide-react";
2828 import type { ReactNode } from "react";
2929 import { Link } from "react-router";
30−
31−import type { WorkspaceSession } from "@g1t/contracts";
3230
3331 import {
3432 type Attention,
6260 import { useArrived } from "../lib/use-home-live";
6361 import { cn } from "../lib/cn";
6462 import { money } from "../lib/money";
65−import { repoName, runsIn, sessionHref, workspaceSessionTitle } from "../lib/sessions";
6663 import { CommitText } from "./commit-message";
67−import { HarnessMark, StateDot } from "./sessions";
6864 import { Button } from "./ui/button";
6965 import { Hint } from "./ui/hint";
7066 import { ButtonLink } from "./ui";
846842 deploy: <Rocket size={15} className="text-info" />,
847843 };
848844
849−export function RunningList({ rows, missing, now, sessions = 0 }: { rows: RunningRow[]; missing: string[]; now: number; sessions?: number }) {
845+export function RunningList({ rows, missing, now }: { rows: RunningRow[]; missing: string[]; now: number }) {
850846 const arrived = useArrived(rows.map((row) => row.key));
851847 return (
852848 <>
853849 {rows.length === 0 ? (
854− // With sessions going above it, an empty list of runs says nothing.
855− sessions > 0 ? null : (
856− <Quiet>
857− {missing.length > 0
858− ? "Nothing is running in what could be read."
859− : "Nothing is running. Agents at work, workflow runs and builds show here while they go."}
860− </Quiet>
861− )
850+ <Quiet>
851+ {missing.length > 0 ? "Nothing is running in what could be read." : "Nothing is running. Workflow runs and builds show here while they go."}
852+ </Quiet>
862853 ) : (
863854 <Folded
864855 items={rows}
885876 )}
886877 <Missing names={missing} what="everything running" />
887878 </>
888− );
889−}
890−
891−/**
892− * The agents at work now, each with what it is on, who runs it, how long
893− * it has gone and what it has cost so far (list price, from its token
894− * usage). Only g1t sees these: they are its sessions.
895− */
896−export function LiveSessions({ sessions, slug, now }: { sessions: WorkspaceSession[] | null; slug: string; now: number }) {
897− const arrived = useArrived((sessions ?? []).map((session) => session.id));
898− if (sessions == null) return <p className="px-5 pb-3 text-xs text-faint">Sessions couldn&apos;t be read just now.</p>;
899− if (sessions.length === 0) return null;
900− return (
901− <Folded
902− items={sessions}
903− render={(session) => {
904− const to = sessionHref(slug, session);
905− const cost = session.session?.costUsd;
906− const title = workspaceSessionTitle(session);
907− return (
908− <li key={session.id} className={cn(rowClass, "items-start px-4", arrived.has(session.id) && ARRIVED)}>
909− <span className="mt-1">
910− <StateDot state={session.state} />
911− </span>
912− <div className="min-w-0 grow">
913− {to ? (
914− <Link to={to} className="line-clamp-2 text-sm text-fg hover:underline">
915− {title}
916− </Link>
917− ) : (
918− <p className="line-clamp-2 text-sm text-fg">{title}</p>
919− )}
920− <p className="flex min-w-0 items-center gap-1.5 truncate text-xs text-muted">
921− <HarnessMark session={session} size={12} />
922− <span className="truncate">{[session.actor, runsIn(session), repoName(session.repo, slug)].filter(Boolean).join(" · ")}</span>
923− </p>
924− <p className="mt-1 text-xs text-fg-soft tabular-nums" suppressHydrationWarning>
925− {waited(Date.parse(session.startedAt), now)}
926− {cost != null && Number.isFinite(cost) && <span className="text-faint"> · {money(Math.round(cost * 1_000_000))} so far</span>}
927− {session.subagents > 0 && <span className="text-faint"> · {plural(session.subagents, "subagent")}</span>}
928− </p>
929− </div>
930− </li>
931− );
932− }}
933− />
934879 );
935880 }
+155−5
11 /**
2− * The week's figures on the Workspace overview (lib/signals.ts), each
3− * against the week before and drawn over ten, and the trend line they and
4− * Home's week (components/home-week.tsx) draw with.
2+ * The daily signal on Home (lib/signals.ts): the past week in four
3+ * figures, each against the week before and drawn over ten, and the
4+ * findings worth a look today, each a sentence with its evidence, where
5+ * to see it, and a way to ask g1t about it.
56 */
6−import { useId, useState } from "react";
7+import { ChevronRight, MessageSquare, TriangleAlert } from "lucide-react";
8+import { type ReactNode, useId, useState } from "react";
79 import { Link } from "react-router";
810
11+import { askG1t } from "../lib/ask";
912 import { cn } from "../lib/cn";
10−import type { PulseTile } from "../lib/signals";
13+import type { DailySignal as DailySignalView, PulseTile, Signal } from "../lib/signals";
1114 import { Skeleton } from "./ui/skeleton";
1215
1316 // --- A trend line -------------------------------------------------------------------
158161 </div>
159162 );
160163 }
164+
165+// --- The findings -------------------------------------------------------------------
166+
167+function Facts({ facts }: { facts: string[] }) {
168+ return (
169+ <p className="font-mono text-[0.75rem] leading-relaxed text-muted">
170+ {facts.map((fact, i) => (
171+ <span key={i}>
172+ {i > 0 && <span className="px-1.5 text-faint">·</span>}
173+ {fact}
174+ </span>
175+ ))}
176+ </p>
177+ );
178+}
179+
180+function Actions({ signal }: { signal: Signal }) {
181+ return (
182+ <div className="flex flex-wrap items-center gap-x-4 gap-y-1">
183+ {signal.link && (
184+ <Link to={signal.link.to} className="inline-flex items-center gap-0.5 text-sm font-medium text-accent hover:underline">
185+ {signal.link.label}
186+ <ChevronRight size={14} />
187+ </Link>
188+ )}
189+ <button type="button" onClick={() => askG1t(signal.ask)} className="inline-flex items-center gap-1 text-xs text-muted hover:text-fg">
190+ <MessageSquare size={12} />
191+ Ask g1t
192+ </button>
193+ </div>
194+ );
195+}
196+
197+function Trend({ trend }: { trend: NonNullable<Signal["trend"]> }) {
198+ const [point, setPoint] = useState<number | null>(null);
199+ return (
200+ <figure className="space-y-1.5">
201+ <TrendLine values={trend.values} points={trend.points} label={`${trend.caption}, ${trend.range}`} width={196} height={40} onPoint={setPoint} />
202+ <figcaption>
203+ <p className="text-xs text-fg-soft">{trend.caption}</p>
204+ <p className="font-mono text-[0.6875rem] text-faint">{point != null ? trend.points[point] : trend.range}</p>
205+ </figcaption>
206+ </figure>
207+ );
208+}
209+
210+function Finding({ signal }: { signal: Signal }) {
211+ return (
212+ <li className={cn("grid gap-4 py-5 first:pt-0 last:pb-0", signal.trend && "md:grid-cols-[minmax(0,1fr)_12.5rem] md:gap-8")}>
213+ <div className="min-w-0 space-y-2">
214+ <h3 className="font-display text-lg leading-snug text-fg md:text-xl">{signal.headline}</h3>
215+ <Facts facts={signal.facts} />
216+ <Actions signal={signal} />
217+ </div>
218+ {signal.trend && (
219+ <div className="md:pt-1">
220+ <Trend trend={signal.trend} />
221+ </div>
222+ )}
223+ </li>
224+ );
225+}
226+
227+/** Findings under the lead, before "and N more". */
228+const SHOWN = 4;
229+
230+/**
231+ * The daily signal: the day, the finding that matters most as the page's
232+ * headline, and the others under it. A quiet week still gets a sentence.
233+ * Null `view`: the work service did not answer.
234+ */
235+export function DailySignal({ date, view, line, footer }: { date: string; view: DailySignalView | null; line?: ReactNode; footer?: ReactNode }) {
236+ const [more, setMore] = useState(false);
237+ const lead = view?.findings[0] ?? null;
238+ const rest = view?.findings.slice(1) ?? [];
239+ const shown = more ? rest : rest.slice(0, SHOWN);
240+ return (
241+ <section aria-labelledby="daily-signal" className="rounded-2xl border border-line bg-surface px-5 py-6 md:px-8 md:py-8">
242+ <p className="font-mono text-[0.6875rem] tracking-[0.16em] text-accent uppercase">Daily signal</p>
243+ <p className="mt-2 font-display text-xl text-fg-soft md:text-2xl">{date}</p>
244+ {view == null ? (
245+ <>
246+ <h1 id="daily-signal" className="mt-4 font-display text-3xl leading-tight tracking-tight text-fg md:text-[2.5rem]">
247+ Here&apos;s your day.
248+ </h1>
249+ <p className="mt-3 flex items-center gap-1.5 text-sm text-muted">
250+ <TriangleAlert size={13} className="shrink-0 text-warn" />
251+ What the work says couldn&apos;t be read just now, so today&apos;s findings aren&apos;t here.
252+ </p>
253+ </>
254+ ) : (
255+ <>
256+ <h1
257+ id="daily-signal"
258+ className="mt-4 max-w-3xl font-display text-3xl leading-[1.12] tracking-tight text-balance text-fg md:text-[2.25rem] xl:text-[2.5rem]"
259+ >
260+ {lead?.headline ?? view.quiet}
261+ </h1>
262+ {lead && (
263+ <div className={cn("mt-4 grid gap-4", lead.trend && "md:grid-cols-[minmax(0,1fr)_12.5rem] md:gap-8")}>
264+ <div className="space-y-2">
265+ <Facts facts={lead.facts} />
266+ <Actions signal={lead} />
267+ </div>
268+ {lead.trend && <Trend trend={lead.trend} />}
269+ </div>
270+ )}
271+ {!view.complete && (
272+ <p className="mt-3 flex items-center gap-1.5 text-xs text-faint">
273+ <TriangleAlert size={12} className="shrink-0" />
274+ Some projects didn&apos;t answer, so these findings leave them out.
275+ </p>
276+ )}
277+ </>
278+ )}
279+ {line && <p className="mt-4 text-sm text-muted">{line}</p>}
280+ {shown.length > 0 && (
281+ <>
282+ <hr className="my-7 border-line" />
283+ <ul className="divide-y divide-line">
284+ {shown.map((signal) => (
285+ <Finding key={signal.key} signal={signal} />
286+ ))}
287+ </ul>
288+ {rest.length > SHOWN && (
289+ <button type="button" onClick={() => setMore(!more)} className="mt-5 text-xs text-muted hover:text-fg">
290+ {more ? "Show fewer" : `${rest.length - SHOWN} more ${rest.length - SHOWN === 1 ? "finding" : "findings"}`}
291+ </button>
292+ )}
293+ </>
294+ )}
295+ {footer && <div className="mt-8 border-t border-line pt-6">{footer}</div>}
296+ </section>
297+ );
298+}
299+
300+export function DailySignalSkeleton({ date }: { date: string }) {
301+ return (
302+ <section className="rounded-2xl border border-line bg-surface px-5 py-6 md:px-8 md:py-8">
303+ <p className="font-mono text-[0.6875rem] tracking-[0.16em] text-accent uppercase">Daily signal</p>
304+ <p className="mt-2 font-display text-xl text-fg-soft md:text-2xl">{date}</p>
305+ <Skeleton className="mt-5 h-9 w-full max-w-2xl" />
306+ <Skeleton className="mt-2 h-9 w-2/3 max-w-xl" />
307+ <Skeleton className="mt-5 h-3 w-64" />
308+ </section>
309+ );
310+}
+15−35
88 type WeekTotals,
99 agentStep,
1010 blockedSignal,
11− dayBars,
1211 dayList,
1312 emptySignals,
1413 findings,
1918 pulse,
2019 quarterSpend,
2120 quarterStart,
21+ quietHeadline,
2222 reviewSignal,
2323 reworkSignal,
2424 rhythm,
3333 spreading,
3434 stalledSignal,
3535 weekStarts,
36− weekSummary,
3736 weekTotals,
3837 weeklySpend,
3938 } from "./signals.ts";
197196 ],
198197 };
199198 const signal = blockedSignal(ctx({ signals }))!;
200− assert.equal(signal.headline, "tax#4821 holds up three work items, and no one has reviewed it yet.");
199+ assert.equal(signal.headline, "Three work items are waiting on one unreviewed change.");
201200 assert.deepEqual(signal.facts, ["tax#4821 Nexus threshold configuration", "open 2 days", "holds up #8, #9, #10"]);
202201 assert.equal(signal.link!.to, "/acme/tax/pull/4821");
203202 assert.equal(blockedSignal(ctx()), null);
233232 ],
234233 });
235234 const signal = spendSignal(ctx({ signals, quarterUsage }))!;
236− assert.equal(signal.headline, "The Checkout replatform took more than half of the agent spend this quarter.");
235+ assert.equal(signal.headline, "More than half of this quarter's agent spend is going into the Checkout replatform.");
237236 assert.deepEqual(signal.facts, ["$28.3K of $52.4K", "quarter to date"]);
238237 const even = usage({ byPull: quarterUsage.byPull.map((slice) => ({ ...slice, micros: 1_000_000 })) });
239238 assert.equal(spendSignal(ctx({ signals, quarterUsage: even })), null);
252251 ],
253252 };
254253 const signal = shippedSignal(ctx({ signals }))!;
255− assert.equal(signal.headline, "Single sign-on closed Tuesday, three weeks ahead of schedule.");
256− assert.equal(signal.day, "2026-10-13");
254+ assert.equal(signal.headline, "Single sign-on shipped on Tuesday, three weeks ahead of schedule.");
257255 assert.deepEqual(signal.facts, ["11 weeks in build", "2 changes", "2 projects"]);
258256 const old = { ...signals, milestones: [{ ...signals.milestones[0]!, closedAt: "2026-09-01T00:00:00Z" }] };
259257 assert.equal(shippedSignal(ctx({ signals: old })), null, "only the last week's");
300298 assert.equal(signal.headline, "bo is starting to work in web, which ana has carried almost alone.");
301299 });
302300
303−test("findings come highest first, and the week is said in a sentence that claims nothing about what needs anyone", () => {
301+test("findings come highest first, and a quiet week still says something", () => {
304302 const signals = {
305303 ...emptySignals(),
306304 waiting: [{ repoId: "r1", number: 2, title: "Fix", createdAt: "2026-10-16T00:00:00Z", agent: false, draftSeconds: null, item: 1, blocks: [3, 4] }],
310308 found.map((s) => s.key),
311309 ["blocked", "stalled"],
312310 );
313− assert.equal(weekSummary(week("x")), "Nothing has landed this week yet.");
314− assert.equal(weekSummary(week("x", { merged: 4, agentMerged: 2 })), "Four changes landed this week, 50% of them by agents.");
311+ assert.equal(quietHeadline(week("x")), "A quiet week: nothing has landed yet, and nothing is stuck.");
312+ assert.equal(
313+ quietHeadline(week("x", { merged: 4, agentMerged: 2 })),
314+ "Four changes landed this week, 50% of them an agent's, and nothing needs a closer look.",
315+ );
315316 // A workspace that pushes straight to main: agents' direct commits are what landed.
316− assert.equal(weekSummary(week("x", { merged: 1, agentMerged: 0, agentCommits: 9 })), "Ten changes landed this week, 90% of them by agents.");
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.",
320+ );
317321 });
318322
319323 test("waits are said in words and in a few characters", () => {
437441 test("agents' direct commits count as what they landed, and as what spend is spread over", () => {
438442 const weeks = [week("2026-10-05", { merged: 2, agentMerged: 1, agentCommits: 3 }), week("2026-10-12", { merged: 1, agentMerged: 1, agentCommits: 9 })];
439443 const [changes, spend] = pulse({ slug: "acme", weeks, spend: [4_000_000, 20_000_000] });
440− assert.equal(changes!.label, "Landed by agents");
444+ assert.equal(changes!.label, "Agent-authored changes");
441445 assert.equal(changes!.value, "10");
442446 assert.deepEqual(changes!.delta, { text: "+6", tone: "good" });
443447 assert.equal(changes!.points[1], "Week of Oct 12: 1 merged, 9 pushed directly, of 10 changes");
444448 assert.equal(spend!.value, "$2.00", "$20 over ten agent changes");
445−});
446−
447−test("the last two weeks day by day: every repository added up, agents' and people's, and empty days as zeros", () => {
448− const bars = dayBars(
449− [
450− { day: "2026-10-18", merged: 3, agentMerged: 2, agentCommits: 4, sessions: 5 },
451− { day: "2026-10-18", merged: 1, agentMerged: 0, agentCommits: 0, sessions: 1 },
452− { day: "2026-10-10", merged: 2, agentMerged: 2, agentCommits: 0, sessions: 0 },
453− // Outside the two weeks: left out.
454− { day: "2026-09-01", merged: 9, agentMerged: 9, agentCommits: 9, sessions: 9 },
455− ],
456− NOW,
457− );
458− assert.equal(bars.length, 14);
459− assert.equal(bars[0]!.day, "2026-10-05");
460− assert.deepEqual(bars.at(-1), { day: "2026-10-18", agent: 6, people: 2, sessions: 6 });
461− assert.deepEqual(
462− bars.find((bar) => bar.day === "2026-10-10"),
463− { day: "2026-10-10", agent: 2, people: 0, sessions: 0 },
464− );
465− assert.deepEqual(
466− bars.find((bar) => bar.day === "2026-10-11"),
467− { day: "2026-10-11", agent: 0, people: 0, sessions: 0 },
468− );
469449 });
+17−55
315315 return [
316316 {
317317 key: "merges",
318− label: "Landed by agents",
318+ label: "Agent-authored changes",
319319 value: String(agentChanges(now)),
320320 delta: deltaOf(agentChanges(now), before ? agentChanges(before) : null, count, true),
321321 series: weeks.map(agentChanges),
325325 },
326326 {
327327 key: "spend",
328− label: "Cost per agent change",
328+ label: "Spend per agent change",
329329 value: dollars(perMerge[at] ?? null),
330330 delta: deltaOf(perMerge[at] ?? null, before ? (perMerge[at - 1] ?? null) : null, (abs) => dollars(abs), false),
331331 series: perMerge,
335335 },
336336 {
337337 key: "stalled",
338− label: "Gone quiet",
338+ label: "Stalled changes",
339339 value: String(now.stalled),
340340 delta: deltaOf(now.stalled, before?.stalled ?? null, count, false),
341341 series: weeks.map((week) => week.stalled),
345345 },
346346 {
347347 key: "rework",
348− label: "Sent back",
348+ label: "Agent rework rate",
349349 value: percent(rework[at] ?? null),
350350 delta: deltaOf(rework[at] ?? null, before ? (rework[at - 1] ?? null) : null, (abs) => `${(abs * 100).toFixed(1).replace(/\.0$/, "")} pts`, false),
351351 series: rework,
369369 trend?: { values: (number | null)[]; points: string[]; caption: string; range: string };
370370 /** How much it matters today: findings are shown highest first. */
371371 weight: number;
372− /** The UTC day it happened (`YYYY-MM-DD`), when it happened on one: Home pins it there on the week. */
373− day?: string;
374372 /** What Ask is asked, to look into it. */
375373 ask: string;
376374 };
413411 const shown = top.blocks.slice(0, 3).map((number) => `#${number}`);
414412 return {
415413 key: "blocked",
416− headline:
417− n === 1
418− ? `${repo.name}#${top.number} holds up a work item, and no one has reviewed it yet.`
419− : `${repo.name}#${top.number} holds up ${countWord(n).toLowerCase()} work items, and no one has reviewed it yet.`,
414+ headline: n === 1 ? "A work item is waiting on a change no one has reviewed." : `${countWord(n)} work items are waiting on one unreviewed change.`,
420415 facts: [
421416 `${repo.name}#${top.number} ${top.title}`,
422417 `open ${spoken(ago(ctx.now, top.createdAt))}`,
547542 if (!top) return null;
548543 const share = top.micros / total;
549544 if (share < 0.35 || groups.size < 2) return null;
550− const amount = share > 0.5 ? "more than half" : share > 1 / 3 ? "more than a third" : `${Math.round(share * 100)}%`;
545+ const amount = share > 0.5 ? "More than half" : share > 1 / 3 ? "More than a third" : `${Math.round(share * 100)}%`;
551546 return {
552547 key: "spend",
553− headline: `${top.label.charAt(0).toUpperCase()}${top.label.slice(1)} took ${amount} of the agent spend this quarter.`,
548+ headline: `${amount} of this quarter's agent spend is going into ${top.label}.`,
554549 facts: [`${dollars(top.micros)} of ${dollars(total)}`, "quarter to date"],
555550 link: { label: "See the spend", to: `/${ctx.slug}/-/workspace#spend` },
556551 weight: 25 + share * 40,
585580 }
586581 return {
587582 key: "shipped",
588− headline: `${recent.title} closed ${weekday}${schedule}.`,
589− day: recent.closedAt!.slice(0, 10),
583+ headline: `${recent.title} shipped on ${weekday}${schedule}.`,
590584 facts: [
591585 `${weeksIn} ${weeksIn === 1 ? "week" : "weeks"} in build`,
592586 `${pulls.length} ${pulls.length === 1 ? "change" : "changes"}`,
613607 trend: {
614608 values: ctx.weeks.map((week) => week.stalled),
615609 points: ctx.weeks.map((week) => `Week of ${weekLabel(week.start)}: ${week.stalled}`),
616− caption: "Gone quiet",
610+ caption: "Stalled changes",
617611 range: `${ctx.weeks[0]!.stalled} → ${now.stalled} over ${ctx.weeks.length} weeks`,
618612 },
619613 weight: 30 + Math.min(25, more * 2),
821815 facts: [`${first.elsewhere} merges elsewhere before`, `first in ${repo.name}`],
822816 link: { label: "See the people", to: `/${ctx.slug}/-/workspace#quiet` },
823817 weight: 20 + Math.min(10, others * 3),
824− day: first.firstMergedAt.slice(0, 10),
825818 ask: `What did ${first.name} change in ${repo.namespace}/${repo.name}?`,
826819 };
827820 }
842835 return found.sort((a, b) => b.weight - a.weight);
843836 }
844837
845−/**
846− * The week in a sentence: what landed and how much of it was agents'. It
847− * says nothing about what needs anyone; Home leads with that on its own.
848− */
849−export function weekSummary(week: WeekTotals | undefined): string {
838+/** The headline when nothing stands out: still a sentence about the week. */
839+export function quietHeadline(week: WeekTotals | undefined): string {
850840 const landed = week ? allChanges(week) : 0;
851− if (!week || landed === 0) return "Nothing has landed this week yet.";
841+ if (!week || landed === 0) return "A quiet week: nothing has landed yet, and nothing is stuck.";
852842 const share = agentShare(week);
853− return `${countWord(landed)} ${landed === 1 ? "change" : "changes"} landed this week${share ? `, ${percent(share)} of them by agents` : ""}.`;
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.`;
854844 }
855845
856−/** One day on Home's week: what landed, agents' and people's, and the sessions started. */
857−export type DayBar = { day: string; agent: number; people: number; sessions: number };
858−
859−/**
860− * The last `count` UTC days to `now`, oldest first, every repository's
861− * rows added up; a day with nothing in it is zeros. Agents' are their
862− * merged pull requests and the commits their sessions pushed directly;
863− * people's, the other merged pull requests.
864− */
865−export function dayBars(days: Pick<SignalDay, "day" | "merged" | "agentMerged" | "agentCommits" | "sessions">[], now: number, count = SIGNAL_DAYS): DayBar[] {
866− const until = utcDay(now);
867− const bars = new Map<string, DayBar>();
868− for (let i = count - 1; i >= 0; i--) {
869− const day = addDays(until, -i);
870− bars.set(day, { day, agent: 0, people: 0, sessions: 0 });
871− }
872− for (const row of days) {
873− const bar = bars.get(row.day);
874− if (!bar) continue;
875− bar.agent += row.agentMerged + (row.agentCommits ?? 0);
876− bar.people += Math.max(0, row.merged - row.agentMerged);
877− bar.sessions += row.sessions;
878− }
879− return [...bars.values()];
880−}
881−
882−/** What Home shows of the work: the week's figures, its days, and the findings, most important first. */
846+/** What Home shows: the pulse, and the findings with the one that leads. */
883847 export type DailySignal = {
884848 tiles: PulseTile[];
885849 findings: Signal[];
886− /** The week in a sentence (`weekSummary`). */
887− summary: string;
888− days: DayBar[];
850+ /** The headline when no finding stands out. */
851+ quiet: string;
889852 complete: boolean;
890853 };
891854
911874 quarterUsage: data.quarterUsage,
912875 quarter: data.range.quarter,
913876 }),
914− summary: weekSummary(data.weeks[data.weeks.length - 1]),
915− days: dayBars(data.signals.days, data.now),
877+ quiet: quietHeadline(data.weeks[data.weeks.length - 1]),
916878 complete: data.complete,
917879 };
918880 }
+142−124
11 import { Suspense, useEffect, useState } from "react";
22 import { Await, data } from "react-router";
33
4−import { type WorkspaceSession, hasCodeAccess } from "@g1t/contracts";
4+import { hasCodeAccess } from "@g1t/contracts";
55
66 import type { Route } from "./+types/home";
77 import {
88 Card,
99 LandedFeed,
10− LiveSessions,
1110 NeedsList,
1211 PeopleSummary,
1312 RowsSkeleton,
1918 WindowSwitch,
2019 } from "../../components/home";
2120 import { HomeAsk, WhatChangedCard } from "../../components/home-ask";
22−import { HomeWeek, HomeWeekSkeleton } from "../../components/home-week";
21+import { DailySignal, DailySignalSkeleton, PulseSkeleton, PulseStrip } from "../../components/signals";
2322 import { Skeleton } from "../../components/ui/skeleton";
2423 import { sidebarOrNull } from "../../lib/chat.server";
2524 import {
6766 } from "../../lib/home.server";
6867 import { HOME_WATCH_MAX, type HomeRepo } from "../../lib/home-live";
6968 import { readCookie } from "../../lib/mission";
70−import { dailySignal } from "../../lib/signals";
69+import { dailySignal, signalDate } from "../../lib/signals";
7170 import { loadSignalData } from "../../lib/signals.server";
7271 import { useHomeLive } from "../../lib/use-home-live";
7372 import { guideIsFor, loadOnboarding } from "../../lib/onboarding.server";
7473 import { GettingStartedButton, OnboardingOnPage } from "../../components/onboarding";
7574 import { loadMissionControl } from "../../lib/mission-control.server";
7675 import { page } from "../../lib/meta";
77−import { inbox, work } from "../../lib/services.server";
76+import { inbox } from "../../lib/services.server";
7877 import { requireUser, roleIn } from "../../lib/session.server";
7978 import { homePagePath } from "../../lib/workspace-nav";
8079 import { Boundary } from "../../components/boundary";
8382 return page(args, { title: `Home · ${params.owner} · g1t` });
8483 }
8584
86−/** Unread notifications read for Your move. */
85+/** Unread notifications read for Needs you. */
8786 const NOTIFICATIONS_READ = 50;
88−/** Live sessions read for Live. */
89−const LIVE_READ = 12;
9087
9188 /**
92− * Home: the workspace's front page. First the box that asks g1t
89+ * Home: the workspace's front page. First the past week in four figures
90+ * and the daily signal: what the workspace's work says today, the finding
91+ * that matters most as the page's headline, each with its evidence
92+ * (lib/signals.ts), and under them a box that asks g1t about any of it
9393 * (components/home-ask: What changed summarizes the span on the page, and
94− * Explain on each change that landed asks about it). Then Your move:
95− * everything waiting on you, with no time window, beside Live: the agents
96− * at work, workflow runs and builds going now. Then the week's work
97− * (components/home-week, lib/signals.ts): four figures, the last two weeks
98− * day by day, and what is worth a look, each with its evidence and pinned
99− * to its day where it has one. Then what happened since you were last
100− * here (or over the last 24 hours or 7 days, `?window=`): what people
101− * did, from the events service's digest of the span and chat's, then what
102− * landed; beside it what the span cost and what settled by itself. The last visit is kept by notify, per
94+ * Explain on each change that landed asks about it); then where you're
95+ * needed now, with no time window; what happened since you were last here (or over the last 24
96+ * hours or 7 days, `?window=`): what people did, from the events
97+ * service's digest of the span and chat's, then what landed; what is
98+ * running; what the span cost. The last visit is kept by notify, per
10399 * person and workspace, and marked by the page once it has been looked
104100 * at (routes/workspace/home-seen.ts), so a refresh never wipes it. Only
105101 * the last visit (for the span) is waited for; every section streams in
223219 missing: [...(code && (!workflows || !workflows.complete) ? ["Workflows"] : []), ...(code && !deploys ? ["Deployments"] : [])],
224220 }))
225221 .catch(() => ({ rows: [], missing: ["Workflows", "Deployments"] }));
226− // The agents at work now: harness and hosted sessions that are live.
227− const live: Promise<WorkspaceSession[] | null> = code
228− ? work
229− .listWorkspaceSessions(slug, viewer, { shown: "live", sessionsOnly: true, limit: LIVE_READ })
230− .then((result) => (result.ok ? result.value.sessions : null))
231− .catch(() => null)
232− : Promise.resolve([]);
233222 const spend = loadSpend(viewer, slug, span).catch(() => null);
234223 // What the workspace's work says: the pulse and the findings, for members who see Code.
235224 const signal = code
258247 attention: waiting,
259248 away,
260249 running: runningNow,
261− live,
262− /** The sentence about the span needs the digest and what landed. */
263− head: Promise.all([digest, happened]),
250+ /** The sentence under the heading needs the digest, what landed and what waits. */
251+ head: Promise.all([digest, happened, waiting]),
264252 spend,
265253 signal,
254+ date: signalDate(now, tz),
266255 watch,
267256 onboarding,
268257 setupAsked,
327316 }
328317
329318 export default function Home({ loaderData }: Route.ComponentProps) {
330− const { slug, now, tz, span, digest, happened, attention: waiting, away, running: runningNow, live, head, spend, signal } = loaderData;
319+ const { slug, now, tz, span, digest, happened, attention: waiting, away, running: runningNow, head, spend, signal } = loaderData;
331320 useMarkSeen(slug, now);
332321 useHomeLive(slug, useWatched(loaderData.watch));
333322 return (
334− <div className="mx-auto w-full max-w-360 space-y-5 px-4 py-5 md:px-10 md:py-8">
323+ <div className="mx-auto w-full max-w-260 space-y-5 px-4 py-5 md:px-10 md:py-8">
335324 <OnboardingOnPage slug={slug} view={loaderData.onboarding} asked={loaderData.setupAsked} />
336325 {loaderData.guide && (
337326 <div className="flex justify-end">
338327 <GettingStartedButton slug={slug} variant="soft" />
339328 </div>
340329 )}
341− {/* 0. The box that asks g1t leads the page, above everything it can be asked about. */}
342− <HomeAsk span={span} />
343− {/*
344− What needs you and what is going on now, side by side; on a phone,
345− one after the other. Then the week's work, then what happened since
346− you were last here beside what it cost.
347− */}
348− <div className="grid grid-cols-[minmax(0,1fr)] items-start gap-5 lg:grid-cols-[minmax(0,1fr)_24rem]">
349− {/* 1. Your move: everything waiting on you, the one to start with on top, with no time window. */}
350− <div id="needed" className="scroll-mt-4">
351− <Suspense
352− fallback={
353− <Card title="Your move">
354− <RowsSkeleton rows={4} />
355− </Card>
356− }
357− >
358− {/* Each section's data says what did not answer; a section that fails to draw keeps that to itself. */}
359− <Await resolve={waiting}>
360− {(att) => (
361− <Card title="Your move" hint={waitingSentence(att.rows.length)} link={{ label: "Notifications", to: "/notifications" }}>
362− <Boundary what="Your move">
363− <NeedsList attention={att} now={now} />
364− </Boundary>
365− </Card>
366− )}
367− </Await>
368− </Suspense>
369− </div>
370−
371− {/* 2. Live: agents at work, then workflow runs and builds, as they go. */}
372− <Card title="Live" hint="Agents, runs and builds going now">
373− <Suspense fallback={<RowsSkeleton rows={2} />}>
374− <Await resolve={Promise.all([live, runningNow])}>
375− {([sessions, value]) => (
376− <Boundary what="Live">
377− <LiveSessions sessions={sessions} slug={slug} now={now} />
378− <RunningList rows={value.rows} missing={value.missing} now={now} sessions={sessions?.length ?? 0} />
379− </Boundary>
380− )}
381− </Await>
382− </Suspense>
383− </Card>
384− </div>
385−
386− {/* 3. The week's work, for members who see Code: its figures, its days, and what is worth a look. */}
330+ {/* 0. The past week, against the week before, for members who see Code. */}
387331 {signal && (
388− <Suspense fallback={<HomeWeekSkeleton />}>
332+ <Suspense fallback={<PulseSkeleton />}>
389333 <Await resolve={signal}>
390− {(value) => (
391− <Boundary what="This week">
392− <HomeWeek view={value} />
393− </Boundary>
394− )}
334+ {(value) =>
335+ value && (
336+ <Boundary what="The past week">
337+ <PulseStrip tiles={value.tiles} />
338+ </Boundary>
339+ )
340+ }
395341 </Await>
396342 </Suspense>
397343 )}
344+ {/*
345+ Two columns on wide screens: what to do and what happened on the
346+ left, read top to bottom; what is going on now and what it costs on
347+ the right, kept in view. On a phone it is one column: Where you're
348+ needed, Running now, what happened, then Spent and Settled (the side
349+ column's cards join it, `contents`, each in its place by `order`).
350+ */}
351+ <div className="grid grid-cols-[minmax(0,1fr)] items-start gap-5 lg:grid-cols-[minmax(0,1fr)_20rem]">
352+ {/* 1. Where you're needed now, with the one to start with on top: no window. */}
353+ <div className="order-1 space-y-5 lg:col-start-1 lg:row-start-1">
354+ {/* 1. The daily signal, then a box that asks g1t about any of it; the line under it is yours. */}
355+ {(() => {
356+ const line = (
357+ <Suspense fallback={<Skeleton className="inline-block h-3.5 w-72 align-middle" />}>
358+ <Await resolve={head}>
359+ {([digest, happened, att]) => (
360+ <>
361+ <span className="text-fg-soft">{waitingSentence(att.rows.length)}</span> {spanSentence(span, digest, happened?.landed?.length ?? null)}
362+ </>
363+ )}
364+ </Await>
365+ </Suspense>
366+ );
367+ const ask = <HomeAsk span={span} compact />;
368+ // Without Code there is no work to read: the box leads, as it always has.
369+ if (!signal)
370+ return (
371+ <header className="space-y-3">
372+ <HomeAsk span={span} />
373+ <p className="mx-auto max-w-2xl text-center text-sm leading-relaxed text-muted">{line}</p>
374+ </header>
375+ );
376+ return (
377+ <Suspense fallback={<DailySignalSkeleton date={loaderData.date} />}>
378+ <Await resolve={signal}>
379+ {(value) => (
380+ <Boundary what="The daily signal">
381+ <DailySignal date={loaderData.date} view={value} line={line} footer={ask} />
382+ </Boundary>
383+ )}
384+ </Await>
385+ </Suspense>
386+ );
387+ })()}
388+ <div id="needed" className="scroll-mt-4">
389+ <Card
390+ title="Where you're needed"
391+ hint="Everything waiting on you, however long ago it started"
392+ link={{ label: "Notifications", to: "/notifications" }}
393+ >
394+ <Suspense fallback={<RowsSkeleton rows={4} />}>
395+ {/* Each section's data says what did not answer; a section that fails to draw keeps that to itself. */}
396+ <Await resolve={waiting}>
397+ {(att) => (
398+ <Boundary what="Where you're needed">
399+ <NeedsList attention={att} now={now} />
400+ </Boundary>
401+ )}
402+ </Await>
403+ </Suspense>
404+ </Card>
405+ </div>
406+ </div>
398407
399− <div className="grid grid-cols-[minmax(0,1fr)] items-start gap-5 lg:grid-cols-[minmax(0,1fr)_24rem]">
400− {/* 4. What happened in the span: in a sentence, then in plain English, then who did what, then what landed. */}
401− <section aria-labelledby="since" className="rounded-2xl border border-line bg-surface">
408+ {/* 2. Now: what is running, what it cost, what settled by itself. */}
409+ <aside aria-label="Now" className="contents lg:sticky lg:top-4 lg:col-start-2 lg:row-span-2 lg:row-start-1 lg:block lg:space-y-5">
410+ <Card title="Running now" className="order-2">
411+ <Suspense fallback={<RowsSkeleton rows={2} />}>
412+ <Await resolve={runningNow}>
413+ {(value) => (
414+ <Boundary what="Running now">
415+ <RunningList rows={value.rows} missing={value.missing} now={now} />
416+ </Boundary>
417+ )}
418+ </Await>
419+ </Suspense>
420+ </Card>
421+ <div className="order-4">
422+ <Suspense
423+ fallback={
424+ <Card title="Spent">
425+ <SpendSkeleton />
426+ </Card>
427+ }
428+ >
429+ <Await resolve={spend}>
430+ {(value) => (
431+ <Boundary what="Spent">
432+ <SpendCard spend={value} slug={slug} span={span} />
433+ </Boundary>
434+ )}
435+ </Await>
436+ </Suspense>
437+ </div>
438+ <div className="order-5">
439+ <Suspense
440+ fallback={
441+ <Card title="Settled for you">
442+ <RowsSkeleton rows={1} />
443+ </Card>
444+ }
445+ >
446+ <Await resolve={away}>
447+ {(value) => (
448+ <Boundary what="Settled for you">
449+ <SettledCard away={value} now={now} />
450+ </Boundary>
451+ )}
452+ </Await>
453+ </Suspense>
454+ </div>
455+ </aside>
456+
457+ {/* 3. What happened in the span: what changed in plain English first, then who did what, then what landed. */}
458+ <section aria-labelledby="since" className="order-3 rounded-2xl border border-line bg-surface lg:col-start-1 lg:row-start-2">
402459 <header className="flex flex-wrap items-center gap-x-3 gap-y-2 px-5 pt-4">
403460 <div className="flex min-w-0 flex-wrap items-baseline gap-x-3 gap-y-1">
404461 <h2 id="since" className="text-base font-semibold tracking-tight text-fg">
410467 <WindowSwitch span={span} />
411468 </div>
412469 </header>
413− <p className="px-5 pt-2 text-sm text-muted">
414− <Suspense fallback={<Skeleton className="inline-block h-3.5 w-72 align-middle" />}>
415− <Await resolve={head}>{([digest, happened]) => spanSentence(span, digest, happened?.landed?.length ?? null)}</Await>
416− </Suspense>
417− </p>
418470 {/* Summarize leads: the plain-English account is what most people come for. */}
419471 <div className="mt-4">
420472 <WhatChangedCard key={span.key} slug={slug} span={span} tz={tz} />
454506 </Await>
455507 </Suspense>
456508 </section>
457−
458− {/* 5. What the span cost, and what settled by itself: kept in view beside it. */}
459− <aside aria-label="Cost and settled" className="space-y-5 lg:sticky lg:top-4">
460− <Suspense
461− fallback={
462− <Card title="Spent">
463− <SpendSkeleton />
464− </Card>
465− }
466− >
467− <Await resolve={spend}>
468− {(value) => (
469− <Boundary what="Spent">
470− <SpendCard spend={value} slug={slug} span={span} />
471− </Boundary>
472− )}
473− </Await>
474− </Suspense>
475− <Suspense
476− fallback={
477− <Card title="Settled for you">
478− <RowsSkeleton rows={1} />
479− </Card>
480− }
481− >
482− <Await resolve={away}>
483− {(value) => (
484− <Boundary what="Settled for you">
485− <SettledCard away={value} now={now} />
486− </Boundary>
487− )}
488− </Await>
489− </Suspense>
490− </aside>
491509 </div>
492510 </div>
493511 );
+6−4
872872 }
873873
874874 #[test]
875−fn deploy_builds_rust_on_the_larger_machine_with_its_target_cached() {
875+fn deploy_builds_on_ubuntu_latest_with_its_target_cached() {
876876 let deploy = read("deploy.yml");
877877 for stage in ["core", "edge", "front"] {
878878 let job = deploy.jobs.iter().find(|j| j.id == stage).unwrap();
889889 };
890890 expr::interpolate_value(&job.runs_on, &scope).unwrap()
891891 };
892− assert_eq!(on(true, false), json!("g1t-4core"), "{stage}");
893− // The runner's image is built with the job's own Docker Engine.
894− assert_eq!(on(false, true), json!("g1t-4core"), "{stage}");
892+ // Every job on ubuntu-latest, as on GitHub: 4 vCPU in this public
893+ // repository (g1t_contracts::actions::instance_for), Rust builds and
894+ // the runner's image included.
895+ assert_eq!(on(true, false), json!("ubuntu-latest"), "{stage}");
896+ assert_eq!(on(false, true), json!("ubuntu-latest"), "{stage}");
895897 assert_eq!(on(false, false), json!("ubuntu-latest"), "{stage}");
896898 }
897899 let source = std::fs::read_to_string(workflows_dir().join("deploy.yml")).unwrap();
+32−11
10891089 /// Minutes before the job is stopped.
10901090 pub timeout_minutes: u32,
10911091 /// The machine its `runs-on` asked for, by label (`instance_for`):
1092− /// `g1t-2core` or `g1t-4core`; absent, the standard one.
1092+ /// `g1t-2core` or `g1t-4core`, which is also what `ubuntu-latest`
1093+ /// gets in a public repository; absent, the standard one.
10931094 #[serde(default)]
10941095 pub instance: Option<String>,
10951096 /// Whether the job may be given brokered secrets, so the runner sends
11161117 /// 16 GB of disk), `g1t-2core` is kept as a name of it so workflows that
11171118 /// ask for it keep working, and `g1t-4core` is standard-4 (4 vCPU, 12 GiB,
11181119 /// 20 GB).
1120+///
1121+/// `ubuntu-latest` and every other label not named here follow GitHub's
1122+/// hosted runners ([`instance_for`]): in a public repository the 4-core
1123+/// machine (GitHub's is 4 vCPU and 16 GB; 12 GiB is the most a Containers
1124+/// instance has without a raised limit), in a private one the standard
1125+/// machine (GitHub's is 2 vCPU and 8 GB).
11191126 #[derive(Clone, Copy, Debug, PartialEq)]
11201127 pub struct InstanceType {
11211128 /// The `runs-on` label, or `standard` for the default.
11541161 }
11551162 }
11561163
1157−/// The default: what `ubuntu-latest` and every other hosted label get.
1164+/// The default in a private repository: what `ubuntu-latest` and every
1165+/// other hosted label get there, as on GitHub. Checks and the merge queue
1166+/// run on it too.
11581167 pub const STANDARD_INSTANCE: InstanceType = InstanceType {
11591168 label: "standard",
11601169 machine: "2-core",
12071216 if seconds <= 0 { 0 } else { (seconds + 59) / 60 }
12081217 }
12091218
1219+/// The label of the machine `ubuntu-latest` gets in a public repository:
1220+/// GitHub gives public repositories 4 vCPU.
1221+pub const PUBLIC_DEFAULT_LABEL: &str = "g1t-4core";
1222+
12101223 /// The machine a job's `runs-on` labels ask for: the dearest named (the
1211−/// later row when two cost the same), or the standard one. Labels compare
1212−/// without regard to case.
1213−pub fn instance_for(labels: &[String]) -> InstanceType {
1224+/// later row when two cost the same). With none named (`ubuntu-latest`,
1225+/// say), what GitHub gives: the 4-core machine in a public repository,
1226+/// the standard one in a private one. Labels compare without regard to
1227+/// case.
1228+pub fn instance_for(labels: &[String], public: bool) -> InstanceType {
12141229 INSTANCE_TYPES
12151230 .iter()
12161231 .filter(|instance| instance.label != STANDARD_INSTANCE.label && labels.iter().any(|label| label.trim().eq_ignore_ascii_case(instance.label)))
12171232 .max_by_key(|instance| instance.micros_per_minute)
12181233 .copied()
1219− .unwrap_or(STANDARD_INSTANCE)
1234+ .unwrap_or_else(|| if public { instance_named(PUBLIC_DEFAULT_LABEL).unwrap_or(STANDARD_INSTANCE) } else { STANDARD_INSTANCE })
12201235 }
12211236
12221237 /// An instance type by its label, if it is one.
17171732
17181733 #[test]
17191734 fn runs_on_picks_the_machine() {
1720− assert_eq!(instance_for(&labels(&["ubuntu-latest"])), STANDARD_INSTANCE);
1721− assert_eq!(instance_for(&labels(&[])).label, "standard");
1722− assert_eq!(instance_for(&labels(&["g1t-4core"])).label, "g1t-4core");
1723− assert_eq!(instance_for(&labels(&["ubuntu-latest", "G1T-2Core"])).label, "g1t-2core");
1735+ // As on GitHub: ubuntu-latest is 2 vCPU in a private repository, 4 in a public one.
1736+ assert_eq!(instance_for(&labels(&["ubuntu-latest"]), false), STANDARD_INSTANCE);
1737+ assert_eq!(instance_for(&labels(&["ubuntu-latest"]), true).machine, "4-core");
1738+ assert_eq!(instance_for(&labels(&["ubuntu-24.04"]), true).vcpu, 4.0);
1739+ assert_eq!(instance_for(&labels(&[]), false).label, "standard");
1740+ assert_eq!(instance_for(&labels(&[]), true).label, PUBLIC_DEFAULT_LABEL);
1741+ // A machine asked for by name is that machine, public or not.
1742+ assert_eq!(instance_for(&labels(&["g1t-4core"]), false).label, "g1t-4core");
1743+ assert_eq!(instance_for(&labels(&["ubuntu-latest", "G1T-2Core"]), false).label, "g1t-2core");
1744+ assert_eq!(instance_for(&labels(&["ubuntu-latest", "g1t-2core"]), true).machine, "2-core");
17241745 // Both named: the later one.
1725− assert_eq!(instance_for(&labels(&["g1t-2core", "g1t-4core"])).label, "g1t-4core");
1746+ assert_eq!(instance_for(&labels(&["g1t-2core", "g1t-4core"]), false).label, "g1t-4core");
17261747 assert_eq!(instance_named("g1t-2core").map(|i| i.vcpu), Some(2.0));
17271748 assert_eq!(instance_named("g1t-4core").map(|i| i.vcpu), Some(4.0));
17281749 assert_eq!(instance_named("standard"), Some(STANDARD_INSTANCE));
+0−186
Loading the diff…
+1−1
Loading the diff…
+35−2
Loading the diff…