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 asked | 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; | |
| 3 | ||
| 4 | import type { User } from "@g1t/contracts"; | |
| 5 | ||
| 6 | import { Audience, type AudienceInfo, type AudiencePorts, type RepoRef, WITHHELD } from "./audience.ts"; | |
| 7 | import { type MemoryViewer, type RecallPlace, changeableBy, cleanFact, memorySection, recallable, scopeFor, visibleTo } from "./memory.ts"; | |
| 8 | import { alertDue, checkPolicy, DEFAULT_POLICY, policyBlock } from "./policy.ts"; | |
| 9 | import { checkRoutine, checkSchedule, describeSchedule, nextRun } from "./schedule.ts"; | |
| 10 | import { type ActionPorts, type ToolPorts, MAX_SESSION_TOOL_CALLS, MAX_TOOL_CALLS, ToolBox } from "./tools.ts"; | |
| 11 | import { dollars } from "./money.ts"; | |
| 12 | ||
| 13 | // ── Memory: where a fact is recalled, who sees it, who changes it ─────── | |
| 14 | ||
| 15 | const dmWithAnn: RecallPlace = { channel_id: "chn_dm_ann", kind: "dm", people: ["ann"] }; | |
| 16 | const groupDm: RecallPlace = { channel_id: "chn_dm_group", kind: "dm", people: ["ann", "bob"] }; | |
| 17 | const privateOps: RecallPlace = { channel_id: "chn_ops", kind: "private", people: ["ann", "bob"] }; | |
| 18 | const publicGeneral: RecallPlace = { channel_id: "chn_general", kind: "public", people: ["ann"] }; | |
| 19 | ||
| 20 | test("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 | ||
| 29 | test("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 | ||
| 36 | test("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 asked | 46 | }); |
| 47 | ||
| 48 | test("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 | ||
| 60 | test("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 | ||
| 74 | test("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 | ||
| 84 | test("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 | ||
| 93 | test("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 | ||
| 107 | test("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 | ||
| 116 | test("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 | ||
| 125 | const WEB: RepoRef = { id: "rep_web", namespace: "acme", name: "web", isPrivate: true, defaultBranch: "main" }; | |
| 126 | const 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 | ||
| 139 | function 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 | ||
| 148 | const member = (id: string, code = true): User => ({ id, username: id, workspaces: [{ slug: "acme", role: "member", code_access: code }] }) as User; | |
| 149 | ||
| 150 | function 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 calendar | 154 | 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 asked | 155 | 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 | ||
| 162 | const 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 calendar | 164 | test("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 asked | 165 | 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 calendar | 169 | 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 asked | 170 | 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 calendar | 171 | 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 asked | 174 | // 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 calendar | 175 | 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 asked | 176 | 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' | 187 | test("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 calendar | 212 | test("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 asked | 213 | 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 calendar | 216 | 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 asked | 218 | 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 | ||
| 227 | test("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 opened | 233 | |
| 234 | // ── Routines that run when something happens ───────────────────────────── | |
| 235 | ||
| 236 | import { describeEvents, suggestRoutines } from "./suggest.ts"; | |
| 237 | import { checkRoutine as checkEventRoutine } from "./schedule.ts"; | |
| 238 | ||
| 239 | test("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 | ||
| 259 | test("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 calendar | 269 | |
| 270 | test("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 together | 288 | |
| 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 | ||
| 291 | import type { FolioAgentRead, FolioRef } from "@g1t/contracts"; | |
| 292 | ||
| 293 | import { type FoliosPorts, folioReadText, folioRef, sourceLink } from "./tools.ts"; | |
| 294 | ||
| 295 | const FOL = "fol_01jabcdefghjkmnpqrstvwxyz0"; | |
| 296 | const SECRET = "fol_01jabcdefghjkmnpqrstvwxyz1"; | |
| 297 | const NEW = "fol_01jabcdefghjkmnpqrstvwxyz2"; | |
| Docs: a workspace knowledge base people and agents write together | 298 | |
| 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. | 299 | const 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 together | 300 | |
| 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. | 301 | const 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. | 311 | function 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 together | 336 | }; |
| 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 | ||
| 339 | const dmWithCal = () => Audience.build("acme", "cal", audienceWorld({ kind: "dm", member_user_ids: ["cal"], member_count: 1 }, [member("cal", false)], {})); | |
| 340 | const annAndBob = () => Audience.build("acme", "ann", audienceWorld({ kind: "private", member_user_ids: ["ann", "bob"], member_count: 2 }, [member("ann"), member("bob")], {})); | |
| 341 | const publicChannel = () => Audience.build("acme", "ann", audienceWorld({ kind: "public", member_user_ids: ["ann"], member_count: 30 }, [member("ann")], {})); | |
| 342 | ||
| 343 | test("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 together | 346 | 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 together | 348 | 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 | ||
| 368 | test("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 together | 381 | }); |
| 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. | 383 | test("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 | ||
| 397 | test("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 | ||
| 429 | test("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 | ||
| 453 | test("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 | ||
| 464 | test("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 together | 470 | }); |
| A technical writer's 'update the docs after every change' suggests a routine that keeps the docs current when a pull request merges | 471 | |
| 472 | test("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.