@orkastery/cli 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/adapters/README.md +11 -0
  2. package/adapters/claude-code/.claude-plugin/plugin.json +3 -1
  3. package/adapters/claude-code/agents/ork-check.md +1 -1
  4. package/adapters/claude-code/agents/ork-go.md +2 -2
  5. package/adapters/claude-code/agents/ork-goal.md +1 -1
  6. package/adapters/claude-code/agents/ork-master.md +1 -1
  7. package/adapters/claude-code/agents/ork-plan.md +1 -1
  8. package/adapters/claude-code/agents/ork-ship.md +1 -1
  9. package/adapters/claude-code/commands/master.md +11 -8
  10. package/adapters/claude-code/commands/ork.md +8 -0
  11. package/adapters/claude-code/commands/ship.md +2 -2
  12. package/adapters/codex/skills/ork/SKILL.md +12 -1
  13. package/adapters/hermes/README.md +3 -0
  14. package/adapters/hermes/bin/ork-abrir-thread.sh +17 -2
  15. package/adapters/hermes/bin/ork-brain.sh +3 -0
  16. package/adapters/hermes/bin/ork-maestro.sh +4 -1
  17. package/adapters/hermes/bin/ork-network-roadmap.sh +11 -0
  18. package/adapters/hermes/bin/ork-roadmap-status.sh +9 -0
  19. package/adapters/hermes/hermes.plugin.json +2 -0
  20. package/adapters/hermes/hitl-ingress/__init__.py +36 -3
  21. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +19 -20
  22. package/adapters/openclaw/README.md +34 -3
  23. package/adapters/openclaw/dist/hitl-ingress.js +37 -3
  24. package/adapters/openclaw/dist/index.js +92 -39
  25. package/adapters/openclaw/openclaw.plugin.json +10 -3
  26. package/adapters/openclaw/package.json +2 -2
  27. package/adapters/openclaw/src/hitl-ingress.ts +32 -3
  28. package/adapters/openclaw/src/index.ts +108 -41
  29. package/assets/docs/padroes/documentacao-de-produto.md +2 -2
  30. package/assets/docs/padroes/roadmap-de-produto.md +1 -1
  31. package/assets/docs/produto/_modelo-feature.md +2 -0
  32. package/assets/orkmind_bridge.py +214 -1
  33. package/dist/adapters/claude-bg.js +88 -9
  34. package/dist/adapters/codex-events.js +3 -1
  35. package/dist/board.js +74 -0
  36. package/dist/branch-de-estado.js +5 -4
  37. package/dist/busca-semantica.js +161 -0
  38. package/dist/ci.js +141 -10
  39. package/dist/company-brain-cli.js +27 -2
  40. package/dist/company-brain-context.js +132 -0
  41. package/dist/company-brain-dossie.js +376 -0
  42. package/dist/company-brain-mcp.js +4 -1
  43. package/dist/conducao.js +117 -31
  44. package/dist/decisao-autonoma.js +42 -0
  45. package/dist/docs.js +64 -22
  46. package/dist/doctor.js +33 -0
  47. package/dist/entrega-pr.js +109 -5
  48. package/dist/experiencia-instalacao.js +256 -0
  49. package/dist/experiencia.js +72 -0
  50. package/dist/fabrica-estado.js +43 -10
  51. package/dist/fechamento.js +105 -0
  52. package/dist/fix.js +1 -1
  53. package/dist/forja.js +435 -0
  54. package/dist/gates.js +10 -1
  55. package/dist/hitl-classificacao.js +11 -1
  56. package/dist/hitl-contract.js +50 -7
  57. package/dist/hitl-curto.js +158 -0
  58. package/dist/hitl-gates.js +67 -1
  59. package/dist/hitl-lote.js +30 -15
  60. package/dist/hitl-presentation.js +10 -7
  61. package/dist/hitl-resumo.js +33 -12
  62. package/dist/hitl-texto-livre.js +150 -0
  63. package/dist/hosts.js +86 -1
  64. package/dist/index.js +605 -41
  65. package/dist/indice-vetorial.js +317 -0
  66. package/dist/init.js +13 -0
  67. package/dist/integracoes-locais.js +1 -1
  68. package/dist/intelligence-benchmark-contract.js +570 -0
  69. package/dist/intelligence-graph-cli.js +430 -0
  70. package/dist/intelligence-graph-contract.js +577 -0
  71. package/dist/intelligence-graph-extract-md.js +469 -0
  72. package/dist/intelligence-graph-extract-ts.js +810 -0
  73. package/dist/intelligence-graph-extract.js +369 -0
  74. package/dist/intelligence-graph-index.js +512 -0
  75. package/dist/intelligence-graph-parsers.js +327 -0
  76. package/dist/intelligence-graph-query.js +344 -0
  77. package/dist/intelligence-graph-repo.js +187 -0
  78. package/dist/leases.js +84 -0
  79. package/dist/ledger.js +14 -0
  80. package/dist/maestro-cli.js +10 -1
  81. package/dist/maestro-contract.js +5 -1
  82. package/dist/maestro-discovery.js +16 -2
  83. package/dist/maestro-snapshot.js +6 -2
  84. package/dist/manifest.js +142 -5
  85. package/dist/master-batch.js +3 -2
  86. package/dist/master-digest.js +2 -2
  87. package/dist/master-nota.js +203 -0
  88. package/dist/master.js +4 -0
  89. package/dist/mcp-experiencia.js +164 -0
  90. package/dist/mcp-git.js +36 -10
  91. package/dist/mcp-install.js +3 -1
  92. package/dist/mcp-maestro.js +3 -2
  93. package/dist/mcp-server.js +84 -5
  94. package/dist/memoria.js +147 -1
  95. package/dist/network-roadmap.js +907 -0
  96. package/dist/ocupacao.js +38 -2
  97. package/dist/onboarding.js +113 -7
  98. package/dist/orkmind.js +165 -9
  99. package/dist/phase.js +166 -13
  100. package/dist/policies.js +2 -0
  101. package/dist/projeto-alvo.js +551 -0
  102. package/dist/prompts.js +1 -1
  103. package/dist/pulse-consentimento.js +4 -2
  104. package/dist/pulse-delivery.js +5 -0
  105. package/dist/pulse-resposta.js +674 -49
  106. package/dist/pulse.js +21 -0
  107. package/dist/ratelimit.js +3 -2
  108. package/dist/retry.js +230 -9
  109. package/dist/roadmap-reservas.js +262 -20
  110. package/dist/roadmap-status.js +213 -0
  111. package/dist/runtime-ambiente.js +12 -1
  112. package/dist/session-watcher-claude.js +26 -0
  113. package/dist/sessoes.js +49 -15
  114. package/dist/thread-close.js +3 -0
  115. package/dist/verify.js +5 -4
  116. package/dist/worktree.js +77 -0
  117. package/eval/casos/orchestration-experience-pt-br.json +48 -0
  118. package/eval/casos/orchestration-experience.json +48 -0
  119. package/package.json +1 -1
  120. package/schemas/code-artifact-graph.v1.schema.json +581 -0
  121. package/schemas/graph-benchmark.v1.schema.json +2240 -0
  122. package/schemas/maestro-snapshot.schema.json +23 -0
  123. package/skills/README.md +7 -5
  124. package/skills/core/onboarding/SKILL.md +7 -0
  125. package/skills/core/orchestration-experience/SKILL.md +113 -0
  126. package/skills/core/orchestration-experience-pt-br/SKILL.md +112 -0
  127. package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
  128. package/skills/phases/go-implementation/SKILL.md +2 -0
@@ -33,15 +33,26 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.chaveDoGate = exports.GRAMATICA_DO_PULSE = exports.CONTRATO_LOTE_SERVIDO = exports.CONTRATO_RESPOSTA_DO_PULSE = void 0;
36
+ exports.CONTRATO_ESCUTA = exports.chaveDoGate = exports.GUARDA_DA_PERGUNTA_MS = exports.GRAMATICA_DO_PULSE = exports.CONTRATO_LOTE_SERVIDO = exports.CONTRATO_RESPOSTA_DO_PULSE = void 0;
37
37
  exports.interpretarRespostaDoPulse = interpretarRespostaDoPulse;
38
38
  exports.arquivoDoLoteServido = arquivoDoLoteServido;
39
39
  exports.lerLoteServido = lerLoteServido;
40
40
  exports.perguntaViva = perguntaViva;
41
+ exports.perguntaEmAberto = perguntaEmAberto;
41
42
  exports.threadsComPerguntaViva = threadsComPerguntaViva;
42
43
  exports.gatesJaServidos = gatesJaServidos;
43
44
  exports.avaliarFila = avaliarFila;
44
45
  exports.servirLote = servirLote;
46
+ exports.pedidoParaResponder = pedidoParaResponder;
47
+ exports.pedidosComCodigo = pedidosComCodigo;
48
+ exports.codigosEmUso = codigosEmUso;
49
+ exports.textoDoDetalhe = textoDoDetalhe;
50
+ exports.opcoesReais = opcoesReais;
51
+ exports.registrarPorCodigo = registrarPorCodigo;
52
+ exports.pedidosAbertosDaThread = pedidosAbertosDaThread;
53
+ exports.abertosDoDono = abertosDoDono;
54
+ exports.arquivoDaEscuta = arquivoDaEscuta;
55
+ exports.atualizarEscuta = atualizarEscuta;
45
56
  exports.responderPeloPulse = responderPeloPulse;
46
57
  /**
47
58
  * I-41 (GO-FIX 1, B1 e B2): o caminho de volta.
@@ -73,6 +84,15 @@ const hitl_gates_1 = require("./hitl-gates");
73
84
  const hitl_contract_1 = require("./hitl-contract");
74
85
  const monitor_lock_1 = require("./monitor-lock");
75
86
  const hitl_lote_1 = require("./hitl-lote");
87
+ const hitl_curto_1 = require("./hitl-curto");
88
+ const hitl_texto_livre_1 = require("./hitl-texto-livre");
89
+ const hitl_presentation_1 = require("./hitl-presentation");
90
+ const roadmap_status_1 = require("./roadmap-status");
91
+ const master_nota_1 = require("./master-nota");
92
+ const master_1 = require("./master");
93
+ const master_batch_1 = require("./master-batch");
94
+ const master_digest_1 = require("./master-digest");
95
+ const hitl_classificacao_1 = require("./hitl-classificacao");
76
96
  const pulse_cadencia_1 = require("./pulse-cadencia");
77
97
  const ledger_1 = require("./ledger");
78
98
  const thread_1 = require("./thread");
@@ -95,31 +115,60 @@ exports.CONTRATO_LOTE_SERVIDO = 'ork.pulse-lote-servido/v1';
95
115
  * Tag no meio de uma frase continua sendo conversa com o assistente.
96
116
  */
