Skip to content

Commit

Merge branch 'main' into worktree-agent-a7dbe21737f9ad26a

# Conflicts: # apps/api/src/operations.rs # apps/web/app/routes/repo/deployments.tsx # apps/web/app/routes/repo/overview.tsx

syntaqxcommitted Parents22590554167ae8Browse files
58 files+806−650/58 viewed
+77−0
1+# Tests every pull request into main, so main can always be deployed. The
2+# "Default branch" ruleset requires this workflow ("CI") to pass before a
3+# pull request merges; people may still push to main directly, agents
4+# may not. Deploy (deploy.yml) ships what lands on main.
5+#
6+# rust every crate's tests, natively
7+# typescript type checks and tests of the apps and TS services, the
8+# deploy and ops scripts, and the deploy manifest
9+# build the site, sudo and the docs build as they deploy
10+name: CI
11+
12+on:
13+ pull_request:
14+ branches: [main]
15+ workflow_dispatch:
16+
17+concurrency:
18+ group: ci-${{ github.ref }}
19+ cancel-in-progress: true
20+
21+env:
22+ CARGO_TERM_COLOR: never
23+ WRANGLER_SEND_METRICS: "false"
24+
25+jobs:
26+ rust:
27+ name: Rust
28+ runs-on: g1t-4core
29+ timeout-minutes: 45
30+ steps:
31+ - uses: actions/checkout@v5
32+ - name: Cache crates
33+ uses: actions/cache@v4
34+ with:
35+ path: ~/.cargo/registry/cache
36+ key: cargo-crates-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
37+ restore-keys: cargo-crates-${{ runner.os }}-
38+ - name: Cache the test build
39+ uses: actions/cache@v4
40+ with:
41+ path: |
42+ target/debug
43+ !target/debug/incremental
44+ key: cargo-test-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'services/runner/base.json') }}
45+ restore-keys: cargo-test-${{ runner.os }}-
46+ - name: Tests
47+ run: cargo test --workspace --locked --quiet
48+
49+ typescript:
50+ name: TypeScript
51+ runs-on: ubuntu-latest
52+ timeout-minutes: 30
53+ steps:
54+ - uses: actions/checkout@v5
55+ - name: Install
56+ run: npm ci --no-audit --no-fund
57+ - name: Type checks
58+ run: npm run typecheck
59+ - name: Tests
60+ run: npm test --workspaces --if-present
61+ - name: The deploy tool's tests
62+ run: npm run test:deploy
63+ - name: The ops scripts' tests
64+ run: npm run test:ops
65+ - name: The manifest matches every wrangler.jsonc
66+ run: node scripts/deploy.mjs manifest --check
67+
68+ build:
69+ name: Build
70+ runs-on: ubuntu-latest
71+ timeout-minutes: 30
72+ steps:
73+ - uses: actions/checkout@v5
74+ - name: Install
75+ run: npm ci --no-audit --no-fund
76+ - name: The site, sudo and the docs
77+ run: node scripts/deploy.mjs build --only web,sudo,docs
+1−0
1515 mod oauth;
1616 mod openapi;
1717 mod pins;
18+mod projects;
1819 mod operations;
1920 mod renamed;
2021 #[cfg(test)]
+8−0
4747 &[Op::ListPinnedProjects, Op::PinProject, Op::UnpinProject, Op::ReorderPinnedProjects],
4848 ),
4949 (
50+ "Projects",
51+ "A project is what a workspace builds and runs, from a repository or a root directory in one. Each says what it is, where it runs and where to find it: its homepage, docs and other links.",
52+ &[Op::ListProjects, Op::GetProject, Op::UpdateProject],
53+ ),
54+ (
5055 "Workspaces",
5156 "A workspace owns repositories and is the first part of their address. People and agents work in workspaces.",
5257 &[Op::GetWorkspace, Op::CreateWorkspace, Op::UpdateWorkspace, Op::DeleteWorkspace],
552557 Op::PinProject => "Pin a project",
553558 Op::UnpinProject => "Unpin a project",
554559 Op::ReorderPinnedProjects => "Reorder your pinned projects",
560+ Op::ListProjects => "List a workspace's projects",
561+ Op::GetProject => "Get a project",
562+ Op::UpdateProject => "Update a project",
555563 Op::ListTeams => "List teams",
556564 Op::GetTeam => "Get a team",
557565 Op::CreateTeam => "Create a team",
+73−1
242242 PinProject,
243243 UnpinProject,
244244 ReorderPinnedProjects,
245+ ListProjects,
246+ GetProject,
247+ UpdateProject,
245248 ListTeams,
246249 GetTeam,
247250 CreateTeam,
635638 }
636639
637640 impl Op {
638− pub const ALL: [Op; 226] = [
641+ pub const ALL: [Op; 229] = [
639642 Op::Whoami,
640643 Op::GetWorkspace,
641644 Op::CreateWorkspace,
786789 Op::PinProject,
787790 Op::UnpinProject,
788791 Op::ReorderPinnedProjects,
792+ Op::ListProjects,
793+ Op::GetProject,
794+ Op::UpdateProject,
789795 Op::ListTeams,
790796 Op::GetTeam,
791797 Op::CreateTeam,
10211027 Op::PinProject => "pin_project",
10221028 Op::UnpinProject => "unpin_project",
10231029 Op::ReorderPinnedProjects => "reorder_pinned_projects",
1030+ Op::ListProjects => "list_projects",
1031+ Op::GetProject => "get_project",
1032+ Op::UpdateProject => "update_project",
10241033 Op::ListTeams => "list_teams",
10251034 Op::GetTeam => "get_team",
10261035 Op::CreateTeam => "create_team",
14701479 Op::ReorderPinnedProjects => {
14711480 "Put your pins in a workspace in a new order: `projects` names every pinned project's slug, once, in the order you want them. Returns your pins, in order."
14721481 }
1482+ Op::ListProjects => {
1483+ "A workspace's projects that you can see, by name. A project is what a workspace builds and runs, from a repository or a root directory in one; every repository has a project of its own name. Each has what it is (`kind`: app, library, tool, docs or other) and why (`kind_reason`), where it runs (`runs`: `g1t` when g1t deploys it, `elsewhere` when it is deployed by other means, at `production_url`), and its `links`."
1484+ }
1485+ Op::GetProject => {
1486+ "A project: what it is (`kind`, and `kind_reason` saying why), where it runs (`runs` and `production_url`), what you set and what detection decides (`setting` and `detected`), its repository and `root_dir`, and its homepage, docs and other `links`. A private repository's project is found only by those who can see the repository."
1487+ }
1488+ Op::UpdateProject => {
1489+ "Change a project: its name, description, root directory, what it is, where it runs and its links. Only what you give changes. kind auto and runs auto leave each to detection. Setting runs makes it an app unless it is docs; making it a library, tool or other while Deployments are on is refused, so turn Deployments off first. Give description or homepage as null or \"\" to follow the repository's again, and production_url or docs_url as null or \"\" to clear it. links replaces its other links: at most 10, each a label of up to 40 characters and an http or https address (https:// is added when you leave the scheme out). Needs the Maintain role or higher on its repository."
1490+ }
14731491 Op::ListTeams => {
14741492 "A workspace's teams that you can see, yours first, then by name. A team is a group of the workspace's members, given roles on repositories together, mentioned as @workspace/team and asked to review together. A secret team is seen only by its own people and the workspace's owners. Each team has its `slug`, `name`, `description`, `visibility` (`visible` or `secret`), `parent`, whether its people are notified when it is mentioned (`notify`), its `review_assignment`, how many people, repositories and child teams it has (`members_count`, `repos_count`, `child_teams_count`), your own `viewer_role` in it, and whether you may change it (`can_manage`). `query` narrows them by name or slug. Members of the workspace only."
14751493 }
26922710 }),
26932711 &["workspace", "projects"],
26942712 ),
2713+ Op::ListProjects => object(json!({ "workspace": workspace_schema() }), &["workspace"]),
2714+ Op::GetProject => object(
2715+ json!({
2716+ "workspace": workspace_schema(),
2717+ "project": { "type": "string", "description": "The project's slug, as in g1t.sh/{workspace}/{project}." },
2718+ }),
2719+ &["workspace", "project"],
2720+ ),
2721+ Op::UpdateProject => object(
2722+ json!({
2723+ "workspace": workspace_schema(),
2724+ "project": { "type": "string", "description": "The project's slug, as in g1t.sh/{workspace}/{project}." },
2725+ "name": { "type": "string", "description": "Its name." },
2726+ "description": { "type": ["string", "null"], "description": "Its own description. null or \"\" follows its repository's again." },
2727+ "root_dir": { "type": "string", "description": "Where in the repository it lives, such as apps/web; \"\" for the whole repository." },
2728+ "kind": {
2729+ "type": "string",
2730+ "enum": ["auto", "app", "library", "tool", "docs", "other"],
2731+ "description": "What it is. auto leaves it to detection. A library, tool or other runs nowhere.",
2732+ },
2733+ "runs": {
2734+ "type": "string",
2735+ "enum": ["auto", "g1t", "elsewhere"],
2736+ "description": "Where it runs: g1t when g1t deploys it, elsewhere when it is deployed by other means. auto leaves it to Deployments.",
2737+ },
2738+ "production_url": { "type": ["string", "null"], "description": "Production's address when it runs elsewhere. null or \"\" clears it." },
2739+ "homepage": { "type": ["string", "null"], "description": "Its homepage. null or \"\" follows its repository's website again." },
2740+ "docs_url": { "type": ["string", "null"], "description": "Where its documentation is read. null or \"\" clears it." },
2741+ "links": {
2742+ "type": "array",
2743+ "maxItems": 10,
2744+ "items": {
2745+ "type": "object",
2746+ "properties": {
2747+ "label": { "type": "string", "maxLength": 40 },
2748+ "url": { "type": "string", "description": "An http or https address; https:// is added when you leave the scheme out." },
2749+ },
2750+ "required": ["label", "url"],
2751+ },
2752+ "description": "Its other links, replacing the ones it has. [] removes them all.",
2753+ },
2754+ }),
2755+ &["workspace", "project"],
2756+ ),
26952757 Op::ListTeams => object(
26962758 json!({
26972759 "workspace": workspace_schema(),
28842946 | Op::ListCheckNames
28852947 | Op::GetMergeQueue
28862948 | Op::GetCodeownersErrors
2949+ | Op::ListProjects
2950+ | Op::GetProject
28872951 | Op::Rules(RulesOp::ListRepoRulesets | RulesOp::GetRepoRuleset | RulesOp::GetBranchRules)
28882952 | Op::Deployments(
28892953 DeploymentsOp::ListDeployments
29793043 | Op::PinProject
29803044 | Op::UnpinProject
29813045 | Op::ReorderPinnedProjects
3046+ | Op::ListProjects
3047+ | Op::GetProject
3048+ | Op::UpdateProject
29823049 | Op::ListTeams
29833050 | Op::GetTeam
29843051 | Op::CreateTeam
48674934 Op::ListPinnedProjects | Op::PinProject | Op::UnpinProject | Op::ReorderPinnedProjects => {
48684935 crate::pins::run(self, services, viewer, input).await
48694936 }
4937+ // What a project is, where it runs and its links: the projects
4938+ // service keeps them and decides who may change them.
4939+ Op::ListProjects | Op::GetProject | Op::UpdateProject => {
4940+ crate::projects::run(self, services, viewer, input).await
4941+ }
48704942 // The security suite: the security service decides, this gives
48714943 // each answer its public shape.
48724944 Op::Security(op) => crate::security::run(op, services, viewer, input).await,
+262−0
1+//! Projects: what a workspace builds and runs, where each runs, and its
2+//! links. The projects service keeps them and decides who may see or change
3+//! them; this is their public shape, in snake_case.
4+
5+use g1t_contracts::{FailureCode, Outcome, Viewer};
6+use serde_json::{Map, Value, json};
7+use worker::Result;
8+
9+use crate::operations::{Op, Services};
10+
11+fn failed(code: FailureCode, message: &str) -> Result<Outcome<Value>> {
12+ Ok(Outcome::fail(code, message))
13+}
14+
15+fn slug(input: &Value, key: &str) -> Option<String> {
16+ input[key].as_str().map(str::trim).filter(|value| !value.is_empty()).map(str::to_lowercase)
17+}
18+
19+/// A reason a kind was decided, as the API shows it.
20+fn reason_json(reason: &Value) -> Value {
21+ json!({ "by": reason["by"], "detail": reason["detail"] })
22+}
23+
24+/// A project as the projects service answers (camelCase), as the API shows it.
25+pub(crate) fn project_json(project: &Value, site: &str) -> Value {
26+ let workspace = project["workspace"].as_str().unwrap_or_default();
27+ let slug = project["slug"].as_str().unwrap_or_default();
28+ let source = &project["source"];
29+ let repository = match (source["repo"]["namespace"].as_str(), source["repo"]["name"].as_str()) {
30+ (Some(namespace), Some(name)) => json!(format!("{namespace}/{name}")),
31+ _ => Value::Null,
32+ };
33+ let links = &project["links"];
34+ let custom: Vec<Value> = links["custom"]
35+ .as_array()
36+ .map(|items| items.iter().map(|link| json!({ "label": link["label"], "url": link["url"] })).collect())
37+ .unwrap_or_default();
38+ json!({
39+ "id": project["id"],
40+ "workspace": workspace,
41+ "slug": slug,
42+ "name": project["name"],
43+ "description": project["description"],
44+ "description_inherited": project["descriptionInherited"].as_bool().unwrap_or(false),
45+ "url": format!("{}/{workspace}/{slug}", site.trim_end_matches('/')),
46+ "repository": repository,
47+ "root_dir": source["rootDir"].as_str().unwrap_or_default(),
48+ "default_branch": source["defaultBranch"],
49+ "private": project["private"],
50+ "archived": project["archived"],
51+ "primary": project["primary"],
52+ "kind": project["kind"],
53+ "kind_reason": reason_json(&project["kindReason"]),
54+ "runs": project["runs"],
55+ "production_url": project["productionUrl"],
56+ "setting": { "kind": project["setting"]["kind"], "runs": project["setting"]["runs"] },
57+ "detected": {
58+ "kind": project["detected"]["kind"],
59+ "reason": reason_json(&project["detected"]["reason"]),
60+ },
61+ "ecosystem": project["ecosystem"],
62+ "links": {
63+ "homepage": links["homepage"],
64+ "homepage_inherited": links["homepageInherited"].as_bool().unwrap_or(false),
65+ "docs": links["docs"],
66+ "custom": custom,
67+ },
68+ "created_at": project["createdAt"],
69+ "updated_at": project["updatedAt"],
70+ "pushed_at": project["pushedAt"],
71+ })
72+}
73+
74+/// The fields a change may set: (input, service, whether null clears it).
75+const CHANGES: &[(&str, &str, bool)] = &[
76+ ("name", "name", false),
77+ ("description", "description", true),
78+ ("root_dir", "rootDir", false),
79+ ("kind", "kind", false),
80+ ("runs", "runs", false),
81+ ("production_url", "productionUrl", true),
82+ ("homepage", "homepage", true),
83+ ("docs_url", "docsUrl", true),
84+];
85+
86+/// The change `input` asks for, in the service's spelling: only what it
87+/// gives, with null passed on for what null clears.
88+pub(crate) fn changes(input: &Value) -> std::result::Result<Value, String> {
89+ let mut out = Map::new();
90+ for (key, field, clearable) in CHANGES {
91+ match input.get(*key) {
92+ None => {}
93+ Some(Value::Null) if *clearable => {
94+ out.insert((*field).to_owned(), Value::Null);
95+ }
96+ Some(Value::Null) => {}
97+ Some(Value::String(text)) => {
98+ out.insert((*field).to_owned(), json!(text));
99+ }
100+ Some(_) => {
101+ let kind = if *clearable { "a string or null" } else { "a string" };
102+ return Err(format!("{key} must be {kind}."));
103+ }
104+ }
105+ }
106+ match input.get("links") {
107+ None | Some(Value::Null) => {}
108+ Some(Value::Array(items)) => {
109+ let mut links = Vec::with_capacity(items.len());
110+ for item in items {
111+ match (item["label"].as_str(), item["url"].as_str()) {
112+ (Some(label), Some(url)) => links.push(json!({ "label": label, "url": url })),
113+ _ => return Err("Each of links needs a label and a url.".to_owned()),
114+ }
115+ }
116+ out.insert("links".to_owned(), Value::Array(links));
117+ }
118+ Some(_) => return Err("links must be a list of { label, url }.".to_owned()),
119+ }
120+ Ok(Value::Object(out))
121+}
122+
123+/// Runs one of the project operations.
124+pub async fn run(op: Op, services: &Services, viewer: &Viewer, input: &Value) -> Result<Outcome<Value>> {
125+ let Some(workspace) = slug(input, "workspace") else {
126+ return failed(FailureCode::Invalid, "Give the workspace's slug.");
127+ };
128+ let site = services.addresses.site.as_str();
129+ let one = |outcome: Outcome<Value>| -> Outcome<Value> {
130+ match outcome {
131+ Outcome::Ok(project) => Outcome::Ok(project_json(&project, site)),
132+ Outcome::Fail(failure) => Outcome::Fail(failure),
133+ }
134+ };
135+ if op == Op::ListProjects {
136+ let listed: Outcome<Vec<Value>> =
137+ g1t_kit::call(&services.projects, "list", &json!({ "workspace": workspace, "viewer": viewer })).await?;
138+ return Ok(match listed {
139+ Outcome::Ok(projects) => {
140+ Outcome::Ok(Value::Array(projects.iter().map(|project| project_json(project, site)).collect()))
141+ }
142+ Outcome::Fail(failure) => Outcome::Fail(failure),
143+ });
144+ }
145+ let Some(project) = slug(input, "project") else {
146+ return failed(FailureCode::Invalid, "Give the project's slug.");
147+ };
148+ match op {
149+ Op::GetProject => Ok(one(
150+ g1t_kit::call(
151+ &services.projects,
152+ "get",
153+ &json!({ "workspace": workspace, "slug": project, "viewer": viewer }),
154+ )
155+ .await?,
156+ )),
157+ Op::UpdateProject => {
158+ let Some(actor) = viewer else {
159+ return failed(FailureCode::Unauthenticated, "This needs a g1t access token.");
160+ };
161+ let changes = match changes(input) {
162+ Ok(changes) => changes,
163+ Err(message) => return failed(FailureCode::Invalid, &message),
164+ };
165+ Ok(one(
166+ g1t_kit::call(
167+ &services.projects,
168+ "update",
169+ &json!({ "actor": actor, "workspace": workspace, "slug": project, "changes": changes }),
170+ )
171+ .await?,
172+ ))
173+ }
174+ _ => failed(FailureCode::Invalid, "Not a project operation."),
175+ }
176+}
177+
178+#[cfg(test)]
179+mod tests {
180+ use super::*;
181+ use g1t_kit::wire;
182+
183+ fn project() -> Value {
184+ json!({
185+ "id": "prj_1", "workspace": "flagon-io", "slug": "g1t", "name": "g1t",
186+ "description": "Git hosting for people and agents.", "descriptionInherited": true,
187+ "source": {
188+ "kind": "hosted", "repoId": "repo_1", "repo": { "namespace": "flagon-io", "name": "g1t" },
189+ "rootDir": "", "defaultBranch": "main",
190+ },
191+ "private": false, "archived": false, "primary": true,
192+ "setting": { "kind": "app", "runs": "elsewhere" },
193+ "kind": "app", "kindReason": { "by": "set", "detail": "Set to an app." },
194+ "runs": "elsewhere", "productionUrl": "https://g1t.sh",
195+ "detected": { "kind": "app", "reason": { "by": "files", "detail": "It has a wrangler.jsonc." } },
196+ "ecosystem": null,
197+ "links": {
198+ "homepage": "https://g1t.sh", "homepageInherited": true, "docs": "https://docs.g1t.sh",
199+ "custom": [{ "label": "Status", "url": "https://status.g1t.sh" }],
200+ },
201+ "createdBy": "usr_1", "createdAt": "2026-09-01T10:00:00.000Z", "updatedAt": "2026-10-07T10:00:00.000Z",
202+ "pushedAt": "2026-10-07T09:00:00.000Z", "activity": 12.5,
203+ })
204+ }
205+
206+ #[test]
207+ fn a_project_is_snake_case_with_its_address() {
208+ let shown = project_json(&project(), "https://g1t.sh/");
209+ assert!(wire::camel_case_keys(&shown).is_empty(), "{:?}", wire::camel_case_keys(&shown));
210+ assert_eq!(shown["url"], "https://g1t.sh/flagon-io/g1t");
211+ assert_eq!(shown["repository"], "flagon-io/g1t");
212+ assert_eq!(shown["root_dir"], "");
213+ assert_eq!(shown["default_branch"], "main");
214+ assert_eq!(shown["description_inherited"], true);
215+ assert_eq!(shown["kind_reason"]["by"], "set");
216+ assert_eq!(shown["production_url"], "https://g1t.sh");
217+ assert_eq!(shown["setting"]["runs"], "elsewhere");
218+ assert_eq!(shown["detected"]["reason"]["by"], "files");
219+ assert_eq!(shown["links"]["homepage_inherited"], true);
220+ assert_eq!(shown["links"]["custom"][0]["label"], "Status");
221+ assert_eq!(shown["pushed_at"], "2026-10-07T09:00:00.000Z");
222+ // What is the service's own business stays there.
223+ assert!(shown.get("source").is_none());
224+ assert!(shown.get("activity").is_none());
225+ assert!(shown.get("created_by").is_none());
226+ }
227+
228+ #[test]
229+ fn a_change_is_only_what_is_given_in_the_service_s_spelling() {
230+ let asked = json!({
231+ "workspace": "flagon-io", "project": "g1t",
232+ "kind": "app", "runs": "elsewhere", "production_url": "https://g1t.sh",
233+ "root_dir": "apps/web", "docs_url": "docs.g1t.sh",
234+ "links": [{ "label": "Status", "url": "https://status.g1t.sh", "extra": 1 }],
235+ });
236+ let sent = changes(&asked).unwrap();
237+ assert_eq!(
238+ sent,
239+ json!({
240+ "kind": "app", "runs": "elsewhere", "productionUrl": "https://g1t.sh", "rootDir": "apps/web",
241+ "docsUrl": "docs.g1t.sh", "links": [{ "label": "Status", "url": "https://status.g1t.sh" }],
242+ })
243+ );
244+ assert!(sent.get("workspace").is_none());
245+ assert!(sent.get("name").is_none());
246+ }
247+
248+ #[test]
249+ fn null_clears_what_it_can_and_absent_changes_nothing() {
250+ let sent = changes(&json!({ "description": null, "homepage": null, "production_url": null, "docs_url": null, "name": null, "kind": null })).unwrap();
251+ assert_eq!(sent, json!({ "description": null, "homepage": null, "productionUrl": null, "docsUrl": null }));
252+ assert_eq!(changes(&json!({})).unwrap(), json!({}));
253+ assert_eq!(changes(&json!({ "links": [] })).unwrap(), json!({ "links": [] }));
254+ }
255+
256+ #[test]
257+ fn a_change_of_the_wrong_type_is_refused() {
258+ assert!(changes(&json!({ "kind": 3 })).is_err());
259+ assert!(changes(&json!({ "links": "https://g1t.sh" })).is_err());
260+ assert!(changes(&json!({ "links": [{ "url": "https://g1t.sh" }] })).is_err());
261+ }
262+}
+221−0
63176317 ],
63186318 "notes": "Name every pinned project once; anything else is refused with `422 invalid`."
63196319 },
6320+ "list_projects": {
6321+ "params": {
6322+ "workspace": "flagon-io"
6323+ },
6324+ "response": [
6325+ {
6326+ "id": "prj_01kkp3m8w2f6t9qh4c7d1r5n0x",
6327+ "workspace": "flagon-io",
6328+ "slug": "g1t",
6329+ "name": "g1t",
6330+ "description": "Git hosting where people and agents work together.",
6331+ "description_inherited": true,
6332+ "url": "https://g1t.sh/flagon-io/g1t",
6333+ "repository": "flagon-io/g1t",
6334+ "root_dir": "",
6335+ "default_branch": "main",
6336+ "private": false,
6337+ "archived": false,
6338+ "primary": true,
6339+ "kind": "app",
6340+ "kind_reason": {
6341+ "by": "set",
6342+ "detail": "Set to an app that runs elsewhere."
6343+ },
6344+ "runs": "elsewhere",
6345+ "production_url": "https://g1t.sh",
6346+ "setting": {
6347+ "kind": "app",
6348+ "runs": "elsewhere"
6349+ },
6350+ "detected": {
6351+ "kind": "app",
6352+ "reason": {
6353+ "by": "files",
6354+ "detail": "wrangler.jsonc at its root makes it an app."
6355+ }
6356+ },
6357+ "ecosystem": null,
6358+ "links": {
6359+ "homepage": "https://g1t.sh",
6360+ "homepage_inherited": true,
6361+ "docs": "https://docs.g1t.sh",
6362+ "custom": [
6363+ {
6364+ "label": "Status",
6365+ "url": "https://status.g1t.sh"
6366+ }
6367+ ]
6368+ },
6369+ "created_at": "2026-09-01T10:00:00.000Z",
6370+ "updated_at": "2026-10-07T11:20:00.000Z",
6371+ "pushed_at": "2026-10-07T10:00:00.000Z"
6372+ },
6373+ {
6374+ "id": "prj_01kkp3n2a7e5v8sk3b6g9j4m1y",
6375+ "workspace": "flagon-io",
6376+ "slug": "hello",
6377+ "name": "hello",
6378+ "description": "Greets people from the command line.",
6379+ "description_inherited": false,
6380+ "url": "https://g1t.sh/flagon-io/hello",
6381+ "repository": "flagon-io/hello",
6382+ "root_dir": "",
6383+ "default_branch": "main",
6384+ "private": false,
6385+ "archived": false,
6386+ "primary": true,
6387+ "kind": "library",
6388+ "kind_reason": {
6389+ "by": "files",
6390+ "detail": "composer.json says it is a library."
6391+ },
6392+ "runs": null,
6393+ "production_url": null,
6394+ "setting": {
6395+ "kind": null,
6396+ "runs": null
6397+ },
6398+ "detected": {
6399+ "kind": "library",
6400+ "reason": {
6401+ "by": "files",
6402+ "detail": "composer.json says it is a library."
6403+ }
6404+ },
6405+ "ecosystem": "composer",
6406+ "links": {
6407+ "homepage": null,
6408+ "homepage_inherited": false,
6409+ "docs": null,
6410+ "custom": []
6411+ },
6412+ "created_at": "2026-09-12T14:30:00.000Z",
6413+ "updated_at": "2026-10-01T09:12:00.000Z",
6414+ "pushed_at": null
6415+ }
6416+ ],
6417+ "notes": "By name. Projects whose repository you cannot see are left out; without a token you see public ones only. `kind` is what the project is, from `setting` where a person set it and from detection otherwise (`detected`, shown beside it); `kind_reason` says why. `runs` is null for what is not deployed. `homepage_inherited` is true while the homepage is its repository's website. `pushed_at` is null until its repository is pushed to. See [Projects](/guides/projects/)."
6418+ },
6419+ "get_project": {
6420+ "params": {
6421+ "workspace": "flagon-io",
6422+ "project": "g1t"
6423+ },
6424+ "response": {
6425+ "id": "prj_01kkp3m8w2f6t9qh4c7d1r5n0x",
6426+ "workspace": "flagon-io",
6427+ "slug": "g1t",
6428+ "name": "g1t",
6429+ "description": "Git hosting where people and agents work together.",
6430+ "description_inherited": true,
6431+ "url": "https://g1t.sh/flagon-io/g1t",
6432+ "repository": "flagon-io/g1t",
6433+ "root_dir": "",
6434+ "default_branch": "main",
6435+ "private": false,
6436+ "archived": false,
6437+ "primary": true,
6438+ "kind": "app",
6439+ "kind_reason": {
6440+ "by": "set",
6441+ "detail": "Set to an app that runs elsewhere."
6442+ },
6443+ "runs": "elsewhere",
6444+ "production_url": "https://g1t.sh",
6445+ "setting": {
6446+ "kind": "app",
6447+ "runs": "elsewhere"
6448+ },
6449+ "detected": {
6450+ "kind": "app",
6451+ "reason": {
6452+ "by": "files",
6453+ "detail": "wrangler.jsonc at its root makes it an app."
6454+ }
6455+ },
6456+ "ecosystem": null,
6457+ "links": {
6458+ "homepage": "https://g1t.sh",
6459+ "homepage_inherited": true,
6460+ "docs": "https://docs.g1t.sh",
6461+ "custom": [
6462+ {
6463+ "label": "Status",
6464+ "url": "https://status.g1t.sh"
6465+ }
6466+ ]
6467+ },
6468+ "created_at": "2026-09-01T10:00:00.000Z",
6469+ "updated_at": "2026-10-07T11:20:00.000Z",
6470+ "pushed_at": "2026-10-07T10:00:00.000Z"
6471+ },
6472+ "notes": "`404` for a project that does not exist or whose repository you cannot see. `setting` holds what a person set, each part null while it is left to detection. `ecosystem` is where a library's files say it is published (`composer`, `npm`, `cargo`, `go` or `python`), or null. See [What a project is](/guides/projects/#what-a-project-is)."
6473+ },
6474+ "update_project": {
6475+ "params": {
6476+ "workspace": "flagon-io",
6477+ "project": "g1t"
6478+ },
6479+ "request": {
6480+ "kind": "app",
6481+ "runs": "elsewhere",
6482+ "production_url": "https://g1t.sh",
6483+ "docs_url": "https://docs.g1t.sh",
6484+ "links": [
6485+ {
6486+ "label": "Status",
6487+ "url": "https://status.g1t.sh"
6488+ }
6489+ ]
6490+ },
6491+ "response": {
6492+ "id": "prj_01kkp3m8w2f6t9qh4c7d1r5n0x",
6493+ "workspace": "flagon-io",
6494+ "slug": "g1t",
6495+ "name": "g1t",
6496+ "description": "Git hosting where people and agents work together.",
6497+ "description_inherited": true,
6498+ "url": "https://g1t.sh/flagon-io/g1t",
6499+ "repository": "flagon-io/g1t",
6500+ "root_dir": "",
6501+ "default_branch": "main",
6502+ "private": false,
6503+ "archived": false,
6504+ "primary": true,
6505+ "kind": "app",
6506+ "kind_reason": {
6507+ "by": "set",
6508+ "detail": "Set to an app that runs elsewhere."
6509+ },
6510+ "runs": "elsewhere",
6511+ "production_url": "https://g1t.sh",
6512+ "setting": {
6513+ "kind": "app",
6514+ "runs": "elsewhere"
6515+ },
6516+ "detected": {
6517+ "kind": "app",
6518+ "reason": {
6519+ "by": "files",
6520+ "detail": "wrangler.jsonc at its root makes it an app."
6521+ }
6522+ },
6523+ "ecosystem": null,
6524+ "links": {
6525+ "homepage": "https://g1t.sh",
6526+ "homepage_inherited": true,
6527+ "docs": "https://docs.g1t.sh",
6528+ "custom": [
6529+ {
6530+ "label": "Status",
6531+ "url": "https://status.g1t.sh"
6532+ }
6533+ ]
6534+ },
6535+ "created_at": "2026-09-01T10:00:00.000Z",
6536+ "updated_at": "2026-10-07T11:20:00.000Z",
6537+ "pushed_at": "2026-10-07T10:00:00.000Z"
6538+ },
6539+ "notes": "Only the fields given change. `kind` and `runs` take `auto` to go back to detection. Setting `runs` makes the project an app unless it is docs. Making it a `library`, `tool` or `other` while [Deployments](/guides/deployments/) are on is refused with `409 conflict`: turn them off first. `description` and `homepage` given as null or `\"\"` follow the repository's again; `production_url` and `docs_url` given as null or `\"\"` are cleared. `links` replaces the project's other links: at most 10, each with a `label` of up to 40 characters and an http or https `url` (`https://` is added when you leave the scheme out); anything else is refused with `422 invalid`. Needs the Maintain role or higher on the project's repository. Recorded in the [audit log](/guides/audit-log/). See [Settings](/guides/projects/#settings)."
6540+ },
63206541 "list_secret_scanning_alerts": {
63216542 "params": {
63226543 "owner": "flagon-io",
+4−0
9191 route("PATCH", "/user/repository_invitations/:id", Op::AcceptRepoInvitation, &[]),
9292 route("DELETE", "/user/repository_invitations/:id", Op::DeclineRepoInvitation, &[]),
9393 route("PATCH", "/workspaces/:workspace", Op::UpdateWorkspace, &[]),
94+ // A workspace's projects: what each is, where it runs, its links.
95+ route("GET", "/workspaces/:workspace/projects", Op::ListProjects, &[]),
96+ route("GET", "/workspaces/:workspace/projects/:project", Op::GetProject, &[]),
97+ route("PATCH", "/workspaces/:workspace/projects/:project", Op::UpdateProject, &[]),
9498 route("PUT", "/workspaces/:workspace/base_permission", Op::SetBasePermission, &[]),
9599 route(
96100 "GET",
+4−1
283283 Tool {
284284 name: "workspace",
285285 title: "Workspaces",
286− description: "Workspaces own repositories (g1t.sh/{workspace}/{repo}): create, update or delete one, invite members, connect integrations and model providers, set rulesets that hold across its repositories, and keep your own pinned projects at the top of its sidebar.",
286+ description: "Workspaces own repositories (g1t.sh/{workspace}/{repo}): create, update or delete one, invite members, connect integrations and model providers, set rulesets that hold across its repositories, read and change its projects (what each is, where it runs, its links), and keep your own pinned projects at the top of its sidebar.",
287287 default_action: None,
288288 actions: &[
289289 a("get", Op::GetWorkspace, "A workspace's details and settings"),
299299 a("test_integration", Op::TestIntegration, "Check its credentials"),
300300 a("get_model_routes", Op::GetModelRoutes, "Where each kind of work's model requests go"),
301301 a("set_model_routes", Op::SetModelRoutes, "Replace them"),
302+ a("list_projects", Op::ListProjects, "Its projects you can see: what each is, where it runs, its links"),
303+ a("get_project", Op::GetProject, "One project"),
304+ a("update_project", Op::UpdateProject, "Change a project's name, description, kind, where it runs or its links"),
302305 a("list_pinned_projects", Op::ListPinnedProjects, "Your pinned projects in it, in your order"),
303306 a("pin_project", Op::PinProject, "Pin a project, at a position or the end"),
304307 a("unpin_project", Op::UnpinProject, "Unpin a project"),
+11−8
5151 nothing and costs nothing, and the next visit wakes it in milliseconds.
5252
5353 Deployments are part of the [g1t plan](/guides/usage-and-billing/#the-g1t-plan),
54−and are opt-in per project. A project with deployments off says
55−**Deployments are off** on its overview, with **Turn on deployments** for
54+and are opt-in per project. A project set to deploy on g1t, with
55+deployments off, says **Deployments are off** on its overview, with **Turn on deployments** for
5656 people with the Admin [role](/guides/access-and-roles/) on its repository. Nothing builds or runs until you turn them on, and one click turns
5757 them off again.
5858
7272 Production starts building at once from the default branch. Every pull
7373 request opened or pushed to from then on gets a preview.
7474
75−A project g1t takes for a library or a tool, such as a Composer package,
76−does not offer to deploy on its overview; it shows its packages instead.
77−Its **Deployments** page still turns them on, and doing so makes it an
78−app. A project set to **Doesn't deploy** cannot have deployments turned
79−on until the setting changes. See
80−[apps and libraries](/guides/projects/#apps-and-libraries).
75+Deployments are for projects g1t runs. Not every project is one: a
76+library, a tool or documentation is published rather than deployed, and
77+an app may be deployed by its own pipeline somewhere else. Only a project
78+that is [an app or site deployed on g1t](/guides/projects/#what-a-project-is)
79+is asked to turn deployments on. Any other project's **Deployments** page
80+still turns them on; doing so makes it an app deployed on g1t, including
81+one that was deployed elsewhere. A project set to be a library, a tool or
82+something else cannot have deployments turned on until that setting
83+changes.
8184
8285 While payments on g1t are in test mode, no real card is charged: use the
8386 test card `4242 4242 4242 4242` with any future date and any code.
+9−6
2525
2626 On g1t's models you do not have to pick a model. **Auto**, the default,
2727 sends each job to the least costly model that can do it, from three tiers:
28−**fast** (Claude Haiku 4.5 today), **standard** (Claude Sonnet 5.5) and
28+**fast** (Claude Haiku 5.5 today), **standard** (Claude Sonnet 5.5) and
2929 **most capable** (Claude Opus 5.5). It decides by the kind of job, the
3030 size of the change it reads, the issue's labels, whether the last attempt
3131 at the same work failed, and what has worked in the repository before:
3232
33−- Catching up, answering a question and reviewing a small change that
34− touches no sensitive path start on the fast model.
35−- Making and revising changes, planning, and most reviews start on the
36− standard model.
33+- Catching up, answering a question, planning, and reviewing a small
34+ change that touches no sensitive path start on the fast model.
35+- Making and revising changes, and most reviews, start on the standard
36+ model.
37+- Planning runs at high effort (the model thinks longer before it
38+ answers), answering at medium and catching up at low, on models that
39+ take an effort level.
3740 - A review of a very large change, work on an issue labelled
3841 `architecture`, and work that failed twice in a row go to the most
3942 capable model. One failure moves the next attempt up one tier.
4245 keeps failing, up.
4346
4447 Every run says which model it used and why, in one line on its run and in
45−its pull request's session, such as *Used a fast model (Claude Haiku 4.5):
48+its pull request's session, such as *Used a fast model (Claude Haiku 5.5):
4649 small change, 3 files and 80 lines.* The full rules are in
4750 [which model runs](/guides/working-with-g1t/#which-model-runs).
4851
+117−41
3535
3636 | Page | Address | |
3737 | --- | --- | --- |
38−| **Overview** | `g1t.sh/<workspace>/<project>` | Production with a screenshot, or for a library its packages; the steps left to get to production or to a first release; what is in progress, active branches, live previews, recent builds, and the source. See [the overview](#the-overview). |
38+| **Overview** | `g1t.sh/<workspace>/<project>` | What it is and its links; production wherever it is deployed, or for a library its packages; the steps that apply to it; what is in progress, active branches, the latest commits, and its About. See [the overview](#the-overview). |
3939 | **Code** | `…/code` | The repository's files, commits and branches. |
4040 | **Issues**, **Pull requests**, **Merge queue**, **Plan** | `…/issues` and so on | As they always were. |
4141 | **Actions** | `…/actions` | [GitHub Actions workflows](/guides/actions/). |
5353
5454 | Part | What it shows |
5555 | --- | --- |
56−| **Production** | For an app: a screenshot of the live site, which opens it; its address, the commit it runs and when it went up; **Visit** and **Redeploy**. See [the production screenshot](/guides/deployments/#the-production-screenshot). |
57−| **Packages** | For a [library or a tool](#apps-and-libraries), in place of Production: the packages its repository publishes, each with its latest version and how to install it, or how to publish a first one. |
58−| **Get to production**, or **Ship a release** for a library | The checklists below, until every step is done or you dismiss it. |
56+| **What it is** | A badge, such as **App, deployed elsewhere** or **Library**, beside its homepage and docs. People who can change its settings open the badge to change [what it is](#what-a-project-is) in one click. |
57+| **Production** | For an app [deployed on g1t](/guides/deployments/): a screenshot of the live site, which opens it; its address, the commit it runs and when it went up; **Visit** and **Redeploy**. For an app deployed elsewhere: production's address and **Visit**, with the default branch's latest commit. For an app nobody has placed yet: one question, where it runs. |
58+| **Packages** | For a library or a tool, in place of Production: the packages its repository publishes, each with its latest version and how to install it, or how to publish a first one. |
59+| **Documentation**, **Homepage** | For docs, where they are read; for anything else, its homepage. |
60+| **Get to production**, **Ship a release** or **Get started** | The [checklist](#the-checklist) for what the project is, until every step is done or you dismiss it. |
5961 | **Right now** | Agents at work, and open pull requests moving from working to landed. |
6062 | **Needs you** | What is waiting on a person: a failed production build, a pull request to merge or review, a stuck run. |
6163 | **Active branches** | Branches other than the default, newest first. See [active branches](#active-branches). |
62−| **Recent changes**, **Activity**, **Previews** | What landed, everything that happened, and the previews that are up. |
64+| **Recent changes**, **Latest on main**, **Activity**, **Previews** | What landed, the default branch's latest commits, everything that happened, and the previews that are up. |
65+| **About** | Its description, its [links](#links), what it is, and its latest release (its newest tag). People who can change its settings edit the description and links from here. |
6366 | **Health**, **Dependencies**, **Clone** | How often checks pass, recent builds and open issues by age; what it uses and what uses it; the clone address. |
6467
68+### The checklist
69+
70+People with a role on its repository see a card that counts what the
71+project has done, such as **3/5**. Its steps follow
72+[what the project is](#what-a-project-is): only an app deployed on g1t is
73+asked to deploy, add a domain or open a preview. Each step is worked out
74+from the project itself, and each links to where you do it. The card goes
75+away when every step is done. To hide it sooner, choose **×** on it. That
76+hides it for this project in this browser only.
77+
78+| What it is | Card | Steps |
79+| --- | --- | --- |
80+| App or site, deployed on g1t | **Get to production** | [Below](#get-to-production). |
81+| Library, tool | **Ship a release** | [Below](#ship-a-release). |
82+| App, deployed elsewhere | **Get started** | Push code; add production's address; add checks on pull requests; repository instructions; a first issue for g1t. |
83+| App nobody has placed yet | **Get started** | Push code; say where it runs; add checks on pull requests; repository instructions; a first issue for g1t. |
84+| Documentation | **Get started** | Push code; add where its docs are read (a docs or homepage address); repository instructions; a first issue for g1t. |
85+| Something else | **Get started** | Push code; add its links; repository instructions; a first issue for g1t. |
86+
6587 ### Get to production
6688
67−People with a role on its repository see a card that counts what the project has done towards
68−production, such as **3/6**. Each step is worked out from the project
69−itself, and each links to where you do it:
89+For an app or site deployed on g1t:
7090
7191 | Step | Done when | Links to |
7292 | --- | --- | --- |
7797 | Set up repository instructions | `AGENTS.md` or `CLAUDE.md` is at the root of the default branch. | The instructions on the **Agents** page. See [repository instructions](/guides/working-with-g1t/#repository-instructions). |
7898 | Assign a first issue to g1t | g1t has had a run, a pull request or an issue here. | A new issue. |
7999
80−The card goes away when every step is done. To hide it sooner, choose
81−**×** on it. That hides it for this project in this browser only.
82−
83100 ### Ship a release
84101
85−A [library or a tool](#apps-and-libraries) does not deploy, so its card
86−counts the steps to a first release instead:
102+A library or a tool does not deploy, so its card counts the steps to a
103+first release instead:
87104
88105 | Step | Done when | Links to |
89106 | --- | --- | --- |
93110 | Set up repository instructions | As above. | The **Agents** page. |
94111 | Assign a first issue to g1t | As above. | A new issue. |
95112
96−## Apps and libraries
113+## What a project is
97114
98−Every project is either an **app**, which deploys, or a **library or a
99−tool**, which is published and installed. An app's overview shows
100−production and the steps to get there; a library's shows its packages
101−and the steps to a first release, and never offers to turn on
102−deployments. Its **Deployments** page stays in the sidebar either way.
115+A project is one of these, and its overview follows:
103116
104−g1t works it out for itself, in this order:
117+| What it is | Its overview shows |
118+| --- | --- |
119+| **App or site, deployed on g1t** | Production on g1t.page, with previews and domains, and **Get to production**. |
120+| **App or site, deployed elsewhere** | Production at the address you give, deployed by your own pipeline, with **Visit**. It is never asked to turn on Deployments; Deployment settings stay one link away. |
121+| **Library or package** | The packages its repository publishes and how to install them, and **Ship a release**. |
122+| **Tool or CLI** | The same as a library: its packages and releases. |
123+| **Documentation** | Where its docs are read. Docs can be published on g1t.page too, by turning on Deployments. |
124+| **Something else** | Its homepage and links. |
105125
106−1. If [Deployments](/guides/deployments/) are on for the project, it is an app.
126+Its **Deployments** page stays in the sidebar whatever it is.
127+
128+### How g1t works it out
129+
130+Until you say, g1t works it out for itself, in this order:
131+
132+1. If [Deployments](/guides/deployments/) are on for the project, it is
133+ an app deployed on g1t.
107134 2. If its repository publishes a package other than a container image,
108135 such as a [Composer](/guides/composer/) or [npm](/guides/npm/)
109136 package, it is a library.
110137 3. If the files at the root of its default branch (or of its root
111− directory) say it is a library, it is one:
138+ directory) say what it is, it is that:
112139
113− | File | A library when |
140+ | File | Says |
114141 | --- | --- |
115− | `composer.json` | Its `type` is anything but `project`, such as `library`; or it has no `type`, has `autoload`, and has no `public/index.php`. |
116− | `Cargo.toml` | It builds a library (`[lib]` or `src/lib.rs`) and no binary (`[[bin]]` or `src/main.rs`). |
117− | `go.mod` | No `.go` file at the root is `package main`. |
118− | `pyproject.toml` | It has a build backend and depends on no app framework, such as Django, Flask or FastAPI. |
119− | `package.json` | It has `main`, `exports`, `module`, `files` or `bin`, no `start` or `dev` script, and no app framework such as Next.js, Astro, Nuxt, Remix or SvelteKit. |
142+ | `wrangler.jsonc`, `wrangler.json` or `wrangler.toml` | An app. |
143+ | `mkdocs.yml`, `book.toml`, `docusaurus.config.js` (or `.ts`, `.mjs`) or `antora.yml` | Documentation. |
144+ | `composer.json` | A library when its `type` is anything but `project`, such as `library`; or it has no `type`, has `autoload`, and has no `public/index.php`. |
145+ | `Cargo.toml` | A library when it builds a library (`[lib]` or `src/lib.rs`) and no binary (`[[bin]]` or `src/main.rs`). |
146+ | `go.mod` | A library when no `.go` file at the root is `package main`. |
147+ | `pyproject.toml` | A library when it has a build backend and depends on no app framework, such as Django, Flask or FastAPI. |
148+ | `package.json` | A tool when it has `bin` and nothing to import (no `main`, `module` or `exports`); a library when it has `main`, `exports`, `module`, `files` or `bin`; either way only with no `start` or `dev` script and no app framework such as Next.js, Astro, Nuxt, Remix or SvelteKit. |
120149
121150 The language's own manifest is read before `package.json`, which many
122− projects carry only for tooling. A Workers config (`wrangler.jsonc`,
123− `wrangler.json` or `wrangler.toml`) or an `index.html` at the root
124− makes it an app.
151+ projects carry only for tooling. An `index.html` at the root makes it
152+ an app.
125153 4. Anything else is an app.
126154
127155 The files are read again on every push to the default branch.
128156
129−To decide for yourself, open **Settings**, then **General**, and under
130−**Deployments for this project** choose:
157+An app found this way runs on g1t only while Deployments are on. Until
158+you say where it runs, its overview asks, in place of production:
159+**Deploy on g1t**, **It's deployed elsewhere** (with production's
160+address), or **It isn't deployed** (a library, a tool, documentation or
161+something else).
162+
163+### Choosing it yourself
164+
165+From the overview, open the badge beside its name, such as **App**, and
166+choose one. Or open **Settings**, then **General**, and under **What it
167+is** choose **Detect automatically**, which shows what was detected and
168+why, or one of the kinds above. **App or site, deployed elsewhere** asks
169+for production's address.
170+
171+Choosing a library, a tool or something else while Deployments are on is
172+refused: turn them off first under **Settings**, then **Deployments**;
173+g1t does not turn them off for you. While it is set that way, Deployments
174+cannot be turned on. Turning Deployments on for an app deployed elsewhere
175+makes it an app deployed on g1t.
131176
132−- **Detect automatically**: the rules above. It shows what was detected
133− and why.
134−- **Deploys**: an app, whatever its files say.
135−- **Doesn't deploy**: a library or a tool. Its overview offers no
136− deploying. If Deployments are on for it, turn them off first under
137− **Settings**, then **Deployments**; g1t does not turn them off for you.
138− While it is set this way, Deployments cannot be turned on.
177+Changing what it is needs the role that changes the project's settings.
139178
140179 ### Active branches
141180
152191 are read, those with open pull requests first; a project with more says
153192 how many it has.
154193
194+## Links
195+
196+A project has a **homepage**, a **docs** address, and up to 10 other
197+links, each a label and an address, such as a status page or its listing
198+in a registry. They show in its overview's About and beside what it is,
199+on its card on its workspace's page, in its workspace's Projects, and on
200+Explore for a public project.
201+
202+To set them, open **Settings**, then **General**, and fill in **Links**;
203+or choose the pencil on the overview's **About**. Each is an http or
204+https address; `https://` is added when you leave it out. A label left
205+empty is the address's host name. Removing every row of other links
206+clears them.
207+
208+A repository's own project shows the repository's website as its
209+homepage until you give the project one of its own. For an app deployed
210+on g1t, its g1t.page address still shows as production; set the homepage
211+to production's own domain if you have one.
212+
155213 ## Settings
156214
157215 A project's **Settings** has a tab for each part:
158216
159217 | Tab | What it holds |
160218 | --- | --- |
161−| **General** | The project's name and description, its source, its **root directory**, and whether it deploys (see [apps and libraries](#apps-and-libraries)). A project shows its repository's description, and follows it as it changes, until you give the project one of its own; **Use the repository's description** goes back. |
219+| **General** | The project's name and description, its source, its **root directory**, [what it is](#what-a-project-is), and its [links](#links). A project shows its repository's description, and follows it as it changes, until you give the project one of its own; **Use the repository's description** goes back. |
162220 | **Deployments** | Production, previews, build command, output directory and idle days. See [Deployments](/guides/deployments/#settings). |
163221 | **Domains** | Custom domains for production. See [custom domains](/guides/deployments/#custom-domains). |
164222 | **Dependencies** | The projects this one uses, and the ones that use it. See [Dependencies](#dependencies). |
259317 Projects keep their repository's routes: `/repos/{workspace}/{project}/…`
260318 reaches the project's repository, and its secrets and variables are the
261319 project's. See the [API reference](/reference/api/).
320+
321+What a project is, where it runs and its links have routes of their own,
322+and actions on the [MCP](/reference/mcp/) `workspace` tool:
323+
324+| Route | MCP action | |
325+| --- | --- | --- |
326+| `GET /workspaces/{workspace}/projects` | `list_projects` | The workspace's projects you can see. |
327+| `GET /workspaces/{workspace}/projects/{project}` | `get_project` | One project, with `kind`, `runs`, `production_url`, `setting`, `detected` and `links`. |
328+| `PATCH /workspaces/{workspace}/projects/{project}` | `update_project` | Changes `kind`, `runs`, `production_url`, `homepage`, `docs_url`, `links`, or its name, description or root directory. Needs `repo:write`. |
329+
330+To say an app is deployed by your own pipeline, with its docs:
331+
332+```sh
333+curl -X PATCH https://api.g1t.sh/workspaces/acme/projects/web \
334+ -H "Authorization: Bearer $G1T_TOKEN" \
335+ -H "Content-Type: application/json" \
336+ -d '{"runs": "elsewhere", "production_url": "https://acme.dev", "docs_url": "https://docs.acme.dev"}'
337+```
+8−3
446446
447447 | Tier | Model today | For |
448448 | --- | --- | --- |
449−| Fast | Claude Haiku 4.5 | Small, well-bounded work |
449+| Fast | Claude Haiku 5.5 | Small, well-bounded work, and plans |
450450 | Standard | Claude Sonnet 5.5 | Most changes and reviews |
451451 | Most capable | Claude Opus 5.5 | Hard work, and work that failed on the standard model |
452452
460460 | Reviewing a pull request that changes more than 60 files or 3,000 lines | Most capable |
461461 | Reviewing any other pull request, or one whose changed files g1t does not know yet | Standard |
462462 | Catching up with the base branch and resolving conflicts | Fast |
463−| Planning an outcome | Standard |
463+| Planning an outcome | Fast, at high effort |
464464
465465 Then, in this order:
466466
486486
487487 The agent's own small background steps run on the fast tier.
488488
489+Some work also has an effort level, how long the model thinks before it
490+answers: planning runs at high effort, answering a question at medium and
491+catching up at low, whichever tier the work lands on. Other work runs at
492+the model's default. A workspace's own provider is sent no effort level.
493+
489494 Every run says which model it used and why, in one line: as the first
490495 step on its run, and at the top of its pull request's session. For
491−example, *Used a fast model (Claude Haiku 4.5): small change, 3 files and
496+example, *Used a fast model (Claude Haiku 5.5): small change, 3 files and
492497 80 lines.* An agent's review also says which model wrote it. When a better
493498 model for a tier appears, g1t changes the route and nothing you have set
494499 up needs to change.
+2−1
347347 - **Find a project** matches every word you type in a project's name, its
348348 address or its description. Press <kbd>/</kbd> anywhere on the page to
349349 start typing.
350−- **Filters**: public or private; apps or libraries; the language its
350+- **Filters**: public or private; [what it is](/guides/projects/#what-a-project-is)
351+ (apps, libraries, and tools, docs or other when the workspace has any); the language its
351352 manifests say it is written in; only projects with
352353 [Deployments](/guides/deployments/) on; and archived projects, which are
353354 left out unless you ask for them. Each choice shows how many projects it
+8−3
541541 ## `workspace`
542542
543543 Workspaces own repositories: create, update or delete one, invite members,
544−connect [integrations](/guides/integrations/) and model providers, and keep
545−your own [pinned projects](/guides/workspaces/#pinned-and-recent-projects)
546−at the top of its sidebar. See [workspaces](/guides/workspaces/).
544+connect [integrations](/guides/integrations/) and model providers, read and
545+change its [projects](/guides/projects/) (what each is, where it runs, its
546+links), and keep your own
547+[pinned projects](/guides/workspaces/#pinned-and-recent-projects) at the top
548+of its sidebar. See [workspaces](/guides/workspaces/).
547549
548550 | Action | What it does | Required | Scope |
549551 | --- | --- | --- | --- |
560562 | [`test_integration`](/reference/api/integrations/test-integration/) | Check its credentials against the system it connects to. Owners only. | `workspace`, `id` | `workspace:admin` |
561563 | [`get_model_routes`](/reference/api/integrations/get-model-routes/) | Which provider and model each kind of work goes to. Members only. | `workspace` | `workspace:read` |
562564 | [`set_model_routes`](/reference/api/integrations/set-model-routes/) | Replace them: each route has `task`, `connection_id` (null for g1t's models) and `model` (on g1t's models: `small`, `large`, `frontier`, or null for Auto). Owners only. | `workspace`, `routes` | `workspace:admin` |
565+| [`list_projects`](/reference/api/projects/list-projects/) | Its projects you can see, by name: each with its `kind` and `kind_reason`, where it `runs` and its `production_url`, its repository and `root_dir`, and its `links`. | `workspace` | `repo:read` |
566+| [`get_project`](/reference/api/projects/get-project/) | One project, with what a person set (`setting`) and what detection decides (`detected`). | `workspace`, `project` | `repo:read` |
567+| [`update_project`](/reference/api/projects/update-project/) | Change its `name`, `description`, `root_dir`, `kind`, `runs`, `production_url`, `homepage`, `docs_url` or `links`; only what you give changes. `auto` leaves `kind` or `runs` to detection, null clears a link or goes back to the repository's, and `links` replaces its other links (at most 10). Maintain role or higher. | `workspace`, `project` | `repo:write` |
563568 | [`list_pinned_projects`](/reference/api/pinned-projects/list-pinned-projects/) | Your pinned projects in it, in your order, each with its `position`. Your own: a personal token or an OAuth sign-in. | `workspace` | `account:read` |
564569 | [`pin_project`](/reference/api/pinned-projects/pin-project/) | Pin a project you can see, at `position` (0 first) or at the end; at most 8 a workspace. Returns your pins. | `workspace`, `project` | `account:write` |
565570 | [`unpin_project`](/reference/api/pinned-projects/unpin-project/) | Unpin it. Returns your pins. | `workspace`, `project` | `account:write` |
+0−0

Binary or large file; its contents are not shown.

+1−1
736736 <Section
737737 id="reset"
738738 title="Reset billing (testing)"
739− description="Wipes this workspace's billing: its ledger and balance, plan, limits, trial, invoices, holds, signals and cost rows. The workspace, its members and its repositories stay. Only while billing is on Stripe's test key."
739+ description="Wipes this workspace's billing: its ledger and balance, plan, limits, trial, invoices, holds, signals and cost rows. What its usage cost g1t is kept, and counted on Costs & margin as given away (testing resets). The workspace, its members and its repositories stay. Only while billing is on Stripe's test key."
740740 >
741741 <form method="post" action={`${pathname}#reset`} className="space-y-4">
742742 <input type="hidden" name="intent" value="reset" />
+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

This change is too large to show in full.