The API and MCP server coordinate work: start_session, update_session (an empty body is a heartbeat), end_session, get_session, list_sessions and record_session_entries at /repos/{owner}/{name}/sessions, as the new session tool's start, update, end, get, list and record; claim_issue, release_claim, hand_off_issue and get_work at /repos/{owner}/{name}/issues/{number}/claim, /release, /handoff and /work, and list_claims at /repos/{owner}/{name}/claims, as the issue tool's claim, release, handoff, work and list_claims; and update_issue takes blocked_by. Sessions need pull_requests scopes and claims and get_work issues scopes, reads are open to anyone who can read the repository, the reference has an example of each, and the OpenAPI copy and the CLI's table of operations are regenerated.
11 files+1374−130/11 viewed
| 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 | + | ||
| 11 | + | use g1t_contracts::work::*; | |
| 12 | + | use g1t_contracts::{FailureCode, Outcome, Viewer}; | |
| 13 | + | use serde_json::{Value, json}; | |
| 14 | + | use worker::Result; | |
| 15 | + | ||
| 16 | + | use crate::operations::{Services, repo_path}; | |
| 17 | + | ||
| 18 | + | /// One operation on sessions, claims or an issue's context. | |
| 19 | + | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 20 | + | pub enum CoordinationOp { | |
| 21 | + | StartSession, | |
| 22 | + | UpdateSession, | |
| 23 | + | EndSession, | |
| 24 | + | GetSession, | |
| 25 | + | ListSessions, | |
| 26 | + | RecordSessionEntries, | |
| 27 | + | ClaimIssue, | |
| 28 | + | ReleaseClaim, | |
| 29 | + | HandOffIssue, | |
| 30 | + | ListClaims, | |
| 31 | + | GetWork, | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | impl CoordinationOp { | |
| 35 | + | /// Every one: `Op::ALL` lists each as `Op::Coordination(…)`, which a | |
| 36 | + | /// test checks against this. | |
| 37 | + | pub const ALL: [CoordinationOp; 11] = [ | |
| 38 | + | CoordinationOp::StartSession, | |
| 39 | + | CoordinationOp::UpdateSession, | |
| 40 | + | CoordinationOp::EndSession, | |
| 41 | + | CoordinationOp::GetSession, | |
| 42 | + | CoordinationOp::ListSessions, | |
| 43 | + | CoordinationOp::RecordSessionEntries, | |
| 44 | + | CoordinationOp::ClaimIssue, | |
| 45 | + | CoordinationOp::ReleaseClaim, | |
| 46 | + | CoordinationOp::HandOffIssue, | |
| 47 | + | CoordinationOp::ListClaims, | |
| 48 | + | CoordinationOp::GetWork, | |
| 49 | + | ]; | |
| 50 | + | ||
| 51 | + | pub fn name(self) -> &'static str { | |
| 52 | + | match self { | |
| 53 | + | CoordinationOp::StartSession => "start_session", | |
| 54 | + | CoordinationOp::UpdateSession => "update_session", | |
| 55 | + | CoordinationOp::EndSession => "end_session", | |
| 56 | + | CoordinationOp::GetSession => "get_session", | |
| 57 | + | CoordinationOp::ListSessions => "list_sessions", | |
| 58 | + | CoordinationOp::RecordSessionEntries => "record_session_entries", | |
| 59 | + | CoordinationOp::ClaimIssue => "claim_issue", | |
| 60 | + | CoordinationOp::ReleaseClaim => "release_claim", | |
| 61 | + | CoordinationOp::HandOffIssue => "hand_off_issue", | |
| 62 | + | CoordinationOp::ListClaims => "list_claims", | |
| 63 | + | CoordinationOp::GetWork => "get_work", | |
| 64 | + | } | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | /// For the API reference. | |
| 68 | + | pub fn title(self) -> &'static str { | |
| 69 | + | match self { | |
| 70 | + | CoordinationOp::StartSession => "Start a session", | |
| 71 | + | CoordinationOp::UpdateSession => "Update a session", | |
| 72 | + | CoordinationOp::EndSession => "End a session", | |
| 73 | + | CoordinationOp::GetSession => "Get a session", | |
| 74 | + | CoordinationOp::ListSessions => "List sessions", | |
| 75 | + | CoordinationOp::RecordSessionEntries => "Record entries in a session", | |
| 76 | + | CoordinationOp::ClaimIssue => "Claim an issue", | |
| 77 | + | CoordinationOp::ReleaseClaim => "Release a claim", | |
| 78 | + | CoordinationOp::HandOffIssue => "Hand off an issue", | |
| 79 | + | CoordinationOp::ListClaims => "List claims", | |
| 80 | + | CoordinationOp::GetWork => "Get an issue's work context", | |
| 81 | + | } | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | /// Whether it only reads. | |
| 85 | + | pub fn reads(self) -> bool { | |
| 86 | + | matches!( | |
| 87 | + | self, | |
| 88 | + | CoordinationOp::GetSession | CoordinationOp::ListSessions | CoordinationOp::ListClaims | CoordinationOp::GetWork | |
| 89 | + | ) | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | pub fn description(self) -> &'static str { | |
| 93 | + | match self { | |
| 94 | + | 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. 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.", | |
| 95 | + | 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. Returns the session; one ended with end_session is refused with `409`.", | |
| 96 | + | CoordinationOp::EndSession => "End one of your sessions, with an optional `reason`. A session.ended event. Ending one already ended returns it as it is. Its claims are not released: release them, hand the issue off, or let them expire.", | |
| 97 | + | 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. Anyone who can read the repository may.", | |
| 98 | + | 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, and `issue` or `pull` for those attached to one. Anyone who can read the repository may.", | |
| 99 | + | CoordinationOp::RecordSessionEntries => "Record entries in one of your sessions: up to 200 at a time, each with `kind` (prompt, message, tool_call, tool_result or note), `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. 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`.", | |
| 100 | + | 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`.", | |
| 101 | + | 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.", | |
| 102 | + | 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.", | |
| 103 | + | 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.", | |
| 104 | + | 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.", | |
| 105 | + | } | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | pub fn input(self) -> Value { | |
| 109 | + | let repo = json!({ "type": "string", "description": "Repository as \"owner/name\", e.g. \"flagon-io/hello\"." }); | |
| 110 | + | let id = json!({ "type": "string", "description": "The session's id (ses_…), from start_session or list_sessions." }); | |
| 111 | + | let number = json!({ "type": "integer", "description": "The issue's number." }); | |
| 112 | + | let issue = json!({ "type": "integer", "description": "The issue it works on." }); | |
| 113 | + | let pull = json!({ "type": "integer", "description": "The pull request it works on." }); | |
| 114 | + | let fields = |more: Value| -> Value { | |
| 115 | + | let mut properties = json!({ | |
| 116 | + | "repo": repo, | |
| 117 | + | "issue": issue, | |
| 118 | + | "pull": pull, | |
| 119 | + | "branch": { "type": "string", "description": "The branch it works on." }, | |
| 120 | + | "title": { "type": "string", "description": "What it is doing, in a few words. Up to 200 characters." }, | |
| 121 | + | "head_commit": { "type": "string", "description": "The commit it is at, such as after a push." }, | |
| 122 | + | }); | |
| 123 | + | if let (Some(all), Value::Object(more)) = (properties.as_object_mut(), more) { | |
| 124 | + | all.extend(more); | |
| 125 | + | } | |
| 126 | + | properties | |
| 127 | + | }; | |
| 128 | + | let (properties, required): (Value, &[&str]) = match self { | |
| 129 | + | CoordinationOp::StartSession => ( | |
| 130 | + | fields(json!({ | |
| 131 | + | "harness": { | |
| 132 | + | "type": "string", | |
| 133 | + | "enum": Harness::ALL.map(Harness::as_str), | |
| 134 | + | "description": "The harness the session runs in.", | |
| 135 | + | }, | |
| 136 | + | "harness_session_id": { | |
| 137 | + | "type": "string", | |
| 138 | + | "description": "The harness's own id for the session, such as Claude Code's session_id. Starting again with it returns the same session.", | |
| 139 | + | }, | |
| 140 | + | })), | |
| 141 | + | &["repo", "harness"], | |
| 142 | + | ), | |
| 143 | + | CoordinationOp::UpdateSession => (fields(json!({ "id": id })), &["repo", "id"]), | |
| 144 | + | CoordinationOp::EndSession => ( | |
| 145 | + | json!({ | |
| 146 | + | "repo": repo, | |
| 147 | + | "id": id, | |
| 148 | + | "reason": { "type": "string", "description": "Why it ended, in a few words. \"ended\" if left out." }, | |
| 149 | + | }), | |
| 150 | + | &["repo", "id"], | |
| 151 | + | ), | |
| 152 | + | CoordinationOp::GetSession => ( | |
| 153 | + | json!({ | |
| 154 | + | "repo": repo, | |
| 155 | + | "id": id, | |
| 156 | + | "after": { "type": "integer", "description": "Only entries after this sequence number, for the next page." }, | |
| 157 | + | }), | |
| 158 | + | &["repo", "id"], | |
| 159 | + | ), | |
| 160 | + | CoordinationOp::ListSessions => ( | |
| 161 | + | json!({ | |
| 162 | + | "repo": repo, | |
| 163 | + | "state": { "type": "string", "enum": ["active", "idle", "ended"], "description": "Only sessions in this state." }, | |
| 164 | + | "issue": { "type": "integer", "description": "Only sessions on this issue." }, | |
| 165 | + | "pull": { "type": "integer", "description": "Only sessions on this pull request." }, | |
| 166 | + | }), | |
| 167 | + | &["repo"], | |
| 168 | + | ), | |
| 169 | + | CoordinationOp::RecordSessionEntries => ( | |
| 170 | + | json!({ | |
| 171 | + | "repo": repo, | |
| 172 | + | "id": id, | |
| 173 | + | "entries": { | |
| 174 | + | "type": "array", | |
| 175 | + | "description": "Up to 200 entries, oldest first.", | |
| 176 | + | "items": { | |
| 177 | + | "type": "object", | |
| 178 | + | "properties": { | |
| 179 | + | "kind": { "type": "string", "enum": ["prompt", "message", "tool_call", "tool_result", "note"] }, | |
| 180 | + | "text": { "type": "string" }, | |
| 181 | + | "tool": { "type": "string", "description": "For tool_call and tool_result: the tool's name." }, | |
| 182 | + | "commit": { "type": "string", "description": "The commit it was recorded at; the session's head if left out." }, | |
| 183 | + | "key": { "type": "string", "description": "An idempotency key, unique in the session: an entry with a key recorded before is skipped." }, | |
| 184 | + | }, | |
| 185 | + | "required": ["kind", "text"], | |
| 186 | + | }, | |
| 187 | + | }, | |
| 188 | + | }), | |
| 189 | + | &["repo", "id", "entries"], | |
| 190 | + | ), | |
| 191 | + | CoordinationOp::ClaimIssue => ( | |
| 192 | + | json!({ | |
| 193 | + | "repo": repo, | |
| 194 | + | "number": number, | |
| 195 | + | "session_id": { "type": "string", "description": "One of your sessions (ses_…) on the repository: its heartbeats renew the claim." }, | |
| 196 | + | "note": { "type": "string", "description": "What you are doing, for whoever else looks. Up to 500 characters." }, | |
| 197 | + | "ttl_minutes": { "type": "integer", "description": "How long it lasts unless renewed: 30 unless you say, at most 240." }, | |
| 198 | + | }), | |
| 199 | + | &["repo", "number"], | |
| 200 | + | ), | |
| 201 | + | CoordinationOp::ReleaseClaim => ( | |
| 202 | + | json!({ | |
| 203 | + | "repo": repo, | |
| 204 | + | "number": number, | |
| 205 | + | "reason": { "type": "string", "description": "Why, in a few words. \"released\" if left out." }, | |
| 206 | + | }), | |
| 207 | + | &["repo", "number"], | |
| 208 | + | ), | |
| 209 | + | CoordinationOp::HandOffIssue => ( | |
| 210 | + | json!({ | |
| 211 | + | "repo": repo, | |
| 212 | + | "number": number, | |
| 213 | + | "to": { "type": "string", "description": "The username it is for. Left out, it is for anyone." }, | |
| 214 | + | "note": { "type": "string", "description": "Where the work stands: what is done, what is left, what to watch for. Up to 500 characters." }, | |
| 215 | + | }), | |
| 216 | + | &["repo", "number", "note"], | |
| 217 | + | ), | |
| 218 | + | CoordinationOp::ListClaims => ( | |
| 219 | + | json!({ | |
| 220 | + | "repo": repo, | |
| 221 | + | "state": { | |
| 222 | + | "type": "string", | |
| 223 | + | "enum": ["active", "released", "expired", "handed_off"], | |
| 224 | + | "description": "Claims in this state; active if left out.", | |
| 225 | + | }, | |
| 226 | + | "issue": { "type": "integer", "description": "Only claims on this issue." }, | |
| 227 | + | }), | |
| 228 | + | &["repo"], | |
| 229 | + | ), | |
| 230 | + | CoordinationOp::GetWork => (json!({ "repo": repo, "number": number }), &["repo", "number"]), | |
| 231 | + | }; | |
| 232 | + | json!({ "type": "object", "properties": properties, "required": required }) | |
| 233 | + | } | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | fn text(input: &Value, key: &str) -> Option<String> { | |
| 237 | + | input[key].as_str().map(str::trim).filter(|text| !text.is_empty()).map(str::to_owned) | |
| 238 | + | } | |
| 239 | + | ||
| 240 | + | /// A whole number given as a number, as digits, or as `#12`. | |
| 241 | + | fn number(input: &Value, key: &str) -> Option<u32> { | |
| 242 | + | match &input[key] { | |
| 243 | + | Value::Number(number) => number.as_u64().and_then(|n| u32::try_from(n).ok()), | |
| 244 | + | Value::String(digits) => digits.trim().trim_start_matches('#').parse().ok(), | |
| 245 | + | _ => None, | |
| 246 | + | } | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | /// An optional whole number: none when left out, refused when not one. | |
| 250 | + | fn optional_number(input: &Value, key: &str) -> std::result::Result<Option<u32>, String> { | |
| 251 | + | match &input[key] { | |
| 252 | + | Value::Null => Ok(None), | |
| 253 | + | Value::String(text) if text.trim().is_empty() => Ok(None), | |
| 254 | + | _ => number(input, key).map(Some).ok_or_else(|| format!("{key} is a number.")), | |
| 255 | + | } | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | /// `blocked_by` for `update_issue`: issue numbers, as numbers, digits or | |
| 259 | + | /// `#12`. None when left out; refused when it holds anything else. | |
| 260 | + | pub fn blocked_by(input: &Value) -> std::result::Result<Option<Vec<u32>>, String> { | |
| 261 | + | let items = match &input["blocked_by"] { | |
| 262 | + | Value::Null => return Ok(None), | |
| 263 | + | Value::Array(items) => items, | |
| 264 | + | _ => return Err("blocked_by is a list of issue numbers.".to_owned()), | |
| 265 | + | }; | |
| 266 | + | items | |
| 267 | + | .iter() | |
| 268 | + | .map(|item| number(&json!({ "n": item }), "n").ok_or_else(|| format!("blocked_by holds {item}, which is not an issue number."))) | |
| 269 | + | .collect::<std::result::Result<Vec<u32>, String>>() | |
| 270 | + | .map(Some) | |
| 271 | + | } | |
| 272 | + | ||
| 273 | + | pub async fn run(op: CoordinationOp, services: &Services, viewer: &Viewer, input: &Value) -> Result<Outcome<Value>> { | |
| 274 | + | let Some(repo) = repo_path(input) else { | |
| 275 | + | return Ok(Outcome::fail(FailureCode::Invalid, "Give the repository as \"owner/name\".")); | |
| 276 | + | }; | |
| 277 | + | let invalid = |message: String| Ok(Outcome::fail(FailureCode::Invalid, message)); | |
| 278 | + | let (issue, pull) = match (optional_number(input, "issue"), optional_number(input, "pull")) { | |
| 279 | + | (Ok(issue), Ok(pull)) => (issue, pull), | |
| 280 | + | (Err(message), _) | (_, Err(message)) => return invalid(message), | |
| 281 | + | }; | |
| 282 | + | let actor = || viewer.clone().unwrap_or_default(); | |
| 283 | + | let id = || text(input, "id").unwrap_or_default(); | |
| 284 | + | let issue_number = number(input, "number").unwrap_or_default(); | |
| 285 | + | let work = &services.work; | |
| 286 | + | if matches!( | |
| 287 | + | op, | |
| 288 | + | CoordinationOp::UpdateSession | CoordinationOp::EndSession | CoordinationOp::GetSession | CoordinationOp::RecordSessionEntries | |
| 289 | + | ) && id().is_empty() | |
| 290 | + | { | |
| 291 | + | return invalid("Name the session by its id (ses_…).".to_owned()); | |
| 292 | + | } | |
| 293 | + | match op { | |
| 294 | + | CoordinationOp::StartSession => { | |
| 295 | + | let Some(harness) = input["harness"].as_str().and_then(Harness::parse) else { | |
| 296 | + | let names: Vec<&str> = Harness::ALL.iter().map(|harness| harness.as_str()).collect(); | |
| 297 | + | return invalid(format!("harness is one of {}.", names.join(", "))); | |
| 298 | + | }; | |
| 299 | + | let args = StartSessionArgs { | |
| 300 | + | actor: actor(), | |
| 301 | + | repo, | |
| 302 | + | harness, | |
| 303 | + | harness_session_id: text(input, "harness_session_id"), | |
| 304 | + | issue, | |
| 305 | + | pull, | |
| 306 | + | branch: text(input, "branch"), | |
| 307 | + | title: text(input, "title"), | |
| 308 | + | head_commit: text(input, "head_commit"), | |
| 309 | + | }; | |
| 310 | + | g1t_kit::call(work, "start_session", &args).await | |
| 311 | + | } | |
| 312 | + | CoordinationOp::UpdateSession => { | |
| 313 | + | let args = UpdateSessionArgs { | |
| 314 | + | actor: actor(), | |
| 315 | + | repo, | |
| 316 | + | id: id(), | |
| 317 | + | issue, | |
| 318 | + | pull, | |
| 319 | + | branch: text(input, "branch"), | |
| 320 | + | head_commit: text(input, "head_commit"), | |
| 321 | + | title: text(input, "title"), | |
| 322 | + | }; | |
| 323 | + | g1t_kit::call(work, "update_session", &args).await | |
| 324 | + | } | |
| 325 | + | CoordinationOp::EndSession => { | |
| 326 | + | let args = EndSessionArgs { actor: actor(), repo, id: id(), reason: text(input, "reason") }; | |
| 327 | + | g1t_kit::call(work, "end_session", &args).await | |
| 328 | + | } | |
| 329 | + | CoordinationOp::GetSession => { | |
| 330 | + | let args = WorkSessionArgs { viewer: viewer.clone(), repo, id: id(), after_seq: number(input, "after").unwrap_or_default() }; | |
| 331 | + | g1t_kit::call(work, "get_work_session", &args).await | |
| 332 | + | } | |
| 333 | + | CoordinationOp::ListSessions => { | |
| 334 | + | let state = match text(input, "state") { | |
| 335 | + | None => None, | |
| 336 | + | Some(state) => match SessionState::parse(&state) { | |
| 337 | + | Some(state) => Some(state), | |
| 338 | + | None => return invalid(format!("state is active, idle or ended, not {state}.")), | |
| 339 | + | }, | |
| 340 | + | }; | |
| 341 | + | let args = ListWorkSessionsArgs { viewer: viewer.clone(), repo, state, issue, pull }; | |
| 342 | + | g1t_kit::call(work, "list_work_sessions", &args).await | |
| 343 | + | } | |
| 344 | + | CoordinationOp::RecordSessionEntries => { | |
| 345 | + | let entries: Vec<KeyedSessionEntry> = match serde_json::from_value(input["entries"].clone()) { | |
| 346 | + | Ok(entries) => entries, | |
| 347 | + | Err(_) => { | |
| 348 | + | return invalid( | |
| 349 | + | "entries is a list of { kind, text }, kind one of prompt, message, tool_call, tool_result or note, with tool, commit and key as needed.".to_owned(), | |
| 350 | + | ); | |
| 351 | + | } | |
| 352 | + | }; | |
| 353 | + | let args = RecordEntriesArgs { actor: actor(), repo, id: id(), entries }; | |
| 354 | + | g1t_kit::call(work, "record_session_entries", &args).await | |
| 355 | + | } | |
| 356 | + | CoordinationOp::ClaimIssue => { | |
| 357 | + | let ttl_minutes = match optional_number(input, "ttl_minutes") { | |
| 358 | + | Ok(minutes) => minutes, | |
| 359 | + | Err(message) => return invalid(message), | |
| 360 | + | }; | |
| 361 | + | let args = ClaimArgs { | |
| 362 | + | actor: actor(), | |
| 363 | + | repo, | |
| 364 | + | number: issue_number, | |
| 365 | + | session_id: text(input, "session_id"), | |
| 366 | + | note: text(input, "note"), | |
| 367 | + | ttl_minutes, | |
| 368 | + | }; | |
| 369 | + | g1t_kit::call(work, "claim_issue", &args).await | |
| 370 | + | } | |
| 371 | + | CoordinationOp::ReleaseClaim => { | |
| 372 | + | let args = ReleaseArgs { actor: actor(), repo, number: issue_number, reason: text(input, "reason") }; | |
| 373 | + | g1t_kit::call(work, "release_claim", &args).await | |
| 374 | + | } | |
| 375 | + | CoordinationOp::HandOffIssue => { | |
| 376 | + | let Some(note) = text(input, "note") else { | |
| 377 | + | return invalid("Say where the work stands in note, for whoever picks it up.".to_owned()); | |
| 378 | + | }; | |
| 379 | + | let args = HandOffArgs { actor: actor(), repo, number: issue_number, to: text(input, "to"), note }; | |
| 380 | + | g1t_kit::call(work, "hand_off_issue", &args).await | |
| 381 | + | } | |
| 382 | + | CoordinationOp::ListClaims => { | |
| 383 | + | let state = match text(input, "state") { | |
| 384 | + | None => None, | |
| 385 | + | Some(state) => match ClaimState::parse(&state) { | |
| 386 | + | Some(state) => Some(state), | |
| 387 | + | None => return invalid(format!("state is active, released, expired or handed_off, not {state}.")), | |
| 388 | + | }, | |
| 389 | + | }; | |
| 390 | + | let args = ListClaimsArgs { viewer: viewer.clone(), repo, state, issue }; | |
| 391 | + | g1t_kit::call(work, "list_claims", &args).await | |
| 392 | + | } | |
| 393 | + | CoordinationOp::GetWork => { | |
| 394 | + | let args = GetWorkArgs { viewer: viewer.clone(), repo, number: issue_number }; | |
| 395 | + | g1t_kit::call(work, "get_work", &args).await | |
| 396 | + | } | |
| 397 | + | } | |
| 398 | + | } | |
| 399 | + | ||
| 400 | + | #[cfg(test)] | |
| 401 | + | mod tests { | |
| 402 | + | use super::*; | |
| 403 | + | use crate::operations::Op; | |
| 404 | + | use g1t_contracts::scopes::{Level, Scope, scope_for}; | |
| 405 | + | ||
| 406 | + | #[test] | |
| 407 | + | fn each_operation_is_described_scoped_and_listed() { | |
| 408 | + | for op in CoordinationOp::ALL { | |
| 409 | + | assert!(Op::ALL.contains(&Op::Coordination(op)), "{}", op.name()); | |
| 410 | + | assert_eq!(Op::by_name(op.name()), Some(Op::Coordination(op))); | |
| 411 | + | assert!(!op.title().is_empty() && op.description().len() > 80, "{}", op.name()); | |
| 412 | + | assert!(op.input()["required"].as_array().unwrap().contains(&json!("repo")), "{}", op.name()); | |
| 413 | + | let scope = scope_for(op.name()).unwrap(); | |
| 414 | + | assert_eq!(scope.level() == Level::Read, op.reads(), "{}", op.name()); | |
| 415 | + | let sessions = op.name().contains("session"); | |
| 416 | + | let expected = match (sessions, op.reads()) { | |
| 417 | + | (true, true) => Scope::PullRequestsRead, | |
| 418 | + | (true, false) => Scope::PullRequestsWrite, | |
| 419 | + | (false, true) => Scope::IssuesRead, | |
| 420 | + | (false, false) => Scope::IssuesWrite, | |
| 421 | + | }; | |
| 422 | + | assert_eq!(scope, expected, "{}", op.name()); | |
| 423 | + | } | |
| 424 | + | } | |
| 425 | + | ||
| 426 | + | #[test] | |
| 427 | + | fn routes_reach_each_operation_with_its_input() { | |
| 428 | + | let resolve = |method: &str, path: &str, query: &[(&str, &str)], body: Value| { | |
| 429 | + | let query: Vec<(String, String)> = query.iter().map(|(k, v)| ((*k).to_owned(), (*v).to_owned())).collect(); | |
| 430 | + | let (route, input) = crate::rest::resolve(method, path, &query, body).unwrap(); | |
| 431 | + | (route.op, input) | |
| 432 | + | }; | |
| 433 | + | let coordination = |op| Op::Coordination(op); | |
| 434 | + | let (op, input) = resolve("POST", "/repos/acme/web/sessions", &[], json!({ "harness": "claude_code" })); | |
| 435 | + | assert_eq!(op, coordination(CoordinationOp::StartSession)); | |
| 436 | + | assert_eq!(input, json!({ "harness": "claude_code", "repo": "acme/web" })); | |
| 437 | + | let (op, input) = resolve("PATCH", "/repos/acme/web/sessions/ses_1", &[], json!({})); | |
| 438 | + | assert_eq!(op, coordination(CoordinationOp::UpdateSession)); | |
| 439 | + | assert_eq!(input, json!({ "id": "ses_1", "repo": "acme/web" })); | |
| 440 | + | let (op, _) = resolve("POST", "/repos/acme/web/sessions/ses_1/end", &[], json!({})); | |
| 441 | + | assert_eq!(op, coordination(CoordinationOp::EndSession)); | |
| 442 | + | let (op, input) = resolve("GET", "/repos/acme/web/sessions/ses_1", &[("after", "40")], Value::Null); | |
| 443 | + | assert_eq!(op, coordination(CoordinationOp::GetSession)); | |
| 444 | + | assert_eq!(input["after"], "40"); | |
| 445 | + | let (op, input) = resolve("GET", "/repos/acme/web/sessions", &[("state", "idle"), ("issue", "4")], Value::Null); | |
| 446 | + | assert_eq!(op, coordination(CoordinationOp::ListSessions)); | |
| 447 | + | assert_eq!(input, json!({ "state": "idle", "issue": "4", "repo": "acme/web" })); | |
| 448 | + | let (op, _) = resolve("POST", "/repos/acme/web/sessions/ses_1/entries", &[], json!({ "entries": [] })); | |
| 449 | + | assert_eq!(op, coordination(CoordinationOp::RecordSessionEntries)); | |
| 450 | + | let (op, input) = resolve("POST", "/repos/acme/web/issues/4/claim", &[], json!({ "note": "on it" })); | |
| 451 | + | assert_eq!(op, coordination(CoordinationOp::ClaimIssue)); | |
| 452 | + | assert_eq!(input, json!({ "note": "on it", "number": 4, "repo": "acme/web" })); | |
| 453 | + | let (op, _) = resolve("POST", "/repos/acme/web/issues/4/release", &[], json!({})); | |
| 454 | + | assert_eq!(op, coordination(CoordinationOp::ReleaseClaim)); | |
| 455 | + | let (op, _) = resolve("POST", "/repos/acme/web/issues/4/handoff", &[], json!({ "note": "half done" })); | |
| 456 | + | assert_eq!(op, coordination(CoordinationOp::HandOffIssue)); | |
| 457 | + | let (op, _) = resolve("GET", "/repos/acme/web/claims", &[("state", "active")], Value::Null); | |
| 458 | + | assert_eq!(op, coordination(CoordinationOp::ListClaims)); | |
| 459 | + | let (op, input) = resolve("GET", "/repos/acme/web/issues/4/work", &[], Value::Null); | |
| 460 | + | assert_eq!(op, coordination(CoordinationOp::GetWork)); | |
| 461 | + | assert_eq!(input, json!({ "number": 4, "repo": "acme/web" })); | |
| 462 | + | } | |
| 463 | + | ||
| 464 | + | #[test] | |
| 465 | + | fn mcp_actions_reach_each_operation() { | |
| 466 | + | use crate::tools::{Tool, resolve}; | |
| 467 | + | 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" })); | |
| 468 | + | assert_eq!(call("session", "start"), Ok(Op::Coordination(CoordinationOp::StartSession))); | |
| 469 | + | assert_eq!(call("session", "update"), Ok(Op::Coordination(CoordinationOp::UpdateSession))); | |
| 470 | + | assert_eq!(call("session", "record"), Ok(Op::Coordination(CoordinationOp::RecordSessionEntries))); | |
| 471 | + | assert_eq!(call("issue", "claim"), Ok(Op::Coordination(CoordinationOp::ClaimIssue))); | |
| 472 | + | assert_eq!(call("issue", "work"), Ok(Op::Coordination(CoordinationOp::GetWork))); | |
| 473 | + | } | |
| 474 | + | ||
| 475 | + | #[test] | |
| 476 | + | fn numbers_come_as_numbers_digits_or_with_a_hash() { | |
| 477 | + | assert_eq!(blocked_by(&json!({})), Ok(None)); | |
| 478 | + | assert_eq!(blocked_by(&json!({ "blocked_by": [3, "4", "#5"] })), Ok(Some(vec![3, 4, 5]))); | |
| 479 | + | assert_eq!(blocked_by(&json!({ "blocked_by": [] })), Ok(Some(vec![]))); | |
| 480 | + | assert!(blocked_by(&json!({ "blocked_by": ["x"] })).is_err()); | |
| 481 | + | assert!(blocked_by(&json!({ "blocked_by": 3 })).is_err()); | |
| 482 | + | assert_eq!(optional_number(&json!({ "issue": "" }), "issue"), Ok(None)); | |
| 483 | + | assert!(optional_number(&json!({ "issue": "soon" }), "issue").is_err()); | |
| 484 | + | } | |
| 485 | + | } |
| 14 | 14 | mod blobs; | |
| 15 | 15 | mod broker; | |
| 16 | 16 | mod checks; | |
| 17 | + | mod coordination; | |
| 17 | 18 | mod deployments; | |
| 18 | 19 | mod telemetry; | |
| 19 | 20 | mod deploy_keys; |
| 11 | 11 | const SUPPORTED_VERSIONS: [&str; 3] = ["2025-06-18", "2025-03-26", "2024-11-05"]; | |
| 12 | 12 | ||
| 13 | 13 | const INSTRUCTIONS: &str = "g1t is where your code, its history and the work around it live: repositories, issues, pull requests, workflows, deployments and packages. You bring your own agent; these tools are how it reads and changes what is on g1t, as the person or token it acts for. Repositories are named \"owner/name\"; issues and pull requests in one share a sequence of numbers. | |
| 14 | − | Tools are resources, each with an `action`: search, repository, issue, pull_request, memory, workflow, observability, package, secret, webhook, access, team, workspace, billing, security, notifications, account, artifact. The `action` field lists each action and the fields it needs. You see only what your token's scopes allow; a refusal names the scope it needs. | |
| 14 | + | Tools are resources, each with an `action`: search, repository, issue, pull_request, session, memory, workflow, observability, package, secret, webhook, access, team, workspace, billing, security, notifications, account, artifact. The `action` field lists each action and the fields it needs. You see only what your token's scopes allow; a refusal names the scope it needs. | |
| 15 | 15 | Find a repository: account whoami lists your workspaces; repository list or search finds one. | |
| 16 | − | Work on an issue: issue get (read it and the pull requests already made for it), memory recall, then pull_request create with the issue's number: you get a draft with its own fork to clone and push to. Push your commits, then pull_request ready with a summary. Watch `overlaps` and `behind` on pull_request get, and its checks there: `statuses` from the repository's workflows and `required_checks`, which must pass before it merges. If one fails, read why with workflow get_run and job_logs, push a fix, and the checks run again. | |
| 17 | − | Record your session: as you work, pull_request record_session appends your prompts, reasoning and the tools you ran to the pull request's session, so reviewers see why the change is what it is; pull_request read_session reads it back. | |
| 16 | + | Work on an issue: issue work (everything about it: what done means, who is on it, its pull requests, what it waits on), issue claim (advisory: it tells you who else is on it), memory recall, then pull_request create with the issue's number: you get a draft with its own fork to clone and push to. Push your commits, then pull_request ready with a summary. Watch `overlaps` and `behind` on pull_request get, and its checks there: `statuses` from the repository's workflows and `required_checks`, which must pass before it merges. If one fails, read why with workflow get_run and job_logs, push a fix, and the checks run again. | |
| 17 | + | Coordinate: session start once per run of your harness (harness_session_id makes it idempotent), session update as a heartbeat, session record your prompts and tool calls, session end when done; issue release or handoff (with a note) when you stop. What a session on a pull request records shows in that pull request's session, so reviewers see why the change is what it is; pull_request record_session appends to it directly and read_session reads it back. | |
| 18 | 18 | Memory: memory recall before you start to learn how the project builds and what it has decided; when you learn something the next agent needs, memory remember it, one short fact at a time (scope project or workspace). Never a secret. | |
| 19 | 19 | Security: security code_alerts, vulnerability_alerts and secret_alerts show what to fix; fix it in your pull request, which the Code scanning and Dependency review checks judge. | |
| 20 | 20 | Production: observability overview and errors show what a project's running software did; follow a request or trace to its logs."; |
| 15 | 15 | use crate::telemetry::TelemetryOp; | |
| 16 | 16 | use crate::packages::PackagesOp; | |
| 17 | 17 | use crate::folios::FoliosOp; | |
| 18 | + | use crate::coordination::CoordinationOp; | |
| 18 | 19 | use crate::protection::ProtectionOp; | |
| 19 | 20 | use crate::token_policy::TokenOp; | |
| 20 | 21 | use crate::operations::Op; | |
| ⋯ | |||
| 336 | 337 | ], | |
| 337 | 338 | ), | |
| 338 | 339 | ( | |
| 340 | + | "Claims and work", | |
| 341 | + | "Coordinating work on issues: an advisory, expiring claim that says you are on one (never a lock), handing it off with a note, and everything an agent needs before starting on an issue in one call. What an issue is blocked by is set with update_issue.", | |
| 342 | + | &[ | |
| 343 | + | Op::Coordination(CoordinationOp::GetWork), | |
| 344 | + | Op::Coordination(CoordinationOp::ClaimIssue), | |
| 345 | + | Op::Coordination(CoordinationOp::ReleaseClaim), | |
| 346 | + | Op::Coordination(CoordinationOp::HandOffIssue), | |
| 347 | + | Op::Coordination(CoordinationOp::ListClaims), | |
| 348 | + | ], | |
| 349 | + | ), | |
| 350 | + | ( | |
| 339 | 351 | "Labels and milestones", | |
| 340 | 352 | "A repository's labels, which issues and pull requests carry by name, and its milestones, which gather them under a goal and a due date.", | |
| 341 | 353 | &[ | |
| ⋯ | |||
| 373 | 385 | ), | |
| 374 | 386 | ( | |
| 375 | 387 | "Sessions", | |
| 376 | − | "The record of how a pull request was made: prompts, reasoning and the tools that ran.", | |
| 377 | − | &[Op::ReadSession, Op::RecordSession], | |
| 388 | + | "Who is working on what, and how a pull request was made. A session is one run of your own harness (a Claude Code session, a Codex run) on a repository: started once, kept alive by heartbeats, attached to an issue and a pull request, and recording prompts, reasoning and the tools that ran. A pull request's session shows what was recorded on it.", | |
| 389 | + | &[ | |
| 390 | + | Op::Coordination(CoordinationOp::StartSession), | |
| 391 | + | Op::Coordination(CoordinationOp::UpdateSession), | |
| 392 | + | Op::Coordination(CoordinationOp::EndSession), | |
| 393 | + | Op::Coordination(CoordinationOp::GetSession), | |
| 394 | + | Op::Coordination(CoordinationOp::ListSessions), | |
| 395 | + | Op::Coordination(CoordinationOp::RecordSessionEntries), | |
| 396 | + | Op::ReadSession, | |
| 397 | + | Op::RecordSession, | |
| 398 | + | ], | |
| 378 | 399 | ), | |
| 379 | 400 | ( | |
| 380 | 401 | "Memory", | |
| ⋯ | |||
| 772 | 793 | Op::DeployKeys(op) => op.title(), | |
| 773 | 794 | Op::Packages(op) => op.title(), | |
| 774 | 795 | Op::Folios(op) => op.title(), | |
| 796 | + | Op::Coordination(op) => op.title(), | |
| 775 | 797 | } | |
| 776 | 798 | } | |
| 777 | 799 | ||
| 35 | 35 | use crate::telemetry::TelemetryOp; | |
| 36 | 36 | use crate::packages::PackagesOp; | |
| 37 | 37 | use crate::folios::FoliosOp; | |
| 38 | + | use crate::coordination::CoordinationOp; | |
| 38 | 39 | use crate::protection::ProtectionOp; | |
| 39 | 40 | use crate::token_policy::TokenOp; | |
| 40 | 41 | use crate::rules::RulesOp; | |
| ⋯ | |||
| 317 | 318 | /// Artifacts mode's docs, slides, designs and dashboards, kept by the | |
| 318 | 319 | /// artifacts service: folios.rs. | |
| 319 | 320 | Folios(FoliosOp), | |
| 321 | + | /// Coordinating work: harness sessions, claims on issues and an | |
| 322 | + | /// issue's whole context, kept by the work service: coordination.rs. | |
| 323 | + | Coordination(CoordinationOp), | |
| 320 | 324 | } | |
| 321 | 325 | ||
| 322 | 326 | fn failed(code: FailureCode, message: &str) -> Result<Outcome<Value>> { | |
| ⋯ | |||
| 686 | 690 | } | |
| 687 | 691 | ||
| 688 | 692 | impl Op { | |
| 689 | − | pub const ALL: [Op; 353] = [ | |
| 693 | + | pub const ALL: [Op; 364] = [ | |
| 690 | 694 | Op::Whoami, | |
| 691 | 695 | Op::GetWorkspace, | |
| 692 | 696 | Op::CreateWorkspace, | |
| ⋯ | |||
| 1040 | 1044 | Op::Folios(FoliosOp::RestoreVersion), | |
| 1041 | 1045 | Op::Folios(FoliosOp::SetAccess), | |
| 1042 | 1046 | Op::Folios(FoliosOp::Purge), | |
| 1047 | + | Op::Coordination(CoordinationOp::StartSession), | |
| 1048 | + | Op::Coordination(CoordinationOp::UpdateSession), | |
| 1049 | + | Op::Coordination(CoordinationOp::EndSession), | |
| 1050 | + | Op::Coordination(CoordinationOp::GetSession), | |
| 1051 | + | Op::Coordination(CoordinationOp::ListSessions), | |
| 1052 | + | Op::Coordination(CoordinationOp::RecordSessionEntries), | |
| 1053 | + | Op::Coordination(CoordinationOp::ClaimIssue), | |
| 1054 | + | Op::Coordination(CoordinationOp::ReleaseClaim), | |
| 1055 | + | Op::Coordination(CoordinationOp::HandOffIssue), | |
| 1056 | + | Op::Coordination(CoordinationOp::ListClaims), | |
| 1057 | + | Op::Coordination(CoordinationOp::GetWork), | |
| 1043 | 1058 | ]; | |
| 1044 | 1059 | ||
| 1045 | 1060 | pub fn by_name(name: &str) -> Option<Op> { | |
| ⋯ | |||
| 1238 | 1253 | Op::DeployKeys(op) => op.name(), | |
| 1239 | 1254 | Op::Packages(op) => op.name(), | |
| 1240 | 1255 | Op::Folios(op) => op.name(), | |
| 1256 | + | Op::Coordination(op) => op.name(), | |
| 1241 | 1257 | } | |
| 1242 | 1258 | } | |
| 1243 | 1259 | ||
| ⋯ | |||
| 1379 | 1395 | "Open an issue on a repository. Say what done means in the body if it helps, for instance under a \"Definition of done\" heading; what must pass before a pull request for it merges is the default branch's required checks, the same for every pull request. labels are the repository's labels by name; a name it does not have yet is created when you have the Triage role or higher, and refused otherwise. milestone, a milestone's number, needs the Triage role." | |
| 1380 | 1396 | } | |
| 1381 | 1397 | Op::UpdateIssue => { | |
| 1382 | − | "Change an issue's title, body, labels, milestone or the people it is assigned to. Only the fields given are changed; labels and assignees each replace the whole set, and milestone null or 0 takes it out of its milestone. Its author may change their own issue, as may the person g1t filed one for; anyone else needs the Triage role or higher, and so does the milestone. Each label added or removed is an issue.labeled or issue.unlabeled event." | |
| 1398 | + | "Change an issue's title, body, labels, milestone, the people it is assigned to, or what it is blocked by. Only the fields given are changed; labels, assignees and blocked_by each replace the whole set, and milestone null or 0 takes it out of its milestone. blocked_by lists the numbers of the issues of the same repository that have to land first: one that is not an issue there, the issue itself, or a set that would make a cycle is refused with `422`, and a change is a dependency.changed event (as is a blocker closing, or its pull request merging, later). Its author may change their own issue, as may the person g1t filed one for; anyone else needs the Triage role or higher, and so does the milestone. Each label added or removed is an issue.labeled or issue.unlabeled event." | |
| 1383 | 1399 | } | |
| 1384 | 1400 | Op::CloseIssue => { | |
| 1385 | 1401 | "Close an issue without a pull request. Merging a pull request made for an issue closes it for you. Its author may close their own issue, as may the person g1t filed one for; anyone else needs the Triage role or higher." | |
| ⋯ | |||
| 1748 | 1764 | Op::DeployKeys(op) => op.description(), | |
| 1749 | 1765 | Op::Packages(op) => op.description(), | |
| 1750 | 1766 | Op::Folios(op) => op.description(), | |
| 1767 | + | Op::Coordination(op) => op.description(), | |
| 1751 | 1768 | } | |
| 1752 | 1769 | } | |
| 1753 | 1770 | ||
| ⋯ | |||
| 2263 | 2280 | "items": { "type": "string" }, | |
| 2264 | 2281 | "description": "Usernames of the people it is assigned to. Replaces the whole set; an empty list unassigns everyone.", | |
| 2265 | 2282 | }, | |
| 2283 | + | "blocked_by": { | |
| 2284 | + | "type": "array", | |
| 2285 | + | "items": { "type": "integer" }, | |
| 2286 | + | "description": "The numbers of the issues of this repository that have to land first. Replaces the whole set; an empty list clears it. No cycles, and never the issue itself.", | |
| 2287 | + | }, | |
| 2266 | 2288 | })), | |
| 2267 | 2289 | &["repo", "number"], | |
| 2268 | 2290 | ), | |
| ⋯ | |||
| 3074 | 3096 | Op::DeployKeys(op) => op.input(), | |
| 3075 | 3097 | Op::Packages(op) => op.input(), | |
| 3076 | 3098 | Op::Folios(op) => op.input(), | |
| 3099 | + | Op::Coordination(op) => op.input(), | |
| 3077 | 3100 | } | |
| 3078 | 3101 | } | |
| 3079 | 3102 | ||
| ⋯ | |||
| 3113 | 3136 | if let Op::Packages(op) = self { | |
| 3114 | 3137 | return !op.anonymous(); | |
| 3115 | 3138 | } | |
| 3139 | + | // And who is working on what in a public repository. | |
| 3140 | + | if let Op::Coordination(op) = self { | |
| 3141 | + | return !op.reads(); | |
| 3142 | + | } | |
| 3116 | 3143 | !matches!( | |
| 3117 | 3144 | self, | |
| 3118 | 3145 | Op::ListRepos | |
| ⋯ | |||
| 4150 | 4177 | Ok(with_deprecation(opened, !checks.is_empty())) | |
| 4151 | 4178 | } | |
| 4152 | 4179 | Op::UpdateIssue => { | |
| 4180 | + | let blocked_by = match crate::coordination::blocked_by(input) { | |
| 4181 | + | Ok(blocked_by) => blocked_by, | |
| 4182 | + | Err(message) => return failed(FailureCode::Invalid, &message), | |
| 4183 | + | }; | |
| 4153 | 4184 | pass( | |
| 4154 | 4185 | work, | |
| 4155 | 4186 | "update_issue", | |
| ⋯ | |||
| 4162 | 4193 | labels: strings(input, "labels"), | |
| 4163 | 4194 | assignees: strings(input, "assignees"), | |
| 4164 | 4195 | milestone: milestone_input(input), | |
| 4196 | + | blocked_by, | |
| 4165 | 4197 | }, | |
| 4166 | 4198 | ) | |
| 4167 | 4199 | .await | |
| ⋯ | |||
| 5261 | 5293 | Op::DeployKeys(op) => crate::deploy_keys::run(op, services, viewer, input).await, | |
| 5262 | 5294 | Op::Packages(op) => crate::packages::run(op, services, viewer, input).await, | |
| 5263 | 5295 | Op::Folios(op) => crate::folios::run(op, services, viewer, input).await, | |
| 5296 | + | Op::Coordination(op) => crate::coordination::run(op, services, viewer, input).await, | |
| 5264 | 5297 | Op::ReopenSecurityAlert => { | |
| 5265 | 5298 | let changed: Outcome<AlertChange> = call( | |
| 5266 | 5299 | &services.security, | |
| 1186 | 1186 | "assignees": [ | |
| 1187 | 1187 | "syntaqx" | |
| 1188 | 1188 | ], | |
| 1189 | − | "milestone": 3 | |
| 1189 | + | "milestone": 3, | |
| 1190 | + | "blocked_by": [9] | |
| 1190 | 1191 | }, | |
| 1191 | 1192 | "response": { | |
| 1192 | 1193 | "id": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| ⋯ | |||
| 1218 | 1219 | "assignees": [ | |
| 1219 | 1220 | "syntaqx" | |
| 1220 | 1221 | ], | |
| 1221 | − | "blocked_by": [], | |
| 1222 | + | "blocked_by": [9], | |
| 1222 | 1223 | "queued": false, | |
| 1223 | 1224 | "agent": null, | |
| 1224 | 1225 | "milestone": { | |
| ⋯ | |||
| 2227 | 2228 | }, | |
| 2228 | 2229 | "notes": "Up to 200 entries can be appended per request. Each is recorded against the pull request's head commit at the time. See [sessions and why-blame](/guides/why-blame/)." | |
| 2229 | 2230 | }, | |
| 2231 | + | "start_session": { | |
| 2232 | + | "request": { | |
| 2233 | + | "harness": "claude_code", | |
| 2234 | + | "harness_session_id": "5b0c1f9e-3d2a-4e8b-9f61-0c7d2a9e4b13", | |
| 2235 | + | "issue": 12, | |
| 2236 | + | "branch": "greet-caller", | |
| 2237 | + | "title": "Make the greeting name the caller" | |
| 2238 | + | }, | |
| 2239 | + | "response": { | |
| 2240 | + | "id": "ses_01m4b2c3d4e5f6g7h8j9k0m1n2", | |
| 2241 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2242 | + | "issue": 12, | |
| 2243 | + | "pull": null, | |
| 2244 | + | "branch": "greet-caller", | |
| 2245 | + | "actor": { | |
| 2246 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2247 | + | "username": "syntaqx" | |
| 2248 | + | }, | |
| 2249 | + | "harness": "claude_code", | |
| 2250 | + | "harness_session_id": "5b0c1f9e-3d2a-4e8b-9f61-0c7d2a9e4b13", | |
| 2251 | + | "title": "Make the greeting name the caller", | |
| 2252 | + | "state": "active", | |
| 2253 | + | "head_commit": null, | |
| 2254 | + | "started_at": "2026-10-12T14:02:11.204Z", | |
| 2255 | + | "last_seen_at": "2026-10-12T14:02:11.204Z", | |
| 2256 | + | "ended_at": null, | |
| 2257 | + | "end_reason": null | |
| 2258 | + | }, | |
| 2259 | + | "notes": "Idempotent on `harness_session_id`: a second start with it returns this session, so call it from a hook without checking first. `state` is `active`, `idle` (no heartbeat for 15 minutes) or `ended` (ended, or no heartbeat for 2 hours: `end_reason` `timed_out`). See [Sessions](/guides/sessions/)." | |
| 2260 | + | }, | |
| 2261 | + | "update_session": { | |
| 2262 | + | "request": { | |
| 2263 | + | "pull": 14, | |
| 2264 | + | "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13" | |
| 2265 | + | }, | |
| 2266 | + | "response": { | |
| 2267 | + | "id": "ses_01m4b2c3d4e5f6g7h8j9k0m1n2", | |
| 2268 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2269 | + | "issue": 12, | |
| 2270 | + | "pull": 14, | |
| 2271 | + | "branch": "greet-caller", | |
| 2272 | + | "actor": { | |
| 2273 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2274 | + | "username": "syntaqx" | |
| 2275 | + | }, | |
| 2276 | + | "harness": "claude_code", | |
| 2277 | + | "harness_session_id": "5b0c1f9e-3d2a-4e8b-9f61-0c7d2a9e4b13", | |
| 2278 | + | "title": "Make the greeting name the caller", | |
| 2279 | + | "state": "active", | |
| 2280 | + | "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13", | |
| 2281 | + | "started_at": "2026-10-12T14:02:11.204Z", | |
| 2282 | + | "last_seen_at": "2026-10-12T14:31:40.518Z", | |
| 2283 | + | "ended_at": null, | |
| 2284 | + | "end_reason": null | |
| 2285 | + | }, | |
| 2286 | + | "notes": "Send `{}` as a heartbeat. Each call renews the session's claims and brings back one that timed out. Refused with `404` for a session that is not yours, and `409` for one you ended." | |
| 2287 | + | }, | |
| 2288 | + | "end_session": { | |
| 2289 | + | "request": { | |
| 2290 | + | "reason": "done" | |
| 2291 | + | }, | |
| 2292 | + | "response": { | |
| 2293 | + | "id": "ses_01m4b2c3d4e5f6g7h8j9k0m1n2", | |
| 2294 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2295 | + | "issue": 12, | |
| 2296 | + | "pull": 14, | |
| 2297 | + | "branch": "greet-caller", | |
| 2298 | + | "actor": { | |
| 2299 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2300 | + | "username": "syntaqx" | |
| 2301 | + | }, | |
| 2302 | + | "harness": "claude_code", | |
| 2303 | + | "harness_session_id": "5b0c1f9e-3d2a-4e8b-9f61-0c7d2a9e4b13", | |
| 2304 | + | "title": "Make the greeting name the caller", | |
| 2305 | + | "state": "ended", | |
| 2306 | + | "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13", | |
| 2307 | + | "started_at": "2026-10-12T14:02:11.204Z", | |
| 2308 | + | "last_seen_at": "2026-10-12T14:40:03.115Z", | |
| 2309 | + | "ended_at": "2026-10-12T14:40:03.115Z", | |
| 2310 | + | "end_reason": "done" | |
| 2311 | + | } | |
| 2312 | + | }, | |
| 2313 | + | "get_session": { | |
| 2314 | + | "query": { | |
| 2315 | + | "after": 0 | |
| 2316 | + | }, | |
| 2317 | + | "response": { | |
| 2318 | + | "session": { | |
| 2319 | + | "id": "ses_01m4b2c3d4e5f6g7h8j9k0m1n2", | |
| 2320 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2321 | + | "issue": 12, | |
| 2322 | + | "pull": 14, | |
| 2323 | + | "branch": "greet-caller", | |
| 2324 | + | "actor": { | |
| 2325 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2326 | + | "username": "syntaqx" | |
| 2327 | + | }, | |
| 2328 | + | "harness": "claude_code", | |
| 2329 | + | "harness_session_id": "5b0c1f9e-3d2a-4e8b-9f61-0c7d2a9e4b13", | |
| 2330 | + | "title": "Make the greeting name the caller", | |
| 2331 | + | "state": "active", | |
| 2332 | + | "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13", | |
| 2333 | + | "started_at": "2026-10-12T14:02:11.204Z", | |
| 2334 | + | "last_seen_at": "2026-10-12T14:31:40.518Z", | |
| 2335 | + | "ended_at": null, | |
| 2336 | + | "end_reason": null | |
| 2337 | + | }, | |
| 2338 | + | "entries": [ | |
| 2339 | + | { | |
| 2340 | + | "seq": 1, | |
| 2341 | + | "kind": "prompt", | |
| 2342 | + | "text": "Make the greeting name the caller (issue #12).", | |
| 2343 | + | "tool": null, | |
| 2344 | + | "commit": null, | |
| 2345 | + | "at": "2026-10-12T14:02:12.008Z" | |
| 2346 | + | }, | |
| 2347 | + | { | |
| 2348 | + | "seq": 2, | |
| 2349 | + | "kind": "tool_call", | |
| 2350 | + | "text": "cargo test", | |
| 2351 | + | "tool": "Bash", | |
| 2352 | + | "commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13", | |
| 2353 | + | "at": "2026-10-12T14:30:12.660Z" | |
| 2354 | + | } | |
| 2355 | + | ] | |
| 2356 | + | }, | |
| 2357 | + | "notes": "Entries are numbered within the session and come 500 at a time; send the last `seq` you have as `after` for the next page." | |
| 2358 | + | }, | |
| 2359 | + | "list_sessions": { | |
| 2360 | + | "query": { | |
| 2361 | + | "state": "active", | |
| 2362 | + | "issue": 12 | |
| 2363 | + | }, | |
| 2364 | + | "response": [ | |
| 2365 | + | { | |
| 2366 | + | "id": "ses_01m4b2c3d4e5f6g7h8j9k0m1n2", | |
| 2367 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2368 | + | "issue": 12, | |
| 2369 | + | "pull": 14, | |
| 2370 | + | "branch": "greet-caller", | |
| 2371 | + | "actor": { | |
| 2372 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2373 | + | "username": "syntaqx" | |
| 2374 | + | }, | |
| 2375 | + | "harness": "claude_code", | |
| 2376 | + | "harness_session_id": "5b0c1f9e-3d2a-4e8b-9f61-0c7d2a9e4b13", | |
| 2377 | + | "title": "Make the greeting name the caller", | |
| 2378 | + | "state": "active", | |
| 2379 | + | "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13", | |
| 2380 | + | "started_at": "2026-10-12T14:02:11.204Z", | |
| 2381 | + | "last_seen_at": "2026-10-12T14:31:40.518Z", | |
| 2382 | + | "ended_at": null, | |
| 2383 | + | "end_reason": null | |
| 2384 | + | } | |
| 2385 | + | ] | |
| 2386 | + | }, | |
| 2387 | + | "record_session_entries": { | |
| 2388 | + | "request": { | |
| 2389 | + | "entries": [ | |
| 2390 | + | { | |
| 2391 | + | "kind": "prompt", | |
| 2392 | + | "text": "Make the greeting name the caller (issue #12).", | |
| 2393 | + | "key": "prompt-1" | |
| 2394 | + | }, | |
| 2395 | + | { | |
| 2396 | + | "kind": "tool_call", | |
| 2397 | + | "text": "cargo test", | |
| 2398 | + | "tool": "Bash", | |
| 2399 | + | "key": "tool-7" | |
| 2400 | + | } | |
| 2401 | + | ] | |
| 2402 | + | }, | |
| 2403 | + | "response": { | |
| 2404 | + | "recorded": 2, | |
| 2405 | + | "duplicates": 0 | |
| 2406 | + | }, | |
| 2407 | + | "notes": "Up to 200 entries a request. An entry whose `key` the session recorded before is counted in `duplicates` and not recorded again. Entries of a session on a pull request show in that pull request's session too (read_session)." | |
| 2408 | + | }, | |
| 2409 | + | "claim_issue": { | |
| 2410 | + | "request": { | |
| 2411 | + | "session_id": "ses_01m4b2c3d4e5f6g7h8j9k0m1n2", | |
| 2412 | + | "note": "Taking the argument parsing first.", | |
| 2413 | + | "ttl_minutes": 30 | |
| 2414 | + | }, | |
| 2415 | + | "response": { | |
| 2416 | + | "claim": { | |
| 2417 | + | "id": "clm_01m4b3d5f7h9k1m3p5r7t9w1y3", | |
| 2418 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2419 | + | "issue": 12, | |
| 2420 | + | "session_id": "ses_01m4b2c3d4e5f6g7h8j9k0m1n2", | |
| 2421 | + | "actor": { | |
| 2422 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2423 | + | "username": "syntaqx" | |
| 2424 | + | }, | |
| 2425 | + | "harness": "claude_code", | |
| 2426 | + | "note": "Taking the argument parsing first.", | |
| 2427 | + | "state": "active", | |
| 2428 | + | "created_at": "2026-10-12T14:03:02.771Z", | |
| 2429 | + | "expires_at": "2026-10-12T15:01:40.518Z", | |
| 2430 | + | "ended_at": null, | |
| 2431 | + | "end_reason": null, | |
| 2432 | + | "handoff_to": null | |
| 2433 | + | }, | |
| 2434 | + | "others": [ | |
| 2435 | + | { | |
| 2436 | + | "id": "clm_01m4b2a4c6e8g0j2l4n6q8s0u2", | |
| 2437 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2438 | + | "issue": 12, | |
| 2439 | + | "session_id": null, | |
| 2440 | + | "actor": { | |
| 2441 | + | "id": "usr_01kkp2v7d4fh8m3q6t9w2z5c8e", | |
| 2442 | + | "username": "ana" | |
| 2443 | + | }, | |
| 2444 | + | "harness": null, | |
| 2445 | + | "note": "Looking at the tests.", | |
| 2446 | + | "state": "active", | |
| 2447 | + | "created_at": "2026-10-12T13:50:12.000Z", | |
| 2448 | + | "expires_at": "2026-10-12T14:20:12.000Z", | |
| 2449 | + | "ended_at": null, | |
| 2450 | + | "end_reason": null, | |
| 2451 | + | "handoff_to": null | |
| 2452 | + | } | |
| 2453 | + | ], | |
| 2454 | + | "blocked_by": [ | |
| 2455 | + | { | |
| 2456 | + | "number": 9, | |
| 2457 | + | "title": "Read arguments with a parser", | |
| 2458 | + | "state": "open", | |
| 2459 | + | "claims": [ | |
| 2460 | + | { | |
| 2461 | + | "id": "clm_01m4b2a4c6e8g0j2l4n6q8s0u2", | |
| 2462 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2463 | + | "issue": 9, | |
| 2464 | + | "session_id": null, | |
| 2465 | + | "actor": { | |
| 2466 | + | "id": "usr_01kkp2v7d4fh8m3q6t9w2z5c8e", | |
| 2467 | + | "username": "ana" | |
| 2468 | + | }, | |
| 2469 | + | "harness": null, | |
| 2470 | + | "note": null, | |
| 2471 | + | "state": "active", | |
| 2472 | + | "created_at": "2026-10-12T13:50:12.000Z", | |
| 2473 | + | "expires_at": "2026-10-12T14:20:12.000Z", | |
| 2474 | + | "ended_at": null, | |
| 2475 | + | "end_reason": null, | |
| 2476 | + | "handoff_to": null | |
| 2477 | + | } | |
| 2478 | + | ], | |
| 2479 | + | "pulls": [ | |
| 2480 | + | { | |
| 2481 | + | "number": 11, | |
| 2482 | + | "title": "Parse arguments with clap", | |
| 2483 | + | "status": "open" | |
| 2484 | + | } | |
| 2485 | + | ] | |
| 2486 | + | } | |
| 2487 | + | ] | |
| 2488 | + | }, | |
| 2489 | + | "notes": "A claim is advisory: it is made even when `others` lists someone already on the issue. Claiming an issue you hold renews your claim. Refused with `409` for a closed issue. See [Claims](/guides/sessions/#claims)." | |
| 2490 | + | }, | |
| 2491 | + | "release_claim": { | |
| 2492 | + | "request": { | |
| 2493 | + | "reason": "Ana has it covered." | |
| 2494 | + | }, | |
| 2495 | + | "response": { | |
| 2496 | + | "id": "clm_01m4b3d5f7h9k1m3p5r7t9w1y3", | |
| 2497 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2498 | + | "issue": 12, | |
| 2499 | + | "session_id": "ses_01m4b2c3d4e5f6g7h8j9k0m1n2", | |
| 2500 | + | "actor": { | |
| 2501 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2502 | + | "username": "syntaqx" | |
| 2503 | + | }, | |
| 2504 | + | "harness": "claude_code", | |
| 2505 | + | "note": "Taking the argument parsing first.", | |
| 2506 | + | "state": "released", | |
| 2507 | + | "created_at": "2026-10-12T14:03:02.771Z", | |
| 2508 | + | "expires_at": "2026-10-12T15:01:40.518Z", | |
| 2509 | + | "ended_at": "2026-10-12T14:12:30.000Z", | |
| 2510 | + | "end_reason": "Ana has it covered.", | |
| 2511 | + | "handoff_to": null | |
| 2512 | + | } | |
| 2513 | + | }, | |
| 2514 | + | "hand_off_issue": { | |
| 2515 | + | "request": { | |
| 2516 | + | "to": "ana", | |
| 2517 | + | "note": "Parsing is done and tested; the README still needs the new usage." | |
| 2518 | + | }, | |
| 2519 | + | "response": { | |
| 2520 | + | "id": "clm_01m4b3d5f7h9k1m3p5r7t9w1y3", | |
| 2521 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2522 | + | "issue": 12, | |
| 2523 | + | "session_id": "ses_01m4b2c3d4e5f6g7h8j9k0m1n2", | |
| 2524 | + | "actor": { | |
| 2525 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2526 | + | "username": "syntaqx" | |
| 2527 | + | }, | |
| 2528 | + | "harness": "claude_code", | |
| 2529 | + | "note": "Parsing is done and tested; the README still needs the new usage.", | |
| 2530 | + | "state": "handed_off", | |
| 2531 | + | "created_at": "2026-10-12T14:03:02.771Z", | |
| 2532 | + | "expires_at": "2026-10-12T15:01:40.518Z", | |
| 2533 | + | "ended_at": "2026-10-12T14:38:00.000Z", | |
| 2534 | + | "end_reason": "handed_off", | |
| 2535 | + | "handoff_to": "ana" | |
| 2536 | + | } | |
| 2537 | + | }, | |
| 2538 | + | "list_claims": { | |
| 2539 | + | "query": { | |
| 2540 | + | "state": "active" | |
| 2541 | + | }, | |
| 2542 | + | "response": [ | |
| 2543 | + | { | |
| 2544 | + | "id": "clm_01m4b3d5f7h9k1m3p5r7t9w1y3", | |
| 2545 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2546 | + | "issue": 12, | |
| 2547 | + | "session_id": "ses_01m4b2c3d4e5f6g7h8j9k0m1n2", | |
| 2548 | + | "actor": { | |
| 2549 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2550 | + | "username": "syntaqx" | |
| 2551 | + | }, | |
| 2552 | + | "harness": "claude_code", | |
| 2553 | + | "note": "Taking the argument parsing first.", | |
| 2554 | + | "state": "active", | |
| 2555 | + | "created_at": "2026-10-12T14:03:02.771Z", | |
| 2556 | + | "expires_at": "2026-10-12T15:01:40.518Z", | |
| 2557 | + | "ended_at": null, | |
| 2558 | + | "end_reason": null, | |
| 2559 | + | "handoff_to": null | |
| 2560 | + | }, | |
| 2561 | + | { | |
| 2562 | + | "id": "clm_01m4b2a4c6e8g0j2l4n6q8s0u2", | |
| 2563 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2564 | + | "issue": 12, | |
| 2565 | + | "session_id": null, | |
| 2566 | + | "actor": { | |
| 2567 | + | "id": "usr_01kkp2v7d4fh8m3q6t9w2z5c8e", | |
| 2568 | + | "username": "ana" | |
| 2569 | + | }, | |
| 2570 | + | "harness": null, | |
| 2571 | + | "note": "Looking at the tests.", | |
| 2572 | + | "state": "active", | |
| 2573 | + | "created_at": "2026-10-12T13:50:12.000Z", | |
| 2574 | + | "expires_at": "2026-10-12T14:20:12.000Z", | |
| 2575 | + | "ended_at": null, | |
| 2576 | + | "end_reason": null, | |
| 2577 | + | "handoff_to": null | |
| 2578 | + | } | |
| 2579 | + | ] | |
| 2580 | + | }, | |
| 2581 | + | "get_work": { | |
| 2582 | + | "response": { | |
| 2583 | + | "issue": { | |
| 2584 | + | "id": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| 2585 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2586 | + | "number": 12, | |
| 2587 | + | "title": "Greeting should name the caller", | |
| 2588 | + | "body": "Take a name from the first argument; fall back to world.\n\n## Definition of done\n\n- `cargo test` passes.\n- The README shows the new usage.", | |
| 2589 | + | "labels": [ | |
| 2590 | + | "feature", | |
| 2591 | + | "good first issue" | |
| 2592 | + | ], | |
| 2593 | + | "state": "open", | |
| 2594 | + | "reason": null, | |
| 2595 | + | "resolved_by": null, | |
| 2596 | + | "author": { | |
| 2597 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2598 | + | "username": "syntaqx", | |
| 2599 | + | "display_username": "syntaqx", | |
| 2600 | + | "kind": "user", | |
| 2601 | + | "verified": false, | |
| 2602 | + | "workspaces": [] | |
| 2603 | + | }, | |
| 2604 | + | "requested_by": null, | |
| 2605 | + | "created_at": "2026-10-01T18:04:11.482Z", | |
| 2606 | + | "updated_at": "2026-10-01T18:09:47.305Z", | |
| 2607 | + | "closed_at": null, | |
| 2608 | + | "pull_count": 0, | |
| 2609 | + | "comment_count": 0, | |
| 2610 | + | "assignees": [ | |
| 2611 | + | "syntaqx" | |
| 2612 | + | ], | |
| 2613 | + | "blocked_by": [ | |
| 2614 | + | 9 | |
| 2615 | + | ], | |
| 2616 | + | "queued": false, | |
| 2617 | + | "agent": null, | |
| 2618 | + | "milestone": { | |
| 2619 | + | "number": 3, | |
| 2620 | + | "title": "Launch" | |
| 2621 | + | } | |
| 2622 | + | }, | |
| 2623 | + | "definition_of_done": [ | |
| 2624 | + | "`cargo test` passes.", | |
| 2625 | + | "The README shows the new usage." | |
| 2626 | + | ], | |
| 2627 | + | "required_checks": [ | |
| 2628 | + | "CI" | |
| 2629 | + | ], | |
| 2630 | + | "claims": [ | |
| 2631 | + | { | |
| 2632 | + | "id": "clm_01m4b3d5f7h9k1m3p5r7t9w1y3", | |
| 2633 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2634 | + | "issue": 12, | |
| 2635 | + | "session_id": "ses_01m4b2c3d4e5f6g7h8j9k0m1n2", | |
| 2636 | + | "actor": { | |
| 2637 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2638 | + | "username": "syntaqx" | |
| 2639 | + | }, | |
| 2640 | + | "harness": "claude_code", | |
| 2641 | + | "note": "Taking the argument parsing first.", | |
| 2642 | + | "state": "active", | |
| 2643 | + | "created_at": "2026-10-12T14:03:02.771Z", | |
| 2644 | + | "expires_at": "2026-10-12T15:01:40.518Z", | |
| 2645 | + | "ended_at": null, | |
| 2646 | + | "end_reason": null, | |
| 2647 | + | "handoff_to": null | |
| 2648 | + | } | |
| 2649 | + | ], | |
| 2650 | + | "sessions": [ | |
| 2651 | + | { | |
| 2652 | + | "id": "ses_01m4b2c3d4e5f6g7h8j9k0m1n2", | |
| 2653 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2654 | + | "issue": 12, | |
| 2655 | + | "pull": 14, | |
| 2656 | + | "branch": "greet-caller", | |
| 2657 | + | "actor": { | |
| 2658 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2659 | + | "username": "syntaqx" | |
| 2660 | + | }, | |
| 2661 | + | "harness": "claude_code", | |
| 2662 | + | "harness_session_id": "5b0c1f9e-3d2a-4e8b-9f61-0c7d2a9e4b13", | |
| 2663 | + | "title": "Make the greeting name the caller", | |
| 2664 | + | "state": "active", | |
| 2665 | + | "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13", | |
| 2666 | + | "started_at": "2026-10-12T14:02:11.204Z", | |
| 2667 | + | "last_seen_at": "2026-10-12T14:31:40.518Z", | |
| 2668 | + | "ended_at": null, | |
| 2669 | + | "end_reason": null | |
| 2670 | + | } | |
| 2671 | + | ], | |
| 2672 | + | "pulls": [ | |
| 2673 | + | { | |
| 2674 | + | "number": 14, | |
| 2675 | + | "title": "Greeting should name the caller", | |
| 2676 | + | "status": "draft", | |
| 2677 | + | "branch": "greet-caller", | |
| 2678 | + | "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13", | |
| 2679 | + | "author": { | |
| 2680 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2681 | + | "username": "syntaqx" | |
| 2682 | + | }, | |
| 2683 | + | "check_status": null, | |
| 2684 | + | "required_checks": [ | |
| 2685 | + | { | |
| 2686 | + | "name": "CI", | |
| 2687 | + | "state": "success", | |
| 2688 | + | "description": "CI passed", | |
| 2689 | + | "target_url": "https://g1t.sh/syntaqx/hello/actions/runs/run_01m43sw8e2g6j0m4q8t2x6a0c4" | |
| 2690 | + | } | |
| 2691 | + | ], | |
| 2692 | + | "mergeable": "clean", | |
| 2693 | + | "overlaps": [ | |
| 2694 | + | { | |
| 2695 | + | "number": 11, | |
| 2696 | + | "title": "Parse arguments with clap", | |
| 2697 | + | "issue": 9, | |
| 2698 | + | "paths": [ | |
| 2699 | + | "src/main.rs" | |
| 2700 | + | ] | |
| 2701 | + | } | |
| 2702 | + | ], | |
| 2703 | + | "queue": null | |
| 2704 | + | } | |
| 2705 | + | ], | |
| 2706 | + | "blocked_by": [ | |
| 2707 | + | { | |
| 2708 | + | "number": 9, | |
| 2709 | + | "title": "Read arguments with a parser", | |
| 2710 | + | "state": "open", | |
| 2711 | + | "claims": [ | |
| 2712 | + | { | |
| 2713 | + | "id": "clm_01m4b2a4c6e8g0j2l4n6q8s0u2", | |
| 2714 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2715 | + | "issue": 9, | |
| 2716 | + | "session_id": null, | |
| 2717 | + | "actor": { | |
| 2718 | + | "id": "usr_01kkp2v7d4fh8m3q6t9w2z5c8e", | |
| 2719 | + | "username": "ana" | |
| 2720 | + | }, | |
| 2721 | + | "harness": null, | |
| 2722 | + | "note": null, | |
| 2723 | + | "state": "active", | |
| 2724 | + | "created_at": "2026-10-12T13:50:12.000Z", | |
| 2725 | + | "expires_at": "2026-10-12T14:20:12.000Z", | |
| 2726 | + | "ended_at": null, | |
| 2727 | + | "end_reason": null, | |
| 2728 | + | "handoff_to": null | |
| 2729 | + | } | |
| 2730 | + | ], | |
| 2731 | + | "pulls": [ | |
| 2732 | + | { | |
| 2733 | + | "number": 11, | |
| 2734 | + | "title": "Parse arguments with clap", | |
| 2735 | + | "status": "open" | |
| 2736 | + | } | |
| 2737 | + | ] | |
| 2738 | + | } | |
| 2739 | + | ], | |
| 2740 | + | "blocks": [ | |
| 2741 | + | { | |
| 2742 | + | "number": 15, | |
| 2743 | + | "title": "Greet in the caller's language", | |
| 2744 | + | "state": "open" | |
| 2745 | + | } | |
| 2746 | + | ], | |
| 2747 | + | "decisions": [], | |
| 2748 | + | "last_integration_failure": null, | |
| 2749 | + | "recent": [ | |
| 2750 | + | { | |
| 2751 | + | "id": "evt_01m4b3d5f7h9k1m3p5r7t9w1y4", | |
| 2752 | + | "type": "work.claimed", | |
| 2753 | + | "source": "work", | |
| 2754 | + | "time": "2026-10-12T14:03:02.771Z", | |
| 2755 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2756 | + | "actor": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2757 | + | "data": { | |
| 2758 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2759 | + | "number": 12, | |
| 2760 | + | "issue": 12, | |
| 2761 | + | "claim": { | |
| 2762 | + | "id": "clm_01m4b3d5f7h9k1m3p5r7t9w1y3", | |
| 2763 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2764 | + | "issue": 12, | |
| 2765 | + | "session_id": "ses_01m4b2c3d4e5f6g7h8j9k0m1n2", | |
| 2766 | + | "actor": { | |
| 2767 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2768 | + | "username": "syntaqx" | |
| 2769 | + | }, | |
| 2770 | + | "harness": "claude_code", | |
| 2771 | + | "note": "Taking the argument parsing first.", | |
| 2772 | + | "state": "active", | |
| 2773 | + | "created_at": "2026-10-12T14:03:02.771Z", | |
| 2774 | + | "expires_at": "2026-10-12T15:01:40.518Z", | |
| 2775 | + | "ended_at": null, | |
| 2776 | + | "end_reason": null, | |
| 2777 | + | "handoff_to": null | |
| 2778 | + | }, | |
| 2779 | + | "others": [ | |
| 2780 | + | { | |
| 2781 | + | "id": "clm_01m4b2a4c6e8g0j2l4n6q8s0u2", | |
| 2782 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2783 | + | "issue": 12, | |
| 2784 | + | "session_id": null, | |
| 2785 | + | "actor": { | |
| 2786 | + | "id": "usr_01kkp2v7d4fh8m3q6t9w2z5c8e", | |
| 2787 | + | "username": "ana" | |
| 2788 | + | }, | |
| 2789 | + | "harness": null, | |
| 2790 | + | "note": "Looking at the tests.", | |
| 2791 | + | "state": "active", | |
| 2792 | + | "created_at": "2026-10-12T13:50:12.000Z", | |
| 2793 | + | "expires_at": "2026-10-12T14:20:12.000Z", | |
| 2794 | + | "ended_at": null, | |
| 2795 | + | "end_reason": null, | |
| 2796 | + | "handoff_to": null | |
| 2797 | + | } | |
| 2798 | + | ] | |
| 2799 | + | } | |
| 2800 | + | } | |
| 2801 | + | ] | |
| 2802 | + | }, | |
| 2803 | + | "notes": "`pulls` are the issue's pull requests, oldest first; `required_checks`, `mergeable`, `overlaps` and `queue` (its `state` and `position` in the merge queue, 1 at the front) are filled in for those still open or draft. `recent` holds up to 20 coordination events about the issue or its pull requests, newest first. See [Picking up an issue](/guides/sessions/#picking-up-an-issue)." | |
| 2804 | + | }, | |
| 2230 | 2805 | "list_workflows": { | |
| 2231 | 2806 | "response": [ | |
| 2232 | 2807 | { | |
| 18 | 18 | use crate::checks::ChecksOp; | |
| 19 | 19 | use crate::rules::RulesOp; | |
| 20 | 20 | use crate::deploy_keys::DeployKeysOp; | |
| 21 | + | use crate::coordination::CoordinationOp; | |
| 21 | 22 | ||
| 22 | 23 | /// A key as `#[serde(rename_all = "camelCase")]` writes it. | |
| 23 | 24 | fn camel_key(key: &str) -> String { | |
| ⋯ | |||
| 231 | 232 | Op::Checks(ChecksOp::ListCheckRunsForRef) => through::<checks::CheckRunList>(op, sent), | |
| 232 | 233 | Op::Checks(ChecksOp::ListCheckSuitesForRef) => through::<checks::CheckSuiteList>(op, sent), | |
| 233 | 234 | Op::Checks(ChecksOp::GetCheckSuite) => through::<checks::CommitCheckSuite>(op, sent), | |
| 235 | + | Op::Coordination(CoordinationOp::StartSession | CoordinationOp::UpdateSession | CoordinationOp::EndSession) => { | |
| 236 | + | through::<work::Session>(op, sent) | |
| 237 | + | } | |
| 238 | + | Op::Coordination(CoordinationOp::GetSession) => through::<work::SessionDetail>(op, sent), | |
| 239 | + | Op::Coordination(CoordinationOp::ListSessions) => through::<Vec<work::Session>>(op, sent), | |
| 240 | + | Op::Coordination(CoordinationOp::RecordSessionEntries) => through::<work::Recorded>(op, sent), | |
| 241 | + | Op::Coordination(CoordinationOp::ClaimIssue) => through::<work::Claimed>(op, sent), | |
| 242 | + | Op::Coordination(CoordinationOp::ReleaseClaim | CoordinationOp::HandOffIssue) => through::<work::Claim>(op, sent), | |
| 243 | + | Op::Coordination(CoordinationOp::ListClaims) => through::<Vec<work::Claim>>(op, sent), | |
| 244 | + | Op::Coordination(CoordinationOp::GetWork) => through::<work::WorkContext>(op, sent), | |
| 234 | 245 | _ => sent, | |
| 235 | 246 | } | |
| 236 | 247 | } | |
| 10 | 10 | use crate::telemetry::TelemetryOp; | |
| 11 | 11 | use crate::packages::PackagesOp; | |
| 12 | 12 | use crate::folios::FoliosOp; | |
| 13 | + | use crate::coordination::CoordinationOp; | |
| 13 | 14 | use crate::protection::ProtectionOp; | |
| 14 | 15 | use crate::token_policy::TokenOp; | |
| 15 | 16 | use crate::operations::Op; | |
| ⋯ | |||
| 584 | 585 | Op::ImportIssue, | |
| 585 | 586 | &[], | |
| 586 | 587 | ), | |
| 588 | + | // Coordinating work: claims on issues, an issue's whole context, and | |
| 589 | + | // sessions of people's own harnesses. | |
| 590 | + | route("GET", "/repos/:owner/:name/issues/:number/work", Op::Coordination(CoordinationOp::GetWork), &[]), | |
| 591 | + | route("POST", "/repos/:owner/:name/issues/:number/claim", Op::Coordination(CoordinationOp::ClaimIssue), &[]), | |
| 592 | + | route("POST", "/repos/:owner/:name/issues/:number/release", Op::Coordination(CoordinationOp::ReleaseClaim), &[]), | |
| 593 | + | route("POST", "/repos/:owner/:name/issues/:number/handoff", Op::Coordination(CoordinationOp::HandOffIssue), &[]), | |
| 594 | + | route( | |
| 595 | + | "GET", | |
| 596 | + | "/repos/:owner/:name/claims", | |
| 597 | + | Op::Coordination(CoordinationOp::ListClaims), | |
| 598 | + | &[("state", "state"), ("issue", "issue")], | |
| 599 | + | ), | |
| 600 | + | route("POST", "/repos/:owner/:name/sessions", Op::Coordination(CoordinationOp::StartSession), &[]), | |
| 601 | + | route( | |
| 602 | + | "GET", | |
| 603 | + | "/repos/:owner/:name/sessions", | |
| 604 | + | Op::Coordination(CoordinationOp::ListSessions), | |
| 605 | + | &[("state", "state"), ("issue", "issue"), ("pull", "pull")], | |
| 606 | + | ), | |
| 607 | + | route("GET", "/repos/:owner/:name/sessions/:id", Op::Coordination(CoordinationOp::GetSession), &[("after", "after")]), | |
| 608 | + | route("PATCH", "/repos/:owner/:name/sessions/:id", Op::Coordination(CoordinationOp::UpdateSession), &[]), | |
| 609 | + | route("POST", "/repos/:owner/:name/sessions/:id/end", Op::Coordination(CoordinationOp::EndSession), &[]), | |
| 610 | + | route( | |
| 611 | + | "POST", | |
| 612 | + | "/repos/:owner/:name/sessions/:id/entries", | |
| 613 | + | Op::Coordination(CoordinationOp::RecordSessionEntries), | |
| 614 | + | &[], | |
| 615 | + | ), | |
| 587 | 616 | route( | |
| 588 | 617 | "GET", | |
| 589 | 618 | "/repos/:owner/:name/context", | |
| 26 | 26 | use crate::telemetry::TelemetryOp; | |
| 27 | 27 | use crate::packages::PackagesOp; | |
| 28 | 28 | use crate::folios::FoliosOp; | |
| 29 | + | use crate::coordination::CoordinationOp; | |
| 29 | 30 | use crate::protection::ProtectionOp; | |
| 30 | 31 | use crate::token_policy::TokenOp; | |
| 31 | 32 | use crate::operations::Op; | |
| ⋯ | |||
| 134 | 135 | Tool { | |
| 135 | 136 | name: "issue", | |
| 136 | 137 | title: "Issues", | |
| 137 | − | description: "Issues: what should change. Read one before working on it to see the pull requests already made for it. Issues and pull requests share numbers; `comment` works on either.", | |
| 138 | + | description: "Issues: what should change. Before working on one, `work` gives everything about it (definition of done, who is on it, its pull requests, what it waits on); `claim` says you are on it (advisory and expiring, never a lock), `release` or `handoff` when you stop. `update` with blocked_by sets the issues that must land first. Issues and pull requests share numbers; `comment` works on either.", | |
| 138 | 139 | default_action: None, | |
| 139 | 140 | actions: &[ | |
| 140 | 141 | a("list", Op::ListIssues, "Issues on a repository, newest first"), | |
| 141 | 142 | a("get", Op::GetIssue, "One issue with comments and its pull requests"), | |
| 142 | 143 | a("create", Op::CreateIssue, "Open an issue"), | |
| 143 | − | a("update", Op::UpdateIssue, "Change title, body, labels, milestone or assignees"), | |
| 144 | + | a("update", Op::UpdateIssue, "Change title, body, labels, milestone, assignees or blocked_by"), | |
| 144 | 145 | a("labels", Op::ListIssueLabels, "The labels an issue or pull request carries"), | |
| 145 | 146 | a("add_labels", Op::AddIssueLabels, "Add labels to an issue or pull request"), | |
| 146 | 147 | a("set_labels", Op::SetIssueLabels, "Replace the labels of an issue or pull request"), | |
| ⋯ | |||
| 151 | 152 | a("edit_comment", Op::EditComment, "Change a comment's text: your own, or any with the Maintain role"), | |
| 152 | 153 | a("delete_comment", Op::DeleteComment, "Delete a comment: your own, or any with the Maintain role"), | |
| 153 | 154 | a("import", Op::ImportIssue, "Open an issue from a Jira, Linear or Sentry item"), | |
| 155 | + | a("work", Op::Coordination(CoordinationOp::GetWork), "Everything to pick it up: done means, claims, sessions, pull requests, blockers, decisions, recent events"), | |
| 156 | + | a("claim", Op::Coordination(CoordinationOp::ClaimIssue), "Say you are working on it; lists who else is, and what it is blocked by"), | |
| 157 | + | a("release", Op::Coordination(CoordinationOp::ReleaseClaim), "Stop working on it"), | |
| 158 | + | a("handoff", Op::Coordination(CoordinationOp::HandOffIssue), "Hand it off, to someone or anyone, with a note on where it stands"), | |
| 159 | + | a("list_claims", Op::Coordination(CoordinationOp::ListClaims), "Claims on a repository's issues: who is working on what"), | |
| 154 | 160 | ], | |
| 155 | 161 | }, | |
| 156 | 162 | Tool { | |
| ⋯ | |||
| 178 | 184 | ], | |
| 179 | 185 | }, | |
| 180 | 186 | Tool { | |
| 187 | + | name: "session", | |
| 188 | + | title: "Sessions", | |
| 189 | + | description: "Your harness session on a repository, so others see who is working on what: `start` once (idempotent on harness_session_id), `update` as a heartbeat or to attach an issue, pull request, branch or head commit, `record` prompts, reasoning and tool calls (idempotent on key), `end` when done. A session goes idle after 15 minutes without a heartbeat and ends after 2 hours.", | |
| 190 | + | default_action: None, | |
| 191 | + | actions: &[ | |
| 192 | + | a("start", Op::Coordination(CoordinationOp::StartSession), "Start one, or get the one started with this harness_session_id"), | |
| 193 | + | a("update", Op::Coordination(CoordinationOp::UpdateSession), "Heartbeat; or attach an issue, pull request, branch, head commit or title"), | |
| 194 | + | a("record", Op::Coordination(CoordinationOp::RecordSessionEntries), "Record prompts, reasoning and tool calls; a key makes each idempotent"), | |
| 195 | + | a("end", Op::Coordination(CoordinationOp::EndSession), "End it"), | |
| 196 | + | a("get", Op::Coordination(CoordinationOp::GetSession), "One session and its entries"), | |
| 197 | + | a("list", Op::Coordination(CoordinationOp::ListSessions), "A repository's sessions: active, idle or ended, by issue or pull request"), | |
| 198 | + | ], | |
| 199 | + | }, | |
| 200 | + | Tool { | |
| 181 | 201 | name: "memory", | |
| 182 | 202 | title: "Memory", | |
| 183 | 203 | description: "What the project and its workspace remember for the next agent: how to build, conventions, decisions, traps. Recall before you start; remember one short fact at a time, never a secret.", | |
| ⋯ | |||
| 846 | 866 | assert!(tool.action(default).is_some(), "{}", tool.name); | |
| 847 | 867 | } | |
| 848 | 868 | } | |
| 849 | − | assert!(TOOLS.len() <= 18, "{} tools", TOOLS.len()); | |
| 869 | + | assert!(TOOLS.len() <= 19, "{} tools", TOOLS.len()); | |
| 850 | 870 | } | |
| 851 | 871 | ||
| 852 | 872 | #[test] | |
| ⋯ | |||
| 911 | 931 | assert_eq!(tool["annotations"]["destructiveHint"], false); | |
| 912 | 932 | } | |
| 913 | 933 | let issue = tools.iter().find(|tool| tool["name"] == "issue").unwrap(); | |
| 914 | − | assert_eq!(issue["inputSchema"]["properties"]["action"]["enum"], json!(["list", "get", "labels"])); | |
| 934 | + | assert_eq!(issue["inputSchema"]["properties"]["action"]["enum"], json!(["list", "get", "labels", "work", "list_claims"])); | |
| 915 | 935 | } | |
| 916 | 936 | ||
| 917 | 937 | #[test] | |
Binary or large file; its contents are not shown.
| 1206 | 1206 | ], | |
| 1207 | 1207 | }, | |
| 1208 | 1208 | Operation { | |
| 1209 | + | id: "claim_issue", | |
| 1210 | + | method: "POST", | |
| 1211 | + | path: "/repos/{owner}/{name}/issues/{number}/claim", | |
| 1212 | + | tag: "Claims and work", | |
| 1213 | + | summary: "Claim an issue", | |
| 1214 | + | scope: Some("issues:write"), | |
| 1215 | + | params: &[ | |
| 1216 | + | Param { name: "owner", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 1217 | + | Param { name: "name", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 1218 | + | Param { name: "number", location: In::Path, kind: "integer", required: true, values: &[] }, | |
| 1219 | + | ], | |
| 1220 | + | body: &[ | |
| 1221 | + | Field { name: "note", kind: "string", required: false, values: &[] }, | |
| 1222 | + | Field { name: "session_id", kind: "string", required: false, values: &[] }, | |
| 1223 | + | Field { name: "ttl_minutes", kind: "integer", required: false, values: &[] }, | |
| 1224 | + | ], | |
| 1225 | + | }, | |
| 1226 | + | Operation { | |
| 1227 | + | id: "get_work", | |
| 1228 | + | method: "GET", | |
| 1229 | + | path: "/repos/{owner}/{name}/issues/{number}/work", | |
| 1230 | + | tag: "Claims and work", | |
| 1231 | + | summary: "Get an issue's work context", | |
| 1232 | + | scope: Some("issues:read"), | |
| 1233 | + | params: &[ | |
| 1234 | + | Param { name: "owner", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 1235 | + | Param { name: "name", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 1236 | + | Param { name: "number", location: In::Path, kind: "integer", required: true, values: &[] }, | |
| 1237 | + | ], | |
| 1238 | + | body: &[], | |
| 1239 | + | }, | |
| 1240 | + | Operation { | |
| 1241 | + | id: "hand_off_issue", | |
| 1242 | + | method: "POST", | |
| 1243 | + | path: "/repos/{owner}/{name}/issues/{number}/handoff", | |
| 1244 | + | tag: "Claims and work", | |
| 1245 | + | summary: "Hand off an issue", | |
| 1246 | + | scope: Some("issues:write"), | |
| 1247 | + | params: &[ | |
| 1248 | + | Param { name: "owner", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 1249 | + | Param { name: "name", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 1250 | + | Param { name: "number", location: In::Path, kind: "integer", required: true, values: &[] }, | |
| 1251 | + | ], | |
| 1252 | + | body: &[ | |
| 1253 | + | Field { name: "note", kind: "string", required: true, values: &[] }, | |
| 1254 | + | Field { name: "to", kind: "string", required: false, values: &[] }, | |
| 1255 | + | ], | |
| 1256 | + | }, | |
| 1257 | + | Operation { | |
| 1258 | + | id: "list_claims", | |
| 1259 | + | method: "GET", | |
| 1260 | + | path: "/repos/{owner}/{name}/claims", | |
| 1261 | + | tag: "Claims and work", | |
| 1262 | + | summary: "List claims", | |
| 1263 | + | scope: Some("issues:read"), | |
| 1264 | + | params: &[ | |
| 1265 | + | Param { name: "owner", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 1266 | + | Param { name: "name", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 1267 | + | Param { name: "state", location: In::Query, kind: "string", required: false, values: &["active", "released", "expired", "handed_off"] }, | |
| 1268 | + | Param { name: "issue", location: In::Query, kind: "integer", required: false, values: &[] }, | |
| 1269 | + | ], | |
| 1270 | + | body: &[], | |
| 1271 | + | }, | |
| 1272 | + | Operation { | |
| 1273 | + | id: "release_claim", | |
| 1274 | + | method: "POST", | |
| 1275 | + | path: "/repos/{owner}/{name}/issues/{number}/release", | |
| 1276 | + | tag: "Claims and work", | |
| 1277 | + | summary: "Release a claim", | |
| 1278 | + | scope: Some("issues:write"), | |
| 1279 | + | params: &[ | |
| 1280 | + | Param { name: "owner", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 1281 | + | Param { name: "name", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 1282 | + | Param { name: "number", location: In::Path, kind: "integer", required: true, values: &[] }, | |
| 1283 | + | ], | |
| 1284 | + | body: &[ | |
| 1285 | + | Field { name: "reason", kind: "string", required: false, values: &[] }, | |
| 1286 | + | ], | |
| 1287 | + | }, | |
| 1288 | + | Operation { | |
| 1209 | 1289 | id: "get_code_scanning_alert", | |
| 1210 | 1290 | method: "GET", | |
| 1211 | 1291 | path: "/repos/{owner}/{name}/code-scanning/alerts/{number}", | |
| ⋯ | |||
| 1854 | 1934 | ], | |
| 1855 | 1935 | body: &[ | |
| 1856 | 1936 | Field { name: "assignees", kind: "array", required: false, values: &[] }, | |
| 1937 | + | Field { name: "blocked_by", kind: "array", required: false, values: &[] }, | |
| 1857 | 1938 | Field { name: "body", kind: "string", required: false, values: &[] }, | |
| 1858 | 1939 | Field { name: "labels", kind: "array", required: false, values: &[] }, | |
| 1859 | 1940 | Field { name: "milestone", kind: "integer", required: false, values: &[] }, | |
| ⋯ | |||
| 5444 | 5525 | ], | |
| 5445 | 5526 | }, | |
| 5446 | 5527 | Operation { | |
| 5528 | + | id: "end_session", | |
| 5529 | + | method: "POST", | |
| 5530 | + | path: "/repos/{owner}/{name}/sessions/{id}/end", | |
| 5531 | + | tag: "Sessions", | |
| 5532 | + | summary: "End a session", | |
| 5533 | + | scope: Some("pull_requests:write"), | |
| 5534 | + | params: &[ | |
| 5535 | + | Param { name: "owner", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 5536 | + | Param { name: "name", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 5537 | + | Param { name: "id", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 5538 | + | ], | |
| 5539 | + | body: &[ | |
| 5540 | + | Field { name: "reason", kind: "string", required: false, values: &[] }, | |
| 5541 | + | ], | |
| 5542 | + | }, | |
| 5543 | + | Operation { | |
| 5544 | + | id: "get_session", | |
| 5545 | + | method: "GET", | |
| 5546 | + | path: "/repos/{owner}/{name}/sessions/{id}", | |
| 5547 | + | tag: "Sessions", | |
| 5548 | + | summary: "Get a session", | |
| 5549 | + | scope: Some("pull_requests:read"), | |
| 5550 | + | params: &[ | |
| 5551 | + | Param { name: "owner", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 5552 | + | Param { name: "name", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 5553 | + | Param { name: "id", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 5554 | + | Param { name: "after", location: In::Query, kind: "integer", required: false, values: &[] }, | |
| 5555 | + | ], | |
| 5556 | + | body: &[], | |
| 5557 | + | }, | |
| 5558 | + | Operation { | |
| 5559 | + | id: "list_sessions", | |
| 5560 | + | method: "GET", | |
| 5561 | + | path: "/repos/{owner}/{name}/sessions", | |
| 5562 | + | tag: "Sessions", | |
| 5563 | + | summary: "List sessions", | |
| 5564 | + | scope: Some("pull_requests:read"), | |
| 5565 | + | params: &[ | |
| 5566 | + | Param { name: "owner", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 5567 | + | Param { name: "name", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 5568 | + | Param { name: "state", location: In::Query, kind: "string", required: false, values: &["active", "idle", "ended"] }, | |
| 5569 | + | Param { name: "issue", location: In::Query, kind: "integer", required: false, values: &[] }, | |
| 5570 | + | Param { name: "pull", location: In::Query, kind: "integer", required: false, values: &[] }, | |
| 5571 | + | ], | |
| 5572 | + | body: &[], | |
| 5573 | + | }, | |
| 5574 | + | Operation { | |
| 5447 | 5575 | id: "read_session", | |
| 5448 | 5576 | method: "GET", | |
| 5449 | 5577 | path: "/repos/{owner}/{name}/pulls/{number}/session", | |
| ⋯ | |||
| 5475 | 5603 | ], | |
| 5476 | 5604 | }, | |
| 5477 | 5605 | Operation { | |
| 5606 | + | id: "record_session_entries", | |
| 5607 | + | method: "POST", | |
| 5608 | + | path: "/repos/{owner}/{name}/sessions/{id}/entries", | |
| 5609 | + | tag: "Sessions", | |
| 5610 | + | summary: "Record entries in a session", | |
| 5611 | + | scope: Some("pull_requests:write"), | |
| 5612 | + | params: &[ | |
| 5613 | + | Param { name: "owner", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 5614 | + | Param { name: "name", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 5615 | + | Param { name: "id", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 5616 | + | ], | |
| 5617 | + | body: &[ | |
| 5618 | + | Field { name: "entries", kind: "array", required: true, values: &[] }, | |
| 5619 | + | ], | |
| 5620 | + | }, | |
| 5621 | + | Operation { | |
| 5622 | + | id: "start_session", | |
| 5623 | + | method: "POST", | |
| 5624 | + | path: "/repos/{owner}/{name}/sessions", | |
| 5625 | + | tag: "Sessions", | |
| 5626 | + | summary: "Start a session", | |
| 5627 | + | scope: Some("pull_requests:write"), | |
| 5628 | + | params: &[ | |
| 5629 | + | Param { name: "owner", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 5630 | + | Param { name: "name", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 5631 | + | ], | |
| 5632 | + | body: &[ | |
| 5633 | + | Field { name: "branch", kind: "string", required: false, values: &[] }, | |
| 5634 | + | Field { name: "harness", kind: "string", required: true, values: &["claude_code", "codex", "cursor", "copilot", "other"] }, | |
| 5635 | + | Field { name: "harness_session_id", kind: "string", required: false, values: &[] }, | |
| 5636 | + | Field { name: "head_commit", kind: "string", required: false, values: &[] }, | |
| 5637 | + | Field { name: "issue", kind: "integer", required: false, values: &[] }, | |
| 5638 | + | Field { name: "pull", kind: "integer", required: false, values: &[] }, | |
| 5639 | + | Field { name: "title", kind: "string", required: false, values: &[] }, | |
| 5640 | + | ], | |
| 5641 | + | }, | |
| 5642 | + | Operation { | |
| 5643 | + | id: "update_session", | |
| 5644 | + | method: "PATCH", | |
| 5645 | + | path: "/repos/{owner}/{name}/sessions/{id}", | |
| 5646 | + | tag: "Sessions", | |
| 5647 | + | summary: "Update a session", | |
| 5648 | + | scope: Some("pull_requests:write"), | |
| 5649 | + | params: &[ | |
| 5650 | + | Param { name: "owner", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 5651 | + | Param { name: "name", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 5652 | + | Param { name: "id", location: In::Path, kind: "string", required: true, values: &[] }, | |
| 5653 | + | ], | |
| 5654 | + | body: &[ | |
| 5655 | + | Field { name: "branch", kind: "string", required: false, values: &[] }, | |
| 5656 | + | Field { name: "head_commit", kind: "string", required: false, values: &[] }, | |
| 5657 | + | Field { name: "issue", kind: "integer", required: false, values: &[] }, | |
| 5658 | + | Field { name: "pull", kind: "integer", required: false, values: &[] }, | |
| 5659 | + | Field { name: "title", kind: "string", required: false, values: &[] }, | |
| 5660 | + | ], | |
| 5661 | + | }, | |
| 5662 | + | Operation { | |
| 5478 | 5663 | id: "check_starred", | |
| 5479 | 5664 | method: "GET", | |
| 5480 | 5665 | path: "/user/starred/{owner}/{name}", | |
Comments
Sign in to comment.