From 1610de79d44d6944cf014620be4757cc6ef60eee Mon Sep 17 00:00:00 2001 From: Steve Calvert Date: Sun, 19 Jul 2026 14:13:34 -0700 Subject: [PATCH] feat(agent-help): add self-describing usage command for coding agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit glean agent-help [command...] [--json] emits usage generated from the installed binary (Entire-style): environment context (version, server, auth state, API mode) plus a when-to-use command map, or full per-command detail. Works unauthenticated. glean schema becomes a hidden alias. A bidirectional drift test now forces every visible command to have a registry entry and every entry to name a live command — it immediately caught and removed a stale 'version' entry documenting a command that does not exist. Co-Authored-By: Claude Fable 5 --- cmd/agent_help.go | 43 ++++++++++++ cmd/agent_help_test.go | 145 +++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 1 + cmd/schema.go | 30 +++++---- 4 files changed, 208 insertions(+), 11 deletions(-) create mode 100644 cmd/agent_help.go create mode 100644 cmd/agent_help_test.go diff --git a/cmd/agent_help.go b/cmd/agent_help.go new file mode 100644 index 0000000..8b1b237 --- /dev/null +++ b/cmd/agent_help.go @@ -0,0 +1,43 @@ +package cmd + +import ( + "github.com/gleanwork/glean-cli/internal/agenthelp" + "github.com/spf13/cobra" +) + +// NewCmdAgentHelp creates the agent-help command: agent-facing usage +// generated from the installed binary, so it is version-accurate by +// construction. It must work unauthenticated — an agent's first command is +// often discovery, before credentials exist. +func NewCmdAgentHelp() *cobra.Command { + var asJSON bool + + cmd := &cobra.Command{ + Use: "agent-help [command...]", + Short: "Show agent-facing usage that matches this binary", + Long: `Show agent-facing usage generated from the installed glean binary. + +With no arguments, prints the environment context (version, server, auth +state, API mode) and a map of all commands with when-to-use guidance. With a +command path, prints that command's flags, payload shapes, subcommands, and +examples. + +Because the output comes from the binary itself, it cannot drift from the +installed version — use it as the source of truth when it differs from +static documentation or pre-installed skills. + +Examples: + glean agent-help # command map + environment context + glean agent-help search # full usage for search + glean agent-help agents run --json # machine-readable detail`, + Args: cobra.ArbitraryArgs, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := agenthelp.BuildContext(cliVersion) + docs := agenthelp.Collect(cmd.Root()) + return agenthelp.Render(cmd.OutOrStdout(), ctx, docs, args, asJSON) + }, + } + + cmd.Flags().BoolVar(&asJSON, "json", false, "Emit machine-readable JSON") + return cmd +} diff --git a/cmd/agent_help_test.go b/cmd/agent_help_test.go new file mode 100644 index 0000000..f4f3ad9 --- /dev/null +++ b/cmd/agent_help_test.go @@ -0,0 +1,145 @@ +package cmd + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/gleanwork/glean-cli/internal/schema" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// driftAllowlist names visible top-level commands that deliberately have no +// schema registry entry: they are interactive or maintenance commands, not +// agent-invocable API operations. Everything else MUST be registered — the +// registry powers agent-help, so an unregistered command is invisible +// guidance-wise. +var driftAllowlist = map[string]bool{ + "auth": true, // interactive login flow + "update": true, // self-update maintenance +} + +func TestAgentHelp_RegistryDriftBidirectional(t *testing.T) { + root := NewCmdRoot() + + visible := map[string]bool{} + for _, cmd := range root.Commands() { + if cmd.Hidden || cmd.Name() == "help" || cmd.Name() == "completion" { + continue + } + visible[cmd.Name()] = true + if driftAllowlist[cmd.Name()] { + continue + } + _, err := schema.Get(cmd.Name()) + assert.NoError(t, err, + "visible command %q has no schema registry entry — register it in cmd/schema.go (with WhenToUse and Surface) so agent-help can describe it", + cmd.Name()) + } + + // Reverse direction: every registry entry must name a live command + // (hidden commands count — `schema` stays registered as a hidden alias + // would, but stale entries for deleted commands must fail). + all := map[string]bool{} + for _, cmd := range root.Commands() { + all[cmd.Name()] = true + } + for _, name := range schema.List() { + assert.True(t, all[name], + "schema registry entry %q does not correspond to any command — remove the stale entry from cmd/schema.go", name) + } +} + +func TestAgentHelp_Overview(t *testing.T) { + usePlatformAPIs(t) + root := NewCmdRoot() + buf := &bytes.Buffer{} + root.SetOut(buf) + root.SetArgs([]string{"agent-help"}) + require.NoError(t, root.Execute()) + + out := buf.String() + assert.Contains(t, out, "source of truth") + assert.Contains(t, out, "COMMAND") + assert.Contains(t, out, "search") + assert.Contains(t, out, "agents") + assert.Contains(t, out, "platform") + assert.NotContains(t, out, "generate-skills", "hidden commands stay out of the map") +} + +func TestAgentHelp_WorksUnauthenticated(t *testing.T) { + // No credentials anywhere: agent-help must still succeed and say so. + t.Setenv("GLEAN_SERVER_URL", "") + t.Setenv("GLEAN_API_TOKEN", "") + t.Setenv("HOME", t.TempDir()) + + root := NewCmdRoot() + buf := &bytes.Buffer{} + root.SetOut(buf) + root.SetArgs([]string{"agent-help"}) + require.NoError(t, root.Execute()) + assert.Contains(t, buf.String(), "authenticated: no") + assert.Contains(t, buf.String(), "glean auth login") +} + +func TestAgentHelp_CommandDetail(t *testing.T) { + usePlatformAPIs(t) + root := NewCmdRoot() + buf := &bytes.Buffer{} + root.SetOut(buf) + root.SetArgs([]string{"agent-help", "agents", "run"}) + require.NoError(t, root.Execute()) + + out := buf.String() + assert.Contains(t, out, "agents run") + assert.Contains(t, out, "--json") +} + +func TestAgentHelp_JSON(t *testing.T) { + usePlatformAPIs(t) + root := NewCmdRoot() + buf := &bytes.Buffer{} + root.SetOut(buf) + root.SetArgs([]string{"agent-help", "search", "--json"}) + require.NoError(t, root.Execute()) + + var envelope struct { + Context map[string]any `json:"context"` + Command struct { + Path string `json:"path"` + Surface string `json:"surface"` + Flags map[string]map[string]any `json:"flags"` + } `json:"command"` + } + require.NoError(t, json.Unmarshal(buf.Bytes(), &envelope)) + assert.Equal(t, "search", envelope.Command.Path) + assert.Equal(t, schema.SurfacePlatform, envelope.Command.Surface) + assert.Contains(t, envelope.Command.Flags, "--page-size") +} + +func TestAgentHelp_UnknownCommand(t *testing.T) { + root := NewCmdRoot() + root.SetOut(&bytes.Buffer{}) + root.SetErr(&bytes.Buffer{}) + root.SetArgs([]string{"agent-help", "frobnicate"}) + err := root.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown command") +} + +func TestSchemaCommand_HiddenAliasStillWorks(t *testing.T) { + root := NewCmdRoot() + buf := &bytes.Buffer{} + root.SetOut(buf) + root.SetArgs([]string{"schema", "search"}) + require.NoError(t, root.Execute()) + assert.Contains(t, buf.String(), `"command": "search"`) + + // And it is hidden from help. + for _, cmd := range root.Commands() { + if cmd.Name() == "schema" { + assert.True(t, cmd.Hidden, "schema must be a hidden alias") + } + } +} diff --git a/cmd/root.go b/cmd/root.go index 6b80a45..41df226 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -137,6 +137,7 @@ func NewCmdRoot() *cobra.Command { NewCmdSearch(), NewCmdChat(), NewCmdAPI(), + NewCmdAgentHelp(), NewCmdSchema(), } { sub.GroupID = "core" diff --git a/cmd/schema.go b/cmd/schema.go index ff5ec5d..d547e3e 100644 --- a/cmd/schema.go +++ b/cmd/schema.go @@ -71,15 +71,6 @@ glean chat --json '{"messages":[{"author":"USER","messageType":"CONTENT","fragme Example: `glean api search --method POST --raw-field '{"query":"test"}' --no-color | jq .results`, }) - schema.Register(schema.CommandSchema{ - Command: "version", - Description: "Print the glean CLI version string.", - WhenToUse: "Check the installed CLI version, e.g. before reporting a bug or verifying an upgrade.", - Surface: schema.SurfaceLocal, - Flags: map[string]schema.FlagSchema{}, - Example: `glean version`, - }) - schema.Register(schema.CommandSchema{ Command: "shortcuts", Description: "Manage Glean shortcuts (go-links). Subcommands: list, get, create, update, delete.", @@ -235,6 +226,18 @@ glean agents run --json '{"agent_id":"my-agent","input":{"query":"test"}}'`, Example: `glean messages get --json '{"messageId":"MSG_ID"}' | jq .`, }) + schema.Register(schema.CommandSchema{ + Command: "agent-help", + Description: "Show agent-facing usage generated from the installed glean binary: environment context plus a command map, or full detail for one command.", + WhenToUse: "Start every session here: discover available commands, exact flags, payload shapes, and whether the environment is authenticated — without static docs in context.", + Surface: schema.SurfaceLocal, + Flags: map[string]schema.FlagSchema{ + "--json": {Type: "boolean", Default: false, Description: "Emit machine-readable JSON"}, + }, + Example: `glean agent-help +glean agent-help agents run --json`, + }) + schema.Register(schema.CommandSchema{ Command: "announcements", Description: "Manage Glean announcements. Subcommands: create, update, delete.", @@ -250,10 +253,15 @@ glean agents run --json '{"agent_id":"my-agent","input":{"query":"test"}}'`, } // NewCmdSchema creates and returns the schema command. +// +// Deprecated: superseded by `glean agent-help`, which derives structure from +// the live command tree and adds context awareness. Kept as a hidden alias +// for compatibility with existing agent skills that call `glean schema`. func NewCmdSchema() *cobra.Command { cmd := &cobra.Command{ - Use: "schema [command]", - Short: "Show JSON schema for a command's flags and request format", + Use: "schema [command]", + Hidden: true, + Short: "Show JSON schema for a command's flags and request format (deprecated: use agent-help)", Long: `Show machine-readable JSON schema for any glean command. Agents can call this before invoking a command to understand parameter