Skip to content
970 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,
3 * chat, the roster, and its colleagues (docs/WORKSPACE.md, "What an agent
4 * can and can't know", "Agents know each other").
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 }>;
130}
131
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)132/** The most tool calls one reply makes. */
133export 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 asked134/** The most tool calls one step of a session makes. */
135export 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)136/** The most of a file or result an answer is given, in characters. */
137const MAX_RESULT = 20_000;
138
139export type ToolCall = {
140 tool: string;
141 /** Its arguments, with long text cut, as recorded. */
142 args: string;
143 outcome: "allowed" | "withheld" | "refused" | "error";
144 bytes: number;
145};
146
147export type ToolResult = { text: string; outcome: ToolCall["outcome"] };
148
149/**
150 * Text a tool read, marked as data. Anything in it that looks like the
151 * closing mark is defused, so content can't end the block early.
152 */
153export function untrusted(source: string, content: string): string {
154 const safe = (text: string) => text.replace(/<\/?untrusted/gi, (mark) => mark.replace("<", "&lt;"));
155 const body = content.length > MAX_RESULT ? `${content.slice(0, MAX_RESULT)}\n[cut: ${content.length - MAX_RESULT} more characters]` : content;
156 return `<untrusted source="${safe(source).replace(/"/g, "'")}">\n${safe(body)}\n</untrusted>`;
157}
158
159/** Arguments as recorded: every string cut to 120 characters. */
160export function redact(args: unknown): string {
161 const cut = (value: unknown): unknown => {
162 if (typeof value === "string") return value.length > 120 ? `${value.slice(0, 120)}…` : value;
163 if (Array.isArray(value)) return value.slice(0, 10).map(cut);
164 if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).slice(0, 10).map(([k, v]) => [k, cut(v)]));
165 return value;
166 };
167 return JSON.stringify(cut(args ?? {})).slice(0, 1000);
168}
169
170const CODE_TOOLS: ToolDef[] = [
171 {
172 name: "list_repositories",
173 description: "The workspace's repositories everyone in this conversation can read. Start here to know what you can look at.",
174 input_schema: { type: "object", properties: {} },
175 },
176 {
177 name: "search_code",
178 description: "Search code on default branches. Optionally only in one repository (`name` or `workspace/name`).",
179 input_schema: { type: "object", properties: { query: { type: "string" }, repo: { type: "string" } }, required: ["query"] },
180 },
181 {
182 name: "read_file",
183 description: "Read a file from a repository, at its default branch or a ref.",
184 input_schema: { type: "object", properties: { repo: { type: "string" }, path: { type: "string" }, ref: { type: "string" } }, required: ["repo", "path"] },
185 },
186 {
187 name: "list_issues",
188 description: "A repository's newest issues, open by default.",
189 input_schema: { type: "object", properties: { repo: { type: "string" }, state: { type: "string", enum: ["open", "closed"] } }, required: ["repo"] },
190 },
191 {
192 name: "get_issue",
193 description: "One issue with its comments.",
194 input_schema: { type: "object", properties: { repo: { type: "string" }, number: { type: "integer" } }, required: ["repo", "number"] },
195 },
196 {
197 name: "get_pull",
198 description: "One pull request: what it changes, its status and checks.",
199 input_schema: { type: "object", properties: { repo: { type: "string" }, number: { type: "integer" } }, required: ["repo", "number"] },
200 },
201 {
202 name: "recent_activity",
203 description: "Recently merged and open pull requests, in one repository or across those you can read.",
204 input_schema: { type: "object", properties: { repo: { type: "string" } } },
205 },
206];
207
208const CHAT_TOOLS: ToolDef[] = [
209 {
210 name: "search_messages",
211 description: "Search chat messages this conversation's people can all read.",
212 input_schema: { type: "object", properties: { query: { type: "string" } }, required: ["query"] },
213 },
214 {
215 name: "read_thread",
216 description: "Read a chat thread by its channel id and a message id in it (from search_messages).",
217 input_schema: { type: "object", properties: { channel: { type: "string" }, id: { type: "string" } }, required: ["channel", "id"] },
218 },
219 {
220 name: "workspace_roster",
221 description: "The workspace's people and agents: names, teams, titles and roles.",
222 input_schema: { type: "object", properties: {} },
223 },
224];
225
226const ASK_COLLEAGUE: ToolDef = {
227 name: "ask_colleague",
228 description:
229 "Ask another agent of the workspace a question and get their answer here, without handing the work over. Use it when their role knows something yours doesn't.",
230 input_schema: { type: "object", properties: { handle: { type: "string" }, question: { type: "string" } }, required: ["handle", "question"] },
231};
232
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 asked233const REMEMBER: ToolDef = {
234 name: "remember",
235 description:
236 "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.",
237 input_schema: {
238 type: "object",
239 properties: { fact: { type: "string" }, scope: { type: "string", enum: ["workspace", "channel", "person"] } },
240 required: ["fact"],
241 },
242};
243
244const FORGET: ToolDef = {
245 name: "forget",
246 description: "Forget one of the notes under 'What you remember', by its id, when it is wrong or out of date.",
247 input_schema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
248};
249
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar250const DRAFT_ISSUE: ToolDef = {
251 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 asked252 description:
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar253 "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 asked254 input_schema: {
255 type: "object",
256 properties: {
257 repo: { type: "string" },
258 title: { type: "string" },
259 body: { type: "string" },
260 labels: { type: "array", items: { type: "string" } },
261 },
262 required: ["repo", "title", "body"],
263 },
264};
265
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar266const COMMENT: ToolDef = {
267 name: "comment",
268 description:
269 "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.",
270 input_schema: {
271 type: "object",
272 properties: { repo: { type: "string" }, number: { type: "integer" }, body: { type: "string" } },
273 required: ["repo", "number", "body"],
274 },
275};
276
277const REVIEW_PULL: ToolDef = {
278 name: "review_pull",
279 description:
280 "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.",
281 input_schema: {
282 type: "object",
283 properties: {
284 repo: { type: "string" },
285 number: { type: "integer" },
286 verdict: { type: "string", enum: ["comment", "approve", "request_changes"] },
287 body: { type: "string" },
288 },
289 required: ["repo", "number", "verdict", "body"],
290 },
291};
292
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 asked293const START_SESSION: ToolDef = {
294 name: "start_session",
295 description:
296 "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.",
297 input_schema: { type: "object", properties: { title: { type: "string" }, goal: { type: "string" } }, required: ["title", "goal"] },
298};
299
300const POST_UPDATE: ToolDef = {
301 name: "post_update",
302 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.",
303 input_schema: { type: "object", properties: { text: { type: "string" } }, required: ["text"] },
304};
305
306const USE_SUBAGENT: ToolDef = {
307 name: "use_subagent",
308 description:
309 "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.",
310 input_schema: { type: "object", properties: { name: { type: "string" }, brief: { type: "string" } }, required: ["name", "brief"] },
311};
312
313const BRING_IN: ToolDef = {
314 name: "bring_in",
315 description:
316 "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.",
317 input_schema: { type: "object", properties: { handle: { type: "string" }, brief: { type: "string" } }, required: ["handle", "brief"] },
318};
319
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.320/** What every artifact tool's description starts from, so the model never mixes them up with workflow run artifacts. */
321const ARTIFACTS =
322 "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.";
323
324const FOLIO_TOOLS: ToolDef[] = [
Docs: a workspace knowledge base people and agents write together325 {
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.326 name: "search_artifacts",
327 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.`,
328 input_schema: {
329 type: "object",
330 properties: {
331 query: { type: "string" },
332 kind: { type: "string", enum: [...FOLIO_KINDS] },
333 space: { type: "string", description: "A space's name or id." },
334 project: { type: "string", description: "A repository, `workspace/name`." },
335 },
336 required: ["query"],
337 },
Docs: a workspace knowledge base people and agents write together338 },
339 {
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.340 name: "read_artifact",
341 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.`,
342 input_schema: { type: "object", properties: { id: { type: "string", description: "Its id or link." } }, required: ["id"] },
Docs: a workspace knowledge base people and agents write together343 },
344 {
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.345 name: "list_spaces",
346 description: `${ARTIFACTS} The spaces whose artifacts everyone in this conversation can read, with what you may do in each (read, suggest, edit).`,
347 input_schema: { type: "object", properties: {} },
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store348 },
349 {
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.350 name: "stale_artifacts",
351 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.`,
352 input_schema: { type: "object", properties: { repo: { type: "string" } } },
Docs: a workspace knowledge base people and agents write together353 },
354];
355
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.356const FOLIO_WRITE_TOOLS: ToolDef[] = [
357 {
358 name: "create_artifact",
359 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.`,
360 input_schema: {
361 type: "object",
362 properties: {
363 kind: { type: "string", enum: [...FOLIO_KINDS] },
364 title: { type: "string" },
365 content: { type: "string", description: "Markdown." },
366 template: { type: "string", description: "A template id, instead of content." },
367 where: {
368 description: '{ "space": "<name or id>" }, "private" or "conversation".',
369 anyOf: [
370 { type: "string", enum: ["private", "conversation"] },
371 { type: "object", properties: { space: { type: "string" } }, required: ["space"] },
372 ],
373 },
374 parent: { type: "string", description: "A doc to put it under: its id or link." },
375 source: { type: "string", description: "The link of the thread it was written up from." },
376 },
377 required: ["kind", "title"],
378 },
379 },
Docs: a workspace knowledge base people and agents write together380 {
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.381 name: "edit_artifact",
382 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 together383 input_schema: {
384 type: "object",
385 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.386 id: { type: "string", description: "Its id or link." },
Docs: a workspace knowledge base people and agents write together387 target: { type: "string", enum: ["append", "section", "blocks", "document"] },
388 heading: { type: "string", description: "For target section: the heading's text." },
389 from_block: { type: "string" },
390 to_block: { type: "string" },
391 markdown: { type: "string" },
392 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.393 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 together394 suggest_only: { type: "boolean", description: "Suggest even where you could edit." },
395 },
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.396 required: ["id", "target", "markdown"],
Docs: a workspace knowledge base people and agents write together397 },
398 },
399 {
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.400 name: "share_artifact",
401 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 together402 input_schema: {
403 type: "object",
404 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.405 id: { type: "string", description: "Its id or link." },
406 people: { type: "array", items: { type: "string" }, description: "Usernames of people in this conversation." },
407 role: { type: "string", enum: ["view", "comment"] },
Docs: a workspace knowledge base people and agents write together408 },
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.409 required: ["id", "people", "role"],
Docs: a workspace knowledge base people and agents write together410 },
411 },
412];
413
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.414const FOLIO_NAMES = new Set([...FOLIO_TOOLS, ...FOLIO_WRITE_TOOLS].map((tool) => tool.name));
Docs: a workspace knowledge base people and agents write together415
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)416const CODE_NAMES = new Set(CODE_TOOLS.map((tool) => tool.name));
417
418export type ToolContext = {
419 /** The agent replying. */
420 agentId: string;
421 /** Handles nobody may consult from here: the agent itself, and whoever sent it the work. */
422 notConsult: string[];
423 /** Hops so far: a consult is one more, and none is offered at the limit. */
424 hops: number;
425 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 asked426 /** Whether this is a session's step (more calls, session tools) or a reply. */
427 session?: boolean;
428 /** Told of every call as it is made, for a session's transcript. */
429 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)430};
431
432export class ToolBox {
433 /** Every call this reply made, shared with the tool boxes of colleagues it consults: one budget for the reply. */
434 readonly calls: ToolCall[];
435 private readonly audience: Audience;
436 private readonly ports: ToolPorts;
437 private readonly context: ToolContext;
438
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 asked439 private readonly actions: ActionPorts | null;
440 /** Updates posted in this step. */
441 private updates = 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.442 /** Artifacts read or made in this turn that not everyone here can read: never named here. */
443 private readonly notHere = new Set<string>();
444 /**
445 * Whether this turn read an artifact the whole workspace can't: then what
446 * it remembers is kept for the person who asked alone
447 * (docs/ARTIFACTS_MODE.md, section 4.3, rule 7).
448 */
449 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 asked450
451 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)452 this.audience = audience;
453 this.ports = ports;
454 this.context = context;
455 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 asked456 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)457 }
458
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 asked459 /** 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)460 forColleague(ports: ToolPorts, context: ToolContext): ToolBox {
461 return new ToolBox(this.audience, ports, context, this.calls);
462 }
463
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 asked464 /** The most calls this box makes. */
465 get maxCalls(): number {
466 return this.context.session ? MAX_SESSION_TOOL_CALLS : MAX_TOOL_CALLS;
467 }
468
469 /** Whether the person who asked can be acted for: resolved, and able to read code here. */
470 private canFile(): boolean {
471 return !!this.actions && !!this.audience.asker && this.audience.codeAllowed();
472 }
473
474 /**
475 * The tools offered: no code tools for an audience that can't read code,
476 * no consults or hand-offs at the hop limit, session tools only in a
477 * session, and a spin-off only from chat.
478 */
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)479 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 asked480 const roomForHop = this.context.hops + 1 <= this.context.maxHops;
481 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)482 return [
483 ...(this.audience.codeAllowed() ? CODE_TOOLS : []),
484 ...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.485 // Artifacts are for everyone, Code or not: the docs service decides what this person and audience can read.
486 ...(this.ports.folios && this.audience.asker ? FOLIO_TOOLS : []),
487 ...(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 asked488 ...(roomForHop ? [ASK_COLLEAGUE] : []),
489 ...(actions ? [REMEMBER, FORGET] : []),
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar490 ...(this.canFile() ? [DRAFT_ISSUE] : []),
491 ...(this.canFile() && actions?.comment ? [COMMENT] : []),
492 ...(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 asked493 ...(actions?.startSession && !this.context.session ? [START_SESSION] : []),
494 ...(actions?.postUpdate && this.context.session ? [POST_UPDATE] : []),
495 ...(actions?.useSubagent && this.context.session && roomForHop ? [USE_SUBAGENT] : []),
496 ...(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)497 ];
498 }
499
500 /** Whether another call may be made. */
501 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 asked502 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)503 }
504
505 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 asked506 const result = await this.attempt(name, input);
507 const call: ToolCall = { tool: name, args: redact(input), outcome: result.outcome, bytes: result.text.length };
508 this.calls.push(call);
509 this.context.onCall?.(call);
510 return result;
511 }
512
513 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)514 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 asked515 const what = this.context.session ? "step" : "reply";
516 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)517 else {
518 try {
519 result = await this.dispatch(name, input ?? {});
520 } catch (error) {
521 console.error("agents: a tool failed", name, String(error));
522 result = { text: "That didn't work just now. Answer with what you have.", outcome: "error" };
523 }
524 }
525 return result;
526 }
527
528 private withheld(): ToolResult {
529 return { text: WITHHELD, outcome: "withheld" };
530 }
531
532 private async dispatch(name: string, input: Record<string, unknown>): Promise<ToolResult> {
533 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.534 if (FOLIO_NAMES.has(name)) {
535 if (!this.definitions().some((tool) => tool.name === name) || !asker || !this.ports.folios) return { text: `There is no tool called ${name} here.`, outcome: "refused" };
536 return this.folios(name, input, asker, this.ports.folios);
Docs: a workspace knowledge base people and agents write together537 }
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)538 if (CODE_NAMES.has(name)) {
539 // Not offered, and refused if asked for anyway: the check is here, not in the prompt.
540 if (!this.audience.codeAllowed() || !asker) return this.withheld();
541 return this.code(name, input, asker);
542 }
543 switch (name) {
544 case "search_messages": {
545 const query = String(input.query ?? "").trim();
546 if (query.length < 2) return { text: "Search for at least two characters.", outcome: "refused" };
547 const found = await this.ports.searchMessages(query);
548 if (found === null) return { text: "Search didn't work just now.", outcome: "error" };
549 if (!found.length) return { text: "No messages found.", outcome: "allowed" };
550 return { text: untrusted(`search_messages "${query}"`, found.map(messageLine).join("\n")), outcome: "allowed" };
551 }
552 case "read_thread": {
553 const thread = await this.ports.readThread(String(input.channel ?? ""), String(input.id ?? ""));
554 if (!thread || !thread.length) return this.withheld();
555 return { text: untrusted("read_thread", thread.map(messageLine).join("\n")), outcome: "allowed" };
556 }
557 case "workspace_roster":
558 return { text: untrusted("workspace_roster", await this.ports.roster(asker)), outcome: "allowed" };
559 case "ask_colleague": {
560 const handle = String(input.handle ?? "").trim().replace(/^@/, "").toLowerCase();
561 const question = String(input.question ?? "").trim();
562 if (!handle || !question) return { text: "Name the colleague and the question.", outcome: "refused" };
563 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" };
564 if (this.context.notConsult.includes(handle)) {
565 return { text: `You can't consult @${handle} here: they sent you this work, or it is you. Answer with what you have.`, outcome: "refused" };
566 }
567 const answer = await this.ports.consult(handle, question.slice(0, 2000));
568 if (!answer.ok) return { text: answer.message, outcome: "refused" };
569 return { text: untrusted(`@${answer.colleague}'s answer`, answer.answer), outcome: "allowed" };
570 }
571 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 asked572 return this.act(name, input);
573 }
574 }
575
Agents recall what Docs say before they answer or work, and each has required reading576 /**
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.577 * What the workspace's artifacts say about `query`, for this person and
578 * this audience, before the agent answers: no tool call, nothing counted
579 * against its tools. Empty when there is no docs service or nothing
580 * relevant.
Agents recall what Docs say before they answer or work, and each has required reading581 */
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.582 async recall(query: string | null, spaces: string[]): Promise<FolioPassage[]> {
583 const folios = this.ports.folios;
Agents recall what Docs say before they answer or work, and each has required reading584 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.585 if (!folios || !asker || !query) return [];
Agents recall what Docs say before they answer or work, and each has required reading586 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.587 return (await folios.recall(asker, this.folioAudience(), query, spaces)) ?? [];
Agents recall what Docs say before they answer or work, and each has required reading588 } 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.589 console.error("agents: artifacts recall failed", String(error));
Agents recall what Docs say before they answer or work, and each has required reading590 return [];
591 }
592 }
593
Docs: a workspace knowledge base people and agents write together594 /** 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.595 private folioAudience(): FolioAudience {
Docs: a workspace knowledge base people and agents write together596 return this.audience.shared ? { kind: "workspace" } : { kind: "people", user_ids: this.audience.members.map((m) => m.id) };
597 }
598
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 /** Whether anyone besides the person who asked reads what is said here. */
600 private othersHere(asker: User): boolean {
601 return this.audience.shared || this.audience.members.some((m) => m.id !== asker.id);
602 }
603
604 private async folios(name: string, input: Record<string, unknown>, asker: User, folios: FoliosPorts): Promise<ToolResult> {
Docs: a workspace knowledge base people and agents write together605 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.606 const audience = this.folioAudience();
Docs: a workspace knowledge base people and agents write together607 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.608 case "list_spaces": {
609 const spaces = await folios.spaces(asker, audience);
610 if (spaces === null) return { text: "Spaces couldn't be listed just now.", outcome: "error" };
611 if (!spaces.length) return { text: "There are no spaces everyone here can read.", outcome: "allowed" };
612 return { text: untrusted("list_spaces", spaces.map(spaceLine).join("\n")), outcome: "allowed" };
613 }
614 case "search_artifacts": {
Docs: a workspace knowledge base people and agents write together615 const query = text("query", 200);
616 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.617 const kind = text("kind", 20) || null;
618 if (kind && !isFolioKind(kind)) return { text: `There is no kind of artifact called ${kind}: it is doc, slides, design or dashboard.`, outcome: "refused" };
619 let spaceId: string | null = null;
620 if (text("space", 200)) {
621 const space = findSpace((await folios.spaces(asker, audience)) ?? [], text("space", 200));
622 if (!space) return this.withheld();
623 spaceId = space.id;
624 }
625 const found = await folios.search(asker, audience, { query, kind: kind as FolioKind | null, space_id: spaceId, project: text("project", 200).toLowerCase() || null });
626 if (found === null) return { text: "Search didn't work just now.", outcome: "error" };
627 return { text: untrusted(`search_artifacts "${query}"`, found), outcome: "allowed" };
628 }
629 case "stale_artifacts": {
630 const found = await folios.stale(asker, audience, text("repo", 200).toLowerCase() || null);
631 if (found === null) return { text: "Out-of-date artifacts couldn't be listed just now.", outcome: "error" };
632 return { text: untrusted("stale_artifacts", found), outcome: "allowed" };
Docs: a workspace knowledge base people and agents write together633 }
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.634 case "read_artifact": {
635 const id = folioRef(text("id", 500));
636 if (!id) return { text: "Give the artifact's id (fol_…) or its link.", outcome: "refused" };
637 const found = await folios.read(asker, audience, id);
638 if (!found.ok) return found.code === "not_found" || found.code === "forbidden" ? this.withheld() : { text: found.message, outcome: "refused" };
639 const read = found.value;
640 if (!this.audience.shared || !read.audience_can_read) this.privateRead = true;
641 if (!read.audience_can_read) return this.notForEveryone(asker, read.folio, folios, "found");
642 return { text: untrusted(`read_artifact ${id}`, folioReadText(read)), outcome: "allowed" };
Docs: a workspace knowledge base people and agents write together643 }
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.644 case "create_artifact": {
645 const kind = text("kind", 20) || "doc";
646 if (!isFolioKind(kind)) return { text: `There is no kind of artifact called ${kind}.`, outcome: "refused" };
647 if (kind !== "doc") return { text: NOT_YET, outcome: "refused" };
648 const title = text("title", 200);
649 const template = text("template", 100) || null;
650 const markdown = String(input.content ?? "").slice(0, 100_000);
651 if (!title) return { text: "An artifact needs a title.", outcome: "refused" };
652 if (!markdown.trim() && !template) return { text: "Give its content as Markdown, or a template.", outcome: "refused" };
653 const parentGiven = text("parent", 500);
654 const parent = parentGiven ? folioRef(parentGiven) : null;
655 if (parentGiven && !parent) return { text: "Give the parent doc's id or link.", outcome: "refused" };
656 const place = await this.whereFor(input.where, asker, folios);
657 if (!place.ok) return { text: place.message, outcome: "refused" };
658 const make = (where: FolioWhere) =>
659 folios.create(asker, { kind, title, markdown: template ? null : markdown, template_id: template, where, parent_id: parent, source: sourceLink(text("source", 2000)) });
660 let made = await make(place.where);
661 // The General space by default, unless the person who asked can't add there: then their Private.
662 if (!made.ok && place.fallback && made.code === "forbidden") made = await make("private");
663 if (!made.ok) return { text: `It couldn't be made: ${made.message}`, outcome: "refused" };
664 const ref = made.value;
665 if (this.othersHere(asker)) {
666 const check = await folios.read(asker, audience, ref.id).catch(() => null);
667 if (!check?.ok || !check.value.audience_can_read) return this.notForEveryone(asker, ref, folios, "made");
668 }
669 return { text: `Wrote ${ref.title} (${ref.path}, id ${ref.id}). Link it.`, outcome: "allowed" };
670 }
671 case "edit_artifact": {
672 const id = folioRef(text("id", 500));
Docs: a workspace knowledge base people and agents write together673 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.674 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 together675 const kind = text("target", 20);
676 const target: DocEditTarget | null =
677 kind === "append"
678 ? { kind: "append" }
679 : kind === "document"
680 ? { kind: "document" }
681 : kind === "section" && text("heading", 300)
682 ? { kind: "section", heading: text("heading", 300) }
683 : kind === "blocks" && text("from_block", 100) && text("to_block", 100)
684 ? { kind: "blocks", from_block: text("from_block", 100), to_block: text("to_block", 100) }
685 : null;
686 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.687 const edit: FolioAgentEdit = { kind: "doc", target, markdown, note: text("note", 300) || null, suggest_only: input.suggest_only === true, marks_current: input.marks_current === true };
688 const done = await folios.edit(asker, id, edit);
689 if (!done.ok) return { text: `That didn't work: ${done.message}`, outcome: "refused" };
690 return { text: editMessage(done.value, !this.notHere.has(done.value.folio.id)), outcome: "allowed" };
Docs: a workspace knowledge base people and agents write together691 }
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.692 case "share_artifact": {
693 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" };
694 const id = folioRef(text("id", 500));
695 if (!id) return { text: "Give the artifact's id (fol_…) or its link.", outcome: "refused" };
696 const role = input.role === "view" || input.role === "comment" ? input.role : null;
697 if (!role) return { text: "You can share to view or comment only. For more, ask the person to use Share.", outcome: "refused" };
698 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);
699 const people = named.map((n) => this.audience.members.find((m) => m.username.toLowerCase() === n || m.id === n) ?? n);
700 const outside = people.filter((p): p is string => typeof p === "string");
701 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" };
702 const users = [...new Map((people as User[]).filter((u) => u.id !== asker.id).map((u) => [u.id, u])).values()];
703 if (!users.length) return { text: "Name who to share it with: people in this conversation besides the person who asked.", outcome: "refused" };
704 const done = await folios.share(asker, audience, id, users.map((u) => u.id), role);
705 if (!done.ok) return { text: `It couldn't be shared: ${done.message}`, outcome: "refused" };
706 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 together707 }
708 default:
709 return { text: `There is no tool called ${name}.`, outcome: "refused" };
710 }
711 }
712
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.713 /**
714 * Where a new artifact goes (docs/ARTIFACTS_MODE.md, section 4.1):
715 * - a space named by its name or id, among those everyone here can read;
716 * - "private": the asker's Private;
717 * - "conversation": Private, plus `view` for this conversation's people;
718 * - nothing: the conversation in a DM or private channel, and in a public
719 * channel the General space if the asker can add there (`fallback`:
720 * their Private if the docs service says they can't).
721 */
722 private async whereFor(given: unknown, asker: User, folios: FoliosPorts): Promise<{ ok: true; where: FolioWhere; fallback: boolean } | { ok: false; message: string }> {
723 const named = typeof given === "object" && given !== null ? (given as { space?: unknown }).space : given;
724 const wanted = typeof named === "string" ? named.trim().slice(0, 200) : "";
725 const others = this.audience.members.filter((m) => m.id !== asker.id).map((m) => m.id);
726 const byDefault = async (): Promise<{ ok: true; where: FolioWhere; fallback: boolean }> => {
727 if (!this.audience.shared) return { ok: true, where: others.length ? { conversation: this.audience.members.map((m) => m.id) } : "private", fallback: false };
728 const spaces = (await folios.spaces(asker, this.folioAudience())) ?? [];
729 const general = spaces.find((s) => s.slug === "general") ?? spaces.find((s) => s.name.toLowerCase() === "general");
730 return general && general.can.suggest ? { ok: true, where: { space_id: general.id }, fallback: true } : { ok: true, where: "private", fallback: false };
731 };
732 if (!wanted) return byDefault();
733 const lower = wanted.toLowerCase();
734 if (typeof given === "string" && lower === "private") return { ok: true, where: "private", fallback: false };
735 // A public channel has no list of people to share with: its default instead.
736 if (typeof given === "string" && lower === "conversation") return byDefault();
737 const space = findSpace((await folios.spaces(asker, this.folioAudience())) ?? [], wanted);
738 if (space) return { ok: true, where: { space_id: space.id }, fallback: false };
739 // An id the asker gave, for a space not everyone here can read: the docs service checks it.
740 if (/^spc_[A-Za-z0-9]+$/.test(wanted)) return { ok: true, where: { space_id: wanted }, fallback: false };
741 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".` };
742 }
743
744 /**
745 * An artifact someone here can't read (docs/ARTIFACTS_MODE.md, section
746 * 4.3, rule 2): the link goes to the asker directly, and the agent says
747 * only that it found or made something, never what.
748 */
749 private async notForEveryone(asker: User, folio: FolioRef, folios: FoliosPorts, what: "found" | "made"): Promise<ToolResult> {
750 this.notHere.add(folio.id);
751 const who = `@${asker.username}`;
752 const note =
753 what === "made"
754 ? "I made this for you. Not everyone in the conversation you asked from can open it, so here is the link:"
755 : "Here is what you asked about. Not everyone in the conversation you asked from can open it, so here is the link:";
756 const sent = await folios.sendLink(asker, { title: folio.title, path: folio.path }, note).catch(() => false);
757 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;`;
758 const text = sent
759 ? `${lead} say you ${what} it and that you've sent the link to ${who} directly.`
760 : what === "made"
761 ? `${lead} say you made it and that ${who} will find it under Artifacts, in their Private or shared with them.`
762 : `${lead} say you found it but can't share it here, and ask ${who} to message you directly.`;
763 return { text, outcome: what === "made" ? "allowed" : "withheld" };
764 }
765
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 asked766 /** Doing, not reading: memory, issues, sessions. Each refused unless offered. */
767 private async act(name: string, input: Record<string, unknown>): Promise<ToolResult> {
768 const actions = this.actions;
769 const offered = this.definitions().some((tool) => tool.name === name);
770 if (!actions || !offered) return { text: `There is no tool called ${name} here.`, outcome: "refused" };
771 const said = (answer: { ok: boolean; message: string }): ToolResult => ({ text: answer.message, outcome: answer.ok ? "allowed" : "refused" });
772 const text = (key: string, max: number) => String(input[key] ?? "").trim().slice(0, max);
773 switch (name) {
774 case "remember": {
775 const fact = text("fact", 2000);
776 if (!fact) return { text: "Say what to remember.", outcome: "refused" };
777 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.778 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 asked779 }
780 case "forget":
781 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 calendar782 case "draft_issue": {
783 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 asked784 const repo = await this.audience.repo(input.repo);
785 if (!repo) return this.withheld();
786 const title = text("title", 200);
787 const body = text("body", 20_000);
788 if (!title || !body) return { text: "An issue needs a title and a body.", outcome: "refused" };
789 const labels = Array.isArray(input.labels)
790 ? input.labels.filter((l): l is string => typeof l === "string").map((l) => l.trim()).filter(Boolean).slice(0, 5)
791 : [];
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar792 return said(await actions.draftIssue(repo, { title, body, labels }));
793 }
794 case "comment":
795 case "review_pull": {
796 const asker = this.audience.asker;
797 if (!asker || !this.audience.codeAllowed()) return this.withheld();
798 const repo = await this.audience.repo(input.repo);
799 if (!repo) return this.withheld();
800 const number = Math.floor(Number(input.number));
801 if (!Number.isFinite(number) || number < 1) return { text: "Give the issue or pull request's number.", outcome: "refused" };
802 const body = text("body", 20_000);
803 if (name === "comment") {
804 if (!body) return { text: "Say what to comment.", outcome: "refused" };
805 return said(await actions.comment!(repo, asker, number, body));
806 }
807 const verdict = input.verdict === "approve" || input.verdict === "request_changes" ? input.verdict : "comment";
808 if (!body && verdict !== "approve") return { text: "A review needs its text.", outcome: "refused" };
809 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 asked810 }
811 case "start_session": {
812 const title = text("title", 120);
813 const goal = text("goal", 8000);
814 if (!title || !goal) return { text: "A session needs a title and a goal.", outcome: "refused" };
815 return said(await actions.startSession!(title, goal));
816 }
817 case "post_update": {
818 const note = text("text", 2000);
819 if (!note) return { text: "Say what to post.", outcome: "refused" };
820 if (this.updates >= 3) return { text: "You've posted enough updates for this step; carry on with the work.", outcome: "refused" };
821 this.updates++;
822 return said(await actions.postUpdate!(note));
823 }
824 case "use_subagent": {
825 const helper = text("name", 60).toLowerCase();
826 const brief = text("brief", 8000);
827 if (!helper || !brief) return { text: "Name the subagent and give it a brief.", outcome: "refused" };
828 return said(await actions.useSubagent!(helper, brief));
829 }
830 case "bring_in": {
831 const handle = text("handle", 60).replace(/^@/, "").toLowerCase();
832 const brief = text("brief", 8000);
833 if (!handle || !brief) return { text: "Name the colleague and give them a brief.", outcome: "refused" };
834 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" };
835 return said(await actions.bringIn!(handle, brief));
836 }
837 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)838 return { text: `There is no tool called ${name}.`, outcome: "refused" };
839 }
840 }
841
842 private async code(name: string, input: Record<string, unknown>, viewer: User): Promise<ToolResult> {
843 if (name === "list_repositories") {
844 const repos = [...(await this.audience.repos()).values()];
845 if (!repos.length) return { text: "There are no repositories everyone here can read.", outcome: "allowed" };
846 return { text: untrusted("list_repositories", repos.map((repo) => `${repo.namespace}/${repo.name}${repo.isPrivate ? " (private)" : ""}`).join("\n")), outcome: "allowed" };
847 }
848 if (name === "search_code") {
849 const query = String(input.query ?? "").trim();
850 if (query.length < 2) return { text: "Search for at least two characters.", outcome: "refused" };
851 const only = input.repo === undefined || input.repo === null || input.repo === "" ? null : await this.audience.repo(input.repo);
852 if (input.repo && !only) return this.withheld();
853 const allowed = await this.audience.repos();
854 // Whatever search returns, only hits in allowed repositories come through.
855 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()));
856 if (!hits.length) return { text: "No code found.", outcome: "allowed" };
857 return { text: untrusted(`search_code "${query}"`, hits.slice(0, 10).map((hit) => `${hit.repo}:${hit.path}\n${hit.snippet}`).join("\n\n")), outcome: "allowed" };
858 }
859 if (name === "recent_activity") {
860 const one = input.repo ? await this.audience.repo(input.repo) : null;
861 if (input.repo && !one) return this.withheld();
862 const repos = one ? [one] : [...(await this.audience.repos()).values()].slice(0, 20);
863 if (!repos.length) return { text: "There are no repositories everyone here can read.", outcome: "allowed" };
864 const pulls = await this.ports.recentPulls(repos, viewer);
865 if (!pulls.length) return { text: "No recent pull requests.", outcome: "allowed" };
866 return { text: untrusted("recent_activity", pulls.map((p) => `${p.repo}#${p.number} ${p.status}: ${p.title} (${p.updated_at})`).join("\n")), outcome: "allowed" };
867 }
868 // The rest name one repository; it must be on the allow-list.
869 const repo = await this.audience.repo(input.repo);
870 if (!repo) return this.withheld();
871 const full = `${repo.namespace}/${repo.name}`;
872 switch (name) {
873 case "read_file": {
874 const path = String(input.path ?? "").trim().replace(/^\/+/, "");
875 if (!path || path.split("/").some((part) => part === "..")) return { text: "Give a path inside the repository.", outcome: "refused" };
876 const ref = typeof input.ref === "string" && input.ref.trim() ? input.ref.trim() : repo.defaultBranch;
877 const file = await this.ports.readFile(repo, viewer, ref, path);
878 if (!file) return { text: `No file ${path} at ${ref} in ${full}.`, outcome: "allowed" };
879 if (file.text === null) return { text: `${full}:${path} is binary or too large to read (${file.size} bytes).`, outcome: "allowed" };
880 return { text: untrusted(`${full}:${path}@${ref}`, file.text), outcome: "allowed" };
881 }
882 case "list_issues": {
883 const state = input.state === "closed" ? "closed" : "open";
884 const issues = await this.ports.listIssues(repo, viewer, state);
885 if (!issues) return this.withheld();
886 if (!issues.length) return { text: `No ${state} issues in ${full}.`, outcome: "allowed" };
887 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" };
888 }
889 case "get_issue": {
890 const issue = await this.ports.getIssue(repo, Math.floor(Number(input.number)), viewer);
891 if (!issue) return { text: `No such issue in ${full}.`, outcome: "allowed" };
892 const comments = issue.comments.map((c) => `@${c.author}: ${c.body}`).join("\n\n");
893 return { text: untrusted(`${full}#${issue.number}`, `#${issue.number} [${issue.state}] ${issue.title}\n\n${issue.body}${comments ? `\n\nComments:\n\n${comments}` : ""}`), outcome: "allowed" };
894 }
895 case "get_pull": {
896 const pull = await this.ports.getPull(repo, Math.floor(Number(input.number)), viewer);
897 if (!pull) return { text: `No such pull request in ${full}.`, outcome: "allowed" };
898 return { text: untrusted(`${full}#${pull.number}`, `#${pull.number} [${pull.status}] ${pull.title}\n\n${pull.body}${pull.checks ? `\n\nChecks: ${pull.checks}` : ""}`), outcome: "allowed" };
899 }
900 default:
901 return { text: `There is no tool called ${name}.`, outcome: "refused" };
902 }
903 }
904}
905
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.906/** What an agent hears when it asks for a kind that isn't built yet. */
907const 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.";
908
909/**
910 * A folio id from an id or any artifact link (`/acme/-/artifacts/runbook-fol_…`,
911 * with or without the site and a query); null when there is none.
912 */
913export function folioRef(given: string): string | null {
914 const last = given.trim().split(/[?#]/)[0].split("/").filter(Boolean).at(-1) ?? "";
915 return folioIdFrom(last);
916}
917
918/** 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. */
919export function sourceLink(given: string): { title: string; href: string } | null {
920 let href = given.trim();
921 if (!href) return null;
922 if (/^https?:\/\//i.test(href)) {
923 try {
924 const url = new URL(href);
925 href = `${url.pathname}${url.search}${url.hash}`;
926 } catch {
927 return null;
928 }
929 }
930 return href.startsWith("/") && !href.startsWith("//") ? { title: "A conversation", href } : null;
931}
932
933/** A space by its id, address or name (any case), among those given. */
934function findSpace(spaces: FolioSpaceLine[], given: string): FolioSpaceLine | null {
935 const wanted = given.trim().toLowerCase();
936 return spaces.find((s) => s.id === given.trim()) ?? spaces.find((s) => s.slug.toLowerCase() === wanted) ?? spaces.find((s) => s.name.toLowerCase() === wanted) ?? null;
937}
938
939function spaceLine(s: FolioSpaceLine): string {
940 const can = s.can.edit ? "you can edit" : s.can.suggest ? "you can suggest edits" : "read only";
941 const projects = s.projects.length ? `; about ${s.projects.join(", ")}` : "";
942 return `- ${s.name} (id ${s.id}, ${s.kind}; ${can}${projects})${s.description ? `: ${s.description}` : ""}`;
943}
944
945/** 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). */
946export function folioReadText(read: FolioAgentRead): string {
947 const f = read.folio;
948 const can = read.can.edit ? "you can edit it" : read.can.suggest ? "you can suggest edits" : "you can only read it";
949 const where = read.space ? `in the ${read.space.name} space` : "not in a space";
950 const blocks = read.blocks?.length ? `\nTop-level blocks: ${read.blocks.map((b) => `${b.id} ${b.type}${b.level ? ` ${b.level}` : ""}`).join(", ")}` : "";
951 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}`;
952}
953
954/** What an edit did, naming the artifact only where everyone here can read it. */
955function editMessage(result: FolioAgentEditResult, name: boolean): string {
956 const on = name ? ` on ${result.folio.title} (${result.folio.path})` : "";
957 switch (result.mode) {
958 case "applied":
959 return `Changed${on}. It's in its history as yours.`;
960 case "suggested":
961 return `Suggested${on}: people accept or reject it there.${name ? " Link it so they can." : ""}`;
962 case "proposed":
963 return `Proposed a change${on}: a person previews and applies it.`;
964 }
Docs: a workspace knowledge base people and agents write together965}
966
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)967function messageLine(m: FoundMessage): string {
968 const where = m.channel ? `#${m.channel}` : "a direct message";
969 return `[${m.created_at.slice(0, 16)} in ${where}, channel ${m.channel_id}, message ${m.id}] @${m.author}: ${m.body}`;
970}

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