@orkastery/cli 0.2.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +86 -65
  3. package/adapters/README.md +30 -15
  4. package/adapters/claude-code/.claude-plugin/plugin.json +11 -5
  5. package/adapters/claude-code/README.md +110 -8
  6. package/adapters/claude-code/commands/check.md +4 -3
  7. package/adapters/claude-code/commands/goal.md +3 -2
  8. package/adapters/claude-code/commands/master.md +3 -2
  9. package/adapters/claude-code/commands/onboarding.md +21 -0
  10. package/adapters/claude-code/commands/ork.md +99 -27
  11. package/adapters/claude-code/hooks/hooks.json +6 -1
  12. package/adapters/claude-code/hooks/ork-guard.js +1 -1
  13. package/adapters/claude-code/hooks/ork-sensor.js +66 -0
  14. package/adapters/codex/skills/ork/SKILL.md +57 -0
  15. package/adapters/hermes/README.md +161 -0
  16. package/adapters/hermes/bin/ork-abrir-thread.sh +2 -0
  17. package/adapters/hermes/bin/ork-brain.sh +4 -0
  18. package/adapters/hermes/bin/ork-hitl-answer.py +82 -0
  19. package/adapters/hermes/bin/ork-maestro.sh +6 -0
  20. package/adapters/hermes/bin/ork-master-enviar.py +84 -0
  21. package/adapters/hermes/bin/ork-pulse-enviar.py +47 -0
  22. package/adapters/hermes/hermes.plugin.json +24 -4
  23. package/adapters/hermes/hitl-ingress/__init__.py +356 -0
  24. package/adapters/hermes/hitl-ingress/plugin.yaml +4 -0
  25. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +75 -22
  26. package/adapters/openclaw/README.md +136 -27
  27. package/adapters/openclaw/bin/ork-brain.sh +4 -0
  28. package/adapters/openclaw/construir.sh +19 -0
  29. package/adapters/openclaw/dist/hitl-ingress.js +199 -0
  30. package/adapters/openclaw/dist/index.js +437 -0
  31. package/adapters/openclaw/openclaw.plugin.json +39 -134
  32. package/adapters/openclaw/package.json +26 -0
  33. package/adapters/openclaw/src/hitl-ingress.ts +180 -0
  34. package/adapters/openclaw/src/index.ts +460 -0
  35. package/adapters/openclaw/src/tipos-openclaw.d.ts +58 -0
  36. package/adapters/openclaw/tsconfig.json +15 -0
  37. package/assets/docs/markdownlint-cli2.jsonc +22 -0
  38. package/assets/docs/padroes/documentacao-de-produto.md +133 -0
  39. package/assets/docs/padroes/roadmap-de-produto.md +98 -0
  40. package/assets/docs/produto/README.md +14 -0
  41. package/assets/docs/produto/_modelo-feature.md +59 -0
  42. package/assets/docs/roadmap/README.md +14 -0
  43. package/assets/docs/roadmap/_modelo-item.md +73 -0
  44. package/assets/orkmind-native-schema.json +32 -0
  45. package/assets/orkmind_bridge.py +357 -0
  46. package/assets/orkmind_fixture.py +67 -0
  47. package/assets/orkmind_prospective.py +138 -0
  48. package/assets/reference-tariffs-i07.json +23 -0
  49. package/dist/adapters/claude-bg.js +488 -39
  50. package/dist/adapters/codex-controller-sensor.js +449 -0
  51. package/dist/adapters/codex-controller-worker.js +361 -0
  52. package/dist/adapters/codex-controller.js +252 -0
  53. package/dist/adapters/codex-events.js +205 -0
  54. package/dist/adapters/codex-question.js +26 -0
  55. package/dist/adapters/codex-runner.js +133 -0
  56. package/dist/adapters/codex.js +394 -0
  57. package/dist/agents-md.js +84 -0
  58. package/dist/auditoria.js +12 -9
  59. package/dist/auditrun.js +5 -4
  60. package/dist/board.js +174 -23
  61. package/dist/branch-de-estado.js +136 -0
  62. package/dist/canarios-hitl.js +177 -0
  63. package/dist/canarios-i43.js +543 -0
  64. package/dist/canarios-pulse.js +147 -0
  65. package/dist/canarios-sensores.js +129 -0
  66. package/dist/canarios.js +111 -2
  67. package/dist/catalogo.js +9 -0
  68. package/dist/ci.js +223 -0
  69. package/dist/ciclos.js +2 -1
  70. package/dist/claim-lint.js +64 -0
  71. package/dist/claims.js +60 -0
  72. package/dist/company-brain-capture.js +195 -0
  73. package/dist/company-brain-cli.js +123 -0
  74. package/dist/company-brain-client.js +61 -0
  75. package/dist/company-brain-contract.js +166 -0
  76. package/dist/company-brain-journal.js +192 -0
  77. package/dist/company-brain-mcp.js +33 -0
  78. package/dist/company-brain-migration.js +60 -0
  79. package/dist/company-brain-source.js +177 -0
  80. package/dist/company-brain-worker.js +18 -0
  81. package/dist/conducao-texto.js +61 -0
  82. package/dist/conducao.js +876 -0
  83. package/dist/contrato-publico.js +39 -0
  84. package/dist/creation-operation-store.js +251 -0
  85. package/dist/creation-operation.js +148 -0
  86. package/dist/decisao-autonoma.js +183 -0
  87. package/dist/delegation.js +79 -0
  88. package/dist/demo.js +120 -0
  89. package/dist/docs.js +828 -0
  90. package/dist/doctor.js +184 -25
  91. package/dist/entrega-pr.js +117 -0
  92. package/dist/escopo-escrita.js +71 -0
  93. package/dist/estado-thread.js +222 -0
  94. package/dist/evalrunner.js +12 -0
  95. package/dist/fabrica-estado.js +329 -0
  96. package/dist/fabrica-publicar.js +76 -0
  97. package/dist/fix.js +22 -1
  98. package/dist/gates.js +59 -11
  99. package/dist/handoff.js +39 -23
  100. package/dist/hitl-canais.js +333 -0
  101. package/dist/hitl-classificacao.js +131 -0
  102. package/dist/hitl-contract.js +465 -0
  103. package/dist/hitl-estado.js +136 -0
  104. package/dist/hitl-gates.js +554 -0
  105. package/dist/hitl-ingress-receipt.js +382 -0
  106. package/dist/hitl-local-atestado.js +57 -0
  107. package/dist/hitl-local-receipt.js +328 -0
  108. package/dist/hitl-local.js +143 -0
  109. package/dist/hitl-lock.js +139 -0
  110. package/dist/hitl-lote.js +223 -0
  111. package/dist/hitl-native-offer.js +97 -0
  112. package/dist/hitl-native.js +65 -0
  113. package/dist/hitl-presentation.js +209 -0
  114. package/dist/hitl-public-receipt.js +176 -0
  115. package/dist/hitl-resumo.js +209 -0
  116. package/dist/hitl-sessions.js +475 -0
  117. package/dist/hitl.js +711 -0
  118. package/dist/horario.js +269 -0
  119. package/dist/hosts.js +198 -22
  120. package/dist/index.js +1741 -94
  121. package/dist/indice.js +99 -0
  122. package/dist/init.js +28 -3
  123. package/dist/integracoes-locais.js +17 -0
  124. package/dist/leases.js +65 -21
  125. package/dist/ledger-stats.js +272 -0
  126. package/dist/ledger.js +109 -2
  127. package/dist/licoes.js +221 -0
  128. package/dist/liveness.js +218 -0
  129. package/dist/maestro-actions.js +51 -0
  130. package/dist/maestro-authority.js +152 -0
  131. package/dist/maestro-cli.js +97 -0
  132. package/dist/maestro-contract.js +55 -0
  133. package/dist/maestro-discovery.js +132 -0
  134. package/dist/maestro-runtime.js +268 -0
  135. package/dist/maestro-snapshot.js +86 -0
  136. package/dist/maestro-sources.js +224 -0
  137. package/dist/manifest.js +165 -6
  138. package/dist/maquina.js +102 -0
  139. package/dist/master-audit.js +101 -0
  140. package/dist/master-batch.js +43 -0
  141. package/dist/master-digest.js +149 -0
  142. package/dist/master-migracao.js +155 -0
  143. package/dist/master.js +361 -77
  144. package/dist/mcp-artifacts.js +234 -0
  145. package/dist/mcp-git.js +433 -0
  146. package/dist/mcp-install.js +310 -0
  147. package/dist/mcp-maestro.js +25 -0
  148. package/dist/mcp-server.js +469 -0
  149. package/dist/mcp-ship.js +436 -0
  150. package/dist/mcp-verify.js +173 -0
  151. package/dist/memoria-humana.js +207 -0
  152. package/dist/memoria.js +314 -78
  153. package/dist/memory-migration.js +279 -0
  154. package/dist/memory-prospective.js +148 -0
  155. package/dist/modos-migracao.js +191 -0
  156. package/dist/modos.js +151 -15
  157. package/dist/monitor-lock.js +110 -0
  158. package/dist/objective.js +449 -0
  159. package/dist/ocupacao.js +267 -0
  160. package/dist/onboarding.js +303 -0
  161. package/dist/orkmind.js +389 -90
  162. package/dist/orquestracao.js +96 -69
  163. package/dist/phase.js +612 -151
  164. package/dist/playbook-capabilities.js +150 -0
  165. package/dist/playbook-contracts.js +188 -0
  166. package/dist/policies.js +72 -0
  167. package/dist/portfolio-context.js +61 -0
  168. package/dist/portfolio.js +180 -0
  169. package/dist/preflight.js +207 -0
  170. package/dist/process-audit.js +112 -0
  171. package/dist/project-state.js +93 -0
  172. package/dist/prompts.js +25 -5
  173. package/dist/prova-minima.js +112 -0
  174. package/dist/pulse-cadencia.js +163 -0
  175. package/dist/pulse-consentimento.js +260 -0
  176. package/dist/pulse-delivery.js +323 -0
  177. package/dist/pulse-resposta.js +542 -0
  178. package/dist/pulse.js +262 -0
  179. package/dist/ratelimit.js +35 -2
  180. package/dist/recall.js +80 -3
  181. package/dist/redacao-saida.js +45 -0
  182. package/dist/redacao-url.js +66 -0
  183. package/dist/retry.js +640 -118
  184. package/dist/roadmap-reservas.js +243 -0
  185. package/dist/runtime-ambiente.js +38 -0
  186. package/dist/runtime-context.js +215 -0
  187. package/dist/runtime-profiles.js +869 -0
  188. package/dist/runtimes.js +103 -0
  189. package/dist/sandbox.js +9 -0
  190. package/dist/session-events.js +203 -0
  191. package/dist/session-watcher-claude.js +637 -0
  192. package/dist/session-watcher.js +700 -0
  193. package/dist/sessoes-adopt.js +154 -0
  194. package/dist/sessoes-inventario.js +159 -0
  195. package/dist/sessoes.js +16 -35
  196. package/dist/setup.js +691 -0
  197. package/dist/ship.js +168 -19
  198. package/dist/slug.js +1 -1
  199. package/dist/thread-close.js +115 -0
  200. package/dist/thread.js +142 -28
  201. package/dist/tokens.js +1 -1
  202. package/dist/util.js +4 -2
  203. package/dist/verify-sandbox.js +265 -0
  204. package/dist/verify.js +270 -25
  205. package/dist/versao.js +60 -0
  206. package/dist/worktree.js +31 -2
  207. package/dist/write-activation.js +291 -0
  208. package/dist/yaml.js +5 -1
  209. package/eval/casos/master-metrics.json +1 -1
  210. package/eval/casos/onboarding.json +102 -0
  211. package/eval/casos/orkastery-bootstrap.json +37 -3
  212. package/eval/casos/scope-check-capability-map.json +45 -7
  213. package/eval/casos/ship-release.json +3 -3
  214. package/eval/fixtures/b0-slug-e-modos/caso.json +35 -22
  215. package/eval/fixtures/b2-master-log/caso.json +1 -1
  216. package/eval/fixtures/fx-adapter-editado-detectado/caso.json +19 -0
  217. package/eval/fixtures/fx-auto-quiet/caso.json +11 -0
  218. package/eval/fixtures/fx-blanket-approve/caso.json +10 -0
  219. package/eval/fixtures/fx-check-runtime-cruzado/caso.json +20 -0
  220. package/eval/fixtures/fx-codex-dry/caso.json +18 -0
  221. package/eval/fixtures/fx-donewhen-executavel/caso.json +15 -0
  222. package/eval/fixtures/fx-estado-dividido/caso.json +15 -0
  223. package/eval/fixtures/fx-fase-orfa/caso.json +116 -0
  224. package/eval/fixtures/fx-happy/caso.json +6 -4
  225. package/eval/fixtures/fx-hitl-latency/caso.json +20 -0
  226. package/eval/fixtures/fx-indice-reversao/caso.json +25 -0
  227. package/eval/fixtures/fx-listagem-abertas/caso.json +16 -0
  228. package/eval/fixtures/fx-maestro-bootstrap/caso.json +21 -0
  229. package/eval/fixtures/fx-modo-aposentado-escritor/caso.json +21 -0
  230. package/eval/fixtures/fx-modo-aposentado-leitor/caso.json +16 -0
  231. package/eval/fixtures/fx-objective-oscillation/caso.json +14 -0
  232. package/eval/fixtures/fx-omnicanal/caso.json +17 -0
  233. package/eval/fixtures/fx-sensores-runtime/caso.json +17 -0
  234. package/monitor/company-brain.cjs +17 -0
  235. package/monitor/pulse-scope.cjs +45 -0
  236. package/monitor/pulse.cron +18 -0
  237. package/monitor/varredura-pulse.sh +27 -0
  238. package/package.json +28 -7
  239. package/references/definition-of-done.md +2 -2
  240. package/schemas/claims.schema.json +25 -0
  241. package/schemas/company-brain.schema.json +1158 -0
  242. package/schemas/creation-operation.schema.json +365 -0
  243. package/schemas/maestro-snapshot.schema.json +2716 -0
  244. package/skills/README.md +3 -3
  245. package/skills/core/onboarding/SKILL.md +50 -0
  246. package/skills/core/orkastery-bootstrap/SKILL.md +95 -65
  247. package/skills/core/thread-state/SKILL.md +3 -1
  248. package/skills/governance/decision-triage/SKILL.md +4 -4
  249. package/skills/governance/narrative-guardian/SKILL.md +1 -1
  250. package/skills/governance/roadmap-keeper/SKILL.md +1 -1
  251. package/skills/governance/scope-check-capability-map/SKILL.md +17 -7
  252. package/skills/observability/thread-tracing/SKILL.md +1 -1
  253. package/skills/phases/check-quality/SKILL.md +2 -2
  254. package/skills/phases/goal-definition/SKILL.md +1 -1
  255. package/skills/phases/master-metrics/SKILL.md +9 -7
  256. package/skills/phases/plan-specification/SKILL.md +1 -1
  257. package/skills/phases/ship-release/SKILL.md +1 -1
