18516c8f8b
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
189 lines
6.4 KiB
Go
189 lines
6.4 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
// repeatableFlag collects one or more occurrences of a repeatable CLI flag
|
|
// (e.g. --status todo --status in_progress) into an ordered slice, so a
|
|
// caller can reach the board-read endpoint's per-value union semantics for
|
|
// a repeatable parameter.
|
|
type repeatableFlag []string
|
|
|
|
func (f *repeatableFlag) String() string {
|
|
if f == nil {
|
|
return ""
|
|
}
|
|
return strings.Join(*f, ",")
|
|
}
|
|
|
|
func (f *repeatableFlag) Set(value string) error {
|
|
*f = append(*f, value)
|
|
return nil
|
|
}
|
|
|
|
// runTasksCmd dispatches `agentctl kandev tasks <subcmd>`. The
|
|
// singular `agentctl kandev task <subcmd>` group (get/update/create)
|
|
// remains the back-compat path; this plural group adds the verbs
|
|
// the office runtime needs without breaking older scripts.
|
|
func runTasksCmd(args []string) int {
|
|
if len(args) == 0 {
|
|
cliError("usage: agentctl kandev tasks <list|move|archive|message|conversation> [flags]")
|
|
return 1
|
|
}
|
|
switch args[0] {
|
|
case subcmdList:
|
|
return tasksList(args[1:])
|
|
case "move":
|
|
return tasksMove(args[1:])
|
|
case "archive":
|
|
return tasksArchive(args[1:])
|
|
case "message":
|
|
return tasksMessage(args[1:])
|
|
case "conversation":
|
|
return tasksConversation(args[1:])
|
|
default:
|
|
cliError("unknown tasks subcommand: %s", args[0])
|
|
return 1
|
|
}
|
|
}
|
|
|
|
// tasksList returns the board a run may act on: it calls the signed
|
|
// runtime board-read endpoint (GET /api/v1/office/runtime/tasks), which
|
|
// resolves its workspace from the run token's claim rather than
|
|
// KANDEV_WORKSPACE_ID, so it succeeds even when that variable is unset.
|
|
// One flag per query parameter the endpoint accepts; --status and
|
|
// --priority are repeatable and reach the endpoint as repeated query
|
|
// values with union semantics.
|
|
func tasksList(args []string) int {
|
|
fs := flag.NewFlagSet("tasks list", flag.ContinueOnError)
|
|
var status repeatableFlag
|
|
var priority repeatableFlag
|
|
fs.Var(&status, "status", "Filter by status (repeatable)")
|
|
fs.Var(&priority, "priority", "Filter by priority (repeatable)")
|
|
assignee := fs.String("assignee", "", "Filter by assignee agent ID")
|
|
project := fs.String("project", "", "Filter by project ID")
|
|
sortField := fs.String("sort", "", "Sort column: updated_at | created_at | priority")
|
|
order := fs.String("order", "", "Sort direction: asc | desc (default desc)")
|
|
limit := fs.String("limit", "", "Page size, up to 500 (default 100)")
|
|
cursor := fs.String("cursor", "", "Continuation cursor value from a prior page")
|
|
cursorID := fs.String("cursor-id", "", "Continuation cursor id from a prior page")
|
|
includeSystem := fs.String("include-system", "", "Include kandev-managed system-workflow tasks: true | false")
|
|
if err := fs.Parse(args); err != nil {
|
|
cliError("parse flags: %v", err)
|
|
return 1
|
|
}
|
|
values := url.Values{}
|
|
for _, v := range status {
|
|
values.Add("status", v)
|
|
}
|
|
for _, v := range priority {
|
|
values.Add("priority", v)
|
|
}
|
|
setQueryIfNonEmpty(values, "assignee", *assignee)
|
|
setQueryIfNonEmpty(values, "project", *project)
|
|
setQueryIfNonEmpty(values, "sort", *sortField)
|
|
setQueryIfNonEmpty(values, "order", *order)
|
|
setQueryIfNonEmpty(values, "limit", *limit)
|
|
setQueryIfNonEmpty(values, "cursor", *cursor)
|
|
setQueryIfNonEmpty(values, "cursor_id", *cursorID)
|
|
setQueryIfNonEmpty(values, "include_system", *includeSystem)
|
|
return getWithQuery("/api/v1/office/runtime/tasks", values)
|
|
}
|
|
|
|
func setQueryIfNonEmpty(values url.Values, key, value string) {
|
|
if value != "" {
|
|
values.Set(key, value)
|
|
}
|
|
}
|
|
|
|
// tasksMove fails closed because Office runs do not have a signed runtime
|
|
// capability for arbitrary workflow-step moves.
|
|
func tasksMove(args []string) int {
|
|
fs := flag.NewFlagSet("tasks move", flag.ContinueOnError)
|
|
_ = fs.String("id", "", "Task ID (defaults to $KANDEV_TASK_ID)")
|
|
_ = fs.String("step", "", "Destination workflow step ID")
|
|
_ = fs.String("prompt", "", "Optional handoff prompt")
|
|
if err := fs.Parse(args); err != nil {
|
|
cliError("parse flags: %v", err)
|
|
return 1
|
|
}
|
|
cliError("tasks move is unavailable to Office agents; use `kandev task update --status <status>` for signed status changes, or ask a human or admin to move the task between workflow steps")
|
|
return 1
|
|
}
|
|
|
|
// tasksArchive fails closed because Office runs do not have a signed runtime
|
|
// capability for archival.
|
|
func tasksArchive(args []string) int {
|
|
fs := flag.NewFlagSet("tasks archive", flag.ContinueOnError)
|
|
_ = fs.String("id", "", "Task ID (defaults to $KANDEV_TASK_ID)")
|
|
if err := fs.Parse(args); err != nil {
|
|
cliError("parse flags: %v", err)
|
|
return 1
|
|
}
|
|
cliError("tasks archive is unavailable to Office agents; ask a human or admin to archive the task")
|
|
return 1
|
|
}
|
|
|
|
// tasksMessage posts a comment through the signed Office runtime surface.
|
|
// The runtime validates task scope and derives attribution from the run token.
|
|
func tasksMessage(args []string) int {
|
|
fs := flag.NewFlagSet("tasks message", flag.ContinueOnError)
|
|
id := fs.String("id", "", "Task ID (required; defaults to $KANDEV_TASK_ID)")
|
|
prompt := fs.String("prompt", "", "Message body (required; use \"-\" for stdin)")
|
|
if err := fs.Parse(args); err != nil {
|
|
cliError("parse flags: %v", err)
|
|
return 1
|
|
}
|
|
client, err := newKandevClient()
|
|
if err != nil {
|
|
cliError("%v", err)
|
|
return 1
|
|
}
|
|
taskID := resolveTaskID(*id, client.taskID)
|
|
message, err := resolveBody(*prompt)
|
|
if err != nil {
|
|
cliError("read prompt: %v", err)
|
|
return 1
|
|
}
|
|
if taskID == "" || message == "" {
|
|
cliError("--id (or KANDEV_TASK_ID) and --prompt are required")
|
|
return 1
|
|
}
|
|
payload := map[string]string{
|
|
"task_id": taskID,
|
|
"body": message,
|
|
}
|
|
body, status, err := client.do(http.MethodPost,
|
|
"/api/v1/office/runtime/comments", payload)
|
|
return handleResponse(body, status, err)
|
|
}
|
|
|
|
// tasksConversation lists comments on a task. Useful for catching
|
|
// up after a wake when the prepended-summary slice is empty.
|
|
func tasksConversation(args []string) int {
|
|
fs := flag.NewFlagSet("tasks conversation", flag.ContinueOnError)
|
|
id := fs.String("id", "", "Task ID (required; defaults to $KANDEV_TASK_ID)")
|
|
if err := fs.Parse(args); err != nil {
|
|
cliError("parse flags: %v", err)
|
|
return 1
|
|
}
|
|
client, err := newKandevClient()
|
|
if err != nil {
|
|
cliError("%v", err)
|
|
return 1
|
|
}
|
|
taskID := resolveTaskID(*id, client.taskID)
|
|
if taskID == "" {
|
|
cliError("--id (or KANDEV_TASK_ID) required")
|
|
return 1
|
|
}
|
|
body, status, err := client.do(http.MethodGet,
|
|
fmt.Sprintf("/api/v1/office/tasks/%s/comments", taskID), nil)
|
|
return handleResponse(body, status, err)
|
|
}
|