Skip to content

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

Commit

Chat cards, dashboard templates and billing leave issues behind

Stored chat cards become work_item and draft_work_item, dashboard templates read the work_items dataset, and billing drops the issue caps nothing used.

G1T-Session:
ses_01m56adwzvenhby748vvbn96c5
syntaqxcommitted Parent412c63fBrowse files
10 files+116−110/10 viewed
+2−2
164164 leads to `flagon-io`, Flagon, Inc. (seeded by identity's migration
165165 `0029_workspace_aliases.sql`), so nobody mistakes the trading name for
166166 the organization. Every address under an alias leads to the workspace:
167− pages answer with a 301 to the same page (`/g1t/g1t/issues` to
168− `/flagon-io/g1t/issues`), git over HTTPS is answered in place as the
167+ pages answer with a 301 to the same page (`/g1t/g1t/pulls` to
168+ `/flagon-io/g1t/pulls`), git over HTTPS is answered in place as the
169169 workspace's repository (pushes do not follow redirects), the API and MCP
170170 run the call again under the workspace's slug, and the package
171171 registries answer a 301 (308 for a publish). An alias points at the
+1−1
127127 },
128128 });
129129 assert.deepEqual(
130− parseAllowances(form({ plan: "on", oss: "5", trial: "2.50", agents: "20", runCap: "25", issueCap: "500", auditDays: "365", hold: " Card disputed;\nwaiting on the bank " })),
130+ parseAllowances(form({ plan: "on", oss: "5", trial: "2.50", agents: "20", runCap: "25", auditDays: "365", hold: " Card disputed;\nwaiting on the bank " })),
131131 {
132132 ok: true,
133133 value: {
+2−2
193193 * (for the audit log, the plan's). A hold is a line saying why new compute
194194 * is held; blank is no hold. A run's cap is not staff's to set here: it is
195195 * always sent cleared, so the plan's applies. The overrides retired with
196− * g1t's own agents (agents at once, an issue's cap) control nothing and are
197− * not sent; billing keeps them empty.
196+ * g1t's own agents (agents at once) control nothing and are not sent;
197+ * billing keeps them empty.
198198 */
199199 export function parseAllowances(form: FormData): Parsed<Allowances> {
200200 const amount = (name: string, what: string, max: number, maxText: string, min = 0): Parsed<number | null> => {
+2−2
149149
150150 /** A card g1t or an agent posts: an event, a task, an approval. */
151151 export type MessageCard = {
152− /** What it is about, e.g. `pull`, `issue`, `task`, `deploy`, `approval`, `session`, `draft_issue`. */
152+ /** What it is about, e.g. `pull`, `work_item`, `task`, `deploy`, `approval`, `session`, `draft_work_item`. */
153153 kind: string;
154154 title: string;
155155 /** A short line under the title: "3/3 checks · +214 −87". */
158158 state: string | null;
159159 /** Where clicking the card goes, relative to the site. */
160160 href: string | null;
161− /** A preview in Markdown under the title: a draft issue's body, a report's first lines. */
161+ /** A preview in Markdown under the title: a draft work item's body, a report's first lines. */
162162 body?: string | null;
163163 /** Labelled facts shown in two columns: "Repository · acme/web", "Cap · $2.00". */
164164 fields?: CardField[];
+17−0
1+-- Issues became work items (g1t #57), and the dataset `issues` is
2+-- `work_items` now (packages/contracts/src/datasets.ts). A dashboard's
3+-- tiles are kept in its Yjs document, which no dashboard has yet (the kind
4+-- is not built: kinds/index.ts); a workspace's dashboard templates keep
5+-- theirs as JSON (`{ tiles: [{ query: { dataset } }] }`), rewritten here.
6+UPDATE folio_templates
7+SET body = json_set(body, '$.tiles', (
8+ SELECT json_group_array(json(tile)) FROM (
9+ SELECT CASE WHEN json_extract(t.value, '$.query.dataset') = 'issues'
10+ THEN json_set(t.value, '$.query.dataset', 'work_items')
11+ ELSE t.value END AS tile
12+ FROM json_each(folio_templates.body, '$.tiles') t
13+ ORDER BY t.key
14+ )
15+))
16+WHERE kind = 'dashboard' AND json_valid(body) AND json_type(body, '$.tiles') = 'array'
17+ AND EXISTS (SELECT 1 FROM json_each(folio_templates.body, '$.tiles') t WHERE json_extract(t.value, '$.query.dataset') = 'issues');
+41−0
1+import assert from "node:assert/strict";
2+import { readFileSync, readdirSync } from "node:fs";
3+import { DatabaseSync } from "node:sqlite";
4+import { test } from "node:test";
5+
6+const dir = new URL("../migrations/", import.meta.url);
7+const MIGRATION = "0011_work_items_dataset.sql";
8+
9+test("dashboard templates that read issues read work items after the migration, run once or twice", () => {
10+ const db = new DatabaseSync(":memory:");
11+ for (const file of readdirSync(dir).sort()) {
12+ if (file >= MIGRATION) break;
13+ db.exec(readFileSync(new URL(file, dir), "utf8"));
14+ }
15+ const insert = db.prepare(
16+ "INSERT INTO folio_templates (id, workspace_id, kind, name, body, created_by, created_at) VALUES (?, 'wsp_1', ?, ?, ?, 'user:usr_1', 'now')",
17+ );
18+ const tiles = [
19+ { id: "t1", type: "stat", query: { dataset: "issues", measure: { op: "count" } } },
20+ { id: "t2", type: "markdown", query: null, markdown: "Notes" },
21+ { id: "t3", type: "line", query: { dataset: "pull_requests", measure: { op: "count" } } },
22+ ];
23+ insert.run("tpl_1", "dashboard", "Triage", JSON.stringify({ tiles }));
24+ insert.run("tpl_2", "doc", "Notes", "# issues");
25+ insert.run("tpl_3", "dashboard", "Not JSON", "not json");
26+ const sql = readFileSync(new URL(MIGRATION, dir), "utf8");
27+ db.exec(sql);
28+ db.exec(sql);
29+ const body = (id: string) => (db.prepare("SELECT body FROM folio_templates WHERE id = ?").get(id) as { body: string }).body;
30+ const migrated = JSON.parse(body("tpl_1")) as { tiles: { id: string; query: { dataset: string } | null }[] };
31+ assert.deepEqual(
32+ migrated.tiles.map((tile) => [tile.id, tile.query?.dataset ?? null]),
33+ [
34+ ["t1", "work_items"],
35+ ["t2", null],
36+ ["t3", "pull_requests"],
37+ ],
38+ );
39+ assert.equal(body("tpl_2"), "# issues");
40+ assert.equal(body("tpl_3"), "not json");
41+});
+4−0
1+-- The caps on what the agents on one issue may spend in all (0018) went
2+-- with g1t's own agents and nothing reads them; issues are work items now.
3+ALTER TABLE limits DROP COLUMN issue_cap_micros;
4+ALTER TABLE billing_accounts DROP COLUMN issue_cap_micros;
+2−4
44 //! here first (see `g1t_contracts::billing::ReserveArgs`):
55 //!
66 //! - **`entitlements`**: the workspace's plan, whether it may start compute
7−//! at all, its caps (agents at once, a run's time and spend, an issue's
8−//! spend), its ceiling and exposure, and whether compute is paused.
7+//! at all, its caps (agents at once, a run's time and spend), its
8+//! ceiling and exposure, and whether compute is paused.
99 //! - **`reserve`**: holds the work's estimated cost against what may pay
1010 //! for it (the plan's included usage, the trial, the open-source pool,
1111 //! then on-demand room under the ceiling and the spend limit), so starts
919919 }
920920 }
921921
922−/// Whether owners' caps are in bounds: a run $0.10 to $100 (the
923−/// guardrails' most), an issue $1 to $1,000.
924922 pub(crate) fn paid_by_text(paid_by: PaidBy) -> &'static str {
925923 match paid_by {
926924 PaidBy::Credit => "credit",
+7−0
1+-- Issues became work items (g1t #57): cards kept about one are about a
2+-- work item now, and a draft of one is a draft work item.
3+UPDATE messages SET card = json_set(card, '$.kind', 'work_item')
4+WHERE kind = 'card' AND json_valid(card) AND json_extract(card, '$.kind') = 'issue';
5+
6+UPDATE messages SET card = json_set(card, '$.kind', 'draft_work_item')
7+WHERE kind = 'card' AND json_valid(card) AND json_extract(card, '$.kind') = 'draft_issue';
+38−0
1+import assert from "node:assert/strict";
2+import { readFileSync, readdirSync } from "node:fs";
3+import { DatabaseSync } from "node:sqlite";
4+import { test } from "node:test";
5+
6+const dir = new URL("../migrations/", import.meta.url);
7+const MIGRATION = "0008_work_item_cards.sql";
8+
9+test("cards kept about issues are about work items after the migration, run once or twice", () => {
10+ const db = new DatabaseSync(":memory:");
11+ for (const file of readdirSync(dir).sort()) {
12+ if (file >= MIGRATION) break;
13+ db.exec(readFileSync(new URL(file, dir), "utf8"));
14+ }
15+ db.exec("INSERT INTO channels (id, workspace_id, kind, name, created_by, created_at) VALUES ('chn_1', 'wsp_1', 'channel', 'general', 'g1t', 'now')");
16+ const insert = db.prepare(
17+ "INSERT INTO messages (id, channel_id, author, kind, body, card, created_at) VALUES (?, 'chn_1', 'g1t', ?, '', ?, 'now')",
18+ );
19+ insert.run("msg_1", "card", JSON.stringify({ kind: "issue", title: "Header", detail: null, state: "Open", href: null }));
20+ insert.run("msg_2", "card", JSON.stringify({ kind: "draft_issue", title: "Draft", detail: null, state: "Draft", href: null }));
21+ insert.run("msg_3", "card", JSON.stringify({ kind: "pull", title: "Fix", detail: null, state: null, href: null }));
22+ insert.run("msg_4", "text", null);
23+ const sql = readFileSync(new URL(MIGRATION, dir), "utf8");
24+ db.exec(sql);
25+ db.exec(sql);
26+ const kinds = db
27+ .prepare("SELECT id, json_extract(card, '$.kind') AS kind, json_extract(card, '$.title') AS title FROM messages ORDER BY id")
28+ .all();
29+ assert.deepEqual(
30+ kinds.map((row) => [row.id, row.kind, row.title]),
31+ [
32+ ["msg_1", "work_item", "Header"],
33+ ["msg_2", "draft_work_item", "Draft"],
34+ ["msg_3", "pull", "Fix"],
35+ ["msg_4", null, null],
36+ ],
37+ );
38+});

Comments

Sign in to comment.