From 782f62cc784754512b22e8e6c1b7374525be3201 Mon Sep 17 00:00:00 2001 From: hzhang Date: Wed, 10 Jun 2026 21:32:31 +0100 Subject: [PATCH] =?UTF-8?q?feat(agent):=20hf=20agent=20status-of=20=20=E2=80=94=20read=20another=20agent's=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backend has GET /calendar/agent/status?agent_id= (read an agent's runtime status, no side effects) but the CLI only exposed `hf agent status --set` (sets the CALLER's own status). So delegate-task / on-call-handoff status gates — which need to check whether the RECEIVER/incoming agent is idle before pinging/handing off — had no CLI path; agents had to guess from is_active. Add `hf agent status-of ` wrapping the GET endpoint (X-Claw-Identifier header, no token). Prints {agent_id, status}; surfaces the backend's 404 for unknown agents so callers can fail-open/closed. Verified on sim: `hf agent status-of plxrec2` → idle; --json → {"agent_id", "status"}; unknown agent → 404 "Agent not found". Co-Authored-By: Claude Opus 4.8 (1M context) --- cmd/hf/main.go | 10 +++++- internal/commands/agent.go | 67 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/cmd/hf/main.go b/cmd/hf/main.go index 281d8cb..b73cc9a 100644 --- a/cmd/hf/main.go +++ b/cmd/hf/main.go @@ -60,11 +60,19 @@ func main() { // `POST /calendar/agent/status`, identifies caller via // AGENT_ID/CLAW_IDENTIFIER env, no token needed). if len(args) < 2 { - output.Error("usage: hf agent status --set ") + output.Error("usage: hf agent status --set | hf agent status-of ") } switch args[1] { case "status": commands.RunAgentStatus(args[2:]) + case "status-of": + // `hf agent status-of ` — 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 ") + } + commands.RunAgentStatusOf(args[2]) default: output.Errorf("unknown agent subcommand: %s", args[1]) } diff --git a/internal/commands/agent.go b/internal/commands/agent.go index e091217..f56c897 100644 --- a/internal/commands/agent.go +++ b/internal/commands/agent.go @@ -16,6 +16,7 @@ import ( "encoding/json" "fmt" "net/http" + neturl "net/url" "os" "strings" "time" @@ -138,3 +139,69 @@ func RunAgentStatus(args []string) { "ok", "true", ) } + +// RunAgentStatusOf implements `hf agent status-of ` — read ANOTHER +// agent's current runtime status without side effects, wrapping +// `GET /calendar/agent/status?agent_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 ") + } + + 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, + ) +}