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 +4 -1
- package/public/app.js +60 -0
- package/public/index.html +23 -0
- package/public/style.css +97 -0
- package/src/orchestrator.js +209 -8
- package/src/verifier.js +24 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudeboard",
|
|
3
|
-
"version": "3.1.
|
|
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">⚠</span>
|
|
199
|
+
<div>
|
|
200
|
+
<div class="human-drawer-label">Acción requerida — 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">✕</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">✓ Ya lo hice — 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;
|
package/src/orchestrator.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
|
820
|
+
// Upload PRD: uses a dedicated planner agent (bypasses the 10k conversational limit)
|
|
759
821
|
function uploadPRD(content) {
|
|
760
|
-
|
|
761
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|