@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.
- package/dist/assets/index-CfHK8y_H.css +32 -0
- package/dist/assets/{index-D-YjltED.js → index-D8uNxHf1.js} +163 -157
- package/dist/index.html +2 -2
- package/dist-server/server/database/db.js +4 -2
- package/dist-server/server/database/db.js.map +1 -1
- package/dist-server/server/index.js +3 -0
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js +10 -1
- package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js.map +1 -1
- package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js +7 -0
- package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js.map +1 -1
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +127 -24
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
- package/dist-server/server/routes/taskmaster.js +194 -0
- package/dist-server/server/routes/taskmaster.js.map +1 -1
- package/dist-server/server/services/install-jobs.js +1 -0
- package/dist-server/server/services/install-jobs.js.map +1 -1
- package/dist-server/server/services/notification-orchestrator.js +66 -9
- package/dist-server/server/services/notification-orchestrator.js.map +1 -1
- package/dist-server/server/services/telegram/control-center.js +144 -2
- package/dist-server/server/services/telegram/control-center.js.map +1 -1
- package/dist-server/server/services/telegram/translations.js +14 -2
- package/dist-server/server/services/telegram/translations.js.map +1 -1
- package/package.json +1 -1
- package/scripts/smoke/chat-realtime-hydration.mjs +44 -0
- package/scripts/smoke/multi-worker-slots.mjs +42 -0
- package/scripts/smoke/notification-center.mjs +63 -0
- package/scripts/smoke/orchestration-execution-dashboard.mjs +33 -0
- package/scripts/smoke/strict-handoff-compact.mjs +60 -0
- package/scripts/smoke/taskmaster-execution-telegram.mjs +52 -0
- package/scripts/smoke/taskmaster-onboarding.mjs +52 -0
- package/scripts/smoke/update-issue-progress.mjs +69 -0
- package/server/database/db.js +4 -2
- package/server/index.js +3 -0
- package/server/modules/orchestration/tasks/orchestration-task.routes.ts +10 -1
- package/server/modules/orchestration/tasks/orchestration-task.service.ts +7 -0
- package/server/modules/orchestration/tasks/orchestration-task.types.ts +3 -0
- package/server/modules/orchestration/workflows/workflow-runner.ts +132 -24
- package/server/modules/orchestration/workflows/workflow.types.ts +2 -0
- package/server/routes/taskmaster.js +201 -0
- package/server/services/install-jobs.js +1 -0
- package/server/services/notification-orchestrator.js +76 -8
- package/server/services/telegram/control-center.js +153 -2
- package/server/services/telegram/translations.js +14 -2
- 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');
|
package/server/database/db.js
CHANGED
|
@@ -493,7 +493,7 @@ const credentialsDb = {
|
|
|
493
493
|
// Notification preferences
|
|
494
494
|
// ---------------------------------------------------------------------------
|
|
495
495
|
const DEFAULT_NOTIFICATION_PREFERENCES = {
|
|
496
|
-
channels: { inApp:
|
|
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
|
|
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, {
|
|
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);
|
|
@@ -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:
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|