Skip to content
459 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 → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.1/**
2 * The skill format (docs.g1t.sh/guides/agent-skills/, "The skill format"):
3 * a skill is a folder holding `SKILL.md`, whose YAML front-matter names it
4 * and says when to use it, followed by the instructions, plus optional
5 * files: `scripts/`, `resources/` (or `references/` and `assets/`, as other
6 * tools write them). It is the open `SKILL.md` format, so a skill written
7 * elsewhere imports as it is.
8 *
9 * g1t reads two extra front-matter keys:
10 *
11 * - `tools:` the agent tools the skill uses (a list, or names separated by
12 * commas). Only tools agents have are accepted, and naming one never
13 * gives it to an agent: an agent without it is told that part doesn't
14 * work where it is.
15 * - `requires_computer:` true when the skill needs the agent's own
16 * computer. A skill with files in `scripts/` needs one whatever it says.
17 *
18 * Other keys (`license`, `metadata`, `allowed-tools`, ...) are kept as they
19 * are and change nothing.
20 *
21 * Pure and standalone (no value imports), so services and the site share it
22 * and Node runs its tests on the file as it is.
23 */
24import type { AgentSkill } from "./skills";
25
26/** The most one skill's folder holds, every file together: 1 MB. */
27export const SKILL_FOLDER_MAX_BYTES = 1024 * 1024;
28/** The most skills from the library one agent has, however they are attached. */
29export const SKILLS_PER_AGENT_MAX = 100;
30/** The most files in one skill's folder, `SKILL.md` included. */
31export const SKILL_FILES_MAX = 200;
32/** The longest name: lowercase letters, digits and hyphens. */
33export const SKILL_NAME_MAX = 64;
34/** The longest description ("when to use it"). */
35export const SKILL_DESCRIPTION_MAX = 1024;
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.36/**
37 * Where a repository keeps the skills it mirrors: `.claude/skills/<name>/SKILL.md`,
38 * the folder Claude Code reads project skills from, so one repository
39 * serves both. `.g1t/skills` is still read for libraries linked before.
40 */
41export const SKILLS_REPO_DIR = ".claude/skills";
42export const SKILLS_REPO_DIRS: readonly string[] = [".claude/skills", ".g1t/skills"];
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.43
44/**
45 * Names the library can't use: g1t's foundational skills', so `use_skill`
46 * always means one thing. The same list as `FOUNDATIONAL_SKILL_IDS`
47 * (a test keeps them equal).
48 */
49export const RESERVED_SKILL_NAMES: readonly string[] = ["documents", "research", "data", "code", "communication", "files"];
50
51/** The tools a skill may name in `tools:`, grouped as the editor shows them. The agents service's tool box has exactly these (a test there checks). */
52export const AGENT_TOOL_GROUPS: { group: string; tools: string[] }[] = [
53 { group: "Code", tools: ["list_repositories", "search_code", "read_file", "list_issues", "get_issue", "get_pull", "recent_activity", "draft_issue", "comment", "review_pull"] },
54 { group: "Artifacts and files", tools: ["search_artifacts", "read_artifact", "list_spaces", "stale_artifacts", "create_artifact", "edit_artifact", "share_artifact", "make_file"] },
55 { group: "Chat", tools: ["search_messages", "read_thread", "workspace_roster"] },
56 { group: "Teamwork", tools: ["ask_colleague", "hand_off", "start_session", "post_update", "use_subagent", "bring_in", "use_skill"] },
57 { group: "Memory", tools: ["remember", "forget"] },
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.58 { group: "Outside g1t", tools: ["lookup_outside", "import_outside", "act_outside", "request_ability"] },
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.59 // Its own computer (docs.g1t.sh/guides/agents/, "Its computer"): offered in sessions, as its abilities allow.
60 { group: "Its computer", tools: ["run_command", "computer_read_file", "computer_write_file"] },
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.61];
62
63/** Every tool a skill may name. */
64export const AGENT_TOOL_NAMES: readonly string[] = AGENT_TOOL_GROUPS.flatMap((group) => group.tools);
65
66/** One file in a skill's folder: text as it is, anything else as standard base64. */
67export type SkillFile = { path: string; content: string; encoding?: "utf8" | "base64" };
68
69/** A skill folder that passed every check. */
70export type CheckedSkill = {
71 name: string;
72 /** When to use it, from the front-matter. */
73 description: string;
74 tools: string[];
75 /** Said in the front-matter, or it has scripts. */
76 requires_computer: boolean;
77 /** The instructions: SKILL.md after its front-matter. */
78 body: string;
79 /** SKILL.md as written. */
80 skill_md: string;
81 /** Every other file, by path. */
82 files: SkillFile[];
83 /** The files under `scripts/`. */
84 scripts: string[];
85 /** Every file's bytes together. */
86 bytes: number;
87 /** Front-matter keys g1t doesn't read, kept as they are. */
88 extra: Record<string, unknown>;
89};
90
91export type SkillCheck = { ok: true; skill: CheckedSkill } | { ok: false; message: string };
92
93// ── Front-matter ─────────────────────────────────────────────────────────
94
95/** SKILL.md split into its front-matter's text and the body after it. */
96export function splitFrontMatter(text: string): { ok: true; yaml: string; body: string } | { ok: false; message: string } {
97 const normal = text.replace(/^/, "").replace(/\r\n?/g, "\n");
98 if (!normal.startsWith("---\n")) return { ok: false, message: "SKILL.md starts with front-matter: a line of ---, then name: and description:, then another line of ---." };
99 const end = normal.indexOf("\n---", 3);
100 if (end < 0) return { ok: false, message: "SKILL.md's front-matter has no closing line of ---." };
101 const after = normal.slice(end + 4);
102 if (after && !after.startsWith("\n") && !/^-*\s*(\n|$)/.test(after)) return { ok: false, message: "SKILL.md's front-matter has no closing line of ---." };
103 return { ok: true, yaml: normal.slice(4, end + 1), body: after.replace(/^-*[ \t]*\n?/, "") };
104}
105
106type Line = { indent: number; text: string; no: number };
107
108function unquote(raw: string, no: number): unknown {
109 const value = raw.trim();
110 if (value.startsWith('"')) {
111 try {
112 const end = closingQuote(value, '"');
113 if (end < 0) throw new Error();
114 return JSON.parse(value.slice(0, end + 1).replace(/\\'/g, "'"));
115 } catch {
116 throw new Error(`line ${no}: a double-quoted value isn't closed`);
117 }
118 }
119 if (value.startsWith("'")) {
120 const end = closingQuote(value, "'");
121 if (end < 0) throw new Error(`line ${no}: a single-quoted value isn't closed`);
122 return value.slice(1, end).replace(/''/g, "'");
123 }
124 if (value.startsWith("[")) {
125 if (!value.endsWith("]")) throw new Error(`line ${no}: a list in [ ] isn't closed`);
126 const inner = value.slice(1, -1).trim();
127 if (!inner) return [];
128 return splitFlow(inner).map((item) => unquote(item, no));
129 }
130 const plain = value.replace(/\s+#.*$/, "");
131 if (plain === "" || plain === "~" || plain === "null") return null;
132 if (plain === "true" || plain === "True") return true;
133 if (plain === "false" || plain === "False") return false;
134 if (/^-?\d+(\.\d+)?$/.test(plain)) return Number(plain);
135 return plain;
136}
137
138/** Where a quoted value starting at 0 closes, or -1. */
139function closingQuote(value: string, quote: string): number {
140 for (let i = 1; i < value.length; i++) {
141 if (quote === '"' && value[i] === "\\") {
142 i++;
143 continue;
144 }
145 if (value[i] === quote) {
146 if (quote === "'" && value[i + 1] === "'") {
147 i++;
148 continue;
149 }
150 return i;
151 }
152 }
153 return -1;
154}
155
156/** `a, "b, c", d` into its items. */
157function splitFlow(inner: string): string[] {
158 const items: string[] = [];
159 let current = "";
160 let quote: string | null = null;
161 for (let i = 0; i < inner.length; i++) {
162 const c = inner[i]!;
163 if (quote) {
164 current += c;
165 if (c === "\\" && quote === '"') current += inner[++i] ?? "";
166 else if (c === quote) quote = null;
167 } else if (c === '"' || c === "'") {
168 quote = c;
169 current += c;
170 } else if (c === ",") {
171 items.push(current.trim());
172 current = "";
173 } else current += c;
174 }
175 if (current.trim()) items.push(current.trim());
176 return items;
177}
178
179/** A block scalar (`|` or `>`) from the lines under its key. */
180function blockScalar(style: string, lines: Line[]): string {
181 if (!lines.length) return "";
182 const base = Math.min(...lines.filter((l) => l.text.trim()).map((l) => l.indent));
183 const texts = lines.map((l) => (l.text.trim() ? " ".repeat(Math.max(0, l.indent - base)) + l.text.trimEnd() : ""));
184 let out: string;
185 if (style.startsWith("|")) out = texts.join("\n");
186 else {
187 out = "";
188 for (const t of texts) {
189 if (!t) out += "\n";
190 else out += out && !out.endsWith("\n") ? ` ${t}` : t;
191 }
192 }
193 if (style.includes("-")) return out.replace(/\n+$/, "");
194 return `${out.replace(/\n+$/, "")}\n`;
195}
196
197/**
198 * The front-matter as values: the YAML that SKILL.md files use, which is
199 * keys with plain, quoted or block (`|`, `>`) strings, booleans, numbers,
200 * lists (`[a, b]` or `- a` lines) and one level of nested keys
201 * (`metadata:`). Throws with the line that can't be read.
202 */
203export function parseFrontMatter(yaml: string): Record<string, unknown> {
204 const lines: Line[] = yaml.split("\n").map((raw, i) => ({ indent: raw.length - raw.trimStart().length, text: raw.trimStart(), no: i + 2 }));
205 return readMap(lines, 0, lines.length, 0);
206}
207
208function readMap(lines: Line[], from: number, to: number, indent: number): Record<string, unknown> {
209 const out: Record<string, unknown> = {};
210 let i = from;
211 while (i < to) {
212 const line = lines[i]!;
213 if (!line.text || line.text.startsWith("#")) {
214 i++;
215 continue;
216 }
217 if (line.indent !== indent) throw new Error(`line ${line.no}: unexpected indentation`);
218 const match = line.text.match(/^("[^"]+"|'[^']+'|[A-Za-z0-9_.\-]+)\s*:(?:\s+(.*)|\s*)$/);
219 if (!match) throw new Error(`line ${line.no}: expected "key: value"`);
220 const key = match[1]!.replace(/^["']|["']$/g, "");
221 const rest = (match[2] ?? "").trim();
222 // The lines that belong to this key: more indented than it, or blank.
223 let j = i + 1;
224 while (j < to && (!lines[j]!.text || lines[j]!.indent > indent || (lines[j]!.indent === indent && lines[j]!.text.startsWith("- ") && !rest))) j++;
225 // Trailing blank lines belong to the next key.
226 let end = j;
227 while (end > i + 1 && !lines[end - 1]!.text) end--;
228 const child = lines.slice(i + 1, end);
229 if (key in out) throw new Error(`line ${line.no}: ${key} is given twice`);
230 if (/^[|>][+-]?$/.test(rest)) out[key] = blockScalar(rest, child);
231 else if (rest && !rest.startsWith("#")) {
232 const value = unquote(rest, line.no);
233 const more = child.filter((l) => l.text && !l.text.startsWith("#"));
234 // A plain value folded over more lines.
235 if (more.length && typeof value === "string" && !/^["'[]/.test(rest)) out[key] = [value, ...more.map((l) => l.text.trim())].join(" ");
236 else if (more.length) throw new Error(`line ${more[0]!.no}: unexpected indentation`);
237 else out[key] = value;
238 } else {
239 const content = child.filter((l) => l.text && !l.text.startsWith("#"));
240 if (!content.length) out[key] = null;
241 else if (content[0]!.text.startsWith("- ") || content[0]!.text === "-") {
242 out[key] = content.map((l) => {
243 if (!l.text.startsWith("-")) throw new Error(`line ${l.no}: expected "- item"`);
244 return unquote(l.text.slice(1), l.no);
245 });
246 } else {
247 const start = lines.indexOf(content[0]!);
248 out[key] = readMap(lines, start, end, content[0]!.indent);
249 }
250 }
251 i = j;
252 }
253 return out;
254}
255
256/** A value as YAML on one line: plain when that reads the same, else double-quoted. */
257function yamlScalar(value: unknown): string {
258 if (value === null || value === undefined) return "null";
259 if (typeof value === "boolean" || typeof value === "number") return String(value);
260 const text = String(value);
261 if (/^[A-Za-z0-9_][A-Za-z0-9_ ,.;()/'’&+-]*$/.test(text) && !/^(true|false|null|yes|no|~|-?\d+(\.\d+)?)$/i.test(text) && !/\s$/.test(text)) return text;
262 return JSON.stringify(text);
263}
264
265function yamlValue(value: unknown, indent: string): string {
266 if (Array.isArray(value)) return value.length ? `[${value.map(yamlScalar).join(", ")}]` : "[]";
267 if (value && typeof value === "object") {
268 const entries = Object.entries(value as Record<string, unknown>);
269 return `\n${entries.map(([k, v]) => `${indent} ${k}:${keyed(v, `${indent} `)}`).join("\n")}`;
270 }
271 return yamlScalar(value);
272}
273
274/** What follows `key:`: a space and the value, or the nested keys on their own lines. */
275function keyed(value: unknown, indent: string): string {
276 const nested = !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0;
277 return nested ? yamlValue(value, indent) : ` ${yamlValue(value && typeof value === "object" && !Array.isArray(value) ? null : value, indent)}`;
278}
279
280/** SKILL.md for a skill written in the editor: front-matter (name, description, then g1t's keys and any kept ones), then the instructions. */
281export function renderSkillMd(input: { name: string; description: string; tools?: string[]; requires_computer?: boolean; body: string; extra?: Record<string, unknown> }): string {
282 const lines = ["---", `name: ${input.name}`, `description: ${yamlScalar(input.description.replace(/\s*\n\s*/g, " ").trim())}`];
283 if (input.tools?.length) lines.push(`tools: ${yamlValue(input.tools, "")}`);
284 if (input.requires_computer) lines.push("requires_computer: true");
285 for (const [key, value] of Object.entries(input.extra ?? {})) {
286 if (["name", "description", "tools", "requires_computer"].includes(key)) continue;
287 lines.push(`${key}:${keyed(value, "")}`);
288 }
289 lines.push("---", "", input.body.replace(/\r\n?/g, "\n").trim(), "");
290 return lines.join("\n");
291}
292
293// ── Checks ───────────────────────────────────────────────────────────────
294
295/** Why `name` can't name a skill, or null. */
296export function skillNameProblem(name: string): string | null {
297 if (!name) return "A skill needs a name.";
298 if (name.length > SKILL_NAME_MAX) return `A skill's name is at most ${SKILL_NAME_MAX} characters.`;
299 if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) return "A skill's name is lowercase letters, digits and single hyphens, like release-notes.";
300 if (RESERVED_SKILL_NAMES.includes(name)) return `${name} is one of g1t's foundational skills. Choose another name.`;
301 return null;
302}
303
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.304/** A plugin skill's name, as Claude Code takes it: letters, digits, dots, underscores and hyphens, at most 64. */
305export function lenientSkillNameProblem(name: string): string | null {
306 if (!name) return "A skill needs a name.";
307 if (name.length > SKILL_NAME_MAX) return `A skill's name is at most ${SKILL_NAME_MAX} characters.`;
308 if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) || name.includes("..")) return "A skill's name is letters, digits, dots, underscores and hyphens.";
309 return null;
310}
311
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.312/** Whether `path` is a plain relative path inside the folder. */
313export function skillPathProblem(path: string): string | null {
314 if (!path || path.length > 255) return `${path || "A file"} isn't a path a skill can hold.`;
315 if (path.startsWith("/") || path.includes("\\") || /[\u0000-\u001f]/.test(path)) return `${path} isn't a path a skill can hold.`;
316 const parts = path.split("/");
317 if (parts.some((part) => !part || part === "." || part === ".." || part === ".git")) return `${path} isn't a path a skill can hold.`;
318 return null;
319}
320
321/** A file's size in bytes. */
322export function skillFileBytes(file: SkillFile): number {
323 if (file.encoding === "base64") {
324 const clean = file.content.replace(/\s+/g, "");
325 return Math.floor((clean.length * 3) / 4) - (clean.endsWith("==") ? 2 : clean.endsWith("=") ? 1 : 0);
326 }
327 return new TextEncoder().encode(file.content).length;
328}
329
330/** "12 KB". */
331export function skillSize(bytes: number): string {
332 if (bytes < 1024) return `${bytes} B`;
333 if (bytes < 1024 * 1024) return `${Math.max(1, Math.round(bytes / 1024))} KB`;
334 return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
335}
336
337function listOf(value: unknown): string[] | null {
338 if (value === null || value === undefined) return [];
339 if (Array.isArray(value)) return value.every((v) => typeof v === "string") ? (value as string[]) : null;
340 if (typeof value === "string") return value.split(/[\s,]+/).filter(Boolean);
341 return null;
342}
343
344/**
345 * Checks a skill's folder: one `SKILL.md` at its top with a name and a
346 * description, tools agents have, plain paths, at most `SKILL_FILES_MAX`
347 * files and `SKILL_FOLDER_MAX_BYTES` together. `expectName` is the
348 * folder's name when it must match (a repository's `.g1t/skills/<name>/`).
349 */
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.350export type SkillCheckOptions = {
351 expectName?: string | null;
352 /**
353 * For a skill from a plugin: its name keeps its case and may be one of
354 * g1t's foundational names (agents call it as `plugin:name`), `tools:`
355 * names agents don't have are kept aside rather than refused, and the
356 * folder may be larger (`limits`).
357 */
358 lenient?: boolean;
359 limits?: { files: number; bytes: number };
360};
361
362/**
363 * How large a plugin's skill kept here may be: public skills carry
364 * references and schemas, and a version is one database row (2 MB at
365 * most, with its JSON). Larger folders keep their text and leave their
366 * biggest binaries out (the runner fetches the plugin whole anyway).
367 */
368export const PLUGIN_SKILL_LIMITS = { files: 1000, bytes: 1_500_000 };
369
370export function checkSkillFolder(input: SkillFile[], options: SkillCheckOptions = {}): SkillCheck {
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.371 const bad = (message: string): SkillCheck => ({ ok: false, message });
372 if (!Array.isArray(input) || !input.length) return bad("A skill is a folder with a SKILL.md in it.");
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.373 const limits = options.limits ?? { files: SKILL_FILES_MAX, bytes: SKILL_FOLDER_MAX_BYTES };
374 if (input.length > limits.files) return bad(`A skill holds at most ${limits.files} files; this one has ${input.length}.`);
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.375 const seen = new Set<string>();
376 let skillMd: SkillFile | null = null;
377 const files: SkillFile[] = [];
378 let bytes = 0;
379 for (const raw of input) {
380 if (!raw || typeof raw.path !== "string" || typeof raw.content !== "string") return bad("Each file needs a path and its content.");
381 const path = raw.path.replace(/^\.\//, "");
382 const problem = skillPathProblem(path);
383 if (problem) return bad(problem);
384 if (seen.has(path.toLowerCase())) return bad(`${path} is in the folder twice.`);
385 seen.add(path.toLowerCase());
386 const file: SkillFile = { path, content: raw.content, encoding: raw.encoding === "base64" ? "base64" : "utf8" };
387 if (file.encoding === "base64" && !/^[A-Za-z0-9+/\s]*=*\s*$/.test(file.content)) return bad(`${path} isn't valid base64.`);
388 bytes += skillFileBytes(file);
389 if (path.toLowerCase() === "skill.md") {
390 if (file.encoding === "base64") return bad("SKILL.md is text.");
391 skillMd = { ...file, path: "SKILL.md" };
392 } else files.push(file);
393 }
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.394 if (bytes > limits.bytes) return bad(`A skill's folder is at most ${limits.bytes === SKILL_FOLDER_MAX_BYTES ? "1 MB" : skillSize(limits.bytes)}; this one is ${skillSize(bytes)}.`);
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.395 if (!skillMd) {
396 const nested = files.find((f) => f.path.toLowerCase().endsWith("/skill.md"));
397 return bad(nested ? `SKILL.md belongs at the top of the folder, not in ${nested.path.slice(0, nested.path.lastIndexOf("/"))}.` : "A skill's folder needs a SKILL.md at its top.");
398 }
399 const split = splitFrontMatter(skillMd.content);
400 if (!split.ok) return bad(split.message);
401 let front: Record<string, unknown>;
402 try {
403 front = parseFrontMatter(split.yaml);
404 } catch (error) {
405 return bad(`SKILL.md's front-matter can't be read: ${error instanceof Error ? error.message : String(error)}.`);
406 }
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.407 const name = typeof front.name === "string" ? front.name.trim() : options.lenient && options.expectName ? options.expectName : "";
408 const nameProblem = options.lenient ? lenientSkillNameProblem(name) : skillNameProblem(name);
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.409 if (nameProblem) return bad(name ? nameProblem : "SKILL.md's front-matter needs a name: the skill's name, like release-notes.");
410 if (options.expectName && options.expectName !== name) return bad(`The folder is ${options.expectName}, but its SKILL.md is named ${name}. They must match.`);
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.411 const full = typeof front.description === "string" ? front.description.replace(/\s+/g, " ").trim() : "";
412 if (!full) return bad("SKILL.md's front-matter needs a description: when an agent should use the skill.");
413 if (full.length > SKILL_DESCRIPTION_MAX && !options.lenient) return bad(`A skill's description is at most ${SKILL_DESCRIPTION_MAX} characters.`);
414 // A plugin's skill keeps its text as written; the listing shows the first part.
415 const description = full.length > SKILL_DESCRIPTION_MAX ? `${full.slice(0, SKILL_DESCRIPTION_MAX - 1).trimEnd()}…` : full;
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.416 const listed = listOf(front.tools);
417 if (!listed) return bad("tools: is a list of tool names, like [read_file, make_file].");
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.418 const named = [...new Set(listed.map((t) => t.trim()))].filter(Boolean);
419 const unknown = named.filter((tool) => !AGENT_TOOL_NAMES.includes(tool));
420 const tools = options.lenient ? named.filter((tool) => AGENT_TOOL_NAMES.includes(tool)) : named;
421 if (unknown.length && !options.lenient) return bad(`tools: names ${unknown.join(", ")}, which ${unknown.length === 1 ? "isn't a tool" : "aren't tools"} agents have. Agents' tools are listed in the skills guide.`);
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.422 const flag = front.requires_computer;
423 if (flag !== undefined && flag !== null && typeof flag !== "boolean") return bad("requires_computer: is true or false.");
424 const body = split.body.trim();
425 if (!body) return bad("SKILL.md needs instructions under its front-matter.");
426 const scripts = files.filter((f) => f.path.startsWith("scripts/")).map((f) => f.path);
427 const extra: Record<string, unknown> = {};
428 for (const [key, value] of Object.entries(front)) if (!["name", "description", "tools", "requires_computer"].includes(key)) extra[key] = value;
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.429 if (options.lenient && unknown.length) extra.tools = unknown;
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.430 files.sort((a, b) => a.path.localeCompare(b.path));
431 return {
432 ok: true,
433 skill: { name, description, tools, requires_computer: flag === true || scripts.length > 0, body, skill_md: skillMd.content, files, scripts, bytes, extra },
434 };
435}
436
437// ── g1t's foundational skills, as SKILL.md ───────────────────────────────
438
439/**
440 * A foundational skill written out in the same format: its name, when to
441 * use it, the tools its working parts use, then its playbook and, part by
442 * part, what works today and what is coming.
443 */
444export function foundationalSkillMd(skill: AgentSkill): string {
445 const ready = skill.abilities.filter((a) => a.status === "ready");
446 const coming = skill.abilities.filter((a) => a.status === "coming");
447 const tools = [...new Set(ready.flatMap((a) => a.tools))];
448 const body = [
449 `# ${skill.name}`,
450 "",
451 skill.instructions,
452 "",
453 "## What works today",
454 "",
455 ...ready.map((a) => `- **${a.label}**${a.tools.length ? ` (${a.tools.map((t) => `\`${t}\``).join(", ")})` : ""}: ${a.note}`),
456 ...(coming.length ? ["", "## Not yet in g1t", "", ...coming.map((a) => `- **${a.label}**: ${a.note}`)] : []),
457 ].join("\n");
458 return renderSkillMd({ name: skill.id, description: skill.when, tools, body, extra: { metadata: { source: "g1t", version: skill.version } } });
459}

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