Skip to content

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

g1t.sh

Commit

An issue says what closed it, linked: on flagon-io/demo #1 the session that built the page closed the issue through the issue tool's close, and its timeline said only "syntaqx closed this as completed", with nothing to follow to the commit that did the work or the session that made it. close_issue (REST, the MCP issue tool's close, `g1t issue close --commit`) now takes an optional commit: one on the default branch (repos' log and branch_drift) whose message names the issue after a closing keyword (closing::references). The issue then closes as completed in that commit exactly as its push would (close_by_commit: resolved_commit, "closed this in <commit>", issue.closed with commit), by the caller; a commit not on the default branch, or one not naming the issue, is refused saying why, and an already-closed issue says what closed it. The work service's CloseIssueArgs flattens IssueActionArgs, so integrations and security keep calling close_issue as they do. When an issue is closed in a session, the note ends "via session ses_…" and issue.closed carries session: the session the closing commit names in a G1t-Session trailer (on a push too), or, for a close made with a token, the closer's own active session on the issue (attached or linked; the person it acts for when it acts for one). The timeline renders that id as a session chip linking /<owner>/<repo>/work/sessions/<id>, next to the existing commit chip and #N links (messageParts gains a session part). The pull requests guide's Closing issues, the sessions guide, webhooks, the API reference notes, openapi.json, the CLI reference and ops table say so. Deploy: work and api together, then the site; the CLI flag ships with the next CLI release. Checked: cargo test -p g1t-work (202), -p g1t-api (209, openapi regenerated), -p g1t-contracts, -p g1t (ops_table and cli_reference regenerated); wasm32 check of g1t-work and g1t-api; apps/web npm run typecheck; node --test app/lib/commit-message.test.ts.