97
117
  exports.GRAMATICA_DO_PULSE = Object.freeze({
98
- consentimento: '^[ \\t]*(?=[A-HJKMNP-TV-Z][2-9A-HJKMNP-TV-Z]{0,2}[2-9])[A-HJKMNP-TV-Z][2-9A-HJKMNP-TV-Z]{3}[ \\t]+[^\\r\\n]{1,40}$',
118
+ // RM-048 (D9): o texto depois do codigo vai a 200 caracteres, para caber o porque da nota.
119
+ consentimento: '^[ \\t]*(?=[A-HJKMNP-TV-Z][2-9A-HJKMNP-TV-Z]{0,2}[2-9])[A-HJKMNP-TV-Z][2-9A-HJKMNP-TV-Z]{3}[ \\t]+[^\\r\\n]{1,200}$',
99
120
  lote: '^[ \\t]*[0-9]{1,2}[ \\t]*[a-zA-Z](?:[ \\t,;]*[0-9]{1,2}[ \\t]*[a-zA-Z])*[ \\t]*[.!]?[ \\t]*$',
100
121
  cadencia: '^[ \\t]*#OrkPulse(?:On(?:-(?:15|30|60)m)?|Off)[ \\t]*[.!]?[ \\t]*$',
122
+ // RM-048 (D2 e D3): as formas do texto livre, geradas do vocabulario fechado. `lista` e
123
+ // interceptada sempre; `livre` so dentro da janela de escuta (`pulse-escuta.json`).
124
+ lista: hitl_texto_livre_1.FORMAS_DO_TEXTO_LIVRE.lista,
125
+ livre: hitl_texto_livre_1.FORMAS_DO_TEXTO_LIVRE.livre,
126
+ // RM-048 (item 8): o teclado do digest semanal ("ratificar <thread> <assinatura> <classe>" e
127
+ // "ratificar-lote <semana> <assinatura>") passa pelo mesmo endereco assinado, e nao pelo agente.
128
+ ratificacao: '^[ \\t]*ratificar(?:-lote)?(?:[ \\t]+[A-Za-z0-9._-]{1,80}){2,3}[ \\t]*$',
101
129
  });
130
+ const RATIFICACAO = new RegExp(exports.GRAMATICA_DO_PULSE.ratificacao, 'i');
102
131
  const CONSENTIMENTO = new RegExp(exports.GRAMATICA_DO_PULSE.consentimento, 'i');
103
132
  const LOTE = new RegExp(exports.GRAMATICA_DO_PULSE.lote);
104
133
  const CADENCIA = new RegExp(exports.GRAMATICA_DO_PULSE.cadencia, 'i');
105
134
  /** O que o dono quis dizer, pela forma. Nada aqui le intencao: ou a forma bate, ou nao. */
106
135
  function interpretarRespostaDoPulse(texto) {
107
- if (typeof texto !== 'string' || texto.length > 200)
136
+ if (typeof texto !== 'string' || texto.length > hitl_texto_livre_1.TETO_DA_FORMA)
108
137
  return { forma: 'desconhecida' };
138
+ // S2 do CHECK: a lista vem antes, porque "1 s" e "1 n" sao palavras do vocabulario e nao letras.
139
+ // "1a 2c" casa as duas e sai igual pelas duas; o lote segue para letra fora de a-d ("1e").
140
+ const lista = (0, hitl_texto_livre_1.lerLista)(texto);
141
+ if (lista && lista.some(i => i.trecho.tipo !== 'letra'))
142
+ return { forma: 'lista', itens: lista };
109
143
  if (LOTE.test(texto)) {
110
144
  const escolhas = [...texto.matchAll(/([0-9]{1,2})[ \t]*([a-zA-Z])/g)]
111
145
  .map(m => ({ numero: Number(m[1]), letra: m[2].toLowerCase() }));
112
146
  return { forma: 'lote', escolhas };
113
147
  }
148
+ if (lista)
149
+ return { forma: 'lista', itens: lista };
150
+ if (RATIFICACAO.test(texto))
151
+ return { forma: 'ratificacao', texto: texto.trim() };
114
152
  if (CONSENTIMENTO.test(texto))
115
153
  return { forma: 'consentimento', codigo: texto.trim().slice(0, 4).toUpperCase() };
116
154
  const tag = CADENCIA.test(texto) ? (0, pulse_cadencia_1.extrairTagDoPulse)(texto) : null;
117
155
  if (tag)
118
156
  return { forma: 'cadencia', tag };
157
+ const solta = (0, hitl_texto_livre_1.lerSolta)(texto);
158
+ if (solta)
159
+ return { forma: 'livre', trecho: solta };
119
160
  return { forma: 'desconhecida' };
120
161
  }
121
162
  /** Quanto tempo uma pergunta respondida continua no registro, para "ja estava registrada". */
122
163
  const GUARDA_DO_RESPONDIDO_MS = 24 * 60 * 60 * 1000;
164
+ /**
165
+ * RM-048 (D4): quanto tempo o NUMERO de uma pergunta nao respondida continua valendo depois de
166
+ * servido. O prazo do pedido e de uma hora; o numero que o dono recebeu nao pode vencer junto:
167
+ * respondido depois, ele vai ao pedido renovado quando a pergunta e a mesma.
168
+ */
169
+ exports.GUARDA_DA_PERGUNTA_MS = 24 * 60 * 60 * 1000;
170
+ /** O maior numero que a gramatica do ingresso le (dois digitos). */
171
+ const MAIOR_NUMERO = 99;
123
172
  const sha = (s) => (0, node_crypto_1.createHash)('sha256').update(s).digest('hex');
