claudeboard 3.1.0 → 3.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeboard",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
4
4
  "description": "Visual orchestrator for Claude Code agent teams",
5
5
  "bin": {
6
6
  "claudeboard": "bin/cli.js"
@@ -32,5 +32,8 @@
32
32
  "express": "^4.18.2",
33
33
  "open": "^10.1.0",
34
34
  "ws": "^8.16.0"
35
+ },
36
+ "devDependencies": {
37
+ "supabase": "^2.78.1"
35
38
  }
36
39
  }
package/public/app.js CHANGED
@@ -76,6 +76,13 @@ const dom = {
76
76
  deployBtn: document.getElementById('deployBtn'),
77
77
  // Board empty state
78
78
  boardEmptyState: document.getElementById('boardEmptyState'),
79
+ // Human action drawer
80
+ humanActionDrawer: document.getElementById('humanActionDrawer'),
81
+ humanActionTitle: document.getElementById('humanActionTitle'),
82
+ humanActionBody: document.getElementById('humanActionBody'),
83
+ humanActionTaskId: document.getElementById('humanActionTaskId'),
84
+ humanActionResolve: document.getElementById('humanActionResolve'),
85
+ humanActionDismiss: document.getElementById('humanActionDismiss'),
79
86
  // PRD upload
80
87
  prdFileInput: document.getElementById('prdFileInput'),
81
88
  // Supabase modal
@@ -257,6 +264,9 @@ function handleMessage(msg) {
257
264
  case 'deploy:done':
258
265
  handleDeployDone(msg.url);
259
266
  break;
267
+ case 'task:blocked':
268
+ handleTaskBlocked(msg.taskId, msg.taskTitle, msg.humanActions);
269
+ break;
260
270
  case 'supabase:preflight':
261
271
  handleSupabasePreflight(msg.taskTitles || []);
262
272
  break;
@@ -570,6 +580,44 @@ function handleDeployDone(url) {
570
580
  }
571
581
  }
572
582
 
583
+ // ── Human Intervention Notification ──────────────────────────
584
+ function handleTaskBlocked(taskId, taskTitle, humanActions) {
585
+ // Update task state
586
+ const task = state.tasks.get(taskId);
587
+ if (task) {
588
+ task.status = 'error';
589
+ task.needsHuman = true;
590
+ task.humanActions = humanActions;
591
+ state.tasks.set(taskId, task);
592
+ renderBoard();
593
+ }
594
+
595
+ // Show notification drawer
596
+ const drawer = dom.humanActionDrawer;
597
+ if (!drawer) return;
598
+
599
+ dom.humanActionTitle.textContent = taskTitle || 'Tarea';
600
+ dom.humanActionBody.innerHTML = '';
601
+
602
+ // Render each action as a list item
603
+ const lines = (humanActions || '').split('\n').map(l => l.trim()).filter(Boolean);
604
+ lines.forEach(line => {
605
+ const li = document.createElement('div');
606
+ li.className = 'human-action-item';
607
+ // Strip leading dashes/bullets
608
+ li.textContent = line.replace(/^[-•*]\s*/, '');
609
+ dom.humanActionBody.appendChild(li);
610
+ });
611
+
612
+ dom.humanActionTaskId.value = taskId;
613
+ drawer.classList.add('visible');
614
+ drawer.dataset.taskId = taskId;
615
+ }
616
+
617
+ function dismissHumanDrawer() {
618
+ dom.humanActionDrawer.classList.remove('visible');
619
+ }
620
+
573
621
  // ── Supabase Pre-flight Modal ─────────────────────────────────
574
622
  function handleSupabasePreflight(taskTitles) {
575
623
  // Populate task list
@@ -1302,6 +1350,18 @@ if (dom.deployBtn) {
1302
1350
 
1303
1351
  dom.prdBtn.addEventListener('click', openPRD);
1304
1352
 
1353
+ // Human action drawer
1354
+ if (dom.humanActionResolve) {
1355
+ dom.humanActionResolve.addEventListener('click', () => {
1356
+ const taskId = dom.humanActionDrawer.dataset.taskId;
1357
+ if (taskId) sendWS({ type: 'task:resolve', taskId });
1358
+ dismissHumanDrawer();
1359
+ });
1360
+ }
1361
+ if (dom.humanActionDismiss) {
1362
+ dom.humanActionDismiss.addEventListener('click', dismissHumanDrawer);
1363
+ }
1364
+
1305
1365
  // PRD file upload
1306
1366
  dom.prdFileInput.addEventListener('change', (e) => {
1307
1367
  handlePRDFileUpload(e.target.files[0]);
package/public/index.html CHANGED
@@ -190,6 +190,29 @@
190
190
  <span id="qaToastText">Agente QA revisando el proyecto…</span>
191
191
  </div>
192
192
 
193
+ <!-- Human Action Required Drawer -->
194
+ <div id="humanActionDrawer" class="human-drawer">
195
+ <div class="human-drawer-inner">
196
+ <div class="human-drawer-header">
197
+ <div class="human-drawer-title-row">
198
+ <span class="human-drawer-icon">&#9888;</span>
199
+ <div>
200
+ <div class="human-drawer-label">Acción requerida &mdash; los agentes están esperando</div>
201
+ <div class="human-drawer-task" id="humanActionTitle"></div>
202
+ </div>
203
+ </div>
204
+ <button class="btn-icon" id="humanActionDismiss" title="Cerrar">&#10005;</button>
205
+ </div>
206
+ <div class="human-drawer-desc">El agente completó el código pero necesita que vos hagas estos pasos externos:</div>
207
+ <div class="human-action-list" id="humanActionBody"></div>
208
+ <input type="hidden" id="humanActionTaskId">
209
+ <div class="human-drawer-footer">
210
+ <button class="btn-secondary" id="humanActionDismiss2" onclick="dismissHumanDrawer()">Omitir por ahora</button>
211
+ <button class="btn-primary" id="humanActionResolve">&#10003;&nbsp; Ya lo hice &mdash; continuar</button>
212
+ </div>
213
+ </div>
214
+ </div>
215
+
193
216
  <!-- Supabase Credentials Modal -->
194
217
  <div id="supabaseModal" class="modal" style="display:none">
195
218
  <div class="modal-backdrop" id="supabaseModalBackdrop"></div>
package/public/style.css CHANGED
@@ -1419,6 +1419,103 @@ html, body {
1419
1419
  color: var(--accent);
1420
1420
  }
1421
1421
 
1422
+ /* ── Human Action Required Drawer ───────────────────────── */
1423
+ .human-drawer {
1424
+ position: fixed;
1425
+ bottom: 0;
1426
+ left: 0;
1427
+ right: 0;
1428
+ z-index: 1100;
1429
+ transform: translateY(110%);
1430
+ transition: transform 0.3s cubic-bezier(0.34, 1.1, 0.64, 1);
1431
+ pointer-events: none;
1432
+ }
1433
+
1434
+ .human-drawer.visible {
1435
+ transform: translateY(0);
1436
+ pointer-events: all;
1437
+ }
1438
+
1439
+ .human-drawer-inner {
1440
+ max-width: 680px;
1441
+ margin: 0 auto 1.5rem;
1442
+ background: #1a1208;
1443
+ border: 1.5px solid rgba(245, 158, 11, 0.55);
1444
+ border-radius: var(--radius-lg);
1445
+ box-shadow: 0 -4px 40px rgba(245, 158, 11, 0.12), 0 8px 60px rgba(0,0,0,0.6);
1446
+ padding: 1.25rem 1.5rem 1rem;
1447
+ display: flex;
1448
+ flex-direction: column;
1449
+ gap: 0.85rem;
1450
+ }
1451
+
1452
+ .human-drawer-header {
1453
+ display: flex;
1454
+ align-items: flex-start;
1455
+ justify-content: space-between;
1456
+ gap: 1rem;
1457
+ }
1458
+
1459
+ .human-drawer-title-row {
1460
+ display: flex;
1461
+ align-items: flex-start;
1462
+ gap: 0.75rem;
1463
+ }
1464
+
1465
+ .human-drawer-icon {
1466
+ font-size: 1.5rem;
1467
+ color: #f59e0b;
1468
+ flex-shrink: 0;
1469
+ line-height: 1;
1470
+ margin-top: 2px;
1471
+ }
1472
+
1473
+ .human-drawer-label {
1474
+ font-size: 0.7rem;
1475
+ font-weight: 600;
1476
+ letter-spacing: 0.08em;
1477
+ text-transform: uppercase;
1478
+ color: #f59e0b;
1479
+ margin-bottom: 3px;
1480
+ }
1481
+
1482
+ .human-drawer-task {
1483
+ font-size: 0.9rem;
1484
+ font-weight: 600;
1485
+ color: var(--text);
1486
+ line-height: 1.3;
1487
+ }
1488
+
1489
+ .human-drawer-desc {
1490
+ font-size: 0.8rem;
1491
+ color: var(--text-muted);
1492
+ }
1493
+
1494
+ .human-action-list {
1495
+ display: flex;
1496
+ flex-direction: column;
1497
+ gap: 6px;
1498
+ max-height: 220px;
1499
+ overflow-y: auto;
1500
+ }
1501
+
1502
+ .human-action-item {
1503
+ font-size: 0.82rem;
1504
+ color: var(--text);
1505
+ background: rgba(245, 158, 11, 0.07);
1506
+ border-left: 3px solid rgba(245, 158, 11, 0.5);
1507
+ padding: 0.5rem 0.75rem;
1508
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
1509
+ line-height: 1.5;
1510
+ }
1511
+
1512
+ .human-drawer-footer {
1513
+ display: flex;
1514
+ gap: 0.75rem;
1515
+ justify-content: flex-end;
1516
+ padding-top: 0.25rem;
1517
+ }
1518
+
1422
1519
  /* ── Subir PRD button (label styled as btn-secondary) ───── */
1423
1520
  .btn-upload-prd {
1424
1521
  cursor: pointer;
@@ -78,6 +78,42 @@ function isSupabaseTask(task) {
78
78
  return /supabase/.test(text);
79
79
  }
80
80
 
81
+ // Patterns that indicate the agent flagged a required manual step in its HANDOFF
82
+ const HUMAN_REQUIRED_PATTERNS = [
83
+ /paso manual obligatorio/i,
84
+ /configurar.*dashboard/i,
85
+ /supabase dashboard/i,
86
+ /google cloud console/i,
87
+ /authentication.*providers/i,
88
+ /client id.*client secret/i,
89
+ /client secret/i,
90
+ /authorized redirect uri/i,
91
+ /redirect url.*permitid/i,
92
+ /habilitar.*provider/i,
93
+ /manual step/i,
94
+ /manually configure/i,
95
+ /variables de entorno.*producción/i,
96
+ /netlify.*env/i,
97
+ /stripe.*dashboard/i,
98
+ /twilio.*console/i,
99
+ /sendgrid.*dashboard/i,
100
+ /configurar.*en.*panel/i,
101
+ ];
102
+
103
+ function requiresHumanIntervention(handoffText) {
104
+ return HUMAN_REQUIRED_PATTERNS.some(p => p.test(handoffText));
105
+ }
106
+
107
+ // Extract the "Lo que falta:" section from a HANDOFF as the action items
108
+ function extractHumanActions(handoffText) {
109
+ const sectionMatch = handoffText.match(/lo que falta[:\s]*([\s\S]*?)(?:\n\nArchivos|\n\nDecisiones|$)/i);
110
+ if (sectionMatch && sectionMatch[1].trim()) return sectionMatch[1].trim().slice(0, 2000);
111
+ // Fallback: lines that match manual patterns
112
+ const lines = handoffText.split('\n').filter(l => HUMAN_REQUIRED_PATTERNS.some(p => p.test(l)));
113
+ if (lines.length) return lines.join('\n').slice(0, 2000);
114
+ return handoffText.slice(0, 500);
115
+ }
116
+
81
117
  // Strip null bytes and enforce length limit before any input reaches the CLI
82
118
  function sanitizeInput(str) {
83
119
  if (typeof str !== 'string') return '';
@@ -402,16 +438,42 @@ Lo que falta: [si algo quedó pendiente, si no nada]
402
438
  clearTimeout(timer);
403
439
  activeAgents.delete(task.id);
404
440
  const current = getTask(task.id);
405
- // Extract and save handoff note for future agents
441
+
406
442
  if (current) {
407
443
  const outputText = Array.isArray(current.output)
408
444
  ? current.output.map(e => e.chunk || '').join('')
409
445
  : (current.output || '');
446
+
447
+ // Extract and save handoff for future agents
410
448
  const handoffMatch = outputText.match(/<HANDOFF>([\s\S]*?)<\/HANDOFF>/);
411
- if (handoffMatch) writeHandoff(task.id, handoffMatch[1].trim());
412
- }
413
- if (current && current.status === 'in_progress') {
414
- runVerifier(current, broadcast, processQueue);
449
+ const handoffText = handoffMatch ? handoffMatch[1].trim() : '';
450
+ if (handoffText) writeHandoff(task.id, handoffText);
451
+
452
+ // ── Human intervention detection ──────────────────────────
453
+ // If the agent flagged manual steps (e.g. "configure in Supabase Dashboard"),
454
+ // block the task instead of verifying — the code is done but needs external config.
455
+ if (handoffText && requiresHumanIntervention(handoffText)) {
456
+ const humanActions = extractHumanActions(handoffText);
457
+ updateTask(task.id, {
458
+ status: 'error',
459
+ needsHuman: true,
460
+ humanReason: 'Requiere configuración manual externa',
461
+ humanActions,
462
+ });
463
+ if (broadcast) broadcast({
464
+ type: 'task:blocked',
465
+ taskId: task.id,
466
+ taskTitle: current.title,
467
+ humanActions,
468
+ });
469
+ notify('task:failed', { taskTitle: current.title, status: 'blocked' });
470
+ processQueue(); // continue with other tasks that don't need manual steps
471
+ return;
472
+ }
473
+
474
+ if (current.status === 'in_progress') {
475
+ runVerifier(current, broadcast, processQueue);
476
+ }
415
477
  }
416
478
  processQueue();
417
479
  });
@@ -755,10 +817,149 @@ function runQA() {
755
817
  spawnQAAgent(getTasks());
756
818
  }
757
819
 
758
- // Upload PRD from a markdown file sends it through the orchestrator to generate tasks
820
+ // Upload PRD: uses a dedicated planner agent (bypasses the 10k conversational limit)
759
821
  function uploadPRD(content) {
760
- const trimmed = content.slice(0, 20000);
761
- sendMessage(`Tengo mi PRD listo. Analizalo y creá las tareas necesarias para implementarlo:\n\n${trimmed}`);
822
+ spawnPRDPlannerAgent(content);
823
+ }
824
+
825
+ // Dedicated PRD planner agent — decomposes a full PRD into granular tasks
826
+ function spawnPRDPlannerAgent(rawContent) {
827
+ // Save the full PRD to disk first
828
+ try { savePRD(rawContent); } catch { /* non-fatal */ }
829
+ if (broadcast) broadcast({ type: 'prd:generated' });
830
+ if (broadcast) broadcast({ type: 'orchestrator:thinking' });
831
+
832
+ const projectPath = getProjectPath();
833
+ let plannerBuffer = '';
834
+ let tasksParsed = false;
835
+
836
+ // Collect project context to help Claude understand the tech stack
837
+ let projectCtx = '';
838
+ try {
839
+ const ctx = scanProject(projectPath);
840
+ if (ctx) {
841
+ const lines = [];
842
+ if (ctx.techStack.length) lines.push(`Stack detectado: ${ctx.techStack.join(', ')}`);
843
+ if (ctx.pkgDescription) lines.push(`Descripción: ${ctx.pkgDescription}`);
844
+ if (ctx.fileTree) lines.push(`Árbol de archivos:\n${ctx.fileTree.slice(0, 1500)}`);
845
+ if (ctx.existingPrd) lines.push(`PRD anterior:\n${ctx.existingPrd.slice(0, 500)}`);
846
+ projectCtx = lines.length ? `\n\nContexto del proyecto:\n${lines.join('\n')}` : '';
847
+ }
848
+ } catch { /* ignore */ }
849
+
850
+ // Cap PRD at 180 KB — well above any realistic PRD
851
+ const prdContent = rawContent.slice(0, 180 * 1024);
852
+
853
+ const prompt = `Sos el arquitecto de software de ClaudeBoard. Tu único trabajo es leer el PRD completo y descomponerlo en TODAS las tareas atómicas necesarias para construir el proyecto de cero a producción.${projectCtx}
854
+
855
+ ━━━ REGLAS DE GRANULARIDAD ━━━
856
+ • Cada tarea = 1 unidad de trabajo concreto que un agente puede completar solo (1 endpoint, 1 tabla, 1 componente, 1 página, 1 integración, etc.)
857
+ • Tamaño justo: ni mega-tareas ("hacer todo el backend") ni micro-tareas triviales de 2 líneas
858
+ • Para un proyecto mediano esperás 30–60 tareas. Para uno grande, 60–150 tareas. No te limites.
859
+ • Cada tarea debe poder ejecutarse de forma independiente con el contexto del handoff del agente anterior
860
+
861
+ ━━━ ORDEN DE CONSTRUCCIÓN (seguir esta secuencia) ━━━
862
+ 1. Setup del proyecto (estructura, configs, variables de entorno, dependencias)
863
+ 2. Base de datos (cada tabla/schema/migración es una tarea separada)
864
+ 3. Autenticación y permisos
865
+ 4. Backend — APIs y lógica de negocio (un endpoint o módulo por tarea)
866
+ 5. Integraciones externas (pagos, emails, storage, webhooks, etc.)
867
+ 6. Frontend — layout, componentes base y sistema de diseño
868
+ 7. Frontend — páginas y features (una por tarea)
869
+ 8. Lógica de tiempo real / subscripciones si aplica
870
+ 9. Testing y validación
871
+ 10. Deploy, CI/CD y variables de producción
872
+
873
+ ━━━ FORMATO EXACTO (sin texto antes ni después del bloque) ━━━
874
+ <TASKS>
875
+ <TASK>
876
+ title: [verbo + módulo concreto — ej: "Crear tabla profiles con RLS en Supabase"]
877
+ description: [qué crear o modificar exactamente, qué archivos tocar, qué debe funcionar al terminar — todo en una sola línea]
878
+ successCriteria: [cómo verificar que está hecho — en una sola línea]
879
+ priority: high|medium|low
880
+ </TASK>
881
+ </TASKS>
882
+
883
+ ━━━ PRD COMPLETO A PLANIFICAR ━━━
884
+ ${prdContent}`;
885
+
886
+ const proc = spawnClaude(prompt, projectPath);
887
+
888
+ proc.stdout.on('data', (data) => {
889
+ const chunk = data.toString('utf-8');
890
+ plannerBuffer += chunk;
891
+ // Stream progress to chat so the user sees something happening
892
+ if (broadcast) broadcast({ type: 'orchestrator:chunk', chunk });
893
+ });
894
+
895
+ proc.stderr.on('data', () => { /* swallow */ });
896
+
897
+ proc.on('close', () => {
898
+ if (!tasksParsed) {
899
+ tasksParsed = true;
900
+ const created = parsePlannerTasks(plannerBuffer);
901
+ if (broadcast) broadcast({ type: 'orchestrator:done' });
902
+ if (created.length > 0) {
903
+ if (broadcast) broadcast({
904
+ type: 'orchestrator:chunk',
905
+ chunk: `\n\n✅ **PRD procesado:** ${created.length} tareas creadas en el board. Los agentes arrancan ahora.`,
906
+ });
907
+ } else {
908
+ if (broadcast) broadcast({
909
+ type: 'orchestrator:chunk',
910
+ chunk: '\n\n⚠️ No se pudieron extraer tareas del PRD. Intentá pegarlo en el chat directamente.',
911
+ });
912
+ }
913
+ }
914
+ });
915
+
916
+ proc.on('error', (err) => {
917
+ console.error('[prd-planner] spawn error:', err.message);
918
+ if (broadcast) broadcast({ type: 'orchestrator:error', message: 'Error al procesar el PRD. ¿Está el CLI de Claude instalado?' });
919
+ });
920
+ }
921
+
922
+ // Parse tasks from planner output and enqueue them
923
+ function parsePlannerTasks(buffer) {
924
+ let taskDefs = [];
925
+
926
+ const taskBlocks = [...buffer.matchAll(/<TASK>([\s\S]*?)<\/TASK>/g)];
927
+ if (taskBlocks.length > 0) {
928
+ taskDefs = taskBlocks.map(m => {
929
+ const block = m[1];
930
+ const get = (key) => {
931
+ const match = block.match(new RegExp(`^${key}:\\s*(.+)$`, 'mi'));
932
+ return match ? match[1].trim() : '';
933
+ };
934
+ return {
935
+ title: get('title'),
936
+ description: get('description'),
937
+ successCriteria: get('successCriteria'),
938
+ priority: get('priority') || 'medium',
939
+ };
940
+ }).filter(t => t.title);
941
+ }
942
+
943
+ if (taskDefs.length === 0) return [];
944
+
945
+ const created = taskDefs.map(t => createTask(t));
946
+ if (broadcast) broadcast({ type: 'tasks:created', tasks: created });
947
+ notify('tasks:created', { taskTitle: null, status: 'created' });
948
+ qaAgentRan = false;
949
+
950
+ // Check if any task requires Supabase credentials
951
+ const needsSupabase = taskDefs.some(isSupabaseTask);
952
+ if (needsSupabase && !awaitingSupabaseCreds) {
953
+ awaitingSupabaseCreds = true;
954
+ if (broadcast) broadcast({
955
+ type: 'supabase:preflight',
956
+ taskTitles: created.map(t => t.title),
957
+ });
958
+ } else {
959
+ processQueue();
960
+ }
961
+
962
+ return created;
762
963
  }
763
964
 
764
965
  // Called when the user submits Supabase credentials from the modal
package/src/verifier.js CHANGED
@@ -50,11 +50,14 @@ ${agentOutput || '(no output)'}
50
50
 
51
51
  Instructions:
52
52
  1. Read the relevant files in the project directory to check the actual result.
53
- 2. Your ENTIRE response must be ONE of these two formats, nothing else:
53
+ 2. Your ENTIRE response must be ONE of these three formats, nothing else:
54
54
  VERIFIED
55
55
  FAILED: <one sentence reason>
56
+ NEEDS_HUMAN: <one sentence describing what manual external step is required>
56
57
 
57
- Do not add explanations, greetings, or any other text. Start your response with VERIFIED or FAILED.`;
58
+ Use NEEDS_HUMAN if and only if the task requires external manual configuration that cannot be verified from code — for example: configuring OAuth providers in a dashboard, setting environment variables in a deployment service, adding API credentials in a third-party console, or any step that requires clicking in a web UI outside the codebase.
59
+
60
+ Do not add explanations, greetings, or any other text. Start your response with VERIFIED, FAILED, or NEEDS_HUMAN.`;
58
61
 
59
62
  broadcast({ type: 'task:verifying', taskId: task.id });
60
63
  updateTask(task.id, { status: 'verifying' });
@@ -72,9 +75,9 @@ Do not add explanations, greetings, or any other text. Start your response with
72
75
 
73
76
  proc.on('close', () => {
74
77
  const trimmed = output.trim();
75
- // Flexible parsing: check anywhere in the first 300 chars
76
78
  const head = trimmed.slice(0, 300).toUpperCase();
77
- const isVerified = head.includes('VERIFIED') && !head.includes('FAILED');
79
+ const isVerified = head.includes('VERIFIED') && !head.includes('FAILED') && !head.includes('NEEDS_HUMAN');
80
+ const needsHuman = head.includes('NEEDS_HUMAN') || head.includes('NEEDS HUMAN');
78
81
 
79
82
  if (isVerified) {
80
83
  updateTask(task.id, { status: 'done', verifierOutput: trimmed.slice(0, 500) });
@@ -83,6 +86,23 @@ Do not add explanations, greetings, or any other text. Start your response with
83
86
  return;
84
87
  }
85
88
 
89
+ // Verifier detected this needs manual external configuration
90
+ if (needsHuman) {
91
+ const humanMatch = trimmed.match(/NEEDS[_\s]HUMAN[:\s]+(.+?)(?:\n|$)/i);
92
+ const humanReason = humanMatch ? humanMatch[1].trim().slice(0, 500) : 'Requiere configuración manual externa';
93
+ updateTask(task.id, {
94
+ status: 'error',
95
+ needsHuman: true,
96
+ humanReason,
97
+ humanActions: humanReason,
98
+ verifierOutput: trimmed.slice(0, 500),
99
+ });
100
+ broadcast({ type: 'task:blocked', taskId: task.id, taskTitle: task.title, humanActions: humanReason });
101
+ notify('task:failed', { taskTitle: task.title, status: 'blocked' });
102
+ if (onRetry) onRetry(); // continue queue with other tasks
103
+ return;
104
+ }
105
+
86
106
  // Extract failure reason
87
107
  const failedMatch = trimmed.match(/FAILED[:\s]+(.+?)(?:\n|$)/i);
88
108
  const reason = failedMatch