Skip to content

Commit

The landing page no longer promises a workspace in a minute: sign-ups are about to be held for invites, so both buttons say Get started and still lead to /register. Artifacts drops its Coming soon tag on the page, since docs work today, and its card links to how Artifacts work rather than what they will do; only slides, designs and dashboards are still to come, as the card says. The g1t CLI is named beside MCP and the API wherever the page says how a coding agent works on g1t, with its install line under the MCP address and a button to its guide, and the forge's list includes it. Chat's coming-soon list says chat mentions in Notifications, as the Chat guide does, rather than approvals, which Notifications already has. The tour's preview address has the form deployments give one (web-git-csv-acme.g1t.page), the step pills stay on one line on a phone, and the long git and curl lines wrap at their slashes there instead of hiding their ends.

syntaqxcommitted Parent2eed2fbBrowse files
13 files+29−40770/13 viewed
+27−15
226226 name: "Artifacts",
227227 about:
228228 "Docs now, and slides, designs and dashboards next, made together live.",
229− soon: true,
230229 to: `${DOCS}/guides/artifacts/`,
231230 },
232231 {
233232 icon: <Bot size={18} />,
234233 name: "Your coding agents",
235234 about:
236− "Claude Code, Codex, Cursor or any MCP client, working as you with the access you give them.",
235+ "Claude Code, Codex, Cursor or any MCP client, working as you through MCP, the g1t CLI or the API, with only the access you give them.",
237236 to: `${DOCS}/guides/bring-your-own-agent/`,
238237 },
239238 ];
308307 "Import from any git host",
309308 "REST API and OpenAPI",
310309 "MCP server",
310+ "The g1t CLI",
311311 ];
312312
313313 const SECURE: { icon: ReactNode; title: string; about: string; to: string }[] =
402402 </p>
403403 <div className="mt-9 flex animate-fade-up flex-wrap items-center justify-center gap-3">
404404 <ButtonLink to="/register" size="lg" className="rounded-full">
405− Start for free
405+ Get started
406406 <ArrowRight size={15} />
407407 </ButtonLink>
408408 <ButtonLink
454454 </h2>
455455 <p className="mt-5 max-w-2xl text-lg leading-8 text-muted">
456456 Code, Chat and Artifacts share one set of members, one sign-in and
457− one bill. The coding agents you connect work as you, through MCP
458− and the API.
457+ one bill. The coding agents you connect work as you, through MCP,
458+ the g1t CLI and the API.
459459 </p>
460460 </div>
461461 <div className="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
599599 text: "Pull requests, checks and deploys as cards in the channel",
600600 soon: true,
601601 },
602− { text: "Mentions and approvals in Notifications", soon: true },
602+ { text: "Chat mentions in Notifications", soon: true },
603603 {
604604 text: "A phone app with notifications, Android first, for g1t.sh or your own g1t",
605605 soon: true,
746746 <More to={`${DOCS}/guides/merge-queue/`}>The merge queue</More>
747747 </div>
748748 <div className="rounded-3xl bg-surface p-8 ring-1 ring-line">
749− <div className="flex items-center gap-3">
750− <Eyebrow>Artifacts</Eyebrow>
751− <Soon />
752− </div>
749+ <Eyebrow>Artifacts</Eyebrow>
753750 <h3 className="mt-3 text-2xl font-semibold tracking-tight text-balance">
754751 A knowledge base that keeps itself true
755752 </h3>
759756 change touches something a doc cites, the doc is flagged for its
760757 owner to update.
761758 </p>
762− <More to={`${DOCS}/guides/artifacts/`}>What Artifacts will do</More>
759+ <More to={`${DOCS}/guides/artifacts/`}>How Artifacts work</More>
763760 </div>
764761 </section>
765762
847844 <p className="mt-4 max-w-md leading-7 text-muted">
848845 Every action is a REST route and an MCP tool, with an OpenAPI
849846 document, OAuth apps, access tokens and signed webhooks for every
850− event.
847+ event. The g1t CLI does the same from a terminal or a script:
848+ open a pull request, wait on its checks, read a failed run&apos;s
849+ logs.
851850 </p>
852851 <div className="mt-6 flex flex-wrap gap-3">
853852 <ButtonLink
857856 Connect an agent
858857 <ArrowRight size={14} />
859858 </ButtonLink>
859+ <ButtonLink to={`${DOCS}/guides/cli/`} variant="outline">
860+ The g1t CLI
861+ </ButtonLink>
860862 <ButtonLink to={`${DOCS}/reference/api/`} variant="outline">
861863 API reference
862864 </ButtonLink>
867869 Add g1t as a remote MCP server, then sign in through your browser:
868870 </p>
869871 <CopyLine text="https://mcp.g1t.sh" />
872+ <p className="pt-3 text-sm text-muted">
873+ Install the CLI, then sign in with{" "}
874+ <code className="font-mono text-fg-soft">g1t auth login</code>:
875+ </p>
876+ <CopyLine prompt text="curl -fsSL https://g1t.sh/install | sh" />
877+ <p className="pt-3 text-sm text-muted">
878+ Underneath, it is plain git and HTTP:
879+ </p>
870880 <CopyLine
871881 prompt
882+ breakAtSlashes
872883 text="git clone https://g1t.sh/flagon-io/g1t.git"
873884 />
874885 <CopyLine
875886 prompt
887+ breakAtSlashes
876888 text="curl https://api.g1t.sh/repos/flagon-io/g1t/queue"
877889 />
878890 </div>
977989 Bring your team. Bring your agents.
978990 </h2>
979991 <p className="mx-auto mt-4 max-w-xl text-fg-soft/80 text-balance">
980− Create a workspace in a minute, push a repository and connect the
981− coding agent you already use. Chat and the forge are free.
992+ Make a workspace, push a repository and connect the coding agent
993+ you already use. Chat and the forge are free.
982994 </p>
983995 <div className="mt-8 flex flex-wrap justify-center gap-3">
984996 <ButtonLink to="/register" size="lg" className="rounded-full">
985− Create your workspace
997+ Get started
986998 <ArrowRight size={15} />
987999 </ButtonLink>
9881000 <ButtonLink
+2−2
740740 )}
741741 >
742742 <p className="text-[11px] font-medium text-muted">Preview</p>
743− <p className="mt-1 truncate font-mono text-[11px] text-accent">pr-431.web.acme.g1t.page</p>
743+ <p className="mt-1 truncate font-mono text-[11px] text-accent">web-git-csv-acme.g1t.page</p>
744744 </div>
745745 </div>
746746 </div>
12311231 aria-selected={on}
12321232 onClick={() => clock.jump(index)}
12331233 className={cn(
1234− "group relative overflow-hidden rounded-full px-3 py-1.5 text-xs font-medium ring-1 transition-colors",
1234+ "group relative overflow-hidden rounded-full px-2 py-1.5 text-[11px] font-medium whitespace-nowrap ring-1 transition-colors sm:px-3 sm:text-xs",
12351235 on ? "bg-surface text-fg ring-line-strong" : "text-muted ring-line hover:text-fg-soft",
12361236 )}
12371237 >
+0−351
1−//! Memory that fills itself. Kept by the work service, beside memory.
2−//!
3−//! What agents and people learn arrives as a **candidate**: from what an
4−//! agent reports learning at the end of a run, a person's correction in a
5−//! review of an agent's pull request, a merged pull request's decision, or
6−//! a project's own docs and manifests. A candidate is given to no agent
7−//! until it is **kept**: by a member in the Review queue, or by g1t when
8−//! two independent sources say the same thing, or a doc says it with high
9−//! confidence ([`promotes`]). Nothing that looks like a secret is stored.
10−//!
11−//! Each `*Args` struct is the argument of the method of the same name,
12−//! served at `POST /rpc/<method>`.
13−
14−use serde::{Deserialize, Serialize};
15−
16−use crate::agents::{Memory, MemoryKind, MemoryScope, MemoryStatus};
17−use crate::repos::RepoPath;
18−use crate::{User, Viewer};
19−
20−/// The confidence at or above which a doc's word is kept without review.
21−pub const DOC_CONFIDENCE: f64 = 0.85;
22−/// How many independent sources keep a candidate without review.
23−pub const INDEPENDENT_SOURCES: usize = 2;
24−/// The most items one capture takes.
25−pub const MAX_CAPTURE: usize = 50;
26−/// The most an agent reports learning in one run.
27−pub const MAX_LEARNED: usize = 8;
28−/// The longest piece of evidence kept, in characters.
29−pub const MAX_EVIDENCE_CHARS: usize = 400;
30−
31−/// Where a captured memory came from.
32−#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
33−#[serde(rename_all = "snake_case")]
34−pub enum CaptureSource {
35− /// An agent's run, reporting what it learned.
36− Run,
37− /// A person's review of an agent's pull request.
38− Review,
39− /// A merged pull request.
40− Pr,
41− /// A project's README, AGENTS.md, docs or manifests.
42− Doc,
43− /// Written by a person.
44− Manual,
45−}
46−
47−impl CaptureSource {
48− pub fn as_str(self) -> &'static str {
49− match self {
50− CaptureSource::Run => "run",
51− CaptureSource::Review => "review",
52− CaptureSource::Pr => "pr",
53− CaptureSource::Doc => "doc",
54− CaptureSource::Manual => "manual",
55− }
56− }
57−}
58−
59−/// Whether a candidate is kept without anyone reviewing it: said by
60−/// `sources` independent sources, or by a doc with `confidence` at or above
61−/// [`DOC_CONFIDENCE`].
62−pub fn promotes(sources: usize, kind: CaptureSource, confidence: Option<f64>) -> bool {
63− sources >= INDEPENDENT_SOURCES
64− || (kind == CaptureSource::Doc && confidence.is_some_and(|c| c >= DOC_CONFIDENCE))
65−}
66−
67−/// A memory's text folded for comparing: lowercase words and digits, one
68−/// space apart, so wording that differs only in case, punctuation or
69−/// spacing is the same memory.
70−pub fn fingerprint(text: &str) -> String {
71− text.to_lowercase()
72− .split(|c: char| !c.is_alphanumeric())
73− .filter(|word| !word.is_empty())
74− .collect::<Vec<_>>()
75− .join(" ")
76−}
77−
78−/// How alike two memories' words must be (shared over all, as sets) to be
79−/// one memory.
80−pub const SAME_WORDS: f64 = 0.85;
81−/// The fewest distinct words a memory needs before it can be found inside
82−/// another: shorter ones are too general to be the same thing.
83−const CONTAINED_MIN_WORDS: usize = 6;
84−
85−/// Whether two memories say the same thing: the same words once folded
86−/// ([`fingerprint`]); one's words, in order, inside the other's; all of a
87−/// memory's words (at least six of them) among the other's; or most of
88−/// their words shared (Jaccard at or above [`SAME_WORDS`]). "g1t is a
89−/// Cargo workspace (apps/*, crates/*)" and the same sentence listing every
90−/// crate are one memory.
91−pub fn same_memory(a: &str, b: &str) -> bool {
92− let (a, b) = (fingerprint(a), fingerprint(b));
93− if a.is_empty() || b.is_empty() {
94− return false;
95− }
96− if a == b {
97− return true;
98− }
99− let (short, long) = if a.len() <= b.len() { (&a, &b) } else { (&b, &a) };
100− let short_words: std::collections::HashSet<&str> = short.split(' ').collect();
101− let long_words: std::collections::HashSet<&str> = long.split(' ').collect();
102− // Contiguous: the shorter one's words, in order, inside the longer one's.
103− if short.split(' ').count() >= 4 && format!(" {long} ").contains(&format!(" {short} ")) {
104− return true;
105− }
106− if short_words.len() >= CONTAINED_MIN_WORDS && short_words.is_subset(&long_words) {
107− return true;
108− }
109− let shared = short_words.intersection(&long_words).count();
110− let all = short_words.union(&long_words).count();
111− all > 0 && shared as f64 / all as f64 >= SAME_WORDS
112−}
113−
114−/// One thing learned, as a source reports it.
115−#[derive(Clone, Debug, Serialize, Deserialize)]
116−#[serde(rename_all = "camelCase")]
117−pub struct CaptureItem {
118− pub scope: MemoryScope,
119− /// For a project's memory: its repository's id.
120− #[serde(default)]
121− pub repo_id: Option<String>,
122− #[serde(default)]
123− pub kind: MemoryKind,
124− pub text: String,
125− /// 0 to 1.
126− #[serde(default)]
127− pub confidence: Option<f64>,
128− pub source: CaptureSource,
129− /// What it came from: `run:<id>`, `comment:<id>`, `pull:<repo id>#<n>`,
130− /// `doc:<repo id>:<path>`. Two items with the same reference are one
131− /// source, however often they arrive.
132− pub reference: String,
133− /// What it was learned from, quoted.
134− #[serde(default)]
135− pub evidence: Option<String>,
136− /// The pull request it was learned on.
137− #[serde(default)]
138− pub number: Option<u32>,
139− /// The agent run it was learned in.
140− #[serde(default)]
141− pub run_id: Option<String>,
142−}
143−
144−/// `capture_memories`: candidates from a service (the context service's
145−/// backfill and doc reading). Called by services only. Returns `Captured`.
146−#[derive(Debug, Serialize, Deserialize)]
147−#[serde(rename_all = "camelCase")]
148−pub struct CaptureMemoriesArgs {
149− /// The workspace's slug.
150− pub workspace: String,
151− pub items: Vec<CaptureItem>,
152− /// Who it is recorded as written by, such as `g1t`.
153− #[serde(default)]
154− pub by: Option<String>,
155−}
156−
157−#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
158−#[serde(rename_all = "camelCase")]
159−pub struct Captured {
160− /// New candidates.
161− pub added: u32,
162− /// Ones that matched a memory already there, as another sighting.
163− pub merged: u32,
164− /// Ones kept by the promotion rule, new or merged.
165− pub kept: u32,
166− /// Ones refused: empty, too long, or holding something like a secret.
167− pub refused: u32,
168−}
169−
170−/// What an agent says it learned, as the harness reports it.
171−#[derive(Clone, Debug, Serialize, Deserialize)]
172−#[serde(rename_all = "camelCase")]
173−pub struct LearnedItem {
174− /// `fact`, `convention`, `decision` or `gotcha`.
175− #[serde(default)]
176− pub kind: Option<String>,
177− /// `project` or `workspace`.
178− #[serde(default)]
179− pub scope: Option<String>,
180− pub text: String,
181− /// What showed it: a command's output, a file, a failing test.
182− #[serde(default)]
183− pub evidence: Option<String>,
184−}
185−
186−/// `report_learned`: a sandbox reporting what its agent learned, with the
187−/// run's token. Each item arrives as a candidate from the run. Returns
188−/// `Outcome<Captured>`.
189−#[derive(Debug, Default, Serialize, Deserialize)]
190−#[serde(rename_all = "camelCase", default)]
191−pub struct ReportLearnedArgs {
192− pub run_id: String,
193− pub token: String,
194− pub items: Vec<LearnedItem>,
195−}
196−
197−/// `list_candidates`: memory waiting for review in a workspace and, with
198−/// `repo`, only that project's. Members only. Returns `Outcome<Vec<Memory>>`.
199−#[derive(Debug, Serialize, Deserialize)]
200−pub struct ListCandidatesArgs {
201− pub viewer: Viewer,
202− pub workspace: String,
203− #[serde(default)]
204− pub repo: Option<RepoPath>,
205−}
206−
207−#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
208−#[serde(rename_all = "snake_case")]
209−pub enum ReviewDecision {
210− Keep,
211− Dismiss,
212−}
213−
214−/// `review_memory`: a member keeps a candidate, edited or as it is, or
215−/// dismisses it. A dismissed memory is not suggested again from the same
216−/// wording. Returns `Outcome<Memory>`.
217−#[derive(Debug, Serialize, Deserialize)]
218−pub struct ReviewMemoryArgs {
219− pub actor: User,
220− pub workspace: String,
221− pub id: String,
222− pub decision: ReviewDecision,
223− #[serde(default)]
224− pub text: Option<String>,
225− #[serde(default)]
226− pub kind: Option<MemoryKind>,
227−}
228−
229−/// `memories_by_id`: memories as the context service indexes them, in any
230−/// status, so a dismissed one can be taken out of search. Services only.
231−/// Returns `Vec<Memory>`.
232−#[derive(Debug, Serialize, Deserialize)]
233−pub struct MemoriesByIdArgs {
234− pub workspace: String,
235− pub ids: Vec<String>,
236−}
237−
238−/// `search_memories`: kept memories in a workspace whose text has every
239−/// word of `query`, pinned first; with `repo_ids`, only the workspace's own
240−/// and those projects'. Services only: the caller has decided the viewer
241−/// may read the workspace's memory. Returns `Vec<Memory>`.
242−#[derive(Debug, Serialize, Deserialize)]
243−#[serde(rename_all = "camelCase")]
244−pub struct SearchMemoriesArgs {
245− pub workspace: String,
246− #[serde(default)]
247− pub query: Option<String>,
248− #[serde(default)]
249− pub repo_ids: Option<Vec<String>>,
250− #[serde(default)]
251− pub status: Option<MemoryStatus>,
252− #[serde(default)]
253− pub limit: Option<u32>,
254−}
255−
256−/// `seed_from_pulls`: decision candidates from a repository's last merged
257−/// pull requests, and convention candidates from people's reviews of its
258−/// agents' ones. Services only (the backfill). Returns `Captured`.
259−#[derive(Debug, Serialize, Deserialize)]
260−#[serde(rename_all = "camelCase")]
261−pub struct SeedFromPullsArgs {
262− pub repo_id: String,
263− /// At most 50; 20 if not given.
264− #[serde(default)]
265− pub limit: Option<u32>,
266−}
267−
268−/// `prune_doc_candidates`: after the context service has read every file
269−/// of a project, removes the project's candidates that came only from its
270−/// docs and manifests and are still waiting, unless one of `texts` (what
271−/// they suggest now) is the same memory ([`same_memory`]). Kept and
272−/// dismissed memory is never touched. Services only. Returns `Pruned`.
273−#[derive(Debug, Serialize, Deserialize)]
274−#[serde(rename_all = "camelCase")]
275−pub struct PruneDocCandidatesArgs {
276− pub workspace: String,
277− pub repo_id: String,
278− #[serde(default)]
279− pub texts: Vec<String>,
280−}
281−
282−#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
283−pub struct Pruned {
284− /// Candidates removed.
285− pub removed: u32,
286−}
287−
288−/// Memories, newest first, for listing a review queue.
289−pub type Candidates = Vec<Memory>;
290−
291−/// The `memory.changed` event: a memory was added, changed, reviewed or
292−/// forgotten. Carries no text; a subscriber asks for the memory by id. Its
293−/// `repo_id` is the project's for a project's memory, none for the
294−/// workspace's.
295−#[derive(Debug, Serialize, Deserialize)]
296−#[serde(rename_all = "camelCase")]
297−pub struct MemoryChanged {
298− pub memory_id: String,
299− pub workspace: String,
300− /// `candidate`, `kept` or `dismissed`; `deleted` once forgotten.
301− pub status: String,
302−}
303−
304−#[cfg(test)]
305−mod tests {
306− use super::*;
307−
308− #[test]
309− fn two_independent_sources_keep_a_candidate() {
310− assert!(!promotes(1, CaptureSource::Run, Some(0.9)));
311− assert!(promotes(2, CaptureSource::Run, None));
312− assert!(promotes(3, CaptureSource::Review, Some(0.1)));
313− }
314−
315− #[test]
316− fn a_confident_doc_is_kept_and_a_doubtful_one_waits() {
317− assert!(promotes(1, CaptureSource::Doc, Some(0.9)));
318− assert!(promotes(1, CaptureSource::Doc, Some(DOC_CONFIDENCE)));
319− assert!(!promotes(1, CaptureSource::Doc, Some(0.6)));
320− assert!(!promotes(1, CaptureSource::Doc, None));
321− // Only a doc's confidence counts on its own.
322− assert!(!promotes(1, CaptureSource::Pr, Some(1.0)));
323− }
324−
325− #[test]
326− fn fingerprints_ignore_case_punctuation_and_spacing() {
327− assert_eq!(fingerprint("Use pnpm, never npm!"), "use pnpm never npm");
328− assert_eq!(fingerprint("use pnpm never NPM"), fingerprint("Use pnpm; never npm."));
329− assert_ne!(fingerprint("use pnpm"), fingerprint("use npm"));
330− }
331−
332− #[test]
333− fn near_duplicates_are_one_memory() {
334− assert!(same_memory("Use pnpm, never npm.", "use pnpm never NPM"));
335− // A crate list that grew, and the same fact without the list.
336− let before = "g1t is a Cargo workspace (apps/api, crates/*, services/actions, services/billing); `cargo test` runs its tests.";
337− let after = "g1t is a Cargo workspace (apps/api, crates/*, services/actions, services/billing, services/work); `cargo test` runs its tests.";
338− assert!(same_memory(before, after));
339− assert!(same_memory(before, "g1t is a Cargo workspace (apps/*, crates/*, services/*); `cargo test` runs its tests."));
340− // One cut short, inside the whole of it.
341− assert!(same_memory(
342− "Roles. Viewer, commenter, planner and approver map onto",
343− "Roles. Viewer, commenter, planner and approver map onto the five repository roles."
344− ));
345− // Different facts that share words are not.
346− assert!(!same_memory("Use pnpm to install.", "Use npm to install."));
347− assert!(!same_memory("Run cargo test in the crate you changed.", "Run npm test in the app you changed."));
348− assert!(!same_memory("use pnpm", "use pnpm in the web app and npm in the docs, which predates it"));
349− assert!(!same_memory("", "anything"));
350− }
351−}
+0−197
1−import assert from "node:assert/strict";
2−import { test } from "node:test";
3−
4−import {
5− ABILITY_LEVELS,
6− COMPUTER_ABILITIES,
7− G1T_ABILITIES,
8− abilitiesSummary,
9− allAbilities,
10− askedFor,
11− autonomyOfLevel,
12− checkMcpName,
13− checkMcpUrl,
14− connectorsWithAbilities,
15− defaultLevel,
16− findAbility,
17− integrationAbilities,
18− levelOfAutonomy,
19− maxLevel,
20− mcpAbility,
21− mcpToolName,
22− resolveAbilities,
23− withSetting,
24− withinLevel,
25−} from "./abilities.ts";
26−import { CONNECTORS } from "./connectors.ts";
27−
28−const AUTONOMY = { open_pull_requests: "alone", merge: "approval", deploy_production: "approval", edit_docs: "suggest" } as const;
29−const resolve = (over: Partial<Parameters<typeof resolveAbilities>[0]> = {}) => resolveAbilities({ connectors: CONNECTORS, abilities: null, autonomy: AUTONOMY, connected: [], ...over });
30−
31−test("defaults by kind: reads alone, writes inside g1t when asked for, sending outside asks first, restricted never and no freer than asking", () => {
32− assert.equal(defaultLevel("read"), "alone");
33− assert.equal(defaultLevel("write"), "asked");
34− assert.equal(defaultLevel("send"), "ask");
35− assert.equal(defaultLevel("restricted"), "never");
36− assert.equal(maxLevel("restricted"), "ask");
37− assert.equal(maxLevel("send"), "alone");
38− assert.ok(withinLevel("never", "ask"));
39− assert.ok(!withinLevel("alone", "ask"));
40− assert.deepEqual(ABILITY_LEVELS, ["alone", "asked", "ask", "never"]);
41−});
42−
43−test("every integration ability belongs to an available workspace connector with a provider, and its kind fits the connector's capabilities", () => {
44− const withAbilities = connectorsWithAbilities(CONNECTORS);
45− assert.ok(withAbilities.length >= 3, "Linear, Jira and Sentry at least");
46− for (const connector of withAbilities) {
47− assert.equal(connector.status, "available", `${connector.id} is connectable today`);
48− assert.ok(connector.scopes.includes("workspace"), `${connector.id} connects for a workspace`);
49− assert.ok(connector.provider, `${connector.id} has a provider the integrations service knows`);
50− const defs = integrationAbilities(connector.id);
51− const caps = connector.capabilities ?? [];
52− if (defs.some((d) => d.kind === "read")) assert.ok(caps.includes("Agents can read"), `${connector.id} says agents can read`);
53− if (defs.some((d) => d.kind === "send")) assert.ok(caps.includes("Writes back"), `${connector.id} says it writes back`);
54− for (const def of defs) {
55− assert.equal(def.id, `integration:${connector.id}:${def.id.split(":")[2]}`);
56− assert.equal(def.tools.length, 1, "one tool per integration ability");
57− }
58− }
59− assert.deepEqual(integrationAbilities("datadog"), [], "Datadog opens issues by itself: nothing for an agent to call");
60−});
61−
62−test("g1t's own are always on and the four autonomy ones keep their choices as levels", () => {
63− const sections = resolve();
64− const g1t = sections.find((s) => s.group === "g1t")!;
65− const artifacts = g1t.sources[0]!.abilities.find((a) => a.id === "g1t:artifacts")!;
66− assert.equal(artifacts.level, "alone");
67− assert.equal(artifacts.can_change, false);
68− const merge = g1t.sources[0]!.abilities.find((a) => a.id === "g1t:merge")!;
69− assert.equal(merge.level, "ask", "approval reads as Ask first");
70− assert.deepEqual(merge.choices, ["alone", "ask", "never"]);
71− const deploy = g1t.sources[0]!.abilities.find((a) => a.id === "g1t:deploy")!;
72− assert.equal(deploy.kind, "restricted");
73− assert.ok(!deploy.choices.includes("alone"), "production deploys never go alone");
74− assert.equal(levelOfAutonomy("suggest"), "ask");
75− assert.equal(autonomyOfLevel("edit_docs", "ask"), "suggest");
76− assert.equal(autonomyOfLevel("merge", "ask"), "approval");
77− assert.equal(autonomyOfLevel("merge", "never"), "never");
78− assert.equal(G1T_ABILITIES.filter((d) => d.autonomy).length, 4);
79− const computer = sections.find((s) => s.group === "computer")!;
80− const shell = computer.sources[0]!.abilities.find((a) => a.id === "computer:shell")!;
81− assert.equal(shell.status, "ready");
82− assert.equal(shell.level, "asked", "commands run when asked for, until the person says otherwise");
83− assert.deepEqual(shell.choices, ["alone", "asked", "ask", "never"]);
84− const files = computer.sources[0]!.abilities.find((a) => a.id === "computer:files")!;
85− assert.equal(files.level, "alone");
86− assert.ok(files.can_change);
87− const coming = computer.sources[0]!.abilities.filter((a) => a.id === "computer:browser" || a.id === "computer:web");
88− assert.equal(coming.length, 2);
89− assert.ok(coming.every((a) => a.status === "coming" && a.choices.length === 0 && !a.can_change), "the browser and web search are still coming");
90− assert.equal(COMPUTER_ABILITIES.length, 4);
91−});
92−
93−test("a connected integration lists its rows at their defaults; one that isn't is listed, but nothing there can be changed", () => {
94− const sections = resolve({ connected: ["linear"] });
95− const integrations = sections.find((s) => s.group === "integration")!;
96− const linear = integrations.sources.find((s) => s.id === "linear")!;
97− assert.equal(linear.connected, true);
98− assert.deepEqual(
99− linear.abilities.map((a) => [a.id, a.level, a.credentials, a.can_change]),
100− [
101− ["integration:linear:read", "alone", "workspace", true],
102− ["integration:linear:import", "asked", "workspace", true],
103− ["integration:linear:comment", "ask", "workspace", true],
104− ],
105− );
106− assert.equal(linear.abilities[0]!.personal_available, false, "Linear's personal side is still coming");
107− const jira = integrations.sources.find((s) => s.id === "jira")!;
108− assert.equal(jira.connected, false);
109− assert.ok(jira.abilities.every((a) => !a.can_change));
110− assert.equal(integrations.sources[0]!.id, "linear", "connected first");
111− assert.ok(!integrations.sources.some((s) => s.id === "webhooks" || s.id === "anthropic"), "nothing to call, not connected: not listed");
112− const withDatadog = resolve({ connected: ["datadog"] }).find((s) => s.group === "integration")!;
113− const datadog = withDatadog.sources.find((s) => s.id === "datadog")!;
114− assert.equal(datadog.abilities.length, 0);
115− assert.match(datadog.note ?? "", /Opens issues by itself/);
116−});
117−
118−test("a setting moves a level within its kind's limit, and is dropped when it is the default again", () => {
119− let abilities = withSetting(null, "integration:linear:comment", { level: "alone" });
120− assert.deepEqual(abilities.settings, { "integration:linear:comment": { level: "alone" } });
121− const sections = resolve({ connected: ["linear"], abilities });
122− assert.equal(findAbility(sections, "integration:linear:comment")!.ability.level, "alone");
123− abilities = withSetting(abilities, "integration:linear:comment", { credentials: "asker" });
124− assert.deepEqual(abilities.settings["integration:linear:comment"], { level: "alone", credentials: "asker" });
125− abilities = withSetting(abilities, "integration:linear:comment", { level: null, credentials: null });
126− assert.deepEqual(abilities.settings, {}, "nothing kept once everything is the default");
127− // A restricted ability set freer than Ask is held at Ask.
128− const held = resolveAbilities({ connectors: CONNECTORS, abilities: null, autonomy: { ...AUTONOMY, deploy_production: "approval" }, connected: [] });
129− assert.equal(findAbility(held, "g1t:deploy")!.ability.max, "ask");
130−});
131−
132−test("a personal agent runs on the asker's connections unless an owner chose the workspace's", () => {
133− const sections = resolve({ connected: ["linear"], personal: true });
134− assert.equal(findAbility(sections, "integration:linear:read")!.ability.credentials, "asker");
135− const chosen = resolve({ connected: ["linear"], personal: true, abilities: withSetting(null, "integration:linear:read", { credentials: "workspace" }) });
136− assert.equal(findAbility(chosen, "integration:linear:read")!.ability.credentials, "workspace");
137−});
138−
139−test("MCP servers: each listed tool is a row, a write unless the server says it reads, offered under <server>__<tool>", () => {
140− const server = {
141− id: "mcp_1",
142− name: "weather",
143− url: "https://mcp.example.com/",
144− tools: [
145− { name: "get_forecast", description: "Today's forecast", kind: "read" as const, input_schema: { type: "object", properties: {} } },
146− { name: "set_alert", description: "", kind: "write" as const, input_schema: { type: "object", properties: {} } },
147− ],
148− added_by: "ana",
149− added_at: "2026-10-10T00:00:00.000Z",
150− checked_at: null,
151− problem: null,
152− };
153− const sections = resolve({ abilities: { settings: {}, mcp_servers: [server] } });
154− const mcp = sections.find((s) => s.group === "mcp")!;
155− assert.equal(mcp.sources.length, 1);
156− const [read, write] = mcp.sources[0]!.abilities;
157− assert.equal(read!.level, "alone");
158− assert.equal(write!.level, "ask", "a write outside g1t asks first");
159− assert.equal(write!.kind, "send");
160− assert.deepEqual(read!.tools, ["weather__get_forecast"]);
161− assert.equal(mcpToolName("My Server", "do.thing"), "my_server__do_thing");
162− assert.equal(mcpAbility(server, server.tools[1]!).id, "mcp:mcp_1:set_alert");
163−});
164−
165−test("an MCP server's address is HTTPS on a public host, never local, an address or g1t's own", () => {
166− assert.equal(checkMcpUrl("https://mcp.example.com/sse").ok, true);
167− assert.equal(checkMcpUrl("http://mcp.example.com/").ok, false);
168− assert.equal(checkMcpUrl("https://localhost:3000/").ok, false);
169− assert.equal(checkMcpUrl("https://10.0.0.5/").ok, false);
170− assert.equal(checkMcpUrl("https://[::1]/").ok, false);
171− assert.equal(checkMcpUrl("https://mcp.internal/").ok, false);
172− assert.equal(checkMcpUrl("https://api.g1t.sh/mcp").ok, false);
173− assert.equal(checkMcpUrl("https://user:pw@mcp.example.com/").ok, false);
174− assert.equal(checkMcpUrl("not a url").ok, false);
175− assert.equal(checkMcpName("Weather").ok, true);
176− assert.equal(checkMcpName("a").ok, false);
177− assert.equal(checkMcpName("bad name").ok, false);
178−});
179−
180−test("alone when asked for it: the item's key or the ability's name in what the person said", () => {
181− assert.ok(askedFor("Can you comment on ENG-42 with the test plan?", ["ENG-42", "comment"]));
182− assert.ok(askedFor("what's eng-42 about", ["ENG-42"]), "any case");
183− assert.ok(!askedFor("Summarise the thread", ["ENG-42", "import"]));
184− assert.ok(!askedFor("", ["ENG-42"]));
185−});
186−
187−test("the summary says what it does alone, when asked, after asking, and never", () => {
188− const abilities = withSetting(withSetting(null, "integration:linear:comment", { level: "never" }), "integration:sentry:resolve", { level: "alone" });
189− const sections = resolve({ connected: ["linear", "sentry"], abilities });
190− const summary = abilitiesSummary(sections);
191− assert.equal(
192− summary,
193− "Can open pull requests, change files on its computer, read issues in Linear, read issues in Sentry and resolve issues in Sentry on its own; runs commands on its computer, imports issues in Linear and imports issues in Sentry when asked for it; asks before merging, deploying to production, editing docs and commenting in Sentry; never comments in Linear.",
194− );
195− assert.equal(allAbilities(sections).filter((a) => a.group === "integration" && a.can_change).length, 7);
196− assert.match(abilitiesSummary(resolve()), /open pull requests and change files on its computer on its own/);
197−});
+0−677
1−/**
2− * An agent's abilities (docs.g1t.sh/guides/agent-abilities/): what it can
3− * do, in groups, and for each whether it does it on its own, only when the
4− * person asked for it, after asking first, or never.
5− *
6− * - **g1t's own** (artifacts, chat, code, issues and pull requests, files,
7− * colleagues) are always on, within the access of the person it acts
8− * for. The four that write code and docs keep the agent's `autonomy`
9− * choices, shown here as levels.
10− * - **Its computer**: a shell and files on a computer of its own, which
11− * it uses in sessions (docs.g1t.sh/guides/agents/, "Its computer"); a
12− * browser and web search are coming.
13− * - **Integrations**: every connector the workspace has connected lists
14− * what an agent can do through it, one row per action, from the
15− * connector catalog (./connectors.ts) and what the integrations service
16− * does today. Each row says whose connection it runs on: the
17− * workspace's, or the asking person's own.
18− * - **MCP servers** an owner adds: each tool the server lists is a row. A
19− * tool that doesn't say it only reads is treated as a write.
20− *
21− * Levels default by what a thing does: reading is alone; writing inside
22− * g1t is alone when the person asked for it; sending outside g1t asks
23− * first; purchases, credentials and permission changes are never, and
24− * can't be raised above asking.
25− *
26− * The agents service enforces these in code when it offers tools and when
27− * one is called; the Abilities tab and the agents service both resolve an
28− * agent's choices with `resolveAbilities`, handing it the connector catalog
29− * (./connectors.ts `CONNECTORS`): no value imports here, so services test
30− * it under Node as it is. Wire shapes are snake_case.
31− */
32−import type { Connector } from "./connectors";
33−import type { AgentAutonomy } from "./workspace-agents";
34−
35−/** Alone; alone when the asker asked for it; ask first; never. */
36−export type AbilityLevel = "alone" | "asked" | "ask" | "never";
37−
38−export const ABILITY_LEVELS: readonly AbilityLevel[] = ["alone", "asked", "ask", "never"];
39−
40−export const ABILITY_LEVEL_LABELS: Record<AbilityLevel, string> = {
41− alone: "Alone",
42− asked: "Alone when asked for it",
43− ask: "Ask first",
44− never: "Never",
45−};
46−
47−/** How much a level lets through: a lower number is freer. */
48−const ORDER: Record<AbilityLevel, number> = { alone: 0, asked: 1, ask: 2, never: 3 };
49−
50−export type AbilityGroup = "g1t" | "computer" | "integration" | "mcp";
51−
52−/**
53− * What an ability does: `read`, `write` (inside g1t), `send` (something
54− * leaves g1t: a comment, a message, a change in another system) or
55− * `restricted` (purchases, credentials, permission changes).
56− */
57−export type AbilityKind = "read" | "write" | "send" | "restricted";
58−
59−/** Whose connection an integration ability runs on. */
60−export type AbilityCredentials = "workspace" | "asker";
61−
62−export type AbilityStatus = "ready" | "coming";
63−
64−/** The level a kind starts at. */
65−export function defaultLevel(kind: AbilityKind): AbilityLevel {
66− switch (kind) {
67− case "read":
68− return "alone";
69− case "write":
70− return "asked";
71− case "send":
72− return "ask";
73− case "restricted":
74− return "never";
75− }
76−}
77−
78−/** The freest level a kind may be set to: restricted things never go above asking. */
79−export function maxLevel(kind: AbilityKind): AbilityLevel {
80− return kind === "restricted" ? "ask" : "alone";
81−}
82−
83−/** Whether `level` is within `max`: no freer than it. */
84−export function withinLevel(level: AbilityLevel, max: AbilityLevel): boolean {
85− return ORDER[level] >= ORDER[max];
86−}
87−
88−/** One ability as the catalog defines it. */
89−export type AbilityDef = {
90− /** `g1t:artifacts`, `integration:linear:read`, `mcp:<server>:<tool>`, `computer:shell`. */
91− id: string;
92− group: AbilityGroup;
93− label: string;
94− /** What it does, in a line. */
95− about: string;
96− kind: AbilityKind;
97− /** The agent tools it offers (the agents service's names), for the row's chips. */
98− tools: string[];
99− status: AbilityStatus;
100− /**
101− * The agent's `autonomy` field it reads and writes, for g1t's own code
102− * and docs abilities, with the levels that field allows.
103− */
104− autonomy?: { key: keyof AgentAutonomy; levels: AbilityLevel[] } | null;
105− /** The level it starts at, when that is not what its kind would say. */
106− default?: AbilityLevel | null;
107−};
108−
109−/** What an agent keeps for one ability. Absent fields mean the default. */
110−export type AbilitySetting = { level?: AbilityLevel | null; credentials?: AbilityCredentials | null };
111−
112−/** A tool an MCP server lists, as it was found. */
113−export type McpTool = {
114− name: string;
115− description: string;
116− /** `read` when the server says the tool only reads; `write` otherwise, which is also what an unknown tool is. */
117− kind: "read" | "write";
118− /** Its arguments, as the server describes them (JSON Schema); what the agent is offered. */
119− input_schema: Record<string, unknown>;
120−};
121−
122−/** An MCP server an owner added to one agent. */
123−export type McpServer = {
124− /** `mcp_…`, stable: ability ids are made from it. */
125− id: string;
126− /** Lowercase letters, digits and hyphens: the tools are offered as `<name>__<tool>`. */
127− name: string;
128− /** Its HTTPS address; the host is checked when it is added. */
129− url: string;
130− tools: McpTool[];
131− added_by: string;
132− /** RFC 3339. */
133− added_at: string;
134− /** When its tools were last listed, and what went wrong if they couldn't be. */
135− checked_at: string | null;
136− problem: string | null;
137−};
138−
139−/** What an agent's definition keeps about its abilities. */
140−export type AgentAbilities = {
141− /** By ability id: only what differs from the default is kept. */
142− settings: Record<string, AbilitySetting>;
143− mcp_servers: McpServer[];
144−};
145−
146−export const EMPTY_ABILITIES: AgentAbilities = { settings: {}, mcp_servers: [] };
147−
148−/** What a change to an agent sends for its abilities: the settings, whole. MCP servers have their own calls. */
149−export type AgentAbilitiesChange = { settings: Record<string, AbilitySetting> };
150−
151−/** The most MCP servers one agent has, and the most tools one server lists. */
152−export const MAX_MCP_SERVERS = 10;
153−export const MAX_MCP_TOOLS = 40;
154−
155−/** An ability as one agent has it now. */
156−export type Ability = AbilityDef & {
157− level: AbilityLevel;
158− /** The freest level it may be set to. */
159− max: AbilityLevel;
160− /** The levels it may be set to, freest first. */
161− choices: AbilityLevel[];
162− /** Whether the level can be changed at all: g1t's own reads are always on. */
163− can_change: boolean;
164− /** For an integration: whose connection it runs on. Null elsewhere. */
165− credentials: AbilityCredentials | null;
166− /** Whether the asking person could connect it themselves (the connector has a personal side that is available). */
167− personal_available: boolean;
168−};
169−
170−/** One source of abilities: a connector, an MCP server, or g1t itself. */
171−export type AbilitySource = {
172− id: string;
173− name: string;
174− /** For an integration or an MCP server: whether the workspace has it. */
175− connected: boolean;
176− /** Where it is connected or managed; null when there is nowhere. */
177− href: string | null;
178− /** A line about it when it has no abilities, or something is wrong with it. */
179− note: string | null;
180− abilities: Ability[];
181−};
182−
183−export type AbilitySection = {
184− group: AbilityGroup;
185− title: string;
186− about: string;
187− sources: AbilitySource[];
188−};
189−
190−/** What a level means for an agent, in its own words. */
191−export function levelWords(level: AbilityLevel): string {
192− switch (level) {
193− case "alone":
194− return "on its own";
195− case "asked":
196− return "only when the person asked for that";
197− case "ask":
198− return "after asking first";
199− case "never":
200− return "never";
201− }
202−}
203−
204−// g1t's own ---------------------------------------------------------------
205−
206−/** g1t's own abilities: always on, within the asker's access; the four autonomy ones keep their choices. */
207−export const G1T_ABILITIES: AbilityDef[] = [
208− {
209− id: "g1t:artifacts",
210− group: "g1t",
211− label: "Artifacts",
212− about: "Search, read, write, edit and share the workspace's artifacts, where the person it acts for can.",
213− kind: "read",
214− tools: ["search_artifacts", "read_artifact", "list_spaces", "stale_artifacts", "create_artifact", "edit_artifact", "share_artifact"],
215− status: "ready",
216− },
217− {
218− id: "g1t:chat",
219− group: "g1t",
220− label: "Chat",
221− about: "Search messages and read threads everyone in the conversation can read, and the roster.",
222− kind: "read",
223− tools: ["search_messages", "read_thread", "workspace_roster"],
224− status: "ready",
225− },
226− {
227− id: "g1t:code",
228− group: "g1t",
229− label: "Code",
230− about: "Read files and search code in repositories everyone in the conversation can read.",
231− kind: "read",
232− tools: ["list_repositories", "search_code", "read_file"],
233− status: "ready",
234− },
235− {
236− id: "g1t:issues",
237− group: "g1t",
238− label: "Issues and pull requests",
239− about: "Read issues and pull requests, draft issues as cards, and comment or review on behalf of the person who asked.",
240− kind: "read",
241− tools: ["list_issues", "get_issue", "get_pull", "recent_activity", "draft_issue", "comment", "review_pull"],
242− status: "ready",
243− },
244− {
245− id: "g1t:pulls",
246− group: "g1t",
247− label: "Open pull requests",
248− about: "Open pull requests from its sessions. Rules, protected branches and required checks still apply.",
249− kind: "write",
250− tools: [],
251− status: "ready",
252− autonomy: { key: "open_pull_requests", levels: ["alone", "ask"] },
253− },
254− {
255− id: "g1t:merge",
256− group: "g1t",
257− label: "Merge",
258− about: "Merge pull requests that have what the branch requires.",
259− kind: "write",
260− tools: [],
261− status: "ready",
262− autonomy: { key: "merge", levels: ["alone", "ask", "never"] },
263− },
264− {
265− id: "g1t:deploy",
266− group: "g1t",
267− label: "Deploy to production",
268− about: "Deploy a project's production environment.",
269− kind: "restricted",
270− tools: [],
271− status: "ready",
272− autonomy: { key: "deploy_production", levels: ["ask", "never"] },
273− },
274− {
275− id: "g1t:docs",
276− group: "g1t",
277− label: "Edit docs",
278− about: "Change docs directly, or leave each change as a suggestion someone accepts.",
279− kind: "write",
280− tools: ["edit_artifact"],
281− status: "ready",
282− autonomy: { key: "edit_docs", levels: ["alone", "ask"] },
283− },
284− {
285− id: "g1t:files",
286− group: "g1t",
287− label: "Make files",
288− about: "Make PDFs, Word documents, spreadsheets, CSVs and Markdown files, kept with a doc the person can open.",
289− kind: "write",
290− tools: ["make_file"],
291− status: "ready",
292− },
293− {
294− id: "g1t:colleagues",
295− group: "g1t",
296− label: "Colleagues and memory",
297− about: "Ask a colleague, hand work off, bring one into a session, use its subagents, start sessions, and remember facts.",
298− kind: "write",
299− tools: ["ask_colleague", "hand_off", "bring_in", "use_subagent", "start_session", "post_update", "remember", "forget", "use_skill"],
300− status: "ready",
301− },
302−];
303−
304−/** `autonomy` values as levels, and back. */
305−export function levelOfAutonomy(value: string): AbilityLevel {
306− switch (value) {
307− case "alone":
308− return "alone";
309− case "never":
310− return "never";
311− default:
312− // `approval` and `suggest` both mean a person acts first.
313− return "ask";
314− }
315−}
316−
317−export function autonomyOfLevel(key: keyof AgentAutonomy, level: AbilityLevel): string {
318− if (level === "alone") return "alone";
319− if (level === "never") return "never";
320− return key === "edit_docs" ? "suggest" : "approval";
321−}
322−
323−// Its computer -----------------------------------------------------------
324−
325−/**
326− * The agent's own computer: a persistent home on g1t cloud that its
327− * sessions use and that sleeps when idle. Running commands is a write it
328− * does alone when asked for it; reading and writing its own files is alone,
329− * since nothing leaves the computer. Chat replies never use it.
330− */
331−export const COMPUTER_ABILITIES: AbilityDef[] = [
332− { id: "computer:shell", group: "computer", label: "Shell", about: "Run commands on its own computer, with a persistent home, from its sessions.", kind: "write", tools: ["run_command"], status: "ready" },
333− { id: "computer:files", group: "computer", label: "Files", about: "Read and write files in its computer's home.", kind: "write", tools: ["computer_read_file", "computer_write_file"], status: "ready", default: "alone" },
334− { id: "computer:browser", group: "computer", label: "Browser", about: "Open sites in a browser of its own, signed in where you let it be.", kind: "send", tools: [], status: "coming" },
335− { id: "computer:web", group: "computer", label: "Web search", about: "Search and read the open web, as its team's web access allows.", kind: "read", tools: [], status: "coming" },
336−];
337−
338−/** The tools the computer offers, by ability id: what a session offers once the ability is not Never. */
339−export const COMPUTER_TOOLS: Record<string, string[]> = Object.fromEntries(COMPUTER_ABILITIES.filter((def) => def.status === "ready").map((def) => [def.id, def.tools]));
340−
341−// Integrations -----------------------------------------------------------
342−
343−/**
344− * What an agent can do through each connector today, as the integrations
345− * service does it: look an item up by its key or address, open an issue in
346− * g1t from it, comment on it there, and (Sentry) mark it resolved. A
347− * connector not named here has nothing an agent calls: Datadog and the
348− * alerts webhook open issues by themselves; GitHub imports repositories;
349− * model providers run models.
350− */
351−const INTEGRATION_ACTIONS: Record<string, { action: string; label: string; about: string; kind: AbilityKind; tool: string }[]> = {
352− linear: [
353− { action: "read", label: "Read issues", about: "Look up an issue by its key (ENG-42) or address: its title, status and description.", kind: "read", tool: "lookup_outside" },
354− { action: "import", label: "Import issues", about: "Open an issue in a repository here from a Linear issue, linked back to it.", kind: "write", tool: "import_outside" },
355− { action: "comment", label: "Comment", about: "Comment on an issue in Linear, as the workspace's connection, naming the person it acts for.", kind: "send", tool: "act_outside" },
356− ],
357− jira: [
358− { action: "read", label: "Read tickets", about: "Look up a ticket by its key (TECH-1234) or address: its summary, status and description.", kind: "read", tool: "lookup_outside" },
359− { action: "import", label: "Import tickets", about: "Open an issue in a repository here from a Jira ticket, linked back to it.", kind: "write", tool: "import_outside" },
360− { action: "comment", label: "Comment", about: "Comment on a ticket in Jira, as the workspace's connection, naming the person it acts for.", kind: "send", tool: "act_outside" },
361− ],
362− sentry: [
363− { action: "read", label: "Read issues", about: "Look up a Sentry issue by its address: its title, status and latest stack trace.", kind: "read", tool: "lookup_outside" },
364− { action: "import", label: "Import issues", about: "Open an issue in a repository here from a Sentry issue, linked back to it.", kind: "write", tool: "import_outside" },
365− { action: "comment", label: "Comment", about: "Leave a note on a Sentry issue, as the workspace's connection.", kind: "send", tool: "act_outside" },
366− { action: "resolve", label: "Resolve issues", about: "Mark a Sentry issue resolved, with a note.", kind: "send", tool: "act_outside" },
367− ],
368−};
369−
370−/** What a connected connector with nothing for an agent to call says. */
371−function nothingToCall(connector: Connector): string {
372− switch (connector.category) {
373− case "monitoring":
374− return "Opens issues by itself when something fires. Nothing for an agent to call.";
375− case "ai":
376− return "Runs models. Which ones an agent uses is set under Models on its profile.";
377− case "code":
378− return "Imports repositories. Agents read the repositories themselves.";
379− default:
380− return "Nothing for an agent to call yet.";
381− }
382−}
383−
384−/** The abilities one connector gives an agent; empty when it has none. */
385−export function integrationAbilities(connectorId: string): AbilityDef[] {
386− return (INTEGRATION_ACTIONS[connectorId] ?? []).map((row) => ({
387− id: `integration:${connectorId}:${row.action}`,
388− group: "integration",
389− label: row.label,
390− about: row.about,
391− kind: row.kind,
392− tools: [row.tool],
393− status: "ready",
394− }));
395−}
396−
397−/** The connectors among `connectors` that give agents abilities, in catalog order. */
398−export function connectorsWithAbilities(connectors: Connector[]): Connector[] {
399− return connectors.filter((connector) => (INTEGRATION_ACTIONS[connector.id] ?? []).length > 0);
400−}
401−
402−// MCP servers ------------------------------------------------------------
403−
404−/** An MCP tool as an ability of its server. */
405−export function mcpAbility(server: Pick<McpServer, "id" | "name">, tool: McpTool): AbilityDef {
406− return {
407− id: `mcp:${server.id}:${tool.name}`,
408− group: "mcp",
409− label: tool.name,
410− about: tool.description || (tool.kind === "read" ? "Reads, as the server says." : "The server doesn't say it only reads, so it counts as a write."),
411− // Anything an MCP server changes happens outside g1t.
412− kind: tool.kind === "read" ? "read" : "send",
413− tools: [mcpToolName(server.name, tool.name)],
414− status: "ready",
415− };
416−}
417−
418−/** The tool name an agent calls an MCP tool by: `<server>__<tool>`, in the characters the model API allows. */
419−export function mcpToolName(server: string, tool: string): string {
420− const clean = (text: string) => text.toLowerCase().replace(/[^a-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
421− return `${clean(server)}__${clean(tool)}`.slice(0, 64);
422−}
423−
424−/** An MCP server's name as kept: lowercase letters, digits and hyphens, 2 to 32. */
425−export function checkMcpName(value: unknown): { ok: true; value: string } | { ok: false; message: string } {
426− const name = typeof value === "string" ? value.trim().toLowerCase() : "";
427− if (!/^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){1,31}$/.test(name)) return { ok: false, message: "A server's name is 2 to 32 lowercase letters, digits and single hyphens." };
428− return { ok: true, value: name };
429−}
430−
431−/**
432− * An MCP server's address, checked: HTTPS, a public host name (not an
433− * address, not local, not g1t's own), no sign-in in the address.
434− */
435−export function checkMcpUrl(value: unknown): { ok: true; value: string } | { ok: false; message: string } {
436− const text = typeof value === "string" ? value.trim() : "";
437− if (!text || text.length > 500) return { ok: false, message: "Give the server's HTTPS address." };
438− let url: URL;
439− try {
440− url = new URL(text);
441− } catch {
442− return { ok: false, message: "That isn't an address." };
443− }
444− if (url.protocol !== "https:") return { ok: false, message: "An MCP server is reached over HTTPS." };
445− if (url.username || url.password) return { ok: false, message: "Don't put a sign-in in the address." };
446− const host = url.hostname.toLowerCase();
447− const ip = /^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.startsWith("[") || host.includes(":");
448− const local = host === "localhost" || /\.(local|localhost|internal|lan|home|arpa)$/.test(host) || !host.includes(".");
449− if (ip || local) return { ok: false, message: "Name the server by a public host name, not an address or a local name." };
450− if (/(^|\.)(g1t\.sh|g1tusercontent\.com)$/.test(host)) return { ok: false, message: "g1t's own addresses aren't MCP servers to add here." };
451− url.hash = "";
452− return { ok: true, value: url.toString() };
453−}
454−
455−// Resolving --------------------------------------------------------------
456−
457−export type ResolveInput = {
458− /** The connector catalog (./connectors.ts `CONNECTORS`). */
459− connectors: Connector[];
460− abilities: AgentAbilities | null | undefined;
461− autonomy: AgentAutonomy;
462− /** Connector ids the workspace has connected. */
463− connected: string[];
464− /** Where a connector is set up or managed, by id; absent: nowhere to link. */
465− hrefs?: Record<string, string | null>;
466− /** Whether the agent is a member's personal one: then the workspace's connection isn't its to use unless an owner chose it. */
467− personal?: boolean;
468−};
469−
470−function settingOf(input: ResolveInput, id: string): AbilitySetting {
471− return input.abilities?.settings?.[id] ?? {};
472−}
473−
474−/** A definition as one agent has it: its level, within what the kind allows. */
475−export function resolveOne(def: AbilityDef, input: ResolveInput, connected = true): Ability {
476− const setting = settingOf(input, def.id);
477− const max = maxLevel(def.kind);
478− let level: AbilityLevel;
479− let choices: AbilityLevel[];
480− let canChange: boolean;
481− if (def.autonomy) {
482− level = levelOfAutonomy(input.autonomy[def.autonomy.key]);
483− choices = def.autonomy.levels;
484− canChange = true;
485− } else if (def.group === "g1t") {
486− level = "alone";
487− choices = ["alone"];
488− canChange = false;
489− } else if (def.status === "coming") {
490− level = defaultLevel(def.kind);
491− choices = [];
492− canChange = false;
493− } else {
494− const wanted = setting.level ?? def.default ?? defaultLevel(def.kind);
495− level = withinLevel(wanted, max) ? wanted : max;
496− choices = ABILITY_LEVELS.filter((l) => withinLevel(l, max));
497− canChange = true;
498− }
499− const integration = def.group === "integration";
500− const connectorId = integration ? def.id.split(":")[1]! : null;
501− const connector = connectorId ? input.connectors.find((c) => c.id === connectorId) : null;
502− const personalAvailable = !!connector && connector.scopes.includes("personal") && (connector.personal?.status ?? connector.status) === "available";
503− return {
504− ...def,
505− level,
506− max,
507− choices,
508− can_change: canChange && (def.group !== "integration" || connected),
509− credentials: integration ? (setting.credentials ?? (input.personal ? "asker" : "workspace")) : null,
510− personal_available: personalAvailable,
511− };
512−}
513−
514−/** The abilities an agent has, in sections, as the tab shows them and the service enforces them. */
515−export function resolveAbilities(input: ResolveInput): AbilitySection[] {
516− const connected = new Set(input.connected.map((id) => id.toLowerCase()));
517− const hrefs = input.hrefs ?? {};
518− const sections: AbilitySection[] = [
519− {
520− group: "g1t",
521− title: "g1t",
522− about: "Always on, within the access of the person it acts for. Code and docs keep the choices below.",
523− sources: [{ id: "g1t", name: "g1t", connected: true, href: null, note: null, abilities: G1T_ABILITIES.map((def) => resolveOne(def, input)) }],
524− },
525− {
526− group: "computer",
527− title: "Its computer",
528− about: "A computer of its own on g1t cloud, used in its sessions: a shell and files now, a browser and web search coming. It sleeps when idle and keeps its home.",
529− sources: [{ id: "computer", name: "Computer", connected: true, href: null, note: null, abilities: COMPUTER_ABILITIES.map((def) => resolveOne(def, input)) }],
530− },
531− ];
532− // Every connected connector, then the ones with abilities that aren't connected yet.
533− const sources: AbilitySource[] = [];
534− for (const connector of input.connectors) {
535− if (connector.status !== "available" || !connector.scopes.includes("workspace")) continue;
536− const is = connected.has(connector.id);
537− const defs = integrationAbilities(connector.id);
538− if (!is && !defs.length) continue;
539− // Nothing an agent calls: not worth a row unless it is connected.
540− if (!defs.length && (connector.id === "webhooks" || connector.id === "ai-gateway")) continue;
541− sources.push({
542− id: connector.id,
543− name: connector.name,
544− connected: is,
545− href: hrefs[connector.id] ?? null,
546− note: defs.length ? null : nothingToCall(connector),
547− abilities: defs.map((def) => resolveOne(def, input, is)),
548− });
549− }
550− sources.sort((a, b) => Number(b.connected) - Number(a.connected));
551− sections.push({ group: "integration", title: "Integrations", about: "What it can do through what the workspace has connected, one row per action, and whose connection each runs on.", sources });
552− const servers = input.abilities?.mcp_servers ?? [];
553− sections.push({
554− group: "mcp",
555− title: "MCP servers",
556− about: "Servers an owner adds. Each tool the server lists is a row; a tool that doesn't say it only reads counts as a write.",
557− sources: servers.map((server) => ({
558− id: server.id,
559− name: server.name,
560− connected: true,
561− href: server.url,
562− note: server.problem ?? (server.tools.length ? null : "It listed no tools."),
563− abilities: server.tools.slice(0, MAX_MCP_TOOLS).map((tool) => resolveOne(mcpAbility(server, tool), input)),
564− })),
565− });
566− return sections;
567−}
568−
569−/** Every ability in the sections, flat. */
570−export function allAbilities(sections: AbilitySection[]): Ability[] {
571− return sections.flatMap((section) => section.sources.flatMap((source) => source.abilities));
572−}
573−
574−/** The ability by id, among the sections; null when there is none. */
575−export function findAbility(sections: AbilitySection[], id: string): { ability: Ability; source: AbilitySource } | null {
576− for (const section of sections) {
577− for (const source of section.sources) {
578− const ability = source.abilities.find((a) => a.id === id);
579− if (ability) return { ability, source };
580− }
581− }
582− return null;
583−}
584−
585−/**
586− * Whether what the person said asked for this: the item's key (ENG-42), or
587− * the ability's own name, appears in it. For a level of `asked`.
588− */
589−export function askedFor(said: string, keys: string[]): boolean {
590− const text = said.toLowerCase();
591− if (!text.trim()) return false;
592− return keys.some((key) => {
593− const needle = key.trim().toLowerCase();
594− return needle.length >= 2 && text.includes(needle);
595− });
596−}
597−
598−/** A list in words: "a", "a and b", "a, b and c". */
599−function list(items: string[]): string {
600− if (items.length <= 1) return items.join("");
601− return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
602−}
603−
604−const lower = (text: string) => text.charAt(0).toLowerCase() + text.slice(1);
605−
606−/**
607− * The agent's abilities in a sentence or two: "Can read Linear issues and
608− * open pull requests on its own; imports Linear issues when asked for it;
609− * asks before commenting in Linear and merging; never deploys to
610− * production." Only what is connected and ready counts.
611− */
612−export function abilitiesSummary(sections: AbilitySection[]): string {
613− const by: Record<AbilityLevel, string[]> = { alone: [], asked: [], ask: [], never: [] };
614− for (const section of sections) {
615− for (const source of section.sources) {
616− if (!source.connected) continue;
617− for (const ability of source.abilities) {
618− if (ability.status !== "ready") continue;
619− // g1t's always-on reads go without saying; its choices and everything outside are the point.
620− if (section.group === "g1t" && !ability.autonomy) continue;
621− const what =
622− section.group === "g1t"
623− ? lower(ability.label)
624− : section.group === "computer"
625− ? computerWords(ability.id)
626− : section.group === "mcp"
627− ? `call ${ability.label} on ${source.name}`
628− : `${lower(ability.label)} in ${source.name}`;
629− by[ability.level].push(what);
630− }
631− }
632− }
633− const parts: string[] = [];
634− if (by.alone.length) parts.push(`can ${list(by.alone)} on its own`);
635− if (by.asked.length) parts.push(`${list(by.asked.map(verb))} when asked for it`);
636− if (by.ask.length) parts.push(`asks before ${list(by.ask.map(gerund))}`);
637− if (by.never.length) parts.push(`never ${list(by.never.map(verb))}`);
638− if (!parts.length) return "Reads code, chat, issues and artifacts within the asker's access; nothing outside g1t yet.";
639− const text = parts.join("; ");
640− return `${text.charAt(0).toUpperCase()}${text.slice(1)}.`;
641−}
642−
643−/** A computer ability as what the agent does with it. */
644−function computerWords(id: string): string {
645− return id === "computer:shell" ? "run commands on its computer" : "change files on its computer";
646−}
647−
648−/** "read issues in Linear" → "reads issues in Linear". */
649−function verb(what: string): string {
650− const [first, ...rest] = what.split(" ");
651− if (!first) return what;
652− const third = first.endsWith("s") ? first : first.endsWith("y") && !/[aeiou]y$/.test(first) ? `${first.slice(0, -1)}ies` : `${first}s`;
653− return [third, ...rest].join(" ");
654−}
655−
656−/** "comment in Linear" → "commenting in Linear". */
657−function gerund(what: string): string {
658− const [first, ...rest] = what.split(" ");
659− if (!first) return what;
660− const ing = first.endsWith("e") && first !== "be" ? `${first.slice(0, -1)}ing` : first.endsWith("ing") ? first : `${first}ing`;
661− return [ing, ...rest].join(" ");
662−}
663−
664−/** The settings with one ability's level or credentials changed, keeping only what differs from the default. */
665−export function withSetting(abilities: AgentAbilities | null | undefined, id: string, change: AbilitySetting): AgentAbilities {
666− const base = abilities ?? EMPTY_ABILITIES;
667− const current = { ...(base.settings[id] ?? {}) };
668− if (change.level !== undefined) current.level = change.level;
669− if (change.credentials !== undefined) current.credentials = change.credentials;
670− const next = { ...base.settings };
671− const kept: AbilitySetting = {};
672− if (current.level) kept.level = current.level;
673− if (current.credentials) kept.credentials = current.credentials;
674− if (Object.keys(kept).length) next[id] = kept;
675− else delete next[id];
676− return { settings: next, mcp_servers: base.mcp_servers ?? [] };
677−}
+0−171
1−/**
2− * An agent's face (docs.g1t.sh/guides/agents/, "Its face"): the little bot
3− * every agent wears when it has no uploaded picture. A face is a `look`,
4− * seven choices the agent's owner can make; an agent with no look of its
5− * own wears the one drawn from its `avatar_seed`, so every agent has a
6− * stable face from the moment it is made, and keeps it through a rename.
7− *
8− * The site draws the face (apps/web/app/components/agent-face.tsx) and the
9− * agents service keeps it (the `look` column, JSON). No value imports, so
10− * services test it under Node as it is. Wire shapes are snake_case.
11− */
12−
13−export const FACE_SHAPES = ["round", "square", "squircle", "blob", "hex"] as const;
14−/** Ten named colours, each with a light and a dark rendering that read on both themes. */
15−export const FACE_COLORS = ["lavender", "mint", "peach", "sky", "pink", "lemon", "teal", "coral", "moss", "slate"] as const;
16−export const FACE_EYES = ["dots", "round", "wide", "happy", "visor", "sleepy"] as const;
17−export const FACE_MOUTHS = ["smile", "grin", "flat", "open", "none"] as const;
18−export const FACE_ANTENNAS = ["none", "single", "double", "bulb"] as const;
19−export const FACE_ACCESSORIES = ["none", "headphones", "bow", "glasses", "hat", "spark"] as const;
20−export const FACE_PATTERNS = ["none", "stripe", "dots", "gradient"] as const;
21−
22−export type FaceShape = (typeof FACE_SHAPES)[number];
23−export type FaceColor = (typeof FACE_COLORS)[number];
24−export type FaceEyes = (typeof FACE_EYES)[number];
25−export type FaceMouth = (typeof FACE_MOUTHS)[number];
26−export type FaceAntenna = (typeof FACE_ANTENNAS)[number];
27−export type FaceAccessory = (typeof FACE_ACCESSORIES)[number];
28−export type FacePattern = (typeof FACE_PATTERNS)[number];
29−
30−/** What an agent's face is made of. Every field is one of its listed choices. */
31−export type AgentLook = {
32− shape: FaceShape;
33− color: FaceColor;
34− eyes: FaceEyes;
35− mouth: FaceMouth;
36− antenna: FaceAntenna;
37− accessory: FaceAccessory;
38− pattern: FacePattern;
39−};
40−
41−/** Each part of a look and its choices, in the order the Face editor shows them. */
42−export const LOOK_OPTIONS: { [K in keyof AgentLook]: readonly AgentLook[K][] } = {
43− shape: FACE_SHAPES,
44− color: FACE_COLORS,
45− eyes: FACE_EYES,
46− mouth: FACE_MOUTHS,
47− antenna: FACE_ANTENNAS,
48− accessory: FACE_ACCESSORIES,
49− pattern: FACE_PATTERNS,
50−};
51−
52−export const LOOK_KEYS = Object.keys(LOOK_OPTIONS) as (keyof AgentLook)[];
53−
54−/** FNV-1a, 32 bits: a seed as a number. */
55−function fnv(text: string): number {
56− let h = 0x811c9dc5;
57− for (let i = 0; i < text.length; i++) {
58− h ^= text.charCodeAt(i);
59− h = Math.imul(h, 0x01000193);
60− }
61− return h >>> 0;
62−}
63−
64−/** One choice for a part, from the seed and the part's name: parts vary independently. */
65−function pick<T>(seed: string, part: string, options: readonly T[], weights?: readonly number[]): T {
66− const roll = fnv(`${part}:${seed}`) >>> 8;
67− if (!weights) return options[roll % options.length]!;
68− const total = weights.reduce((sum, w) => sum + w, 0);
69− let at = roll % total;
70− for (let i = 0; i < options.length; i++) {
71− at -= weights[i]!;
72− if (at < 0) return options[i]!;
73− }
74− return options[options.length - 1]!;
75−}
76−
77−/**
78− * The face an agent wears when it has chosen none: the same for the same
79− * seed everywhere, and different from its neighbours' as far as seven
80− * parts allow. Plainer choices (no antenna, no accessory, no pattern) are
81− * weighted so faces read as a set rather than a costume party, and `none`
82− * never lands on both the eyes' expression and the mouth.
83− */
84−export function lookFromSeed(seed: string): AgentLook {
85− const s = seed || "agent";
86− const look: AgentLook = {
87− shape: pick(s, "shape", FACE_SHAPES, [3, 2, 4, 2, 2]),
88− color: pick(s, "color", FACE_COLORS),
89− eyes: pick(s, "eyes", FACE_EYES, [3, 3, 2, 2, 2, 1]),
90− mouth: pick(s, "mouth", FACE_MOUTHS, [4, 2, 2, 2, 1]),
91− antenna: pick(s, "antenna", FACE_ANTENNAS, [4, 3, 2, 2]),
92− accessory: pick(s, "accessory", FACE_ACCESSORIES, [7, 1, 1, 1, 1, 1]),
93− pattern: pick(s, "pattern", FACE_PATTERNS, [6, 1, 1, 2]),
94− };
95− if (look.mouth === "none" && look.eyes === "visor") look.mouth = "flat";
96− return look;
97−}
98−
99−/**
100− * A look as it was sent or stored, checked: every part present and one of
101− * its choices. Null when it is not one (a service refuses it, a page draws
102− * the seed's face instead).
103− */
104−export function readLook(raw: unknown): AgentLook | null {
105− const value = typeof raw === "string" ? parse(raw) : raw;
106− if (!value || typeof value !== "object" || Array.isArray(value)) return null;
107− const given = value as Record<string, unknown>;
108− const look: Partial<AgentLook> = {};
109− for (const key of LOOK_KEYS) {
110− const options = LOOK_OPTIONS[key] as readonly string[];
111− const choice = given[key];
112− if (typeof choice !== "string" || !options.includes(choice)) return null;
113− (look as Record<string, string>)[key] = choice;
114− }
115− return look as AgentLook;
116−}
117−
118−function parse(text: string): unknown {
119− try {
120− return JSON.parse(text);
121− } catch {
122− return null;
123− }
124−}
125−
126−/** Whether two looks are the same face. */
127−export function sameLook(a: AgentLook | null | undefined, b: AgentLook | null | undefined): boolean {
128− if (!a || !b) return a == b;
129− return LOOK_KEYS.every((key) => a[key] === b[key]);
130−}
131−
132−/** The face an agent shows: its own look, else the one its seed draws. */
133−export function lookOf(agent: { look?: AgentLook | null; avatar_seed?: string | null; id?: string | null; handle?: string | null; name?: string | null }): AgentLook {
134− return agent.look ?? lookFromSeed(agent.avatar_seed || agent.id || agent.handle || agent.name || "agent");
135−}
136−
137−/**
138− * A random look, for the Shuffle button: `random` is `Math.random` unless
139− * a test hands in its own. Every part is drawn evenly, so shuffling shows
140− * the whole range.
141− */
142−export function randomLook(random: () => number = Math.random): AgentLook {
143− const draw = <T>(options: readonly T[]): T => options[Math.min(options.length - 1, Math.floor(random() * options.length))]!;
144− return {
145− shape: draw(FACE_SHAPES),
146− color: draw(FACE_COLORS),
147− eyes: draw(FACE_EYES),
148− mouth: draw(FACE_MOUTHS),
149− antenna: draw(FACE_ANTENNAS),
150− accessory: draw(FACE_ACCESSORIES),
151− pattern: draw(FACE_PATTERNS),
152− };
153−}
154−
155−/**
156− * What a personality preset suggests for a face that has not been chosen:
157− * a precise voice looks precise. Only a suggestion, applied by the Face
158− * editor when the person has made no choice; it never changes a look an
159− * agent already has.
160− */
161−export const PRESET_EXPRESSIONS: Record<string, Partial<AgentLook>> = {
162− crisp: { eyes: "dots", mouth: "flat" },
163− friendly: { eyes: "happy", mouth: "smile" },
164− socratic: { eyes: "round", mouth: "open" },
165− terse: { eyes: "visor", mouth: "flat" },
166−};
167−
168−/** The seed's face with the preset's expression laid over it, for an agent that has chosen no look. */
169−export function suggestedLook(seed: string, preset: string | null | undefined): AgentLook {
170− return { ...lookFromSeed(seed), ...(preset ? (PRESET_EXPRESSIONS[preset] ?? {}) : {}) };
171−}
+0−267
1−/**
2− * The context hub: one place to ask about a workspace. The context service
3− * keeps a **catalog** of what the workspace builds and runs (projects,
4− * their apps, APIs, packages, languages, owners, environments, integrations
5− * and docs, and how they relate), built by itself from the repositories and
6− * from projects, deployments and integrations; **search** across the
7− * catalog, docs, issues, pull requests and memory; and **scorecards** for
8− * each project. Memory itself, candidates and their review, is kept by
9− * the work service: see `memoryReviewClient`.
10− */
11−
12−import type { Memory, MemoryKind } from "./agents";
13−import type { ServiceBinding } from "./clients";
14−import type { User, Viewer } from "./identity";
15−import type { RepoPath } from "./repos";
16−import type { Result } from "./result";
17−
18−export type EntityKind =
19− | "project"
20− | "app"
21− | "api"
22− | "package"
23− | "language"
24− | "owner"
25− | "environment"
26− | "integration"
27− | "doc";
28−
29−export const ENTITY_KINDS: EntityKind[] = ["project", "app", "api", "package", "language", "owner", "environment", "integration", "doc"];
30−
31−export type RelationKind = "depends_on" | "owned_by" | "deploys_to" | "documented_by" | "exposes" | "uses";
32−
33−export type Entity = {
34− id: string;
35− workspace: string;
36− kind: EntityKind;
37− /** Unique for its kind in the workspace: a project's slug, `npm:<name>`, a username. */
38− key: string;
39− name: string;
40− summary: string | null;
41− /** The project it belongs to, by slug; null for what the workspace shares (owners, languages, integrations). */
42− project: string | null;
43− /** Whether only members see it: it comes from a private repository. */
44− private: boolean;
45− /** What it is, by kind: a package's version and dependencies, an API's routes, an app's address. */
46− data: Record<string, unknown>;
47− /** Where it was found: `scan` (the repository), `projects`, `deployments`, `integrations`. */
48− source: string;
49− /** Where to see it: a path on g1t.sh, or an address. */
50− ref: string | null;
51− /** RFC 3339. */
52− updatedAt: string;
53−};
54−
55−export type Relation = { from: string; kind: RelationKind; to: string };
56−
57−export type Catalog = {
58− entities: Entity[];
59− relations: Relation[];
60− /** When the catalog was last built from the repositories. */
61− builtAt: string | null;
62−};
63−
64−export type EntityDetail = {
65− entity: Entity;
66− /** Each relation with the entity at its other end. */
67− relations: { kind: RelationKind; direction: "out" | "in"; entity: Entity }[];
68−};
69−
70−/** What search can return: a catalog entity's kind, or one of these. */
71−export type SearchKind = EntityKind | "memory" | "issue" | "pull";
72−export const SEARCH_KINDS: SearchKind[] = [...ENTITY_KINDS, "memory", "issue", "pull"];
73−
74−export type SearchHit = {
75− kind: SearchKind;
76− id: string;
77− title: string;
78− snippet: string;
79− /** The project it is about, by slug. */
80− project: string | null;
81− /** Where to see it on g1t.sh, or an address. */
82− url: string | null;
83− /** Higher is closer; text matches score below semantic ones. */
84− score: number;
85− /** Where it came from, such as `AGENTS.md`, `review on #12`, `catalog`. */
86− source: string;
87− /** Who wrote it, for memory, issues and pull requests. */
88− by: string | null;
89− /** RFC 3339. */
90− updatedAt: string | null;
91−};
92−
93−export type SearchResult = {
94− query: string;
95− hits: SearchHit[];
96− /** `semantic` when the search index answered; `text` when it fell back to matching words. */
97− mode: "semantic" | "text";
98−};
99−
100−export type SearchOptions = { project?: string | null; kinds?: SearchKind[] | null; limit?: number | null };
101−
102−export type ScoreRule = "has_owner" | "has_readme" | "has_agents_md" | "tests_in_ci" | "production_green" | "no_secret_findings";
103−
104−export type RuleResult = {
105− rule: ScoreRule;
106− title: string;
107− /** `na` when the rule does not apply, such as production for a project that does not deploy. */
108− status: "pass" | "fail" | "na";
109− detail: string;
110− /** For a failing rule: an issue an agent can fix it from. */
111− fix: { title: string; body: string; checks: string[] } | null;
112−};
113−
114−export type Scorecard = {
115− project: string;
116− name: string;
117− repo: RepoPath;
118− passed: number;
119− /** The rules that apply. */
120− total: number;
121− rules: RuleResult[];
122−};
123−
124−export type Backfill = {
125− workspace: string;
126− status: "running" | "done" | "failed";
127− by: string;
128− projects: number;
129− done: number;
130− entities: number;
131− /** Memory candidates added, and how many of them were kept at once. */
132− candidates: number;
133− kept: number;
134− /** Things put in the search index. */
135− indexed: number;
136− error: string | null;
137− startedAt: string;
138− finishedAt: string | null;
139−};
140−
141−export type ContextStatus = {
142− backfill: Backfill | null;
143− counts: Partial<Record<EntityKind, number>>;
144− /** What the search index cost the workspace this month. */
145− usage: { month: string; tokens: number; costMicros: number };
146− /** Whether semantic search is available: set up, and on a paid plan or the trial. Text search always is. */
147− semantic: boolean;
148−};
149−
150−export type RunContext = {
151− /** The Context section for an agent's prompt; null when there is nothing to say. */
152− text: string | null;
153− /** What it drew on, such as `catalog:web`, `memory:mem_…`. */
154− sources: string[];
155−};
156−
157−export interface ContextApi {
158− /** The workspace's catalog, by kind and project. Members, or anyone for public projects' entries. */
159− catalog(workspace: string, viewer: Viewer, filter?: { kind?: EntityKind | null; project?: string | null }): Promise<Result<Catalog>>;
160− /** One entity, by its id or its key, with its relations. */
161− entity(workspace: string, viewer: Viewer, kind: EntityKind, id: string): Promise<Result<EntityDetail>>;
162− /** One search across the catalog, docs, issues, pull requests and, for members, memory. */
163− search(workspace: string, viewer: Viewer, query: string, options?: SearchOptions): Promise<Result<SearchResult>>;
164− /** Each project's scorecard. Members only. */
165− scorecards(workspace: string, viewer: Viewer): Promise<Result<Scorecard[]>>;
166− /** Builds the catalog for every project and seeds memory from docs and merged pull requests. Members only; once at a time. */
167− backfill(actor: User, workspace: string): Promise<Result<Backfill>>;
168− /** The hub's state for a workspace. Starts its first backfill when it has none. Members only. */
169− status(workspace: string, viewer: Viewer): Promise<Result<ContextStatus>>;
170− /**
171− * For the runner: the Context section for an agent starting work in a
172− * repository, within `budget` characters. `requester` is the person the
173− * run acts for: it holds only what they may read. One who is not a member
174− * of the workspace gets the project's memory only, never the workspace's,
175− * and only the dependencies whose projects they can read.
176− */
177− runContext(repoId: string, task: string, budget?: number, requester?: Viewer): Promise<RunContext>;
178−}
179−
180−async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> {
181− const response = await service.fetch(`https://service/rpc/${method}`, {
182− method: "POST",
183− headers: { "content-type": "application/json" },
184− body: JSON.stringify(args),
185− });
186− if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
187− return (await response.json()) as T;
188−}
189−
190−export function contextClient(service: ServiceBinding): ContextApi {
191− const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
192− return {
193− catalog: (workspace, viewer, filter = {}) => call("catalog", { workspace, viewer, ...filter }),
194− entity: (workspace, viewer, kind, id) => call("entity", { workspace, viewer, kind, id }),
195− search: (workspace, viewer, query, options = {}) => call("search", { workspace, viewer, query, ...options }),
196− scorecards: (workspace, viewer) => call("scorecards", { workspace, viewer }),
197− backfill: (actor, workspace) => call("backfill", { actor, workspace }),
198− status: (workspace, viewer) => call("status", { workspace, viewer }),
199− runContext: (repoId, task, budget, requester) => call("run_context", { repoId, task, budget, requester: requester ?? null }),
200− };
201−}
202−
203−// --- Memory capture and review (the work service) ---------------------------
204−
205−/** Where a captured memory came from. */
206−export type CaptureSource = "run" | "review" | "pr" | "doc" | "manual";
207−
208−export type CaptureItem = {
209− scope: "project" | "workspace";
210− /** For a project's memory: its repository's id. */
211− repoId?: string | null;
212− kind?: MemoryKind;
213− text: string;
214− confidence?: number | null;
215− source: CaptureSource;
216− /** One source: `doc:<repo id>:<path>`, `run:<id>`, `comment:<id>`, `pull:<repo id>#<n>`. */
217− reference: string;
218− evidence?: string | null;
219− number?: number | null;
220− runId?: string | null;
221−};
222−
223−export type Captured = { added: number; merged: number; kept: number; refused: number };
224−
225−export type ReviewDecision = "keep" | "dismiss";
226−
227−export interface MemoryReviewApi {
228− /** Candidates waiting for review in a workspace, or for one project. Members only. */
229− listCandidates(viewer: Viewer, workspace: string, repo?: RepoPath | null): Promise<Result<Memory[]>>;
230− /** Keep a candidate, edited or as it is, or dismiss it. Members only. */
231− reviewMemory(
232− actor: User,
233− workspace: string,
234− id: string,
235− decision: ReviewDecision,
236− change?: { text?: string; kind?: MemoryKind },
237− ): Promise<Result<Memory>>;
238− /** For services: candidates from docs and backfills. */
239− captureMemories(workspace: string, items: CaptureItem[], by?: string): Promise<Captured>;
240− /**
241− * For services: removes a project's candidates that came only from its
242− * docs and are still waiting, unless one of `texts` (what its docs suggest
243− * now) says the same thing. Kept and dismissed memory is never touched.
244− */
245− pruneDocCandidates(workspace: string, repoId: string, texts: string[]): Promise<{ removed: number }>;
246− /** For services: memories by id, in any status. */
247− memoriesById(workspace: string, ids: string[]): Promise<Memory[]>;
248− /** For services: kept memories with every word of `query`; the caller has checked the viewer may read them. */
249− searchMemories(workspace: string, query: string | null, options?: { repoIds?: string[] | null; limit?: number }): Promise<Memory[]>;
250− /** For services: decision and convention candidates from a repository's last merged pull requests. */
251− seedFromPulls(repoId: string, limit?: number): Promise<Captured>;
252−}
253−
254−/** Memory capture and review: methods of the work service. */
255−export function memoryReviewClient(service: ServiceBinding): MemoryReviewApi {
256− const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
257− return {
258− listCandidates: (viewer, workspace, repo) => call("list_candidates", { viewer, workspace, repo: repo ?? null }),
259− reviewMemory: (actor, workspace, id, decision, change = {}) => call("review_memory", { actor, workspace, id, decision, ...change }),
260− captureMemories: (workspace, items, by) => call("capture_memories", { workspace, items, by: by ?? null }),
261− pruneDocCandidates: (workspace, repoId, texts) => call("prune_doc_candidates", { workspace, repoId, texts }),
262− memoriesById: (workspace, ids) => call("memories_by_id", { workspace, ids }),
263− searchMemories: (workspace, query, options = {}) =>
264− call("search_memories", { workspace, query, repoIds: options.repoIds ?? null, limit: options.limit ?? 20 }),
265− seedFromPulls: (repoId, limit) => call("seed_from_pulls", { repoId, limit: limit ?? null }),
266− };
267−}
+0−88
1−/**
2− * `@g1t` in comments, and rules that put g1t to work by
3− * itself. Kept by the work service; mirrors `services/work/src/mentions.rs`.
4− */
5−import type { ServiceBinding } from "./clients";
6−import type { User, Viewer } from "./identity";
7−import type { RepoPath } from "./repos";
8−import type { Result } from "./result";
9−import type { LifecycleJob, PullStatus } from "./work";
10−
11−/** How g1t's agent is mentioned in a comment. */
12−export const AGENT_HANDLE = "@g1t";
13−
14−/** What someone who mentioned `@g1t` wants. */
15−export type MentionIntent = "work" | "question" | "review";
16−
17−/** What the runner needs to act on a comment that mentioned `@g1t`. */
18−export type MentionJob = {
19− commentId: string;
20− /** Who wrote it, with the memberships they had then. */
21− actor: User;
22− repo: RepoPath;
23− number: number;
24− /** The comment as written. */
25− body: string;
26− intent: MentionIntent;
27− /** Whether they belong to the repository's workspace. */
28− member: boolean;
29− defaultBranch: string;
30− /** Set when the comment is on an issue: whether it is still open. */
31− issueOpen: boolean | null;
32− /** On an issue: the pull request g1t is already working on for it, if any. */
33− workingPull: number | null;
34− /** Set when the comment is on a pull request. */
35− pull: {
36− id: string;
37− status: PullStatus;
38− /** Made by g1t, which sees it through. */
39− agentAuthored: boolean;
40− /** Where its change is: its fork, or the repository itself. */
41− source: RepoPath;
42− /** The head is a branch of the repository itself, not a fork. */
43− inRepo: boolean;
44− branch: string | null;
45− headCommit: string | null;
46− files: string[];
47− } | null;
48−};
49−
50−/** A repository's rules for putting g1t to work by itself. */
51−export type AgentRules = {
52− /** When an issue is given this label, g1t takes it. */
53− label: string | null;
54− updatedBy: string | null;
55− updatedAt: string | null;
56−};
57−
58−export interface MentionsApi {
59− /** For the runner: claims a comment's mention, once. Null when there is none to take. */
60− takeMention(commentId: string): Promise<MentionJob | null>;
61− /** For the runner: sends the author of a g1t pull request back to address the comment. */
62− mentionRevision(commentId: string): Promise<Result<LifecycleJob>>;
63− /** For the runner: says something in the mention's thread as g1t, once. */
64− replyMention(commentId: string, body: string): Promise<boolean>;
65− getAgentRules(repo: RepoPath, viewer: Viewer): Promise<Result<AgentRules>>;
66− /** Members only. A null or empty label turns the rule off. */
67− setAgentRules(actor: User, repo: RepoPath, rules: { label: string | null }): Promise<Result<AgentRules>>;
68−}
69−
70−/** The mention and rule methods of the work service. */
71−export function mentionsClient(service: ServiceBinding): MentionsApi {
72− const call = async <T>(method: string, args: object): Promise<T> => {
73− const response = await service.fetch(`https://service/rpc/${method}`, {
74− method: "POST",
75− headers: { "content-type": "application/json" },
76− body: JSON.stringify(args),
77− });
78− if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
79− return (await response.json()) as T;
80− };
81− return {
82− takeMention: (commentId) => call("take_mention", { commentId }),
83− mentionRevision: (commentId) => call("mention_revision", { commentId }),
84− replyMention: (commentId, body) => call("reply_mention", { commentId, body }),
85− getAgentRules: (repo, viewer) => call("get_agent_rules", { repo, viewer }),
86− setAgentRules: (actor, repo, rules) => call("set_agent_rules", { actor, repo, label: rules.label }),
87− };
88−}
+0−235
Loading the diff…
+0−164
Loading the diff…
+0−459
Loading the diff…
+0−267
Loading the diff…
+0−1184
A large diff (1,184 lines) is not shown until you ask for it.