124
173
  function arquivoDoLoteServido(raiz, estadoDir) {
125
174
  return path.join(estadoDir ?? path.join((0, estado_thread_1.raizDoEstado)(raiz), '.orkastery', 'monitor'), 'pulse-lote.json');
@@ -145,16 +194,39 @@ function gravarLoteServido(raiz, lote, estadoDir) {
145
194
  function perguntaViva(p, quando) {
146
195
  return !p.respondida && (p.prazo === null || Date.parse(quando) < Date.parse(p.prazo));
147
196
  }
197
+ /**
198
+ * RM-048 (D4): a pergunta que o dono ainda pode responder pelo numero. Sem resposta e dentro da
199
+ * guarda de 24 h desde que saiu: depois do prazo do pedido ela vai ao pedido renovado.
200
+ */
201
+ function perguntaEmAberto(p, quando) {
202
+ return !p.respondida && (perguntaViva(p, quando) || Date.parse(quando) - Date.parse(p.servidaEm) < exports.GUARDA_DA_PERGUNTA_MS);
203
+ }
148
204
  /** O que sai do registro: vencida sem resposta, ou respondida ha mais de um dia. */
149
205
  function podar(lote, quando) {
150
206
  const agora = Date.parse(quando);
151
207
  const perguntas = lote.perguntas.filter(p => perguntaViva(p, quando) ||
208
+ (!p.respondida && agora - Date.parse(p.servidaEm) < exports.GUARDA_DA_PERGUNTA_MS) ||
152
209
  (p.respondida && agora - Date.parse(p.respondida.em) < GUARDA_DO_RESPONDIDO_MS));
153
210
  const numeros = new Set(perguntas.map(p => p.numero));
154
211
  const porCodigo = Object.fromEntries(Object.entries(lote.porCodigo)
155
212
  .map(([codigo, ns]) => [codigo, ns.filter(n => numeros.has(n))]).filter(([, ns]) => ns.length));
156
213
  return { ...lote, perguntas, porCodigo };
157
214
  }
215
+ /**
216
+ * O primeiro numero do proximo lote. Continua os do registro, para "1a" nunca querer dizer duas
217
+ * perguntas; quando passaria de 99, que a gramatica nao le, volta ao menor bloco livre.
218
+ */
219
+ function numeroInicialLivre(servido) {
220
+ const usados = new Set(servido.perguntas.map(p => p.numero));
221
+ const seguinte = Math.max(0, ...usados) + 1;
222
+ if (seguinte + hitl_lote_1.TETO_DE_PERGUNTAS_POR_LOTE - 1 <= MAIOR_NUMERO)
223
+ return seguinte;
224
+ for (let n = 1; n + hitl_lote_1.TETO_DE_PERGUNTAS_POR_LOTE - 1 <= MAIOR_NUMERO; n++) {
225
+ if (Array.from({ length: hitl_lote_1.TETO_DE_PERGUNTAS_POR_LOTE }, (_, i) => n + i).every(k => !usados.has(k)))
226
+ return n;
227
+ }
228
+ return 1;
229
+ }
158
230
  /** Threads com pergunta servida esperando resposta: elas nao entram de novo no resumo. */
159
231
  function threadsComPerguntaViva(raiz, quando, estadoDir) {
160
232
  return new Set(lerLoteServido(raiz, estadoDir).perguntas.filter(p => perguntaViva(p, quando)).map(p => p.thread));
@@ -189,7 +261,9 @@ function avaliarFila(raiz, itens, quando, opcoes = {}) {
189
261
  const candidatos = [], atos = new Map();
190
262
  let consertos = 0;
191
263
  for (const [thread, doThread] of porThread) {
192
- const escalacoes = doThread.map(i => i.motivo).filter(m => hitl_gates_1.MOTIVOS_DE_ESCALACAO_HUMANA.includes(m));
264
+ // RM-048 (D6): escalacao tecnica nao vira pergunta ao dono; o resumo a mostra como "Conosco".
265
+ const escalacoes = doThread.map(i => i.motivo).filter(m => hitl_gates_1.MOTIVOS_DE_ESCALACAO_HUMANA.includes(m) &&
266
+ (0, hitl_classificacao_1.quemDecide)(m) === 'dono');
193
267
  let achado;
194
268
  for (const motivo of [...new Set([...escalacoes, 'human.pending'])]) {
195
269
  try {
@@ -214,7 +288,7 @@ function avaliarFila(raiz, itens, quando, opcoes = {}) {
214
288
  // Sem gate que espere, um pedido aberto ainda e pergunta se for respondivel agora; senao, e
215
289
  // conserto quando o formato e o problema, e historia quando o prazo ja passou.
216
290
  const pedido = doThread.map(i => i.pedido).find((p) => !!p);
217
- if (!pedido)
291
+ if (!pedido || (0, hitl_classificacao_1.quemDecide)((0, hitl_contract_1.motivoDoPedido)(pedido)) !== 'dono')
218
292
  continue;
219
293
  let aberto = false;
220
294
  try {
@@ -261,7 +335,7 @@ function servirLote(raiz, entrada) {
261
335
  const { quando } = entrada;
262
336
  let servido = podar(lerLoteServido(raiz, entrada.estadoDir), quando);
263
337
  const vivas = new Set(servido.perguntas.filter(p => perguntaViva(p, quando)).map(p => p.thread));
264
- const inicio = servido.perguntas.reduce((max, p) => Math.max(max, p.numero), 0) + 1;
338
+ const inicio = numeroInicialLivre(servido);
265
339
  const abertos = [];
266
340
  // Thread ocupada agora (despacho ou outra resposta segurando o lock) nao e thread que deixou de
267
341
  // esperar: ela vai para o proximo lote, e nao some do que o dono pediu.
@@ -289,12 +363,25 @@ function servirLote(raiz, entrada) {
289
363
  }
290
364
  const lote = (0, hitl_lote_1.montarLote)(abertos.map(a => a.pedido), { numeroInicial: inicio });
291
365
  const restantes = [...ocupadas, ...candidatos.slice(i)];
366
+ // RM-048 (D1): "desde" sai do ledger, do primeiro pedido com o mesmo codigo; renovar nao zera.
367
+ for (const p of lote.perguntas) {
368
+ const pedido = abertos.find(a => a.pedido.id === p.pedidoId).pedido;
369
+ try {
370
+ p.desde = (0, hitl_curto_1.desdeDoPedido)((0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, pedido.thread)), pedido);
371
+ }
372
+ catch {
373
+ p.desde = pedido.criadoEm;
374
+ }
375
+ }
292
376
  const novas = lote.perguntas.map(p => {
293
377
  const pedido = abertos.find(a => a.pedido.id === p.pedidoId).pedido;
378
+ const recomendada = p.alternativas.find(a => a.recomendada);
294
379
  return {
295
380
  numero: p.numero, thread: pedido.thread, fase: pedido.fase, pedidoId: pedido.id,
296
381
  pedidoSha256: sha(JSON.stringify(pedido)), letras: p.alternativas.map(a => a.letra),
297
382
  alternativas: p.alternativas.map(a => a.texto), consequencias: p.alternativas.map(a => a.consequencia),
383
+ ...(recomendada ? { recomendada: { letra: recomendada.letra, porque: recomendada.recomendada.porque } } : {}),
384
+ ...(p.corpo ? { corpo: p.corpo } : {}), ...(p.ato ? { ato: p.ato } : {}), desde: p.desde ?? null,
298
385
  pergunta: p.pergunta, prazo: (0, hitl_contract_1.prazoDoPedido)(pedido) ?? null, servidaEm: quando,
299
386
  };
300
387
  });
@@ -308,7 +395,7 @@ function servirLote(raiz, entrada) {
308
395
  quando, resumoSha256: entrada.pedido.resumoSha256, candidatos: restantes,
309
396
  prazoMin: pulse_consentimento_1.PRAZO_PADRAO_MIN, estadoDir: entrada.estadoDir,
310
397
  }) : undefined;
311
- const texto = (0, hitl_lote_1.textoDoLote)(lote, { canal: entrada.canal, naoEsperam,
398
+ const texto = (0, hitl_lote_1.textoDoLote)(lote, { canal: entrada.canal, naoEsperam, quando,
312
399
  ...(proximo ? { proximo: { codigo: proximo.codigo, faltam: restantes.length } } : {}) });
313
400
  return { texto, numeros, restantes, naoEsperam, ...(proximo ? { proximo } : {}) };
314
401
  }
@@ -316,21 +403,514 @@ function servirLote(raiz, entrada) {
316
403
  * O lote de um sim ja servido, para quem pede de novo, ou o aviso de que ele ja nao serve. Reenviar
317
404
  * pergunta morta (toda respondida ou vencida) seria pedir uma resposta que o gate vai recusar.
318
405
  */
319
- function loteDeNovo(servido, numeros, codigo, quando, canal, aberto) {
320
- const vivas = servido.perguntas.filter(p => numeros.includes(p.numero) && perguntaViva(p, quando)).map(p => p.numero);
406
+ function loteDeNovo(raiz, servido, numeros, codigo, quando, canal, aberto) {
407
+ const vivas = servido.perguntas.filter(p => numeros.includes(p.numero) && perguntaEmAberto(p, quando)).map(p => p.numero);
321
408
  if (vivas.length)
322
- return reenviarLote(servido, vivas, canal);
409
+ return reenviarLote(raiz, servido, vivas, canal, quando);
323
410
  return `As perguntas do código ${codigo} já foram respondidas ou venceram. ` +
324
411
  (aberto ? `Para as de agora, responda ${aberto.codigo} a.` : 'O próximo resumo traz as que ainda esperarem você.');
325
412
  }
326
413
  /** O MESMO lote de novo, para quem repete o sim porque nao viu a mensagem chegar. */
327
- function reenviarLote(servido, numeros, canal) {
328
- const perguntas = servido.perguntas.filter(p => numeros.includes(p.numero)).map(p => ({
329
- numero: p.numero, thread: p.thread, fase: p.fase, pergunta: p.pergunta, pedidoId: p.pedidoId,
330
- alternativas: p.letras.map((letra, i) => ({ letra: letra, texto: p.alternativas[i],
331
- consequencia: p.consequencias[i] })),
332
- }));
333
- return (0, hitl_lote_1.textoDoLote)({ contrato: 'ork.hitl-lote/v1', perguntas, restantes: 0, recusadas: [], abertas: 0 }, { canal });
414
+ function reenviarLote(raiz, servido, numeros, canal, quando) {
415
+ const semRecomendada = [];
416
+ const perguntas = servido.perguntas.filter(p => numeros.includes(p.numero)).flatMap(p => {
417
+ // Registro gravado antes do RM-048 nao tem a recomendada: ela sai de novo do pedido servido.
418
+ // Sem ela, a pergunta nao e reenviada no contrato curto (que exige uma recomendada), e o
419
+ // nucleo nunca inventa uma: o dono recebe a linha para responder pelo numero.
420
+ const recomendada = p.recomendada ?? recomendadaDoRegistro(raiz, p);
421
+ if (!recomendada) {
422
+ semRecomendada.push(p.numero);
423
+ return [];
424
+ }
425
+ return [{
426
+ numero: p.numero, thread: p.thread, fase: p.fase, pergunta: p.pergunta, pedidoId: p.pedidoId,
427
+ ...(p.corpo ? { corpo: p.corpo } : {}), ...(p.ato ? { ato: p.ato } : {}), desde: p.desde ?? null,
428
+ alternativas: p.letras.map((letra, i) => ({ letra: letra, texto: p.alternativas[i],
429
+ consequencia: p.consequencias[i], ...(recomendada?.letra === letra ? { recomendada: { porque: recomendada.porque } } : {}) })),
430
+ }];
431
+ });
432
+ const aviso = semRecomendada.length
433
+ ? [`Pergunta${semRecomendada.length === 1 ? '' : 's'} ${semRecomendada.join(', ')}: responda pelo número e a letra, por exemplo ${semRecomendada[0]}a.`] : [];
434
+ if (!perguntas.length)
435
+ return aviso.join('\n') || 'Nenhuma pergunta para reenviar agora.';
436
+ return [(0, hitl_lote_1.textoDoLote)({ contrato: 'ork.hitl-lote/v1', perguntas, restantes: 0, recusadas: [], abertas: 0 }, { canal, quando }), ...aviso].join('\n');
437
+ }
438
+ /** A recomendada de uma pergunta servida, relida do pedido exato que saiu com o numero. */
439
+ function recomendadaDoRegistro(raiz, p) {
440
+ try {
441
+ const evento = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, p.thread))
442
+ .find(e => e.tipo === 'hitl_requested' && e.pedido?.id === p.pedidoId);
443
+ const pergunta = (0, hitl_lote_1.perguntaDoPedido)(evento.pedido, p.numero);
444
+ if ('motivo' in pergunta)
445
+ return undefined;
446
+ const r = pergunta.alternativas.find(a => a.recomendada);
447
+ return r ? { letra: r.letra, porque: r.recomendada.porque } : undefined;
448
+ }
449
+ catch {
450
+ return undefined;
451
+ }
452
+ }
453
+ // ---------------------------------------------------------------------------
454
+ // RM-048 (D4): a linha que o dono recebeu vale enquanto o gate esperar a mesma pergunta.
455
+ // ---------------------------------------------------------------------------
456
+ /**
457
+ * O pedido que recebe a resposta: o proprio, quando ainda esta aberto ou ja foi respondido (ai o
458
+ * caminho de sempre diz "repetida" ou "divergente"), ou o renovado, quando venceu sem resposta.
459
+ */
460
+ function pedidoParaResponder(raiz, pedido, quando) {
461
+ const respondido = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, pedido.thread))
462
+ .some(e => ['human_gate', 'session_answered'].includes(e.tipo) && e.pedidoId === pedido.id);
463
+ if (respondido || (0, hitl_contract_1.estadoDoPedido)(pedido, quando) === 'aberto')
464
+ return { vigente: pedido };
465
+ return { vigente: (0, hitl_gates_1.renovarPedidoDoGate)(raiz, pedido, quando), renovadoDe: pedido.id };
466
+ }
467
+ /**
468
+ * As threads abertas cujo ultimo pedido de gate com este codigo existe. Codigo que casa com mais
469
+ * de uma thread e devolvido inteiro: quem chama pergunta de volta, nunca escolhe (R3).
470
+ */
471
+ function pedidosComCodigo(raiz, codigo) {
472
+ const alvo = codigo.trim().toUpperCase(), achados = [];
473
+ for (const id of (0, thread_1.listarIds)(raiz)) {
474
+ try {
475
+ if ((0, thread_1.lerThread)(raiz, id).status === 'fechada')
476
+ continue;
477
+ const eventos = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, id));
478
+ const ultimo = eventos.filter(e => e.tipo === 'hitl_requested').map(e => e.pedido)
479
+ .filter(q => (0, hitl_contract_1.ehV2)(q) && q.classe === 'pergunta' && q.alvo.tipo === 'gate' && String(q.codigo).toUpperCase() === alvo).at(-1);
480
+ if (ultimo)
481
+ achados.push({ thread: id, pedido: ultimo, eventos });
482
+ }
483
+ catch { /* thread ilegivel nao responde por codigo */ }
484
+ }
485
+ return achados;
486
+ }
487
+ /** A1 do CHECK: os codigos curtos em uso agora (gates abertos e o resumo), para a nota nao repetir. */
488
+ function codigosEmUso(raiz, estadoDir) {
489
+ const codigos = [];
490
+ const estado = (() => { try {
491
+ return (0, pulse_consentimento_1.lerConsentimento)(raiz, estadoDir);
492
+ }
493
+ catch {
494
+ return undefined;
495
+ } })();
496
+ if (estado)
497
+ codigos.push(estado.pedido.codigo);
498
+ for (const id of (0, thread_1.listarIds)(raiz)) {
499
+ try {
500
+ if ((0, thread_1.lerThread)(raiz, id).status === 'fechada')
501
+ continue;
502
+ for (const e of (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, id))) {
503
+ const c = e.pedido?.codigo;
504
+ if (e.tipo === 'hitl_requested' && typeof c === 'string')
505
+ codigos.push(c);
506
+ }
507
+ }
508
+ catch { /* thread ilegivel nao contribui */ }
509
+ }
510
+ return codigos;
511
+ }
512
+ /** O bloco de evidencia a um pedido de distancia (item 4): artefato, claims, riscos e diff. */
513
+ function textoDoDetalhe(raiz, pedido, chave, canal) {
514
+ const a = (0, hitl_presentation_1.apresentarHitl)(raiz, pedido.thread, pedido.profundidade);
515
+ const tg = canal === 'telegram';
516
+ return [
517
+ `${tg ? '🔎 ' : ''}Evidências de ${chave} (${pedido.thread} · ${pedido.fase})`,
518
+ ...(a.artefato ? ['', 'Artefato:', a.artefato] : []),
519
+ '', 'Claims:', a.claims, '', 'Riscos:', a.riscos,
520
+ ...(a.diff ? ['', 'Diff:', a.diff] : []),
521
+ ].join('\n').slice(0, hitl_lote_1.TETO_DA_MENSAGEM);
522
+ }
523
+ /** As alternativas reais em uma linha, para devolver a pergunta sem chutar veredito. */
524
+ function opcoesReais(pedido, prefixo) {
525
+ return (0, hitl_contract_1.escolhasDoPedido)(pedido).map(o => `${prefixo}${(0, hitl_contract_1.chaveDaEscolha)(pedido, o.numero)} (${o.texto})`).join(', ');
526
+ }
527
+ /**
528
+ * "DE6H a": a resposta a um gate pelo codigo curto que o dono recebeu, sem numero de lote e sem
529
+ * identificador longo. A prova e a do endereco do pulse, a mesma do lote: o codigo esta DENTRO do
530
+ * texto assinado, e o rastro grava o que o nucleo traduziu. Devolve `undefined` quando nenhum gate
531
+ * aberto usa o codigo, para o chamador dizer isso com as palavras dele.
532
+ */
533
+ function responderPorCodigoDeGate(raiz, envelope, codigo, opcoes) {
534
+ const { quando, canal } = opcoes, tg = canal === 'telegram';
535
+ const achados = pedidosComCodigo(raiz, codigo);
536
+ if (!achados.length)
537
+ return undefined;
538
+ if (achados.length > 1) {
539
+ return resultado('codigo', `O código ${codigo} está em ${achados.length} threads (${achados.map(a => a.thread).join(', ')}). ` +
540
+ 'Responda pelo número da pergunta no lote, por exemplo 1a.', { recusas: [{ motivo: 'codigo.ambiguo' }] });
541
+ }
542
+ const { thread, pedido, eventos } = achados[0];
543
+ // A3 do CHECK: a MESMA mensagem entregue de novo pelo gateway e repetida, nunca renova nem grava.
544
+ const mesma = eventos.find(e => e.tipo === 'human_gate' && e.mensagem === envelope.mensagem);
545
+ if (mesma) {
546
+ return resultado('codigo', `${codigo}: esta mensagem já estava registrada (${String(mesma.estado)}).`, { repetida: true, registradas: [{ numero: 0, thread, letra: '', estado: String(mesma.estado), repetida: true }] });
547
+ }
548
+ const resto = envelope.resposta.trim().slice(codigo.length).trim();
549
+ if (/^(detalhes?|evid[eê]ncias?|\?)[.!]?$/i.test(resto))
550
+ return resultado('codigo', textoDoDetalhe(raiz, pedido, codigo, canal));
551
+ const anterior = eventos.find(e => e.tipo === 'human_gate' && e.pedidoId === pedido.id);
552
+ if (anterior && anterior.estado !== 'aguardando') {
553
+ return resultado('codigo', `${tg ? '⚠️ ' : '! '}${codigo} já foi respondido (${String(anterior.estado)}) às ` +
554
+ `${(0, horario_1.formatarHora)(anterior.ts, { agora: quando })}; a resposta não muda.`, { recusas: [{ motivo: 'ja-respondido' }] });
555
+ }
556
+ const trecho = (0, hitl_texto_livre_1.lerTrecho)(resto);
557
+ const r = trecho ? (0, hitl_texto_livre_1.resolverTrecho)(trecho, (0, hitl_contract_1.escolhasDoPedido)(pedido).map(o => o.acao), (0, hitl_contract_1.escolhasDoPedido)(pedido).map(o => (0, hitl_contract_1.chaveDaEscolha)(pedido, o.numero)))
558
+ : { tipo: 'nenhuma' };
559
+ if (r.tipo === 'detalhe')
560
+ return resultado('codigo', textoDoDetalhe(raiz, pedido, codigo, canal));
561
+ if (r.tipo !== 'escolha') {
562
+ return resultado('codigo', `${r.tipo === 'ambigua' ? `"${resto.slice(0, 40)}" serve para mais de uma alternativa de ${codigo}` : `Não entendi "${resto.slice(0, 40)}" para ${codigo}`}; ` +
563
+ `nada foi registrado. Responda com uma destas: ${opcoesReais(pedido, `${codigo} `)}.`, { recusas: [{ motivo: r.tipo === 'ambigua' ? 'ambigua' : 'nao-entendida' }] });
564
+ }
565
+ // D2: com mais de um pedido aberto na mesma thread, palavra nao registra; letra continua.
566
+ if (trecho.tipo === 'intencao') {
567
+ const abertos = pedidosAbertosDaThread(raiz, thread, quando);
568
+ if (abertos > 1) {
569
+ return resultado('codigo', `A thread de ${codigo} tem ${abertos} pedidos abertos; resposta por palavra não registra. ` +
570
+ `Responda com a letra: ${opcoesReais(pedido, `${codigo} `)}.`, { recusas: [{ motivo: 'varios-pedidos-na-thread' }] });
571
+ }
572
+ }
573
+ return registrarPorCodigo(raiz, envelope, { thread, pedido, codigo, indice: r.indice, quando, canal,
574
+ rastroExtra: trecho.tipo === 'intencao' ? { textoLivre: trecho.palavra } : {} });
575
+ }
576
+ /** Registra a escolha num pedido achado pelo codigo, renovando-o se venceu. Nao escolhe nada. */
577
+ function registrarPorCodigo(raiz, envelope, e) {
578
+ const tg = e.canal === 'telegram';
579
+ let vigente, renovadoDe;
580
+ try {
581
+ const anterior = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, e.thread)).find(x => x.tipo === 'human_gate' && x.pedidoId === e.pedido.id);
582
+ // Respondido com "continuar esperando", o gate ainda espera: a nova resposta vai ao renovado.
583
+ ({ vigente, renovadoDe } = anterior && anterior.estado === 'aguardando'
584
+ ? { vigente: (0, hitl_gates_1.renovarPedidoDoGate)(raiz, e.pedido, e.quando), renovadoDe: e.pedido.id }
585
+ : pedidoParaResponder(raiz, e.pedido, e.quando));
586
+ }
587
+ catch (erro) {
588
+ const m = erro.message;
589
+ if (m.includes('a pergunta mudou')) {
590
+ // A pergunta de agora e outra, com outro codigo: ela e aberta e mostrada, nunca respondida.
591
+ try {
592
+ const agora = (0, hitl_gates_1.abrirPedidoGate)(raiz, e.thread, (0, hitl_contract_1.motivoDoPedido)(e.pedido), e.quando);
593
+ const novo = agora.codigo;
594
+ const texto = typeof novo === 'string' ? (0, hitl_curto_1.textoDoPedidoCurto)((0, hitl_curto_1.montarPedidoCurto)((0, hitl_curto_1.entradaDoPedido)(agora), { quando: e.quando, responder: { tipo: 'codigo', codigo: novo } }), e.canal) : '';
595
+ return resultado('codigo', [`${tg ? '⚠️ ' : '! '}A pergunta de ${e.codigo} mudou desde que saiu; nada foi registrado. A de agora:`, '', texto].join('\n'), { recusas: [{ motivo: 'pergunta-mudou' }] });
596
+ }
597
+ catch { /* cai na frase de baixo */ }
598
+ }
599
+ return resultado('codigo', m.includes('o gate já foi respondido')
600
+ ? `${tg ? '⚠️ ' : '! '}${e.codigo} já foi respondido por outra linha; a resposta não muda.`
601
+ : `${tg ? '⚠️ ' : '! '}${e.codigo} já não espera você: a fase mudou desde que a pergunta saiu. Nada foi registrado.`, { recusas: [{ motivo: 'pedido-antigo' }] });
602
+ }
603
+ const chave = (0, hitl_contract_1.chaveDaEscolha)(vigente, e.indice + 1);
604
+ const derivada = { ...envelope, resposta: chave };
605
+ const assinado = { alvo: pulse_consentimento_1.ALVO_DO_PULSE, endereco: pulse_consentimento_1.ENDERECO_DA_RESPOSTA, envelope,
606
+ rastro: { contrato: exports.CONTRATO_RESPOSTA_DO_PULSE, codigo: e.codigo, letra: chave, respostaDoDonoSha256: sha(envelope.resposta),
607
+ ...(renovadoDe ? { renovadoDe } : {}), ...e.rastroExtra } };
608
+ try {
609
+ const r = (0, hitl_gates_1.responderGate)(raiz, e.thread, vigente.id, derivada, e.quando, assinado);
610
+ const o = (0, hitl_contract_1.escolhasDoPedido)(vigente)[e.indice];
611
+ const consequencia = (0, hitl_contract_1.ehV2)(vigente) && vigente.classe === 'pergunta' ? vigente.alternativas[e.indice]?.consequencia : undefined;
612
+ return resultado('codigo', `${tg ? '✅ ' : ''}${e.codigo} → ${chave}) ${o.texto}${consequencia ? `: ${consequencia}` : ''}.`, { registradas: [{ numero: 0, thread: e.thread, letra: chave, estado: r.estado, repetida: r.repetida }], repetida: r.repetida });
613
+ }
614
+ catch (erro) {
615
+ const m = erro.message;
616
+ const motivo = m.includes('ocupado') ? `a thread de ${e.codigo} estava ocupada; mande de novo em um minuto.`
617
+ : m.includes('proveniência') || m.includes('não autenticada') ? `não consegui provar a origem da resposta a tempo; mande ${e.codigo} ${chave} de novo.`
618
+ : m.includes('já respondido') ? `${e.codigo} já tinha resposta registrada; a resposta não muda.`
619
+ : m.includes('pedido antigo') ? `${e.codigo} já não espera você: a fase mudou desde que a pergunta saiu. Nada foi registrado.`
620
+ : `não consegui registrar ${e.codigo}: ${m.slice(0, 120)}`;
621
+ return resultado('codigo', `${tg ? '⚠️ ' : '! '}${motivo}`, { recusas: [{ motivo }] });
622
+ }
623
+ }
624
+ // ---------------------------------------------------------------------------
625
+ // RM-048 (item 2): o texto livre, quando e inequivoco.
626
+ // ---------------------------------------------------------------------------
627
+ /**
628
+ * Quantos pedidos (gate ou sessao) estao abertos agora nesta thread: sem resposta, dentro do
629
+ * prazo e no contexto corrente. D2: com mais de um, palavra nao registra.
630
+ */
631
+ function pedidosAbertosDaThread(raiz, thread, quando) {
632
+ const t = (0, thread_1.lerThread)(raiz, thread), eventos = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, thread));
633
+ const contexto = (0, hitl_gates_1.contextoHitlDosEventos)(t, eventos);
634
+ const respondidos = new Set(eventos.filter(e => ['human_gate', 'session_answered'].includes(e.tipo)).map(e => String(e.pedidoId)));
635
+ const abertos = new Set();
636
+ for (const e of eventos) {
637
+ if (e.tipo !== 'hitl_requested' || e.contexto !== contexto)
638
+ continue;
639
+ const q = e.pedido;
640
+ try {
641
+ if (!(0, hitl_contract_1.alvoDoPedido)(q) || respondidos.has(q.id) || (0, hitl_contract_1.estadoDoPedido)(q, quando) !== 'aberto')
642
+ continue;
643
+ abertos.add(q.id);
644
+ }
645
+ catch { /* pedido invalido nao conta */ }
646
+ }
647
+ return abertos.size;
648
+ }
649
+ /** O pedido exato que saiu com o numero, relido do ledger. */
650
+ function pedidoDaServida(raiz, p) {
651
+ try {
652
+ const evento = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, p.thread))
653
+ .find(e => e.tipo === 'hitl_requested' && e.pedido?.id === p.pedidoId);
654
+ (0, hitl_contract_1.validarPedidoHitl)(evento?.pedido);
655
+ return evento.pedido;
656
+ }
657
+ catch {
658
+ return undefined;
659
+ }
660
+ }
661
+ /**
662
+ * Traduz uma palavra numa letra para a pergunta servida, ou devolve por que nao da. Nunca escolhe
663
+ * pelo dono: ambigua, fora do vocabulario ou com mais de um pedido aberto na thread volta como
664
+ * pergunta, com as letras reais.
665
+ */
666
+ function letraDaPalavra(raiz, p, trecho, quando) {
667
+ const pedido = pedidoDaServida(raiz, p);
668
+ if (!pedido)
669
+ return { recusa: `não consegui ler a pergunta ${p.numero}; responda com o número e a letra.` };
670
+ const acoes = (0, hitl_contract_1.escolhasDoPedido)(pedido).map(o => o.acao);
671
+ const r = (0, hitl_texto_livre_1.resolverTrecho)(trecho, acoes, p.letras);
672
+ const reais = p.letras.map((l, i) => `${p.numero}${l} (${p.alternativas[i]})`).join(', ');
673
+ if (r.tipo === 'detalhe')
674
+ return { detalhe: pedido };
675
+ if (r.tipo === 'ambigua')
676
+ return { recusa: `para a pergunta ${p.numero}, "${trecho.tipo === 'intencao' ? trecho.palavra : ''}" serve para mais de uma alternativa; mande uma: ${reais}.` };
677
+ if (r.tipo === 'nenhuma')
678
+ return { recusa: `a pergunta ${p.numero} não tem alternativa para essa resposta; mande uma destas: ${reais}.` };
679
+ if (trecho.tipo === 'intencao') {
680
+ const abertos = pedidosAbertosDaThread(raiz, p.thread, quando);
681
+ if (abertos > 1)
682
+ return { recusa: `a thread da pergunta ${p.numero} tem ${abertos} pedidos abertos; resposta por palavra não registra. Mande a letra: ${reais}.` };
683
+ return { letra: p.letras[r.indice], palavra: trecho.palavra };
684
+ }
685
+ return { letra: p.letras[r.indice] };
686
+ }
687
+ /** "1. B, 2. aprovo, 3 detalhes": cada item vira letra, detalhe ou recusa, e as letras seguem o lote. */
688
+ function responderALista(raiz, envelope, itens, opcoes) {
689
+ const { quando, canal, estadoDir } = opcoes, tg = canal === 'telegram';
690
+ const servido = lerLoteServido(raiz, estadoDir);
691
+ const escolhas = [], avisos = [], detalhes = [];
692
+ for (const { numero, trecho } of itens) {
693
+ const p = servido.perguntas.find(q => q.numero === numero);
694
+ if (trecho.tipo === 'letra' || !p) {
695
+ escolhas.push({ numero, letra: trecho.tipo === 'letra' ? trecho.letra : '?' });
696
+ continue;
697
+ }
698
+ const r = letraDaPalavra(raiz, p, trecho, quando);
699
+ if ('detalhe' in r)
700
+ detalhes.push(textoDoDetalhe(raiz, r.detalhe, String(numero), canal));
701
+ else if ('recusa' in r)
702
+ avisos.push(`${tg ? '⚠️ ' : '! '}${r.recusa}`);
703
+ else
704
+ escolhas.push({ numero, letra: r.letra, ...(r.palavra ? { palavra: r.palavra } : {}) });
705
+ }
706
+ if (!escolhas.length) {
707
+ return resultado('lote', [...avisos, ...detalhes].join('\n\n') || 'Nada para registrar.', { recusas: avisos.map(motivo => ({ motivo })) });
708
+ }
709
+ const r = responderAsPerguntas(raiz, envelope, escolhas, { quando, canal, estadoDir, avisos });
710
+ return detalhes.length ? { ...r, mensagem: [r.mensagem, ...detalhes].join('\n\n').slice(0, hitl_lote_1.TETO_DA_MENSAGEM) } : r;
711
+ }
712
+ function abertosDoDono(raiz, quando, estadoDir) {
713
+ const janela = hitl_texto_livre_1.JANELA_DO_TEXTO_LIVRE_MIN * 60000, agora = Date.parse(quando);
714
+ const abertos = [];
715
+ const estado = (0, pulse_consentimento_1.lerConsentimento)(raiz, estadoDir);
716
+ if (estado && !estado.respondido && estado.pedido.candidatos.length) {
717
+ abertos.push({ tipo: 'consentimento', codigo: estado.pedido.codigo, rotulo: `${estado.pedido.codigo} a (receber as perguntas)`,
718
+ recente: agora - Date.parse(estado.pedido.criadoEm) < janela });
719
+ }
720
+ // A mesma pergunta (mesma thread e mesmo alvo) conta uma vez, servida ou nao, vencida ou nao.
721
+ const chave = (q) => { const a = (0, hitl_contract_1.alvoDoPedido)(q); return `${q.thread}|${a?.tipo === 'session' ? `s:${a.sessionId}` : `g:${q.fase}`}`; };
722
+ // Os gates que o resumo aberto oferece sao o que o "sim" pede: o consentimento os representa.
723
+ const vistas = new Set(estado && !estado.respondido
724
+ ? estado.pedido.candidatos.map(c => `${c.thread}|g:${c.fase ?? ''}`) : []);
725
+ for (const p of lerLoteServido(raiz, estadoDir).perguntas.filter(q => perguntaEmAberto(q, quando))) {
726
+ const pedido = pedidoDaServida(raiz, p);
727
+ if (pedido)
728
+ vistas.add(chave(pedido));
729
+ abertos.push({ tipo: 'pergunta', servida: p, rotulo: `${p.numero} com a letra (${[p.thread, p.fase].filter(Boolean).join(' · ')})`,
730
+ recente: agora - Date.parse(p.servidaEm) < janela });
731
+ }
732
+ for (const id of (0, thread_1.listarIds)(raiz)) {
733
+ try {
734
+ const t = (0, thread_1.lerThread)(raiz, id);
735
+ if (t.status === 'fechada')
736
+ continue;
737
+ const eventos = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, id));
738
+ const contexto = (0, hitl_gates_1.contextoHitlDosEventos)(t, eventos);
739
+ const respondidos = new Set(eventos.filter(e => ['human_gate', 'session_answered'].includes(e.tipo)).map(e => String(e.pedidoId)));
740
+ for (const e of eventos) {
741
+ if (e.tipo !== 'hitl_requested' || e.contexto !== contexto)
742
+ continue;
743
+ const q = e.pedido;
744
+ try {
745
+ if (!(0, hitl_contract_1.alvoDoPedido)(q) || respondidos.has(q.id) || (0, hitl_contract_1.estadoDoPedido)(q, quando) !== 'aberto' || (0, hitl_classificacao_1.quemDecide)((0, hitl_contract_1.motivoDoPedido)(q)) !== 'dono')
746
+ continue;
747
+ }
748
+ catch {
749
+ continue;
750
+ }
751
+ if (vistas.has(chave(q)))
752
+ continue;
753
+ vistas.add(chave(q));
754
+ const codigo = (0, hitl_contract_1.ehV2)(q) && q.classe === 'pergunta' ? q.codigo : undefined;
755
+ abertos.push({ tipo: 'pedido', recente: false, ...(codigo ? { codigo } : {}),
756
+ rotulo: codigo ? `${codigo} a (${q.thread} · ${q.fase})` : `${q.thread} · ${q.fase}` });
757
+ }
758
+ // Segunda passada do CHECK: o gate cujo pedido venceu continua esperando o dono, e a linha
759
+ // dele ("DE6H a") nao vence (D4). Ele conta mesmo sem pedido aberto agora.
760
+ const gate = `${id}|g:${t.faseAtual}`;
761
+ if (!vistas.has(gate)) {
762
+ const espera = (0, roadmap_status_1.esperaDoDono)(raiz, t, quando);
763
+ if (espera && !abertos.some(a => a.servida?.thread === id)) {
764
+ vistas.add(gate);
765
+ abertos.push({ tipo: 'pedido', recente: false, ...(espera.codigo ? { codigo: espera.codigo } : {}),
766
+ rotulo: espera.codigo ? `${espera.codigo} a (${id} · ${t.faseAtual})` : `${id} · ${t.faseAtual}` });
767
+ }
768
+ }
769
+ }
770
+ catch { /* thread ilegivel nao entra na conta; a palavra solta so fica mais cautelosa */ }
771
+ }
772
+ return abertos;
773
+ }
774
+ /**
775
+ * "aprovo", "sim", "a", "1": a palavra solta. Registra quando ha UMA pergunta recente e ela nao e
776
+ * sem volta; senao devolve a pergunta com as opcoes reais. A prova ja foi conferida por quem chama.
777
+ */
778
+ function responderLivre(raiz, envelope, trecho, opcoes) {
779
+ const { quando, canal, estadoDir } = opcoes, tg = canal === 'telegram';
780
+ const abertos = abertosDoDono(raiz, quando, estadoDir);
781
+ const volta = (mensagem, motivo) => resultado('nao-entendida', mensagem, { recusas: [{ motivo }] });
782
+ if (!abertos.length) {
783
+ return volta('Não há pergunta aberta para responder só com uma palavra; nada foi registrado. ' +
784
+ 'Responda com o número e a letra (por exemplo 1a) ou com o código da pergunta (por exemplo DE6H a).', 'sem-pergunta-recente');
785
+ }
786
+ if (abertos.length > 1) {
787
+ const lista = abertos.slice(0, 5).map(a => a.rotulo).join('; ') + (abertos.length > 5 ? `; e mais ${abertos.length - 5}` : '');
788
+ return volta(`Há ${abertos.length} perguntas esperando você, e uma palavra só não diz qual; nada foi registrado. ` +
789
+ `Responda pelo número ou pelo código: ${lista}.`, 'varias-perguntas');
790
+ }
791
+ const [unico] = abertos;
792
+ if (!unico.recente) {
793
+ return volta(`A pergunta que espera você não acabou de sair por aqui; palavra solta não registra. Responda: ${unico.rotulo}.`, 'sem-pergunta-recente');
794
+ }
795
+ if (unico.tipo === 'consentimento') {
796
+ // O resumo pergunta sim ou nao: a palavra vira a letra, e o caminho e o do codigo, com a prova de sempre.
797
+ return responderAoResumo(raiz, envelope, unico.codigo, { quando, canal, estadoDir, palavra: trecho });
798
+ }
799
+ const p = unico.servida;
800
+ const pedido = pedidoDaServida(raiz, p);
801
+ if (pedido && (0, hitl_contract_1.ehV2)(pedido) && pedido.classe === 'pergunta' && pedido.irreversivel) {
802
+ return volta(`${tg ? '🔒 ' : '! '}A pergunta ${p.numero} é sem volta; palavra solta não registra. Responda com o número e a letra: ` +
803
+ `${p.letras.map((l, i) => `${p.numero}${l} (${p.alternativas[i]})`).join(', ')}.`, 'irreversivel');
804
+ }
805
+ // D2: a resposta solta nao diz a qual pedido se refere; com mais de um aberto na thread, nem
806
+ // letra nem digito soltos registram. Numerados ("1a") e por codigo ("DE6H a") continuam valendo.
807
+ const naThread = pedidosAbertosDaThread(raiz, p.thread, quando);
808
+ if (naThread > 1) {
809
+ return volta(`${tg ? '⚠️ ' : '! '}a thread da pergunta ${p.numero} tem ${naThread} pedidos abertos; resposta solta não registra. ` +
810
+ `Mande o número e a letra: ${p.letras.map((l, i) => `${p.numero}${l} (${p.alternativas[i]})`).join(', ')}.`, 'varios-pedidos-na-thread');
811
+ }
812
+ const r = letraDaPalavra(raiz, p, trecho, quando);
813
+ if ('detalhe' in r)
814
+ return resultado('lote', textoDoDetalhe(raiz, r.detalhe, String(p.numero), canal));
815
+ if ('recusa' in r)
816
+ return volta(`${tg ? '⚠️ ' : '! '}${r.recusa}`, 'nao-registrou');
817
+ return responderAsPerguntas(raiz, envelope, [{ numero: p.numero, letra: r.letra, palavra: r.palavra ?? normalizarTrecho(trecho) }], { quando, canal, estadoDir });
818
+ }
819
+ const normalizarTrecho = (t) => t.tipo === 'letra' ? t.letra : t.tipo === 'digito' ? String(t.numero) : t.palavra;
820
+ exports.CONTRATO_ESCUTA = 'ork.pulse-escuta/v1';
821
+ function arquivoDaEscuta(raiz, estadoDir) {
822
+ return path.join(estadoDir ?? path.join((0, estado_thread_1.raizDoEstado)(raiz), '.orkastery', 'monitor'), 'pulse-escuta.json');
823
+ }
824
+ /**
825
+ * D3: a janela da palavra solta, para o adaptador decidir a ROTA de "sim", "ok", "a". Nao e prova:
826
+ * a mensagem interceptada ainda passa pela assinatura do ingresso e por esta mesma regra no nucleo.
827
+ * Janela fechada (ou arquivo ausente), a palavra vai ao assistente, como sempre foi.
828
+ */
829
+ function atualizarEscuta(raiz, quando, estadoDir) {
830
+ const janela = hitl_texto_livre_1.JANELA_DO_TEXTO_LIVRE_MIN * 60000;
831
+ const limites = [];
832
+ const estado = (0, pulse_consentimento_1.lerConsentimento)(raiz, estadoDir);
833
+ if (estado && !estado.respondido && estado.pedido.candidatos.length)
834
+ limites.push(Date.parse(estado.pedido.criadoEm) + janela);
835
+ for (const p of lerLoteServido(raiz, estadoDir).perguntas)
836
+ if (perguntaEmAberto(p, quando))
837
+ limites.push(Date.parse(p.servidaEm) + janela);
838
+ const ate = limites.filter(l => l > Date.parse(quando)).sort((a, b) => b - a)[0];
839
+ const escuta = { contrato: exports.CONTRATO_ESCUTA, livreAte: ate ? new Date(ate).toISOString() : null, atualizadoEm: quando };
840
+ const arquivo = arquivoDaEscuta(raiz, estadoDir);
841
+ fs.mkdirSync(path.dirname(arquivo), { recursive: true });
842
+ const tmp = `${arquivo}.${process.pid}.tmp`;
843
+ fs.writeFileSync(tmp, JSON.stringify(escuta) + '\n', { mode: 0o600 });
844
+ fs.renameSync(tmp, arquivo);
845
+ return { livreAte: escuta.livreAte };
846
+ }
847
+ // ---------------------------------------------------------------------------
848
+ // RM-048 (item 8): a nota do MASTER e a ratificacao, pelo mesmo endereco assinado.
849
+ // ---------------------------------------------------------------------------
850
+ /**
851
+ * "K7QX 4 entregou o que pedi": a nota vai ao ledger com `por` = o remetente autenticado e com o
852
+ * recibo do canal. Quem chama ja conferiu a prova; aqui so se le o conteudo. Nota humana que ja
853
+ * existe nao e trocada por este caminho; a aceita por omissao e, porque a nota do dono sobrescreve.
854
+ */
855
+ function responderNota(raiz, envelope, p, opcoes) {
856
+ const tg = opcoes.canal === 'telegram';
857
+ const nota = (0, master_nota_1.lerNota)(envelope.resposta.trim().slice(p.codigo.length));
858
+ if (!nota) {
859
+ return resultado('nota', `Para dar a nota de ${p.thread}, responda ${p.codigo} <0 a 5> <porquê>, por exemplo: ` +
860
+ `${p.codigo} 4 entregou o que pedi. Nada foi registrado.`, { recusas: [{ motivo: 'nota.formato' }] });
861
+ }
862
+ if ((0, master_nota_1.mensagemJaUsadaEmNota)(raiz, p.thread, envelope.mensagem)) {
863
+ return resultado('nota', `A nota de ${p.thread} desta mensagem já estava registrada.`, { repetida: true });
864
+ }
865
+ try {
866
+ // A4 do CHECK: ler a nota que existe e gravar a nova sob o lock da thread, para uma nota humana
867
+ // dada no mesmo instante por outro caminho nao ser sobrescrita.
868
+ const ja = (0, hitl_gates_1.comLockHitl)(raiz, p.thread, () => {
869
+ const t = (0, thread_1.lerThread)(raiz, p.thread);
870
+ if (t.score && t.score.regime !== 'omissao' && (0, master_1.autoriaHumana)(t.score.avaliadoPor))
871
+ return t.score.valor;
872
+ const prova = (0, master_nota_1.reciboDoCanal)(raiz, envelope, { notaPedida: { contrato: master_nota_1.CONTRATO_PEDIDO_DE_NOTA, pedidoId: p.pedidoId, codigo: p.codigo },
873
+ enderecoAssinado: { alvo: pulse_consentimento_1.ALVO_DO_PULSE, endereco: pulse_consentimento_1.ENDERECO_DA_RESPOSTA, contrato: exports.CONTRATO_RESPOSTA_DO_PULSE, codigo: p.codigo,
874
+ respostaDoDonoSha256: sha(envelope.resposta) } }, opcoes.estadoDir);
875
+ (0, master_1.registrarMaster)(raiz, p.thread, { score: nota.score, justificativa: nota.justificativa, por: envelope.por, refazer: !!t.score, prova });
876
+ return undefined;
877
+ });
878
+ if (ja !== undefined) {
879
+ return resultado('nota', `${tg ? '⚠️ ' : '! '}${p.thread} já tem nota humana ${ja}/5; a nota não muda por aqui.`, { recusas: [{ motivo: 'nota.ja-dada' }] });
880
+ }
881
+ return resultado('nota', `${tg ? '🧾 ' : ''}Nota ${nota.score}/5 registrada para ${p.thread}, por ${envelope.por}. ` +
882
+ 'O índice derivado do ledger continua ao lado.', { registradas: [{ numero: 0, thread: p.thread, letra: String(nota.score), estado: 'nota', repetida: false }] });
883
+ }
884
+ catch (e) {
885
+ return resultado('nota', `${tg ? '⚠️ ' : '! '}não consegui registrar a nota de ${p.thread}: ${e.message.slice(0, 160)}`, { recusas: [{ motivo: 'nota.recusada' }] });
886
+ }
887
+ }
888
+ /** O teclado do digest: a ratificacao da proposta, com o remetente autenticado e o recibo. */
889
+ function responderRatificacao(raiz, envelope, texto, opcoes) {
890
+ const tg = opcoes.canal === 'telegram';
891
+ const partes = texto.split(/\s+/);
892
+ const prova = () => (0, master_nota_1.reciboDoCanal)(raiz, envelope, { ratificacao: { contrato: master_nota_1.CONTRATO_PEDIDO_DE_NOTA, texto },
893
+ enderecoAssinado: { alvo: pulse_consentimento_1.ALVO_DO_PULSE, endereco: pulse_consentimento_1.ENDERECO_DA_RESPOSTA, contrato: exports.CONTRATO_RESPOSTA_DO_PULSE,
894
+ respostaDoDonoSha256: sha(envelope.resposta) } }, opcoes.estadoDir);
895
+ try {
896
+ let feitas;
897
+ if (partes[0].toLowerCase() === 'ratificar-lote' && partes.length === 3) {
898
+ feitas = (0, master_digest_1.responderLoteDigest)(raiz, ['ratificar-lote', partes[1], partes[2]].join(' '), envelope.por, prova())
899
+ .map(r => ({ thread: r.thread.id, score: r.masterLog.score }));
900
+ }
901
+ else {
902
+ const classe = (0, master_1.parseClasse)(partes[3]);
903
+ if (partes[0].toLowerCase() !== 'ratificar' || partes.length !== 4 || !classe) {
904
+ return resultado('nota', 'Para ratificar, use a linha do teclado do digest: ratificar <thread> <assinatura> <classe>.', { recusas: [{ motivo: 'ratificacao.formato' }] });
905
+ }
906
+ feitas = (0, master_batch_1.ratificarBatch)(raiz, [{ thread: partes[1], assinatura: partes[2], classe }], envelope.por, prova())
907
+ .map(r => ({ thread: r.thread.id, score: r.masterLog.score }));
908
+ }
909
+ return resultado('nota', `${tg ? '🧾 ' : ''}Ratificado por ${envelope.por}: ${feitas.map(f => `${f.thread} ${f.score}/5`).join(', ')}.`, { registradas: feitas.map(f => ({ numero: 0, thread: f.thread, letra: String(f.score), estado: 'nota', repetida: false })) });
910
+ }
911
+ catch (e) {
912
+ return resultado('nota', `${tg ? '⚠️ ' : '! '}não consegui ratificar: ${e.message.slice(0, 160)}`, { recusas: [{ motivo: 'ratificacao.recusada' }] });
913
+ }
334
914
  }
