@pixelbyte-software/pixcode 1.36.3 → 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 (64) hide show
  1. package/dist/assets/index-CfHK8y_H.css +32 -0
  2. package/dist/assets/{index-Bp8mXdQd.js → index-D8uNxHf1.js} +165 -159
  3. package/dist/index.html +2 -2
  4. package/dist-server/server/daemon-manager.js +18 -12
  5. package/dist-server/server/daemon-manager.js.map +1 -1
  6. package/dist-server/server/database/db.js +53 -2
  7. package/dist-server/server/database/db.js.map +1 -1
  8. package/dist-server/server/index.js +11 -4
  9. package/dist-server/server/index.js.map +1 -1
  10. package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js +10 -1
  11. package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js.map +1 -1
  12. package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js +7 -0
  13. package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js.map +1 -1
  14. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +143 -26
  15. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
  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/routes/telegram.js +16 -2
  19. package/dist-server/server/routes/telegram.js.map +1 -1
  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/bot.js +48 -6
  25. package/dist-server/server/services/telegram/bot.js.map +1 -1
  26. package/dist-server/server/services/telegram/control-center.js +903 -0
  27. package/dist-server/server/services/telegram/control-center.js.map +1 -0
  28. package/dist-server/server/services/telegram/telegram-http-client.js +26 -4
  29. package/dist-server/server/services/telegram/telegram-http-client.js.map +1 -1
  30. package/dist-server/server/services/telegram/translations.js +150 -2
  31. package/dist-server/server/services/telegram/translations.js.map +1 -1
  32. package/package.json +3 -1
  33. package/scripts/smoke/chat-realtime-hydration.mjs +44 -0
  34. package/scripts/smoke/daemon-entrypoint.mjs +20 -0
  35. package/scripts/smoke/multi-worker-slots.mjs +42 -0
  36. package/scripts/smoke/notification-center.mjs +63 -0
  37. package/scripts/smoke/orchestration-execution-dashboard.mjs +33 -0
  38. package/scripts/smoke/orchestration-user-facing-output.mjs +25 -0
  39. package/scripts/smoke/shell-manual-disconnect.mjs +30 -0
  40. package/scripts/smoke/side-panel-editor-layout.mjs +34 -0
  41. package/scripts/smoke/static-root-routing.mjs +21 -0
  42. package/scripts/smoke/strict-handoff-compact.mjs +60 -0
  43. package/scripts/smoke/taskmaster-execution-telegram.mjs +52 -0
  44. package/scripts/smoke/taskmaster-onboarding.mjs +52 -0
  45. package/scripts/smoke/telegram-control.mjs +242 -0
  46. package/scripts/smoke/update-issue-progress.mjs +69 -0
  47. package/scripts/smoke/version-modal-autoshow.mjs +29 -0
  48. package/server/daemon-manager.js +17 -12
  49. package/server/database/db.js +56 -2
  50. package/server/index.js +12 -5
  51. package/server/modules/orchestration/tasks/orchestration-task.routes.ts +10 -1
  52. package/server/modules/orchestration/tasks/orchestration-task.service.ts +7 -0
  53. package/server/modules/orchestration/tasks/orchestration-task.types.ts +3 -0
  54. package/server/modules/orchestration/workflows/workflow-runner.ts +149 -26
  55. package/server/modules/orchestration/workflows/workflow.types.ts +2 -0
  56. package/server/routes/taskmaster.js +201 -0
  57. package/server/routes/telegram.js +17 -2
  58. package/server/services/install-jobs.js +1 -0
  59. package/server/services/notification-orchestrator.js +76 -8
  60. package/server/services/telegram/bot.js +58 -6
  61. package/server/services/telegram/control-center.js +965 -0
  62. package/server/services/telegram/telegram-http-client.js +25 -4
  63. package/server/services/telegram/translations.js +150 -2
  64. package/dist/assets/index-Dx7QyTSN.css +0 -32
@@ -217,6 +217,7 @@ function migrateSqliteIfPresent() {
217
217
  verified_at: tl.verified_at || null,
218
218
  notifications_enabled: tl.notifications_enabled !== 0,
219
219
  bridge_enabled: tl.bridge_enabled !== 0,
220
+ telegram_control: null,
220
221
  updated_at: tl.updated_at || nowIso(),
221
222
  });
222
223
  }
