argsbarg 6.1.4 → 6.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,10 +5,256 @@
5
5
  "version": "1.0.0",
6
6
  "description": "Argsbarg full example reference app"
7
7
  },
8
+ "tags": [
9
+ {
10
+ "name": "health",
11
+ "description": "Orchestrator health probes — liveness (online) vs readiness (online + checks passed)."
12
+ },
13
+ {
14
+ "name": "echo",
15
+ "description": "Echo a message (MCP-friendly leaf)."
16
+ },
17
+ {
18
+ "name": "render-json",
19
+ "description": "Echo a JSON message (schema-first JSON leaf demo)."
20
+ },
21
+ {
22
+ "name": "status",
23
+ "description": "Show app version."
24
+ },
25
+ {
26
+ "name": "workspaces",
27
+ "description": "Workspace collection and CRUD."
28
+ }
29
+ ],
8
30
  "paths": {
9
- "/api/echo": {
31
+ "/health/liveness": {
32
+ "get": {
33
+ "tags": [
34
+ "health"
35
+ ],
36
+ "operationId": "health_liveness",
37
+ "summary": "Liveness probe",
38
+ "description": "Returns 200 when the HTTP server is online and accepting requests. Does not run config or readiness checks — use for orchestrator liveness probes only.",
39
+ "responses": {
40
+ "200": {
41
+ "description": "Server is online",
42
+ "content": {
43
+ "application/json; charset=utf-8": {
44
+ "schema": {
45
+ "type": "object",
46
+ "properties": {
47
+ "ok": {
48
+ "type": "boolean",
49
+ "const": true
50
+ }
51
+ },
52
+ "required": [
53
+ "ok"
54
+ ]
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
60
+ }
61
+ },
62
+ "/health/readiness": {
63
+ "get": {
64
+ "tags": [
65
+ "health"
66
+ ],
67
+ "operationId": "health_readiness",
68
+ "summary": "Readiness probe",
69
+ "description": "Returns 200 when the server is online and all readiness checks pass (config file, required app config, and optional program.readiness). Returns 503 when any check fails — use for orchestrator readiness probes before routing traffic.",
70
+ "responses": {
71
+ "200": {
72
+ "description": "Online and ready to serve traffic",
73
+ "content": {
74
+ "application/json; charset=utf-8": {
75
+ "schema": {
76
+ "type": "object",
77
+ "properties": {
78
+ "ok": {
79
+ "type": "boolean"
80
+ },
81
+ "checks": {
82
+ "type": "object",
83
+ "properties": {
84
+ "config_file": {
85
+ "type": "object",
86
+ "properties": {
87
+ "ok": {
88
+ "type": "boolean"
89
+ },
90
+ "error": {
91
+ "type": "string"
92
+ },
93
+ "missing": {
94
+ "type": "array",
95
+ "items": {
96
+ "type": "string"
97
+ }
98
+ }
99
+ },
100
+ "required": [
101
+ "ok"
102
+ ]
103
+ },
104
+ "config_required": {
105
+ "type": "object",
106
+ "properties": {
107
+ "ok": {
108
+ "type": "boolean"
109
+ },
110
+ "error": {
111
+ "type": "string"
112
+ },
113
+ "missing": {
114
+ "type": "array",
115
+ "items": {
116
+ "type": "string"
117
+ }
118
+ }
119
+ },
120
+ "required": [
121
+ "ok"
122
+ ]
123
+ },
124
+ "custom": {
125
+ "type": "object",
126
+ "properties": {
127
+ "ok": {
128
+ "type": "boolean"
129
+ },
130
+ "error": {
131
+ "type": "string"
132
+ },
133
+ "missing": {
134
+ "type": "array",
135
+ "items": {
136
+ "type": "string"
137
+ }
138
+ }
139
+ },
140
+ "required": [
141
+ "ok"
142
+ ]
143
+ }
144
+ },
145
+ "required": [
146
+ "config_file",
147
+ "config_required",
148
+ "custom"
149
+ ]
150
+ }
151
+ },
152
+ "required": [
153
+ "ok",
154
+ "checks"
155
+ ]
156
+ }
157
+ }
158
+ }
159
+ },
160
+ "503": {
161
+ "description": "Online but not ready (one or more checks failed)",
162
+ "content": {
163
+ "application/json; charset=utf-8": {
164
+ "schema": {
165
+ "type": "object",
166
+ "properties": {
167
+ "ok": {
168
+ "type": "boolean"
169
+ },
170
+ "checks": {
171
+ "type": "object",
172
+ "properties": {
173
+ "config_file": {
174
+ "type": "object",
175
+ "properties": {
176
+ "ok": {
177
+ "type": "boolean"
178
+ },
179
+ "error": {
180
+ "type": "string"
181
+ },
182
+ "missing": {
183
+ "type": "array",
184
+ "items": {
185
+ "type": "string"
186
+ }
187
+ }
188
+ },
189
+ "required": [
190
+ "ok"
191
+ ]
192
+ },
193
+ "config_required": {
194
+ "type": "object",
195
+ "properties": {
196
+ "ok": {
197
+ "type": "boolean"
198
+ },
199
+ "error": {
200
+ "type": "string"
201
+ },
202
+ "missing": {
203
+ "type": "array",
204
+ "items": {
205
+ "type": "string"
206
+ }
207
+ }
208
+ },
209
+ "required": [
210
+ "ok"
211
+ ]
212
+ },
213
+ "custom": {
214
+ "type": "object",
215
+ "properties": {
216
+ "ok": {
217
+ "type": "boolean"
218
+ },
219
+ "error": {
220
+ "type": "string"
221
+ },
222
+ "missing": {
223
+ "type": "array",
224
+ "items": {
225
+ "type": "string"
226
+ }
227
+ }
228
+ },
229
+ "required": [
230
+ "ok"
231
+ ]
232
+ }
233
+ },
234
+ "required": [
235
+ "config_file",
236
+ "config_required",
237
+ "custom"
238
+ ]
239
+ }
240
+ },
241
+ "required": [
242
+ "ok",
243
+ "checks"
244
+ ]
245
+ }
246
+ }
247
+ }
248
+ }
249
+ }
250
+ }
251
+ },
252
+ "/echo": {
10
253
  "post": {
11
- "operationId": "_api_echo",
254
+ "tags": [
255
+ "echo"
256
+ ],
257
+ "operationId": "_echo",
12
258
  "summary": "Echo a message (MCP-friendly leaf).",
13
259
  "responses": {
14
260
  "201": {
@@ -115,9 +361,12 @@
115
361
  }
116
362
  }
117
363
  },
118
- "/api/render-json": {
364
+ "/render-json": {
119
365
  "post": {
120
- "operationId": "_api_render-json",
366
+ "tags": [
367
+ "render-json"
368
+ ],
369
+ "operationId": "_render-json",
121
370
  "summary": "Echo a JSON message (schema-first JSON leaf demo).",
122
371
  "responses": {
123
372
  "201": {
@@ -226,9 +475,12 @@
226
475
  }
227
476
  }
228
477
  },
229
- "/api/status": {
478
+ "/status": {
230
479
  "post": {
231
- "operationId": "_api_status",
480
+ "tags": [
481
+ "status"
482
+ ],
483
+ "operationId": "_status",
232
484
  "summary": "Show app version.",
233
485
  "responses": {
234
486
  "201": {
@@ -343,9 +595,12 @@
343
595
  }
344
596
  }
345
597
  },
346
- "/api/workspaces": {
598
+ "/workspaces": {
347
599
  "get": {
348
- "operationId": "_api_workspaces",
600
+ "tags": [
601
+ "workspaces"
602
+ ],
603
+ "operationId": "_workspaces",
349
604
  "summary": "List workspaces.",
350
605
  "responses": {
351
606
  "200": {
@@ -434,7 +689,10 @@
434
689
  "parameters": []
435
690
  },
436
691
  "post": {
437
- "operationId": "_api_workspaces",
692
+ "tags": [
693
+ "workspaces"
694
+ ],
695
+ "operationId": "_workspaces",
438
696
  "summary": "Create a workspace.",
439
697
  "responses": {
440
698
  "201": {
@@ -543,9 +801,12 @@
543
801
  }
544
802
  }
545
803
  },
546
- "/api/workspaces/{id}": {
804
+ "/workspaces/{id}": {
547
805
  "get": {
548
- "operationId": "_api_workspaces_id",
806
+ "tags": [
807
+ "workspaces"
808
+ ],
809
+ "operationId": "_workspaces_id",
549
810
  "summary": "Get one workspace.",
550
811
  "responses": {
551
812
  "200": {
@@ -643,7 +904,10 @@
643
904
  ]
644
905
  },
645
906
  "put": {
646
- "operationId": "_api_workspaces_id",
907
+ "tags": [
908
+ "workspaces"
909
+ ],
910
+ "operationId": "_workspaces_id",
647
911
  "summary": "Replace a workspace.",
648
912
  "responses": {
649
913
  "200": {
@@ -762,7 +1026,10 @@
762
1026
  }
763
1027
  },
764
1028
  "patch": {
765
- "operationId": "_api_workspaces_id",
1029
+ "tags": [
1030
+ "workspaces"
1031
+ ],
1032
+ "operationId": "_workspaces_id",
766
1033
  "summary": "Patch a workspace name.",
767
1034
  "responses": {
768
1035
  "200": {
@@ -881,7 +1148,10 @@
881
1148
  }
882
1149
  },
883
1150
  "delete": {
884
- "operationId": "_api_workspaces_id",
1151
+ "tags": [
1152
+ "workspaces"
1153
+ ],
1154
+ "operationId": "_workspaces_id",
885
1155
  "summary": "Delete a workspace.",
886
1156
  "responses": {
887
1157
  "204": {
@@ -4,10 +4,10 @@ This example shows the smallest end-to-end CLI+MCP+API setup.
4
4
  It includes one command, a couple of options, and a direct call to the runtime so
5
5
  readers can copy the pattern into their own scripts quickly.
6
6
 
7
- Demonstrates: `servers.ts hello`, MCP tool `hello`, and `POST /api/hello`.
7
+ Demonstrates: `servers.ts hello`, MCP tool `hello`, and `POST /hello`.
8
8
 
9
9
  Ex API Call:
10
- curl -s -X POST http://127.0.0.1:3000/api/hello \
10
+ curl -s -X POST http://127.0.0.1:3000/hello \
11
11
  -H 'content-type: application/json' \
12
12
  -d '{"name":"alice"}'
13
13
  Ex API Response:
package/index.d.ts CHANGED
@@ -353,6 +353,11 @@ export interface CliHttpServerConfig {
353
353
  host?: string;
354
354
  /** Listen port (default: `3000`). */
355
355
  port?: number;
356
+ /**
357
+ * URL prefix for user command routes (default: `""` — routes at server root, e.g. `/workspaces`).
358
+ * Set to `"/api"` for `/api/workspaces`-style paths.
359
+ */
360
+ pathPrefix?: string;
356
361
  /** Honor `X-Forwarded-For` for client IP in hooks and logs. */
357
362
  trustProxy?: boolean;
358
363
  /** HTTP error response defaults. */
@@ -744,7 +749,7 @@ export type CliProgram = CliNode & {
744
749
  docs?: CliDocsConfig;
745
750
  /** Invoke and error hooks for user commands on CLI, HTTP, and MCP. */
746
751
  hooks?: CliProgramHooks;
747
- /** Optional readiness probe for HTTP/MCP `GET /health/ready` only. */
752
+ /** Optional readiness probe for HTTP/MCP `GET /health/readiness` only. */
748
753
  readiness?: (ctx: ReadinessContext) => boolean | Promise<boolean>;
749
754
  /** Framework logging (stderr + optional file). */
750
755
  log?: CliLogConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "6.1.4",
3
+ "version": "6.1.6",
4
4
  "main": "./src/index.ts",
5
5
  "module": "./src/index.ts",
6
6
  "dependencies": {
@@ -7,6 +7,7 @@ import {
7
7
  type CliRouter,
8
8
  } from "~/core/types.ts";
9
9
  import { docsEnabled } from "~/docs/resolve.ts";
10
+ import { httpUserPathGlob, resolveHttpPathPrefix } from "~/http/paths.ts";
10
11
  import { resolveHttpListenAddress } from "~/http/server.ts";
11
12
  import { resolveCapabilities } from "~/runtime/capabilities.ts";
12
13
 
@@ -34,10 +35,11 @@ const HTTP_SERVE_OPTIONS: CliOption[] = [
34
35
  export function cliBuiltinHttpCommand(program: CliProgram): CliRouter {
35
36
  const caps = resolveCapabilities(program);
36
37
  const { hostname, port } = resolveHttpListenAddress(program);
38
+ const userGlob = httpUserPathGlob(resolveHttpPathPrefix(program));
37
39
  const lines = [
38
40
  `HTTP tool server on http://${hostname}:${port}.`,
39
41
  "",
40
- "Endpoints: GET /health, GET /health/ready, GET /openapi.json, GET /swagger, /api/*",
42
+ `Endpoints: GET /health/liveness, GET /health/readiness, GET /openapi.json, GET /swagger, ${userGlob}`,
41
43
  "",
42
44
  ];
43
45
  if (caps.configure) {
package/src/core/types.ts CHANGED
@@ -246,6 +246,11 @@ export interface CliHttpServerConfig {
246
246
  host?: string;
247
247
  /** Listen port (default: `3000`). */
248
248
  port?: number;
249
+ /**
250
+ * URL prefix for user command routes (default: `""` — routes at server root, e.g. `/workspaces`).
251
+ * Set to `"/api"` for `/api/workspaces`-style paths.
252
+ */
253
+ pathPrefix?: string;
249
254
  /** Honor `X-Forwarded-For` for client IP in hooks and logs. */
250
255
  trustProxy?: boolean;
251
256
  /** HTTP error response defaults. */
@@ -669,7 +674,7 @@ export type CliProgram = CliNode & {
669
674
  docs?: CliDocsConfig;
670
675
  /** Invoke and error hooks for user commands on CLI, HTTP, and MCP. */
671
676
  hooks?: CliProgramHooks;
672
- /** Optional readiness probe for HTTP/MCP `GET /health/ready` only. */
677
+ /** Optional readiness probe for HTTP/MCP `GET /health/readiness` only. */
673
678
  readiness?: (ctx: ReadinessContext) => boolean | Promise<boolean>;
674
679
  /** Framework logging (stderr + optional file). */
675
680
  log?: CliLogConfig;
@@ -5,6 +5,7 @@ This module validates CLI schemas before execution.
5
5
  import { AGENT_PAIRS, MCP_KEYS, mcpServerRequiredForArtifact } from "~/configure/artifacts/target-registry.ts";
6
6
  import { reservedDocsTopicResourceUris } from "~/docs/mcp-resources.ts";
7
7
  import { DOCS_BUILTIN_TOPIC_KEYS, docsEnabled } from "~/docs/resolve.ts";
8
+ import { HTTP_RESERVED_TOP_LEVEL_SEGMENTS } from "~/http/paths.ts";
8
9
  import { resolveMcpSchemaUri } from "~/mcp/tools.ts";
9
10
  import { reservedCommandNames, resolveCapabilities } from "~/runtime/capabilities.ts";
10
11
  import { validateFormatValue } from "./formats.ts";
@@ -180,6 +181,8 @@ export function cliValidateProgram(program: CliProgram): void {
180
181
  throw new CliSchemaValidationError("httpServer requires enabled: true; omit httpServer to disable HTTP API");
181
182
  }
182
183
 
184
+ validateHttpPathPrefix(program);
185
+
183
186
  if (docsEnabled(program) && program.docs?.topics !== undefined) {
184
187
  validateDocsConfig(program.docs);
185
188
  }
@@ -212,6 +215,46 @@ function isParamRouterKey(key: string): boolean {
212
215
  return key.startsWith(":");
213
216
  }
214
217
 
218
+ /** Validates `httpServer.pathPrefix` and reserved top-level command keys. */
219
+ function validateHttpPathPrefix(program: CliProgram): void {
220
+ if (!program.httpServer?.enabled) {
221
+ return;
222
+ }
223
+ const raw = program.httpServer.pathPrefix;
224
+ if (raw !== undefined && raw !== "") {
225
+ if (!raw.startsWith("/")) {
226
+ throw new CliSchemaValidationError(`httpServer.pathPrefix must start with / (got ${JSON.stringify(raw)})`);
227
+ }
228
+ if (raw.length > 1 && raw.endsWith("/")) {
229
+ throw new CliSchemaValidationError(`httpServer.pathPrefix must not end with / (got ${JSON.stringify(raw)})`);
230
+ }
231
+ if (raw.includes("//")) {
232
+ throw new CliSchemaValidationError("httpServer.pathPrefix must not contain //");
233
+ }
234
+ if (raw === "/health" || raw === "/swagger" || raw === "/openapi.json" || raw === "/tools") {
235
+ throw new CliSchemaValidationError(
236
+ `httpServer.pathPrefix must not be a framework path (got ${JSON.stringify(raw)})`,
237
+ );
238
+ }
239
+ return;
240
+ }
241
+ if (!isCliRouter(program)) {
242
+ if (isCliLeaf(program) && HTTP_RESERVED_TOP_LEVEL_SEGMENTS.has(program.key)) {
243
+ throw new CliSchemaValidationError(
244
+ `Reserved HTTP program key when httpServer.pathPrefix is empty: ${program.key}`,
245
+ );
246
+ }
247
+ return;
248
+ }
249
+ for (const child of program.commands) {
250
+ if (HTTP_RESERVED_TOP_LEVEL_SEGMENTS.has(child.key)) {
251
+ throw new CliSchemaValidationError(
252
+ `Reserved HTTP command name when httpServer.pathPrefix is empty: ${child.key} (set httpServer.pathPrefix or rename)`,
253
+ );
254
+ }
255
+ }
256
+ }
257
+
215
258
  function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
216
259
  if (!isRoot) {
217
260
  const rogue = node as CliProgram;
@@ -2,6 +2,7 @@ import { defaultConfigEntryTitle } from "~/config/entry.ts";
2
2
  import { displayAppConfigPath } from "~/config/file.ts";
3
3
  import { collectOptionDefs } from "~/core/parse.ts";
4
4
  import { CliOptionKind, type CliProgram } from "~/core/types.ts";
5
+ import { httpUserPathGlob, resolveHttpPathPrefix } from "~/http/paths.ts";
5
6
  import { collectHttpRoutes } from "~/http/routes.ts";
6
7
  import { resolveHttpListenAddress } from "~/http/server.ts";
7
8
 
@@ -27,6 +28,9 @@ export function generateHttpGuide(root: CliProgram): string {
27
28
  const routes = collectHttpRoutes(root);
28
29
  const { hostname, port } = resolveHttpListenAddress(root);
29
30
  const baseUrl = `http://${hostname}:${port}`;
31
+ const pathPrefix = resolveHttpPathPrefix(root);
32
+ const userGlob = httpUserPathGlob(pathPrefix);
33
+ const sampleWorkspaces = `${pathPrefix}/workspaces`;
30
34
 
31
35
  const lines: string[] = [
32
36
  `# HTTP API (${root.key})`,
@@ -47,23 +51,23 @@ export function generateHttpGuide(root: CliProgram): string {
47
51
  "",
48
52
  "| Method | Path | Purpose |",
49
53
  "| --- | --- | --- |",
50
- "| `GET` | `/health` or `/health/live` | Liveness check |",
51
- "| `GET` | `/health/ready` | Readiness (config + `program.readiness`) |",
54
+ "| `GET` | `/health/liveness` | Liveness — server is online and accepting requests |",
55
+ "| `GET` | `/health/readiness` | Readiness — online plus config and `program.readiness` checks passed |",
52
56
  "| `GET` | `/openapi.json` | OpenAPI 3.1 REST paths |",
53
57
  "| `GET` | `/swagger` | Interactive Swagger UI API reference |",
54
- "| `*` | `/api/...` | Invoke user commands (method per route) |",
58
+ `| * | \`${userGlob}\` | Invoke user commands (method per route) |`,
55
59
  "| `OPTIONS` | `*` | CORS preflight |",
56
60
  "",
57
- "Discover paths from `openapi.json` (`/api/...`). Query binds options; POST/PUT/PATCH body binds options and `inputSchema` fields.",
61
+ `Discover paths from \`openapi.json\` (\`${userGlob}\`). Query binds options; POST/PUT/PATCH body binds options and \`inputSchema\` fields.`,
58
62
  "",
59
63
  "## Examples",
60
64
  "",
61
65
  "```bash",
62
- `curl -s ${baseUrl}/health`,
63
- `curl -s ${baseUrl}/health/ready`,
66
+ `curl -s ${baseUrl}/health/liveness`,
67
+ `curl -s ${baseUrl}/health/readiness`,
64
68
  `curl -s ${baseUrl}/openapi.json`,
65
- `curl -s ${baseUrl}/api/workspaces`,
66
- `curl -s -X POST ${baseUrl}/api/workspaces \\`,
69
+ `curl -s ${baseUrl}${sampleWorkspaces}`,
70
+ `curl -s -X POST ${baseUrl}${sampleWorkspaces} \\`,
67
71
  ' -H "content-type: application/json" \\',
68
72
  ` -d '{"name":"qa2"}'`,
69
73
  "```",
@@ -127,7 +131,7 @@ export function generateHttpGuide(root: CliProgram): string {
127
131
  `- **Fetch** — \`curl -s ${baseUrl}/openapi.json\``,
128
132
  `- **Save offline** — \`${root.key} docs openapi --save\` → \`./docs/openapi.json\` (or \`just docgen\` in app repos)`,
129
133
  "",
130
- "Use the spec to discover REST paths and request/response shapes before calling `/api/...`.",
134
+ `Use the spec to discover REST paths and request/response shapes before calling \`${userGlob}\`.`,
131
135
  "",
132
136
  );
133
137
 
@@ -150,35 +150,35 @@ function jsonResponseEntry(description: string, schema: Record<string, unknown>)
150
150
  };
151
151
  }
152
152
 
153
- function livenessGetOp(operationId: string, summary: string): Record<string, unknown> {
153
+ function livenessGetOp(): Record<string, unknown> {
154
154
  return {
155
155
  tags: [HEALTH_TAG],
156
- operationId,
157
- summary,
156
+ operationId: "health_liveness",
157
+ summary: "Liveness probe",
158
+ description:
159
+ "Returns 200 when the HTTP server is online and accepting requests. Does not run config or readiness checks — use for orchestrator liveness probes only.",
158
160
  responses: {
159
- "200": jsonResponseEntry("Server is listening", livenessResponseSchema),
161
+ "200": jsonResponseEntry("Server is online", livenessResponseSchema),
160
162
  },
161
163
  };
162
164
  }
163
165
 
164
- /** Framework health probe paths served alongside `/api/*` routes. */
166
+ /** Framework health probe paths served alongside user command routes. */
165
167
  function buildHealthPaths(): Record<string, unknown> {
166
168
  return {
167
- "/health": {
168
- get: livenessGetOp("health", "Liveness probe (alias of /health/live)"),
169
+ "/health/liveness": {
170
+ get: livenessGetOp(),
169
171
  },
170
- "/health/live": {
171
- get: livenessGetOp("health_live", "Liveness probe"),
172
- },
173
- "/health/ready": {
172
+ "/health/readiness": {
174
173
  get: {
175
174
  tags: [HEALTH_TAG],
176
- operationId: "health_ready",
175
+ operationId: "health_readiness",
177
176
  summary: "Readiness probe",
178
- description: "Config file, required app config, and optional program.readiness checks.",
177
+ description:
178
+ "Returns 200 when the server is online and all readiness checks pass (config file, required app config, and optional program.readiness). Returns 503 when any check fails — use for orchestrator readiness probes before routing traffic.",
179
179
  responses: {
180
- "200": jsonResponseEntry("Ready to serve traffic", readinessResponseSchema),
181
- "503": jsonResponseEntry("Not ready", readinessResponseSchema),
180
+ "200": jsonResponseEntry("Online and ready to serve traffic", readinessResponseSchema),
181
+ "503": jsonResponseEntry("Online but not ready (one or more checks failed)", readinessResponseSchema),
182
182
  },
183
183
  },
184
184
  },
@@ -200,7 +200,7 @@ function findTopLevelCommand(program: CliProgram, key: string): CliNode | undefi
200
200
  return program.commands.find((c) => c.key === key);
201
201
  }
202
202
 
203
- /** OpenAPI tags for user `/api/*` routes, one per top-level command. */
203
+ /** OpenAPI tags for user command routes, one per top-level command. */
204
204
  function collectCommandTags(program: CliProgram, routes: HttpRoute[]): { name: string; description?: string }[] {
205
205
  const names = [...new Set(routes.map((route) => topLevelCommandKey(route, program)))].sort();
206
206
  return names.map((name) => {
@@ -278,7 +278,15 @@ export function generateOpenApi(program: CliProgram): Record<string, unknown> {
278
278
  description: program.description,
279
279
  },
280
280
  ...(program.httpServer?.enabled
281
- ? { tags: [{ name: HEALTH_TAG, description: "Server health probes" }, ...commandTags] }
281
+ ? {
282
+ tags: [
283
+ {
284
+ name: HEALTH_TAG,
285
+ description: "Orchestrator health probes — liveness (online) vs readiness (online + checks passed).",
286
+ },
287
+ ...commandTags,
288
+ ],
289
+ }
282
290
  : {}),
283
291
  paths,
284
292
  };