335
915
  const EXEMPLO = 'Responda ao resumo com o código e a letra, por exemplo: P4EJ a. Às perguntas, com o número e a letra, por exemplo: 1a 2c. ' +
336
916
  'Para trocar a cadência, mande só a tag: #OrkPulseOn, #OrkPulseOn-15m, -30m, -60m ou #OrkPulseOff.';
@@ -361,45 +941,73 @@ function responderPeloPulse(raiz, envelope, opcoes = {}) {
361
941
  }
362
942
  // A conversa do pulse tem dois escritores (a varredura e este receptor): um de cada vez.
363
943
  const estadoDir = opcoes.estadoDir ?? path.join((0, estado_thread_1.raizDoEstado)(raiz), '.orkastery', 'monitor');
364
- return (0, monitor_lock_1.comLockDaConversa)(estadoDir, () => lido.forma === 'lote'
365
- ? responderAsPerguntas(raiz, envelope, lido.escolhas, { quando, canal, estadoDir })
366
- : responderAoResumo(raiz, envelope, lido.codigo, { quando, canal, estadoDir }));
944
+ return (0, monitor_lock_1.comLockDaConversa)(estadoDir, () => {
945
+ const r = (() => {
946
+ try {
947
+ return lido.forma === 'lote' ? responderAsPerguntas(raiz, envelope, lido.escolhas, { quando, canal, estadoDir })
948
+ : lido.forma === 'lista' ? responderALista(raiz, envelope, lido.itens, { quando, canal, estadoDir })
949
+ : lido.forma === 'livre' ? responderLivre(raiz, envelope, lido.trecho, { quando, canal, estadoDir })
950
+ : lido.forma === 'ratificacao' ? responderRatificacao(raiz, envelope, lido.texto, { canal, estadoDir })
951
+ : responderAoResumo(raiz, envelope, lido.codigo, { quando, canal, estadoDir });
952
+ }
953
+ catch (e) {
954
+ // S3 do CHECK: estado ilegivel ou falha inesperada vira resposta util, nunca silencio nem erro cru.
955
+ // O detalhe fica no stderr de quem chamou (o adaptador nao o repassa): caminho local e
956
+ // mensagem interna nao vao ao Telegram.
957
+ console.error(`pulse responder: ${e.message}`);
958
+ return resultado('nao-entendida', 'Não consegui concluir esta resposta agora (erro interno do núcleo). ' +
959
+ 'Confira em ork pulse e mande de novo em um minuto.', { recusas: [{ motivo: 'erro-interno' }] });
960
+ }
961
+ })();
962
+ // RM-048 (D3): a janela da palavra solta acompanha o que ainda espera o dono agora.
963
+ try {
964
+ atualizarEscuta(raiz, quando, estadoDir);
965
+ }
966
+ catch { /* dica de rota; a prova nao depende dela */ }
967
+ return r;
968
+ });
367
969
  }
