@pixelbyte-software/pixcode 1.42.2 → 1.42.4

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 (42) hide show
  1. package/dist/assets/{index-CMeiCqQf.js → index-cTGs3Dvx.js} +72 -72
  2. package/dist/index.html +1 -1
  3. package/dist-server/server/claude-sdk.js +23 -2
  4. package/dist-server/server/claude-sdk.js.map +1 -1
  5. package/dist-server/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.js.map +1 -1
  6. package/dist-server/server/modules/orchestration/a2a/adapters/claude-code.adapter.js +2 -0
  7. package/dist-server/server/modules/orchestration/a2a/adapters/claude-code.adapter.js.map +1 -1
  8. package/dist-server/server/modules/orchestration/a2a/routes.js +2 -0
  9. package/dist-server/server/modules/orchestration/a2a/routes.js.map +1 -1
  10. package/dist-server/server/modules/orchestration/index.js +1 -0
  11. package/dist-server/server/modules/orchestration/index.js.map +1 -1
  12. package/dist-server/server/modules/orchestration/security/permission-policy.js +269 -0
  13. package/dist-server/server/modules/orchestration/security/permission-policy.js.map +1 -0
  14. package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js +86 -0
  15. package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js.map +1 -1
  16. package/dist-server/server/modules/orchestration/tasks/task-run-graph.js +158 -0
  17. package/dist-server/server/modules/orchestration/tasks/task-run-graph.js.map +1 -0
  18. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +121 -1
  19. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
  20. package/dist-server/server/modules/orchestration/workflows/workflow-trace.js +32 -0
  21. package/dist-server/server/modules/orchestration/workflows/workflow-trace.js.map +1 -1
  22. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js +103 -0
  23. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js.map +1 -1
  24. package/dist-server/server/routes/taskmaster.js +93 -25
  25. package/dist-server/server/routes/taskmaster.js.map +1 -1
  26. package/package.json +1 -1
  27. package/scripts/smoke/permission-policy.mjs +50 -0
  28. package/scripts/smoke/taskmaster-run-graph.mjs +55 -0
  29. package/server/claude-sdk.js +24 -2
  30. package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +8 -0
  31. package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +2 -0
  32. package/server/modules/orchestration/a2a/routes.ts +6 -0
  33. package/server/modules/orchestration/index.ts +18 -0
  34. package/server/modules/orchestration/security/permission-policy.ts +401 -0
  35. package/server/modules/orchestration/tasks/orchestration-task.service.ts +94 -0
  36. package/server/modules/orchestration/tasks/orchestration-task.types.ts +10 -0
  37. package/server/modules/orchestration/tasks/task-run-graph.ts +219 -0
  38. package/server/modules/orchestration/workflows/workflow-runner.ts +148 -2
  39. package/server/modules/orchestration/workflows/workflow-trace.ts +32 -0
  40. package/server/modules/orchestration/workflows/workflow.routes.ts +121 -0
  41. package/server/modules/orchestration/workflows/workflow.types.ts +9 -1
  42. package/server/routes/taskmaster.js +90 -23
@@ -19,6 +19,15 @@ export { AbstractA2AAdapter } from './a2a/adapters/abstract-a2a.adapter.js';
19
19
  export { a2aBus } from './a2a/bus.js';
20
20
  export { portWatcher } from './preview/port-watcher.js';
21
21
  export { createPreviewProxyRouter } from './preview/preview-proxy.js';
22
+ export {
23
+ DEFAULT_PERMISSION_POLICY,
24
+ PERMISSION_CAPABILITIES,
25
+ PERMISSION_POLICY_MODES,
26
+ PIXCODE_PERMISSION_POLICY_PROTOCOL,
27
+ evaluatePermissionRequest,
28
+ normalizePermissionPolicy,
29
+ resolvePermissionPolicyFromMetadata,
30
+ } from './security/permission-policy.js';
22
31
  export { createOrchestrationTaskRouter } from './tasks/orchestration-task.routes.js';
23
32
  export { orchestrationTaskService } from './tasks/orchestration-task.service.js';
24
33
  export { createWorkflowRouter } from './workflows/workflow.routes.js';
@@ -42,6 +51,15 @@ export type {
42
51
  PortEvent,
43
52
  PreviewArtifactData,
44
53
  } from './preview/types.js';
