@allpepper/task-orchestrator-tui 1.2.1 → 2.0.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.
@@ -3,6 +3,12 @@
3
3
  *
4
4
  * Implements DataAdapter by directly importing and calling repository functions.
5
5
  * This is used when the UI is running in the same process as the domain layer.
6
+ *
7
+ * v2 changes:
8
+ * - Projects are stateless (no status)
9
+ * - Status transitions via advance/revert/terminate
10
+ * - Dependencies are field-based (blockedBy/relatedTo on entities)
11
+ * - No separate dependencies repo
6
12
  */
7
13
 
8
14
  import type {
@@ -11,17 +17,16 @@ import type {
11
17
  SearchParams,
12
18
  FeatureSearchParams,
13
19
  TaskSearchParams,
20
+ WorkflowState,
21
+ TransitionResult,
14
22
  } from './types';
15
23
  import type {
16
24
  Task,
17
25
  Feature,
18
26
  Project,
19
- TaskStatus,
20
- ProjectStatus,
21
- FeatureStatus,
22
- Priority,
23
27
  Section,
24
28
  EntityType,
29
+ Priority,
25
30
  } from '@allpepper/task-orchestrator';
26
31
  import type {
27
32
  SearchResults,
@@ -30,16 +35,25 @@ import type {
30
35
  FeatureOverview,
31
36
  } from '../lib/types';
32
37
 
33
- // Import repos individually since barrel export is incomplete
34
38
  import * as projects from '@allpepper/task-orchestrator/src/repos/projects';
35
39
  import * as features from '@allpepper/task-orchestrator/src/repos/features';
36
40
  import * as tasks from '@allpepper/task-orchestrator/src/repos/tasks';
37
41
  import * as sections from '@allpepper/task-orchestrator/src/repos/sections';
38
- import * as dependencies from '@allpepper/task-orchestrator/src/repos/dependencies';
39
42
  import {
40
43
  getAllowedTransitions,
44
+ isValidTransition,
41
45
  type ContainerType,
42
46
  } from '@allpepper/task-orchestrator/src/services/status-validator';
47
+ import {
48
+ getWorkflowState as getWorkflowStateFn,
49
+ } from '@allpepper/task-orchestrator/src/services/workflow';
50
+ import {
51
+ getNextState,
52
+ getPrevState,
53
+ getPipelinePosition,
54
+ EXIT_STATE,
55
+ } from '@allpepper/task-orchestrator/src/config';
56
+ import { queryAll, queryOne, execute, now } from '@allpepper/task-orchestrator/src/repos/base';
43
57
 
44
58
  /**
45
59
  * DirectAdapter implementation
@@ -48,14 +62,13 @@ import {
48
62
  */
49
63
  export class DirectAdapter implements DataAdapter {
50
64
  // ============================================================================
51
- // Projects
65
+ // Projects (stateless in v2)
52
66
  // ============================================================================
53
67
 
54
68
  async getProjects(params?: SearchParams): Promise<Result<Project[]>> {
55
69
  return Promise.resolve(
56
70
  projects.searchProjects({
57
71
  query: params?.query,
58
- status: params?.status,
59
72
  tags: params?.tags?.join(','),
60
73
  limit: params?.limit,
61
74
  offset: params?.offset,
@@ -74,13 +87,11 @@ export class DirectAdapter implements DataAdapter {
74
87
  return Promise.resolve(result as Result<ProjectOverview>);
75
88
  }
76
89
 
77
- // Transform repo format to UI format
78
90
  const overview: ProjectOverview = {
79
91
  project: {
80
92
  id: result.data.project.id,
81
93
  name: result.data.project.name,
82
94
  summary: result.data.project.summary,
83
- status: result.data.project.status,
84
95
  },
85
96
  taskCounts: result.data.taskCounts,
86
97
  };
@@ -92,7 +103,6 @@ export class DirectAdapter implements DataAdapter {
92
103
  name: string;
93
104
  summary: string;
94
105
  description?: string;
95
- status?: ProjectStatus;
96
106
  tags?: string[];
97
107
  }): Promise<Result<Project>> {
98
108
  return Promise.resolve(projects.createProject(params));
@@ -104,7 +114,6 @@ export class DirectAdapter implements DataAdapter {
104
114
  name?: string;
105
115
  summary?: string;
106
116
  description?: string;
107
- status?: ProjectStatus;
108
117
  tags?: string[];
109
118
  version: number;
110
119
  }
@@ -145,7 +154,6 @@ export class DirectAdapter implements DataAdapter {
145
154
  return Promise.resolve(result as Result<FeatureOverview>);
146
155
  }
147
156
 
148
- // Transform repo format to UI format
149
157
  const overview: FeatureOverview = {
150
158
  feature: {
151
159
  id: result.data.feature.id,
@@ -165,7 +173,6 @@ export class DirectAdapter implements DataAdapter {
165
173
  name: string;
166
174
  summary: string;
167
175
  description?: string;
168
- status?: FeatureStatus;
169
176
  priority: Priority;
170
177
  tags?: string[];
171
178
  }): Promise<Result<Feature>> {
@@ -178,7 +185,6 @@ export class DirectAdapter implements DataAdapter {
178
185
  name?: string;
179
186
  summary?: string;
180
187
  description?: string;
181
- status?: FeatureStatus;
182
188
  priority?: Priority;
183
189
  projectId?: string;
184
190
  tags?: string[];
@@ -220,7 +226,6 @@ export class DirectAdapter implements DataAdapter {
220
226
  title: string;
221
227
  summary: string;
222
228
  description?: string;
223
- status?: TaskStatus;
224
229
  priority: Priority;
225
230
  complexity: number;
226
231
  tags?: string[];
@@ -234,7 +239,6 @@ export class DirectAdapter implements DataAdapter {
234
239
  title?: string;
235
240
  summary?: string;
236
241
  description?: string;
237
- status?: TaskStatus;
238
242
  priority?: Priority;
239
243
  complexity?: number;
240
244
  projectId?: string;
@@ -251,28 +255,220 @@ export class DirectAdapter implements DataAdapter {
251
255
  return Promise.resolve(tasks.deleteTask(id));
252
256
  }
253
257
 
254
- async setTaskStatus(
258
+ // ============================================================================
259
+ // Pipeline Operations (v2)
260
+ // ============================================================================
261
+
262
+ async advance(
263
+ containerType: 'task' | 'feature',
255
264
  id: string,
256
- status: TaskStatus,
257
265
  version: number
258
- ): Promise<Result<Task>> {
259
- return Promise.resolve(tasks.setTaskStatus(id, status, version));
266
+ ): Promise<Result<TransitionResult>> {
267
+ try {
268
+ const entity = containerType === 'task'
269
+ ? tasks.getTask(id)
270
+ : features.getFeature(id);
271
+
272
+ if (!entity.success) {
273
+ return { success: false, error: entity.error, code: entity.code };
274
+ }
275
+
276
+ if (entity.data.version !== version) {
277
+ return { success: false, error: `Version conflict: expected ${version}, found ${entity.data.version}`, code: 'CONFLICT' };
278
+ }
279
+
280
+ const currentStatus = entity.data.status;
281
+ const nextState = getNextState(containerType, currentStatus);
282
+
283
+ if (!nextState) {
284
+ return { success: false, error: `Cannot advance: no next state from ${currentStatus}`, code: 'INVALID_OPERATION' };
285
+ }
286
+
287
+ // Check if blocked
288
+ if ('blockedBy' in entity.data && entity.data.blockedBy.length > 0) {
289
+ return { success: false, error: `Cannot advance: entity is blocked`, code: 'BLOCKED' };
290
+ }
291
+
292
+ const table = containerType === 'task' ? 'tasks' : 'features';
293
+ const timestamp = now();
294
+ execute(
295
+ `UPDATE ${table} SET status = ?, version = version + 1, modified_at = ? WHERE id = ?`,
296
+ [nextState, timestamp, id]
297
+ );
298
+
299
+ // Re-fetch entity
300
+ const updated = containerType === 'task'
301
+ ? tasks.getTask(id)
302
+ : features.getFeature(id);
303
+
304
+ if (!updated.success) {
305
+ return { success: false, error: updated.error, code: updated.code };
306
+ }
307
+
308
+ return {
309
+ success: true,
310
+ data: {
311
+ entity: updated.data,
312
+ oldStatus: currentStatus,
313
+ newStatus: nextState,
314
+ pipelinePosition: getPipelinePosition(containerType, nextState),
315
+ },
316
+ };
317
+ } catch (error) {
318
+ return {
319
+ success: false,
320
+ error: error instanceof Error ? error.message : 'Unknown error',
321
+ };
322
+ }
260
323
  }
261
324
 
262
- async setProjectStatus(
325
+ async revert(
326
+ containerType: 'task' | 'feature',
263
327
  id: string,
264
- status: ProjectStatus,
265
328
  version: number
266
- ): Promise<Result<Project>> {
267
- return Promise.resolve(projects.updateProject(id, { status, version }));
329
+ ): Promise<Result<TransitionResult>> {
330
+ try {
331
+ const entity = containerType === 'task'
332
+ ? tasks.getTask(id)
333
+ : features.getFeature(id);
334
+
335
+ if (!entity.success) {
336
+ return { success: false, error: entity.error, code: entity.code };
337
+ }
338
+
339
+ if (entity.data.version !== version) {
340
+ return { success: false, error: `Version conflict: expected ${version}, found ${entity.data.version}`, code: 'CONFLICT' };
341
+ }
342
+
343
+ const currentStatus = entity.data.status;
344
+ const prevState = getPrevState(containerType, currentStatus);
345
+
346
+ if (!prevState) {
347
+ return { success: false, error: `Cannot revert: no previous state from ${currentStatus}`, code: 'INVALID_OPERATION' };
348
+ }
349
+
350
+ const table = containerType === 'task' ? 'tasks' : 'features';
351
+ const timestamp = now();
352
+ execute(
353
+ `UPDATE ${table} SET status = ?, version = version + 1, modified_at = ? WHERE id = ?`,
354
+ [prevState, timestamp, id]
355
+ );
356
+
357
+ const updated = containerType === 'task'
358
+ ? tasks.getTask(id)
359
+ : features.getFeature(id);
360
+
361
+ if (!updated.success) {
362
+ return { success: false, error: updated.error, code: updated.code };
363
+ }
364
+
365
+ return {
366
+ success: true,
367
+ data: {
368
+ entity: updated.data,
369
+ oldStatus: currentStatus,
370
+ newStatus: prevState,
371
+ pipelinePosition: getPipelinePosition(containerType, prevState),
372
+ },
373
+ };
374
+ } catch (error) {
375
+ return {
376
+ success: false,
377
+ error: error instanceof Error ? error.message : 'Unknown error',
378
+ };
379
+ }
268
380
  }
269
381
 
270
- async setFeatureStatus(
382
+ async terminate(
383
+ containerType: 'task' | 'feature',
271
384
  id: string,
272
- status: FeatureStatus,
273
385
  version: number
274
- ): Promise<Result<Feature>> {
275
- return Promise.resolve(features.updateFeature(id, { status, version }));
386
+ ): Promise<Result<TransitionResult>> {
387
+ try {
388
+ const entity = containerType === 'task'
389
+ ? tasks.getTask(id)
390
+ : features.getFeature(id);
391
+
392
+ if (!entity.success) {
393
+ return { success: false, error: entity.error, code: entity.code };
394
+ }
395
+
396
+ if (entity.data.version !== version) {
397
+ return { success: false, error: `Version conflict: expected ${version}, found ${entity.data.version}`, code: 'CONFLICT' };
398
+ }
399
+
400
+ const currentStatus = entity.data.status;
401
+ const table = containerType === 'task' ? 'tasks' : 'features';
402
+ const timestamp = now();
403
+
404
+ execute(
405
+ `UPDATE ${table} SET status = ?, version = version + 1, modified_at = ? WHERE id = ?`,
406
+ [EXIT_STATE, timestamp, id]
407
+ );
408
+
409
+ const updated = containerType === 'task'
410
+ ? tasks.getTask(id)
411
+ : features.getFeature(id);
412
+
413
+ if (!updated.success) {
414
+ return { success: false, error: updated.error, code: updated.code };
415
+ }
416
+
417
+ return {
418
+ success: true,
419
+ data: {
420
+ entity: updated.data,
421
+ oldStatus: currentStatus,
422
+ newStatus: EXIT_STATE,
423
+ pipelinePosition: null,
424
+ },
425
+ };
426
+ } catch (error) {
427
+ return {
428
+ success: false,
429
+ error: error instanceof Error ? error.message : 'Unknown error',
430
+ };
431
+ }
432
+ }
433
+
434
+ async getWorkflowState(
435
+ containerType: 'task' | 'feature',
436
+ id: string
437
+ ): Promise<Result<WorkflowState>> {
438
+ try {
439
+ const result = getWorkflowStateFn(containerType, id);
440
+ if (!result.success) {
441
+ return { success: false, error: result.error, code: result.code };
442
+ }
443
+ return { success: true, data: result.data as WorkflowState };
444
+ } catch (error) {
445
+ return {
446
+ success: false,
447
+ error: error instanceof Error ? error.message : 'Unknown error',
448
+ };
449
+ }
450
+ }
451
+
452
+ // ============================================================================
453
+ // Workflow
454
+ // ============================================================================
455
+
456
+ async getAllowedTransitions(
457
+ containerType: string,
458
+ status: string
459
+ ): Promise<Result<string[]>> {
460
+ try {
461
+ const transitions = getAllowedTransitions(
462
+ containerType as ContainerType,
463
+ status
464
+ );
465
+ return Promise.resolve({ success: true, data: transitions });
466
+ } catch (error) {
467
+ return Promise.resolve({
468
+ success: false,
469
+ error: error instanceof Error ? error.message : 'Unknown error',
470
+ });
471
+ }
276
472
  }
277
473
 
278
474
  // ============================================================================
@@ -287,94 +483,129 @@ export class DirectAdapter implements DataAdapter {
287
483
  }
288
484
 
289
485
  // ============================================================================
290
- // Dependencies
486
+ // Dependencies (field-based in v2)
291
487
  // ============================================================================
292
488
 
293
489
  async getDependencies(taskId: string): Promise<Result<DependencyInfo>> {
294
- const result = dependencies.getDependencies(taskId, 'both');
490
+ try {
491
+ const taskResult = tasks.getTask(taskId);
492
+ if (!taskResult.success) {
493
+ return { success: false, error: taskResult.error, code: taskResult.code };
494
+ }
295
495
 
296
- if (!result.success) {
297
- return Promise.resolve(result as Result<DependencyInfo>);
298
- }
496
+ const task = taskResult.data;
299
497
 
300
- // Transform to DependencyInfo format
301
- // Dependencies returned have fromTaskId and toTaskId
302
- // - If fromTaskId === taskId, it's a dependency this task creates (blocks something)
303
- // - If toTaskId === taskId, it's a dependency blocking this task (blocked by)
304
-
305
- const deps = result.data;
306
- const blockedByTaskIds = deps
307
- .filter((d) => d.toTaskId === taskId && d.type === 'BLOCKS')
308
- .map((d) => d.fromTaskId);
309
- const blocksTaskIds = deps
310
- .filter((d) => d.fromTaskId === taskId && d.type === 'BLOCKS')
311
- .map((d) => d.toTaskId);
312
-
313
- // Fetch the actual task objects
314
- const blockedByTasks: Task[] = [];
315
- for (const id of blockedByTaskIds) {
316
- const taskResult = tasks.getTask(id);
317
- if (taskResult.success) {
318
- blockedByTasks.push(taskResult.data);
498
+ // blockedBy: fetch each task that blocks this one
499
+ const blockedByTasks: Task[] = [];
500
+ for (const blockerId of task.blockedBy) {
501
+ const blockerResult = tasks.getTask(blockerId);
502
+ if (blockerResult.success) {
503
+ blockedByTasks.push(blockerResult.data);
504
+ }
319
505
  }
320
- }
321
506
 
322
- const blocksTasks: Task[] = [];
323
- for (const id of blocksTaskIds) {
324
- const taskResult = tasks.getTask(id);
325
- if (taskResult.success) {
326
- blocksTasks.push(taskResult.data);
507
+ // blocks: find all tasks that have this task in their blockedBy
508
+ const blocksTasks: Task[] = [];
509
+ const blockedRows = queryAll<{ id: string; blocked_by: string }>(
510
+ `SELECT id, blocked_by FROM tasks WHERE EXISTS (SELECT 1 FROM json_each(tasks.blocked_by) WHERE value = ?)`,
511
+ [taskId]
512
+ );
513
+ for (const row of blockedRows) {
514
+ const blockedTaskResult = tasks.getTask(row.id);
515
+ if (blockedTaskResult.success) {
516
+ blocksTasks.push(blockedTaskResult.data);
517
+ }
327
518
  }
328
- }
329
519
 
330
- const dependencyInfo: DependencyInfo = {
331
- blockedBy: blockedByTasks,
332
- blocks: blocksTasks,
333
- };
520
+ const dependencyInfo: DependencyInfo = {
521
+ blockedBy: blockedByTasks,
522
+ blocks: blocksTasks,
523
+ };
334
524
 
335
- return Promise.resolve({ success: true, data: dependencyInfo });
525
+ return { success: true, data: dependencyInfo };
526
+ } catch (error) {
527
+ return {
528
+ success: false,
529
+ error: error instanceof Error ? error.message : 'Unknown error',
530
+ };
531
+ }
336
532
  }
337
533
 
338
534
  async getBlockedTasks(params?: {
339
535
  projectId?: string;
340
536
  }): Promise<Result<Task[]>> {
341
- return Promise.resolve(
342
- dependencies.getBlockedTasks({
343
- projectId: params?.projectId,
344
- })
345
- );
537
+ try {
538
+ const conditions: string[] = ["blocked_by != '[]'"];
539
+ const values: any[] = [];
540
+
541
+ if (params?.projectId) {
542
+ conditions.push('project_id = ?');
543
+ values.push(params.projectId);
544
+ }
545
+
546
+ const sql = `SELECT * FROM tasks
547
+ WHERE ${conditions.join(' AND ')}
548
+ ORDER BY
549
+ CASE priority WHEN 'HIGH' THEN 1 WHEN 'MEDIUM' THEN 2 WHEN 'LOW' THEN 3 END ASC,
550
+ created_at ASC`;
551
+
552
+ const rows = queryAll<any>(sql, values);
553
+
554
+ // Convert rows to Task objects via getTask for proper mapping
555
+ const blockedTasks: Task[] = [];
556
+ for (const row of rows) {
557
+ const taskResult = tasks.getTask(row.id);
558
+ if (taskResult.success) {
559
+ blockedTasks.push(taskResult.data);
560
+ }
561
+ }
562
+
563
+ return { success: true, data: blockedTasks };
564
+ } catch (error) {
565
+ return {
566
+ success: false,
567
+ error: error instanceof Error ? error.message : 'Unknown error',
568
+ };
569
+ }
346
570
  }
347
571
 
348
572
  async getNextTask(params?: {
349
573
  projectId?: string;
350
574
  }): Promise<Result<Task | null>> {
351
- return Promise.resolve(
352
- dependencies.getNextTask({
353
- projectId: params?.projectId,
354
- })
355
- );
356
- }
575
+ try {
576
+ const conditions: string[] = ["status = 'NEW'", "blocked_by = '[]'"];
577
+ const values: any[] = [];
357
578
 
358
- // ============================================================================
359
- // Workflow
360
- // ============================================================================
579
+ if (params?.projectId) {
580
+ conditions.push('project_id = ?');
581
+ values.push(params.projectId);
582
+ }
361
583
 
362
- async getAllowedTransitions(
363
- containerType: string,
364
- status: string
365
- ): Promise<Result<string[]>> {
366
- try {
367
- // getAllowedTransitions returns string[] directly, not a Result
368
- const transitions = getAllowedTransitions(
369
- containerType as ContainerType,
370
- status
371
- );
372
- return Promise.resolve({ success: true, data: transitions });
584
+ const sql = `SELECT * FROM tasks
585
+ WHERE ${conditions.join(' AND ')}
586
+ ORDER BY
587
+ CASE priority WHEN 'HIGH' THEN 1 WHEN 'MEDIUM' THEN 2 WHEN 'LOW' THEN 3 END ASC,
588
+ complexity ASC,
589
+ created_at ASC
590
+ LIMIT 1`;
591
+
592
+ const row = queryOne<any>(sql, values);
593
+
594
+ if (!row) {
595
+ return { success: true, data: null };
596
+ }
597
+
598
+ const taskResult = tasks.getTask(row.id);
599
+ if (!taskResult.success) {
600
+ return { success: true, data: null };
601
+ }
602
+
603
+ return { success: true, data: taskResult.data };
373
604
  } catch (error) {
374
- return Promise.resolve({
605
+ return {
375
606
  success: false,
376
607
  error: error instanceof Error ? error.message : 'Unknown error',
377
- });
608
+ };
378
609
  }
379
610
  }
380
611
 
@@ -384,12 +615,10 @@ export class DirectAdapter implements DataAdapter {
384
615
 
385
616
  async search(query: string): Promise<Result<SearchResults>> {
386
617
  try {
387
- // Search across all entity types
388
618
  const projectsResult = projects.searchProjects({ query, limit: 10 });
389
619
  const featuresResult = features.searchFeatures({ query, limit: 10 });
390
620
  const tasksResult = tasks.searchTasks({ query, limit: 10 });
391
621
 
392
- // Build search results
393
622
  const results: SearchResults = {
394
623
  projects: projectsResult.success
395
624
  ? projectsResult.data.map((p) => ({