Skip to content
477 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
Merge branch 'worktree-agent-a1398e81ad1a64c5f'187test("hand_off: offered from chat, never in a session or at the hop limit; not to itself or its sender; two per reply", async () => {
188 const log: string[] = [];
189 const handing: ActionPorts = { ...actions(log), handOff: async (handle, brief) => (log.push(`hand_off:${handle}:${brief}`), { ok: true, message: "handed" }) };
190 const audience = await Audience.build("acme", "ann", audienceWorld({ kind: "dm", member_user_ids: ["ann"], member_count: 1 }, [member("ann")], { ann: [WEB.id] }));
191 const reply = new ToolBox(audience, readPorts, { ...ctx, notConsult: ["me", "g1t"] }, [], handing);
192 const names = reply.definitions().map((t) => t.name);
193 assert.ok(names.includes("hand_off") && names.includes("ask_colleague"));
194 const descriptions = Object.fromEntries(reply.definitions().map((t) => [t.name, t.description]));
195 assert.match(descriptions.hand_off, /only way to get a colleague working: an @mention in your message wakes nobody/);
196 assert.match(descriptions.ask_colleague, /quick question, privately/);
197 assert.match(descriptions.ask_colleague, /To give them the work itself, use hand_off/);
198 assert.equal((await reply.run("hand_off", { handle: "@me", brief: "x" })).outcome, "refused", "not to itself");
199 assert.equal((await reply.run("hand_off", { handle: "g1t", brief: "x" })).outcome, "refused", "not back to the one that sent it");
200 assert.equal((await reply.run("hand_off", { handle: "mike", brief: "" })).outcome, "refused", "a brief is needed");
201 assert.equal((await reply.run("hand_off", { handle: "@Mike", brief: "Draft the role brief." })).outcome, "allowed");
202 assert.equal((await reply.run("hand_off", { handle: "dot", brief: "Plan it." })).outcome, "allowed");
203 assert.equal((await reply.run("hand_off", { handle: "sam", brief: "Tell them." })).outcome, "refused", "two per reply");
204 assert.deepEqual(log, ["hand_off:mike:Draft the role brief.", "hand_off:dot:Plan it."]);
205 // Not in a session (bring_in is), and not at the hop limit.
206 assert.ok(!new ToolBox(audience, readPorts, { ...ctx, session: true }, [], handing).definitions().some((t) => t.name === "hand_off"));
207 assert.ok(!new ToolBox(audience, readPorts, { ...ctx, hops: 6 }, [], handing).definitions().some((t) => t.name === "hand_off"));
208 // Without the port (an old chat), it isn't offered.
209 assert.ok(!new ToolBox(audience, readPorts, ctx, [], actions(log)).definitions().some((t) => t.name === "hand_off"));
210});
211
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar212test("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 asked213 const log: string[] = [];
214 const noCode = await Audience.build("acme", "cal", audienceWorld({ kind: "dm", member_user_ids: ["cal"], member_count: 1 }, [member("cal", false)], { cal: [WEB.id] }));
215 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 calendar216 assert.ok(!box.definitions().some((t) => t.name === "draft_issue"));
217 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 asked218 assert.deepEqual(log, []);
219 const audience = await Audience.build("acme", "ann", audienceWorld({ kind: "dm", member_user_ids: ["ann"], member_count: 1 }, [member("ann")], { ann: [WEB.id] }));
220 const atLimit = new ToolBox(audience, readPorts, { ...ctx, session: true, hops: 6 }, [], actions(log));
221 assert.ok(!atLimit.definitions().some((t) => t.name === "bring_in" || t.name === "use_subagent"));
222 // The agent can't bring itself in.
223 const session = new ToolBox(audience, readPorts, { ...ctx, session: true }, [], actions(log));
224 assert.equal((await session.run("bring_in", { handle: "@me", brief: "help" })).outcome, "refused");
225});
226
227test("updates are limited per step", async () => {
228 const audience = await Audience.build("acme", "ann", audienceWorld({ kind: "dm", member_user_ids: ["ann"], member_count: 1 }, [member("ann")], { ann: [WEB.id] }));
229 const session = new ToolBox(audience, readPorts, { ...ctx, session: true }, [], actions([]));
230 for (let i = 0; i < 3; i++) assert.equal((await session.run("post_update", { text: `step ${i}` })).outcome, "allowed");
231 assert.equal((await session.run("post_update", { text: "again" })).outcome, "refused");
232});
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened233
234// ── Routines that run when something happens ─────────────────────────────
235
236import { describeEvents, suggestRoutines } from "./suggest.ts";
237import { checkRoutine as checkEventRoutine } from "./schedule.ts";
238
239test("Margo's responsibilities suggest the routines that bind to events", () => {
240 const margo = ["Reviewing pull requests for risk and test coverage", "Test plans for new features", "Chasing flaky checks"];
241 const suggested = suggestRoutines(margo, []);
242 assert.deepEqual(
243 suggested.map((s) => [s.routine.name, s.routine.events]),
244 [
245 ["Review pull requests", ["pull_ready"]],
246 ["Test plans for new work", ["issue_opened"]],
247 ["Chase failing checks", ["checks_failed"]],
248 ],
249 );
250 assert.match(suggested[0].routine.instructions, /risk and test coverage/);
251 // What it already runs on isn't suggested again.
252 assert.deepEqual(
253 suggestRoutines(margo, [{ name: "PR reviews", events: ["pull_ready"] }]).map((s) => s.routine.name),
254 ["Test plans for new work", "Chase failing checks"],
255 );
256 assert.deepEqual(suggestRoutines(["Keep the office plants alive"], []), []);
257});
258
259test("an event routine needs no schedule, but a routine needs one or the other", () => {
260 const base = { name: "Review", instructions: "Review pull requests for risk", channel_id: "chn_qa" };
261 assert.equal(checkEventRoutine({ ...base, schedule: null, events: ["pull_ready"] }).ok, true);
262 assert.equal(checkEventRoutine({ ...base, schedule: null, events: [] }).ok, false);
263 assert.equal(checkEventRoutine({ ...base, schedule: null, events: ["pull_exploded" as never] }).ok, false);
264 assert.equal(checkEventRoutine({ ...base, schedule: null, events: ["pull_ready"], repos: ["not a repo"] }).ok, false);
265 const ok = checkEventRoutine({ ...base, schedule: null, events: ["pull_ready", "pull_ready"], repos: ["Acme/Web"] });
266 assert.ok(ok.ok && ok.value.events.length === 1 && ok.value.repos[0] === "acme/web");
267 assert.equal(describeEvents(["pull_ready", "checks_failed"]), "When a pull request is ready for review or checks fail on a pull request");
268});
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar269
270test("agents comment and review on issues and pull requests only where the asker can read", async () => {
271 const log: string[] = [];
272 const ports = {
273 ...actions(log),
274 comment: async (repo: RepoRef, asker: User, number: number) => (log.push(`comment:${repo.name}#${number}:${asker.username}`), { ok: true, message: "ok" }),
275 review: async (repo: RepoRef, asker: User, number: number, verdict: string) => (log.push(`review:${repo.name}#${number}:${verdict}:${asker.username}`), { ok: true, message: "ok" }),
276 };
277 const audience = await Audience.build("acme", "ann", audienceWorld({ kind: "dm", member_user_ids: ["ann"], member_count: 1 }, [member("ann")], { ann: [WEB.id] }));
278 const session = new ToolBox(audience, readPorts, { ...ctx, session: true }, [], ports);
279 assert.ok(session.definitions().some((t) => t.name === "review_pull"));
280 await session.run("review_pull", { repo: "web", number: 12, verdict: "approve", body: "" });
281 await session.run("review_pull", { repo: "web", number: 12, verdict: "made-up", body: "Looks risky" });
282 await session.run("comment", { repo: "web", number: 3, body: "Test plan: …" });
283 assert.equal((await session.run("comment", { repo: "acme/secret", number: 3, body: "x" })).text, WITHHELD);
284 assert.deepEqual(log, ["review:web#12:approve:ann", "review:web#12:comment:ann", "comment:web#3:ann"]);
285 const noCode = await Audience.build("acme", "cal", audienceWorld({ kind: "dm", member_user_ids: ["cal"], member_count: 1 }, [member("cal", false)], { cal: [WEB.id] }));
286 assert.ok(!new ToolBox(noCode, readPorts, { ...ctx, session: true }, [], ports).definitions().some((t) => t.name === "comment" || t.name === "review_pull"));
287});
Docs: a workspace knowledge base people and agents write together288
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.289/// ── Artifacts, for everyone ──────────────────────────────────────────────
290
291import type { FolioAgentRead, FolioRef } from "@g1t/contracts";
292
293import { type FoliosPorts, folioReadText, folioRef, sourceLink } from "./tools.ts";
294
295const FOL = "fol_01jabcdefghjkmnpqrstvwxyz0";
296const SECRET = "fol_01jabcdefghjkmnpqrstvwxyz1";
297const NEW = "fol_01jabcdefghjkmnpqrstvwxyz2";
Docs: a workspace knowledge base people and agents write together298
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.299const 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 together300
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.301const agentRead = (id: string, title: string, content: string, audience_can_read: boolean): FolioAgentRead => ({
302 folio: { ...ref(id, title), edited_at: "2026-10-01T10:00:00Z" },
303 space: { id: "spc_general", slug: "general", name: "General", agent_mode: "suggest" },
304 content,
305 blocks: [{ id: "b1", type: "heading", level: 1, markdown: `# ${title}` }],
306 can: { read: true, suggest: true, edit: false },
307 audience_can_read,
308});
309
310/** A docs service with one readable doc, one secret one, and whatever is made (readable here unless `hidden` says so). */
311function folioWorld(log: string[], options: { hidden?: Set<string>; forbidden?: string } = {}): FoliosPorts {
312 const hidden = options.hidden ?? new Set<string>();
313 return {
314 spaces: async () => [
315 { id: "spc_general", slug: "general", name: "General", description: null, kind: "workspace", projects: [], can: { read: true, suggest: true, edit: false } },
316 { id: "spc_eng", slug: "engineering", name: "Engineering", description: "How we build", kind: "team", projects: ["acme/web"], can: { read: true, suggest: true, edit: true } },
317 ],
318 recall: async () => [],
319 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})`),
320 read: async (_v, _a, id) => {
321 if (id === FOL) return { ok: true, value: agentRead(FOL, "Refunds policy", "# Refunds policy\n\n30 days.", true) };
322 if (id === SECRET) return { ok: true, value: agentRead(SECRET, "Salary bands", "Bands: 1, 2, 3", false) };
323 if (id === NEW) return { ok: true, value: agentRead(NEW, "Made", "x", !hidden.has("new")) };
324 return { ok: false, code: "not_found", message: "No such artifact." };
325 },
326 stale: async () => "No artifacts are marked possibly out of date.",
327 async create(_v, input) {
328 log.push(`create:${input.kind}:${JSON.stringify(input.where)}:${input.source?.href ?? ""}`);
329 const where = input.where;
330 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." };
331 return { ok: true, value: ref(NEW, input.title) };
332 },
333 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") } }),
334 share: async (_v, _a, id, users, role) => (log.push(`share:${id}:${users.join(",")}:${role}`), { ok: true, value: null }),
335 sendLink: async (asker, link) => (log.push(`dm:${asker.username}:${link.path}`), true),
Docs: a workspace knowledge base people and agents write together336 };
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.337}
338
339const dmWithCal = () => Audience.build("acme", "cal", audienceWorld({ kind: "dm", member_user_ids: ["cal"], member_count: 1 }, [member("cal", false)], {}));
340const annAndBob = () => Audience.build("acme", "ann", audienceWorld({ kind: "private", member_user_ids: ["ann", "bob"], member_count: 2 }, [member("ann"), member("bob")], {}));
341const publicChannel = () => Audience.build("acme", "ann", audienceWorld({ kind: "public", member_user_ids: ["ann"], member_count: 30 }, [member("ann")], {}));
342
343test("someone without Code still gets artifacts; what they can't read is withheld, never named", async () => {
344 const log: string[] = [];
345 const box = new ToolBox(await dmWithCal(), { ...readPorts, folios: folioWorld(log) }, ctx, [], actions([]));
Docs: a workspace knowledge base people and agents write together346 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.347 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 together348 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.349 assert.ok(!names.includes("query_data"), "not until dashboards");
350 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)));
351 await box.run("search_artifacts", { query: "refund window", kind: "doc", space: "engineering" });
352 assert.deepEqual(log, ['search:refund window:{"kind":"people","user_ids":["cal"]}:spc_eng:doc']);
353 assert.equal((await box.run("search_artifacts", { query: "refunds", space: "Secret space" })).text, WITHHELD);
354 assert.equal((await box.run("search_artifacts", { query: "refunds", kind: "spreadsheet" })).outcome, "refused");
355 assert.equal((await box.run("read_artifact", { id: "fol_01jabcdefghjkmnpqrstvwxyz9" })).text, WITHHELD);
356 const read = (await box.run("read_artifact", { id: `https://g1t.sh/acme/-/artifacts/refunds-policy-${FOL}?v=2` })).text;
357 assert.match(read, /Refunds policy/);
358 assert.match(read, /Top-level blocks: b1 heading 1/);
359 assert.match(read, /you can suggest edits/);
360 assert.match((await box.run("list_spaces", {})).text, /- Engineering \(id spc_eng, team; you can edit; about acme\/web\): How we build/);
361 assert.equal((await box.run("edit_artifact", { id: FOL, target: "section", markdown: "x" })).outcome, "refused", "a section edit names its heading");
362 // The old Docs tools are gone.
363 assert.equal((await box.run("search_docs", { query: "refunds" })).outcome, "refused");
364 // Without a docs service, no artifact tools.
365 assert.ok(!new ToolBox(await dmWithCal(), readPorts, ctx, [], actions([])).definitions().some((t) => t.name === "search_artifacts"));
366});
367
368test("artifact ids come from ids or any artifact link", () => {
369 assert.equal(folioRef(FOL), FOL);
370 assert.equal(folioRef(`/acme/-/artifacts/refunds-policy-${FOL}`), FOL);
371 assert.equal(folioRef(`https://g1t.sh/acme/-/artifacts/refunds-policy-${FOL}?x=1#h`), FOL);
372 assert.equal(folioRef(`/acme/-/artifacts/${FOL}/`), FOL);
373 assert.equal(folioRef("/acme/-/docs/general/refunds-pag_01jabc"), null, "an old Docs page is not an artifact");
374 assert.equal(folioRef("fol_short"), null);
375 assert.equal(folioRef(" "), null);
376 assert.deepEqual(sourceLink("https://g1t.sh/acme/-/chat/c/general?thread=msg_1"), { title: "A conversation", href: "/acme/-/chat/c/general?thread=msg_1" });
377 assert.deepEqual(sourceLink("/acme/-/chat/c/general?thread=msg_1"), { title: "A conversation", href: "/acme/-/chat/c/general?thread=msg_1" });
378 assert.equal(sourceLink("//evil.example/x"), null);
379 assert.equal(sourceLink("javascript:alert(1)"), null);
380 assert.equal(sourceLink(""), null);
Docs: a workspace knowledge base people and agents write together381});
382
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.383test("only docs can be made for now: other kinds answer plainly and make nothing", async () => {
384 const log: string[] = [];
385 const box = new ToolBox(await dmWithCal(), { ...readPorts, folios: folioWorld(log) }, ctx, [], actions([]));
386 for (const kind of ["slides", "design", "dashboard"]) {
387 const made = await box.run("create_artifact", { kind, title: "Q4 roadmap", content: "# Q4" });
388 assert.equal(made.outcome, "refused");
389 assert.match(made.text, /Slides, designs and dashboards aren't available yet: only docs can be made for now/);
390 }
391 assert.equal((await box.run("create_artifact", { kind: "spreadsheet", title: "x", content: "x" })).outcome, "refused");
392 assert.deepEqual(log, [], "nothing was made");
393 assert.equal((await box.run("create_artifact", { kind: "doc", title: "Q4 roadmap", content: "# Q4" })).outcome, "allowed");
394 assert.deepEqual(log, ['create:doc:"private":'], "a DM with one person: their Private");
395});
396
397test("where a written-up doc goes: the conversation, a space, Private, or the General space in public", async () => {
398 const log: string[] = [];
399 const box = new ToolBox(await annAndBob(), { ...readPorts, folios: folioWorld(log) }, ctx, [], actions([]));
400 const make = (where: unknown, source?: string) => box.run("create_artifact", { kind: "doc", title: "Decision", content: "We ship Thursday.", where, source });
401 assert.match((await make(undefined)).text, new RegExp(`Wrote Decision \\(/acme/-/artifacts/x-${NEW}, id ${NEW}\\)`));
402 await make("conversation", "https://g1t.sh/acme/-/chat/c/ops?thread=msg_9");
403 await make("private");
404 await make({ space: "Engineering" });
405 await make("engineering");
406 assert.deepEqual(log, [
407 'create:doc:{"conversation":["ann","bob"]}:',
408 'create:doc:{"conversation":["ann","bob"]}:/acme/-/chat/c/ops?thread=msg_9',
409 'create:doc:"private":',
410 'create:doc:{"space_id":"spc_eng"}:',
411 'create:doc:{"space_id":"spc_eng"}:',
412 ]);
413 assert.equal((await make({ space: "Nowhere" })).outcome, "refused");
414
415 // In a public channel there is no list of people: the General space, else Private.
416 const pub: string[] = [];
417 const open = new ToolBox(await publicChannel(), { ...readPorts, folios: folioWorld(pub) }, ctx, [], actions([]));
418 await open.run("create_artifact", { kind: "doc", title: "Decision", content: "x" });
419 await open.run("create_artifact", { kind: "doc", title: "Decision", content: "x", where: "conversation" });
420 assert.deepEqual(pub, ['create:doc:{"space_id":"spc_general"}:', 'create:doc:{"space_id":"spc_general"}:']);
421 const barred: string[] = [];
422 const noGeneral = new ToolBox(await publicChannel(), { ...readPorts, folios: folioWorld(barred, { forbidden: "spc_general", hidden: new Set(["new"]) }) }, ctx, [], actions([]));
423 const made = await noGeneral.run("create_artifact", { kind: "doc", title: "Decision", content: "x" });
424 assert.deepEqual(barred, ['create:doc:{"space_id":"spc_general"}:', 'create:doc:"private":', `dm:ann:/acme/-/artifacts/x-${NEW}`]);
425 assert.ok(!made.text.includes("Decision"), "a private doc made in public isn't named there");
426 assert.match(made.text, /sent the link to @ann directly/);
427});
428
429test("an artifact someone here can't read is never quoted: the link goes to the asker, and what is remembered stays theirs", async () => {
430 const log: string[] = [];
431 const remembered: boolean[] = [];
432 const acts: ActionPorts = { ...actions([]), remember: async (_body, _scope, onlyForAsker) => (remembered.push(!!onlyForAsker), { ok: true, message: "ok" }) };
433 const box = new ToolBox(await annAndBob(), { ...readPorts, folios: folioWorld(log) }, ctx, [], acts);
434 await box.run("remember", { fact: "Before reading anything" });
435 const read = await box.run("read_artifact", { id: `/acme/-/artifacts/salary-bands-${SECRET}` });
436 assert.equal(read.outcome, "withheld");
437 assert.ok(!read.text.includes("Bands") && !read.text.includes("Salary"), "neither its content nor its title");
438 assert.match(read.text, /say you found it and that you've sent the link to @ann directly/);
439 assert.deepEqual(log, [`dm:ann:/acme/-/artifacts/x-${SECRET}`]);
440 await box.run("remember", { fact: "Bands are reviewed in March" });
441 assert.deepEqual(remembered, [false, true]);
442 const edited = await box.run("edit_artifact", { id: SECRET, target: "append", markdown: "x" });
443 assert.equal(edited.text, "Suggested: people accept or reject it there.", "an edit there isn't named here either");
444
445 // In a public channel, even a doc everyone can read counts as the workspace's.
446 const pub: boolean[] = [];
447 const open = new ToolBox(await publicChannel(), { ...readPorts, folios: folioWorld([]) }, ctx, [], { ...actions([]), remember: async (_b, _s, only) => (pub.push(!!only), { ok: true, message: "ok" }) });
448 await open.run("read_artifact", { id: FOL });
449 await open.run("remember", { fact: "Refunds are 30 days" });
450 assert.deepEqual(pub, [false]);
451});
452
453test("an agent shares only in a private conversation, only with people in it, to view or comment", async () => {
454 const log: string[] = [];
455 const box = new ToolBox(await annAndBob(), { ...readPorts, folios: folioWorld(log) }, ctx, [], actions([]));
456 assert.equal((await box.run("share_artifact", { id: FOL, people: ["@bob"], role: "comment" })).outcome, "allowed");
457 assert.match((await box.run("share_artifact", { id: FOL, people: ["carol"], role: "view" })).text, /@carol isn't in this conversation/);
458 assert.equal((await box.run("share_artifact", { id: FOL, people: ["bob"], role: "edit" })).outcome, "refused");
459 assert.deepEqual(log, [`share:${FOL}:bob:comment`]);
460 const open = new ToolBox(await publicChannel(), { ...readPorts, folios: folioWorld(log) }, ctx, [], actions([]));
461 assert.match((await open.run("share_artifact", { id: FOL, people: ["bob"], role: "view" })).text, /only in a direct message or a private channel/);
462});
463
464test("a doc reads as Markdown with where it is, what the agent may do, and its block ids", () => {
465 const text = folioReadText(agentRead(FOL, "Refunds policy", "# Refunds policy\n\n30 days.", true));
466 assert.equal(
467 text,
468 `# 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.`,
469 );
Docs: a workspace knowledge base people and agents write together470});
A technical writer's 'update the docs after every change' suggests a routine that keeps the docs current when a pull request merges471
472test("a technical writer's duties suggest keeping the docs current when a pull request merges", () => {
473 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"];
474 const names = suggestRoutines(inky, []).map((s) => [s.routine.name, s.routine.events]);
475 assert.deepEqual(names[0], ["Keep the docs current", ["pull_merged"]]);
476 assert.ok(names.some(([name]) => name === "Weekly summary"));
477});

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