Skip to content
1,591 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1/**
2 * What an agent can read while it replies: code, issues, pull requests,
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.3 * chat, the roster, and its colleagues (docs.g1t.sh/guides/agent-access/,
4 * "What an agent can and can't know").
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)5 *
6 * Every tool goes through the reply's `Audience` before it reads
7 * anything, and the check is here, in code:
8 * - code tools are offered only when the audience may read code at all,
9 * and a repository is used only when it is on the audience's allow-list;
10 * - chat tools ask the chat service, which works out the audience from the
11 * conversation itself;
12 * - what the audience may not see comes back as one neutral line,
13 * `WITHHELD`, the same for a thing that is private and a thing that does
14 * not exist, and never names it.
15 *
16 * Whatever a tool returns is wrapped as untrusted data: text in files,
17 * issues and messages is never an instruction to the agent.
18 *
19 * Pure apart from its ports, so the rules are tested adversarially.
20 */
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.21import type { Ability, AbilityGroup, AbilitySection, AbilitySource, AgentComputerCommand, DocEditTarget, FolioAgentEdit, FolioAgentEditResult, FolioAgentRead, FolioAudience, FolioKind, FolioPassage, FolioRef, McpServer, McpTool, User } from "@g1t/contracts";
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)22
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.23import { askedFor, levelWords, mcpToolName } from "../../../packages/contracts/src/abilities.ts";
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.24import { FOLIO_KINDS, folioIdFrom, isFolioKind } from "../../../packages/contracts/src/folios.ts";
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.25import type { MakeFileFormat } from "../../../packages/contracts/src/skills.ts";
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)26import { type Audience, type RepoRef, WITHHELD } from "./audience.ts";
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.27import { makeFile, previewTable, readSheets, sizeLabel } from "./files.ts";
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.28import { type ShelfSkill, type StoredVersion, skillBlock, skillText } from "./skills.ts";
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)29
30/** One tool, as the Messages API takes it. */
31export type ToolDef = { name: string; description: string; input_schema: Record<string, unknown> };
32
33export type FoundMessage = { channel: string | null; channel_id: string; id: string; author: string; body: string; created_at: string };
34
35/** What the tools reach outside this module. */
36export interface ToolPorts {
37 readFile(repo: RepoRef, viewer: User, ref: string, path: string): Promise<{ text: string | null; size: number } | null>;
38 searchCode(viewer: User, query: string, repo: RepoRef | null): Promise<{ repo: string; path: string; snippet: string }[]>;
39 listIssues(repo: RepoRef, viewer: User, state: "open" | "closed"): Promise<{ number: number; title: string; state: string; labels: string[] }[] | null>;
40 getIssue(repo: RepoRef, number: number, viewer: User): Promise<{ number: number; title: string; state: string; body: string; comments: { author: string; body: string }[] } | null>;
41 getPull(repo: RepoRef, number: number, viewer: User): Promise<{ number: number; title: string; status: string; body: string; checks: string | null } | null>;
42 recentPulls(repos: RepoRef[], viewer: User): Promise<{ repo: string; number: number; title: string; status: string; updated_at: string }[]>;
43 /** Chat's own audience rule applies; null when the search failed. */
44 searchMessages(query: string): Promise<FoundMessage[] | null>;
45 /** Null when the audience may not read it (or it does not exist). */
46 readThread(channelId: string, id: string): Promise<FoundMessage[] | null>;
47 roster(viewer: User | null): Promise<string>;
48 consult(handle: string, question: string): Promise<{ ok: true; colleague: string; answer: string } | { ok: false; message: string }>;
Docs: a workspace knowledge base people and agents write together49 /**
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.50 * The workspace's artifacts (Artifacts mode), as the artifacts service lets
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.51 * this agent use them for the person it acts for and everyone who will
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.52 * read the answer. Absent where there is no artifacts service.
Docs: a workspace knowledge base people and agents write together53 */
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.54 folios?: FoliosPorts;
Docs: a workspace knowledge base people and agents write together55}
56
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.57/** A space as an agent sees it, with what it may do there for the person it acts for. */
58export type FolioSpaceLine = {
59 id: string;
60 slug: string;
61 name: string;
62 description: string | null;
63 kind: string;
64 projects: string[];
65 can: { read: boolean; suggest: boolean; edit: boolean };
66};
67
68/** Where a new artifact goes: a space, its asker's Private, or Private shared with the conversation's people. */
69export type FolioWhere = { space_id: string } | "private" | { conversation: string[] };
70
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.71/** A call's answer: the value, or the artifacts service's error code and sentence. */
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.72export type FolioDone<T> = { ok: true; value: T } | { ok: false; code: string; message: string };
73
74/**
75 * Artifacts, as an agent uses them. Every call names the person it acts
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.76 * for, and the reads also who reads the answer; the artifacts service checks
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.77 * both.
78 */
79export interface FoliosPorts {
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.80 /** Spaces everyone here can read; null when the artifacts service couldn't answer. */
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.81 spaces(viewer: User, audience: FolioAudience): Promise<FolioSpaceLine[] | null>;
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.82 /** Passages closest in meaning to `query`; `spaces` (required reading) first. Null when the artifacts service couldn't answer. */
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.83 recall(viewer: User, audience: FolioAudience, query: string, spaces: string[], kinds?: FolioKind[]): Promise<FolioPassage[] | null>;
84 /** Artifacts matching `query` (words and meaning), as lines with links. */
85 search(viewer: User, audience: FolioAudience, input: { query: string; kind: FolioKind | null; space_id: string | null; project: string | null }): Promise<string | null>;
86 read(viewer: User, audience: FolioAudience, folioId: string): Promise<FolioDone<FolioAgentRead>>;
87 /** Artifacts possibly out of date since code they cite changed. */
88 stale(viewer: User, audience: FolioAudience, repo: string | null): Promise<string | null>;
89 create(
90 viewer: User,
91 input: { kind: FolioKind; title: string; markdown: string | null; template_id: string | null; where: FolioWhere; parent_id: string | null; source: { title: string; href: string } | null },
92 ): Promise<FolioDone<FolioRef>>;
93 edit(viewer: User, folioId: string, edit: FolioAgentEdit): Promise<FolioDone<FolioAgentEditResult>>;
94 /** `view` or `comment` for people already in this conversation. */
95 share(viewer: User, audience: FolioAudience, folioId: string, userIds: string[], role: "view" | "comment"): Promise<FolioDone<null>>;
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.96 /**
97 * Keeps a file the agent made with a doc the asker can edit; `url` is
98 * where it is served (on the usercontent origin). Absent where files
99 * can't be kept.
100 */
101 attach?(viewer: User, folioId: string, file: { name: string; content_type: string; bytes: Uint8Array }): Promise<FolioDone<{ url: string; name: string; bytes: number }>>;
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.102 /** Sends the asker a link directly, as a message from the agent in their DM with it; false when it couldn't. */
103 sendLink(asker: User, link: { title: string; path: string }, note: string): Promise<boolean>;
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)104}
105
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked106/**
107 * What an agent may do, beyond reading: remember, file an issue for the
108 * person who asked, and start or shape work. Each is checked here before it
109 * runs (the audience, the asker, the hop limit) and again by the service
110 * that does it.
111 */
112export interface ActionPorts {
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.113 /**
114 * `onlyForAsker`: this turn read an artifact the whole workspace can't
115 * read, so the fact is kept for the person who asked alone.
116 */
117 remember(body: string, scope: "workspace" | "channel" | "person" | null, onlyForAsker?: boolean): Promise<{ ok: boolean; message: string }>;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked118 forget(id: string): Promise<{ ok: boolean; message: string }>;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar119 /**
120 * Posts a draft issue as a card in the conversation, with File issue and
121 * Discard: whoever presses File files it as themselves, if they can read
122 * the repository. Nothing is filed by the agent.
123 */
124 draftIssue(repo: RepoRef, input: { title: string; body: string; labels: string[] }): Promise<{ ok: boolean; message: string }>;
125 /**
126 * Comments on an issue or pull request, or reviews a pull request, as the
127 * agent on behalf of the person who asked. Reviews are advisory: they
128 * never count toward required approvals.
129 */
130 comment?(repo: RepoRef, asker: User, number: number, body: string): Promise<{ ok: boolean; message: string }>;
131 review?(repo: RepoRef, asker: User, number: number, verdict: "comment" | "approve" | "request_changes", body: string): Promise<{ ok: boolean; message: string }>;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked132 /** From chat: spins off a session for real work. */
133 startSession?(title: string, goal: string): Promise<{ ok: boolean; message: string }>;
134 /** In a session: a short progress note in its thread. */
135 postUpdate?(text: string): Promise<{ ok: boolean; message: string }>;
136 /** In a session: one of the agent's own subagents takes part of the work. */
137 useSubagent?(name: string, brief: string): Promise<{ ok: boolean; message: string }>;
138 /** In a session: a colleague works on part of it, paid from this session's budget. */
139 bringIn?(handle: string, brief: string): Promise<{ ok: boolean; message: string }>;
Merge branch 'worktree-agent-a1398e81ad1a64c5f'140 /** From chat: a colleague takes the work on for the person who asked, here or in a group message. */
141 handOff?(handle: string, brief: string): Promise<{ ok: boolean; message: string }>;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked142}
143
Merge branch 'worktree-agent-a1398e81ad1a64c5f'144/** The most hand-offs one reply makes. */
145export const MAX_HAND_OFFS = 2;
146
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)147/** The most tool calls one reply makes. */
148export const MAX_TOOL_CALLS = 8;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked149/** The most tool calls one step of a session makes. */
150export const MAX_SESSION_TOOL_CALLS = 24;
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)151/** The most of a file or result an answer is given, in characters. */
152const MAX_RESULT = 20_000;
153
154export type ToolCall = {
155 tool: string;
156 /** Its arguments, with long text cut, as recorded. */
157 args: string;
158 outcome: "allowed" | "withheld" | "refused" | "error";
159 bytes: number;
160};
161
162export type ToolResult = { text: string; outcome: ToolCall["outcome"] };
163
164/**
165 * Text a tool read, marked as data. Anything in it that looks like the
166 * closing mark is defused, so content can't end the block early.
167 */
168export function untrusted(source: string, content: string): string {
169 const safe = (text: string) => text.replace(/<\/?untrusted/gi, (mark) => mark.replace("<", "&lt;"));
170 const body = content.length > MAX_RESULT ? `${content.slice(0, MAX_RESULT)}\n[cut: ${content.length - MAX_RESULT} more characters]` : content;
171 return `<untrusted source="${safe(source).replace(/"/g, "'")}">\n${safe(body)}\n</untrusted>`;
172}
173
174/** Arguments as recorded: every string cut to 120 characters. */
175export function redact(args: unknown): string {
176 const cut = (value: unknown): unknown => {
177 if (typeof value === "string") return value.length > 120 ? `${value.slice(0, 120)}…` : value;
178 if (Array.isArray(value)) return value.slice(0, 10).map(cut);
179 if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).slice(0, 10).map(([k, v]) => [k, cut(v)]));
180 return value;
181 };
182 return JSON.stringify(cut(args ?? {})).slice(0, 1000);
183}
184
185const CODE_TOOLS: ToolDef[] = [
186 {
187 name: "list_repositories",
188 description: "The workspace's repositories everyone in this conversation can read. Start here to know what you can look at.",
189 input_schema: { type: "object", properties: {} },
190 },
191 {
192 name: "search_code",
193 description: "Search code on default branches. Optionally only in one repository (`name` or `workspace/name`).",
194 input_schema: { type: "object", properties: { query: { type: "string" }, repo: { type: "string" } }, required: ["query"] },
195 },
196 {
197 name: "read_file",
198 description: "Read a file from a repository, at its default branch or a ref.",
199 input_schema: { type: "object", properties: { repo: { type: "string" }, path: { type: "string" }, ref: { type: "string" } }, required: ["repo", "path"] },
200 },
201 {
202 name: "list_issues",
203 description: "A repository's newest issues, open by default.",
204 input_schema: { type: "object", properties: { repo: { type: "string" }, state: { type: "string", enum: ["open", "closed"] } }, required: ["repo"] },
205 },
206 {
207 name: "get_issue",
208 description: "One issue with its comments.",
209 input_schema: { type: "object", properties: { repo: { type: "string" }, number: { type: "integer" } }, required: ["repo", "number"] },
210 },
211 {
212 name: "get_pull",
213 description: "One pull request: what it changes, its status and checks.",
214 input_schema: { type: "object", properties: { repo: { type: "string" }, number: { type: "integer" } }, required: ["repo", "number"] },
215 },
216 {
217 name: "recent_activity",
218 description: "Recently merged and open pull requests, in one repository or across those you can read.",
219 input_schema: { type: "object", properties: { repo: { type: "string" } } },
220 },
221];
222
223const CHAT_TOOLS: ToolDef[] = [
224 {
225 name: "search_messages",
226 description: "Search chat messages this conversation's people can all read.",
227 input_schema: { type: "object", properties: { query: { type: "string" } }, required: ["query"] },
228 },
229 {
230 name: "read_thread",
231 description: "Read a chat thread by its channel id and a message id in it (from search_messages).",
232 input_schema: { type: "object", properties: { channel: { type: "string" }, id: { type: "string" } }, required: ["channel", "id"] },
233 },
234 {
235 name: "workspace_roster",
236 description: "The workspace's people and agents: names, teams, titles and roles.",
237 input_schema: { type: "object", properties: {} },
238 },
239];
240
241const ASK_COLLEAGUE: ToolDef = {
242 name: "ask_colleague",
243 description:
Merge branch 'worktree-agent-a1398e81ad1a64c5f'244 "Ask a colleague agent a quick question, privately: their answer comes back to you alone, they do no work in the conversation, and the work stays yours. Use it when their role knows something yours doesn't. To give them the work itself, use hand_off.",
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)245 input_schema: { type: "object", properties: { handle: { type: "string" }, question: { type: "string" } }, required: ["handle", "question"] },
246};
247
Merge branch 'worktree-agent-a1398e81ad1a64c5f'248const HAND_OFF: ToolDef = {
249 name: "hand_off",
250 description:
251 "Hand work to a colleague agent, for the person who asked: they take it on and answer that person themselves, with that person's access. If they are a member of this conversation (a channel or group message), your brief is posted here; otherwise a group message opens (or is reused) with the person who asked, you and them, your brief goes there, and a card here links to it. It is the only way to get a colleague working: an @mention in your message wakes nobody. Give their handle and a complete brief written to them: what is wanted, why, what done looks like, and what they need from this conversation. For a quick question you answer with, use ask_colleague instead.",
252 input_schema: { type: "object", properties: { handle: { type: "string" }, brief: { type: "string" } }, required: ["handle", "brief"] },
253};
254
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked255const REMEMBER: ToolDef = {
256 name: "remember",
257 description:
258 "Keep a short fact for later work: a preference, a decision, who owns what, how something works here. One fact per call, in your own words. It is kept where this conversation allows (this person, this conversation, or the workspace from a public channel), with this conversation as its source. Never keep secrets, credentials or customers' personal data.",
259 input_schema: {
260 type: "object",
261 properties: { fact: { type: "string" }, scope: { type: "string", enum: ["workspace", "channel", "person"] } },
262 required: ["fact"],
263 },
264};
265
266const FORGET: ToolDef = {
267 name: "forget",
268 description: "Forget one of the notes under 'What you remember', by its id, when it is wrong or out of date.",
269 input_schema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
270};
271
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar272const DRAFT_ISSUE: ToolDef = {
273 name: "draft_issue",
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked274 description:
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar275 "Draft an issue (a bug report or a feature request) for a repository with what you found. It appears in the conversation as a card with File issue and Discard buttons: the person files it themselves with one press, so don't ask them to confirm in words. Write it for the team that will fix it: what happens, what should happen, steps or evidence, and where in the code it likely is.",
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked276 input_schema: {
277 type: "object",
278 properties: {
279 repo: { type: "string" },
280 title: { type: "string" },
281 body: { type: "string" },
282 labels: { type: "array", items: { type: "string" } },
283 },
284 required: ["repo", "title", "body"],
285 },
286};
287
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar288const COMMENT: ToolDef = {
289 name: "comment",
290 description:
291 "Comment on an issue or pull request, as yourself on behalf of the person you're working for. Use it when the comment belongs on the issue or pull request (findings, a test plan, a question for its author), not for chatting.",
292 input_schema: {
293 type: "object",
294 properties: { repo: { type: "string" }, number: { type: "integer" }, body: { type: "string" } },
295 required: ["repo", "number", "body"],
296 },
297};
298
299const REVIEW_PULL: ToolDef = {
300 name: "review_pull",
301 description:
302 "Review a pull request on the pull request itself, as yourself on behalf of the person you're working for: approve, request changes, or just comment, with your review in the body. Your review is advisory: people still give the approvals a merge needs. Read the change first.",
303 input_schema: {
304 type: "object",
305 properties: {
306 repo: { type: "string" },
307 number: { type: "integer" },
308 verdict: { type: "string", enum: ["comment", "approve", "request_changes"] },
309 body: { type: "string" },
310 },
311 required: ["repo", "number", "verdict", "body"],
312 },
313};
314
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked315const START_SESSION: ToolDef = {
316 name: "start_session",
317 description:
318 "Spin off a session for work that needs more than a quick answer: investigating, reading a lot of code, writing something long, or anything that takes several steps. It runs on its own with its own context, shows a live card here, and reports back in this conversation when done. Give it a short title and a complete brief: the goal, what done looks like, and everything it needs from this conversation.",
319 input_schema: { type: "object", properties: { title: { type: "string" }, goal: { type: "string" } }, required: ["title", "goal"] },
320};
321
322const POST_UPDATE: ToolDef = {
323 name: "post_update",
324 description: "Post a short progress note in your session's thread, for the people following it. Use it for real milestones or a question, not for every step.",
325 input_schema: { type: "object", properties: { text: { type: "string" } }, required: ["text"] },
326};
327
328const USE_SUBAGENT: ToolDef = {
329 name: "use_subagent",
330 description:
331 "Hand a well-defined part of this session to one of your subagents (listed under Subagents). It works in its own session, paid from this one, and its result comes back to you before you go on. Give a complete brief.",
332 input_schema: { type: "object", properties: { name: { type: "string" }, brief: { type: "string" } }, required: ["name", "brief"] },
333};
334
335const BRING_IN: ToolDef = {
336 name: "bring_in",
337 description:
338 "Bring a colleague in on part of this session when their role owns it. They work in their own session, paid from this one, and their result comes back to you before you go on. Give a complete brief.",
339 input_schema: { type: "object", properties: { handle: { type: "string" }, brief: { type: "string" } }, required: ["handle", "brief"] },
340};
341
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.342/** What every artifact tool's description starts from, so the model never mixes them up with workflow run artifacts. */
343const ARTIFACTS =
344 "Artifacts are the workspace's own documents, made and shared in its Artifacts section: docs now, and later slides, designs and dashboards. They are not a workflow run's build artifacts.";
345
346const FOLIO_TOOLS: ToolDef[] = [
Docs: a workspace knowledge base people and agents write together347 {
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.348 name: "search_artifacts",
349 description: `${ARTIFACTS} Search the artifacts everyone in this conversation can read (specs, runbooks, policies, onboarding, decisions), by words and meaning, plus projects' docs. Optionally only one kind, one space (its name or id from list_spaces) or one project (\`workspace/name\`). Each result has its link and id. Look here first for how things work and what was decided.`,
350 input_schema: {
351 type: "object",
352 properties: {
353 query: { type: "string" },
354 kind: { type: "string", enum: [...FOLIO_KINDS] },
355 space: { type: "string", description: "A space's name or id." },
356 project: { type: "string", description: "A repository, `workspace/name`." },
357 },
358 required: ["query"],
359 },
Docs: a workspace knowledge base people and agents write together360 },
361 {
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.362 name: "read_artifact",
363 description: `${ARTIFACTS} Read one artifact by its id (fol_…) or its link (…/-/artifacts/<name>-fol_…). A doc comes back as Markdown with the ids of its top-level blocks (for edit_artifact), and says what you may do with it.`,
364 input_schema: { type: "object", properties: { id: { type: "string", description: "Its id or link." } }, required: ["id"] },
Docs: a workspace knowledge base people and agents write together365 },
366 {
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.367 name: "list_spaces",
368 description: `${ARTIFACTS} The spaces whose artifacts everyone in this conversation can read, with what you may do in each (read, suggest, edit).`,
369 input_schema: { type: "object", properties: {} },
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store370 },
371 {
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.372 name: "stale_artifacts",
373 description: `${ARTIFACTS} Artifacts possibly out of date because code they cite changed. Optionally only for one repository (\`workspace/name\`). Start here when keeping the docs current: read each, then bring it up to date with edit_artifact and marks_current.`,
374 input_schema: { type: "object", properties: { repo: { type: "string" } } },
Docs: a workspace knowledge base people and agents write together375 },
376];
377
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.378const FOLIO_WRITE_TOOLS: ToolDef[] = [
379 {
380 name: "create_artifact",
381 description: `${ARTIFACTS} Make a new artifact ("write this up"). Only kind "doc" can be made for now. Give a title and its content as Markdown (or a template id). where: a space (its name or id from list_spaces) as { "space": "..." }, "private" for the person who asked alone, or "conversation" for them plus view access for this conversation's people. Left out: shared with this conversation in a direct message or private channel, the General space in a public channel. It belongs to the person who asked, and you can keep editing it. When it comes from a conversation, set source to that thread's link.`,
382 input_schema: {
383 type: "object",
384 properties: {
385 kind: { type: "string", enum: [...FOLIO_KINDS] },
386 title: { type: "string" },
387 content: { type: "string", description: "Markdown." },
388 template: { type: "string", description: "A template id, instead of content." },
389 where: {
390 description: '{ "space": "<name or id>" }, "private" or "conversation".',
391 anyOf: [
392 { type: "string", enum: ["private", "conversation"] },
393 { type: "object", properties: { space: { type: "string" } }, required: ["space"] },
394 ],
395 },
396 parent: { type: "string", description: "A doc to put it under: its id or link." },
397 source: { type: "string", description: "The link of the thread it was written up from." },
398 },
399 required: ["kind", "title"],
400 },
401 },
Docs: a workspace knowledge base people and agents write together402 {
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.403 name: "edit_artifact",
404 description: `${ARTIFACTS} Change a doc: replace a section (by its heading), a range of top-level blocks (ids from read_artifact), the whole doc, or add to the end. Where you may edit, it applies at once and shows in its history as yours; elsewhere it becomes a suggestion people accept or reject inline. Read it first. Write Markdown. Only docs can be changed this way for now.`,
Docs: a workspace knowledge base people and agents write together405 input_schema: {
406 type: "object",
407 properties: {
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.408 id: { type: "string", description: "Its id or link." },
Docs: a workspace knowledge base people and agents write together409 target: { type: "string", enum: ["append", "section", "blocks", "document"] },
410 heading: { type: "string", description: "For target section: the heading's text." },
411 from_block: { type: "string" },
412 to_block: { type: "string" },
413 markdown: { type: "string" },
414 note: { type: "string", description: "Why, in a line, for the history or the suggestion." },
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.415 marks_current: { type: "boolean", description: "This edit brings a doc marked possibly out of date up to date: it clears the mark when it applies or is accepted." },
Docs: a workspace knowledge base people and agents write together416 suggest_only: { type: "boolean", description: "Suggest even where you could edit." },
417 },
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.418 required: ["id", "target", "markdown"],
Docs: a workspace knowledge base people and agents write together419 },
420 },
421 {
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.422 name: "share_artifact",
423 description: `${ARTIFACTS} Let people in this conversation view or comment on an artifact, when the person who asked has full access to it. Only in a direct message or a private channel, and only with people already in it. You can't give edit or full access, or change who else can open it: for that, ask the person to use Share.`,
Docs: a workspace knowledge base people and agents write together424 input_schema: {
425 type: "object",
426 properties: {
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.427 id: { type: "string", description: "Its id or link." },
428 people: { type: "array", items: { type: "string" }, description: "Usernames of people in this conversation." },
429 role: { type: "string", enum: ["view", "comment"] },
Docs: a workspace knowledge base people and agents write together430 },
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.431 required: ["id", "people", "role"],
Docs: a workspace knowledge base people and agents write together432 },
433 },
434];
435
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.436/**
437 * Files people asked for (the Documents, Data and Files and media skills):
438 * written here, kept with a doc the person who asked owns or can edit, and
439 * served from the usercontent origin like an upload.
440 */
441const MAKE_FILE: ToolDef = {
442 name: "make_file",
443 description:
444 'Make a file someone asked for and keep it with a doc in Artifacts: format "pdf" or "docx" (a Word document) from Markdown in content (headings, paragraphs, bold, italic, code, links, lists, quotes, code blocks, tables); "xlsx" (a spreadsheet, one or more sheets) or "csv" (one sheet) from sheets, each { name, rows }, the first row its header and numbers as numbers; or "md". Give a title (the file is named after it). Without artifact, a new doc is made holding the content (or a preview of the rows) with the file attached; with artifact (a doc\'s id or link you can edit), the file is attached to that doc. where works as for create_artifact. It returns the file\'s link: give it to them.',
445 input_schema: {
446 type: "object",
447 properties: {
448 format: { type: "string", enum: ["pdf", "docx", "xlsx", "csv", "md"] },
449 title: { type: "string" },
450 content: { type: "string", description: "Markdown, for pdf, docx and md." },
451 sheets: {
452 type: "array",
453 description: "For xlsx and csv.",
454 items: {
455 type: "object",
456 properties: { name: { type: "string" }, rows: { type: "array", items: { type: "array", items: {} } } },
457 required: ["rows"],
458 },
459 },
460 artifact: { type: "string", description: "A doc to attach it to: its id or link. Left out: a new doc." },
461 where: {
462 description: '{ "space": "<name or id>" }, "private" or "conversation", for a new doc.',
463 anyOf: [{ type: "string" }, { type: "object", properties: { space: { type: "string" } }, required: ["space"] }],
464 },
465 },
466 required: ["format", "title"],
467 },
468};
469
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.470/**
471 * Reading one of the agent's skills (skills.ts): the prompt lists them by
472 * name and when to use them, and this reads one when a request matches.
473 * Reading a skill never offers another tool.
474 */
475const USE_SKILL: ToolDef = {
476 name: "use_skill",
477 description:
478 "Read one of your skills (listed under Your skills) before you do work it covers, and follow it. With file, read one of the files a skill lists instead, such as a template or a reference.",
479 input_schema: {
480 type: "object",
481 properties: { name: { type: "string", description: "The skill's name, as listed." }, file: { type: "string", description: "A file of the skill, by its path, such as resources/template.md." } },
482 required: ["name"],
483 },
484};
485
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.486/**
487 * Outside g1t, through the workspace's integrations (abilities.ts,
488 * docs.g1t.sh/guides/agent-abilities/): each call is checked against the
489 * agent's abilities for the system that knows the item, in `gate`, before
490 * anything runs.
491 */
492const LOOKUP_OUTSIDE: ToolDef = {
493 name: "lookup_outside",
494 description:
495 "Look up an item in one of the workspace's connected integrations by its key or address: a Linear issue (ENG-42), a Jira ticket (TECH-1234) or a Sentry issue (its link). You get its title, status and description. Only where your abilities allow reading it.",
496 input_schema: { type: "object", properties: { reference: { type: "string", description: "A key such as ENG-42, or the item's address." } }, required: ["reference"] },
497};
498
499const IMPORT_OUTSIDE: ToolDef = {
500 name: "import_outside",
501 description:
502 "Open an issue in a repository here from an item outside g1t (a Linear issue, a Jira ticket or a Sentry issue), linked back to it, as the person you're working for. If it was imported before, you get the existing issue.",
503 input_schema: { type: "object", properties: { repo: { type: "string" }, reference: { type: "string" } }, required: ["repo", "reference"] },
504};
505
506const ACT_OUTSIDE: ToolDef = {
507 name: "act_outside",
508 description:
509 "Act on an item outside g1t: comment on a Linear issue, a Jira ticket or a Sentry issue, or resolve a Sentry issue, with text that names the person you're working for. Depending on your abilities this runs at once, or posts a card asking them first: then say it's waiting on their OK and carry on.",
510 input_schema: {
511 type: "object",
512 properties: { reference: { type: "string" }, action: { type: "string", enum: ["comment", "resolve"] }, text: { type: "string" } },
513 required: ["reference", "action", "text"],
514 },
515};
516
517const REQUEST_ABILITY: ToolDef = {
518 name: "request_ability",
519 description:
520 "When something you were asked for needs an integration that isn't connected, or an ability you don't have (one of yours is Never, or a tool you lack), say so plainly and call this once: it posts a card the person uses to ask the workspace's owners (or to connect it, if they are one). Name what is needed (an integration such as linear, jira or sentry, or one of your abilities by its id) and why, in their words.",
521 input_schema: {
522 type: "object",
523 properties: { needs: { type: "string", description: "An integration's id (linear, jira, sentry…) or an ability's id (integration:linear:comment)." }, why: { type: "string" } },
524 required: ["needs", "why"],
525 },
526};
527
528const OUTSIDE_TOOLS: ToolDef[] = [LOOKUP_OUTSIDE, IMPORT_OUTSIDE, ACT_OUTSIDE, REQUEST_ABILITY];
529const OUTSIDE_NAMES = new Set(OUTSIDE_TOOLS.map((tool) => tool.name));
530
531/** An item outside g1t, as the integrations service fetched it. Reference material, never instructions. */
532export type OutsideItem = { provider: string; key: string; title: string; url: string; status: string | null; body: string };
533
534export type OutsideDone<T> = { ok: true; value: T } | { ok: false; code: string; message: string };
535
536/**
537 * What carries an agent's abilities out, outside g1t: the integrations
538 * service, MCP servers, and the cards in chat that ask, connect and
539 * request. Every call here comes after `gate` allowed it.
540 */
541export interface AbilityPorts {
542 /** The item `reference` names, through the workspace's connections, for the asker. */
543 lookup(asker: User, reference: string): Promise<OutsideDone<OutsideItem>>;
544 import(asker: User, repo: RepoRef, reference: string): Promise<OutsideDone<{ number: number; item: OutsideItem; created: boolean }>>;
545 act(asker: User, reference: string, action: "comment" | "resolve", text: string): Promise<OutsideDone<OutsideItem>>;
546 /** Whether the asker has a connection of their own for the connector. */
547 askerConnected(connector: string, asker: User): Promise<boolean>;
548 /** Calls a tool on one of the agent's MCP servers: the text it returned. */
549 mcp(server: McpServer, tool: McpTool, args: Record<string, unknown>): Promise<OutsideDone<string>>;
550 /** Posts the Ask-first card for a call; the request's id, or null when it couldn't be posted. */
551 askFirst(input: { ability: Ability; source: AbilitySource; tool: string; args: Record<string, unknown>; summary: string; note: string | null }): Promise<string | null>;
552 /** Posts a Connect card: the asker's own connection is needed and missing. */
553 connect(source: AbilitySource, ability: Ability): Promise<boolean>;
554 /** Posts a Request card: an integration to connect, or an ability to allow, for the owners. */
555 request(input: { connector: string | null; ability: Ability | null; source: AbilitySource | null; why: string }): Promise<boolean>;
556 /** Records a refusal in the audit log, naming the rule. */
557 refused(input: { ability: Ability; source: AbilitySource; rule: string; call: string }): void;
558}
559
560/** What a gate decided: go on, or what the agent is told instead. */
561type Gated = { ok: true } | { ok: false; result: ToolResult };
562
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.563/**
564 * The agent's own computer, in a session (computer.ts): what carries a
565 * command or a file to it. Every call here comes after `gate` allowed it
566 * under the `computer:shell` or `computer:files` ability.
567 */
568export interface ComputerPorts {
569 /** This session's directory under the home, where commands run unless told otherwise. */
570 cwd: string;
571 exec(cmd: string, cwd: string | null, timeoutSeconds: number | null): Promise<OutsideDone<AgentComputerCommand>>;
572 readFile(path: string): Promise<OutsideDone<{ path: string; text: string; bytes: number }>>;
573 writeFile(path: string, text: string): Promise<OutsideDone<{ path: string; bytes: number }>>;
574}
575
576const RUN_COMMAND: ToolDef = {
577 name: "run_command",
578 description:
579 "Run a shell command on your own computer (bash -lc, with git, Node, Python, Go, Rust, Java, .NET and Ruby installed). It runs in this session's directory unless cwd names another under /home/agent, your home, which persists between sessions: clones, installed tools and notes stay. Output comes back when the command ends, so chain steps with && and avoid anything that waits for input or runs forever; a command is killed at timeout_seconds (120 unless given, 1800 at most). Quote real output; never say you ran something you didn't.",
580 input_schema: {
581 type: "object",
582 properties: {
583 command: { type: "string" },
584 cwd: { type: "string", description: "A directory under /home/agent; made if missing." },
585 timeout_seconds: { type: "integer", description: "1 to 1800." },
586 },
587 required: ["command"],
588 },
589};
590
591const COMPUTER_READ_FILE: ToolDef = {
592 name: "computer_read_file",
593 description: "Read a text file from your computer's home (/home/agent), by path: absolute under the home, or relative to it. Up to 5 MB. For a file in a repository on g1t, read_file reads it without the computer.",
594 input_schema: { type: "object", properties: { path: { type: "string" } }, required: ["path"] },
595};
596
597const COMPUTER_WRITE_FILE: ToolDef = {
598 name: "computer_write_file",
599 description: "Write a text file on your computer, by path under /home/agent (parents are made). Up to 5 MB. It replaces what was there.",
600 input_schema: { type: "object", properties: { path: { type: "string" }, text: { type: "string" } }, required: ["path", "text"] },
601};
602
603const COMPUTER_TOOLS: ToolDef[] = [RUN_COMMAND, COMPUTER_READ_FILE, COMPUTER_WRITE_FILE];
604const COMPUTER_NAMES = new Set(COMPUTER_TOOLS.map((tool) => tool.name));
605
606/**
607 * Words in what the person said that count as asking for the computer, for
608 * a shell set to "alone when asked for it": the work named running,
609 * building, testing or the like.
610 */
611export const COMPUTER_ASKED_WORDS: readonly string[] = ["run", "shell", "command", "terminal", "script", "computer", "install", "build", "test", "clone", "execute", "compile", "benchmark", "reproduce"];
612
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.613const FOLIO_NAMES = new Set([...FOLIO_TOOLS, ...FOLIO_WRITE_TOOLS, MAKE_FILE].map((tool) => tool.name));
Docs: a workspace knowledge base people and agents write together614
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.615/** Every tool an agent may be offered, by name: what skills may name (@g1t/contracts skill-format.ts `AGENT_TOOL_NAMES`, which a test keeps equal). */
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.616export const TOOL_NAMES: ReadonlySet<string> = new Set(
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.617 [
618 ...CODE_TOOLS,
619 ...CHAT_TOOLS,
620 ...FOLIO_TOOLS,
621 ...FOLIO_WRITE_TOOLS,
622 ...OUTSIDE_TOOLS,
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.623 ...COMPUTER_TOOLS,
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.624 MAKE_FILE,
625 ASK_COLLEAGUE,
626 HAND_OFF,
627 REMEMBER,
628 FORGET,
629 DRAFT_ISSUE,
630 COMMENT,
631 REVIEW_PULL,
632 START_SESSION,
633 POST_UPDATE,
634 USE_SUBAGENT,
635 BRING_IN,
636 USE_SKILL,
637 ].map((tool) => tool.name),
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.638);
639
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.640/** Reads a library skill's version (skill-library.ts), for `use_skill`. */
641export type SkillReader = (skillId: string, version: number) => Promise<StoredVersion | null>;
642
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)643const CODE_NAMES = new Set(CODE_TOOLS.map((tool) => tool.name));
644
645export type ToolContext = {
646 /** The agent replying. */
647 agentId: string;
648 /** Handles nobody may consult from here: the agent itself, and whoever sent it the work. */
649 notConsult: string[];
650 /** Hops so far: a consult is one more, and none is offered at the limit. */
651 hops: number;
652 maxHops: number;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked653 /** Whether this is a session's step (more calls, session tools) or a reply. */
654 session?: boolean;
655 /** Told of every call as it is made, for a session's transcript. */
656 onCall?: (call: ToolCall) => void;
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)657};
658
659export class ToolBox {
660 /** Every call this reply made, shared with the tool boxes of colleagues it consults: one budget for the reply. */
661 readonly calls: ToolCall[];
662 private readonly audience: Audience;
663 private readonly ports: ToolPorts;
664 private readonly context: ToolContext;
665
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked666 private readonly actions: ActionPorts | null;
667 /** Updates posted in this step. */
668 private updates = 0;
Merge branch 'worktree-agent-a1398e81ad1a64c5f'669 /** Hand-offs made in this reply. */
670 private handOffs = 0;
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.671 /** Artifacts read or made in this turn that not everyone here can read: never named here. */
672 private readonly notHere = new Set<string>();
673 /**
674 * Whether this turn read an artifact the whole workspace can't: then what
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.675 * it remembers is kept for the person who asked alone.
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.676 */
677 private privateRead = false;
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.678 /** The agent's skills this turn (skills.ts), and how to read a library skill's text. */
679 private shelf: readonly ShelfSkill[] = [];
680 private readSkill: SkillReader | null = null;
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.681 /** The agent's abilities this turn (abilities.ts), what carries them out, and what the asker said (for "alone when asked for it"). */
682 private sections: AbilitySection[] | null = null;
683 private abilityPorts: AbilityPorts | null = null;
684 private said = "";
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked685
686 constructor(audience: Audience, ports: ToolPorts, context: ToolContext, calls: ToolCall[] = [], actions: ActionPorts | null = null) {
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)687 this.audience = audience;
688 this.ports = ports;
689 this.context = context;
690 this.calls = calls;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked691 this.actions = actions;
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)692 }
693
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.694 /** The agent's own computer this session (computer.ts); null in a reply, which never has one. */
695 private computer: ComputerPorts | null = null;
696
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.697 /** Gives the agent its skills: `use_skill` is offered while it has any. */
698 useShelf(shelf: readonly ShelfSkill[], read: SkillReader): void {
699 this.shelf = shelf;
700 this.readSkill = read;
701 }
702
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.703 /**
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.704 * Gives a session its agent's computer. The tools are offered only once
705 * `useAbilities` said what the shell and files abilities allow, and only
706 * in a session: a reply is never given a computer.
707 */
708 useComputer(ports: ComputerPorts): void {
709 if (!this.context.session) return;
710 this.computer = ports;
711 }
712
713 /**
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.714 * Gives the agent its abilities: the sections `resolveAbilities` made
715 * for it, the ports that carry them out, and what the person said (the
716 * latest messages, or a session's goal), which "alone when asked for it"
717 * reads. Outside tools and MCP tools are offered from here on.
718 */
719 useAbilities(sections: AbilitySection[], ports: AbilityPorts, said: string, servers: McpServer[] = []): void {
720 this.sections = sections;
721 this.abilityPorts = ports;
722 this.said = said.slice(0, 20_000);
723 this.servers = servers;
724 }
725
726 /** The abilities of one group's sources, flat, with their source. */
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.727 private abilities(group: AbilityGroup): { ability: Ability; source: AbilitySource }[] {
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.728 return (this.sections ?? []).filter((section) => section.group === group).flatMap((section) => section.sources.flatMap((source) => source.abilities.map((ability) => ({ ability, source }))));
729 }
730
731 /** The MCP tools offered: every tool of every server whose ability isn't Never. */
732 private mcpTools(): { def: ToolDef; server: McpServer; tool: McpTool; ability: Ability; source: AbilitySource }[] {
733 if (!this.abilityPorts) return [];
734 const out: { def: ToolDef; server: McpServer; tool: McpTool; ability: Ability; source: AbilitySource }[] = [];
735 for (const { ability, source } of this.abilities("mcp")) {
736 if (ability.level === "never" || !source.connected) continue;
737 const server = this.servers.find((s) => s.id === source.id);
738 const tool = server?.tools.find((t) => `mcp:${server.id}:${t.name}` === ability.id);
739 if (!server || !tool) continue;
740 out.push({
741 def: {
742 name: mcpToolName(server.name, tool.name),
743 description: `${tool.description || tool.name} (a tool of the ${server.name} MCP server, outside g1t; ${tool.kind === "read" ? "it reads" : "it may change things there"}).`.slice(0, 1024),
744 input_schema: tool.input_schema && typeof tool.input_schema === "object" ? tool.input_schema : { type: "object", properties: {} },
745 },
746 server,
747 tool,
748 ability,
749 source,
750 });
751 }
752 return out;
753 }
754
755 /** The agent's MCP servers themselves (addresses and tools), given with `useAbilities`. */
756 private servers: McpServer[] = [];
757
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked758 /** A colleague's tool box for a consult: the same audience, the same budget, one hop further, reading only. */
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)759 forColleague(ports: ToolPorts, context: ToolContext): ToolBox {
760 return new ToolBox(this.audience, ports, context, this.calls);
761 }
762
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked763 /** The most calls this box makes. */
764 get maxCalls(): number {
765 return this.context.session ? MAX_SESSION_TOOL_CALLS : MAX_TOOL_CALLS;
766 }
767
768 /** Whether the person who asked can be acted for: resolved, and able to read code here. */
769 private canFile(): boolean {
770 return !!this.actions && !!this.audience.asker && this.audience.codeAllowed();
771 }
772
773 /**
774 * The tools offered: no code tools for an audience that can't read code,
775 * no consults or hand-offs at the hop limit, session tools only in a
776 * session, and a spin-off only from chat.
777 */
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)778 definitions(): ToolDef[] {
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked779 const roomForHop = this.context.hops + 1 <= this.context.maxHops;
780 const actions = this.actions;
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)781 return [
782 ...(this.audience.codeAllowed() ? CODE_TOOLS : []),
783 ...CHAT_TOOLS,
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.784 // Artifacts are for everyone, Code or not: the artifacts service decides what this person and audience can read.
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.785 ...(this.ports.folios && this.audience.asker ? FOLIO_TOOLS : []),
786 ...(this.ports.folios && this.audience.asker && actions ? FOLIO_WRITE_TOOLS : []),
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.787 ...(this.ports.folios?.attach && this.audience.asker && actions ? [MAKE_FILE] : []),
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked788 ...(roomForHop ? [ASK_COLLEAGUE] : []),
789 ...(actions ? [REMEMBER, FORGET] : []),
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar790 ...(this.canFile() ? [DRAFT_ISSUE] : []),
791 ...(this.canFile() && actions?.comment ? [COMMENT] : []),
792 ...(this.canFile() && actions?.review ? [REVIEW_PULL] : []),
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked793 ...(actions?.startSession && !this.context.session ? [START_SESSION] : []),
Merge branch 'worktree-agent-a1398e81ad1a64c5f'794 ...(actions?.handOff && !this.context.session && roomForHop ? [HAND_OFF] : []),
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked795 ...(actions?.postUpdate && this.context.session ? [POST_UPDATE] : []),
796 ...(actions?.useSubagent && this.context.session && roomForHop ? [USE_SUBAGENT] : []),
797 ...(actions?.bringIn && this.context.session && roomForHop ? [BRING_IN] : []),
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.798 ...(this.shelf.length ? [USE_SKILL] : []),
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.799 ...this.outsideTools(),
800 ...this.mcpTools().map((entry) => entry.def),
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.801 ...this.computerTools(),
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.802 ];
803 }
804
805 /**
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.806 * The computer's tools offered: in a session with a computer, each tool
807 * of a shell or files ability that is ready and not Never, for a person
808 * the agent acts for.
809 */
810 private computerTools(): ToolDef[] {
811 if (!this.computer || !this.context.session || !this.sections || !this.audience.asker) return [];
812 const live = this.abilities("computer").filter(({ ability }) => ability.status === "ready" && ability.level !== "never");
813 return COMPUTER_TOOLS.filter((tool) => live.some(({ ability }) => ability.tools.includes(tool.name)));
814 }
815
816 /**
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.817 * The outside tools offered: each only where some connected integration
818 * lets the agent use it at a level other than Never, for a person it can
819 * act for; `request_ability` whenever it has abilities at all.
820 */
821 private outsideTools(): ToolDef[] {
822 if (!this.sections || !this.abilityPorts || !this.audience.asker || !this.actions) return [];
823 const live = this.abilities("integration").filter(({ ability, source }) => source.connected && ability.level !== "never");
824 const offers = (tool: string) => live.some(({ ability }) => ability.tools.includes(tool));
825 return [
826 ...(offers("lookup_outside") ? [LOOKUP_OUTSIDE] : []),
827 ...(offers("import_outside") && this.canFile() ? [IMPORT_OUTSIDE] : []),
828 ...(offers("act_outside") ? [ACT_OUTSIDE] : []),
829 REQUEST_ABILITY,
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)830 ];
831 }
832
833 /** Whether another call may be made. */
834 get spent(): boolean {
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked835 return this.calls.length >= this.maxCalls;
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)836 }
837
838 async run(name: string, input: Record<string, unknown>): Promise<ToolResult> {
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked839 const result = await this.attempt(name, input);
840 const call: ToolCall = { tool: name, args: redact(input), outcome: result.outcome, bytes: result.text.length };
841 this.calls.push(call);
842 this.context.onCall?.(call);
843 return result;
844 }
845
846 private async attempt(name: string, input: Record<string, unknown>): Promise<ToolResult> {
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)847 let result: ToolResult;
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked848 const what = this.context.session ? "step" : "reply";
849 if (this.spent) result = { text: `No more tool calls in this ${what} (at most ${this.maxCalls}). Answer with what you have.`, outcome: "refused" };
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)850 else {
851 try {
852 result = await this.dispatch(name, input ?? {});
853 } catch (error) {
854 console.error("agents: a tool failed", name, String(error));
855 result = { text: "That didn't work just now. Answer with what you have.", outcome: "error" };
856 }
857 }
858 return result;
859 }
860
861 private withheld(): ToolResult {
862 return { text: WITHHELD, outcome: "withheld" };
863 }
864
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.865 /** One of the agent's skills, as `use_skill` reads it; only skills it has, and never a tool it lacks. */
866 private async skill(name: string, file: string | null): Promise<ToolResult> {
867 const entry = this.shelf.find((skill) => skill.name === name.toLowerCase());
868 if (!entry) {
869 const names = this.shelf.map((skill) => skill.name).join(", ");
870 return { text: `You have no skill called ${name || "that"}. Your skills: ${names || "none"}.`, outcome: "refused" };
871 }
872 const offered = new Set(this.definitions().map((tool) => tool.name));
873 if (entry.kind === "foundational") {
874 if (file) return { text: `${entry.name} is one of g1t's skills and has no files; its instructions are all there is.`, outcome: "refused" };
875 return { text: skillBlock(entry.skill, offered), outcome: "allowed" };
876 }
877 const stored = this.readSkill ? await this.readSkill(entry.id, entry.version) : null;
878 if (!stored) return { text: `${entry.name} couldn't be read just now. Do the work as you would without it.`, outcome: "error" };
879 return { text: skillText(entry, stored, offered, file), outcome: "allowed" };
880 }
881
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)882 private async dispatch(name: string, input: Record<string, unknown>): Promise<ToolResult> {
883 const asker = this.audience.asker;
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.884 if (OUTSIDE_NAMES.has(name)) return this.outside(name, input);
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.885 if (COMPUTER_NAMES.has(name)) return this.computerCall(name, input);
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.886 const mcp = this.mcpTools().find((entry) => entry.def.name === name);
887 if (mcp) return this.mcp(mcp, input);
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.888 if (FOLIO_NAMES.has(name)) {
889 if (!this.definitions().some((tool) => tool.name === name) || !asker || !this.ports.folios) return { text: `There is no tool called ${name} here.`, outcome: "refused" };
890 return this.folios(name, input, asker, this.ports.folios);
Docs: a workspace knowledge base people and agents write together891 }
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)892 if (CODE_NAMES.has(name)) {
893 // Not offered, and refused if asked for anyway: the check is here, not in the prompt.
894 if (!this.audience.codeAllowed() || !asker) return this.withheld();
895 return this.code(name, input, asker);
896 }
897 switch (name) {
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.898 case "use_skill":
899 return this.skill(String(input.name ?? "").trim(), typeof input.file === "string" && input.file.trim() ? input.file.trim() : null);
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)900 case "search_messages": {
901 const query = String(input.query ?? "").trim();
902 if (query.length < 2) return { text: "Search for at least two characters.", outcome: "refused" };
903 const found = await this.ports.searchMessages(query);
904 if (found === null) return { text: "Search didn't work just now.", outcome: "error" };
905 if (!found.length) return { text: "No messages found.", outcome: "allowed" };
906 return { text: untrusted(`search_messages "${query}"`, found.map(messageLine).join("\n")), outcome: "allowed" };
907 }
908 case "read_thread": {
909 const thread = await this.ports.readThread(String(input.channel ?? ""), String(input.id ?? ""));
910 if (!thread || !thread.length) return this.withheld();
911 return { text: untrusted("read_thread", thread.map(messageLine).join("\n")), outcome: "allowed" };
912 }
913 case "workspace_roster":
914 return { text: untrusted("workspace_roster", await this.ports.roster(asker)), outcome: "allowed" };
915 case "ask_colleague": {
916 const handle = String(input.handle ?? "").trim().replace(/^@/, "").toLowerCase();
917 const question = String(input.question ?? "").trim();
918 if (!handle || !question) return { text: "Name the colleague and the question.", outcome: "refused" };
919 if (this.context.hops + 1 > this.context.maxHops) return { text: "This request has been passed along too many times; answer with what you have.", outcome: "refused" };
920 if (this.context.notConsult.includes(handle)) {
921 return { text: `You can't consult @${handle} here: they sent you this work, or it is you. Answer with what you have.`, outcome: "refused" };
922 }
923 const answer = await this.ports.consult(handle, question.slice(0, 2000));
924 if (!answer.ok) return { text: answer.message, outcome: "refused" };
925 return { text: untrusted(`@${answer.colleague}'s answer`, answer.answer), outcome: "allowed" };
926 }
927 default:
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked928 return this.act(name, input);
929 }
930 }
931
Agents recall what Docs say before they answer or work, and each has required reading932 /**
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.933 * What the workspace's artifacts say about `query`, for this person and
934 * this audience, before the agent answers: no tool call, nothing counted
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.935 * against its tools. Empty when there is no artifacts service or nothing
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.936 * relevant.
Agents recall what Docs say before they answer or work, and each has required reading937 */
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.938 async recall(query: string | null, spaces: string[]): Promise<FolioPassage[]> {
939 const folios = this.ports.folios;
Agents recall what Docs say before they answer or work, and each has required reading940 const asker = this.audience.asker;
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.941 if (!folios || !asker || !query) return [];
Agents recall what Docs say before they answer or work, and each has required reading942 try {
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.943 return (await folios.recall(asker, this.folioAudience(), query, spaces)) ?? [];
Agents recall what Docs say before they answer or work, and each has required reading944 } catch (error) {
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.945 console.error("agents: artifacts recall failed", String(error));
Agents recall what Docs say before they answer or work, and each has required reading946 return [];
947 }
948 }
949
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.950 /** Who reads what an agent says here, as the artifacts service takes it. */
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.951 private folioAudience(): FolioAudience {
Docs: a workspace knowledge base people and agents write together952 return this.audience.shared ? { kind: "workspace" } : { kind: "people", user_ids: this.audience.members.map((m) => m.id) };
953 }
954
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.955 /** Whether anyone besides the person who asked reads what is said here. */
956 private othersHere(asker: User): boolean {
957 return this.audience.shared || this.audience.members.some((m) => m.id !== asker.id);
958 }
959
960 private async folios(name: string, input: Record<string, unknown>, asker: User, folios: FoliosPorts): Promise<ToolResult> {
Docs: a workspace knowledge base people and agents write together961 const text = (key: string, max: number) => String(input[key] ?? "").trim().slice(0, max);
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.962 const audience = this.folioAudience();
Docs: a workspace knowledge base people and agents write together963 switch (name) {
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.964 case "list_spaces": {
965 const spaces = await folios.spaces(asker, audience);
966 if (spaces === null) return { text: "Spaces couldn't be listed just now.", outcome: "error" };
967 if (!spaces.length) return { text: "There are no spaces everyone here can read.", outcome: "allowed" };
968 return { text: untrusted("list_spaces", spaces.map(spaceLine).join("\n")), outcome: "allowed" };
969 }
970 case "search_artifacts": {
Docs: a workspace knowledge base people and agents write together971 const query = text("query", 200);
972 if (query.length < 2) return { text: "Search for at least two characters.", outcome: "refused" };
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.973 const kind = text("kind", 20) || null;
974 if (kind && !isFolioKind(kind)) return { text: `There is no kind of artifact called ${kind}: it is doc, slides, design or dashboard.`, outcome: "refused" };
975 let spaceId: string | null = null;
976 if (text("space", 200)) {
977 const space = findSpace((await folios.spaces(asker, audience)) ?? [], text("space", 200));
978 if (!space) return this.withheld();
979 spaceId = space.id;
980 }
981 const found = await folios.search(asker, audience, { query, kind: kind as FolioKind | null, space_id: spaceId, project: text("project", 200).toLowerCase() || null });
982 if (found === null) return { text: "Search didn't work just now.", outcome: "error" };
983 return { text: untrusted(`search_artifacts "${query}"`, found), outcome: "allowed" };
984 }
985 case "stale_artifacts": {
986 const found = await folios.stale(asker, audience, text("repo", 200).toLowerCase() || null);
987 if (found === null) return { text: "Out-of-date artifacts couldn't be listed just now.", outcome: "error" };
988 return { text: untrusted("stale_artifacts", found), outcome: "allowed" };
Docs: a workspace knowledge base people and agents write together989 }
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.990 case "read_artifact": {
991 const id = folioRef(text("id", 500));
992 if (!id) return { text: "Give the artifact's id (fol_…) or its link.", outcome: "refused" };
993 const found = await folios.read(asker, audience, id);
994 if (!found.ok) return found.code === "not_found" || found.code === "forbidden" ? this.withheld() : { text: found.message, outcome: "refused" };
995 const read = found.value;
996 if (!this.audience.shared || !read.audience_can_read) this.privateRead = true;
997 if (!read.audience_can_read) return this.notForEveryone(asker, read.folio, folios, "found");
998 return { text: untrusted(`read_artifact ${id}`, folioReadText(read)), outcome: "allowed" };
Docs: a workspace knowledge base people and agents write together999 }
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.1000 case "create_artifact": {
1001 const kind = text("kind", 20) || "doc";
1002 if (!isFolioKind(kind)) return { text: `There is no kind of artifact called ${kind}.`, outcome: "refused" };
1003 if (kind !== "doc") return { text: NOT_YET, outcome: "refused" };
1004 const title = text("title", 200);
1005 const template = text("template", 100) || null;
1006 const markdown = String(input.content ?? "").slice(0, 100_000);
1007 if (!title) return { text: "An artifact needs a title.", outcome: "refused" };
1008 if (!markdown.trim() && !template) return { text: "Give its content as Markdown, or a template.", outcome: "refused" };
1009 const parentGiven = text("parent", 500);
1010 const parent = parentGiven ? folioRef(parentGiven) : null;
1011 if (parentGiven && !parent) return { text: "Give the parent doc's id or link.", outcome: "refused" };
1012 const place = await this.whereFor(input.where, asker, folios);
1013 if (!place.ok) return { text: place.message, outcome: "refused" };
1014 const make = (where: FolioWhere) =>
1015 folios.create(asker, { kind, title, markdown: template ? null : markdown, template_id: template, where, parent_id: parent, source: sourceLink(text("source", 2000)) });
1016 let made = await make(place.where);
1017 // The General space by default, unless the person who asked can't add there: then their Private.
1018 if (!made.ok && place.fallback && made.code === "forbidden") made = await make("private");
1019 if (!made.ok) return { text: `It couldn't be made: ${made.message}`, outcome: "refused" };
1020 const ref = made.value;
1021 if (this.othersHere(asker)) {
1022 const check = await folios.read(asker, audience, ref.id).catch(() => null);
1023 if (!check?.ok || !check.value.audience_can_read) return this.notForEveryone(asker, ref, folios, "made");
1024 }
1025 return { text: `Wrote ${ref.title} (${ref.path}, id ${ref.id}). Link it.`, outcome: "allowed" };
1026 }
1027 case "edit_artifact": {
1028 const id = folioRef(text("id", 500));
Docs: a workspace knowledge base people and agents write together1029 const markdown = String(input.markdown ?? "").slice(0, 100_000);
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.1030 if (!id || !markdown.trim()) return { text: "Give the artifact's id or link, and the Markdown.", outcome: "refused" };
Docs: a workspace knowledge base people and agents write together1031 const kind = text("target", 20);
1032 const target: DocEditTarget | null =
1033 kind === "append"
1034 ? { kind: "append" }
1035 : kind === "document"
1036 ? { kind: "document" }
1037 : kind === "section" && text("heading", 300)
1038 ? { kind: "section", heading: text("heading", 300) }
1039 : kind === "blocks" && text("from_block", 100) && text("to_block", 100)
1040 ? { kind: "blocks", from_block: text("from_block", 100), to_block: text("to_block", 100) }
1041 : null;
1042 if (!target) return { text: "Say what to change: append, a section by its heading, blocks by their ids, or the whole document.", outcome: "refused" };
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.1043 const edit: FolioAgentEdit = { kind: "doc", target, markdown, note: text("note", 300) || null, suggest_only: input.suggest_only === true, marks_current: input.marks_current === true };
1044 const done = await folios.edit(asker, id, edit);
1045 if (!done.ok) return { text: `That didn't work: ${done.message}`, outcome: "refused" };
1046 return { text: editMessage(done.value, !this.notHere.has(done.value.folio.id)), outcome: "allowed" };
Docs: a workspace knowledge base people and agents write together1047 }
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.1048 case "share_artifact": {
1049 if (this.audience.shared) return { text: "You can share only in a direct message or a private channel. Ask the person to use Share on the artifact instead.", outcome: "refused" };
1050 const id = folioRef(text("id", 500));
1051 if (!id) return { text: "Give the artifact's id (fol_…) or its link.", outcome: "refused" };
1052 const role = input.role === "view" || input.role === "comment" ? input.role : null;
1053 if (!role) return { text: "You can share to view or comment only. For more, ask the person to use Share.", outcome: "refused" };
1054 const named = (Array.isArray(input.people) ? input.people : []).filter((p): p is string => typeof p === "string").map((p) => p.trim().replace(/^@/, "").toLowerCase()).filter(Boolean).slice(0, 50);
1055 const people = named.map((n) => this.audience.members.find((m) => m.username.toLowerCase() === n || m.id === n) ?? n);
1056 const outside = people.filter((p): p is string => typeof p === "string");
1057 if (outside.length) return { text: `${outside.map((n) => `@${n}`).join(", ")} ${outside.length === 1 ? "isn't" : "aren't"} in this conversation: you can share only with people in it.`, outcome: "refused" };
1058 const users = [...new Map((people as User[]).filter((u) => u.id !== asker.id).map((u) => [u.id, u])).values()];
1059 if (!users.length) return { text: "Name who to share it with: people in this conversation besides the person who asked.", outcome: "refused" };
1060 const done = await folios.share(asker, audience, id, users.map((u) => u.id), role);
1061 if (!done.ok) return { text: `It couldn't be shared: ${done.message}`, outcome: "refused" };
1062 return { text: `Shared with ${users.map((u) => `@${u.username}`).join(", ")}: they can ${role} it.`, outcome: "allowed" };
Docs: a workspace knowledge base people and agents write together1063 }
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.1064 case "make_file":
1065 return this.makeFile(input, asker, folios);
Docs: a workspace knowledge base people and agents write together1066 default:
1067 return { text: `There is no tool called ${name}.`, outcome: "refused" };
1068 }
1069 }
1070
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.1071 /**
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.1072 * A file someone asked for (files.ts), kept with a doc: the one named,
1073 * which the asker must be able to edit, or a new one holding the content.
1074 * Where not everyone here can read that doc, the link goes to the asker
1075 * directly, as for any artifact.
1076 */
1077 private async makeFile(input: Record<string, unknown>, asker: User, folios: FoliosPorts): Promise<ToolResult> {
1078 if (!folios.attach) return { text: "There is no tool called make_file here.", outcome: "refused" };
1079 const title = String(input.title ?? "").trim().slice(0, 200);
1080 const made = makeFile({ format: input.format, title, content: input.content, sheets: input.sheets });
1081 if (!made.ok) return { text: made.message, outcome: "refused" };
1082 const file = made.file;
1083 const audience = this.folioAudience();
1084 const given = String(input.artifact ?? "").trim().slice(0, 500);
1085 let ref: FolioRef;
1086 let hidden = false;
1087 if (given) {
1088 const id = folioRef(given);
1089 if (!id) return { text: "Give the doc's id (fol_…) or its link, or leave artifact out for a new doc.", outcome: "refused" };
1090 const found = await folios.read(asker, audience, id);
1091 if (!found.ok) return found.code === "not_found" || found.code === "forbidden" ? this.withheld() : { text: found.message, outcome: "refused" };
1092 if (!found.value.can.edit) return { text: "You can't edit that doc for them, so nothing can be attached to it. Leave artifact out to make a new doc.", outcome: "refused" };
1093 ref = found.value.folio;
1094 if (!this.audience.shared || !found.value.audience_can_read) this.privateRead = true;
1095 hidden = !found.value.audience_can_read;
1096 } else {
1097 const place = await this.whereFor(input.where, asker, folios);
1098 if (!place.ok) return { text: place.message, outcome: "refused" };
1099 const body = docBody(file.format, title, input);
1100 const make = (where: FolioWhere) => folios.create(asker, { kind: "doc", title, markdown: body, template_id: null, where, parent_id: null, source: null });
1101 let created = await make(place.where);
1102 if (!created.ok && place.fallback && created.code === "forbidden") created = await make("private");
1103 if (!created.ok) return { text: `The doc to keep it in couldn't be made: ${created.message}`, outcome: "refused" };
1104 ref = created.value;
1105 if (this.othersHere(asker)) {
1106 const check = await folios.read(asker, audience, ref.id).catch(() => null);
1107 hidden = !check?.ok || !check.value.audience_can_read;
1108 }
1109 }
1110 const kept = await folios.attach(asker, ref.id, { name: file.name, content_type: file.content_type, bytes: file.bytes });
1111 if (!kept.ok) return { text: `The file was made but couldn't be kept: ${kept.message}`, outcome: "refused" };
1112 const size = sizeLabel(kept.value.bytes);
1113 // The doc links its file, so whoever opens the doc finds it.
1114 await folios
1115 .edit(asker, ref.id, { kind: "doc", target: { kind: "append" }, markdown: `**File:** [${kept.value.name.replace(/[[\]]/g, "")}](${kept.value.url}) (${size})`, note: `Attached ${kept.value.name}`, suggest_only: false, marks_current: false })
1116 .catch(() => null);
1117 if (hidden) return this.notForEveryone(asker, ref, folios, "made");
1118 return {
1119 text: `Made ${kept.value.name} (${file.summary}, ${size}) and kept it with the doc ${ref.title} (${ref.path}). Give them the file's link, ${kept.value.url}, and the doc's if it helps.`,
1120 outcome: "allowed",
1121 };
1122 }
1123
1124 /**
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.1125 * Where a new artifact goes:
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.1126 * - a space named by its name or id, among those everyone here can read;
1127 * - "private": the asker's Private;
1128 * - "conversation": Private, plus `view` for this conversation's people;
1129 * - nothing: the conversation in a DM or private channel, and in a public
1130 * channel the General space if the asker can add there (`fallback`:
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.1131 * their Private if the artifacts service says they can't).
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.1132 */
1133 private async whereFor(given: unknown, asker: User, folios: FoliosPorts): Promise<{ ok: true; where: FolioWhere; fallback: boolean } | { ok: false; message: string }> {
1134 const named = typeof given === "object" && given !== null ? (given as { space?: unknown }).space : given;
1135 const wanted = typeof named === "string" ? named.trim().slice(0, 200) : "";
1136 const others = this.audience.members.filter((m) => m.id !== asker.id).map((m) => m.id);
1137 const byDefault = async (): Promise<{ ok: true; where: FolioWhere; fallback: boolean }> => {
1138 if (!this.audience.shared) return { ok: true, where: others.length ? { conversation: this.audience.members.map((m) => m.id) } : "private", fallback: false };
1139 const spaces = (await folios.spaces(asker, this.folioAudience())) ?? [];
1140 const general = spaces.find((s) => s.slug === "general") ?? spaces.find((s) => s.name.toLowerCase() === "general");
1141 return general && general.can.suggest ? { ok: true, where: { space_id: general.id }, fallback: true } : { ok: true, where: "private", fallback: false };
1142 };
1143 if (!wanted) return byDefault();
1144 const lower = wanted.toLowerCase();
1145 if (typeof given === "string" && lower === "private") return { ok: true, where: "private", fallback: false };
1146 // A public channel has no list of people to share with: its default instead.
1147 if (typeof given === "string" && lower === "conversation") return byDefault();
1148 const space = findSpace((await folios.spaces(asker, this.folioAudience())) ?? [], wanted);
1149 if (space) return { ok: true, where: { space_id: space.id }, fallback: false };
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.1150 // An id the asker gave, for a space not everyone here can read: the artifacts service checks it.
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.1151 if (/^spc_[A-Za-z0-9]+$/.test(wanted)) return { ok: true, where: { space_id: wanted }, fallback: false };
1152 return { ok: false, message: `There's no space called ${wanted} that everyone here can read. Use list_spaces, or put it in "private" or "conversation".` };
1153 }
1154
1155 /**
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.1156 * An artifact someone here can't read: the link goes to the asker
1157 * directly, and the agent says only that it found or made something,
1158 * never what.
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.1159 */
1160 private async notForEveryone(asker: User, folio: FolioRef, folios: FoliosPorts, what: "found" | "made"): Promise<ToolResult> {
1161 this.notHere.add(folio.id);
1162 const who = `@${asker.username}`;
1163 const note =
1164 what === "made"
1165 ? "I made this for you. Not everyone in the conversation you asked from can open it, so here is the link:"
1166 : "Here is what you asked about. Not everyone in the conversation you asked from can open it, so here is the link:";
1167 const sent = await folios.sendLink(asker, { title: folio.title, path: folio.path }, note).catch(() => false);
1168 const lead = `Not everyone in this conversation can read this artifact, so ${what === "made" ? "it" : "its content"} isn't shown here. Don't quote it, name it or describe it here;`;
1169 const text = sent
1170 ? `${lead} say you ${what} it and that you've sent the link to ${who} directly.`
1171 : what === "made"
1172 ? `${lead} say you made it and that ${who} will find it under Artifacts, in their Private or shared with them.`
1173 : `${lead} say you found it but can't share it here, and ask ${who} to message you directly.`;
1174 return { text, outcome: what === "made" ? "allowed" : "withheld" };
1175 }
1176
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked1177 /** Doing, not reading: memory, issues, sessions. Each refused unless offered. */
1178 private async act(name: string, input: Record<string, unknown>): Promise<ToolResult> {
1179 const actions = this.actions;
1180 const offered = this.definitions().some((tool) => tool.name === name);
1181 if (!actions || !offered) return { text: `There is no tool called ${name} here.`, outcome: "refused" };
1182 const said = (answer: { ok: boolean; message: string }): ToolResult => ({ text: answer.message, outcome: answer.ok ? "allowed" : "refused" });
1183 const text = (key: string, max: number) => String(input[key] ?? "").trim().slice(0, max);
1184 switch (name) {
1185 case "remember": {
1186 const fact = text("fact", 2000);
1187 if (!fact) return { text: "Say what to remember.", outcome: "refused" };
1188 const scope = input.scope === "workspace" || input.scope === "channel" || input.scope === "person" ? input.scope : null;
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.1189 return said(await actions.remember(fact, scope, this.privateRead));
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked1190 }
1191 case "forget":
1192 return said(await actions.forget(text("id", 100)));
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1193 case "draft_issue": {
1194 if (!this.audience.asker || !this.audience.codeAllowed()) return this.withheld();
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked1195 const repo = await this.audience.repo(input.repo);
1196 if (!repo) return this.withheld();
1197 const title = text("title", 200);
1198 const body = text("body", 20_000);
1199 if (!title || !body) return { text: "An issue needs a title and a body.", outcome: "refused" };
1200 const labels = Array.isArray(input.labels)
1201 ? input.labels.filter((l): l is string => typeof l === "string").map((l) => l.trim()).filter(Boolean).slice(0, 5)
1202 : [];
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1203 return said(await actions.draftIssue(repo, { title, body, labels }));
1204 }
1205 case "comment":
1206 case "review_pull": {
1207 const asker = this.audience.asker;
1208 if (!asker || !this.audience.codeAllowed()) return this.withheld();
1209 const repo = await this.audience.repo(input.repo);
1210 if (!repo) return this.withheld();
1211 const number = Math.floor(Number(input.number));
1212 if (!Number.isFinite(number) || number < 1) return { text: "Give the issue or pull request's number.", outcome: "refused" };
1213 const body = text("body", 20_000);
1214 if (name === "comment") {
1215 if (!body) return { text: "Say what to comment.", outcome: "refused" };
1216 return said(await actions.comment!(repo, asker, number, body));
1217 }
1218 const verdict = input.verdict === "approve" || input.verdict === "request_changes" ? input.verdict : "comment";
1219 if (!body && verdict !== "approve") return { text: "A review needs its text.", outcome: "refused" };
1220 return said(await actions.review!(repo, asker, number, verdict, body));
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked1221 }
1222 case "start_session": {
1223 const title = text("title", 120);
1224 const goal = text("goal", 8000);
1225 if (!title || !goal) return { text: "A session needs a title and a goal.", outcome: "refused" };
1226 return said(await actions.startSession!(title, goal));
1227 }
1228 case "post_update": {
1229 const note = text("text", 2000);
1230 if (!note) return { text: "Say what to post.", outcome: "refused" };
1231 if (this.updates >= 3) return { text: "You've posted enough updates for this step; carry on with the work.", outcome: "refused" };
1232 this.updates++;
1233 return said(await actions.postUpdate!(note));
1234 }
1235 case "use_subagent": {
1236 const helper = text("name", 60).toLowerCase();
1237 const brief = text("brief", 8000);
1238 if (!helper || !brief) return { text: "Name the subagent and give it a brief.", outcome: "refused" };
1239 return said(await actions.useSubagent!(helper, brief));
1240 }
1241 case "bring_in": {
1242 const handle = text("handle", 60).replace(/^@/, "").toLowerCase();
1243 const brief = text("brief", 8000);
1244 if (!handle || !brief) return { text: "Name the colleague and give them a brief.", outcome: "refused" };
1245 if (this.context.notConsult.includes(handle)) return { text: `You can't bring in @${handle} here: they sent you this work, or it is you.`, outcome: "refused" };
1246 return said(await actions.bringIn!(handle, brief));
1247 }
Merge branch 'worktree-agent-a1398e81ad1a64c5f'1248 case "hand_off": {
1249 const handle = text("handle", 60).replace(/^@/, "").toLowerCase();
1250 const brief = text("brief", 8000);
1251 if (!handle || !brief) return { text: "Name the colleague and give them a brief.", outcome: "refused" };
1252 if (this.context.notConsult.includes(handle)) {
1253 return { text: `You can't hand work to @${handle}: they sent you this work, or it is you. Answer with what you have.`, outcome: "refused" };
1254 }
1255 if (this.handOffs >= MAX_HAND_OFFS) return { text: `You've handed off to ${MAX_HAND_OFFS} colleagues from this message; hand off the rest later.`, outcome: "refused" };
1256 const done = await actions.handOff!(handle, brief);
1257 if (done.ok) this.handOffs++;
1258 return said(done);
1259 }
Agents work in sessions: bounded, visible, steerable work spun off from chat, with subagents and colleagues in a tree paid by its root; memory with sources and scopes; routines; a workspace budget for every agent; agents file issues for whoever asked1260 default:
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1261 return { text: `There is no tool called ${name}.`, outcome: "refused" };
1262 }
1263 }
1264
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.1265 /**
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.1266 * The computer's tools, each gated by its ability (`computer:shell` for
1267 * commands, `computer:files` for files) before the runner is asked. A
1268 * computer that could not wake because the plan refused is said plainly,
1269 * so the agent reports it rather than trying another way.
1270 */
1271 private async computerCall(name: string, input: Record<string, unknown>): Promise<ToolResult> {
1272 const asker = this.audience.asker;
1273 const ports = this.computer;
1274 if (!ports || !asker || !this.definitions().some((tool) => tool.name === name)) return { text: `There is no tool called ${name} here.`, outcome: "refused" };
1275 const own = this.abilities("computer").find(({ ability }) => ability.tools.includes(name));
1276 if (!own) return { text: `There is no tool called ${name} here.`, outcome: "refused" };
1277 const text = (key: string, max: number) => String(input[key] ?? "").trim().slice(0, max);
1278 const failed = (done: { code: string; message: string }, what: string): ToolResult =>
1279 done.code === "payment_required"
1280 ? { text: `Your computer couldn't wake: ${done.message} Say so in your report; don't try another way.`, outcome: "refused" }
1281 : { text: `${what}: ${done.message}`, outcome: done.code === "not_found" || done.code === "invalid" ? "refused" : "error" };
1282 switch (name) {
1283 case "run_command": {
1284 const command = text("command", 8000);
1285 if (!command) return { text: "Give the command to run.", outcome: "refused" };
1286 const cwd = text("cwd", 500) || null;
1287 const timeout = Number.isFinite(Number(input.timeout_seconds)) && Number(input.timeout_seconds) > 0 ? Math.min(1800, Math.floor(Number(input.timeout_seconds))) : null;
1288 const gated = await this.gate(own, { tool: name, args: input, summary: `Run \`${command.split("\n")[0].slice(0, 80)}\` on its computer`, keys: [...COMPUTER_ASKED_WORDS, own.ability.label] }, asker);
1289 if (!gated.ok) return gated.result;
1290 const done = await ports.exec(command, cwd, timeout);
1291 if (!done.ok) return failed(done, "The command couldn't run");
1292 const ran = done.value;
1293 const how = [`exit ${ran.exit_code}`, `${(ran.duration_ms / 1000).toFixed(1)} s`, ran.timed_out ? "killed at the timeout" : null, ran.truncated ? "output cut" : null].filter(Boolean).join(", ");
1294 return { text: `$ ${ran.cmd}\n(${how}; in ${ran.cwd})\n${untrusted("run_command on your computer", ran.output || "(no output)")}`, outcome: "allowed" };
1295 }
1296 case "computer_read_file": {
1297 const path = text("path", 1000);
1298 if (!path) return { text: "Give the file's path under /home/agent.", outcome: "refused" };
1299 const gated = await this.gate(own, { tool: name, args: input, summary: `Read ${path} on its computer`, keys: [path, "read", "file", "open", "look", own.ability.label] }, asker);
1300 if (!gated.ok) return gated.result;
1301 const done = await ports.readFile(path);
1302 if (!done.ok) return failed(done, "The file couldn't be read");
1303 return { text: untrusted(`${done.value.path} on your computer`, done.value.text), outcome: "allowed" };
1304 }
1305 case "computer_write_file": {
1306 const path = text("path", 1000);
1307 const body = String(input.text ?? "");
1308 if (!path) return { text: "Give the file's path under /home/agent.", outcome: "refused" };
1309 const gated = await this.gate(own, { tool: name, args: input, summary: `Write ${path} on its computer`, keys: [path, "write", "save", "create", "file", "note", own.ability.label] }, asker);
1310 if (!gated.ok) return gated.result;
1311 const done = await ports.writeFile(path, body);
1312 if (!done.ok) return failed(done, "The file couldn't be written");
1313 return { text: `Wrote ${done.value.bytes} bytes to ${done.value.path}.`, outcome: "allowed" };
1314 }
1315 default:
1316 return { text: `There is no tool called ${name}.`, outcome: "refused" };
1317 }
1318 }
1319
1320 /**
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.1321 * The rule for one ability, applied before a call: Never refuses and
1322 * names the rule; the asker's own connection must exist; Ask first posts
1323 * the card and tells the agent to wait; "alone when asked for it" is
1324 * alone only when what the person said names the item or the ability.
1325 */
1326 private async gate(found: { ability: Ability; source: AbilitySource }, call: { tool: string; args: Record<string, unknown>; summary: string; keys: string[] }, asker: User): Promise<Gated> {
1327 const { ability, source } = found;
1328 const ports = this.abilityPorts!;
1329 const rule = `${source.name}: ${ability.label}`;
1330 const refused = (text: string): Gated => ({ ok: false, result: { text, outcome: "refused" } });
1331 if (!source.connected) return refused(`${source.name} isn't connected to this workspace, so you can't ${ability.label.toLowerCase()} there. Say so, and use request_ability if they want it connected.`);
1332 if (ability.level === "never") {
1333 ports.refused({ ability, source, rule: `${ability.id}=never`, call: call.summary });
1334 return refused(`Not allowed: your abilities say "${rule}" is Never. Tell them plainly you aren't allowed to, without trying another way; an owner can change it on your Abilities tab.`);
1335 }
1336 if (ability.credentials === "asker" && !(await ports.askerConnected(source.id, asker))) {
1337 const posted = await ports.connect(source, ability);
1338 ports.refused({ ability, source, rule: `${ability.id}=asker-not-connected`, call: call.summary });
1339 return refused(
1340 `This runs on @${asker.username}'s own ${source.name} connection, and they haven't connected one. ${posted ? "A Connect card was posted: tell them to press it, then ask you again." : "Ask them to connect it under their Integrations settings, then ask you again."}`,
1341 );
1342 }
1343 let note: string | null = null;
1344 if (ability.level === "asked") {
1345 if (askedFor(this.said, call.keys)) return { ok: true };
1346 note = `${ability.label} in ${source.name} runs on its own only when asked for it, and this wasn't.`;
1347 } else if (ability.level !== "ask") return { ok: true };
1348 const id = await ports.askFirst({ ability, source, tool: call.tool, args: call.args, summary: call.summary, note });
1349 ports.refused({ ability, source, rule: `${ability.id}=${ability.level}`, call: call.summary });
1350 if (!id) return refused(`"${rule}" is ${levelWords(ability.level)}, and the card asking for it couldn't be posted just now. Say what you'd do and ask them to allow it.`);
1351 return refused(
1352 `${note ? `${note} ` : ""}"${rule}" is ${levelWords(ability.level)}: a card was posted asking @${asker.username} (or an owner) to allow it. Don't do it another way. Say in a sentence that it's waiting on their OK, and carry on with the rest.${this.context.session ? " Your session pauses at the end of this step until they answer; the result comes to you then." : ""}`,
1353 );
1354 }
1355
1356 /** The ability `tool` on the connector `provider`, among the agent's. */
1357 private integrationAbility(provider: string, tool: string): { ability: Ability; source: AbilitySource } | null {
1358 return this.abilities("integration").find(({ ability, source }) => source.id === provider.toLowerCase() && ability.tools.includes(tool)) ?? null;
1359 }
1360
1361 /** The outside tools: each checked against the agent's abilities for the system that knows the item. */
1362 private async outside(name: string, input: Record<string, unknown>): Promise<ToolResult> {
1363 const asker = this.audience.asker;
1364 const ports = this.abilityPorts;
1365 if (!ports || !this.sections || !asker || !this.actions || !this.definitions().some((tool) => tool.name === name)) return { text: `There is no tool called ${name} here.`, outcome: "refused" };
1366 const text = (key: string, max: number) => String(input[key] ?? "").trim().slice(0, max);
1367 const reference = text("reference", 500);
1368 const notFound = (): ToolResult => ({ text: `No connected integration knows ${reference || "that"}. If it's in a system that isn't connected, say so; request_ability lets them ask for it.`, outcome: "refused" });
1369 switch (name) {
1370 case "request_ability": {
1371 const needs = text("needs", 120).toLowerCase();
1372 const why = text("why", 500);
1373 if (!needs || !why) return { text: "Say what is needed and why.", outcome: "refused" };
1374 const abilityFound = this.abilities("integration").find(({ ability }) => ability.id === needs) ?? this.abilities("mcp").find(({ ability }) => ability.id === needs) ?? null;
1375 const connector = abilityFound ? null : needs.replace(/^integration:/, "").split(":")[0]!;
1376 const posted = await ports.request({ connector, ability: abilityFound?.ability ?? null, source: abilityFound?.source ?? null, why });
1377 if (!posted) return { text: "The request card couldn't be posted just now. Say what's needed and that an owner can add it from the Marketplace or your Abilities tab.", outcome: "error" };
1378 return { text: "A Request card was posted. Say in a sentence what it asks for, and that the owners will see it once they press it.", outcome: "allowed" };
1379 }
1380 case "lookup_outside": {
1381 if (!reference) return { text: "Give the item's key or address.", outcome: "refused" };
1382 const found = await ports.lookup(asker, reference);
1383 if (!found.ok) return found.code === "not_found" ? notFound() : { text: found.message, outcome: found.code === "forbidden" ? "withheld" : "refused" };
1384 const item = found.value;
1385 const own = this.integrationAbility(item.provider, "lookup_outside");
1386 if (!own) return notFound();
1387 const gated = await this.gate(own, { tool: name, args: input, summary: `Read ${item.key} in ${own.source.name}`, keys: [item.key, reference, own.ability.label] }, asker);
1388 if (!gated.ok) return gated.result;
1389 return { text: untrusted(`${own.source.name} ${item.key}`, `${item.key}: ${item.title}${item.status ? ` [${item.status}]` : ""}
1390${item.url}
1391
1392${item.body}`), outcome: "allowed" };
1393 }
1394 case "import_outside": {
1395 if (!reference) return { text: "Give the item's key or address.", outcome: "refused" };
1396 if (!this.audience.codeAllowed()) return this.withheld();
1397 const repo = await this.audience.repo(input.repo);
1398 if (!repo) return this.withheld();
1399 const found = await ports.lookup(asker, reference);
1400 if (!found.ok) return found.code === "not_found" ? notFound() : { text: found.message, outcome: found.code === "forbidden" ? "withheld" : "refused" };
1401 const own = this.integrationAbility(found.value.provider, "import_outside");
1402 if (!own) return { text: `Importing from ${found.value.provider} isn't one of your abilities.`, outcome: "refused" };
1403 const gated = await this.gate(own, { tool: name, args: input, summary: `Import ${found.value.key} from ${own.source.name} into ${repo.namespace}/${repo.name}`, keys: [found.value.key, reference, "import"] }, asker);
1404 if (!gated.ok) return gated.result;
1405 const done = await ports.import(asker, repo, reference);
1406 if (!done.ok) return { text: `It couldn't be imported: ${done.message}`, outcome: "refused" };
1407 const path = `/${repo.namespace}/${repo.name}/issues/${done.value.number}`;
1408 return { text: `${done.value.created ? "Opened" : "Already imported as"} ${repo.namespace}/${repo.name}#${done.value.number} from ${done.value.item.key}. Link it: ${path}`, outcome: "allowed" };
1409 }
1410 case "act_outside": {
1411 const action = input.action === "resolve" ? "resolve" : input.action === "comment" ? "comment" : null;
1412 const body = String(input.text ?? "").trim().slice(0, 8000);
1413 if (!reference || !action) return { text: "Give the item, and whether to comment or resolve.", outcome: "refused" };
1414 if (!body) return { text: "Say what to write.", outcome: "refused" };
1415 const found = await ports.lookup(asker, reference);
1416 if (!found.ok) return found.code === "not_found" ? notFound() : { text: found.message, outcome: found.code === "forbidden" ? "withheld" : "refused" };
1417 const item = found.value;
1418 const own = this.abilities("integration").find(({ ability, source }) => source.id === item.provider.toLowerCase() && ability.id.endsWith(`:${action}`)) ?? null;
1419 if (!own) return { text: `${action === "resolve" ? "Resolving" : "Commenting on"} items in ${item.provider} isn't one of your abilities.`, outcome: "refused" };
1420 const summary = action === "resolve" ? `Resolve ${item.key} in ${own.source.name}` : `Comment on ${item.key} in ${own.source.name}`;
1421 const gated = await this.gate(own, { tool: name, args: input, summary, keys: [item.key, reference, action, own.ability.label] }, asker);
1422 if (!gated.ok) return gated.result;
1423 const done = await ports.act(asker, reference, action, body);
1424 if (!done.ok) return { text: `It didn't work: ${done.message}`, outcome: "refused" };
1425 return { text: `${action === "resolve" ? "Resolved" : "Commented on"} ${done.value.key} in ${own.source.name} (${done.value.url}).`, outcome: "allowed" };
1426 }
1427 default:
1428 return { text: `There is no tool called ${name}.`, outcome: "refused" };
1429 }
1430 }
1431
1432 /** A tool of one of the agent's MCP servers, gated by its ability. */
1433 private async mcp(entry: { def: ToolDef; server: McpServer; tool: McpTool; ability: Ability; source: AbilitySource }, input: Record<string, unknown>): Promise<ToolResult> {
1434 const asker = this.audience.asker;
1435 const ports = this.abilityPorts;
1436 if (!ports || !asker) return { text: `There is no tool called ${entry.def.name} here.`, outcome: "refused" };
1437 const summary = `Call ${entry.tool.name} on ${entry.server.name}`;
1438 const gated = await this.gate({ ability: entry.ability, source: entry.source }, { tool: entry.def.name, args: input, summary, keys: [entry.tool.name, entry.server.name] }, asker);
1439 if (!gated.ok) return gated.result;
1440 const done = await ports.mcp(entry.server, entry.tool, input);
1441 if (!done.ok) return { text: `${entry.server.name} didn't do it: ${done.message}`, outcome: "error" };
1442 return { text: untrusted(`${entry.server.name} ${entry.tool.name}`, done.value), outcome: "allowed" };
1443 }
1444
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1445 private async code(name: string, input: Record<string, unknown>, viewer: User): Promise<ToolResult> {
1446 if (name === "list_repositories") {
1447 const repos = [...(await this.audience.repos()).values()];
1448 if (!repos.length) return { text: "There are no repositories everyone here can read.", outcome: "allowed" };
1449 return { text: untrusted("list_repositories", repos.map((repo) => `${repo.namespace}/${repo.name}${repo.isPrivate ? " (private)" : ""}`).join("\n")), outcome: "allowed" };
1450 }
1451 if (name === "search_code") {
1452 const query = String(input.query ?? "").trim();
1453 if (query.length < 2) return { text: "Search for at least two characters.", outcome: "refused" };
1454 const only = input.repo === undefined || input.repo === null || input.repo === "" ? null : await this.audience.repo(input.repo);
1455 if (input.repo && !only) return this.withheld();
1456 const allowed = await this.audience.repos();
1457 // Whatever search returns, only hits in allowed repositories come through.
1458 const hits = (await this.ports.searchCode(viewer, query, only)).filter((hit) => allowed.has(hit.repo.toLowerCase()) && (!only || hit.repo.toLowerCase() === `${only.namespace}/${only.name}`.toLowerCase()));
1459 if (!hits.length) return { text: "No code found.", outcome: "allowed" };
1460 return { text: untrusted(`search_code "${query}"`, hits.slice(0, 10).map((hit) => `${hit.repo}:${hit.path}\n${hit.snippet}`).join("\n\n")), outcome: "allowed" };
1461 }
1462 if (name === "recent_activity") {
1463 const one = input.repo ? await this.audience.repo(input.repo) : null;
1464 if (input.repo && !one) return this.withheld();
1465 const repos = one ? [one] : [...(await this.audience.repos()).values()].slice(0, 20);
1466 if (!repos.length) return { text: "There are no repositories everyone here can read.", outcome: "allowed" };
1467 const pulls = await this.ports.recentPulls(repos, viewer);
1468 if (!pulls.length) return { text: "No recent pull requests.", outcome: "allowed" };
1469 return { text: untrusted("recent_activity", pulls.map((p) => `${p.repo}#${p.number} ${p.status}: ${p.title} (${p.updated_at})`).join("\n")), outcome: "allowed" };
1470 }
1471 // The rest name one repository; it must be on the allow-list.
1472 const repo = await this.audience.repo(input.repo);
1473 if (!repo) return this.withheld();
1474 const full = `${repo.namespace}/${repo.name}`;
1475 switch (name) {
1476 case "read_file": {
1477 const path = String(input.path ?? "").trim().replace(/^\/+/, "");
1478 if (!path || path.split("/").some((part) => part === "..")) return { text: "Give a path inside the repository.", outcome: "refused" };
1479 const ref = typeof input.ref === "string" && input.ref.trim() ? input.ref.trim() : repo.defaultBranch;
1480 const file = await this.ports.readFile(repo, viewer, ref, path);
1481 if (!file) return { text: `No file ${path} at ${ref} in ${full}.`, outcome: "allowed" };
1482 if (file.text === null) return { text: `${full}:${path} is binary or too large to read (${file.size} bytes).`, outcome: "allowed" };
1483 return { text: untrusted(`${full}:${path}@${ref}`, file.text), outcome: "allowed" };
1484 }
1485 case "list_issues": {
1486 const state = input.state === "closed" ? "closed" : "open";
1487 const issues = await this.ports.listIssues(repo, viewer, state);
1488 if (!issues) return this.withheld();
1489 if (!issues.length) return { text: `No ${state} issues in ${full}.`, outcome: "allowed" };
1490 return { text: untrusted(`list_issues ${full}`, issues.slice(0, 30).map((i) => `#${i.number} [${i.state}] ${i.title}${i.labels.length ? ` (${i.labels.join(", ")})` : ""}`).join("\n")), outcome: "allowed" };
1491 }
1492 case "get_issue": {
1493 const issue = await this.ports.getIssue(repo, Math.floor(Number(input.number)), viewer);
1494 if (!issue) return { text: `No such issue in ${full}.`, outcome: "allowed" };
1495 const comments = issue.comments.map((c) => `@${c.author}: ${c.body}`).join("\n\n");
1496 return { text: untrusted(`${full}#${issue.number}`, `#${issue.number} [${issue.state}] ${issue.title}\n\n${issue.body}${comments ? `\n\nComments:\n\n${comments}` : ""}`), outcome: "allowed" };
1497 }
1498 case "get_pull": {
1499 const pull = await this.ports.getPull(repo, Math.floor(Number(input.number)), viewer);
1500 if (!pull) return { text: `No such pull request in ${full}.`, outcome: "allowed" };
1501 return { text: untrusted(`${full}#${pull.number}`, `#${pull.number} [${pull.status}] ${pull.title}\n\n${pull.body}${pull.checks ? `\n\nChecks: ${pull.checks}` : ""}`), outcome: "allowed" };
1502 }
1503 default:
1504 return { text: `There is no tool called ${name}.`, outcome: "refused" };
1505 }
1506 }
1507}
1508
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.1509/** What an agent hears when it asks for a kind that isn't built yet. */
1510const NOT_YET = "Slides, designs and dashboards aren't available yet: only docs can be made for now. Say so, and offer to write it as a doc instead.";
1511
1512/**
1513 * A folio id from an id or any artifact link (`/acme/-/artifacts/runbook-fol_…`,
1514 * with or without the site and a query); null when there is none.
1515 */
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.1516/**
1517 * What a doc made to hold a file says: the Markdown the file was made from
1518 * (less a first heading repeating the title, which the doc has), or a
1519 * preview of a spreadsheet's rows.
1520 */
1521export function docBody(format: MakeFileFormat, title: string, input: Record<string, unknown>): string {
1522 let body = "";
1523 if (format === "xlsx" || format === "csv") {
1524 const read = readSheets(input.sheets);
1525 if (read.ok) body = read.sheets.map((sheet) => `${read.sheets.length > 1 ? `## ${sheet.name}\n\n` : ""}${previewTable(sheet)}`).join("\n\n");
1526 } else {
1527 const markdown = String(input.content ?? "");
1528 const first = /^\s*#\s+(.+?)\s*#*\s*(?:\n|$)/.exec(markdown);
1529 body = (first && first[1]!.trim().toLowerCase() === title.trim().toLowerCase() ? markdown.slice(first[0].length).trimStart() : markdown).slice(0, 100_000);
1530 }
1531 return body.trim() ? body : "The file is attached below.";
1532}
1533
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.1534export function folioRef(given: string): string | null {
1535 const last = given.trim().split(/[?#]/)[0].split("/").filter(Boolean).at(-1) ?? "";
1536 return folioIdFrom(last);
1537}
1538
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.1539/** Where an artifact was written up from: a thread's link, cut to the site path the artifacts service keeps; null when it isn't one. */
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.1540export function sourceLink(given: string): { title: string; href: string } | null {
1541 let href = given.trim();
1542 if (!href) return null;
1543 if (/^https?:\/\//i.test(href)) {
1544 try {
1545 const url = new URL(href);
1546 href = `${url.pathname}${url.search}${url.hash}`;
1547 } catch {
1548 return null;
1549 }
1550 }
1551 return href.startsWith("/") && !href.startsWith("//") ? { title: "A conversation", href } : null;
1552}
1553
1554/** A space by its id, address or name (any case), among those given. */
1555function findSpace(spaces: FolioSpaceLine[], given: string): FolioSpaceLine | null {
1556 const wanted = given.trim().toLowerCase();
1557 return spaces.find((s) => s.id === given.trim()) ?? spaces.find((s) => s.slug.toLowerCase() === wanted) ?? spaces.find((s) => s.name.toLowerCase() === wanted) ?? null;
1558}
1559
1560function spaceLine(s: FolioSpaceLine): string {
1561 const can = s.can.edit ? "you can edit" : s.can.suggest ? "you can suggest edits" : "read only";
1562 const projects = s.projects.length ? `; about ${s.projects.join(", ")}` : "";
1563 return `- ${s.name} (id ${s.id}, ${s.kind}; ${can}${projects})${s.description ? `: ${s.description}` : ""}`;
1564}
1565
1566/** An artifact as the agent reads it: where it is, what it may do, and its content (a doc's Markdown, with its top-level block ids). */
1567export function folioReadText(read: FolioAgentRead): string {
1568 const f = read.folio;
1569 const can = read.can.edit ? "you can edit it" : read.can.suggest ? "you can suggest edits" : "you can only read it";
1570 const where = read.space ? `in the ${read.space.name} space` : "not in a space";
1571 const blocks = read.blocks?.length ? `\nTop-level blocks: ${read.blocks.map((b) => `${b.id} ${b.type}${b.level ? ` ${b.level}` : ""}`).join(", ")}` : "";
1572 return `# ${f.title} (${f.path}, id ${f.id})\nA ${f.kind}, ${where}; ${can}. Edited ${f.edited_at.slice(0, 16)}.${blocks}\n\n${read.content}`;
1573}
1574
1575/** What an edit did, naming the artifact only where everyone here can read it. */
1576function editMessage(result: FolioAgentEditResult, name: boolean): string {
1577 const on = name ? ` on ${result.folio.title} (${result.folio.path})` : "";
1578 switch (result.mode) {
1579 case "applied":
1580 return `Changed${on}. It's in its history as yours.`;
1581 case "suggested":
1582 return `Suggested${on}: people accept or reject it there.${name ? " Link it so they can." : ""}`;
1583 case "proposed":
1584 return `Proposed a change${on}: a person previews and applies it.`;
1585 }
Docs: a workspace knowledge base people and agents write together1586}
1587
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1588function messageLine(m: FoundMessage): string {
1589 const where = m.channel ? `#${m.channel}` : "a direct message";
1590 return `[${m.created_at.slice(0, 16)} in ${where}, channel ${m.channel_id}, message ${m.id}] @${m.author}: ${m.body}`;
1591}

This file's history is long; its oldest lines are credited to the oldest commit read.