You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Sub-issue of #246. Hardened spec — do not re-derive decisions. Mirrors internal/cmd/pipelines/agent/show/show.go (the org-scoped sibling that already uses a 1-3 segment positional with shared.ResolvePool/shared.ResolvePoolAgent) and internal/cmd/pipelines/build/queue (the planned spec #253, which folds ID/name resolution into a single positional).
Command Description
List the agents in an Azure DevOps agent pool. The command takes the pool as a positional target (accepts either a numeric ID or a name), resolves it to a pool ID via shared.ResolvePool, and then fetches the matching agents via the Agents REST 7.1 endpoint. The result is rendered as a default table (with optional JSON export). All output passes through ctx.Printer("list") for the table and opts.exporter.Write(ios, view) for JSON.
GET https://dev.azure.com/{organization}/_apis/distributedtask/pools/{poolId}/agents?agentName={filter}&includeCapabilities={bool}&propertyFilters={csv}&demands={csv}&api-version=7.1
Locked Decisions (do not re-derive)
#
Decision
Rationale
1
Use the vendored SDK taskagent.Client.GetAgents (not raw HTTP). Mock already generated at internal/mocks/taskagent_client_mock.go:531-540.
Consistent with the other taskagent subgroup siblings.
2
The pool is required and identified by the positional POOL argument (the last segment of [ORGANIZATION/]POOL). The pool target accepts either a numeric ID (fast path) or a name (resolved via shared.ResolvePool from internal/cmd/pipelines/agent/shared/resolve.go:50). Folds the previous --pool-id flag into a single positional.
Mirrors the agent show (#247) and build queue (#253) target-resolution pattern. UX is consistent with the rest of the taskagent subgroup.
3
The pool target is validated by shared.ResolvePool: a positive integer is used directly, a string calls taskagent.Client.GetAgentPools(name=...) and the first case-insensitive name match is selected. Ambiguous matches (multiple pools with the same name) or missing pools return fmt.Errorf. Removes the previous --pool-id int-flag validation since pool ID validation is now a property of the resolver.
One source of truth for "target → ID" resolution. Matches agent show (#247) and build queue (#253) sibling behavior.
4
The positional [ORGANIZATION/]POOL is parsed with util.ParseTargetWithDefaultOrganization from internal/cmd/util/scope.go:157. The function returns a *util.Path with Organization populated (defaults resolved from ctx.Config().Authentication().GetDefaultOrganization() when the org segment is omitted) and Targets[0] set to the pool target. Project is not used.
Mirrors the security-membership and work-item delete/update target parsers; the pool is a single-target org-scoped lookup.
4.5
cobra.ExactArgs(1) — exactly 1 positional. Combined with the ParseTargetWithDefaultOrganization parser, the resulting shape is azdo pipelines agent list [ORGANIZATION/]POOL, where POOL accepts either an ID or a name. The PoolId of GetAgentsArgs is the integer returned by shared.ResolvePool, NOT the raw scope.Targets[0] string.
Mirrors the build queue (#253) UX: 1- or 2-segment positional with ID-or-name target resolution.
5
Default output is a table. JSON output via --json passes a dedicated view struct []agentJSON to opts.exporter.Write. The view struct flattens identity and time fields with omitempty.
Mirrors pipelines/variablegroup/list/list.go and boards/workitem/list/list.go.
6
JSON view struct fields: id, name, status, enabled, version, osDescription, pool (name only when available), maxParallelism, createdOn (string), createdBy (flattened to id, displayName, uniqueName).
Same shape as the boards/list siblings.
7
Optional filter --filter (string, short form -f) maps to GetAgentsArgs.AgentName. Renames the previous --name flag to --filter to make the agent-name filtering role explicit (the positional is the pool, not the agent name).
Aligns the agent-name filter with the same flag used by the security/group add/remove siblings. Short form -f is the same letter used by other --filter flags in the codebase.
8
--include-capabilities (bool) maps to GetAgentsArgs.IncludeCapabilities.
Mirrors Azure CLI filter.
9
--max-items (int, default 0 = no cap) — if positive, truncate the result slice client-side.
Mirrors pipelines/variablegroup/list--max-items.
10
Aliases: ls, l.
Project convention from pr/list and pipelines/variablegroup/list.
11
No new SDK client, no new package beyond internal/cmd/pipelines/agent/list. Reuse shared.ResolvePool from internal/cmd/pipelines/agent/shared/resolve.go (already implemented and tested).
Mandate: minimal code.
12
Mock for GetAgents is already generated at internal/mocks/taskagent_client_mock.go:531-540. Do not regenerate.
Positional structure (parser: util.ParseTargetWithDefaultOrganization at internal/cmd/util/scope.go:157)
The parser accepts 1 or 2 /-separated segments in the single positional. The resulting *util.Path has the following fields after a successful parse:
Field
Value
Notes
*Path.Organization
string
Required. Resolved from the first segment, or from ctx.Config().Authentication().GetDefaultOrganization() when the segment is omitted.
*Path.Project
""
Always empty. This command is org-only; a 3-segment input (e.g., "org/proj/pool") is rejected by the command (after parsing) via an explicit if scope.Project != "" guard returning util.FlagErrorf("agent list does not accept a project scope; got %q", args[0]).
*Path.Targets
[]string{rawPoolTarget}
Length 1. The single target is the raw pool identifier passed to shared.ResolvePool.
Segment-count table
Input
Result
(no positional)
Error — "accepts 1 arg(s), received 0" (cobra-level).
"1"
Organization = default org, Targets[0] = "1". Resolver fast-path: pool ID = 1.
"Default"
Organization = default org, Targets[0] = "Default". Resolver calls GetAgentPools(name=...) and picks the first case-insensitive match.
"myorg/1"
Organization = "myorg", Targets[0] = "1". Resolver fast-path: pool ID = 1.
Parser returns Organization="myorg", Project="proj", Targets[0]="pool". Command-level guard rejects with "agent list does not accept a project scope; got \"myorg/proj/pool\"".
""
Error — parser's "invalid input …" (empty input).
cobra.Args validator
cobra.ExactArgs(1). The RunE handler assigns args[0] to opts.targetArg, then run():
Calls util.ParseTargetWithDefaultOrganization(ctx, opts.targetArg) to obtain the *util.Path.
Guards scope.Project == "" — returns util.FlagErrorf if non-empty.
Calls cmdCtx.ClientFactory().TaskAgent(ctx, scope.Organization) to obtain the SDK client.
Calls shared.ResolvePool(ctx, taskClient, scope.Targets[0]) to obtain the integer poolID.
Calls taskClient.GetAgents(ctx, GetAgentsArgs{PoolId: &poolID, AgentName: …, IncludeCapabilities: …}) with the resolved pool ID.
Applies --max-items truncation and renders the table or JSON.
Flags
Flag
Type
Default
Required
Maps to
Description
--filter, -f
string
""
no
GetAgentsArgs.AgentName
Optional name filter on the agent. Empty string → no name filter. Replaces the previous --name flag.
--include-capabilities
bool
false
no
GetAgentsArgs.IncludeCapabilities
When true, the response includes each agent's capabilities.
--max-items
int
0
no
(client-side cap)
Maximum number of agents to return. 0 = no cap; truncates the slice after the SDK call.
No --pool-id flag — the pool is now a positional target (Decision 2). For backward compatibility, run azdo pipelines agent list 1 to list agents in pool ID 1.
Update internal/cmd/pipelines/agent/agent.go to add listcmd "github.com/tmeckel/azdo-cli/internal/cmd/pipelines/agent/list" and cmd.AddCommand(listcmd.NewCmd(ctx)). Update the Example block to use the new positional shape (e.g., $ azdo pipelines agent list Default --filter my-agent).
Add an import for internal/cmd/pipelines/agent/shared to call shared.ResolvePool.
API Surface
Reuse the already-vendored client and the existing shared.ResolvePool helper. No new SDK clients required.
shared.ResolvePool (internal/cmd/pipelines/agent/shared/resolve.go:50) → (int, error). Accepts a pool ID or name; performs the strconv.Atoi fast path and the GetAgentPools name-lookup fallback. Returns the integer pool ID.
Mock for GetAgents is already generated at internal/mocks/taskagent_client_mock.go:531-540. GetAgentPools mock is already generated (used by shared.ResolvePool). No mock regeneration needed.
Reference Existing Patterns
internal/cmd/pipelines/agent/show/show.go:107 — primary sibling reference. Already uses the 1-3 segment positional [ORGANIZATION/]POOL/AGENT parsed with util.ParsePoolAgentTargetWithDefaultOrganization and resolved via shared.ResolvePoolAgent (which itself calls shared.ResolvePool). Copy the parsing flow + project-scope guard.
internal/cmd/pipelines/agent/shared/resolve.go:50 — shared.ResolvePool (Decision 2). Already implemented, tested, and ready to reuse. Tests cover numeric fast path, name lookup, not-found, and ambiguous-name error cases.
internal/cmd/pipelines/build/queue (planned spec feat: Implement azdo pipelines build queue command #253) — closest UX sibling. Uses shared.ResolvePipelineDefinition to fold ID/name resolution into a single positional. The agent list redesign mirrors this UX.
az pipelines agent list (reference for UX) — Note: the Azure CLI uses --pool-id, but the redesigned azdo UX folds the pool into the positional with ID-or-name resolution, matching the azdo pipelines build queue UX.
Sub-issue of #246. Hardened spec — do not re-derive decisions. Mirrors
internal/cmd/pipelines/agent/show/show.go(the org-scoped sibling that already uses a 1-3 segment positional withshared.ResolvePool/shared.ResolvePoolAgent) andinternal/cmd/pipelines/build/queue(the planned spec #253, which folds ID/name resolution into a single positional).Command Description
List the agents in an Azure DevOps agent pool. The command takes the pool as a positional target (accepts either a numeric ID or a name), resolves it to a pool ID via
shared.ResolvePool, and then fetches the matching agents via the Agents REST 7.1 endpoint. The result is rendered as a default table (with optional JSON export). All output passes throughctx.Printer("list")for the table andopts.exporter.Write(ios, view)for JSON.Locked Decisions (do not re-derive)
taskagent.Client.GetAgents(not raw HTTP). Mock already generated atinternal/mocks/taskagent_client_mock.go:531-540.taskagentsubgroup siblings.POOLargument (the last segment of[ORGANIZATION/]POOL). The pool target accepts either a numeric ID (fast path) or a name (resolved viashared.ResolvePoolfrominternal/cmd/pipelines/agent/shared/resolve.go:50). Folds the previous--pool-idflag into a single positional.agent show(#247) andbuild queue(#253) target-resolution pattern. UX is consistent with the rest of the taskagent subgroup.shared.ResolvePool: a positive integer is used directly, a string callstaskagent.Client.GetAgentPools(name=...)and the first case-insensitive name match is selected. Ambiguous matches (multiple pools with the same name) or missing pools returnfmt.Errorf. Removes the previous--pool-idint-flag validation since pool ID validation is now a property of the resolver.agent show(#247) andbuild queue(#253) sibling behavior.[ORGANIZATION/]POOLis parsed withutil.ParseTargetWithDefaultOrganizationfrominternal/cmd/util/scope.go:157. The function returns a*util.PathwithOrganizationpopulated (defaults resolved fromctx.Config().Authentication().GetDefaultOrganization()when the org segment is omitted) andTargets[0]set to the pool target.Projectis not used.delete/updatetarget parsers; the pool is a single-target org-scoped lookup.cobra.ExactArgs(1)— exactly 1 positional. Combined with theParseTargetWithDefaultOrganizationparser, the resulting shape isazdo pipelines agent list [ORGANIZATION/]POOL, where POOL accepts either an ID or a name. ThePoolIdofGetAgentsArgsis the integer returned byshared.ResolvePool, NOT the rawscope.Targets[0]string.build queue(#253) UX: 1- or 2-segment positional with ID-or-name target resolution.--jsonpasses a dedicated view struct[]agentJSONtoopts.exporter.Write. The view struct flattens identity and time fields withomitempty.pipelines/variablegroup/list/list.goandboards/workitem/list/list.go.id,name,status,enabled,version,osDescription,pool(name only when available),maxParallelism,createdOn(string),createdBy(flattened toid,displayName,uniqueName).--filter(string, short form-f) maps toGetAgentsArgs.AgentName. Renames the previous--nameflag to--filterto make the agent-name filtering role explicit (the positional is the pool, not the agent name).add/removesiblings. Short form-fis the same letter used by other--filterflags in the codebase.--include-capabilities(bool) maps toGetAgentsArgs.IncludeCapabilities.--max-items(int, default 0 = no cap) — if positive, truncate the result slice client-side.pipelines/variablegroup/list--max-items.ls,l.pr/listandpipelines/variablegroup/list.internal/cmd/pipelines/agent/list. Reuseshared.ResolvePoolfrominternal/cmd/pipelines/agent/shared/resolve.go(already implemented and tested).GetAgentsis already generated atinternal/mocks/taskagent_client_mock.go:531-540. Do not regenerate.Command Signature
Aliases:
ls,l.Positional structure (parser:
util.ParseTargetWithDefaultOrganizationatinternal/cmd/util/scope.go:157)The parser accepts 1 or 2
/-separated segments in the single positional. The resulting*util.Pathhas the following fields after a successful parse:*Path.Organizationstringctx.Config().Authentication().GetDefaultOrganization()when the segment is omitted.*Path.Project"""org/proj/pool") is rejected by the command (after parsing) via an explicitif scope.Project != ""guard returningutil.FlagErrorf("agent list does not accept a project scope; got %q", args[0]).*Path.Targets[]string{rawPoolTarget}shared.ResolvePool.Segment-count table
"accepts 1 arg(s), received 0"(cobra-level)."1"Organization= default org,Targets[0]="1". Resolver fast-path: pool ID = 1."Default"Organization= default org,Targets[0]="Default". Resolver callsGetAgentPools(name=...)and picks the first case-insensitive match."myorg/1"Organization="myorg",Targets[0]="1". Resolver fast-path: pool ID = 1."myorg/Default"Organization="myorg",Targets[0]="Default". Resolver callsGetAgentPools(name=...)."myorg/proj/pool"Organization="myorg",Project="proj",Targets[0]="pool". Command-level guard rejects with"agent list does not accept a project scope; got \"myorg/proj/pool\""."""invalid input …"(empty input).cobra.Argsvalidatorcobra.ExactArgs(1). The RunE handler assignsargs[0]toopts.targetArg, thenrun():util.ParseTargetWithDefaultOrganization(ctx, opts.targetArg)to obtain the*util.Path.scope.Project == ""— returnsutil.FlagErrorfif non-empty.cmdCtx.ClientFactory().TaskAgent(ctx, scope.Organization)to obtain the SDK client.shared.ResolvePool(ctx, taskClient, scope.Targets[0])to obtain the integerpoolID.taskClient.GetAgents(ctx, GetAgentsArgs{PoolId: &poolID, AgentName: …, IncludeCapabilities: …})with the resolved pool ID.--max-itemstruncation and renders the table or JSON.Flags
--filter,-fstring""GetAgentsArgs.AgentName--nameflag.--include-capabilitiesboolfalseGetAgentsArgs.IncludeCapabilities--max-itemsint00= no cap; truncates the slice after the SDK call.--json/--jq/--templateutil.AddJSONFlags--json id,name,statusfilters fields;--template @fileaccepts a Go-template file.JSON Output Contract
Table Output Contract
Default columns:
ID, NAME, STATUS, ENABLED, VERSION, OS, CREATED ON(mirrorstaskagent.TaskAgentcore fields, sorted by ID asc).Command Wiring
internal/cmd/pipelines/agent/listlist.go—NewCmd(ctx util.CmdContext) *cobra.Command+listOptions+runListlist_test.go— table-driven gomock testsinternal/cmd/pipelines/agent/agent.goto addlistcmd "github.com/tmeckel/azdo-cli/internal/cmd/pipelines/agent/list"andcmd.AddCommand(listcmd.NewCmd(ctx)). Update theExampleblock to use the new positional shape (e.g.,$ azdo pipelines agent list Default --filter my-agent).pipelines→agent→list.internal/cmd/pipelines/agent/sharedto callshared.ResolvePool.API Surface
Reuse the already-vendored client and the existing
shared.ResolvePoolhelper. No new SDK clients required.shared.ResolvePool(internal/cmd/pipelines/agent/shared/resolve.go:50) →(int, error). Accepts a pool ID or name; performs thestrconv.Atoifast path and theGetAgentPoolsname-lookup fallback. Returns the integer pool ID.taskagent.Client.GetAgents→ Agents - List (REST 7.1)taskagent.GetAgentsArgsstruct:{PoolId *int, AgentName *string, IncludeCapabilities *bool, IncludeAssignedRequest *bool, IncludeLastCompletedRequest *bool, PropertyFilters *[]string, Demands *[]string}.Mock for
GetAgentsis already generated atinternal/mocks/taskagent_client_mock.go:531-540.GetAgentPoolsmock is already generated (used byshared.ResolvePool). No mock regeneration needed.Reference Existing Patterns
internal/cmd/pipelines/agent/show/show.go:107— primary sibling reference. Already uses the 1-3 segment positional[ORGANIZATION/]POOL/AGENTparsed withutil.ParsePoolAgentTargetWithDefaultOrganizationand resolved viashared.ResolvePoolAgent(which itself callsshared.ResolvePool). Copy the parsing flow + project-scope guard.internal/cmd/pipelines/agent/shared/resolve.go:50—shared.ResolvePool(Decision 2). Already implemented, tested, and ready to reuse. Tests cover numeric fast path, name lookup, not-found, and ambiguous-name error cases.internal/cmd/pipelines/build/queue(planned spec feat: Implementazdo pipelines build queuecommand #253) — closest UX sibling. Usesshared.ResolvePipelineDefinitionto fold ID/name resolution into a single positional. Theagent listredesign mirrors this UX.internal/cmd/boards/workitem/list/list_test.go:765-844—setupFakeDeps/stub*fixture.internal/mocks/taskagent_client_mock.go:531-540— mock forGetAgents(already generated, do not regenerate).internal/azdo/factory.go:133-139—ClientFactory().TaskAgent(...)accessor (reuse).internal/cmd/util/scope.go:157—util.ParseTargetWithDefaultOrganization(org-only target parser; also used bywork-item deletefeat: Implementazdo boards work-item deletecommand #269,work-item updatefeat: Implementazdo boards work-item updatecommand #270, and the security membership commands).References
--pool-id, but the redesignedazdoUX folds the pool into the positional with ID-or-name resolution, matching theazdo pipelines build queueUX.