@pixelbyte-software/pixcode 1.34.0 → 1.35.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.
Files changed (155) hide show
  1. package/dist/api-docs.html +162 -9
  2. package/dist/assets/index-B8w57E1r.css +32 -0
  3. package/dist/assets/index-Djuh0wHV.js +854 -0
  4. package/dist/favicon.svg +8 -8
  5. package/dist/icons/icon-128x128.svg +9 -9
  6. package/dist/icons/icon-144x144.svg +9 -9
  7. package/dist/icons/icon-152x152.svg +9 -9
  8. package/dist/icons/icon-192x192.svg +9 -9
  9. package/dist/icons/icon-384x384.svg +9 -9
  10. package/dist/icons/icon-512x512.svg +9 -9
  11. package/dist/icons/icon-72x72.svg +9 -9
  12. package/dist/icons/icon-96x96.svg +9 -9
  13. package/dist/icons/icon-template.svg +9 -9
  14. package/dist/index.html +2 -2
  15. package/dist/logo.svg +12 -12
  16. package/dist/openapi.yaml +383 -1
  17. package/dist-server/server/claude-sdk.js +38 -7
  18. package/dist-server/server/claude-sdk.js.map +1 -1
  19. package/dist-server/server/cli.js +12 -17
  20. package/dist-server/server/cli.js.map +1 -1
  21. package/dist-server/server/daemon-manager.js +98 -51
  22. package/dist-server/server/daemon-manager.js.map +1 -1
  23. package/dist-server/server/database/json-store.js +8 -5
  24. package/dist-server/server/database/json-store.js.map +1 -1
  25. package/dist-server/server/index.js +31 -10
  26. package/dist-server/server/index.js.map +1 -1
  27. package/dist-server/server/modules/orchestration/a2a/adapter-registry.js +45 -19
  28. package/dist-server/server/modules/orchestration/a2a/adapter-registry.js.map +1 -1
  29. package/dist-server/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.js.map +1 -1
  30. package/dist-server/server/modules/orchestration/a2a/adapters/claude-code.adapter.js +1 -0
  31. package/dist-server/server/modules/orchestration/a2a/adapters/claude-code.adapter.js.map +1 -1
  32. package/dist-server/server/modules/orchestration/a2a/adapters/codex.adapter.js +202 -0
  33. package/dist-server/server/modules/orchestration/a2a/adapters/codex.adapter.js.map +1 -0
  34. package/dist-server/server/modules/orchestration/a2a/adapters/cursor.adapter.js +205 -0
  35. package/dist-server/server/modules/orchestration/a2a/adapters/cursor.adapter.js.map +1 -0
  36. package/dist-server/server/modules/orchestration/a2a/adapters/gemini.adapter.js +205 -0
  37. package/dist-server/server/modules/orchestration/a2a/adapters/gemini.adapter.js.map +1 -0
  38. package/dist-server/server/modules/orchestration/a2a/adapters/opencode.adapter.js +205 -0
  39. package/dist-server/server/modules/orchestration/a2a/adapters/opencode.adapter.js.map +1 -0
  40. package/dist-server/server/modules/orchestration/a2a/adapters/qwen.adapter.js +205 -0
  41. package/dist-server/server/modules/orchestration/a2a/adapters/qwen.adapter.js.map +1 -0
  42. package/dist-server/server/modules/orchestration/a2a/routes.js +298 -34
  43. package/dist-server/server/modules/orchestration/a2a/routes.js.map +1 -1
  44. package/dist-server/server/modules/orchestration/a2a/task-store.js +144 -0
  45. package/dist-server/server/modules/orchestration/a2a/task-store.js.map +1 -0
  46. package/dist-server/server/modules/orchestration/a2a/validator.js +16 -0
  47. package/dist-server/server/modules/orchestration/a2a/validator.js.map +1 -1
  48. package/dist-server/server/modules/orchestration/index.js +14 -0
  49. package/dist-server/server/modules/orchestration/index.js.map +1 -1
  50. package/dist-server/server/modules/orchestration/preview/port-watcher.js +90 -0
  51. package/dist-server/server/modules/orchestration/preview/port-watcher.js.map +1 -0
  52. package/dist-server/server/modules/orchestration/preview/preview-proxy.js +58 -0
  53. package/dist-server/server/modules/orchestration/preview/preview-proxy.js.map +1 -0
  54. package/dist-server/server/modules/orchestration/preview/types.js +2 -0
  55. package/dist-server/server/modules/orchestration/preview/types.js.map +1 -0
  56. package/dist-server/server/modules/orchestration/tasks/orchestration-task-store.js +37 -0
  57. package/dist-server/server/modules/orchestration/tasks/orchestration-task-store.js.map +1 -0
  58. package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js +68 -0
  59. package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js.map +1 -0
  60. package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js +128 -0
  61. package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js.map +1 -0
  62. package/dist-server/server/modules/orchestration/tasks/orchestration-task.types.js +2 -0
  63. package/dist-server/server/modules/orchestration/tasks/orchestration-task.types.js.map +1 -0
  64. package/dist-server/server/modules/orchestration/workflows/built-in-workflows.js +126 -0
  65. package/dist-server/server/modules/orchestration/workflows/built-in-workflows.js.map +1 -0
  66. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +1047 -0
  67. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -0
  68. package/dist-server/server/modules/orchestration/workflows/workflow-store.js +76 -0
  69. package/dist-server/server/modules/orchestration/workflows/workflow-store.js.map +1 -0
  70. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js +151 -0
  71. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js.map +1 -0
  72. package/dist-server/server/modules/orchestration/workflows/workflow.types.js +2 -0
  73. package/dist-server/server/modules/orchestration/workflows/workflow.types.js.map +1 -0
  74. package/dist-server/server/modules/orchestration/workflows/workspace-target.js +98 -0
  75. package/dist-server/server/modules/orchestration/workflows/workspace-target.js.map +1 -0
  76. package/dist-server/server/modules/orchestration/workspace/docker-workspace.js +122 -0
  77. package/dist-server/server/modules/orchestration/workspace/docker-workspace.js.map +1 -0
  78. package/dist-server/server/modules/orchestration/workspace/path-safety.js +48 -0
  79. package/dist-server/server/modules/orchestration/workspace/path-safety.js.map +1 -0
  80. package/dist-server/server/modules/orchestration/workspace/types.js +11 -0
  81. package/dist-server/server/modules/orchestration/workspace/types.js.map +1 -0
  82. package/dist-server/server/modules/orchestration/workspace/workspace-manager.js +80 -0
  83. package/dist-server/server/modules/orchestration/workspace/workspace-manager.js.map +1 -0
  84. package/dist-server/server/modules/orchestration/workspace/worktree-workspace.js +96 -0
  85. package/dist-server/server/modules/orchestration/workspace/worktree-workspace.js.map +1 -0
  86. package/dist-server/server/modules/providers/index.js +3 -0
  87. package/dist-server/server/modules/providers/index.js.map +1 -0
  88. package/dist-server/server/openai-codex.js +35 -4
  89. package/dist-server/server/openai-codex.js.map +1 -1
  90. package/dist-server/server/routes/taskmaster.js +106 -89
  91. package/dist-server/server/routes/taskmaster.js.map +1 -1
  92. package/package.json +3 -1
  93. package/scripts/smoke/a2a-roundtrip.mjs +167 -98
  94. package/scripts/smoke/orchestration-api.mjs +172 -0
  95. package/scripts/smoke/orchestration-live-run.mjs +176 -0
  96. package/server/claude-sdk.js +48 -7
  97. package/server/cli.js +12 -17
  98. package/server/daemon-manager.js +90 -51
  99. package/server/database/db.js +794 -794
  100. package/server/database/json-store.js +8 -5
  101. package/server/index.js +40 -9
  102. package/server/modules/orchestration/a2a/adapter-registry.ts +108 -58
  103. package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +55 -49
  104. package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +284 -283
  105. package/server/modules/orchestration/a2a/adapters/codex.adapter.ts +244 -0
  106. package/server/modules/orchestration/a2a/adapters/cursor.adapter.ts +249 -0
  107. package/server/modules/orchestration/a2a/adapters/gemini.adapter.ts +248 -0
  108. package/server/modules/orchestration/a2a/adapters/opencode.adapter.ts +248 -0
  109. package/server/modules/orchestration/a2a/adapters/qwen.adapter.ts +248 -0
  110. package/server/modules/orchestration/a2a/agent-card.ts +55 -55
  111. package/server/modules/orchestration/a2a/auth.middleware.ts +29 -29
  112. package/server/modules/orchestration/a2a/bus.ts +46 -46
  113. package/server/modules/orchestration/a2a/routes.ts +577 -264
  114. package/server/modules/orchestration/a2a/task-store.ts +178 -0
  115. package/server/modules/orchestration/a2a/types.ts +125 -111
  116. package/server/modules/orchestration/a2a/validator.ts +113 -90
  117. package/server/modules/orchestration/index.ts +66 -26
  118. package/server/modules/orchestration/preview/port-watcher.ts +112 -0
  119. package/server/modules/orchestration/preview/preview-proxy.ts +60 -0
  120. package/server/modules/orchestration/preview/types.ts +19 -0
  121. package/server/modules/orchestration/tasks/orchestration-task-store.ts +45 -0
  122. package/server/modules/orchestration/tasks/orchestration-task.routes.ts +73 -0
  123. package/server/modules/orchestration/tasks/orchestration-task.service.ts +145 -0
  124. package/server/modules/orchestration/tasks/orchestration-task.types.ts +29 -0
  125. package/server/modules/orchestration/workflows/built-in-workflows.ts +127 -0
  126. package/server/modules/orchestration/workflows/workflow-runner.ts +1206 -0
  127. package/server/modules/orchestration/workflows/workflow-store.ts +97 -0
  128. package/server/modules/orchestration/workflows/workflow.routes.ts +169 -0
  129. package/server/modules/orchestration/workflows/workflow.types.ts +70 -0
  130. package/server/modules/orchestration/workflows/workspace-target.ts +120 -0
  131. package/server/modules/orchestration/workspace/docker-workspace.ts +135 -0
  132. package/server/modules/orchestration/workspace/path-safety.ts +55 -0
  133. package/server/modules/orchestration/workspace/types.ts +52 -0
  134. package/server/modules/orchestration/workspace/workspace-manager.ts +97 -0
  135. package/server/modules/orchestration/workspace/worktree-workspace.ts +125 -0
  136. package/server/modules/providers/index.ts +2 -0
  137. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +130 -130
  138. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +126 -126
  139. package/server/modules/providers/list/opencode/opencode.provider.ts +29 -29
  140. package/server/modules/providers/list/qwen/qwen-auth.provider.ts +145 -145
  141. package/server/modules/providers/list/qwen/qwen-mcp.provider.ts +114 -114
  142. package/server/modules/providers/list/qwen/qwen.provider.ts +21 -21
  143. package/server/modules/providers/shared/provider-configs.ts +142 -142
  144. package/server/openai-codex.js +40 -4
  145. package/server/qwen-code-cli.js +395 -395
  146. package/server/qwen-response-handler.js +73 -73
  147. package/server/routes/qwen.js +27 -27
  148. package/server/routes/taskmaster.js +116 -91
  149. package/server/services/external-access.js +171 -171
  150. package/server/services/provider-models.js +381 -381
  151. package/server/services/telegram/telegram-http-client.js +130 -130
  152. package/server/services/vapid-keys.js +36 -36
  153. package/server/utils/port-access.js +209 -209
  154. package/dist/assets/index-B1ghfb4w.css +0 -32
  155. package/dist/assets/index-BvClqlMf.js +0 -852
