Skip to content

feat(core): add FleetManager.listAgentCommands + re-export SlashCommand (fixes #300)#301

Merged
edspencer merged 1 commit into
mainfrom
feat/list-agent-commands
Jul 8, 2026
Merged

feat(core): add FleetManager.listAgentCommands + re-export SlashCommand (fixes #300)#301
edspencer merged 1 commit into
mainfrom
feat/list-agent-commands

Conversation

@edspencer

@edspencer edspencer commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a lightweight, one-shot FleetManager.listAgentCommands(agentName, options?) that returns the slash commands available to an agent — opening, querying, and closing the underlying streaming session internally — and re-exports the SlashCommand type from the @herdctl/core barrel. Gives consumers (e.g. Paddock) a clean way to populate a slash-command autocomplete without hand-managing a live claude subprocess.

Fixes #300.

Why

The capability already exists via RuntimeSession.listCommands(), but consuming it today means correctly managing subprocess lifecycle on every path:

const session = await manager.openChatSession(agent, { workingDirectory, injectedMcpServers });
const cmds = await session.listCommands();
await session.close(); // MUST happen, even on error

That lifecycle care belongs in the library, not duplicated in each consumer.

What changed

  • FleetManager.listAgentCommands(agentName, options?) (delegates to JobControl) — openChatSessionsession.listCommands()session.close() in a finally (guaranteed teardown even if listCommands() throws). Returns the full SlashCommand[] ({ name, description, argumentHint }): built-ins + project .claude/commands + MCP-provided commands, for the resolved session's cwd/config.
  • Accepts the same ChatSessionOptions as openChatSession (notably workingDirectory and injectedMcpServers). Runs on the SDK runtime regardless of the agent's configured runtime, so it works for cli-runtime agents; surfaces StreamingSessionUnsupportedError unchanged for Docker-wrapped agents.
  • Re-export SlashCommand from @herdctl/core (via the runtime → runner → core barrel chain) so consumers can import type { SlashCommand } from "@herdctl/core".
  • Docs: new library-reference/fleet-manager.mdx entry under Session Management Methods.
  • Changeset: @herdctl/core minor.

Acceptance (from the issue)

  • listAgentCommands(agent, opts?) returns SlashCommand[] and always closes the session — the throw-path test asserts the session's teardown (return()) still runs when listCommands() rejects.
  • SlashCommand importable from @herdctl/core (verified in the built dist barrel).
  • Changeset + minor bump so Paddock can depend on the published version.

Tests

New list-agent-commands.test.ts (5 tests), mocking the SDK so no real subprocess spawns:

  1. returns the full SlashCommand[] and closes the session
  2. closes the session even when listCommands() throws
  3. AgentNotFoundError for an unknown agent (no session opened)
  4. InvalidStateError before initialize()
  5. surfaces StreamingSessionUnsupportedError for Docker-wrapped agents

Verification

  • pnpm typecheck — 11/11 pass
  • pnpm build — 7/7 pass; docs site builds clean
  • pnpm test (core) — new 5 pass; full suite green except the pre-existing root-only directory.test.ts chmod case (passes under non-root CI), unrelated to this change

Notes

The optional short-lived cache floated in the issue is intentionally left out of v1 — each call spawns/tears down a claude subprocess (~seconds), documented in the JSDoc/docs, and consumers are encouraged to cache.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a new way to list an agent’s available slash commands in one call.
    • SlashCommand is now available for direct use in type definitions.
  • Documentation

    • Added API reference and usage examples for the new command-listing feature.
  • Bug Fixes

    • Sessions are now closed automatically after command listing, even if an error occurs.
    • Improved handling for unsupported agent runtime scenarios and invalid setup states.

Reading the slash commands available to an agent (to populate a command
palette / autocomplete) previously required consumers to hand-manage a live
`claude` streaming subprocess: openChatSession → listCommands → close, with the
close guaranteed on every path including errors. That lifecycle care belongs in
the library, not duplicated in each consumer (e.g. Paddock).

Add a one-shot `FleetManager.listAgentCommands(agentName, options?)` that opens
a chat session, reads its command list, and ALWAYS closes the session (in a
`finally`, even if the listing throws):

- Returns the full `SlashCommand[]` — { name, description, argumentHint } per
  command (built-ins + project `.claude/commands` + MCP-provided commands).
- Accepts the same `ChatSessionOptions` as openChatSession (workingDirectory,
  injectedMcpServers) so the list reflects the intended project context.
- Runs on the SDK runtime regardless of the agent's configured runtime (works
  for `cli`-runtime agents) and surfaces StreamingSessionUnsupportedError
  unchanged for Docker-wrapped agents.

Also re-export the `SlashCommand` type from `@herdctl/core` so consumers can
`import type { SlashCommand } from "@herdctl/core"` instead of reaching into the
Claude Agent SDK directly.

Adds 5 tests (happy path, close-on-throw, agent-not-found, invalid-state,
docker-unsupported), a fleet-manager library-reference doc entry, and a
changeset (minor).

Fixes #300.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new FleetManager.listAgentCommands(agentName, options?) method that opens a streaming chat session, retrieves the agent's SlashCommand[], and always closes the session. Implements the delegation through JobControl, re-exports the SlashCommand type, adds tests, documentation, and a changeset.

Changes

listAgentCommands API

Layer / File(s) Summary
SlashCommand type re-export
packages/core/src/runner/runtime/interface.ts, packages/core/src/runner/runtime/index.ts, packages/core/src/runner/index.ts
SlashCommand type from the Claude Agent SDK is re-exported through the runtime and runner barrel modules.
Core implementation
packages/core/src/fleet-manager/job-control.ts, packages/core/src/fleet-manager/fleet-manager.ts
JobControl.listAgentCommands opens a chat session, calls listCommands(), and closes the session in a finally block; FleetManager.listAgentCommands delegates to it.
Test suite
packages/core/src/fleet-manager/__tests__/list-agent-commands.test.ts
Vitest suite mocks the SDK query and verifies success, error propagation with teardown, unknown agent, uninitialized state, and Docker-unsupported error scenarios.
Docs and changeset
docs/src/content/docs/library-reference/fleet-manager.mdx, .changeset/list-agent-commands.md
Adds API reference documentation and a changeset entry describing the new method and type re-export.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant FleetManager
  participant JobControl
  participant RuntimeSession

  Caller->>FleetManager: listAgentCommands(agentName, options)
  FleetManager->>JobControl: listAgentCommands(agentName, options)
  JobControl->>RuntimeSession: openChatSession(agentName, options)
  JobControl->>RuntimeSession: listCommands()
  RuntimeSession-->>JobControl: SlashCommand[]
  JobControl->>RuntimeSession: close()
  JobControl-->>FleetManager: SlashCommand[]
  FleetManager-->>Caller: SlashCommand[]
Loading

Possibly related issues

Possibly related PRs

  • edspencer/herdctl#286: Both PRs touch job-control.ts and rely on the same streaming chat session/RuntimeSession.listCommands() machinery.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding FleetManager.listAgentCommands and re-exporting SlashCommand.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/list-agent-commands

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying herdctl with  Cloudflare Pages  Cloudflare Pages

Latest commit: 1395fb2
Status: ✅  Deploy successful!
Preview URL: https://1d137382.herdctl.pages.dev
Branch Preview URL: https://feat-list-agent-commands.herdctl.pages.dev

View logs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
packages/core/src/fleet-manager/job-control.ts (1)

399-436: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider guarding session.close() against masking the original error.

If listCommands() throws and session.close() also throws in the finally block, the close() error will replace the original error, making debugging harder. Since JobControl has access to this.ctx.getLogger(), you could swallow and log the close error:

🛡️ Optional: prevent close() from masking the original error
   async listAgentCommands(
     agentName: string,
     options?: ChatSessionOptions,
   ): Promise<SlashCommand[]> {
     const session = await this.openChatSession(agentName, options);
     try {
       return await session.listCommands();
     } finally {
-      // Guarantee teardown of the underlying subprocess on every path,
-      // including when listCommands() throws.
-      await session.close();
+      // Guarantee teardown of the underlying subprocess on every path,
+      // including when listCommands() throws. Swallow close errors so they
+      // don't mask the original failure from listCommands().
+      try {
+        await session.close();
+      } catch (closeError) {
+        this.ctx.getLogger().warn(
+          `Failed to close session in listAgentCommands: ${(closeError as Error).message}`,
+        );
+      }
     }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/fleet-manager/job-control.ts` around lines 399 - 436, The
`listAgentCommands` method in `JobControl` should not let `session.close()` hide
an error from `session.listCommands()`. Update the `finally` cleanup so
`session.close()` failures are caught and logged with `this.ctx.getLogger()`,
while preserving the original exception from `listCommands()` or
`openChatSession()`. Keep the teardown behavior in `listAgentCommands` and
`openChatSession` intact, but make close errors non-fatal unless no prior error
occurred.
packages/core/src/fleet-manager/__tests__/list-agent-commands.test.ts (2)

86-100: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider hoisting the yaml import to module level.

Both createConfig and createAgentConfig dynamically import("yaml") on every call. A top-level static import would be cleaner and avoid repeated module resolution overhead.

♻️ Suggested refactor
+import yaml from "yaml";
+
 // ... other imports ...

 async function createConfig(config: object) {
   const configPath = join(configDir, "herdctl.yaml");
-  const yaml = await import("yaml");
   await writeFile(configPath, yaml.stringify(config));
   return configPath;
 }

 async function createAgentConfig(name: string, config: object) {
   const agentDir = join(configDir, "agents");
   await mkdir(agentDir, { recursive: true });
   const agentPath = join(agentDir, `${name}.yaml`);
-  const yaml = await import("yaml");
   await writeFile(agentPath, yaml.stringify(config));
   return agentPath;
 }

Note: If the dynamic import was intentional to avoid loading yaml until needed, the current approach is fine — the overhead is negligible in tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/fleet-manager/__tests__/list-agent-commands.test.ts` around
lines 86 - 100, The helper functions createConfig and createAgentConfig both
dynamically import yaml on every call, so hoist that import to module scope and
reuse it from both helpers. Update the test file to use a single top-level yaml
import, then keep createConfig and createAgentConfig focused on writing the
serialized config without repeated module resolution.

81-84: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The 50ms setTimeout in afterEach appears unnecessary.

Since the SDK is fully mocked and return() resolves synchronously, there are no lingering subprocesses or async operations that would race with the temp directory cleanup. The delay adds 50ms per test (250ms total) without clear benefit. If this was added to work around a specific flaky cleanup issue, a comment explaining why would help future maintainers.

♻️ Suggested cleanup
 afterEach(async () => {
-  await new Promise((resolve) => setTimeout(resolve, 50));
   await rm(tempDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/fleet-manager/__tests__/list-agent-commands.test.ts` around
lines 81 - 84, The `afterEach` cleanup in `list-agent-commands.test.ts` includes
an unnecessary 50ms delay before removing `tempDir`; remove the `setTimeout` and
keep the `rm` cleanup directly in `afterEach` since the mocked SDK and
synchronous `return()` do not need a race buffer. If the delay is intentionally
guarding a flaky cleanup edge case, keep the timing only with a brief comment in
`afterEach` explaining why it is required.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/core/src/fleet-manager/__tests__/list-agent-commands.test.ts`:
- Around line 86-100: The helper functions createConfig and createAgentConfig
both dynamically import yaml on every call, so hoist that import to module scope
and reuse it from both helpers. Update the test file to use a single top-level
yaml import, then keep createConfig and createAgentConfig focused on writing the
serialized config without repeated module resolution.
- Around line 81-84: The `afterEach` cleanup in `list-agent-commands.test.ts`
includes an unnecessary 50ms delay before removing `tempDir`; remove the
`setTimeout` and keep the `rm` cleanup directly in `afterEach` since the mocked
SDK and synchronous `return()` do not need a race buffer. If the delay is
intentionally guarding a flaky cleanup edge case, keep the timing only with a
brief comment in `afterEach` explaining why it is required.

In `@packages/core/src/fleet-manager/job-control.ts`:
- Around line 399-436: The `listAgentCommands` method in `JobControl` should not
let `session.close()` hide an error from `session.listCommands()`. Update the
`finally` cleanup so `session.close()` failures are caught and logged with
`this.ctx.getLogger()`, while preserving the original exception from
`listCommands()` or `openChatSession()`. Keep the teardown behavior in
`listAgentCommands` and `openChatSession` intact, but make close errors
non-fatal unless no prior error occurred.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b60af444-127c-4a7d-aa0d-475902ef3535

📥 Commits

Reviewing files that changed from the base of the PR and between 0810c29 and 1395fb2.

📒 Files selected for processing (8)
  • .changeset/list-agent-commands.md
  • docs/src/content/docs/library-reference/fleet-manager.mdx
  • packages/core/src/fleet-manager/__tests__/list-agent-commands.test.ts
  • packages/core/src/fleet-manager/fleet-manager.ts
  • packages/core/src/fleet-manager/job-control.ts
  • packages/core/src/runner/index.ts
  • packages/core/src/runner/runtime/index.ts
  • packages/core/src/runner/runtime/interface.ts

@edspencer edspencer merged commit 000fec8 into main Jul 8, 2026
8 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FleetManager.listAgentCommands(): one-shot slash-command listing + re-export SlashCommand

1 participant