@gonovus-ai/spec-flow-cli 0.0.0-stage → 0.3.0

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.
Files changed (62) hide show
  1. package/README.md +805 -2
  2. package/dist/authentication-command-topology.d.ts +7 -0
  3. package/dist/authentication-command-topology.js +9 -0
  4. package/dist/authentication-command-topology.js.map +1 -0
  5. package/dist/bin.d.ts +2 -0
  6. package/dist/bin.js +52 -0
  7. package/dist/bin.js.map +1 -0
  8. package/dist/cli.d.ts +16 -0
  9. package/dist/cli.js +3355 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/client-identity.d.ts +12 -0
  12. package/dist/client-identity.js +29 -0
  13. package/dist/client-identity.js.map +1 -0
  14. package/dist/credential-store.d.ts +20 -0
  15. package/dist/credential-store.js +146 -0
  16. package/dist/credential-store.js.map +1 -0
  17. package/dist/default-api-origin.d.ts +1 -0
  18. package/dist/default-api-origin.js +1 -0
  19. package/dist/default-api-origin.js.map +1 -0
  20. package/dist/github-account-commands.d.ts +25 -0
  21. package/dist/github-account-commands.js +117 -0
  22. package/dist/github-account-commands.js.map +1 -0
  23. package/dist/github-connection-commands.d.ts +4 -0
  24. package/dist/github-connection-commands.js +99 -0
  25. package/dist/github-connection-commands.js.map +1 -0
  26. package/dist/github-import-commands.d.ts +3 -0
  27. package/dist/github-import-commands.js +64 -0
  28. package/dist/github-import-commands.js.map +1 -0
  29. package/dist/http-origin.d.ts +13 -0
  30. package/dist/http-origin.js +39 -0
  31. package/dist/http-origin.js.map +1 -0
  32. package/dist/index.d.ts +22 -0
  33. package/dist/index.js +240 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/node-version.d.ts +1 -0
  36. package/dist/node-version.js +8 -0
  37. package/dist/node-version.js.map +1 -0
  38. package/dist/product-map-commands.d.ts +17 -0
  39. package/dist/product-map-commands.js +48 -0
  40. package/dist/product-map-commands.js.map +1 -0
  41. package/dist/safe-cli-error.d.ts +2 -0
  42. package/dist/safe-cli-error.js +3 -0
  43. package/dist/safe-cli-error.js.map +1 -0
  44. package/dist/user-agent.d.ts +3 -0
  45. package/dist/user-agent.js +10 -0
  46. package/dist/user-agent.js.map +1 -0
  47. package/dist/user-api-key-commands.d.ts +28 -0
  48. package/dist/user-api-key-commands.js +129 -0
  49. package/dist/user-api-key-commands.js.map +1 -0
  50. package/dist/work-item-asset-commands.d.ts +24 -0
  51. package/dist/work-item-asset-commands.js +217 -0
  52. package/dist/work-item-asset-commands.js.map +1 -0
  53. package/dist/work-item-exchange-file.d.ts +4 -0
  54. package/dist/work-item-exchange-file.js +66 -0
  55. package/dist/work-item-exchange-file.js.map +1 -0
  56. package/dist/work-item-resource-link-commands.d.ts +23 -0
  57. package/dist/work-item-resource-link-commands.js +96 -0
  58. package/dist/work-item-resource-link-commands.js.map +1 -0
  59. package/dist/work-item-time-entry-input.d.ts +4 -0
  60. package/dist/work-item-time-entry-input.js +27 -0
  61. package/dist/work-item-time-entry-input.js.map +1 -0
  62. package/package.json +54 -4
