nothumanallowed 13.5.200 β†’ 14.0.1

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.
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Studio routes β€” /api/studio/plan, /api/studio/run, /api/studio/deliberate
3
+ * Ported directly from ui.mjs with identical logic, zero monolith dependency.
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { sendJSON, sendError, parseBody } from '../index.mjs';
9
+ import { loadConfig } from '../../config.mjs';
10
+ import { NHA_DIR, AGENTS_DIR } from '../../constants.mjs';
11
+ import { callLLM, callLLMStream, parseAgentFile } from '../../services/llm.mjs';
12
+
13
+ export function register(router) {
14
+ router.post('/api/studio/plan', async (req, res) => {
15
+ const body = await parseBody(req);
16
+ const task = (body.task || '').trim();
17
+ if (!task) return sendError(res, 400, 'task required');
18
+ const config = loadConfig();
19
+
20
+ const LANG_MAP = {en:'English',it:'Italian',es:'Spanish',fr:'French',de:'German',pt:'Portuguese',zh:'Chinese',ja:'Japanese',ar:'Arabic',hi:'Hindi',ru:'Russian',nl:'Dutch',pl:'Polish',tr:'Turkish',ko:'Korean'};
21
+ const plannerLang = LANG_MAP[(config?.language||'it').slice(0,2)] || 'Italian';
22
+ const it = plannerLang === 'Italian';
23
+ const taskLow = task.toLowerCase();
24
+
25
+ const hasPdf = !!(body.hasPdf) || /pdf|allegat|catalogo|scheda\s*tecnic/i.test(taskLow);
26
+ const hasEmail = /email|mail|inbox|posta/i.test(taskLow);
27
+ const hasCalendar = /calendar|agenda|calendari|eventi|schedule/i.test(taskLow);
28
+ const hasSearch = /cerca|search|notizie|news|ultime|latest|web|internet|tendenz|trend|acquista|compra|dove\s+trovare|where\s+to\s+buy|similar|simile/i.test(taskLow);
29
+ const hasCanvas = /html|dashboard|visua|report|grafico|chart/i.test(taskLow);
30
+ const hasGitHub = /github|git|issue|pr|pull request/i.test(taskLow);
31
+ const hasSlack = /slack|channel|messag/i.test(taskLow);
32
+ const hasNotion = /notion|note|page/i.test(taskLow);
33
+ const hasBriefing = /briefing|analisi|analizza|summary|sommario|riassunto|riepiloga|valutazione|valuta/i.test(taskLow);
34
+ const hasFinance = /finance|mercato|market|stock|trading|finanz|investiment|cripto/i.test(taskLow);
35
+ const hasSecurity = /security|sicurezza|vulnerabilit|audit|pentest|rischi|dipendenz/i.test(taskLow);
36
+ const hasStrategy = /strateg|competitiv|posizionament|raccomandaz|competitive|positioning/i.test(taskLow);
37
+ const hasReputation = /reputazion|reputation|online|brand|review|recension/i.test(taskLow);
38
+ const hasCode = /codice|code|refactor|debug|bug|sviluppo|software|npm|package/i.test(taskLow);
39
+ const hasWriting = /scrivi|write|articolo|article|blog|testo|text|documento|document/i.test(taskLow);
40
+ const hasData = /dati|data|dataset|csv|json|analizza i dati|pattern|statistich/i.test(taskLow);
41
+ const hasTranslate = /traduci|translate|traduzione|translation/i.test(taskLow);
42
+ const hasTravel = /ristorante|restaurant|b&b|hotel|albergo|agriturismo|locanda|osteria|prenotaz|vacanz|romantico|sushi|giapponese|cinese|pizza|cena|dinner|pranzo|lunch|soggiorno|weekend|pernottament|posto\s+dove\s+mangiare|posto\s+dove\s+dormire|dove\s+mangiare|dove\s+dormire|posto\s+romantico|gita|escursione/i.test(taskLow);
43
+
44
+ const extractSearchQuery = (t) => {
45
+ const m = t.match(/(?:cerca|search|find|ricerca|notizie su|news about|latest on|aggiornamenti su|ultime su|tendenz|trend)\s+(.{5,80}?)(?:\s+(?:e |and |per |for |poi |then )|[,\n]|$)/i);
46
+ if (m) return m[1].trim();
47
+ const domainMatch = t.match(/(?:https?:\/\/)?(?:www\.)?([a-z0-9-]+\.[a-z]{2,}(?:\.[a-z]{2,})?)/i);
48
+ if (domainMatch) return domainMatch[0].replace(/^https?:\/\//,'');
49
+ return t.replace(/^[^:]+:\s*/,'').split(/[,\n]/)[0].slice(0,100).trim() || t.slice(0,80).trim();
50
+ };
51
+ const searchQuery = extractSearchQuery(task);
52
+
53
+ const buildKeywordPlan = () => {
54
+ const steps = [];
55
+ if (hasPdf) {
56
+ const pdfName = body.pdfName || 'documento allegato';
57
+ steps.push({icon:'πŸ“„',agent:'DocumentReaderAgent',label:it?'Leggi documento':'Read document',reason:it?'Allegato PDF rilevato β€” estraggo dati tecnici prima di ogni altra operazione':'PDF attachment detected β€” extracting technical data first',prompt:`Extract all technical specifications, model numbers, part codes, product names, manufacturer, dimensions, ratings, and any other key data from the attached document "${pdfName}". List every technical detail precisely.`});
58
+ }
59
+ if (hasTravel) steps.push({icon:'🍽',agent:'TravelAgent', label:it?'Ricerca ristoranti & hotel':'Search restaurants & hotels', reason:it?'Task di viaggio/prenotazione':'Travel/booking task', prompt:task});
60
+ if (hasEmail) steps.push({icon:'πŸ“§',agent:'EmailAgent', label:it?'Controlla email':'Check emails', reason:it?'Parola chiave email rilevata':'Email keyword detected', prompt:'Read the latest unread emails and identify urgent items, deadlines, and required actions'});
61
+ if (hasCalendar) steps.push({icon:'πŸ“…',agent:'CalendarAgent',label:it?'Rivedi calendario':'Review calendar',reason:it?'Parola chiave calendario rilevata':'Calendar keyword detected', prompt:"Check today's events and identify any scheduling conflicts or important meetings"});
62
+ if (hasGitHub) steps.push({icon:'πŸ’»',agent:'GitHubAgent', label:'GitHub', reason:it?'Parola chiave GitHub rilevata':'GitHub keyword detected', prompt:'Read open issues and pull requests, identify what needs attention'});
63
+ if (hasSlack) steps.push({icon:'πŸ’¬',agent:'SlackAgent', label:'Slack', reason:it?'Parola chiave Slack rilevata':'Slack keyword detected', prompt:'Check recent Slack messages and identify important conversations'});
64
+ if (hasNotion) steps.push({icon:'πŸ“',agent:'NotionAgent', label:'Notion', reason:it?'Parola chiave Notion rilevata':'Notion keyword detected', prompt:'Search Notion for relevant pages and notes'});
65
+ if (!hasTravel && (hasPdf || hasSearch || hasReputation || (!hasEmail && !hasCalendar && !hasGitHub && !hasSlack))) {
66
+ const sp = hasPdf ? (it?'Usa le specifiche tecniche estratte dal documento per cercare online dove acquistare il prodotto o articoli equivalenti.':'Use the technical specifications extracted from the document to search online for where to buy this product or equivalent alternatives.') : searchQuery;
67
+ steps.push({icon:'πŸ”',agent:'WebSearchAgent',label:it?'Ricerca web':'Web search',reason:it?'Fonte dati web principale':'Primary web data source',prompt:sp});
68
+ }
69
+ if (hasSecurity) steps.push({icon:'πŸ›‘',agent:'cassandra', label:it?'CASSANDRA β€” Rischi':'CASSANDRA β€” Risks', reason:it?'Keyword sicurezza rilevata':'Security keyword detected', prompt:'Analyze the collected data and identify security risks, vulnerabilities and concrete recommendations'});
70
+ if (hasFinance) steps.push({icon:'πŸ’°',agent:'mercury', label:it?'MERCURY β€” Mercato':'MERCURY β€” Market', reason:it?'Keyword finanza rilevata':'Finance keyword detected', prompt:'Analyze the financial data and market trends from the collected information'});
71
+ if (hasStrategy) steps.push({icon:'β™Ÿ',agent:'athena', label:it?'ATHENA β€” Strategia':'ATHENA β€” Strategy', reason:it?'Keyword strategia rilevata':'Strategy keyword detected', prompt:'Based on the collected data, produce strategic analysis with competitive positioning and concrete recommendations'});
72
+ if (hasReputation) steps.push({icon:'πŸ”­',agent:'oracle', label:it?'ORACLE β€” Reputazione':'ORACLE β€” Reputation', reason:it?'Keyword reputazione rilevata':'Reputation keyword detected', prompt:'Analyze the online reputation data, sentiment and brand positioning from the collected information'});
73
+ if (hasCode) steps.push({icon:'πŸ”§',agent:'forge', label:it?'FORGE β€” Codice':'FORGE β€” Code', reason:it?'Keyword codice rilevata':'Code keyword detected', prompt:'Analyze the code, dependencies and technical issues identified in the data'});
74
+ if (hasWriting) steps.push({icon:'πŸ–Š',agent:'quill', label:it?'QUILL β€” Redazione':'QUILL β€” Writing', reason:it?'Keyword scrittura rilevata':'Writing keyword detected', prompt:'Write a polished, professional document based on all the collected information'});
75
+ if (hasData) steps.push({icon:'πŸ“Š',agent:'DataAnalystAgent',label:it?'Analisi dati':'Data analysis', reason:it?'Keyword dati rilevata':'Data keyword detected', prompt:'Analyze the data and extract key patterns, trends and insights'});
76
+ if (hasTranslate) steps.push({icon:'🌐',agent:'polyglot', label:it?'POLYGLOT β€” Traduzione':'POLYGLOT β€” Translation',reason:it?'Keyword traduzione rilevata':'Translation keyword detected', prompt:'Translate the content as requested, maintaining meaning and style'});
77
+ const hasSpecialist = hasSecurity || hasFinance || hasStrategy || hasReputation || hasCode || hasWriting || hasData || hasTranslate;
78
+ if (!hasSpecialist && (hasBriefing || steps.length > 0)) {
79
+ steps.push({icon:'πŸ“°',agent:'HERALD',label:it?'HERALD β€” Briefing':'HERALD β€” Briefing',reason:it?'HERALD sintetizza tutti i dati':'HERALD synthesizes all data',prompt:'Based on ALL the data collected by the previous steps, write a complete executive briefing with priorities, findings, and strategic recommendations. Do NOT invent data β€” only use what was provided.'});
80
+ }
81
+ const specialistCount = [hasSecurity,hasFinance,hasStrategy,hasReputation,hasCode,hasWriting,hasData].filter(Boolean).length;
82
+ if (hasCanvas || specialistCount >= 2 || (hasSpecialist && hasBriefing)) {
83
+ steps.push({icon:'πŸ“Š',agent:'CanvasAgent',label:it?'Dashboard HTML':'HTML Dashboard',reason:it?'Analisi complessa β€” dashboard visuale':'Complex analysis β€” visual dashboard',prompt:'Create a professional HTML dashboard report summarizing all findings from the previous agents'});
84
+ }
85
+ return steps;
86
+ };
87
+
88
+ const keywordSteps = buildKeywordPlan();
89
+ const hasKeywordPlan = keywordSteps.length > 0;
90
+ const sanitizedTask = task.replace(/<[^>]*>/g, ' ').replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '').trim();
91
+ const keywordPlanJson = hasKeywordPlan ? JSON.stringify(keywordSteps.map(s => ({ agent: s.agent, label: s.label, reason: s.reason || '' }))) : '[]';
92
+ const planConfig = Object.assign({}, config, { thinking: 'off' });
93
+
94
+ try {
95
+ let steps = keywordSteps;
96
+ if (config && (config.llm?.provider || config.llm?.apiKey)) {
97
+ try {
98
+ const planSys = `You are a workflow planner for NHA Studio. Output ONLY valid JSON β€” no explanation, no markdown.`;
99
+ const planPrompt = hasKeywordPlan
100
+ ? `Task: ${sanitizedTask}\n\nKeyword-detected plan (JSON):\n${keywordPlanJson}\n\nLanguage for labels: ${plannerLang}.\n\nReview the plan. You may ADD missing steps, REMOVE wrong ones, REORDER, ADJUST prompts. Keep existing reasons where step is unchanged.\n\nAvailable agents: TravelAgent, WebSearchAgent, DocumentReaderAgent, EmailAgent, CalendarAgent, GitHubAgent, SlackAgent, NotionAgent, HERALD, ORACLE, ATHENA, CASSANDRA, MERCURY, QUILL, DataAnalystAgent, polyglot, CanvasAgent (last only).\n\nOutput ONLY:\n{"steps":[{"icon":"EMOJI","agent":"AGENT_NAME","label":"LABEL","reason":"WHY","prompt":"INSTRUCTION"}]}\n\nMax 6 steps.`
101
+ : `Task: ${sanitizedTask}\n\nLanguage for labels: ${plannerLang}.\n\nBuild a workflow plan. Available agents: TravelAgent, WebSearchAgent, DocumentReaderAgent, EmailAgent, CalendarAgent, GitHubAgent, SlackAgent, NotionAgent, HERALD, ORACLE, ATHENA, CASSANDRA, MERCURY, QUILL, DataAnalystAgent, polyglot, CanvasAgent.\n\nOutput ONLY:\n{"steps":[{"icon":"EMOJI","agent":"AGENT_NAME","label":"LABEL","reason":"WHY","prompt":"INSTRUCTION"}]}\n\n2-5 steps.`;
102
+ const planRaw = await callLLM(planConfig, planSys, planPrompt, { max_tokens: 900 });
103
+ let clean = planRaw.replace(/<think>[\s\S]*?<\/think>/g, '').trim().replace(/^```[\w]*\r?\n?/, '').replace(/\r?\n?```$/, '').trim();
104
+ const jm = clean.match(/\{[\s\S]*\}/);
105
+ const parsed = JSON.parse(jm ? jm[0] : clean);
106
+ if (Array.isArray(parsed.steps) && parsed.steps.length > 0) {
107
+ const krMap = {};
108
+ keywordSteps.forEach(s => { krMap[s.agent] = s.reason || ''; });
109
+ steps = parsed.steps.map(s => ({ icon: s.icon || 'πŸ€–', agent: s.agent, label: s.label, reason: s.reason || krMap[s.agent] || '', prompt: s.prompt }));
110
+ }
111
+ } catch {}
112
+ }
113
+ if (!Array.isArray(steps) || !steps.length) {
114
+ steps = [{ icon: 'πŸ”', agent: 'WebSearchAgent', label: it ? 'Ricerca web' : 'Web search', reason: 'Fallback', prompt: sanitizedTask }];
115
+ }
116
+ sendJSON(res, 200, { steps });
117
+ } catch (e) { sendError(res, 500, e.message); }
118
+ });
119
+
120
+ // ── /api/studio/run β€” SSE streaming agent execution ──────────────────
121
+ router.post('/api/studio/run', async (req, res) => {
122
+ const body = await parseBody(req, 4_194_304);
123
+ const config = loadConfig();
124
+ const { agent, task, context, stepDef } = body;
125
+ if (!agent || !task) return sendError(res, 400, 'agent and task required');
126
+
127
+ res.writeHead(200, {
128
+ 'Content-Type': 'text/event-stream',
129
+ 'Cache-Control': 'no-cache',
130
+ 'Connection': 'keep-alive',
131
+ 'Access-Control-Allow-Origin': '*',
132
+ });
133
+ const sse = (data) => { try { res.write(`data: ${JSON.stringify(data)}\n\n`); } catch {} };
134
+ const keepalive = setInterval(() => { try { res.write(': keepalive\n\n'); } catch {} }, 5000);
135
+
136
+ try {
137
+ const LANG_MAP = {en:'English',it:'Italian',es:'Spanish',fr:'French',de:'German',pt:'Portuguese',zh:'Chinese',ja:'Japanese',ar:'Arabic',hi:'Hindi',ru:'Russian',nl:'Dutch',pl:'Polish',tr:'Turkish',ko:'Korean'};
138
+ const language = LANG_MAP[(config?.language||'it').slice(0,2)] || 'Italian';
139
+ const today = new Date().toISOString().slice(0,10);
140
+
141
+ // Load agent definition if available
142
+ let agentSysDef = null;
143
+ const agentFile = path.join(AGENTS_DIR, `${agent.toLowerCase()}.mjs`);
144
+ if (fs.existsSync(agentFile)) {
145
+ try {
146
+ const src = fs.readFileSync(agentFile, 'utf-8');
147
+ const parsed = parseAgentFile(src, agent);
148
+ if (parsed.systemPrompt) agentSysDef = parsed.systemPrompt;
149
+ } catch {}
150
+ }
151
+
152
+ const contextBlock = context ? `\n\n## CONTEXT FROM PREVIOUS STEPS:\n${context.slice(0, 8000)}` : '';
153
+ const proposalContextBlock = body.proposalContext ? `\n\n## OTHER AGENTS' PROPOSALS (CROSS-READING):\n${body.proposalContext.slice(0, 6000)}` : '';
154
+
155
+ const sysParts = [
156
+ agentSysDef || `You are ${agent}, a specialist AI agent in NHA Studio. Respond entirely in ${language}. Today is ${today}.`,
157
+ `\n\n## WORKFLOW GOAL: ${task}`,
158
+ contextBlock,
159
+ proposalContextBlock,
160
+ stepDef?.prompt ? `\n\n## YOUR SPECIFIC TASK:\n${stepDef.prompt}` : '',
161
+ ];
162
+ const systemPrompt = sysParts.join('');
163
+ const userMessage = stepDef?.prompt || task;
164
+
165
+ let output = '';
166
+ let tokensOut = 0;
167
+ await callLLMStream(config, systemPrompt, userMessage, (tok) => {
168
+ output += tok;
169
+ tokensOut += Math.ceil(tok.length / 4);
170
+ sse({ token: tok });
171
+ }, { max_tokens: 8192 });
172
+
173
+ clearInterval(keepalive);
174
+ sse({ done: true, output, tokensOut });
175
+ res.write('data: [DONE]\n\n');
176
+ res.end();
177
+ } catch (e) {
178
+ clearInterval(keepalive);
179
+ sse({ error: e.message });
180
+ res.end();
181
+ }
182
+ });
183
+
184
+ // ── /api/studio/deliberate β€” Parliament Geth Consensus ───────────────
185
+ router.post('/api/studio/deliberate', async (req, res) => {
186
+ const body = await parseBody(req);
187
+ const { task, proposals, language: bodyLang } = body;
188
+ if (!task || !Array.isArray(proposals) || proposals.length < 2) {
189
+ return sendError(res, 400, 'task and at least 2 proposals required');
190
+ }
191
+
192
+ res.writeHead(200, {
193
+ 'Content-Type': 'text/event-stream',
194
+ 'Cache-Control': 'no-cache',
195
+ 'Connection': 'keep-alive',
196
+ 'Access-Control-Allow-Origin': '*',
197
+ });
198
+ const sse = (data) => { try { res.write(`data: ${JSON.stringify(data)}\n\n`); } catch {} };
199
+ const tok = (t) => sse({ token: t });
200
+ const keepalive = setInterval(() => { try { res.write(': keepalive\n\n'); } catch {} }, 5000);
201
+ const config = loadConfig();
202
+ const language = bodyLang || 'Italian';
203
+ const today = new Date().toISOString().slice(0,10);
204
+
205
+ const jaccard = (a, b) => {
206
+ const terms = (s) => new Set(s.toLowerCase().match(/\b\w{4,}\b/g) || []);
207
+ const sa = terms(a), sb = terms(b);
208
+ let inter = 0;
209
+ for (const w of sa) if (sb.has(w)) inter++;
210
+ const union = sa.size + sb.size - inter;
211
+ return union > 0 ? inter / union : 1;
212
+ };
213
+ const measureConvergence = (outputs) => {
214
+ if (outputs.length < 2) return 1.0;
215
+ let total = 0, pairs = 0;
216
+ for (let i = 0; i < outputs.length; i++)
217
+ for (let j = i+1; j < outputs.length; j++) { total += jaccard(outputs[i], outputs[j]); pairs++; }
218
+ return pairs > 0 ? total / pairs : 1.0;
219
+ };
220
+
221
+ try {
222
+ const eligible = proposals.filter(p => !['CanvasAgent','GitHubAgent','EmailAgent','CalendarAgent'].includes(p.agent));
223
+ if (eligible.length < 2) {
224
+ sse({ deliberation_done: true, skipped: true, reason: 'not enough specialist agents' });
225
+ sse({ done: true }); res.write('data: [DONE]\n\n'); res.end(); clearInterval(keepalive); return;
226
+ }
227
+
228
+ const r1Conv = measureConvergence(eligible.map(p => p.output));
229
+ tok(`[Parlamento β€” Round 1 convergenza: ${(r1Conv*100).toFixed(0)}%] `);
230
+
231
+ const crossCtx = (excludeAgent) => eligible
232
+ .filter(p => p.agent !== excludeAgent)
233
+ .map(p => `## ${p.label || p.agent} (Round 1):\n${p.output.slice(0,4000)}`)
234
+ .join('\n\n---\n\n');
235
+
236
+ tok('[Parlamento β€” Round 2: Cross-Reading & Refinamento] ');
237
+ const r2Results = [];
238
+ for (const proposal of eligible) {
239
+ tok(`[Round 2: ${proposal.label || proposal.agent}] `);
240
+ const r2Sys = `You are ${proposal.agent}, a specialist AI agent in NHA Studio Parliament. Today is ${today}. Respond entirely in ${language}.\n\n## WORKFLOW GOAL: ${task}\n\n## YOUR ROUND 1 RESPONSE:\n${proposal.output.slice(0,1500)}\n\n## OTHER AGENTS' ROUND 1 PROPOSALS:\n${crossCtx(proposal.agent)}\n\nDELIBERATION ROUND 2 β€” REFINEMENT:\n1. Review the other agents' proposals\n2. Incorporate valid points where you AGREE β€” mark with [ASSIST]\n3. Flag genuine disagreements with [CONTRADICTION] and explain your reasoning\n4. Produce your COMPLETE REFINED response\n5. Keep analysis focused on: ${task}`;
241
+ let r2Out = '';
242
+ try {
243
+ await callLLMStream(config, r2Sys, 'Produce your refined Round 2 response. Write complete content under every heading β€” never leave a section title without body text.',
244
+ (t) => { r2Out += t; }, { max_tokens: 8192 });
245
+ } catch { r2Out = proposal.output; }
246
+ r2Results.push({ agent: proposal.agent, label: proposal.label, icon: proposal.icon, output: r2Out });
247
+ sse({ deliberation_r2: { agent: proposal.agent, label: proposal.label, icon: proposal.icon, output: r2Out } });
248
+ }
249
+
250
+ const r2Conv = measureConvergence(r2Results.map(r => r.output));
251
+ tok(`[Parlamento β€” Round 2 convergenza: ${(r2Conv*100).toFixed(0)}%] `);
252
+ const converged = r2Conv >= 0.30;
253
+
254
+ const allR2Ctx = r2Results.map(r => `## ${r.label || r.agent}:\n${r.output.slice(0,2000)}`).join('\n\n---\n\n');
255
+ const contradictions = [];
256
+ for (const r of r2Results) {
257
+ const matches = r.output.match(/\[CONTRADICTION\][^\n]*/g) || [];
258
+ matches.forEach(m => contradictions.push(`- ${r.label || r.agent}: ${m.replace('[CONTRADICTION]','').trim()}`));
259
+ }
260
+ const contBlock = contradictions.length > 0 ? `\n\n## DIVERGENZE RILEVATE DAL ROUND 2:\n${contradictions.join('\n')}` : '';
261
+
262
+ tok(converged ? '[Parlamento β€” Round 3: Sintesi finale HERALD...] ' : '[Parlamento β€” Round 3: Mediazione HERALD...] ');
263
+
264
+ const medTask = converged
265
+ ? `SYNTHESIS TASK (convergenza ${(r2Conv*100).toFixed(0)}%):\n1. Presenta il CONSENSO raggiunto\n2. Segnala ogni sfumatura o punto di divergenza residua\n3. Produci un executive summary unificato con azioni concrete per: ${task}\n4. Sezione "Voci dissonanti" se esistono posizioni che meritano attenzione`
266
+ : `MEDIATION TASK (convergenza ${(r2Conv*100).toFixed(0)}% β€” divergenza significativa):\n1. Identifica i punti di ACCORDO tra tutti gli agenti\n2. Per ogni disaccordo: valuta quale posizione ha evidenze piΓΉ solide, NOMINA l'agente e spiega perchΓ© Γ¨ stata accolta o scartata\n3. Produci una sintesi UNIFICATA\n4. Fai scelte editoriali nette\n5. Executive summary con azioni concrete per: ${task}`;
267
+
268
+ const medSys = `You are HERALD, the Parliament Mediator in NHA Studio. Today is ${today}. Respond entirely in ${language}.\n\n## WORKFLOW GOAL: ${task}\n\n## ALL AGENTS' REFINED POSITIONS (Round 2):\n${allR2Ctx}${contBlock}\n\n${medTask}\n\nCRITICAL: NEVER write a heading without immediately writing full content below it. Every section MUST have at least 3-5 concrete bullet points or sentences.`;
269
+
270
+ let mediationOutput = '';
271
+ try {
272
+ await callLLMStream(config, medSys, 'Produce the Parliament final synthesis.', (t) => { mediationOutput += t; }, { max_tokens: 8192 });
273
+ } catch {}
274
+ sse({ deliberation_r3: { output: mediationOutput, converged } });
275
+
276
+ clearInterval(keepalive);
277
+ sse({ deliberation_done: true, r1_convergence: r1Conv, r2_convergence: r2Conv, converged, r2_results: r2Results, mediation: mediationOutput || null });
278
+ sse({ done: true });
279
+ res.write('data: [DONE]\n\n');
280
+ res.end();
281
+ } catch (e) {
282
+ clearInterval(keepalive);
283
+ sse({ error: e.message });
284
+ res.end();
285
+ }
286
+ });
287
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Tasks routes β€” local task store + Google Tasks
3
+ * All task operations: CRUD, priority, move, bulk, week view, Google Tasks sync
4
+ */
5
+
6
+ import { sendJSON, sendError, parseBody } from '../index.mjs';
7
+ import { loadConfig } from '../../config.mjs';
8
+ import {
9
+ getTasks, addTask, completeTask, editTask, moveTask, deleteTask,
10
+ clearTasks, editTaskPriority, getWeekTasks, bulkAddTasks, getDayStats,
11
+ } from '../../services/task-store.mjs';
12
+
13
+ export function register(router) {
14
+ // GET /api/tasks?date=YYYY-MM-DD
15
+ router.get('/api/tasks', (req, res) => {
16
+ try {
17
+ const url = new URL(req.url, 'http://localhost');
18
+ const date = url.searchParams.get('date');
19
+ const week = url.searchParams.get('week') === '1';
20
+ if (week) return sendJSON(res, 200, { tasks: getWeekTasks() });
21
+ const tasks = getTasks(date);
22
+ sendJSON(res, 200, { tasks, stats: getDayStats(date) });
23
+ } catch (e) { sendError(res, 500, e.message); }
24
+ });
25
+
26
+ // POST /api/tasks β€” add / complete / edit / move / delete / clear / bulk / priority
27
+ router.post('/api/tasks', async (req, res) => {
28
+ try {
29
+ const body = await parseBody(req);
30
+ const { action } = body;
31
+
32
+ if (action === 'complete') {
33
+ completeTask(body.id, body.date);
34
+ return sendJSON(res, 200, { ok: true });
35
+ }
36
+ if (action === 'edit') {
37
+ editTask(body.id, body.description, body.date);
38
+ return sendJSON(res, 200, { ok: true });
39
+ }
40
+ if (action === 'priority') {
41
+ editTaskPriority(body.id, body.priority, body.date);
42
+ return sendJSON(res, 200, { ok: true });
43
+ }
44
+ if (action === 'move') {
45
+ moveTask(body.id, body.fromDate, body.toDate);
46
+ return sendJSON(res, 200, { ok: true });
47
+ }
48
+ if (action === 'delete') {
49
+ deleteTask(body.id, body.date);
50
+ return sendJSON(res, 200, { ok: true });
51
+ }
52
+ if (action === 'clear') {
53
+ clearTasks(body.mode || 'all', body.date);
54
+ return sendJSON(res, 200, { ok: true });
55
+ }
56
+ if (action === 'bulk') {
57
+ const tasks = bulkAddTasks(body.tasks, body.date);
58
+ return sendJSON(res, 201, { tasks });
59
+ }
60
+
61
+ // Default: add task
62
+ const task = addTask(body, body.date);
63
+ sendJSON(res, 201, { task });
64
+ } catch (e) { sendError(res, 500, e.message); }
65
+ });
66
+
67
+ // ── Google Tasks ─────────────────────────────────────────────────────
68
+
69
+ router.get('/api/gtasks/lists', async (_req, res) => {
70
+ try {
71
+ const { listTaskLists } = await import('../../services/google-tasks.mjs');
72
+ const config = loadConfig();
73
+ sendJSON(res, 200, { lists: await listTaskLists(config) });
74
+ } catch (e) {
75
+ if (e.message?.includes('token') || e.message?.includes('auth')) {
76
+ return sendJSON(res, 200, { lists: [], authRequired: true });
77
+ }
78
+ sendError(res, 500, e.message);
79
+ }
80
+ });
81
+
82
+ router.get('/api/gtasks', async (req, res) => {
83
+ try {
84
+ const { listTasks } = await import('../../services/google-tasks.mjs');
85
+ const config = loadConfig();
86
+ const url = new URL(req.url, 'http://localhost');
87
+ const listId = url.searchParams.get('listId') || '@default';
88
+ sendJSON(res, 200, { tasks: await listTasks(config, listId) });
89
+ } catch (e) {
90
+ if (e.message?.includes('token') || e.message?.includes('auth')) {
91
+ return sendJSON(res, 200, { tasks: [], authRequired: true });
92
+ }
93
+ sendError(res, 500, e.message);
94
+ }
95
+ });
96
+
97
+ router.post('/api/gtasks', async (req, res) => {
98
+ try {
99
+ const body = await parseBody(req);
100
+ const config = loadConfig();
101
+ if (body.action === 'complete') {
102
+ const { completeTask: gComplete } = await import('../../services/google-tasks.mjs');
103
+ await gComplete(config, body.listId || '@default', body.taskId);
104
+ return sendJSON(res, 200, { ok: true });
105
+ }
106
+ if (body.action === 'delete') {
107
+ const { deleteTask: gDelete } = await import('../../services/google-tasks.mjs');
108
+ await gDelete(config, body.listId || '@default', body.taskId);
109
+ return sendJSON(res, 200, { ok: true });
110
+ }
111
+ const { createTask } = await import('../../services/google-tasks.mjs');
112
+ const task = await createTask(config, body.listId || '@default', body.title, body.notes, body.due);
113
+ sendJSON(res, 201, { task });
114
+ } catch (e) { sendError(res, 500, e.message); }
115
+ });
116
+ }