@pixelbyte-software/pixcode 1.36.4 → 1.37.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 (45) hide show
  1. package/dist/assets/index-CfHK8y_H.css +32 -0
  2. package/dist/assets/{index-D-YjltED.js → index-D8uNxHf1.js} +163 -157
  3. package/dist/index.html +2 -2
  4. package/dist-server/server/database/db.js +4 -2
  5. package/dist-server/server/database/db.js.map +1 -1
  6. package/dist-server/server/index.js +3 -0
  7. package/dist-server/server/index.js.map +1 -1
  8. package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js +10 -1
  9. package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js.map +1 -1
  10. package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js +7 -0
  11. package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js.map +1 -1
  12. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +127 -24
  13. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
  14. package/dist-server/server/routes/taskmaster.js +194 -0
  15. package/dist-server/server/routes/taskmaster.js.map +1 -1
  16. package/dist-server/server/services/install-jobs.js +1 -0
  17. package/dist-server/server/services/install-jobs.js.map +1 -1
  18. package/dist-server/server/services/notification-orchestrator.js +66 -9
  19. package/dist-server/server/services/notification-orchestrator.js.map +1 -1
  20. package/dist-server/server/services/telegram/control-center.js +144 -2
  21. package/dist-server/server/services/telegram/control-center.js.map +1 -1
  22. package/dist-server/server/services/telegram/translations.js +14 -2
  23. package/dist-server/server/services/telegram/translations.js.map +1 -1
  24. package/package.json +1 -1
  25. package/scripts/smoke/chat-realtime-hydration.mjs +44 -0
  26. package/scripts/smoke/multi-worker-slots.mjs +42 -0
  27. package/scripts/smoke/notification-center.mjs +63 -0
  28. package/scripts/smoke/orchestration-execution-dashboard.mjs +33 -0
  29. package/scripts/smoke/strict-handoff-compact.mjs +60 -0
  30. package/scripts/smoke/taskmaster-execution-telegram.mjs +52 -0
  31. package/scripts/smoke/taskmaster-onboarding.mjs +52 -0
  32. package/scripts/smoke/update-issue-progress.mjs +69 -0
  33. package/server/database/db.js +4 -2
  34. package/server/index.js +3 -0
  35. package/server/modules/orchestration/tasks/orchestration-task.routes.ts +10 -1
  36. package/server/modules/orchestration/tasks/orchestration-task.service.ts +7 -0
  37. package/server/modules/orchestration/tasks/orchestration-task.types.ts +3 -0
  38. package/server/modules/orchestration/workflows/workflow-runner.ts +132 -24
  39. package/server/modules/orchestration/workflows/workflow.types.ts +2 -0
  40. package/server/routes/taskmaster.js +201 -0
  41. package/server/services/install-jobs.js +1 -0
  42. package/server/services/notification-orchestrator.js +76 -8
  43. package/server/services/telegram/control-center.js +153 -2
  44. package/server/services/telegram/translations.js +14 -2
  45. package/dist/assets/index-CgF0-_6Z.css +0 -32
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from 'node:assert/strict';
4
+ import { readFileSync } from 'node:fs';
5
+
6
+ const installJobs = readFileSync('server/services/install-jobs.js', 'utf8');
7
+ const taskmasterRoutes = readFileSync('server/routes/taskmaster.js', 'utf8');
8
+ const onboarding = readFileSync('src/components/onboarding/view/Onboarding.tsx', 'utf8');
9
+ const stepProgress = readFileSync('src/components/onboarding/view/subcomponents/OnboardingStepProgress.tsx', 'utf8');
10
+ const taskStep = readFileSync('src/components/onboarding/view/subcomponents/TaskSystemStep.tsx', 'utf8');
11
+
12
+ assert.ok(
13
+ installJobs.includes("'task-master': 'task-master'"),
14
+ 'TaskMaster package should be verified by the sandbox CLI installer.',
15
+ );
16
+
17
+ assert.ok(
18
+ taskmasterRoutes.includes("router.post('/install'"),
19
+ 'TaskMaster routes should expose an authenticated install endpoint.',
20
+ );
21
+
22
+ assert.ok(
23
+ taskmasterRoutes.includes("router.get('/install/:jobId/stream'"),
24
+ 'TaskMaster install should expose the same resilient log stream pattern as provider installs.',
25
+ );
26
+
27
+ assert.ok(
28
+ taskmasterRoutes.includes("provider: 'taskmaster'") && taskmasterRoutes.includes("packageName: 'task-master'"),
29
+ 'TaskMaster install route should install the task-master npm package under the taskmaster job provider.',
30
+ );
31
+
32
+ assert.ok(
33
+ onboarding.includes('TaskSystemStep') && onboarding.includes('currentStep < 2'),
34
+ 'Onboarding should include a third Task system step before completion.',
35
+ );
36
+
37
+ assert.ok(
38
+ onboarding.includes("localStorage.setItem('tasks-enabled'"),
39
+ 'Onboarding should persist the user task-system choice.',
40
+ );
41
+
42
+ assert.ok(
43
+ stepProgress.includes('Task System'),
44
+ 'Onboarding progress should show the Task System step.',
45
+ );
46
+
47
+ assert.ok(
48
+ taskStep.includes('/api/taskmaster/installation-status') && taskStep.includes('/api/taskmaster/install'),
49
+ 'TaskSystemStep should check and install TaskMaster through the backend API.',
50
+ );
51
+
52
+ console.log('taskmaster onboarding smoke passed');
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from 'node:assert/strict';
4
+ import { existsSync, readFileSync } from 'node:fs';
5
+
6
+ const parserPath = 'src/components/version-upgrade/utils/releaseIssueProgress.ts';
7
+ const componentPath = 'src/components/version-upgrade/view/ReleaseIssueProgress.tsx';
8
+ const modalPath = 'src/components/version-upgrade/view/VersionUpgradeModal.tsx';
9
+ const trackingPath = 'RELEASE_TRACKING_v1.37.md';
10
+
11
+ assert.ok(existsSync(parserPath), 'Release issue progress parser should exist.');
12
+ assert.ok(existsSync(componentPath), 'Release issue progress component should exist.');
13
+ assert.ok(existsSync(trackingPath), 'v1.37 release tracking document should exist.');
14
+
15
+ const parserSource = readFileSync(parserPath, 'utf8');
16
+ assert.ok(
17
+ parserSource.includes('RELEASE_ISSUE_PROGRESS_MARKER'),
18
+ 'Parser should expose a stable release-note marker for issue progress blocks.',
19
+ );
20
+ assert.ok(
21
+ parserSource.includes('extractIssueProgress'),
22
+ 'Parser should export extractIssueProgress for release body parsing.',
23
+ );
24
+ assert.ok(
25
+ parserSource.includes('DEFAULT_V137_ISSUE_PROGRESS'),
26
+ 'Parser should provide a v1.37 fallback issue map until the release body is published.',
27
+ );
28
+ assert.ok(
29
+ parserSource.includes('\\[([xX ~-])\\]') || parserSource.includes('[x]'),
30
+ 'Parser should understand checked issue/task rows.',
31
+ );
32
+
33
+ const componentSource = readFileSync(componentPath, 'utf8');
34
+ assert.ok(
35
+ componentSource.includes('extractIssueProgress'),
36
+ 'Component should render parsed issue progress from release notes.',
37
+ );
38
+ assert.ok(
39
+ componentSource.includes('completedCount'),
40
+ 'Component should summarize completed issue progress.',
41
+ );
42
+ assert.ok(
43
+ componentSource.includes("version?.startsWith('1.37')") || componentSource.includes('version.startsWith'),
44
+ 'Component should only use the v1.37 fallback for v1.37 release notes.',
45
+ );
46
+
47
+ const modalSource = readFileSync(modalPath, 'utf8');
48
+ assert.ok(
49
+ modalSource.includes('ReleaseIssueProgress'),
50
+ 'Version modal should include release issue progress.',
51
+ );
52
+ assert.ok(
53
+ modalSource.includes('releaseInfo.body'),
54
+ 'Version modal should pass release body text to the issue progress view.',
55
+ );
56
+
57
+ const trackingSource = readFileSync(trackingPath, 'utf8');
58
+ for (const issueNumber of [6, 7, 8, 9, 10, 11, 12, 13, 14]) {
59
+ assert.ok(
60
+ trackingSource.includes(`#${issueNumber}`),
61
+ `Release tracking should mention issue #${issueNumber}.`,
62
+ );
63
+ }
64
+ assert.ok(
65
+ trackingSource.includes('<!-- pixcode:issue-progress -->'),
66
+ 'Release tracking should include the issue-progress marker block used in release notes.',
67
+ );
68
+
69
+ console.log('update issue progress smoke passed');
@@ -493,7 +493,7 @@ const credentialsDb = {
493
493
  // Notification preferences
494
494
  // ---------------------------------------------------------------------------
495
495
  const DEFAULT_NOTIFICATION_PREFERENCES = {
496
- channels: { inApp: false, webPush: false },
496
+ channels: { inApp: true, webPush: false, telegram: true, desktop: true },
497
497
  events: { actionRequired: true, stop: true, error: true, updates: true },
498
498
  };
499
499
 
@@ -501,8 +501,10 @@ const normalizeNotificationPreferences = (value) => {
501
501
  const source = value && typeof value === 'object' ? value : {};
502
502
  return {
503
503
  channels: {
504
- inApp: source.channels?.inApp === true,
504
+ inApp: source.channels?.inApp !== false,
505
505
  webPush: source.channels?.webPush === true,
506
+ telegram: source.channels?.telegram !== false,
507
+ desktop: source.channels?.desktop !== false,
506
508
  },
507
509
  events: {
508
510
  actionRequired: source.events?.actionRequired !== false,
package/server/index.js CHANGED
@@ -99,6 +99,7 @@ import {
99
99
  import { primeCliBinPath } from './services/install-jobs.js';
100
100
  import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
101
101
  import { initializeDatabase, sessionNamesDb, applyCustomSessionNames } from './database/db.js';
102
+ import { setNotificationWebSocketServer } from './services/notification-orchestrator.js';
102
103
  import { configureWebPush } from './services/vapid-keys.js';
103
104
  import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
104
105
  import { IS_PLATFORM } from './constants/config.js';
@@ -319,6 +320,7 @@ const wss = new WebSocketServer({
319
320
 
320
321
  // Make WebSocket server available to routes
321
322
  app.locals.wss = wss;
323
+ setNotificationWebSocketServer(wss);
322
324
 
323
325
  app.use(cors({ exposedHeaders: ['X-Refreshed-Token'] }));
324
326
  app.use(express.json({
@@ -1813,6 +1815,7 @@ function handleChatConnection(ws, request) {
1813
1815
  console.log('[INFO] Chat WebSocket connected');
1814
1816
 
1815
1817
  // Add to connected clients for project updates
1818
+ ws.userId = request?.user?.id ?? request?.user?.userId ?? null;
1816
1819
  connectedClients.add(ws);
1817
1820
 
1818
1821
  // Wrap WebSocket with writer for consistent interface with SSEStreamWriter
@@ -48,11 +48,20 @@ export function createOrchestrationTaskRouter(): Router {
48
48
  try {
49
49
  const adapterId = typeof req.body?.adapterId === 'string' ? req.body.adapterId : '';
50
50
  const isolation = req.body?.isolation;
51
+ const projectPath = typeof req.body?.projectPath === 'string' ? req.body.projectPath : undefined;
52
+ const model = typeof req.body?.model === 'string' ? req.body.model : undefined;
53
+ const permissionMode = typeof req.body?.permissionMode === 'string' ? req.body.permissionMode : undefined;
51
54
  if (!adapterId) {
52
55
  res.status(400).json({ error: { code: 'ADAPTER_REQUIRED', message: 'adapterId is required' } });
53
56
  return;
54
57
  }
55
- const task = await orchestrationTaskService.dispatch(req.params.id, { adapterId, isolation });
58
+ const task = await orchestrationTaskService.dispatch(req.params.id, {
59
+ adapterId,
60
+ isolation,
61
+ projectPath,
62
+ model,
63
+ permissionMode,
64
+ });
56
65
  res.json(task);
57
66
  } catch (error) {
58
67
  const message = error instanceof Error ? error.message : String(error);
@@ -71,6 +71,12 @@ class OrchestrationTaskService {
71
71
  },
72
72
  metadata: {
73
73
  isolation: input.isolation ?? 'worktree',
74
+ model: input.model,
75
+ permissionMode: input.permissionMode,
76
+ workspace: {
77
+ kind: input.isolation ?? 'worktree',
78
+ projectPath: input.projectPath,
79
+ },
74
80
  orchestrationTaskId: task.id,
75
81
  taskmasterId: task.taskmasterId,
76
82
  },
@@ -86,6 +92,7 @@ class OrchestrationTaskService {
86
92
  task.adapterId = input.adapterId;
87
93
  task.adapterSelector = input.adapterId;
88
94
  task.workspaceKind = input.isolation ?? 'worktree';
95
+ task.workspacePath = input.projectPath;
89
96
  task.state = 'in_progress';
90
97
  task.updatedAt = Date.now();
91
98
  this.store.set(task);
@@ -26,4 +26,7 @@ export interface CreateOrchestrationTaskInput {
26
26
  export interface DispatchOrchestrationTaskInput {
27
27
  adapterId: string;
28
28
  isolation?: 'host' | 'worktree' | 'docker';
29
+ projectPath?: string;
30
+ model?: string;
31
+ permissionMode?: string;
29
32
  }
@@ -299,6 +299,56 @@ function handoffPrompt(agent: AgentAssignment, role: AgentRole): string {
299
299
  ].filter(Boolean).join('\n');
300
300
  }
301
301
 
302
+ function handoffInitPrompt(agent: AgentAssignment, index: number): string {
303
+ return [
304
+ `You are preparing ${agent.label} for a strict Pixcode handoff chain.`,
305
+ `This is internal step ${index + 1}.`,
306
+ 'Create a compact init packet for the next visible work step.',
307
+ 'Use the original user goal and any prior compact handoff packet included above.',
308
+ agent.instruction ? `The explicit assignment for this agent is: ${agent.instruction}` : '',
309
+ 'Output only this internal init packet:',
310
+ '- user goal in one sentence',
311
+ '- prior agent handoff summary, if present',
312
+ '- this agent responsibility',
313
+ '- exact constraints and blockers this agent must respect',
314
+ privacyGuardPrompt(),
315
+ 'Do not perform the task yet. Do not mention that this is hidden from the user.',
316
+ 'Respond in the same language as the user request.',
317
+ ].filter(Boolean).join('\n');
318
+ }
319
+
320
+ function handoffWorkPrompt(agent: AgentAssignment, index: number): string {
321
+ return [
322
+ `You are ${agent.label} in a strict Pixcode handoff chain.`,
323
+ `This is visible work step ${index + 1}.`,
324
+ 'The internal init packet above is your starting context. Do the assigned work now.',
325
+ agent.instruction
326
+ ? `Your explicit assignment from the user is: ${agent.instruction}`
327
+ : 'Use the init packet and original user goal to choose the next useful work for this step.',
328
+ rolePrompt(agent.role ?? 'implementation'),
329
+ privacyGuardPrompt(),
330
+ 'Report only user-facing progress, changed files, commands, verification, blockers, and next actions.',
331
+ 'Respond in the same language as the user request.',
332
+ ].filter(Boolean).join('\n');
333
+ }
334
+
335
+ function handoffCompactPrompt(agent: AgentAssignment, index: number): string {
336
+ return [
337
+ `You are compacting ${agent.label}'s strict handoff output for the next Pixcode agent.`,
338
+ `This is internal compact step ${index + 1}.`,
339
+ 'Read the prior visible work output included above and create a compact handoff packet.',
340
+ 'Output only this internal compact packet:',
341
+ '- Ben ne yaptım / What I did',
342
+ '- Dokunduğum alanlar / Touched areas',
343
+ '- Kanıt, komut veya çıktı / Evidence, commands, outputs',
344
+ '- Sonraki ajan şunu bilsin / What the next agent must know',
345
+ '- Bloker veya risk / Blockers or risks',
346
+ privacyGuardPrompt(),
347
+ 'Do not include raw logs unless they are essential. Keep it concise and actionable.',
348
+ 'Respond in the same language as the user request.',
349
+ ].join('\n');
350
+ }
351
+
302
352
  function compactOutputForContext(text: string): string {
303
353
  if (text.length <= MAX_OUTPUT_CONTEXT_CHARS) {
304
354
  return text;
@@ -595,32 +645,89 @@ function expandSequentialHandoffWorkflow(workflow: Workflow, metadata?: Record<s
595
645
  throw new Error('Select at least one CLI agent.');
596
646
  }
597
647
 
648
+ const nodes: WorkflowNode[] = agents.flatMap((agent, index): WorkflowNode[] => {
649
+ const initNodeId = safeAgentNodeId(agent, index, 'init');
650
+ const workNodeId = safeAgentNodeId(agent, index, 'work');
651
+ const compactNodeId = safeAgentNodeId(agent, index, 'compact');
652
+
653
+ return [
654
+ {
655
+ id: initNodeId,
656
+ adapterId: agent.adapterId,
657
+ agentInstanceId: agent.instanceId,
658
+ agentLabel: `${agent.label} Init`,
659
+ assignment: agent.instruction,
660
+ stage: 'handoff_init',
661
+ model: agent.model,
662
+ permissionMode: agent.permissionMode,
663
+ toolsSettings: agent.toolsSettings,
664
+ prompt: handoffInitPrompt(agent, index),
665
+ inputs: index === 0 ? [] : [safeAgentNodeId(agents[index - 1], index - 1, 'compact')],
666
+ output: 'message',
667
+ onFail: 'abort',
668
+ internal: true,
669
+ },
670
+ {
671
+ id: workNodeId,
672
+ adapterId: agent.adapterId,
673
+ agentInstanceId: agent.instanceId,
674
+ agentLabel: agent.label,
675
+ assignment: agent.instruction,
676
+ stage: agent.role ?? 'implementation',
677
+ model: agent.model,
678
+ permissionMode: agent.permissionMode,
679
+ toolsSettings: agent.toolsSettings,
680
+ prompt: handoffWorkPrompt(agent, index),
681
+ inputs: [initNodeId],
682
+ output: 'both',
683
+ onFail: 'abort',
684
+ },
685
+ {
686
+ id: compactNodeId,
687
+ adapterId: agent.adapterId,
688
+ agentInstanceId: agent.instanceId,
689
+ agentLabel: `${agent.label} Compact`,
690
+ assignment: agent.instruction,
691
+ stage: 'handoff_compact',
692
+ model: agent.model,
693
+ permissionMode: agent.permissionMode,
694
+ toolsSettings: agent.toolsSettings,
695
+ prompt: handoffCompactPrompt(agent, index),
696
+ inputs: [workNodeId],
697
+ output: 'message',
698
+ onFail: 'abort',
699
+ internal: true,
700
+ },
701
+ ];
702
+ });
703
+ const reportAgent = agents[0];
704
+ const lastCompactNodeId = safeAgentNodeId(agents[agents.length - 1], agents.length - 1, 'compact');
705
+
598
706
  return {
599
707
  ...workflow,
600
- nodes: agents.map((agent, index): WorkflowNode => ({
601
- id: safeAgentNodeId(agent, index, 'handoff'),
602
- adapterId: agent.adapterId,
603
- agentInstanceId: agent.instanceId,
604
- agentLabel: agent.label,
605
- assignment: agent.instruction,
606
- stage: agent.role ?? 'implementation',
607
- model: agent.model,
608
- permissionMode: agent.permissionMode,
609
- toolsSettings: agent.toolsSettings,
610
- prompt: [
611
- `You are ${agent.label} in a sequential Pixcode handoff.`,
612
- `This is step ${index + 1} of ${agents.length}.`,
613
- agent.instruction
614
- ? `Your explicit assignment from the user is: ${agent.instruction}`
615
- : 'Use the prior step output and do the next most useful handoff step for the user goal.',
616
- 'Report changed files, commands, blockers, and the next handoff requirement.',
617
- privacyGuardPrompt(),
618
- 'Respond in the same language as the user request.',
619
- ].filter(Boolean).join('\n'),
620
- inputs: index === 0 ? [] : [safeAgentNodeId(agents[index - 1], index - 1, 'handoff')],
621
- output: 'both',
622
- onFail: 'abort',
623
- })),
708
+ nodes: [
709
+ ...nodes,
710
+ {
711
+ id: 'final_report',
712
+ adapterId: reportAgent.adapterId,
713
+ agentInstanceId: reportAgent.instanceId,
714
+ agentLabel: reportAgent.label,
715
+ stage: 'final_report',
716
+ model: reportAgent.model,
717
+ permissionMode: reportAgent.permissionMode,
718
+ toolsSettings: reportAgent.toolsSettings,
719
+ prompt: [
720
+ 'Create the final user-facing result for this strict handoff run.',
721
+ 'Use the final compact handoff packet and the original user goal.',
722
+ 'Summarize what each visible agent did, what changed, verification, blockers, and next actions.',
723
+ 'Do not expose internal init packets, compact packets, prompts, memory lookup, skill/tool instructions, raw agent logs, or role prefixes like "agent:" and "user:".',
724
+ 'Respond in the same language as the user request.',
725
+ ].join('\n'),
726
+ inputs: [lastCompactNodeId],
727
+ output: 'message',
728
+ onFail: 'abort',
729
+ },
730
+ ],
624
731
  };
625
732
  }
626
733
 
@@ -772,6 +879,7 @@ function nodeRunFromNode(node: WorkflowNode): WorkflowNodeRun {
772
879
  permissionMode: node.permissionMode,
773
880
  timeoutMs: node.timeoutMs,
774
881
  stage: node.stage,
882
+ internal: node.internal,
775
883
  status: 'queued',
776
884
  };
777
885
  }
@@ -17,6 +17,7 @@ export interface WorkflowNode {
17
17
  toolsSettings?: Record<string, unknown>;
18
18
  isolation?: 'host' | 'worktree' | 'docker';
19
19
  timeoutMs?: number;
20
+ internal?: boolean;
20
21
  }
21
22
 
22
23
  export interface Workflow {
@@ -38,6 +39,7 @@ export interface WorkflowNodeRun {
38
39
  permissionMode?: string;
39
40
  timeoutMs?: number;
40
41
  stage?: string;
42
+ internal?: boolean;
41
43
  status: WorkflowNodeStatus;
42
44
  a2aTaskId?: string;
43
45
  startedAt?: number;
@@ -17,6 +17,12 @@ import express from 'express';
17
17
  import { orchestrationTaskService } from '@/modules/orchestration/tasks/orchestration-task.service.js';
18
18
 
19
19
  import { extractProjectDirectory } from '../projects.js';
20
+ import {
21
+ cancelInstallJob,
22
+ createInstallJob,
23
+ getInstallJob,
24
+ snapshotDonePayload
25
+ } from '../services/install-jobs.js';
20
26
  import { broadcastTaskMasterProjectUpdate, broadcastTaskMasterTasksUpdate } from '../utils/taskmaster-websocket.js';
21
27
  import { detectTaskMasterMCPServer } from '../utils/mcp-detector.js';
22
28
 
@@ -144,6 +150,17 @@ async function readTaskMasterTasks(projectName) {
144
150
  return { projectPath, transformedTasks, currentTag };
145
151
  }
146
152
 
153
+ function taskMasterExecutionDescription(task) {
154
+ return [
155
+ task.description ? `Description:\n${task.description}` : '',
156
+ task.details ? `Details:\n${task.details}` : '',
157
+ task.testStrategy ? `Test strategy:\n${task.testStrategy}` : '',
158
+ Array.isArray(task.dependencies) && task.dependencies.length
159
+ ? `Dependencies: ${task.dependencies.join(', ')}`
160
+ : '',
161
+ ].filter(Boolean).join('\n\n');
162
+ }
163
+
147
164
  // API Routes
148
165
 
149
166
  /**
@@ -181,6 +198,121 @@ router.get('/installation-status', async (req, res) => {
181
198
  }
182
199
  });
183
200
 
201
+ /**
202
+ * POST /api/taskmaster/install
203
+ * Install TaskMaster CLI into Pixcode's sandboxed CLI bin.
204
+ */
205
+ router.post('/install', async (req, res) => {
206
+ try {
207
+ const job = createInstallJob({
208
+ provider: 'taskmaster',
209
+ installCmd: 'npm install -g task-master',
210
+ packageName: 'task-master'
211
+ });
212
+
213
+ res.json({
214
+ success: true,
215
+ jobId: job.id,
216
+ provider: 'taskmaster',
217
+ packageName: 'task-master',
218
+ startedAt: job.startedAt
219
+ });
220
+ } catch (error) {
221
+ console.error('TaskMaster install start error:', error);
222
+ res.status(500).json({
223
+ success: false,
224
+ error: 'Failed to start TaskMaster install',
225
+ message: error.message
226
+ });
227
+ }
228
+ });
229
+
230
+ /**
231
+ * GET /api/taskmaster/install/:jobId/stream
232
+ * Replay and stream TaskMaster install output.
233
+ */
234
+ router.get('/install/:jobId/stream', async (req, res) => {
235
+ const job = getInstallJob(req.params.jobId);
236
+ if (!job || job.provider !== 'taskmaster') {
237
+ return res.status(404).json({
238
+ success: false,
239
+ error: 'Install job not found or already expired'
240
+ });
241
+ }
242
+
243
+ res.setHeader('Content-Type', 'text/event-stream');
244
+ res.setHeader('Cache-Control', 'no-cache, no-transform');
245
+ res.setHeader('Connection', 'keep-alive');
246
+ res.setHeader('X-Accel-Buffering', 'no');
247
+ if (typeof res.flushHeaders === 'function') res.flushHeaders();
248
+
249
+ let closed = false;
250
+ const write = (event, payload) => {
251
+ if (closed) return;
252
+ try {
253
+ res.write(`event: ${event}\n`);
254
+ res.write(`data: ${JSON.stringify(payload)}\n\n`);
255
+ } catch {
256
+ // Socket is gone.
257
+ }
258
+ };
259
+
260
+ try { res.write(': start\n\n'); } catch { /* noop */ }
261
+ const heartbeat = setInterval(() => {
262
+ if (!closed) {
263
+ try { res.write(': ping\n\n'); } catch { /* noop */ }
264
+ }
265
+ }, 5000);
266
+
267
+ for (const entry of job.logs) {
268
+ write('log', { stream: entry.stream, chunk: entry.chunk });
269
+ }
270
+
271
+ const cleanup = () => {
272
+ if (closed) return;
273
+ closed = true;
274
+ clearInterval(heartbeat);
275
+ job.emitter.off('log', onLog);
276
+ job.emitter.off('done', onDone);
277
+ };
278
+ const onLog = (entry) => {
279
+ write('log', { stream: entry.stream, chunk: entry.chunk });
280
+ };
281
+ const onDone = (payload) => {
282
+ write('done', payload);
283
+ cleanup();
284
+ try { res.end(); } catch { /* noop */ }
285
+ };
286
+
287
+ if (job.status !== 'running') {
288
+ write('done', snapshotDonePayload(job));
289
+ cleanup();
290
+ try { res.end(); } catch { /* noop */ }
291
+ return;
292
+ }
293
+
294
+ job.emitter.on('log', onLog);
295
+ job.emitter.once('done', onDone);
296
+
297
+ req.on('close', cleanup);
298
+ });
299
+
300
+ /**
301
+ * DELETE /api/taskmaster/install/:jobId
302
+ * Cancel a running TaskMaster install job.
303
+ */
304
+ router.delete('/install/:jobId', async (req, res) => {
305
+ const job = getInstallJob(req.params.jobId);
306
+ if (!job || job.provider !== 'taskmaster') {
307
+ return res.status(404).json({
308
+ success: false,
309
+ error: 'Install job not found'
310
+ });
311
+ }
312
+
313
+ res.json({ success: true, cancelled: cancelInstallJob(req.params.jobId) });
314
+ });
315
+
184
316
  /**
185
317
  * GET /api/taskmaster/tasks/:projectName
186
318
  * Load actual tasks from .taskmaster/tasks/tasks.json
@@ -230,6 +362,75 @@ router.get('/tasks/:projectName', async (req, res) => {
230
362
  }
231
363
  });
232
364
 
365
+ /**
366
+ * POST /api/taskmaster/execute/:projectName/:taskId
367
+ * Import a TaskMaster task into orchestration and dispatch it to a CLI agent.
368
+ */
369
+ router.post('/execute/:projectName/:taskId', async (req, res) => {
370
+ try {
371
+ const { projectName, taskId } = req.params;
372
+ const adapterId = typeof req.body?.adapterId === 'string'
373
+ ? req.body.adapterId
374
+ : typeof req.body?.provider === 'string'
375
+ ? req.body.provider
376
+ : '';
377
+ const model = typeof req.body?.model === 'string' ? req.body.model : undefined;
378
+ const permissionMode = typeof req.body?.permissionMode === 'string' ? req.body.permissionMode : undefined;
379
+ const isolation = ['host', 'worktree', 'docker'].includes(req.body?.isolation)
380
+ ? req.body.isolation
381
+ : 'worktree';
382
+ const projectId = typeof req.body?.projectId === 'string' ? req.body.projectId : projectName;
383
+
384
+ if (!adapterId) {
385
+ return res.status(400).json({
386
+ success: false,
387
+ error: 'Missing adapterId',
388
+ message: 'adapterId or provider is required'
389
+ });
390
+ }
391
+
392
+ const { projectPath, transformedTasks } = await readTaskMasterTasks(projectName);
393
+ const task = transformedTasks.find((candidate) => String(candidate.id) === String(taskId));
394
+ if (!task) {
395
+ return res.status(404).json({
396
+ success: false,
397
+ error: 'TaskMaster task not found',
398
+ message: `Task "${taskId}" was not found in project "${projectName}"`
399
+ });
400
+ }
401
+
402
+ const orchestrationTask = orchestrationTaskService.upsertFromTaskMaster({
403
+ projectId,
404
+ taskmasterId: String(task.id),
405
+ title: `TaskMaster #${task.id}: ${task.title}`,
406
+ description: taskMasterExecutionDescription(task)
407
+ });
408
+
409
+ const dispatchedTask = await orchestrationTaskService.dispatch(orchestrationTask.id, {
410
+ adapterId,
411
+ isolation,
412
+ projectPath,
413
+ model,
414
+ permissionMode
415
+ });
416
+
417
+ res.json({
418
+ success: true,
419
+ projectName,
420
+ projectPath,
421
+ taskmasterTask: task,
422
+ task: dispatchedTask
423
+ });
424
+ } catch (error) {
425
+ console.error('TaskMaster execute error:', error);
426
+ res.status(500).json({
427
+ success: false,
428
+ error: 'Failed to execute TaskMaster task',
429
+ message: error.message
430
+ });
431
+ }
432
+ });
433
+
233
434
  /**
234
435
  * POST /api/taskmaster/sync-orchestration/:projectName
235
436
  * One-way sync: TaskMaster -> Orchestration tasks
@@ -63,6 +63,7 @@ const PACKAGE_BINARIES = {
63
63
  '@google/gemini-cli': 'gemini',
64
64
  '@qwen-code/qwen-code': 'qwen',
65
65
  'opencode-ai': 'opencode',
66
+ 'task-master': 'task-master',
66
67
  };
67
68
 
68
69
  /**