@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,105 @@
1
+ import { canonicalDescription, isTaskClaimed } from "../domain/task.js";
2
+ import { resolveWorkStatus } from "../domain/work-status.js";
3
+ import { emptyToNull } from "../lib/strings.js";
4
+ import { getProjectById } from "./project-registry.js";
5
+ import { buildEpicClaimIndex, canActorClaimTask, isWorkflowClaimable, normalizeClaimActor, sortWorkflowClaimCandidates, userAwaitingReply, workflowClaimBlockReason, } from "./task-claim-policy.js";
6
+ import { claimBridgeTask, listBridgeTasks } from "./task-service.js";
7
+ export { userAwaitingReply } from "./task-claim-policy.js";
8
+ export function turnIdForTask(task) {
9
+ if (userAwaitingReply(task)) {
10
+ for (let index = task.comments.length - 1; index >= 0; index -= 1) {
11
+ const comment = task.comments[index];
12
+ if (!comment) {
13
+ continue;
14
+ }
15
+ if (comment.role === "user")
16
+ return `user-${comment.id}`;
17
+ }
18
+ }
19
+ if (isTaskClaimed(task) && task.claimedAt)
20
+ return `claimed-${task.claimedAt}`;
21
+ return `create-${task.createdAt}`;
22
+ }
23
+ function resolveWorkspacePath(task) {
24
+ const project = getProjectById(task.projectId);
25
+ if (project === null)
26
+ return null;
27
+ return emptyToNull(project.repoPath);
28
+ }
29
+ function buildClaimPayload(task, turnId) {
30
+ let workStatus;
31
+ if (task.parentId !== null) {
32
+ workStatus = resolveWorkStatus(task);
33
+ }
34
+ else {
35
+ workStatus = null;
36
+ }
37
+ return {
38
+ taskId: task.id,
39
+ turnId,
40
+ projectId: task.projectId,
41
+ projectName: task.projectName,
42
+ parentId: task.parentId,
43
+ epicId: task.epicId,
44
+ title: task.title,
45
+ description: canonicalDescription(task),
46
+ workspacePath: resolveWorkspacePath(task),
47
+ stageId: task.stageId,
48
+ workStatus,
49
+ createdAt: task.createdAt,
50
+ comments: task.comments,
51
+ };
52
+ }
53
+ function scopeTasks(tasks, projectId) {
54
+ if (!projectId)
55
+ return tasks;
56
+ return tasks.filter((task) => task.projectId === projectId);
57
+ }
58
+ export function listPendingTasks(projectId = null, rawActor = null) {
59
+ let actor;
60
+ if (rawActor !== null) {
61
+ actor = normalizeClaimActor(rawActor);
62
+ }
63
+ else {
64
+ actor = null;
65
+ }
66
+ const tasks = scopeTasks(listBridgeTasks(), projectId);
67
+ const index = buildEpicClaimIndex(tasks);
68
+ return sortWorkflowClaimCandidates(tasks.filter((task) => isWorkflowClaimable(task, index, actor)), index).map((task) => buildClaimPayload(task, turnIdForTask(task)));
69
+ }
70
+ export function claimNextTask(rawActor, options = null) {
71
+ const actor = normalizeClaimActor(rawActor);
72
+ let projectId;
73
+ if (options !== null) {
74
+ projectId = options.projectId;
75
+ }
76
+ else {
77
+ projectId = null;
78
+ }
79
+ const tasks = scopeTasks(listBridgeTasks(), projectId);
80
+ const index = buildEpicClaimIndex(tasks);
81
+ const candidates = sortWorkflowClaimCandidates(tasks.filter((task) => canActorClaimTask(task, index, actor)), index);
82
+ for (const candidate of candidates) {
83
+ const claimed = claimBridgeTask(candidate.id, actor.claimedBy);
84
+ if (!claimed)
85
+ continue;
86
+ return {
87
+ task: claimed,
88
+ item: buildClaimPayload(claimed, turnIdForTask(claimed)),
89
+ };
90
+ }
91
+ return null;
92
+ }
93
+ export function validateTaskClaim(taskId, rawActor) {
94
+ const actor = normalizeClaimActor(rawActor);
95
+ const tasks = listBridgeTasks();
96
+ const task = tasks.find((entry) => entry.id === taskId);
97
+ if (!task)
98
+ return "Task not found";
99
+ const index = buildEpicClaimIndex(tasks);
100
+ if (canActorClaimTask(task, index, actor))
101
+ return null;
102
+ if (isTaskClaimed(task) && !userAwaitingReply(task))
103
+ return "Task is already claimed";
104
+ return workflowClaimBlockReason(task, index, actor);
105
+ }
@@ -0,0 +1,198 @@
1
+ import { allocateTaskRowId, listTaskRows, mutateTaskRow, upsertTaskRow, } from "../db/tasks-db.js";
2
+ import { isDoneStage, resolveEpicId, touchTask, } from "../domain/task.js";
3
+ import { isWorkDone } from "../domain/work-status.js";
4
+ import { emptyToNull } from "../lib/strings.js";
5
+ import { resolveTaskAssignee } from "./task-assignee-service.js";
6
+ import { syncTaskIntoWorkflowState } from "./workflow-state-service.js";
7
+ export { assertCanCompleteTask, canonicalDescription, DONE_STAGE_ID, isDoneStage, isTaskClaimed, listSubtasks, sortTasks, } from "../domain/task.js";
8
+ export function allocateTaskId() {
9
+ return allocateTaskRowId();
10
+ }
11
+ export function listBridgeTasks() {
12
+ return listTaskRows({ id: 0 });
13
+ }
14
+ export function getBridgeTask(id) {
15
+ const rows = listTaskRows({ id });
16
+ if (rows.length === 0)
17
+ return null;
18
+ const row = rows[0];
19
+ if (!row)
20
+ return null;
21
+ return row;
22
+ }
23
+ export function upsertBridgeTask(input) {
24
+ const existingRows = listTaskRows({ id: input.id });
25
+ if (existingRows.length > 0) {
26
+ const existing = existingRows[0];
27
+ if (!existing)
28
+ throw new Error("Unexpected missing row");
29
+ existing.title = input.title;
30
+ existing.description = input.description;
31
+ existing.projectId = input.projectId;
32
+ existing.projectName = input.projectName;
33
+ touchTask(existing);
34
+ upsertTaskRow(existing);
35
+ return existing;
36
+ }
37
+ let createdAt = new Date().toISOString();
38
+ if (input.createdAt !== null) {
39
+ createdAt = input.createdAt;
40
+ }
41
+ let createdBy = "mobile";
42
+ if (input.createdBy !== null) {
43
+ createdBy = input.createdBy;
44
+ }
45
+ const existingTasks = listTaskRows({ id: 0 });
46
+ let parentRow = null;
47
+ if (input.parentId !== null) {
48
+ const parentRows = listTaskRows({ id: input.parentId });
49
+ if (parentRows.length > 0 && parentRows[0]) {
50
+ parentRow = parentRows[0];
51
+ }
52
+ }
53
+ let resolvedEpicId = null;
54
+ if (input.epicId !== null) {
55
+ resolvedEpicId = input.epicId;
56
+ }
57
+ else if (parentRow !== null) {
58
+ resolvedEpicId = resolveEpicId(existingTasks, parentRow);
59
+ }
60
+ let workStatus = null;
61
+ if (input.workStatus !== null) {
62
+ workStatus = input.workStatus;
63
+ }
64
+ else if (input.parentId !== null) {
65
+ workStatus = "todo";
66
+ }
67
+ const resolvedAssignee = resolveTaskAssignee({
68
+ projectId: input.projectId,
69
+ assignee: input.assignee,
70
+ assigneeRole: input.assigneeRole,
71
+ stageId: input.stageId,
72
+ });
73
+ const task = {
74
+ id: input.id,
75
+ projectId: input.projectId,
76
+ projectName: input.projectName,
77
+ parentId: input.parentId,
78
+ epicId: resolvedEpicId,
79
+ templateId: input.templateId,
80
+ title: input.title,
81
+ description: input.description,
82
+ acceptanceCriteria: null,
83
+ priority: null,
84
+ labels: [],
85
+ createdBy,
86
+ createdAt,
87
+ updatedAt: createdAt,
88
+ claimedBy: null,
89
+ claimedAt: null,
90
+ answeredBy: null,
91
+ answeredAt: null,
92
+ answer: null,
93
+ stageId: input.stageId,
94
+ workStatus,
95
+ assignee: resolvedAssignee.assignee,
96
+ assigneeRole: resolvedAssignee.assigneeRole,
97
+ assigneeKind: input.assigneeKind,
98
+ comments: [],
99
+ events: [{ type: "created", at: createdAt, by: createdBy, note: null }],
100
+ };
101
+ upsertTaskRow(task);
102
+ return task;
103
+ }
104
+ export function transitionBridgeTask(id, input) {
105
+ const existing = getBridgeTask(id);
106
+ if (!existing)
107
+ return null;
108
+ const resolved = resolveTaskAssignee({
109
+ projectId: existing.projectId,
110
+ assignee: input.assignee,
111
+ assigneeRole: existing.assigneeRole,
112
+ stageId: input.stageId,
113
+ });
114
+ return mutateTaskRow(id, (task) => {
115
+ const at = new Date().toISOString();
116
+ const fromStage = task.stageId;
117
+ task.stageId = input.stageId;
118
+ task.assignee = resolved.assignee;
119
+ if (resolved.assigneeRole) {
120
+ task.assigneeRole = resolved.assigneeRole;
121
+ }
122
+ if (isDoneStage(input.stageId)) {
123
+ task.claimedBy = null;
124
+ task.claimedAt = null;
125
+ }
126
+ task.events.push({
127
+ type: "stage_changed",
128
+ at,
129
+ by: input.by,
130
+ note: `${fromStage || "none"} -> ${input.stageId}`,
131
+ });
132
+ touchTask(task);
133
+ });
134
+ }
135
+ export function claimBridgeTask(id, claimedBy) {
136
+ const existingRows = listTaskRows({ id });
137
+ if (existingRows.length === 0)
138
+ return null;
139
+ const existing = existingRows[0];
140
+ if (!existing)
141
+ return null;
142
+ if (existing.claimedBy)
143
+ return null;
144
+ if (existing.parentId === null)
145
+ return null;
146
+ if (isWorkDone(existing))
147
+ return null;
148
+ return mutateTaskRow(id, (task) => {
149
+ const claimedAt = new Date().toISOString();
150
+ task.claimedBy = claimedBy;
151
+ task.claimedAt = claimedAt;
152
+ task.events.push({ type: "claimed", at: claimedAt, by: claimedBy, note: null });
153
+ touchTask(task);
154
+ });
155
+ }
156
+ export function releaseBridgeTask(id) {
157
+ return mutateTaskRow(id, (task) => {
158
+ task.claimedBy = null;
159
+ task.claimedAt = null;
160
+ touchTask(task);
161
+ });
162
+ }
163
+ export function updateBridgeTaskSpec(id, input) {
164
+ return mutateTaskRow(id, (task) => {
165
+ if (input.title !== null)
166
+ task.title = input.title;
167
+ if (input.description !== null) {
168
+ task.description = input.description;
169
+ }
170
+ const at = new Date().toISOString();
171
+ task.events.push({ type: "spec_updated", at, by: input.by, note: null });
172
+ touchTask(task);
173
+ });
174
+ }
175
+ export function addBridgeTaskUserComment(id, by, text) {
176
+ const body = emptyToNull(text);
177
+ if (!body)
178
+ return null;
179
+ const updated = mutateTaskRow(id, (task) => {
180
+ const at = new Date().toISOString();
181
+ task.comments.push({
182
+ id: `user-${id}-${Date.now()}`,
183
+ role: "user",
184
+ authorId: by,
185
+ tags: [],
186
+ body,
187
+ at,
188
+ metadata: null,
189
+ });
190
+ task.claimedBy = null;
191
+ task.claimedAt = null;
192
+ task.events.push({ type: "commented", at, by, note: body.slice(0, 200) });
193
+ touchTask(task);
194
+ });
195
+ if (updated)
196
+ syncTaskIntoWorkflowState(updated);
197
+ return updated;
198
+ }
@@ -0,0 +1,18 @@
1
+ import { incompleteSubtasks } from "../domain/task.js";
2
+ import { AppError } from "../errors/app-error.js";
3
+ import { listBridgeTasks } from "./task-service.js";
4
+ export function validateStageTransition(task, stageId) {
5
+ if (stageId === task.stageId)
6
+ return;
7
+ const tasks = listBridgeTasks();
8
+ const blocked = incompleteSubtasks(tasks, task.id);
9
+ if (blocked.length === 0)
10
+ return;
11
+ throw new AppError(`Cannot move task: ${blocked.length} subtask(s) are not done`, 409, {
12
+ subtasks: blocked.map((entry) => ({
13
+ id: entry.id,
14
+ title: entry.title,
15
+ stageId: entry.stageId,
16
+ })),
17
+ });
18
+ }