@@ -0,0 +1,542 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
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;
37
+ exports.interpretarRespostaDoPulse = interpretarRespostaDoPulse;
38
+ exports.arquivoDoLoteServido = arquivoDoLoteServido;
39
+ exports.lerLoteServido = lerLoteServido;
40
+ exports.perguntaViva = perguntaViva;
41
+ exports.threadsComPerguntaViva = threadsComPerguntaViva;
42
+ exports.gatesJaServidos = gatesJaServidos;
43
+ exports.avaliarFila = avaliarFila;
44
+ exports.servirLote = servirLote;
45
+ exports.responderPeloPulse = responderPeloPulse;
46
+ /**
47
+ * I-41 (GO-FIX 1, B1 e B2): o caminho de volta.
48
+ *
49
+ * Ate aqui o produto mandava um resumo perguntando "Posso te mandar as perguntas agora?" e um lote
50
+ * pedindo "responda com o numero e a letra", e nada no produto sabia receber a resposta. Parecia
51
+ * funcionar, que e pior do que nao funcionar. Este modulo e o receptor das duas coisas que o dono
52
+ * digita no canal em que recebeu o resumo:
53
+ *
54
+ * "P4EJ a" responde ao resumo: sim, pode mandar as perguntas (ou "P4EJ b", agora nao);
55
+ * "1a 2c" responde as perguntas do lote, uma letra por numero;
56
+ * "#OrkPulseOn-15m" troca a cadencia do resumo (I-50, RM-039), sozinha na mensagem.
57
+ *
58
+ * A PROVA NAO MUDA. O ingresso autenticado do Telegram (Hermes ou OpenClaw) assina o texto que o
59
+ * dono digitou com a chave do proprio canal, no endereco do pulse, pelo corpo de sempre, e o
60
+ * nucleo confere com `autenticarResposta` sem uma linha alterada, ANTES de ler o conteudo. Quem
61
+ * traduz "pergunta 1, letra a" no pedido certo e o nucleo, pelo lote que ele mesmo serviu e
62
+ * gravou; cada resposta vira um `human_gate` pelo `responderGate` de sempre, com recibo duravel,
63
+ * uso unico, janela, contexto e memoria. Nao existe um segundo caminho de autenticacao.
64
+ *
65
+ * Quem monta o texto que volta ao dono e o nucleo. O adaptador so transporta.
66
+ */
67
+ const fs = __importStar(require("node:fs"));
68
+ const path = __importStar(require("node:path"));
69
+ const node_crypto_1 = require("node:crypto");
70
+ const estado_thread_1 = require("./estado-thread");
71
+ const horario_1 = require("./horario");
72
+ const hitl_gates_1 = require("./hitl-gates");
73
+ const hitl_contract_1 = require("./hitl-contract");
74
+ const monitor_lock_1 = require("./monitor-lock");
75
+ const hitl_lote_1 = require("./hitl-lote");
76
+ const pulse_cadencia_1 = require("./pulse-cadencia");
77
+ const ledger_1 = require("./ledger");
78
+ const thread_1 = require("./thread");
79
+ const pulse_consentimento_1 = require("./pulse-consentimento");
80
+ exports.CONTRATO_RESPOSTA_DO_PULSE = 'ork.pulse-resposta/v1';
81
+ exports.CONTRATO_LOTE_SERVIDO = 'ork.pulse-lote-servido/v1';
82
+ /**
83
+ * As duas formas que o ingresso reconhece, em fonte de expressao regular. As MESMAS strings vivem
84
+ * nos adaptadores do Hermes e do OpenClaw, e um teste do nucleo confere que continuam iguais: se
85
+ * o adaptador reconhecesse uma forma que o nucleo nao entende, a mensagem do dono sumiria.
86
+ *
87
+ * O codigo comeca por letra e tem pelo menos um digito (`gerarCodigo`). Por ter digito, palavra
88
+ * comum de quatro letras nunca tem a forma de resposta ao resumo, e conversa com o assistente
89
+ * continua indo para o assistente. Por comecar com letra, ele nunca e lido como resposta ao lote,
90
+ * que comeca com o numero da pergunta: "22A2 a" seria "22a 2a". As duas formas sao disjuntas pelo
91
+ * primeiro caractere. `consentimento` e lida sem diferenca de caixa; `lote` declara as duas.
92
+ *
93
+ * I-50 (RM-039): a terceira forma e a tag da cadencia, sozinha na mensagem. Ela comeca por `#`,
94
+ * entao tambem e disjunta das outras duas pelo primeiro caractere; e lida sem diferenca de caixa.
95
+ * Tag no meio de uma frase continua sendo conversa com o assistente.
96
+ */
97
+ 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}$',
99
+ lote: '^[ \\t]*[0-9]{1,2}[ \\t]*[a-zA-Z](?:[ \\t,;]*[0-9]{1,2}[ \\t]*[a-zA-Z])*[ \\t]*[.!]?[ \\t]*$',
100
+ cadencia: '^[ \\t]*#OrkPulse(?:On(?:-(?:15|30|60)m)?|Off)[ \\t]*[.!]?[ \\t]*$',
101
+ });
102
+ const CONSENTIMENTO = new RegExp(exports.GRAMATICA_DO_PULSE.consentimento, 'i');
103
+ const LOTE = new RegExp(exports.GRAMATICA_DO_PULSE.lote);
104
+ const CADENCIA = new RegExp(exports.GRAMATICA_DO_PULSE.cadencia, 'i');
105
+ /** O que o dono quis dizer, pela forma. Nada aqui le intencao: ou a forma bate, ou nao. */
106
+ function interpretarRespostaDoPulse(texto) {
107
+ if (typeof texto !== 'string' || texto.length > 200)
108
+ return { forma: 'desconhecida' };
109
+ if (LOTE.test(texto)) {
110
+ const escolhas = [...texto.matchAll(/([0-9]{1,2})[ \t]*([a-zA-Z])/g)]
111
+ .map(m => ({ numero: Number(m[1]), letra: m[2].toLowerCase() }));
112
+ return { forma: 'lote', escolhas };
113
+ }
114
+ if (CONSENTIMENTO.test(texto))
115
+ return { forma: 'consentimento', codigo: texto.trim().slice(0, 4).toUpperCase() };
116
+ const tag = CADENCIA.test(texto) ? (0, pulse_cadencia_1.extrairTagDoPulse)(texto) : null;
117
+ if (tag)
118
+ return { forma: 'cadencia', tag };
119
+ return { forma: 'desconhecida' };
120
+ }
121
+ /** Quanto tempo uma pergunta respondida continua no registro, para "ja estava registrada". */
122
+ const GUARDA_DO_RESPONDIDO_MS = 24 * 60 * 60 * 1000;
123
+ const sha = (s) => (0, node_crypto_1.createHash)('sha256').update(s).digest('hex');
124
+ function arquivoDoLoteServido(raiz, estadoDir) {
125
+ return path.join(estadoDir ?? path.join((0, estado_thread_1.raizDoEstado)(raiz), '.orkastery', 'monitor'), 'pulse-lote.json');
126
+ }
127
+ function lerLoteServido(raiz, estadoDir) {
128
+ const arquivo = arquivoDoLoteServido(raiz, estadoDir);
129
+ if (!fs.existsSync(arquivo))
130
+ return { contrato: exports.CONTRATO_LOTE_SERVIDO, versao: 1, perguntas: [], porCodigo: {} };
131
+ const salvo = JSON.parse(fs.readFileSync(arquivo, 'utf8'));
132
+ if (salvo.contrato !== exports.CONTRATO_LOTE_SERVIDO || salvo.versao !== 1 || !Array.isArray(salvo.perguntas)) {
133
+ throw new Error('lote servido do pulse: estado inválido; exige inspeção');
134
+ }
135
+ return { ...salvo, porCodigo: salvo.porCodigo ?? {} };
136
+ }
137
+ function gravarLoteServido(raiz, lote, estadoDir) {
138
+ const arquivo = arquivoDoLoteServido(raiz, estadoDir);
139
+ fs.mkdirSync(path.dirname(arquivo), { recursive: true });
140
+ const tmp = arquivo + '.tmp';
141
+ fs.writeFileSync(tmp, JSON.stringify(lote) + '\n', { mode: 0o600 });
142
+ fs.renameSync(tmp, arquivo);
143
+ }
144
+ /** Pergunta que ainda pode ser respondida: sem resposta e dentro do prazo do pedido. */
145
+ function perguntaViva(p, quando) {
146
+ return !p.respondida && (p.prazo === null || Date.parse(quando) < Date.parse(p.prazo));
147
+ }
148
+ /** O que sai do registro: vencida sem resposta, ou respondida ha mais de um dia. */
149
+ function podar(lote, quando) {
150
+ const agora = Date.parse(quando);
151
+ const perguntas = lote.perguntas.filter(p => perguntaViva(p, quando) ||
152
+ (p.respondida && agora - Date.parse(p.respondida.em) < GUARDA_DO_RESPONDIDO_MS));
153
+ const numeros = new Set(perguntas.map(p => p.numero));
154
+ const porCodigo = Object.fromEntries(Object.entries(lote.porCodigo)
155
+ .map(([codigo, ns]) => [codigo, ns.filter(n => numeros.has(n))]).filter(([, ns]) => ns.length));
156
+ return { ...lote, perguntas, porCodigo };
157
+ }
158
+ /** Threads com pergunta servida esperando resposta: elas nao entram de novo no resumo. */
159
+ function threadsComPerguntaViva(raiz, quando, estadoDir) {
160
+ return new Set(lerLoteServido(raiz, estadoDir).perguntas.filter(p => perguntaViva(p, quando)).map(p => p.thread));
161
+ }
162
+ /** A chave de um gate oferecido: a mesma thread numa fase nova e pergunta nova. */
163
+ const chaveDoGate = (thread, fase) => `${thread}|${fase ?? ''}`;
164
+ exports.chaveDoGate = chaveDoGate;
165
+ /**
166
+ * Os gates que o dono ja recebeu, respondidos ou nao: o de "continuar esperando", o que venceu sem
167
+ * resposta. Eles continuam sendo perguntas e continuam no proximo resumo; o que nao fazem e tocar
168
+ * um resumo sozinhos, porque o dono ja os viu e a resposta dele (ou o silencio) vale.
169
+ */
170
+ function gatesJaServidos(raiz, estadoDir) {
171
+ return new Set(lerLoteServido(raiz, estadoDir).perguntas.map(p => (0, exports.chaveDoGate)(p.thread, p.fase)));
172
+ }
173
+ /**
174
+ * I-41 (GO-FIX 1, B3): o que o resumo pode chamar de pergunta.
175
+ *
176
+ * Um item vira candidato quando o gate da thread dele ainda espera o dono agora, conferido por
177
+ * `prepararPedidoGate`, que nao escreve nada. Pedido velho de thread que ja seguiu em frente e
178
+ * historia e nao conta. Pedido que espera mas nao vira pergunta (formato recusado) conta como
179
+ * conserto nosso, nunca como pergunta. Uma thread com pergunta servida e ainda sem resposta nao
180
+ * entra de novo: o dono ja tem o numero dela.
181
+ */
182
+ function avaliarFila(raiz, itens, quando, opcoes = {}) {
183
+ const porThread = new Map();
184
+ for (const item of itens) {
185
+ if (!item.thread || item.classe === 'score_pendente' || opcoes.excluir?.has(item.thread))
186
+ continue;
187
+ porThread.set(item.thread, [...(porThread.get(item.thread) ?? []), item]);
188
+ }
189
+ const candidatos = [], atos = new Map();
190
+ let consertos = 0;
191
+ 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));
193
+ let achado;
194
+ for (const motivo of [...new Set([...escalacoes, 'human.pending'])]) {
195
+ try {
196
+ const preparado = (0, hitl_gates_1.prepararPedidoGate)(raiz, thread, motivo, quando);
197
+ achado = 'aberto' in preparado ? { motivo, pedido: preparado.aberto, aberto: true } : { motivo, pedido: preparado.novo, aberto: false };
198
+ break;
199
+ }
200
+ catch { /* este motivo nao abre gate agora; o proximo pode abrir */ }
201
+ }
202
+ if (achado) {
203
+ const lote = (0, hitl_lote_1.montarLote)([achado.pedido]);
204
+ if (!lote.perguntas.length) {
205
+ consertos++;
206
+ continue;
207
+ }
208
+ candidatos.push({ thread, fase: achado.pedido.fase, motivo: achado.motivo, ...(achado.aberto ? { pedidoId: achado.pedido.id } : {}) });
209
+ const ato = achado.pedido.ato;
210
+ if (ato)
211
+ atos.set(`${thread}|${achado.pedido.fase}`, ato);
212
+ continue;
213
+ }
214
+ // Sem gate que espere, um pedido aberto ainda e pergunta se for respondivel agora; senao, e
215
+ // conserto quando o formato e o problema, e historia quando o prazo ja passou.
216
+ const pedido = doThread.map(i => i.pedido).find((p) => !!p);
217
+ if (!pedido)
218
+ continue;
219
+ let aberto = false;
220
+ try {
221
+ aberto = (0, hitl_contract_1.estadoDoPedido)(pedido, quando) === 'aberto';
222
+ }
223
+ catch {
224
+ aberto = false;
225
+ }
226
+ if (!aberto)
227
+ continue;
228
+ const lote = (0, hitl_lote_1.montarLote)([pedido]);
229
+ if (!lote.perguntas.length) {
230
+ if (lote.recusadas.length)
231
+ consertos++;
232
+ continue;
233
+ }
234
+ candidatos.push({ thread, fase: pedido.fase, motivo: (0, hitl_contract_1.motivoDoPedido)(pedido), pedidoId: pedido.id, fonte: 'pedido' });
235
+ }
236
+ return { candidatos, consertos, atos };
237
+ }
238
+ /** O pedido de um candidato, reaberto agora: prazo novo, contexto de agora, pergunta de agora. */
239
+ function pedidoDoCandidato(raiz, c, quando) {
240
+ if (c.fonte === 'pedido' && c.pedidoId) {
241
+ const evento = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, c.thread))
242
+ .find(e => e.tipo === 'hitl_requested' && e.pedido?.id === c.pedidoId);
243
+ (0, hitl_contract_1.validarPedidoHitl)(evento?.pedido);
244
+ const pedido = evento.pedido;
245
+ if ((0, hitl_contract_1.estadoDoPedido)(pedido, quando) !== 'aberto')
246
+ throw new Error('pedido HITL expirado; nenhuma autorização concedida');
247
+ return pedido;
248
+ }
249
+ // `abrirPedidoGate` devolve o pedido aberto que ja existe, ou abre um novo com prazo de agora.
250
+ return (0, hitl_gates_1.abrirPedidoGate)(raiz, c.thread, c.motivo, quando);
251
+ }
252
+ /**
253
+ * Serve UM lote de ate cinco perguntas para os candidatos do consentimento respondido.
254
+ *
255
+ * O pedido de cada gate e (re)aberto AGORA: o dono disse sim agora, e e agora que o prazo da
256
+ * pergunta comeca. Um gate que deixou de esperar entre o resumo e o sim nao vira pergunta. As
257
+ * perguntas ganham numeros que continuam os do lote anterior enquanto ele ainda pode ser
258
+ * respondido, para "1a" nunca querer dizer duas coisas. O que nao coube vai com um codigo novo.
259
+ */
260
+ function servirLote(raiz, entrada) {
261
+ const { quando } = entrada;
262
+ let servido = podar(lerLoteServido(raiz, entrada.estadoDir), quando);
263
+ 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;
265
+ const abertos = [];
266
+ // Thread ocupada agora (despacho ou outra resposta segurando o lock) nao e thread que deixou de
267
+ // esperar: ela vai para o proximo lote, e nao some do que o dono pediu.
268
+ const ocupadas = [];
269
+ let naoEsperam = 0, i = 0;
270
+ const candidatos = entrada.pedido.candidatos;
271
+ for (; i < candidatos.length && abertos.length < 5; i++) {
272
+ const c = candidatos[i];
273
+ if (vivas.has(c.thread))
274
+ continue; // o dono ja tem o numero desta; nao pergunto duas vezes
275
+ try {
276
+ const pedido = pedidoDoCandidato(raiz, c, quando);
277
+ if (!(0, hitl_contract_1.alvoDoPedido)(pedido)) {
278
+ naoEsperam++;
279
+ continue;
280
+ }
281
+ abertos.push({ candidato: c, pedido });
282
+ }
283
+ catch (e) {
284
+ if (/ocupad/i.test(e.message))
285
+ ocupadas.push(c);
286
+ else
287
+ naoEsperam++;
288
+ }
289
+ }
290
+ const lote = (0, hitl_lote_1.montarLote)(abertos.map(a => a.pedido), { numeroInicial: inicio });
291
+ const restantes = [...ocupadas, ...candidatos.slice(i)];
292
+ const novas = lote.perguntas.map(p => {
293
+ const pedido = abertos.find(a => a.pedido.id === p.pedidoId).pedido;
294
+ return {
295
+ numero: p.numero, thread: pedido.thread, fase: pedido.fase, pedidoId: pedido.id,
296
+ pedidoSha256: sha(JSON.stringify(pedido)), letras: p.alternativas.map(a => a.letra),
297
+ alternativas: p.alternativas.map(a => a.texto), consequencias: p.alternativas.map(a => a.consequencia),
298
+ pergunta: p.pergunta, prazo: (0, hitl_contract_1.prazoDoPedido)(pedido) ?? null, servidaEm: quando,
299
+ };
300
+ });
301
+ const numeros = novas.map(p => p.numero);
302
+ servido = { ...servido, perguntas: [...servido.perguntas, ...novas],
303
+ porCodigo: { ...servido.porCodigo, [entrada.pedido.codigo]: numeros } };
304
+ gravarLoteServido(raiz, servido, entrada.estadoDir);
305
+ // A4: o que sobrou sai com um codigo novo, na mesma mensagem. Sem isto, servido um lote, o resto
306
+ // ficava parado ate algum item mudar, porque o resumo so sai com novidade.
307
+ const proximo = restantes.length ? (0, pulse_consentimento_1.abrirConsentimento)(raiz, {
308
+ quando, resumoSha256: entrada.pedido.resumoSha256, candidatos: restantes,
309
+ prazoMin: pulse_consentimento_1.PRAZO_PADRAO_MIN, estadoDir: entrada.estadoDir,
310
+ }) : undefined;
311
+ const texto = (0, hitl_lote_1.textoDoLote)(lote, { canal: entrada.canal, naoEsperam,
312
+ ...(proximo ? { proximo: { codigo: proximo.codigo, faltam: restantes.length } } : {}) });
313
+ return { texto, numeros, restantes, naoEsperam, ...(proximo ? { proximo } : {}) };
314
+ }
315
+ /**
316
+ * O lote de um sim ja servido, para quem pede de novo, ou o aviso de que ele ja nao serve. Reenviar
317
+ * pergunta morta (toda respondida ou vencida) seria pedir uma resposta que o gate vai recusar.
318
+ */
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);
321
+ if (vivas.length)
322
+ return reenviarLote(servido, vivas, canal);
323
+ return `As perguntas do código ${codigo} já foram respondidas ou venceram. ` +
324
+ (aberto ? `Para as de agora, responda ${aberto.codigo} a.` : 'O próximo resumo traz as que ainda esperarem você.');
325
+ }
326
+ /** 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 });
334
+ }
335
+ 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
+ 'Para trocar a cadência, mande só a tag: #OrkPulseOn, #OrkPulseOn-15m, -30m, -60m ou #OrkPulseOff.';
337
+ function resultado(tipo, mensagem, extra = {}) {
338
+ return { contrato: exports.CONTRATO_RESPOSTA_DO_PULSE, ok: true, tipo, repetida: false, registradas: [], recusas: [], mensagem, ...extra };
339
+ }
340
+ /**
341
+ * Recebe o que o dono digitou no canal do resumo.
342
+ *
343
+ * A ordem e a de sempre, e ela e o que impede um agente responder pelo dono: primeiro a
344
+ * proveniencia do envelope (a mesma `autenticarResposta`, a mesma chave, as mesmas allowlists e a
345
+ * mesma janela), depois o conteudo. Envelope sem prova lanca e nao recebe resposta nenhuma; texto
346
+ * do dono que nao da para cumprir recebe uma resposta util, nunca silencio nem erro cru.
347
+ */
348
+ function responderPeloPulse(raiz, envelope, opcoes = {}) {
349
+ const quando = opcoes.quando ?? new Date().toISOString();
350
+ const canal = opcoes.canal ?? 'telegram';
351
+ (0, hitl_gates_1.autenticarResposta)(pulse_consentimento_1.ALVO_DO_PULSE, pulse_consentimento_1.ENDERECO_DA_RESPOSTA, envelope, envelope.recebidoEm);
352
+ (0, hitl_gates_1.autenticarResposta)(pulse_consentimento_1.ALVO_DO_PULSE, pulse_consentimento_1.ENDERECO_DA_RESPOSTA, envelope, quando);
353
+ const lido = interpretarRespostaDoPulse(envelope.resposta);
354
+ if (lido.forma === 'desconhecida')
355
+ return resultado('nao-entendida', `Não entendi. ${EXEMPLO}`);
356
+ if (lido.forma === 'cadencia') {
357
+ // I-50: preferencia de entrega, nao decisao de gate. Grava quem mandou, pelo remetente autenticado.
358
+ const repetida = (0, pulse_cadencia_1.lerCadencia)(raiz, opcoes.estadoDir).gravada?.tag === lido.tag;
359
+ (0, pulse_cadencia_1.gravarCadencia)(raiz, lido.tag, { por: envelope.por, canal: envelope.canal ?? envelope.origem, em: quando }, opcoes.estadoDir);
360
+ return resultado('cadencia', (0, pulse_cadencia_1.textoDaCadencia)(pulse_cadencia_1.CADENCIAS[lido.tag], quando), { cadencia: lido.tag, repetida });
361
+ }
362
+ // A conversa do pulse tem dois escritores (a varredura e este receptor): um de cada vez.
363
+ 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 }));
367
+ }
368
+ function responderAoResumo(raiz, envelope, codigo, opcoes) {
369
+ const { quando, canal, estadoDir } = opcoes;
370
+ 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) {
374
+ const servido = lerLoteServido(raiz, estadoDir);
375
+ const numeros = servido.porCodigo[codigo];
376
+ // Sim repetido a um codigo que ja serviu lote: devolve o MESMO lote, nunca um novo.
377
+ if (numeros?.length) {
378
+ // 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 });
382
+ }
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.`);
385
+ }
386
+ let r;
387
+ try {
388
+ r = (0, pulse_consentimento_1.responderConsentimento)(raiz, { codigo, envelope, quando, estadoDir });
389
+ }
390
+ catch (e) {
391
+ const m = e.message;
392
+ if (!m.startsWith('consentimento do pulse: '))
393
+ 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
+ if (m.includes('divergente')) {
404
+ const antes = estado.respondido;
405
+ return resultado('consentimento', antes.resposta === 'sim'
406
+ ? `Você já respondeu sim a este resumo às ${(0, horario_1.formatarHora)(antes.respondidoEm, { agora: quando })}, e as perguntas já foram. Responda a elas pelo número e a letra.`
407
+ : `Você já respondeu "agora não" a este resumo às ${(0, horario_1.formatarHora)(antes.respondidoEm, { agora: quando })}. O próximo resumo pergunta de novo.`);
408
+ }
409
+ return resultado('consentimento', `Responda com ${codigo} a (sim) ou ${codigo} b (agora não).`);
410
+ }
411
+ // `repetida` para o canal quer dizer "esta MESMA mensagem ja foi tratada" (entrega repetida do
412
+ // gateway): ai nao se reenvia nada. O dono que manda de novo, por outra mensagem, e respondido.
413
+ const mesmaMensagem = r.repetida && r.respondido.mensagem === envelope.mensagem;
414
+ if (r.resposta === 'nao') {
415
+ const n = r.pedido.candidatos.length;
416
+ return resultado('consentimento', `Combinado, não mando agora. ${n === 1 ? 'A pergunta continua guardada' :
417
+ `As ${n} perguntas continuam guardadas`}, e o próximo resumo lembra ${n === 1 ? 'dela' : 'delas'}.`, { resposta: 'nao', repetida: mesmaMensagem });
418
+ }
419
+ if (r.repetida && r.respondido.numeros) {
420
+ return resultado('consentimento', loteDeNovo(lerLoteServido(raiz, estadoDir), r.respondido.numeros, codigo, quando, canal), { resposta: 'sim', repetida: mesmaMensagem });
421
+ }
422
+ const entregue = servirLote(raiz, { pedido: r.pedido, quando, canal, estadoDir });
423
+ marcarLoteEntregueSeAindaFor(raiz, quando, estadoDir, r.pedido.id, entregue.numeros);
424
+ return resultado('consentimento', entregue.texto, { resposta: 'sim', repetida: false });
425
+ }
426
+ /**
427
+ * Marca o sim como servido quando ele ainda e o pedido aberto. Quando sobraram perguntas, o
428
+ * proximo codigo ja substituiu este pedido, e o registro de quais numeros ele serviu fica no lote.
429
+ */
430
+ function marcarLoteEntregueSeAindaFor(raiz, quando, estadoDir, pedidoId, numeros) {
431
+ const estado = (0, pulse_consentimento_1.lerConsentimento)(raiz, estadoDir);
432
+ if (estado?.pedido.id === pedidoId && estado.respondido?.resposta === 'sim' && !estado.respondido.loteEntregueEm) {
433
+ (0, pulse_consentimento_1.marcarLoteEntregue)(raiz, quando, estadoDir, numeros);
434
+ }
435
+ }
436
+ /** O motivo, em uma linha para o dono, de uma resposta que o gate nao aceitou. */
437
+ function motivoParaODono(p, letra, erro, quando) {
438
+ const m = erro.message, n = p.numero;
439
+ if (m.includes('expirado'))
440
+ 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ê.`;
441
+ if (m.includes('pedido antigo'))
442
+ return `a pergunta ${n} já não espera você: a fase mudou desde que ela saiu.`;
443
+ if (m.includes('já respondido'))
444
+ return `a pergunta ${n} já tinha resposta registrada; a resposta não muda.`;
445
+ if (m.includes('ocupado'))
446
+ return `a thread da pergunta ${n} estava ocupada; mande ${n}${letra} de novo em um minuto.`;
447
+ if (m.includes('proveniência') || m.includes('não autenticada'))
448
+ return `não consegui provar a origem da resposta ${n} a tempo; mande ${n}${letra} de novo.`;
449
+ return `não consegui registrar a pergunta ${n}: ${m.slice(0, 120)}`;
450
+ }
451
+ function responderAsPerguntas(raiz, envelope, escolhas, opcoes) {
452
+ const { quando, canal, estadoDir } = opcoes;
453
+ let servido = lerLoteServido(raiz, estadoDir);
454
+ if (!servido.perguntas.length) {
455
+ return resultado('lote', 'Não há pergunta aberta agora. Quando houver, o resumo avisa.');
456
+ }
457
+ const registradas = [], recusas = [];
458
+ const linhas = [];
459
+ const telegram = canal === 'telegram';
460
+ // Duas letras para o mesmo numero na mesma mensagem: nao escolho por ele.
461
+ const porNumero = new Map();
462
+ for (const e of escolhas)
463
+ porNumero.set(e.numero, new Set([...(porNumero.get(e.numero) ?? []), e.letra]));
464
+ const vistos = new Set();
465
+ for (const { numero, letra } of escolhas) {
466
+ if (vistos.has(numero))
467
+ continue;
468
+ vistos.add(numero);
469
+ const letras = [...porNumero.get(numero)];
470
+ if (letras.length > 1) {
471
+ const motivo = `você respondeu a pergunta ${numero} duas vezes (${letras.map(l => `${numero}${l}`).join(' e ')}); mande de novo só a que vale.`;
472
+ recusas.push({ numero, motivo });
473
+ linhas.push(`${telegram ? '⚠️ ' : '! '}${motivo}`);
474
+ continue;
475
+ }
476
+ const p = servido.perguntas.find(q => q.numero === numero);
477
+ if (!p) {
478
+ const abertas = servido.perguntas.filter(q => perguntaViva(q, quando)).map(q => q.numero);
479
+ const motivo = `não há pergunta ${numero} ${abertas.length ? `aberta; as abertas são ${abertas.join(', ')}` : 'aberta agora'}.`;
480
+ recusas.push({ numero, motivo });
481
+ linhas.push(`${telegram ? '⚠️ ' : '! '}${motivo}`);
482
+ continue;
483
+ }
484
+ const indice = p.letras.indexOf(letra);
485
+ if (indice < 0) {
486
+ const motivo = `a pergunta ${numero} vai de a até ${p.letras.at(-1)}; "${letra}" não é uma das alternativas.`;
487
+ recusas.push({ numero, motivo });
488
+ linhas.push(`${telegram ? '⚠️ ' : '! '}${motivo}`);
489
+ continue;
490
+ }
491
+ if (p.respondida) {
492
+ if (p.respondida.letra === letra) {
493
+ // Repetida para o canal so quando e a MESMA mensagem; o dono que manda de novo e respondido.
494
+ registradas.push({ numero, thread: p.thread, letra, estado: p.respondida.estado, repetida: p.respondida.mensagem === envelope.mensagem });
495
+ linhas.push(`${telegram ? '✅ ' : ''}${numero} → ${letra}) ${p.alternativas[indice]}: já estava registrada.`);
496
+ }
497
+ else {
498
+ const motivo = `a pergunta ${numero} já foi respondida com ${p.respondida.letra} às ${(0, horario_1.formatarHora)(p.respondida.em, { agora: quando })}; a resposta não muda.`;
499
+ recusas.push({ numero, motivo });
500
+ linhas.push(`${telegram ? '⚠️ ' : '! '}${motivo}`);
501
+ }
502
+ continue;
503
+ }
504
+ try {
505
+ const evento = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, p.thread))
506
+ .find(e => e.tipo === 'hitl_requested' && e.pedido?.id === p.pedidoId);
507
+ (0, hitl_contract_1.validarPedidoHitl)(evento?.pedido);
508
+ const pedido = evento.pedido;
509
+ // O numero aponta para o pedido que saiu, byte a byte. Pedido trocado nao recebe a letra.
510
+ if (sha(JSON.stringify(pedido)) !== p.pedidoSha256)
511
+ throw new Error('pedido antigo: fase, modo ou sessão mudou');
512
+ const derivada = { ...envelope, resposta: (0, hitl_contract_1.chaveDaEscolha)(pedido, indice + 1) };
513
+ 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);
516
+ registradas.push({ numero, thread: p.thread, letra, estado: r.estado, repetida: r.repetida });
517
+ linhas.push(`${telegram ? '✅ ' : ''}${numero} → ${letra}) ${p.alternativas[indice]}: ${p.consequencias[indice]}.`);
518
+ servido = { ...servido, perguntas: servido.perguntas.map(q => q.numero === numero
519
+ ? { ...q, respondida: { letra, em: quando, mensagem: envelope.mensagem, estado: r.estado } } : q) };
520
+ gravarLoteServido(raiz, servido, estadoDir);
521
+ }
522
+ catch (e) {
523
+ const motivo = motivoParaODono(p, letra, e, quando);
524
+ recusas.push({ numero, motivo });
525
+ linhas.push(`${telegram ? '⚠️ ' : '! '}${motivo}`);
526
+ }
527
+ }
528
+ const cabecalho = registradas.length
529
+ ? `${telegram ? '📝 ' : ''}Orkastery, ${registradas.length === 1 ? '1 resposta registrada' : `${registradas.length} respostas registradas`}`
530
+ : `${telegram ? '📝 ' : ''}Orkastery, nenhuma resposta registrada`;
531
+ const pendentes = servido.perguntas.filter(q => perguntaViva(q, quando)).map(q => q.numero);
532
+ 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;
535
+ const texto = [
536
+ cabecalho, '', ...linhas,
537
+ ...(pendentes.length ? ['', `Ainda sem resposta: ${pendentes.join(', ')}.`] : []),
538
+ ...(proximo ? [`Faltam ${proximo.candidatos.length}. Para receber as próximas, responda ${proximo.codigo} a.`] : []),
539
+ ].join('\n');
540
+ return resultado('lote', texto, { registradas, recusas,
541
+ repetida: registradas.length > 0 && recusas.length === 0 && registradas.every(r => r.repetida) });
542
+ }