Screens 2 and 3: new-task form, live log and diff; measured the merge gap they expose

This commit is contained in:
Sam
2026-11-10 12:15:45 +11:00
parent 34f60872f6
commit 50fde87450
12 changed files with 801 additions and 44 deletions

View File

@@ -57,17 +57,104 @@ func New() (*Client, error) {
func (c *Client) UsingUserKey() bool { return os.Getenv("PAPERCLIP_BOARD_KEY") != "" }
type Issue struct {
ID string `json:"id"`
Identifier string `json:"identifier"`
Title string `json:"title"`
Status string `json:"status"`
Priority string `json:"priority"`
ProjectID string `json:"projectId"`
ParentID string `json:"parentId"`
AssigneeAgent string `json:"assigneeAgentId"`
AssigneeUser string `json:"assigneeUserId"`
ExecutionState string `json:"-"`
UpdatedAt string `json:"updatedAt"`
ID string `json:"id"`
Description string `json:"description"`
Identifier string `json:"identifier"`
Title string `json:"title"`
Status string `json:"status"`
Priority string `json:"priority"`
ProjectID string `json:"projectId"`
ParentID string `json:"parentId"`
AssigneeAgent string `json:"assigneeAgentId"`
AssigneeUser string `json:"assigneeUserId"`
UpdatedAt string `json:"updatedAt"`
// Workspace is where Paperclip put this task's own copy of the code. It is
// null when the instance does not make worktrees.
Workspace *struct {
CWD string `json:"cwd"`
Branch string `json:"branchName"`
BaseRef string `json:"baseRef"`
Strategy string `json:"strategyType"`
} `json:"currentExecutionWorkspace"`
}
// Run is one heartbeat run: when it started, how it ended, what it cost.
type Run struct {
ID string `json:"runId"`
Status string `json:"status"`
Source string `json:"invocationSource"`
AgentID string `json:"agentId"`
StartedAt string `json:"startedAt"`
FinishedAt string `json:"finishedAt"`
ErrorCode string `json:"errorCode"`
}
// Activity is one structural event on a task.
type Activity struct {
ID string `json:"id"`
ActorType string `json:"actorType"`
Action string `json:"action"`
CreatedAt string `json:"createdAt"`
}
// Runs returns a task's run history, newest first.
func (c *Client) Runs(issueID string) ([]Run, error) {
var out []Run
err := c.do("GET", "/api/issues/"+issueID+"/runs", nil, &out)
return out, err
}
// Activity returns a task's structural events, newest first.
func (c *Client) Activity(issueID string) ([]Activity, error) {
var out []Activity
err := c.do("GET", "/api/issues/"+issueID+"/activity", nil, &out)
return out, err
}
// CreateTask creates a task, unassigned. Blockers are never accepted at
// creation: they must be a separate call, or the write is ignored.
type NewTask struct {
Title string
Description string
ProjectID string
}
func (c *Client) CreateTask(t NewTask) (Issue, error) {
if !c.UsingUserKey() {
return Issue{}, errors.New("refusing to write with the agent key: set PAPERCLIP_BOARD_KEY")
}
var out Issue
body := map[string]any{
"title": t.Title,
"description": t.Description,
"projectId": t.ProjectID,
"status": "todo",
}
err := c.do("POST", "/api/companies/"+c.Company+"/issues", body, &out)
return out, err
}
// SetBlockers replaces a task's blocked-by list.
func (c *Client) SetBlockers(issueID string, blockerIDs []string) (Issue, error) {
if !c.UsingUserKey() {
return Issue{}, errors.New("refusing to write with the agent key: set PAPERCLIP_BOARD_KEY")
}
var out Issue
err := c.do("PATCH", "/api/issues/"+issueID, map[string]any{"blockedByIssueIds": blockerIDs}, &out)
return out, err
}
// Assign gives a task to an agent. It is always a separate call from creation:
// an assigned task is claimed by that agent's run within seconds, and the run
// then owns the task's fields.
func (c *Client) Assign(issueID, agentID string) (Issue, error) {
if !c.UsingUserKey() {
return Issue{}, errors.New("refusing to write with the agent key: set PAPERCLIP_BOARD_KEY")
}
var out Issue
err := c.do("PATCH", "/api/issues/"+issueID, map[string]any{"assigneeAgentId": agentID}, &out)
return out, err
}
type Project struct {