@umudik/task-bridge 0.0.1

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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +75 -0
  3. package/apps/backend/dist/config.js +26 -0
  4. package/apps/backend/dist/db/epic-workflow-db.js +125 -0
  5. package/apps/backend/dist/db/library-db.js +123 -0
  6. package/apps/backend/dist/db/projects-db.js +110 -0
  7. package/apps/backend/dist/db/tasks-db.js +282 -0
  8. package/apps/backend/dist/db/users-db.js +117 -0
  9. package/apps/backend/dist/db/workflow-db.js +186 -0
  10. package/apps/backend/dist/db/workflow-template-db.js +715 -0
  11. package/apps/backend/dist/domain/project-member.js +15 -0
  12. package/apps/backend/dist/domain/task-template-graph.js +63 -0
  13. package/apps/backend/dist/domain/task.js +93 -0
  14. package/apps/backend/dist/domain/work-status.js +30 -0
  15. package/apps/backend/dist/domain/workflow-stage.js +186 -0
  16. package/apps/backend/dist/domain/workflow-state.js +73 -0
  17. package/apps/backend/dist/domain/workflow-template-id.js +6 -0
  18. package/apps/backend/dist/errors/app-error.js +24 -0
  19. package/apps/backend/dist/index.js +67 -0
  20. package/apps/backend/dist/lib/bridge-project.js +24 -0
  21. package/apps/backend/dist/lib/inbox-cursor.js +34 -0
  22. package/apps/backend/dist/lib/strings.js +15 -0
  23. package/apps/backend/dist/logger.js +29 -0
  24. package/apps/backend/dist/mappers/task-response.js +261 -0
  25. package/apps/backend/dist/middleware/auth.js +29 -0
  26. package/apps/backend/dist/openapi.js +716 -0
  27. package/apps/backend/dist/routes/admin-users.js +79 -0
  28. package/apps/backend/dist/routes/auth.js +81 -0
  29. package/apps/backend/dist/routes/connect.js +1 -0
  30. package/apps/backend/dist/routes/docs.js +13 -0
  31. package/apps/backend/dist/routes/health.js +6 -0
  32. package/apps/backend/dist/routes/library.js +139 -0
  33. package/apps/backend/dist/routes/projects.js +95 -0
  34. package/apps/backend/dist/routes/tasks.js +522 -0
  35. package/apps/backend/dist/routes/web.js +79 -0
  36. package/apps/backend/dist/routes/workflow-templates.js +152 -0
  37. package/apps/backend/dist/routes/workflow.js +165 -0
  38. package/apps/backend/dist/services/connect-target.js +4 -0
  39. package/apps/backend/dist/services/epic-service.js +269 -0
  40. package/apps/backend/dist/services/library-service.js +222 -0
  41. package/apps/backend/dist/services/project-registry.js +122 -0
  42. package/apps/backend/dist/services/task-assignee-service.js +42 -0
  43. package/apps/backend/dist/services/task-claim-policy.js +310 -0
  44. package/apps/backend/dist/services/task-queue.js +105 -0
  45. package/apps/backend/dist/services/task-service.js +198 -0
  46. package/apps/backend/dist/services/workflow-rules.js +18 -0
  47. package/apps/backend/dist/services/workflow-service.js +418 -0
  48. package/apps/backend/dist/services/workflow-spawn-service.js +179 -0
  49. package/apps/backend/dist/services/workflow-state-service.js +157 -0
  50. package/apps/backend/dist/services/workflow-template-service.js +204 -0
  51. package/apps/backend/public/assets/index-Bl1ciVpY.js +409 -0
  52. package/apps/backend/public/assets/index-ByKECv-I.css +1 -0
  53. package/apps/backend/public/index.html +13 -0
  54. package/bin/task-bridge.mjs +86 -0
  55. package/package.json +41 -0
