Skip to content
269 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.

Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { FolioRef, User } from "@g1t/contracts";
5
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.6import { AGENT_TOOL_NAMES, RESERVED_SKILL_NAMES, SKILLS_PER_AGENT_MAX } from "../../../packages/contracts/src/skill-format.ts";
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.7import { FOUNDATIONAL_SKILLS, FOUNDATIONAL_SKILL_IDS, skillTools, skillsOn } from "../../../packages/contracts/src/skills.ts";
8import { Audience, type AudienceInfo } from "./audience.ts";
9import { applyChanges } from "./definition.ts";
10import { systemPrompt } from "./prompt.ts";
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.11import { type AttachedRow, shelfFrom, skillsSection, teamSlugs } from "./skills.ts";
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.12import { TEMPLATE_IDS } from "./templates.ts";
13import { type FoliosPorts, type ToolPorts, TOOL_NAMES, ToolBox, docBody } from "./tools.ts";
14
15test("the foundational skills name only tools agents have, and say what's coming", () => {
16 assert.deepEqual(FOUNDATIONAL_SKILL_IDS, ["documents", "research", "data", "code", "communication", "files"]);
17 for (const skill of FOUNDATIONAL_SKILLS) {
18 assert.equal(skill.source, "foundational");
19 assert.ok(skill.instructions.length > 100, `${skill.id} has a playbook`);
20 for (const ability of skill.abilities) {
21 for (const tool of ability.tools) assert.ok(TOOL_NAMES.has(tool), `${skill.id}/${ability.id} names ${tool}, which agents have`);
22 if (ability.status === "coming") assert.deepEqual(ability.tools, [], `${skill.id}/${ability.id} is coming, so it uses no tool yet`);
23 assert.ok(ability.note, `${skill.id}/${ability.id} says how or what's missing`);
24 }
25 // Every tool a playbook tells the agent to call is one it can have.
26 for (const [, tool] of skill.instructions.matchAll(/\b([a-z]+_[a-z_]+)\b/g)) assert.ok(TOOL_NAMES.has(tool!), `${skill.id}'s playbook names ${tool}`);
27 }
28 assert.ok(skillTools(FOUNDATIONAL_SKILLS[0]!).includes("make_file"));
29 const coming = FOUNDATIONAL_SKILLS.flatMap((s) => s.abilities.filter((a) => a.status === "coming").map((a) => a.id));
30 for (const id of ["slides", "search", "browse", "sql", "run", "schedule", "ocr", "images"]) assert.ok(coming.includes(id), `${id} is marked coming`);
31});
32
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.33test("the tools a skill may name are exactly the tools agents have", () => {
34 assert.deepEqual([...AGENT_TOOL_NAMES].sort(), [...TOOL_NAMES].sort());
35 assert.deepEqual(RESERVED_SKILL_NAMES, FOUNDATIONAL_SKILL_IDS);
36});
37
38const library = (over: Partial<AttachedRow> = {}): AttachedRow => ({
39 skill_id: "skl_01k7a0b1c2d3e4f5g6h7j8k9mn",
40 name: "release-notes",
41 description: "Use when someone asks for release notes.",
42 version: 3,
43 tools: '["recent_activity","create_artifact","teleport_tool"]',
44 requires_computer: 0,
45 scope: "workspace",
46 attached_at: "2026-10-01T00:00:00Z",
47 ...over,
48});
49
50test("the prompt names each skill and when to use it; the playbooks stay out until read", () => {
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.51 const all = [...TOOL_NAMES];
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.52 const { skills } = shelfFrom([], [library()]);
53 const section = skillsSection(skills, all)!;
54 for (const skill of FOUNDATIONAL_SKILLS) assert.match(section, new RegExp(`^- ${skill.id}: ${skill.when.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m"));
55 assert.match(section, /^- release-notes: Use when someone asks for release notes\.$/m);
56 assert.match(section, /call use_skill with its name before you start/);
57 assert.match(section, /never add one/);
58 for (const skill of FOUNDATIONAL_SKILLS) assert.ok(!section.includes(skill.instructions), `${skill.id}'s playbook isn't in the prompt`);
59 // Without use_skill (no tools at all), nothing is listed: no skill could be followed.
60 assert.equal(skillsSection(skills, all.filter((t) => t !== "use_skill")), null);
61 // Off: gone from the list, the rest stays; all off and none attached, no section.
62 const someOff = skillsSection(shelfFrom(["communication", "files", "skl_01k7a0b1c2d3e4f5g6h7j8k9mn"], [library()]).skills, all)!;
63 assert.doesNotMatch(someOff, /^- communication:/m);
64 assert.doesNotMatch(someOff, /^- files:/m);
65 assert.doesNotMatch(someOff, /release-notes/);
66 assert.match(someOff, /^- documents:/m);
67 assert.equal(skillsSection(shelfFrom(FOUNDATIONAL_SKILL_IDS, []).skills, all), null);
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.68 assert.equal(skillsOn(["data"]).length, 5);
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.69 // A skill that needs a computer is marked: as usable when the session has one (run_command offered), as out of reach otherwise.
70 assert.match(skillsSection(shelfFrom([], [library({ requires_computer: 1 })]).skills, all)!, /release-notes: .* Uses your computer\./);
71 assert.match(skillsSection(shelfFrom([], [library({ requires_computer: 1 })]).skills, all.filter((t) => t !== "run_command"))!, /release-notes: .* Needs your computer, which you don't have in this turn\./);
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.72 const prompt = systemPrompt({
73 agent: { id: "agt_1", handle: "ship", display_name: "Ship", role: "Release manager", instructions: "Ship.", personality_preset: "crisp", personality: "" },
74 workspace: "acme",
75 channel: { kind: "dm", name: null },
76 asker: { name: "dana", display_name: null, access: null },
77 today: new Date("2026-10-10T00:00:00Z"),
78 skills: section,
79 });
80 assert.ok(prompt.indexOf("## Your skills") > prompt.indexOf("## How to answer"), "skills come after the rules");
81});
82
83test("owners turn skills off by id; unknown ids are refused, and the list reads in the skills' order", () => {
84 const made = applyChanges(null, { handle: "ship", display_name: "Ship", role: "r", instructions: "i" }, TEMPLATE_IDS);
85 assert.ok(made.ok);
86 if (!made.ok) return;
87 assert.deepEqual(made.value.skills_off, []);
88 const off = applyChanges(made.value, { skills_off: ["files", " data ", "files"] }, TEMPLATE_IDS);
89 assert.ok(off.ok);
90 assert.deepEqual(off.ok && off.value.skills_off, ["data", "files"]);
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.91 // Library skills by their ids, after the foundational ones.
92 const lib = applyChanges(made.value, { skills_off: ["skl_01k7a0b1c2d3e4f5g6h7j8k9mn", "files"] }, TEMPLATE_IDS);
93 assert.deepEqual(lib.ok && lib.value.skills_off, ["files", "skl_01k7a0b1c2d3e4f5g6h7j8k9mn"]);
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.94 const bad = applyChanges(made.value, { skills_off: ["teleport"] }, TEMPLATE_IDS);
95 assert.equal(bad.ok, false);
96 assert.match(!bad.ok ? bad.message : "", /no skill called teleport/);
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.97 assert.equal(applyChanges(made.value, { skills_off: ["skl_short"] }, TEMPLATE_IDS).ok, false);
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.98 assert.equal(applyChanges(made.value, { skills_off: "data" as unknown as string[] }, TEMPLATE_IDS).ok, false);
99});
100
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.101test("a library skill reaches an agent once, at the version where it is attached closest, and at most 100 do", () => {
102 const id = (n: number) => `skl_${String(n).padStart(26, "0")}`;
103 const { skills } = shelfFrom(
104 [],
105 [
106 library({ scope: "workspace", version: 1 }),
107 library({ scope: "agent", version: 3, attached_at: "2026-10-05T00:00:00Z" }),
108 library({ scope: "team", version: 2 }),
109 ],
110 );
111 const lib = skills.filter((s) => s.kind === "library");
112 assert.equal(lib.length, 1);
113 assert.equal(lib[0]!.kind === "library" && lib[0]!.version, 3, "attached to the agent itself wins");
114 assert.equal(lib[0]!.kind === "library" && lib[0]!.via, "agent");
115 const many = Array.from({ length: SKILLS_PER_AGENT_MAX + 5 }, (_, n) => library({ skill_id: id(n), name: `s-${String(n).padStart(3, "0")}`, attached_at: `2026-10-01T00:00:${String(n % 60).padStart(2, "0")}Z` }));
116 const capped = shelfFrom([], many);
117 assert.equal(capped.skills.filter((s) => s.kind === "library").length, SKILLS_PER_AGENT_MAX);
118 assert.equal(capped.over, 5);
An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how.119 assert.deepEqual(teamSlugs({ teams: [{ slug: "qa" }, { slug: "web" }, { slug: "qa" }] as never, agents: [], presence: [] }), ["qa", "web"]);
120 assert.deepEqual(teamSlugs(null), [], "none when its teams couldn't be read");
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.121});
122
123test("use_skill reads a skill the agent has: the playbook with what isn't here, files, and scripts never run", async () => {
124 const { folios } = fakeFolios();
125 const tools = await box({ kind: "dm", member_user_ids: ["asker"], member_count: 1 }, folios);
126 assert.ok(!tools.definitions().some((t) => t.name === "use_skill"), "no skills, no use_skill");
127 const stored = {
128 skill_md: "---\nname: release-notes\ndescription: Use when someone asks for release notes.\ntools: [recent_activity, create_artifact]\n---\n\n# Release notes\n\nGroup changes by area.\n",
129 files: [
130 { path: "resources/template.md", content: "## Added\n\n## Fixed\n", encoding: "utf8" as const },
131 { path: "scripts/collect.py", content: "print('hi')\n", encoding: "utf8" as const },
132 { path: "resources/logo.png", content: "iVBORw0KGgo=", encoding: "base64" as const },
133 ],
134 };
135 const reads: string[] = [];
136 const shelf = shelfFrom(["files"], [library({ tools: '["recent_activity","create_artifact"]', requires_computer: 1 })]).skills;
137 tools.useShelf(shelf, async (skillId, version) => {
138 reads.push(`${skillId}@${version}`);
139 return stored;
140 });
141 assert.ok(tools.definitions().some((t) => t.name === "use_skill"));
142 const documents = await tools.run("use_skill", { name: "documents" });
143 assert.equal(documents.outcome, "allowed");
144 assert.match(documents.text, /When someone asks for a document, give them the document/);
145 assert.match(documents.text, /Not yet in g1t: slide decks\./);
146 assert.doesNotMatch(documents.text, /Not available in this conversation/, "every tool it uses is offered here");
147 const off = await tools.run("use_skill", { name: "files" });
148 assert.equal(off.outcome, "refused", "a skill that is off isn't the agent's");
149 const notes = await tools.run("use_skill", { name: "release-notes" });
150 assert.equal(notes.outcome, "allowed");
151 assert.deepEqual(reads, ["skl_01k7a0b1c2d3e4f5g6h7j8k9mn@3"], "the pinned version is read");
152 assert.match(notes.text, /^# release-notes \(your workspace's skill, version 3\)\n\n# Release notes\n\nGroup changes by area\./);
153 assert.doesNotMatch(notes.text, /^name:/m, "front-matter isn't repeated");
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.154 assert.match(notes.text, /needs a computer of its own, which you don't have in this turn: its scripts can't run/, "a reply has no computer");
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.155 assert.match(notes.text, /Its files, which you can read with use_skill and file: resources\/template\.md \(19 B\), scripts\/collect\.py \(12 B\), resources\/logo\.png \(8 B\)\./);
156 const template = await tools.run("use_skill", { name: "release-notes", file: "resources/template.md" });
157 assert.match(template.text, /## Added/);
158 const script = await tools.run("use_skill", { name: "release-notes", file: "scripts/collect.py" });
159 assert.match(script.text, /This is a script: it needs a computer of its own/);
160 const image = await tools.run("use_skill", { name: "release-notes", file: "resources/logo.png" });
161 assert.match(image.text, /isn't text/);
162 const missing = await tools.run("use_skill", { name: "teleport" });
163 assert.equal(missing.outcome, "refused");
164 assert.match(missing.text, /no skill called teleport\. Your skills: documents, research, data, code, communication, release-notes\./);
165});
166
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.167// ── make_file, end to end through the tool box ──────────────────────────
168
169const person = (id: string): User => ({ id, username: id, workspaces: [{ slug: "acme", role: "member" }] }) as User;
170
171const basePorts: ToolPorts = {
172 readFile: async () => null,
173 searchCode: async () => [],
174 listIssues: async () => [],
175 getIssue: async () => null,
176 getPull: async () => null,
177 recentPulls: async () => [],
178 searchMessages: async () => [],
179 readThread: async () => null,
180 roster: async () => "",
181 consult: async () => ({ ok: false, message: "no" }),
182};
183
184function fakeFolios(opts: { audienceCanRead?: boolean; canEdit?: boolean } = {}) {
185 const log: { created: { title: string; markdown: string | null }[]; attached: { folio: string; name: string; type: string; bytes: number }[]; edits: string[]; links: string[] } = { created: [], attached: [], edits: [], links: [] };
186 const ref = (id: string, title: string): FolioRef => ({ id, kind: "doc", title, path: `/acme/-/artifacts/${id}`, icon: null }) as unknown as FolioRef;
187 const folios: FoliosPorts = {
188 spaces: async () => [],
189 recall: async () => [],
190 search: async () => "",
191 read: async (_v, _a, id) => ({
192 ok: true,
193 value: { folio: { ...ref(id, "Existing"), edited_at: "2026-10-10" }, space: null, content: "", can: { read: true, suggest: true, edit: opts.canEdit ?? true }, audience_can_read: opts.audienceCanRead ?? true } as never,
194 }),
195 stale: async () => "",
196 create: async (_v, input) => {
197 log.created.push({ title: input.title, markdown: input.markdown });
198 return { ok: true, value: ref("fol_new", input.title) };
199 },
200 edit: async (_v, _id, edit) => {
201 log.edits.push(edit.kind === "doc" ? edit.markdown : "");
202 return { ok: true, value: { mode: "applied", version_id: null, folio: ref("fol_new", "x"), summary: "" } };
203 },
204 share: async () => ({ ok: true, value: null }),
205 attach: async (_v, folio, file) => {
206 log.attached.push({ folio, name: file.name, type: file.content_type, bytes: file.bytes.length });
207 return { ok: true, value: { url: "https://g1tusercontent.com/docs-files/abc", name: file.name, bytes: file.bytes.length } };
208 },
209 sendLink: async (_a, link) => {
210 log.links.push(link.path);
211 return true;
212 },
213 };
214 return { folios, log };
215}
216
217const actions = { remember: async () => ({ ok: true, message: "" }), forget: async () => ({ ok: true, message: "" }), draftIssue: async () => ({ ok: true, message: "" }) };
218const context = { agentId: "agt_me", notConsult: [], hops: 0, maxHops: 4 };
219
220async function box(info: AudienceInfo, folios: FoliosPorts) {
221 const people = [person("asker"), person("bea")];
222 const audience = await Audience.build("acme", "asker", {
223 info: async () => info,
224 users: async (ids) => people.filter((u) => ids.includes(u.id)),
225 workspaceRepos: async () => [],
226 readable: async () => [],
227 });
228 return new ToolBox(audience, { ...basePorts, folios }, context, [], actions);
229}
230
231test("make_file makes the PDF, keeps it with a new doc holding its text, links it, and hands back the link", async () => {
232 const { folios, log } = fakeFolios();
233 const tools = await box({ kind: "dm", member_user_ids: ["asker"], member_count: 1 }, folios);
234 assert.ok(tools.definitions().some((t) => t.name === "make_file"));
235 const result = await tools.run("make_file", { format: "pdf", title: "Q3 report", content: "# Q3 report\n\nRevenue grew." });
236 assert.equal(result.outcome, "allowed");
237 assert.match(result.text, /Made Q3 report\.pdf \(1 page, \d+ KB\)/);
238 assert.match(result.text, /https:\/\/g1tusercontent\.com\/docs-files\/abc/);
239 assert.deepEqual(log.created, [{ title: "Q3 report", markdown: "Revenue grew." }], "the doc has its own title, so the heading isn't repeated");
240 assert.equal(log.attached[0]!.type, "application/pdf");
241 assert.equal(log.attached[0]!.folio, "fol_new");
242 assert.match(log.edits[0]!, /^\*\*File:\*\* \[Q3 report\.pdf\]\(https:\/\/g1tusercontent\.com\/docs-files\/abc\)/);
243});
244
245test("make_file attaches to a doc only where it may edit, and keeps a doc others here can't read out of the conversation", async () => {
246 const readOnly = fakeFolios({ canEdit: false });
247 const tools = await box({ kind: "dm", member_user_ids: ["asker"], member_count: 1 }, readOnly.folios);
248 const refused = await tools.run("make_file", { format: "csv", title: "Rows", sheets: [{ rows: [["a"], [1]] }], artifact: "fol_01k7a0b1c2d3e4f5g6h7j8k9mn" });
249 assert.equal(refused.outcome, "refused");
250 assert.match(refused.text, /can.t edit that doc/);
251 assert.equal(readOnly.log.attached.length, 0);
252
253 const hidden = fakeFolios({ audienceCanRead: false });
254 const group = await box({ kind: "private", member_user_ids: ["asker", "bea"], member_count: 2 }, hidden.folios);
255 const sent = await group.run("make_file", { format: "xlsx", title: "Salaries", sheets: [{ rows: [["name", "pay"], ["bea", 1]] }], artifact: "fol_01k7a0b1c2d3e4f5g6h7j8k9mn" });
256 assert.equal(hidden.log.attached.length, 1, `kept with the doc: ${sent.text}`);
257 assert.doesNotMatch(sent.text, /docs-files/, "the link isn't given here");
258 assert.deepEqual(hidden.log.links, ["/acme/-/artifacts/fol_01k7a0b1c2d3e4f5g6h7j8k9mn"], "the person who asked gets it directly");
259});
260
261test("a spreadsheet's doc shows its rows; a format it can't write is refused with what to do instead", async () => {
262 assert.match(docBody("xlsx", "Sales", { sheets: [{ name: "S", rows: [["a", "b"], [1, 2]] }] }), /^\| a \| b \|/);
263 assert.equal(docBody("pdf", "T", { content: "# T" }), "The file is attached below.");
264 const { folios } = fakeFolios();
265 const tools = await box({ kind: "dm", member_user_ids: ["asker"], member_count: 1 }, folios);
266 const deck = await tools.run("make_file", { format: "pptx", title: "Deck" });
267 assert.equal(deck.outcome, "refused");
268 assert.match(deck.text, /offer a PDF or a doc instead/);
269});