package/dist/cli.js ADDED
@@ -0,0 +1,3355 @@
1
+ import { Command, CommanderError, InvalidArgumentError } from "commander";
2
+ import { randomUUID } from "node:crypto";
3
+ import createClient from "openapi-fetch";
4
+ import { z } from "zod";
5
+ import { addAuthenticationCommandTopology } from "./authentication-command-topology.js";
6
+ import { registerGitHubAccountCommands } from "./github-account-commands.js";
7
+ import { parseHttpOrigin, unresolvedSpecFlowApiOrigin, } from "./http-origin.js";
8
+ import { registerProductMapCommands } from "./product-map-commands.js";
9
+ import { specFlowCliUserAgent } from "./user-agent.js";
10
+ import { registerUserApiKeyCommands } from "./user-api-key-commands.js";
11
+ import { registerWorkItemAssetCommands } from "./work-item-asset-commands.js";
12
+ import { readWorkItemImportFile } from "./work-item-exchange-file.js";
13
+ import { registerWorkItemResourceLinkCommands } from "./work-item-resource-link-commands.js";
14
+ import { parseTimeEntryTimestamp, parseTimeEntryUuid, parseTimeEntryDescription } from "./work-item-time-entry-input.js";
15
+ export function formatResourceDiagnostic(message) {
16
+ return `${JSON.stringify({ diagnostic: { level: "warning", message } })}\n`;
17
+ }
18
+ const workItemBulkActions = [
19
+ "updateStatus",
20
+ "updatePriority",
21
+ "updateAssignee",
22
+ "delete",
23
+ "addLabel",
24
+ "removeLabel",
25
+ "updateDueDate",
26
+ ];
27
+ const glossaryEntryInputSchema = z
28
+ .object({
29
+ canonicalTerm: z.string().trim().min(1).max(200),
30
+ definition: z.string().trim().min(1).max(4_000),
31
+ id: z.string().trim().min(1).max(200),
32
+ usageNote: z.string().trim().min(1).max(2_000).nullable(),
33
+ })
34
+ .strict();
35
+ const glossaryEntriesInputSchema = z.array(glossaryEntryInputSchema).max(500);
36
+ const sourceReferenceInputSchema = z
37
+ .object({
38
+ externalRevision: z.string().trim().min(1).max(200).optional(),
39
+ section: z.string().trim().min(1).max(500).optional(),
40
+ title: z.string().trim().min(1).max(300),
41
+ url: z.url().max(2_000).refine((value) => {
42
+ const url = new URL(value);
43
+ return ((url.protocol === "https:" || url.protocol === "http:") &&
44
+ url.username.length === 0 &&
45
+ url.password.length === 0);
46
+ }),
47
+ })
48
+ .strict();
49
+ const sourceReferencesInputSchema = z.array(sourceReferenceInputSchema).max(20);
50
+ const roleStatementsInputSchema = z.record(z.string().trim().min(1).max(200), z.array(z.string().trim().min(1).max(200)).min(1).max(50));
51
+ const findingReferenceSchema = z
52
+ .object({
53
+ kind: z.enum(["SharedPrecondition", "ExampleTable", "Title", "Rule", "Scenario", "Line"]),
54
+ text: z.string().trim().min(1).max(20_000),
55
+ })
56
+ .strict();
57
+ const findingTargetSchema = z
58
+ .object({
59
+ references: z.array(findingReferenceSchema).min(1).max(50),
60
+ specId: z.string().trim().toUpperCase().regex(/^[A-Z]{2,10}-S[1-9]\d*$/u),
61
+ })
62
+ .strict();
63
+ const findingEvidenceSchema = z.discriminatedUnion("kind", [
64
+ z
65
+ .object({
66
+ incompatibleClaims: z.tuple([
67
+ z.string().trim().min(1).max(4_000),
68
+ z.string().trim().min(1).max(4_000),
69
+ ]),
70
+ kind: z.literal("Contradiction"),
71
+ })
72
+ .strict(),
73
+ z
74
+ .object({
75
+ incompatibleRequirements: z.tuple([
76
+ z.string().trim().min(1).max(4_000),
77
+ z.string().trim().min(1).max(4_000),
78
+ ]),
79
+ kind: z.literal("Conflict"),
80
+ })
81
+ .strict(),
82
+ z
83
+ .object({
84
+ duplicatedBehavior: z.string().trim().min(1).max(4_000),
85
+ kind: z.literal("Duplicate"),
86
+ })
87
+ .strict(),
88
+ z
89
+ .object({
90
+ interpretations: z.tuple([
91
+ z
92
+ .object({
93
+ behavior: z.string().trim().min(1).max(4_000),
94
+ meaning: z.string().trim().min(1).max(4_000),
95
+ })
96
+ .strict(),
97
+ z
98
+ .object({
99
+ behavior: z.string().trim().min(1).max(4_000),
100
+ meaning: z.string().trim().min(1).max(4_000),
101
+ })
102
+ .strict(),
103
+ ]),
104
+ kind: z.literal("Ambiguity"),
105
+ })
106
+ .strict(),
107
+ z
108
+ .object({
109
+ glossaryEntryIds: z.array(z.string().trim().min(1).max(200)).max(20),
110
+ issue: z.enum(["MissingDefinition", "GlossaryConflict", "MultipleMeanings"]),
111
+ kind: z.literal("Terminology"),
112
+ term: z.string().trim().min(1).max(200),
113
+ })
114
+ .strict(),
115
+ z
116
+ .object({
117
+ kind: z.literal("Unverifiable"),
118
+ missingCriterion: z.string().trim().min(1).max(4_000),
119
+ })
120
+ .strict(),
121
+ ]);
122
+ const findingCreateInputSchema = z
123
+ .object({
124
+ affectedScenarios: z.array(z.string().trim().min(1).max(300)).max(50),
125
+ conclusion: z.string().trim().min(1).max(4_000),
126
+ evidence: findingEvidenceSchema,
127
+ explanation: z.string().trim().min(1).max(8_000),
128
+ targets: z.array(findingTargetSchema).min(1).max(10),
129
+ })
130
+ .strict();
131
+ export async function runCli(arguments_, dependencies) {
132
+ let exitCode = 0;
133
+ const program = new Command()
134
+ .name("specflow")
135
+ .description("Read and govern SpecFlow through supported commands")
136
+ .option("--allow-file-fallback", "use a warned owner-only local credential file if the system store is unavailable")
137
+ .option("--server <url>", "SpecFlow server origin")
138
+ .option("--api-key-id <id>", "Use an origin-scoped User API Key from the credential store")
139
+ .exitOverride()
140
+ .configureOutput({
141
+ writeErr: () => undefined,
142
+ writeOut: dependencies.stdout,
143
+ });
144
+ const run = (request, options = {}) => executeRequest(dependencies, request, options, program.getOptionValue("server"), program.getOptionValue("apiKeyId"));
145
+ registerUserApiKeyCommands(program, { dependencies, run, setExitCode: (value) => { exitCode = value; }, parseScopes: parseRoleStatements, parseLimit: parseSearchLimit });
146
+ registerGitHubAccountCommands(program, { dependencies, run, setExitCode: (value) => { exitCode = value; } });
147
+ const authentication = addAuthenticationCommandTopology(program);
148
+ program.hook("preAction", (_command, actionCommand) => {
149
+ const options = actionCommand.opts();
150
+ if (typeof options.projectId === "string") {
151
+ actionCommand.setOptionValue("projectId", parseProjectId(options.projectId));
152
+ }
153
+ if (typeof options.specId === "string") {
154
+ actionCommand.setOptionValue("specId", parseSpecId(options.specId));
155
+ }
156
+ if (typeof options.workItemId === "string") {
157
+ actionCommand.setOptionValue("workItemId", parseWorkItemId(options.workItemId));
158
+ }
159
+ });
160
+ for (const command of [
161
+ authentication.login,
162
+ authentication.logout,
163
+ authentication.status,
164
+ ]) {
165
+ command.action(() => {
166
+ throw new CommanderError(2, "specflow.authenticationCommand", "Authentication commands use the human-facing CLI boundary");
167
+ });
168
+ }
169
+ const workspace = program.command("workspace");
170
+ workspace
171
+ .command("list")
172
+ .action(async () => {
173
+ exitCode = await run((client) => client.GET("/v1/workspaces"), { rejectWorkspaceReadToken: true });
174
+ });
175
+ workspace
176
+ .command("create")
177
+ .requiredOption("--name <name>")
178
+ .requiredOption("--prefix <prefix>", "Immutable Workspace number prefix, 2 to 10 uppercase letters")
179
+ .option("--spec-language <language>", "Workspace keyword language: zh-CN or en", (value) => z.enum(["zh-CN", "en"]).parse(value), "zh-CN")
180
+ .action(async (options) => {
181
+ if (hasWorkspaceReadToken(dependencies.environment)) {
182
+ exitCode = rejectTokenWrite(dependencies);
183
+ return;
184
+ }
185
+ const idempotencyKey = randomUUID();
186
+ exitCode = await run((client) => client.POST("/v1/workspaces", {
187
+ body: { name: options.name, prefix: options.prefix, specLanguage: options.specLanguage },
188
+ params: { header: { "idempotency-key": idempotencyKey } },
189
+ }), { retryTransportOnce: true, ambiguousOutcomeMessage: "Workspace creation outcome is unknown; run workspace list before trying a new command identity" });
190
+ });
191
+ workspace.command("rename").requiredOption("--workspace-id <workspaceId>").requiredOption("--name <name>").requiredOption("--expected-version <version>", "Observed Workspace settings version", parseOpaqueVersion).requiredOption("--idempotency-key <key>").action(async (options) => {
192
+ exitCode = await run(client => client.PATCH("/v1/workspaces/{workspaceId}/settings", { params: { path: { workspaceId: options.workspaceId }, header: { "idempotency-key": options.idempotencyKey, "if-match": options.expectedVersion } }, body: { name: options.name } }), { retryTransportOnce: true });
193
+ });
194
+ const workspaceSettings = workspace.command("settings");
195
+ workspaceSettings.command("read").requiredOption("--workspace-id <workspaceId>").action(async (options) => {
196
+ exitCode = await run(client => client.GET("/v1/workspaces/{workspaceId}/settings", { params: { path: options } }));
197
+ });
198
+ workspaceSettings.command("update").requiredOption("--workspace-id <workspaceId>").requiredOption("--spec-language <language>", "Workspace keyword language", (value) => z.enum(["zh-CN", "en"]).parse(value)).requiredOption("--expected-version <version>", "Observed Workspace settings version", parseOpaqueVersion).requiredOption("--idempotency-key <key>").action(async (options) => {
199
+ if (hasWorkspaceReadToken(dependencies.environment)) {
200
+ exitCode = rejectTokenWrite(dependencies);
201
+ return;
202
+ }
203
+ exitCode = await run(client => client.PATCH("/v1/workspaces/{workspaceId}/settings", { params: { path: { workspaceId: options.workspaceId }, header: { "idempotency-key": options.idempotencyKey, "if-match": options.expectedVersion } }, body: { specLanguage: options.specLanguage } }), { retryTransportOnce: true });
204
+ });
205
+ const workspaceMember = workspace.command("member");
206
+ workspaceMember
207
+ .command("list")
208
+ .requiredOption("--workspace-id <workspaceId>")
209
+ .action(async (options) => {
210
+ exitCode = await run((client) => client.GET("/v1/workspaces/{workspaceId}/members", {
211
+ params: { path: { workspaceId: options.workspaceId } },
212
+ }), { rejectWorkspaceReadToken: true });
213
+ });
214
+ workspaceMember
215
+ .command("search")
216
+ .requiredOption("--workspace-id <workspaceId>")
217
+ .requiredOption("--query <query>")
218
+ .option("--limit <limit>", "Maximum directory candidates", parseSearchLimit, 10)
219
+ .action(async (options) => {
220
+ exitCode = await run((client) => client.GET("/v1/workspaces/{workspaceId}/members/directory", {
221
+ params: {
222
+ path: { workspaceId: options.workspaceId },
223
+ query: { limit: options.limit, query: options.query },
224
+ },
225
+ }), { rejectWorkspaceReadToken: true });
226
+ });
227
+ workspaceMember
228
+ .command("add")
229
+ .requiredOption("--workspace-id <workspaceId>")
230
+ .requiredOption("--issuer <issuer>", "Authentik issuer URL from `workspace member search`")
231
+ .requiredOption("--subject <subject>", "Authentik subject from `workspace member search`")
232
+ .requiredOption("--role <role>", "Workspace Role name")
233
+ .action(async (options) => {
234
+ if (hasWorkspaceReadToken(dependencies.environment)) {
235
+ exitCode = rejectTokenWrite(dependencies);
236
+ return;
237
+ }
238
+ const idempotencyKey = randomUUID();
239
+ exitCode = await run((client) => client.POST("/v1/workspaces/{workspaceId}/members", {
240
+ body: {
241
+ issuer: options.issuer,
242
+ role: options.role,
243
+ subject: options.subject,
244
+ },
245
+ params: {
246
+ header: { "idempotency-key": idempotencyKey },
247
+ path: { workspaceId: options.workspaceId },
248
+ },
249
+ }), { retryTransportOnce: true });
250
+ });
251
+ workspaceMember
252
+ .command("set-role")
253
+ .requiredOption("--workspace-id <workspaceId>")
254
+ .requiredOption("--user-id <userId>", "Member User ID from `workspace member list`")
255
+ .requiredOption("--role <role>", "Workspace Role name")
256
+ .requiredOption("--expected-version <version>", "Opaque Member version from `workspace member list`", parseOpaqueVersion)
257
+ .action(async (options) => {
258
+ if (hasWorkspaceReadToken(dependencies.environment)) {
259
+ exitCode = rejectTokenWrite(dependencies);
260
+ return;
261
+ }
262
+ const idempotencyKey = randomUUID();
263
+ exitCode = await run((client) => client.PATCH("/v1/workspaces/{workspaceId}/members/{userId}", {
264
+ body: { role: options.role },
265
+ params: {
266
+ header: {
267
+ "idempotency-key": idempotencyKey,
268
+ "if-match": options.expectedVersion,
269
+ },
270
+ path: {
271
+ userId: options.userId,
272
+ workspaceId: options.workspaceId,
273
+ },
274
+ },
275
+ }), {
276
+ ambiguousOutcomeMessage: "Member Role change outcome is unknown; read `workspace member list` before retrying",
277
+ retryTransportOnce: true,
278
+ });
279
+ });
280
+ workspaceMember
281
+ .command("remove")
282
+ .requiredOption("--workspace-id <workspaceId>")
283
+ .requiredOption("--user-id <userId>", "Member User ID from `workspace member list`")
284
+ .requiredOption("--expected-version <version>", "Opaque Member version from `workspace member list`", parseOpaqueVersion)
285
+ .action(async (options) => {
286
+ if (hasWorkspaceReadToken(dependencies.environment)) {
287
+ exitCode = rejectTokenWrite(dependencies);
288
+ return;
289
+ }
290
+ const idempotencyKey = randomUUID();
291
+ exitCode = await run((client) => client.DELETE("/v1/workspaces/{workspaceId}/members/{userId}", {
292
+ params: {
293
+ header: {
294
+ "idempotency-key": idempotencyKey,
295
+ "if-match": options.expectedVersion,
296
+ },
297
+ path: {
298
+ userId: options.userId,
299
+ workspaceId: options.workspaceId,
300
+ },
301
+ },
302
+ }), {
303
+ ambiguousOutcomeMessage: "Member removal outcome is unknown; read `workspace member list` before retrying",
304
+ retryTransportOnce: true,
305
+ });
306
+ });
307
+ const workspaceRole = workspace.command("role");
308
+ workspaceRole
309
+ .command("list")
310
+ .requiredOption("--workspace-id <workspaceId>")
311
+ .action(async (options) => {
312
+ exitCode = await run((client) => client.GET("/v1/workspaces/{workspaceId}/roles", {
313
+ params: { path: { workspaceId: options.workspaceId } },
314
+ }), { rejectWorkspaceReadToken: true });
315
+ });
316
+ workspaceRole
317
+ .command("create")
318
+ .requiredOption("--workspace-id <workspaceId>")
319
+ .requiredOption("--name <name>", "Custom Workspace Role name")
320
+ .requiredOption("--statements <statements>", "JSON object mapping permission resources to granted actions", parseRoleStatements)
321
+ .action(async (options) => {
322
+ if (hasWorkspaceReadToken(dependencies.environment)) {
323
+ exitCode = rejectTokenWrite(dependencies);
324
+ return;
325
+ }
326
+ const idempotencyKey = randomUUID();
327
+ exitCode = await run((client) => client.POST("/v1/workspaces/{workspaceId}/roles", {
328
+ body: { name: options.name, statements: options.statements },
329
+ params: {
330
+ header: { "idempotency-key": idempotencyKey },
331
+ path: { workspaceId: options.workspaceId },
332
+ },
333
+ }), { retryTransportOnce: true });
334
+ });
335
+ workspaceRole
336
+ .command("update")
337
+ .requiredOption("--workspace-id <workspaceId>")
338
+ .requiredOption("--name <name>", "Workspace Role name from `workspace role list`")
339
+ .requiredOption("--statements <statements>", "JSON object mapping permission resources to granted actions", parseRoleStatements)
340
+ .requiredOption("--expected-version <version>", "Opaque Role version from `workspace role list`", parseOpaqueVersion)
341
+ .action(async (options) => {
342
+ if (hasWorkspaceReadToken(dependencies.environment)) {
343
+ exitCode = rejectTokenWrite(dependencies);
344
+ return;
345
+ }
346
+ const idempotencyKey = randomUUID();
347
+ exitCode = await run((client) => client.PUT("/v1/workspaces/{workspaceId}/roles/{roleName}", {
348
+ body: { statements: options.statements },
349
+ params: {
350
+ header: {
351
+ "idempotency-key": idempotencyKey,
352
+ "if-match": options.expectedVersion,
353
+ },
354
+ path: {
355
+ roleName: options.name,
356
+ workspaceId: options.workspaceId,
357
+ },
358
+ },
359
+ }), {
360
+ ambiguousOutcomeMessage: "Role update outcome is unknown; read `workspace role list` before retrying",
361
+ retryTransportOnce: true,
362
+ });
363
+ });
364
+ workspaceRole
365
+ .command("delete")
366
+ .requiredOption("--workspace-id <workspaceId>")
367
+ .requiredOption("--name <name>", "Custom Workspace Role name")
368
+ .requiredOption("--expected-version <version>", "Opaque Role version from `workspace role list`", parseOpaqueVersion)
369
+ .action(async (options) => {
370
+ if (hasWorkspaceReadToken(dependencies.environment)) {
371
+ exitCode = rejectTokenWrite(dependencies);
372
+ return;
373
+ }
374
+ const idempotencyKey = randomUUID();
375
+ exitCode = await run((client) => client.DELETE("/v1/workspaces/{workspaceId}/roles/{roleName}", {
376
+ params: {
377
+ header: {
378
+ "idempotency-key": idempotencyKey,
379
+ "if-match": options.expectedVersion,
380
+ },
381
+ path: {
382
+ roleName: options.name,
383
+ workspaceId: options.workspaceId,
384
+ },
385
+ },
386
+ }), {
387
+ ambiguousOutcomeMessage: "Role deletion outcome is unknown; read `workspace role list` before retrying",
388
+ retryTransportOnce: true,
389
+ });
390
+ });
391
+ const project = program.command("project");
392
+ project
393
+ .command("list")
394
+ .option("--status <status>", "Filter by Project lifecycle status", parseProjectStatus)
395
+ .option("--workspace-id <workspaceId>", "Filter by owning Workspace ID from `workspace list`")
396
+ .action(async (options) => {
397
+ exitCode = await run((client) => client.GET("/v1/projects", {
398
+ params: {
399
+ query: {
400
+ ...(options.status === undefined
401
+ ? {}
402
+ : { status: options.status }),
403
+ ...(options.workspaceId === undefined
404
+ ? {}
405
+ : { workspaceId: options.workspaceId }),
406
+ },
407
+ },
408
+ }), { rejectWorkspaceReadToken: true });
409
+ });
410
+ project
411
+ .command("create")
412
+ .requiredOption("--name <name>")
413
+ .requiredOption("--workspace-id <workspaceId>", "Workspace ID returned by `workspace list` or `workspace create`")
414
+ .action(async (options) => {
415
+ if (hasWorkspaceReadToken(dependencies.environment)) {
416
+ exitCode = rejectTokenWrite(dependencies);
417
+ return;
418
+ }
419
+ const idempotencyKey = randomUUID();
420
+ exitCode = await run((client) => client.POST("/v1/projects", {
421
+ body: {
422
+ name: options.name,
423
+ workspaceId: options.workspaceId,
424
+ },
425
+ params: { header: { "idempotency-key": idempotencyKey } },
426
+ }), { retryTransportOnce: true });
427
+ });
428
+ for (const operation of ["archive", "restore", "trash"]) {
429
+ project
430
+ .command(operation)
431
+ .requiredOption("--project-id <projectId>")
432
+ .requiredOption("--expected-version <version>", "Opaque Project version", parseOpaqueVersion)
433
+ .action(async (options) => {
434
+ const idempotencyKey = randomUUID();
435
+ exitCode = await run((client) => client.POST(`/v1/projects/{projectId}/${operation}`, {
436
+ params: {
437
+ header: {
438
+ "idempotency-key": idempotencyKey,
439
+ "if-match": options.expectedVersion,
440
+ },
441
+ path: { projectId: options.projectId },
442
+ },
443
+ }), {
444
+ ambiguousOutcomeMessage: `Project ${operation} outcome is unknown; read Project context before retrying`,
445
+ rejectWorkspaceReadToken: true,
446
+ retryTransportOnce: true,
447
+ });
448
+ });
449
+ }
450
+ project
451
+ .command("context")
452
+ .requiredOption("--project-id <projectId>")
453
+ .action(async (options) => {
454
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/context", {
455
+ params: { path: { projectId: options.projectId } },
456
+ }));
457
+ });
458
+ const sharing = project.command("sharing").description("Manage anonymous task-only Project sharing");
459
+ sharing.command("read")
460
+ .requiredOption("--project-id <projectId>")
461
+ .action(async (options) => {
462
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/sharing", {
463
+ params: { path: { projectId: options.projectId } },
464
+ }), { rejectWorkspaceReadToken: true });
465
+ });
466
+ sharing.command("set")
467
+ .requiredOption("--project-id <projectId>")
468
+ .requiredOption("--public <boolean>", "true or false", (value) => {
469
+ if (value !== "true" && value !== "false")
470
+ throw new InvalidArgumentError("--public must be true or false");
471
+ return value === "true";
472
+ })
473
+ .requiredOption("--expected-version <version>", "Opaque sharing version from project sharing read", parseOpaqueVersion)
474
+ .requiredOption("--idempotency-key <key>", "UUID; reuse the same key and intent after an uncertain response", (value) => {
475
+ if (!z.uuid().safeParse(value).success)
476
+ throw new InvalidArgumentError("The sharing command key must be a UUID");
477
+ return value;
478
+ })
479
+ .action(async (options) => {
480
+ if (hasWorkspaceReadToken(dependencies.environment)) {
481
+ exitCode = rejectTokenWrite(dependencies);
482
+ return;
483
+ }
484
+ exitCode = await run((client) => client.PUT("/v1/projects/{projectId}/sharing", {
485
+ body: { isPublic: options.public },
486
+ params: { path: { projectId: options.projectId }, header: { "if-match": options.expectedVersion, "idempotency-key": options.idempotencyKey } },
487
+ }), { rejectWorkspaceReadToken: true, retryTransportOnce: true, ambiguousOutcomeMessage: "Sharing outcome is unknown; reread project sharing and retry the same command key and intent" });
488
+ });
489
+ const spec = program.command("spec");
490
+ spec.command("list").requiredOption("--workspace-id <workspaceId>").option("--project-id <projectId>").option("--query <query>").option("--limit <limit>", "Maximum results", parseSearchLimit, 50).option("--cursor <cursor>").action(async (options) => {
491
+ exitCode = await run(client => client.GET("/v1/workspaces/{workspaceId}/spec-directory", { params: { path: { workspaceId: options.workspaceId }, query: { ...(options.projectId === undefined ? {} : { projectId: options.projectId }), ...(options.query === undefined ? {} : { query: options.query }), limit: options.limit, ...(options.cursor === undefined ? {} : { cursor: options.cursor }) } } }), { rejectWorkspaceReadToken: true });
492
+ });
493
+ spec
494
+ .command("read")
495
+ .requiredOption("--workspace-id <workspaceId>")
496
+ .requiredOption("--spec-id <specId>")
497
+ .action(async (options) => {
498
+ exitCode = await run((client) => client.GET("/v1/workspaces/{workspaceId}/specs/{specId}", {
499
+ params: { path: options },
500
+ }), { rejectWorkspaceReadToken: true });
501
+ });
502
+ spec
503
+ .command("changelog")
504
+ .requiredOption("--workspace-id <workspaceId>")
505
+ .requiredOption("--spec-id <specId>")
506
+ .action(async (options) => {
507
+ exitCode = await run((client) => client.GET("/v1/workspaces/{workspaceId}/specs/{specId}/changelog", { params: { path: options } }), { rejectWorkspaceReadToken: true });
508
+ });
509
+ spec
510
+ .command("search")
511
+ .requiredOption("--workspace-id <workspaceId>")
512
+ .requiredOption("--query <query>")
513
+ .option("--limit <limit>", "Maximum results", parseSearchLimit, 20)
514
+ .option("--cursor <cursor>")
515
+ .action(async (options) => {
516
+ exitCode = await run((client) => client.GET("/v1/workspaces/{workspaceId}/spec-search", {
517
+ params: {
518
+ path: { workspaceId: options.workspaceId },
519
+ query: {
520
+ ...(options.cursor === undefined
521
+ ? {}
522
+ : { cursor: options.cursor }),
523
+ limit: options.limit,
524
+ query: options.query,
525
+ },
526
+ },
527
+ }));
528
+ });
529
+ spec
530
+ .command("current")
531
+ .requiredOption("--workspace-id <workspaceId>")
532
+ .requiredOption("--spec-id <specId>")
533
+ .action(async (options) => {
534
+ exitCode = await run((client) => client.GET(hasWorkspaceReadToken(dependencies.environment) ? "/v1/workspaces/{workspaceId}/published/specs/{specId}/current-revision" : "/v1/workspaces/{workspaceId}/specs/{specId}/current-revision", { params: { path: options } }));
535
+ });
536
+ spec
537
+ .command("revision")
538
+ .requiredOption("--workspace-id <workspaceId>")
539
+ .requiredOption("--spec-id <specId>")
540
+ .requiredOption("--revision-id <revisionId>")
541
+ .action(async (options) => {
542
+ exitCode = await run((client) => client.GET(hasWorkspaceReadToken(dependencies.environment) ? "/v1/workspaces/{workspaceId}/published/specs/{specId}/revisions/{revisionId}" : "/v1/workspaces/{workspaceId}/specs/{specId}/revisions/{revisionId}", { params: { path: options } }));
543
+ });
544
+ spec
545
+ .command("edit")
546
+ .requiredOption("--workspace-id <workspaceId>")
547
+ .requiredOption("--spec-id <specId>")
548
+ .requiredOption("--content <content>")
549
+ .option("--agent-revision <agentRevision>")
550
+ .option("--agent-model <agentModel>")
551
+ .option("--source-reference-ids <sourceReferenceIds>")
552
+ .action(async (options) => {
553
+ if (hasWorkspaceReadToken(dependencies.environment)) {
554
+ exitCode = rejectTokenWrite(dependencies);
555
+ return;
556
+ }
557
+ exitCode = await run((client) => client.PUT("/v1/workspaces/{workspaceId}/specs/{specId}/draft", {
558
+ body: {
559
+ content: options.content,
560
+ provenance: toAgentProvenance(options),
561
+ },
562
+ params: { path: options },
563
+ }));
564
+ });
565
+ spec
566
+ .command("assign-scenario-ids")
567
+ .requiredOption("--workspace-id <workspaceId>")
568
+ .requiredOption("--spec-id <specId>")
569
+ .option("--agent-revision <agentRevision>")
570
+ .option("--agent-model <agentModel>")
571
+ .action(async (options) => {
572
+ if (hasWorkspaceReadToken(dependencies.environment)) {
573
+ exitCode = rejectTokenWrite(dependencies);
574
+ return;
575
+ }
576
+ const idempotencyKey = randomUUID();
577
+ exitCode = await run((client) => client.POST("/v1/workspaces/{workspaceId}/specs/{specId}/draft/scenario-ids", {
578
+ body: { provenance: toAgentProvenance(options) },
579
+ params: {
580
+ header: { "idempotency-key": idempotencyKey },
581
+ path: options,
582
+ },
583
+ }), {
584
+ ambiguousOutcomeMessage: "Scenario ID assignment outcome is unknown; inspect the Working Draft before retrying",
585
+ retryTransportOnce: true,
586
+ });
587
+ });
588
+ spec
589
+ .command("publish")
590
+ .requiredOption("--workspace-id <workspaceId>")
591
+ .requiredOption("--spec-id <specId>")
592
+ .action(async (options) => {
593
+ if (hasWorkspaceReadToken(dependencies.environment)) {
594
+ exitCode = rejectTokenWrite(dependencies);
595
+ return;
596
+ }
597
+ const idempotencyKey = randomUUID();
598
+ exitCode = await run((client) => client.POST("/v1/workspaces/{workspaceId}/specs/{specId}/publish", {
599
+ params: {
600
+ header: { "idempotency-key": idempotencyKey },
601
+ path: options,
602
+ },
603
+ }), {
604
+ ambiguousOutcomeMessage: "Spec Publish outcome is unknown; inspect the Spec Changelog before retrying",
605
+ retryTransportOnce: true,
606
+ });
607
+ });
608
+ spec
609
+ .command("create")
610
+ .requiredOption("--workspace-id <workspaceId>")
611
+ .option("--capability-id <capabilityId>")
612
+ .requiredOption("--feature-name <featureName>")
613
+ .option("--source-references <sourceReferences>")
614
+ .action(async (options) => {
615
+ if (hasWorkspaceReadToken(dependencies.environment)) {
616
+ exitCode = rejectTokenWrite(dependencies);
617
+ return;
618
+ }
619
+ const idempotencyKey = randomUUID();
620
+ const sourceReferences = parseSourceReferences(options.sourceReferences ?? "[]");
621
+ exitCode = await run((client) => client.POST("/v1/workspaces/{workspaceId}/specs", {
622
+ body: {
623
+ capabilityId: options.capabilityId ?? null,
624
+ featureName: options.featureName,
625
+ sourceReferences,
626
+ },
627
+ params: {
628
+ header: { "idempotency-key": idempotencyKey },
629
+ path: { workspaceId: options.workspaceId },
630
+ },
631
+ }), {
632
+ ambiguousOutcomeMessage: "Spec Creation outcome is unknown; inspect the Project directory before retrying",
633
+ retryTransportOnce: true,
634
+ });
635
+ });
636
+ spec
637
+ .command("update")
638
+ .requiredOption("--workspace-id <workspaceId>")
639
+ .requiredOption("--spec-id <specId>")
640
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
641
+ .option("--capability-id <capabilityId>")
642
+ .option("--source-references <sourceReferences>")
643
+ .action(async (options) => {
644
+ if (hasWorkspaceReadToken(dependencies.environment)) {
645
+ exitCode = rejectTokenWrite(dependencies);
646
+ return;
647
+ }
648
+ if (options.capabilityId === undefined &&
649
+ options.sourceReferences === undefined) {
650
+ throw invalidArgument("Spec update requires Metadata to change");
651
+ }
652
+ const sourceReferences = options.sourceReferences === undefined
653
+ ? undefined
654
+ : parseSourceReferences(options.sourceReferences);
655
+ exitCode = await run((client) => client.PATCH("/v1/workspaces/{workspaceId}/specs/{specId}", {
656
+ body: {
657
+ ...(options.capabilityId === undefined
658
+ ? {}
659
+ : { capabilityId: options.capabilityId === "none" ? null : options.capabilityId }),
660
+ ...(sourceReferences === undefined
661
+ ? {}
662
+ : { sourceReferences }),
663
+ },
664
+ params: {
665
+ header: { "if-match": options.expectedVersion },
666
+ path: options,
667
+ },
668
+ }));
669
+ });
670
+ spec
671
+ .command("draft")
672
+ .requiredOption("--workspace-id <workspaceId>")
673
+ .requiredOption("--spec-id <specId>")
674
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
675
+ .action(async (options) => {
676
+ if (hasWorkspaceReadToken(dependencies.environment)) {
677
+ exitCode = rejectTokenWrite(dependencies);
678
+ return;
679
+ }
680
+ const idempotencyKey = randomUUID();
681
+ exitCode = await run((client) => client.POST("/v1/workspaces/{workspaceId}/specs/{specId}/draft-session", {
682
+ params: {
683
+ header: {
684
+ "idempotency-key": idempotencyKey,
685
+ "if-match": options.expectedVersion,
686
+ },
687
+ path: options,
688
+ },
689
+ }), {
690
+ ambiguousOutcomeMessage: "Spec Draft Session outcome is unknown; read the Spec before retrying",
691
+ retryTransportOnce: true,
692
+ });
693
+ });
694
+ spec
695
+ .command("state")
696
+ .requiredOption("--workspace-id <workspaceId>")
697
+ .requiredOption("--spec-id <specId>")
698
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
699
+ .requiredOption("--state <state>", "Ready or Unavailable", parseSpecState)
700
+ .option("--unavailability-reason <unavailabilityReason>")
701
+ .action(async (options) => {
702
+ if (hasWorkspaceReadToken(dependencies.environment)) {
703
+ exitCode = rejectTokenWrite(dependencies);
704
+ return;
705
+ }
706
+ if (options.state === "Unavailable" &&
707
+ options.unavailabilityReason === undefined) {
708
+ throw invalidArgument("Unavailable requires --unavailability-reason");
709
+ }
710
+ if (options.state === "Ready" &&
711
+ options.unavailabilityReason !== undefined) {
712
+ throw invalidArgument("Ready does not accept --unavailability-reason");
713
+ }
714
+ exitCode = await run((client) => client.PATCH("/v1/workspaces/{workspaceId}/specs/{specId}/state", {
715
+ body: options.state === "Ready"
716
+ ? { state: "Ready" }
717
+ : {
718
+ state: "Unavailable",
719
+ unavailabilityReason: options.unavailabilityReason ?? "",
720
+ },
721
+ params: {
722
+ header: { "if-match": options.expectedVersion },
723
+ path: options,
724
+ },
725
+ }));
726
+ });
727
+ spec
728
+ .command("rollback")
729
+ .requiredOption("--workspace-id <workspaceId>")
730
+ .requiredOption("--spec-id <specId>")
731
+ .requiredOption("--revision-id <revisionId>")
732
+ .action(async (options) => {
733
+ if (hasWorkspaceReadToken(dependencies.environment)) {
734
+ exitCode = rejectTokenWrite(dependencies);
735
+ return;
736
+ }
737
+ const idempotencyKey = randomUUID();
738
+ exitCode = await run((client) => client.POST("/v1/workspaces/{workspaceId}/specs/{specId}/rollback", {
739
+ body: { revisionId: options.revisionId },
740
+ params: {
741
+ header: { "idempotency-key": idempotencyKey },
742
+ path: options,
743
+ },
744
+ }), {
745
+ ambiguousOutcomeMessage: "Spec Rollback outcome is unknown; inspect the Spec Changelog before retrying",
746
+ retryTransportOnce: true,
747
+ });
748
+ });
749
+ spec
750
+ .command("delete")
751
+ .requiredOption("--workspace-id <workspaceId>")
752
+ .requiredOption("--spec-id <specId>")
753
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
754
+ .action(async (options) => {
755
+ if (hasWorkspaceReadToken(dependencies.environment)) {
756
+ exitCode = rejectTokenWrite(dependencies);
757
+ return;
758
+ }
759
+ exitCode = await run(async (client) => withSuccessData(await client.DELETE("/v1/workspaces/{workspaceId}/specs/{specId}", {
760
+ params: {
761
+ header: { "if-match": options.expectedVersion },
762
+ path: options,
763
+ },
764
+ }), { deleted: { specId: options.specId } }));
765
+ });
766
+ const workspaceGlossary = workspace.command("glossary");
767
+ workspaceGlossary
768
+ .command("revision")
769
+ .requiredOption("--workspace-id <workspaceId>")
770
+ .requiredOption("--revision-id <revisionId>")
771
+ .action(async (options) => {
772
+ exitCode = await run((client) => client.GET(hasWorkspaceReadToken(dependencies.environment) ? "/v1/workspaces/{workspaceId}/published/glossary/revisions/{revisionId}" : "/v1/workspaces/{workspaceId}/glossary/revisions/{revisionId}", { params: { path: options } }));
773
+ });
774
+ workspaceGlossary
775
+ .command("read")
776
+ .requiredOption("--workspace-id <workspaceId>")
777
+ .action(async (options) => {
778
+ exitCode = await run((client) => client.GET("/v1/workspaces/{workspaceId}/glossary", {
779
+ params: { path: { workspaceId: options.workspaceId } },
780
+ }), { rejectWorkspaceReadToken: true });
781
+ });
782
+ workspaceGlossary
783
+ .command("changelog")
784
+ .requiredOption("--workspace-id <workspaceId>")
785
+ .action(async (options) => {
786
+ exitCode = await run((client) => client.GET("/v1/workspaces/{workspaceId}/glossary/changelog", {
787
+ params: { path: { workspaceId: options.workspaceId } },
788
+ }), { rejectWorkspaceReadToken: true });
789
+ });
790
+ workspaceGlossary
791
+ .command("draft")
792
+ .requiredOption("--workspace-id <workspaceId>")
793
+ .action(async (options) => {
794
+ if (hasWorkspaceReadToken(dependencies.environment)) {
795
+ exitCode = rejectTokenWrite(dependencies);
796
+ return;
797
+ }
798
+ const idempotencyKey = randomUUID();
799
+ exitCode = await run((client) => client.POST("/v1/workspaces/{workspaceId}/glossary/draft-session", {
800
+ params: {
801
+ header: { "idempotency-key": idempotencyKey },
802
+ path: { workspaceId: options.workspaceId },
803
+ },
804
+ }), {
805
+ ambiguousOutcomeMessage: "Glossary Draft Session outcome is unknown; read the Glossary before retrying",
806
+ retryTransportOnce: true,
807
+ });
808
+ });
809
+ workspaceGlossary
810
+ .command("edit")
811
+ .requiredOption("--workspace-id <workspaceId>")
812
+ .requiredOption("--entries <entries>")
813
+ .option("--agent-revision <agentRevision>")
814
+ .option("--agent-model <agentModel>")
815
+ .option("--source-reference-ids <sourceReferenceIds>")
816
+ .action(async (options) => {
817
+ if (hasWorkspaceReadToken(dependencies.environment)) {
818
+ exitCode = rejectTokenWrite(dependencies);
819
+ return;
820
+ }
821
+ const entries = parseGlossaryEntries(options.entries);
822
+ exitCode = await run((client) => client.PUT("/v1/workspaces/{workspaceId}/glossary/draft", {
823
+ body: { entries, provenance: toAgentProvenance(options) },
824
+ params: { path: { workspaceId: options.workspaceId } },
825
+ }));
826
+ });
827
+ workspaceGlossary
828
+ .command("publish")
829
+ .requiredOption("--workspace-id <workspaceId>")
830
+ .option("--entry-ids <entryIds...>", "Publish only the selected term changes")
831
+ .action(async (options) => {
832
+ if (hasWorkspaceReadToken(dependencies.environment)) {
833
+ exitCode = rejectTokenWrite(dependencies);
834
+ return;
835
+ }
836
+ const idempotencyKey = randomUUID();
837
+ exitCode = await run((client) => client.POST("/v1/workspaces/{workspaceId}/glossary/publish", {
838
+ body: options.entryIds === undefined ? {} : { entryIds: options.entryIds },
839
+ params: {
840
+ header: { "idempotency-key": idempotencyKey },
841
+ path: { workspaceId: options.workspaceId },
842
+ },
843
+ }), {
844
+ ambiguousOutcomeMessage: "Glossary Publish outcome is unknown; inspect the Glossary Changelog before retrying",
845
+ retryTransportOnce: true,
846
+ });
847
+ });
848
+ workspaceGlossary
849
+ .command("rollback")
850
+ .requiredOption("--workspace-id <workspaceId>")
851
+ .requiredOption("--revision-id <revisionId>")
852
+ .action(async (options) => {
853
+ if (hasWorkspaceReadToken(dependencies.environment)) {
854
+ exitCode = rejectTokenWrite(dependencies);
855
+ return;
856
+ }
857
+ const idempotencyKey = randomUUID();
858
+ exitCode = await run((client) => client.POST("/v1/workspaces/{workspaceId}/glossary/rollback", {
859
+ body: { revisionId: options.revisionId },
860
+ params: {
861
+ header: { "idempotency-key": idempotencyKey },
862
+ path: { workspaceId: options.workspaceId },
863
+ },
864
+ }), {
865
+ ambiguousOutcomeMessage: "Glossary Rollback outcome is unknown; inspect the Glossary Changelog before retrying",
866
+ retryTransportOnce: true,
867
+ });
868
+ });
869
+ workspace.command("context").requiredOption("--workspace-id <workspaceId>").action(async (options) => {
870
+ exitCode = await run((client) => client.GET("/v1/workspaces/{workspaceId}/knowledge-context", { params: { path: { workspaceId: options.workspaceId } } }), { rejectWorkspaceReadToken: true });
871
+ });
872
+ const proposal = program.command("proposal");
873
+ proposal
874
+ .command("create-spec")
875
+ .requiredOption("--workspace-id <workspaceId>")
876
+ .requiredOption("--spec-id <specId>")
877
+ .requiredOption("--baseline-content <baselineContent>")
878
+ .requiredOption("--proposed-content <proposedContent>")
879
+ .option("--agent-revision <agentRevision>")
880
+ .option("--agent-model <agentModel>")
881
+ .option("--source-reference-ids <sourceReferenceIds>")
882
+ .action(async (options) => {
883
+ if (hasWorkspaceReadToken(dependencies.environment)) {
884
+ exitCode = rejectTokenWrite(dependencies);
885
+ return;
886
+ }
887
+ const idempotencyKey = randomUUID();
888
+ exitCode = await run((client) => client.POST("/v1/workspaces/{workspaceId}/draft-proposals", {
889
+ body: {
890
+ baselineContent: options.baselineContent,
891
+ kind: "Spec",
892
+ proposedContent: options.proposedContent,
893
+ provenance: toAgentProvenance(options),
894
+ specId: options.specId,
895
+ },
896
+ params: {
897
+ header: { "idempotency-key": idempotencyKey },
898
+ path: { workspaceId: options.workspaceId },
899
+ },
900
+ }), {
901
+ ambiguousOutcomeMessage: "Draft Proposal Creation outcome is unknown; list open Proposals before retrying",
902
+ retryTransportOnce: true,
903
+ });
904
+ });
905
+ proposal
906
+ .command("create-glossary")
907
+ .requiredOption("--workspace-id <workspaceId>")
908
+ .requiredOption("--baseline-entries <baselineEntries>")
909
+ .requiredOption("--proposed-entries <proposedEntries>")
910
+ .option("--agent-revision <agentRevision>")
911
+ .option("--agent-model <agentModel>")
912
+ .option("--source-reference-ids <sourceReferenceIds>")
913
+ .action(async (options) => {
914
+ if (hasWorkspaceReadToken(dependencies.environment)) {
915
+ exitCode = rejectTokenWrite(dependencies);
916
+ return;
917
+ }
918
+ const idempotencyKey = randomUUID();
919
+ exitCode = await run((client) => client.POST("/v1/workspaces/{workspaceId}/draft-proposals", {
920
+ body: {
921
+ baselineEntries: parseGlossaryEntries(options.baselineEntries),
922
+ kind: "Glossary",
923
+ proposedEntries: parseGlossaryEntries(options.proposedEntries),
924
+ provenance: toAgentProvenance(options),
925
+ },
926
+ params: {
927
+ header: { "idempotency-key": idempotencyKey },
928
+ path: { workspaceId: options.workspaceId },
929
+ },
930
+ }), {
931
+ ambiguousOutcomeMessage: "Draft Proposal Creation outcome is unknown; list open Proposals before retrying",
932
+ retryTransportOnce: true,
933
+ });
934
+ });
935
+ proposal
936
+ .command("list")
937
+ .requiredOption("--workspace-id <workspaceId>")
938
+ .option("--target-kind <targetKind>", "Spec or Glossary", parseProposalTargetKind)
939
+ .option("--spec-id <specId>")
940
+ .option("--status <status>", "Open, Applied, or Discarded", parseProposalStatus)
941
+ .action(async (options) => {
942
+ if ((options.targetKind === "Spec" && options.specId === undefined) ||
943
+ (options.targetKind !== "Spec" && options.specId !== undefined)) {
944
+ throw invalidArgument("--spec-id requires --target-kind Spec");
945
+ }
946
+ exitCode = await run((client) => client.GET("/v1/workspaces/{workspaceId}/draft-proposals", {
947
+ params: {
948
+ path: { workspaceId: options.workspaceId },
949
+ query: {
950
+ ...(options.specId === undefined ? {} : { specId: options.specId }),
951
+ ...(options.status === undefined ? {} : { status: options.status }),
952
+ ...(options.targetKind === undefined
953
+ ? {}
954
+ : { targetKind: options.targetKind }),
955
+ },
956
+ },
957
+ }));
958
+ });
959
+ proposal
960
+ .command("apply")
961
+ .requiredOption("--workspace-id <workspaceId>")
962
+ .requiredOption("--proposal-id <proposalId>")
963
+ .option("--change-ids <changeIds>")
964
+ .action(async (options) => {
965
+ if (hasWorkspaceReadToken(dependencies.environment)) {
966
+ exitCode = rejectTokenWrite(dependencies);
967
+ return;
968
+ }
969
+ const idempotencyKey = randomUUID();
970
+ exitCode = await run((client) => client.POST("/v1/workspaces/{workspaceId}/draft-proposals/{proposalId}/apply", {
971
+ body: {
972
+ changeIds: options.changeIds === undefined
973
+ ? null
974
+ : parseStringArray(options.changeIds, "change IDs"),
975
+ },
976
+ params: {
977
+ header: { "idempotency-key": idempotencyKey },
978
+ path: {
979
+ workspaceId: options.workspaceId,
980
+ proposalId: options.proposalId,
981
+ },
982
+ },
983
+ }), {
984
+ ambiguousOutcomeMessage: "Draft Proposal Apply outcome is unknown; reread the Proposal and Working Draft",
985
+ retryTransportOnce: true,
986
+ });
987
+ });
988
+ proposal
989
+ .command("discard")
990
+ .requiredOption("--workspace-id <workspaceId>")
991
+ .requiredOption("--proposal-id <proposalId>")
992
+ .action(async (options) => {
993
+ if (hasWorkspaceReadToken(dependencies.environment)) {
994
+ exitCode = rejectTokenWrite(dependencies);
995
+ return;
996
+ }
997
+ const idempotencyKey = randomUUID();
998
+ exitCode = await run((client) => client.POST("/v1/workspaces/{workspaceId}/draft-proposals/{proposalId}/discard", {
999
+ params: {
1000
+ header: { "idempotency-key": idempotencyKey },
1001
+ path: {
1002
+ workspaceId: options.workspaceId,
1003
+ proposalId: options.proposalId,
1004
+ },
1005
+ },
1006
+ }), {
1007
+ ambiguousOutcomeMessage: "Draft Proposal Discard outcome is unknown; reread the Proposal",
1008
+ retryTransportOnce: true,
1009
+ });
1010
+ });
1011
+ program
1012
+ .command("authoring-history")
1013
+ .requiredOption("--workspace-id <workspaceId>")
1014
+ .action(async (options) => {
1015
+ exitCode = await run((client) => client.GET("/v1/workspaces/{workspaceId}/authoring-history", { params: { path: options } }));
1016
+ });
1017
+ const capability = program.command("capability");
1018
+ capability
1019
+ .command("create")
1020
+ .requiredOption("--workspace-id <workspaceId>")
1021
+ .requiredOption("--name <name>")
1022
+ .option("--parent-id <parentId>")
1023
+ .action(async (options) => {
1024
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1025
+ exitCode = rejectTokenWrite(dependencies);
1026
+ return;
1027
+ }
1028
+ const idempotencyKey = randomUUID();
1029
+ exitCode = await run((client) => client.POST("/v1/workspaces/{workspaceId}/capabilities", {
1030
+ body: {
1031
+ name: options.name,
1032
+ parentId: options.parentId ?? null,
1033
+ },
1034
+ params: {
1035
+ header: { "idempotency-key": idempotencyKey },
1036
+ path: { workspaceId: options.workspaceId },
1037
+ },
1038
+ }), {
1039
+ ambiguousOutcomeMessage: "Capability Creation outcome is unknown; inspect the Project directory before retrying",
1040
+ retryTransportOnce: true,
1041
+ });
1042
+ });
1043
+ capability
1044
+ .command("update")
1045
+ .requiredOption("--workspace-id <workspaceId>")
1046
+ .requiredOption("--capability-id <capabilityId>")
1047
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
1048
+ .option("--name <name>")
1049
+ .option("--parent-id <parentId>")
1050
+ .option("--move-to-root")
1051
+ .action(async (options) => {
1052
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1053
+ exitCode = rejectTokenWrite(dependencies);
1054
+ return;
1055
+ }
1056
+ if (options.parentId !== undefined && options.moveToRoot === true) {
1057
+ throw invalidArgument("Capability update cannot combine --parent-id and --move-to-root");
1058
+ }
1059
+ if (options.name === undefined &&
1060
+ options.parentId === undefined &&
1061
+ options.moveToRoot !== true) {
1062
+ throw invalidArgument("Capability update requires a change");
1063
+ }
1064
+ exitCode = await run((client) => client.PATCH("/v1/workspaces/{workspaceId}/capabilities/{capabilityId}", {
1065
+ body: {
1066
+ ...(options.name === undefined ? {} : { name: options.name }),
1067
+ ...(options.moveToRoot === true
1068
+ ? { parentId: null }
1069
+ : options.parentId === undefined
1070
+ ? {}
1071
+ : { parentId: options.parentId }),
1072
+ },
1073
+ params: {
1074
+ header: { "if-match": options.expectedVersion },
1075
+ path: options,
1076
+ },
1077
+ }));
1078
+ });
1079
+ capability
1080
+ .command("delete")
1081
+ .option("--idempotency-key <key>", "Reuse this key when retrying the same deletion")
1082
+ .requiredOption("--workspace-id <workspaceId>")
1083
+ .requiredOption("--capability-id <capabilityId>")
1084
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
1085
+ .action(async (options) => {
1086
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1087
+ exitCode = rejectTokenWrite(dependencies);
1088
+ return;
1089
+ }
1090
+ const idempotencyKey = options.idempotencyKey ?? randomUUID();
1091
+ exitCode = await run(async (client) => withSuccessData(await client.DELETE("/v1/workspaces/{workspaceId}/capabilities/{capabilityId}", {
1092
+ params: {
1093
+ header: { "if-match": options.expectedVersion, "idempotency-key": idempotencyKey },
1094
+ path: options,
1095
+ },
1096
+ }), { deleted: { capabilityId: options.capabilityId } }));
1097
+ });
1098
+ const workspaceFinding = workspace.command("finding");
1099
+ workspaceFinding
1100
+ .command("list")
1101
+ .requiredOption("--workspace-id <workspaceId>")
1102
+ .option("--spec-id <specId>")
1103
+ .option("--status <status>", "Finding status", parseFindingStatus)
1104
+ .option("--limit <limit>", "Maximum results", parseSearchLimit, 20)
1105
+ .option("--cursor <cursor>")
1106
+ .action(async (options) => {
1107
+ exitCode = await run(client => options.specId === undefined
1108
+ ? client.GET("/v1/workspaces/{workspaceId}/findings", { params: { path: { workspaceId: options.workspaceId }, query: { limit: options.limit, ...(options.cursor === undefined ? {} : { cursor: options.cursor }), ...(options.status === undefined ? {} : { status: options.status }) } } })
1109
+ : client.GET("/v1/workspaces/{workspaceId}/specs/{specId}/findings", { params: { path: { workspaceId: options.workspaceId, specId: options.specId }, query: { limit: options.limit, ...(options.cursor === undefined ? {} : { cursor: options.cursor }), ...(options.status === undefined ? {} : { status: options.status }) } } }), { rejectWorkspaceReadToken: true });
1110
+ });
1111
+ workspaceFinding
1112
+ .command("read")
1113
+ .requiredOption("--workspace-id <workspaceId>")
1114
+ .requiredOption("--finding-id <findingId>")
1115
+ .action(async (options) => {
1116
+ exitCode = await run((client) => client.GET("/v1/workspaces/{workspaceId}/findings/{findingId}", {
1117
+ params: { path: options },
1118
+ }), { rejectWorkspaceReadToken: true });
1119
+ });
1120
+ workspaceFinding
1121
+ .command("create")
1122
+ .requiredOption("--workspace-id <workspaceId>")
1123
+ .requiredOption("--input <input>")
1124
+ .action(async (options) => {
1125
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1126
+ exitCode = rejectTokenWrite(dependencies);
1127
+ return;
1128
+ }
1129
+ const idempotencyKey = randomUUID();
1130
+ const input = parseFindingInput(options.input);
1131
+ exitCode = await run((client) => client.POST("/v1/workspaces/{workspaceId}/findings", {
1132
+ body: input,
1133
+ params: {
1134
+ header: { "idempotency-key": idempotencyKey },
1135
+ path: { workspaceId: options.workspaceId },
1136
+ },
1137
+ }), {
1138
+ ambiguousOutcomeMessage: "Finding Creation outcome is unknown; inspect the Finding list before retrying",
1139
+ retryTransportOnce: true,
1140
+ });
1141
+ });
1142
+ for (const [commandName, status] of [
1143
+ ["confirm", "Confirmed"],
1144
+ ["dismiss", "Dismissed"],
1145
+ ["resolve", "Resolved"],
1146
+ ]) {
1147
+ workspaceFinding
1148
+ .command(commandName)
1149
+ .requiredOption("--workspace-id <workspaceId>")
1150
+ .requiredOption("--finding-id <findingId>")
1151
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
1152
+ .action(async (options) => {
1153
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1154
+ exitCode = rejectTokenWrite(dependencies);
1155
+ return;
1156
+ }
1157
+ const idempotencyKey = randomUUID();
1158
+ exitCode = await run((client) => client.PATCH("/v1/workspaces/{workspaceId}/findings/{findingId}/status", {
1159
+ body: { status },
1160
+ params: {
1161
+ header: {
1162
+ "idempotency-key": idempotencyKey,
1163
+ "if-match": options.expectedVersion,
1164
+ },
1165
+ path: options,
1166
+ },
1167
+ }), {
1168
+ ambiguousOutcomeMessage: "Finding disposition outcome is unknown; read the Finding before retrying",
1169
+ retryTransportOnce: true,
1170
+ });
1171
+ });
1172
+ }
1173
+ workspaceFinding.command("handler").requiredOption("--workspace-id <workspaceId>").requiredOption("--finding-id <findingId>").option("--user-id <userId>", "Workspace member; omit to clear the handler").requiredOption("--expected-version <expectedVersion>", "Opaque version", parseOpaqueVersion).requiredOption("--idempotency-key <key>").action(async (options) => {
1174
+ exitCode = await run(client => client.PATCH("/v1/workspaces/{workspaceId}/findings/{findingId}/handler", { body: { handlerUserId: options.userId ?? null }, params: { path: options, header: { "idempotency-key": options.idempotencyKey, "if-match": options.expectedVersion } } }), { rejectWorkspaceReadToken: true });
1175
+ });
1176
+ workspaceFinding.command("follow-up").requiredOption("--workspace-id <workspaceId>").requiredOption("--finding-id <findingId>").requiredOption("--project-id <projectId>").requiredOption("--idempotency-key <key>").action(async (options) => {
1177
+ exitCode = await run(client => client.POST("/v1/workspaces/{workspaceId}/findings/{findingId}/work-item", { body: { projectId: options.projectId }, params: { path: { workspaceId: options.workspaceId, findingId: options.findingId }, header: { "idempotency-key": options.idempotencyKey } } }), { rejectWorkspaceReadToken: true });
1178
+ });
1179
+ workspaceFinding.command("discussion").requiredOption("--workspace-id <workspaceId>").requiredOption("--finding-id <findingId>").action(async (options) => {
1180
+ exitCode = await run(client => client.GET("/v1/workspaces/{workspaceId}/findings/{findingId}/discussion", { params: { path: options } }), { rejectWorkspaceReadToken: true });
1181
+ });
1182
+ workspaceFinding.command("comment").requiredOption("--workspace-id <workspaceId>").requiredOption("--finding-id <findingId>").requiredOption("--body <body>").option("--parent-id <parentId>").requiredOption("--idempotency-key <key>").action(async (options) => {
1183
+ exitCode = await run(client => client.POST("/v1/workspaces/{workspaceId}/findings/{findingId}/discussion", { body: { body: options.body, parentId: options.parentId ?? null }, params: { path: options, header: { "idempotency-key": options.idempotencyKey } } }), { rejectWorkspaceReadToken: true });
1184
+ });
1185
+ const workspaceReview = workspace.command("review");
1186
+ workspaceReview.command("request").requiredOption("--workspace-id <workspaceId>").requiredOption("--spec-id <specId>").option("--idempotency-key <key>").action(async (options) => {
1187
+ exitCode = await run(client => client.POST("/v1/workspaces/{workspaceId}/specs/{specId}/reviews", { params: { path: options, header: { "idempotency-key": options.idempotencyKey ?? randomUUID() } } }), { rejectWorkspaceReadToken: true });
1188
+ });
1189
+ workspaceReview.command("history").requiredOption("--workspace-id <workspaceId>").action(async (options) => {
1190
+ exitCode = await run(client => client.GET("/v1/workspaces/{workspaceId}/reviews", { params: { path: options } }), { rejectWorkspaceReadToken: true });
1191
+ });
1192
+ workspaceReview.command("read").requiredOption("--workspace-id <workspaceId>").requiredOption("--review-id <reviewId>").action(async (options) => {
1193
+ exitCode = await run(client => client.GET("/v1/workspaces/{workspaceId}/reviews/{reviewId}", { params: { path: options } }), { rejectWorkspaceReadToken: true });
1194
+ });
1195
+ workspaceReview.command("plan").requiredOption("--workspace-id <workspaceId>").action(async (options) => {
1196
+ exitCode = await run(client => client.GET("/v1/workspaces/{workspaceId}/review-schedule", { params: { path: options } }), { rejectWorkspaceReadToken: true });
1197
+ });
1198
+ workspaceReview.command("configure").requiredOption("--workspace-id <workspaceId>").requiredOption("--expected-version <expectedVersion>", "Opaque version", parseOpaqueVersion).requiredOption("--input <input>", "Review plan JSON").action(async (options) => {
1199
+ const body = reviewPlanInputSchema.parse(JSON.parse(options.input));
1200
+ exitCode = await run(client => client.PUT("/v1/workspaces/{workspaceId}/review-schedule", { body, params: { path: options, header: { "if-match": options.expectedVersion } } }), { rejectWorkspaceReadToken: true });
1201
+ });
1202
+ const notification = program.command("notification").description("Read and manage your personal task notifications");
1203
+ notification.command("list").action(async () => {
1204
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1205
+ exitCode = rejectWorkspaceReadTokenOperation(dependencies);
1206
+ return;
1207
+ }
1208
+ exitCode = await run((client) => client.GET("/v1/notifications"));
1209
+ });
1210
+ notification.command("create")
1211
+ .option("--title <title>")
1212
+ .option("--message <message>")
1213
+ .option("--type <type>", "Notification type", "info")
1214
+ .option("--event-data <json>", "JSON object for presentation", parseNotificationEventData)
1215
+ .option("--related-entity-id <id>", "Public task ID or Workspace ID")
1216
+ .option("--related-entity-type <type>", "task or workspace", (value) => { if (value !== "task" && value !== "workspace")
1217
+ throw invalidArgument("Notification target must be task or workspace"); return value; })
1218
+ .requiredOption("--idempotency-key <key>", "Reuse the same command key and inputs when retrying", parseNotificationUuid)
1219
+ .action(async (options) => {
1220
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1221
+ exitCode = rejectWorkspaceReadTokenOperation(dependencies);
1222
+ return;
1223
+ }
1224
+ if ((options.relatedEntityId === undefined) !== (options.relatedEntityType === undefined))
1225
+ throw invalidArgument("Notification target identity and type must be provided together");
1226
+ exitCode = await run((client) => client.POST("/v1/notifications", { body: { type: options.type, ...(options.title === undefined ? {} : { title: options.title }), ...(options.message === undefined ? {} : { message: options.message }), ...(options.eventData === undefined ? {} : { eventData: options.eventData }), ...(options.relatedEntityId === undefined ? {} : { relatedEntityId: options.relatedEntityId }), ...(options.relatedEntityType === undefined ? {} : { relatedEntityType: options.relatedEntityType }) }, params: { header: { "idempotency-key": options.idempotencyKey } } }), { retryTransportOnce: true, ambiguousOutcomeMessage: "Notification creation outcome is unknown; retry with the same inputs and command key" });
1227
+ });
1228
+ notification.command("read")
1229
+ .requiredOption("--notification-id <id>", "Notification UUID returned by the list", parseNotificationUuid)
1230
+ .requiredOption("--idempotency-key <key>", "Reuse the same key when retrying", parseNotificationUuid)
1231
+ .action(async (options) => {
1232
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1233
+ exitCode = rejectWorkspaceReadTokenOperation(dependencies);
1234
+ return;
1235
+ }
1236
+ exitCode = await run((client) => client.PATCH("/v1/notifications/{notificationId}/read", { params: { path: { notificationId: options.notificationId }, header: { "idempotency-key": options.idempotencyKey } } }), { retryTransportOnce: true, ambiguousOutcomeMessage: "Notification read outcome is unknown; retry with the same notification and command key" });
1237
+ });
1238
+ for (const action of ["read-all", "clear"]) {
1239
+ notification.command(action).requiredOption("--idempotency-key <key>", "Reuse the same key to avoid changing notifications that arrived after the first command", parseNotificationUuid)
1240
+ .action(async (options) => {
1241
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1242
+ exitCode = rejectWorkspaceReadTokenOperation(dependencies);
1243
+ return;
1244
+ }
1245
+ const params = { header: { "idempotency-key": options.idempotencyKey } };
1246
+ exitCode = await run((client) => action === "read-all" ? client.PATCH("/v1/notifications/read", { params }) : client.DELETE("/v1/notifications", { params }), { retryTransportOnce: true, ambiguousOutcomeMessage: "Notification bulk command outcome is unknown; retry with the same command key" });
1247
+ });
1248
+ }
1249
+ const notificationPreferences = notification.command("preferences");
1250
+ notificationPreferences.command("get").action(async () => {
1251
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1252
+ exitCode = rejectWorkspaceReadTokenOperation(dependencies);
1253
+ return;
1254
+ }
1255
+ exitCode = await run((client) => client.GET("/v1/notification-preferences"));
1256
+ });
1257
+ notificationPreferences.command("update")
1258
+ .requiredOption("--expected-version <version>", "Opaque preference version returned by get")
1259
+ .requiredOption("--preferences <json>", "JSON category settings; omitted settings remain unchanged", parseNotificationPreferences)
1260
+ .requiredOption("--idempotency-key <key>", "Reuse the same key and settings when retrying", parseNotificationUuid)
1261
+ .action(async (options) => {
1262
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1263
+ exitCode = rejectWorkspaceReadTokenOperation(dependencies);
1264
+ return;
1265
+ }
1266
+ exitCode = await run((client) => client.PATCH("/v1/notification-preferences", { body: options.preferences, params: { header: { "idempotency-key": options.idempotencyKey, "if-match": options.expectedVersion } } }), { retryTransportOnce: true, ambiguousOutcomeMessage: "Notification preference outcome is unknown; retry with the same settings and command key" });
1267
+ });
1268
+ const workItem = program.command("work-item");
1269
+ registerWorkItemAssetCommands(workItem, { dependencies, run, setExitCode: (value) => { exitCode = value; }, rejectWrite: () => rejectTokenWrite(dependencies) });
1270
+ registerProductMapCommands(program, { run, setExitCode: value => { exitCode = value; }, canWrite: () => !hasWorkspaceReadToken(dependencies.environment), rejectWrite: () => rejectTokenWrite(dependencies), parseOpaqueVersion });
1271
+ registerWorkItemResourceLinkCommands(workItem, spec, { run, setExitCode: (value) => { exitCode = value; }, canWrite: () => !hasWorkspaceReadToken(dependencies.environment), rejectWrite: () => rejectTokenWrite(dependencies), parseOpaqueVersion, parseLimit: parseSearchLimit });
1272
+ const timeEntry = workItem.command("time-entry").description("Record and read task time entries");
1273
+ timeEntry.command("list")
1274
+ .requiredOption("--project-id <projectId>")
1275
+ .requiredOption("--work-item-id <workItemId>")
1276
+ .option("--limit <limit>", "Maximum results, 1 through 100", parseSearchLimit, 50)
1277
+ .option("--cursor <cursor>", "Opaque cursor returned by the previous page")
1278
+ .action(async (options) => {
1279
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-items/{workItemId}/time-entries", { params: { path: { projectId: options.projectId, workItemId: options.workItemId }, query: { limit: options.limit, ...(options.cursor === undefined ? {} : { cursor: options.cursor }) } } }));
1280
+ });
1281
+ timeEntry.command("get")
1282
+ .requiredOption("--project-id <projectId>")
1283
+ .requiredOption("--work-item-id <workItemId>")
1284
+ .requiredOption("--entry-id <entryId>", "Time Entry UUID", (value) => { try {
1285
+ return parseTimeEntryUuid(value);
1286
+ }
1287
+ catch {
1288
+ throw invalidArgument("Time Entry ID must be a UUID");
1289
+ } })
1290
+ .action(async (options) => {
1291
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-items/{workItemId}/time-entries/{entryId}", { params: { path: options } }));
1292
+ });
1293
+ for (const action of ["create", "update"]) {
1294
+ const command = timeEntry.command(action)
1295
+ .requiredOption("--project-id <projectId>")
1296
+ .requiredOption("--work-item-id <workItemId>")
1297
+ .requiredOption("--start-time <timestamp>", "ISO date-time with an offset", (value) => { try {
1298
+ return parseTimeEntryTimestamp(value);
1299
+ }
1300
+ catch {
1301
+ throw invalidArgument("Start time must be a valid ISO 8601 timestamp with an offset");
1302
+ } })
1303
+ .option("--end-time <timestamp>", "Omit to start running, or preserve the stored end on update", (value) => { try {
1304
+ return parseTimeEntryTimestamp(value);
1305
+ }
1306
+ catch {
1307
+ throw invalidArgument("End time must be a valid ISO 8601 timestamp with an offset");
1308
+ } })
1309
+ .option("--description <description>", "Optional recorded work description", (value) => { try {
1310
+ return parseTimeEntryDescription(value);
1311
+ }
1312
+ catch {
1313
+ throw invalidArgument("Time Entry description exceeds the allowed limit");
1314
+ } })
1315
+ .requiredOption("--idempotency-key <key>", "Reuse this key and inputs for the same action", (value) => { try {
1316
+ return parseTimeEntryUuid(value);
1317
+ }
1318
+ catch {
1319
+ throw invalidArgument("Command key must be a UUID");
1320
+ } });
1321
+ if (action === "update")
1322
+ command.requiredOption("--entry-id <entryId>", "Time Entry UUID", (value) => { try {
1323
+ return parseTimeEntryUuid(value);
1324
+ }
1325
+ catch {
1326
+ throw invalidArgument("Time Entry ID must be a UUID");
1327
+ } }).requiredOption("--expected-version <version>", "Opaque version returned by SpecFlow", parseOpaqueVersion);
1328
+ command.action(async (options) => {
1329
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1330
+ exitCode = rejectTokenWrite(dependencies);
1331
+ return;
1332
+ }
1333
+ const input = { startTime: options.startTime, ...(options.endTime === undefined ? {} : { endTime: options.endTime }), ...(options.description === undefined ? {} : { description: options.description }) };
1334
+ if (options.endTime !== undefined && Date.parse(options.startTime) > Date.parse(options.endTime))
1335
+ throw invalidArgument("Start time cannot follow end time");
1336
+ const path = { projectId: options.projectId, workItemId: options.workItemId };
1337
+ const header = { "idempotency-key": options.idempotencyKey };
1338
+ exitCode = await run((client) => {
1339
+ if (action === "create")
1340
+ return client.POST("/v1/projects/{projectId}/work-items/{workItemId}/time-entries", { body: input, params: { path, header } });
1341
+ if (options.entryId === undefined || options.expectedVersion === undefined)
1342
+ throw new Error("Time Entry update requires its identity and observed version");
1343
+ return client.PATCH("/v1/projects/{projectId}/work-items/{workItemId}/time-entries/{entryId}", { body: input, params: { path: { ...path, entryId: options.entryId }, header: { ...header, "if-match": options.expectedVersion } } });
1344
+ }, { retryTransportOnce: true, ambiguousOutcomeMessage: "Time Entry outcome is unknown; retry the same inputs and command key" });
1345
+ });
1346
+ }
1347
+ const workItemComment = workItem.command("comment").description("Read and manage authored task comments");
1348
+ workItemComment.command("list")
1349
+ .requiredOption("--project-id <projectId>")
1350
+ .requiredOption("--work-item-id <workItemId>")
1351
+ .option("--limit <limit>", "Maximum results, 1 through 100", parseSearchLimit, 50)
1352
+ .option("--cursor <cursor>", "Opaque cursor returned by the previous page")
1353
+ .action(async (options) => {
1354
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-items/{workItemId}/comments", {
1355
+ params: { path: { projectId: options.projectId, workItemId: options.workItemId }, query: { limit: options.limit, ...(options.cursor === undefined ? {} : { cursor: options.cursor }) } },
1356
+ }));
1357
+ });
1358
+ workItem.command("discussion").description("Read comments and immutable activity newest first")
1359
+ .requiredOption("--project-id <projectId>")
1360
+ .requiredOption("--work-item-id <workItemId>")
1361
+ .option("--limit <limit>", "Maximum results, 1 through 100", parseSearchLimit, 50)
1362
+ .option("--cursor <cursor>", "Opaque cursor returned by the previous page")
1363
+ .action(async (options) => {
1364
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-items/{workItemId}/discussion", {
1365
+ params: { path: { projectId: options.projectId, workItemId: options.workItemId }, query: { limit: options.limit, ...(options.cursor === undefined ? {} : { cursor: options.cursor }) } },
1366
+ }));
1367
+ });
1368
+ workItemComment.command("create")
1369
+ .requiredOption("--project-id <projectId>")
1370
+ .requiredOption("--work-item-id <workItemId>")
1371
+ .requiredOption("--content <markdown>", "Markdown comment body, up to 20000 characters", parseCommentContent)
1372
+ .requiredOption("--idempotency-key <key>", "Reuse the same key and inputs when retrying", parseCommentCommandKey)
1373
+ .action(async (options) => {
1374
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1375
+ exitCode = rejectTokenWrite(dependencies);
1376
+ return;
1377
+ }
1378
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/work-items/{workItemId}/comments", {
1379
+ body: { content: options.content },
1380
+ params: { path: { projectId: options.projectId, workItemId: options.workItemId }, header: { "idempotency-key": options.idempotencyKey } },
1381
+ }), { retryTransportOnce: true, ambiguousOutcomeMessage: "Comment creation outcome is unknown; retry with the same inputs and command key" });
1382
+ });
1383
+ workItemComment.command("update")
1384
+ .requiredOption("--project-id <projectId>")
1385
+ .requiredOption("--work-item-id <workItemId>")
1386
+ .requiredOption("--comment-id <commentId>", "Identifier returned by comments", parseCommentId)
1387
+ .requiredOption("--content <markdown>", "Markdown comment body, up to 20000 characters", parseCommentContent)
1388
+ .requiredOption("--expected-version <version>", "Opaque comment version returned by SpecFlow", parseOpaqueVersion)
1389
+ .requiredOption("--idempotency-key <key>", "Reuse the same key and inputs when retrying", parseCommentCommandKey)
1390
+ .action(async (options) => {
1391
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1392
+ exitCode = rejectTokenWrite(dependencies);
1393
+ return;
1394
+ }
1395
+ exitCode = await run((client) => client.PATCH("/v1/projects/{projectId}/work-items/{workItemId}/comments/{commentId}", {
1396
+ body: { content: options.content },
1397
+ params: { path: { projectId: options.projectId, workItemId: options.workItemId, commentId: options.commentId },
1398
+ header: { "idempotency-key": options.idempotencyKey, "if-match": options.expectedVersion } },
1399
+ }), { retryTransportOnce: true, ambiguousOutcomeMessage: "Comment edit outcome is unknown; reread the comment or retry the same inputs and command key" });
1400
+ });
1401
+ workItemComment.command("delete")
1402
+ .requiredOption("--project-id <projectId>")
1403
+ .requiredOption("--work-item-id <workItemId>")
1404
+ .requiredOption("--comment-id <commentId>", "Identifier returned by comments", parseCommentId)
1405
+ .requiredOption("--expected-version <version>", "Opaque comment version returned by SpecFlow", parseOpaqueVersion)
1406
+ .requiredOption("--idempotency-key <key>", "Reuse the same key and inputs when retrying", parseCommentCommandKey)
1407
+ .action(async (options) => {
1408
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1409
+ exitCode = rejectTokenWrite(dependencies);
1410
+ return;
1411
+ }
1412
+ exitCode = await run((client) => client.DELETE("/v1/projects/{projectId}/work-items/{workItemId}/comments/{commentId}", {
1413
+ params: { path: { projectId: options.projectId, workItemId: options.workItemId, commentId: options.commentId },
1414
+ header: { "idempotency-key": options.idempotencyKey, "if-match": options.expectedVersion } },
1415
+ }), { retryTransportOnce: true, ambiguousOutcomeMessage: "Comment deletion outcome is unknown; reread the comment or retry the same inputs and command key" });
1416
+ });
1417
+ const workItemRelation = workItem.command("relation").description("Manage subtask, blocking and related task links");
1418
+ workItemRelation.command("list")
1419
+ .requiredOption("--project-id <projectId>")
1420
+ .requiredOption("--work-item-id <workItemId>")
1421
+ .action(async (options) => {
1422
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-items/{workItemId}/relations", { params: { path: options } }));
1423
+ });
1424
+ workItemRelation.command("create")
1425
+ .requiredOption("--project-id <projectId>")
1426
+ .requiredOption("--work-item-id <workItemId>")
1427
+ .requiredOption("--target-project-id <projectId>", "Target task's current Project", parseProjectId)
1428
+ .requiredOption("--target-work-item-id <workItemId>", "Target task's public identity", parseWorkItemId)
1429
+ .requiredOption("--type <type>", "subtask, blocks or related", (value) => {
1430
+ const result = z.enum(["subtask", "blocks", "related"]).safeParse(value);
1431
+ if (!result.success)
1432
+ throw invalidArgument("Relation type must be subtask, blocks or related");
1433
+ return result.data;
1434
+ })
1435
+ .requiredOption("--idempotency-key <key>", "Reuse the same key and inputs when retrying", parseRelationCommandKey)
1436
+ .action(async (options) => {
1437
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1438
+ exitCode = rejectTokenWrite(dependencies);
1439
+ return;
1440
+ }
1441
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/work-items/{workItemId}/relations", {
1442
+ body: { targetProjectId: options.targetProjectId, targetWorkItemId: options.targetWorkItemId, relationType: options.type },
1443
+ params: { path: { projectId: options.projectId, workItemId: options.workItemId }, header: { "idempotency-key": options.idempotencyKey } },
1444
+ }), { retryTransportOnce: true, ambiguousOutcomeMessage: "Relation creation outcome is unknown; retry with the same inputs and command key" });
1445
+ });
1446
+ workItemRelation.command("delete")
1447
+ .requiredOption("--project-id <projectId>")
1448
+ .requiredOption("--work-item-id <workItemId>")
1449
+ .requiredOption("--peer-project-id <projectId>", "Other endpoint's current Project", parseProjectId)
1450
+ .requiredOption("--relation-id <relationId>", "Relation identifier returned by list", (value) => {
1451
+ if (!z.uuid().safeParse(value).success)
1452
+ throw invalidArgument("Relation ID must be a UUID returned by SpecFlow");
1453
+ return value;
1454
+ })
1455
+ .requiredOption("--expected-version <expectedVersion>", "Opaque relation version returned by SpecFlow", parseOpaqueVersion)
1456
+ .requiredOption("--idempotency-key <key>", "Reuse the same key and inputs when retrying", parseRelationCommandKey)
1457
+ .action(async (options) => {
1458
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1459
+ exitCode = rejectTokenWrite(dependencies);
1460
+ return;
1461
+ }
1462
+ exitCode = await run((client) => client.DELETE("/v1/projects/{projectId}/work-items/{workItemId}/relations/{relationId}", {
1463
+ params: { path: { projectId: options.projectId, workItemId: options.workItemId, relationId: options.relationId },
1464
+ query: { peerProjectId: options.peerProjectId }, header: { "idempotency-key": options.idempotencyKey, "if-match": options.expectedVersion } },
1465
+ }), { retryTransportOnce: true, ambiguousOutcomeMessage: "Relation removal outcome is unknown; retry with the same inputs and command key" });
1466
+ });
1467
+ workItemSelectionFlags(workItem.command("list"))
1468
+ .requiredOption("--project-id <projectId>")
1469
+ .option("--limit <limit>", "Maximum results", parseSearchLimit, 20)
1470
+ .option("--cursor <cursor>")
1471
+ .option("--group-by <field>", "Group the page by field (status)", (value) => {
1472
+ if (value === "status") {
1473
+ return "status";
1474
+ }
1475
+ throw invalidArgument("Work Items can only be grouped by status");
1476
+ })
1477
+ .action(async (options) => {
1478
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-items", {
1479
+ params: {
1480
+ path: { projectId: options.projectId },
1481
+ query: {
1482
+ ...workItemSelectionQuery(options),
1483
+ ...(options.cursor === undefined
1484
+ ? {}
1485
+ : { cursor: options.cursor }),
1486
+ ...(options.groupBy === undefined
1487
+ ? {}
1488
+ : { groupBy: options.groupBy }),
1489
+ limit: options.limit,
1490
+ },
1491
+ },
1492
+ }));
1493
+ });
1494
+ workItem
1495
+ .command("read")
1496
+ .requiredOption("--project-id <projectId>")
1497
+ .requiredOption("--work-item-id <workItemId>")
1498
+ .action(async (options) => {
1499
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-items/{workItemId}", {
1500
+ params: { path: options },
1501
+ }));
1502
+ });
1503
+ workItem
1504
+ .command("assignees")
1505
+ .requiredOption("--project-id <projectId>")
1506
+ .action(async (options) => {
1507
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-item-assignees", {
1508
+ params: { path: options },
1509
+ }));
1510
+ });
1511
+ workItem
1512
+ .command("create")
1513
+ .requiredOption("--project-id <projectId>")
1514
+ .requiredOption("--title <title>")
1515
+ .option("--description <description>")
1516
+ .option("--label <labelId>", "Workspace label definition ID; repeat for several labels", collectOption, [])
1517
+ .option("--assignee <assignee>", "Workspace member User ID")
1518
+ .option("--custom-field <fieldId=value>", "Custom Field value as fieldId=value; repeat for several fields", collectOption, [])
1519
+ .option("--priority <priority>", "Work Item priority", parseWorkItemPriority)
1520
+ .option("--start-date <startDate>", "ISO 8601 date", parseWorkItemDate)
1521
+ .option("--due-date <dueDate>", "ISO 8601 date", parseWorkItemDate)
1522
+ .option("--status <status>", "Workflow Column slug or virtual status to create the Work Item in")
1523
+ .action(async (options) => {
1524
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1525
+ exitCode = rejectTokenWrite(dependencies);
1526
+ return;
1527
+ }
1528
+ const idempotencyKey = randomUUID();
1529
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/work-items", {
1530
+ body: {
1531
+ ...(options.assignee === undefined
1532
+ ? {}
1533
+ : { assigneeId: options.assignee }),
1534
+ ...(options.customField.length === 0
1535
+ ? {}
1536
+ : {
1537
+ customFields: options.customField.map(parseCustomFieldAssignment),
1538
+ }),
1539
+ ...(options.description === undefined
1540
+ ? {}
1541
+ : { description: options.description }),
1542
+ ...(options.label.length === 0 ? {} : { labelIds: [...new Set(options.label)] }),
1543
+ ...(options.dueDate === undefined
1544
+ ? {}
1545
+ : { dueDate: options.dueDate }),
1546
+ ...(options.priority === undefined
1547
+ ? {}
1548
+ : { priority: options.priority }),
1549
+ ...(options.startDate === undefined
1550
+ ? {}
1551
+ : { startDate: options.startDate }),
1552
+ ...(options.status === undefined
1553
+ ? {}
1554
+ : { status: options.status }),
1555
+ title: options.title,
1556
+ },
1557
+ params: {
1558
+ header: { "idempotency-key": idempotencyKey },
1559
+ path: { projectId: options.projectId },
1560
+ },
1561
+ }), {
1562
+ ambiguousOutcomeMessage: "Work Item creation outcome is unknown; list Work Items before retrying",
1563
+ retryTransportOnce: true,
1564
+ });
1565
+ });
1566
+ workItem
1567
+ .command("update")
1568
+ .requiredOption("--project-id <projectId>")
1569
+ .requiredOption("--work-item-id <workItemId>")
1570
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
1571
+ .option("--title <title>")
1572
+ .option("--description <description>")
1573
+ .option("--clear-description", "Remove the Work Item description")
1574
+ .option("--priority <priority>", "Work Item priority", parseWorkItemPriority)
1575
+ .option("--start-date <startDate>", "ISO 8601 date", parseWorkItemDate)
1576
+ .option("--due-date <dueDate>", "ISO 8601 date", parseWorkItemDate)
1577
+ .option("--clear-start-date", "Remove the Work Item start date")
1578
+ .option("--clear-due-date", "Remove the Work Item due date")
1579
+ .action(async (options) => {
1580
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1581
+ exitCode = rejectTokenWrite(dependencies);
1582
+ return;
1583
+ }
1584
+ if (options.title === undefined &&
1585
+ options.description === undefined &&
1586
+ options.clearDescription !== true &&
1587
+ options.priority === undefined &&
1588
+ options.startDate === undefined &&
1589
+ options.dueDate === undefined &&
1590
+ options.clearStartDate !== true &&
1591
+ options.clearDueDate !== true) {
1592
+ throw invalidArgument("Work Item update requires a field change");
1593
+ }
1594
+ if (options.clearDescription === true &&
1595
+ options.description !== undefined) {
1596
+ throw invalidArgument("Work Item update cannot set and clear the description together");
1597
+ }
1598
+ if (options.clearStartDate === true && options.startDate !== undefined) {
1599
+ throw invalidArgument("Work Item update cannot set and clear the start date together");
1600
+ }
1601
+ if (options.clearDueDate === true && options.dueDate !== undefined) {
1602
+ throw invalidArgument("Work Item update cannot set and clear the due date together");
1603
+ }
1604
+ const idempotencyKey = randomUUID();
1605
+ exitCode = await run((client) => client.PATCH("/v1/projects/{projectId}/work-items/{workItemId}", {
1606
+ body: {
1607
+ ...(options.clearDescription === true
1608
+ ? { description: null }
1609
+ : options.description === undefined
1610
+ ? {}
1611
+ : { description: options.description }),
1612
+ ...(options.clearDueDate === true
1613
+ ? { dueDate: null }
1614
+ : options.dueDate === undefined
1615
+ ? {}
1616
+ : { dueDate: options.dueDate }),
1617
+ ...(options.clearStartDate === true
1618
+ ? { startDate: null }
1619
+ : options.startDate === undefined
1620
+ ? {}
1621
+ : { startDate: options.startDate }),
1622
+ ...(options.priority === undefined
1623
+ ? {}
1624
+ : { priority: options.priority }),
1625
+ ...(options.title === undefined
1626
+ ? {}
1627
+ : { title: options.title }),
1628
+ },
1629
+ params: {
1630
+ header: {
1631
+ "idempotency-key": idempotencyKey,
1632
+ "if-match": options.expectedVersion,
1633
+ },
1634
+ path: {
1635
+ projectId: options.projectId,
1636
+ workItemId: options.workItemId,
1637
+ },
1638
+ },
1639
+ }), {
1640
+ ambiguousOutcomeMessage: "Work Item update outcome is unknown; read the Work Item before retrying",
1641
+ retryTransportOnce: true,
1642
+ });
1643
+ });
1644
+ workItem
1645
+ .command("assign")
1646
+ .requiredOption("--project-id <projectId>")
1647
+ .requiredOption("--work-item-id <workItemId>")
1648
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
1649
+ .option("--assignee <assignee>", "Workspace member User ID")
1650
+ .option("--unassign", "Remove the Work Item assignee")
1651
+ .action(async (options) => {
1652
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1653
+ exitCode = rejectTokenWrite(dependencies);
1654
+ return;
1655
+ }
1656
+ if (options.assignee === undefined && options.unassign !== true) {
1657
+ throw invalidArgument("Work Item assign requires an assignee or --unassign");
1658
+ }
1659
+ if (options.assignee !== undefined && options.unassign === true) {
1660
+ throw invalidArgument("Work Item assign cannot set and clear the assignee together");
1661
+ }
1662
+ const idempotencyKey = randomUUID();
1663
+ exitCode = await run((client) => client.PUT("/v1/projects/{projectId}/work-items/{workItemId}/assignee", {
1664
+ body: {
1665
+ assigneeId: options.unassign === true ? null : (options.assignee ?? null),
1666
+ },
1667
+ params: {
1668
+ header: {
1669
+ "idempotency-key": idempotencyKey,
1670
+ "if-match": options.expectedVersion,
1671
+ },
1672
+ path: {
1673
+ projectId: options.projectId,
1674
+ workItemId: options.workItemId,
1675
+ },
1676
+ },
1677
+ }), {
1678
+ ambiguousOutcomeMessage: "Work Item assignment outcome is unknown; read the Work Item before retrying",
1679
+ retryTransportOnce: true,
1680
+ });
1681
+ });
1682
+ workItem
1683
+ .command("delete")
1684
+ .requiredOption("--project-id <projectId>")
1685
+ .requiredOption("--work-item-id <workItemId>")
1686
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
1687
+ .action(async (options) => {
1688
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1689
+ exitCode = rejectTokenWrite(dependencies);
1690
+ return;
1691
+ }
1692
+ const idempotencyKey = randomUUID();
1693
+ exitCode = await run(async (client) => withSuccessData(await client.DELETE("/v1/projects/{projectId}/work-items/{workItemId}", {
1694
+ params: {
1695
+ header: {
1696
+ "idempotency-key": idempotencyKey,
1697
+ "if-match": options.expectedVersion,
1698
+ },
1699
+ path: {
1700
+ projectId: options.projectId,
1701
+ workItemId: options.workItemId,
1702
+ },
1703
+ },
1704
+ }), { deleted: { workItemId: options.workItemId } }), {
1705
+ ambiguousOutcomeMessage: "Work Item deletion outcome is unknown; list Work Items before retrying",
1706
+ retryTransportOnce: true,
1707
+ });
1708
+ });
1709
+ workItemSelectionFlags(workItem.command("board"))
1710
+ .requiredOption("--project-id <projectId>")
1711
+ .action(async (options) => {
1712
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-item-board", {
1713
+ params: {
1714
+ path: { projectId: options.projectId },
1715
+ query: workItemSelectionQuery(options),
1716
+ },
1717
+ }));
1718
+ });
1719
+ workItem
1720
+ .command("bulk")
1721
+ .requiredOption("--project-id <projectId>")
1722
+ .requiredOption("--action <action>", `Bulk action: ${workItemBulkActions.join(", ")}`, parseWorkItemBulkAction)
1723
+ .requiredOption("--target <[projectId/]id@version>", "Target with its observed version, e.g. PAY-1@3 or project-id/PAY-2@4; repeat within the same Workspace", parseWorkItemBulkTarget, [])
1724
+ .option("--status <status>", "Destination Workflow Column slug or virtual status (updateStatus)")
1725
+ .option("--priority <priority>", "Priority (updatePriority)", parseWorkItemPriority)
1726
+ .option("--assignee <assignee>", "Assignee User ID (updateAssignee)")
1727
+ .option("--unassign", "Clear the assignee (updateAssignee)")
1728
+ .option("--label-id <labelId>", "Work Item Label ID (addLabel/removeLabel)")
1729
+ .option("--due-date <dueDate>", "Due date (updateDueDate)", parseWorkItemDate)
1730
+ .option("--clear-due-date", "Clear the due date (updateDueDate)")
1731
+ .description("Apply one action to several Work Items, which may span Projects inside the same Workspace")
1732
+ .action(async (options) => {
1733
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1734
+ exitCode = rejectTokenWrite(dependencies);
1735
+ return;
1736
+ }
1737
+ if (options.unassign === true && options.assignee !== undefined) {
1738
+ writeSkillError(dependencies, {
1739
+ message: "--assignee and --unassign cannot be combined for updateAssignee",
1740
+ reason: "Invalid Operation",
1741
+ retryable: false,
1742
+ });
1743
+ exitCode = 1;
1744
+ return;
1745
+ }
1746
+ if (options.clearDueDate === true && options.dueDate !== undefined) {
1747
+ writeSkillError(dependencies, {
1748
+ message: "--due-date and --clear-due-date cannot be combined for updateDueDate",
1749
+ reason: "Invalid Operation",
1750
+ retryable: false,
1751
+ });
1752
+ exitCode = 1;
1753
+ return;
1754
+ }
1755
+ const idempotencyKey = randomUUID();
1756
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/work-items/bulk", {
1757
+ body: {
1758
+ action: options.action,
1759
+ ...(options.action === "updateAssignee"
1760
+ ? options.unassign === true
1761
+ ? { assigneeId: null }
1762
+ : options.assignee === undefined
1763
+ ? {}
1764
+ : { assigneeId: options.assignee }
1765
+ : {}),
1766
+ ...(options.action === "updateDueDate"
1767
+ ? options.clearDueDate === true
1768
+ ? { dueDate: null }
1769
+ : options.dueDate === undefined
1770
+ ? {}
1771
+ : { dueDate: options.dueDate }
1772
+ : {}),
1773
+ ...((options.action === "addLabel" ||
1774
+ options.action === "removeLabel") &&
1775
+ options.labelId !== undefined
1776
+ ? { labelId: options.labelId }
1777
+ : {}),
1778
+ ...(options.action === "updatePriority" &&
1779
+ options.priority !== undefined
1780
+ ? { priority: options.priority }
1781
+ : {}),
1782
+ ...(options.action === "updateStatus" &&
1783
+ options.status !== undefined
1784
+ ? { status: options.status }
1785
+ : {}),
1786
+ workItems: options.target.map((target) => ({
1787
+ id: target.id,
1788
+ ...(target.projectId === undefined ? {} : { projectId: target.projectId }),
1789
+ version: target.expectedVersion,
1790
+ })),
1791
+ },
1792
+ params: {
1793
+ header: { "idempotency-key": idempotencyKey },
1794
+ path: { projectId: options.projectId },
1795
+ },
1796
+ }), {
1797
+ ambiguousOutcomeMessage: "Work Item bulk outcome is unknown; list Work Items before retrying",
1798
+ retryTransportOnce: true,
1799
+ });
1800
+ });
1801
+ workItem
1802
+ .command("import")
1803
+ .description("Import JSON tasks into the explicit Project; imported labels are reported as skipped")
1804
+ .requiredOption("--project-id <projectId>")
1805
+ .requiredOption("--file <path>", "JSON task file, at most 1 MiB and 1000 tasks")
1806
+ .requiredOption("--idempotency-key <key>", "Reuse this key with the same file when retrying")
1807
+ .action(async (options) => {
1808
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1809
+ exitCode = rejectTokenWrite(dependencies);
1810
+ return;
1811
+ }
1812
+ const key = options.idempotencyKey.trim();
1813
+ if (key.length === 0 || key.length > 200) {
1814
+ throw invalidArgument("The import idempotency key must contain 1 to 200 characters");
1815
+ }
1816
+ const body = await readWorkItemImportFile(options.file);
1817
+ if (body === null) {
1818
+ writeSkillError(dependencies, {
1819
+ message: "The import file must be valid task JSON within 1 MiB and 1000 tasks",
1820
+ reason: "Invalid Operation", retryable: false,
1821
+ });
1822
+ exitCode = 2;
1823
+ return;
1824
+ }
1825
+ let failed = 0;
1826
+ exitCode = await run(async (client) => {
1827
+ const result = await client.POST("/v1/projects/{projectId}/work-items/import", {
1828
+ body, params: { header: { "idempotency-key": key }, path: { projectId: options.projectId } },
1829
+ });
1830
+ if (result.data !== undefined) {
1831
+ failed = result.data.results.failed;
1832
+ }
1833
+ return result;
1834
+ }, {
1835
+ ambiguousOutcomeMessage: "Import outcome is unknown; retry the same file and idempotency key",
1836
+ retryTransportOnce: true,
1837
+ });
1838
+ if (exitCode === 0 && failed > 0) {
1839
+ writeSkillError(dependencies, {
1840
+ message: "Some tasks were refused; inspect the per-row results and import only corrected failed rows with a new key",
1841
+ reason: "Invalid Operation", retryable: false,
1842
+ });
1843
+ exitCode = 2;
1844
+ }
1845
+ });
1846
+ workItemSelectionFlags(workItem.command("export"))
1847
+ .description("Export selected readable Project tasks as JSON on stdout")
1848
+ .requiredOption("--project-id <projectId>")
1849
+ .action(async (options) => {
1850
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-items/export", {
1851
+ params: { path: { projectId: options.projectId }, query: workItemSelectionQuery(options) },
1852
+ }));
1853
+ });
1854
+ workItem
1855
+ .command("transfer")
1856
+ .description("Move a Work Item into another Project in its Workspace")
1857
+ .requiredOption("--project-id <projectId>")
1858
+ .requiredOption("--work-item-id <workItemId>")
1859
+ .requiredOption("--destination-project-id <projectId>")
1860
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
1861
+ .option("--destination-status <status>", "Destination Workflow Column slug; defaults to the matching or first column")
1862
+ .action(async (options) => {
1863
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1864
+ exitCode = rejectTokenWrite(dependencies);
1865
+ return;
1866
+ }
1867
+ const idempotencyKey = randomUUID();
1868
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/work-items/{workItemId}/transfer", {
1869
+ body: {
1870
+ destinationProjectId: options.destinationProjectId,
1871
+ ...(options.destinationStatus === undefined ? {} : { destinationStatus: options.destinationStatus }),
1872
+ },
1873
+ params: {
1874
+ header: { "idempotency-key": idempotencyKey, "if-match": options.expectedVersion },
1875
+ path: { projectId: options.projectId, workItemId: options.workItemId },
1876
+ },
1877
+ }), {
1878
+ ambiguousOutcomeMessage: "Work Item transfer outcome is unknown; inspect both Projects before retrying",
1879
+ retryTransportOnce: true,
1880
+ });
1881
+ });
1882
+ workItem
1883
+ .command("move")
1884
+ .requiredOption("--project-id <projectId>")
1885
+ .requiredOption("--work-item-id <workItemId>")
1886
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
1887
+ .requiredOption("--status <status>", "Destination Workflow Column slug or virtual status")
1888
+ .option("--position <position>", "Board index inside the destination column", parseBoardPosition)
1889
+ .action(async (options) => {
1890
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1891
+ exitCode = rejectTokenWrite(dependencies);
1892
+ return;
1893
+ }
1894
+ const idempotencyKey = randomUUID();
1895
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/work-items/{workItemId}/move", {
1896
+ body: {
1897
+ ...(options.position === undefined
1898
+ ? {}
1899
+ : { position: options.position }),
1900
+ status: options.status,
1901
+ },
1902
+ params: {
1903
+ header: {
1904
+ "idempotency-key": idempotencyKey,
1905
+ "if-match": options.expectedVersion,
1906
+ },
1907
+ path: {
1908
+ projectId: options.projectId,
1909
+ workItemId: options.workItemId,
1910
+ },
1911
+ },
1912
+ }), {
1913
+ ambiguousOutcomeMessage: "Work Item move outcome is unknown; read the Work Item board before retrying",
1914
+ retryTransportOnce: true,
1915
+ });
1916
+ });
1917
+ workItem
1918
+ .command("complete")
1919
+ .requiredOption("--project-id <projectId>")
1920
+ .requiredOption("--work-item-id <workItemId>")
1921
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
1922
+ .description("Move a Work Item into the Project's first final Workflow Column")
1923
+ .action(async (options) => {
1924
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1925
+ exitCode = rejectTokenWrite(dependencies);
1926
+ return;
1927
+ }
1928
+ const idempotencyKey = randomUUID();
1929
+ exitCode = await run(async (client) => {
1930
+ const board = await client.GET("/v1/projects/{projectId}/work-item-board", { params: { path: { projectId: options.projectId } } });
1931
+ if (!board.response.ok) {
1932
+ return board;
1933
+ }
1934
+ const finalColumn = board.data?.workflowColumns.find((column) => column.isFinal);
1935
+ if (finalColumn === undefined) {
1936
+ return {
1937
+ error: {
1938
+ code: "WORKFLOW_COLUMN_NO_FINAL",
1939
+ message: "The Project has no final Workflow Column; mark a column final or move the Work Item to its slug directly",
1940
+ },
1941
+ response: new Response(null, { status: 422 }),
1942
+ };
1943
+ }
1944
+ return await client.POST("/v1/projects/{projectId}/work-items/{workItemId}/move", {
1945
+ body: { status: finalColumn.slug },
1946
+ params: {
1947
+ header: {
1948
+ "idempotency-key": idempotencyKey,
1949
+ "if-match": options.expectedVersion,
1950
+ },
1951
+ path: {
1952
+ projectId: options.projectId,
1953
+ workItemId: options.workItemId,
1954
+ },
1955
+ },
1956
+ });
1957
+ }, {
1958
+ ambiguousOutcomeMessage: "Work Item completion outcome is unknown; read the Work Item board before retrying",
1959
+ retryTransportOnce: true,
1960
+ });
1961
+ });
1962
+ const workItemLabel = program.command("work-item-label");
1963
+ workItemLabel
1964
+ .command("list")
1965
+ .requiredOption("--project-id <projectId>")
1966
+ .option("--work-item-id <workItemId>", "List only the labels attached to one Work Item")
1967
+ .action(async (options) => {
1968
+ exitCode = await run((client) => options.workItemId === undefined
1969
+ ? client.GET("/v1/projects/{projectId}/work-item-labels", {
1970
+ params: { path: { projectId: options.projectId } },
1971
+ })
1972
+ : client.GET("/v1/projects/{projectId}/work-items/{workItemId}/labels", {
1973
+ params: {
1974
+ path: {
1975
+ projectId: options.projectId,
1976
+ workItemId: options.workItemId,
1977
+ },
1978
+ },
1979
+ }));
1980
+ });
1981
+ workItemLabel
1982
+ .command("read")
1983
+ .requiredOption("--project-id <projectId>")
1984
+ .requiredOption("--label-id <labelId>")
1985
+ .action(async (options) => {
1986
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-item-labels/{labelId}", { params: { path: options } }));
1987
+ });
1988
+ workItemLabel
1989
+ .command("create")
1990
+ .requiredOption("--project-id <projectId>")
1991
+ .requiredOption("--name <name>")
1992
+ .requiredOption("--color <color>")
1993
+ .option("--work-item-id <workItemId>", "Create the label directly on the named Work Item instead of the Workspace definition")
1994
+ .action(async (options) => {
1995
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1996
+ exitCode = rejectTokenWrite(dependencies);
1997
+ return;
1998
+ }
1999
+ const idempotencyKey = randomUUID();
2000
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/work-item-labels", {
2001
+ body: {
2002
+ color: options.color,
2003
+ name: options.name,
2004
+ ...(options.workItemId === undefined
2005
+ ? {}
2006
+ : { workItemId: options.workItemId }),
2007
+ },
2008
+ params: {
2009
+ header: { "idempotency-key": idempotencyKey },
2010
+ path: { projectId: options.projectId },
2011
+ },
2012
+ }), {
2013
+ ambiguousOutcomeMessage: "Work Item Label creation outcome is unknown; list Work Item Labels before retrying",
2014
+ retryTransportOnce: true,
2015
+ });
2016
+ });
2017
+ workItemLabel
2018
+ .command("update")
2019
+ .requiredOption("--project-id <projectId>")
2020
+ .requiredOption("--label-id <labelId>")
2021
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
2022
+ .option("--name <name>")
2023
+ .option("--color <color>")
2024
+ .action(async (options) => {
2025
+ if (hasWorkspaceReadToken(dependencies.environment)) {
2026
+ exitCode = rejectTokenWrite(dependencies);
2027
+ return;
2028
+ }
2029
+ if (options.name === undefined && options.color === undefined) {
2030
+ throw invalidArgument("Work Item Label update requires a field change");
2031
+ }
2032
+ const idempotencyKey = randomUUID();
2033
+ exitCode = await run((client) => client.PATCH("/v1/projects/{projectId}/work-item-labels/{labelId}", {
2034
+ body: {
2035
+ ...(options.color === undefined
2036
+ ? {}
2037
+ : { color: options.color }),
2038
+ ...(options.name === undefined ? {} : { name: options.name }),
2039
+ },
2040
+ params: {
2041
+ header: {
2042
+ "idempotency-key": idempotencyKey,
2043
+ "if-match": options.expectedVersion,
2044
+ },
2045
+ path: {
2046
+ labelId: options.labelId,
2047
+ projectId: options.projectId,
2048
+ },
2049
+ },
2050
+ }), {
2051
+ ambiguousOutcomeMessage: "Work Item Label update outcome is unknown; read the Work Item Label before retrying",
2052
+ retryTransportOnce: true,
2053
+ });
2054
+ });
2055
+ workItemLabel
2056
+ .command("delete")
2057
+ .requiredOption("--project-id <projectId>")
2058
+ .requiredOption("--label-id <labelId>")
2059
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
2060
+ .description("Delete a Work Item Label; a Workspace definition's task-copy cascade resumes automatically across bounded batches")
2061
+ .action(async (options) => {
2062
+ if (hasWorkspaceReadToken(dependencies.environment)) {
2063
+ exitCode = rejectTokenWrite(dependencies);
2064
+ return;
2065
+ }
2066
+ const idempotencyKey = randomUUID();
2067
+ exitCode = await run(async (client) => {
2068
+ const request = () => client.DELETE("/v1/projects/{projectId}/work-item-labels/{labelId}", {
2069
+ params: {
2070
+ header: {
2071
+ "idempotency-key": idempotencyKey,
2072
+ "if-match": options.expectedVersion,
2073
+ },
2074
+ path: {
2075
+ labelId: options.labelId,
2076
+ projectId: options.projectId,
2077
+ },
2078
+ },
2079
+ });
2080
+ let result = await request();
2081
+ // HTTP 202 marks a definition cascade still in flight; the same
2082
+ // request resumes it, bounded so a runaway copy count stops.
2083
+ for (let attempt = 0; result.response.status === 202 && attempt < 40; attempt += 1) {
2084
+ result = await request();
2085
+ }
2086
+ return withSuccessData(result, {
2087
+ deleted: { labelId: options.labelId },
2088
+ });
2089
+ }, {
2090
+ ambiguousOutcomeMessage: "Work Item Label deletion outcome is unknown; list Work Item Labels before retrying",
2091
+ retryTransportOnce: true,
2092
+ // A bounded 202 stream that never completes is not a success —
2093
+ // report the resumable deletion as retryable instead of printing
2094
+ // the pending payload as if it were done.
2095
+ validateSuccess: (data) => isWorkItemLabelDeletionPending(data)
2096
+ ? {
2097
+ message: "Work Item Label deletion is still in progress; run the same command again to resume it",
2098
+ reason: "Temporary Failure",
2099
+ retryable: true,
2100
+ }
2101
+ : null,
2102
+ });
2103
+ });
2104
+ workItemLabel
2105
+ .command("attach")
2106
+ .requiredOption("--project-id <projectId>")
2107
+ .requiredOption("--work-item-id <workItemId>")
2108
+ .requiredOption("--label-id <labelId>")
2109
+ .option("--expected-version <expectedVersion>", "Opaque version of the task copy being moved; required when the Label is already attached elsewhere", parseOpaqueVersion)
2110
+ .description("Attach a Work Item Label to a Work Item as a task-level copy")
2111
+ .action(async (options) => {
2112
+ if (hasWorkspaceReadToken(dependencies.environment)) {
2113
+ exitCode = rejectTokenWrite(dependencies);
2114
+ return;
2115
+ }
2116
+ const idempotencyKey = randomUUID();
2117
+ exitCode = await run((client) => client.PUT("/v1/projects/{projectId}/work-items/{workItemId}/labels/{labelId}", {
2118
+ params: {
2119
+ header: {
2120
+ "idempotency-key": idempotencyKey,
2121
+ ...(options.expectedVersion === undefined
2122
+ ? {}
2123
+ : { "if-match": options.expectedVersion }),
2124
+ },
2125
+ path: {
2126
+ labelId: options.labelId,
2127
+ projectId: options.projectId,
2128
+ workItemId: options.workItemId,
2129
+ },
2130
+ },
2131
+ }), {
2132
+ ambiguousOutcomeMessage: "Work Item Label attachment outcome is unknown; list the Work Item's labels before retrying",
2133
+ retryTransportOnce: true,
2134
+ });
2135
+ });
2136
+ workItemLabel
2137
+ .command("detach")
2138
+ .requiredOption("--project-id <projectId>")
2139
+ .requiredOption("--work-item-id <workItemId>")
2140
+ .requiredOption("--label-id <labelId>")
2141
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow for the attached copy", parseOpaqueVersion)
2142
+ .description("Detach a Work Item Label copy from a Work Item")
2143
+ .action(async (options) => {
2144
+ if (hasWorkspaceReadToken(dependencies.environment)) {
2145
+ exitCode = rejectTokenWrite(dependencies);
2146
+ return;
2147
+ }
2148
+ const idempotencyKey = randomUUID();
2149
+ exitCode = await run((client) => client.DELETE("/v1/projects/{projectId}/work-items/{workItemId}/labels/{labelId}", {
2150
+ params: {
2151
+ header: {
2152
+ "idempotency-key": idempotencyKey,
2153
+ "if-match": options.expectedVersion,
2154
+ },
2155
+ path: {
2156
+ labelId: options.labelId,
2157
+ projectId: options.projectId,
2158
+ workItemId: options.workItemId,
2159
+ },
2160
+ },
2161
+ }), {
2162
+ ambiguousOutcomeMessage: "Work Item Label detachment outcome is unknown; list the Work Item's labels before retrying",
2163
+ retryTransportOnce: true,
2164
+ });
2165
+ });
2166
+ const workItemCustomField = program.command("work-item-custom-field");
2167
+ workItemCustomField
2168
+ .command("list")
2169
+ .requiredOption("--project-id <projectId>")
2170
+ .action(async (options) => {
2171
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-item-custom-fields", {
2172
+ params: { path: options },
2173
+ }));
2174
+ });
2175
+ workItemCustomField
2176
+ .command("create")
2177
+ .requiredOption("--project-id <projectId>")
2178
+ .requiredOption("--name <name>")
2179
+ .requiredOption("--type <type>", "text, number, date, dropdown, or boolean", parseWorkItemCustomFieldType)
2180
+ .option("--option <option>", "Dropdown option; repeat for every choice", collectOption, [])
2181
+ .option("--default-value <defaultValue>")
2182
+ .option("--required", "Reject Work Items that leave the field blank")
2183
+ .action(async (options) => {
2184
+ if (hasWorkspaceReadToken(dependencies.environment)) {
2185
+ exitCode = rejectTokenWrite(dependencies);
2186
+ return;
2187
+ }
2188
+ const idempotencyKey = randomUUID();
2189
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/work-item-custom-fields", {
2190
+ body: {
2191
+ ...(options.defaultValue === undefined
2192
+ ? {}
2193
+ : { defaultValue: options.defaultValue }),
2194
+ name: options.name,
2195
+ ...(options.option.length === 0
2196
+ ? {}
2197
+ : { options: options.option }),
2198
+ ...(options.required === undefined
2199
+ ? {}
2200
+ : { required: options.required }),
2201
+ type: options.type,
2202
+ },
2203
+ params: {
2204
+ header: { "idempotency-key": idempotencyKey },
2205
+ path: { projectId: options.projectId },
2206
+ },
2207
+ }), {
2208
+ ambiguousOutcomeMessage: "Work Item Custom Field creation outcome is unknown; list Work Item Custom Fields before retrying",
2209
+ retryTransportOnce: true,
2210
+ });
2211
+ });
2212
+ workItemCustomField
2213
+ .command("delete")
2214
+ .requiredOption("--project-id <projectId>")
2215
+ .requiredOption("--field-id <fieldId>")
2216
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
2217
+ .description("Delete a Work Item Custom Field; its stored values cascade with the definition")
2218
+ .action(async (options) => {
2219
+ if (hasWorkspaceReadToken(dependencies.environment)) {
2220
+ exitCode = rejectTokenWrite(dependencies);
2221
+ return;
2222
+ }
2223
+ const idempotencyKey = randomUUID();
2224
+ exitCode = await run(async (client) => withSuccessData(await client.DELETE("/v1/projects/{projectId}/work-item-custom-fields/{fieldId}", {
2225
+ params: {
2226
+ header: {
2227
+ "idempotency-key": idempotencyKey,
2228
+ "if-match": options.expectedVersion,
2229
+ },
2230
+ path: {
2231
+ fieldId: options.fieldId,
2232
+ projectId: options.projectId,
2233
+ },
2234
+ },
2235
+ }), { deleted: { fieldId: options.fieldId } }), {
2236
+ ambiguousOutcomeMessage: "Work Item Custom Field deletion outcome is unknown; list Work Item Custom Fields before retrying",
2237
+ retryTransportOnce: true,
2238
+ });
2239
+ });
2240
+ workItemCustomField
2241
+ .command("reorder")
2242
+ .requiredOption("--project-id <projectId>")
2243
+ .requiredOption("--order <order>", "Comma-separated id@version pairs in display order — version is the integer inside the field's opaque \"N\" version")
2244
+ .action(async (options) => {
2245
+ if (hasWorkspaceReadToken(dependencies.environment)) {
2246
+ exitCode = rejectTokenWrite(dependencies);
2247
+ return;
2248
+ }
2249
+ const fields = parseFieldOrder(options.order);
2250
+ const idempotencyKey = randomUUID();
2251
+ exitCode = await run((client) => client.PUT("/v1/projects/{projectId}/work-item-custom-fields/reorder", {
2252
+ body: { fields },
2253
+ params: {
2254
+ header: { "idempotency-key": idempotencyKey },
2255
+ path: { projectId: options.projectId },
2256
+ },
2257
+ }), {
2258
+ ambiguousOutcomeMessage: "Work Item Custom Field reorder outcome is unknown; list Work Item Custom Fields before retrying",
2259
+ retryTransportOnce: true,
2260
+ });
2261
+ });
2262
+ workItemCustomField
2263
+ .command("values")
2264
+ .requiredOption("--project-id <projectId>")
2265
+ .option("--work-item-id <workItemId>", "List only the values stored on one Work Item")
2266
+ .action(async (options) => {
2267
+ exitCode = await run((client) => options.workItemId === undefined
2268
+ ? client.GET("/v1/projects/{projectId}/work-item-custom-field-values", {
2269
+ params: { path: { projectId: options.projectId } },
2270
+ })
2271
+ : client.GET("/v1/projects/{projectId}/work-items/{workItemId}/custom-field-values", {
2272
+ params: {
2273
+ path: {
2274
+ projectId: options.projectId,
2275
+ workItemId: options.workItemId,
2276
+ },
2277
+ },
2278
+ }));
2279
+ });
2280
+ workItemCustomField
2281
+ .command("filter-values")
2282
+ .requiredOption("--project-id <projectId>")
2283
+ .action(async (options) => {
2284
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-item-custom-field-filter-values", { params: { path: options } }));
2285
+ });
2286
+ workItemCustomField
2287
+ .command("set")
2288
+ .requiredOption("--project-id <projectId>")
2289
+ .requiredOption("--work-item-id <workItemId>")
2290
+ .requiredOption("--field-id <fieldId>")
2291
+ .requiredOption("--expected-version <version>", "The opaque Work Item version returned by SpecFlow")
2292
+ .requiredOption("--value <value>", "Stored value; an empty string clears a non-required field")
2293
+ .action(async (options) => {
2294
+ if (hasWorkspaceReadToken(dependencies.environment)) {
2295
+ exitCode = rejectTokenWrite(dependencies);
2296
+ return;
2297
+ }
2298
+ const expectedVersion = parseOpaqueVersion(options.expectedVersion);
2299
+ const idempotencyKey = randomUUID();
2300
+ exitCode = await run((client) => client.PUT("/v1/projects/{projectId}/work-items/{workItemId}/custom-field-values/{fieldId}", {
2301
+ body: { value: options.value },
2302
+ params: {
2303
+ header: {
2304
+ "idempotency-key": idempotencyKey,
2305
+ "if-match": expectedVersion,
2306
+ },
2307
+ path: {
2308
+ fieldId: options.fieldId,
2309
+ projectId: options.projectId,
2310
+ workItemId: options.workItemId,
2311
+ },
2312
+ },
2313
+ }), {
2314
+ ambiguousOutcomeMessage: "Work Item Custom Field value outcome is unknown; list the Work Item's Custom Field values before retrying",
2315
+ retryTransportOnce: true,
2316
+ });
2317
+ });
2318
+ const workflowColumn = program.command("workflow-column");
2319
+ workflowColumn
2320
+ .command("list")
2321
+ .requiredOption("--project-id <projectId>")
2322
+ .action(async (options) => {
2323
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/workflow-columns", {
2324
+ params: { path: options },
2325
+ }));
2326
+ });
2327
+ workflowColumn
2328
+ .command("create")
2329
+ .requiredOption("--project-id <projectId>")
2330
+ .requiredOption("--name <name>")
2331
+ .option("--icon <icon>", "Column icon name")
2332
+ .option("--color <color>", "Column color")
2333
+ .option("--final", "Mark the column as an explicit completion lane")
2334
+ .action(async (options) => {
2335
+ if (hasWorkspaceReadToken(dependencies.environment)) {
2336
+ exitCode = rejectTokenWrite(dependencies);
2337
+ return;
2338
+ }
2339
+ const idempotencyKey = randomUUID();
2340
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/workflow-columns", {
2341
+ body: {
2342
+ ...(options.color === undefined
2343
+ ? {}
2344
+ : { color: options.color }),
2345
+ ...(options.final === undefined
2346
+ ? {}
2347
+ : { isFinal: options.final }),
2348
+ ...(options.icon === undefined ? {} : { icon: options.icon }),
2349
+ name: options.name,
2350
+ },
2351
+ params: {
2352
+ header: { "idempotency-key": idempotencyKey },
2353
+ path: { projectId: options.projectId },
2354
+ },
2355
+ }), {
2356
+ ambiguousOutcomeMessage: "Workflow Column creation outcome is unknown; list Workflow Columns before retrying",
2357
+ retryTransportOnce: true,
2358
+ });
2359
+ });
2360
+ workflowColumn
2361
+ .command("update")
2362
+ .requiredOption("--project-id <projectId>")
2363
+ .requiredOption("--column-slug <columnSlug>")
2364
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
2365
+ .option("--name <name>")
2366
+ .option("--icon <icon>", "Column icon name")
2367
+ .option("--color <color>", "Column color")
2368
+ .option("--clear-icon", "Remove the column icon")
2369
+ .option("--clear-color", "Remove the column color")
2370
+ .option("--final", "Mark the column as an explicit completion lane")
2371
+ .option("--not-final", "Remove the column's completion marker")
2372
+ .action(async (options) => {
2373
+ if (hasWorkspaceReadToken(dependencies.environment)) {
2374
+ exitCode = rejectTokenWrite(dependencies);
2375
+ return;
2376
+ }
2377
+ if (options.name === undefined &&
2378
+ options.icon === undefined &&
2379
+ options.clearIcon !== true &&
2380
+ options.color === undefined &&
2381
+ options.clearColor !== true &&
2382
+ options.final !== true &&
2383
+ options.notFinal !== true) {
2384
+ throw invalidArgument("Workflow Column update requires a field change");
2385
+ }
2386
+ if (options.clearIcon === true && options.icon !== undefined) {
2387
+ throw invalidArgument("Workflow Column update cannot set and clear the icon together");
2388
+ }
2389
+ if (options.clearColor === true && options.color !== undefined) {
2390
+ throw invalidArgument("Workflow Column update cannot set and clear the color together");
2391
+ }
2392
+ if (options.final === true && options.notFinal === true) {
2393
+ throw invalidArgument("Workflow Column update cannot set and clear the final marker together");
2394
+ }
2395
+ const idempotencyKey = randomUUID();
2396
+ exitCode = await run((client) => client.PATCH("/v1/projects/{projectId}/workflow-columns/{columnSlug}", {
2397
+ body: {
2398
+ ...(options.clearColor === true
2399
+ ? { color: null }
2400
+ : options.color === undefined
2401
+ ? {}
2402
+ : { color: options.color }),
2403
+ ...(options.clearIcon === true
2404
+ ? { icon: null }
2405
+ : options.icon === undefined
2406
+ ? {}
2407
+ : { icon: options.icon }),
2408
+ ...(options.final === true
2409
+ ? { isFinal: true }
2410
+ : options.notFinal === true
2411
+ ? { isFinal: false }
2412
+ : {}),
2413
+ ...(options.name === undefined
2414
+ ? {}
2415
+ : { name: options.name }),
2416
+ },
2417
+ params: {
2418
+ header: {
2419
+ "idempotency-key": idempotencyKey,
2420
+ "if-match": options.expectedVersion,
2421
+ },
2422
+ path: {
2423
+ columnSlug: options.columnSlug,
2424
+ projectId: options.projectId,
2425
+ },
2426
+ },
2427
+ }), {
2428
+ ambiguousOutcomeMessage: "Workflow Column update outcome is unknown; list Workflow Columns before retrying",
2429
+ retryTransportOnce: true,
2430
+ });
2431
+ });
2432
+ workflowColumn
2433
+ .command("delete")
2434
+ .requiredOption("--project-id <projectId>")
2435
+ .requiredOption("--column-slug <columnSlug>")
2436
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
2437
+ .action(async (options) => {
2438
+ if (hasWorkspaceReadToken(dependencies.environment)) {
2439
+ exitCode = rejectTokenWrite(dependencies);
2440
+ return;
2441
+ }
2442
+ const idempotencyKey = randomUUID();
2443
+ exitCode = await run(async (client) => withSuccessData(await client.DELETE("/v1/projects/{projectId}/workflow-columns/{columnSlug}", {
2444
+ params: {
2445
+ header: {
2446
+ "idempotency-key": idempotencyKey,
2447
+ "if-match": options.expectedVersion,
2448
+ },
2449
+ path: {
2450
+ columnSlug: options.columnSlug,
2451
+ projectId: options.projectId,
2452
+ },
2453
+ },
2454
+ }), { deleted: { columnSlug: options.columnSlug } }), {
2455
+ ambiguousOutcomeMessage: "Workflow Column deletion outcome is unknown; list Workflow Columns before retrying",
2456
+ retryTransportOnce: true,
2457
+ });
2458
+ });
2459
+ workflowColumn
2460
+ .command("reorder")
2461
+ .requiredOption("--project-id <projectId>")
2462
+ .requiredOption("--order <order>", "Comma-separated slug@version pairs in board order — version is the integer inside the column's opaque \"N\" version")
2463
+ .action(async (options) => {
2464
+ if (hasWorkspaceReadToken(dependencies.environment)) {
2465
+ exitCode = rejectTokenWrite(dependencies);
2466
+ return;
2467
+ }
2468
+ const columns = parseColumnOrder(options.order);
2469
+ const idempotencyKey = randomUUID();
2470
+ exitCode = await run((client) => client.PUT("/v1/projects/{projectId}/workflow-column-order", {
2471
+ body: { columns },
2472
+ params: {
2473
+ header: { "idempotency-key": idempotencyKey },
2474
+ path: { projectId: options.projectId },
2475
+ },
2476
+ }), {
2477
+ ambiguousOutcomeMessage: "Workflow Column reorder outcome is unknown; list Workflow Columns before retrying",
2478
+ retryTransportOnce: true,
2479
+ });
2480
+ });
2481
+ const token = program.command("token");
2482
+ token
2483
+ .command("issue")
2484
+ .requiredOption("--workspace-id <workspaceId>")
2485
+ .requiredOption("--name <name>")
2486
+ .option("--expires-at <expiresAt>", "ISO 8601 expiry date-time", parseIsoDateTime)
2487
+ .action(async (options) => {
2488
+ if (hasWorkspaceReadToken(dependencies.environment)) {
2489
+ exitCode = rejectTokenWrite(dependencies);
2490
+ return;
2491
+ }
2492
+ const idempotencyKey = randomUUID();
2493
+ exitCode = await run((client) => client.POST("/v1/workspaces/{workspaceId}/read-tokens", {
2494
+ body: {
2495
+ expiresAt: options.expiresAt ?? null,
2496
+ name: options.name,
2497
+ },
2498
+ params: {
2499
+ header: { "idempotency-key": idempotencyKey },
2500
+ path: { workspaceId: options.workspaceId },
2501
+ },
2502
+ }), {
2503
+ ambiguousOutcomeMessage: "Workspace read-only Token issuance outcome is unknown; list tokens before retrying",
2504
+ retryTransportOnce: true,
2505
+ validateSuccess: validateIssuedWorkspaceReadToken,
2506
+ });
2507
+ });
2508
+ token
2509
+ .command("list")
2510
+ .requiredOption("--workspace-id <workspaceId>")
2511
+ .action(async (options) => {
2512
+ exitCode = await run((client) => client.GET("/v1/workspaces/{workspaceId}/read-tokens", {
2513
+ params: { path: { workspaceId: options.workspaceId } },
2514
+ }), { rejectWorkspaceReadToken: true });
2515
+ });
2516
+ token
2517
+ .command("revoke")
2518
+ .requiredOption("--workspace-id <workspaceId>")
2519
+ .requiredOption("--token-id <tokenId>")
2520
+ .action(async (options) => {
2521
+ if (hasWorkspaceReadToken(dependencies.environment)) {
2522
+ exitCode = rejectTokenWrite(dependencies);
2523
+ return;
2524
+ }
2525
+ exitCode = await run(async (client) => withSuccessData(await client.DELETE("/v1/workspaces/{workspaceId}/read-tokens/{tokenId}", { params: { path: options } }), { revoked: { tokenId: options.tokenId } }), {
2526
+ ambiguousOutcomeMessage: "Workspace read-only Token revocation outcome is unknown; list tokens before retrying",
2527
+ retryTransportOnce: true,
2528
+ });
2529
+ });
2530
+ try {
2531
+ await program.parseAsync([...arguments_], { from: "user" });
2532
+ return exitCode;
2533
+ }
2534
+ catch (cause) {
2535
+ if (cause instanceof CommanderError) {
2536
+ if (cause.code === "commander.helpDisplayed") {
2537
+ return cause.exitCode;
2538
+ }
2539
+ writeSkillError(dependencies, {
2540
+ message: cause.message.replace(/'[^']*'/gu, (quoted) => /^'--?[a-z][a-z-]*(?: [<[][^>\]]+[>\]])?'$/u.test(quoted) ? quoted : "'<redacted>'").replace(/[\r\n]/gu, " ").slice(0, 500),
2541
+ code: "INVALID_CLI_INPUT",
2542
+ reason: "Invalid Operation",
2543
+ retryable: false,
2544
+ });
2545
+ return 2;
2546
+ }
2547
+ throw cause;
2548
+ }
2549
+ }
2550
+ async function executeRequest(dependencies, request, options = {}, serverFlag, apiKeyIdFlag) {
2551
+ if (dependencies.environment.SPEC_FLOW_PROJECT_READ_TOKEN !== undefined) {
2552
+ writeSkillError(dependencies, { message: "Project Read Tokens are retired; use a Workspace read-only Token", reason: "Authentication Failed", retryable: false });
2553
+ return 2;
2554
+ }
2555
+ const environmentCredential = dependencies.environment.SPEC_FLOW_WORKSPACE_READ_TOKEN;
2556
+ const apiKeyEnvironment = dependencies.environment.SPEC_FLOW_API_KEY;
2557
+ const usesApiKey = apiKeyEnvironment !== undefined || apiKeyIdFlag !== undefined;
2558
+ if (options.requireUserSession === true && (usesApiKey || hasWorkspaceReadToken(dependencies.environment))) {
2559
+ writeSkillError(dependencies, { message: "A User Session is required for account management", reason: "Permission Denied", retryable: false });
2560
+ return 3;
2561
+ }
2562
+ if ((apiKeyEnvironment !== undefined && apiKeyIdFlag !== undefined) || (usesApiKey && hasWorkspaceReadToken(dependencies.environment))) {
2563
+ writeSkillError(dependencies, { message: "Select exactly one API Key or Workspace read-only Token credential", reason: "Invalid Operation", retryable: false });
2564
+ return 2;
2565
+ }
2566
+ if (apiKeyIdFlag !== undefined && !z.uuid().safeParse(apiKeyIdFlag).success) {
2567
+ writeSkillError(dependencies, { message: "API Key ID must be a UUID", reason: "Invalid Operation", retryable: false });
2568
+ return 2;
2569
+ }
2570
+ if (apiKeyEnvironment !== undefined && !/^sfk_[A-Za-z0-9_-]{43}$/u.test(apiKeyEnvironment)) {
2571
+ writeSkillError(dependencies, { message: "SPEC_FLOW_API_KEY does not contain a User API Key", reason: "Authentication Failed", retryable: false });
2572
+ return 2;
2573
+ }
2574
+ if (options.rejectWorkspaceReadToken === true &&
2575
+ hasWorkspaceReadToken(dependencies.environment)) {
2576
+ return rejectWorkspaceReadTokenOperation(dependencies);
2577
+ }
2578
+ const baseUrl = resolveApiOrigin(dependencies, serverFlag);
2579
+ if (baseUrl === null) {
2580
+ return 2;
2581
+ }
2582
+ let credential;
2583
+ try {
2584
+ if (apiKeyEnvironment !== undefined)
2585
+ credential = apiKeyEnvironment;
2586
+ else if (typeof apiKeyIdFlag === "string") {
2587
+ if (dependencies.credentialStore.readApiKey === undefined)
2588
+ throw new Error("API Key storage is unavailable");
2589
+ credential = await dependencies.credentialStore.readApiKey(apiKeyIdFlag.toLowerCase());
2590
+ }
2591
+ else {
2592
+ credential = environmentCredential ?? await dependencies.credentialStore.read();
2593
+ }
2594
+ }
2595
+ catch {
2596
+ writeSkillError(dependencies, {
2597
+ message: "The CLI credential store is temporarily unavailable",
2598
+ reason: "Temporary Failure",
2599
+ retryable: true,
2600
+ });
2601
+ return 4;
2602
+ }
2603
+ if (credential === null || credential.trim().length === 0) {
2604
+ writeSkillError(dependencies, {
2605
+ message: "An authenticated CLI Session, User API Key or Workspace read-only Token is required",
2606
+ reason: "Authentication Failed",
2607
+ retryable: false,
2608
+ });
2609
+ return 2;
2610
+ }
2611
+ const client = createClient({
2612
+ baseUrl,
2613
+ ...(usesApiKey || options.requireUserSession === true ? { redirect: "error" } : {}),
2614
+ fetch: options.retryTransportOnce === true
2615
+ ? retryTransportOnce(dependencies.fetch)
2616
+ : dependencies.fetch,
2617
+ headers: {
2618
+ ...(usesApiKey ? { "x-api-key": credential } : { authorization: `Bearer ${credential}` }),
2619
+ "user-agent": specFlowCliUserAgent,
2620
+ },
2621
+ });
2622
+ try {
2623
+ const result = await request(client);
2624
+ if (result.response.ok && result.data !== undefined) {
2625
+ const validationError = options.validateSuccess?.(result.data) ?? null;
2626
+ if (validationError !== null) {
2627
+ writeSkillError(dependencies, validationError);
2628
+ return skillErrorExitCode(validationError);
2629
+ }
2630
+ dependencies.stdout(`${JSON.stringify(result.data)}\n`);
2631
+ return 0;
2632
+ }
2633
+ if (result.response.ok && options.retryTransportOnce === true) {
2634
+ const error = ambiguousMutationError(options.ambiguousOutcomeMessage);
2635
+ writeSkillError(dependencies, error);
2636
+ return skillErrorExitCode(error);
2637
+ }
2638
+ const error = mapHttpFailure(result.response, result.error);
2639
+ writeSkillError(dependencies, error);
2640
+ return skillErrorExitCode(error);
2641
+ }
2642
+ catch {
2643
+ const error = options.retryTransportOnce === true
2644
+ ? ambiguousMutationError(options.ambiguousOutcomeMessage)
2645
+ : {
2646
+ message: "SpecFlow is temporarily unavailable",
2647
+ reason: "Temporary Failure",
2648
+ retryable: true,
2649
+ };
2650
+ writeSkillError(dependencies, error);
2651
+ return skillErrorExitCode(error);
2652
+ }
2653
+ }
2654
+ function retryTransportOnce(fetch) {
2655
+ return async (request) => {
2656
+ const retryRequest = request.clone();
2657
+ try {
2658
+ return await fetch(request);
2659
+ }
2660
+ catch {
2661
+ return fetch(retryRequest);
2662
+ }
2663
+ };
2664
+ }
2665
+ function ambiguousMutationError(message) {
2666
+ return {
2667
+ message: message ??
2668
+ "Write outcome is unknown; read the target resource before trying a new command identity",
2669
+ code: "EXECUTION_OUTCOME_UNKNOWN",
2670
+ reason: "Temporary Failure",
2671
+ retryable: false,
2672
+ };
2673
+ }
2674
+ function validateIssuedWorkspaceReadToken(data) {
2675
+ if (typeof data === "object" &&
2676
+ data !== null &&
2677
+ "credential" in data &&
2678
+ data.credential === null) {
2679
+ return {
2680
+ message: "Workspace read-only Token issuance completed, but its credential cannot be recovered; list or revoke the token before deliberately issuing another",
2681
+ reason: "Temporary Failure",
2682
+ retryable: false,
2683
+ };
2684
+ }
2685
+ return null;
2686
+ }
2687
+ function resolveApiOrigin(dependencies, serverFlag) {
2688
+ const flag = typeof serverFlag === "string" ? serverFlag : undefined;
2689
+ const result = parseHttpOrigin(unresolvedSpecFlowApiOrigin(flag, dependencies.environment.SPEC_FLOW_API_URL));
2690
+ if (result.valid) {
2691
+ return result.origin;
2692
+ }
2693
+ writeSkillError(dependencies, {
2694
+ message: flag === undefined
2695
+ ? "SPEC_FLOW_API_URL must be an HTTP(S) origin without a path"
2696
+ : "--server must be an HTTP(S) origin without a path",
2697
+ reason: "Invalid Operation",
2698
+ retryable: false,
2699
+ });
2700
+ return null;
2701
+ }
2702
+ function parseRelationCommandKey(value) {
2703
+ const key = value.trim();
2704
+ if (key.length === 0 || key.length > 200)
2705
+ throw invalidArgument("Relation command key must contain 1 to 200 characters");
2706
+ return key;
2707
+ }
2708
+ function parseSearchLimit(value) {
2709
+ const limit = Number(value);
2710
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
2711
+ throw new CommanderError(1, "commander.invalidArgument", "Search limit must be an integer from 1 to 100");
2712
+ }
2713
+ return limit;
2714
+ }
2715
+ function parseFindingStatus(value) {
2716
+ if (value === "Confirmed" ||
2717
+ value === "Dismissed" ||
2718
+ value === "Proposed" ||
2719
+ value === "Resolved" ||
2720
+ value === "Stale") {
2721
+ return value;
2722
+ }
2723
+ throw invalidArgument("Finding status is not supported");
2724
+ }
2725
+ function parseProjectStatus(value) {
2726
+ if (value === "Active" || value === "Archived" || value === "Trashed") {
2727
+ return value;
2728
+ }
2729
+ throw new CommanderError(1, "commander.invalidArgument", "Project status must be Active, Archived, or Trashed");
2730
+ }
2731
+ function parseProjectId(value) {
2732
+ const normalized = value.trim();
2733
+ if (/^[A-Za-z0-9_-]{1,200}$/u.test(normalized))
2734
+ return normalized;
2735
+ throw invalidArgument("Project ID must be the system identity returned by Project discovery");
2736
+ }
2737
+ function parseSpecId(value) {
2738
+ const normalized = value.trim().toUpperCase();
2739
+ if (/^[A-Z]{2,10}-S[1-9]\d*$/u.test(normalized)) {
2740
+ return normalized;
2741
+ }
2742
+ throw invalidArgument("Spec ID must have the Workspace prefix, S marker and positive number");
2743
+ }
2744
+ function parseWorkItemId(value) {
2745
+ const normalized = value.trim().toUpperCase();
2746
+ if (/^[A-Z]{2,10}-[1-9]\d*$/u.test(normalized)) {
2747
+ return normalized;
2748
+ }
2749
+ throw invalidArgument("Work Item ID must be a Project ID followed by a positive number");
2750
+ }
2751
+ const workItemPriorities = [
2752
+ "no-priority",
2753
+ "low",
2754
+ "medium",
2755
+ "high",
2756
+ "urgent",
2757
+ ];
2758
+ function parseWorkItemPriority(value) {
2759
+ const normalized = value.trim().toLowerCase();
2760
+ if (workItemPriorities.includes(normalized)) {
2761
+ return normalized;
2762
+ }
2763
+ throw invalidArgument("Work Item priority must be no-priority, low, medium, high, or urgent");
2764
+ }
2765
+ function parseWorkItemDate(value) {
2766
+ if (z.iso.date().safeParse(value).success ||
2767
+ z.iso.datetime().safeParse(value).success) {
2768
+ return value;
2769
+ }
2770
+ throw invalidArgument("Work Item dates must be an ISO 8601 date or date-time");
2771
+ }
2772
+ const workItemDueDateFilters = [
2773
+ "dueThisWeek",
2774
+ "dueNextWeek",
2775
+ "noDueDate",
2776
+ ];
2777
+ const workItemQuerySortFields = [
2778
+ "createdAt",
2779
+ "dueDate",
2780
+ "number",
2781
+ "position",
2782
+ "priority",
2783
+ "title",
2784
+ ];
2785
+ const workItemSortOrders = ["asc", "desc"];
2786
+ function parseWorkItemDueFilter(value) {
2787
+ if (workItemDueDateFilters.includes(value)) {
2788
+ return value;
2789
+ }
2790
+ throw invalidArgument("Due filter must be dueThisWeek, dueNextWeek, or noDueDate");
2791
+ }
2792
+ function parseWorkItemSortField(value) {
2793
+ if (workItemQuerySortFields.includes(value)) {
2794
+ return value;
2795
+ }
2796
+ throw invalidArgument("Work Item sort must be createdAt, dueDate, number, position, priority, or title");
2797
+ }
2798
+ function parseWorkItemSortOrder(value) {
2799
+ if (workItemSortOrders.includes(value)) {
2800
+ return value;
2801
+ }
2802
+ throw invalidArgument("Work Item sort order must be asc or desc");
2803
+ }
2804
+ /**
2805
+ * Builds the wire selection query shared by `work-item list` and
2806
+ * `work-item board`: repeatable flags become the API's CSV fields and
2807
+ * repeated `fieldId=value` pairs merge into the `customFields` JSON map.
2808
+ */
2809
+ function workItemSelectionQuery(options) {
2810
+ const customFields = {};
2811
+ for (const entry of options.customField) {
2812
+ const { fieldId, value } = parseCustomFieldAssignment(entry);
2813
+ customFields[fieldId] = [...(customFields[fieldId] ?? []), value];
2814
+ }
2815
+ return {
2816
+ ...(options.assignee.length === 0
2817
+ ? {}
2818
+ : { assignee: options.assignee.join(",") }),
2819
+ ...(Object.keys(customFields).length === 0
2820
+ ? {}
2821
+ : { customFields: JSON.stringify(customFields) }),
2822
+ ...(options.due.length === 0 ? {} : { due: options.due.join(",") }),
2823
+ ...(options.dueAfter === undefined ? {} : { dueAfter: options.dueAfter }),
2824
+ ...(options.dueBefore === undefined
2825
+ ? {}
2826
+ : { dueBefore: options.dueBefore }),
2827
+ ...(options.label.length === 0 ? {} : { label: options.label.join(",") }),
2828
+ ...(options.priority.length === 0
2829
+ ? {}
2830
+ : { priority: options.priority.join(",") }),
2831
+ ...(options.repositoryConnectionId === undefined ? {} : { repositoryConnectionId: options.repositoryConnectionId }),
2832
+ ...(options.search === undefined ? {} : { search: options.search }),
2833
+ ...(options.sort === undefined ? {} : { sortBy: options.sort }),
2834
+ ...(options.order === undefined ? {} : { sortOrder: options.order }),
2835
+ ...(options.startAfter === undefined
2836
+ ? {}
2837
+ : { startAfter: options.startAfter }),
2838
+ ...(options.startBefore === undefined
2839
+ ? {}
2840
+ : { startBefore: options.startBefore }),
2841
+ ...(options.status.length === 0
2842
+ ? {}
2843
+ : { status: options.status.join(",") }),
2844
+ };
2845
+ }
2846
+ /**
2847
+ * The selection options `work-item list` and `work-item board` share.
2848
+ * Attach with `workItemSelectionFlags(command)`.
2849
+ */
2850
+ function workItemSelectionFlags(command) {
2851
+ return command
2852
+ .option("--assignee <assignee>", "Filter by assignee User ID, or unassigned for items with none; repeat for several", collectOption, [])
2853
+ .option("--custom-field <fieldId=value>", "Filter by Custom Field value; repeat to OR values or combine fields", collectOption, [])
2854
+ .option("--due <filter>", "Filter by due bucket: dueThisWeek, dueNextWeek, or noDueDate; repeat for several", (value, previous) => [
2855
+ ...previous,
2856
+ parseWorkItemDueFilter(value),
2857
+ ], [])
2858
+ .option("--due-after <date>", "Filter due on/after", parseWorkItemDate)
2859
+ .option("--due-before <date>", "Filter due on/before", parseWorkItemDate)
2860
+ .option("--label <labelId>", "Filter by Work Item Label ID; repeat for several", collectOption, [])
2861
+ .option("--priority <priority>", "Filter by priority; repeat for several", (value, previous) => [
2862
+ ...previous,
2863
+ parseWorkItemPriority(value),
2864
+ ], [])
2865
+ .option("--repository-connection-id <id>", "Filter by an explicit GitHub repository connection")
2866
+ .option("--search <search>", "Filter by title search text")
2867
+ .option("--sort <field>", "Sort by field", parseWorkItemSortField)
2868
+ .option("--order <order>", "Sort order", parseWorkItemSortOrder)
2869
+ .option("--start-after <date>", "Filter start on/after", parseWorkItemDate)
2870
+ .option("--start-before <date>", "Filter start on/before", parseWorkItemDate)
2871
+ .option("--status <status>", "Filter by Workflow Column slug or virtual status; repeat for several", collectOption, []);
2872
+ }
2873
+ function parseWorkItemBulkAction(value) {
2874
+ if (workItemBulkActions.includes(value)) {
2875
+ return value;
2876
+ }
2877
+ throw invalidArgument(`Bulk action must be ${workItemBulkActions.join(", ")}`);
2878
+ }
2879
+ /**
2880
+ * Targets default to the route Project; an explicit `projectId/` selects
2881
+ * another owner without inferring Project identity from Workspace numbering.
2882
+ */
2883
+ function parseWorkItemBulkTarget(value, previous) {
2884
+ const [identity, version, extra] = value.split("@");
2885
+ const segments = identity?.split("/") ?? [];
2886
+ const id = segments.length === 2 ? segments[1] : segments[0];
2887
+ const projectId = segments.length === 2 ? parseProjectId(segments[0] ?? "") : undefined;
2888
+ if (segments.length > 2 ||
2889
+ id === undefined ||
2890
+ version === undefined ||
2891
+ extra !== undefined ||
2892
+ !/^[A-Z]{2,10}-[1-9]\d*$/u.test(id.trim().toUpperCase()) ||
2893
+ !/^[1-9]\d*$/u.test(version)) {
2894
+ throw invalidArgument(`Bulk target "${value}" must be a WORK-ITEM-ID@version pair`);
2895
+ }
2896
+ const numericVersion = Number(version);
2897
+ if (!Number.isSafeInteger(numericVersion) ||
2898
+ numericVersion > 2_147_483_647) {
2899
+ throw invalidArgument(`Bulk target "${value}" has a version outside the supported range`);
2900
+ }
2901
+ return [
2902
+ ...previous,
2903
+ { expectedVersion: `"${version}"`, id: id.trim().toUpperCase(), ...(projectId === undefined ? {} : { projectId }) },
2904
+ ];
2905
+ }
2906
+ function parseBoardPosition(value) {
2907
+ const position = Number(value);
2908
+ if (!Number.isInteger(position) ||
2909
+ position < 0 ||
2910
+ position > 2_147_483_646) {
2911
+ throw invalidArgument("Board position must be a nonnegative integer within the supported range");
2912
+ }
2913
+ return position;
2914
+ }
2915
+ /**
2916
+ * Comma-separated `slug@version` pairs become the verbatim board order —
2917
+ * position equals list index, matching the reorder contract, and each
2918
+ * version is the column's observed version sent as its reorder
2919
+ * precondition.
2920
+ */
2921
+ function parseColumnOrder(value) {
2922
+ const entries = value
2923
+ .split(",")
2924
+ .map((entry) => entry.trim())
2925
+ .filter((entry) => entry.length > 0);
2926
+ if (entries.length === 0 || entries.length > 200) {
2927
+ throw invalidArgument("Workflow Column order must list between 1 and 200 slug@version pairs");
2928
+ }
2929
+ const parsed = entries.map((entry, position) => {
2930
+ const [slug, version, extra] = entry.split("@");
2931
+ if (slug === undefined ||
2932
+ version === undefined ||
2933
+ extra !== undefined ||
2934
+ !/^[1-9]\d*$/u.test(version)) {
2935
+ throw invalidArgument(`Workflow Column order entry "${entry}" must be a slug@version pair`);
2936
+ }
2937
+ return { expectedVersion: `"${version}"`, position, slug };
2938
+ });
2939
+ const slugs = parsed.map((entry) => entry.slug);
2940
+ if (new Set(slugs).size !== slugs.length) {
2941
+ throw invalidArgument("Workflow Column order cannot repeat a slug");
2942
+ }
2943
+ return parsed;
2944
+ }
2945
+ const workItemCustomFieldTypes = [
2946
+ "text",
2947
+ "number",
2948
+ "date",
2949
+ "dropdown",
2950
+ "boolean",
2951
+ ];
2952
+ function parseWorkItemCustomFieldType(value) {
2953
+ const normalized = value.trim().toLowerCase();
2954
+ if (workItemCustomFieldTypes.includes(normalized)) {
2955
+ return normalized;
2956
+ }
2957
+ throw invalidArgument("Custom Field type must be text, number, date, dropdown, or boolean");
2958
+ }
2959
+ function collectOption(value, previous) {
2960
+ return [...previous, value];
2961
+ }
2962
+ /**
2963
+ * A `fieldId=value` assignment splits on the first `=` so values may carry
2964
+ * the separator; the field id itself is an opaque server identity.
2965
+ */
2966
+ function parseCustomFieldAssignment(value) {
2967
+ const separator = value.indexOf("=");
2968
+ const fieldId = separator === -1 ? "" : value.slice(0, separator).trim();
2969
+ if (fieldId.length === 0 || fieldId.length > 200) {
2970
+ throw invalidArgument("Custom Field values must be written as fieldId=value");
2971
+ }
2972
+ const fieldValue = value.slice(separator + 1);
2973
+ if (fieldValue.length > 2_000) {
2974
+ throw invalidArgument("Custom Field values must stay within the supported length");
2975
+ }
2976
+ return { fieldId, value: fieldValue };
2977
+ }
2978
+ /**
2979
+ * Comma-separated `id@version` pairs become the verbatim display order —
2980
+ * position equals list index, matching the reorder contract, and each
2981
+ * version is the field's observed version sent as its reorder
2982
+ * precondition.
2983
+ */
2984
+ function parseFieldOrder(value) {
2985
+ const entries = value
2986
+ .split(",")
2987
+ .map((entry) => entry.trim())
2988
+ .filter((entry) => entry.length > 0);
2989
+ if (entries.length === 0 || entries.length > 500) {
2990
+ throw invalidArgument("Custom Field order must list between 1 and 500 id@version pairs");
2991
+ }
2992
+ const parsed = entries.map((entry, position) => {
2993
+ const [id, version, extra] = entry.split("@");
2994
+ if (id === undefined ||
2995
+ version === undefined ||
2996
+ extra !== undefined ||
2997
+ !/^[1-9]\d*$/u.test(version)) {
2998
+ throw invalidArgument(`Custom Field order entry "${entry}" must be an id@version pair`);
2999
+ }
3000
+ return { expectedVersion: `"${version}"`, id, position };
3001
+ });
3002
+ const ids = parsed.map((entry) => entry.id);
3003
+ if (new Set(ids).size !== ids.length) {
3004
+ throw invalidArgument("Custom Field order cannot repeat a field id");
3005
+ }
3006
+ return parsed;
3007
+ }
3008
+ function parseOpaqueVersion(value) {
3009
+ if (/^"[1-9]\d*"$/u.test(value)) {
3010
+ return value;
3011
+ }
3012
+ throw invalidArgument("Expected version must be the opaque version returned by SpecFlow");
3013
+ }
3014
+ function parseIsoDateTime(value) {
3015
+ if (z.iso.datetime().safeParse(value).success) {
3016
+ return value;
3017
+ }
3018
+ throw invalidArgument("Expiry must be an ISO 8601 date-time");
3019
+ }
3020
+ function parseSpecState(value) {
3021
+ if (value === "Ready" || value === "Unavailable") {
3022
+ return value;
3023
+ }
3024
+ throw invalidArgument("Spec State must be Ready or Unavailable");
3025
+ }
3026
+ function parseSourceReferences(value) {
3027
+ let parsed;
3028
+ try {
3029
+ parsed = JSON.parse(value);
3030
+ }
3031
+ catch {
3032
+ throw invalidArgument("Source References must be valid JSON");
3033
+ }
3034
+ const sourceReferences = sourceReferencesInputSchema.safeParse(parsed);
3035
+ if (!sourceReferences.success) {
3036
+ throw invalidArgument("Source References do not match the supported bounded contract");
3037
+ }
3038
+ return sourceReferences.data.map((reference) => ({
3039
+ ...(reference.externalRevision === undefined
3040
+ ? {}
3041
+ : { externalRevision: reference.externalRevision }),
3042
+ ...(reference.section === undefined ? {} : { section: reference.section }),
3043
+ title: reference.title,
3044
+ url: reference.url,
3045
+ }));
3046
+ }
3047
+ function invalidArgument(message) {
3048
+ return new CommanderError(1, "commander.invalidArgument", message);
3049
+ }
3050
+ function parseGlossaryEntries(value) {
3051
+ let parsed;
3052
+ try {
3053
+ parsed = JSON.parse(value);
3054
+ }
3055
+ catch {
3056
+ throw new CommanderError(1, "commander.invalidArgument", "Glossary Entries must be valid JSON");
3057
+ }
3058
+ const entries = glossaryEntriesInputSchema.safeParse(parsed);
3059
+ if (!entries.success) {
3060
+ throw new CommanderError(1, "commander.invalidArgument", "Glossary Entries do not match the supported contract");
3061
+ }
3062
+ return entries.data;
3063
+ }
3064
+ function parseStringArray(value, label) {
3065
+ let parsed;
3066
+ try {
3067
+ parsed = JSON.parse(value);
3068
+ }
3069
+ catch {
3070
+ throw invalidArgument(`${label} must be valid JSON`);
3071
+ }
3072
+ const result = z.array(z.string().trim().min(1).max(200)).max(500).safeParse(parsed);
3073
+ if (!result.success) {
3074
+ throw invalidArgument(`${label} must be a bounded JSON string array`);
3075
+ }
3076
+ return result.data;
3077
+ }
3078
+ function toAgentProvenance(options) {
3079
+ return {
3080
+ ...(options.agentRevision === undefined
3081
+ ? {}
3082
+ : { agentRevision: options.agentRevision }),
3083
+ ...(options.agentModel === undefined ? {} : { model: options.agentModel }),
3084
+ sourceReferenceIds: options.sourceReferenceIds === undefined
3085
+ ? []
3086
+ : parseStringArray(options.sourceReferenceIds, "source reference IDs"),
3087
+ };
3088
+ }
3089
+ function parseRoleStatements(value) {
3090
+ let parsed;
3091
+ try {
3092
+ parsed = JSON.parse(value);
3093
+ }
3094
+ catch {
3095
+ throw invalidArgument("Role statements must be valid JSON");
3096
+ }
3097
+ const statements = roleStatementsInputSchema.safeParse(parsed);
3098
+ if (!statements.success) {
3099
+ throw invalidArgument("Role statements must map permission resources to non-empty action arrays");
3100
+ }
3101
+ return statements.data;
3102
+ }
3103
+ function parseProposalTargetKind(value) {
3104
+ if (value === "Glossary" || value === "Spec") {
3105
+ return value;
3106
+ }
3107
+ throw invalidArgument("Proposal target kind must be Spec or Glossary");
3108
+ }
3109
+ function parseProposalStatus(value) {
3110
+ if (value === "Applied" || value === "Discarded" || value === "Open") {
3111
+ return value;
3112
+ }
3113
+ throw invalidArgument("Proposal status must be Open, Applied, or Discarded");
3114
+ }
3115
+ function parseFindingInput(value) {
3116
+ let parsed;
3117
+ try {
3118
+ parsed = JSON.parse(value);
3119
+ }
3120
+ catch {
3121
+ throw invalidArgument("Finding input must be valid JSON");
3122
+ }
3123
+ const finding = findingCreateInputSchema.safeParse(parsed);
3124
+ if (!finding.success) {
3125
+ throw invalidArgument("Finding input does not match the supported contract");
3126
+ }
3127
+ return finding.data;
3128
+ }
3129
+ function withSuccessData(result, data) {
3130
+ return result.response.ok && result.data === undefined
3131
+ ? { ...result, data }
3132
+ : result;
3133
+ }
3134
+ /**
3135
+ * Whether a `work-item-label delete` payload is the still-running cascade —
3136
+ * the 202 body carries the definition with its deletion boundary set.
3137
+ */
3138
+ function isWorkItemLabelDeletionPending(data) {
3139
+ if (typeof data !== "object" || data === null || !("workItemLabel" in data)) {
3140
+ return false;
3141
+ }
3142
+ const label = data.workItemLabel;
3143
+ return (typeof label === "object" &&
3144
+ label !== null &&
3145
+ label.deletionStartedAt !==
3146
+ null &&
3147
+ label.deletionStartedAt !==
3148
+ undefined);
3149
+ }
3150
+ function rejectTokenWrite(dependencies) {
3151
+ if (hasWorkspaceReadToken(dependencies.environment)) {
3152
+ writeSkillError(dependencies, {
3153
+ message: "Workspace read-only Token does not permit write operations",
3154
+ reason: "Permission Denied",
3155
+ retryable: false,
3156
+ });
3157
+ return 3;
3158
+ }
3159
+ writeSkillError(dependencies, {
3160
+ message: "An authenticated CLI Session is required",
3161
+ reason: "Authentication Failed",
3162
+ retryable: false,
3163
+ });
3164
+ return 2;
3165
+ }
3166
+ function rejectWorkspaceReadTokenOperation(dependencies) {
3167
+ writeSkillError(dependencies, {
3168
+ message: "Workspace read-only Token does not permit this operation",
3169
+ reason: "Permission Denied",
3170
+ retryable: false,
3171
+ });
3172
+ return 3;
3173
+ }
3174
+ function hasWorkspaceReadToken(environment) {
3175
+ const credential = environment.SPEC_FLOW_WORKSPACE_READ_TOKEN;
3176
+ return credential !== undefined && credential.trim().length > 0;
3177
+ }
3178
+ function mapHttpFailure(response, payload) {
3179
+ const failure = classifyHttpFailure(response, payload);
3180
+ const issues = z.array(z.object({ location: z.enum(["body", "query", "path", "headers", "tool"]), path: z.array(z.union([z.string().max(100), z.number()])), rule: z.string().max(100), message: z.string().max(500) })).max(20).safeParse(z.object({ issues: z.unknown() }).safeParse(payload).data?.issues);
3181
+ const code = readServerCode(payload);
3182
+ const requestId = response.headers.get("x-request-id");
3183
+ return { ...failure, ...(requestId !== null && /^[A-Za-z0-9._:-]{1,100}$/u.test(requestId) ? { requestId } : {}), status: response.status, ...(code !== null && /^[A-Z][A-Z0-9_]{0,79}$/u.test(code) ? { code } : {}), ...(issues.success ? { issues: issues.data } : {}) };
3184
+ }
3185
+ function classifyHttpFailure(response, payload) {
3186
+ const { status } = response;
3187
+ const serverMessage = readServerMessage(payload);
3188
+ const serverCode = readServerCode(payload);
3189
+ if (status === 401) {
3190
+ return {
3191
+ message: serverMessage ?? "Workspace read-only Token is invalid, expired, or revoked",
3192
+ reason: "Authentication Failed",
3193
+ retryable: false,
3194
+ };
3195
+ }
3196
+ if (status === 403) {
3197
+ return {
3198
+ message: serverMessage ?? "Workspace read-only Token does not permit this operation",
3199
+ reason: "Permission Denied",
3200
+ retryable: false,
3201
+ };
3202
+ }
3203
+ if (status === 404) {
3204
+ if (serverCode === "PROJECT_NOT_ACCESSIBLE") {
3205
+ return {
3206
+ message: serverMessage ?? "The Project is not accessible",
3207
+ reason: "Project Not Accessible",
3208
+ retryable: false,
3209
+ };
3210
+ }
3211
+ return {
3212
+ message: serverMessage ?? "The requested Project resource was not found",
3213
+ reason: "Resource Not Found",
3214
+ retryable: false,
3215
+ };
3216
+ }
3217
+ if (status === 409 && serverCode === "RESOURCE_CHANGED") {
3218
+ return {
3219
+ message: serverMessage ?? "The Working Draft changed after the Proposal baseline",
3220
+ reason: "Resource Changed",
3221
+ retryable: false,
3222
+ };
3223
+ }
3224
+ if (status === 400 || status === 409 || status === 413 || status === 422 || status === 428) {
3225
+ const diagnostics = readServerDiagnostics(payload);
3226
+ return {
3227
+ ...(diagnostics === undefined ? {} : { diagnostics }),
3228
+ message: serverMessage ?? "The requested operation is invalid",
3229
+ reason: "Invalid Operation",
3230
+ retryable: false,
3231
+ };
3232
+ }
3233
+ if (status === 412) {
3234
+ return {
3235
+ message: serverMessage ?? "The resource changed after it was read",
3236
+ reason: "Resource Changed",
3237
+ retryable: false,
3238
+ };
3239
+ }
3240
+ if (status === 429) {
3241
+ const retryAfter = response.headers.get("retry-after");
3242
+ return {
3243
+ message: serverMessage ?? "SpecFlow is temporarily throttling this client",
3244
+ reason: "Temporary Failure",
3245
+ ...(retryAfter === null ? {} : { retryAfter }),
3246
+ retryable: true,
3247
+ };
3248
+ }
3249
+ return {
3250
+ message: serverMessage ?? "SpecFlow is temporarily unavailable",
3251
+ reason: "Temporary Failure",
3252
+ retryable: status >= 500,
3253
+ };
3254
+ }
3255
+ function readServerMessage(payload) {
3256
+ return typeof payload === "object" &&
3257
+ payload !== null &&
3258
+ "message" in payload &&
3259
+ typeof payload.message === "string"
3260
+ ? payload.message
3261
+ : null;
3262
+ }
3263
+ function readServerCode(payload) {
3264
+ return typeof payload === "object" &&
3265
+ payload !== null &&
3266
+ "code" in payload &&
3267
+ typeof payload.code === "string"
3268
+ ? payload.code
3269
+ : null;
3270
+ }
3271
+ function readServerDiagnostics(payload) {
3272
+ if (typeof payload !== "object" ||
3273
+ payload === null ||
3274
+ !("diagnostics" in payload) ||
3275
+ !Array.isArray(payload.diagnostics)) {
3276
+ return undefined;
3277
+ }
3278
+ const diagnostics = payload.diagnostics.flatMap((entry) => {
3279
+ if (typeof entry !== "object" || entry === null) {
3280
+ return [];
3281
+ }
3282
+ const { column, line, message, reason } = entry;
3283
+ if (typeof reason !== "string" || typeof message !== "string") {
3284
+ return [];
3285
+ }
3286
+ return [
3287
+ {
3288
+ ...(typeof column === "number" ? { column } : {}),
3289
+ ...(typeof line === "number" ? { line } : {}),
3290
+ message,
3291
+ reason,
3292
+ },
3293
+ ];
3294
+ });
3295
+ return diagnostics.length === 0 ? undefined : diagnostics;
3296
+ }
3297
+ function skillErrorExitCode(error) {
3298
+ if (error.reason === "Permission Denied") {
3299
+ return 3;
3300
+ }
3301
+ return error.reason === "Temporary Failure" ? 4 : 2;
3302
+ }
3303
+ function writeSkillError(dependencies, error) {
3304
+ dependencies.stderr(`${JSON.stringify({ error })}\n`);
3305
+ }
3306
+ function parseCommentContent(value) {
3307
+ const result = z.string().trim().min(1).max(20_000).refine((content) => !content.includes("\0")).safeParse(value);
3308
+ if (!result.success)
3309
+ throw invalidArgument("Comment content must contain 1 through 20000 characters without null characters");
3310
+ return result.data;
3311
+ }
3312
+ function parseCommentId(value) {
3313
+ if (!z.uuid().safeParse(value).success)
3314
+ throw invalidArgument("Comment ID must be a UUID returned by SpecFlow");
3315
+ return value;
3316
+ }
3317
+ function parseCommentCommandKey(value) {
3318
+ if (!z.uuid().safeParse(value).success)
3319
+ throw invalidArgument("Comment command key must be a UUID; reuse the same key and inputs when retrying");
3320
+ return value;
3321
+ }
3322
+ function parseNotificationUuid(value) {
3323
+ if (!z.uuid().safeParse(value).success)
3324
+ throw invalidArgument("Notification identity and command keys must be UUIDs");
3325
+ return value;
3326
+ }
3327
+ function parseNotificationEventData(value) {
3328
+ let parsed;
3329
+ try {
3330
+ parsed = JSON.parse(value);
3331
+ }
3332
+ catch {
3333
+ throw invalidArgument("Notification event data must be a JSON object");
3334
+ }
3335
+ const result = z.record(z.string(), z.json()).safeParse(parsed);
3336
+ if (!result.success)
3337
+ throw invalidArgument("Notification event data must be a JSON object");
3338
+ return result.data;
3339
+ }
3340
+ function parseNotificationPreferences(value) {
3341
+ let parsed;
3342
+ try {
3343
+ parsed = JSON.parse(value);
3344
+ }
3345
+ catch {
3346
+ throw invalidArgument("Notification preferences must be a JSON object");
3347
+ }
3348
+ const result = z.object({ taskAssignmentEnabled: z.boolean().optional(), taskCommentEnabled: z.boolean().optional(), taskStatusChangeEnabled: z.boolean().optional(), dueDateReminderEnabled: z.boolean().optional(), dueDateReminderMinutes: z.number().int().min(5).max(43200).optional() }).strict().safeParse(parsed);
3349
+ if (!result.success)
3350
+ throw invalidArgument("Notification preferences contain unsupported settings or invalid values");
3351
+ const data = result.data;
3352
+ return { ...(data.taskAssignmentEnabled === undefined ? {} : { taskAssignmentEnabled: data.taskAssignmentEnabled }), ...(data.taskCommentEnabled === undefined ? {} : { taskCommentEnabled: data.taskCommentEnabled }), ...(data.taskStatusChangeEnabled === undefined ? {} : { taskStatusChangeEnabled: data.taskStatusChangeEnabled }), ...(data.dueDateReminderEnabled === undefined ? {} : { dueDateReminderEnabled: data.dueDateReminderEnabled }), ...(data.dueDateReminderMinutes === undefined ? {} : { dueDateReminderMinutes: data.dueDateReminderMinutes }) };
3353
+ }
3354
+ const reviewPlanInputSchema = z.discriminatedUnion("cadence", [z.object({ cadence: z.literal("Disabled") }).strict(), z.object({ cadence: z.literal("Daily"), timeOfDay: z.string(), timeZone: z.string() }).strict(), z.object({ cadence: z.literal("Weekly"), dayOfWeek: z.number().int().min(1).max(7), timeOfDay: z.string(), timeZone: z.string() }).strict()]);
3355
+ //# sourceMappingURL=cli.js.map