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-a6ec0a89d7fe7a9ae' | 1 | //! Coordinating work over REST and MCP: sessions of people's own harnesses |
| 2 | //! (the MCP `session` tool), and claims and an issue's whole context | |
| 3 | //! (actions of the `issue` tool). | |
| 4 | //! | |
| 5 | //! The work service keeps them and decides who may see and change them | |
| 6 | //! (`g1t_contracts::work::coordination`): anyone who can read the | |
| 7 | //! repository reads them; taking part needs a confirmed email address and a | |
| 8 | //! role that may open issues and pull requests; only the person whose | |
| 9 | //! token started a session, or made a claim, changes it. | |
| 10 | ||
| 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. | 11 | use g1t_contracts::agent_sessions::{AgentMode, StartAgentSessionArgs}; |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 12 | use g1t_contracts::work::*; |
| 13 | use g1t_contracts::{FailureCode, Outcome, Viewer}; | |
| 14 | use serde_json::{Value, json}; | |
| 15 | use worker::Result; | |
| 16 | ||
| 17 | use crate::operations::{Services, repo_path}; | |
| 18 | ||
| 19 | /// One operation on sessions, claims or an issue's context. | |
| 20 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 21 | pub enum CoordinationOp { | |
| 22 | StartSession, | |
| 23 | UpdateSession, | |
| 24 | EndSession, | |
| 25 | GetSession, | |
| 26 | ListSessions, | |
| 27 | RecordSessionEntries, | |
| 28 | ClaimIssue, | |
| 29 | ReleaseClaim, | |
| 30 | HandOffIssue, | |
| 31 | ListClaims, | |
| 32 | GetWork, | |
| 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. | 33 | StartAgentSession, |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 34 | } |
| 35 | ||
| 36 | impl CoordinationOp { | |
| 37 | /// Every one: `Op::ALL` lists each as `Op::Coordination(…)`, which a | |
| 38 | /// test checks against this. | |
| 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. | 39 | pub const ALL: [CoordinationOp; 12] = [ |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 40 | CoordinationOp::StartSession, |
| 41 | CoordinationOp::UpdateSession, | |
| 42 | CoordinationOp::EndSession, | |
| 43 | CoordinationOp::GetSession, | |
| 44 | CoordinationOp::ListSessions, | |
| 45 | CoordinationOp::RecordSessionEntries, | |
| 46 | CoordinationOp::ClaimIssue, | |
| 47 | CoordinationOp::ReleaseClaim, | |
| 48 | CoordinationOp::HandOffIssue, | |
| 49 | CoordinationOp::ListClaims, | |
| 50 | CoordinationOp::GetWork, | |
| 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. | 51 | CoordinationOp::StartAgentSession, |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 52 | ]; |
| 53 | ||
| 54 | pub fn name(self) -> &'static str { | |
| 55 | match self { | |
| 56 | CoordinationOp::StartSession => "start_session", | |
| 57 | CoordinationOp::UpdateSession => "update_session", | |
| 58 | CoordinationOp::EndSession => "end_session", | |
| 59 | CoordinationOp::GetSession => "get_session", | |
| 60 | CoordinationOp::ListSessions => "list_sessions", | |
| 61 | CoordinationOp::RecordSessionEntries => "record_session_entries", | |
| 62 | CoordinationOp::ClaimIssue => "claim_issue", | |
| 63 | CoordinationOp::ReleaseClaim => "release_claim", | |
| 64 | CoordinationOp::HandOffIssue => "hand_off_issue", | |
| 65 | CoordinationOp::ListClaims => "list_claims", | |
| 66 | CoordinationOp::GetWork => "get_work", | |
| 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. | 67 | CoordinationOp::StartAgentSession => "start_agent_session", |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 68 | } |
| 69 | } | |
| 70 | ||
| 71 | /// For the API reference. | |
| 72 | pub fn title(self) -> &'static str { | |
| 73 | match self { | |
| 74 | CoordinationOp::StartSession => "Start a session", | |
| 75 | CoordinationOp::UpdateSession => "Update a session", | |
| 76 | CoordinationOp::EndSession => "End a session", | |
| 77 | CoordinationOp::GetSession => "Get a session", | |
| 78 | CoordinationOp::ListSessions => "List sessions", | |
| 79 | CoordinationOp::RecordSessionEntries => "Record entries in a session", | |
| 80 | CoordinationOp::ClaimIssue => "Claim an issue", | |
| 81 | CoordinationOp::ReleaseClaim => "Release a claim", | |
| 82 | CoordinationOp::HandOffIssue => "Hand off an issue", | |
| 83 | CoordinationOp::ListClaims => "List claims", | |
| 84 | CoordinationOp::GetWork => "Get an issue's work context", | |
| 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. | 85 | CoordinationOp::StartAgentSession => "Start an agent session on an issue", |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 86 | } |
| 87 | } | |
| 88 | ||
| 89 | /// Whether it only reads. | |
| 90 | pub fn reads(self) -> bool { | |
| 91 | matches!( | |
| 92 | self, | |
| 93 | CoordinationOp::GetSession | CoordinationOp::ListSessions | CoordinationOp::ListClaims | CoordinationOp::GetWork | |
| 94 | ) | |
| 95 | } | |
| 96 | ||
| 97 | pub fn description(self) -> &'static str { | |
| 98 | match self { | |
| 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. | 99 | CoordinationOp::StartSession => "Start a session: one run of your own harness (a Claude Code session, a Codex run) on a repository, so others can see who is working on what. `harness` is claude_code, codex, cursor, copilot or other. Give `harness_session_id`, the harness's own id for the session, and starting again with it returns the session already started (refreshing the issue, pull request, branch, title and head given) instead of a new one, so a hook that fires twice is harmless. `issue` and `pull` attach it to an issue and a pull request of the repository; a pull request for an issue attaches that issue too. A subagent is a session of its own: give `parent_id` (one of your sessions here), its `agent_type`, the `task` and `prompt` its parent gave it, and as `harness_session_id` the parent's harness id, `/agent/` and the subagent's id; it starts on the parent's issue, pull request and branch unless it says otherwise, sees and is seen by its siblings in get_work and list_sessions, and once ended stays ended when started again. Returns the session (`id` `ses_…`, `state` active, idle or ended). A new session, or one started again after it ended, is a session.started event. Needs a confirmed email address and a role that may open issues.", |
| 100 | CoordinationOp::UpdateSession => "Update one of your sessions: attach it to an issue or pull request, or change its branch, head commit or title. Only the fields given change. An empty body is a heartbeat: every call keeps the session active and renews the claims made from it. A session reads as idle after 15 minutes without one and ends (`end_reason` timed_out) after 2 hours; a heartbeat brings a timed-out one back. Attaching a pull request moves the entries recorded so far into that pull request's session. A subagent's `task`, `prompt`, `model` and `result` (what it reported, up to 8,000 characters) can be set too; a subagent that ended takes them and stays ended. Returns the session; one ended with end_session is refused with `409`.", | |
| 101 | CoordinationOp::EndSession => "End one of your sessions, with an optional `reason` and `result` (what it reported when it finished: a subagent's final message, up to 8,000 characters, never a tool's output). A session.ended event. Ending one already ended returns it as it is, taking `result` if it had none. Its subagents still going end with it (`parent ended`). Its claims are not released: release them, hand the issue off, or let them expire.", | |
| 102 | CoordinationOp::GetSession => "A session and its entries, oldest first, numbered within the session (`seq`); `after` gives the entries after that number, 500 at a time. With them: `children`, its subagents and theirs in the order they started (each names its `parent_id`); `touched`, the files, commits, branches, pull requests and issues it and they touched; `links`, the issues and pull requests they worked on and how each was tied (`via`: attached, claim, branch, commit, trailer, opened or ref); `skills`, the skills they used by name; and for a subagent, `root`, the session at the top of its tree. Anyone who can read the repository may.", | |
| 103 | CoordinationOp::ListSessions => "A repository's sessions, most recently heard from first, up to 100: `state` active (heard from in the last 15 minutes), idle or ended; `issue` or `pull` for those on one, attached or tied to it any other way; `path` for those that touched a file (or any under a folder ending in `/`). Subagents are left out unless `subagents` is true, which lists each tree whole. Each says how many `subagents` it started. Anyone who can read the repository may.", | |
| 104 | CoordinationOp::RecordSessionEntries => "Record entries in one of your sessions: up to 200 at a time, each with `kind` (prompt, message, tool_call, tool_result, note, or skill for a skill it used, by name), `text` (up to 64,000 characters), and for tools `tool`. Give each a `key` and recording it again does nothing, so a retried hook records it once. `touched` says what it touched, up to 200: `{ kind, value }` with kind file (a path from the repository's root), commit, branch, pull (one it opened) or issue (one it named); each is kept once, ties the session to the pull requests and issues it names, and a file another active session touched too (outside its own parents and subagents, so siblings yes) is an overlap.detected event for both, once per pair. Entries of a session attached to a pull request show in that pull request's session too, where reviewers read why the change is what it is. Recording is a heartbeat. Returns how many were `recorded` and how many were `duplicates`.", | |
| Work with no issue yet is a session of its own, titled with what it is for, until it becomes a change: Claude Code's plugin titles each session from its first prompt that is not a slash command (its first line or sentence, without code blocks or markdown, up to 80 characters cut on a word), in the background, and only when the session has no title, so a title Claude, a person or an earlier prompt set is never replaced; g1t session attach --title names one by hand and replaces the prompt's. Nothing is titled with prompts kept back. | 105 | CoordinationOp::ClaimIssue => "Claim an open issue: say you are working on it. A claim is advisory, never a lock: anyone may claim an issue someone else has, and `others` lists the claims already on it, so you can coordinate or pick something else. It lasts `ttl_minutes` (30 unless you say, up to 240) and is renewed by claiming again or, when you give the `session_id` of one of your sessions, by that session's heartbeats. Returns your `claim`, the `others`, and `blocked_by`: each issue this one waits on, with its claims and pull requests. A new claim is a work.claimed event; one taking over a hand-off says which in `took_over`. For work that has no issue yet, leave out `number` and give `title` (and `body`): the issue is opened first (an issue.opened event), then claimed, the session named by `session_id` is attached to it, and `issue` is the issue opened. The session is checked first, so a claim it would refuse opens nothing.", |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 106 | CoordinationOp::ReleaseClaim => "Release your claim on an issue, with an optional `reason`: you stopped working on it. A work.released event. Claims also end when the issue closes, and expire when not renewed. Refused with `404` when you hold no active claim on it.", |
| 107 | CoordinationOp::HandOffIssue => "Hand an issue you claimed off: your claim ends as handed_off, with `note` saying where the work stands, for `to` (a username) or, without one, for anyone. A work.handoff_requested event, which whoever follows the issue hears; the next claim on it says it took the hand-off over. Refused with `404` when you hold no active claim on it.", | |
| 108 | CoordinationOp::ListClaims => "A repository's claims, newest first, up to 100: active ones unless `state` says released, expired or handed_off, and `issue` for one issue's. Anyone who can read the repository may.", | |
| 109 | CoordinationOp::GetWork => "Everything about an issue an agent needs before working on it, in one call: the `issue`, its `definition_of_done` (the items under its \"Definition of done\" heading) and the `required_checks` every pull request into the default branch must pass; active `claims`; `sessions` on it or its pull requests; its `pulls`, each with its status, required checks as they stand on its head, whether it merges cleanly, the other pull requests it overlaps and its place in the merge queue; `blocked_by` (each issue it waits on, with claims and pull requests) and `blocks` (the issues waiting on it); `decisions` recorded about it; `last_integration_failure`; and `recent`, its latest coordination events, newest first.", | |
| 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. | 110 | CoordinationOp::StartAgentSession => "Start work on an issue in a session of its own, on the person's own Claude: g1t runs no model and bills nothing for it. Give `issue`, or `title` and `body` to open a new issue first; `instruction` adds to what the session is asked; `mode` (plan, implement or review) says what kind of work it is. With the workspace's Anthropic connection (an owner adds it under Integrations), g1t starts a Claude Managed Agents session with that key: g1t's MCP server attached with a credential for you and this repository only that lasts the session and is kept in a vault on the Anthropic account (never in the prompt), g1t's skills (work-on-issue, coordinate, fix-integration) next to the account's own, and the session, its subagents (threads), what they touched and what they reported recorded here as a g1t session (`harness` claude_managed_agents). Returns `session`, its `session_url` on g1t, `provider_session_url`, and `prompt`. With `handoff` true, or without an Anthropic connection when `handoff` is not false, nothing is started: `command` is a Claude Code command line, and `prompt` what to paste, for someone with g1t's Claude Code plugin, whose session then attaches to the issue as it starts.", |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 111 | } |
| 112 | } | |
| 113 | ||
| 114 | pub fn input(self) -> Value { | |
| 115 | let repo = json!({ "type": "string", "description": "Repository as \"owner/name\", e.g. \"flagon-io/hello\"." }); | |
| 116 | let id = json!({ "type": "string", "description": "The session's id (ses_…), from start_session or list_sessions." }); | |
| 117 | let number = json!({ "type": "integer", "description": "The issue's number." }); | |
| 118 | let issue = json!({ "type": "integer", "description": "The issue it works on." }); | |
| 119 | let pull = json!({ "type": "integer", "description": "The pull request it works on." }); | |
| 120 | let fields = |more: Value| -> Value { | |
| 121 | let mut properties = json!({ | |
| 122 | "repo": repo, | |
| 123 | "issue": issue, | |
| 124 | "pull": pull, | |
| 125 | "branch": { "type": "string", "description": "The branch it works on." }, | |
| 126 | "title": { "type": "string", "description": "What it is doing, in a few words. Up to 200 characters." }, | |
| 127 | "head_commit": { "type": "string", "description": "The commit it is at, such as after a push." }, | |
| 128 | }); | |
| 129 | if let (Some(all), Value::Object(more)) = (properties.as_object_mut(), more) { | |
| 130 | all.extend(more); | |
| 131 | } | |
| 132 | properties | |
| 133 | }; | |
| 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. | 134 | let session_id = json!({ "type": "string", "description": "The session (ses_…) whose claim it is, when you have several on the issue, such as sibling subagents." }); |
| 135 | let subagent = json!({ | |
| 136 | "agent_type": { "type": "string", "description": "For a subagent: its kind, as the harness names it (Explore, security-reviewer)." }, | |
| 137 | "task": { "type": "string", "description": "For a subagent: the task it was given, in a few words. Up to 200 characters." }, | |
| 138 | "prompt": { "type": "string", "description": "For a subagent: the prompt its parent gave it. Kept up to 4,000 characters." }, | |
| 139 | "model": { "type": "string", "description": "The model it runs on, when known." }, | |
| 140 | }); | |
| 141 | let with = |base: Value, more: Value| -> Value { | |
| 142 | let mut base = base; | |
| 143 | if let (Some(all), Value::Object(more)) = (base.as_object_mut(), more) { | |
| 144 | all.extend(more); | |
| 145 | } | |
| 146 | base | |
| 147 | }; | |
| 148 | let result = json!({ "type": "string", "description": "What it reported when it finished: a subagent's final message. Kept up to 8,000 characters. Never a tool's output." }); | |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 149 | let (properties, required): (Value, &[&str]) = match self { |
| 150 | CoordinationOp::StartSession => ( | |
| 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. | 151 | with( |
| 152 | fields(json!({ | |
| 153 | "harness": { | |
| 154 | "type": "string", | |
| 155 | "enum": Harness::ALL.map(Harness::as_str), | |
| 156 | "description": "The harness the session runs in.", | |
| 157 | }, | |
| 158 | "harness_session_id": { | |
| 159 | "type": "string", | |
| 160 | "description": "The harness's own id for the session, such as Claude Code's session_id; for a subagent, the parent's, `/agent/` and the subagent's id. Starting again with it returns the same session.", | |
| 161 | }, | |
| 162 | "parent_id": { "type": "string", "description": "For a subagent: the session that started it (ses_…), one of yours on the repository." }, | |
| 163 | })), | |
| 164 | subagent.clone(), | |
| 165 | ), | |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 166 | &["repo", "harness"], |
| 167 | ), | |
| 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. | 168 | CoordinationOp::UpdateSession => (with(fields(json!({ "id": id, "result": result })), subagent.clone()), &["repo", "id"]), |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 169 | CoordinationOp::EndSession => ( |
| 170 | json!({ | |
| 171 | "repo": repo, | |
| 172 | "id": id, | |
| 173 | "reason": { "type": "string", "description": "Why it ended, in a few words. \"ended\" if left out." }, | |
| 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. | 174 | "result": result, |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 175 | }), |
| 176 | &["repo", "id"], | |
| 177 | ), | |
| 178 | CoordinationOp::GetSession => ( | |
| 179 | json!({ | |
| 180 | "repo": repo, | |
| 181 | "id": id, | |
| 182 | "after": { "type": "integer", "description": "Only entries after this sequence number, for the next page." }, | |
| 183 | }), | |
| 184 | &["repo", "id"], | |
| 185 | ), | |
| 186 | CoordinationOp::ListSessions => ( | |
| 187 | json!({ | |
| 188 | "repo": repo, | |
| 189 | "state": { "type": "string", "enum": ["active", "idle", "ended"], "description": "Only sessions in this state." }, | |
| 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. | 190 | "issue": { "type": "integer", "description": "Only sessions on this issue: attached, or tied to it any other way." }, |
| 191 | "pull": { "type": "integer", "description": "Only sessions on this pull request: attached, or tied to it any other way." }, | |
| 192 | "path": { "type": "string", "description": "Only sessions that touched this file, or any under it when it ends in `/`." }, | |
| 193 | "subagents": { "type": "boolean", "description": "List subagents too, each tree whole. Left out, only the sessions at the top." }, | |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 194 | }), |
| 195 | &["repo"], | |
| 196 | ), | |
| 197 | CoordinationOp::RecordSessionEntries => ( | |
| 198 | json!({ | |
| 199 | "repo": repo, | |
| 200 | "id": id, | |
| 201 | "entries": { | |
| 202 | "type": "array", | |
| 203 | "description": "Up to 200 entries, oldest first.", | |
| 204 | "items": { | |
| 205 | "type": "object", | |
| 206 | "properties": { | |
| 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. | 207 | "kind": { "type": "string", "enum": ["prompt", "message", "tool_call", "tool_result", "note", "skill"] }, |
| 208 | "text": { "type": "string", "description": "For a skill, its name only." }, | |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 209 | "tool": { "type": "string", "description": "For tool_call and tool_result: the tool's name." }, |
| 210 | "commit": { "type": "string", "description": "The commit it was recorded at; the session's head if left out." }, | |
| 211 | "key": { "type": "string", "description": "An idempotency key, unique in the session: an entry with a key recorded before is skipped." }, | |
| 212 | }, | |
| 213 | "required": ["kind", "text"], | |
| 214 | }, | |
| 215 | }, | |
| 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. | 216 | "touched": { |
| 217 | "type": "array", | |
| 218 | "description": "What it touched, up to 200, each kept once.", | |
| 219 | "items": { | |
| 220 | "type": "object", | |
| 221 | "properties": { | |
| 222 | "kind": { "type": "string", "enum": ["file", "commit", "branch", "pull", "issue"] }, | |
| 223 | "value": { "type": "string", "description": "A path from the repository's root, a commit, a branch, or a number." }, | |
| 224 | }, | |
| 225 | "required": ["kind", "value"], | |
| 226 | }, | |
| 227 | }, | |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 228 | }), |
| 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. | 229 | &["repo", "id"], |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 230 | ), |
| 231 | CoordinationOp::ClaimIssue => ( | |
| 232 | json!({ | |
| 233 | "repo": repo, | |
| Work with no issue yet is a session of its own, titled with what it is for, until it becomes a change: Claude Code's plugin titles each session from its first prompt that is not a slash command (its first line or sentence, without code blocks or markdown, up to 80 characters cut on a word), in the background, and only when the session has no title, so a title Claude, a person or an earlier prompt set is never replaced; g1t session attach --title names one by hand and replaces the prompt's. Nothing is titled with prompts kept back. | 234 | "number": { "type": "integer", "description": "The issue's number. Left out, give title (and body) to open one and claim it." }, |
| 235 | "title": { "type": "string", "description": "Without number: the title of a new issue to open and claim." }, | |
| 236 | "body": { "type": "string", "description": "Without number: the new issue's description." }, | |
| 237 | "session_id": { "type": "string", "description": "One of your sessions (ses_…) on the repository: its heartbeats renew the claim. With title, it is attached to the new issue." }, | |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 238 | "note": { "type": "string", "description": "What you are doing, for whoever else looks. Up to 500 characters." }, |
| 239 | "ttl_minutes": { "type": "integer", "description": "How long it lasts unless renewed: 30 unless you say, at most 240." }, | |
| 240 | }), | |
| Work with no issue yet is a session of its own, titled with what it is for, until it becomes a change: Claude Code's plugin titles each session from its first prompt that is not a slash command (its first line or sentence, without code blocks or markdown, up to 80 characters cut on a word), in the background, and only when the session has no title, so a title Claude, a person or an earlier prompt set is never replaced; g1t session attach --title names one by hand and replaces the prompt's. Nothing is titled with prompts kept back. | 241 | &["repo"], |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 242 | ), |
| 243 | CoordinationOp::ReleaseClaim => ( | |
| 244 | json!({ | |
| 245 | "repo": repo, | |
| 246 | "number": number, | |
| 247 | "reason": { "type": "string", "description": "Why, in a few words. \"released\" if left out." }, | |
| 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. | 248 | "session_id": session_id, |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 249 | }), |
| 250 | &["repo", "number"], | |
| 251 | ), | |
| 252 | CoordinationOp::HandOffIssue => ( | |
| 253 | json!({ | |
| 254 | "repo": repo, | |
| 255 | "number": number, | |
| 256 | "to": { "type": "string", "description": "The username it is for. Left out, it is for anyone." }, | |
| 257 | "note": { "type": "string", "description": "Where the work stands: what is done, what is left, what to watch for. Up to 500 characters." }, | |
| 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. | 258 | "session_id": session_id, |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 259 | }), |
| 260 | &["repo", "number", "note"], | |
| 261 | ), | |
| 262 | CoordinationOp::ListClaims => ( | |
| 263 | json!({ | |
| 264 | "repo": repo, | |
| 265 | "state": { | |
| 266 | "type": "string", | |
| 267 | "enum": ["active", "released", "expired", "handed_off"], | |
| 268 | "description": "Claims in this state; active if left out.", | |
| 269 | }, | |
| 270 | "issue": { "type": "integer", "description": "Only claims on this issue." }, | |
| 271 | }), | |
| 272 | &["repo"], | |
| 273 | ), | |
| 274 | CoordinationOp::GetWork => (json!({ "repo": repo, "number": number }), &["repo", "number"]), | |
| 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. | 275 | CoordinationOp::StartAgentSession => ( |
| 276 | json!({ | |
| 277 | "repo": repo, | |
| 278 | "issue": { "type": "integer", "description": "The issue to work on. Left out, give title (and body) to open one." }, | |
| 279 | "title": { "type": "string", "description": "Without issue: the title of a new issue to open and work on." }, | |
| 280 | "body": { "type": "string", "description": "Without issue: the new issue's description." }, | |
| 281 | "instruction": { "type": "string", "description": "What to do beyond what the issue says. Up to 4,000 characters." }, | |
| 282 | "mode": { "type": "string", "enum": ["plan", "implement", "review"], "description": "What kind of work: plan (say how, change nothing), implement (the default) or review (review its pull request)." }, | |
| 283 | "handoff": { "type": "boolean", "description": "true for a Claude Code command and prompt to run yourself instead of a session on the workspace's Anthropic account; false to refuse when there is no Anthropic connection. Left out, a session when the workspace can, else the hand-off." }, | |
| 284 | }), | |
| 285 | &["repo"], | |
| 286 | ), | |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 287 | }; |
| 288 | json!({ "type": "object", "properties": properties, "required": required }) | |
| 289 | } | |
| 290 | } | |
| 291 | ||
| 292 | fn text(input: &Value, key: &str) -> Option<String> { | |
| 293 | input[key].as_str().map(str::trim).filter(|text| !text.is_empty()).map(str::to_owned) | |
| 294 | } | |
| 295 | ||
| 296 | /// A whole number given as a number, as digits, or as `#12`. | |
| 297 | fn number(input: &Value, key: &str) -> Option<u32> { | |
| 298 | match &input[key] { | |
| 299 | Value::Number(number) => number.as_u64().and_then(|n| u32::try_from(n).ok()), | |
| 300 | Value::String(digits) => digits.trim().trim_start_matches('#').parse().ok(), | |
| 301 | _ => None, | |
| 302 | } | |
| 303 | } | |
| 304 | ||
| 305 | /// An optional whole number: none when left out, refused when not one. | |
| 306 | fn optional_number(input: &Value, key: &str) -> std::result::Result<Option<u32>, String> { | |
| 307 | match &input[key] { | |
| 308 | Value::Null => Ok(None), | |
| 309 | Value::String(text) if text.trim().is_empty() => Ok(None), | |
| 310 | _ => number(input, key).map(Some).ok_or_else(|| format!("{key} is a number.")), | |
| 311 | } | |
| 312 | } | |
| 313 | ||
| 314 | /// `blocked_by` for `update_issue`: issue numbers, as numbers, digits or | |
| 315 | /// `#12`. None when left out; refused when it holds anything else. | |
| 316 | pub fn blocked_by(input: &Value) -> std::result::Result<Option<Vec<u32>>, String> { | |
| 317 | let items = match &input["blocked_by"] { | |
| 318 | Value::Null => return Ok(None), | |
| 319 | Value::Array(items) => items, | |
| 320 | _ => return Err("blocked_by is a list of issue numbers.".to_owned()), | |
| 321 | }; | |
| 322 | items | |
| 323 | .iter() | |
| 324 | .map(|item| number(&json!({ "n": item }), "n").ok_or_else(|| format!("blocked_by holds {item}, which is not an issue number."))) | |
| 325 | .collect::<std::result::Result<Vec<u32>, String>>() | |
| 326 | .map(Some) | |
| 327 | } | |
| 328 | ||
| 329 | pub async fn run(op: CoordinationOp, services: &Services, viewer: &Viewer, input: &Value) -> Result<Outcome<Value>> { | |
| 330 | let Some(repo) = repo_path(input) else { | |
| 331 | return Ok(Outcome::fail(FailureCode::Invalid, "Give the repository as \"owner/name\".")); | |
| 332 | }; | |
| 333 | let invalid = |message: String| Ok(Outcome::fail(FailureCode::Invalid, message)); | |
| 334 | let (issue, pull) = match (optional_number(input, "issue"), optional_number(input, "pull")) { | |
| 335 | (Ok(issue), Ok(pull)) => (issue, pull), | |
| 336 | (Err(message), _) | (_, Err(message)) => return invalid(message), | |
| 337 | }; | |
| 338 | let actor = || viewer.clone().unwrap_or_default(); | |
| 339 | let id = || text(input, "id").unwrap_or_default(); | |
| 340 | let issue_number = number(input, "number").unwrap_or_default(); | |
| 341 | let work = &services.work; | |
| 342 | if matches!( | |
| 343 | op, | |
| 344 | CoordinationOp::UpdateSession | CoordinationOp::EndSession | CoordinationOp::GetSession | CoordinationOp::RecordSessionEntries | |
| 345 | ) && id().is_empty() | |
| 346 | { | |
| 347 | return invalid("Name the session by its id (ses_…).".to_owned()); | |
| 348 | } | |
| 349 | match op { | |
| 350 | CoordinationOp::StartSession => { | |
| 351 | let Some(harness) = input["harness"].as_str().and_then(Harness::parse) else { | |
| 352 | let names: Vec<&str> = Harness::ALL.iter().map(|harness| harness.as_str()).collect(); | |
| 353 | return invalid(format!("harness is one of {}.", names.join(", "))); | |
| 354 | }; | |
| 355 | let args = StartSessionArgs { | |
| 356 | actor: actor(), | |
| 357 | repo, | |
| 358 | harness, | |
| 359 | harness_session_id: text(input, "harness_session_id"), | |
| 360 | issue, | |
| 361 | pull, | |
| 362 | branch: text(input, "branch"), | |
| 363 | title: text(input, "title"), | |
| 364 | head_commit: text(input, "head_commit"), | |
| 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. | 365 | parent_id: text(input, "parent_id"), |
| 366 | agent_type: text(input, "agent_type"), | |
| 367 | task: text(input, "task"), | |
| 368 | prompt: text(input, "prompt"), | |
| 369 | model: text(input, "model"), | |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 370 | }; |
| 371 | g1t_kit::call(work, "start_session", &args).await | |
| 372 | } | |
| 373 | CoordinationOp::UpdateSession => { | |
| 374 | let args = UpdateSessionArgs { | |
| 375 | actor: actor(), | |
| 376 | repo, | |
| 377 | id: id(), | |
| 378 | issue, | |
| 379 | pull, | |
| 380 | branch: text(input, "branch"), | |
| 381 | head_commit: text(input, "head_commit"), | |
| 382 | title: text(input, "title"), | |
| 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. | 383 | agent_type: text(input, "agent_type"), |
| 384 | task: text(input, "task"), | |
| 385 | prompt: text(input, "prompt"), | |
| 386 | model: text(input, "model"), | |
| 387 | result: text(input, "result"), | |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 388 | }; |
| 389 | g1t_kit::call(work, "update_session", &args).await | |
| 390 | } | |
| 391 | CoordinationOp::EndSession => { | |
| 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. | 392 | let args = EndSessionArgs { actor: actor(), repo, id: id(), reason: text(input, "reason"), result: text(input, "result") }; |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 393 | g1t_kit::call(work, "end_session", &args).await |
| 394 | } | |
| 395 | CoordinationOp::GetSession => { | |
| 396 | let args = WorkSessionArgs { viewer: viewer.clone(), repo, id: id(), after_seq: number(input, "after").unwrap_or_default() }; | |
| 397 | g1t_kit::call(work, "get_work_session", &args).await | |
| 398 | } | |
| 399 | CoordinationOp::ListSessions => { | |
| 400 | let state = match text(input, "state") { | |
| 401 | None => None, | |
| 402 | Some(state) => match SessionState::parse(&state) { | |
| 403 | Some(state) => Some(state), | |
| 404 | None => return invalid(format!("state is active, idle or ended, not {state}.")), | |
| 405 | }, | |
| 406 | }; | |
| 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. | 407 | let subagents = matches!(&input["subagents"], Value::Bool(true)) || input["subagents"].as_str() == Some("true"); |
| 408 | let args = ListWorkSessionsArgs { viewer: viewer.clone(), repo, state, issue, pull, subagents, path: text(input, "path") }; | |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 409 | g1t_kit::call(work, "list_work_sessions", &args).await |
| 410 | } | |
| 411 | CoordinationOp::RecordSessionEntries => { | |
| 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. | 412 | let entries: Vec<KeyedSessionEntry> = match serde_json::from_value(match &input["entries"] { |
| 413 | Value::Null => json!([]), | |
| 414 | entries => entries.clone(), | |
| 415 | }) { | |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 416 | Ok(entries) => entries, |
| 417 | Err(_) => { | |
| 418 | return invalid( | |
| 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. | 419 | "entries is a list of { kind, text }, kind one of prompt, message, tool_call, tool_result, note or skill, with tool, commit and key as needed.".to_owned(), |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 420 | ); |
| 421 | } | |
| 422 | }; | |
| 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. | 423 | let touched: Vec<NewTouch> = match serde_json::from_value(match &input["touched"] { |
| 424 | Value::Null => json!([]), | |
| 425 | touched => touched.clone(), | |
| 426 | }) { | |
| 427 | Ok(touched) => touched, | |
| 428 | Err(_) => return invalid("touched is a list of { kind, value }, kind one of file, commit, branch, pull or issue.".to_owned()), | |
| 429 | }; | |
| 430 | let args = RecordEntriesArgs { actor: actor(), repo, id: id(), entries, touched }; | |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 431 | g1t_kit::call(work, "record_session_entries", &args).await |
| 432 | } | |
| 433 | CoordinationOp::ClaimIssue => { | |
| 434 | let ttl_minutes = match optional_number(input, "ttl_minutes") { | |
| 435 | Ok(minutes) => minutes, | |
| 436 | Err(message) => return invalid(message), | |
| 437 | }; | |
| Work with no issue yet is a session of its own, titled with what it is for, until it becomes a change: Claude Code's plugin titles each session from its first prompt that is not a slash command (its first line or sentence, without code blocks or markdown, up to 80 characters cut on a word), in the background, and only when the session has no title, so a title Claude, a person or an earlier prompt set is never replaced; g1t session attach --title names one by hand and replaces the prompt's. Nothing is titled with prompts kept back. | 438 | let title = text(input, "title"); |
| 439 | if issue_number == 0 && title.is_none() { | |
| 440 | return invalid("Name the issue by its number, or give a title (and body) to open one.".to_owned()); | |
| 441 | } | |
| 442 | if issue_number != 0 && title.is_some() { | |
| 443 | return invalid("Give the issue's number or a title for a new one, not both.".to_owned()); | |
| 444 | } | |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 445 | let args = ClaimArgs { |
| 446 | actor: actor(), | |
| 447 | repo, | |
| 448 | number: issue_number, | |
| Work with no issue yet is a session of its own, titled with what it is for, until it becomes a change: Claude Code's plugin titles each session from its first prompt that is not a slash command (its first line or sentence, without code blocks or markdown, up to 80 characters cut on a word), in the background, and only when the session has no title, so a title Claude, a person or an earlier prompt set is never replaced; g1t session attach --title names one by hand and replaces the prompt's. Nothing is titled with prompts kept back. | 449 | title, |
| 450 | body: text(input, "body"), | |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 451 | session_id: text(input, "session_id"), |
| 452 | note: text(input, "note"), | |
| 453 | ttl_minutes, | |
| 454 | }; | |
| 455 | g1t_kit::call(work, "claim_issue", &args).await | |
| 456 | } | |
| 457 | CoordinationOp::ReleaseClaim => { | |
| 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. | 458 | let args = ReleaseArgs { actor: actor(), repo, number: issue_number, reason: text(input, "reason"), session_id: text(input, "session_id") }; |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 459 | g1t_kit::call(work, "release_claim", &args).await |
| 460 | } | |
| 461 | CoordinationOp::HandOffIssue => { | |
| 462 | let Some(note) = text(input, "note") else { | |
| 463 | return invalid("Say where the work stands in note, for whoever picks it up.".to_owned()); | |
| 464 | }; | |
| 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. | 465 | let args = HandOffArgs { actor: actor(), repo, number: issue_number, to: text(input, "to"), note, session_id: text(input, "session_id") }; |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 466 | g1t_kit::call(work, "hand_off_issue", &args).await |
| 467 | } | |
| 468 | CoordinationOp::ListClaims => { | |
| 469 | let state = match text(input, "state") { | |
| 470 | None => None, | |
| 471 | Some(state) => match ClaimState::parse(&state) { | |
| 472 | Some(state) => Some(state), | |
| 473 | None => return invalid(format!("state is active, released, expired or handed_off, not {state}.")), | |
| 474 | }, | |
| 475 | }; | |
| 476 | let args = ListClaimsArgs { viewer: viewer.clone(), repo, state, issue }; | |
| 477 | g1t_kit::call(work, "list_claims", &args).await | |
| 478 | } | |
| 479 | CoordinationOp::GetWork => { | |
| 480 | let args = GetWorkArgs { viewer: viewer.clone(), repo, number: issue_number }; | |
| 481 | g1t_kit::call(work, "get_work", &args).await | |
| 482 | } | |
| 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. | 483 | CoordinationOp::StartAgentSession => { |
| 484 | let mode = match text(input, "mode") { | |
| 485 | None => None, | |
| 486 | Some(mode) => match AgentMode::parse(&mode) { | |
| 487 | Some(mode) => Some(mode), | |
| 488 | None => return invalid(format!("mode is plan, implement or review, not {mode}.")), | |
| 489 | }, | |
| 490 | }; | |
| 491 | if issue.is_none() && text(input, "title").is_none() { | |
| 492 | return invalid("Name the issue, or give a title (and body) for a new one.".to_owned()); | |
| 493 | } | |
| 494 | let handoff = match &input["handoff"] { | |
| 495 | Value::Bool(handoff) => Some(*handoff), | |
| 496 | Value::String(text) if text == "true" => Some(true), | |
| 497 | Value::String(text) if text == "false" => Some(false), | |
| 498 | _ => None, | |
| 499 | }; | |
| 500 | let args = StartAgentSessionArgs { | |
| 501 | actor: actor(), | |
| 502 | repo, | |
| 503 | issue, | |
| 504 | title: text(input, "title"), | |
| 505 | body: text(input, "body"), | |
| 506 | instruction: text(input, "instruction"), | |
| 507 | mode, | |
| 508 | handoff, | |
| 509 | }; | |
| 510 | g1t_kit::call(&services.integrations, "start_agent_session", &args).await | |
| 511 | } | |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 512 | } |
| 513 | } | |
| 514 | ||
| 515 | #[cfg(test)] | |
| 516 | mod tests { | |
| 517 | use super::*; | |
| 518 | use crate::operations::Op; | |
| 519 | use g1t_contracts::scopes::{Level, Scope, scope_for}; | |
| 520 | ||
| 521 | #[test] | |
| 522 | fn each_operation_is_described_scoped_and_listed() { | |
| 523 | for op in CoordinationOp::ALL { | |
| 524 | assert!(Op::ALL.contains(&Op::Coordination(op)), "{}", op.name()); | |
| 525 | assert_eq!(Op::by_name(op.name()), Some(Op::Coordination(op))); | |
| 526 | assert!(!op.title().is_empty() && op.description().len() > 80, "{}", op.name()); | |
| 527 | assert!(op.input()["required"].as_array().unwrap().contains(&json!("repo")), "{}", op.name()); | |
| 528 | let scope = scope_for(op.name()).unwrap(); | |
| 529 | assert_eq!(scope.level() == Level::Read, op.reads(), "{}", op.name()); | |
| 530 | let sessions = op.name().contains("session"); | |
| 531 | let expected = match (sessions, op.reads()) { | |
| 532 | (true, true) => Scope::PullRequestsRead, | |
| 533 | (true, false) => Scope::PullRequestsWrite, | |
| 534 | (false, true) => Scope::IssuesRead, | |
| 535 | (false, false) => Scope::IssuesWrite, | |
| 536 | }; | |
| 537 | assert_eq!(scope, expected, "{}", op.name()); | |
| 538 | } | |
| 539 | } | |
| 540 | ||
| 541 | #[test] | |
| 542 | fn routes_reach_each_operation_with_its_input() { | |
| 543 | let resolve = |method: &str, path: &str, query: &[(&str, &str)], body: Value| { | |
| 544 | let query: Vec<(String, String)> = query.iter().map(|(k, v)| ((*k).to_owned(), (*v).to_owned())).collect(); | |
| 545 | let (route, input) = crate::rest::resolve(method, path, &query, body).unwrap(); | |
| 546 | (route.op, input) | |
| 547 | }; | |
| 548 | let coordination = |op| Op::Coordination(op); | |
| 549 | let (op, input) = resolve("POST", "/repos/acme/web/sessions", &[], json!({ "harness": "claude_code" })); | |
| 550 | assert_eq!(op, coordination(CoordinationOp::StartSession)); | |
| 551 | assert_eq!(input, json!({ "harness": "claude_code", "repo": "acme/web" })); | |
| 552 | let (op, input) = resolve("PATCH", "/repos/acme/web/sessions/ses_1", &[], json!({})); | |
| 553 | assert_eq!(op, coordination(CoordinationOp::UpdateSession)); | |
| 554 | assert_eq!(input, json!({ "id": "ses_1", "repo": "acme/web" })); | |
| 555 | let (op, _) = resolve("POST", "/repos/acme/web/sessions/ses_1/end", &[], json!({})); | |
| 556 | assert_eq!(op, coordination(CoordinationOp::EndSession)); | |
| 557 | let (op, input) = resolve("GET", "/repos/acme/web/sessions/ses_1", &[("after", "40")], Value::Null); | |
| 558 | assert_eq!(op, coordination(CoordinationOp::GetSession)); | |
| 559 | assert_eq!(input["after"], "40"); | |
| 560 | let (op, input) = resolve("GET", "/repos/acme/web/sessions", &[("state", "idle"), ("issue", "4")], Value::Null); | |
| 561 | assert_eq!(op, coordination(CoordinationOp::ListSessions)); | |
| 562 | assert_eq!(input, json!({ "state": "idle", "issue": "4", "repo": "acme/web" })); | |
| 563 | let (op, _) = resolve("POST", "/repos/acme/web/sessions/ses_1/entries", &[], json!({ "entries": [] })); | |
| 564 | assert_eq!(op, coordination(CoordinationOp::RecordSessionEntries)); | |
| 565 | let (op, input) = resolve("POST", "/repos/acme/web/issues/4/claim", &[], json!({ "note": "on it" })); | |
| 566 | assert_eq!(op, coordination(CoordinationOp::ClaimIssue)); | |
| 567 | assert_eq!(input, json!({ "note": "on it", "number": 4, "repo": "acme/web" })); | |
| Work with no issue yet is a session of its own, titled with what it is for, until it becomes a change: Claude Code's plugin titles each session from its first prompt that is not a slash command (its first line or sentence, without code blocks or markdown, up to 80 characters cut on a word), in the background, and only when the session has no title, so a title Claude, a person or an earlier prompt set is never replaced; g1t session attach --title names one by hand and replaces the prompt's. Nothing is titled with prompts kept back. | 568 | // Work with no issue yet: one is opened and claimed. |
| 569 | let (op, input) = resolve("POST", "/repos/acme/web/claims", &[], json!({ "title": "Fix previews" })); | |
| 570 | assert_eq!(op, coordination(CoordinationOp::ClaimIssue)); | |
| 571 | assert_eq!(input, json!({ "title": "Fix previews", "repo": "acme/web" })); | |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 572 | let (op, _) = resolve("POST", "/repos/acme/web/issues/4/release", &[], json!({})); |
| 573 | assert_eq!(op, coordination(CoordinationOp::ReleaseClaim)); | |
| 574 | let (op, _) = resolve("POST", "/repos/acme/web/issues/4/handoff", &[], json!({ "note": "half done" })); | |
| 575 | assert_eq!(op, coordination(CoordinationOp::HandOffIssue)); | |
| 576 | let (op, _) = resolve("GET", "/repos/acme/web/claims", &[("state", "active")], Value::Null); | |
| 577 | assert_eq!(op, coordination(CoordinationOp::ListClaims)); | |
| 578 | let (op, input) = resolve("GET", "/repos/acme/web/issues/4/work", &[], Value::Null); | |
| 579 | assert_eq!(op, coordination(CoordinationOp::GetWork)); | |
| 580 | assert_eq!(input, json!({ "number": 4, "repo": "acme/web" })); | |
| 581 | } | |
| 582 | ||
| 583 | #[test] | |
| 584 | fn mcp_actions_reach_each_operation() { | |
| 585 | use crate::tools::{Tool, resolve}; | |
| 586 | let call = |tool: &str, action: &str| resolve(Tool::by_name(tool).unwrap(), &json!({ "action": action, "repo": "a/b", "id": "ses_1", "number": 4, "harness": "codex", "entries": [], "note": "x" })); | |
| 587 | assert_eq!(call("session", "start"), Ok(Op::Coordination(CoordinationOp::StartSession))); | |
| 588 | assert_eq!(call("session", "update"), Ok(Op::Coordination(CoordinationOp::UpdateSession))); | |
| 589 | assert_eq!(call("session", "record"), Ok(Op::Coordination(CoordinationOp::RecordSessionEntries))); | |
| 590 | assert_eq!(call("issue", "claim"), Ok(Op::Coordination(CoordinationOp::ClaimIssue))); | |
| Work with no issue yet is a session of its own, titled with what it is for, until it becomes a change: Claude Code's plugin titles each session from its first prompt that is not a slash command (its first line or sentence, without code blocks or markdown, up to 80 characters cut on a word), in the background, and only when the session has no title, so a title Claude, a person or an earlier prompt set is never replaced; g1t session attach --title names one by hand and replaces the prompt's. Nothing is titled with prompts kept back. | 591 | let claim_new = resolve(Tool::by_name("issue").unwrap(), &json!({ "action": "claim", "repo": "a/b", "title": "Fix previews" })); |
| 592 | assert_eq!(claim_new, Ok(Op::Coordination(CoordinationOp::ClaimIssue))); | |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 593 | assert_eq!(call("issue", "work"), Ok(Op::Coordination(CoordinationOp::GetWork))); |
| 594 | } | |
| 595 | ||
| 596 | #[test] | |
| 597 | fn numbers_come_as_numbers_digits_or_with_a_hash() { | |
| 598 | assert_eq!(blocked_by(&json!({})), Ok(None)); | |
| 599 | assert_eq!(blocked_by(&json!({ "blocked_by": [3, "4", "#5"] })), Ok(Some(vec![3, 4, 5]))); | |
| 600 | assert_eq!(blocked_by(&json!({ "blocked_by": [] })), Ok(Some(vec![]))); | |
| 601 | assert!(blocked_by(&json!({ "blocked_by": ["x"] })).is_err()); | |
| 602 | assert!(blocked_by(&json!({ "blocked_by": 3 })).is_err()); | |
| 603 | assert_eq!(optional_number(&json!({ "issue": "" }), "issue"), Ok(None)); | |
| 604 | assert!(optional_number(&json!({ "issue": "soon" }), "issue").is_err()); | |
| 605 | } | |
| 606 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.