G1t-Session:
ses_01m4z1z7nse8w9cjnecymt7rzn
syntaqxcommitted Parent6207345Browse files
20 files+107−224 files not counted0/20 viewed
+16−12
16141614 "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."
16151615 }
16161616 Op::CloseIssue => {
1617− "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."
1617+ "Close an issue without a pull request. Merging a pull request made for an issue closes it for you, as does pushing a commit whose message names it after a closing keyword (Fixes #12) to the default branch. When a commit already on the default branch did the work, give it as commit: the issue closes as completed in that commit, as its push would have, and its timeline links the commit. A commit that is not on the default branch, or whose message does not name the issue after a closing keyword, is refused with `422`; an issue already closed, with `409`. Closed with a token from a session working on the issue (or by a commit with a `G1t-Session` trailer), its timeline links that session too. Its author may close their own issue, as may the person g1t filed one for; anyone else needs the Triage role or higher."
16181618 }
16191619 Op::ReopenIssue => "Reopen a closed issue. Its author may reopen their own issue, as may the person g1t filed one for; anyone else needs the Triage role or higher.",
16201620 Op::ListLabels => {
26912691 "enum": ["completed", "not_planned"],
26922692 "description": "Defaults to completed.",
26932693 },
2694+ "commit": {
2695+ "type": "string",
2696+ "description": "The commit that closed it: one on the default branch whose message names this issue after a closing keyword (Fixes #12). It closes as completed in that commit, as pushing it would have.",
2697+ },
26942698 })),
26952699 &["repo", "number"],
26962700 ),
49854989 } else {
49864990 "reopen_issue"
49874991 };
4988− pass(
4989− work,
4990− method,
4991− &IssueActionArgs {
4992− actor: actor(),
4993− repo,
4994− number,
4995− reason: Some(reason),
4996− },
4997− )
4998− .await
4992+ let issue = IssueActionArgs {
4993+ actor: actor(),
4994+ repo,
4995+ number,
4996+ reason: Some(reason),
4997+ };
4998+ if self == Op::CloseIssue {
4999+ let commit = optional_text(input, "commit");
5000+ return pass(work, method, &CloseIssueArgs { issue, commit }).await;
5001+ }
5002+ pass(work, method, &issue).await
49995003 }
50005004 Op::ListLabels => pass(work, "list_labels", &view()).await,
50015005 Op::CreateLabel | Op::UpdateLabel => {
+2−1
14891489 "blocked_by": [],
14901490 "agent": null,
14911491 "milestone": null
1492− }
1492+ },
1493+ "notes": "With `commit`, a commit already on the default branch whose message names the issue after a closing keyword (`Fixes #12`), the issue closes as completed in that commit, as pushing it would have: `resolved_commit` is set, the event is `issue.closed` with `data.commit`, and the timeline says \"closed this in\" the commit, linked. Give `commit` as its full hash. A commit not on the default branch, or one whose message does not name the issue, is refused (`422`); an issue already closed is `409`, saying what closed it. See [Closing issues](/guides/pull-requests/#closing-issues)."
14931494 },
14941495 "reopen_issue": {
14951496 "response": {
+1−1
160160 a("add_labels", Op::AddIssueLabels, "Add labels to an issue or pull request"),
161161 a("set_labels", Op::SetIssueLabels, "Replace the labels of an issue or pull request"),
162162 a("remove_labels", Op::RemoveIssueLabels, "Take labels off an issue or pull request"),
163− a("close", Op::CloseIssue, "Close it without a pull request"),
163+ a("close", Op::CloseIssue, "Close it without a pull request; commit names the commit on the default branch that did it"),
164164 a("reopen", Op::ReopenIssue, "Reopen it"),
165165 a("comment", Op::AddComment, "Comment on an issue or pull request; path and line for one line of a change"),
166166 a("edit_comment", Op::EditComment, "Change a comment's text: your own, or any with the Maintain role"),
+20−0
525525 branches close nothing until they reach the default branch, and
526526 `auto_close_issues` turns this off as well.
527527
528+A commit that is already on the default branch can close an issue later,
529+say one pushed before the issue was open or while `auto_close_issues` was
530+off: [`close_issue`](/reference/api/issues/close-issue/) with `commit` (its
531+full hash), `g1t issue close 12 --commit <hash>`, or the `issue` tool's
532+`close` with `commit`. The commit's message must name the issue after a
533+closing keyword. The issue then closes as completed in that commit, the way
534+a push of it would have, by whoever closed it: **closed this in** the
535+commit, `resolved_commit`, and `issue.closed` with `commit`. A commit that is
536+not on the default branch, or whose message does not name the issue, is
537+refused, and so is an issue that is already closed.
538+
539+An issue's timeline links whatever closed it: **closed this in** a commit
540+links the commit, **closed this by merging #14** links the pull request,
541+and when it was closed in a [session](/guides/sessions/) the line ends
542+**via session**, linking that session. That session is the one the closing
543+commit names in a `G1t-Session: ses_…` trailer, or, for an issue closed
544+with a token (from an agent, the CLI or the MCP server), the closer's own
545+session on the issue that is still going. `issue.closed` carries it as
546+`session`. An issue closed on the site says only who closed it.
547+
528548 Wherever a commit's message shows, `#12` links to that issue or pull
529549 request, with its title and state when you hover over it.
530550
+4−1
255255 A session's links cover its subagents too, and the issue and pull request
256256 pages list every session tied to them with its subagents. Put the trailer
257257 on the commits a session makes (`git commit --trailer "G1t-Session:
258−$G1T_SESSION"`) and they lead back to it.
258+$G1T_SESSION"`) and they lead back to it. An issue a session closes says
259+so on its timeline, **via session** and a link to it: closed with the
260+session's token while it works on the issue, or by a commit naming it in
261+that trailer (see [Closing issues](/guides/pull-requests/#closing-issues)).
259262
260263 When a file a session touched was touched by another active session as
261264 well (outside its own parents and subagents), g1t sends `overlap.detected`
+1−1
8787 | `issue.labeled`, `issue.unlabeled`, `pull.labeled`, `pull.unlabeled` | A [label](/guides/labels/) was put on an issue or a pull request, or taken off: one event for each label. `data.number` and `data.label` (`name` and `color`). |
8888 | `issue.milestoned`, `issue.demilestoned`, `pull.milestoned`, `pull.demilestoned` | An issue or a pull request was put in a [milestone](/guides/milestones/), or taken out of one. `data.milestone` (`number` and `title`). Moving it from one to another is both. |
8989 | `pull.base_changed` | The branch a pull request merges into changed. `data.base` names it. See [pull requests into other branches](/guides/base-branches/). |
90−| `issue.opened`, `issue.updated`, `issue.assigned`, `issue.closed`, `issue.reopened` | An issue changed. `data.number` and `data.author` (`id` and `username`); on close, `data.reason` and `data.resolved_by` (the pull request that closed it) or `data.commit` (the commit pushed to the default branch that did); on assignment, `data.assignees` and the newly assigned `data.added`. |
90+| `issue.opened`, `issue.updated`, `issue.assigned`, `issue.closed`, `issue.reopened` | An issue changed. `data.number` and `data.author` (`id` and `username`); on close, `data.reason` and `data.resolved_by` (the pull request that closed it) or `data.commit` (the commit on the default branch that did), and `data.session` when it was closed in a session; on assignment, `data.assignees` and the newly assigned `data.added`. |
9191 | `comment.created` | A comment or review on an issue or pull request. |
9292 | `comment.edited` | A comment's text was [edited](/guides/pull-requests/#editing-and-deleting-comments). `data.comment_id`, `data.number`, `data.pull_id` on a pull request, and `data.changes.body.from`, what it said before. |
9393 | `comment.deleted` | A comment was [deleted](/guides/pull-requests/#editing-and-deleting-comments). `data.comment_id`, `data.number`, `data.pull_id` on a pull request, and `data.comment` as it was: `id`, `body`, `author` (`id` and `username`), `created_at`, `path` and `line`. |
+1−0
330330 | `<NUMBER>` | |
331331 | `-r, --reason <REASON>` | One of `completed`, `not_planned`. |
332332 | `-c, --comment <COMMENT>` | Leave a comment as it closes. |
333+| `--commit <COMMIT>` | The commit on the default branch that closed it: its message names the issue after a closing keyword (Fixes #12). Its full hash. |
333334
334335 Calls [`close_issue`](/reference/api/issues/close-issue/), [`add_comment`](/reference/api/issues/add-comment/).
335336
+0−0

Binary or large file; its contents are not shown.

+11−1
66 * conversation shows them (components/chat/text.tsx for people's messages,
77 * components/ai-elements/response.tsx for g1t's answers).
88 */
9−import { CircleDot, GitCommitHorizontal, GitPullRequest, Link2 } from "lucide-react";
9+import { Bot, CircleDot, GitCommitHorizontal, GitPullRequest, Link2 } from "lucide-react";
1010 import { type ReactNode, useEffect, useState } from "react";
1111 import { Link } from "react-router";
1212
109109 );
110110 }
111111
112+/** A session, by the end of its id, which is what tells sessions started together apart. */
113+export function SessionChip({ href, id }: { href: string; id: string }) {
114+ return (
115+ <Link to={href} className={cn(CHIP, "font-mono")} data-ref="session">
116+ <Bot size={12} className="text-faint" />
117+ {id.length > 12 ? `${id.slice(0, 4)}…${id.slice(-6)}` : id}
118+ </Link>
119+ );
120+}
121+
112122 /** Another page of the site, linked by its path or a full link to it. */
113123 export function PageChip({ href, children }: { href: string; children: ReactNode }) {
114124 return (
+4−1
88 import { messageParts } from "../lib/commit-message";
99 import { chipStyle } from "../lib/labels";
1010 import { repoAt } from "../lib/markdown-plugins";
11−import { CommitChip } from "./chat/ref-chip";
11+import { CommitChip, SessionChip } from "./chat/ref-chip";
1212 import { RefLink } from "./commit-message";
1313 import { Markdown } from "./markdown";
1414 import { Reactions } from "./reactions";
409409 ) : part.kind === "commit" ? (
410410 // The commit that closed or merged it, by its short hash.
411411 <CommitChip key={at} href={`${base}/commit/${part.sha}`} sha={part.sha} />
412+ ) : part.kind === "session" ? (
413+ // The session it was closed in.
414+ <SessionChip key={at} href={`${base}/work/sessions/${part.id}`} id={part.id} />
412415 ) : (
413416 part.text
414417 ),
+14−0
5555 }
5656 assert.deepEqual(messageParts(""), []);
5757 });
58+
59+test("a session's id is a session, as a note says what it closed the issue in", () => {
60+ const sha = "0123456789abcdef0123456789abcdef01234567";
61+ const id = "ses_01m4z1z7nse8w9cjnecymt7rzn";
62+ assert.deepEqual(messageParts(`closed this in ${sha} via session ${id}`), [
63+ { kind: "text", text: "closed this in " },
64+ { kind: "commit", text: sha, sha },
65+ { kind: "text", text: " via session " },
66+ { kind: "session", text: id, id },
67+ ]);
68+ for (const text of ["ses_short", "uses_01m4z1z7nse8w9cjnecymt7rzn"]) {
69+ assert.deepEqual(messageParts(text), [{ kind: "text", text }], text);
70+ }
71+});
+8−4
4141 /**
4242 * A run of a commit message, or of a note on an issue's timeline: plain
4343 * text, a reference to an issue or pull request (`#12` in the repository
44− * it is in, `acme/web#12` in another), or a commit by its full hash.
44+ * it is in, `acme/web#12` in another), a commit by its full hash, or a
45+ * session by its id (`ses_…`, as in "closed this in … via session ses_…").
4546 */
4647 export type MessagePart =
4748 | { kind: "text"; text: string }
4849 | { kind: "ref"; text: string; repo: string | null; number: number }
49− | { kind: "commit"; text: string; sha: string };
50+ | { kind: "commit"; text: string; sha: string }
51+ | { kind: "session"; text: string; id: string };
5052
51−// `#12` and `acme/web#12`, but not `a#12` or a link's `/page#12`; a full hash on its own.
52−const REFERENCE = /(?<![\w/#.-])(?:([A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9._-]+))?#(\d+)\b|\b([0-9a-f]{40})\b/g;
53+// `#12` and `acme/web#12`, but not `a#12` or a link's `/page#12`; a full hash on its own; a session's id.
54+const REFERENCE =
55+ /(?<![\w/#.-])(?:([A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9._-]+))?#(\d+)\b|\b([0-9a-f]{40})\b|\b(ses_[0-9a-z]{10,60})\b/g;
5356
5457 /** Splits text into its references and the text between them; `Closes #12` keeps `Closes ` as text. */
5558 export function messageParts(text: string): MessagePart[] {
5962 const start = match.index;
6063 if (start > at) parts.push({ kind: "text", text: text.slice(at, start) });
6164 if (match[3]) parts.push({ kind: "commit", text: match[0], sha: match[3] });
65+ else if (match[4]) parts.push({ kind: "session", text: match[0], id: match[4] });
6266 else parts.push({ kind: "ref", text: match[0], repo: match[1] ?? null, number: Number(match[2]) });
6367 at = start + match[0].length;
6468 }
+5−0
183183 /// closed it (`Fixes #12`), when one did.
184184 #[serde(skip_serializing_if = "Option::is_none")]
185185 pub commit: Option<String>,
186+ /// On close: the session it was closed in, when it was: the one the
187+ /// closing commit names (`G1t-Session: ses_…`), or the caller's own
188+ /// session on the issue when it closed it with a token.
189+ #[serde(skip_serializing_if = "Option::is_none")]
190+ pub session: Option<String>,
186191 /// On `issue.assigned`: the people it is now assigned to.
187192 #[serde(skip_serializing_if = "Option::is_none")]
188193 pub assignees: Option<Vec<String>>,
+15−0
18401840 }
18411841
18421842 /// `close_issue` and `reopen_issue`. Each returns `Outcome<Issue>`.
1843+/// `close_issue` takes [`CloseIssueArgs`], which these are a part of.
18431844 #[derive(Debug, Serialize, Deserialize)]
18441845 pub struct IssueActionArgs {
18451846 pub actor: User,
18501851 pub reason: Option<IssueReason>,
18511852 }
18521853
1854+/// `close_issue`: an [`IssueActionArgs`], and the commit that did it, if
1855+/// one did. With `commit`, a commit on the default branch whose message
1856+/// names the issue after a closing keyword (`Fixes #12`), it closes as the
1857+/// push of that commit would have: completed, in that commit, by the
1858+/// caller; a commit that is not on the default branch, or does not name
1859+/// the issue, is refused. Returns `Outcome<Issue>`.
1860+#[derive(Debug, Serialize, Deserialize)]
1861+pub struct CloseIssueArgs {
1862+ #[serde(flatten)]
1863+ pub issue: IssueActionArgs,
1864+ #[serde(default, skip_serializing_if = "Option::is_none")]
1865+ pub commit: Option<String>,
1866+}
1867+
18531868 /// `add_comment`, on an issue or a pull request. On a pull request it may
18541869 /// name a line of the change, and may carry a verdict; nobody can give a
18551870 /// verdict on their own pull request. Returns `Outcome<Comment>`.
+4−0
444444 /// Leave a comment as it closes.
445445 #[arg(short, long)]
446446 pub comment: Option<String>,
447+ /// The commit on the default branch that closed it: its message names
448+ /// the issue after a closing keyword (Fixes #12). Its full hash.
449+ #[arg(long)]
450+ pub commit: Option<String>,
447451 }
448452
449453 #[derive(Debug, Args)]
+1−0
356356 .repo(&repo)
357357 .arg("number", args.number)
358358 .field_opt("reason", reason)
359+ .field_opt("commit", args.commit.as_deref())
359360 .send()?;
360361 done(
361362 ctx,
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…

Comments

Sign in to comment.