@pixelbyte-software/pixcode 1.36.4 → 1.38.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 (56) hide show
  1. package/dist/assets/{index-D-YjltED.js → index-C-gVa0Gf.js} +163 -157
  2. package/dist/assets/index-CfHK8y_H.css +32 -0
  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 +8 -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/diagnostics.js +12 -0
  15. package/dist-server/server/routes/diagnostics.js.map +1 -0
  16. package/dist-server/server/routes/taskmaster.js +194 -0
  17. package/dist-server/server/routes/taskmaster.js.map +1 -1
  18. package/dist-server/server/services/diagnostics.js +91 -0
  19. package/dist-server/server/services/diagnostics.js.map +1 -0
  20. package/dist-server/server/services/install-jobs.js +1 -0
  21. package/dist-server/server/services/install-jobs.js.map +1 -1
  22. package/dist-server/server/services/notification-orchestrator.js +66 -9
  23. package/dist-server/server/services/notification-orchestrator.js.map +1 -1
  24. package/dist-server/server/services/telegram/control-center.js +144 -2
  25. package/dist-server/server/services/telegram/control-center.js.map +1 -1
  26. package/dist-server/server/services/telegram/translations.js +14 -2
  27. package/dist-server/server/services/telegram/translations.js.map +1 -1
  28. package/package.json +5 -1
  29. package/scripts/github/create-v1.38-issues.mjs +351 -0
  30. package/scripts/smoke/chat-realtime-hydration.mjs +44 -0
  31. package/scripts/smoke/discord-release-workflow.mjs +24 -0
  32. package/scripts/smoke/multi-worker-slots.mjs +42 -0
  33. package/scripts/smoke/notification-center.mjs +63 -0
  34. package/scripts/smoke/orchestration-execution-dashboard.mjs +33 -0
  35. package/scripts/smoke/strict-handoff-compact.mjs +60 -0
  36. package/scripts/smoke/taskmaster-execution-telegram.mjs +52 -0
  37. package/scripts/smoke/taskmaster-onboarding.mjs +52 -0
  38. package/scripts/smoke/update-issue-progress.mjs +69 -0
  39. package/scripts/smoke/v138-desktop-release-hardening.mjs +69 -0
  40. package/scripts/smoke/v138-diagnostics.mjs +63 -0
  41. package/scripts/smoke/v138-issue-planner.mjs +33 -0
  42. package/server/database/db.js +4 -2
  43. package/server/index.js +9 -0
  44. package/server/modules/orchestration/tasks/orchestration-task.routes.ts +10 -1
  45. package/server/modules/orchestration/tasks/orchestration-task.service.ts +7 -0
  46. package/server/modules/orchestration/tasks/orchestration-task.types.ts +3 -0
  47. package/server/modules/orchestration/workflows/workflow-runner.ts +132 -24
  48. package/server/modules/orchestration/workflows/workflow.types.ts +2 -0
  49. package/server/routes/diagnostics.js +15 -0
  50. package/server/routes/taskmaster.js +201 -0
  51. package/server/services/diagnostics.js +105 -0
  52. package/server/services/install-jobs.js +1 -0
  53. package/server/services/notification-orchestrator.js +76 -8
  54. package/server/services/telegram/control-center.js +153 -2
  55. package/server/services/telegram/translations.js +14 -2
  56. 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');
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+
4
+ function readText(path) {
5
+ return fs.readFileSync(path, 'utf8');
6
+ }
7
+
8
+ function readJson(path) {
9
+ return JSON.parse(readText(path));
10
+ }
11
+
12
+ function assertIncludes(path, patterns) {
13
+ const content = readText(path);
14
+ for (const pattern of patterns) {
15
+ if (!content.includes(pattern)) {
16
+ throw new Error(`${path} is missing required release-hardening text: ${pattern}`);
17
+ }
18
+ }
19
+ }
20
+
21
+ const rootPackage = readJson('package.json');
22
+ const desktopPackage = readJson('desktop/package.json');
23
+ const bundledPixcodeVersion = desktopPackage.dependencies?.['@pixelbyte-software/pixcode'];
24
+
25
+ if (desktopPackage.version !== rootPackage.version) {
26
+ throw new Error(`desktop package version ${desktopPackage.version} does not match root ${rootPackage.version}`);
27
+ }
28
+
29
+ if (bundledPixcodeVersion !== rootPackage.version) {
30
+ throw new Error(`desktop bundled pixcode version ${bundledPixcodeVersion} does not match root ${rootPackage.version}`);
31
+ }
32
+
33
+ assertIncludes('README.md', [
34
+ 'macOS Gatekeeper: "Pixcode is damaged"',
35
+ 'Fix Gatekeeper.command',
36
+ 'xattr -dr com.apple.quarantine',
37
+ 'unsigned',
38
+ ]);
39
+
40
+ assertIncludes('README.tr.md', [
41
+ 'macOS Gatekeeper: "Pixcode hasar görmüş"',
42
+ 'Fix Gatekeeper.command',
43
+ 'xattr -dr com.apple.quarantine',
44
+ 'imzalı/notarize değil',
45
+ ]);
46
+
47
+ assertIncludes('desktop/README.md', [
48
+ 'macOS Gatekeeper fix',
49
+ 'Fix Gatekeeper.command',
50
+ 'xattr -dr com.apple.quarantine',
51
+ 'unsigned',
52
+ ]);
53
+
54
+ assertIncludes('desktop/electron-builder.yml', [
55
+ 'Pixcode-Setup-${version}.${ext}',
56
+ 'Pixcode-${version}-${arch}.${ext}',
57
+ 'target: AppImage',
58
+ 'build-resources/Fix Gatekeeper.command',
59
+ 'identity: null',
60
+ ]);
61
+
62
+ assertIncludes('desktop/build-resources/Fix Gatekeeper.command', [
63
+ 'Pixcode Gatekeeper Fix',
64
+ 'xattr -cr "$APP_PATH"',
65
+ 'com.apple.quarantine',
66
+ 'open "$APP_PATH"',
67
+ ]);
68
+
69
+ console.log('v1.38 desktop release hardening smoke passed');
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ import { collectDiagnostics } from '../../server/services/diagnostics.js';
3
+
4
+ const diagnostics = collectDiagnostics({
5
+ now: new Date('2026-05-10T00:00:00.000Z'),
6
+ env: {
7
+ NODE_ENV: 'test',
8
+ SERVER_PORT: '3001',
9
+ GITHUB_TOKEN: 'ghp_super_secret_value',
10
+ NPM_TOKEN: 'npm_super_secret_value',
11
+ TELEGRAM_BOT_TOKEN: 'telegram_super_secret_value',
12
+ },
13
+ uptime: 42,
14
+ memoryUsage: () => ({
15
+ rss: 10,
16
+ heapTotal: 20,
17
+ heapUsed: 5,
18
+ external: 1,
19
+ arrayBuffers: 0,
20
+ }),
21
+ versions: {
22
+ node: '22.0.0',
23
+ v8: '12.0.0',
24
+ },
25
+ platform: 'linux',
26
+ arch: 'x64',
27
+ installMode: 'git',
28
+ serverVersion: '1.38.0',
29
+ wsClientCount: 3,
30
+ });
31
+
32
+ const raw = JSON.stringify(diagnostics);
33
+ for (const secret of ['ghp_super_secret_value', 'npm_super_secret_value', 'telegram_super_secret_value']) {
34
+ if (raw.includes(secret)) {
35
+ throw new Error(`diagnostics leaked secret value: ${secret}`);
36
+ }
37
+ }
38
+
39
+ if (diagnostics.status !== 'ok') {
40
+ throw new Error(`expected ok diagnostics status, got ${diagnostics.status}`);
41
+ }
42
+
43
+ if (diagnostics.timestamp !== '2026-05-10T00:00:00.000Z') {
44
+ throw new Error(`unexpected diagnostics timestamp ${diagnostics.timestamp}`);
45
+ }
46
+
47
+ if (diagnostics.version !== '1.38.0' || diagnostics.installMode !== 'git') {
48
+ throw new Error('diagnostics did not include injected version/install mode');
49
+ }
50
+
51
+ if (diagnostics.websocket.clients !== 3) {
52
+ throw new Error(`expected 3 websocket clients, got ${diagnostics.websocket.clients}`);
53
+ }
54
+
55
+ if (diagnostics.environment.GITHUB_TOKEN !== '[redacted]') {
56
+ throw new Error('GITHUB_TOKEN was not redacted');
57
+ }
58
+
59
+ if (diagnostics.environment.NODE_ENV !== 'test') {
60
+ throw new Error('safe environment value was not preserved');
61
+ }
62
+
63
+ console.log('v1.38 diagnostics smoke passed');
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ import { buildPayload } from '../github/create-v1.38-issues.mjs';
3
+
4
+ const payload = buildPayload();
5
+
6
+ if (payload.version !== '1.38') {
7
+ throw new Error(`expected version 1.38, got ${payload.version}`);
8
+ }
9
+
10
+ if (!Array.isArray(payload.issues) || payload.issues.length !== 7) {
11
+ throw new Error(`expected 7 child issues, got ${payload.issues?.length}`);
12
+ }
13
+
14
+ if (!payload.epic || !payload.epic.title.includes('v1.38')) {
15
+ throw new Error('missing v1.38 epic payload');
16
+ }
17
+
18
+ const expectedKeys = ['remote', 'api', 'telegram', 'taskmaster', 'plugins', 'desktop', 'observability'];
19
+ for (const key of expectedKeys) {
20
+ const issue = payload.issues.find(item => item.key === key);
21
+ if (!issue) {
22
+ throw new Error(`missing issue key ${key}`);
23
+ }
24
+ if (!issue.title || !issue.body.includes('## Acceptance Criteria')) {
25
+ throw new Error(`issue ${key} is missing title or acceptance criteria`);
26
+ }
27
+ }
28
+
29
+ if (!payload.trackingReplacements || payload.trackingReplacements.length !== 8) {
30
+ throw new Error('expected replacement plan for 7 issues plus epic');
31
+ }
32
+
33
+ console.log('v1.38 issue planner 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
@@ -75,6 +75,7 @@ import geminiRoutes from './routes/gemini.js';
75
75
  import qwenRoutes from './routes/qwen.js';
76
76
  import pluginsRoutes from './routes/plugins.js';
77
77
  import messagesRoutes from './routes/messages.js';
78
+ import diagnosticsRoutes from './routes/diagnostics.js';
78
79
  import providerRoutes from './modules/providers/provider.routes.js';
79
80
  import {
80
81
  createA2ARouter,
@@ -99,6 +100,7 @@ import {
99
100
  import { primeCliBinPath } from './services/install-jobs.js';
100
101
  import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
101
102
  import { initializeDatabase, sessionNamesDb, applyCustomSessionNames } from './database/db.js';
103
+ import { setNotificationWebSocketServer } from './services/notification-orchestrator.js';
102
104
  import { configureWebPush } from './services/vapid-keys.js';
103
105
  import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
104
106
  import { IS_PLATFORM } from './constants/config.js';
@@ -319,6 +321,9 @@ const wss = new WebSocketServer({
319
321
 
320
322
  // Make WebSocket server available to routes
321
323
  app.locals.wss = wss;
324
+ app.locals.installMode = installMode;
325
+ app.locals.serverVersion = SERVER_VERSION;
326
+ setNotificationWebSocketServer(wss);
322
327
 
323
328
  app.use(cors({ exposedHeaders: ['X-Refreshed-Token'] }));
324
329
  app.use(express.json({
@@ -389,6 +394,9 @@ app.use('/api/plugins', authenticateToken, pluginsRoutes);
389
394
  // Unified session messages route (protected)
390
395
  app.use('/api/sessions', authenticateToken, messagesRoutes);
391
396
 
397
+ // Diagnostics API Routes (protected)
398
+ app.use('/api/diagnostics', authenticateToken, diagnosticsRoutes);
399
+
392
400
  // Unified provider MCP routes (protected)
393
401
  app.use('/api/providers', authenticateToken, providerRoutes);
394
402
 
@@ -1813,6 +1821,7 @@ function handleChatConnection(ws, request) {
1813
1821
  console.log('[INFO] Chat WebSocket connected');
1814
1822
 
1815
1823
  // Add to connected clients for project updates
1824
+ ws.userId = request?.user?.id ?? request?.user?.userId ?? null;
1816
1825
  connectedClients.add(ws);
1817
1826
 
1818
1827
  // 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;
@@ -0,0 +1,15 @@
1
+ import express from 'express';
2
+
3
+ import { collectDiagnostics } from '../services/diagnostics.js';
4
+
5
+ const router = express.Router();
6
+
7
+ router.get('/', (req, res) => {
8
+ res.json(collectDiagnostics({
9
+ installMode: req.app.locals.installMode,
10
+ serverVersion: req.app.locals.serverVersion,
11
+ wss: req.app.locals.wss,
12
+ }));
13
+ });
14
+
15
+ export default router;