Add Co-authored-by trailers to Merge's squash commit

This commit is contained in:
Ian Gulliver
2026-10-15 07:15:51 -07:00
parent c778741a4a
commit 60a1d221f4
15 changed files with 478 additions and 123 deletions
+32 -3
View File
@@ -173,6 +173,25 @@ paths:
"404": { $ref: "#/components/responses/Error" }
"409": { $ref: "#/components/responses/Error" }
/api/tasks/{taskId}/co-authors:
parameters:
- { name: taskId, in: path, required: true, schema: { type: string } }
get:
operationId: getCoAuthors
description: >
The humans other than the caller with a message in the task's transcript (sent its agent a message, or had
an inbox answer delivered under their name), in order of their first message. A merge by the caller gives each one with a git identity a
Co-authored-by trailer.
responses:
"200":
description: The co-authors.
content:
application/json:
schema:
type: array
items: { $ref: "#/components/schemas/Participant" }
"404": { $ref: "#/components/responses/Error" }
/api/tasks/{taskId}/merge:
parameters:
- { name: taskId, in: path, required: true, schema: { type: string } }
@@ -180,9 +199,10 @@ paths:
operationId: mergeTask
description: >
Starts the same agent turn as merge-in. When that turn ends normally, byre squashes the task's worktree
into one commit titled with the task's merge_title on the project's default branch, fast-forwards the project's
checkout to it, then removes the worktree and branch and marks the task merged. Nothing is pushed. Refused while
a turn is running or queued.
into one commit titled with the task's merge_title on the project's default branch, authored by the caller with
a Co-authored-by trailer for each of the task's co-authors, pushes it to origin, fast-forwards the project's
checkout to it, then removes the worktree and branch and marks the task merged. Refused while a turn is running
or queued.
responses:
"202":
description: The merge turn was started or queued.
@@ -634,6 +654,15 @@ components:
deletions: { type: integer }
uncommitted: { type: integer, description: Paths with uncommitted changes in the worktree. }
Participant:
type: object
required: [login, display_name, git_name, git_email]
properties:
login: { type: string }
display_name: { type: string }
git_name: { type: string, description: Empty when the user hasn't set a git identity; they then get no trailer. }
git_email: { type: string }
TaskCreate:
type: object
required: [title]
+4 -2
View File
@@ -24,9 +24,11 @@ A task ends `merged` or `discarded`; either way its sessions are stopped, its sn
`POST /api/tasks/{id}/merge-in` (the transcript's **Sync from main** button, labelled with the default branch) brings the default branch into the task branch through the agent: it sends `mergeInPrompt` (`internal/agents/mergein.go`) as a `byre` message to the task's session, or starts a session with it when the task has none. The prompt has the agent run `git merge --no-ff --no-commit`, resolve conflicts, regenerate generated files rather than hand-merging them, check the build and tests, and leave the merge uncommitted; byre's end-of-turn commit concludes it as a merge commit. The prompt ends by showing the task's current merge title and asking the agent to set a better one with `set_merge_title` if it no longer describes all the work on the branch. It is refused (409) while a session is `running` or `queued`.
A task's `merge_title` is the title of the squash commit Merge makes. It is the task title until the agent calls the `set_merge_title` MCP tool (`internal/agents/mergein.go`), which it may do at any point: one line of at most 72 characters (`maxMergeTitle`), stored in `tasks.merge_title` (empty there means "use the title"), recorded as a `task.merge_title` event that the transcript shows as a note. Malformed titles are rejected with a corrective message, and so is a call once the task is closed. The Merge confirm dialog quotes the title the commit will get.
A task's `merge_title` is the title of the squash commit Merge makes. It is the task title until the agent calls the `set_merge_title` MCP tool (`internal/agents/mergein.go`), which it may do at any point: one line of at most 72 characters (`maxMergeTitle`), stored in `tasks.merge_title` (empty there means "use the title"), recorded as a `task.merge_title` event that the transcript shows as a note. Malformed titles are rejected with a corrective message, and so is a call once the task is closed. The Merge confirm dialog quotes the title the commit will get and lists its co-authors.
`POST /api/tasks/{id}/merge` starts the same turn and records, in memory, that the task is to be merged by the requesting user, who must have a git identity (400 otherwise). When that turn ends normally (not interrupted, not an error), byre squashes the worktree, now containing the default branch, onto it with `git merge-tree --merge-base`, makes one commit titled with the task's `merge_title` with `git commit-tree`, authored and committed by the merging user with no agent attribution, and pushes that commit to the default branch on the `origin` remote of the project's checkout (`git push --porcelain origin <commit>:refs/heads/<default branch>`, run in the checkout). Only once the push is accepted does it run `git merge --ff-only` in the project's checkout (which must have the default branch checked out) and close the task; the `task.merged` event carries the commit and `pushed` (`origin/<default branch>`), and the transcript shows both. When the merge changes nothing, no commit is made and nothing is pushed.
A task's participants are the users who put a message bubble with their name in its transcript, derived from its events (`Store.Participants`): the actors of its `user.message` events, joined to `users` so `byre` and `agent` drop out. That covers messages sent to the agent and inbox answers, whose delivery message carries the name of the first responder in it (`Deliver` in `internal/agents/inbox.go`); dismissing an item puts nothing in the transcript and doesn't count. They are ordered by their first such message. `GET /api/tasks/{id}/co-authors` returns the participants other than the caller, with their git name and email from `PUT /api/me` (empty for a user who hasn't set one). The Merge confirm fetches it and lists `Name <email>` for each co-author, and separately those with no git identity, who get no trailer.
`POST /api/tasks/{id}/merge` starts the same turn and records, in memory, that the task is to be merged by the requesting user, who must have a git identity (400 otherwise). When that turn ends normally (not interrupted, not an error), byre squashes the worktree, now containing the default branch, onto it with `git merge-tree --merge-base`, makes one commit titled with the task's `merge_title` with `git commit-tree`, authored and committed by the merging user with no agent attribution, its body one `Co-authored-by: Name <email>` trailer per co-author with a git identity (`squashMessage` in `internal/agents/mergein.go`; the co-authors are computed when the squash runs, relative to the user who clicked Merge), and pushes that commit to the default branch on the `origin` remote of the project's checkout (`git push --porcelain origin <commit>:refs/heads/<default branch>`, run in the checkout). Only once the push is accepted does it run `git merge --ff-only` in the project's checkout (which must have the default branch checked out) and close the task; the `task.merged` event carries the commit and `pushed` (`origin/<default branch>`), and the transcript shows both. When the merge changes nothing, no commit is made and nothing is pushed.
Before starting the turn, Merge runs `git fetch origin <default branch>` and `git merge --ff-only origin/<default branch>` in the checkout, so the agent merges in what the remote has; the request waits for the fetch, and a failure (network, or a checkout with commits the remote lacks) is returned as the request's error. If the push is still refused, for example because someone pushed during the turn, or fails for any other reason, byre files the error in the inbox and the task stays active; clicking Merge again fetches and goes round once more. A refused push leaves the checkout's default branch where it was, since the squash commit was never moved onto it. If the squash fails, for example because the default branch moved again during the turn, byre also files an error and the task stays active. An interrupted or failed turn, or a byre restart during it, leaves the task active without squashing. `POST /api/tasks/{id}/discard` removes everything without merging.
+2 -2
View File
@@ -482,8 +482,8 @@ Record transcripts from the spike as the seed of the contract test suite.
- **Not done:** timeline scrubbing, terminals, file browser, previews.
- **M3 — Second vendor and review.** Codex adapter with the same interface coverage, review profiles, review triggers, visual review output, cross-vendor review.
- **M4 — Merging and history.** Full merge cycle with squash, authorship and co-author trailers, drafted commit messages, merge-resolution stage, git notes, task completion and archive, archive search, GitHub Issues integration.
- **Done:** Merge and Discard replace "Complete task". Merge squashes the worktree, uncommitted files included, into one commit on the default branch, authored by the merging user, pushes it to the project's `origin`, and then fast-forwards the project checkout; it refuses while a turn is running. Merge first has the agent merge the default branch in (also available alone as "Sync from main"), then squashes when that turn ends. Merge fetches the remote's default branch into the checkout before the merge-in; a refused or failed push is filed as an error and the task stays active for another Merge. The squash commit's title is the task's merge title: the task title until the agent sets another with the `set_merge_title` MCP tool, which the merge-in prompt asks it to do when the current title no longer describes the branch.
- **Not done:** co-author trailers, human editing of the drafted message before confirming, merge-resolution stage, git notes (and pushing the notes ref), repeated merge cycles, archive search, GitHub Issues.
- **Done:** Merge and Discard replace "Complete task". Merge squashes the worktree, uncommitted files included, into one commit on the default branch, authored by the merging user, pushes it to the project's `origin`, and then fast-forwards the project checkout; it refuses while a turn is running. Merge first has the agent merge the default branch in (also available alone as "Sync from main"), then squashes when that turn ends. Merge fetches the remote's default branch into the checkout before the merge-in; a refused or failed push is filed as an error and the task stays active for another Merge. The squash commit's title is the task's merge title: the task title until the agent sets another with the `set_merge_title` MCP tool, which the merge-in prompt asks it to do when the current title no longer describes the branch. The commit carries a `Co-authored-by` trailer for every other human with a message bubble in the task's transcript (a message to its agent, or an inbox answer delivered under their name; dismissals don't count), derived from the task's `user.message` events rather than a `Participation` table; the Merge confirm lists them.
- **Not done:** co-author trailers for triggering forks, human editing of the drafted message before confirming, merge-resolution stage, git notes (and pushing the notes ref), repeated merge cycles, archive search, GitHub Issues.
- **M5 — Workflows.** Workflow definitions and storage, stages, gates, bounded loops, plan artifact, eval stages, task intake, mid-task swapping.
- **M6 — Fan-out.** `export_context` and handoff bundles, same-vendor forks, cross-vendor handoffs, fork-and-compare, just-in-time fan-out with limits.
- **M7 — Inbox triage.**
+106 -2
View File
@@ -407,11 +407,16 @@ func (e *env) openQuestions(sessionID string) []api.InboxItem {
}
func (e *env) respond(m *agents.Manager, item api.InboxItem, typ api.InboxResponseType, option, text string, state api.InboxState) {
e.t.Helper()
e.respondAs(m, login, item, typ, option, text, state)
}
func (e *env) respondAs(m *agents.Manager, user string, item api.InboxItem, typ api.InboxResponseType, option, text string, state api.InboxState) {
e.t.Helper()
resp := api.InboxResponse{
Id: store.NewID(),
ItemId: item.Id,
User: login,
User: user,
Type: typ,
Attachments: []api.Attachment{},
CreatedAt: time.Now().UTC(),
@@ -423,7 +428,7 @@ func (e *env) respond(m *agents.Manager, item api.InboxItem, typ api.InboxRespon
resp.Text = &text
}
err := e.store.Respond(e.ctx, login, []store.Resolution{{Response: resp, State: state}})
err := e.store.Respond(e.ctx, user, []store.Resolution{{Response: resp, State: state}})
if err != nil {
e.t.Fatalf("respond: %v", err)
}
@@ -672,6 +677,105 @@ func TestMergeResolvesConflict(t *testing.T) {
}
}
func TestMergeCoAuthors(t *testing.T) {
t.Parallel()
e := newEnv(t)
m := e.manager()
people := []struct{ login, name, gitName, gitEmail string }{
{"carol@example.com", "Carol", "Carol Test", "carol@byre.invalid"},
{"bob@example.com", "Bob", "Bob Test", "bob@byre.invalid"},
{"dave@example.com", "Dave", "", ""},
}
const dismisser = "erin@example.com"
_, err := e.store.EnsureUser(e.ctx, dismisser, "Erin")
if err != nil {
t.Fatalf("ensure user: %v", err)
}
_, err = e.store.UpdateUserGit(e.ctx, dismisser, "Erin Test", "erin@byre.invalid")
if err != nil {
t.Fatalf("update user: %v", err)
}
for _, p := range people {
_, err := e.store.EnsureUser(e.ctx, p.login, p.name)
if err != nil {
t.Fatalf("ensure user: %v", err)
}
if p.gitName != "" {
_, err = e.store.UpdateUserGit(e.ctx, p.login, p.gitName, p.gitEmail)
if err != nil {
t.Fatalf("update user: %v", err)
}
}
}
sess, err := m.Create(e.ctx, e.task, e.user, colorPrompt, testModel)
if err != nil {
t.Fatalf("create session: %v", err)
}
e.waitIdle()
questions := e.openQuestions(sess.Id)
if len(questions) != 1 {
t.Fatalf("want one open question, got %+v", questions)
}
options, _ := questions[0].Payload["options"].([]any)
e.respondAs(m, people[0].login, questions[0], api.Option, fmt.Sprint(options[0]), "", api.Answered)
e.waitIdle()
for _, p := range people[1:] {
_, err = m.Send(e.ctx, sess.Id, p.login, "Reply with the single word ok.", "")
if err != nil {
t.Fatalf("send as %s: %v", p.login, err)
}
e.waitIdle()
}
reports, err := e.store.ListInbox(e.ctx, store.InboxFilter{TaskID: e.task.Id, State: api.Open})
if err != nil || len(reports) == 0 {
t.Fatalf("want open reports to dismiss: %+v %v", reports, err)
}
e.respondAs(m, dismisser, reports[0], api.Dismiss, "", "", api.Dismissed)
coAuthors, err := m.CoAuthors(e.ctx, e.task.Id, login)
if err != nil {
t.Fatalf("co-authors: %v", err)
}
got := []string{}
for _, p := range coAuthors {
got = append(got, p.Login+"|"+p.GitName+"|"+p.GitEmail)
}
want := []string{}
for _, p := range people {
want = append(want, p.login+"|"+p.gitName+"|"+p.gitEmail)
}
if strings.Join(got, ",") != strings.Join(want, ",") {
t.Fatalf("co-authors %v, want %v", got, want)
}
_, err = m.Merge(e.ctx, e.task, e.project, e.user)
if err != nil {
t.Fatalf("merge: %v", err)
}
e.waitFor("task merged", func(ev api.Event) bool { return ev.Kind == "task.merged" })
message := gitOut(t, e.repo, "log", "-1", "--format=%B")
t.Logf("merge commit message:\n%s", message)
if author := gitOut(t, e.repo, "log", "-1", "--format=%an <%ae>|%cn <%ce>"); author != gitName+" <"+gitEmail+">|"+gitName+" <"+gitEmail+">" {
t.Fatalf("merge commit author|committer %s, want %s", author, gitName)
}
cmd := exec.Command("git", "interpret-trailers", "--parse")
cmd.Stdin = strings.NewReader(message)
out, err := cmd.Output()
if err != nil {
t.Fatalf("interpret-trailers: %v", err)
}
trailers := strings.TrimSpace(string(out))
wantTrailers := "Co-authored-by: Carol Test <carol@byre.invalid>\nCo-authored-by: Bob Test <bob@byre.invalid>"
if trailers != wantTrailers {
t.Fatalf("trailers:\n%s\nwant:\n%s", trailers, wantTrailers)
}
}
func TestMergeFetchesMovedRemote(t *testing.T) {
t.Parallel()
e := newEnv(t)
+35 -1
View File
@@ -100,7 +100,12 @@ func (m *Manager) squashAndClose(ctx context.Context, taskID, login string) erro
return err
}
commit, err := git.SquashCommit(project.RepoPath, task.WorktreePath, project.DefaultBranch, task.MergeTitle, []string{
coAuthors, err := m.CoAuthors(ctx, taskID, login)
if err != nil {
return err
}
commit, err := git.SquashCommit(project.RepoPath, task.WorktreePath, project.DefaultBranch, squashMessage(task.MergeTitle, coAuthors), []string{
"GIT_AUTHOR_NAME=" + user.GitName,
"GIT_AUTHOR_EMAIL=" + user.GitEmail,
"GIT_COMMITTER_NAME=" + user.GitName,
@@ -132,6 +137,35 @@ func (m *Manager) squashAndClose(ctx context.Context, taskID, login string) erro
return err
}
func (m *Manager) CoAuthors(ctx context.Context, taskID, login string) ([]api.Participant, error) {
participants, err := m.store.Participants(ctx, taskID)
if err != nil {
return nil, err
}
coAuthors := []api.Participant{}
for _, p := range participants {
if p.Login != login {
coAuthors = append(coAuthors, p)
}
}
return coAuthors, nil
}
func squashMessage(title string, coAuthors []api.Participant) string {
trailers := []string{}
for _, p := range coAuthors {
if p.GitName == "" || p.GitEmail == "" {
continue
}
trailers = append(trailers, fmt.Sprintf("Co-authored-by: %s <%s>", p.GitName, p.GitEmail))
}
if len(trailers) == 0 {
return title
}
return title + "\n\n" + strings.Join(trailers, "\n")
}
func (m *Manager) RemoveTask(ctx context.Context, task api.Task, project api.Project, actor string) error {
err := m.StopTask(ctx, task.Id, actor)
if err != nil {
+147 -103
View File
@@ -531,6 +531,16 @@ type NotificationPrefs struct {
Reports bool `json:"reports"`
}
// Participant defines model for Participant.
type Participant struct {
DisplayName string `json:"display_name"`
GitEmail string `json:"git_email"`
// GitName Empty when the user hasn't set a git identity; they then get no trailer.
GitName string `json:"git_name"`
Login string `json:"login"`
}
// Project defines model for Project.
type Project struct {
CreatedAt time.Time `json:"created_at"`
@@ -884,6 +894,9 @@ type ServerInterface interface {
// (GET /api/tasks/{taskId}/changes)
GetTaskChanges(w http.ResponseWriter, r *http.Request, taskId string, params GetTaskChangesParams)
// (GET /api/tasks/{taskId}/co-authors)
GetCoAuthors(w http.ResponseWriter, r *http.Request, taskId string)
// (GET /api/tasks/{taskId}/diff)
GetTaskDiff(w http.ResponseWriter, r *http.Request, taskId string, params GetTaskDiffParams)
@@ -1548,6 +1561,32 @@ func (siw *ServerInterfaceWrapper) GetTaskChanges(w http.ResponseWriter, r *http
handler.ServeHTTP(w, r)
}
// GetCoAuthors operation middleware
func (siw *ServerInterfaceWrapper) GetCoAuthors(w http.ResponseWriter, r *http.Request) {
var err error
_ = err
// ------------- Path parameter "taskId" -------------
var taskId string
err = runtime.BindStyledParameterWithOptions("simple", "taskId", r.PathValue("taskId"), &taskId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "taskId", Err: err})
return
}
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetCoAuthors(w, r, taskId)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r)
}
// GetTaskDiff operation middleware
func (siw *ServerInterfaceWrapper) GetTaskDiff(w http.ResponseWriter, r *http.Request) {
@@ -1999,6 +2038,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/projects/{projectId}/tasks", wrapper.CreateTask)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}", wrapper.GetTask)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/unmerged", wrapper.GetUnmerged)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/co-authors", wrapper.GetCoAuthors)
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/tasks/{taskId}/merge", wrapper.MergeTask)
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/tasks/{taskId}/merge-in", wrapper.MergeIn)
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/tasks/{taskId}/discard", wrapper.DiscardTask)
@@ -2028,109 +2068,113 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
// const string: with thousands of chunks the chained `+` fold is several
// times slower for the Go compiler than parsing a slice literal.
var swaggerSpec = []string{
"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",
"3hf0mIcswhpjBy3883CdzuRfNqN1f//fAQAA//8=",
}
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
+15
View File
@@ -301,6 +301,21 @@ func (s *Server) GetUnmerged(w http.ResponseWriter, r *http.Request, taskID stri
})
}
func (s *Server) GetCoAuthors(w http.ResponseWriter, r *http.Request, taskID string) {
_, err := s.store.GetTask(r.Context(), taskID)
if err != nil {
writeStoreError(w, err)
return
}
coAuthors, err := s.agents.CoAuthors(r.Context(), taskID, actor(r))
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
writeJSON(w, http.StatusOK, coAuthors)
}
func (s *Server) MergeTask(w http.ResponseWriter, r *http.Request, taskID string) {
t, p, ok := s.activeTask(w, r, taskID)
if !ok {
+11 -1
View File
@@ -291,7 +291,17 @@ func TestMergeTask(t *testing.T) {
task := api.Task{}
h.do("POST", "/api/projects/"+p.Id+"/tasks", api.TaskCreate{Title: "Add notes"}, &task)
code := h.do("POST", "/api/tasks/"+task.Id+"/merge", nil, nil)
coAuthors := []api.Participant{}
code := h.do("GET", "/api/tasks/"+task.Id+"/co-authors", nil, &coAuthors)
if code != http.StatusOK || len(coAuthors) != 0 {
t.Fatalf("co-authors: %d %+v", code, coAuthors)
}
code = h.do("GET", "/api/tasks/nope/co-authors", nil, nil)
if code != http.StatusNotFound {
t.Fatalf("want 404 for co-authors of an unknown task, got %d", code)
}
code = h.do("POST", "/api/tasks/"+task.Id+"/merge", nil, nil)
if code != http.StatusBadRequest {
t.Fatalf("want 400 merging without a git identity, got %d", code)
}
+24
View File
@@ -519,6 +519,30 @@ func (s *Store) closeTask(ctx context.Context, id string, status api.TaskStatus,
return s.GetTask(ctx, id)
}
func (s *Store) Participants(ctx context.Context, taskID string) ([]api.Participant, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT u.login, u.display_name, u.git_name, u.git_email
FROM events e JOIN users u ON u.login = e.actor
WHERE e.task_id = ? AND e.kind = 'user.message'
GROUP BY u.login ORDER BY min(e.seq)
`, taskID)
if err != nil {
return nil, err
}
defer rows.Close()
participants := []api.Participant{}
for rows.Next() {
p := api.Participant{}
err = rows.Scan(&p.Login, &p.DisplayName, &p.GitName, &p.GitEmail)
if err != nil {
return nil, err
}
participants = append(participants, p)
}
return participants, rows.Err()
}
func (s *Store) EventsAfter(ctx context.Context, after int64, limit int) ([]api.Event, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT seq, kind, project_id, task_id, agent_session_id, actor, payload, created_at
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
+25 -3
View File
@@ -123,9 +123,19 @@ export function mountTranscript(root) {
const target = state.projects.find((p) => p.id === task.project_id)?.default_branch ?? "the default branch";
const busy = sess?.state === "running" || sess?.state === "queued";
const merge = h("button", { disabled: busy || unmerged?.taskId !== task.id || !unmerged.has, title: `The agent merges ${target} in, then byre squashes into one commit on ${target} and pushes it to origin` }, "Merge");
merge.onclick = () => confirmThen(`Merge "${task.title}" into ${target}?`, "Merge", false, async () => {
await post("/api/tasks/{taskId}/merge", { path: { taskId: task.id } });
});
merge.onclick = async () => {
let coAuthors;
try {
coAuthors = await get("/api/tasks/{taskId}/co-authors", { path: { taskId: task.id } });
}
catch (err) {
void showAlert(errorText(err));
return;
}
await confirmThen(mergeQuestion(task.merge_title, target, coAuthors), "Merge", false, async () => {
await post("/api/tasks/{taskId}/merge", { path: { taskId: task.id } });
});
};
const mergeIn = h("button", { disabled: busy, title: `Ask the agent to bring ${target} into this branch and resolve any conflicts` }, `Sync from ${target}`);
mergeIn.onclick = async () => {
mergeIn.disabled = true;
@@ -747,6 +757,18 @@ function messageImages(ids) {
return h("a", { href: src, target: "_blank", rel: "noopener" }, h("img", { src, alt: "attached image", loading: "lazy" }));
}));
}
function mergeQuestion(title, target, coAuthors) {
const lines = [`Merge "${title}" into ${target}?`];
const credited = coAuthors.filter((p) => p.git_name && p.git_email);
const missing = coAuthors.filter((p) => !p.git_name || !p.git_email);
if (credited.length > 0) {
lines.push("", "Co-authored-by:", ...credited.map((p) => ` ${p.git_name} <${p.git_email}>`));
}
if (missing.length > 0) {
lines.push("", "No git identity, so no trailer:", ...missing.map((p) => ` ${p.display_name} (${p.login})`));
}
return lines.join("\n");
}
async function confirmThen(question, action, danger, act) {
if (!(await showConfirm(question, action, danger))) {
return;
+1 -1
View File
File diff suppressed because one or more lines are too long
+50 -1
View File
@@ -160,6 +160,25 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/tasks/{taskId}/co-authors": {
parameters: {
query?: never;
header?: never;
path: {
taskId: string;
};
cookie?: never;
};
/** @description The humans other than the caller with a message in the task's transcript (sent its agent a message, or had an inbox answer delivered under their name), in order of their first message. A merge by the caller gives each one with a git identity a Co-authored-by trailer. */
get: operations["getCoAuthors"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/tasks/{taskId}/merge": {
parameters: {
query?: never;
@@ -171,7 +190,7 @@ export interface paths {
};
get?: never;
put?: never;
/** @description Starts the same agent turn as merge-in. When that turn ends normally, byre squashes the task's worktree into one commit titled with the task's merge_title on the project's default branch, fast-forwards the project's checkout to it, then removes the worktree and branch and marks the task merged. Nothing is pushed. Refused while a turn is running or queued. */
/** @description Starts the same agent turn as merge-in. When that turn ends normally, byre squashes the task's worktree into one commit titled with the task's merge_title on the project's default branch, authored by the caller with a Co-authored-by trailer for each of the task's co-authors, pushes it to origin, fast-forwards the project's checkout to it, then removes the worktree and branch and marks the task merged. Refused while a turn is running or queued. */
post: operations["mergeTask"];
delete?: never;
options?: never;
@@ -595,6 +614,13 @@ export interface components {
/** @description Paths with uncommitted changes in the worktree. */
uncommitted: number;
};
Participant: {
login: string;
display_name: string;
/** @description Empty when the user hasn't set a git identity; they then get no trailer. */
git_name: string;
git_email: string;
};
TaskCreate: {
title: string;
};
@@ -1163,6 +1189,29 @@ export interface operations {
409: components["responses"]["Error"];
};
};
getCoAuthors: {
parameters: {
query?: never;
header?: never;
path: {
taskId: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description The co-authors. */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Participant"][];
};
};
404: components["responses"]["Error"];
};
};
mergeTask: {
parameters: {
query?: never;
+1
View File
@@ -17,6 +17,7 @@ export type AgentProcess = Schemas["AgentProcess"];
export type Zombie = Schemas["Zombie"];
export type ProtocolErrors = Schemas["ProtocolErrors"];
export type Snapshot = Schemas["Snapshot"];
export type Participant = Schemas["Participant"];
type Method = "get" | "post" | "put" | "delete";
+24 -3
View File
@@ -1,4 +1,4 @@
import { get, post, type ByreEvent, type Snapshot } from "./api.js";
import { get, post, type ByreEvent, type Participant, type Snapshot } from "./api.js";
import { ago, errorText, h, replace, showAlert, showConfirm } from "./dom.js";
import { markdown } from "./markdown.js";
import { openChanges } from "./panes.js";
@@ -161,10 +161,18 @@ export function mountTranscript(root: HTMLElement): () => void {
{ disabled: busy || unmerged?.taskId !== task.id || !unmerged.has, title: `The agent merges ${target} in, then byre squashes into one commit on ${target} and pushes it to origin` },
"Merge",
);
merge.onclick = () =>
confirmThen(`Merge "${task.title}" into ${target}?`, "Merge", false, async () => {
merge.onclick = async () => {
let coAuthors;
try {
coAuthors = await get("/api/tasks/{taskId}/co-authors", { path: { taskId: task.id } });
} catch (err) {
void showAlert(errorText(err));
return;
}
await confirmThen(mergeQuestion(task.merge_title, target, coAuthors), "Merge", false, async () => {
await post("/api/tasks/{taskId}/merge", { path: { taskId: task.id } });
});
};
const mergeIn = h("button", { disabled: busy, title: `Ask the agent to bring ${target} into this branch and resolve any conflicts` }, `Sync from ${target}`);
mergeIn.onclick = async () => {
mergeIn.disabled = true;
@@ -881,6 +889,19 @@ function messageImages(ids: unknown): HTMLElement | null {
);
}
function mergeQuestion(title: string, target: string, coAuthors: Participant[]): string {
const lines = [`Merge "${title}" into ${target}?`];
const credited = coAuthors.filter((p) => p.git_name && p.git_email);
const missing = coAuthors.filter((p) => !p.git_name || !p.git_email);
if (credited.length > 0) {
lines.push("", "Co-authored-by:", ...credited.map((p) => ` ${p.git_name} <${p.git_email}>`));
}
if (missing.length > 0) {
lines.push("", "No git identity, so no trailer:", ...missing.map((p) => ` ${p.display_name} (${p.login})`));
}
return lines.join("\n");
}
async function confirmThen(question: string, action: string, danger: boolean, act: () => Promise<void>) {
if (!(await showConfirm(question, action, danger))) {
return;