Compare commits
4 Commits
fix/worklo
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e4803a5541 | |||
| 782f62cc78 | |||
| 0d656a6678 | |||
| fdf1ba1b17 |
@@ -60,11 +60,19 @@ func main() {
|
|||||||
// `POST /calendar/agent/status`, identifies caller via
|
// `POST /calendar/agent/status`, identifies caller via
|
||||||
// AGENT_ID/CLAW_IDENTIFIER env, no token needed).
|
// AGENT_ID/CLAW_IDENTIFIER env, no token needed).
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
output.Error("usage: hf agent status --set <idle|busy|on_call|exhausted|offline>")
|
output.Error("usage: hf agent status --set <idle|busy|on_call|exhausted|offline> | hf agent status-of <agent-id>")
|
||||||
}
|
}
|
||||||
switch args[1] {
|
switch args[1] {
|
||||||
case "status":
|
case "status":
|
||||||
commands.RunAgentStatus(args[2:])
|
commands.RunAgentStatus(args[2:])
|
||||||
|
case "status-of":
|
||||||
|
// `hf agent status-of <agent-id>` — read ANOTHER agent's runtime
|
||||||
|
// status (no side effects), wrapping GET /calendar/agent/status.
|
||||||
|
// Used by delegate-task / on-call-handoff status gates.
|
||||||
|
if len(args) < 3 {
|
||||||
|
output.Error("usage: hf agent status-of <agent-id>")
|
||||||
|
}
|
||||||
|
commands.RunAgentStatusOf(args[2])
|
||||||
default:
|
default:
|
||||||
output.Errorf("unknown agent subcommand: %s", args[1])
|
output.Errorf("unknown agent subcommand: %s", args[1])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
neturl "net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -138,3 +139,69 @@ func RunAgentStatus(args []string) {
|
|||||||
"ok", "true",
|
"ok", "true",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RunAgentStatusOf implements `hf agent status-of <agent-id>` — read ANOTHER
|
||||||
|
// agent's current runtime status without side effects, wrapping
|
||||||
|
// `GET /calendar/agent/status?agent_id=<id>`. Used by the delegate-task and
|
||||||
|
// on-call-handoff status gates (which previously referenced this endpoint with
|
||||||
|
// no CLI to reach it). Like RunAgentStatus this is header-identified, not
|
||||||
|
// token-authed; the X-Claw-Identifier comes from CLAW_IDENTIFIER env or
|
||||||
|
// hostname. Prints `{agent_id, status}`; backend returns 404 for unknown
|
||||||
|
// agents so callers can fail-open or fail-closed.
|
||||||
|
func RunAgentStatusOf(targetAgentID string) {
|
||||||
|
targetAgentID = strings.TrimSpace(targetAgentID)
|
||||||
|
if targetAgentID == "" {
|
||||||
|
output.Error("usage: hf agent status-of <agent-id>")
|
||||||
|
}
|
||||||
|
|
||||||
|
clawID := strings.TrimSpace(os.Getenv("CLAW_IDENTIFIER"))
|
||||||
|
if clawID == "" {
|
||||||
|
if h, err := os.Hostname(); err == nil {
|
||||||
|
clawID = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if clawID == "" {
|
||||||
|
output.Error("CLAW_IDENTIFIER env not set and hostname() failed — set CLAW_IDENTIFIER explicitly")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
output.Errorf("config error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
url := strings.TrimRight(cfg.BaseURL, "/") + "/calendar/agent/status?agent_id=" + neturl.QueryEscape(targetAgentID)
|
||||||
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
output.Errorf("cannot build request: %v", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("X-Claw-Identifier", clawID)
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
output.Errorf("request failed: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
_, _ = buf.ReadFrom(resp.Body)
|
||||||
|
if resp.StatusCode/100 != 2 {
|
||||||
|
output.Errorf("backend returned %d: %s", resp.StatusCode, buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if output.JSONMode {
|
||||||
|
fmt.Println(buf.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var parsed struct {
|
||||||
|
AgentID string `json:"agent_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(buf.Bytes(), &parsed); err != nil {
|
||||||
|
output.Errorf("cannot parse response: %v", err)
|
||||||
|
}
|
||||||
|
output.PrintKeyValue(
|
||||||
|
"agent_id", parsed.AgentID,
|
||||||
|
"status", parsed.Status,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.hangman-lab.top/zhi/HarborForge.Cli/internal/client"
|
"git.hangman-lab.top/zhi/HarborForge.Cli/internal/client"
|
||||||
"git.hangman-lab.top/zhi/HarborForge.Cli/internal/config"
|
"git.hangman-lab.top/zhi/HarborForge.Cli/internal/config"
|
||||||
@@ -32,11 +33,30 @@ type milestoneProgressResponse struct {
|
|||||||
Progress float64 `json:"progress"`
|
Progress float64 `json:"progress"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// milestoneProject extracts the project code from a milestone code
|
||||||
|
// (e.g. "PFIXTU:00001" -> "PFIXTU"); milestones are nested under their
|
||||||
|
// project in the API (/projects/{project}/milestones/{code}).
|
||||||
|
func milestoneProject(code string) string {
|
||||||
|
if i := strings.IndexByte(code, ':'); i >= 0 {
|
||||||
|
return code[:i]
|
||||||
|
}
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
|
// toMilestoneDateTime anchors a bare YYYY-MM-DD due date to a datetime, since
|
||||||
|
// the backend's due_date field requires a full datetime.
|
||||||
|
func toMilestoneDateTime(d string) string {
|
||||||
|
if len(d) == 10 {
|
||||||
|
return d + "T00:00:00Z"
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
// RunMilestoneList implements `hf milestone list --project <project-code>`.
|
// RunMilestoneList implements `hf milestone list --project <project-code>`.
|
||||||
func RunMilestoneList(args []string, tokenFlag string) {
|
func RunMilestoneList(args []string, tokenFlag string) {
|
||||||
token := ResolveToken(tokenFlag)
|
token := ResolveToken(tokenFlag)
|
||||||
|
|
||||||
query := ""
|
query, project := "", ""
|
||||||
for i := 0; i < len(args); i++ {
|
for i := 0; i < len(args); i++ {
|
||||||
switch args[i] {
|
switch args[i] {
|
||||||
case "--project":
|
case "--project":
|
||||||
@@ -44,7 +64,7 @@ func RunMilestoneList(args []string, tokenFlag string) {
|
|||||||
output.Error("--project requires a value")
|
output.Error("--project requires a value")
|
||||||
}
|
}
|
||||||
i++
|
i++
|
||||||
query = appendQuery(query, "project_code", args[i])
|
project = args[i]
|
||||||
case "--status":
|
case "--status":
|
||||||
if i+1 >= len(args) {
|
if i+1 >= len(args) {
|
||||||
output.Error("--status requires a value")
|
output.Error("--status requires a value")
|
||||||
@@ -66,8 +86,11 @@ func RunMilestoneList(args []string, tokenFlag string) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
output.Errorf("config error: %v", err)
|
output.Errorf("config error: %v", err)
|
||||||
}
|
}
|
||||||
|
if project == "" {
|
||||||
|
output.Error("--project is required (milestones are listed per project)")
|
||||||
|
}
|
||||||
c := client.New(cfg.BaseURL, token)
|
c := client.New(cfg.BaseURL, token)
|
||||||
path := "/milestones"
|
path := "/projects/" + project + "/milestones"
|
||||||
if query != "" {
|
if query != "" {
|
||||||
path += "?" + query
|
path += "?" + query
|
||||||
}
|
}
|
||||||
@@ -110,7 +133,7 @@ func RunMilestoneGet(milestoneCode, tokenFlag string) {
|
|||||||
output.Errorf("config error: %v", err)
|
output.Errorf("config error: %v", err)
|
||||||
}
|
}
|
||||||
c := client.New(cfg.BaseURL, token)
|
c := client.New(cfg.BaseURL, token)
|
||||||
data, err := c.Get("/milestones/" + milestoneCode)
|
data, err := c.Get("/projects/" + milestoneProject(milestoneCode) + "/milestones/" + milestoneCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
output.Errorf("failed to get milestone: %v", err)
|
output.Errorf("failed to get milestone: %v", err)
|
||||||
}
|
}
|
||||||
@@ -196,7 +219,7 @@ func RunMilestoneCreate(args []string, tokenFlag string) {
|
|||||||
payload["description"] = desc
|
payload["description"] = desc
|
||||||
}
|
}
|
||||||
if due != "" {
|
if due != "" {
|
||||||
payload["due_date"] = due
|
payload["due_date"] = toMilestoneDateTime(due)
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := json.Marshal(payload)
|
body, err := json.Marshal(payload)
|
||||||
@@ -209,7 +232,7 @@ func RunMilestoneCreate(args []string, tokenFlag string) {
|
|||||||
output.Errorf("config error: %v", err)
|
output.Errorf("config error: %v", err)
|
||||||
}
|
}
|
||||||
c := client.New(cfg.BaseURL, token)
|
c := client.New(cfg.BaseURL, token)
|
||||||
data, err := c.Post("/milestones", bytes.NewReader(body))
|
data, err := c.Post("/projects/"+project+"/milestones", bytes.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
output.Errorf("failed to create milestone: %v", err)
|
output.Errorf("failed to create milestone: %v", err)
|
||||||
}
|
}
|
||||||
@@ -261,7 +284,7 @@ func RunMilestoneUpdate(milestoneCode string, args []string, tokenFlag string) {
|
|||||||
output.Error("--due requires a value")
|
output.Error("--due requires a value")
|
||||||
}
|
}
|
||||||
i++
|
i++
|
||||||
payload["due_date"] = args[i]
|
payload["due_date"] = toMilestoneDateTime(args[i])
|
||||||
default:
|
default:
|
||||||
output.Errorf("unknown flag: %s", args[i])
|
output.Errorf("unknown flag: %s", args[i])
|
||||||
}
|
}
|
||||||
@@ -281,7 +304,7 @@ func RunMilestoneUpdate(milestoneCode string, args []string, tokenFlag string) {
|
|||||||
output.Errorf("config error: %v", err)
|
output.Errorf("config error: %v", err)
|
||||||
}
|
}
|
||||||
c := client.New(cfg.BaseURL, token)
|
c := client.New(cfg.BaseURL, token)
|
||||||
_, err = c.Patch("/milestones/"+milestoneCode, bytes.NewReader(body))
|
_, err = c.Patch("/projects/"+milestoneProject(milestoneCode)+"/milestones/"+milestoneCode, bytes.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
output.Errorf("failed to update milestone: %v", err)
|
output.Errorf("failed to update milestone: %v", err)
|
||||||
}
|
}
|
||||||
@@ -297,7 +320,7 @@ func RunMilestoneDelete(milestoneCode, tokenFlag string) {
|
|||||||
output.Errorf("config error: %v", err)
|
output.Errorf("config error: %v", err)
|
||||||
}
|
}
|
||||||
c := client.New(cfg.BaseURL, token)
|
c := client.New(cfg.BaseURL, token)
|
||||||
_, err = c.Delete("/milestones/" + milestoneCode)
|
_, err = c.Delete("/projects/" + milestoneProject(milestoneCode) + "/milestones/" + milestoneCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
output.Errorf("failed to delete milestone: %v", err)
|
output.Errorf("failed to delete milestone: %v", err)
|
||||||
}
|
}
|
||||||
@@ -312,7 +335,7 @@ func RunMilestoneProgress(milestoneCode, tokenFlag string) {
|
|||||||
output.Errorf("config error: %v", err)
|
output.Errorf("config error: %v", err)
|
||||||
}
|
}
|
||||||
c := client.New(cfg.BaseURL, token)
|
c := client.New(cfg.BaseURL, token)
|
||||||
data, err := c.Get("/milestones/" + milestoneCode + "/progress")
|
data, err := c.Get("/projects/" + milestoneProject(milestoneCode) + "/milestones/" + milestoneCode + "/progress")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
output.Errorf("failed to get milestone progress: %v", err)
|
output.Errorf("failed to get milestone progress: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user