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}` }],
Four small things seen today. An agent can attach a file it made to any doc the person who asked can edit, whatever the space's agent mode: attaching changes nothing in the doc, so the suggest mode that stopped every PDF and spreadsheet no longer does; the ability is its own, attach, beside read, suggest and edit. Home no longer counts robots among the people: events that name an agent by its own id, or g1t's upkeep as the workspace or as g1t, read as the agent and as g1t, in the digest and on the page, so the faces say who instead of someone. Tooltips are the inverted bubble, the page's foreground behind the page's background, with no border. The Spend page's slice tabs keep their ring inside the row instead of losing its top to the scroll edge, and the Apps launcher keeps its search box and footer in place while it reads which apps there are, with skeleton tiles between.306 can: { read: true, suggest: true, edit: false, attach: true },
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.307 audience_can_read,
308});
309
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.310/** An artifacts service with one readable doc, one secret one, and whatever is made (readable here unless `hidden` says so). */
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.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");
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.364 // Without an artifacts service, no artifact tools.
Workspace agents find, read, write and share the workspace's artifacts with search_artifacts, read_artifact, create_artifact (docs for now), edit_artifact, stale_artifacts, share_artifact and list_spaces, and their recall reads artifacts and projects' docs through the folio RPCs instead of Docs' pages.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.