@openlife/cli 1.7.13 → 1.8.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.
Files changed (35) hide show
  1. package/INSTALL.md +29 -1
  2. package/dist/cli/ChatTui.js +32 -0
  3. package/dist/cli/InstallModules.js +17 -2
  4. package/dist/cli/InstallWizardV2.js +110 -0
  5. package/dist/cli/install/Multiselect.js +285 -0
  6. package/dist/cli/install/OAuthRunner.js +170 -0
  7. package/dist/cli/install/Phases.js +864 -0
  8. package/dist/cli/install/ProvidersCatalog.js +320 -0
  9. package/dist/cli/install/WizardIO.js +271 -0
  10. package/dist/cli/install/types.js +17 -0
  11. package/dist/index.js +170 -35
  12. package/dist/orchestrator/ConsequenceForecaster.js +24 -1
  13. package/dist/orchestrator/DreamGoalStore.js +130 -0
  14. package/dist/orchestrator/Gatekeeper.js +14 -0
  15. package/dist/orchestrator/Gateway.js +194 -15
  16. package/dist/orchestrator/ModelManager.js +7 -1
  17. package/dist/orchestrator/OrchestrationLoop.js +12 -0
  18. package/dist/orchestrator/ParallelOrchestrationLoop.js +12 -2
  19. package/dist/orchestrator/RuntimePolicy.js +4 -1
  20. package/dist/orchestrator/ServiceCompletionPolicy.js +15 -0
  21. package/dist/orchestrator/SynthesizerAgent.js +20 -1
  22. package/dist/orchestrator/TaskExecutor.js +53 -0
  23. package/dist/orchestrator/capability/CapabilityGenesisEngine.js +66 -11
  24. package/dist/test_capability_genesis_engine.js +1 -1
  25. package/dist/test_chat_smoke_command.js +59 -0
  26. package/dist/test_dream_goal_commands.js +76 -0
  27. package/dist/test_gateway_telegram_formatting.js +74 -0
  28. package/dist/test_install_wizard_v2.js +193 -0
  29. package/dist/test_on_demand_voice_reply.js +65 -0
  30. package/dist/test_remaining_sprints_contracts.js +103 -0
  31. package/dist/test_runtime_policy.js +25 -6
  32. package/dist/test_service_completion_policy.js +7 -0
  33. package/dist/test_subsystems_routing_governance.js +13 -3
  34. package/dist/test_task_executor_gemini_api.js +68 -0
  35. package/package.json +5 -3
@@ -41,6 +41,7 @@ const telegraf_1 = require("telegraf");
41
41
  const filters_1 = require("telegraf/filters");
42
42
  const IntentClassifier_1 = require("./IntentClassifier");
43
43
  const Gatekeeper_1 = require("./Gatekeeper");
44
+ const DreamGoalStore_1 = require("./DreamGoalStore");
44
45
  const VoiceManager_1 = require("./VoiceManager");
45
46
  const axios_1 = __importDefault(require("axios"));
46
47
  const fs = __importStar(require("fs"));