368
970
  function responderAoResumo(raiz, envelope, codigo, opcoes) {
369
971
  const { quando, canal, estadoDir } = opcoes;
370
972
  const estado = (0, pulse_consentimento_1.lerConsentimento)(raiz, estadoDir);
371
- if (!estado)
372
- return resultado('consentimento', 'Não há resumo esperando resposta agora. Quando houver pergunta para você, o resumo avisa.');
373
- if (estado.pedido.codigo !== codigo) {
973
+ if (!estado || estado.pedido.codigo !== codigo) {
374
974
  const servido = lerLoteServido(raiz, estadoDir);
375
975
  const numeros = servido.porCodigo[codigo];
376
976
  // Sim repetido a um codigo que ja serviu lote: devolve o MESMO lote, nunca um novo.
377
977
  if (numeros?.length) {
378
978
  // O dono pediu de novo, por outra mensagem: ele recebe o mesmo lote, e nao silencio.
379
- const aberto = !estado.respondido && Date.parse(quando) < Date.parse(estado.pedido.prazo) && estado.pedido.candidatos.length
380
- ? estado.pedido : undefined;
381
- return resultado('consentimento', loteDeNovo(servido, numeros, codigo, quando, canal, aberto), { resposta: 'sim', repetida: false });
979
+ // RM-048 (D5): o resumo mais recente nao vence para o sim; o codigo dele continua valendo.
980
+ const aberto = estado && !estado.respondido && estado.pedido.candidatos.length ? estado.pedido : undefined;
981
+ return resultado('consentimento', loteDeNovo(raiz, servido, numeros, codigo, quando, canal, aberto), { resposta: 'sim', repetida: false });
382
982
  }
383
- return resultado('consentimento', `O código ${codigo} não é o do resumo aberto. O resumo aberto usa ${estado.pedido.codigo}: ` +
384
- `responda ${estado.pedido.codigo} a para receber as perguntas.`);
983
+ // RM-048 (item 8): o codigo de um pedido de nota do MASTER ("K7QX 4 entregou o que pedi").
984
+ // A1 do CHECK: se o mesmo codigo tambem e de um gate aberto, nada e escolhido pelo dono.
985
+ const nota = (0, master_nota_1.pedidoDeNotaDoCodigo)(raiz, codigo, estadoDir);
986
+ if (nota && pedidosComCodigo(raiz, codigo).length) {
987
+ return resultado('codigo', `O código ${codigo} é de uma nota e de uma pergunta ao mesmo tempo; nada foi registrado. ` +
988
+ 'Responda a pergunta pelo número do lote e peça outro código de nota (ork master pedir).', { recusas: [{ motivo: 'codigo.ambiguo' }] });
989
+ }
990
+ // RM-048 (D4): o codigo curto de um gate ("DE6H a") responde ao gate, pelo mesmo endereco assinado.
991
+ const peloGate = responderPorCodigoDeGate(raiz, envelope, codigo, { quando, canal });
992
+ if (peloGate)
993
+ return peloGate;
994
+ if (nota)
995
+ return responderNota(raiz, envelope, nota, { canal, estadoDir });
996
+ if (!estado)
997
+ return resultado('consentimento', `Não reconheço o código ${codigo} agora: nenhum resumo ou pergunta aberta usa ele. ` +
998
+ 'Quando houver pergunta para você, o resumo avisa.');
999
+ return resultado('consentimento', `O código ${codigo} não é o do resumo aberto nem de uma pergunta aberta. O resumo aberto usa ` +
1000
+ `${estado.pedido.codigo}: responda ${estado.pedido.codigo} a para receber as perguntas.`);
385
1001
  }
386
1002
  let r;
387
1003
  try {
388
- r = (0, pulse_consentimento_1.responderConsentimento)(raiz, { codigo, envelope, quando, estadoDir });
1004
+ // RM-048 (D5): o sim ao resumo mais recente vale depois do prazo; o lote reconfere cada gate.
1005
+ r = (0, pulse_consentimento_1.responderConsentimento)(raiz, { codigo, envelope, quando, estadoDir, aceitarVencido: true });
389
1006
  }
390
1007
  catch (e) {
391
1008
  const m = e.message;
392
1009
  if (!m.startsWith('consentimento do pulse: '))
393
1010
  throw e;
394
- if (m.includes('vencido')) {
395
- // Vencido nao autoriza, e o dono nao fica sem caminho: o mesmo conjunto ganha codigo novo.
396
- if (!estado.pedido.candidatos.length)
397
- return resultado('consentimento', `Este resumo venceu às ${(0, horario_1.formatarHora)(estado.pedido.prazo, { agora: quando })}.`);
398
- const novo = (0, pulse_consentimento_1.abrirConsentimento)(raiz, { quando, resumoSha256: estado.pedido.resumoSha256,
399
- candidatos: estado.pedido.candidatos, prazoMin: pulse_consentimento_1.PRAZO_PADRAO_MIN, estadoDir });
400
- return resultado('consentimento', `Este resumo venceu às ${(0, horario_1.formatarHora)(estado.pedido.prazo, { agora: quando })}. ` +
401
- `Para receber as perguntas agora, responda ${novo.codigo} a.`);
402
- }
403
1011
  if (m.includes('divergente')) {
404
1012
  const antes = estado.respondido;
405
1013
  return resultado('consentimento', antes.resposta === 'sim'
@@ -417,7 +1025,7 @@ function responderAoResumo(raiz, envelope, codigo, opcoes) {
417
1025
  `As ${n} perguntas continuam guardadas`}, e o próximo resumo lembra ${n === 1 ? 'dela' : 'delas'}.`, { resposta: 'nao', repetida: mesmaMensagem });
418
1026
  }
419
1027
  if (r.repetida && r.respondido.numeros) {
420
- return resultado('consentimento', loteDeNovo(lerLoteServido(raiz, estadoDir), r.respondido.numeros, codigo, quando, canal), { resposta: 'sim', repetida: mesmaMensagem });
1028
+ return resultado('consentimento', loteDeNovo(raiz, lerLoteServido(raiz, estadoDir), r.respondido.numeros, codigo, quando, canal), { resposta: 'sim', repetida: mesmaMensagem });
421
1029
  }
422
1030
  const entregue = servirLote(raiz, { pedido: r.pedido, quando, canal, estadoDir });
423
1031
  marcarLoteEntregueSeAindaFor(raiz, quando, estadoDir, r.pedido.id, entregue.numeros);
@@ -438,8 +1046,15 @@ function motivoParaODono(p, letra, erro, quando) {
438
1046
  const m = erro.message, n = p.numero;
439
1047
  if (m.includes('expirado'))
440
1048
  return `a pergunta ${n} venceu às ${(0, horario_1.formatarHora)(p.prazo, { agora: quando })}; o próximo resumo a traz de novo se ela ainda esperar você.`;
1049
+ if (m.includes('o gate já foi respondido'))
1050
+ return `a pergunta ${n} já foi respondida por outra linha; a resposta não muda.`;
1051
+ if (m.includes('a pergunta mudou'))
1052
+ return `a pergunta ${n} mudou desde que saiu; nada foi registrado, e ela volta no próximo resumo com o texto de agora.`;
441
1053
  if (m.includes('pedido antigo'))
442
1054
  return `a pergunta ${n} já não espera você: a fase mudou desde que ela saiu.`;
1055
+ // RM-048 (D4): vencida, a renovacao so acontece se o gate ainda espera; senao, ela e historia.
1056
+ if (/pausa humana nesta fase|conclusão da fase|escalação tipada não comprovada/.test(m))
1057
+ return `a pergunta ${n} já não espera você.`;
443
1058
  if (m.includes('já respondido'))
444
1059
  return `a pergunta ${n} já tinha resposta registrada; a resposta não muda.`;
445
1060
  if (m.includes('ocupado'))
@@ -462,7 +1077,7 @@ function responderAsPerguntas(raiz, envelope, escolhas, opcoes) {
462
1077
  for (const e of escolhas)
463
1078
  porNumero.set(e.numero, new Set([...(porNumero.get(e.numero) ?? []), e.letra]));
464
1079
  const vistos = new Set();
465
- for (const { numero, letra } of escolhas) {
1080
+ for (const { numero, letra, palavra } of escolhas) {
466
1081
  if (vistos.has(numero))
467
1082
  continue;
468
1083
  vistos.add(numero);
@@ -475,7 +1090,7 @@ function responderAsPerguntas(raiz, envelope, escolhas, opcoes) {
475
1090
  }
476
1091
  const p = servido.perguntas.find(q => q.numero === numero);
477
1092
  if (!p) {
478
- const abertas = servido.perguntas.filter(q => perguntaViva(q, quando)).map(q => q.numero);
1093
+ const abertas = servido.perguntas.filter(q => perguntaEmAberto(q, quando)).map(q => q.numero);
479
1094
  const motivo = `não há pergunta ${numero} ${abertas.length ? `aberta; as abertas são ${abertas.join(', ')}` : 'aberta agora'}.`;
480
1095
  recusas.push({ numero, motivo });
481
1096
  linhas.push(`${telegram ? '⚠️ ' : '! '}${motivo}`);
@@ -488,6 +1103,12 @@ function responderAsPerguntas(raiz, envelope, escolhas, opcoes) {
488
1103
  linhas.push(`${telegram ? '⚠️ ' : '! '}${motivo}`);
489
1104
  continue;
490
1105
  }
1106
+ if (!p.respondida && !perguntaEmAberto(p, quando)) {
1107
+ const motivo = `a pergunta ${numero} saiu há mais de 24 h; ela volta no próximo resumo se ainda esperar você.`;
1108
+ recusas.push({ numero, motivo });
1109
+ linhas.push(`${telegram ? '⚠️ ' : '! '}${motivo}`);
1110
+ continue;
1111
+ }
491
1112
  if (p.respondida) {
492
1113
  if (p.respondida.letra === letra) {
493
1114
  // Repetida para o canal so quando e a MESMA mensagem; o dono que manda de novo e respondido.
@@ -509,10 +1130,14 @@ function responderAsPerguntas(raiz, envelope, escolhas, opcoes) {
509
1130
  // O numero aponta para o pedido que saiu, byte a byte. Pedido trocado nao recebe a letra.
510
1131
  if (sha(JSON.stringify(pedido)) !== p.pedidoSha256)
511
1132
  throw new Error('pedido antigo: fase, modo ou sessão mudou');
512
- const derivada = { ...envelope, resposta: (0, hitl_contract_1.chaveDaEscolha)(pedido, indice + 1) };
1133
+ // RM-048 (D4): vencido e sem resposta, o numero vai ao pedido renovado, e so quando a
1134
+ // pergunta e o contexto sao os mesmos. Respondido, segue o caminho de sempre (repetida).
1135
+ const { vigente, renovadoDe } = pedidoParaResponder(raiz, pedido, quando);
1136
+ const derivada = { ...envelope, resposta: (0, hitl_contract_1.chaveDaEscolha)(vigente, indice + 1) };
513
1137
  const assinado = { alvo: pulse_consentimento_1.ALVO_DO_PULSE, endereco: pulse_consentimento_1.ENDERECO_DA_RESPOSTA, envelope,
514
- rastro: { contrato: exports.CONTRATO_RESPOSTA_DO_PULSE, numero, letra, respostaDoDonoSha256: sha(envelope.resposta) } };
515
- const r = (0, hitl_gates_1.responderGate)(raiz, p.thread, p.pedidoId, derivada, quando, assinado);
1138
+ rastro: { contrato: exports.CONTRATO_RESPOSTA_DO_PULSE, numero, letra, respostaDoDonoSha256: sha(envelope.resposta),
1139
+ ...(renovadoDe ? { renovadoDe } : {}), ...(palavra ? { textoLivre: palavra } : {}) } };
1140
+ const r = (0, hitl_gates_1.responderGate)(raiz, p.thread, vigente.id, derivada, quando, assinado);
516
1141
  registradas.push({ numero, thread: p.thread, letra, estado: r.estado, repetida: r.repetida });
517
1142
  linhas.push(`${telegram ? '✅ ' : ''}${numero} → ${letra}) ${p.alternativas[indice]}: ${p.consequencias[indice]}.`);
518
1143
  servido = { ...servido, perguntas: servido.perguntas.map(q => q.numero === numero
@@ -528,15 +1153,15 @@ function responderAsPerguntas(raiz, envelope, escolhas, opcoes) {
528
1153
  const cabecalho = registradas.length
529
1154
  ? `${telegram ? '📝 ' : ''}Orkastery, ${registradas.length === 1 ? '1 resposta registrada' : `${registradas.length} respostas registradas`}`
530
1155
  : `${telegram ? '📝 ' : ''}Orkastery, nenhuma resposta registrada`;
531
- const pendentes = servido.perguntas.filter(q => perguntaViva(q, quando)).map(q => q.numero);
1156
+ const pendentes = servido.perguntas.filter(q => perguntaEmAberto(q, quando)).map(q => q.numero);
532
1157
  const aberto = (0, pulse_consentimento_1.lerConsentimento)(raiz, estadoDir);
533
- const proximo = aberto && !aberto.respondido && aberto.pedido.candidatos.length && Date.parse(quando) < Date.parse(aberto.pedido.prazo)
534
- ? aberto.pedido : undefined;
1158
+ // RM-048 (D5): o codigo do resumo mais recente continua valendo depois do prazo.
1159
+ const proximo = aberto && !aberto.respondido && aberto.pedido.candidatos.length ? aberto.pedido : undefined;
535
1160
  const texto = [
536
- cabecalho, '', ...linhas,
1161
+ cabecalho, '', ...(opcoes.avisos ?? []), ...linhas,
537
1162
  ...(pendentes.length ? ['', `Ainda sem resposta: ${pendentes.join(', ')}.`] : []),
538
1163
  ...(proximo ? [`Faltam ${proximo.candidatos.length}. Para receber as próximas, responda ${proximo.codigo} a.`] : []),
539
1164
  ].join('\n');
540
- return resultado('lote', texto, { registradas, recusas,
1165
+ return resultado('lote', texto, { registradas, recusas: [...(opcoes.avisos ?? []).map(motivo => ({ motivo })), ...recusas],
541
1166
  repetida: registradas.length > 0 && recusas.length === 0 && registradas.every(r => r.repetida) });
542
1167
  }