Commit

Usage while free is shown at cost; agents get rustfmt and clippy

The sidebar's "Usage this month" and the usage page summed what was charged. While g1t is free every charge is zero, so both stopped at what had been charged before free mode began, with the margin in it, and every run since showed as nothing: $6.01 against $6.48 of real use, and further below the AI Gateway's own estimate. Billing now reports usedMicros, what the runs cost on g1t's models and the workspace's own provider together, and measures the breakdowns at cost while free. The card and the usage page show "Used", at cost, and say nothing is charged and credit is not drawn down. The runner image installed Rust with the minimal profile, which has no rustfmt or clippy, so an agent could not format or lint its change and first heard about it from a failed workflow (hello's #85: Formatting failed, the agent was sent back and fixed it). Both are installed now. docs/DEMO.md is rewritten for what g1t is today: GitHub Actions from .g1t/workflows, an agent writing a CI step (#80/#81), an agent sent back by a failed workflow and fixing it (#84/#85), and the merge queue testing combinations with merge_group.

syntaqxcommitted Parent8f91758Browse files
9 files+169−940/9 viewed
+4−3
6565 /** Whether g1t charges nothing for now, while it is being built out. */
6666 free?: boolean;
6767 /** What its agents have cost since the start of the month. */
68− monthSpentMicros: number | null;
68+ /** This month's usage: charged, or at cost while g1t is free. */
69+ monthUsageMicros: number | null;
6970 };
7071
7172 function SidebarLink({
176177
177178 /** This month's spend against what is left, as Vercel shows a plan's usage. */
178179 function UsageCard({ slug, shell }: { slug: string; shell: ShellData }) {
179− if (shell.monthSpentMicros == null) return null;
180− const spent = shell.monthSpentMicros;
180+ if (shell.monthUsageMicros == null) return null;
181+ const spent = shell.monthUsageMicros;
181182 const left = shell.creditMicros;
182183 const share = left != null && spent + left > 0 ? Math.min(1, spent / (spent + Math.max(left, 0))) : 0;
183184 return (
+2−1
113113 creditMicros:
114114 account?.ok && account.value.status.enabled && !account.value.status.free ? account.value.balanceMicros : null,
115115 free: account?.ok ? Boolean(account.value.status.free) : false,
116− monthSpentMicros: usage?.ok ? usage.value.spentMicros : null,
116+ // While g1t is free every charge is zero, so usage is shown at cost.
117+ monthUsageMicros: usage?.ok ? (usage.value.free ? usage.value.usedMicros : usage.value.spentMicros) : null,
117118 };
118119 }
119120
+1−1
264264
265265 const workspace = shell?.workspace?.slug;
266266 const first = repos[0];
267− const handedOff = active.length > 0 || (shell?.monthSpentMicros ?? 0) > 0;
267+ const handedOff = active.length > 0 || (shell?.monthUsageMicros ?? 0) > 0;
268268 const steps: Step[] = [
269269 {
270270 done: Boolean(workspace),
+36−19
189189 const { period, since, usage, account } = loaderData;
190190 const base = `/${params.owner}`;
191191 const days = Math.max(1, Math.ceil((Date.now() - new Date(since).getTime()) / 86_400_000));
192− const perDay = usage.spentMicros / days;
192+ // While g1t is free nothing is charged, so usage is measured at cost
193+ // and credit is not drawn down.
194+ const total = usage.free ? usage.usedMicros : usage.spentMicros;
195+ const perDay = usage.free ? 0 : total / days;
193196 const runway = perDay > 0 ? Math.floor(account.balanceMicros / perDay) : null;
194197 return (
195198 <div className="space-y-8">
211214 </div>
212215
213216 <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
214− <Stat
215− label="Spent"
216− value={dollars(usage.spentMicros)}
217− note={
218− usage.providerMicros > 0
219− ? `Plus about ${dollars(usage.providerMicros)} billed by your own model provider`
220− : `${dollars(usage.costMicros)} of it the model provider's`
221− }
222− />
217+ {usage.free ? (
218+ <Stat
219+ label="Used"
220+ value={dollars(total)}
221+ note="At cost. Nothing is charged while g1t is being built out."
222+ />
223+ ) : (
224+ <Stat
225+ label="Spent"
226+ value={dollars(usage.spentMicros)}
227+ note={
228+ usage.providerMicros > 0
229+ ? `Plus about ${dollars(usage.providerMicros)} billed by your own model provider`
230+ : `${dollars(usage.costMicros)} of it the model provider's`
231+ }
232+ />
233+ )}
223234 <Stat label="Agent runs" value={String(usage.runs)} note={PERIODS[period]} />
224235 <Stat
225236 label="Average run"
226− value={usage.runs ? dollars(usage.spentMicros / usage.runs, 3) : "—"}
237+ value={usage.runs ? dollars(total / usage.runs, 3) : "—"}
227238 note="Making a change, reviewing, revising…"
228239 />
229240 <div className="rounded-2xl bg-surface p-5 ring-1 ring-line">
230241 <p className="flex items-center justify-between text-sm text-muted">
231242 Credit left
232− <Link to={`${base}/-/billing`} className="text-xs text-accent hover:underline">
233− Add credit
234− </Link>
243+ {!usage.free && (
244+ <Link to={`${base}/-/billing`} className="text-xs text-accent hover:underline">
245+ Add credit
246+ </Link>
247+ )}
235248 </p>
236249 <p className={`mt-2 text-3xl font-semibold tracking-tight tabular-nums ${account.balanceMicros <= 0 ? "text-warn" : ""}`}>
237250 {dollars(account.balanceMicros)}
238251 </p>
239252 <p className="mt-1 text-xs text-faint">
240− {runway == null ? "Nothing spent in this period." : `About ${runway} ${runway === 1 ? "day" : "days"} at this rate.`}
253+ {usage.free
254+ ? "Not drawn down while g1t is free."
255+ : runway == null
256+ ? "Nothing spent in this period."
257+ : `About ${runway} ${runway === 1 ? "day" : "days"} at this rate.`}
241258 </p>
242259 </div>
243260 </div>
244261
245262 <section className="rounded-2xl bg-surface p-5 ring-1 ring-line">
246263 <div className="flex flex-wrap items-baseline justify-between gap-3">
247− <h3 className="text-sm font-medium">Spend per day</h3>
264+ <h3 className="text-sm font-medium">{usage.free ? "Usage per day" : "Spend per day"}</h3>
248265 <ul className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted">
249266 {usage.byTask.map((slice) => (
250267 <li key={slice.key} className="flex items-center gap-1.5">
263280 <Breakdown
264281 title="By kind of work"
265282 slices={usage.byTask}
266− total={usage.spentMicros}
283+ total={total}
267284 label={(key) => task(key).label}
268285 color={(key) => task(key).color}
269286 />
270− <Breakdown title="By repository" slices={usage.byRepo} total={usage.spentMicros} link={(key) => `/${key}`} />
287+ <Breakdown title="By repository" slices={usage.byRepo} total={total} link={(key) => `/${key}`} />
271288 <Breakdown
272289 title="Pull requests that cost most"
273290 slices={usage.byPull}
280297 return number && number !== "0" ? `/${repo}/pull/${number}` : `/${repo}/plans`;
281298 }}
282299 />
283− <Breakdown title="By model" slices={usage.byModel} total={usage.spentMicros} color={() => "var(--color-info)"} />
300+ <Breakdown title="By model" slices={usage.byModel} total={total} color={() => "var(--color-info)"} />
284301 </div>
285302
286303 <div className="flex flex-wrap items-center justify-between gap-3 rounded-2xl bg-surface px-5 py-4 text-sm ring-1 ring-line">
+6−0
207207 /// What runs on the workspace's own provider cost there, as the harness
208208 /// estimated it. Not charged by g1t.
209209 pub provider_micros: i64,
210+ /// What the runs used, at cost: g1t's models and the workspace's own
211+ /// provider together, whatever was charged for them.
212+ pub used_micros: i64,
213+ /// g1t charges nothing for now. The slices then measure usage at cost,
214+ /// since every charge is zero.
215+ pub free: bool,
210216 pub runs: u32,
211217 /// Spend per day (`YYYY-MM-DD`) and task, as `day/task` keys.
212218 pub by_day: Vec<UsageSlice>,
+106−67
11 # Demo script
22
3−A walk through g1t for the submission video. It runs seven to eight minutes
4−at a normal speaking pace and uses `syntaqx/hello`, which is seeded for it.
3+A walk through g1t for the submission video. It runs about eight minutes at
4+a normal speaking pace and uses `syntaqx/hello`, a small Rust greeter whose
5+whole history was written by agents working on issues.
56
67 Everything shown is live on g1t.sh. Nothing is mocked.
78
9+The judges weigh agent collaboration (half), concurrency and conflicts (a
10+quarter) and ease of use (a quarter). Sections 3 to 6 carry the first two;
11+sections 2 and 8 the third.
12+
813 ## Before recording
914
1015 - Sign in as `syntaqx`.
1116 - Have a terminal open in an empty directory, with Claude Code installed.
1217 - Open `https://g1t.sh/syntaqx/hello` in one tab and `https://g1t.sh/` in
1318 another.
14−- Agents take one to three minutes. Start them, talk over something else,
15− and come back. Do not deploy the runner while they work.
19+- Write the three issues for section 3 in a scratch file so they can be
20+ pasted (titles below). Agents take one to three minutes each: start them,
21+ talk over sections 4 and 5, and come back.
22+- Do not deploy the runner while agents work.
1623
1724 ## 1. The problem (30 seconds)
1825
2532 > is ordinary git, with issues and pull requests, and it runs entirely on
2633 > Cloudflare.
2734
28−## 2. It is still git (45 seconds)
35+## 2. It is still git, and your CI comes with you (1 minute)
2936
3037 On `syntaqx/hello`, Code tab.
3138
3239 - Show the clone box: HTTPS, and the one line that connects an agent.
3340 - In the terminal: `git clone https://g1t.sh/syntaqx/hello.git`.
34−- Mention: storage is Cloudflare Artifacts; the site, the API and every
35− service are Workers.
41+- Open `.g1t/workflows/ci.yml`. It is a GitHub Actions workflow, unchanged:
42+ `actions/checkout@v7`, a Rust toolchain action, `actions/cache@v6`, then
43+ formatting, lints, an "Every flag is documented" step, and tests.
44+
45+> Moving from GitHub is renaming `.github` to `.g1t`. The same workflow
46+> syntax, the same actions from the marketplace, the same `push`,
47+> `pull_request` and `merge_group` events. Storage is Cloudflare Artifacts;
48+> every job runs in its own Cloudflare Container.
3649
37−## 3. Many issues, an agent on each (2 minutes)
50+- Actions tab: the runs, by event. Open one and show the steps and the log.
51+
52+## 3. Many issues, an agent on each (1 minute 30 seconds)
3853
3954 Issues tab.
4055
41−- Point out labels, and that issues come from people, agents or anything
42− with a token, such as an error tracker.
43−- Tick several open issues and press **Assign to g1t agent**. Say that
44− there is nothing else to choose: no number of agents and no model. Each
45− issue gets an agent of its own and g1t routes the work; every session
46− opens by naming the model that ran.
47−- Open **Document the command-line options**. Its draft pull request has
48− appeared. Show the **Session** tab filling in live: the prompt, the
49− agent's reasoning, every command.
56+- Create three issues quickly, pasting them in:
57+ - **Add a --sparkle flag** that ends the greeting with a sparkle emoji.
58+ - **Greet in German** with `--lang de`.
59+ - **Explain in the README what happens with no name.**
60+- Tick all three and press **Assign to g1t agent**. Say there is nothing
61+ else to choose: no number of agents, no model. Each issue gets an agent of
62+ its own and g1t routes the work; every session opens by naming the model
63+ that ran.
64+- Open one. Its draft pull request has appeared. Show the **Session** tab
65+ filling in live: the prompt, what the agent was told about the other work
66+ in progress, every command it runs.
5067
51−> Each agent has its own sandbox, a Cloudflare Container, and its own fork.
52−> A fork is copy-on-write, so it costs about what a branch would, and an
53−> agent cannot damage what it cannot write to.
68+> Each agent has its own sandbox and its own fork. A fork is copy-on-write,
69+> so it costs about what a branch would, and an agent cannot damage what it
70+> cannot write to. Each is told what else is in flight, so two agents on
71+> the same file know about each other before they collide.
5472
55−While they run, go to the next section.
73+While they run, go on.
5674
57−## 4. Checks nobody can fake (1 minute)
75+## 4. Agents keep CI honest, and fix what it catches (1 minute 30 seconds)
5876
59−Open **Say goodbye too** and its pull request, **Add a farewell**.
77+Open issue **#80, CI: fail when a flag is missing from the README**, and its
78+pull request **#81**.
6079
61−- This one was pushed as a branch by a person, the way you already work.
62−- **Checks failed.** Expand `cargo test` and show the output.
63−- Changes tab: the reviewer's comment sits on the faulty line.
80+- An agent wrote this CI step. Changes tab: the shell step it added to
81+ `ci.yml`. It went through review and the merge queue like any change.
6482
65−> The issue says what done means: here, `cargo test`. g1t runs that itself,
66−> in a clean sandbox that holds only this commit. The agent that wrote the
67−> code never touches the result, so a pass means something. And a pull
68−> request that has not passed cannot be merged.
83+Open issue **#84, Add a --reverse flag**, and its pull request **#85**.
6984
70−## 5. Choosing between pull requests (1 minute 15 seconds)
85+- Its checks, `cargo test`, passed. Its first workflow run did not:
86+ **Formatting** failed. Open the run and show the step and its log.
87+- Session tab: the agent's second session opens with the failed run, the
88+ instruction to read it with `get_workflow_run` and `get_job_logs`, and to
89+ fix the code rather than the workflow. Show it reading the log, fixing
90+ the formatting, and pushing. The second run is green.
7191
72−Open **Greet in Spanish and French**.
92+> Nobody marks their own homework. Workflows run in a clean sandbox on the
93+> exact commit; the agent that wrote the code never touches the result. A
94+> failure goes back to the agent with the log, and the pull request cannot
95+> merge until it is green.
7396
74−- Two pull requests for the same issue, side by side: checks, size of the
75− change, who reviewed.
76−- Open one. Show **Review by a g1t agent**: comments on lines, a summary, a
77− verdict. Say that an agent cannot review its own pull request.
78−- Show **Other work is changing the same files**.
97+## 5. Checks, reviews and choosing between pull requests (1 minute)
7998
80−> This is the overlap radar. Two pull requests for different issues are
81−> editing the same file. g1t says so while the work is still going on, not
82−> at the end as a merge conflict. Agents see the same thing through the API.
99+Open **Say goodbye too** (#4) and its pull request **Add a farewell** (#9).
83100
84−## 6. Converging on main (1 minute 15 seconds)
101+- This one was pushed as a branch by a person, the way you already work.
102+- **Checks failed.** Expand `cargo test` and show the output. Changes tab:
103+ the reviewer's comment sits on the faulty line. The agent's pull request
104+ for the same issue, #10, passed and was merged; #9 was closed.
85105
86−Open **A blank name greets nobody**.
106+Open **Greet in Spanish and French** (#2).
87107
88−- It is closed, and it says which pull request resolved it. The other one
89− is marked superseded.
108+- Two pull requests for one issue, side by side: checks, size of the change,
109+ who reviewed. Open one and show **Other work is changing the same files**.
90110
91−> Two pull requests for one issue, one merged. The issue records which.
111+> This is the overlap radar. g1t says so while the work is still going on,
112+> not at the end as a merge conflict. Agents see the same thing through the
113+> API, which is how the agents in section 3 were told about each other.
92114
93−Go back to a pull request for the Spanish and French issue that says main
94−has moved.
115+Open **A blank name greets nobody** (#1): closed, saying which pull request
116+resolved it; the other is marked superseded.
117+
118+## 6. The merge queue (1 minute 15 seconds)
119+
120+Back to the pull requests from section 3. Their checks have passed and a g1t
121+agent has reviewed them.
122+
123+- With auto-merge on, they enter the **Merge queue** on their own. Open it.
124+
125+> Three changes, written at the same time, each green on its own. That
126+> proves nothing about all three together. The queue builds main with the
127+> first, main with the first and second, and so on, and tests every one of
128+> those combinations at once, in parallel sandboxes: the issues' acceptance
129+> checks, every check main has promised so far, and the repository's
130+> `merge_group` workflows, exactly as GitHub's merge queue sends them.
95131
96−- Press **Catch up with main**. Open the Session tab.
132+- As each lands, the issue closes, recording which pull request resolved it.
133+- Show #79 and #81 under **Recent**: landed, with the `merge_group` run.
97134
98−> Something else landed first, so this pull request is behind. A g1t agent
99−> merges main in. If that conflicts, the agent is given both sides and what
100−> this pull request is for, and resolves it. Then the checks run again on
101−> the result.
135+> When a combination fails, that entry is taken out with the reason, the
136+> ones behind it are tested again without it, and its agent is sent back
137+> to fix it. A conflict with something ahead of it says which.
102138
103−- When it is done, merge it. The issue closes; the other pull request for
104− it closes as superseded.
139+If one conflicts on camera, so much the better: open its Session and show
140+the agent being given both sides and what the pull request is for.
105141
106142 ## 7. Bring your own agent (45 seconds)
107143
114150 - In Claude Code, `/mcp`, choose g1t. The browser opens on g1t's consent
115151 page. Approve.
116152 - Ask: "What issues are open on syntaqx/hello on g1t, and which pull
117− requests overlap?"
153+ requests overlap? Did the last CI run pass?"
118154
119−> No token to paste. The same operations are a REST API at api.g1t.sh, and
120−> the two are generated from one list, so they cannot drift apart.
155+> No token to paste. The same operations are a REST API at api.g1t.sh,
156+> including GitHub's own Actions endpoints, and the two are generated from
157+> one list, so they cannot drift apart.
121158
122159 ## 8. Close (30 seconds)
123160
124−Back on the first issue: the three pull requests from section 3 are ready,
125−with their checks.
161+Back on the Issues tab: the three issues from section 3, closed, each saying
162+which pull request resolved it.
126163
127−> Issues and pull requests, as you know them. What changes is the number of
128−> hands. Every pull request isolated in its own fork, every decision
129−> recorded with the code, checks that agents cannot mark themselves, overlap
130−> flagged while it is happening, and one clear answer to which change you
131−> took. g1t is open source, and it is hosted on itself.
164+> Issues and pull requests, as you know them, and your GitHub Actions as
165+> they are. What changes is the number of hands. Every agent isolated in its
166+> own fork, told what the others are doing, held to checks and workflows it
167+> cannot mark itself, reviewed, and landed through a queue that tests the
168+> combinations. g1t is open source, free while it is being built out, and
169+> hosted on itself.
132170
133171 Show `https://g1t.sh/syntaqx/g1t`.
134172
136174
137175 | What | Do |
138176 | --- | --- |
139−| An agent's pull request closes itself | Open its Session; the last note says why. Assign another. |
177+| An agent's pull request closes itself | Open its Session; the last note says why. Assign the issue again. |
178+| A workflow stays queued | Open the run and press **Re-run all jobs**. |
140179 | Checks stay queued | Press the re-run button on the checks panel. |
141−| Catch up does nothing | The pull request was already up to date; refresh. |
142−| Merge is refused | Read the message: it is a draft, its checks have not passed, or main moved. |
180+| Nothing enters the queue | Auto-merge waits for checks, workflows and a review; the pull request's sidebar says which is missing. |
181+| Merge is refused | Read the message: it is a draft, its checks or workflows have not passed, or it needs a review. |
+4−0
116116 costMicros: number;
117117 /** What runs on the workspace's own provider cost there, estimated. Not charged by g1t. */
118118 providerMicros: number;
119+ /** What the runs used, at cost: g1t's models and the workspace's own provider together. */
120+ usedMicros: number;
121+ /** g1t charges nothing for now; the slices then measure usage at cost. */
122+ free: boolean;
119123 runs: number;
120124 /** Spend per day and task, keyed `YYYY-MM-DD/task`. */
121125 byDay: UsageSlice[];
+5−1
261261 micros: Option<i64>,
262262 runs: Option<u32>,
263263 }
264+ // While nothing is charged, what was used is what there is to show.
265+ let measure = if self.free { "COALESCE(cost_micros, 0)" } else { "-amount_micros" };
264266 let slices = |key: &str, limit: u32| {
265267 format!(
266− "SELECT {key} AS key, -SUM(amount_micros) AS micros, COUNT(*) AS runs FROM ledger
268+ "SELECT {key} AS key, SUM({measure}) AS micros, COUNT(*) AS runs FROM ledger
267269 WHERE workspace = ?1 AND kind = 'usage' AND created_at >= ?2
268270 GROUP BY 1 ORDER BY micros DESC LIMIT {limit}"
269271 )
323325 spent_micros: totals.spent.unwrap_or_default(),
324326 cost_micros: totals.cost.unwrap_or_default(),
325327 provider_micros: totals.provider.unwrap_or_default(),
328+ used_micros: totals.cost.unwrap_or_default() + totals.provider.unwrap_or_default(),
329+ free: self.free,
326330 runs: totals.runs.unwrap_or_default(),
327331 added_micros: totals.added.unwrap_or_default(),
328332 by_day: query(slices("substr(created_at, 1, 10) || '/' || COALESCE(task, 'other')", 400)).await?,
+5−2
3232 # Claude Code refuses to skip permission prompts as root.
3333 USER node
3434 ENV HOME=/home/node
35−# Rust, for the agent's own use, installed for the user it runs as.
35+# Rust, for the agent's own use, installed for the user it runs as, with
36+# the formatter and linter that CI so often checks with: an agent that
37+# cannot run them only finds out from a failed workflow.
3638 RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
37− | sh -s -- -y --profile minimal --default-toolchain stable
39+ | sh -s -- -y --profile minimal --default-toolchain stable \
40+ --component rustfmt --component clippy
3841 ENV PATH=/home/node/.cargo/bin:$PATH
3942 # Commits are the g1t agent's; the harness does not sign them as its own.
4043 RUN mkdir -p /home/node/.claude \