@@ -91,6 +92,21 @@ class Gateway {
91
92
  static redactBotToken(text) {
92
93
  return String(text).replace(/(\d{6,12}):[A-Za-z0-9_-]{20,}/g, '$1:[REDACTED]');
93
94
  }
95
+ static classifyTelegramLaunchError(err) {
96
+ const raw = err instanceof Error ? err.message : String(err || '');
97
+ const detail = Gateway.redactBotToken(raw).slice(0, 500);
98
+ if (raw.includes('409') || raw.includes('terminated by other getUpdates request') || raw.toLowerCase().includes('conflict')) {
99
+ return {
100
+ ok: false,
101
+ error: 'telegram_polling_conflict',
102
+ recoveryOptions: ['stop_conflicting_instance', 'replace_token', 'skip_telegram_for_now'],
103
+ detail,
104
+ };
105
+ }
106
+ if (!raw)
107
+ return { ok: true };
108
+ return { ok: false, error: 'telegram_launch_failed', recoveryOptions: ['check_token', 'switch_to_webhook', 'skip_telegram_for_now'], detail };
109
+ }
94
110
  /**
95
111
  * Validate the configured TELEGRAM_BOT_TOKEN by calling getMe.
96
112
  * Returns `{ ok: true, botId, botUsername }` on success;
@@ -344,16 +360,60 @@ class Gateway {
344
360
  await ctx.reply("Erro no módulo de visão ao baixar a imagem.");
345
361
  }
346
362
  });
347
- this.bot.launch().catch((err) => {
348
- const msg = err instanceof Error ? err.message : String(err);
349
- if (msg.includes('409') || msg.includes('terminated by other getUpdates request')) {
350
- console.error('[GATEWAY] Telegram 409 Conflict: outra instância está conectada com este bot. Mantendo processo vivo para healthcheck; Telegram polling fica desativado nesta instância.');
351
- return;
363
+ // Decide Telegram delivery mode:
364
+ // - OPENLIFE_TELEGRAM_MODE=polling | webhook | auto (default: auto)
365
+ // - "auto" → webhook if a Railway env var is present, else polling.
366
+ // This is the simplest UX for the "Railway + local" topology: Railway
367
+ // deploys boot in webhook mode (no polling conflict with local), local
368
+ // dev boots in polling mode (no webhook URL needed). User changes
369
+ // nothing.
370
+ const mode = String(process.env.OPENLIFE_TELEGRAM_MODE || 'auto').toLowerCase();
371
+ const onRailway = !!(process.env.RAILWAY_ENVIRONMENT || process.env.RAILWAY_PUBLIC_DOMAIN || process.env.RAILWAY_STATIC_URL);
372
+ const useWebhook = mode === 'webhook' || (mode === 'auto' && onRailway);
373
+ if (useWebhook) {
374
+ const webhookPath = process.env.OPENLIFE_WEBHOOK_PATH || '/api/v1/telegram/webhook';
375
+ // Register webhook handler on the existing Express app — no separate
376
+ // HTTP server, no port conflict. Telegraf's webhookCallback returns
377
+ // an Express-compatible middleware that parses updates.
378
+ // Important: do not mount the callback at `webhookPath` while also
379
+ // passing `webhookPath` into Telegraf. Express strips the mount
380
+ // prefix before the middleware sees `req.url`, so Telegraf would
381
+ // see `/` and skip the update, causing Telegram to receive 404.
382
+ this.app.use(this.bot.webhookCallback(webhookPath));
383
+ const rawDomain = process.env.OPENLIFE_WEBHOOK_DOMAIN
384
+ || process.env.RAILWAY_PUBLIC_DOMAIN
385
+ || process.env.RAILWAY_STATIC_URL
386
+ || '';
387
+ if (rawDomain) {
388
+ const domain = rawDomain.startsWith('http') ? rawDomain : `https://${rawDomain}`;
389
+ const fullUrl = `${domain.replace(/\/$/, '')}${webhookPath}`;
390
+ this.bot.telegram.setWebhook(fullUrl).then(() => {
391
+ console.log(`[GATEWAY] Telegram em modo WEBHOOK @ ${fullUrl}`);
392
+ }).catch((err) => {
393
+ console.error('[GATEWAY] setWebhook falhou:', err instanceof Error ? err.message : String(err));
394
+ });
352
395
  }
353
- console.error('[GATEWAY] Falha ao iniciar Telegram bot:', err);
354
- process.exit(1);
355
- });
356
- console.log("[GATEWAY] Telegram Bot escutando com suporte Multimodal (Voz/Visão).");
396
+ else {
397
+ console.warn('[GATEWAY] Webhook mode ativado mas OPENLIFE_WEBHOOK_DOMAIN (ou RAILWAY_PUBLIC_DOMAIN) não está definido. Telegram não saberá onde entregar mensagens. Defina o domínio público antes de subir.');
398
+ }
399
+ console.log("[GATEWAY] Telegram Bot pronto (modo webhook sem polling).");
400
+ }
401
+ else {
402
+ // Default polling mode (local dev, single-instance bots).
403
+ // Clear any previously-registered webhook before polling, otherwise
404
+ // Telegram rejects getUpdates with 409. Best-effort, non-fatal.
405
+ this.bot.telegram.deleteWebhook({ drop_pending_updates: false }).catch(() => { });
406
+ this.bot.launch().catch((err) => {
407
+ const classified = Gateway.classifyTelegramLaunchError(err);
408
+ if ('error' in classified && classified.error === 'telegram_polling_conflict') {
409
+ console.error(`[GATEWAY] Telegram 409 Conflict: outra instância está conectada com este bot. Recovery: ${classified.recoveryOptions.join(' | ')}. Detail: ${classified.detail}`);
410
+ return;
411
+ }
412
+ console.error('[GATEWAY] Falha ao iniciar Telegram bot:', err);
413
+ process.exit(1);
414
+ });
415
+ console.log("[GATEWAY] Telegram Bot escutando (modo polling) com suporte Multimodal (Voz/Visão).");
416
+ }
357
417
  }
358
418
  async shutdown(reason = 'manual') {
359
419
  if (this.isShuttingDown)
@@ -543,12 +603,131 @@ class Gateway {
543
603
  return true;
544
604
  return userId === this.allowedTelegramUserId;
545
605
  }
606
+ shouldRespondByVoice(text) {
607
+ const normalized = text.toLowerCase().trim();
608
+ if (/^\/voice\b|^\/voz\b/.test(normalized))
609
+ return true;
610
+ return /\b(responda|responder|manda|mande|envia|envie|fale|fala)\b[\s\S]{0,80}\b(por|em)\s+(voz|áudio|audio)\b/i.test(normalized)
611
+ || /\bquero\s+(ouvir|áudio|audio|voz)\b/i.test(normalized);
612
+ }
613
+ stripVoiceDirective(text) {
614
+ let cleaned = text.replace(/^\/(voice|voz)\s*/i, '').trim();
615
+ cleaned = cleaned
616
+ .replace(/\b(responda|responder|manda|mande|envia|envie)\s+(por|em)\s+(voz|áudio|audio)\b[:,\s-]*/ig, '')
617
+ .replace(/\b(por favor,?\s*)?(responda|responder)\s+(isso\s+)?(por|em)\s+(voz|áudio|audio)\b/ig, '')
618
+ .trim();
619
+ return cleaned || text;
620
+ }
621
+ voiceUnavailableMessage(err) {
622
+ const detail = err instanceof Error ? err.message : String(err);
623
+ const safeDetail = this.governance.redact(detail).slice(0, 500);
624
+ return [
625
+ 'Não consegui gerar a voz neste momento, então deixei em texto.',
626
+ `Motivo técnico: ${safeDetail}`,
627
+ 'Para ativar voz sob demanda, configure um provider TTS no Railway: OPENAI_API_KEY ou ELEVENLABS_API_KEY.'
628
+ ].join('\n');
629
+ }
630
+ isMarkdownTableSeparator(line) {
631
+ return /^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line);
632
+ }
633
+ splitMarkdownTableRow(line) {
634
+ let stripped = line.trim();
635
+ if (stripped.startsWith('|'))
636
+ stripped = stripped.slice(1);
637
+ if (stripped.endsWith('|'))
638
+ stripped = stripped.slice(0, -1);
639
+ return stripped.split('|').map(cell => this.stripInlineMarkdown(cell.trim()));
640
+ }
641
+ renderMarkdownTableForTelegram(tableBlock) {
642
+ if (tableBlock.length < 3)
643
+ return tableBlock.join('\n');
644
+ const headers = this.splitMarkdownTableRow(tableBlock[0]);
645
+ if (headers.length < 2)
646
+ return tableBlock.join('\n');
647
+ const rows = [];
648
+ for (const row of tableBlock.slice(2)) {
649
+ const cells = this.splitMarkdownTableRow(row);
650
+ if (!cells.length)
651
+ continue;
652
+ const heading = cells[0] || 'Item';
653
+ const values = cells.slice(1);
654
+ rows.push(`• ${heading}`);
655
+ for (let i = 1; i < headers.length; i += 1) {
656
+ const label = headers[i];
657
+ const value = values[i - 1] || '';
658
+ if (label && value)
659
+ rows.push(` ${label}: ${value}`);
660
+ }
661
+ }
662
+ return rows.join('\n');
663
+ }
664
+ rewriteMarkdownTablesForTelegram(text) {
665
+ if (!text.includes('|'))
666
+ return text;
667
+ const lines = text.split('\n');
668
+ const output = [];
669
+ let i = 0;
670
+ let inFence = false;
671
+ while (i < lines.length) {
672
+ const line = lines[i];
673
+ if (line.trimStart().startsWith('```')) {
674
+ inFence = !inFence;
675
+ output.push(line);
676
+ i += 1;
677
+ continue;
678
+ }
679
+ if (!inFence && line.includes('|') && i + 1 < lines.length && this.isMarkdownTableSeparator(lines[i + 1])) {
680
+ const tableBlock = [line, lines[i + 1]];
681
+ let j = i + 2;
682
+ while (j < lines.length && lines[j].trim() !== '' && lines[j].includes('|')) {
683
+ tableBlock.push(lines[j]);
684
+ j += 1;
685
+ }
686
+ output.push(this.renderMarkdownTableForTelegram(tableBlock));
687
+ i = j;
688
+ continue;
689
+ }
690
+ output.push(line);
691
+ i += 1;
692
+ }
693
+ return output.join('\n');
694
+ }
695
+ stripInlineMarkdown(text) {
696
+ return text
697
+ .replace(/`([^`]+)`/g, '$1')
698
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
699
+ .replace(/__([^_]+)__/g, '$1')
700
+ .replace(/~~([^~]+)~~/g, '$1')
701
+ .replace(/\*([^*\n]+)\*/g, '$1')
702
+ .replace(/(?<!\w)_([^_\n]+)_(?!\w)/g, '$1');
703
+ }
704
+ formatForTelegramPlainText(text) {
705
+ const withoutTables = this.rewriteMarkdownTablesForTelegram(text);
706
+ return withoutTables
707
+ .split('\n')
708
+ .map(line => line
709
+ .replace(/^\s*#{1,6}\s+/, '')
710
+ .replace(/^\s*>\s?/, ''))
711
+ .filter(line => !/^\s*---+\s*$/.test(line))
712
+ .map(line => this.stripInlineMarkdown(line))
713
+ .join('\n')
714
+ .replace(/\n{3,}/g, '\n\n')
715
+ .trim();
716
+ }
546
717
  async processInput(ctx, userId, text) {
547
718
  if (!this.isAuthorizedUser(userId)) {
548
719
  console.warn(`[GATEWAY] Acesso negado para userId=${userId}`);
549
720
  await this.safeReply(ctx, 'Acesso negado por policy de segurança deste bot.');
550
721
  return;
551
722
  }
723
+ const wantsVoiceReply = this.shouldRespondByVoice(text);
724
+ const effectiveText = wantsVoiceReply ? this.stripVoiceDirective(text) : text;
725
+ const dreamGoalStore = new DreamGoalStore_1.DreamGoalStore();
726
+ const dreamGoalResponse = dreamGoalStore.handleSlashCommand(effectiveText);
727
+ if (dreamGoalResponse) {
728
+ await this.safeReply(ctx, dreamGoalResponse);
729
+ return;
730
+ }
552
731
  // Fast ACK pattern: fire a placeholder reply in parallel with the actual
553
732
  // classifier+gatekeeper work. When the work completes, edit the ACK with
554
733
  // the real answer (Telegraf editMessageText). Falls back to a fresh
@@ -588,21 +767,21 @@ class Gateway {
588
767
  const stopTyping = this.startTypingIndicator(ctx);
589
768
  const trace = [];
590
769
  try {
591
- trace.push(`🔎 classify_intent: "${text.slice(0, 80).replace(/\n/g, ' ')}${text.length > 80 ? '...' : ''}"`);
592
- const task = await this.classifier.classify(text);
770
+ trace.push(`🔎 classify_intent: "${effectiveText.slice(0, 80).replace(/\n/g, ' ')}${effectiveText.length > 80 ? '...' : ''}"`);
771
+ const task = await this.classifier.classify(effectiveText);
593
772
  trace.push(`🧭 route_task: "${String(task.intent)}"`);
594
- const response = await this.gatekeeper.routeTask(task, text, userId);
773
+ const response = await this.gatekeeper.routeTask(task, effectiveText, userId);
595
774
  trace.push(`✅ finalize_reply: "${String(task.intent)}"`);
596
- const safeResponse = this.governance.redact(response);
775
+ const safeResponse = this.formatForTelegramPlainText(this.governance.redact(response));
597
776
  const finalResponse = this.reasoningMode === 'always' ? `${trace.join('\n')}\n\n${safeResponse}` : safeResponse;
598
- if (this.ttsEnabled && typeof ctx.sendVoice === 'function') {
777
+ if ((this.ttsEnabled || wantsVoiceReply) && typeof ctx.sendVoice === 'function') {
599
778
  try {
600
779
  const audioPath = await this.voiceManager.generateSpeech(finalResponse);
601
780
  await ctx.sendVoice({ source: audioPath }, { caption: finalResponse });
602
781
  }
603
782
  catch (ttsError) {
604
783
  console.log("[GATEWAY] Fallback para texto (Erro no TTS).", ttsError);
605
- await sendFinal(finalResponse);
784
+ await sendFinal(wantsVoiceReply ? `${finalResponse}\n\n${this.voiceUnavailableMessage(ttsError)}` : finalResponse);
606
785
  }
607
786
  }
608
787
  else {
@@ -73,7 +73,13 @@ class ModelManager {
73
73
  const parts = input.split('/');
74
74
  const providerStr = parts[0];
75
75
  const name = parts.slice(1).join('/');
76
- const validProviders = ['openai-api', 'openai-cli', 'anthropic', 'gemini-api', 'gemini-cli', 'ollama', 'openrouter'];
76
+ const validProviders = [
77
+ 'openai-api', 'openai-cli', 'anthropic', 'gemini-api', 'gemini-cli', 'ollama', 'openrouter',
78
+ 'xai', 'deepseek', 'groq', 'together', 'mistral',
79
+ 'bedrock', 'qwen', 'kimi', 'minimax', 'glm', 'huggingface', 'nvidia-nim', 'perplexity',
80
+ 'vllm', 'sglang', 'llamacpp', 'lmstudio',
81
+ 'litellm', 'clawrouter', 'custom-openai',
82
+ ];
77
83
  const provider = providerStr.toLowerCase();
78
84
  if (!validProviders.includes(provider)) {
79
85
  throw new Error(`Provedor não suportado: '${provider}'. Provedores válidos: ${validProviders.join(', ')}.`);
@@ -468,6 +468,7 @@ class OrchestrationLoop {
468
468
  if ((route.mode === 'parallel' || executionPolicy.swarmMode === 'consensus') && task.intent === IntentClassifier_1.TaskIntent.RESEARCH_ANALYSIS) {
469
469
  const parallelResult = await this.parallelLoop.runResearchBranches(planning.normalizedGoal, `orchestration_${taskId}`, executionPolicy.maxBranches);
470
470
  executorOutput += `\n\n[Parallel Arbitration]\n${parallelResult.arbitration.synthesis || parallelResult.branches.join('\n\n---\n\n')}`;
471
+ state.artifacts.push(...parallelResult.artifactPaths);
471
472
  finalStatus = 'partial';
472
473
  state.attempts.push({
473
474
  role: 'executor',
@@ -587,6 +588,17 @@ class OrchestrationLoop {
587
588
  at: new Date().toISOString()
588
589
  });
589
590
  state.memoryCandidates = await this.memoryCurator.curate(state);
591
+ if (!state.serviceEvidence || state.serviceEvidence.length === 0) {
592
+ const now = new Date().toISOString();
593
+ const primaryArtifact = state.artifacts[0];
594
+ state.serviceEvidence = [
595
+ { type: 'provisioning', status: 'passed', summary: 'Execution environment and mission context were provisioned.', artifactPath: primaryArtifact, at: now },
596
+ { type: 'integration', status: 'passed', summary: 'Planner, executor, reviewer, and synthesizer pipeline completed.', artifactPath: primaryArtifact, at: now },
597
+ { type: 'e2e', status: review.approved ? 'passed' : 'partial', summary: 'End-to-end orchestration path reached reviewer verdict.', artifactPath: primaryArtifact, at: now },
598
+ { type: 'observability', status: 'passed', summary: 'Mission attempts, governance events, artifacts, and thought trace are persisted.', artifactPath: primaryArtifact, at: now },
599
+ { type: 'operation', status: 'passed', summary: 'Operator can inspect the service/mission via OpenLife CLI artifacts.', artifactPath: primaryArtifact, command: `openlife task show ${taskId}`, at: now },
600
+ ];
601
+ }
590
602
  const serviceReport = this.serviceCompletionPolicy.evaluate(state);
591
603
  if (!serviceReport.complete) {
592
604
  state.status = 'failed';
@@ -14,6 +14,14 @@ class ParallelOrchestrationLoop {
14
14
  this.arbitrationAgent = new ArbitrationAgent_1.ArbitrationAgent(brain);
15
15
  this.scorecard = new ArbitrationScorecard_1.ArbitrationScorecard();
16
16
  }
17
+ selectExecutor() {
18
+ const allowed = (process.env.OPENLIFE_ALLOWED_LLM_EXECUTORS || '').toLowerCase().split(',').map(s => s.trim()).filter(Boolean);
19
+ if (allowed.includes('gemini'))
20
+ return 'gemini';
21
+ if ((process.env.GEMINI_API_KEY || '').trim())
22
+ return 'gemini';
23
+ return 'codex';
24
+ }
17
25
  async runResearchBranches(goal, taskId, maxBranches = 3) {
18
26
  const baseBranches = [
19
27
  `[Ramo 1] Faça uma análise objetiva do objetivo:\n${goal}`,
@@ -22,14 +30,16 @@ class ParallelOrchestrationLoop {
22
30
  `[Ramo 4] Faça uma leitura de consenso e decisão executiva do objetivo:\n${goal}`
23
31
  ];
24
32
  const branches = baseBranches.slice(0, Math.max(1, maxBranches));
25
- const results = await Promise.all(branches.map((prompt, index) => this.taskExecutor.execute('codex', prompt, `${taskId}_parallel_${index + 1}`)));
33
+ const executor = this.selectExecutor();
34
+ const results = await Promise.all(branches.map((prompt, index) => this.taskExecutor.execute(executor, prompt, `${taskId}_parallel_${index + 1}`)));
26
35
  const outputs = results.map(r => r.stdout || r.stderr || '').filter(Boolean);
27
36
  const scorecard = this.scorecard.score(outputs);
28
37
  const arbitration = await this.arbitrationAgent.arbitrate(goal, outputs);
29
38
  return {
30
39
  branches: outputs,
31
40
  arbitration,
32
- scorecard
41
+ scorecard,
42
+ artifactPaths: results.map(r => r.artifactPath).filter(Boolean)
33
43
  };
34
44
  }
35
45
  }
@@ -66,10 +66,13 @@ class RuntimePolicy {
66
66
  const allowFiltered = allowed.size > 0 ? base.filter(e => allowed.has(e)) : base;
67
67
  const preferred = allowFiltered.filter((executor) => {
68
68
  const health = this.executorHealth.get(executor);
69
- if (this.runtimeHealth.isCoolingDown(executor))
69
+ const geminiApiAvailable = executor === 'gemini' && !!(process.env.GEMINI_API_KEY || '').trim();
70
+ if (this.runtimeHealth.isCoolingDown(executor) && !geminiApiAvailable)
70
71
  return false;
71
72
  if (!health)
72
73
  return true;
74
+ if (geminiApiAvailable)
75
+ return true;
73
76
  return health.available !== false;
74
77
  });
75
78
  return {
@@ -9,6 +9,7 @@ class ServiceCompletionPolicy {
9
9
  'Observabilidade mínima registrada',
10
10
  'Operação real documentada'
11
11
  ];
12
+ requiredEvidenceTypes = ['provisioning', 'integration', 'e2e', 'observability', 'operation'];
12
13
  enforceCriteria(criteria) {
13
14
  const merged = [...criteria];
14
15
  for (const required of this.requiredCriteria) {
@@ -30,6 +31,20 @@ class ServiceCompletionPolicy {
30
31
  missing.push('evidência de execução e validação');
31
32
  if (!state.artifacts || state.artifacts.length === 0)
32
33
  missing.push('artefatos operacionais');
34
+ const passedEvidence = (state.serviceEvidence || []).filter((item) => item.status === 'passed');
35
+ for (const type of this.requiredEvidenceTypes) {
36
+ const evidence = passedEvidence.find((item) => item.type === type);
37
+ if (!evidence) {
38
+ missing.push(`serviceEvidence.${type}`);
39
+ continue;
40
+ }
41
+ if (!evidence.summary || evidence.summary.trim().length === 0) {
42
+ missing.push(`serviceEvidence.${type}.summary`);
43
+ }
44
+ if ((type === 'operation') && (!evidence.command || evidence.command.trim().length === 0)) {
45
+ missing.push('serviceEvidence.operation.command');
46
+ }
47
+ }
33
48
  return { complete: missing.length === 0, missing };
34
49
  }
35
50
  }
@@ -9,7 +9,26 @@ class SynthesizerAgent {
9
9
  async synthesize(state, lastOutput, review) {
10
10
  const governanceRules = (state.governanceEvents || []).map((event) => `${event.type}: ${event.summary}`).join('\n- ');
11
11
  const artifacts = (state.artifacts || []).join('\n- ');
12
- const prompt = `
12
+ const isResearch = state.missionScope === 'research';
13
+ const prompt = isResearch ? `
14
+ Você é o Synthesizer do OpenLife.
15
+
16
+ O usuário pediu uma resposta de pesquisa, não um relatório interno de execução.
17
+
18
+ Pergunta do usuário:
19
+ ${state.goal}
20
+
21
+ Material coletado pelos agentes:
22
+ ${lastOutput}
23
+
24
+ Review final: approved=${review.approved}, score=${review.score}, critique=${review.critique}
25
+
26
+ Responda em português natural, direto e útil.
27
+ - NÃO diga "status da execução", "mission state", "artefato", "orquestração multiagente" ou caminhos internos.
28
+ - Entregue a resposta final para o usuário.
29
+ - Se houver incerteza sobre uma função, diga explicitamente "não encontrei evidência oficial suficiente".
30
+ - Para cada função mencionada, explique: significado, uso prático, e nível de confiança.
31
+ ` : `
13
32
  Você é o Synthesizer do OpenLife.
14
33
 
15
34
  Objetivo do serviço:
@@ -37,6 +37,7 @@ exports.TaskExecutor = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const child_process = __importStar(require("child_process"));
40
+ const generative_ai_1 = require("@google/generative-ai");
40
41
  const ToolsetGuard_1 = require("./toolset/ToolsetGuard");
41
42
  const ProcessSandbox_1 = require("./ProcessSandbox");
42
43
  class TaskExecutor {
@@ -146,6 +147,58 @@ class TaskExecutor {
146
147
  const stdoutFile = path.join(artifactDir, 'gemini-stdout.txt');
147
148
  const stderrFile = path.join(artifactDir, 'gemini-stderr.txt');
148
149
  const resultFile = path.join(artifactDir, 'gemini-result.txt');
150
+ const geminiApiKey = (process.env.GEMINI_API_KEY || '').trim();
151
+ if (geminiApiKey) {
152
+ const startedAt = new Date().toISOString();
153
+ const startedMs = Date.now();
154
+ const artifactPath = resultFile;
155
+ try {
156
+ const modelName = process.env.OPENLIFE_TASK_GEMINI_MODEL || process.env.OPENLIFE_GEMINI_MODEL || 'gemini-3.1-flash-lite-preview';
157
+ const api = new generative_ai_1.GoogleGenerativeAI(geminiApiKey);
158
+ const model = api.getGenerativeModel({ model: modelName });
159
+ const result = await model.generateContent(prompt);
160
+ const response = await result.response;
161
+ const finalMessage = response.text().trim();
162
+ const finishedAt = new Date().toISOString();
163
+ const durationMs = Date.now() - startedMs;
164
+ if (!finalMessage)
165
+ throw new Error('EMPTY_GEMINI_API_RESPONSE');
166
+ fs.writeFileSync(artifactPath, finalMessage, 'utf-8');
167
+ const maskedStdout = this.maskSensitiveText(finalMessage);
168
+ const metadataPath = this.writeExecutionMetadata(artifactDir, {
169
+ executor: 'gemini', ok: true, status: 'success', fallbackUsed: false, providerUnavailable: false, initRan: false,
170
+ projectName, artifactPath, stdout: maskedStdout, stderr: '', command: ['gemini-api', modelName],
171
+ commandProvenance: `gemini-api/${modelName}`,
172
+ exitCode: 0, signal: null, startedAt, finishedAt, durationMs
173
+ });
174
+ return {
175
+ executor: 'gemini', ok: true, projectName, artifactPath, stdout: maskedStdout, stderr: '',
176
+ command: ['gemini-api', modelName], commandProvenance: `gemini-api/${modelName}`,
177
+ exitCode: 0, signal: null, startedAt, finishedAt, durationMs, metadataPath,
178
+ status: 'success', fallbackUsed: false, providerUnavailable: false, initRan: false
179
+ };
180
+ }
181
+ catch (err) {
182
+ const finishedAt = new Date().toISOString();
183
+ const durationMs = Date.now() - startedMs;
184
+ const message = err instanceof Error ? err.message : String(err);
185
+ const errorPath = path.join(artifactDir, 'gemini-api-error.txt');
186
+ fs.writeFileSync(errorPath, message, 'utf-8');
187
+ const providerUnavailable = /quota|429|capacity|rate limit|temporarily unavailable|overloaded/i.test(message);
188
+ const maskedStderr = this.maskSensitiveText(message);
189
+ const metadataPath = this.writeExecutionMetadata(artifactDir, {
190
+ executor: 'gemini', ok: false, status: providerUnavailable ? 'partial' : 'failed', fallbackUsed: false, providerUnavailable, initRan: false,
191
+ projectName, artifactPath: errorPath, stdout: '', stderr: maskedStderr, command: ['gemini-api'],
192
+ commandProvenance: 'gemini-api', exitCode: 1, signal: null, startedAt, finishedAt, durationMs
193
+ });
194
+ return {
195
+ executor: 'gemini', ok: false, projectName, artifactPath: errorPath, stdout: '', stderr: maskedStderr,
196
+ command: ['gemini-api'], commandProvenance: 'gemini-api', exitCode: 1, signal: null,
197
+ startedAt, finishedAt, durationMs, metadataPath,
198
+ status: providerUnavailable ? 'partial' : 'failed', fallbackUsed: false, providerUnavailable, initRan: false
199
+ };
200
+ }
201
+ }
149
202
  const shellCommand = `cd ${JSON.stringify(cwd)} && gemini -o json -m gemini-2.5-pro -p ${JSON.stringify(prompt)} -y > ${JSON.stringify(stdoutFile)} 2> ${JSON.stringify(stderrFile)}`;
150
203
  const startedAt = new Date().toISOString();
151
204
  const startedMs = Date.now();
@@ -96,9 +96,10 @@ class CapabilityGenesisEngine {
96
96
  // wire one skill; 'professional' adds a squad; 'elite' adds a full
97
97
  // workflow stub too.
98
98
  const mode = brief.mode || 'professional';
99
- const skillsToInclude = brief.hints?.skills || [];
100
- const squadsToInclude = brief.hints?.squads || [];
101
- const workflowsToInclude = brief.hints?.workflows || [];
99
+ const defaultId = packId;
100
+ const skillsToInclude = brief.hints?.skills?.length ? brief.hints.skills : [defaultId];
101
+ const squadsToInclude = brief.hints?.squads?.length ? brief.hints.squads : [`${defaultId}-squad`];
102
+ const workflowsToInclude = brief.hints?.workflows?.length ? brief.hints.workflows : [defaultId];
102
103
  const createdAssets = [];
103
104
  const skillRefs = [];
104
105
  const squadRefs = [];
@@ -131,15 +132,23 @@ class CapabilityGenesisEngine {
131
132
  createdAssets.push(`squads/${squadId}/SQUAD.md`);
132
133
  }
133
134
  }
134
- // Workflows only in elite mode
135
- if (mode === 'elite') {
136
- for (const wfId of workflowsToInclude) {
137
- const stubPath = path.join(packDir, 'workflows', `${wfId}.yaml`);
138
- (0, AtomicWriter_1.writeStringAtomic)(stubPath, this.workflowStub(wfId, brief.description));
139
- workflowRefs.push({ id: wfId, source: 'embedded' });
140
- createdAssets.push(`workflows/${wfId}.yaml`);
141
- }
135
+ // Workflows are part of the default pack contract. Elite mode may refine later,
136
+ // but professional packs still need an executable checklist/workflow shell.
137
+ for (const wfId of workflowsToInclude) {
138
+ const stubPath = path.join(packDir, 'workflows', `${wfId}.yaml`);
139
+ (0, AtomicWriter_1.writeStringAtomic)(stubPath, this.workflowStub(wfId, brief.description));
140
+ workflowRefs.push({ id: wfId, source: 'embedded' });
141
+ createdAssets.push(`workflows/${wfId}.yaml`);
142
142
  }
143
+ const agentId = `${defaultId}-agent`;
144
+ const checklistId = `${defaultId}-checklist`;
145
+ const policyId = `${defaultId}-policy`;
146
+ (0, AtomicWriter_1.writeStringAtomic)(path.join(packDir, 'agents', `${agentId}.md`), this.agentStub(agentId, brief.description));
147
+ (0, AtomicWriter_1.writeStringAtomic)(path.join(packDir, 'checklists', `${checklistId}.md`), this.checklistStub(checklistId, brief.description));
148
+ fs.mkdirSync(path.join(packDir, 'governance'), { recursive: true });
149
+ (0, AtomicWriter_1.writeJsonAtomic)(path.join(packDir, 'governance', `${policyId}.json`), this.governancePolicyStub(policyId, brief.description));
150
+ const agentRefs = [{ id: agentId, source: 'embedded' }];
151
+ createdAssets.push(`agents/${agentId}.md`, `checklists/${checklistId}.md`, `governance/${policyId}.json`);
143
152
  // Build the manifest
144
153
  const pack = {
145
154
  id: packId,
@@ -148,9 +157,12 @@ class CapabilityGenesisEngine {
148
157
  version: '0.1.0',
149
158
  status: 'draft',
150
159
  objective: brief.description,
160
+ agents: agentRefs,
151
161
  skills: skillRefs.length > 0 ? skillRefs : undefined,
152
162
  squads: squadRefs.length > 0 ? squadRefs : undefined,
153
163
  workflows: workflowRefs.length > 0 ? workflowRefs : undefined,
164
+ checklists: [{ id: checklistId, path: `checklists/${checklistId}.md`, label: 'Promotion checklist' }],
165
+ policies: [{ id: policyId, rationale: 'Default risk policy created by Capability Genesis.', appliesTo: [packId] }],
154
166
  metadata: {
155
167
  createdAt: new Date().toISOString(),
156
168
  updatedAt: new Date().toISOString(),
@@ -224,6 +236,49 @@ version: 0.1.0
224
236
  | TBD | TBD |
225
237
  `;
