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

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 asked1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { User } from "@g1t/contracts";
5
6import { Audience, type AudienceInfo, type AudiencePorts, type RepoRef, WITHHELD } from "./audience.ts";
7import { type MemoryViewer, type RecallPlace, changeableBy, cleanFact, memorySection, recallable, scopeFor, visibleTo } from "./memory.ts";
8import { alertDue, checkPolicy, DEFAULT_POLICY, policyBlock } from "./policy.ts";
9import { checkRoutine, checkSchedule, describeSchedule, nextRun } from "./schedule.ts";
10import { type ActionPorts, type ToolPorts, MAX_SESSION_TOOL_CALLS, MAX_TOOL_CALLS, ToolBox } from "./tools.ts";
11import { dollars } from "./money.ts";
12
13// ── Memory: where a fact is recalled, who sees it, who changes it ───────
14
15const dmWithAnn: RecallPlace = { channel_id: "chn_dm_ann", kind: "dm", people: ["ann"] };
16const groupDm: RecallPlace = { channel_id: "chn_dm_group", kind: "dm", people: ["ann", "bob"] };
17const privateOps: RecallPlace = { channel_id: "chn_ops", kind: "private", people: ["ann", "bob"] };
18const publicGeneral: RecallPlace = { channel_id: "chn_general", kind: "public", people: ["ann"] };
19
20test("a person's fact is recalled only in a direct message with that one person", () => {
21 const fact = { scope: "person", scope_ref: "ann" };
22 assert.equal(recallable(fact, dmWithAnn), true);
23 assert.equal(recallable(fact, groupDm), false, "not with Bob there too");
24 assert.equal(recallable(fact, privateOps), false);
25 assert.equal(recallable(fact, publicGeneral), false);
26 assert.equal(recallable(fact, { channel_id: "chn_dm_bob", kind: "dm", people: ["bob"] }), false);
27});
28
29test("a conversation's fact stays in it; a workspace fact goes anywhere", () => {
30 assert.equal(recallable({ scope: "channel", scope_ref: "chn_ops" }, privateOps), true);
31 assert.equal(recallable({ scope: "channel", scope_ref: "chn_ops" }, publicGeneral), false);
32 assert.equal(recallable({ scope: "workspace", scope_ref: "" }, dmWithAnn), true);
33 assert.equal(recallable({ scope: "made-up", scope_ref: "" }, dmWithAnn), false);
34});
35
36test("an agent remembers into the narrowest scope the conversation allows", () => {
37 assert.deepEqual(scopeFor(dmWithAnn, null), { scope: "person", ref: "ann" });
38 assert.deepEqual(scopeFor(dmWithAnn, "workspace"), { scope: "person", ref: "ann" }, "a DM can't write for the workspace");
39 assert.deepEqual(scopeFor(privateOps, "workspace"), { scope: "channel", ref: "chn_ops" }, "nor can a private channel");
40 assert.deepEqual(scopeFor(groupDm, "person"), { scope: "channel", ref: "chn_dm_group" });
41 assert.deepEqual(scopeFor(publicGeneral, "workspace"), { scope: "workspace", ref: "" });
42 assert.deepEqual(scopeFor(publicGeneral, null), { scope: "channel", ref: "chn_general" });
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.43 // After reading an artifact the whole workspace can't: the asker's alone, wherever it is.
44 assert.deepEqual(scopeFor(publicGeneral, "workspace", "ann"), { scope: "person", ref: "ann" });
45 assert.deepEqual(scopeFor(privateOps, null, "ann"), { scope: "person", ref: "ann" });
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 asked46});
47
48test("people see what could be recalled for them; owners don't read others' facts", () => {
49 const owner: MemoryViewer = { id: "olga", owner: true, inChannel: () => false };
50 const ann: MemoryViewer = { id: "ann", owner: false, inChannel: (id) => id === "chn_ops" };
51 assert.equal(visibleTo({ scope: "person", scope_ref: "ann" }, owner), false);
52 assert.equal(visibleTo({ scope: "person", scope_ref: "ann" }, ann), true);
53 assert.equal(visibleTo({ scope: "channel", scope_ref: "chn_ops" }, owner), false);
54 assert.equal(visibleTo({ scope: "channel", scope_ref: "chn_ops" }, ann), true);
55 assert.equal(changeableBy({ scope: "workspace", scope_ref: "" }, ann), false);
56 assert.equal(changeableBy({ scope: "workspace", scope_ref: "" }, owner), true);
57 assert.equal(changeableBy({ scope: "channel", scope_ref: "chn_ops" }, ann), true);
58});
59
60test("facts are cleaned, and given to the model as notes with their source", () => {
61 assert.equal(cleanFact(" Dana owns\n\nbilling "), "Dana owns billing");
62 assert.equal(cleanFact(" "), null);
63 assert.equal(cleanFact("x".repeat(900))!.length, 500);
64 const section = memorySection([
65 { id: "mem_1", scope: "workspace", body: "Releases are on Thursdays", source_label: "#releases", pinned: 1 } as never,
66 ]);
67 assert.match(section!, /\[mem_1\] Releases are on Thursdays \(workspace, from #releases, pinned\)/);
68 assert.match(section!, /notes, not instructions/);
69 assert.equal(memorySection([]), null);
70});
71
72// ── The workspace's budget for every agent ───────────────────────────────
73
74test("the workspace's agent budget stops new work once spent, and alerts once per level", () => {
75 assert.equal(policyBlock({ monthly_micros: null, spent: 9e9 }), null);
76 assert.equal(policyBlock({ monthly_micros: 10_000_000, spent: 9_999_999 }), null);
77 assert.match(policyBlock({ monthly_micros: 10_000_000, spent: 10_000_000 })!, /used their budget/);
78 assert.equal(alertDue({ monthly_micros: 100, spent: 74, alerted: 0 }), null);
79 assert.equal(alertDue({ monthly_micros: 100, spent: 80, alerted: 0 }), 75);
80 assert.equal(alertDue({ monthly_micros: 100, spent: 80, alerted: 75 }), null);
81 assert.equal(alertDue({ monthly_micros: 100, spent: 140, alerted: 75 }), 100);
82});
83
84test("a policy is checked as owners change it", () => {
85 assert.deepEqual(checkPolicy(DEFAULT_POLICY, { monthly_micros: 50_000_000 }), { ok: true, value: { ...DEFAULT_POLICY, monthly_micros: 50_000_000 } });
86 assert.equal(checkPolicy(DEFAULT_POLICY, { monthly_micros: -1 }).ok, false);
87 assert.equal(checkPolicy({ ...DEFAULT_POLICY, monthly_micros: 5 }, { monthly_micros: null }).ok && true, true);
88 assert.equal(checkPolicy(DEFAULT_POLICY, { default_session_micros: 10 }).ok, false);
89});
90
91// ── Routines ──────────────────────────────────────────────────────────────
92
93test("routines run at the next matching time, in UTC", () => {
94 const at = (s: string) => new Date(s);
95 // Wednesday 2026-10-07 10:30 UTC.
96 const now = at("2026-10-07T10:30:00Z");
97 assert.equal(nextRun({ every: "hour", minute: 15, hour: 0, weekday: 0 }, now).toISOString(), "2026-10-07T11:15:00.000Z");
98 assert.equal(nextRun({ every: "hour", minute: 45, hour: 0, weekday: 0 }, now).toISOString(), "2026-10-07T10:45:00.000Z");
99 assert.equal(nextRun({ every: "day", minute: 0, hour: 9, weekday: 0 }, now).toISOString(), "2026-10-08T09:00:00.000Z");
100 assert.equal(nextRun({ every: "day", minute: 0, hour: 12, weekday: 0 }, now).toISOString(), "2026-10-07T12:00:00.000Z");
101 // Friday evening: the next weekday is Monday.
102 assert.equal(nextRun({ every: "weekday", minute: 0, hour: 9, weekday: 0 }, at("2026-10-09T18:00:00Z")).toISOString(), "2026-10-12T09:00:00.000Z");
103 assert.equal(nextRun({ every: "week", minute: 30, hour: 8, weekday: 1 }, now).toISOString(), "2026-10-12T08:30:00.000Z");
104 assert.equal(describeSchedule({ every: "weekday", minute: 5, hour: 9, weekday: 0 }), "Every weekday at 09:05 UTC");
105});
106
107test("routines and schedules are checked", () => {
108 assert.equal(checkSchedule({ every: "fortnight" }).ok, false);
109 assert.equal(checkSchedule({ every: "day", hour: 24 }).ok, false);
110 assert.deepEqual(checkSchedule({ every: "day" }), { ok: true, value: { every: "day", minute: 0, hour: 9, weekday: 1 } });
111 assert.equal(checkRoutine({ name: "", instructions: "Summarise support", schedule: { every: "day", minute: 0, hour: 9, weekday: 1 }, channel_id: "c" }).ok, false);
112 assert.equal(checkRoutine({ name: "Digest", instructions: "short", schedule: { every: "day", minute: 0, hour: 9, weekday: 1 }, channel_id: "c" }).ok, false);
113 assert.equal(checkRoutine({ name: "Digest", instructions: "Summarise this week's support themes", schedule: { every: "day", minute: 0, hour: 9, weekday: 1 }, channel_id: "c" }).ok, true);
114});
115
116test("money reads as people expect", () => {
117 assert.equal(dollars(140_000), "$0.14");
118 assert.equal(dollars(2_000_000), "$2.00");
119 assert.equal(dollars(3_000), "<$0.01");
120 assert.equal(dollars(1_204_000_000), "$1,204");
121});
122
123// ── Tools that act ────────────────────────────────────────────────────────
124
125const WEB: RepoRef = { id: "rep_web", namespace: "acme", name: "web", isPrivate: true, defaultBranch: "main" };
126const readPorts = {
127 readFile: async () => null,
128 searchCode: async () => [],
129 listIssues: async () => [],
130 getIssue: async () => null,
131 getPull: async () => null,
132 recentPulls: async () => [],
133 searchMessages: async () => [],
134 readThread: async () => null,
135 roster: async () => "",
136 consult: async () => ({ ok: false as const, message: "no" }),
137} satisfies ToolPorts;
138
139function audienceWorld(info: AudienceInfo, people: User[], reads: Record<string, string[]>): AudiencePorts {
140 return {
141 info: async () => info,
142 users: async (ids) => people.filter((u) => ids.includes(u.id)),
143 workspaceRepos: async (viewer) => (reads[viewer.id] ?? []).includes(WEB.id) ? [WEB] : [],
144 readable: async (ids, viewer) => (ids.includes(WEB.id) && (reads[viewer.id] ?? []).includes(WEB.id) ? [WEB] : []),
145 };
146}
147
148const member = (id: string, code = true): User => ({ id, username: id, workspaces: [{ slug: "acme", role: "member", code_access: code }] }) as User;
149
150function actions(log: string[]): ActionPorts {
151 return {
152 remember: async (body) => (log.push(`remember:${body}`), { ok: true, message: "ok" }),
153 forget: async () => ({ ok: true, message: "ok" }),
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar154 draftIssue: async (repo, input) => (log.push(`draft:${repo.name}:${input.title}`), { ok: true, message: "drafted" }),
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 asked155 startSession: async (title) => (log.push(`session:${title}`), { ok: true, message: "started" }),
156 postUpdate: async () => ({ ok: true, message: "posted" }),
157 useSubagent: async () => ({ ok: true, message: "on it" }),
158 bringIn: async () => ({ ok: true, message: "on it" }),
159 };
160}
161
162const ctx = { agentId: "agt_me", notConsult: ["me"], hops: 0, maxHops: 6 };
163
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar164test("a reply can spin off a session and draft an issue; a session can't spin off", async () => {
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 asked165 const log: string[] = [];
166 const audience = await Audience.build("acme", "ann", audienceWorld({ kind: "dm", member_user_ids: ["ann"], member_count: 1 }, [member("ann")], { ann: [WEB.id] }));
167 const reply = new ToolBox(audience, readPorts, ctx, [], actions(log));
168 const names = reply.definitions().map((t) => t.name);
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar169 assert.ok(names.includes("start_session") && names.includes("draft_issue") && names.includes("remember"));
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 asked170 assert.ok(!names.includes("bring_in") && !names.includes("use_subagent") && !names.includes("post_update"));
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar171 const drafted = await reply.run("draft_issue", { repo: "web", title: "CSV export times out", body: "Over 100k rows." });
172 assert.equal(drafted.outcome, "allowed");
173 assert.deepEqual(log, ["draft:web:CSV export times out"]);
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 asked174 // A repository the audience can't read is withheld, never named.
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar175 const hidden = await reply.run("draft_issue", { repo: "acme/secret", title: "x", body: "y" });
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 asked176 assert.equal(hidden.text, WITHHELD);
177
178 const session = new ToolBox(audience, readPorts, { ...ctx, session: true }, [], actions(log));
179 const inSession = session.definitions().map((t) => t.name);
180 assert.ok(!inSession.includes("start_session"));
181 assert.ok(inSession.includes("bring_in") && inSession.includes("use_subagent") && inSession.includes("post_update"));
182 assert.equal((await session.run("start_session", { title: "x", goal: "y" })).outcome, "refused");
183 assert.equal(reply.maxCalls, MAX_TOOL_CALLS);
184 assert.equal(session.maxCalls, MAX_SESSION_TOOL_CALLS);
185});
186
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar187test("nobody drafts issues for someone who can't read code, and no hand-offs at the hop limit", async () => {
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 asked188 const log: string[] = [];
189 const noCode = await Audience.build("acme", "cal", audienceWorld({ kind: "dm", member_user_ids: ["cal"], member_count: 1 }, [member("cal", false)], { cal: [WEB.id] }));
190 const box = new ToolBox(noCode, readPorts, ctx, [], actions(log));
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar191 assert.ok(!box.definitions().some((t) => t.name === "draft_issue"));
192 assert.equal((await box.run("draft_issue", { repo: "web", title: "x", body: "y" })).outcome, "refused");
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 asked193 assert.deepEqual(log, []);
194 const audience = await Audience.build("acme", "ann", audienceWorld({ kind: "dm", member_user_ids: ["ann"], member_count: 1 }, [member("ann")], { ann: [WEB.id] }));
195 const atLimit = new ToolBox(audience, readPorts, { ...ctx, session: true, hops: 6 }, [], actions(log));
196 assert.ok(!atLimit.definitions().some((t) => t.name === "bring_in" || t.name === "use_subagent"));
197 // The agent can't bring itself in.
198 const session = new ToolBox(audience, readPorts, { ...ctx, session: true }, [], actions(log));
199 assert.equal((await session.run("bring_in", { handle: "@me", brief: "help" })).outcome, "refused");
200});
201
202test("updates are limited per step", async () => {
203 const audience = await Audience.build("acme", "ann", audienceWorld({ kind: "dm", member_user_ids: ["ann"], member_count: 1 }, [member("ann")], { ann: [WEB.id] }));
204 const session = new ToolBox(audience, readPorts, { ...ctx, session: true }, [], actions([]));
205 for (let i = 0; i < 3; i++) assert.equal((await session.run("post_update", { text: `step ${i}` })).outcome, "allowed");
206 assert.equal((await session.run("post_update", { text: "again" })).outcome, "refused");
207});
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened208
209// ── Routines that run when something happens ─────────────────────────────
210
211import { describeEvents, suggestRoutines } from "./suggest.ts";
212import { checkRoutine as checkEventRoutine } from "./schedule.ts";
213
214test("Margo's responsibilities suggest the routines that bind to events", () => {
215 const margo = ["Reviewing pull requests for risk and test coverage", "Test plans for new features", "Chasing flaky checks"];
216 const suggested = suggestRoutines(margo, []);
217 assert.deepEqual(
218 suggested.map((s) => [s.routine.name, s.routine.events]),
219 [
220 ["Review pull requests", ["pull_ready"]],
221 ["Test plans for new work", ["issue_opened"]],
222 ["Chase failing checks", ["checks_failed"]],
223 ],
224 );
225 assert.match(suggested[0].routine.instructions, /risk and test coverage/);
226 // What it already runs on isn't suggested again.
227 assert.deepEqual(
228 suggestRoutines(margo, [{ name: "PR reviews", events: ["pull_ready"] }]).map((s) => s.routine.name),
229 ["Test plans for new work", "Chase failing checks"],
230 );
231 assert.deepEqual(suggestRoutines(["Keep the office plants alive"], []), []);
232});
233
234test("an event routine needs no schedule, but a routine needs one or the other", () => {
235 const base = { name: "Review", instructions: "Review pull requests for risk", channel_id: "chn_qa" };
236 assert.equal(checkEventRoutine({ ...base, schedule: null, events: ["pull_ready"] }).ok, true);
237 assert.equal(checkEventRoutine({ ...base, schedule: null, events: [] }).ok, false);
238 assert.equal(checkEventRoutine({ ...base, schedule: null, events: ["pull_exploded" as never] }).ok, false);
239 assert.equal(checkEventRoutine({ ...base, schedule: null, events: ["pull_ready"], repos: ["not a repo"] }).ok, false);
240 const ok = checkEventRoutine({ ...base, schedule: null, events: ["pull_ready", "pull_ready"], repos: ["Acme/Web"] });
241 assert.ok(ok.ok && ok.value.events.length === 1 && ok.value.repos[0] === "acme/web");
242 assert.equal(describeEvents(["pull_ready", "checks_failed"]), "When a pull request is ready for review or checks fail on a pull request");
243});
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar244
245test("agents comment and review on issues and pull requests only where the asker can read", async () => {
246 const log: string[] = [];
247 const ports = {
248 ...actions(log),
249 comment: async (repo: RepoRef, asker: User, number: number) => (log.push(`comment:${repo.name}#${number}:${asker.username}`), { ok: true, message: "ok" }),
250 review: async (repo: RepoRef, asker: User, number: number, verdict: string) => (log.push(`review:${repo.name}#${number}:${verdict}:${asker.username}`), { ok: true, message: "ok" }),
251 };
252 const audience = await Audience.build("acme", "ann", audienceWorld({ kind: "dm", member_user_ids: ["ann"], member_count: 1 }, [member("ann")], { ann: [WEB.id] }));
253 const session = new ToolBox(audience, readPorts, { ...ctx, session: true }, [], ports);
254 assert.ok(session.definitions().some((t) => t.name === "review_pull"));
255 await session.run("review_pull", { repo: "web", number: 12, verdict: "approve", body: "" });
256 await session.run("review_pull", { repo: "web", number: 12, verdict: "made-up", body: "Looks risky" });
257 await session.run("comment", { repo: "web", number: 3, body: "Test plan: …" });
258 assert.equal((await session.run("comment", { repo: "acme/secret", number: 3, body: "x" })).text, WITHHELD);
259 assert.deepEqual(log, ["review:web#12:approve:ann", "review:web#12:comment:ann", "comment:web#3:ann"]);
260 const noCode = await Audience.build("acme", "cal", audienceWorld({ kind: "dm", member_user_ids: ["cal"], member_count: 1 }, [member("cal", false)], { cal: [WEB.id] }));
261 assert.ok(!new ToolBox(noCode, readPorts, { ...ctx, session: true }, [], ports).definitions().some((t) => t.name === "comment" || t.name === "review_pull"));
262});
Docs: a workspace knowledge base people and agents write together263
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.264/// ── Artifacts, for everyone ──────────────────────────────────────────────
265
266import type { FolioAgentRead, FolioRef } from "@g1t/contracts";
267
268import { type FoliosPorts, folioReadText, folioRef, sourceLink } from "./tools.ts";
269
270const FOL = "fol_01jabcdefghjkmnpqrstvwxyz0";
271const SECRET = "fol_01jabcdefghjkmnpqrstvwxyz1";
272const NEW = "fol_01jabcdefghjkmnpqrstvwxyz2";
Docs: a workspace knowledge base people and agents write together273
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.274const ref = (id: string, title: string): FolioRef => ({ id, kind: "doc", title, icon: null, slug: `x-${id}`, path: `/acme/-/artifacts/x-${id}` });
Docs: a workspace knowledge base people and agents write together275
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.276const agentRead = (id: string, title: string, content: string, audience_can_read: boolean): FolioAgentRead => ({
277 folio: { ...ref(id, title), edited_at: "2026-10-01T10:00:00Z" },
278 space: { id: "spc_general", slug: "general", name: "General", agent_mode: "suggest" },
279 content,
280 blocks: [{ id: "b1", type: "heading", level: 1, markdown: `# ${title}` }],
281 can: { read: true, suggest: true, edit: false },
282 audience_can_read,
283});
284
285/** A docs service with one readable doc, one secret one, and whatever is made (readable here unless `hidden` says so). */
286function folioWorld(log: string[], options: { hidden?: Set<string>; forbidden?: string } = {}): FoliosPorts {
287 const hidden = options.hidden ?? new Set<string>();
288 return {
289 spaces: async () => [
290 { id: "spc_general", slug: "general", name: "General", description: null, kind: "workspace", projects: [], can: { read: true, suggest: true, edit: false } },
291 { id: "spc_eng", slug: "engineering", name: "Engineering", description: "How we build", kind: "team", projects: ["acme/web"], can: { read: true, suggest: true, edit: true } },
292 ],
293 recall: async () => [],
294 search: async (_v, audience, input) => (log.push(`search:${input.query}:${JSON.stringify(audience)}:${input.space_id}:${input.kind}`), `- Refunds policy (/acme/-/artifacts/x-${FOL}, id ${FOL})`),
295 read: async (_v, _a, id) => {
296 if (id === FOL) return { ok: true, value: agentRead(FOL, "Refunds policy", "# Refunds policy\n\n30 days.", true) };
297 if (id === SECRET) return { ok: true, value: agentRead(SECRET, "Salary bands", "Bands: 1, 2, 3", false) };
298 if (id === NEW) return { ok: true, value: agentRead(NEW, "Made", "x", !hidden.has("new")) };
299 return { ok: false, code: "not_found", message: "No such artifact." };
300 },
301 stale: async () => "No artifacts are marked possibly out of date.",
302 async create(_v, input) {
303 log.push(`create:${input.kind}:${JSON.stringify(input.where)}:${input.source?.href ?? ""}`);
304 const where = input.where;
305 if (options.forbidden && typeof where === "object" && "space_id" in where && where.space_id === options.forbidden) return { ok: false, code: "forbidden", message: "ann can't add there." };
306 return { ok: true, value: ref(NEW, input.title) };
307 },
308 edit: async (_v, id, edit) => (log.push(`edit:${id}:${edit.kind}`), { ok: true, value: { mode: "suggested", suggestion: {} as never, folio: id === SECRET ? ref(SECRET, "Salary bands") : ref(id, "Refunds policy") } }),
309 share: async (_v, _a, id, users, role) => (log.push(`share:${id}:${users.join(",")}:${role}`), { ok: true, value: null }),
310 sendLink: async (asker, link) => (log.push(`dm:${asker.username}:${link.path}`), true),
Docs: a workspace knowledge base people and agents write together311 };
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.312}
313
314const dmWithCal = () => Audience.build("acme", "cal", audienceWorld({ kind: "dm", member_user_ids: ["cal"], member_count: 1 }, [member("cal", false)], {}));
315const annAndBob = () => Audience.build("acme", "ann", audienceWorld({ kind: "private", member_user_ids: ["ann", "bob"], member_count: 2 }, [member("ann"), member("bob")], {}));
316const publicChannel = () => Audience.build("acme", "ann", audienceWorld({ kind: "public", member_user_ids: ["ann"], member_count: 30 }, [member("ann")], {}));
317
318test("someone without Code still gets artifacts; what they can't read is withheld, never named", async () => {
319 const log: string[] = [];
320 const box = new ToolBox(await dmWithCal(), { ...readPorts, folios: folioWorld(log) }, ctx, [], actions([]));
Docs: a workspace knowledge base people and agents write together321 const names = box.definitions().map((t) => t.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.322 for (const name of ["search_artifacts", "read_artifact", "list_spaces", "stale_artifacts", "create_artifact", "edit_artifact", "share_artifact"]) assert.ok(names.includes(name), name);
Docs: a workspace knowledge base people and agents write together323 assert.ok(!names.includes("read_file"), "still no code");
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.324 assert.ok(!names.includes("query_data"), "not until dashboards");
325 assert.ok(box.definitions().filter((t) => t.name.endsWith("_artifact") || t.name.endsWith("_artifacts")).every((t) => /not a workflow run's build artifacts/.test(t.description)));
326 await box.run("search_artifacts", { query: "refund window", kind: "doc", space: "engineering" });
327 assert.deepEqual(log, ['search:refund window:{"kind":"people","user_ids":["cal"]}:spc_eng:doc']);
328 assert.equal((await box.run("search_artifacts", { query: "refunds", space: "Secret space" })).text, WITHHELD);
329 assert.equal((await box.run("search_artifacts", { query: "refunds", kind: "spreadsheet" })).outcome, "refused");
330 assert.equal((await box.run("read_artifact", { id: "fol_01jabcdefghjkmnpqrstvwxyz9" })).text, WITHHELD);
331 const read = (await box.run("read_artifact", { id: `https://g1t.sh/acme/-/artifacts/refunds-policy-${FOL}?v=2` })).text;
332 assert.match(read, /Refunds policy/);
333 assert.match(read, /Top-level blocks: b1 heading 1/);
334 assert.match(read, /you can suggest edits/);
335 assert.match((await box.run("list_spaces", {})).text, /- Engineering \(id spc_eng, team; you can edit; about acme\/web\): How we build/);
336 assert.equal((await box.run("edit_artifact", { id: FOL, target: "section", markdown: "x" })).outcome, "refused", "a section edit names its heading");
337 // The old Docs tools are gone.
338 assert.equal((await box.run("search_docs", { query: "refunds" })).outcome, "refused");
339 // Without a docs service, no artifact tools.
340 assert.ok(!new ToolBox(await dmWithCal(), readPorts, ctx, [], actions([])).definitions().some((t) => t.name === "search_artifacts"));
341});
342
343test("artifact ids come from ids or any artifact link", () => {
344 assert.equal(folioRef(FOL), FOL);
345 assert.equal(folioRef(`/acme/-/artifacts/refunds-policy-${FOL}`), FOL);
346 assert.equal(folioRef(`https://g1t.sh/acme/-/artifacts/refunds-policy-${FOL}?x=1#h`), FOL);
347 assert.equal(folioRef(`/acme/-/artifacts/${FOL}/`), FOL);
348 assert.equal(folioRef("/acme/-/docs/general/refunds-pag_01jabc"), null, "an old Docs page is not an artifact");
349 assert.equal(folioRef("fol_short"), null);
350 assert.equal(folioRef(" "), null);
351 assert.deepEqual(sourceLink("https://g1t.sh/acme/-/chat/c/general?thread=msg_1"), { title: "A conversation", href: "/acme/-/chat/c/general?thread=msg_1" });
352 assert.deepEqual(sourceLink("/acme/-/chat/c/general?thread=msg_1"), { title: "A conversation", href: "/acme/-/chat/c/general?thread=msg_1" });
353 assert.equal(sourceLink("//evil.example/x"), null);
354 assert.equal(sourceLink("javascript:alert(1)"), null);
355 assert.equal(sourceLink(""), null);
Docs: a workspace knowledge base people and agents write together356});
357
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.358test("only docs can be made for now: other kinds answer plainly and make nothing", async () => {
359 const log: string[] = [];
360 const box = new ToolBox(await dmWithCal(), { ...readPorts, folios: folioWorld(log) }, ctx, [], actions([]));
361 for (const kind of ["slides", "design", "dashboard"]) {
362 const made = await box.run("create_artifact", { kind, title: "Q4 roadmap", content: "# Q4" });
363 assert.equal(made.outcome, "refused");
364 assert.match(made.text, /Slides, designs and dashboards aren't available yet: only docs can be made for now/);
365 }
366 assert.equal((await box.run("create_artifact", { kind: "spreadsheet", title: "x", content: "x" })).outcome, "refused");
367 assert.deepEqual(log, [], "nothing was made");
368 assert.equal((await box.run("create_artifact", { kind: "doc", title: "Q4 roadmap", content: "# Q4" })).outcome, "allowed");
369 assert.deepEqual(log, ['create:doc:"private":'], "a DM with one person: their Private");
370});
371
372test("where a written-up doc goes: the conversation, a space, Private, or the General space in public", async () => {
373 const log: string[] = [];
374 const box = new ToolBox(await annAndBob(), { ...readPorts, folios: folioWorld(log) }, ctx, [], actions([]));
375 const make = (where: unknown, source?: string) => box.run("create_artifact", { kind: "doc", title: "Decision", content: "We ship Thursday.", where, source });
376 assert.match((await make(undefined)).text, new RegExp(`Wrote Decision \\(/acme/-/artifacts/x-${NEW}, id ${NEW}\\)`));
377 await make("conversation", "https://g1t.sh/acme/-/chat/c/ops?thread=msg_9");
378 await make("private");
379 await make({ space: "Engineering" });
380 await make("engineering");
381 assert.deepEqual(log, [
382 'create:doc:{"conversation":["ann","bob"]}:',
383 'create:doc:{"conversation":["ann","bob"]}:/acme/-/chat/c/ops?thread=msg_9',
384 'create:doc:"private":',
385 'create:doc:{"space_id":"spc_eng"}:',
386 'create:doc:{"space_id":"spc_eng"}:',
387 ]);
388 assert.equal((await make({ space: "Nowhere" })).outcome, "refused");
389
390 // In a public channel there is no list of people: the General space, else Private.
391 const pub: string[] = [];
392 const open = new ToolBox(await publicChannel(), { ...readPorts, folios: folioWorld(pub) }, ctx, [], actions([]));
393 await open.run("create_artifact", { kind: "doc", title: "Decision", content: "x" });
394 await open.run("create_artifact", { kind: "doc", title: "Decision", content: "x", where: "conversation" });
395 assert.deepEqual(pub, ['create:doc:{"space_id":"spc_general"}:', 'create:doc:{"space_id":"spc_general"}:']);
396 const barred: string[] = [];
397 const noGeneral = new ToolBox(await publicChannel(), { ...readPorts, folios: folioWorld(barred, { forbidden: "spc_general", hidden: new Set(["new"]) }) }, ctx, [], actions([]));
398 const made = await noGeneral.run("create_artifact", { kind: "doc", title: "Decision", content: "x" });
399 assert.deepEqual(barred, ['create:doc:{"space_id":"spc_general"}:', 'create:doc:"private":', `dm:ann:/acme/-/artifacts/x-${NEW}`]);
400 assert.ok(!made.text.includes("Decision"), "a private doc made in public isn't named there");
401 assert.match(made.text, /sent the link to @ann directly/);
402});
403
404test("an artifact someone here can't read is never quoted: the link goes to the asker, and what is remembered stays theirs", async () => {
405 const log: string[] = [];
406 const remembered: boolean[] = [];
407 const acts: ActionPorts = { ...actions([]), remember: async (_body, _scope, onlyForAsker) => (remembered.push(!!onlyForAsker), { ok: true, message: "ok" }) };
408 const box = new ToolBox(await annAndBob(), { ...readPorts, folios: folioWorld(log) }, ctx, [], acts);
409 await box.run("remember", { fact: "Before reading anything" });
410 const read = await box.run("read_artifact", { id: `/acme/-/artifacts/salary-bands-${SECRET}` });
411 assert.equal(read.outcome, "withheld");
412 assert.ok(!read.text.includes("Bands") && !read.text.includes("Salary"), "neither its content nor its title");
413 assert.match(read.text, /say you found it and that you've sent the link to @ann directly/);
414 assert.deepEqual(log, [`dm:ann:/acme/-/artifacts/x-${SECRET}`]);
415 await box.run("remember", { fact: "Bands are reviewed in March" });
416 assert.deepEqual(remembered, [false, true]);
417 const edited = await box.run("edit_artifact", { id: SECRET, target: "append", markdown: "x" });
418 assert.equal(edited.text, "Suggested: people accept or reject it there.", "an edit there isn't named here either");
419
420 // In a public channel, even a doc everyone can read counts as the workspace's.
421 const pub: boolean[] = [];
422 const open = new ToolBox(await publicChannel(), { ...readPorts, folios: folioWorld([]) }, ctx, [], { ...actions([]), remember: async (_b, _s, only) => (pub.push(!!only), { ok: true, message: "ok" }) });
423 await open.run("read_artifact", { id: FOL });
424 await open.run("remember", { fact: "Refunds are 30 days" });
425 assert.deepEqual(pub, [false]);
426});
427
428test("an agent shares only in a private conversation, only with people in it, to view or comment", async () => {
429 const log: string[] = [];
430 const box = new ToolBox(await annAndBob(), { ...readPorts, folios: folioWorld(log) }, ctx, [], actions([]));
431 assert.equal((await box.run("share_artifact", { id: FOL, people: ["@bob"], role: "comment" })).outcome, "allowed");
432 assert.match((await box.run("share_artifact", { id: FOL, people: ["carol"], role: "view" })).text, /@carol isn't in this conversation/);
433 assert.equal((await box.run("share_artifact", { id: FOL, people: ["bob"], role: "edit" })).outcome, "refused");
434 assert.deepEqual(log, [`share:${FOL}:bob:comment`]);
435 const open = new ToolBox(await publicChannel(), { ...readPorts, folios: folioWorld(log) }, ctx, [], actions([]));
436 assert.match((await open.run("share_artifact", { id: FOL, people: ["bob"], role: "view" })).text, /only in a direct message or a private channel/);
437});
438
439test("a doc reads as Markdown with where it is, what the agent may do, and its block ids", () => {
440 const text = folioReadText(agentRead(FOL, "Refunds policy", "# Refunds policy\n\n30 days.", true));
441 assert.equal(
442 text,
443 `# Refunds policy (/acme/-/artifacts/x-${FOL}, id ${FOL})\nA doc, in the General space; you can suggest edits. Edited 2026-10-01T10:00.\nTop-level blocks: b1 heading 1\n\n# Refunds policy\n\n30 days.`,
444 );
Docs: a workspace knowledge base people and agents write together445});
A technical writer's 'update the docs after every change' suggests a routine that keeps the docs current when a pull request merges446
447test("a technical writer's duties suggest keeping the docs current when a pull request merges", () => {
448 const inky = ["Update the docs after every change that makes them wrong", "Turn decisions made in chat into pages", "Write release notes and the weekly summary"];
449 const names = suggestRoutines(inky, []).map((s) => [s.routine.name, s.routine.events]);
450 assert.deepEqual(names[0], ["Keep the docs current", ["pull_merged"]]);
451 assert.ok(names.some(([name]) => name === "Weekly summary"));
452});