@@ -0,0 +1,716 @@
1
+ /**
2
+ * OpenAPI 3.1 spec for Task Bridge API.
3
+ * Served as raw JSON at GET /api/docs — no UI, AI-friendly.
4
+ *
5
+ * servers.url is "/api", so paths here are relative to that prefix.
6
+ * Full URL = origin + /api + path (e.g. /api/auth/login).
7
+ *
8
+ * All protected routes require: Authorization: Bearer <token>
9
+ */
10
+ const UserRole = {
11
+ type: "string",
12
+ enum: ["admin", "read-write", "read"],
13
+ };
14
+ const User = {
15
+ type: "object",
16
+ required: ["id", "name", "email", "role", "isSystemAdmin"],
17
+ properties: {
18
+ id: { type: "string" },
19
+ name: { type: "string" },
20
+ email: { type: "string", format: "email" },
21
+ role: { $ref: "#/components/schemas/UserRole" },
22
+ isSystemAdmin: { type: "boolean" },
23
+ },
24
+ };
25
+ const ErrorBody = {
26
+ type: "object",
27
+ required: ["error"],
28
+ properties: { error: { type: "string" } },
29
+ };
30
+ const WorkStatus = {
31
+ type: "string",
32
+ enum: ["todo", "in_progress", "done"],
33
+ };
34
+ const ActorKind = {
35
+ type: "string",
36
+ enum: ["ai", ""],
37
+ };
38
+ const TaskSummary = {
39
+ type: "object",
40
+ properties: {
41
+ id: { type: "integer" },
42
+ title: { type: "string" },
43
+ projectId: { type: "string" },
44
+ projectName: { type: "string" },
45
+ parentId: { type: "integer", nullable: true },
46
+ stageId: { type: "string", nullable: true },
47
+ assignee: { type: "string", nullable: true },
48
+ createdBy: { type: "string" },
49
+ createdAt: { type: "string", format: "date-time" },
50
+ claimedBy: { type: "string", nullable: true },
51
+ claimedAt: { type: "string", nullable: true },
52
+ },
53
+ };
54
+ const CreatedTaskResponse = {
55
+ type: "object",
56
+ required: ["id", "title", "createdAt", "projectId", "projectName"],
57
+ properties: {
58
+ id: { type: "integer" },
59
+ title: { type: "string" },
60
+ createdAt: { type: "string", format: "date-time" },
61
+ projectId: { type: "string" },
62
+ projectName: { type: "string" },
63
+ parentId: { type: "integer", nullable: true },
64
+ stageId: { type: "string", nullable: true },
65
+ assignee: { type: "string", nullable: true },
66
+ },
67
+ };
68
+ const Member = {
69
+ type: "object",
70
+ required: ["id", "name", "role", "projectId"],
71
+ properties: {
72
+ id: { type: "string" },
73
+ projectId: { type: "string" },
74
+ name: { type: "string" },
75
+ role: { type: "string" },
76
+ openTasks: { type: "integer" },
77
+ },
78
+ };
79
+ const TaskTemplate = {
80
+ type: "object",
81
+ required: ["id", "title"],
82
+ properties: {
83
+ id: { type: "string" },
84
+ title: { type: "string" },
85
+ description: { type: "string" },
86
+ assigneeRole: { type: "string" },
87
+ assigneeKind: { $ref: "#/components/schemas/ActorKind" },
88
+ kind: { type: "string", enum: ["task", "group"] },
89
+ dependsOn: { type: "array", items: { type: "string" } },
90
+ children: { type: "array", items: { $ref: "#/components/schemas/TaskTemplate" } },
91
+ },
92
+ };
93
+ const Stage = {
94
+ type: "object",
95
+ required: ["id", "title", "position"],
96
+ properties: {
97
+ id: { type: "string" },
98
+ title: { type: "string" },
99
+ description: { type: "string" },
100
+ position: { type: "integer" },
101
+ autoAssignRole: { type: "string" },
102
+ layoutX: { type: "number", nullable: true },
103
+ layoutY: { type: "number", nullable: true },
104
+ spawnTaskCount: { type: "integer" },
105
+ taskTemplates: { type: "array", items: { $ref: "#/components/schemas/TaskTemplate" } },
106
+ },
107
+ };
108
+ const Workflow = {
109
+ type: "object",
110
+ required: ["projectId", "stages", "roles", "members"],
111
+ properties: {
112
+ projectId: { type: "string" },
113
+ stages: { type: "array", items: { $ref: "#/components/schemas/Stage" } },
114
+ roles: { type: "array", items: { type: "string" } },
115
+ members: { type: "array", items: { $ref: "#/components/schemas/Member" } },
116
+ },
117
+ };
118
+ const WorkflowTemplate = {
119
+ type: "object",
120
+ required: ["id", "title"],
121
+ properties: {
122
+ id: { type: "string" },
123
+ title: { type: "string" },
124
+ description: { type: "string" },
125
+ stages: { type: "array", items: { $ref: "#/components/schemas/Stage" } },
126
+ },
127
+ };
128
+ const Project = {
129
+ type: "object",
130
+ required: ["id", "name", "repoPath"],
131
+ properties: {
132
+ id: { type: "string" },
133
+ name: { type: "string" },
134
+ repoPath: { type: "string" },
135
+ },
136
+ };
137
+ // ── Reusable response helpers ─────────────────────────────────────────────────
138
+ function ok(schema) {
139
+ return { "200": { description: "OK", content: { "application/json": { schema } } } };
140
+ }
141
+ function created(schema) {
142
+ return { "201": { description: "Created", content: { "application/json": { schema } } } };
143
+ }
144
+ function noContent() {
145
+ return { "204": { description: "No Content" } };
146
+ }
147
+ function err(codes) {
148
+ const result = {};
149
+ for (const code of codes) {
150
+ result[String(code)] = {
151
+ description: { 400: "Bad Request", 401: "Unauthorized", 403: "Forbidden", 404: "Not Found", 409: "Conflict", 500: "Server Error" }[code] || "Error",
152
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } },
153
+ };
154
+ }
155
+ return result;
156
+ }
157
+ function json(schema) {
158
+ return { required: true, content: { "application/json": { schema } } };
159
+ }
160
+ function mergeResponses(first, second = null) {
161
+ const result = Object.assign({}, first);
162
+ if (second !== null) {
163
+ Object.assign(result, second);
164
+ }
165
+ return result;
166
+ }
167
+ const bearer = [{ bearerAuth: [] }];
168
+ // ── Spec ──────────────────────────────────────────────────────────────────────
169
+ export const openapiSpec = {
170
+ openapi: "3.1.0",
171
+ info: {
172
+ title: "Task Bridge API",
173
+ version: "1.0.0",
174
+ description: "REST API for Task Bridge. Protected routes require `Authorization: Bearer <token>`. " +
175
+ "Tokens are permanent and verified against the DB on every request. " +
176
+ "Get a token via POST /auth/login.",
177
+ },
178
+ servers: [
179
+ { url: "/api", description: "Same-origin API prefix" },
180
+ ],
181
+ components: {
182
+ securitySchemes: {
183
+ bearerAuth: { type: "http", scheme: "bearer" },
184
+ },
185
+ schemas: {
186
+ UserRole,
187
+ User,
188
+ Error: ErrorBody,
189
+ WorkStatus,
190
+ ActorKind,
191
+ TaskSummary,
192
+ CreatedTaskResponse,
193
+ Member,
194
+ TaskTemplate,
195
+ Stage,
196
+ Workflow,
197
+ WorkflowTemplate,
198
+ Project,
199
+ },
200
+ },
201
+ paths: {
202
+ // ── Auth ──────────────────────────────────────────────────────────────────
203
+ "/auth/status": {
204
+ get: {
205
+ tags: ["auth"],
206
+ summary: "Check if any users exist (first-run detection)",
207
+ security: [],
208
+ responses: ok({ type: "object", required: ["hasUsers"], properties: { hasUsers: { type: "boolean" } } }),
209
+ },
210
+ },
211
+ "/auth/setup": {
212
+ post: {
213
+ tags: ["auth"],
214
+ summary: "Create the initial admin account (only works when no users exist)",
215
+ security: [],
216
+ requestBody: json({
217
+ type: "object",
218
+ required: ["name", "email", "password"],
219
+ properties: {
220
+ name: { type: "string", minLength: 1 },
221
+ email: { type: "string", format: "email" },
222
+ password: { type: "string", minLength: 6 },
223
+ },
224
+ }),
225
+ responses: mergeResponses(created({ type: "object", properties: { user: { $ref: "#/components/schemas/User" }, message: { type: "string" } } }), err([409])),
226
+ },
227
+ },
228
+ "/auth/login": {
229
+ post: {
230
+ tags: ["auth"],
231
+ summary: "Login with email + password → permanent token",
232
+ security: [],
233
+ requestBody: json({
234
+ type: "object",
235
+ required: ["email", "password"],
236
+ properties: {
237
+ email: { type: "string", format: "email" },
238
+ password: { type: "string", minLength: 1 },
239
+ },
240
+ }),
241
+ responses: mergeResponses(ok({
242
+ type: "object",
243
+ required: ["token", "user"],
244
+ properties: {
245
+ token: { type: "string", description: "Bearer token — store in localStorage, send in Authorization header" },
246
+ user: { $ref: "#/components/schemas/User" },
247
+ },
248
+ }), err([401])),
249
+ },
250
+ },
251
+ "/auth/me": {
252
+ get: {
253
+ tags: ["auth"],
254
+ summary: "Get the currently authenticated user",
255
+ security: bearer,
256
+ responses: mergeResponses(ok({ $ref: "#/components/schemas/User" }), err([401])),
257
+ },
258
+ },
259
+ // ── Admin: Users ──────────────────────────────────────────────────────────
260
+ "/admin/users": {
261
+ get: {
262
+ tags: ["admin"],
263
+ summary: "List all users (admin only)",
264
+ security: bearer,
265
+ responses: mergeResponses(ok({ type: "object", required: ["users"], properties: { users: { type: "array", items: { $ref: "#/components/schemas/User" } } } }), err([401, 403])),
266
+ },
267
+ post: {
268
+ tags: ["admin"],
269
+ summary: "Create a new user (admin only)",
270
+ security: bearer,
271
+ requestBody: json({
272
+ type: "object",
273
+ required: ["name", "email", "password"],
274
+ properties: {
275
+ name: { type: "string", minLength: 1 },
276
+ email: { type: "string", format: "email" },
277
+ password: { type: "string", minLength: 6 },
278
+ role: { $ref: "#/components/schemas/UserRole" },
279
+ },
280
+ }),
281
+ responses: mergeResponses(created({ type: "object", properties: { user: { $ref: "#/components/schemas/User" } } }), err([400, 401, 403, 409])),
282
+ },
283
+ },
284
+ "/admin/users/{userId}": {
285
+ patch: {
286
+ tags: ["admin"],
287
+ summary: "Update user name or role (admin only; system admin role cannot be changed)",
288
+ security: bearer,
289
+ parameters: [{ name: "userId", in: "path", required: true, schema: { type: "string" } }],
290
+ requestBody: json({
291
+ type: "object",
292
+ properties: {
293
+ name: { type: "string", minLength: 1 },
294
+ role: { $ref: "#/components/schemas/UserRole" },
295
+ },
296
+ }),
297
+ responses: mergeResponses(ok({ type: "object", properties: { user: { $ref: "#/components/schemas/User" } } }), err([400, 401, 403, 404])),
298
+ },
299
+ delete: {
300
+ tags: ["admin"],
301
+ summary: "Delete a user (admin only; system admin cannot be deleted)",
302
+ security: bearer,
303
+ parameters: [{ name: "userId", in: "path", required: true, schema: { type: "string" } }],
304
+ responses: mergeResponses(noContent(), err([400, 401, 403, 404])),
305
+ },
306
+ },
307
+ "/admin/users/{userId}/token": {
308
+ get: {
309
+ tags: ["admin"],
310
+ summary: "Get a user's bearer token — use for mobile QR code generation (admin only)",
311
+ security: bearer,
312
+ parameters: [{ name: "userId", in: "path", required: true, schema: { type: "string" } }],
313
+ responses: mergeResponses(ok({ type: "object", required: ["token"], properties: { token: { type: "string" } } }), err([401, 403, 404])),
314
+ },
315
+ },
316
+ // ── Projects ──────────────────────────────────────────────────────────────
317
+ "/projects": {
318
+ get: {
319
+ tags: ["projects"],
320
+ summary: "List all projects",
321
+ security: bearer,
322
+ responses: mergeResponses(ok({ type: "object", required: ["projects"], properties: { projects: { type: "array", items: { $ref: "#/components/schemas/Project" } } } }), err([401])),
323
+ },
324
+ post: {
325
+ tags: ["projects"],
326
+ summary: "Create a project",
327
+ security: bearer,
328
+ requestBody: json({
329
+ type: "object",
330
+ required: ["name", "repoPath"],
331
+ properties: {
332
+ name: { type: "string", minLength: 1 },
333
+ id: { type: "string", pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$", description: "Optional custom slug; auto-generated if omitted" },
334
+ repoPath: { type: "string", minLength: 1 },
335
+ workflowTemplateId: { type: "string", description: "Apply a workflow template on creation" },
336
+ },
337
+ }),
338
+ responses: mergeResponses(created({ $ref: "#/components/schemas/Project" }), err([400, 401, 409])),
339
+ },
340
+ },
341
+ "/projects/{id}/repo-path": {
342
+ put: {
343
+ tags: ["projects"],
344
+ summary: "Update a project's repository path",
345
+ security: bearer,
346
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
347
+ requestBody: json({ type: "object", required: ["repoPath"], properties: { repoPath: { type: "string", minLength: 1 } } }),
348
+ responses: mergeResponses(ok({ type: "object", properties: { id: { type: "string" }, name: { type: "string" }, repoPath: { type: "string" } } }), err([401, 404])),
349
+ },
350
+ },
351
+ // ── Epics & Tasks ─────────────────────────────────────────────────────────
352
+ "/epics": {
353
+ post: {
354
+ tags: ["tasks"],
355
+ summary: "Create an epic (top-level task). Use `title` or `text` (text first line = title).",
356
+ security: bearer,
357
+ requestBody: json({
358
+ type: "object",
359
+ required: ["projectId"],
360
+ properties: {
361
+ projectId: { type: "string" },
362
+ title: { type: "string" },
363
+ description: { type: "string" },
364
+ text: { type: "string", description: "Alternative: full markdown; first line becomes title" },
365
+ },
366
+ }),
367
+ responses: mergeResponses(created({ $ref: "#/components/schemas/CreatedTaskResponse" }), err([400, 401])),
368
+ },
369
+ },
370
+ "/tasks": {
371
+ get: {
372
+ tags: ["tasks"],
373
+ summary: "List all tasks (epics + subtasks)",
374
+ security: bearer,
375
+ responses: mergeResponses(ok({ type: "object", required: ["items"], properties: { items: { type: "array", items: { $ref: "#/components/schemas/TaskSummary" } } } }), err([401])),
376
+ },
377
+ post: {
378
+ tags: ["tasks"],
379
+ summary: "Create a subtask under an epic",
380
+ security: bearer,
381
+ requestBody: json({
382
+ type: "object",
383
+ required: ["parentId", "title"],
384
+ properties: {
385
+ parentId: { type: "integer", description: "ID of parent epic or task" },
386
+ title: { type: "string", minLength: 1 },
387
+ description: { type: "string" },
388
+ stageId: { type: "string" },
389
+ },
390
+ }),
391
+ responses: mergeResponses(created({ $ref: "#/components/schemas/CreatedTaskResponse" }), err([400, 401])),
392
+ },
393
+ },
394
+ "/tasks/{id}": {
395
+ get: {
396
+ tags: ["tasks"],
397
+ summary: "Get task detail (includes comments, work-status, subtasks for epics)",
398
+ security: bearer,
399
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "integer" } }],
400
+ responses: mergeResponses(ok({ type: "object" }), err([401, 404])),
401
+ },
402
+ patch: {
403
+ tags: ["tasks"],
404
+ summary: "Update task — add a comment or update description (epic only for description)",
405
+ security: bearer,
406
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "integer" } }],
407
+ requestBody: json({
408
+ type: "object",
409
+ properties: {
410
+ description: { type: "string" },
411
+ comment: {
412
+ type: "object",
413
+ required: ["text"],
414
+ properties: { text: { type: "string", minLength: 1 }, by: { type: "string", default: "web" } },
415
+ },
416
+ },
417
+ }),
418
+ responses: mergeResponses(ok({ type: "object" }), err([400, 401, 404])),
419
+ },
420
+ },
421
+ "/tasks/{id}/claim": {
422
+ post: {
423
+ tags: ["tasks"],
424
+ summary: "Claim a task for a worker/user",
425
+ security: bearer,
426
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "integer" } }],
427
+ requestBody: json({
428
+ type: "object",
429
+ required: ["claimedBy"],
430
+ properties: {
431
+ claimedBy: { type: "string", minLength: 1 },
432
+ },
433
+ }),
434
+ responses: mergeResponses(ok({ type: "object" }), err([401, 404, 409])),
435
+ },
436
+ },
437
+ "/tasks/{id}/unclaim": {
438
+ post: {
439
+ tags: ["tasks"],
440
+ summary: "Release a claimed task",
441
+ security: bearer,
442
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "integer" } }],
443
+ responses: mergeResponses(ok({ type: "object", properties: { taskId: { type: "integer" }, stageId: { type: "string", nullable: true } } }), err([401, 404])),
444
+ },
445
+ },
446
+ "/tasks/{id}/work-status": {
447
+ patch: {
448
+ tags: ["tasks"],
449
+ summary: "Update a subtask's work status (todo → in_progress → done)",
450
+ security: bearer,
451
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "integer" } }],
452
+ requestBody: json({
453
+ type: "object",
454
+ required: ["workStatus", "claimedBy"],
455
+ properties: {
456
+ workStatus: { $ref: "#/components/schemas/WorkStatus" },
457
+ claimedBy: { type: "string", minLength: 1 },
458
+ },
459
+ }),
460
+ responses: mergeResponses(ok({ type: "object" }), err([400, 401, 404])),
461
+ },
462
+ },
463
+ "/projects/{projectId}/epics/{epicId}/tasks": {
464
+ get: {
465
+ tags: ["tasks"],
466
+ summary: "Get an epic's subtask list with stage info",
467
+ security: bearer,
468
+ parameters: [
469
+ { name: "projectId", in: "path", required: true, schema: { type: "string" } },
470
+ { name: "epicId", in: "path", required: true, schema: { type: "integer" } },
471
+ ],
472
+ responses: mergeResponses(ok({ type: "object" }), err([401, 404])),
473
+ },
474
+ },
475
+ // ── Worker queue ──────────────────────────────────────────────────────────
476
+ "/worker/pending": {
477
+ get: {
478
+ tags: ["worker"],
479
+ summary: "List claimable pending tasks (optionally filter by project/role)",
480
+ security: bearer,
481
+ parameters: [
482
+ { name: "projectId", in: "query", schema: { type: "string" } },
483
+ { name: "role", in: "query", schema: { type: "string" } },
484
+ { name: "claimedBy", in: "query", schema: { type: "string" } },
485
+ ],
486
+ responses: mergeResponses(ok({ type: "object", required: ["items"], properties: { items: { type: "array", items: { type: "object" } } } }), err([401])),
487
+ },
488
+ },
489
+ "/worker/claim-next": {
490
+ post: {
491
+ tags: ["worker"],
492
+ summary: "Atomically claim the next available task for a role",
493
+ security: bearer,
494
+ requestBody: json({
495
+ type: "object",
496
+ required: ["claimedBy"],
497
+ properties: {
498
+ claimedBy: { type: "string", minLength: 1 },
499
+ projectId: { type: "string" },
500
+ },
501
+ }),
502
+ responses: mergeResponses(ok({ type: "object" }), err([401, 404])),
503
+ },
504
+ },
505
+ // ── Inbox ─────────────────────────────────────────────────────────────────
506
+ "/inbox": {
507
+ get: {
508
+ tags: ["inbox"],
509
+ summary: "Paginated activity feed (comments + status changes)",
510
+ security: bearer,
511
+ parameters: [
512
+ { name: "projectId", in: "query", schema: { type: "string" } },
513
+ { name: "commentsOnly", in: "query", schema: { type: "string", enum: ["true", "false"] } },
514
+ { name: "epicsOnly", in: "query", schema: { type: "string", enum: ["true", "false"] } },
515
+ { name: "cursor", in: "query", schema: { type: "string" } },
516
+ { name: "limit", in: "query", schema: { type: "integer", default: 20, maximum: 100 } },
517
+ ],
518
+ responses: mergeResponses(ok({ type: "object" }), err([401])),
519
+ },
520
+ },
521
+ // ── Workflow ──────────────────────────────────────────────────────────────
522
+ "/projects/{projectId}/workflow": {
523
+ get: {
524
+ tags: ["workflow"],
525
+ summary: "Get a project's workflow (stages + members + roles)",
526
+ security: bearer,
527
+ parameters: [{ name: "projectId", in: "path", required: true, schema: { type: "string" } }],
528
+ responses: mergeResponses(ok({ $ref: "#/components/schemas/Workflow" }), err([401, 404])),
529
+ },
530
+ put: {
531
+ tags: ["workflow"],
532
+ summary: "Replace a project's entire workflow",
533
+ security: bearer,
534
+ parameters: [{ name: "projectId", in: "path", required: true, schema: { type: "string" } }],
535
+ requestBody: json({
536
+ type: "object",
537
+ required: ["stages"],
538
+ properties: {
539
+ stages: { type: "array", items: { $ref: "#/components/schemas/Stage" }, minItems: 1 },
540
+ roles: { type: "array", items: { type: "string" } },
541
+ },
542
+ }),
543
+ responses: mergeResponses(ok({ $ref: "#/components/schemas/Workflow" }), err([400, 401, 404])),
544
+ },
545
+ },
546
+ "/projects/{projectId}/workflow/export": {
547
+ get: {
548
+ tags: ["workflow"],
549
+ summary: "Export workflow in human-readable format",
550
+ security: bearer,
551
+ parameters: [{ name: "projectId", in: "path", required: true, schema: { type: "string" } }],
552
+ responses: mergeResponses(ok({ type: "object" }), err([401, 404])),
553
+ },
554
+ },
555
+ "/projects/{projectId}/workflow/apply-template": {
556
+ post: {
557
+ tags: ["workflow"],
558
+ summary: "Apply a workflow template to a project (replaces existing workflow)",
559
+ security: bearer,
560
+ parameters: [{ name: "projectId", in: "path", required: true, schema: { type: "string" } }],
561
+ requestBody: json({ type: "object", required: ["templateId"], properties: { templateId: { type: "string", minLength: 1 } } }),
562
+ responses: mergeResponses(ok({ $ref: "#/components/schemas/Workflow" }), err([400, 401, 404])),
563
+ },
564
+ },
565
+ // ── Project Members ───────────────────────────────────────────────────────
566
+ "/projects/{projectId}/members": {
567
+ get: {
568
+ tags: ["workflow"],
569
+ summary: "List project members (workflow participants)",
570
+ security: bearer,
571
+ parameters: [{ name: "projectId", in: "path", required: true, schema: { type: "string" } }],
572
+ responses: mergeResponses(ok({ type: "object", properties: { items: { type: "array", items: { $ref: "#/components/schemas/Member" } } } }), err([401, 404])),
573
+ },
574
+ post: {
575
+ tags: ["workflow"],
576
+ summary: "Add a member to a project",
577
+ security: bearer,
578
+ parameters: [{ name: "projectId", in: "path", required: true, schema: { type: "string" } }],
579
+ requestBody: json({
580
+ type: "object",
581
+ required: ["name"],
582
+ properties: {
583
+ name: { type: "string", minLength: 1 },
584
+ role: { type: "string" },
585
+ },
586
+ }),
587
+ responses: mergeResponses(created({ $ref: "#/components/schemas/Member" }), err([400, 401, 404])),
588
+ },
589
+ },
590
+ "/projects/{projectId}/members/{memberId}": {
591
+ patch: {
592
+ tags: ["workflow"],
593
+ summary: "Update a project member",
594
+ security: bearer,
595
+ parameters: [
596
+ { name: "projectId", in: "path", required: true, schema: { type: "string" } },
597
+ { name: "memberId", in: "path", required: true, schema: { type: "string" } },
598
+ ],
599
+ requestBody: json({
600
+ type: "object",
601
+ properties: {
602
+ name: { type: "string", minLength: 1 },
603
+ role: { type: "string" },
604
+ },
605
+ }),
606
+ responses: mergeResponses(ok({ $ref: "#/components/schemas/Member" }), err([401, 404])),
607
+ },
608
+ delete: {
609
+ tags: ["workflow"],
610
+ summary: "Remove a member from a project",
611
+ security: bearer,
612
+ parameters: [
613
+ { name: "projectId", in: "path", required: true, schema: { type: "string" } },
614
+ { name: "memberId", in: "path", required: true, schema: { type: "string" } },
615
+ ],
616
+ responses: mergeResponses(noContent(), err([401, 404])),
617
+ },
618
+ },
619
+ // ── Workflow Templates ────────────────────────────────────────────────────
620
+ "/workflow-templates": {
621
+ get: {
622
+ tags: ["workflow-templates"],
623
+ summary: "List all workflow templates",
624
+ security: bearer,
625
+ responses: mergeResponses(ok({ type: "object", properties: { items: { type: "array", items: { $ref: "#/components/schemas/WorkflowTemplate" } } } }), err([401])),
626
+ },
627
+ post: {
628
+ tags: ["workflow-templates"],
629
+ summary: "Create a new workflow template (empty — add stages via PUT)",
630
+ security: bearer,
631
+ requestBody: json({
632
+ type: "object",
633
+ required: ["title"],
634
+ properties: {
635
+ id: { type: "string", description: "Optional custom ID; auto-generated if omitted" },
636
+ title: { type: "string", minLength: 1 },
637
+ description: { type: "string" },
638
+ },
639
+ }),
640
+ responses: mergeResponses(created({ $ref: "#/components/schemas/WorkflowTemplate" }), err([400, 401])),
641
+ },
642
+ },
643
+ "/workflow-templates/import": {
644
+ post: {
645
+ tags: ["workflow-templates"],
646
+ summary: "Import a template from a previously exported JSON file. ID is reused if free, otherwise auto-suffixed.",
647
+ security: bearer,
648
+ requestBody: json({
649
+ type: "object",
650
+ required: ["title", "stages"],
651
+ properties: {
652
+ id: { type: "string", description: "Preferred ID (hint — may be suffixed if already taken)" },
653
+ title: { type: "string", minLength: 1 },
654
+ description: { type: "string" },
655
+ stages: { type: "array", items: { $ref: "#/components/schemas/Stage" }, minItems: 1 },
656
+ exportedFrom: { type: "string", description: "Ignored — present in export files for identification" },
657
+ version: { type: "integer", description: "Ignored — present in export files for versioning" },
658
+ },
659
+ }),
660
+ responses: mergeResponses(created({ $ref: "#/components/schemas/WorkflowTemplate" }), err([400, 401])),
661
+ },
662
+ },
663
+ "/workflow-templates/{templateId}": {
664
+ get: {
665
+ tags: ["workflow-templates"],
666
+ summary: "Get a workflow template by ID",
667
+ security: bearer,
668
+ parameters: [{ name: "templateId", in: "path", required: true, schema: { type: "string" } }],
669
+ responses: mergeResponses(ok({ $ref: "#/components/schemas/WorkflowTemplate" }), err([401, 404])),
670
+ },
671
+ put: {
672
+ tags: ["workflow-templates"],
673
+ summary: "Replace a workflow template's stages",
674
+ security: bearer,
675
+ parameters: [{ name: "templateId", in: "path", required: true, schema: { type: "string" } }],
676
+ requestBody: json({
677
+ type: "object",
678
+ required: ["stages"],
679
+ properties: {
680
+ stages: { type: "array", items: { $ref: "#/components/schemas/Stage" }, minItems: 1 },
681
+ },
682
+ }),
683
+ responses: mergeResponses(ok({ $ref: "#/components/schemas/WorkflowTemplate" }), err([400, 401, 404])),
684
+ },
685
+ },
686
+ "/workflow-templates/{templateId}/export": {
687
+ get: {
688
+ tags: ["workflow-templates"],
689
+ summary: "Download a template as a JSON file (Content-Disposition: attachment)",
690
+ security: bearer,
691
+ parameters: [{ name: "templateId", in: "path", required: true, schema: { type: "string" } }],
692
+ responses: mergeResponses({
693
+ "200": {
694
+ description: "JSON file download",
695
+ content: {
696
+ "application/json": {
697
+ schema: {
698
+ type: "object",
699
+ required: ["exportedFrom", "version", "id", "title", "stages"],
700
+ properties: {
701
+ exportedFrom: { type: "string", example: "task-bridge" },
702
+ version: { type: "integer", example: 1 },
703
+ id: { type: "string" },
704
+ title: { type: "string" },
705
+ description: { type: "string" },
706
+ stages: { type: "array", items: { $ref: "#/components/schemas/Stage" } },
707
+ },
708
+ },
709
+ },
710
+ },
711
+ },
712
+ }, err([401, 404])),
713
+ },
714
+ },
715
+ },
716
+ };