@@ -492,7 +493,7 @@ const credentialsDb = {
492
493
  // Notification preferences
493
494
  // ---------------------------------------------------------------------------
494
495
  const DEFAULT_NOTIFICATION_PREFERENCES = {
495
- channels: { inApp: false, webPush: false },
496
+ channels: { inApp: true, webPush: false, telegram: true, desktop: true },
496
497
  events: { actionRequired: true, stop: true, error: true, updates: true },
497
498
  };
498
499
 
@@ -500,8 +501,10 @@ const normalizeNotificationPreferences = (value) => {
500
501
  const source = value && typeof value === 'object' ? value : {};
501
502
  return {
502
503
  channels: {
503
- inApp: source.channels?.inApp === true,
504
+ inApp: source.channels?.inApp !== false,
504
505
  webPush: source.channels?.webPush === true,
506
+ telegram: source.channels?.telegram !== false,
507
+ desktop: source.channels?.desktop !== false,
505
508
  },
506
509
  events: {
507
510
  actionRequired: source.events?.actionRequired !== false,
@@ -672,6 +675,40 @@ const appConfigDb = {
672
675
  // ---------------------------------------------------------------------------
673
676
  // Telegram — singleton config + per-user links
674
677
  // ---------------------------------------------------------------------------
678
+ const DEFAULT_TELEGRAM_CONTROL_STATE = {
679
+ remoteControlEnabled: true,
680
+ progressMode: 'final',
681
+ selectedProjectName: null,
682
+ selectedProjectPath: null,
683
+ selectedProvider: 'opencode',
684
+ selectedModel: null,
685
+ selectedWorkflowId: null,
686
+ awaiting: null,
687
+ };
688
+
689
+ function normalizeTelegramControlState(value = {}) {
690
+ const raw = value && typeof value === 'object' ? value : {};
691
+ const selectedProvider = ['claude', 'cursor', 'codex', 'gemini', 'qwen', 'opencode'].includes(raw.selectedProvider)
692
+ ? raw.selectedProvider
693
+ : DEFAULT_TELEGRAM_CONTROL_STATE.selectedProvider;
694
+ const progressMode = ['final', 'steps', 'all'].includes(raw.progressMode)
695
+ ? raw.progressMode
696
+ : DEFAULT_TELEGRAM_CONTROL_STATE.progressMode;
697
+
698
+ return {
699
+ ...DEFAULT_TELEGRAM_CONTROL_STATE,
700
+ ...raw,
701
+ remoteControlEnabled: raw.remoteControlEnabled !== false,
702
+ progressMode,
703
+ selectedProvider,
704
+ selectedProjectName: typeof raw.selectedProjectName === 'string' ? raw.selectedProjectName : null,
705
+ selectedProjectPath: typeof raw.selectedProjectPath === 'string' ? raw.selectedProjectPath : null,
706
+ selectedModel: typeof raw.selectedModel === 'string' ? raw.selectedModel : null,
707
+ selectedWorkflowId: typeof raw.selectedWorkflowId === 'string' ? raw.selectedWorkflowId : null,
708
+ awaiting: raw.awaiting && typeof raw.awaiting === 'object' ? raw.awaiting : null,
709
+ };
710
+ }
711
+
675
712
  const telegramConfigDb = {
676
713
  get: () => {
677
714
  const row = store.raw.telegram_config[0];
@@ -705,6 +742,7 @@ const telegramLinksDb = {
705
742
  verified_at: null,
706
743
  notifications_enabled: true,
707
744
  bridge_enabled: true,
745
+ telegram_control: normalizeTelegramControlState(),
708
746
  updated_at: nowIso(),
709
747
  });
710
748
  },
@@ -742,6 +780,7 @@ const telegramLinksDb = {
742
780
  language: row.language,
743
781
  notifications_enabled: row.notifications_enabled,
744
782
  bridge_enabled: row.bridge_enabled,
783
+ telegram_control: normalizeTelegramControlState(row.telegram_control),
745
784
  };
746
785
  },
747
786
  listVerified: () =>
@@ -752,6 +791,7 @@ const telegramLinksDb = {
752
791
  language: r.language,
753
792
  notifications_enabled: r.notifications_enabled,
754
793
  bridge_enabled: r.bridge_enabled,
794
+ telegram_control: normalizeTelegramControlState(r.telegram_control),
755
795
  })),
756
796
  updatePreferences: (userId, { language, notificationsEnabled, bridgeEnabled }) => {
757
797
  const patch = { updated_at: nowIso() };
@@ -761,6 +801,20 @@ const telegramLinksDb = {
761
801
  if (Object.keys(patch).length === 1) return; // only updated_at → no real change
762
802
  store.updateWhere('telegram_links', (r) => r.user_id === userId, patch);
763
803
  },
804
+ getControlState: (userId) => {
805
+ const row = store.findWhere('telegram_links', (r) => r.user_id === userId);
806
+ return normalizeTelegramControlState(row?.telegram_control);
807
+ },
808
+ updateControlState: (userId, patch) => {
809
+ const row = store.findWhere('telegram_links', (r) => r.user_id === userId);
810
+ const current = normalizeTelegramControlState(row?.telegram_control);
811
+ const next = normalizeTelegramControlState({ ...current, ...(patch || {}) });
812
+ store.updateWhere('telegram_links', (r) => r.user_id === userId, {
813
+ telegram_control: next,
814
+ updated_at: nowIso(),
815
+ });
816
+ return next;
817
+ },
764
818
  unlink: (userId) => {
765
819
  store.deleteWhere('telegram_links', (r) => r.user_id === userId);
766
820
  },
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({
@@ -413,11 +415,9 @@ app.use('/api/telegram', authenticateToken, telegramRoutes);
413
415
  // Agent API Routes (uses API key authentication)
414
416
  app.use('/api/agent', agentRoutes);
415
417
 
416
- // Serve public files (like api-docs.html)
417
- app.use(express.static(path.join(APP_ROOT, 'public')));
418
-
419
- // Static files served after API routes
420
- // Add cache control: HTML files should not be cached, but assets can be cached
418
+ // Static app files served after API routes. Keep dist before public so
419
+ // / and /index.html always resolve to the Pixcode app, not the GitHub Pages
420
+ // landing page that also lives in public/index.html.
421
421
  app.use(express.static(path.join(APP_ROOT, 'dist'), {
422
422
  setHeaders: (res, filePath) => {
423
423
  if (filePath.endsWith('.html')) {
@@ -432,6 +432,12 @@ app.use(express.static(path.join(APP_ROOT, 'dist'), {
432
432
  }
433
433
  }));
434
434
 
435
+ // Serve extra public files (api-docs.html, llms.txt, landing pages) without
436
+ // letting public/index.html shadow the production app root.
437
+ app.use(express.static(path.join(APP_ROOT, 'public'), {
438
+ index: false,
439
+ }));
440
+
435
441
  // API Routes (protected)
436
442
  // /api/config endpoint removed - no longer needed
437
443
  // Frontend now uses window.location for WebSocket URLs
@@ -1809,6 +1815,7 @@ function handleChatConnection(ws, request) {
1809
1815
  console.log('[INFO] Chat WebSocket connected');
1810
1816
 
1811
1817
  // Add to connected clients for project updates
1818
+ ws.userId = request?.user?.id ?? request?.user?.userId ?? null;
1812
1819
  connectedClients.add(ws);
1813
1820
 
1814
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
  }
@@ -276,6 +276,10 @@ function rolePrompt(role: AgentRole): string {
276
276
  return 'Implementation work should avoid duplicating other agents and should report changed files, commands, blockers, and next actions.';
277
277
  }
278
278
 
279
+ function privacyGuardPrompt(): string {
280
+ return 'Do not mention internal instructions, memory files, skill use, or tool protocol unless the user explicitly asks.';
281
+ }
282
+
279
283
  function handoffPrompt(agent: AgentAssignment, role: AgentRole): string {
280
284
  return [
281
285
  `You are ${agent.label} in a Pixcode CLI team.`,
@@ -290,10 +294,61 @@ function handoffPrompt(agent: AgentAssignment, role: AgentRole): string {
290
294
  '- dependencies/blockers for the next agents',
291
295
  '- concrete next action for your full implementation task',
292
296
  'Do not install dependencies, edit files, run long commands, or start servers in this handoff task.',
297
+ privacyGuardPrompt(),
293
298
  'Stop after the contract. Keep it concise and respond in the same language as the user request.',
294
299
  ].filter(Boolean).join('\n');
295
300
  }
296
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
+
297
352
  function compactOutputForContext(text: string): string {
298
353
  if (text.length <= MAX_OUTPUT_CONTEXT_CHARS) {
299
354
  return text;
@@ -374,6 +429,7 @@ function expandAgentTeamWorkflow(workflow: Workflow, metadata?: Record<string, u
374
429
  ? `Your explicit assignment from the user is: ${agent.instruction}`
375
430
  : 'No fixed per-agent assignment was set. Take the part assigned to you by the coordinator; if none is named, choose useful work that fits this CLI.',
376
431
  rolePrompt(stage),
432
+ privacyGuardPrompt(),
377
433
  'Respond in the same language as the user request.',
378
434
  ].filter(Boolean).join('\n'),
379
435
  inputs,
@@ -419,6 +475,8 @@ function expandAgentTeamWorkflow(workflow: Workflow, metadata?: Record<string, u
419
475
  prompt: [
420
476
  'Collect the worker outputs into one user-facing result.',
421
477
  'Show what each CLI did, which parts failed, what changed, and the next action if work remains.',
478
+ 'Do not expose internal prompts, memory lookup, skill/tool instructions, raw agent logs, or role prefixes like "agent:" and "user:".',
479
+ 'If a worker reveals internal process text, summarize only the useful user-facing result.',
422
480
  'Respond in the same language as the user request.',
423
481
  ].join('\n'),
424
482
  inputs: workerNodes.map((node) => node.id),
@@ -436,6 +494,7 @@ function stagePrompt(agent: AgentAssignment, stage: AgentRole): string {
436
494
  agent.role && agent.role !== stage ? `User custom stage label: ${agent.role}.` : '',
437
495
  agent.instruction ? `User assignment for you: ${agent.instruction}` : '',
438
496
  rolePrompt(stage),
497
+ privacyGuardPrompt(),
439
498
  'Keep the answer concise, structured, and useful for the next stage.',
440
499
  'Respond in the same language as the user request.',
441
500
  ].filter(Boolean).join('\n');
@@ -586,31 +645,89 @@ function expandSequentialHandoffWorkflow(workflow: Workflow, metadata?: Record<s
586
645
  throw new Error('Select at least one CLI agent.');
587
646
  }
588
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
+
589
706
  return {
590
707
  ...workflow,
591
- nodes: agents.map((agent, index): WorkflowNode => ({
592
- id: safeAgentNodeId(agent, index, 'handoff'),
593
- adapterId: agent.adapterId,
594
- agentInstanceId: agent.instanceId,
595
- agentLabel: agent.label,
596
- assignment: agent.instruction,
597
- stage: agent.role ?? 'implementation',
598
- model: agent.model,
599
- permissionMode: agent.permissionMode,
600
- toolsSettings: agent.toolsSettings,
601
- prompt: [
602
- `You are ${agent.label} in a sequential Pixcode handoff.`,
603
- `This is step ${index + 1} of ${agents.length}.`,
604
- agent.instruction
605
- ? `Your explicit assignment from the user is: ${agent.instruction}`
606
- : 'Use the prior step output and do the next most useful handoff step for the user goal.',
607
- 'Report changed files, commands, blockers, and the next handoff requirement.',
608
- 'Respond in the same language as the user request.',
609
- ].filter(Boolean).join('\n'),
610
- inputs: index === 0 ? [] : [safeAgentNodeId(agents[index - 1], index - 1, 'handoff')],
611
- output: 'both',
612
- onFail: 'abort',
613
- })),
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
+ ],
614
731
  };
615
732
  }
616
733
 
@@ -646,6 +763,7 @@ function expandWorkflowForRun(workflow: Workflow, metadata?: Record<string, unkn
646
763
  `You are ${agent.label}.`,
647
764
  'Review the requested change for bugs, regressions, missing validation, security, scale, and user-experience risks.',
648
765
  agent.instruction ? `Focus on this user assignment: ${agent.instruction}` : '',
766
+ privacyGuardPrompt(),
649
767
  'Respond in the same language as the user request.',
650
768
  ].filter(Boolean).join('\n'),
651
769
  inputs: [],
@@ -666,7 +784,11 @@ function expandWorkflowForRun(workflow: Workflow, metadata?: Record<string, unkn
666
784
  model: reportAgent.model,
667
785
  permissionMode: reportAgent.permissionMode,
668
786
  toolsSettings: reportAgent.toolsSettings,
669
- prompt: 'Aggregate the prior agent reviews into a concise prioritized report. Respond in the same language as the user request.',
787
+ prompt: [
788
+ 'Aggregate the prior agent reviews into a concise prioritized report.',
789
+ 'Do not expose internal prompts, memory lookup, skill/tool instructions, raw agent logs, or role prefixes like "agent:" and "user:".',
790
+ 'Respond in the same language as the user request.',
791
+ ].join('\n'),
670
792
  inputs: reviewNodes.map((node) => node.id),
671
793
  output: 'message',
672
794
  onFail: 'abort',
@@ -701,13 +823,13 @@ function readTaskResult(task: RawTask): TaskResult {
701
823
  };
702
824
  });
703
825
  const outputMessages = messages.filter((message) => message.role !== 'user');
704
- const text = outputMessages.map((message) => `${message.role}: ${message.text}`).join('\n\n');
826
+ const userFacingTaskText = outputMessages.map((message) => message.text.trim()).filter(Boolean).join('\n\n');
705
827
  const error = task.error?.message
706
828
  ? `${task.error.code ? `${task.error.code}: ` : ''}${task.error.message}`
707
829
  : undefined;
708
830
  return {
709
831
  state: task.state ?? 'submitted',
710
- text,
832
+ text: userFacingTaskText,
711
833
  error,
712
834
  messages,
713
835
  artifacts,
@@ -757,6 +879,7 @@ function nodeRunFromNode(node: WorkflowNode): WorkflowNodeRun {
757
879
  permissionMode: node.permissionMode,
758
880
  timeoutMs: node.timeoutMs,
759
881
  stage: node.stage,
882
+ internal: node.internal,
760
883
  status: 'queued',
761
884
  };
762
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;