@@ -1,26 +1,66 @@
1
- // server/modules/orchestration/index.ts
2
- // Public surface for the orchestration module.
3
- // All cross-module consumers must import from here per
4
- // eslint.config.js boundaries rules.
5
-
6
- export { createA2ARouter } from './a2a/routes.js';
7
- export { adapterRegistry } from './a2a/adapter-registry.js';
8
- export { ClaudeCodeA2AAdapter } from './a2a/adapters/claude-code.adapter.js';
9
- export type {
10
- AdapterContext,
11
- TaskHandle,
12
- } from './a2a/adapters/abstract-a2a.adapter.js';
13
- export { AbstractA2AAdapter } from './a2a/adapters/abstract-a2a.adapter.js';
14
- export { a2aBus } from './a2a/bus.js';
15
- export type {
16
- AgentCard,
17
- Artifact,
18
- ArtifactType,
19
- BusEvent,
20
- Message,
21
- Part,
22
- SubmitTaskInput,
23
- Task,
24
- TaskError,
25
- TaskState,
26
- } from './a2a/types.js';
1
+ // server/modules/orchestration/index.ts
2
+ // Public surface for the orchestration module.
3
+ // All cross-module consumers must import from here per
4
+ // eslint.config.js boundaries rules.
5
+
6
+ export { createA2ARouter } from './a2a/routes.js';
7
+ export { adapterRegistry } from './a2a/adapter-registry.js';
8
+ export { ClaudeCodeA2AAdapter } from './a2a/adapters/claude-code.adapter.js';
9
+ export { CodexA2AAdapter } from './a2a/adapters/codex.adapter.js';
10
+ export { CursorA2AAdapter } from './a2a/adapters/cursor.adapter.js';
11
+ export { GeminiA2AAdapter } from './a2a/adapters/gemini.adapter.js';
12
+ export { QwenA2AAdapter } from './a2a/adapters/qwen.adapter.js';
13
+ export { OpenCodeA2AAdapter } from './a2a/adapters/opencode.adapter.js';
14
+ export type {
15
+ AdapterContext,
16
+ TaskHandle,
17
+ } from './a2a/adapters/abstract-a2a.adapter.js';
18
+ export { AbstractA2AAdapter } from './a2a/adapters/abstract-a2a.adapter.js';
19
+ export { a2aBus } from './a2a/bus.js';
20
+ export { portWatcher } from './preview/port-watcher.js';
21
+ export { createPreviewProxyRouter } from './preview/preview-proxy.js';
22
+ export { createOrchestrationTaskRouter } from './tasks/orchestration-task.routes.js';
23
+ export { orchestrationTaskService } from './tasks/orchestration-task.service.js';
24
+ export { createWorkflowRouter } from './workflows/workflow.routes.js';
25
+ export { workflowRunner } from './workflows/workflow-runner.js';
26
+ export { workflowStore } from './workflows/workflow-store.js';
27
+ export { workspaceManager } from './workspace/workspace-manager.js';
28
+ export type {
29
+ AgentCard,
30
+ Artifact,
31
+ ArtifactType,
32
+ BusEvent,
33
+ Message,
34
+ Part,
35
+ SubmitTaskInput,
36
+ Task,
37
+ TaskSummary,
38
+ TaskError,
39
+ TaskState,
40
+ } from './a2a/types.js';
41
+ export type {
42
+ PortEvent,
43
+ PreviewArtifactData,
44
+ } from './preview/types.js';
45
+ export type {
46
+ CreateOrchestrationTaskInput,
47
+ DispatchOrchestrationTaskInput,
48
+ OrchestrationTask,
49
+ OrchestrationTaskState,
50
+ } from './tasks/orchestration-task.types.js';
51
+ export type {
52
+ Workflow,
53
+ WorkflowNode,
54
+ WorkflowNodeRun,
55
+ WorkflowNodeStatus,
56
+ WorkflowRun,
57
+ WorkflowRunStatus,
58
+ } from './workflows/workflow.types.js';
59
+ export type {
60
+ ExecResult,
61
+ WorkspaceHandle,
62
+ WorkspaceKind,
63
+ WorkspaceMetadata,
64
+ WorkspaceRequest,
65
+ } from './workspace/types.js';
66
+ export { WorkspaceError } from './workspace/types.js';
@@ -0,0 +1,112 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+
4
+ import type { PortEvent } from '@/modules/orchestration/preview/types.js';
5
+ import type { WorkspaceHandle } from '@/modules/orchestration/workspace/types.js';
6
+
7
+ const execFileAsync = promisify(execFile);
8
+
9
+ interface WatchOptions {
10
+ taskId: string;
11
+ workspace: WorkspaceHandle;
12
+ intervalMs?: number;
13
+ onPort(event: PortEvent): void;
14
+ }
15
+
16
+ interface ListeningPort {
17
+ port: number;
18
+ host: string;
19
+ processName?: string;
20
+ }
21
+
22
+ const knownPreviewPorts = new Map<number, PortEvent>();
23
+
24
+ function parseAddress(address: string): { host: string; port: number } | null {
25
+ const normalized = address.trim();
26
+ const idx = normalized.lastIndexOf(':');
27
+ if (idx === -1) return null;
28
+ const port = Number.parseInt(normalized.slice(idx + 1), 10);
29
+ if (!Number.isFinite(port) || port <= 0) return null;
30
+ const host = normalized.slice(0, idx).replace(/^\[|\]$/g, '') || '127.0.0.1';
31
+ return { host: host === '*' || host === '0.0.0.0' ? '127.0.0.1' : host, port };
32
+ }
33
+
34
+ function parseSs(output: string): ListeningPort[] {
35
+ const ports: ListeningPort[] = [];
36
+ for (const line of output.split('\n')) {
37
+ if (!line.includes('LISTEN')) continue;
38
+ const parts = line.trim().split(/\s+/);
39
+ const address = parts[3] ?? parts[4];
40
+ const parsed = address ? parseAddress(address) : null;
41
+ if (!parsed) continue;
42
+ const processMatch = line.match(/users:\(\("([^"]+)"/);
43
+ ports.push({
44
+ ...parsed,
45
+ processName: processMatch?.[1],
46
+ });
47
+ }
48
+ return ports;
49
+ }
50
+
51
+ async function readListeningPorts(): Promise<ListeningPort[]> {
52
+ try {
53
+ const { stdout } = await execFileAsync('ss', ['-ltnp'], {
54
+ maxBuffer: 1024 * 1024,
55
+ });
56
+ return parseSs(String(stdout));
57
+ } catch {
58
+ return [];
59
+ }
60
+ }
61
+
62
+ export function getKnownPreviewPort(port: number): PortEvent | undefined {
63
+ return knownPreviewPorts.get(port);
64
+ }
65
+
66
+ export class PortWatcher {
67
+ watch(options: WatchOptions): () => void {
68
+ const {
69
+ taskId,
70
+ workspace,
71
+ intervalMs = 1000,
72
+ onPort,
73
+ } = options;
74
+ const seen = new Set<number>();
75
+ let stopped = false;
76
+ let timer: NodeJS.Timeout | undefined;
77
+ let initialized = false;
78
+
79
+ const tick = async () => {
80
+ if (stopped) return;
81
+ const ports = await readListeningPorts();
82
+ for (const port of ports) {
83
+ if (seen.has(port.port)) continue;
84
+ seen.add(port.port);
85
+ if (!initialized) continue;
86
+ const event: PortEvent = {
87
+ taskId,
88
+ workspaceId: workspace.id,
89
+ port: port.port,
90
+ host: port.host,
91
+ url: `http://${port.host}:${port.port}`,
92
+ processName: port.processName,
93
+ confidence: 'low',
94
+ detectedAt: Date.now(),
95
+ };
96
+ knownPreviewPorts.set(port.port, event);
97
+ onPort(event);
98
+ }
99
+ initialized = true;
100
+ timer = setTimeout(tick, intervalMs);
101
+ };
102
+
103
+ void tick();
104
+
105
+ return () => {
106
+ stopped = true;
107
+ if (timer) clearTimeout(timer);
108
+ };
109
+ }
110
+ }
111
+
112
+ export const portWatcher = new PortWatcher();
@@ -0,0 +1,60 @@
1
+ import type { RequestHandler, Router } from 'express';
2
+ import express from 'express';
3
+
4
+ import { getKnownPreviewPort } from '@/modules/orchestration/preview/port-watcher.js';
5
+
6
+ function buildTargetUrl(port: number, path: string): string {
7
+ const event = getKnownPreviewPort(port);
8
+ const host = event?.host ?? '127.0.0.1';
9
+ return `http://${host}:${port}${path}`;
10
+ }
11
+
12
+ function proxyHandler(): RequestHandler {
13
+ return async (req, res) => {
14
+ const rawPort = Array.isArray(req.params.port) ? req.params.port[0] : req.params.port;
15
+ const port = Number.parseInt(rawPort, 10);
16
+ if (!Number.isFinite(port) || port <= 0 || !getKnownPreviewPort(port)) {
17
+ res.status(404).json({
18
+ error: {
19
+ code: 'PREVIEW_PORT_NOT_FOUND',
20
+ message: 'Preview port is not registered for an orchestration task.',
21
+ },
22
+ });
23
+ return;
24
+ }
25
+
26
+ const path = req.originalUrl.replace(/^\/preview\/\d+/, '') || '/';
27
+ const target = buildTargetUrl(port, path);
28
+ try {
29
+ const upstream = await fetch(target, {
30
+ method: req.method,
31
+ headers: {
32
+ accept: req.header('accept') ?? '*/*',
33
+ 'user-agent': req.header('user-agent') ?? 'pixcode-preview-proxy',
34
+ },
35
+ });
36
+ res.status(upstream.status);
37
+ upstream.headers.forEach((value, key) => {
38
+ const lower = key.toLowerCase();
39
+ if (lower === 'content-encoding' || lower === 'content-length') return;
40
+ if (lower === 'x-frame-options' || lower === 'content-security-policy') return;
41
+ res.setHeader(key, value);
42
+ });
43
+ res.send(Buffer.from(await upstream.arrayBuffer()));
44
+ } catch (error) {
45
+ res.status(502).json({
46
+ error: {
47
+ code: 'PREVIEW_PROXY_FAILED',
48
+ message: error instanceof Error ? error.message : String(error),
49
+ },
50
+ });
51
+ }
52
+ };
53
+ }
54
+
55
+ export function createPreviewProxyRouter(): Router {
56
+ const router = express.Router();
57
+ router.use('/:port', proxyHandler());
58
+ router.use('/:port/*', proxyHandler());
59
+ return router;
60
+ }
@@ -0,0 +1,19 @@
1
+ export interface PortEvent {
2
+ taskId: string;
3
+ workspaceId?: string;
4
+ port: number;
5
+ host: string;
6
+ url: string;
7
+ processName?: string;
8
+ confidence: 'low' | 'medium' | 'high';
9
+ detectedAt: number;
10
+ }
11
+
12
+ export interface PreviewArtifactData {
13
+ url: string;
14
+ proxiedUrl: string;
15
+ port: number;
16
+ host: string;
17
+ processName?: string;
18
+ confidence: 'low' | 'medium' | 'high';
19
+ }
@@ -0,0 +1,45 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+
4
+ import { JsonStore } from '@/database/json-store.js';
5
+ import type { OrchestrationTask } from '@/modules/orchestration/tasks/orchestration-task.types.js';
6
+
7
+ function defaultPath(): string {
8
+ return process.env.ORCHESTRATION_TASKS_PATH ??
9
+ path.join(os.homedir(), '.pixcode', 'orchestration-tasks.json');
10
+ }
11
+
12
+ export class OrchestrationTaskStore {
13
+ private store: JsonStore;
14
+
15
+ constructor(filePath = defaultPath()) {
16
+ this.store = new JsonStore(filePath, { orchestration_tasks: [] });
17
+ }
18
+
19
+ get(id: string): OrchestrationTask | undefined {
20
+ return this.store.findWhere('orchestration_tasks', (r: OrchestrationTask) => r.id === id) ?? undefined;
21
+ }
22
+
23
+ getByA2ATaskId(a2aTaskId: string): OrchestrationTask | undefined {
24
+ return this.store.findWhere('orchestration_tasks', (r: OrchestrationTask) => r.a2aTaskId === a2aTaskId) ?? undefined;
25
+ }
26
+
27
+ getByTaskMasterId(taskmasterId: string): OrchestrationTask | undefined {
28
+ return this.store.findWhere('orchestration_tasks', (r: OrchestrationTask) => r.taskmasterId === taskmasterId) ?? undefined;
29
+ }
30
+
31
+ list(projectId?: string): OrchestrationTask[] {
32
+ const tasks = this.store.filterWhere('orchestration_tasks', () => true) as OrchestrationTask[];
33
+ const sorted = tasks.sort((a, b) => b.createdAt - a.createdAt);
34
+ return projectId ? sorted.filter((task) => task.projectId === projectId) : sorted;
35
+ }
36
+
37
+ set(task: OrchestrationTask): void {
38
+ const existing = this.store.findWhere('orchestration_tasks', (r: OrchestrationTask) => r.id === task.id);
39
+ if (existing) {
40
+ this.store.updateWhere('orchestration_tasks', (r: OrchestrationTask) => r.id === task.id, task);
41
+ } else {
42
+ this.store.insert('orchestration_tasks', task, { autoId: false });
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,73 @@
1
+ import express, { type Router } from 'express';
2
+ import { orchestrationTaskService } from '@/modules/orchestration/tasks/orchestration-task.service.js';
3
+
4
+ export function createOrchestrationTaskRouter(): Router {
5
+ const router = express.Router();
6
+ router.use(express.json({ limit: '2mb' }));
7
+
8
+ router.get('/tasks', (req, res) => {
9
+ const projectId = typeof req.query.projectId === 'string' ? req.query.projectId : undefined;
10
+ res.json({ tasks: orchestrationTaskService.list(projectId) });
11
+ });
12
+
13
+ router.post('/tasks', (req, res) => {
14
+ const projectId = typeof req.body?.projectId === 'string' ? req.body.projectId : 'default';
15
+ const title = typeof req.body?.title === 'string' ? req.body.title.trim() : '';
16
+ const description = typeof req.body?.description === 'string' ? req.body.description : undefined;
17
+ const taskmasterId = typeof req.body?.taskmasterId === 'string' ? req.body.taskmasterId : undefined;
18
+
19
+ if (!title) {
20
+ res.status(400).json({ error: { code: 'TITLE_REQUIRED', message: 'title is required' } });
21
+ return;
22
+ }
23
+
24
+ const task = orchestrationTaskService.create({ projectId, title, description, taskmasterId });
25
+ res.status(201).json(task);
26
+ });
27
+
28
+ router.post('/tasks/import-taskmaster', (req, res) => {
29
+ const projectId = typeof req.body?.projectId === 'string' ? req.body.projectId : 'default';
30
+ const entries = Array.isArray(req.body?.tasks) ? req.body.tasks : [];
31
+ const imported = entries
32
+ .map((entry: unknown) => {
33
+ if (!entry || typeof entry !== 'object') return null;
34
+ const record = entry as Record<string, unknown>;
35
+ const taskmasterId = typeof record.id === 'string' ? record.id : undefined;
36
+ const title = typeof record.title === 'string' ? record.title : undefined;
37
+ const description = typeof record.description === 'string' ? record.description : undefined;
38
+ if (!taskmasterId || !title) return null;
39
+ return orchestrationTaskService.upsertFromTaskMaster({ projectId, title, description, taskmasterId });
40
+ })
41
+ .filter(Boolean);
42
+
43
+ res.json({ tasks: imported, count: imported.length });
44
+ });
45
+
46
+ router.post('/tasks/:id/dispatch', async (req, res) => {
47
+ try {
48
+ const adapterId = typeof req.body?.adapterId === 'string' ? req.body.adapterId : '';
49
+ const isolation = req.body?.isolation;
50
+ if (!adapterId) {
51
+ res.status(400).json({ error: { code: 'ADAPTER_REQUIRED', message: 'adapterId is required' } });
52
+ return;
53
+ }
54
+ const task = await orchestrationTaskService.dispatch(req.params.id, { adapterId, isolation });
55
+ res.json(task);
56
+ } catch (error) {
57
+ const message = error instanceof Error ? error.message : String(error);
58
+ const status = message === 'TASK_NOT_FOUND' ? 404 : 400;
59
+ res.status(status).json({ error: { code: 'DISPATCH_FAILED', message } });
60
+ }
61
+ });
62
+
63
+ router.post('/tasks/:id/cancel', (req, res) => {
64
+ try {
65
+ const task = orchestrationTaskService.cancel(req.params.id);
66
+ res.json(task);
67
+ } catch {
68
+ res.status(404).json({ error: { code: 'TASK_NOT_FOUND', message: req.params.id } });
69
+ }
70
+ });
71
+
72
+ return router;
73
+ }
@@ -0,0 +1,145 @@
1
+ import { OrchestrationTaskStore } from '@/modules/orchestration/tasks/orchestration-task-store.js';
2
+ import type { CreateOrchestrationTaskInput, DispatchOrchestrationTaskInput, OrchestrationTask } from '@/modules/orchestration/tasks/orchestration-task.types.js';
3
+ import { a2aBus } from '@/modules/orchestration/a2a/bus.js';
4
+ import type { TaskState } from '@/modules/orchestration/a2a/types.js';
5
+
6
+ function newId(prefix: string): string {
7
+ return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
8
+ }
9
+
10
+ const TERMINAL_A2A_STATES: TaskState[] = ['completed', 'canceled', 'failed'];
11
+
12
+ class OrchestrationTaskService {
13
+ private store: OrchestrationTaskStore;
14
+
15
+ constructor(store?: OrchestrationTaskStore) {
16
+ this.store = store ?? new OrchestrationTaskStore();
17
+ this.watchA2ATerminalStates();
18
+ }
19
+
20
+ list(projectId?: string): OrchestrationTask[] {
21
+ return this.store.list(projectId);
22
+ }
23
+
24
+ get(id: string): OrchestrationTask | undefined {
25
+ return this.store.get(id);
26
+ }
27
+
28
+ create(input: CreateOrchestrationTaskInput): OrchestrationTask {
29
+ const now = Date.now();
30
+ const task: OrchestrationTask = {
31
+ id: newId('otask'),
32
+ projectId: input.projectId,
33
+ title: input.title,
34
+ description: input.description,
35
+ taskmasterId: input.taskmasterId,
36
+ state: 'todo',
37
+ createdAt: now,
38
+ updatedAt: now,
39
+ };
40
+ this.store.set(task);
41
+ return task;
42
+ }
43
+
44
+ upsertFromTaskMaster(input: CreateOrchestrationTaskInput): OrchestrationTask {
45
+ const existing = this.store.list(input.projectId).find((task) =>
46
+ task.taskmasterId === input.taskmasterId,
47
+ );
48
+ if (existing) {
49
+ existing.title = input.title;
50
+ existing.description = input.description;
51
+ existing.updatedAt = Date.now();
52
+ this.store.set(existing);
53
+ return existing;
54
+ }
55
+ return this.create(input);
56
+ }
57
+
58
+ async dispatch(taskId: string, input: DispatchOrchestrationTaskInput): Promise<OrchestrationTask> {
59
+ const task = this.store.get(taskId);
60
+ if (!task) throw new Error('TASK_NOT_FOUND');
61
+
62
+ const a2aResponse = await fetch(`http://127.0.0.1:${process.env.SERVER_PORT ?? '3001'}/a2a/tasks`, {
63
+ method: 'POST',
64
+ headers: { 'Content-Type': 'application/json' },
65
+ body: JSON.stringify({
66
+ adapterId: input.adapterId,
67
+ message: {
68
+ messageId: newId('msg'),
69
+ role: 'user',
70
+ parts: [{ kind: 'text', text: `${task.title}\n\n${task.description ?? ''}` }],
71
+ },
72
+ metadata: {
73
+ isolation: input.isolation ?? 'worktree',
74
+ orchestrationTaskId: task.id,
75
+ taskmasterId: task.taskmasterId,
76
+ },
77
+ }),
78
+ });
79
+
80
+ const body = await a2aResponse.json().catch(() => null) as { id?: string; error?: { message?: string } } | null;
81
+ if (!a2aResponse.ok || typeof body?.id !== 'string') {
82
+ throw new Error(body?.error?.message ?? 'DISPATCH_FAILED');
83
+ }
84
+
85
+ task.a2aTaskId = body.id;
86
+ task.adapterId = input.adapterId;
87
+ task.adapterSelector = input.adapterId;
88
+ task.workspaceKind = input.isolation ?? 'worktree';
89
+ task.state = 'in_progress';
90
+ task.updatedAt = Date.now();
91
+ this.store.set(task);
92
+ return task;
93
+ }
94
+
95
+ updateState(taskId: string, state: OrchestrationTask['state']): OrchestrationTask | undefined {
96
+ const task = this.store.get(taskId);
97
+ if (!task) return undefined;
98
+ task.state = state;
99
+ task.updatedAt = Date.now();
100
+ this.store.set(task);
101
+ return task;
102
+ }
103
+
104
+ private watchA2ATerminalStates(): void {
105
+ a2aBus.subscribeAll((event) => {
106
+ if (event.kind !== 'task-state') return;
107
+ if (!TERMINAL_A2A_STATES.includes(event.state)) return;
108
+
109
+ const orchTask = this.store.getByA2ATaskId(event.taskId);
110
+ if (!orchTask) return;
111
+ if (orchTask.state === 'done' || orchTask.state === 'failed' || orchTask.state === 'canceled') return;
112
+
113
+ const mapped = event.state === 'completed' ? 'done'
114
+ : event.state === 'failed' ? 'failed'
115
+ : 'canceled';
116
+
117
+ orchTask.state = mapped;
118
+ orchTask.updatedAt = Date.now();
119
+ this.store.set(orchTask);
120
+
121
+ if (orchTask.taskmasterId && mapped === 'done') {
122
+ this.syncTaskMasterStatus(orchTask.taskmasterId, 'done');
123
+ }
124
+ });
125
+ }
126
+
127
+ private syncTaskMasterStatus(taskmasterId: string, status: string): void {
128
+ const { spawn } = require('node:child_process') as typeof import('node:child_process');
129
+ spawn('task-master', ['set-status', '--id', taskmasterId, '--status', status], {
130
+ stdio: 'ignore',
131
+ detached: true,
132
+ }).unref();
133
+ }
134
+
135
+ cancel(taskId: string): OrchestrationTask {
136
+ const task = this.store.get(taskId);
137
+ if (!task) throw new Error('TASK_NOT_FOUND');
138
+ task.state = 'canceled';
139
+ task.updatedAt = Date.now();
140
+ this.store.set(task);
141
+ return task;
142
+ }
143
+ }
144
+
145
+ export const orchestrationTaskService = new OrchestrationTaskService();
@@ -0,0 +1,29 @@
1
+ export type OrchestrationTaskState = 'todo' | 'in_progress' | 'in_review' | 'done' | 'failed' | 'canceled';
2
+
3
+ export interface OrchestrationTask {
4
+ id: string;
5
+ a2aTaskId?: string;
6
+ taskmasterId?: string;
7
+ projectId: string;
8
+ title: string;
9
+ description?: string;
10
+ state: OrchestrationTaskState;
11
+ adapterId?: string;
12
+ adapterSelector?: string;
13
+ workspaceKind?: 'host' | 'worktree' | 'docker';
14
+ workspacePath?: string;
15
+ createdAt: number;
16
+ updatedAt: number;
17
+ }
18
+
19
+ export interface CreateOrchestrationTaskInput {
20
+ projectId: string;
21
+ title: string;
22
+ description?: string;
23
+ taskmasterId?: string;
24
+ }
25
+
26
+ export interface DispatchOrchestrationTaskInput {
27
+ adapterId: string;
28
+ isolation?: 'host' | 'worktree' | 'docker';
29
+ }