Skip to content

Commit

Merge branch 'worktree-agent-ab8033ab973f8d99a'

syntaqxcommitted Parents271fec1b548a9aBrowse files
33 files+790−1617 files not counted0/33 viewed
+1−0
4040 mod runners;
4141 mod security;
4242 mod tools;
43+mod why;
4344 mod token_policy;
4445 mod toolkit;
4546
+2−1
1414 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.
1515 Find a repository: account whoami lists your workspaces; repository list or search finds one.
1616 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.
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. pull_request record_decision keeps a decision (why the change is made this way, or what fixed a failure) and list_decisions reads them; repository why says why a line of code is the way it is.
18+Merge queue: when your pull request fails combined with the ones ahead of it, g1t says which of them it breaks with and the check that failed (on the pull request, its issue and its session, and as integration.failed). Fix it and push: once its checks pass, it rejoins the queue by itself.
1819 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.
1920 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.
2021 Production: observability overview and errors show what a project's running software did; follow a request or trace to its logs.";
+11−0
377377 &[Op::ReadSession, Op::RecordSession],
378378 ),
379379 (
380+ "Decisions and why",
381+ "Decision records: why the work is the way it is, and how sure that is (claimed, tested by a check or the merge queue, or approved by a person). And why a line of code is the way it is: its commit, pull request, issue, decisions and session.",
382+ &[Op::ListDecisions, Op::RecordDecision, Op::Why],
383+ ),
384+ (
380385 "Memory",
381386 "What agents and people learned that the next agent should know, for one project or across a workspace. Members only; never a secret.",
382387 &[Op::Remember, Op::Recall],
657662 Op::CreatePullRequest => "Create a pull request",
658663 Op::RecordSession => "Record session entries",
659664 Op::ReadSession => "Read a session",
665+ Op::RecordDecision => "Record a decision",
666+ Op::ListDecisions => "List a pull request's decisions",
667+ Op::Why => "Explain a line",
660668 Op::MarkPullRequestReady => "Mark a pull request ready",
661669 Op::ClosePullRequest => "Close a pull request",
662670 Op::ReopenPullRequest => "Reopen a pull request",
871879 ("DELETE", ":label") if route.path.contains("/issues/:number/") => "remove_issue_label".to_owned(),
872880 ("DELETE", "saved") => "unsave_thread".to_owned(),
873881 ("DELETE", "snooze") => "unsnooze_thread".to_owned(),
882+ // An issue's decisions, by its number.
883+ ("GET", "decisions") if route.path.contains("/issues/:number/") => "list_issue_decisions".to_owned(),
874884 _ => op.name().to_owned(),
875885 };
876886 if route.path.starts_with("/workspaces/") && ROUTES.iter().any(|other| other.op == op && other.path.starts_with("/repos/")) {
899909 "delete_issue_subscription" => "Unsubscribe from an issue",
900910 "unsave_thread" => "Unsave a thread",
901911 "unsnooze_thread" => "Bring a snoozed thread back",
912+ "list_issue_decisions" => "List an issue's decisions",
902913 _ => title(route.op),
903914 };
904915 if id.ends_with("_for_workspace") {
+84−1
184184 UpdatePullRequest,
185185 RecordSession,
186186 ReadSession,
187+ RecordDecision,
188+ ListDecisions,
189+ Why,
187190 MarkPullRequestReady,
188191 ConvertPullRequestToDraft,
189192 ClosePullRequest,
686689 }
687690
688691 impl Op {
689− pub const ALL: [Op; 353] = [
692+ pub const ALL: [Op; 356] = [
690693 Op::Whoami,
691694 Op::GetWorkspace,
692695 Op::CreateWorkspace,
761764 Op::UpdatePullRequest,
762765 Op::RecordSession,
763766 Op::ReadSession,
767+ Op::RecordDecision,
768+ Op::ListDecisions,
769+ Op::Why,
764770 Op::MarkPullRequestReady,
765771 Op::ConvertPullRequestToDraft,
766772 Op::ClosePullRequest,
11231129 Op::UpdatePullRequest => "update_pull_request",
11241130 Op::RecordSession => "record_session",
11251131 Op::ReadSession => "read_session",
1132+ Op::RecordDecision => "record_decision",
1133+ Op::ListDecisions => "list_decisions",
1134+ Op::Why => "why",
11261135 Op::MarkPullRequestReady => "mark_pull_request_ready",
11271136 Op::ClosePullRequest => "close_pull_request",
11281137 Op::ReopenPullRequest => "reopen_pull_request",
14531462 "Append entries to a pull request's session: the prompt you were given, your reasoning, the tools you ran. This is how people later see why a change was made, so record as you work, not only at the end."
14541463 }
14551464 Op::ReadSession => "The recorded session of a pull request, oldest entry first.",
1465+ Op::RecordDecision => {
1466+ "Record a decision on a pull request: why the change is made the way it is (proposal), something you say is true of it (claim), what you changed to fix a failure (fix), or a note. It is kept with confidence claimed, attributed to you, with the issue the pull request is for, and published as decision.recorded. evidence is an object of what it rests on, such as {\"commit\": \"…\", \"check\": \"CI\"}; session_id names the session it was made in. g1t records the decisions it saw proved itself: integration_failure and integration_passed when the merge queue combines pull requests (confidence tested), approval when a person approves (approved), and merge."
1467+ }
1468+ Op::ListDecisions => {
1469+ "The decision records on an issue (with those on its pull requests) or on a pull request, oldest first: each with its kind (proposal, claim, integration_failure, integration_passed, fix, approval, merge or note), its confidence (claimed: someone said so; tested: a check or a combination of pull requests proved it; approved: a person approved it), who recorded it (absent for g1t), its summary and its evidence. Issues and pull requests share numbers, so number names either."
1470+ }
1471+ Op::Why => {
1472+ "Why a line of a file is the way it is: the commit that last changed it (by blame, at ref or the default branch), the merged pull request it arrived in, that pull request's issue, the decision records on the pull request, and the session entries recorded around that commit (what was said and the tool calls that touched the file, at most eight). pull, issue, decisions and sessions are empty when the commit did not arrive through a pull request."
1473+ }
14561474 Op::MarkPullRequestReady => {
14571475 "Mark a draft pull request ready for review. Push your commits first. The summary becomes its description and should say what changed and why."
14581476 }
24082426 })),
24092427 &["repo", "number"],
24102428 ),
2429+ Op::RecordDecision => object(
2430+ numbered(json!({
2431+ "kind": {
2432+ "type": "string",
2433+ "enum": ["proposal", "claim", "fix", "note"],
2434+ "description": "What it is: why the change is made this way (proposal), something true of it (claim), what fixed a failure (fix), or a note.",
2435+ },
2436+ "summary": { "type": "string", "description": "The decision, in a sentence or two. At most 2,000 characters." },
2437+ "evidence": {
2438+ "type": "object",
2439+ "description": "What it rests on, such as a commit, a check's name or the pull requests involved. At most 8 KB of JSON.",
2440+ },
2441+ "session_id": { "type": "string", "description": "The session it was made in, if you have one." },
2442+ })),
2443+ &["repo", "number", "kind", "summary"],
2444+ ),
2445+ Op::ListDecisions => object(numbered(json!({})), &["repo", "number"]),
2446+ Op::Why => object(
2447+ json!({
2448+ "repo": repo_schema(),
2449+ "path": { "type": "string", "description": "The file, from the repository's root, such as \"src/main.rs\"." },
2450+ "line": { "type": "integer", "description": "The line, counted from 1." },
2451+ "ref": { "type": "string", "description": "A branch, tag or commit. The default branch if not given." },
2452+ }),
2453+ &["repo", "path", "line"],
2454+ ),
24112455 Op::MarkPullRequestReady => object(
24122456 numbered(json!({ "summary": { "type": "string", "description": "Markdown." } })),
24132457 &["repo", "number", "summary"],
31463190 | Op::ListPullRequests
31473191 | Op::GetPullRequest
31483192 | Op::ReadSession
3193+ | Op::ListDecisions
3194+ | Op::Why
31493195 | Op::GetPullRequestChanges
31503196 | Op::ListEvents
31513197 | Op::GetRepoSettings
44454491 .await
44464492 }
44474493 Op::ReadSession => pass(work, "read_session", &view()).await,
4494+ Op::RecordDecision => {
4495+ let Some(kind) = input["kind"].as_str().and_then(g1t_contracts::decisions::DecisionKind::parse) else {
4496+ return failed(FailureCode::Invalid, "kind must be proposal, claim, fix or note.");
4497+ };
4498+ pass(
4499+ work,
4500+ "record_decision",
4501+ &g1t_contracts::decisions::RecordDecisionArgs {
4502+ actor: actor(),
4503+ repo,
4504+ number,
4505+ kind,
4506+ summary: text(input, "summary"),
4507+ evidence: input.get("evidence").cloned(),
4508+ session_id: optional_text(input, "session_id"),
4509+ },
4510+ )
4511+ .await
4512+ }
4513+ Op::ListDecisions => {
4514+ pass(
4515+ work,
4516+ "list_decisions",
4517+ &g1t_contracts::decisions::ListDecisionsArgs { repo, viewer: viewer.clone(), number },
4518+ )
4519+ .await
4520+ }
4521+ Op::Why => {
4522+ let Some(line) = integer(input, "line").filter(|line| *line > 0) else {
4523+ return failed(FailureCode::Invalid, "Give line, counted from 1.");
4524+ };
4525+ let path = text(input, "path");
4526+ if path.trim().is_empty() {
4527+ return failed(FailureCode::Invalid, "Give path: the file, from the repository's root.");
4528+ }
4529+ crate::why::why(services, viewer, repo, path, line, optional_text(input, "ref")).await
4530+ }
44484531 Op::MarkPullRequestReady => pass(work, "ready_pull", &pull_action()).await,
44494532 Op::ClosePullRequest => pass(work, "close_pull", &pull_action()).await,
44504533 Op::ReopenPullRequest => pass(work, "reopen_pull", &pull_action()).await,
+172−0
22272227 },
22282228 "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/)."
22292229 },
2230+ "record_decision": {
2231+ "request": {
2232+ "kind": "fix",
2233+ "summary": "Renamed the config key #12 also introduced, so both read retry_limit.",
2234+ "evidence": {
2235+ "commit": "7d3e9a1c5b2f4e6a8c0d2e4f6a8b0c2d4e6f8a0b"
2236+ },
2237+ "session_id": "ses_01kpz7x9y1z3a5b7c9d1e3f5gh"
2238+ },
2239+ "response": {
2240+ "id": "dec_01kpz9a1b3c5d7e9f1g3h5j7kb",
2241+ "issue": 9,
2242+ "pull": 14,
2243+ "session_id": "ses_01kpz7x9y1z3a5b7c9d1e3f5gh",
2244+ "actor": "ana",
2245+ "kind": "fix",
2246+ "confidence": "claimed",
2247+ "summary": "Renamed the config key #12 also introduced, so both read retry_limit.",
2248+ "evidence": {
2249+ "commit": "7d3e9a1c5b2f4e6a8c0d2e4f6a8b0c2d4e6f8a0b"
2250+ },
2251+ "created_at": "2026-10-12T15:31:40.019Z"
2252+ },
2253+ "notes": "Kept as `claimed`. g1t records `integration_failure`, `integration_passed`, `approval` and `merge` itself. Publishes `decision.recorded`. See [decision records](/guides/why-blame/#decision-records)."
2254+ },
2255+ "list_decisions": {
2256+ "response": [
2257+ {
2258+ "id": "dec_01kpz8m2q4r6s8t0v2w4x6y8za",
2259+ "issue": 9,
2260+ "pull": 14,
2261+ "session_id": null,
2262+ "actor": null,
2263+ "kind": "integration_failure",
2264+ "confidence": "tested",
2265+ "summary": "#14 failed in the merge queue together with #12: `CI / merge_group` failed when they were combined.",
2266+ "evidence": {
2267+ "commit": "4be1c0d9a2f3e4b5c6d7e8f90a1b2c3d4e5f6a7b",
2268+ "check": {
2269+ "name": "CI / merge_group",
2270+ "conclusion": "failure",
2271+ "url": "https://g1t.sh/acme/web/actions/runs/run_01kpz8k0"
2272+ },
2273+ "combination": [
2274+ 11,
2275+ 12,
2276+ 14
2277+ ],
2278+ "other_pulls": [
2279+ 11,
2280+ 12
2281+ ],
2282+ "suspects": [
2283+ 12
2284+ ],
2285+ "bisected": true
2286+ },
2287+ "created_at": "2026-10-12T15:04:11.204Z"
2288+ },
2289+ {
2290+ "id": "dec_01kpz9a1b3c5d7e9f1g3h5j7kb",
2291+ "issue": 9,
2292+ "pull": 14,
2293+ "session_id": "ses_01kpz7x9y1z3a5b7c9d1e3f5gh",
2294+ "actor": "ana",
2295+ "kind": "fix",
2296+ "confidence": "claimed",
2297+ "summary": "Renamed the config key #12 also introduced, so both read retry_limit.",
2298+ "evidence": {
2299+ "commit": "7d3e9a1c5b2f4e6a8c0d2e4f6a8b0c2d4e6f8a0b"
2300+ },
2301+ "created_at": "2026-10-12T15:31:40.019Z"
2302+ }
2303+ ],
2304+ "notes": "`GET /repos/{owner}/{name}/issues/{number}/decisions` lists an issue's, with those on its pull requests."
2305+ },
2306+ "why": {
2307+ "query": {
2308+ "path": "src/config.rs",
2309+ "line": 42
2310+ },
2311+ "response": {
2312+ "path": "src/config.rs",
2313+ "line": 42,
2314+ "commit": {
2315+ "hash": "7d3e9a1c5b2f4e6a8c0d2e4f6a8b0c2d4e6f8a0b",
2316+ "tree_hash": "c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0",
2317+ "message": "Read retry_limit, as #12 names it",
2318+ "author": {
2319+ "name": "Ana Lima",
2320+ "email": "ana@example.com"
2321+ },
2322+ "parents": [
2323+ "4be1c0d9a2f3e4b5c6d7e8f90a1b2c3d4e5f6a7b"
2324+ ],
2325+ "authored_at": "2026-10-12T15:29:02Z"
2326+ },
2327+ "pull": {
2328+ "number": 14,
2329+ "title": "Retry failed webhook deliveries",
2330+ "author": "ana",
2331+ "agent": "claude-code",
2332+ "merged_by": "g1t",
2333+ "merged_at": "2026-10-12T15:52:18.330Z"
2334+ },
2335+ "issue": {
2336+ "number": 9,
2337+ "title": "Webhook deliveries are not retried",
2338+ "state": "closed",
2339+ "body": "A delivery that times out is never sent again."
2340+ },
2341+ "decisions": [
2342+ {
2343+ "id": "dec_01kpz8m2q4r6s8t0v2w4x6y8za",
2344+ "issue": 9,
2345+ "pull": 14,
2346+ "session_id": null,
2347+ "actor": null,
2348+ "kind": "integration_failure",
2349+ "confidence": "tested",
2350+ "summary": "#14 failed in the merge queue together with #12: `CI / merge_group` failed when they were combined.",
2351+ "evidence": {
2352+ "commit": "4be1c0d9a2f3e4b5c6d7e8f90a1b2c3d4e5f6a7b",
2353+ "check": {
2354+ "name": "CI / merge_group",
2355+ "conclusion": "failure",
2356+ "url": "https://g1t.sh/acme/web/actions/runs/run_01kpz8k0"
2357+ },
2358+ "combination": [
2359+ 11,
2360+ 12,
2361+ 14
2362+ ],
2363+ "other_pulls": [
2364+ 11,
2365+ 12
2366+ ],
2367+ "suspects": [
2368+ 12
2369+ ],
2370+ "bisected": true
2371+ },
2372+ "created_at": "2026-10-12T15:04:11.204Z"
2373+ },
2374+ {
2375+ "id": "dec_01kpz9a1b3c5d7e9f1g3h5j7kb",
2376+ "issue": 9,
2377+ "pull": 14,
2378+ "session_id": "ses_01kpz7x9y1z3a5b7c9d1e3f5gh",
2379+ "actor": "ana",
2380+ "kind": "fix",
2381+ "confidence": "claimed",
2382+ "summary": "Renamed the config key #12 also introduced, so both read retry_limit.",
2383+ "evidence": {
2384+ "commit": "7d3e9a1c5b2f4e6a8c0d2e4f6a8b0c2d4e6f8a0b"
2385+ },
2386+ "created_at": "2026-10-12T15:31:40.019Z"
2387+ }
2388+ ],
2389+ "sessions": [
2390+ {
2391+ "seq": 31,
2392+ "kind": "message",
2393+ "text": "#12 renamed retries to retry_limit; reading the new name here.",
2394+ "tool": null,
2395+ "commit": null,
2396+ "at": "2026-10-12T15:27:40.118Z"
2397+ }
2398+ ]
2399+ },
2400+ "notes": "`ref` reads the file at a branch, tag or commit. See [sessions and why-blame](/guides/why-blame/#why-over-the-api)."
2401+ },
22302402 "list_workflows": {
22312403 "response": [
22322404 {
+3−0
167167 Op::GetRepoSettings | Op::UpdateRepoSettings => through::<work::RepoSettings>(op, sent),
168168 Op::ListCheckNames => through::<Vec<work::SeenCheck>>(op, sent),
169169 Op::GetMergeQueue => through::<work::QueueView>(op, sent),
170+ Op::ListDecisions => through::<Vec<g1t_contracts::decisions::Decision>>(op, sent),
171+ Op::RecordDecision => through::<g1t_contracts::decisions::Decision>(op, sent),
172+ Op::Why => through::<g1t_contracts::decisions::Why>(op, sent),
170173 Op::ListIssues => through::<Vec<work::Issue>>(op, sent),
171174 Op::CreateIssue | Op::UpdateIssue | Op::CloseIssue | Op::ReopenIssue => {
172175 through::<work::Issue>(op, sent)
+6−0
997997 Op::RecordSession,
998998 &[],
999999 ),
1000+ // Decision records, on a pull request or an issue (with its pull
1001+ // requests'), and why a line is the way it is.
1002+ route("GET", "/repos/:owner/:name/pulls/:number/decisions", Op::ListDecisions, &[]),
1003+ route("POST", "/repos/:owner/:name/pulls/:number/decisions", Op::RecordDecision, &[]),
1004+ route("GET", "/repos/:owner/:name/issues/:number/decisions", Op::ListDecisions, &[]),
1005+ route("GET", "/repos/:owner/:name/why", Op::Why, &[("path", "path"), ("line", "line"), ("ref", "ref")]),
10001006 route(
10011007 "POST",
10021008 "/repos/:owner/:name/pulls/:number/ready",
+3−0
126126 a("list_deleted", Op::ListDeletedRepos, "A workspace's deleted repositories"),
127127 a("restore", Op::RestoreRepo, "Restore a deleted one"),
128128 a("purge", Op::PurgeRepo, "Remove a deleted one for good"),
129+ a("why", Op::Why, "Why a line is the way it is: commit, pull request, issue, decisions, session"),
129130 a("security_alerts", Op::ListSecurityAlerts, "Secret and dependency alerts, filtered by state"),
130131 a("dismiss_alert", Op::DismissSecurityAlert, "Dismiss an alert with a reason"),
131132 a("reopen_alert", Op::ReopenSecurityAlert, "Reopen a dismissed alert"),
166167 a("update", Op::UpdatePullRequest, "Change its base branch, labels, milestone, assignees or reviewers"),
167168 a("record_session", Op::RecordSession, "Append prompt, reasoning and tool entries to its session"),
168169 a("read_session", Op::ReadSession, "Its recorded session"),
170+ a("record_decision", Op::RecordDecision, "Record why: a proposal, claim, fix or note, kept as claimed"),
171+ a("list_decisions", Op::ListDecisions, "Decision records on a pull request, or an issue and its pull requests"),
169172 a("ready", Op::MarkPullRequestReady, "Mark a draft ready, with a summary"),
170173 a("draft", Op::ConvertPullRequestToDraft, "Turn it back into a draft"),
171174 a("request_reviewers", Op::RequestReviewers, "Ask people or teams to review it"),
+324−0
1+//! `why`: why a line of a file is the way it is.
2+//!
3+//! The line's commit comes from the repos service's blame; the merged pull
4+//! request it arrived in is the one whose head it is, or else the one whose
5+//! stretch of the default branch's history holds it (as the site's blame
6+//! view finds it, `apps/web/app/lib/provenance.server.ts`); then that pull
7+//! request's issue, its decision records, and the session entries recorded
8+//! around the commit.
9+
10+use g1t_contracts::decisions::{Decision, ListDecisionsArgs, Why, WhyIssue, WhyPull};
11+use g1t_contracts::repos::{Blame, BlameArgs, Commit, LogArgs, RepoPath};
12+use g1t_contracts::work::{IssueDetail, ListPullsArgs, Pull, PullStatus, SessionEntry, SessionEntryKind, State, ViewArgs};
13+use g1t_contracts::{FailureCode, Outcome, Viewer};
14+use serde_json::Value;
15+use worker::Result;
16+
17+use crate::operations::Services;
18+
19+/// How far back the default branch is read to place a commit.
20+const HISTORY_READ: u32 = 300;
21+/// How many steps of a session are shown for one commit.
22+const MAX_STEPS: usize = 8;
23+/// Longest a step's text is shown.
24+const MAX_STEP_CHARS: usize = 600;
25+/// Longest an issue's description is shown.
26+const MAX_BODY_CHARS: usize = 900;
27+
28+/// The commit that last changed `line`, from a blame of the file.
29+pub(crate) fn commit_of_line(blame: &Blame, line: u32) -> Option<&Commit> {
30+ let range = blame.ranges.iter().find(|range| range.start <= line && line <= range.end)?;
31+ blame.commits.iter().find(|commit| commit.hash == range.commit)
32+}
33+
34+/// The merged pull request `hash` arrived in: the one whose head it is,
35+/// or else the one whose stretch of `history` (the default branch, newest
36+/// first) holds it, from its head back to the commit it started from.
37+pub(crate) fn pull_for_commit<'a>(merged: &'a [Pull], history: &[String], hash: &str) -> Option<&'a Pull> {
38+ let merged: Vec<&Pull> = merged
39+ .iter()
40+ .filter(|pull| pull.status == PullStatus::Merged && pull.head_commit.is_some())
41+ .collect();
42+ if let Some(direct) = merged.iter().find(|pull| pull.head_commit.as_deref() == Some(hash)) {
43+ return Some(direct);
44+ }
45+ let position = |commit: &str| history.iter().position(|known| known == commit);
46+ let at = position(hash)?;
47+ let mut best: Option<(&Pull, usize)> = None;
48+ for pull in merged {
49+ let Some(head) = pull.head_commit.as_deref().and_then(position) else { continue };
50+ if head > at {
51+ continue;
52+ }
53+ if let Some(base) = pull.merge_base.as_deref().and_then(position)
54+ && base <= at
55+ {
56+ continue;
57+ }
58+ if best.is_none_or(|(_, best_head)| head > best_head) {
59+ best = Some((pull, head));
60+ }
61+ }
62+ best.map(|(pull, _)| pull)
63+}
64+
65+/// The steps of a session behind `hash`: what was said and done between
66+/// the push before it and the push that carried it. Steps that touch
67+/// `file` come first; at most [`MAX_STEPS`], each cut to
68+/// [`MAX_STEP_CHARS`].
69+pub(crate) fn steps_for(session: &[SessionEntry], hash: &str, file: &str) -> Vec<SessionEntry> {
70+ // A push is noted "Pushed b1c42490059b."
71+ let pushed = |entry: &SessionEntry| -> Option<String> {
72+ if entry.kind != SessionEntryKind::Note {
73+ return None;
74+ }
75+ let rest = entry.text.strip_prefix("Pushed ")?;
76+ let short: String = rest.chars().take_while(char::is_ascii_hexdigit).collect();
77+ (short.len() >= 7).then_some(short)
78+ };
79+ let end = session.iter().position(|entry| {
80+ entry.commit.as_deref() == Some(hash) || pushed(entry).is_some_and(|short| hash.starts_with(&short))
81+ });
82+ let window: &[SessionEntry] = match end {
83+ Some(end) => {
84+ let mut start = end;
85+ while start > 0 && pushed(&session[start - 1]).is_none() {
86+ start -= 1;
87+ }
88+ &session[start..=end]
89+ }
90+ None => session,
91+ };
92+ let name = file.rsplit('/').next().unwrap_or(file);
93+ let useful: Vec<&SessionEntry> = window
94+ .iter()
95+ .filter(|entry| {
96+ entry.kind == SessionEntryKind::Message
97+ || (entry.kind == SessionEntryKind::ToolCall && (entry.text.contains(file) || entry.text.contains(name)))
98+ })
99+ .collect();
100+ let touching: Vec<&SessionEntry> = useful.iter().copied().filter(|entry| entry.text.contains(name)).collect();
101+ let mut chosen: Vec<&SessionEntry> = if touching.is_empty() {
102+ useful.clone()
103+ } else {
104+ touching.into_iter().chain(useful.iter().copied().filter(|entry| entry.kind == SessionEntryKind::Message)).collect()
105+ };
106+ chosen.sort_by_key(|entry| entry.seq);
107+ chosen.dedup_by_key(|entry| entry.seq);
108+ let skip = chosen.len().saturating_sub(MAX_STEPS);
109+ chosen
110+ .into_iter()
111+ .skip(skip)
112+ .map(|entry| {
113+ let mut entry = entry.clone();
114+ if entry.text.chars().count() > MAX_STEP_CHARS {
115+ entry.text = format!("{}…", entry.text.chars().take(MAX_STEP_CHARS).collect::<String>());
116+ }
117+ entry
118+ })
119+ .collect()
120+}
121+
122+/// Answers `why`.
123+pub(crate) async fn why(
124+ services: &Services,
125+ viewer: &Viewer,
126+ repo: RepoPath,
127+ path: String,
128+ line: u32,
129+ git_ref: Option<String>,
130+) -> Result<Outcome<Value>> {
131+ let blame: Outcome<Blame> = g1t_kit::call(
132+ &services.repos,
133+ "blame",
134+ &BlameArgs {
135+ path: repo.clone(),
136+ viewer: viewer.clone(),
137+ git_ref,
138+ file_path: path.clone(),
139+ },
140+ )
141+ .await?;
142+ let blame = match blame {
143+ Outcome::Ok(blame) => blame,
144+ Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
145+ };
146+ let Some(commit) = commit_of_line(&blame, line).cloned() else {
147+ let lines = blame.ranges.last().map_or(0, |range| range.end);
148+ return Ok(Outcome::fail(
149+ FailureCode::Invalid,
150+ format!("{path} has {lines} lines at that ref: give a line from 1 to {lines}."),
151+ ));
152+ };
153+
154+ let closed: Outcome<Vec<Pull>> = g1t_kit::call(
155+ &services.work,
156+ "list_pulls",
157+ &ListPullsArgs {
158+ repo: repo.clone(),
159+ viewer: viewer.clone(),
160+ state: Some(State::Closed),
161+ label: None,
162+ milestone: None,
163+ base: None,
164+ },
165+ )
166+ .await?;
167+ let merged = match closed {
168+ Outcome::Ok(pulls) => pulls,
169+ Outcome::Fail(_) => Vec::new(),
170+ };
171+ let mut pull = pull_for_commit(&merged, &[], &commit.hash).cloned();
172+ if pull.is_none() && !merged.is_empty() {
173+ let log: Outcome<Vec<Commit>> = g1t_kit::call(
174+ &services.repos,
175+ "log",
176+ &LogArgs {
177+ path: repo.clone(),
178+ viewer: viewer.clone(),
179+ git_ref: None,
180+ limit: HISTORY_READ,
181+ },
182+ )
183+ .await?;
184+ if let Outcome::Ok(log) = log {
185+ let history: Vec<String> = log.into_iter().map(|commit| commit.hash).collect();
186+ pull = pull_for_commit(&merged, &history, &commit.hash).cloned();
187+ }
188+ }
189+
190+ let Some(pull) = pull else {
191+ return ok(Why { path, line, commit, pull: None, issue: None, decisions: Vec::new(), sessions: Vec::new() });
192+ };
193+ let view = |number: u32| ViewArgs { repo: repo.clone(), number, viewer: viewer.clone(), after_seq: 0 };
194+ // A few small reads, one after another: the API has no join helper.
195+ let issue = match pull.issue {
196+ Some(number) => match g1t_kit::call::<_, Outcome<IssueDetail>>(&services.work, "get_issue", &view(number)).await? {
197+ Outcome::Ok(detail) => Some(detail.issue),
198+ Outcome::Fail(_) => None,
199+ },
200+ None => None,
201+ };
202+ let decisions: Outcome<Vec<Decision>> = g1t_kit::call(
203+ &services.work,
204+ "list_decisions",
205+ &ListDecisionsArgs { repo: repo.clone(), viewer: viewer.clone(), number: pull.number },
206+ )
207+ .await?;
208+ let session: Outcome<Vec<SessionEntry>> = g1t_kit::call(&services.work, "read_session", &view(pull.number)).await?;
209+ let decisions = match decisions {
210+ Outcome::Ok(decisions) => decisions,
211+ Outcome::Fail(_) => Vec::new(),
212+ };
213+ let sessions = match session {
214+ Outcome::Ok(entries) => steps_for(&entries, &commit.hash, &path),
215+ Outcome::Fail(_) => Vec::new(),
216+ };
217+ let issue = issue.map(|issue| WhyIssue {
218+ number: issue.number,
219+ title: issue.title,
220+ state: if issue.state == State::Open { "open" } else { "closed" }.to_owned(),
221+ body: if issue.body.chars().count() > MAX_BODY_CHARS {
222+ format!("{}…", issue.body.chars().take(MAX_BODY_CHARS).collect::<String>())
223+ } else {
224+ issue.body
225+ },
226+ });
227+ let pull = WhyPull {
228+ number: pull.number,
229+ title: pull.title.clone(),
230+ author: pull.owner().username.clone(),
231+ agent: pull.agent.clone(),
232+ merged_by: pull.merged_by.clone(),
233+ merged_at: pull.merged_at.clone(),
234+ };
235+ ok(Why { path, line, commit, pull: Some(pull), issue, decisions, sessions })
236+}
237+
238+fn ok(why: Why) -> Result<Outcome<Value>> {
239+ Ok(Outcome::Ok(serde_json::to_value(why)?))
240+}
241+
242+#[cfg(test)]
243+mod tests {
244+ use super::*;
245+ use g1t_contracts::repos::{BlameRange, Signature};
246+
247+ fn commit(hash: &str) -> Commit {
248+ Commit {
249+ hash: hash.into(),
250+ tree_hash: "t".into(),
251+ message: "m".into(),
252+ author: Signature { name: "Ana".into(), email: "ana@example.com".into() },
253+ parents: Vec::new(),
254+ authored_at: "2026-10-12T00:00:00Z".into(),
255+ }
256+ }
257+
258+ fn merged(number: u32, head: &str, base: Option<&str>) -> Pull {
259+ serde_json::from_value(serde_json::json!({
260+ "id": format!("pr_{number}"), "repoId": "rep_1", "number": number, "issue": 9, "title": "t", "body": null,
261+ "agent": "claude-code", "runtime": "external", "status": "merged", "fork": null, "forkRepoId": null,
262+ "branch": "b", "headCommit": head, "mergeBase": base, "mergedBy": "ana", "mergedAt": "2026-10-12T00:00:00Z",
263+ "supersededBy": null, "checkStatus": null,
264+ "author": { "id": "usr_1", "username": "ana" },
265+ "createdAt": "", "updatedAt": ""
266+ }))
267+ .unwrap()
268+ }
269+
270+ #[test]
271+ fn a_line_is_found_in_its_range() {
272+ let blame = Blame {
273+ head: "h".into(),
274+ ranges: vec![
275+ BlameRange { start: 1, end: 3, commit: "aaa".into() },
276+ BlameRange { start: 4, end: 4, commit: "bbb".into() },
277+ ],
278+ commits: vec![commit("aaa"), commit("bbb")],
279+ partial: false,
280+ };
281+ assert_eq!(commit_of_line(&blame, 3).map(|c| c.hash.as_str()), Some("aaa"));
282+ assert_eq!(commit_of_line(&blame, 4).map(|c| c.hash.as_str()), Some("bbb"));
283+ assert!(commit_of_line(&blame, 5).is_none());
284+ }
285+
286+ #[test]
287+ fn a_commit_is_placed_in_the_pull_request_it_arrived_in() {
288+ // Newest first: m2 landed #12 on top of b1; m1 landed #11 on top of b0.
289+ let history: Vec<String> = ["m2", "c3", "c2", "b1", "m1", "c1", "b0"].map(String::from).to_vec();
290+ let pulls = vec![merged(11, "m1", Some("b0")), merged(12, "m2", Some("b1"))];
291+ assert_eq!(pull_for_commit(&pulls, &[], "m1").map(|p| p.number), Some(11));
292+ assert_eq!(pull_for_commit(&pulls, &history, "c2").map(|p| p.number), Some(12));
293+ assert_eq!(pull_for_commit(&pulls, &history, "c1").map(|p| p.number), Some(11));
294+ // Before any pull request.
295+ assert!(pull_for_commit(&pulls, &history, "b0").is_none());
296+ assert!(pull_for_commit(&pulls, &history, "zzz").is_none());
297+ }
298+
299+ fn entry(seq: u32, kind: SessionEntryKind, text: &str, commit: Option<&str>) -> SessionEntry {
300+ SessionEntry { seq, kind, text: text.into(), tool: None, commit: commit.map(String::from), at: String::new() }
301+ }
302+
303+ #[test]
304+ fn the_session_steps_are_those_behind_the_commit() {
305+ use SessionEntryKind::*;
306+ let session = vec![
307+ entry(1, Message, "Starting on the parser.", None),
308+ entry(2, Note, "Pushed 1111111aaaa.", None),
309+ entry(3, Message, "The lexer drops the last token.", None),
310+ entry(4, ToolCall, "Edit src/lexer.rs", None),
311+ entry(5, ToolCall, "Run cargo test", None),
312+ entry(6, Note, "Pushed 2222222bbbb.", None),
313+ entry(7, Message, "Now the docs.", None),
314+ ];
315+ let steps = steps_for(&session, "2222222bbbbcccc", "src/lexer.rs");
316+ let seqs: Vec<u32> = steps.iter().map(|step| step.seq).collect();
317+ assert_eq!(seqs, [3, 4]);
318+ // A commit no push names: the whole session is looked through.
319+ let all = steps_for(&session, "9999999", "src/lexer.rs");
320+ assert_eq!(all.iter().map(|s| s.seq).collect::<Vec<_>>(), [1, 3, 4, 7]);
321+ let long = vec![entry(1, Message, &"x".repeat(MAX_STEP_CHARS + 10), None)];
322+ assert!(steps_for(&long, "h", "a.rs")[0].text.ends_with('…'));
323+ }
324+}
+10−0
107107 into the fork and push. The pull request can then be merged, or joins the
108108 [merge queue](/guides/merge-queue/) when the repository uses one.
109109
110+When the merge queue takes an agent's pull request out because it fails
111+combined with the pull requests ahead of it, g1t
112+[says which ones and which check failed](/guides/merge-queue/#who-is-told):
113+in the pull request's session, as an `integration_failure` decision
114+(`pull_request` `list_decisions`), and as the `integration.failed` event.
115+The agent reads the other pull request's changes, fixes its own, and
116+pushes; once the checks pass, the pull request rejoins the queue by
117+itself. It can say what it changed with `pull_request` `record_decision`
118+(`kind` `fix`), and `repository` `why` explains any line it is unsure of.
119+
110120 ### Repository instructions
111121
112122 Each agent reads a repository's instructions from a file at its root:
+86−10
134134 An entry fails when its `merge_group` workflows or required checks fail
135135 on the combined state, when it does not merge cleanly with what is ahead of it, or
136136 when the state cannot be built.
137−It leaves the queue, and:
138137
139−1. Its pull request records the failure, saying why: which workflow failed
140− on the state, which required check did not report, or, for a conflict,
141− the pull request ahead it collided with and the files.
142−2. Its conversation records that it was taken out of the queue, and why: a
143− conflict links the pull request it collided with and each conflicting
144− file, which opens in the pull request's changes.
145−3. The entries that were tested on top of it are tested again without it.
138+### Who answers for it
139+
140+A state that fails while an entry ahead of it is still being tested is
141+held until those ahead are settled. If one of them fails too, the held
142+entry is tested again without it and is not blamed. Once every entry ahead
143+has passed, the held entry answers for the failure. So when #41 is
144+broken, it leaves the queue, and #44 and #46, which were tested with #41
145+in them, are tested again without it rather than taken out with it.
146+
147+The entry that answers for a failure leaves the queue, and g1t works out
148+what it failed with:
149+
150+| What happened | Who it failed with (`suspects`) |
151+| --- | --- |
152+| Tested on `main` alone | Nobody: the change is the cause. |
153+| Its own checks fail on its head too | Nobody: the change is the cause. |
154+| It conflicts with an entry ahead | That entry, and the conflicting files. |
155+| A required check did not report, or the state could not be built | Nobody: the note says what to fix. |
156+| Its own checks pass, one entry ahead | That entry. |
157+| Its own checks pass, several entries ahead | Found by building smaller combinations, below. |
158+
159+### Narrowing it down
160+
161+An entry whose own checks pass but which fails combined with several
162+entries ahead breaks *with* some of them. g1t builds `main` with the first
163+half of those entries and the failing one, and runs the `merge_group`
164+workflows on it, as for any queued state. If that fails, the cause is in
165+that half; if it passes, in the other half. It halves again until one entry
166+is left, or it has built three extra combinations. With the default four
167+entries to a batch, at most three are ahead of a failing entry, and two
168+builds always find the one it breaks with; three builds find one among up
169+to eight.
170+
171+These combinations are built in sandboxes as the queue's states are, on
172+`g1t-queue/<id>` branches that are deleted afterwards, and never land. They
173+do not hold up the queue: the entries behind are tested again at once. When
174+a combination cannot be built or checked, or its checks are still running
175+past the rule's timeout when the queue next moves, g1t stops and reports
176+the entries still suspected.
177+
178+The halving assumes one entry ahead is the cause. When the failure needs
179+two of them together, what is reported is the half that still failed, and
180+can name more than one pull request.
181+
182+### Who is told
183+
184+1. Its pull request records the failed check, saying why: which workflow
185+ failed on the state, which required check did not report, or, for a
186+ conflict, the pull request ahead it collided with and the files.
187+2. Its conversation records that it was taken out of the queue, then what
188+ g1t found: the pull requests it fails with, whether that was narrowed
189+ down, and the check that failed.
190+3. Its issue's conversation says the same in one line, and so does the
191+ pull request's [session](/guides/why-blame/#sessions), so the agent
192+ working on it reads it there.
193+4. A [decision record](/guides/why-blame/#decision-records) of kind
194+ `integration_failure`, confidence `tested`, keeps it, with the
195+ combination, the check and the suspects as evidence.
196+5. The `integration.failed` [event](/guides/webhooks/#events) carries the
197+ same to webhooks and to anyone following the repository's events:
198+ `pull` (the entry that failed, also `number`), `issue`, `combination`
199+ (the pull requests tested together, in order), `check` (`name`,
200+ `conclusion`, `url`), `bisected`, `suspects`, `other_pulls` and `commit`.
201+6. The entries that were tested on top of it are tested again without it.
202+
203+A combined state that passes is recorded too: an `integration_passed`
204+decision and the `integration.passed` event, with `pull` and `combination`.
205+
206+### Getting back in the queue
207+
208+The merge stays asked for. Push a fix to the pull request, starting from
209+`main` as it is now: once every check on the new commit has finished, none
210+failed and every required check passed, g1t merges it again as whoever
211+queued it, which puts it back in the queue. Everything a merge needs is
212+checked again then, so if an approval was dismissed by the push, the
213+conversation says so and you merge it again once it is approved. Until a
214+new commit's checks pass, merging it again is refused: "It failed in the
215+merge queue; push a fix to try again."
146216
147−Its author fixes it, starting from `main` as it is now, and merges it
148−again to put it back in the queue.
217+A pull request whose checks are not reported by workflows (none run on its
218+pushes) is not put back by itself: merge it again once it is fixed.
149219
150220 ## The Merge queue page
151221
209279 | `results` | What building the state recorded, each with `command`, `passed` and `output`. The workflow runs on it are on its commit, `combined_commit`. |
210280 | `error` | Why it failed: a conflict, a workflow that failed on it, a required check that did not report, or what could not be built. |
211281 | `enqueued_by` | Who added it: a username, or `g1t` when it was merged automatically. |
282+
283+An agent working on a pull request learns of a failure without polling the
284+queue: from the `integration.failed` event, from the note in the pull
285+request's session (`pull_request` `read_session`), or from its decisions
286+(`pull_request` `list_decisions`, or
287+`GET /repos/{owner}/{name}/pulls/{number}/decisions`).
+4−1
111111 | `deployment_status.created` | A deployment got a status, from any source. `data.repo_id`, `data.deployment` without its `payload`, and `data.deployment_status`. |
112112 | `release.created`, `.published`, `.released`, `.prereleased`, `.edited`, `.unpublished`, `.deleted` | A [release](/guides/releases/) changed; one change can send several, as the [release activity types](/guides/actions/#releases) say. `data.release_id`, `data.tag_name`, and `data.release` (`tag_name`, `target`, `name`, `body`, `draft`, `prerelease`, `author`, `created_at`, `published_at`), as it was for `release.deleted`. On `release.edited`, `data.changes` holds the old `name` and `body`. |
113113 | `queue.changed` | The merge queue gained, lost or settled an entry. |
114−| `session.appended` | A pull request's [session](/guides/why-blame/#sessions) grew: a coding agent recorded entries with `record_session`. Busy: choose it only if you need it. |
114+| `integration.failed` | A pull request failed in the [merge queue](/guides/merge-queue/#when-an-entry-fails) combined with the ones ahead of it. `pull` (also `number`) is the one that answers for it, `issue` its issue, `combination` the pull requests tested together, `check` the check that failed (`name`, `conclusion`, `url`), `suspects` the pull requests ahead it fails with, `bisected` whether smaller combinations were built to find them, and `other_pulls` the rest of the combination. |
115+| `integration.passed` | A pull request's combined state passed in the merge queue: `pull` and `combination`. |
116+| `decision.recorded` | A [decision record](/guides/why-blame/#decision-records) was kept on an issue or pull request: `decision`, with `number` and `issue`. |
117+| `session.appended` | A pull request's [session](/guides/why-blame/#sessions) grew: a coding agent recorded entries with `record_session`, or g1t noted a merge queue failure. Busy: choose it only if you need it. |
115118
116119 [Managing a repository](/guides/managing-repositories/) says what each of
117120 the `repo.` and `branch.` changes does.
+73−0
8787 request when it caught up with `main` is credited to whoever wrote it on
8888 `main`, not to the merge.
8989
90+### Why over the API
91+
92+The `repository` tool's `why` action, or
93+`GET /repos/{owner}/{name}/why?path=&line=&ref=`, answers the same for one
94+line, with the decision records too. `ref` is a branch, tag or commit; the
95+default branch when left out.
96+
97+```sh
98+curl "https://api.g1t.sh/repos/acme/web/why?path=src/config.rs&line=42" \
99+ -H "Authorization: Bearer $G1T_TOKEN"
100+```
101+
102+| Field | |
103+| --- | --- |
104+| `commit` | The commit that last changed the line: `hash`, `message`, `author`, `parents`, `authored_at`. |
105+| `pull` | The merged pull request it arrived in: `number`, `title`, `author`, `agent`, `merged_by`, `merged_at`. Absent when it did not arrive through one. |
106+| `issue` | That pull request's issue: `number`, `title`, `state` and `body` (cut at 900 characters). |
107+| `decisions` | The [decision records](#decision-records) on the pull request, oldest first. |
108+| `sessions` | Up to eight entries of the pull request's session behind that commit: the messages, and the tool calls that touched the file. |
109+
110+A pull request is found for a commit as on the site: the one whose head it
111+is, or the one whose stretch of the default branch's last 300 commits holds
112+it. It is public for a public repository, and needs the `code:read` scope
113+otherwise.
114+
115+## Decision records
116+
117+A decision record is a reason kept on an issue or pull request, with how
118+sure it is. Sessions say what happened; decisions say why the result is
119+the way it is, in a sentence, with what it rests on.
120+
121+| Confidence | Meaning |
122+| --- | --- |
123+| `claimed` | Someone said so: an agent or a person recorded it. |
124+| `tested` | A check or the merge queue proved it. |
125+| `approved` | A person approved it. |
126+
127+g1t keeps these itself:
128+
129+| Kind | When | Confidence |
130+| --- | --- | --- |
131+| `integration_failure` | The [merge queue](/guides/merge-queue/#when-an-entry-fails) found a combination of pull requests fails: which ones, and the check. | `tested` |
132+| `integration_passed` | A combined state passed in the merge queue. | `tested` |
133+| `approval` | Someone approved the pull request. | `approved` for a person, `claimed` for an agent's token |
134+| `merge` | It was merged. | `tested` through the queue or a repository's own auto-merge, `approved` when a person merged it, `claimed` for an agent's token |
135+
136+Agents and people record their own, of kind `proposal` (why the change is
137+made this way), `claim` (something true of it), `fix` (what fixed a
138+failure) or `note`, always as `claimed`, with the `pull_request` tool's
139+`record_decision` action or
140+`POST /repos/{owner}/{name}/pulls/{number}/decisions`:
141+
142+```sh
143+curl -X POST https://api.g1t.sh/repos/acme/web/pulls/14/decisions \
144+ -H "Authorization: Bearer $G1T_TOKEN" \
145+ -H "Content-Type: application/json" \
146+ -d '{"kind": "fix",
147+ "summary": "Renamed the config key #12 also introduced, so both read retry_limit.",
148+ "evidence": {"commit": "7d3e9a1c"}}'
149+```
150+
151+`summary` is at most 2,000 characters and `evidence` an object of at most
152+8 KB. Whoever opened the pull request can record one, and so can anyone
153+who may comment on it, once their email address is confirmed. Each is
154+attributed to whoever recorded it, carries
155+the issue the pull request is for, and is published as `decision.recorded`.
156+
157+Read them with the `pull_request` tool's `list_decisions` action, with
158+`GET /repos/{owner}/{name}/pulls/{number}/decisions`, or for an issue and
159+all its pull requests with
160+`GET /repos/{owner}/{name}/issues/{number}/decisions`. A decision is as
161+visible as the repository, so do not put secrets in one.
162+
90163 ## Commit pages
91164
92165 Every commit has a page of its own, `g1t.sh/<workspace>/<repo>/commit/<hash>`,
+6−1
300300 | [`security_alerts`](/reference/api/security/list-security-alerts/) | Its security alerts: secrets found in pushes and history (`kind` `secret`) and dependencies with known vulnerabilities (`dependency`), each `open`, `dismissed` or `fixed`. `state` and `kind` filter them. Write role. | `repo` | `repo:read` |
301301 | [`dismiss_alert`](/reference/api/security/dismiss-security-alert/) | Dismiss one by `id` with a `reason` and an optional `comment`. A secret takes `false_positive`, `used_in_tests`, `revoked` or `wont_fix`, and needs the Admin role, since a dismissed secret is let through push protection; a dependency takes `fix_started`, `no_bandwidth`, `tolerable_risk`, `inaccurate` or `not_used`, and needs Write. | `repo`, `id`, `reason` | `repo:admin` |
302302 | [`reopen_alert`](/reference/api/security/reopen-security-alert/) | Open a dismissed alert again. The same roles as dismissing. | `repo`, `id` | `repo:admin` |
303+| [`why`](/reference/api/decisions-and-why/why/) | Why a line is the way it is: the `commit` that last changed `line` of `path` (at `ref`, or the default branch), the merged `pull` request it arrived in, its `issue`, the `decisions` recorded on the pull request, and the `sessions` entries behind the commit. | `repo`, `path`, `line` | `code:read` |
303304
304305 `update_settings` takes `required_checks` (at most 20 names),
305306 `required_approvals`, `allow_ignoring_checks`, `require_up_to_date`,
351352 | [`update`](/reference/api/pull-requests/update-pull-request/) | Change its `base` (the branch it merges into; Write role), `labels`, `milestone`, `assignees` or `reviewers`. `state` `open` reopens it and `closed` closes it. | `repo`, `number` | `pull_requests:write` |
352353 | [`record_session`](/reference/api/sessions/record-session/) | Append entries to a pull request's session. Each has `kind` and `text`, and `tool` for tool entries. | `repo`, `number`, `entries` | `pull_requests:write` |
353354 | [`read_session`](/reference/api/sessions/read-session/) | The recorded session, oldest first. `after` skips to entries after a sequence number. | `repo`, `number` | `pull_requests:read` |
355+| [`record_decision`](/reference/api/decisions-and-why/record-decision/) | Record a decision on a pull request, kept as `claimed`: a `kind` (`proposal`, `claim`, `fix` or `note`), a `summary`, and optional `evidence` (an object) and `session_id`. | `repo`, `number`, `kind`, `summary` | `pull_requests:write` |
356+| [`list_decisions`](/reference/api/decisions-and-why/list-decisions/) | The decision records on a pull request, or on an issue and its pull requests, oldest first, each with its `kind`, `confidence` (`claimed`, `tested` or `approved`), `summary` and `evidence`. | `repo`, `number` | `pull_requests:read` |
354357 | [`ready`](/reference/api/pull-requests/mark-pull-request-ready/) | Mark a draft ready for review. The summary becomes its description. | `repo`, `number`, `summary` | `pull_requests:write` |
355358 | [`draft`](/reference/api/pull-requests/convert-pull-request-to-draft/) | Turn a pull request that is ready for review back into a draft. It leaves the merge queue. Its author, or the Triage role. | `repo`, `number` | `pull_requests:write` |
356359 | [`request_reviewers`](/reference/api/pull-requests/request-reviewers/) | Ask more people (`reviewers`, by username) or teams (`team_reviewers`, as `workspace/team` or a slug of the repository's workspace) to review it, added to whoever is asked already. Its author, or the Triage role. | `repo`, `number` | `pull_requests:write` |
364367 `record_session` takes a list of `entries`, each with a `kind` (`prompt`,
365368 `message`, `tool_call`, `tool_result` or `note`) and `text`, and `tool` for
366369 tool entries. See [sessions and why-blame](/guides/why-blame/) and the
367−[merge queue](/guides/merge-queue/).
370+[merge queue](/guides/merge-queue/). g1t records `integration_failure`,
371+`integration_passed`, `approval` and `merge` decisions itself; see
372+[decision records](/guides/why-blame/#decision-records).
368373
369374 ## `memory`
370375
+0−0

Binary or large file; its contents are not shown.

+5−2
5656 "release.deleted",
5757 ],
5858 },
59− { title: "Checks and the queue", events: ["checks.completed", "workflow.completed", "queue.changed"] },
6059 {
60+ title: "Checks and the queue",
61+ events: ["checks.completed", "workflow.completed", "queue.changed", "integration.failed", "integration.passed"],
62+ },
63+ {
6164 title: "Statuses and check runs",
6265 events: [
6366 "status.created",
7376 title: "Deployments",
7477 events: ["deployment.created", "deployment_status.created", "deployment.succeeded", "deployment.failed"],
7578 },
76− { title: "Sessions", events: ["session.appended"] },
79+ { title: "Sessions and decisions", events: ["session.appended", "decision.recorded"] },
7780 { title: "Access", events: ["workspace.member_joined", "repo.collaborator_added", "repo.collaborator_removed", "repo.collaborator_role_changed"] },
7881 {
7982 title: "Teams",
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…