@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,157 @@
1
+ import { buildInitialWorkflowState, summarizeWorkflowStateNodes, } from "../domain/workflow-state.js";
2
+ import { getEpicWorkflowStateData, insertEpicRow, insertWorkflowStateRow, mutateEpicWorkflowState, saveEpicWorkflowStateData, updateEpicStageRow, } from "../db/epic-workflow-db.js";
3
+ import { listWorkflowStageRows } from "../db/workflow-db.js";
4
+ import { resolveWorkStatus } from "../domain/work-status.js";
5
+ export function rebuildEpicWorkflowState(epicId, projectId, stageId) {
6
+ const stageRows = listWorkflowStageRows({ projectId, stageId: "" });
7
+ const initial = buildInitialWorkflowState({
8
+ stageId,
9
+ stages: stageRows.map((row) => ({
10
+ id: row.id,
11
+ taskTemplatesJson: row.task_templates_json,
12
+ title: row.title,
13
+ })),
14
+ });
15
+ updateEpicStageRow(epicId, stageId);
16
+ return saveEpicWorkflowStateData(epicId, initial);
17
+ }
18
+ export function rollbackEpicWorkflowFromStage(epicId, sourceStageId, projectId) {
19
+ const stageRows = listWorkflowStageRows({ projectId, stageId: "" });
20
+ const ordered = stageRows.slice().sort((a, b) => a.position - b.position);
21
+ const stageIndex = ordered.findIndex((row) => row.id === sourceStageId);
22
+ let laterStageIds;
23
+ if (stageIndex < 0) {
24
+ laterStageIds = new Set();
25
+ }
26
+ else {
27
+ laterStageIds = new Set(ordered.slice(stageIndex + 1).map((row) => row.id));
28
+ }
29
+ updateEpicStageRow(epicId, sourceStageId);
30
+ mutateEpicWorkflowState(epicId, (data) => {
31
+ data.stageId = sourceStageId;
32
+ for (const node of Object.values(data.nodes)) {
33
+ if (!laterStageIds.has(node.stageId))
34
+ continue;
35
+ node.taskId = null;
36
+ node.workStatus = "todo";
37
+ node.comments = [];
38
+ }
39
+ });
40
+ }
41
+ export function resetWorkflowStateNodesForTasks(epicId, taskIds) {
42
+ if (taskIds.size === 0)
43
+ return;
44
+ mutateEpicWorkflowState(epicId, (data) => {
45
+ for (const node of Object.values(data.nodes)) {
46
+ if (node.taskId !== null && taskIds.has(node.taskId)) {
47
+ node.workStatus = "todo";
48
+ }
49
+ }
50
+ });
51
+ }
52
+ export function createEpicRecords(input) {
53
+ insertEpicRow({
54
+ id: input.id,
55
+ projectId: input.projectId,
56
+ title: input.title,
57
+ description: input.description,
58
+ stageId: input.stageId,
59
+ createdBy: input.createdBy,
60
+ });
61
+ const stageRows = listWorkflowStageRows({ projectId: input.projectId, stageId: "" });
62
+ const initial = buildInitialWorkflowState({
63
+ stageId: input.stageId,
64
+ stages: stageRows.map((row) => ({
65
+ id: row.id,
66
+ taskTemplatesJson: row.task_templates_json,
67
+ title: row.title,
68
+ })),
69
+ });
70
+ insertWorkflowStateRow(input.id, initial);
71
+ return initial;
72
+ }
73
+ export function loadEpicWorkflowState(epicId) {
74
+ return getEpicWorkflowStateData(epicId);
75
+ }
76
+ export function syncEpicWorkflowStage(epicId, stageId) {
77
+ updateEpicStageRow(epicId, stageId);
78
+ mutateEpicWorkflowState(epicId, (data) => {
79
+ data.stageId = stageId;
80
+ });
81
+ }
82
+ export function linkSpawnedTemplateTask(epicId, templateId, taskId) {
83
+ mutateEpicWorkflowState(epicId, (data) => {
84
+ const node = data.nodes[templateId];
85
+ if (!node)
86
+ return;
87
+ node.taskId = taskId;
88
+ });
89
+ }
90
+ export function syncTaskIntoWorkflowState(task) {
91
+ const epicId = task.epicId;
92
+ const templateId = task.templateId;
93
+ if (epicId === null || templateId === null)
94
+ return;
95
+ mutateEpicWorkflowState(epicId, (data) => {
96
+ const node = data.nodes[templateId];
97
+ if (!node)
98
+ return;
99
+ node.taskId = task.id;
100
+ node.workStatus = resolveWorkStatus(task);
101
+ node.comments = task.comments.slice();
102
+ node.title = task.title;
103
+ node.description = task.description;
104
+ });
105
+ }
106
+ export function readWorkflowStateNode(epicId, templateId) {
107
+ const data = getEpicWorkflowStateData(epicId);
108
+ if (!data)
109
+ return null;
110
+ const node = data.nodes[templateId];
111
+ if (node) {
112
+ return node;
113
+ }
114
+ return null;
115
+ }
116
+ export function patchWorkflowStateNode(epicId, templateId, patch, apply) {
117
+ mutateEpicWorkflowState(epicId, (data) => {
118
+ const node = data.nodes[templateId];
119
+ if (!node)
120
+ return;
121
+ if (apply.workStatus && patch.workStatus !== null) {
122
+ node.workStatus = patch.workStatus;
123
+ }
124
+ if (apply.comments && patch.comments !== null) {
125
+ node.comments = patch.comments.slice();
126
+ }
127
+ if (apply.taskId) {
128
+ node.taskId = patch.taskId;
129
+ }
130
+ });
131
+ }
132
+ export function listWorkflowStateSummaries(epicId) {
133
+ const data = getEpicWorkflowStateData(epicId);
134
+ if (!data)
135
+ return [];
136
+ return summarizeWorkflowStateNodes(data);
137
+ }
138
+ export function spawnContextFromWorkflowState(data) {
139
+ const spawnedTemplateIds = new Set();
140
+ const doneTemplateIds = new Set();
141
+ for (const node of Object.values(data.nodes)) {
142
+ if (node.taskId !== null)
143
+ spawnedTemplateIds.add(node.templateId);
144
+ if (node.workStatus === "done")
145
+ doneTemplateIds.add(node.templateId);
146
+ }
147
+ return { spawnedTemplateIds, doneTemplateIds };
148
+ }
149
+ export function applyWorkflowStateNodeToTaskInput(epicId, templateId, defaults) {
150
+ const node = readWorkflowStateNode(epicId, templateId);
151
+ if (!node)
152
+ return defaults;
153
+ return {
154
+ workStatus: node.workStatus,
155
+ comments: node.comments.slice(),
156
+ };
157
+ }
@@ -0,0 +1,204 @@
1
+ import { deleteWorkflowStagesForProject, insertWorkflowStageRow, } from "../db/workflow-db.js";
2
+ import { randomUUID } from "node:crypto";
3
+ import { deleteWorkflowTemplateRow, deleteWorkflowTemplateStages, insertWorkflowTemplateRow, insertWorkflowTemplateStageRow, listWorkflowTemplateRows, listWorkflowTemplateStageRows, seedDefaultWorkflowTemplates, } from "../db/workflow-template-db.js";
4
+ import { resolveEpicDescription, resolveStageTaskTemplates, serializeTaskTemplates, } from "../domain/workflow-stage.js";
5
+ import { DEFAULT_WORKFLOW_TEMPLATE_ID } from "../domain/workflow-template-id.js";
6
+ import { AppError } from "../errors/app-error.js";
7
+ import { valueOrEmpty } from "../lib/strings.js";
8
+ function layoutCoord(value) {
9
+ return value;
10
+ }
11
+ function templateStageRowToStage(row) {
12
+ const taskTemplates = resolveStageTaskTemplates({
13
+ taskTemplatesJson: row.task_templates_json,
14
+ stageId: row.id,
15
+ stageTitle: row.title,
16
+ });
17
+ return {
18
+ id: row.id,
19
+ title: row.title,
20
+ description: resolveEpicDescription({
21
+ description: row.description,
22
+ purpose: row.purpose,
23
+ rulesJson: row.rules_json,
24
+ }),
25
+ position: row.position,
26
+ autoAssignRole: null,
27
+ layoutX: layoutCoord(row.layout_x),
28
+ layoutY: layoutCoord(row.layout_y),
29
+ spawnTaskCount: taskTemplates.length,
30
+ taskTemplates,
31
+ activeTaskCount: null,
32
+ };
33
+ }
34
+ function slugify(value) {
35
+ return value
36
+ .replace(/[^a-z0-9]+/g, "-")
37
+ .replace(/^-+|-+$/g, "");
38
+ }
39
+ function defaultTemplateStage() {
40
+ return {
41
+ id: `stage-${randomUUID()}`,
42
+ title: "New epic",
43
+ description: "",
44
+ position: 0,
45
+ autoAssignRole: null,
46
+ layoutX: null,
47
+ layoutY: null,
48
+ spawnTaskCount: 0,
49
+ taskTemplates: [],
50
+ activeTaskCount: null,
51
+ };
52
+ }
53
+ function resolveTemplateIdHint(id) {
54
+ if (id === null) {
55
+ return "";
56
+ }
57
+ return id;
58
+ }
59
+ function stagePosition(stage, index) {
60
+ if (Number.isFinite(stage.position)) {
61
+ return stage.position;
62
+ }
63
+ return index;
64
+ }
65
+ export function ensureDefaultWorkflowTemplates() {
66
+ seedDefaultWorkflowTemplates();
67
+ }
68
+ export function createWorkflowTemplate(input) {
69
+ ensureDefaultWorkflowTemplates();
70
+ const title = input.title;
71
+ if (!title) {
72
+ throw new AppError("Title is required", 400);
73
+ }
74
+ const idHint = resolveTemplateIdHint(input.id);
75
+ const baseId = idHint || slugify(title) || `template-${randomUUID().slice(0, 8)}`;
76
+ let id = baseId;
77
+ let suffix = 2;
78
+ while (listWorkflowTemplateRows({ id }).length > 0) {
79
+ id = `${baseId}-${suffix}`;
80
+ suffix += 1;
81
+ }
82
+ insertWorkflowTemplateRow({
83
+ id,
84
+ title,
85
+ description: valueOrEmpty(input.description),
86
+ });
87
+ return replaceWorkflowTemplate(id, [defaultTemplateStage()]);
88
+ }
89
+ export function listWorkflowTemplates() {
90
+ ensureDefaultWorkflowTemplates();
91
+ return listWorkflowTemplateRows({ id: "" }).map((row) => ({
92
+ id: row.id,
93
+ title: row.title,
94
+ description: row.description,
95
+ }));
96
+ }
97
+ export function getWorkflowTemplate(templateId) {
98
+ ensureDefaultWorkflowTemplates();
99
+ const id = templateId;
100
+ const rows = listWorkflowTemplateRows({ id });
101
+ if (rows.length === 0) {
102
+ return null;
103
+ }
104
+ const row = rows[0];
105
+ if (!row)
106
+ return null;
107
+ const stages = listWorkflowTemplateStageRows(id).map(templateStageRowToStage);
108
+ return {
109
+ id: row.id,
110
+ title: row.title,
111
+ description: row.description,
112
+ stages,
113
+ };
114
+ }
115
+ export function replaceWorkflowTemplate(templateId, stages) {
116
+ ensureDefaultWorkflowTemplates();
117
+ const id = templateId;
118
+ const templateRows = listWorkflowTemplateRows({ id });
119
+ if (templateRows.length === 0) {
120
+ throw new AppError("Workflow template not found", 404);
121
+ }
122
+ const row = templateRows[0];
123
+ if (!row)
124
+ throw new AppError("Workflow template not found", 404);
125
+ deleteWorkflowTemplateStages(id);
126
+ stages.forEach((stage, index) => {
127
+ insertWorkflowTemplateStageRow({
128
+ templateId: id,
129
+ id: stage.id,
130
+ title: stage.title,
131
+ description: stage.description,
132
+ purpose: "",
133
+ rulesJson: "[]",
134
+ position: stagePosition(stage, index),
135
+ autoAssign: false,
136
+ layoutX: stage.layoutX,
137
+ layoutY: stage.layoutY,
138
+ spawnTaskCount: stage.taskTemplates.length,
139
+ taskTemplatesJson: serializeTaskTemplates(stage.taskTemplates),
140
+ });
141
+ });
142
+ const updated = getWorkflowTemplate(id);
143
+ if (!updated) {
144
+ throw new AppError("Workflow template not found", 404);
145
+ }
146
+ return updated;
147
+ }
148
+ export function importWorkflowTemplate(input) {
149
+ ensureDefaultWorkflowTemplates();
150
+ const title = input.title;
151
+ if (!title)
152
+ throw new AppError("Title is required", 400);
153
+ const idHint = resolveTemplateIdHint(input.id);
154
+ const baseId = idHint || slugify(title) || `template-${randomUUID().slice(0, 8)}`;
155
+ let id = baseId;
156
+ let suffix = 2;
157
+ while (listWorkflowTemplateRows({ id }).length > 0) {
158
+ id = `${baseId}-${suffix}`;
159
+ suffix += 1;
160
+ }
161
+ insertWorkflowTemplateRow({
162
+ id,
163
+ title,
164
+ description: valueOrEmpty(input.description),
165
+ });
166
+ return replaceWorkflowTemplate(id, input.stages);
167
+ }
168
+ const PROTECTED_WORKFLOW_TEMPLATE_IDS = new Set(["ai-sdlc", DEFAULT_WORKFLOW_TEMPLATE_ID]);
169
+ export function deleteWorkflowTemplate(templateId) {
170
+ ensureDefaultWorkflowTemplates();
171
+ const id = templateId;
172
+ if (PROTECTED_WORKFLOW_TEMPLATE_IDS.has(id)) {
173
+ throw new AppError("Built-in template cannot be deleted", 403);
174
+ }
175
+ if (listWorkflowTemplateRows({ id }).length === 0) {
176
+ throw new AppError("Workflow template not found", 404);
177
+ }
178
+ deleteWorkflowTemplateRow(id);
179
+ }
180
+ export function copyTemplateStagesToProject(projectId, templateId) {
181
+ const template = getWorkflowTemplate(templateId);
182
+ if (!template) {
183
+ throw new AppError("Workflow template not found", 404);
184
+ }
185
+ const id = projectId;
186
+ deleteWorkflowStagesForProject(id);
187
+ for (const stage of template.stages) {
188
+ insertWorkflowStageRow({
189
+ id: stage.id,
190
+ projectId: id,
191
+ title: stage.title,
192
+ description: stage.description,
193
+ purpose: "",
194
+ rulesJson: "[]",
195
+ position: stage.position,
196
+ autoAssignRole: "",
197
+ layoutX: stage.layoutX,
198
+ layoutY: stage.layoutY,
199
+ spawnTaskCount: stage.taskTemplates.length,
200
+ taskTemplatesJson: serializeTaskTemplates(stage.taskTemplates),
201
+ });
202
+ }
203
+ return template.stages;
204
+ }