Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
819 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.

Merge branch 'worktree-agent-ad52ad80841a37960'1#!/usr/bin/env node
2// g1t's Claude Code hooks. Claude Code runs this file for SessionStart,
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.3// UserPromptSubmit, PreToolUse, PostToolUse, SubagentStart, SubagentStop,
4// Stop and SessionEnd, with the event as JSON on stdin. In a working copy of
5// a g1t repository it records the session on g1t through the `g1t` CLI:
Merge branch 'worktree-agent-ad52ad80841a37960'6//
7// SessionStart g1t session start --harness claude_code --harness-session-id ID
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.8// and, when the branch, G1T_ISSUE or the session names an
9// issue, g1t work context N, given to Claude as context
Merge branch 'worktree-agent-ad52ad80841a37960'10// UserPromptSubmit g1t session log --kind prompt TEXT (first 2,000 characters)
Merge branch 'worktree-agent-ab3c375781462035f'11// and, for a plugin's skill typed as /plugin:name,
12// g1t session log --kind skill NAME
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.13// PreToolUse Agent: the task it is given, kept until its subagent starts.
14// Inside a subagent, its own g1t session for the g1t
Merge branch 'worktree-agent-ab3c375781462035f'15// commands (Bash or PowerShell) and g1t MCP claims it makes.
Merge branch 'worktree-agent-ad52ad80841a37960'16// PostToolUse g1t session log --kind tool_call "Tool: what it was called with"
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.17// (a Skill: g1t session log --kind skill NAME), and
18// g1t session touch for the files it wrote, the commits
19// it made, the branches it pushed and the issues it claimed;
20// after a git push that reached the remote,
21// g1t api update_session -f id=SESSION -f head_commit=SHA;
22// after an Agent call, its subagent's task and, once it
23// finished, its final message
24// SubagentStart g1t session start --parent SESSION --agent-type TYPE …:
25// each subagent is a session of its own, under this one
26// SubagentStop g1t session end --result "its final message" for it
Merge branch 'worktree-agent-ad52ad80841a37960'27// Stop g1t session heartbeat
28// SessionEnd g1t session end --reason REASON
29//
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.30// What a tool returned (tool_response) is never sent, with one exception
31// that is not a tool's output: an Agent call's answer, which is its
32// subagent's final message (as SubagentStop gives it) and its id. Prompts
33// are not sent when G1T_RECORD_PROMPTS=0, and nothing is when G1T_RECORD=0.
Merge branch 'worktree-agent-ad52ad80841a37960'34//
35// Every hook returns at once: the CLI calls run in a detached process of
36// their own, so a slow network never holds Claude up. If `g1t` is not
Merge branch 'worktree-agent-ab3c375781462035f'37// installed, not signed in or older than MIN_CLI (or refuses a command,
38// as an old CLI does), nothing is recorded and Claude Code says so once.
39// Nothing is ever installed. No dependencies; Node 18 or later, and on an
40// older Node it says so once and does nothing.
41//
42// Every CLI call is in test/cli-contract.json, which the tests hold this
43// file and the CLI's own parser (crates/g1t) to.
Merge branch 'worktree-agent-ad52ad80841a37960'44
45import { spawn, spawnSync } from "node:child_process";
46import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
47import { homedir } from "node:os";
48import { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
49import { fileURLToPath } from "node:url";
50
51/** The most of a prompt that is sent. */
52export const PROMPT_LIMIT = 2000;
53/** The most of a tool call's summary that is sent. */
54export const SUMMARY_LIMIT = 200;
55/** The most of an issue's work context given to Claude. */
56export const CONTEXT_LIMIT = 8000;
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.57/** The most of a subagent's task (its description) that is sent. */
58export const TASK_LIMIT = 200;
59/** The most of a subagent's prompt that is sent. */
60export const AGENT_PROMPT_LIMIT = 4000;
61/** The most of a subagent's final message that is sent. */
62export const RESULT_LIMIT = 8000;
Merge branch 'worktree-agent-ad52ad80841a37960'63/** How long SessionStart waits for an issue's work context. */
64const CONTEXT_WAIT_MS = 900;
65/** Exit code the g1t CLI gives when signing in is needed. */
66const EXIT_AUTH = 4;
Merge branch 'worktree-agent-ab3c375781462035f'67/** Exit code the g1t CLI gives for a command it does not take, as a CLI older than the plugin does. */
68const EXIT_USAGE = 2;
69/** The oldest g1t CLI that takes every command the hooks run (test/cli-contract.json's minimum_version). */
70export const MIN_CLI = "0.3.2";
71/** The oldest Node the hooks run on. */
72export const MIN_NODE = 18;
73/** How to update the CLI: its installer, run again. */
74const INSTALL = "`curl -fsSL https://g1t.sh/install | sh` (on Windows, `irm https://g1t.sh/install.ps1 | iex`)";
75/** The tools that run shell commands. */
76const SHELL_TOOLS = new Set(["Bash", "PowerShell"]);
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.77/** The tool that starts subagents (Task before Claude Code renamed it). */
78const AGENT_TOOLS = new Set(["Agent", "Task"]);
79/** Tools that write a file, and where they say which. */
80const WRITERS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
Merge branch 'worktree-agent-ad52ad80841a37960'81
82const SELF = fileURLToPath(import.meta.url);
83
84// --- what to send ------------------------------------------------------
85
86export function clip(text, limit) {
87 const value = String(text ?? "");
88 return value.length > limit ? `${value.slice(0, limit - 1)}…` : value;
89}
90
91/** Hides what looks like a secret in a command line before it is sent. */
92export function redact(text) {
93 return String(text)
94 .replace(/\b(g1t|ghp|gho|ghs|github_pat|glpat|xox[abpr]|sk-ant|sk|rk)[-_][A-Za-z0-9_-]{8,}/g, "$1_…")
95 .replace(/(authorization:\s*(?:bearer|basic|token)\s+)[^\s"']+/gi, "$1…")
96 .replace(/(\b[A-Za-z_]*(?:TOKEN|SECRET|PASSWORD|PASSWD|API_?KEY|PRIVATE_KEY)[A-Za-z_]*=)("[^"]*"|'[^']*'|\S+)/gi, "$1…")
97 .replace(/(--(?:token|password|secret)[= ])\S+/gi, "$1…")
98 .replace(/(https?:\/\/)[^\s/:@]+:[^\s/@]+@/g, "$1…@");
99}
100
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.101/** A path as the repository names it: relative to its root, with forward slashes; null outside it. */
102export function repoPath(path, cwd) {
103 if (typeof path !== "string" || !path) return null;
104 if (!isAbsolute(path)) return path.replaceAll("\\", "/").replace(/^\.\//, "");
105 if (!cwd) return null;
106 const rel = relative(cwd, path);
107 if (!rel || rel.startsWith("..") || isAbsolute(rel)) return null;
108 return rel.replaceAll("\\", "/");
109}
110
Merge branch 'worktree-agent-ab3c375781462035f'111/** What a subagent's shell command starts with to act as its session (see preToolUse), in Bash or PowerShell. */
112export function sessionPrefix(tool, session) {
113 return tool === "PowerShell" ? `$env:G1T_SESSION='${session}'; ` : `export G1T_SESSION=${session}; `;
114}
115
116const PREFIX = /^(?:export G1T_SESSION=[A-Za-z0-9_]+; |\$env:G1T_SESSION='[A-Za-z0-9_]+'; )/;
117
Merge branch 'worktree-agent-ad52ad80841a37960'118/** One line on what a tool was called with: never what it returned. */
119export function summarizeTool(name, input, cwd) {
120 const i = input && typeof input === "object" ? input : {};
121 const path = (p) => {
122 if (typeof p !== "string") return "";
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.123 return repoPath(p, cwd) ?? p;
Merge branch 'worktree-agent-ad52ad80841a37960'124 };
125 let what = "";
Merge branch 'worktree-agent-ab3c375781462035f'126 if (typeof i.command === "string") {
127 // Without the session the plugin gave a subagent's command.
128 const command = i.command.replace(PREFIX, "");
129 what = command.split("\n")[0] + (command.includes("\n") ? " …" : "");
130 }
Merge branch 'worktree-agent-ad52ad80841a37960'131 else if (i.file_path || i.notebook_path) what = path(i.file_path ?? i.notebook_path);
132 else if (typeof i.pattern === "string") what = `${i.pattern}${i.path ? ` in ${path(i.path)}` : ""}`;
133 else if (typeof i.url === "string") what = i.url;
134 else if (typeof i.query === "string") what = i.query;
135 else if (typeof i.description === "string") what = i.description;
136 else if (typeof i.action === "string") what = i.action;
137 const line = what ? `${name}: ${redact(what)}` : String(name);
138 return clip(line, SUMMARY_LIMIT);
139}
140
141/** The issue a branch is for, when its name holds the number: 42-fix, issue-42, fix/42, g1t/42-x. */
142export function issueFromBranch(branch) {
143 if (!branch || branch === "HEAD") return null;
144 if (/^(release|releases|hotfix|v\d)/i.test(branch)) return null;
145 const match = /(?:^|[/_-])(?:issues?-?|#)?(\d{1,7})(?=$|[/_-])/i.exec(branch);
146 if (!match) return null;
147 const n = Number(match[1]);
148 return n > 0 ? n : null;
149}
150
151/** The issue a shell command claims or attaches the session to, if any. */
152export function issueFromCommand(command) {
153 if (typeof command !== "string") return null;
154 const patterns = [
155 /\bg1t\s+work\s+claim\s+#?(\d+)/,
156 /\bg1t\s+session\s+(?:start|attach)\b[^|;&\n]*--issue[= ]#?(\d+)/,
157 /\bg1t\s+pr\s+create\b[^|;&\n]*--issue[= ]#?(\d+)/,
158 ];
159 for (const pattern of patterns) {
160 const match = pattern.exec(command);
161 if (match) return Number(match[1]);
162 }
163 return null;
164}
165
166export const isGitPush = (command) =>
167 typeof command === "string" && /(^|[\s;&|(])git(\s+-\S+(\s+[^-\s]\S*)?)*\s+push\b/.test(command);
168
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.169export const isGitCommit = (command) =>
170 typeof command === "string" && /(^|[\s;&|(])git(\s+-\S+(\s+[^-\s]\S*)?)*\s+commit\b/.test(command);
171
172/** The branch a command makes: git checkout -b X, git switch -c X, git branch X. */
173export function branchFromCommand(command) {
174 if (typeof command !== "string") return null;
175 const match =
176 /\bgit\s+(?:checkout\s+-[bB]|switch\s+-[cC]|switch\s+--create)\s+([^\s;&|]+)/.exec(command) ??
177 /\bgit\s+branch\s+(?!-)([^\s;&|]+)/.exec(command);
178 return match ? match[1] : null;
179}
180
181/** A subagent's harness id: the parent's, `/agent/`, and its id without Claude Code's `agent-` prefix. */
182export function subagentHarnessId(parent, agentId) {
183 return `${String(parent).trim()}/agent/${String(agentId).trim().replace(/^agent-/, "")}`;
184}
185
186/** The text of an Agent call's answer: its subagent's final message. */
187export function agentResult(response) {
188 const content = response?.content;
189 if (typeof content === "string") return content;
190 if (!Array.isArray(content)) return "";
191 return content
192 .filter((block) => block && block.type === "text" && typeof block.text === "string")
193 .map((block) => block.text)
194 .join("\n")
195 .trim();
196}
197
Merge branch 'worktree-agent-ab3c375781462035f'198/** The plugin skill a prompt runs when it is typed as one: `g1t:work-on-issue` for `/g1t:work-on-issue 42`. */
199export function typedSkill(prompt) {
200 const text = String(prompt ?? "").replace(/^/, "");
201 const match = /^\s*\/([A-Za-z0-9][\w.-]*:[A-Za-z0-9][\w.-]*)(?=\s|$)/.exec(text);
202 return match ? match[1] : null;
203}
204
205/**
206 * A skill's step: its name only. `skill` keys it (see hook), so a skill
207 * typed as /g1t:name, which Claude Code may then also run as a Skill tool
208 * call, is recorded once in the turn.
209 */
210function skillStep(name) {
211 return { args: ["session", "log", "--kind", "skill", "--", clip(name, 100)], skill: name.replace(/^.*:/, "") };
212}
213
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.214/** The g1t MCP tool a call is to, by name: `issue` for mcp__plugin_g1t_g1t__issue or mcp__g1t__issue. */
215export function g1tTool(name) {
216 const match = /^mcp__(?:plugin_g1t_)?g1t__([a-z_]+)$/.exec(String(name ?? ""));
217 return match ? match[1] : null;
218}
219
Merge branch 'worktree-agent-ad52ad80841a37960'220/**
221 * What a hook event asks for: the CLI calls to run in the background
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.222 * (`steps`), whether to start the session first, and for a subagent's
223 * events, which subagent (`agent`). Pure, so the tests can check it
224 * without a CLI.
Merge branch 'worktree-agent-ad52ad80841a37960'225 */
226export function plan(event, env = process.env) {
227 const name = event?.hook_event_name;
228 const sid = event?.session_id;
229 if (!name || !sid || env.G1T_RECORD === "0") return null;
230 const cwd = event.cwd || process.cwd();
231 const job = { cwd, sid, steps: [] };
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.232 // Inside a subagent, what it does is its own session's.
233 if (event.agent_id) job.agent = { id: String(event.agent_id), type: event.agent_type || null };
Merge branch 'worktree-agent-ad52ad80841a37960'234 switch (name) {
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.235 case "SessionStart": {
Merge branch 'worktree-agent-ad52ad80841a37960'236 job.start = true;
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.237 const issue = Number(env.G1T_ISSUE);
238 if (issue > 0) job.issue = issue;
Merge branch 'worktree-agent-ad52ad80841a37960'239 return job;
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.240 }
Merge branch 'worktree-agent-ab3c375781462035f'241 case "UserPromptSubmit": {
242 const prompt = typeof event.prompt === "string" ? event.prompt.replace(/^/, "") : "";
243 // A turn Claude Code starts itself for a background task carries that
244 // task's output, which is never sent: it is not a prompt of yours.
245 if (!prompt.trim() || /^\s*<task-notification>/.test(prompt)) return null;
246 if (env.G1T_RECORD_PROMPTS !== "0") job.steps.push({ args: ["session", "log", "--kind", "prompt", "--", clip(prompt, PROMPT_LIMIT)] });
247 // A skill typed as /plugin:name is a skill used (its name), whether or not prompts are sent.
248 const skill = typedSkill(prompt);
249 if (skill) job.steps.push(skillStep(skill));
250 return job.steps.length ? job : null;
251 }
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.252 case "SubagentStart":
253 if (!job.agent) return null;
254 job.agent.start = true;
255 return job;
256 case "SubagentStop": {
257 if (!job.agent) return null;
258 const args = ["session", "end", "--reason", "done"];
259 const result = typeof event.last_assistant_message === "string" ? event.last_assistant_message.trim() : "";
Merge branch 'worktree-agent-ab3c375781462035f'260 // --flag=VALUE throughout for text: one that starts with a dash ("- Found …") is still a value.
261 if (result) args.push(`--result=${clip(result, RESULT_LIMIT)}`);
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.262 job.steps.push({ args });
263 return job;
264 }
Merge branch 'worktree-agent-ad52ad80841a37960'265 case "PostToolUse": {
266 if (!event.tool_name) return null;
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.267 const tool = event.tool_name;
Merge branch 'worktree-agent-ad52ad80841a37960'268 const input = event.tool_input ?? {};
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.269 if (tool === "Skill") {
270 const skill = typeof input.skill === "string" ? input.skill.trim() : "";
271 if (!skill) return null;
272 // Its name, nothing more.
Merge branch 'worktree-agent-ab3c375781462035f'273 job.steps.push(skillStep(skill));
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.274 return job;
275 }
276 job.steps.push({ args: ["session", "log", "--kind", "tool_call", "--", summarizeTool(tool, input, cwd)] });
277 const touch = [];
278 if (WRITERS.has(tool)) {
279 const file = repoPath(input.file_path ?? input.notebook_path, cwd);
Merge branch 'worktree-agent-ab3c375781462035f'280 if (file) touch.push(`--file=${file}`);
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.281 }
282 if (AGENT_TOOLS.has(tool) && !job.agent) {
283 const response = event.tool_response ?? {};
284 const agentId = response.agentId ?? response.agent_id;
285 if (agentId) {
286 job.subagent = {
287 id: String(agentId),
288 type: input.subagent_type || null,
289 task: input.description ? clip(input.description, TASK_LIMIT) : null,
290 prompt: env.G1T_RECORD_PROMPTS === "0" || !input.prompt ? null : clip(input.prompt, AGENT_PROMPT_LIMIT),
291 model: response.resolvedModel || null,
292 result: response.status === "completed" ? clip(agentResult(response), RESULT_LIMIT) || null : null,
293 };
294 }
295 }
Merge branch 'worktree-agent-ab3c375781462035f'296 if (SHELL_TOOLS.has(tool)) {
Merge branch 'worktree-agent-ad52ad80841a37960'297 const issue = issueFromCommand(input.command);
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.298 if (issue) {
299 job.issue = issue;
300 touch.push("--issue", String(issue));
301 }
302 const branch = branchFromCommand(input.command);
303 if (branch) touch.push("--branch", branch);
304 if (isGitCommit(input.command)) job.steps.push({ touchHead: true });
Merge branch 'worktree-agent-ad52ad80841a37960'305 if (isGitPush(input.command)) job.steps.push({ headCommit: true });
306 }
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.307 const claimed = g1tTool(tool) === "issue" && input.action === "claim" && Number(input.number) > 0 ? Number(input.number) : null;
308 if (claimed) {
309 job.issue = claimed;
310 touch.push("--issue", String(claimed));
311 }
312 if (touch.length) job.steps.push({ args: ["session", "touch", ...touch] });
Merge branch 'worktree-agent-ad52ad80841a37960'313 return job;
314 }
315 case "Stop":
316 job.steps.push({ args: ["session", "heartbeat"] });
317 return job;
318 case "SessionEnd":
319 job.steps.push({ args: ["session", "end", "--reason", String(event.reason || "other")] });
320 job.end = true;
321 return job;
322 default:
323 return null;
324 }
325}
326
327// --- the machine -------------------------------------------------------
328
329function git(cwd, args) {
330 const out = spawnSync("git", args, { cwd, encoding: "utf8", windowsHide: true, timeout: 3000 });
331 return out.status === 0 ? out.stdout.trim() : null;
332}
333
334/** The g1t CLI on PATH, without running it. G1T_BIN names another. */
335export function findCli(env = process.env) {
336 if (env.G1T_BIN) return existsSync(env.G1T_BIN) ? env.G1T_BIN : null;
337 const names = process.platform === "win32" ? ["g1t.exe", "g1t"] : ["g1t"];
338 for (const dir of (env.PATH ?? env.Path ?? "").split(delimiter)) {
339 if (!dir) continue;
340 for (const name of names) {
341 const candidate = join(dir.replace(/^"|"$/g, ""), name);
342 try {
343 if (statSync(candidate).isFile()) return candidate;
344 } catch {}
345 }
346 }
347 return null;
348}
349
350/** Runs the CLI. A .js or .mjs G1T_BIN (the tests' stand-in) runs under Node. */
351function cli(bin, args, options) {
352 const script = /\.m?js$/.test(bin);
353 return spawnSync(script ? process.execPath : bin, script ? [bin, ...args] : args, {
354 encoding: "utf8",
355 windowsHide: true,
356 ...options,
357 });
358}
359
360/** Where the plugin keeps its state: Claude Code's data folder for it. */
361export function dataDir(env = process.env) {
362 return env.CLAUDE_PLUGIN_DATA || join(homedir(), ".g1t", "claude-code");
363}
364
365function readJson(path, fallback) {
366 try {
367 return JSON.parse(readFileSync(path, "utf8"));
368 } catch {
369 return fallback;
370 }
371}
372
373function write(path, text) {
374 try {
375 mkdirSync(dirname(path), { recursive: true });
376 writeFileSync(path, text);
377 } catch {}
378}
379
380const readText = (path) => {
381 try {
382 return readFileSync(path, "utf8").trim() || null;
383 } catch {
384 return null;
385 }
386};
387
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.388const safeName = (text) => String(text).replace(/[^A-Za-z0-9_-]/g, "_");
389
390/**
391 * This session's files: its marker (it is in a g1t working copy), its g1t
392 * session id, its issue, the Agent calls waiting for their subagents, and
393 * each subagent's g1t session id.
394 */
Merge branch 'worktree-agent-ad52ad80841a37960'395export function sessionFiles(sid, env = process.env) {
396 const dir = join(dataDir(env), "sessions");
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.397 const safe = safeName(sid);
398 return {
399 dir,
400 prefix: `${safe}.`,
401 marker: join(dir, `${safe}.json`),
402 id: join(dir, `${safe}.id`),
403 issue: join(dir, `${safe}.issue`),
Merge branch 'worktree-agent-ab3c375781462035f'404 turn: join(dir, `${safe}.turn`),
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.405 pending: join(dir, `${safe}.pending.json`),
406 agent: (agentId) => join(dir, `${safe}.agent.${safeName(String(agentId).replace(/^agent-/, ""))}.id`),
407 };
Merge branch 'worktree-agent-ad52ad80841a37960'408}
409
410/** Forgets sessions untouched for a week (ones that never saw SessionEnd). */
411function prune(dir) {
412 try {
413 const cutoff = Date.now() - 7 * 24 * 3600 * 1000;
414 for (const name of readdirSync(dir)) {
415 const path = join(dir, name);
416 if (statSync(path).mtimeMs < cutoff) rmSync(path, { force: true });
417 }
418 } catch {}
419}
420
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.421// --- Agent calls and their subagents -----------------------------------
422
423/** Keeps an Agent call's task until its subagent starts (PreToolUse). */
424export function rememberTask(files, input) {
425 const pending = readJson(files.pending, []);
426 pending.push({
427 type: input?.subagent_type || null,
428 task: input?.description ? clip(input.description, TASK_LIMIT) : null,
429 prompt: input?.prompt ? clip(input.prompt, AGENT_PROMPT_LIMIT) : null,
430 at: Date.now(),
431 });
432 // An Agent call refused or never started is forgotten after an hour.
433 write(files.pending, JSON.stringify(pending.filter((p) => Date.now() - p.at < 3600 * 1000).slice(-50)));
434}
435
436/** The task of the oldest Agent call of `type` still waiting for its subagent, taken. */
437export function takeTask(files, type) {
438 const pending = readJson(files.pending, []);
439 const at = pending.findIndex((p) => !type || !p.type || p.type === type);
440 if (at < 0) return null;
441 const [task] = pending.splice(at, 1);
442 write(files.pending, JSON.stringify(pending));
443 return task;
444}
445
Merge branch 'worktree-agent-ad52ad80841a37960'446// --- saying so once ----------------------------------------------------
447
Merge branch 'worktree-agent-ab3c375781462035f'448const NOT_RECORDED = "g1t: this session is not being recorded on g1t, because";
449
Merge branch 'worktree-agent-ad52ad80841a37960'450const MESSAGES = {
Merge branch 'worktree-agent-ab3c375781462035f'451 missing: () => `${NOT_RECORDED} the g1t CLI is not installed. Install it (https://docs.g1t.sh/guides/cli/) and run \`g1t auth login\`. This is said once.`,
452 signed_out: () => `${NOT_RECORDED} the g1t CLI is not signed in. Run \`g1t auth login\`. This is said once.`,
453 outdated: (version) =>
454 `${NOT_RECORDED} the g1t CLI is ${version}, and the plugin needs ${MIN_CLI} or later. Update it by running the installer again: ${INSTALL}. This is said once.`,
455 refused: (command) =>
456 `${NOT_RECORDED} the g1t CLI refused \`g1t ${command}\`: it is older than the plugin, which needs ${MIN_CLI} or later. Update it by running the installer again: ${INSTALL}. This is said once.`,
457 old_node: (version) =>
458 `${NOT_RECORDED} its hooks need Node ${MIN_NODE} or later, and \`node\` is ${version}. Install a current Node (https://nodejs.org) and start Claude Code again. This is said once.`,
Merge branch 'worktree-agent-ad52ad80841a37960'459};
460
Merge branch 'worktree-agent-ab3c375781462035f'461/** Remembers a problem (missing, signed_out, outdated, refused, old_node) and what it was about, or that there is none (null). */
462export function setProblem(env, problem, detail = null) {
Merge branch 'worktree-agent-ad52ad80841a37960'463 const path = join(dataDir(env), "notice.json");
464 const notice = readJson(path, {});
Merge branch 'worktree-agent-ab3c375781462035f'465 if ((notice.problem ?? null) === problem && (notice.detail ?? null) === detail) return;
466 write(path, JSON.stringify({ problem, detail, shown: false }));
Merge branch 'worktree-agent-ad52ad80841a37960'467}
468
469/** The message for a problem not shown yet, marking it shown. */
470export function takeNotice(env) {
471 const path = join(dataDir(env), "notice.json");
472 const notice = readJson(path, {});
473 if (!notice.problem || notice.shown || !MESSAGES[notice.problem]) return null;
474 write(path, JSON.stringify({ ...notice, shown: true }));
Merge branch 'worktree-agent-ab3c375781462035f'475 return MESSAGES[notice.problem](notice.detail);
Merge branch 'worktree-agent-ad52ad80841a37960'476}
477
Merge branch 'worktree-agent-ab3c375781462035f'478/** Whether version a (1.2.3) is older than b. */
479export function older(a, b) {
480 const pa = String(a).split(".").map(Number);
481 const pb = String(b).split(".").map(Number);
482 for (let i = 0; i < 3; i++) if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) < (pb[i] || 0);
483 return false;
484}
485
486/**
487 * The CLI's version, from `g1t --version`, asked once for each build of
488 * it: kept with the file's size and time, so updating it asks again.
489 * Null when it cannot be told.
490 */
491export function cliVersion(bin, env = process.env) {
492 let stamp;
493 try {
494 const stat = statSync(bin);
495 stamp = `${bin}|${stat.size}|${stat.mtimeMs}`;
496 } catch {
497 return null;
498 }
499 const path = join(dataDir(env), "cli.json");
500 const kept = readJson(path, {});
501 if (kept.stamp === stamp) return kept.version ?? null;
502 const out = cli(bin, ["--version"], { timeout: 5000, env });
503 const version = out.status === 0 ? (/\b(\d+\.\d+\.\d+)\b/.exec(out.stdout ?? "")?.[1] ?? null) : null;
504 write(path, JSON.stringify({ stamp, version }));
505 return version;
506}
507
Merge branch 'worktree-agent-ad52ad80841a37960'508/** Whether a working copy's remotes are on g1t (g1t.sh, or G1T_HOST's host). */
509export function isG1tRemote(urls, env = process.env) {
510 if (env.G1T_REPO) return true;
511 const hosts = ["g1t.sh", env.G1T_HOST].filter(Boolean).map((h) => h.toLowerCase());
512 return urls.some((url) => hosts.some((host) => url.toLowerCase().includes(host)));
513}
514
515// --- the background half -----------------------------------------------
516
517/** Hands a job to a detached copy of this script and returns at once. */
518function background(job, env = process.env) {
519 const payload = Buffer.from(JSON.stringify(job)).toString("base64url");
520 try {
521 const child = spawn(process.execPath, [SELF, "--worker", payload], {
522 cwd: job.cwd,
523 detached: true,
524 stdio: "ignore",
525 windowsHide: true,
526 env,
527 });
528 child.on("error", () => {});
529 child.unref();
530 } catch {}
531}
532
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.533/** The session id in a `g1t session start --json` answer. */
534function startedId(out) {
535 try {
536 return JSON.parse(out.stdout).id ?? null;
537 } catch {
538 return /\bses_[A-Za-z0-9]+/.exec(out.stdout ?? "")?.[0] ?? null;
539 }
540}
541
542/**
543 * Starts a subagent's session under `parentId` (again, when it was: the
544 * same session comes back, taking what is newly said), and keeps its id.
545 */
546function startSubagent(bin, job, files, agent, parentId, opts) {
547 const args = [
548 "session",
549 "start",
550 "--harness",
551 "claude_code",
552 "--harness-session-id",
553 subagentHarnessId(job.sid, agent.id),
554 "--parent",
555 parentId,
556 "--no-remember",
557 ];
Merge branch 'worktree-agent-ab3c375781462035f'558 if (agent.type) args.push(`--agent-type=${agent.type}`);
559 if (agent.task) args.push(`--task=${agent.task}`);
560 if (agent.prompt) args.push(`--prompt=${agent.prompt}`);
561 if (agent.model) args.push(`--model=${agent.model}`);
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.562 args.push("--json");
563 const out = cli(bin, args, { ...opts, env: { ...opts.env, G1T_SESSION: parentId } });
564 if (out.status === EXIT_AUTH) return { auth: true };
Merge branch 'worktree-agent-ab3c375781462035f'565 if (out.status === EXIT_USAGE) return { refused: "session start" };
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.566 const id = out.status === 0 ? startedId(out) : null;
567 if (id) write(files.agent(agent.id), id);
568 return { id };
569}
570
Merge branch 'worktree-agent-ad52ad80841a37960'571/** Starts the session if it needs to, then runs the job's steps in order. */
572export function work(job, env = process.env) {
573 const bin = findCli(env);
574 if (!bin) return setProblem(env, "missing");
Merge branch 'worktree-agent-ab3c375781462035f'575 // An older CLI refuses some of what the hooks send: nothing is sent to it.
576 const version = cliVersion(bin, env);
577 if (version && older(version, MIN_CLI)) return setProblem(env, "outdated", version);
Merge branch 'worktree-agent-ad52ad80841a37960'578 const files = sessionFiles(job.sid, env);
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.579 if (job.issue && !job.agent) write(files.issue, String(job.issue));
Merge branch 'worktree-agent-ad52ad80841a37960'580 const opts = { cwd: job.cwd, timeout: 20000, env: { ...env } };
581 let id = readText(files.id);
582 if (job.start || !id) {
583 const issue = job.issue ?? readText(files.issue);
584 const args = ["session", "start", "--harness", "claude_code", "--harness-session-id", job.sid];
585 if (issue) args.push("--issue", String(issue));
586 args.push("--json");
587 const out = cli(bin, args, opts);
588 if (out.status === EXIT_AUTH) return setProblem(env, "signed_out");
Merge branch 'worktree-agent-ab3c375781462035f'589 if (out.status === EXIT_USAGE) return setProblem(env, "refused", "session start");
Merge branch 'worktree-agent-ad52ad80841a37960'590 if (out.status !== 0) return;
591 setProblem(env, null);
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.592 id = startedId(out);
Merge branch 'worktree-agent-ad52ad80841a37960'593 if (!id) return;
594 write(files.id, id);
Merge branch 'worktree-agent-ab3c375781462035f'595 if (job.envFile && /^[A-Za-z0-9_]+$/.test(id)) {
596 // Claude's own g1t commands act in this session, not another one in
597 // the same working copy. A plain assignment, which Claude Code gives
598 // PowerShell commands too; it reads the file at the session's first
599 // command, so a command before this (rare: it is the first second)
600 // uses the working copy's remembered session.
601 try {
602 writeFileSync(job.envFile, `export G1T_SESSION=${id}\n`, { flag: "a" });
603 } catch {}
604 }
Merge branch 'worktree-agent-ad52ad80841a37960'605 }
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.606 // A subagent's events go to its own session, started first if need be.
607 let session = id;
608 if (job.agent) {
609 let child = readText(files.agent(job.agent.id));
610 if (job.agent.start || !child) {
611 const started = startSubagent(bin, job, files, job.agent, id, opts);
612 if (started.auth) return setProblem(env, "signed_out");
Merge branch 'worktree-agent-ab3c375781462035f'613 if (started.refused) return setProblem(env, "refused", started.refused);
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.614 child = started.id ?? child;
615 }
616 if (!child) return;
617 session = child;
618 }
619 opts.env.G1T_SESSION = session;
Merge branch 'worktree-agent-ad52ad80841a37960'620 for (const step of job.steps) {
621 if (step.args) {
Merge branch 'worktree-agent-ab3c375781462035f'622 const args = step.key ? [...step.args.slice(0, -2), `--key=${step.key}`, ...step.args.slice(-2)] : step.args;
623 const out = cli(bin, args, opts);
Merge branch 'worktree-agent-ad52ad80841a37960'624 if (out.status === EXIT_AUTH) return setProblem(env, "signed_out");
Merge branch 'worktree-agent-ab3c375781462035f'625 if (out.status === EXIT_USAGE) return setProblem(env, "refused", step.args.slice(0, 2).join(" "));
Merge branch 'worktree-agent-ad52ad80841a37960'626 } else if (step.headCommit) {
627 // Only once the push reached the remote: HEAD is on a remote branch.
628 const head = git(job.cwd, ["rev-parse", "HEAD"]);
629 const remote = git(job.cwd, ["branch", "-r", "--contains", "HEAD"]);
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.630 if (head && remote) {
631 cli(bin, ["api", "update_session", "-f", `id=${session}`, "-f", `head_commit=${head}`, "--silent"], opts);
632 const branch = git(job.cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
633 const touch = ["session", "touch", "--commit", head];
634 if (branch && branch !== "HEAD") touch.push("--branch", branch);
635 cli(bin, touch, opts);
636 }
637 } else if (step.touchHead) {
638 const head = git(job.cwd, ["rev-parse", "HEAD"]);
639 if (head) cli(bin, ["session", "touch", "--commit", head], opts);
Merge branch 'worktree-agent-ad52ad80841a37960'640 }
641 }
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.642 // An Agent call answered: its subagent has its task, and once it
643 // finished, what it reported.
644 if (job.subagent) {
645 const started = startSubagent(bin, job, files, job.subagent, id, opts);
646 if (started.id && job.subagent.result) {
Merge branch 'worktree-agent-ab3c375781462035f'647 cli(bin, ["session", "end", "--reason", "done", `--result=${job.subagent.result}`], { ...opts, env: { ...opts.env, G1T_SESSION: started.id } });
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.648 }
649 }
650 if (job.end) {
651 try {
652 for (const name of readdirSync(files.dir)) if (name.startsWith(files.prefix)) rmSync(join(files.dir, name), { force: true });
653 } catch {}
654 }
Merge branch 'worktree-agent-ad52ad80841a37960'655}
656
657// --- the hooks ---------------------------------------------------------
658
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.659/** What every session is told about trailers, so commits find their way back. */
660const TRAILER = "Add the trailer `G1t-Session: $G1T_SESSION` to the commits you make (git commit --trailer), so g1t ties them to this session.";
661
Merge branch 'worktree-agent-ad52ad80841a37960'662/**
663 * SessionStart: marks the session as one in a g1t working copy, and
664 * returns what to print: context for Claude, and a notice for you, once.
665 * Null outside a g1t working copy, where nothing is recorded.
666 */
667export function sessionStart(event, env = process.env, bin = findCli(env)) {
668 const cwd = event.cwd || process.cwd();
669 const gitDir = git(cwd, ["rev-parse", "--absolute-git-dir"]);
670 if (!gitDir) return null;
671 // The branch, from HEAD itself: also on a branch with no commits yet.
672 const branch = /^ref: refs\/heads\/(.+)$/m.exec(readText(join(gitDir, "HEAD")) ?? "")?.[1] ?? null;
673 const urls = (git(cwd, ["config", "--get-regexp", "^remote\\..*\\.url$"]) ?? "").split(/\r?\n/);
674 if (!isG1tRemote(urls, env)) return null;
675
676 const files = sessionFiles(event.session_id, env);
677 prune(files.dir);
678 write(files.marker, JSON.stringify({ cwd, git_dir: gitDir }));
679
680 const output = {};
681 if (!bin) setProblem(env, "missing");
682 const notice = takeNotice(env);
683 if (notice) output.systemMessage = notice;
684 if (!bin) return output;
685
686 const remembered = Number(readText(files.issue)) || null;
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.687 const handedOff = Number(env.G1T_ISSUE) || null;
688 const issue = remembered ?? handedOff ?? issueFromBranch(branch);
Merge branch 'worktree-agent-ad52ad80841a37960'689 let context;
690 if (issue) {
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.691 const where = remembered ? "this session's claim" : handedOff ? "the work handed to it" : `the branch ${branch}`;
Merge branch 'worktree-agent-ad52ad80841a37960'692 const out = cli(bin, ["work", "context", String(issue)], { cwd, timeout: CONTEXT_WAIT_MS, env });
693 if (out.status === 0 && out.stdout.trim()) {
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.694 context = `g1t: this session is working on issue #${issue} (from ${where}). Its work context on g1t:\n\n${clip(out.stdout.trim(), CONTEXT_LIMIT)}\n\nIf it is not claimed by this session yet, claim it with \`g1t work claim ${issue}\` before you start. ${TRAILER}`;
Merge branch 'worktree-agent-ad52ad80841a37960'695 } else if (out.status === EXIT_AUTH) {
696 setProblem(env, "signed_out");
697 const now = takeNotice(env);
698 if (now) output.systemMessage = now;
699 } else {
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.700 context = `g1t: ${where} points at issue #${issue}. Load what is known about it with \`g1t work context ${issue}\`, and claim it with \`g1t work claim ${issue}\` before you start. ${TRAILER}`;
Merge branch 'worktree-agent-ad52ad80841a37960'701 }
702 } else {
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.703 context = `g1t: this is a g1t repository. Before starting on an issue, load it with \`g1t work context N\` and claim it with \`g1t work claim N\`, so the people and agents also working here know. ${TRAILER}`;
Merge branch 'worktree-agent-ad52ad80841a37960'704 }
705 if (context) output.hookSpecificOutput = { hookEventName: "SessionStart", additionalContext: context };
706 return output;
707}
708
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.709/** What a subagent is told as it starts: it is a session of its own, beside its siblings. */
710export function subagentContext(issue) {
711 const on = issue ? ` on issue #${issue}` : "";
712 const see = issue
713 ? `Before you change anything, run \`g1t work context ${issue}\` and \`g1t session list --issue ${issue} --subagents\`: other subagents of the same session may be on it, and you see what each touches.`
714 : "Before you change anything, run `g1t session list --subagents` to see the other subagents at work here.";
715 return `g1t: you are a subagent of a session recorded on g1t${on}, and g1t records you as a session of your own under it: the g1t commands and g1t tools you use act as that session. ${see} Claim only the issue you take on (\`g1t work claim N --note "what you are doing"\`), and if g1t reports an overlap with another session, settle it before going on. ${TRAILER}`;
716}
717
718/**
719 * PreToolUse: an Agent call's task is kept for its subagent; inside a
720 * subagent, its g1t commands and g1t claims act as its own session.
721 * Returns what to print, or null.
722 */
723export function preToolUse(event, env = process.env) {
724 const files = sessionFiles(event.session_id, env);
725 const tool = event.tool_name;
726 const input = event.tool_input ?? {};
727 if (AGENT_TOOLS.has(tool) && !event.agent_id) {
728 rememberTask(files, input);
729 return null;
730 }
731 const session = event.agent_id ? readText(files.agent(event.agent_id)) : readText(files.id);
732 if (!session) return null;
733 let updated = null;
734 const command = typeof input.command === "string" ? input.command : "";
Merge branch 'worktree-agent-ab3c375781462035f'735 const namesG1t = /\bg1t(?:\.exe)?\s/.test(command) || /G1T_SESSION\b/.test(command);
736 const setsIt = /G1T_SESSION\s*=/.test(command);
737 // A subagent's g1t commands act as its own session. Only these commands
738 // are changed. Claude Code checks its permission rules against the
739 // changed command; no permission decision is made here.
740 if (SHELL_TOOLS.has(tool) && event.agent_id && namesG1t && !setsIt && /^[A-Za-z0-9_]+$/.test(session)) {
741 updated = { ...input, command: `${sessionPrefix(tool, session)}${input.command}` };
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.742 }
743 const g1t = g1tTool(tool);
744 if (g1t === "issue" && ["claim", "release", "handoff"].includes(input.action) && !input.session_id) {
745 updated = { ...input, session_id: session };
746 }
747 return updated ? { hookSpecificOutput: { hookEventName: "PreToolUse", updatedInput: updated } } : null;
748}
749
Merge branch 'worktree-agent-ad52ad80841a37960'750/** Runs one hook: what it prints, and the job it hands to the background. */
751export function hook(event, env = process.env) {
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.752 if (env.G1T_RECORD === "0") return { output: null, job: null };
753 const name = event?.hook_event_name;
754 if (name === "PreToolUse") {
755 if (!event.session_id || !existsSync(sessionFiles(event.session_id, env).marker)) return { output: null, job: null };
756 return { output: preToolUse(event, env), job: null };
757 }
Merge branch 'worktree-agent-ab3c375781462035f'758 // Each prompt is a turn; a skill is recorded once in a turn, however it was run.
759 const files = event?.session_id ? sessionFiles(event.session_id, env) : null;
760 let turn = files ? Number(readText(files.turn)) || 0 : 0;
761 if (name === "UserPromptSubmit" && files && existsSync(files.marker)) write(files.turn, String(++turn));
Merge branch 'worktree-agent-ad52ad80841a37960'762 const job = plan(event, env);
763 if (!job) return { output: null, job: null };
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.764 if (name === "SessionStart") {
Merge branch 'worktree-agent-ad52ad80841a37960'765 const bin = findCli(env);
766 const output = sessionStart(event, env, bin);
767 if (!output) return { output: null, job: null };
Merge branch 'worktree-agent-ab3c375781462035f'768 if (env.CLAUDE_ENV_FILE) job.envFile = env.CLAUDE_ENV_FILE;
Merge branch 'worktree-agent-ad52ad80841a37960'769 return { output: Object.keys(output).length ? output : null, job: bin ? job : null };
770 }
771 // Only sessions SessionStart found in a g1t working copy are recorded.
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.772 if (!existsSync(files.marker) || !findCli(env)) return { output: null, job: null };
Merge branch 'worktree-agent-ab3c375781462035f'773 for (const step of job.steps) if (step.skill) step.key = `skill:${safeName(step.skill)}:${turn}`;
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.774 if (name === "SubagentStart") {
775 const task = takeTask(files, job.agent.type);
776 if (task) Object.assign(job.agent, { task: task.task, prompt: env.G1T_RECORD_PROMPTS === "0" ? null : task.prompt });
777 const issue = Number(readText(files.issue)) || null;
778 return { output: { hookSpecificOutput: { hookEventName: "SubagentStart", additionalContext: subagentContext(issue) } }, job };
779 }
780 const notice = name === "UserPromptSubmit" ? takeNotice(env) : null;
Merge branch 'worktree-agent-ad52ad80841a37960'781 return { output: notice ? { systemMessage: notice } : null, job };
782}
783
Merge branch 'worktree-agent-ab3c375781462035f'784/**
785 * On a Node older than MIN_NODE, nothing is recorded: what to print (the
786 * notice, once, at a session's start or a prompt), or null. Undefined on a
787 * current Node.
788 */
789export function oldNode(event, env = process.env, version = process.versions.node) {
790 if (Number(String(version).split(".")[0]) >= MIN_NODE) return undefined;
791 if (env.G1T_RECORD === "0" || !["SessionStart", "UserPromptSubmit"].includes(event?.hook_event_name)) return null;
792 setProblem(env, "old_node", `v${version}`);
793 const notice = takeNotice(env);
794 return notice ? { systemMessage: notice } : null;
795}
796
Merge branch 'worktree-agent-ad52ad80841a37960'797async function main() {
798 if (process.argv[2] === "--worker") {
799 work(JSON.parse(Buffer.from(process.argv[3], "base64url").toString("utf8")));
800 return;
801 }
802 let raw = "";
803 for await (const chunk of process.stdin) raw += chunk;
Merge branch 'worktree-agent-ab3c375781462035f'804 const event = JSON.parse(raw || "{}");
805 const old = oldNode(event);
806 if (old !== undefined) {
807 if (old) process.stdout.write(JSON.stringify(old));
808 return;
809 }
810 const { output, job } = hook(event);
Merge branch 'worktree-agent-ad52ad80841a37960'811 if (output) process.stdout.write(JSON.stringify(output));
812 if (job) background(job);
813}
814
815if (process.argv[1] && resolve(process.argv[1]) === SELF) {
816 main()
817 .catch(() => {})
818 .finally(() => process.exit(0));
819}

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