Run script/setup in new worktrees and give each task a port block

This commit is contained in:
Ian Gulliver
2026-10-15 11:52:13 -07:00
parent f5e5dd30e3
commit cfa9699f4b
15 changed files with 453 additions and 174 deletions
-4
View File
@@ -6,10 +6,6 @@
- Terminals
PTYs via `github.com/creack/pty`, rendered with xterm.js, one WebSocket per open terminal (the only WebSocket use in byre). Scoped to the task's worktree by default. Shown as a pane tab. Terminals don't survive a byre restart.
- Per-project setup config
A setup script run in each new worktree (dependency install, env file copying), and port allocation so parallel dev servers don't collide.
- Decide the format of per-project setup config
It holds the setup script, port allocation, and the run and capture config that visual reviews use.
- Previews
Web: byre supervises a per-task dev server and exposes it over Tailscale, preferably as a per-task port through `tailscale serve`, since path-prefix proxying breaks apps that use absolute paths. iOS: on-demand simulator screenshots and recordings attached to the task, with Screen Sharing over Tailscale as the manual fallback. Shown as a pane tab.
- Confirm `tailscale serve` can expose per-task preview ports
+2 -1
View File
@@ -823,7 +823,7 @@ components:
Task:
type: object
required: [id, project_id, title, merge_title, branch, worktree_path, base_ref, status, created_by, created_at]
required: [id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, status, created_by, created_at]
properties:
id: { type: string }
project_id: { type: string }
@@ -832,6 +832,7 @@ components:
branch: { type: string }
worktree_path: { type: string }
base_ref: { type: string }
port_base: { type: integer, description: "The first of the task's block of ports (port_base to port_base+9), passed to its setup script and agent as PORT." }
status:
type: string
enum: [active, merged, discarded]
+10 -1
View File
@@ -18,6 +18,15 @@ State lives in the `byre` directory under Go's `os.UserConfigDir()` (on macOS, `
Schema changes are the `migrations` slice in `internal/store/store.go`: named entries, each with a short kebab-case name saying what it does. On open, byre applies, in slice order, every entry whose name isn't in the `migrations` table, each in its own transaction that also records the name. Each runs with foreign keys off, so a migration can rebuild a table other tables reference, and `PRAGMA foreign_key_check` must come back empty before it commits. byre is deployed from task branches that each append their own migrations, so after a merge a migration can land before others the database has already applied, and it still runs. Give a new migration a name no other branch will use, and make any data migration safe to run twice. `sqlite3 byre.db 'SELECT * FROM migrations'` lists what has been applied. A database from before the table existed is moved over on open: the first `PRAGMA user_version` entries are recorded as applied, and `user_version` is no longer read or written.
## Worktree setup and ports
Every task worktree is made by `createTask` (`internal/agents/tasks.go`): new tasks, `create_task`, `start_todo` and forks alike.
- **Ports.** Each active task holds a block of `store.PortBlock` ports starting at `tasks.port_base` (`port_base` on the Task). `Store.CreateTask` gives a new task the lowest block from `store.PortFirst` up that no active task holds, so a merged or discarded task's block is reused. The agent's `claude` process gets `PORT` (the first port, which most dev servers read on their own), and the session guidance names the range and says to run dev servers on it. `sqlite3 byre.db "SELECT id, port_base FROM tasks WHERE status = 'active'"` lists the blocks in use.
- **Setup script.** If the new worktree has an executable `script/setup` (`setupScript` in `internal/agents/setup.go`), byre runs it in the background, in the worktree, with `PORT` added to byre's environment. Nothing in its interface is byre-specific: a script that copies untracked files such as `.env` from the project's checkout finds it as the parent of `git rev-parse --path-format=absolute --git-common-dir`. It is the project's own file, so a branch that changes it changes the setup of tasks started or forked from that branch. It is killed after `setupTimeout`. The task's transcript shows a `setup.started` note and a `setup.ended` note holding the last `setupOutputBytes` of its combined output (`output`), how long it took (`duration_ms`) and `error` when it failed.
- **Waiting.** While setup runs, the task's turns are queued as if the caps were full (`Manager.settingUp`, checked in `turn` and `drain`), so the session shows `queued`; when setup ends, byre drains the queue. Task creation itself doesn't wait.
- **Failure.** A failed setup doesn't stop the agent. The next prompt its session submits gets the failure and the output appended to the `UserPromptSubmit` guidance (`remindTaskGuidance`), and at that moment byre files the same text as a `session_error` inbox item. The setup state and failure live in memory, so a byre restart during setup leaves the queued turn to start without it and nothing reruns the script.
## Closing a task
A task ends `merged` or `discarded`; either way its sessions are stopped, its snapshot refs, worktree and branch are removed, its `open` inbox items become `dismissed` (resolved by whoever closed it), and its record and events remain. `GET /api/tasks/{id}/unmerged` reports what would be lost or merged: commits on the branch that aren't on the default branch, and files, lines and uncommitted paths that differ from the merge base. The UI's Merge and Discard buttons each ask a one-line confirmation and are disabled while the task's session is `running` or `queued`. Merge is also disabled until `unmerged` reports a commit or a changed file; the transcript fetches it when the task is opened and again shortly after each tool result or turn end.
@@ -73,7 +82,7 @@ Agents don't commit; byre does. Each `user.message` event that starts a turn rec
byre chooses the Claude session ID (`vendor_session_id`) up front, so every session can be resumed. Processes start on demand: the first message after a restart resumes the session with `--resume`. Sessions are `running` from the moment a turn is sent until its `result` arrives; on startup, every session still marked `running` was cut off mid-turn, and byre resumes it and sends a message telling the agent to re-check the working tree. Stopping the server kills all agent processes. Merging or discarding a task kills its sessions and marks them `completed`.
Every session gets byre's guidance (`guidance` in `internal/agents/guidance.go`): ask questions with `ask_human`, end each turn with a final message that summarizes it (including every departure from the plan), put everything the human needs in a blocking question's context since that turn files no report, don't commit (byre commits each turn), and don't push, touch other branches or run git maintenance. A task started from a `TODO.md` item gets one more line naming the item and telling the agent to remove it from `TODO.md` in its worktree when the work is done (`taskGuidance`; see "TODO"). It is passed as `--append-system-prompt`, which Claude Code records on a session's first request and reuses on resume, so a running session keeps the text it started with; a `UserPromptSubmit` hook also attaches it as `additionalContext` to every user turn, which reaches existing sessions too.
Every session gets byre's guidance (`guidance` in `internal/agents/guidance.go`): ask questions with `ask_human`, end each turn with a final message that summarizes it (including every departure from the plan), put everything the human needs in a blocking question's context since that turn files no report, don't commit (byre commits each turn), and don't push, touch other branches or run git maintenance. A task agent's guidance also names its port range (see "Worktree setup and ports"). A task started from a `TODO.md` item gets one more line naming the item and telling the agent to remove it from `TODO.md` in its worktree when the work is done (`taskGuidance`; see "TODO"). It is passed as `--append-system-prompt`, which Claude Code records on a session's first request and reuses on resume, so a running session keeps the text it started with; a `UserPromptSubmit` hook also attaches it as `additionalContext` to every user turn, which reaches existing sessions too.
A Stop hook (`bounceProseQuestion` in `internal/agents/guidance.go`) bounces a turn whose final message asks the human something in prose: if `ask_human` wasn't called during the turn and the final message (`last_assistant_message`) has a sentence ending in `?` outside code spans, code blocks, quotes and blockquotes, the stop is blocked once with an instruction to ask through `ask_human`. The per-turn flags reset when the next user message is sent. Claude Code reports the block as a "Stop hook error" notification; that is expected. An agent told explicitly to ask in plain text will refuse the bounce.
+3 -1
View File
@@ -8,6 +8,7 @@ import (
"g.fc.run/misc/byre/internal/api"
"g.fc.run/misc/byre/internal/claude"
"g.fc.run/misc/byre/internal/store"
"g.fc.run/misc/byre/internal/todo"
)
@@ -60,7 +61,8 @@ func (m *Manager) bounceProseQuestion(l *live) claude.HookFunc {
}
func (m *Manager) taskGuidance(ctx context.Context, task api.Task) (string, error) {
g := guidance
g := guidance + fmt.Sprintf("\n- This task's ports are %d to %d; $PORT holds the first. Run dev servers and anything else that listens on these, so parallel tasks don't collide.",
task.PortBase, task.PortBase+store.PortBlock-1)
if task.TodoItem != nil {
g += fmt.Sprintf("\n- This task was started from the item %q in %s at the repo root. When the work is done, remove that item from %s in this worktree, so the removal lands in the same merge as the work.",
*task.TodoItem, todo.File, todo.File)
+14 -2
View File
@@ -62,6 +62,10 @@ type Manager struct {
snapMu sync.Mutex
snaps map[string]*snapState
setupMu sync.Mutex
setups map[string]bool
setupFailures map[string]string
bad *badLines
watchStop chan struct{}
}
@@ -105,6 +109,8 @@ func New(st *store.Store, limits Limits, worktreeRoot, leadRoot string) *Manager
paneFocused: map[string]bool{},
signals: make(chan api.Event, 1024),
snaps: map[string]*snapState{},
setups: map[string]bool{},
setupFailures: map[string]string{},
bad: newBadLines(),
watchStop: make(chan struct{}),
}
@@ -406,7 +412,7 @@ func (m *Manager) turn(ctx context.Context, sess api.AgentSession, task api.Task
return api.AgentSession{}, err
}
if room {
if room && !m.settingUp(task.Id) {
return m.start(ctx, sess, task, actor, text, images, api.AgentSessionStateIdle)
}
@@ -500,6 +506,9 @@ func (m *Manager) drain(ctx context.Context) error {
}
for _, q := range queued {
if m.settingUp(q.TaskID) {
continue
}
room, err := m.hasRoom(ctx, q.Vendor)
if err != nil {
return err
@@ -652,7 +661,7 @@ func (m *Manager) ensureLive(ctx context.Context, sess api.AgentSession, task ap
}
tools := []mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.createTaskTool(l), m.setMergeTitleTool(l)}
hooks := map[string][]claude.HookMatcher{
"UserPromptSubmit": {{Func: remindGuidance(g)}},
"UserPromptSubmit": {{Func: m.remindTaskGuidance(l, g)}},
"Stop": {{Func: m.bounceProseQuestion(l)}},
"PostToolUse": {{Func: m.afterTool(l)}},
}
@@ -677,6 +686,9 @@ func (m *Manager) ensureLive(ctx context.Context, sess api.AgentSession, task ap
Hooks: hooks,
OnBadLine: func(line []byte, reason string) { m.recordBadLine(l, line, reason) },
}
if l.owner == "" {
opts.Env = portEnv(task)
}
started, err := m.store.SessionStarted(ctx, sess.Id)
if err != nil {
return nil, err
+113
View File
@@ -0,0 +1,113 @@
package agents
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/fs"
"log/slog"
"os"
"os/exec"
"path/filepath"
"strconv"
"time"
"g.fc.run/misc/byre/internal/api"
"g.fc.run/misc/byre/internal/claude"
)
const (
setupScript = "script/setup"
setupTimeout = 15 * time.Minute
setupOutputBytes = 64 << 10
)
func portEnv(t api.Task) []string {
return []string{"PORT=" + strconv.Itoa(t.PortBase)}
}
func (m *Manager) startSetup(t api.Task) {
path := filepath.Join(t.WorktreePath, setupScript)
_, err := os.Stat(path)
if errors.Is(err, fs.ErrNotExist) {
return
}
m.setupMu.Lock()
m.setups[t.Id] = true
m.setupMu.Unlock()
go m.runSetup(t, path)
}
func (m *Manager) settingUp(taskID string) bool {
m.setupMu.Lock()
defer m.setupMu.Unlock()
return m.setups[taskID]
}
func (m *Manager) takeSetupFailure(taskID string) string {
m.setupMu.Lock()
defer m.setupMu.Unlock()
msg := m.setupFailures[taskID]
delete(m.setupFailures, taskID)
return msg
}
func (m *Manager) runSetup(t api.Task, path string) {
ctx := context.Background()
m.emitTask(ctx, t, "setup.started", map[string]any{"script": setupScript, "port_base": t.PortBase})
start := time.Now()
cctx, cancel := context.WithTimeout(ctx, setupTimeout)
defer cancel()
cmd := exec.CommandContext(cctx, path)
cmd.Dir = t.WorktreePath
cmd.Env = append(os.Environ(), portEnv(t)...)
out, err := cmd.CombinedOutput()
if cctx.Err() != nil {
err = fmt.Errorf("timed out after %s", setupTimeout)
}
output := string(out)
if len(out) > setupOutputBytes {
output = "…" + string(out[len(out)-setupOutputBytes:])
}
payload := map[string]any{"script": setupScript, "output": output, "duration_ms": time.Since(start).Milliseconds()}
if err != nil {
payload["error"] = err.Error()
}
slog.Info("setup ended", "task", t.Id, "err", err, "took", time.Since(start))
m.emitTask(ctx, t, "setup.ended", payload)
m.setupMu.Lock()
delete(m.setups, t.Id)
if err != nil {
m.setupFailures[t.Id] = fmt.Sprintf("byre ran %s in this worktree when the task was created, and it failed (%s). Its output:\n\n```\n%s\n```\n\nThe worktree may be missing dependencies or env files the setup would have provided.", setupScript, err, output)
}
m.setupMu.Unlock()
err = m.drain(ctx)
if err != nil {
slog.Error("start queued turns", "err", err)
}
}
func (m *Manager) emitTask(ctx context.Context, t api.Task, kind string, payload map[string]any) {
err := m.store.AppendEvent(ctx, api.Event{Kind: kind, ProjectId: &t.ProjectId, TaskId: &t.Id, Actor: "byre", Payload: payload})
if err != nil {
slog.Error("record setup event", "task", t.Id, "kind", kind, "err", err)
}
}
func (m *Manager) remindTaskGuidance(l *live, text string) claude.HookFunc {
return func(ctx context.Context, input json.RawMessage) (map[string]any, error) {
failure := m.takeSetupFailure(l.taskID)
if failure == "" {
return remindGuidance(text)(ctx, input)
}
m.fileError(l, failure)
return remindGuidance(text+"\n\n"+failure)(ctx, input)
}
}
+91
View File
@@ -0,0 +1,91 @@
package agents_test
import (
"os"
"path/filepath"
"strings"
"testing"
"g.fc.run/misc/byre/internal/api"
)
func (e *env) commitSetup(script string) {
e.t.Helper()
err := os.MkdirAll(filepath.Join(e.repo, "script"), 0o755)
if err != nil {
e.t.Fatal(err)
}
err = os.WriteFile(filepath.Join(e.repo, "script", "setup"), []byte(script), 0o755)
if err != nil {
e.t.Fatal(err)
}
gitOut(e.t, e.repo, "add", "script/setup")
gitOut(e.t, e.repo, "commit", "--quiet", "--message", "setup")
}
func (e *env) waitSetup(taskID string) api.Event {
e.t.Helper()
seen := e.waitFor("setup.ended", func(ev api.Event) bool {
return ev.Kind == "setup.ended" && ev.TaskId != nil && *ev.TaskId == taskID
})
return seen[len(seen)-1]
}
func TestSetupScriptAndPorts(t *testing.T) {
e := newEnv(t)
e.commitSetup("#!/bin/sh\nset -e\ncp \"$(git rev-parse --path-format=absolute --git-common-dir)/../.env\" .env\necho \"port $PORT\"\n")
err := os.WriteFile(filepath.Join(e.repo, ".env"), []byte("SECRET=1\n"), 0o644)
if err != nil {
t.Fatal(err)
}
m := e.manager()
task, err := m.CreateTask(e.ctx, e.project, "with setup", login, "main", login)
if err != nil {
t.Fatalf("create task: %v", err)
}
if task.PortBase != 41010 {
t.Fatalf("port_base = %d, want 41010 (41000 is taken by the first task)", task.PortBase)
}
ev := e.waitSetup(task.Id)
if ev.Payload["error"] != nil {
t.Fatalf("setup failed: %v", ev.Payload)
}
want := "port 41010"
if out, _ := ev.Payload["output"].(string); !strings.Contains(out, want) {
t.Fatalf("output %q lacks %q", out, want)
}
env, err := os.ReadFile(filepath.Join(task.WorktreePath, ".env"))
if err != nil || string(env) != "SECRET=1\n" {
t.Fatalf("worktree .env = %q, %v", env, err)
}
_, err = e.store.DiscardTask(e.ctx, e.task.Id, login)
if err != nil {
t.Fatalf("discard: %v", err)
}
reused, err := m.CreateTask(e.ctx, e.project, "reuses ports", login, "main", login)
if err != nil {
t.Fatalf("create task: %v", err)
}
if reused.PortBase != 41000 {
t.Fatalf("port_base = %d, want the freed 41000", reused.PortBase)
}
e.waitSetup(reused.Id)
}
func TestSetupFailure(t *testing.T) {
e := newEnv(t)
e.commitSetup("#!/bin/sh\necho installing\nexit 3\n")
m := e.manager()
task, err := m.CreateTask(e.ctx, e.project, "failing setup", login, "main", login)
if err != nil {
t.Fatalf("create task: %v", err)
}
ev := e.waitSetup(task.Id)
if ev.Payload["error"] != "exit status 3" || ev.Payload["output"] != "installing\n" {
t.Fatalf("setup.ended payload = %v", ev.Payload)
}
}
+5
View File
@@ -85,6 +85,11 @@ func (m *Manager) createTask(ctx context.Context, project api.Project, t api.Tas
if err != nil {
return api.Task{}, err
}
t, err = m.store.GetTask(ctx, t.Id)
if err != nil {
return api.Task{}, err
}
m.startSetup(t)
return t, nil
}
+145 -141
View File
@@ -811,6 +811,9 @@ type Task struct {
// MergeTitle The title of the squash commit Merge makes: the task title until the agent sets another with set_merge_title.
MergeTitle string `json:"merge_title"`
// PortBase The first of the task's block of ports (port_base to port_base+9), passed to its setup script and agent as PORT.
PortBase int `json:"port_base"`
ProjectId string `json:"project_id"`
Status TaskStatus `json:"status"`
Title string `json:"title"`
@@ -2606,148 +2609,149 @@ 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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",
}
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
+3 -1
View File
@@ -37,6 +37,7 @@ type Options struct {
ForkSession bool
GitName string
GitEmail string
Env []string
Guidance string
Hooks map[string][]HookMatcher
MCPServers []*mcp.Server
@@ -355,7 +356,7 @@ func buildEnv(opts Options) []string {
env = append(env, kv)
}
return append(env,
env = append(env,
"CLAUDE_CODE_ENTRYPOINT=sdk-go",
"CLAUDE_CODE_DISABLE_AUTOUPDATER=1",
"GIT_AUTHOR_NAME="+opts.GitName,
@@ -363,6 +364,7 @@ func buildEnv(opts Options) []string {
"GIT_COMMITTER_NAME="+opts.GitName,
"GIT_COMMITTER_EMAIL="+opts.GitEmail,
)
return append(env, opts.Env...)
}
func (s *Session) hookConfig(hooks map[string][]HookMatcher) map[string]any {
+26 -6
View File
@@ -262,8 +262,19 @@ ALTER TABLE tasks ADD COLUMN forked_from TEXT NOT NULL DEFAULT '';
ALTER TABLE tasks ADD COLUMN forked_at INTEGER NOT NULL DEFAULT 0;
ALTER TABLE agent_sessions ADD COLUMN forked_from TEXT NOT NULL DEFAULT '';
ALTER TABLE agent_sessions ADD COLUMN forked_at TEXT NOT NULL DEFAULT '';
`}, {"add-task-port-base", `
ALTER TABLE tasks ADD COLUMN port_base INTEGER NOT NULL DEFAULT 0;
UPDATE tasks SET port_base = (
SELECT 41000 + 10 * (count(*) - 1) FROM tasks AS earlier
WHERE earlier.status = 'active' AND (earlier.created_at < tasks.created_at OR (earlier.created_at = tasks.created_at AND earlier.id <= tasks.id))
) WHERE status = 'active' AND port_base = 0;
`}}
const (
PortFirst = 41000
PortBlock = 10
)
type Store struct {
db *sql.DB
path string
@@ -538,10 +549,19 @@ func (s *Store) CreateTask(ctx context.Context, t api.Task, actor string) error
forkedAt = *t.ForkedAt
}
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
_, err := tx.ExecContext(ctx, `
INSERT INTO tasks (id, project_id, title, merge_title, branch, worktree_path, base_ref, status, created_by, created_at, todo_item, forked_from, forked_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, t.Id, t.ProjectId, t.Title, mergeTitle, t.Branch, t.WorktreePath, t.BaseRef, string(t.Status), t.CreatedBy, formatTime(t.CreatedAt), todoItem, forkedFrom, forkedAt)
err := tx.QueryRowContext(ctx, `
SELECT min(candidate) FROM (
SELECT ? AS candidate
UNION ALL SELECT port_base + ? FROM tasks WHERE status = ?
) WHERE candidate NOT IN (SELECT port_base FROM tasks WHERE status = ?)
`, PortFirst, PortBlock, string(api.TaskStatusActive), string(api.TaskStatusActive)).Scan(&t.PortBase)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, `
INSERT INTO tasks (id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, status, created_by, created_at, todo_item, forked_from, forked_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, t.Id, t.ProjectId, t.Title, mergeTitle, t.Branch, t.WorktreePath, t.BaseRef, t.PortBase, string(t.Status), t.CreatedBy, formatTime(t.CreatedAt), todoItem, forkedFrom, forkedAt)
if err != nil {
return err
}
@@ -987,7 +1007,7 @@ func (s *Store) write(ctx context.Context, fn func(tx *sql.Tx, emit emitFunc) er
const projectColumns = `id, name, repo_path, default_branch, remote, created_by, created_at`
const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at`
const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at`
const sessionColumns = `id, coalesce(task_id, ''), owner, vendor, model, vendor_session_id, state, role, created_by, created_at, forked_from, forked_at`
@@ -1015,7 +1035,7 @@ func scanTask(row scanner) (api.Task, error) {
todoItem := ""
forkedFrom := ""
forkedAt := 0
err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt)
err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt)
if err != nil {
return t, err
}
+10
View File
@@ -848,6 +848,16 @@ function renderEvent(ev, ctx) {
}
case "task.merge_title":
return h("div", { class: "note" }, `${ev.actor} set the merge commit title to "${String(p["merge_title"])}" `, when);
case "setup.started": {
const base = Number(p["port_base"]);
return h("div", { class: "note" }, `running ${String(p["script"])} with ports ${base}–${base + 9} `, when);
}
case "setup.ended": {
const secs = Math.round(Number(p["duration_ms"]) / 1000);
const failed = typeof p["error"] === "string";
const summary = failed ? `${String(p["script"])} failed after ${secs}s: ${String(p["error"])}` : `${String(p["script"])} finished in ${secs}s`;
return h("details", { class: failed ? "note error" : "note" }, h("summary", {}, summary, " ", when), h("pre", {}, String(p["output"] ?? "")));
}
case "task.merged":
return h("div", { class: "note" }, `${ev.actor} merged the task${p["commit"] ? ` as ${String(p["commit"]).slice(0, 7)}` : ""}${p["pushed"] ? ` and pushed it to ${String(p["pushed"])}` : ""} `, when);
case "task.discarded":
+1 -1
View File
File diff suppressed because one or more lines are too long
+2
View File
@@ -774,6 +774,8 @@ export interface components {
branch: string;
worktree_path: string;
base_ref: string;
/** @description The first of the task's block of ports (port_base to port_base+9), passed to its setup script and agent as PORT. */
port_base: number;
/** @enum {string} */
status: "active" | "merged" | "discarded";
created_by: string;
+12
View File
@@ -1008,6 +1008,18 @@ function renderEvent(ev: ByreEvent, ctx: Ctx): HTMLElement | null {
case "task.merge_title":
return h("div", { class: "note" }, `${ev.actor} set the merge commit title to "${String(p["merge_title"])}" `, when);
case "setup.started": {
const base = Number(p["port_base"]);
return h("div", { class: "note" }, `running ${String(p["script"])} with ports ${base}–${base + 9} `, when);
}
case "setup.ended": {
const secs = Math.round(Number(p["duration_ms"]) / 1000);
const failed = typeof p["error"] === "string";
const summary = failed ? `${String(p["script"])} failed after ${secs}s: ${String(p["error"])}` : `${String(p["script"])} finished in ${secs}s`;
return h("details", { class: failed ? "note error" : "note" }, h("summary", {}, summary, " ", when), h("pre", {}, String(p["output"] ?? "")));
}
case "task.merged":
return h(
"div",