54
+ export type {
55
+ PermissionApprovalRequest,
56
+ PermissionCapability,
57
+ PermissionDecision,
58
+ PermissionPolicy,
59
+ PermissionPolicyContext,
60
+ PermissionPolicyEvent,
61
+ PermissionPolicyMode,
62
+ } from './security/permission-policy.js';
45
63
  export type {
46
64
  CreateOrchestrationTaskInput,
47
65
  DispatchOrchestrationTaskInput,
@@ -0,0 +1,401 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+
4
+ export const PIXCODE_PERMISSION_POLICY_PROTOCOL = 'pixcode.permission-policy.v1' as const;
5
+
6
+ export const PERMISSION_CAPABILITIES = [
7
+ 'shell',
8
+ 'file_write',
9
+ 'external_directory',
10
+ 'network',
11
+ 'secret',
12
+ ] as const;
13
+
14
+ export const PERMISSION_POLICY_MODES = ['allow', 'deny', 'prompt', 'audit'] as const;
15
+
16
+ export type PermissionCapability = typeof PERMISSION_CAPABILITIES[number];
17
+ export type PermissionPolicyMode = typeof PERMISSION_POLICY_MODES[number];
18
+ export type PermissionDecisionStatus = 'allowed' | 'denied' | 'needs_approval';
19
+
20
+ export interface PermissionPolicy {
21
+ protocol: typeof PIXCODE_PERMISSION_POLICY_PROTOCOL;
22
+ modes: Record<PermissionCapability, PermissionPolicyMode>;
23
+ allowedExternalDirectories: string[];
24
+ audit: boolean;
25
+ }
26
+
27
+ export interface PermissionRequest {
28
+ requestId?: string;
29
+ source: 'workflow_node' | 'provider_tool' | 'api' | 'shell' | 'file' | string;
30
+ capability?: PermissionCapability;
31
+ capabilities?: PermissionCapability[];
32
+ toolName?: string;
33
+ command?: string;
34
+ input?: unknown;
35
+ cwd?: string;
36
+ workspacePath?: string;
37
+ targetPaths?: string[];
38
+ summary?: string;
39
+ }
40
+
41
+ export interface PermissionPolicyContext {
42
+ runId?: string;
43
+ nodeId?: string;
44
+ workflowId?: string;
45
+ adapterId?: string;
46
+ agentLabel?: string;
47
+ userId?: string | number | null;
48
+ }
49
+
50
+ export interface PermissionApprovalRequest {
51
+ id: string;
52
+ protocol: typeof PIXCODE_PERMISSION_POLICY_PROTOCOL;
53
+ status: 'pending' | 'allowed' | 'denied' | 'canceled';
54
+ capabilities: PermissionCapability[];
55
+ source: string;
56
+ toolName?: string;
57
+ runId?: string;
58
+ nodeId?: string;
59
+ workflowId?: string;
60
+ adapterId?: string;
61
+ agentLabel?: string;
62
+ summary?: string;
63
+ message: string;
64
+ createdAt: number;
65
+ resolvedAt?: number;
66
+ resolvedBy?: string | number | null;
67
+ resolutionMessage?: string;
68
+ }
69
+
70
+ export interface PermissionPolicyEvent {
71
+ id: string;
72
+ protocol: typeof PIXCODE_PERMISSION_POLICY_PROTOCOL;
73
+ status: PermissionDecisionStatus;
74
+ behavior: 'allow' | 'deny' | 'prompt';
75
+ capabilities: PermissionCapability[];
76
+ source: string;
77
+ toolName?: string;
78
+ summary?: string;
79
+ message: string;
80
+ modeByCapability: Partial<Record<PermissionCapability, PermissionPolicyMode>>;
81
+ audit: boolean;
82
+ runId?: string;
83
+ nodeId?: string;
84
+ workflowId?: string;
85
+ adapterId?: string;
86
+ agentLabel?: string;
87
+ createdAt: number;
88
+ }
89
+
90
+ export interface PermissionDecision {
91
+ protocol: typeof PIXCODE_PERMISSION_POLICY_PROTOCOL;
92
+ requestId: string;
93
+ status: PermissionDecisionStatus;
94
+ behavior: 'allow' | 'deny' | 'prompt';
95
+ capabilities: PermissionCapability[];
96
+ modeByCapability: Partial<Record<PermissionCapability, PermissionPolicyMode>>;
97
+ message: string;
98
+ audit: boolean;
99
+ event: PermissionPolicyEvent;
100
+ approvalRequest?: PermissionApprovalRequest;
101
+ }
102
+
103
+ const DEFAULT_MODES: Record<PermissionCapability, PermissionPolicyMode> = {
104
+ shell: 'audit',
105
+ file_write: 'audit',
106
+ external_directory: 'audit',
107
+ network: 'audit',
108
+ secret: 'audit',
109
+ };
110
+
111
+ const NETWORK_COMMAND_PATTERN = /\b(curl|wget|fetch|ssh|scp|rsync|gh|git\s+(?:clone|fetch|pull|push)|npm\s+(?:install|publish|view|pack|audit)|pnpm\s+(?:install|publish)|yarn\s+(?:install|publish)|pip\s+install|cargo\s+(?:install|publish)|go\s+(?:get|install)|docker\s+(?:pull|push|run))\b|https?:\/\//iu;
112
+ const FILE_WRITE_COMMAND_PATTERN = /(^|\s)(apply_patch|touch|mkdir|rm|mv|cp|tee|sed\s+-i|perl\s+-pi|truncate|chmod|chown)\b|>>?|\b(write|edit|patch|delete|create)\s+(?:file|folder|directory)\b/iu;
113
+ const SECRET_COMMAND_PATTERN = /\b(printenv|env|set)\b|\b(cat|type|less|more)\s+(\S*\.env\b|\S*secret\S*|\S*token\S*|\S*key\S*)/iu;
114
+ const SECRET_VALUE_PATTERN = /\b(?:sk|ghp|github_pat|glpat|npm)_[A-Za-z0-9_=-]{12,}\b|(?:api[_-]?key|secret|token|password)\s*[:=]\s*['"]?[A-Za-z0-9_./+=-]{8,}/iu;
115
+
116
+ function readRecord(value: unknown): Record<string, unknown> | undefined {
117
+ return value && typeof value === 'object' ? value as Record<string, unknown> : undefined;
118
+ }
119
+
120
+ function readString(value: unknown): string | undefined {
121
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
122
+ }
123
+
124
+ function readMode(value: unknown): PermissionPolicyMode | undefined {
125
+ return typeof value === 'string' && (PERMISSION_POLICY_MODES as readonly string[]).includes(value)
126
+ ? value as PermissionPolicyMode
127
+ : undefined;
128
+ }
129
+
130
+ function uniqueCapabilities(values: Array<PermissionCapability | undefined>): PermissionCapability[] {
131
+ return [...new Set(values.filter((value): value is PermissionCapability => Boolean(value)))];
132
+ }
133
+
134
+ function requestId(value?: string): string {
135
+ return value?.trim() || `perm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
136
+ }
137
+
138
+ function normalizePath(value: string | undefined): string | undefined {
139
+ if (!value?.trim()) return undefined;
140
+ try {
141
+ return path.resolve(value);
142
+ } catch {
143
+ return undefined;
144
+ }
145
+ }
146
+
147
+ function isInsidePath(basePath: string | undefined, candidatePath: string | undefined): boolean {
148
+ const base = normalizePath(basePath);
149
+ const candidate = normalizePath(candidatePath);
150
+ if (!base || !candidate) return true;
151
+ if (candidate === base) return true;
152
+ const relative = path.relative(base, candidate);
153
+ return Boolean(relative && !relative.startsWith('..') && !path.isAbsolute(relative));
154
+ }
155
+
156
+ function collectInputPaths(input: unknown): string[] {
157
+ const paths: string[] = [];
158
+ const visit = (value: unknown) => {
159
+ if (typeof value === 'string') {
160
+ if (/^(?:[A-Za-z]:[\\/]|\/|~\/|\.\.?[\\/])/u.test(value.trim())) {
161
+ paths.push(value.trim().replace(/^~(?=$|[\\/])/u, os.homedir()));
162
+ }
163
+ return;
164
+ }
165
+ if (Array.isArray(value)) {
166
+ value.forEach(visit);
167
+ return;
168
+ }
169
+ const record = readRecord(value);
170
+ if (!record) return;
171
+ for (const [key, nested] of Object.entries(record)) {
172
+ if (/path|file|dir|cwd|target|source|destination/iu.test(key)) {
173
+ visit(nested);
174
+ }
175
+ }
176
+ };
177
+ visit(input);
178
+ return paths;
179
+ }
180
+
181
+ function inferCapabilities(request: PermissionRequest): PermissionCapability[] {
182
+ const capabilities: PermissionCapability[] = [];
183
+ if (request.capability) capabilities.push(request.capability);
184
+ if (Array.isArray(request.capabilities)) capabilities.push(...request.capabilities);
185
+
186
+ const toolName = request.toolName?.toLocaleLowerCase('en') ?? '';
187
+ const command = [
188
+ request.command,
189
+ typeof request.input === 'string' ? request.input : undefined,
190
+ readString(readRecord(request.input)?.command),
191
+ readString(readRecord(request.input)?.query),
192
+ ].filter(Boolean).join('\n');
193
+ const inputText = [
194
+ command,
195
+ typeof request.summary === 'string' ? request.summary : undefined,
196
+ typeof request.input === 'object' ? JSON.stringify(request.input) : undefined,
197
+ ].filter(Boolean).join('\n');
198
+
199
+ if (toolName === 'bash' || toolName.includes('shell') || toolName.includes('terminal') || command.trim()) {
200
+ capabilities.push('shell');
201
+ }
202
+ if (/write|edit|multiedit|notebookedit|delete|patch/iu.test(toolName) || FILE_WRITE_COMMAND_PATTERN.test(command)) {
203
+ capabilities.push('file_write');
204
+ }
205
+ if (/webfetch|websearch|fetch|search/iu.test(toolName) || NETWORK_COMMAND_PATTERN.test(command)) {
206
+ capabilities.push('network');
207
+ }
208
+ if (/secret|credential|token|keychain|vault/iu.test(toolName) || SECRET_COMMAND_PATTERN.test(command) || SECRET_VALUE_PATTERN.test(inputText)) {
209
+ capabilities.push('secret');
210
+ }
211
+
212
+ const targetPaths = [
213
+ ...(Array.isArray(request.targetPaths) ? request.targetPaths : []),
214
+ ...collectInputPaths(request.input),
215
+ ];
216
+ if (targetPaths.some((targetPath) => !isInsidePath(request.workspacePath ?? request.cwd, targetPath))) {
217
+ capabilities.push('external_directory');
218
+ }
219
+
220
+ return uniqueCapabilities(capabilities);
221
+ }
222
+
223
+ export const DEFAULT_PERMISSION_POLICY: PermissionPolicy = {
224
+ protocol: PIXCODE_PERMISSION_POLICY_PROTOCOL,
225
+ modes: DEFAULT_MODES,
226
+ allowedExternalDirectories: [],
227
+ audit: true,
228
+ };
229
+
230
+ export function normalizePermissionPolicy(value?: unknown): PermissionPolicy {
231
+ const record = readRecord(value);
232
+ const modeRecord = readRecord(record?.modes) ?? readRecord(record?.rules) ?? record ?? {};
233
+ const modes = { ...DEFAULT_MODES };
234
+ for (const capability of PERMISSION_CAPABILITIES) {
235
+ modes[capability] = readMode(modeRecord[capability]) ?? modes[capability];
236
+ }
237
+
238
+ const allowedExternalDirectories = Array.isArray(record?.allowedExternalDirectories)
239
+ ? record.allowedExternalDirectories
240
+ .map((item) => readString(item))
241
+ .filter((item): item is string => Boolean(item))
242
+ : [];
243
+
244
+ return {
245
+ protocol: PIXCODE_PERMISSION_POLICY_PROTOCOL,
246
+ modes,
247
+ allowedExternalDirectories,
248
+ audit: typeof record?.audit === 'boolean' ? record.audit : true,
249
+ };
250
+ }
251
+
252
+ export function resolvePermissionPolicyFromMetadata(metadata?: Record<string, unknown>): PermissionPolicy {
253
+ const settings = readRecord(metadata?.settings);
254
+ return normalizePermissionPolicy(metadata?.permissionPolicy ?? settings?.permissionPolicy);
255
+ }
256
+
257
+ export function redactPermissionText(
258
+ value: string | undefined,
259
+ context?: { workspacePath?: string; cwd?: string; projectPath?: string },
260
+ ): string | undefined {
261
+ if (!value?.trim()) return undefined;
262
+ const candidates = [
263
+ os.homedir(),
264
+ context?.workspacePath,
265
+ context?.cwd,
266
+ context?.projectPath,
267
+ ].filter((item): item is string => Boolean(item && item.length > 2));
268
+
269
+ let text = value;
270
+ for (const candidate of candidates) {
271
+ text = text.split(candidate).join('[workspace]');
272
+ }
273
+
274
+ return text
275
+ .replace(SECRET_VALUE_PATTERN, '[redacted-secret]')
276
+ .replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/gu, '[redacted-email]')
277
+ .trim();
278
+ }
279
+
280
+ function modeAllowsExternalDirectory(
281
+ request: PermissionRequest,
282
+ policy: PermissionPolicy,
283
+ ): boolean {
284
+ const targetPaths = [
285
+ ...(Array.isArray(request.targetPaths) ? request.targetPaths : []),
286
+ ...collectInputPaths(request.input),
287
+ ];
288
+ if (!targetPaths.length || !policy.allowedExternalDirectories.length) return false;
289
+
290
+ return targetPaths.every((targetPath) =>
291
+ policy.allowedExternalDirectories.some((allowedPath) => isInsidePath(allowedPath, targetPath)),
292
+ );
293
+ }
294
+
295
+ export function createPermissionApprovalRequest(
296
+ decision: Omit<PermissionDecision, 'approvalRequest'>,
297
+ context?: PermissionPolicyContext,
298
+ ): PermissionApprovalRequest {
299
+ return {
300
+ id: decision.requestId,
301
+ protocol: PIXCODE_PERMISSION_POLICY_PROTOCOL,
302
+ status: 'pending',
303
+ capabilities: decision.capabilities,
304
+ source: decision.event.source,
305
+ toolName: decision.event.toolName,
306
+ runId: context?.runId,
307
+ nodeId: context?.nodeId,
308
+ workflowId: context?.workflowId,
309
+ adapterId: context?.adapterId,
310
+ agentLabel: context?.agentLabel,
311
+ summary: decision.event.summary,
312
+ message: decision.message,
313
+ createdAt: decision.event.createdAt,
314
+ };
315
+ }
316
+
317
+ export function evaluatePermissionRequest({
318
+ policy: policyInput,
319
+ request,
320
+ context,
321
+ }: {
322
+ policy?: unknown;
323
+ request: PermissionRequest;
324
+ context?: PermissionPolicyContext;
325
+ }): PermissionDecision {
326
+ const policy = normalizePermissionPolicy(policyInput);
327
+ const id = requestId(request.requestId);
328
+ const capabilities = inferCapabilities(request);
329
+ const modeByCapability: Partial<Record<PermissionCapability, PermissionPolicyMode>> = {};
330
+
331
+ for (const capability of capabilities) {
332
+ const mode = capability === 'external_directory' && modeAllowsExternalDirectory(request, policy)
333
+ ? 'allow'
334
+ : policy.modes[capability];
335
+ modeByCapability[capability] = mode;
336
+ }
337
+
338
+ const modes = Object.values(modeByCapability);
339
+ const behavior: PermissionDecision['behavior'] = modes.includes('deny')
340
+ ? 'deny'
341
+ : modes.includes('prompt')
342
+ ? 'prompt'
343
+ : 'allow';
344
+ const status: PermissionDecisionStatus = behavior === 'deny'
345
+ ? 'denied'
346
+ : behavior === 'prompt'
347
+ ? 'needs_approval'
348
+ : 'allowed';
349
+ const summary = redactPermissionText(
350
+ request.summary
351
+ ?? request.command
352
+ ?? (request.toolName ? `${request.toolName} tool request` : `${request.source} permission request`),
353
+ {
354
+ workspacePath: request.workspacePath,
355
+ cwd: request.cwd,
356
+ },
357
+ );
358
+ const capabilityText = capabilities.length ? capabilities.join(', ') : 'unclassified';
359
+ const message = behavior === 'deny'
360
+ ? `Permission policy denied ${capabilityText}.`
361
+ : behavior === 'prompt'
362
+ ? `Permission policy requires approval for ${capabilityText}.`
363
+ : `Permission policy allowed ${capabilityText}.`;
364
+ const event: PermissionPolicyEvent = {
365
+ id,
366
+ protocol: PIXCODE_PERMISSION_POLICY_PROTOCOL,
367
+ status,
368
+ behavior,
369
+ capabilities,
370
+ source: request.source,
371
+ toolName: request.toolName,
372
+ summary,
373
+ message,
374
+ modeByCapability,
375
+ audit: policy.audit || modes.includes('audit'),
376
+ runId: context?.runId,
377
+ nodeId: context?.nodeId,
378
+ workflowId: context?.workflowId,
379
+ adapterId: context?.adapterId,
380
+ agentLabel: context?.agentLabel,
381
+ createdAt: Date.now(),
382
+ };
383
+ const decisionBase: Omit<PermissionDecision, 'approvalRequest'> = {
384
+ protocol: PIXCODE_PERMISSION_POLICY_PROTOCOL,
385
+ requestId: id,
386
+ status,
387
+ behavior,
388
+ capabilities,
389
+ modeByCapability,
390
+ message,
391
+ audit: event.audit,
392
+ event,
393
+ };
394
+
395
+ return {
396
+ ...decisionBase,
397
+ approvalRequest: behavior === 'prompt'
398
+ ? createPermissionApprovalRequest(decisionBase, context)
399
+ : undefined,
400
+ };
401
+ }
@@ -2,6 +2,7 @@ import { OrchestrationTaskStore } from '@/modules/orchestration/tasks/orchestrat
2
2
  import type { CreateOrchestrationTaskInput, DispatchOrchestrationTaskInput, OrchestrationTask } from '@/modules/orchestration/tasks/orchestration-task.types.js';
3
3
  import { a2aBus } from '@/modules/orchestration/a2a/bus.js';
4
4
  import type { TaskState } from '@/modules/orchestration/a2a/types.js';
5
+ import type { WorkflowNodeRun, WorkflowRun } from '@/modules/orchestration/workflows/workflow.types.js';
5
6
 
6
7
  function newId(prefix: string): string {
7
8
  return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
@@ -9,6 +10,52 @@ function newId(prefix: string): string {
9
10
 
10
11
  const TERMINAL_A2A_STATES: TaskState[] = ['completed', 'canceled', 'failed'];
11
12
 
13
+ function uniqueStrings(values: Array<string | undefined>): string[] {
14
+ return [...new Set(values.filter((value): value is string => Boolean(value?.trim())))]
15
+ .sort((a, b) => a.localeCompare(b));
16
+ }
17
+
18
+ function readRecord(value: unknown): Record<string, unknown> | undefined {
19
+ return value && typeof value === 'object' ? value as Record<string, unknown> : undefined;
20
+ }
21
+
22
+ function readString(value: unknown): string | undefined {
23
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
24
+ }
25
+
26
+ function changedFilesFromNode(node: WorkflowNodeRun): string[] {
27
+ const files: string[] = [];
28
+ if (node.handoffArtifact?.changedFiles) {
29
+ files.push(...node.handoffArtifact.changedFiles);
30
+ }
31
+ for (const artifact of node.artifacts ?? []) {
32
+ const data = readRecord(artifact.data);
33
+ const metadata = readRecord(artifact.metadata);
34
+ files.push(
35
+ ...[readString(data?.path), readString(data?.file), readString(metadata?.path), readString(metadata?.file)]
36
+ .filter((value): value is string => Boolean(value)),
37
+ );
38
+ for (const key of ['files', 'changedFiles']) {
39
+ const value = data?.[key] ?? metadata?.[key];
40
+ if (Array.isArray(value)) {
41
+ files.push(...value.map((entry) => readString(entry)).filter((entry): entry is string => Boolean(entry)));
42
+ }
43
+ }
44
+ }
45
+ return uniqueStrings(files);
46
+ }
47
+
48
+ function changedFilesFromWorkflowRun(run: WorkflowRun): string[] {
49
+ return uniqueStrings(run.nodeRuns.flatMap((node) => changedFilesFromNode(node)));
50
+ }
51
+
52
+ function workflowRunState(run: WorkflowRun): OrchestrationTask['state'] {
53
+ if (run.status === 'completed') return 'done';
54
+ if (run.status === 'failed') return 'failed';
55
+ if (run.status === 'canceled') return 'canceled';
56
+ return 'in_progress';
57
+ }
58
+
12
59
  class OrchestrationTaskService {
13
60
  private store: OrchestrationTaskStore;
14
61
 
@@ -33,6 +80,8 @@ class OrchestrationTaskService {
33
80
  title: input.title,
34
81
  description: input.description,
35
82
  taskmasterId: input.taskmasterId,
83
+ acceptanceCriteria: input.acceptanceCriteria,
84
+ changedFiles: input.changedFiles,
36
85
  state: 'todo',
37
86
  createdAt: now,
38
87
  updatedAt: now,
@@ -48,6 +97,8 @@ class OrchestrationTaskService {
48
97
  if (existing) {
49
98
  existing.title = input.title;
50
99
  existing.description = input.description;
100
+ existing.acceptanceCriteria = input.acceptanceCriteria ?? existing.acceptanceCriteria;
101
+ existing.changedFiles = uniqueStrings([...(existing.changedFiles ?? []), ...(input.changedFiles ?? [])]);
51
102
  existing.updatedAt = Date.now();
52
103
  this.store.set(existing);
53
104
  return existing;
@@ -99,6 +150,49 @@ class OrchestrationTaskService {
99
150
  return task;
100
151
  }
101
152
 
153
+ linkWorkflowRun(taskId: string, run: WorkflowRun): OrchestrationTask | undefined {
154
+ const task = this.store.get(taskId);
155
+ if (!task) return undefined;
156
+ task.workflowRunIds = uniqueStrings([...(task.workflowRunIds ?? []), run.id]);
157
+ task.state = workflowRunState(run);
158
+ task.updatedAt = Date.now();
159
+ this.store.set(task);
160
+ return task;
161
+ }
162
+
163
+ updateFromWorkflowRun(run: WorkflowRun): OrchestrationTask | undefined {
164
+ const metadata = run.metadata ?? {};
165
+ const taskId = readString(metadata.orchestrationTaskId);
166
+ const taskmasterId = readString(metadata.taskmasterId);
167
+ const task = taskId
168
+ ? this.store.get(taskId)
169
+ : taskmasterId
170
+ ? this.store.list(readString(metadata.projectId)).find((candidate) => candidate.taskmasterId === taskmasterId)
171
+ : undefined;
172
+ if (!task) return undefined;
173
+
174
+ const changedFiles = changedFilesFromWorkflowRun(run);
175
+ task.workflowRunIds = uniqueStrings([...(task.workflowRunIds ?? []), run.id]);
176
+ task.changedFiles = uniqueStrings([...(task.changedFiles ?? []), ...changedFiles]);
177
+ task.state = workflowRunState(run);
178
+ task.acceptanceCriteria = [
179
+ ...(task.acceptanceCriteria ?? []).filter((criterion) => criterion.id !== `run-${run.id}`),
180
+ {
181
+ id: `run-${run.id}`,
182
+ label: `Workflow ${run.workflowId} ${run.status}`,
183
+ status: run.status === 'completed' ? 'passed' : run.status === 'failed' || run.status === 'canceled' ? 'failed' : 'pending',
184
+ source: 'workflow',
185
+ },
186
+ ];
187
+ task.updatedAt = Date.now();
188
+ this.store.set(task);
189
+
190
+ if (task.taskmasterId && task.state === 'done') {
191
+ this.syncTaskMasterStatus(task.taskmasterId, 'done');
192
+ }
193
+ return task;
194
+ }
195
+
102
196
  updateState(taskId: string, state: OrchestrationTask['state']): OrchestrationTask | undefined {
103
197
  const task = this.store.get(taskId);
104
198
  if (!task) return undefined;
@@ -3,11 +3,19 @@ export type OrchestrationTaskState = 'todo' | 'in_progress' | 'in_review' | 'don
3
3
  export interface OrchestrationTask {
4
4
  id: string;
5
5
  a2aTaskId?: string;
6
+ workflowRunIds?: string[];
6
7
  taskmasterId?: string;
7
8
  projectId: string;
8
9
  title: string;
9
10
  description?: string;
10
11
  state: OrchestrationTaskState;
12
+ acceptanceCriteria?: Array<{
13
+ id: string;
14
+ label: string;
15
+ status: 'pending' | 'passed' | 'failed';
16
+ source: 'taskmaster' | 'workflow';
17
+ }>;
18
+ changedFiles?: string[];
11
19
  adapterId?: string;
12
20
  adapterSelector?: string;
13
21
  workspaceKind?: 'host' | 'worktree' | 'docker';
@@ -21,6 +29,8 @@ export interface CreateOrchestrationTaskInput {
21
29
  title: string;
22
30
  description?: string;
23
31
  taskmasterId?: string;
32
+ acceptanceCriteria?: OrchestrationTask['acceptanceCriteria'];
33
+ changedFiles?: string[];
24
34
  }
25
35
 
26
36
  export interface DispatchOrchestrationTaskInput {