@pixelbyte-software/pixcode 1.42.2 → 1.42.3

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.
@@ -0,0 +1,219 @@
1
+ import type { WorkflowNodeRun, WorkflowRun } from '@/modules/orchestration/workflows/workflow.types.js';
2
+ import { workflowStore } from '@/modules/orchestration/workflows/workflow-store.js';
3
+ import { orchestrationTaskService } from '@/modules/orchestration/tasks/orchestration-task.service.js';
4
+ import type { OrchestrationTask } from '@/modules/orchestration/tasks/orchestration-task.types.js';
5
+
6
+ export const PIXCODE_TASK_RUN_GRAPH_PROTOCOL = 'pixcode.task-run-graph.v1';
7
+
8
+ export type TaskRunGraphCriterionStatus = 'pending' | 'passed' | 'failed';
9
+
10
+ export interface TaskRunGraphCriterion {
11
+ id: string;
12
+ label: string;
13
+ status: TaskRunGraphCriterionStatus;
14
+ source: 'taskmaster' | 'workflow';
15
+ }
16
+
17
+ export interface TaskRunGraphRunSummary {
18
+ id: string;
19
+ workflowId: string;
20
+ status: WorkflowRun['status'];
21
+ startedAt: number;
22
+ finishedAt?: number;
23
+ taskmasterId?: string;
24
+ orchestrationTaskId?: string;
25
+ changedFiles: string[];
26
+ }
27
+
28
+ export interface TaskRunGraph {
29
+ protocol: typeof PIXCODE_TASK_RUN_GRAPH_PROTOCOL;
30
+ projectId: string;
31
+ taskmasterId?: string;
32
+ orchestrationTaskId?: string;
33
+ workflowRuns: TaskRunGraphRunSummary[];
34
+ changedFiles: string[];
35
+ acceptanceCriteria: TaskRunGraphCriterion[];
36
+ status: {
37
+ totalRuns: number;
38
+ completedRuns: number;
39
+ failedRuns: number;
40
+ passedCriteria: number;
41
+ failedCriteria: number;
42
+ };
43
+ }
44
+
45
+ function readString(value: unknown): string | undefined {
46
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
47
+ }
48
+
49
+ function readRecord(value: unknown): Record<string, unknown> | undefined {
50
+ return value && typeof value === 'object' ? value as Record<string, unknown> : undefined;
51
+ }
52
+
53
+ function uniqueStrings(values: Array<string | undefined>): string[] {
54
+ return [...new Set(values.filter((value): value is string => Boolean(value?.trim())))]
55
+ .sort((a, b) => a.localeCompare(b));
56
+ }
57
+
58
+ function taskStatusPassed(status: unknown): boolean {
59
+ const normalized = String(status ?? '').toLocaleLowerCase('en');
60
+ return normalized === 'done' || normalized === 'completed';
61
+ }
62
+
63
+ function taskStatusFailed(status: unknown): boolean {
64
+ const normalized = String(status ?? '').toLocaleLowerCase('en');
65
+ return normalized === 'failed' || normalized === 'blocked' || normalized === 'cancelled' || normalized === 'canceled';
66
+ }
67
+
68
+ function criterionStatus(status: unknown): TaskRunGraphCriterionStatus {
69
+ if (taskStatusPassed(status)) return 'passed';
70
+ if (taskStatusFailed(status)) return 'failed';
71
+ return 'pending';
72
+ }
73
+
74
+ function taskmasterAcceptanceCriteria(taskmasterTask: Record<string, unknown> | undefined): TaskRunGraphCriterion[] {
75
+ if (!taskmasterTask) return [];
76
+
77
+ const criteria: TaskRunGraphCriterion[] = [];
78
+ const testStrategy = readString(taskmasterTask.testStrategy) ?? readString(taskmasterTask.test_strategy);
79
+ if (testStrategy) {
80
+ criteria.push({
81
+ id: 'test-strategy',
82
+ label: testStrategy,
83
+ status: criterionStatus(taskmasterTask.status),
84
+ source: 'taskmaster',
85
+ });
86
+ }
87
+
88
+ const subtasks = Array.isArray(taskmasterTask.subtasks) ? taskmasterTask.subtasks : [];
89
+ subtasks.forEach((subtask, index) => {
90
+ const record = readRecord(subtask);
91
+ if (!record) return;
92
+ const title = readString(record.title);
93
+ if (!title) return;
94
+ criteria.push({
95
+ id: `subtask-${readString(record.id) ?? index + 1}`,
96
+ label: title,
97
+ status: criterionStatus(record.status),
98
+ source: 'taskmaster',
99
+ });
100
+ });
101
+
102
+ return criteria;
103
+ }
104
+
105
+ function metadataTaskmasterId(run: WorkflowRun): string | undefined {
106
+ return readString(run.metadata?.taskmasterId)
107
+ ?? readString(readRecord(run.metadata?.taskGraph)?.taskmasterId)
108
+ ?? readString(readRecord(run.metadata?.replay)?.taskmasterId);
109
+ }
110
+
111
+ function metadataOrchestrationTaskId(run: WorkflowRun): string | undefined {
112
+ return readString(run.metadata?.orchestrationTaskId)
113
+ ?? readString(readRecord(run.metadata?.taskGraph)?.orchestrationTaskId);
114
+ }
115
+
116
+ function artifactChangedFiles(node: WorkflowNodeRun): string[] {
117
+ const files: string[] = [];
118
+
119
+ if (node.handoffArtifact?.changedFiles) {
120
+ files.push(...node.handoffArtifact.changedFiles);
121
+ }
122
+
123
+ for (const artifact of node.artifacts ?? []) {
124
+ const data = readRecord(artifact.data);
125
+ const metadata = readRecord(artifact.metadata);
126
+ const candidates = [
127
+ readString(metadata?.path),
128
+ readString(metadata?.file),
129
+ readString(data?.path),
130
+ readString(data?.file),
131
+ ];
132
+ files.push(...candidates.filter((value): value is string => Boolean(value)));
133
+
134
+ for (const key of ['files', 'changedFiles']) {
135
+ const value = data?.[key] ?? metadata?.[key];
136
+ if (Array.isArray(value)) {
137
+ files.push(...value.map((entry) => readString(entry)).filter((entry): entry is string => Boolean(entry)));
138
+ }
139
+ }
140
+ }
141
+
142
+ return uniqueStrings(files);
143
+ }
144
+
145
+ export function changedFilesFromWorkflowRun(run: WorkflowRun): string[] {
146
+ return uniqueStrings(run.nodeRuns.flatMap((node) => artifactChangedFiles(node)));
147
+ }
148
+
149
+ function workflowRunMatchesTask(run: WorkflowRun, task: OrchestrationTask | undefined, taskmasterId: string | undefined): boolean {
150
+ const runTaskmasterId = metadataTaskmasterId(run);
151
+ const runOrchestrationTaskId = metadataOrchestrationTaskId(run);
152
+ if (taskmasterId && runTaskmasterId === taskmasterId) return true;
153
+ if (task?.id && runOrchestrationTaskId === task.id) return true;
154
+ if (task?.workflowRunIds?.includes(run.id)) return true;
155
+ return false;
156
+ }
157
+
158
+ function runSummary(run: WorkflowRun): TaskRunGraphRunSummary {
159
+ return {
160
+ id: run.id,
161
+ workflowId: run.workflowId,
162
+ status: run.status,
163
+ startedAt: run.startedAt,
164
+ finishedAt: run.finishedAt,
165
+ taskmasterId: metadataTaskmasterId(run),
166
+ orchestrationTaskId: metadataOrchestrationTaskId(run),
167
+ changedFiles: changedFilesFromWorkflowRun(run),
168
+ };
169
+ }
170
+
171
+ export function buildTaskRunGraph({
172
+ projectId,
173
+ taskmasterId,
174
+ taskmasterTask,
175
+ }: {
176
+ projectId: string;
177
+ taskmasterId?: string;
178
+ taskmasterTask?: Record<string, unknown>;
179
+ }): TaskRunGraph {
180
+ const orchestrationTask = taskmasterId
181
+ ? orchestrationTaskService.list(projectId).find((task) => task.taskmasterId === taskmasterId)
182
+ : undefined;
183
+ const workflowRuns = workflowStore
184
+ .listRuns()
185
+ .filter((run) => workflowRunMatchesTask(run, orchestrationTask, taskmasterId))
186
+ .map(runSummary);
187
+ const workflowCriteria: TaskRunGraphCriterion[] = workflowRuns.map((run) => ({
188
+ id: `run-${run.id}`,
189
+ label: `Workflow ${run.workflowId} ${run.status}`,
190
+ status: run.status === 'completed' ? 'passed' : run.status === 'failed' || run.status === 'canceled' ? 'failed' : 'pending',
191
+ source: 'workflow',
192
+ }));
193
+ const acceptanceCriteria = [
194
+ ...taskmasterAcceptanceCriteria(taskmasterTask),
195
+ ...(orchestrationTask?.acceptanceCriteria ?? []),
196
+ ...workflowCriteria,
197
+ ];
198
+ const changedFiles = uniqueStrings([
199
+ ...(orchestrationTask?.changedFiles ?? []),
200
+ ...workflowRuns.flatMap((run) => run.changedFiles),
201
+ ]);
202
+
203
+ return {
204
+ protocol: PIXCODE_TASK_RUN_GRAPH_PROTOCOL,
205
+ projectId,
206
+ taskmasterId,
207
+ orchestrationTaskId: orchestrationTask?.id,
208
+ workflowRuns,
209
+ changedFiles,
210
+ acceptanceCriteria,
211
+ status: {
212
+ totalRuns: workflowRuns.length,
213
+ completedRuns: workflowRuns.filter((run) => run.status === 'completed').length,
214
+ failedRuns: workflowRuns.filter((run) => run.status === 'failed' || run.status === 'canceled').length,
215
+ passedCriteria: acceptanceCriteria.filter((criterion) => criterion.status === 'passed').length,
216
+ failedCriteria: acceptanceCriteria.filter((criterion) => criterion.status === 'failed').length,
217
+ },
218
+ };
219
+ }
@@ -28,6 +28,7 @@ import {
28
28
  workspaceTargetMetadata,
29
29
  } from '@/modules/orchestration/workflows/workspace-target.js';
