Skip to content
996 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 */
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.21import type { DocEditTarget, FolioAgentEdit, FolioAgentEditResult, FolioAgentRead, FolioAudience, FolioKind, FolioPassage, FolioRef, 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
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.23import { FOLIO_KINDS, folioIdFrom, isFolioKind } from "../../../packages/contracts/src/folios.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)24import { type Audience, type RepoRef, WITHHELD } from "./audience.ts";
25
26/** One tool, as the Messages API takes it. */
27export type ToolDef = { name: string; description: string; input_schema: Record<string, unknown> };
28
29export type FoundMessage = { channel: string | null; channel_id: string; id: string; author: string; body: string; created_at: string };
30
31/** What the tools reach outside this module. */
32export interface ToolPorts {
33 readFile(repo: RepoRef, viewer: User, ref: string, path: string): Promise<{ text: string | null; size: number } | null>;
34 searchCode(viewer: User, query: string, repo: RepoRef | null): Promise<{ repo: string; path: string; snippet: string }[]>;
35 listIssues(repo: RepoRef, viewer: User, state: "open" | "closed"): Promise<{ number: number; title: string; state: string; labels: string[] }[] | null>;
36 getIssue(repo: RepoRef, number: number, viewer: User): Promise<{ number: number; title: string; state: string; body: string; comments: { author: string; body: string }[] } | null>;
37 getPull(repo: RepoRef, number: number, viewer: User): Promise<{ number: number; title: string; status: string; body: string; checks: string | null } | null>;
38 recentPulls(repos: RepoRef[], viewer: User): Promise<{ repo: string; number: number; title: string; status: string; updated_at: string }[]>;
39 /** Chat's own audience rule applies; null when the search failed. */
40 searchMessages(query: string): Promise<FoundMessage[] | null>;
41 /** Null when the audience may not read it (or it does not exist). */
42 readThread(channelId: string, id: string): Promise<FoundMessage[] | null>;
43 roster(viewer: User | null): Promise<string>;
44 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 together45 /**
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.46 * The workspace's artifacts (Artifacts mode), as the docs service lets
47 * this agent use them for the person it acts for and everyone who will
48 * read the answer. Absent where there is no docs service.
Docs: a workspace knowledge base people and agents write together49 */
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.50 folios?: FoliosPorts;
Docs: a workspace knowledge base people and agents write together51}
52
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.53/** A space as an agent sees it, with what it may do there for the person it acts for. */
54export type FolioSpaceLine = {
55 id: string;
56 slug: string;
57 name: string;
58 description: string | null;
59 kind: string;
60 projects: string[];
61 can: { read: boolean; suggest: boolean; edit: boolean };
62};
63
64/** Where a new artifact goes: a space, its asker's Private, or Private shared with the conversation's people. */
65export type FolioWhere = { space_id: string } | "private" | { conversation: string[] };
66
67/** A call's answer: the value, or the docs service's error code and sentence. */
68export type FolioDone<T> = { ok: true; value: T } | { ok: false; code: string; message: string };
69
70/**
71 * Artifacts, as an agent uses them. Every call names the person it acts
72 * for, and the reads also who reads the answer; the docs service checks
73 * both.
74 */
75export interface FoliosPorts {
76 /** Spaces everyone here can read; null when the docs service couldn't answer. */
77 spaces(viewer: User, audience: FolioAudience): Promise<FolioSpaceLine[] | null>;
78 /** Passages closest in meaning to `query`; `spaces` (required reading) first. Null when the docs service couldn't answer. */
79 recall(viewer: User, audience: FolioAudience, query: string, spaces: string[], kinds?: FolioKind[]): Promise<FolioPassage[] | null>;
80 /** Artifacts matching `query` (words and meaning), as lines with links. */
81 search(viewer: User, audience: FolioAudience, input: { query: string; kind: FolioKind | null; space_id: string | null; project: string | null }): Promise<string | null>;
82 read(viewer: User, audience: FolioAudience, folioId: string): Promise<FolioDone<FolioAgentRead>>;
83 /** Artifacts possibly out of date since code they cite changed. */
84 stale(viewer: User, audience: FolioAudience, repo: string | null): Promise<string | null>;
85 create(
86 viewer: User,
87 input: { kind: FolioKind; title: string; markdown: string | null; template_id: string | null; where: FolioWhere; parent_id: string | null; source: { title: string; href: string } | null },
88 ): Promise<FolioDone<FolioRef>>;
89 edit(viewer: User, folioId: string, edit: FolioAgentEdit): Promise<FolioDone<FolioAgentEditResult>>;
90 /** `view` or `comment` for people already in this conversation. */
91 share(viewer: User, audience: FolioAudience, folioId: string, userIds: string[], role: "view" | "comment"): Promise<FolioDone<null>>;
92 /** Sends the asker a link directly, as a message from the agent in their DM with it; false when it couldn't. */
93 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)94}
95
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 asked96/**
97 * What an agent may do, beyond reading: remember, file an issue for the
98 * person who asked, and start or shape work. Each is checked here before it
99 * runs (the audience, the asker, the hop limit) and again by the service
100 * that does it.
101 */
102export 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.103 /**
104 * `onlyForAsker`: this turn read an artifact the whole workspace can't
105 * read, so the fact is kept for the person who asked alone.
106 */
107 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 asked108 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 calendar109 /**
110 * Posts a draft issue as a card in the conversation, with File issue and
111 * Discard: whoever presses File files it as themselves, if they can read
112 * the repository. Nothing is filed by the agent.
113 */
114 draftIssue(repo: RepoRef, input: { title: string; body: string; labels: string[] }): Promise<{ ok: boolean; message: string }>;
115 /**
116 * Comments on an issue or pull request, or reviews a pull request, as the
117 * agent on behalf of the person who asked. Reviews are advisory: they
118 * never count toward required approvals.
119 */
120 comment?(repo: RepoRef, asker: User, number: number, body: string): Promise<{ ok: boolean; message: string }>;
121 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 asked122 /** From chat: spins off a session for real work. */
123 startSession?(title: string, goal: string): Promise<{ ok: boolean; message: string }>;
124 /** In a session: a short progress note in its thread. */
125 postUpdate?(text: string): Promise<{ ok: boolean; message: string }>;
126 /** In a session: one of the agent's own subagents takes part of the work. */
127 useSubagent?(name: string, brief: string): Promise<{ ok: boolean; message: string }>;
128 /** In a session: a colleague works on part of it, paid from this session's budget. */
129 bringIn?(handle: string, brief: string): Promise<{ ok: boolean; message: string }>;
Merge branch 'worktree-agent-a1398e81ad1a64c5f'130 /** From chat: a colleague takes the work on for the person who asked, here or in a group message. */
131 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 asked132}
133
Merge branch 'worktree-agent-a1398e81ad1a64c5f'134/** The most hand-offs one reply makes. */
135export const MAX_HAND_OFFS = 2;
136
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)137/** The most tool calls one reply makes. */
138export 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 asked139/** The most tool calls one step of a session makes. */
140export 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)141/** The most of a file or result an answer is given, in characters. */
142const MAX_RESULT = 20_000;
143
144export type ToolCall = {
145 tool: string;
146 /** Its arguments, with long text cut, as recorded. */
147 args: string;
148 outcome: "allowed" | "withheld" | "refused" | "error";
149 bytes: number;
150};
151
152export type ToolResult = { text: string; outcome: ToolCall["outcome"] };
153
154/**
155 * Text a tool read, marked as data. Anything in it that looks like the
156 * closing mark is defused, so content can't end the block early.
157 */
158export function untrusted(source: string, content: string): string {
159 const safe = (text: string) => text.replace(/<\/?untrusted/gi, (mark) => mark.replace("<", "&lt;"));
160 const body = content.length > MAX_RESULT ? `${content.slice(0, MAX_RESULT)}\n[cut: ${content.length - MAX_RESULT} more characters]` : content;
161 return `<untrusted source="${safe(source).replace(/"/g, "'")}">\n${safe(body)}\n</untrusted>`;
162}
163
164/** Arguments as recorded: every string cut to 120 characters. */
165export function redact(args: unknown): string {
166 const cut = (value: unknown): unknown => {
167 if (typeof value === "string") return value.length > 120 ? `${value.slice(0, 120)}…` : value;
168 if (Array.isArray(value)) return value.slice(0, 10).map(cut);
169 if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).slice(0, 10).map(([k, v]) => [k, cut(v)]));
170 return value;
171 };
172 return JSON.stringify(cut(args ?? {})).slice(0, 1000);
173}
174
175const CODE_TOOLS: ToolDef[] = [
176 {
177 name: "list_repositories",
178 description: "The workspace's repositories everyone in this conversation can read. Start here to know what you can look at.",
179 input_schema: { type: "object", properties: {} },
180 },
181 {
182 name: "search_code",
183 description: "Search code on default branches. Optionally only in one repository (`name` or `workspace/name`).",
184 input_schema: { type: "object", properties: { query: { type: "string" }, repo: { type: "string" } }, required: ["query"] },
185 },
186 {
187 name: "read_file",
188 description: "Read a file from a repository, at its default branch or a ref.",
189 input_schema: { type: "object", properties: { repo: { type: "string" }, path: { type: "string" }, ref: { type: "string" } }, required: ["repo", "path"] },
190 },
191 {
192 name: "list_issues",
193 description: "A repository's newest issues, open by default.",
194 input_schema: { type: "object", properties: { repo: { type: "string" }, state: { type: "string", enum: ["open", "closed"] } }, required: ["repo"] },
195 },
196 {
197 name: "get_issue",
198 description: "One issue with its comments.",
199 input_schema: { type: "object", properties: { repo: { type: "string" }, number: { type: "integer" } }, required: ["repo", "number"] },
200 },
201 {
202 name: "get_pull",
203 description: "One pull request: what it changes, its status and checks.",
204 input_schema: { type: "object", properties: { repo: { type: "string" }, number: { type: "integer" } }, required: ["repo", "number"] },
205 },
206 {
207 name: "recent_activity",
208 description: "Recently merged and open pull requests, in one repository or across those you can read.",
209 input_schema: { type: "object", properties: { repo: { type: "string" } } },
210 },
211];
212
213const CHAT_TOOLS: ToolDef[] = [
214 {
215 name: "search_messages",
216 description: "Search chat messages this conversation's people can all read.",
217 input_schema: { type: "object", properties: { query: { type: "string" } }, required: ["query"] },
218 },
219 {
220 name: "read_thread",
221 description: "Read a chat thread by its channel id and a message id in it (from search_messages).",
222 input_schema: { type: "object", properties: { channel: { type: "string" }, id: { type: "string" } }, required: ["channel", "id"] },
223 },
224 {
225 name: "workspace_roster",
226 description: "The workspace's people and agents: names, teams, titles and roles.",
227 input_schema: { type: "object", properties: {} },
228 },
229];
230
231const ASK_COLLEAGUE: ToolDef = {
232 name: "ask_colleague",
233 description:
Merge branch 'worktree-agent-a1398e81ad1a64c5f'234 "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)235 input_schema: { type: "object", properties: { handle: { type: "string" }, question: { type: "string" } }, required: ["handle", "question"] },
236};
237
Merge branch 'worktree-agent-a1398e81ad1a64c5f'238const HAND_OFF: ToolDef = {
239 name: "hand_off",
240 description:
241 "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.",
242 input_schema: { type: "object", properties: { handle: { type: "string" }, brief: { type: "string" } }, required: ["handle", "brief"] },
243};
244
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 asked245const REMEMBER: ToolDef = {
246 name: "remember",
247 description:
248 "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.",
249 input_schema: {
250 type: "object",
251 properties: { fact: { type: "string" }, scope: { type: "string", enum: ["workspace", "channel", "person"] } },
252 required: ["fact"],
253 },
254};
255
256const FORGET: ToolDef = {
257 name: "forget",
258 description: "Forget one of the notes under 'What you remember', by its id, when it is wrong or out of date.",
259 input_schema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
260};
261
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar262const DRAFT_ISSUE: ToolDef = {
263 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 asked264 description:
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar265 "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 asked266 input_schema: {
267 type: "object",
268 properties: {
269 repo: { type: "string" },
270 title: { type: "string" },
271 body: { type: "string" },
272 labels: { type: "array", items: { type: "string" } },
273 },
274 required: ["repo", "title", "body"],
275 },
276};
277
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar278const COMMENT: ToolDef = {
279 name: "comment",
280 description:
281 "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.",
282 input_schema: {
283 type: "object",
284 properties: { repo: { type: "string" }, number: { type: "integer" }, body: { type: "string" } },
285 required: ["repo", "number", "body"],
286 },
287};
288
289const REVIEW_PULL: ToolDef = {
290 name: "review_pull",
291 description:
292 "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.",
293 input_schema: {
294 type: "object",
295 properties: {
296 repo: { type: "string" },
297 number: { type: "integer" },
298 verdict: { type: "string", enum: ["comment", "approve", "request_changes"] },
299 body: { type: "string" },
300 },
301 required: ["repo", "number", "verdict", "body"],
302 },
303};
304
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 asked305const START_SESSION: ToolDef = {
306 name: "start_session",
307 description:
308 "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.",
309 input_schema: { type: "object", properties: { title: { type: "string" }, goal: { type: "string" } }, required: ["title", "goal"] },
310};
311
312const POST_UPDATE: ToolDef = {
313 name: "post_update",
314 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.",
315 input_schema: { type: "object", properties: { text: { type: "string" } }, required: ["text"] },
316};
317
318const USE_SUBAGENT: ToolDef = {
319 name: "use_subagent",
320 description:
321 "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.",
322 input_schema: { type: "object", properties: { name: { type: "string" }, brief: { type: "string" } }, required: ["name", "brief"] },
323};
324
325const BRING_IN: ToolDef = {
326 name: "bring_in",
327 description:
328 "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.",
329 input_schema: { type: "object", properties: { handle: { type: "string" }, brief: { type: "string" } }, required: ["handle", "brief"] },
330};
331
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.332/** What every artifact tool's description starts from, so the model never mixes them up with workflow run artifacts. */
333const ARTIFACTS =
334 "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.";
335
336const FOLIO_TOOLS: ToolDef[] = [
Docs: a workspace knowledge base people and agents write together337 {
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.338 name: "search_artifacts",
339 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.`,
340 input_schema: {
341 type: "object",
342 properties: {
343 query: { type: "string" },
344 kind: { type: "string", enum: [...FOLIO_KINDS] },
345 space: { type: "string", description: "A space's name or id." },
346 project: { type: "string", description: "A repository, `workspace/name`." },
347 },
348 required: ["query"],
349 },
Docs: a workspace knowledge base people and agents write together350 },
351 {
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.352 name: "read_artifact",
353 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.`,
354 input_schema: { type: "object", properties: { id: { type: "string", description: "Its id or link." } }, required: ["id"] },
Docs: a workspace knowledge base people and agents write together355 },
356 {
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.357 name: "list_spaces",
358 description: `${ARTIFACTS} The spaces whose artifacts everyone in this conversation can read, with what you may do in each (read, suggest, edit).`,
359 input_schema: { type: "object", properties: {} },
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store360 },
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: "stale_artifacts",
363 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.`,
364 input_schema: { type: "object", properties: { repo: { type: "string" } } },
Docs: a workspace knowledge base people and agents write together365 },
366];
367
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.368const FOLIO_WRITE_TOOLS: ToolDef[] = [
369 {
370 name: "create_artifact",
371 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.`,
372 input_schema: {
373 type: "object",
374 properties: {
375 kind: { type: "string", enum: [...FOLIO_KINDS] },
376 title: { type: "string" },
377 content: { type: "string", description: "Markdown." },
378 template: { type: "string", description: "A template id, instead of content." },
379 where: {
380 description: '{ "space": "<name or id>" }, "private" or "conversation".',
381 anyOf: [
382 { type: "string", enum: ["private", "conversation"] },
383 { type: "object", properties: { space: { type: "string" } }, required: ["space"] },
384 ],
385 },
386 parent: { type: "string", description: "A doc to put it under: its id or link." },
387 source: { type: "string", description: "The link of the thread it was written up from." },
388 },
389 required: ["kind", "title"],
390 },
391 },
Docs: a workspace knowledge base people and agents write together392 {
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.393 name: "edit_artifact",
394 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 together395 input_schema: {
396 type: "object",
397 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.398 id: { type: "string", description: "Its id or link." },
Docs: a workspace knowledge base people and agents write together399 target: { type: "string", enum: ["append", "section", "blocks", "document"] },
400 heading: { type: "string", description: "For target section: the heading's text." },
401 from_block: { type: "string" },
402 to_block: { type: "string" },
403 markdown: { type: "string" },
404 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.405 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 together406 suggest_only: { type: "boolean", description: "Suggest even where you could edit." },
407 },
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 required: ["id", "target", "markdown"],
Docs: a workspace knowledge base people and agents write together409 },
410 },
411 {
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.412 name: "share_artifact",
413 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 together414 input_schema: {
415 type: "object",
416 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.417 id: { type: "string", description: "Its id or link." },
418 people: { type: "array", items: { type: "string" }, description: "Usernames of people in this conversation." },
419 role: { type: "string", enum: ["view", "comment"] },
Docs: a workspace knowledge base people and agents write together420 },
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.421 required: ["id", "people", "role"],
Docs: a workspace knowledge base people and agents write together422 },
423 },
424];
425
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.426const FOLIO_NAMES = new Set([...FOLIO_TOOLS, ...FOLIO_WRITE_TOOLS].map((tool) => tool.name));
Docs: a workspace knowledge base people and agents write together427
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)428const CODE_NAMES = new Set(CODE_TOOLS.map((tool) => tool.name));
429
430export type ToolContext = {
431 /** The agent replying. */
432 agentId: string;
433 /** Handles nobody may consult from here: the agent itself, and whoever sent it the work. */
434 notConsult: string[];
435 /** Hops so far: a consult is one more, and none is offered at the limit. */
436 hops: number;
437 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 asked438 /** Whether this is a session's step (more calls, session tools) or a reply. */
439 session?: boolean;
440 /** Told of every call as it is made, for a session's transcript. */
441 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)442};
443
444export class ToolBox {
445 /** Every call this reply made, shared with the tool boxes of colleagues it consults: one budget for the reply. */
446 readonly calls: ToolCall[];
447 private readonly audience: Audience;
448 private readonly ports: ToolPorts;
449 private readonly context: ToolContext;
450
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 asked451 private readonly actions: ActionPorts | null;
452 /** Updates posted in this step. */
453 private updates = 0;
Merge branch 'worktree-agent-a1398e81ad1a64c5f'454 /** Hand-offs made in this reply. */
455 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.456 /** Artifacts read or made in this turn that not everyone here can read: never named here. */
457 private readonly notHere = new Set<string>();
458 /**
459 * 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.460 * 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.461 */
462 private privateRead = false;
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 asked463
464 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)465 this.audience = audience;
466 this.ports = ports;
467 this.context = context;
468 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 asked469 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)470 }
471
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 asked472 /** 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)473 forColleague(ports: ToolPorts, context: ToolContext): ToolBox {
474 return new ToolBox(this.audience, ports, context, this.calls);
475 }
476
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 asked477 /** The most calls this box makes. */
478 get maxCalls(): number {
479 return this.context.session ? MAX_SESSION_TOOL_CALLS : MAX_TOOL_CALLS;
480 }
481
482 /** Whether the person who asked can be acted for: resolved, and able to read code here. */
483 private canFile(): boolean {
484 return !!this.actions && !!this.audience.asker && this.audience.codeAllowed();
485 }
486
487 /**
488 * The tools offered: no code tools for an audience that can't read code,
489 * no consults or hand-offs at the hop limit, session tools only in a
490 * session, and a spin-off only from chat.
491 */
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)492 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 asked493 const roomForHop = this.context.hops + 1 <= this.context.maxHops;
494 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)495 return [
496 ...(this.audience.codeAllowed() ? CODE_TOOLS : []),
497 ...CHAT_TOOLS,
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.498 // Artifacts are for everyone, Code or not: the docs service decides what this person and audience can read.
499 ...(this.ports.folios && this.audience.asker ? FOLIO_TOOLS : []),
500 ...(this.ports.folios && this.audience.asker && actions ? FOLIO_WRITE_TOOLS : []),
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 asked501 ...(roomForHop ? [ASK_COLLEAGUE] : []),
502 ...(actions ? [REMEMBER, FORGET] : []),
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar503 ...(this.canFile() ? [DRAFT_ISSUE] : []),
504 ...(this.canFile() && actions?.comment ? [COMMENT] : []),
505 ...(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 asked506 ...(actions?.startSession && !this.context.session ? [START_SESSION] : []),
Merge branch 'worktree-agent-a1398e81ad1a64c5f'507 ...(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 asked508 ...(actions?.postUpdate && this.context.session ? [POST_UPDATE] : []),
509 ...(actions?.useSubagent && this.context.session && roomForHop ? [USE_SUBAGENT] : []),
510 ...(actions?.bringIn && this.context.session && roomForHop ? [BRING_IN] : []),
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)511 ];
512 }
513
514 /** Whether another call may be made. */
515 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 asked516 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)517 }
518
519 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 asked520 const result = await this.attempt(name, input);
521 const call: ToolCall = { tool: name, args: redact(input), outcome: result.outcome, bytes: result.text.length };
522 this.calls.push(call);
523 this.context.onCall?.(call);
524 return result;
525 }
526
527 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)528 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 asked529 const what = this.context.session ? "step" : "reply";
530 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)531 else {
532 try {
533 result = await this.dispatch(name, input ?? {});
534 } catch (error) {
535 console.error("agents: a tool failed", name, String(error));
536 result = { text: "That didn't work just now. Answer with what you have.", outcome: "error" };
537 }
538 }
539 return result;
540 }
541
542 private withheld(): ToolResult {
543 return { text: WITHHELD, outcome: "withheld" };
544 }
545
546 private async dispatch(name: string, input: Record<string, unknown>): Promise<ToolResult> {
547 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.548 if (FOLIO_NAMES.has(name)) {
549 if (!this.definitions().some((tool) => tool.name === name) || !asker || !this.ports.folios) return { text: `There is no tool called ${name} here.`, outcome: "refused" };
550 return this.folios(name, input, asker, this.ports.folios);
Docs: a workspace knowledge base people and agents write together551 }
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)552 if (CODE_NAMES.has(name)) {
553 // Not offered, and refused if asked for anyway: the check is here, not in the prompt.
554 if (!this.audience.codeAllowed() || !asker) return this.withheld();
555 return this.code(name, input, asker);
556 }
557 switch (name) {
558 case "search_messages": {
559 const query = String(input.query ?? "").trim();
560 if (query.length < 2) return { text: "Search for at least two characters.", outcome: "refused" };
561 const found = await this.ports.searchMessages(query);
562 if (found === null) return { text: "Search didn't work just now.", outcome: "error" };
563 if (!found.length) return { text: "No messages found.", outcome: "allowed" };
564 return { text: untrusted(`search_messages "${query}"`, found.map(messageLine).join("\n")), outcome: "allowed" };
565 }
566 case "read_thread": {
567 const thread = await this.ports.readThread(String(input.channel ?? ""), String(input.id ?? ""));
568 if (!thread || !thread.length) return this.withheld();
569 return { text: untrusted("read_thread", thread.map(messageLine).join("\n")), outcome: "allowed" };
570 }
571 case "workspace_roster":
572 return { text: untrusted("workspace_roster", await this.ports.roster(asker)), outcome: "allowed" };
573 case "ask_colleague": {
574 const handle = String(input.handle ?? "").trim().replace(/^@/, "").toLowerCase();
575 const question = String(input.question ?? "").trim();
576 if (!handle || !question) return { text: "Name the colleague and the question.", outcome: "refused" };
577 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" };
578 if (this.context.notConsult.includes(handle)) {
579 return { text: `You can't consult @${handle} here: they sent you this work, or it is you. Answer with what you have.`, outcome: "refused" };
580 }
581 const answer = await this.ports.consult(handle, question.slice(0, 2000));
582 if (!answer.ok) return { text: answer.message, outcome: "refused" };
583 return { text: untrusted(`@${answer.colleague}'s answer`, answer.answer), outcome: "allowed" };
584 }
585 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 asked586 return this.act(name, input);
587 }
588 }
589
Agents recall what Docs say before they answer or work, and each has required reading590 /**
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.591 * What the workspace's artifacts say about `query`, for this person and
592 * this audience, before the agent answers: no tool call, nothing counted
593 * against its tools. Empty when there is no docs service or nothing
594 * relevant.
Agents recall what Docs say before they answer or work, and each has required reading595 */
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.596 async recall(query: string | null, spaces: string[]): Promise<FolioPassage[]> {
597 const folios = this.ports.folios;
Agents recall what Docs say before they answer or work, and each has required reading598 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.599 if (!folios || !asker || !query) return [];
Agents recall what Docs say before they answer or work, and each has required reading600 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.601 return (await folios.recall(asker, this.folioAudience(), query, spaces)) ?? [];
Agents recall what Docs say before they answer or work, and each has required reading602 } 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.603 console.error("agents: artifacts recall failed", String(error));
Agents recall what Docs say before they answer or work, and each has required reading604 return [];
605 }
606 }
607
Docs: a workspace knowledge base people and agents write together608 /** Who reads what an agent says here, as the docs 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.609 private folioAudience(): FolioAudience {
Docs: a workspace knowledge base people and agents write together610 return this.audience.shared ? { kind: "workspace" } : { kind: "people", user_ids: this.audience.members.map((m) => m.id) };
611 }
612
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.613 /** Whether anyone besides the person who asked reads what is said here. */
614 private othersHere(asker: User): boolean {
615 return this.audience.shared || this.audience.members.some((m) => m.id !== asker.id);
616 }
617
618 private async folios(name: string, input: Record<string, unknown>, asker: User, folios: FoliosPorts): Promise<ToolResult> {
Docs: a workspace knowledge base people and agents write together619 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.620 const audience = this.folioAudience();
Docs: a workspace knowledge base people and agents write together621 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.622 case "list_spaces": {
623 const spaces = await folios.spaces(asker, audience);
624 if (spaces === null) return { text: "Spaces couldn't be listed just now.", outcome: "error" };
625 if (!spaces.length) return { text: "There are no spaces everyone here can read.", outcome: "allowed" };
626 return { text: untrusted("list_spaces", spaces.map(spaceLine).join("\n")), outcome: "allowed" };
627 }
628 case "search_artifacts": {
Docs: a workspace knowledge base people and agents write together629 const query = text("query", 200);
630 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.631 const kind = text("kind", 20) || null;
632 if (kind && !isFolioKind(kind)) return { text: `There is no kind of artifact called ${kind}: it is doc, slides, design or dashboard.`, outcome: "refused" };
633 let spaceId: string | null = null;
634 if (text("space", 200)) {
635 const space = findSpace((await folios.spaces(asker, audience)) ?? [], text("space", 200));
636 if (!space) return this.withheld();
637 spaceId = space.id;
638 }
639 const found = await folios.search(asker, audience, { query, kind: kind as FolioKind | null, space_id: spaceId, project: text("project", 200).toLowerCase() || null });
640 if (found === null) return { text: "Search didn't work just now.", outcome: "error" };
641 return { text: untrusted(`search_artifacts "${query}"`, found), outcome: "allowed" };
642 }
643 case "stale_artifacts": {
644 const found = await folios.stale(asker, audience, text("repo", 200).toLowerCase() || null);
645 if (found === null) return { text: "Out-of-date artifacts couldn't be listed just now.", outcome: "error" };
646 return { text: untrusted("stale_artifacts", found), outcome: "allowed" };
Docs: a workspace knowledge base people and agents write together647 }
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.648 case "read_artifact": {
649 const id = folioRef(text("id", 500));
650 if (!id) return { text: "Give the artifact's id (fol_…) or its link.", outcome: "refused" };
651 const found = await folios.read(asker, audience, id);
652 if (!found.ok) return found.code === "not_found" || found.code === "forbidden" ? this.withheld() : { text: found.message, outcome: "refused" };
653 const read = found.value;
654 if (!this.audience.shared || !read.audience_can_read) this.privateRead = true;
655 if (!read.audience_can_read) return this.notForEveryone(asker, read.folio, folios, "found");
656 return { text: untrusted(`read_artifact ${id}`, folioReadText(read)), outcome: "allowed" };
Docs: a workspace knowledge base people and agents write together657 }
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.658 case "create_artifact": {
659 const kind = text("kind", 20) || "doc";
660 if (!isFolioKind(kind)) return { text: `There is no kind of artifact called ${kind}.`, outcome: "refused" };
661 if (kind !== "doc") return { text: NOT_YET, outcome: "refused" };
662 const title = text("title", 200);
663 const template = text("template", 100) || null;
664 const markdown = String(input.content ?? "").slice(0, 100_000);
665 if (!title) return { text: "An artifact needs a title.", outcome: "refused" };
666 if (!markdown.trim() && !template) return { text: "Give its content as Markdown, or a template.", outcome: "refused" };
667 const parentGiven = text("parent", 500);
668 const parent = parentGiven ? folioRef(parentGiven) : null;
669 if (parentGiven && !parent) return { text: "Give the parent doc's id or link.", outcome: "refused" };
670 const place = await this.whereFor(input.where, asker, folios);
671 if (!place.ok) return { text: place.message, outcome: "refused" };
672 const make = (where: FolioWhere) =>
673 folios.create(asker, { kind, title, markdown: template ? null : markdown, template_id: template, where, parent_id: parent, source: sourceLink(text("source", 2000)) });
674 let made = await make(place.where);
675 // The General space by default, unless the person who asked can't add there: then their Private.
676 if (!made.ok && place.fallback && made.code === "forbidden") made = await make("private");
677 if (!made.ok) return { text: `It couldn't be made: ${made.message}`, outcome: "refused" };
678 const ref = made.value;
679 if (this.othersHere(asker)) {
680 const check = await folios.read(asker, audience, ref.id).catch(() => null);
681 if (!check?.ok || !check.value.audience_can_read) return this.notForEveryone(asker, ref, folios, "made");
682 }
683 return { text: `Wrote ${ref.title} (${ref.path}, id ${ref.id}). Link it.`, outcome: "allowed" };
684 }
685 case "edit_artifact": {
686 const id = folioRef(text("id", 500));
Docs: a workspace knowledge base people and agents write together687 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.688 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 together689 const kind = text("target", 20);
690 const target: DocEditTarget | null =
691 kind === "append"
692 ? { kind: "append" }
693 : kind === "document"
694 ? { kind: "document" }
695 : kind === "section" && text("heading", 300)
696 ? { kind: "section", heading: text("heading", 300) }
697 : kind === "blocks" && text("from_block", 100) && text("to_block", 100)
698 ? { kind: "blocks", from_block: text("from_block", 100), to_block: text("to_block", 100) }
699 : null;
700 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.701 const edit: FolioAgentEdit = { kind: "doc", target, markdown, note: text("note", 300) || null, suggest_only: input.suggest_only === true, marks_current: input.marks_current === true };
702 const done = await folios.edit(asker, id, edit);
703 if (!done.ok) return { text: `That didn't work: ${done.message}`, outcome: "refused" };
704 return { text: editMessage(done.value, !this.notHere.has(done.value.folio.id)), outcome: "allowed" };
Docs: a workspace knowledge base people and agents write together705 }
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.706 case "share_artifact": {
707 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" };
708 const id = folioRef(text("id", 500));
709 if (!id) return { text: "Give the artifact's id (fol_…) or its link.", outcome: "refused" };
710 const role = input.role === "view" || input.role === "comment" ? input.role : null;
711 if (!role) return { text: "You can share to view or comment only. For more, ask the person to use Share.", outcome: "refused" };
712 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);
713 const people = named.map((n) => this.audience.members.find((m) => m.username.toLowerCase() === n || m.id === n) ?? n);
714 const outside = people.filter((p): p is string => typeof p === "string");
715 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" };
716 const users = [...new Map((people as User[]).filter((u) => u.id !== asker.id).map((u) => [u.id, u])).values()];
717 if (!users.length) return { text: "Name who to share it with: people in this conversation besides the person who asked.", outcome: "refused" };
718 const done = await folios.share(asker, audience, id, users.map((u) => u.id), role);
719 if (!done.ok) return { text: `It couldn't be shared: ${done.message}`, outcome: "refused" };
720 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 together721 }
722 default:
723 return { text: `There is no tool called ${name}.`, outcome: "refused" };
724 }
725 }
726
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.727 /**
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.728 * 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.729 * - a space named by its name or id, among those everyone here can read;
730 * - "private": the asker's Private;
731 * - "conversation": Private, plus `view` for this conversation's people;
732 * - nothing: the conversation in a DM or private channel, and in a public
733 * channel the General space if the asker can add there (`fallback`:
734 * their Private if the docs service says they can't).
735 */
736 private async whereFor(given: unknown, asker: User, folios: FoliosPorts): Promise<{ ok: true; where: FolioWhere; fallback: boolean } | { ok: false; message: string }> {
737 const named = typeof given === "object" && given !== null ? (given as { space?: unknown }).space : given;
738 const wanted = typeof named === "string" ? named.trim().slice(0, 200) : "";
739 const others = this.audience.members.filter((m) => m.id !== asker.id).map((m) => m.id);
740 const byDefault = async (): Promise<{ ok: true; where: FolioWhere; fallback: boolean }> => {
741 if (!this.audience.shared) return { ok: true, where: others.length ? { conversation: this.audience.members.map((m) => m.id) } : "private", fallback: false };
742 const spaces = (await folios.spaces(asker, this.folioAudience())) ?? [];
743 const general = spaces.find((s) => s.slug === "general") ?? spaces.find((s) => s.name.toLowerCase() === "general");
744 return general && general.can.suggest ? { ok: true, where: { space_id: general.id }, fallback: true } : { ok: true, where: "private", fallback: false };
745 };
746 if (!wanted) return byDefault();
747 const lower = wanted.toLowerCase();
748 if (typeof given === "string" && lower === "private") return { ok: true, where: "private", fallback: false };
749 // A public channel has no list of people to share with: its default instead.
750 if (typeof given === "string" && lower === "conversation") return byDefault();
751 const space = findSpace((await folios.spaces(asker, this.folioAudience())) ?? [], wanted);
752 if (space) return { ok: true, where: { space_id: space.id }, fallback: false };
753 // An id the asker gave, for a space not everyone here can read: the docs service checks it.
754 if (/^spc_[A-Za-z0-9]+$/.test(wanted)) return { ok: true, where: { space_id: wanted }, fallback: false };
755 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".` };
756 }
757
758 /**
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.759 * An artifact someone here can't read: the link goes to the asker
760 * directly, and the agent says only that it found or made something,
761 * 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.762 */
763 private async notForEveryone(asker: User, folio: FolioRef, folios: FoliosPorts, what: "found" | "made"): Promise<ToolResult> {
764 this.notHere.add(folio.id);
765 const who = `@${asker.username}`;
766 const note =
767 what === "made"
768 ? "I made this for you. Not everyone in the conversation you asked from can open it, so here is the link:"
769 : "Here is what you asked about. Not everyone in the conversation you asked from can open it, so here is the link:";
770 const sent = await folios.sendLink(asker, { title: folio.title, path: folio.path }, note).catch(() => false);
771 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;`;
772 const text = sent
773 ? `${lead} say you ${what} it and that you've sent the link to ${who} directly.`
774 : what === "made"
775 ? `${lead} say you made it and that ${who} will find it under Artifacts, in their Private or shared with them.`
776 : `${lead} say you found it but can't share it here, and ask ${who} to message you directly.`;
777 return { text, outcome: what === "made" ? "allowed" : "withheld" };
778 }
779
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 asked780 /** Doing, not reading: memory, issues, sessions. Each refused unless offered. */
781 private async act(name: string, input: Record<string, unknown>): Promise<ToolResult> {
782 const actions = this.actions;
783 const offered = this.definitions().some((tool) => tool.name === name);
784 if (!actions || !offered) return { text: `There is no tool called ${name} here.`, outcome: "refused" };
785 const said = (answer: { ok: boolean; message: string }): ToolResult => ({ text: answer.message, outcome: answer.ok ? "allowed" : "refused" });
786 const text = (key: string, max: number) => String(input[key] ?? "").trim().slice(0, max);
787 switch (name) {
788 case "remember": {
789 const fact = text("fact", 2000);
790 if (!fact) return { text: "Say what to remember.", outcome: "refused" };
791 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.792 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 asked793 }
794 case "forget":
795 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 calendar796 case "draft_issue": {
797 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 asked798 const repo = await this.audience.repo(input.repo);
799 if (!repo) return this.withheld();
800 const title = text("title", 200);
801 const body = text("body", 20_000);
802 if (!title || !body) return { text: "An issue needs a title and a body.", outcome: "refused" };
803 const labels = Array.isArray(input.labels)
804 ? input.labels.filter((l): l is string => typeof l === "string").map((l) => l.trim()).filter(Boolean).slice(0, 5)
805 : [];
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar806 return said(await actions.draftIssue(repo, { title, body, labels }));
807 }
808 case "comment":
809 case "review_pull": {
810 const asker = this.audience.asker;
811 if (!asker || !this.audience.codeAllowed()) return this.withheld();
812 const repo = await this.audience.repo(input.repo);
813 if (!repo) return this.withheld();
814 const number = Math.floor(Number(input.number));
815 if (!Number.isFinite(number) || number < 1) return { text: "Give the issue or pull request's number.", outcome: "refused" };
816 const body = text("body", 20_000);
817 if (name === "comment") {
818 if (!body) return { text: "Say what to comment.", outcome: "refused" };
819 return said(await actions.comment!(repo, asker, number, body));
820 }
821 const verdict = input.verdict === "approve" || input.verdict === "request_changes" ? input.verdict : "comment";
822 if (!body && verdict !== "approve") return { text: "A review needs its text.", outcome: "refused" };
823 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 asked824 }
825 case "start_session": {
826 const title = text("title", 120);
827 const goal = text("goal", 8000);
828 if (!title || !goal) return { text: "A session needs a title and a goal.", outcome: "refused" };
829 return said(await actions.startSession!(title, goal));
830 }
831 case "post_update": {
832 const note = text("text", 2000);
833 if (!note) return { text: "Say what to post.", outcome: "refused" };
834 if (this.updates >= 3) return { text: "You've posted enough updates for this step; carry on with the work.", outcome: "refused" };
835 this.updates++;
836 return said(await actions.postUpdate!(note));
837 }
838 case "use_subagent": {
839 const helper = text("name", 60).toLowerCase();
840 const brief = text("brief", 8000);
841 if (!helper || !brief) return { text: "Name the subagent and give it a brief.", outcome: "refused" };
842 return said(await actions.useSubagent!(helper, brief));
843 }
844 case "bring_in": {
845 const handle = text("handle", 60).replace(/^@/, "").toLowerCase();
846 const brief = text("brief", 8000);
847 if (!handle || !brief) return { text: "Name the colleague and give them a brief.", outcome: "refused" };
848 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" };
849 return said(await actions.bringIn!(handle, brief));
850 }
Merge branch 'worktree-agent-a1398e81ad1a64c5f'851 case "hand_off": {
852 const handle = text("handle", 60).replace(/^@/, "").toLowerCase();
853 const brief = text("brief", 8000);
854 if (!handle || !brief) return { text: "Name the colleague and give them a brief.", outcome: "refused" };
855 if (this.context.notConsult.includes(handle)) {
856 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" };
857 }
858 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" };
859 const done = await actions.handOff!(handle, brief);
860 if (done.ok) this.handOffs++;
861 return said(done);
862 }
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 asked863 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)864 return { text: `There is no tool called ${name}.`, outcome: "refused" };
865 }
866 }
867
868 private async code(name: string, input: Record<string, unknown>, viewer: User): Promise<ToolResult> {
869 if (name === "list_repositories") {
870 const repos = [...(await this.audience.repos()).values()];
871 if (!repos.length) return { text: "There are no repositories everyone here can read.", outcome: "allowed" };
872 return { text: untrusted("list_repositories", repos.map((repo) => `${repo.namespace}/${repo.name}${repo.isPrivate ? " (private)" : ""}`).join("\n")), outcome: "allowed" };
873 }
874 if (name === "search_code") {
875 const query = String(input.query ?? "").trim();
876 if (query.length < 2) return { text: "Search for at least two characters.", outcome: "refused" };
877 const only = input.repo === undefined || input.repo === null || input.repo === "" ? null : await this.audience.repo(input.repo);
878 if (input.repo && !only) return this.withheld();
879 const allowed = await this.audience.repos();
880 // Whatever search returns, only hits in allowed repositories come through.
881 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()));
882 if (!hits.length) return { text: "No code found.", outcome: "allowed" };
883 return { text: untrusted(`search_code "${query}"`, hits.slice(0, 10).map((hit) => `${hit.repo}:${hit.path}\n${hit.snippet}`).join("\n\n")), outcome: "allowed" };
884 }
885 if (name === "recent_activity") {
886 const one = input.repo ? await this.audience.repo(input.repo) : null;
887 if (input.repo && !one) return this.withheld();
888 const repos = one ? [one] : [...(await this.audience.repos()).values()].slice(0, 20);
889 if (!repos.length) return { text: "There are no repositories everyone here can read.", outcome: "allowed" };
890 const pulls = await this.ports.recentPulls(repos, viewer);
891 if (!pulls.length) return { text: "No recent pull requests.", outcome: "allowed" };
892 return { text: untrusted("recent_activity", pulls.map((p) => `${p.repo}#${p.number} ${p.status}: ${p.title} (${p.updated_at})`).join("\n")), outcome: "allowed" };
893 }
894 // The rest name one repository; it must be on the allow-list.
895 const repo = await this.audience.repo(input.repo);
896 if (!repo) return this.withheld();
897 const full = `${repo.namespace}/${repo.name}`;
898 switch (name) {
899 case "read_file": {
900 const path = String(input.path ?? "").trim().replace(/^\/+/, "");
901 if (!path || path.split("/").some((part) => part === "..")) return { text: "Give a path inside the repository.", outcome: "refused" };
902 const ref = typeof input.ref === "string" && input.ref.trim() ? input.ref.trim() : repo.defaultBranch;
903 const file = await this.ports.readFile(repo, viewer, ref, path);
904 if (!file) return { text: `No file ${path} at ${ref} in ${full}.`, outcome: "allowed" };
905 if (file.text === null) return { text: `${full}:${path} is binary or too large to read (${file.size} bytes).`, outcome: "allowed" };
906 return { text: untrusted(`${full}:${path}@${ref}`, file.text), outcome: "allowed" };
907 }
908 case "list_issues": {
909 const state = input.state === "closed" ? "closed" : "open";
910 const issues = await this.ports.listIssues(repo, viewer, state);
911 if (!issues) return this.withheld();
912 if (!issues.length) return { text: `No ${state} issues in ${full}.`, outcome: "allowed" };
913 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" };
914 }
915 case "get_issue": {
916 const issue = await this.ports.getIssue(repo, Math.floor(Number(input.number)), viewer);
917 if (!issue) return { text: `No such issue in ${full}.`, outcome: "allowed" };
918 const comments = issue.comments.map((c) => `@${c.author}: ${c.body}`).join("\n\n");
919 return { text: untrusted(`${full}#${issue.number}`, `#${issue.number} [${issue.state}] ${issue.title}\n\n${issue.body}${comments ? `\n\nComments:\n\n${comments}` : ""}`), outcome: "allowed" };
920 }
921 case "get_pull": {
922 const pull = await this.ports.getPull(repo, Math.floor(Number(input.number)), viewer);
923 if (!pull) return { text: `No such pull request in ${full}.`, outcome: "allowed" };
924 return { text: untrusted(`${full}#${pull.number}`, `#${pull.number} [${pull.status}] ${pull.title}\n\n${pull.body}${pull.checks ? `\n\nChecks: ${pull.checks}` : ""}`), outcome: "allowed" };
925 }
926 default:
927 return { text: `There is no tool called ${name}.`, outcome: "refused" };
928 }
929 }
930}
931
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.932/** What an agent hears when it asks for a kind that isn't built yet. */
933const 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.";
934
935/**
936 * A folio id from an id or any artifact link (`/acme/-/artifacts/runbook-fol_…`,
937 * with or without the site and a query); null when there is none.
938 */
939export function folioRef(given: string): string | null {
940 const last = given.trim().split(/[?#]/)[0].split("/").filter(Boolean).at(-1) ?? "";
941 return folioIdFrom(last);
942}
943
944/** Where an artifact was written up from: a thread's link, cut to the site path the docs service keeps; null when it isn't one. */
945export function sourceLink(given: string): { title: string; href: string } | null {
946 let href = given.trim();
947 if (!href) return null;
948 if (/^https?:\/\//i.test(href)) {
949 try {
950 const url = new URL(href);
951 href = `${url.pathname}${url.search}${url.hash}`;
952 } catch {
953 return null;
954 }
955 }
956 return href.startsWith("/") && !href.startsWith("//") ? { title: "A conversation", href } : null;
957}
958
959/** A space by its id, address or name (any case), among those given. */
960function findSpace(spaces: FolioSpaceLine[], given: string): FolioSpaceLine | null {
961 const wanted = given.trim().toLowerCase();
962 return spaces.find((s) => s.id === given.trim()) ?? spaces.find((s) => s.slug.toLowerCase() === wanted) ?? spaces.find((s) => s.name.toLowerCase() === wanted) ?? null;
963}
964
965function spaceLine(s: FolioSpaceLine): string {
966 const can = s.can.edit ? "you can edit" : s.can.suggest ? "you can suggest edits" : "read only";
967 const projects = s.projects.length ? `; about ${s.projects.join(", ")}` : "";
968 return `- ${s.name} (id ${s.id}, ${s.kind}; ${can}${projects})${s.description ? `: ${s.description}` : ""}`;
969}
970
971/** 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). */
972export function folioReadText(read: FolioAgentRead): string {
973 const f = read.folio;
974 const can = read.can.edit ? "you can edit it" : read.can.suggest ? "you can suggest edits" : "you can only read it";
975 const where = read.space ? `in the ${read.space.name} space` : "not in a space";
976 const blocks = read.blocks?.length ? `\nTop-level blocks: ${read.blocks.map((b) => `${b.id} ${b.type}${b.level ? ` ${b.level}` : ""}`).join(", ")}` : "";
977 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}`;
978}
979
980/** What an edit did, naming the artifact only where everyone here can read it. */
981function editMessage(result: FolioAgentEditResult, name: boolean): string {
982 const on = name ? ` on ${result.folio.title} (${result.folio.path})` : "";
983 switch (result.mode) {
984 case "applied":
985 return `Changed${on}. It's in its history as yours.`;
986 case "suggested":
987 return `Suggested${on}: people accept or reject it there.${name ? " Link it so they can." : ""}`;
988 case "proposed":
989 return `Proposed a change${on}: a person previews and applies it.`;
990 }
Docs: a workspace knowledge base people and agents write together991}
992
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)993function messageLine(m: FoundMessage): string {
994 const where = m.channel ? `#${m.channel}` : "a direct message";
995 return `[${m.created_at.slice(0, 16)} in ${where}, channel ${m.channel_id}, message ${m.id}] @${m.author}: ${m.body}`;
996}

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