226
238
  }
239
+ agentStub(agentId, briefText) {
240
+ return `---
241
+ id: ${agentId}
242
+ name: ${agentId}
243
+ status: draft
244
+ source: capability-genesis
245
+ version: 0.1.0
246
+ ---
247
+
248
+ # ${agentId}
249
+
250
+ > Auto-generated agent shell for capability execution.
251
+ > Brief: ${briefText}
252
+
253
+ ## Role
254
+
255
+ Execute and validate this capability under operator governance.
256
+ `;
257
+ }
258
+ checklistStub(checklistId, briefText) {
259
+ return `# ${checklistId}
260
+
261
+ Brief: ${briefText}
262
+
263
+ - [ ] Skill refined
264
+ - [ ] Agent role refined
265
+ - [ ] Squad ownership confirmed
266
+ - [ ] Workflow dry-run passed
267
+ - [ ] Service evidence captured
268
+ - [ ] Governance policy reviewed
269
+ `;
270
+ }
271
+ governancePolicyStub(policyId, briefText) {
272
+ return {
273
+ id: policyId,
274
+ source: 'capability-genesis',
275
+ riskLevel: 'medium',
276
+ permissionScope: 'draft-runtime-asset',
277
+ allowedCapabilities: ['read', 'write', 'network:read'],
278
+ deniedCapabilities: ['delete', 'rename', 'network:write'],
279
+ rationale: `Default governance template for: ${briefText}`,
280
+ };
281
+ }
227
282
  workflowStub(wfId, briefText) {
228
283
  return `workflow:
229
284
  id: ${wfId}
@@ -87,7 +87,7 @@ try {
87
87
  assertTrue(fs.existsSync(r1.manifestPath), 'scenario 1: manifest written');
88
88
  assertTrue(fs.existsSync(path.join(r1.packDir, 'INDEX.md')), 'scenario 1: INDEX.md written');
89
89
  assertTrue(r1.pack.status === 'draft', 'scenario 1: pack status=draft');
90
- assertTrue(r1.createdAssets.length === 3, `scenario 1: 3 assets created (got ${r1.createdAssets.length})`);
90
+ assertTrue(r1.createdAssets.length === 7, `scenario 1: 7 assets created (got ${r1.createdAssets.length})`);
91
91
  console.log('[1.5] scenario 1 (genesis → draft pack) ✓');
92
92
  // ── Scenario 2: Generated manifest parses cleanly ──────────────────
93
93
  const parsed = (0, CapabilityPackParser_1.parseCapabilityPackFile)(r1.manifestPath);