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

Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.1/**
2 * What the prompt says about an agent's abilities outside g1t
3 * (abilities.ts), and what a turn "said" for "alone when asked for it".
4 * Pure, with type-only imports, so it is tested under Node as it is.
5 */
6import type { Ability, AbilitySection } from "@g1t/contracts";
7
8/**
9 * The abilities as the prompt tells the agent about them: what each
10 * connected integration lets it do and at which level, its MCP servers,
11 * and what isn't connected, so it asks rather than guesses.
12 */
13export function abilitiesSection(sections: AbilitySection[]): string | null {
14 const lines: string[] = [];
15 const words = (ability: Ability) => (ability.level === "alone" ? "on your own" : ability.level === "asked" ? "on your own only when they asked for it, else it asks first" : ability.level === "ask" ? "asks first (a card)" : "never");
16 for (const section of sections) {
17 if (section.group !== "integration" && section.group !== "mcp") continue;
18 for (const source of section.sources) {
19 if (!source.connected) continue;
20 const ready = source.abilities.filter((ability) => ability.status === "ready");
21 if (!ready.length) continue;
22 lines.push(`- ${source.name}: ${ready.map((ability) => `${ability.label.toLowerCase()} ${words(ability)}`).join("; ")}.`);
23 }
24 }
25 const missing = sections
26 .filter((section) => section.group === "integration")
27 .flatMap((section) => section.sources)
28 .filter((source) => !source.connected && source.abilities.length)
29 .map((source) => source.name);
30 if (!lines.length && !missing.length) return null;
31 return [
32 "## Your abilities outside g1t",
33 "",
34 "These are set on your Abilities tab and enforced in code: a call that isn't allowed is refused with the rule, and one that asks first posts a card. Never work around a refusal.",
35 ...(lines.length ? ["", ...lines] : []),
36 ...(missing.length ? ["", `Not connected to this workspace: ${missing.join(", ")}. When someone needs one, say so and use request_ability.`] : []),
37 ].join("\n");
38}
39
40/** What a turn says, for "alone when asked for it": the latest messages from people, or a session's goal and steering. */
41export function saidText(parts: (string | null | undefined)[]): string {
42 return parts.filter((part): part is string => typeof part === "string" && part.trim().length > 0).join("\n").slice(0, 20_000);
43}