30
30
  import { workflowStore } from '@/modules/orchestration/workflows/workflow-store.js';
31
+ import { orchestrationTaskService } from '@/modules/orchestration/tasks/orchestration-task.service.js';
31
32
  // @ts-ignore — plain-JS service
32
33
  import {
33
34
  getDefaultProviderModel,
@@ -1215,7 +1216,7 @@ class WorkflowRunner {
1215
1216
  const runtimeWorkflow = expandWorkflowForRun(workflow, metadata);
1216
1217
  validateWorkflow(runtimeWorkflow);
1217
1218
  const workspaceTarget = resolveWorkflowWorkspace(metadata);
1218
- const runMetadata = {
1219
+ const runMetadata: Record<string, unknown> = {
1219
1220
  ...metadata,
1220
1221
  projectPath: workspaceTarget.projectPath,
1221
1222
  selectedProjectPath: workspaceTarget.selectedProjectPath,
@@ -1232,6 +1233,10 @@ class WorkflowRunner {
1232
1233
  metadata: runMetadata,
1233
1234
  };
1234
1235
  workflowStore.setRun(run);
1236
+ const orchestrationTaskId = readString(runMetadata.orchestrationTaskId);
1237
+ if (orchestrationTaskId) {
1238
+ orchestrationTaskService.linkWorkflowRun(orchestrationTaskId, run);
1239
+ }
1235
1240
  void this.execute(runtimeWorkflow, run);
1236
1241
  return run;
1237
1242
  }
@@ -1591,6 +1596,7 @@ class WorkflowRunner {
1591
1596
  } finally {
1592
1597
  run.finishedAt = run.finishedAt ?? Date.now();
1593
1598
  workflowStore.setRun(run);
1599
+ orchestrationTaskService.updateFromWorkflowRun(run);
1594
1600
  notifyWorkflowRunFinished(run);
1595
1601
  this.cancelingRuns.delete(run.id);
1596
1602
  }
@@ -16,6 +16,9 @@ import express from 'express';
16
16
  import crossSpawn from 'cross-spawn';
17
17
 
18
18
  import { orchestrationTaskService } from '@/modules/orchestration/tasks/orchestration-task.service.js';
19
+ import { buildTaskRunGraph } from '@/modules/orchestration/tasks/task-run-graph.js';
20
+ import { workflowRunner } from '@/modules/orchestration/workflows/workflow-runner.js';
21
+ import { workflowStore } from '@/modules/orchestration/workflows/workflow-store.js';
19
22
 
20
23
  import { extractProjectDirectory } from '../projects.js';
21
24
  import {
@@ -167,6 +170,21 @@ function taskMasterExecutionDescription(task) {
167
170
  ].filter(Boolean).join('\n\n');
168
171
  }
169
172
 
173
+ function taskGraphForTask(projectId, task) {
174
+ return buildTaskRunGraph({
175
+ projectId,
176
+ taskmasterId: String(task.id),
177
+ taskmasterTask: task,
178
+ });
179
+ }
180
+
181
+ function attachTaskGraph(projectId, task) {
182
+ return {
183
+ ...task,
184
+ taskGraph: taskGraphForTask(projectId, task),
185
+ };
186
+ }
187
+
170
188
  function buildTaskMasterQueueSummary(projectName, projectPath, tasks) {
171
189
  const normalized = tasks.map((task) => ({
172
190
  ...task,
@@ -389,22 +407,26 @@ router.delete('/install/:jobId', async (req, res) => {
389
407
  router.get('/tasks/:projectName', async (req, res) => {
390
408
  try {
391
409
  const { projectName } = req.params;
410
+ const projectId = typeof req.query.projectId === 'string' && req.query.projectId.trim()
411
+ ? req.query.projectId.trim()
412
+ : projectName;
392
413
 
393
414
  const { projectPath, transformedTasks, currentTag } = await readTaskMasterTasks(projectName);
415
+ const tasksWithGraph = transformedTasks.map((task) => attachTaskGraph(projectId, task));
394
416
 
395
417
  res.json({
396
418
  projectName,
397
419
  projectPath,
398
- tasks: transformedTasks,
420
+ tasks: tasksWithGraph,
399
421
  currentTag,
400
- totalTasks: transformedTasks.length,
422
+ totalTasks: tasksWithGraph.length,
401
423
  tasksByStatus: {
402
- pending: transformedTasks.filter(t => t.status === 'pending').length,
403
- 'in-progress': transformedTasks.filter(t => t.status === 'in-progress').length,
404
- done: transformedTasks.filter(t => t.status === 'done').length,
405
- review: transformedTasks.filter(t => t.status === 'review').length,
406
- deferred: transformedTasks.filter(t => t.status === 'deferred').length,
407
- cancelled: transformedTasks.filter(t => t.status === 'cancelled').length
424
+ pending: tasksWithGraph.filter(t => t.status === 'pending').length,
425
+ 'in-progress': tasksWithGraph.filter(t => t.status === 'in-progress').length,
426
+ done: tasksWithGraph.filter(t => t.status === 'done').length,
427
+ review: tasksWithGraph.filter(t => t.status === 'review').length,
428
+ deferred: tasksWithGraph.filter(t => t.status === 'deferred').length,
429
+ cancelled: tasksWithGraph.filter(t => t.status === 'cancelled').length
408
430
  },
409
431
  timestamp: new Date().toISOString()
410
432
  });
@@ -459,6 +481,9 @@ router.get('/queue/:projectName', async (req, res) => {
459
481
  router.get('/task/:projectName/:taskId', async (req, res) => {
460
482
  try {
461
483
  const { projectName, taskId } = req.params;
484
+ const projectId = typeof req.query.projectId === 'string' && req.query.projectId.trim()
485
+ ? req.query.projectId.trim()
486
+ : projectName;
462
487
  const { projectPath, transformedTasks } = await readTaskMasterTasks(projectName);
463
488
  const task = transformedTasks.find((candidate) => String(candidate.id) === String(taskId));
464
489
  if (!task) {
@@ -472,7 +497,8 @@ router.get('/task/:projectName/:taskId', async (req, res) => {
472
497
  success: true,
473
498
  projectName,
474
499
  projectPath,
475
- task,
500
+ task: attachTaskGraph(projectId, task),
501
+ taskGraph: taskGraphForTask(projectId, task),
476
502
  execution: {
477
503
  supportsProvider: true,
478
504
  supportsModel: true,
@@ -505,6 +531,7 @@ router.post('/execute/:projectName/:taskId', async (req, res) => {
505
531
  : '';
506
532
  const model = typeof req.body?.model === 'string' ? req.body.model : undefined;
507
533
  const permissionMode = typeof req.body?.permissionMode === 'string' ? req.body.permissionMode : undefined;
534
+ const workflowId = typeof req.body?.workflowId === 'string' ? req.body.workflowId : undefined;
508
535
  const fallbackProvider = typeof req.body?.fallbackProvider === 'string' ? req.body.fallbackProvider : undefined;
509
536
  const workerSlot = Number.isInteger(req.body?.workerSlot) ? req.body.workerSlot : undefined;
510
537
  const isolation = ['host', 'worktree', 'docker'].includes(req.body?.isolation)
@@ -544,15 +571,49 @@ router.post('/execute/:projectName/:taskId', async (req, res) => {
544
571
  },
545
572
  });
546
573
 
547
- const dispatchedTask = await orchestrationTaskService.dispatch(orchestrationTask.id, {
548
- adapterId,
549
- isolation,
550
- projectPath,
551
- model,
552
- permissionMode,
553
- fallbackProvider,
554
- workerSlot,
555
- });
574
+ let dispatchedTask;
575
+ let workflowRun;
576
+ if (workflowId) {
577
+ const workflow = workflowStore.getWorkflow(workflowId);
578
+ if (!workflow) {
579
+ return res.status(404).json({
580
+ success: false,
581
+ error: 'Workflow not found',
582
+ message: `Workflow "${workflowId}" was not found`
583
+ });
584
+ }
585
+ workflowRun = workflowRunner.start(workflow, taskMasterExecutionDescription(task), {
586
+ projectId,
587
+ projectName,
588
+ projectPath,
589
+ selectedProjectPath: projectPath,
590
+ taskmasterId: String(task.id),
591
+ taskmasterTaskTitle: task.title,
592
+ orchestrationTaskId: orchestrationTask.id,
593
+ workflowName: workflow.name,
594
+ settings: {
595
+ isolation,
596
+ keepWorkspace: true,
597
+ baseRef: 'HEAD',
598
+ },
599
+ taskGraph: {
600
+ taskmasterId: String(task.id),
601
+ orchestrationTaskId: orchestrationTask.id,
602
+ source: 'taskmaster',
603
+ },
604
+ });
605
+ dispatchedTask = orchestrationTaskService.linkWorkflowRun(orchestrationTask.id, workflowRun) ?? orchestrationTask;
606
+ } else {
607
+ dispatchedTask = await orchestrationTaskService.dispatch(orchestrationTask.id, {
608
+ adapterId,
609
+ isolation,
610
+ projectPath,
611
+ model,
612
+ permissionMode,
613
+ fallbackProvider,
614
+ workerSlot,
615
+ });
616
+ }
556
617
 
557
618
  res.json({
558
619
  success: true,
@@ -565,8 +626,11 @@ router.post('/execute/:projectName/:taskId', async (req, res) => {
565
626
  permissionMode,
566
627
  fallbackProvider,
567
628
  workerSlot,
629
+ workflowId,
568
630
  },
569
- task: dispatchedTask
631
+ task: dispatchedTask,
632
+ run: workflowRun,
633
+ taskGraph: taskGraphForTask(projectId, task),
570
634
  });
571
635
  } catch (error) {
572
636
  console.error('TaskMaster execute error:', error);
@@ -591,14 +655,17 @@ router.post('/sync-orchestration/:projectName', async (req, res) => {
591
655
  ? req.body.projectId.trim()
592
656
  : projectName;
593
657
 
594
- const syncedTasks = transformedTasks.map((task) =>
595
- orchestrationTaskService.upsertFromTaskMaster({
658
+ const syncedTasks = transformedTasks.map((task) => {
659
+ const taskGraph = taskGraphForTask(projectId, task);
660
+ return orchestrationTaskService.upsertFromTaskMaster({
596
661
  projectId,
597
662
  taskmasterId: String(task.id),
598
663
  title: task.title,
599
664
  description: task.description,
600
- })
601
- );
665
+ acceptanceCriteria: taskGraph.acceptanceCriteria,
666
+ changedFiles: taskGraph.changedFiles,
667
+ });
668
+ });
602
669
 
603
670
  res.json({
604
671
  success: true,