@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
package/dist/forja.js ADDED
@@ -0,0 +1,435 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mesmaForja = exports.rotuloDaForja = exports.executorPadrao = void 0;
4
+ exports.identidadeDaForja = identidadeDaForja;
5
+ exports.forjaDoArgumento = forjaDoArgumento;
6
+ exports.detalheSeguro = detalheSeguro;
7
+ exports.classificarFalha = classificarFalha;
8
+ exports.lerCommitsDaForja = lerCommitsDaForja;
9
+ exports.lerDaForja = lerDaForja;
10
+ /**
11
+ * RM-054 (fatia 1): a forja do projeto, lida sem clone e so com consulta.
12
+ *
13
+ * `ork/fabrica-estado` e `ork/roadmap-reservas` vivem no remoto de UM repositorio, e um host fora
14
+ * de um clone (o gateway do OpenClaw, um cron) nao enxergava nenhuma das duas. Aqui a leitura vai
15
+ * direto a forja, pela CLI dela ja autenticada (`gh`, `glab`): nenhum token e lido, copiado ou
16
+ * passado em argumento, e so consulta sai daqui (GraphQL sem mutation e GET no REST). O corpo da
17
+ * consulta vai pelo stdin, com os nomes em variaveis: nada do pedido e interpolado no texto.
18
+ *
19
+ * GitHub responde tudo em UMA consulta: a base com `docs/roadmap`, o manifesto e os commits desde
20
+ * `desde`, e as duas branches de estado, cada parte no commit que a propria consulta devolve.
21
+ * GitLab devolve a mesma leitura por consultas GraphQL e pelo REST de commits; sem `glab` na
22
+ * maquina que construiu, o esquema dele esta provado so com resposta simulada (D4 do PLAN).
23
+ *
24
+ * Resposta fora do formato vira erro tipado, nunca lista vazia: quem le nao confunde "nao li" com
25
+ * "nao tem".
26
+ */
27
+ const node_child_process_1 = require("node:child_process");
28
+ const hitl_1 = require("./hitl");
29
+ const policies_1 = require("./policies");
30
+ const PRAZO_MS = 30000;
31
+ const SHA = /^[0-9a-f]{40}$/;
32
+ const SEGMENTO = /^[A-Za-z0-9_.-]+$/;
33
+ /** Hostname de verdade: rotulos alfanumericos separados por ponto (`..` ou `a..b` nao passam). */
34
+ const HOST = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/;
35
+ /** Sem shell, sem token: a CLI usa a autenticacao que ja tem; prompt e aviso de versao desligados. */
36
+ const executorPadrao = (cmd, args, entrada, timeoutMs) => {
37
+ const r = (0, node_child_process_1.spawnSync)(cmd, [...args], {
38
+ input: entrada, encoding: 'utf8', timeout: timeoutMs, maxBuffer: 32 * 1024 * 1024,
39
+ env: { ...process.env, GH_PROMPT_DISABLED: '1', GH_NO_UPDATE_NOTIFIER: '1', GLAB_CHECK_UPDATE: 'false', NO_COLOR: '1' },
40
+ });
41
+ return { status: r.status, stdout: (r.stdout ?? '').toString(), stderr: (r.stderr ?? '').toString(), sinal: r.signal ?? null,
42
+ ...(r.error ? { erro: r.error.code ?? r.error.message } : {}) };
43
+ };
44
+ exports.executorPadrao = executorPadrao;
45
+ // ---------------------------------------------------------------------------
46
+ // Identidade
47
+ // ---------------------------------------------------------------------------
48
+ function tipoDoHost(host) {
49
+ if (host === 'github.com' || /(^|[.-])github([.-]|$)/.test(host))
50
+ return 'github';
51
+ if (host === 'gitlab.com' || /(^|[.-])gitlab([.-]|$)/.test(host))
52
+ return 'gitlab';
53
+ return null;
54
+ }
55
+ function montar(tipo, host, caminho) {
56
+ if (!HOST.test(host))
57
+ return null;
58
+ const partes = caminho.replace(/\/+$/, '').replace(/\.git$/, '').split('/');
59
+ if (partes.some((p) => !SEGMENTO.test(p) || p === '.' || p === '..'))
60
+ return null;
61
+ if (tipo === 'github' ? partes.length !== 2 : partes.length < 2)
62
+ return null;
63
+ return { tipo, host, repo: partes.join('/') };
64
+ }
65
+ /**
66
+ * A forja de uma URL de remoto (https, ssh ou scp). Credencial na URL nunca entra na identidade;
67
+ * caminho local e forja desconhecida voltam null.
68
+ */
69
+ function identidadeDaForja(url) {
70
+ const t = url.trim();
71
+ const esquema = /^(?:https?|ssh|git|git\+ssh|ssh\+git):\/\/(?:[^@/]*@)?([^/:?#@]+)(?::\d+)?\/([^?#\s]+)$/i.exec(t);
72
+ const scp = esquema ? null : /^(?:[^@\s/:]+@)?([^:\s/@]+):(?!\/\/)([^\s]+)$/.exec(t);
73
+ const achado = esquema ?? scp;
74
+ if (!achado)
75
+ return null;
76
+ const host = achado[1].toLowerCase();
77
+ const tipo = tipoDoHost(host);
78
+ return tipo ? montar(tipo, host, achado[2].replace(/^\/+/, '')) : null;
79
+ }
80
+ /** `github:dono/repo`, `gitlab:grupo/sub/repo` (com host proprio: `gitlab:host.tld/grupo/repo`) ou uma URL. */
81
+ function forjaDoArgumento(texto) {
82
+ const t = texto.trim();
83
+ const m = /^(github|gitlab):(?:\/\/)?(.+)$/i.exec(t);
84
+ if (m) {
85
+ const tipo = m[1].toLowerCase();
86
+ const partes = m[2].replace(/^\/+/, '').split('/');
87
+ const comHost = partes.length > 2 && partes[0].includes('.');
88
+ const host = comHost ? partes[0].toLowerCase() : tipo === 'github' ? 'github.com' : 'gitlab.com';
89
+ return montar(tipo, host, (comHost ? partes.slice(1) : partes).join('/'));
90
+ }
91
+ return /^[a-z+]+:\/\//i.test(t) || /^[^@\s/:]+@[^:\s/]+:/.test(t) ? identidadeDaForja(t) : null;
92
+ }
93
+ /** Como a forja aparece para gente: `github.com/orkastery/orkastery`. */
94
+ const rotuloDaForja = (f) => `${f.host}/${f.repo}`;
95
+ exports.rotuloDaForja = rotuloDaForja;
96
+ const mesmaForja = (a, b) => !!a && !!b && a.tipo === b.tipo && a.host === b.host && a.repo.toLowerCase() === b.repo.toLowerCase();
97
+ exports.mesmaForja = mesmaForja;
98
+ // ---------------------------------------------------------------------------
99
+ // Erros
100
+ // ---------------------------------------------------------------------------
101
+ /** O fim da saida de erro, sem segredo: redigido, e descartado inteiro se ainda casar um padrao. */
102
+ function detalheSeguro(texto) {
103
+ const redigido = (0, hitl_1.redigirSegredos)(texto)
104
+ .replace(/\b(?:glpat|gldt|glrt|glptt|glcbt)-[A-Za-z0-9_-]{16,}\b/g, '[redigido]')
105
+ .replace(/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, '[redigido]');
106
+ const linha = redigido.split('\n').map((l) => l.trim()).filter(Boolean).slice(-2).join(' ').slice(0, 240);
107
+ return (0, policies_1.procurarSegredos)(linha).length ? '[detalhe omitido: um padrão de segredo casou]' : linha;
108
+ }
109
+ function erro(codigo, detalhe) {
110
+ return { ok: false, erro: { codigo, detalhe } };
111
+ }
112
+ /** O motivo tipado de uma chamada que falhou. */
113
+ function classificarFalha(cmd, s) {
114
+ if (s.erro === 'ENOENT')
115
+ return { codigo: 'forja.ausente', detalhe: `${cmd} não está instalado nesta máquina` };
116
+ if (s.erro === 'ETIMEDOUT' || s.sinal === 'SIGTERM')
117
+ return { codigo: 'forja.tempo-esgotado', detalhe: `${cmd} não respondeu em ${PRAZO_MS / 1000} s` };
118
+ const texto = `${s.stderr}\n${s.stdout}`;
119
+ const detalhe = detalheSeguro(s.stderr || s.stdout || s.erro || `${cmd} saiu com ${s.status}`);
120
+ if (/"type"\s*:\s*"NOT_FOUND"|Could not resolve to a Repository|404 Not Found|HTTP 404|project not found/i.test(texto)) {
121
+ return { codigo: 'forja.nao-encontrado', detalhe };
122
+ }
123
+ if (/auth login|not logged|authentication|HTTP 401|Bad credentials|GH_TOKEN|GITLAB_TOKEN|unauthorized|no token/i.test(texto)) {
124
+ return { codigo: 'forja.sem-login', detalhe };
125
+ }
126
+ return { codigo: 'forja.inacessivel', detalhe };
127
+ }
128
+ const obj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
129
+ class RespostaInvalida extends Error {
130
+ }
131
+ class NaoEncontrado extends Error {
132
+ }
133
+ const exigir = (condicao, onde) => { if (!condicao)
134
+ throw new RespostaInvalida(onde); };
135
+ function chamarJson(executor, cmd, args, corpo) {
136
+ const s = executor(cmd, args, corpo, PRAZO_MS);
137
+ let json;
138
+ try {
139
+ json = JSON.parse(s.stdout);
140
+ }
141
+ catch {
142
+ json = undefined;
143
+ }
144
+ const erros = obj(json) && Array.isArray(json.errors) && json.errors.length > 0;
145
+ if (s.erro || s.status !== 0 || erros) {
146
+ const falha = classificarFalha(cmd, s.erro || s.status !== 0 ? s : { ...s, status: 1, stderr: JSON.stringify(json) });
147
+ // `gh` sai 1 com NOT_FOUND no corpo: o tipo do erro GraphQL vale mais que a frase.
148
+ if (erros && json.errors.some((e) => obj(e) && e.type === 'NOT_FOUND')) {
149
+ return { ok: false, erro: { codigo: 'forja.nao-encontrado', detalhe: falha.detalhe } };
150
+ }
151
+ return { ok: false, erro: falha };
152
+ }
153
+ if (json === undefined)
154
+ return { ok: false, erro: { codigo: 'forja.resposta-invalida', detalhe: `${cmd} não devolveu JSON` } };
155
+ return { ok: true, json };
156
+ }
157
+ /**
158
+ * O host vai sempre explicito: sem ele, o `gh` usa `GH_HOST` ou o unico host com login, e o `glab`,
159
+ * o host do remoto do cwd; a consulta iria para outra forja.
160
+ */
161
+ const hostnameDe = (f) => ['--hostname', f.host];
162
+ function arquivosDoGithub(entrada, dir) {
163
+ if (entrada === null)
164
+ return null;
165
+ exigir(obj(entrada), `${dir}: entrada`);
166
+ const objeto = entrada.object;
167
+ if (objeto === null)
168
+ return null;
169
+ exigir(obj(objeto) && Array.isArray(objeto.entries), `${dir}: diretorio`);
170
+ return objeto.entries.filter((e) => obj(e) && e.type === 'blob').map((e) => {
171
+ const x = e, blob = obj(x.object) ? x.object : {};
172
+ exigir(typeof x.name === 'string', `${dir}: nome`);
173
+ const legivel = typeof blob.text === 'string' && blob.isBinary !== true && blob.isTruncated !== true;
174
+ return { caminho: `${dir}/${x.name}`, texto: legivel ? blob.text : null };
175
+ });
176
+ }
177
+ function pontaDoGithub(ref, onde) {
178
+ if (ref === null)
179
+ return null;
180
+ exigir(obj(ref) && obj(ref.target), `${onde}: ref`);
181
+ const alvo = ref.target;
182
+ exigir(typeof alvo.oid === 'string' && SHA.test(alvo.oid), `${onde}: commit`);
183
+ return { commit: alvo.oid, dataDoCommit: typeof alvo.committedDate === 'string' ? alvo.committedDate : null,
184
+ nome: typeof ref.name === 'string' ? ref.name : onde, alvo };
185
+ }
186
+ /** Os commits de um `history` do GitHub, e se a forja cortou a pagina (mais de 100 na janela). */
187
+ function commitsDoHistorico(historico, onde) {
188
+ const h = obj(historico) ? historico : {};
189
+ const nos = h.nodes ?? [];
190
+ exigir(Array.isArray(nos), `${onde}: commits`);
191
+ return {
192
+ commits: nos.filter(obj).map((n) => {
193
+ exigir(typeof n.oid === 'string' && typeof n.messageHeadline === 'string' && typeof n.committedDate === 'string', `${onde}: commit`);
194
+ return { commit: n.oid, assunto: n.messageHeadline, data: n.committedDate };
195
+ }),
196
+ parciais: obj(h.pageInfo) && h.pageInfo.hasNextPage === true,
197
+ };
198
+ }
199
+ /** Os commits da base desde `desde`, pelo REST do GitLab (GET), e se vieram 100 (pagina cheia). */
200
+ function commitsDoGitlab(forja, commit, desde, executor) {
201
+ const rest = executor('glab', ['api', `projects/${encodeURIComponent(forja.repo)}/repository/commits?ref_name=${encodeURIComponent(commit)}` +
202
+ `&since=${encodeURIComponent(desde)}&per_page=100`, ...hostnameDe(forja)], '', PRAZO_MS);
203
+ if (rest.erro || rest.status !== 0)
204
+ return { ok: false, erro: classificarFalha('glab', rest) };
205
+ let lista;
206
+ try {
207
+ lista = JSON.parse(rest.stdout);
208
+ }
209
+ catch {
210
+ lista = undefined;
211
+ }
212
+ exigir(Array.isArray(lista), 'commits');
213
+ const commits = lista.map((c) => {
214
+ exigir(obj(c) && typeof c.id === 'string' && typeof c.title === 'string' && typeof c.committed_date === 'string', 'commit');
215
+ return { commit: c.id, assunto: c.title, data: c.committed_date };
216
+ });
217
+ return { ok: true, commits, parciais: commits.length >= 100 };
218
+ }
219
+ function consultaDoGithub(comBase) {
220
+ return `query($owner: String!, $name: String!${comBase ? ', $base: String!' : ''}, $desde: GitTimestamp!, $dirRoadmap: String!,
221
+ $manifesto: String!, $refReservas: String!, $dirReservas: String!, $refFabrica: String!, $dirFabrica: String!) {
222
+ repository(owner: $owner, name: $name) {
223
+ base: ${comBase ? 'ref(qualifiedName: $base)' : 'defaultBranchRef'} { name target { ... on Commit { oid committedDate
224
+ roadmap: file(path: $dirRoadmap) { object { ...Arquivos } }
225
+ manifesto: file(path: $manifesto) { object { ... on Blob { text isBinary isTruncated } } }
226
+ history(since: $desde, first: 100) { pageInfo { hasNextPage } nodes { oid messageHeadline committedDate } } } } }
227
+ reservas: ref(qualifiedName: $refReservas) { name target { ... on Commit { oid committedDate
228
+ arquivos: file(path: $dirReservas) { object { ...Arquivos } } } } }
229
+ fabrica: ref(qualifiedName: $refFabrica) { name target { ... on Commit { oid committedDate
230
+ arquivos: file(path: $dirFabrica) { object { ...Arquivos } } } } }
231
+ }
232
+ }
233
+ fragment Arquivos on Tree { entries { name type object { ... on Blob { text isBinary isTruncated } } } }`;
234
+ }
235
+ function lerDoGithub(forja, pedido, executor, lidoEm) {
236
+ const [owner, name] = forja.repo.split('/');
237
+ const comBase = !!pedido.base;
238
+ const corpo = JSON.stringify({ query: consultaDoGithub(comBase), variables: {
239
+ owner, name, ...(comBase ? { base: `refs/heads/${pedido.base}` } : {}), desde: pedido.desde,
240
+ dirRoadmap: pedido.dirRoadmap, manifesto: pedido.manifesto,
241
+ refReservas: `refs/heads/${pedido.reservas.branch}`, dirReservas: pedido.reservas.dir,
242
+ refFabrica: `refs/heads/${pedido.fabrica.branch}`, dirFabrica: pedido.fabrica.dir,
243
+ } });
244
+ const r = chamarJson(executor, 'gh', ['api', 'graphql', '--method', 'POST', '--input', '-', ...hostnameDe(forja)], corpo);
245
+ if (!r.ok)
246
+ return r;
247
+ const dados = obj(r.json) ? r.json.data : undefined;
248
+ exigir(obj(dados) && obj(dados.repository), 'repositorio');
249
+ const repo = dados.repository;
250
+ const base = pontaDoGithub(repo.base ?? null, 'base');
251
+ let leituraDaBase = null;
252
+ if (base) {
253
+ const manifesto = obj(base.alvo.manifesto) && obj(base.alvo.manifesto.object) &&
254
+ typeof base.alvo.manifesto.object.text === 'string' ? base.alvo.manifesto.object.text : null;
255
+ const historico = commitsDoHistorico(base.alvo.history, 'base');
256
+ leituraDaBase = { ref: base.nome, commit: base.commit, dataDoCommit: base.dataDoCommit,
257
+ arquivos: arquivosDoGithub(base.alvo.roadmap ?? null, pedido.dirRoadmap), parcial: false, manifesto,
258
+ commits: historico.commits, commitsParciais: historico.parciais };
259
+ }
260
+ const estado = (bruto, b) => {
261
+ const p = pontaDoGithub(bruto ?? null, b.branch);
262
+ return p ? { ref: b.branch, commit: p.commit, dataDoCommit: p.dataDoCommit, arquivos: arquivosDoGithub(p.alvo.arquivos ?? null, b.dir),
263
+ parcial: false } : null;
264
+ };
265
+ return { ok: true, leitura: { forja, lidoEm, chamadas: 1, base: leituraDaBase,
266
+ reservas: estado(repo.reservas, pedido.reservas), fabrica: estado(repo.fabrica, pedido.fabrica) } };
267
+ }
268
+ const CONSULTA_RAIZ_GITLAB = 'query($path: ID!) { project(fullPath: $path) { repository { rootRef } } }';
269
+ const CONSULTA_ARVORES_GITLAB = `query($path: ID!, $base: String!, $dirRoadmap: String!, $refReservas: String!, $dirReservas: String!,
270
+ $refFabrica: String!, $dirFabrica: String!) {
271
+ project(fullPath: $path) { repository {
272
+ baseTopo: tree(ref: $base) { lastCommit { sha committedDate } }
273
+ baseDir: tree(ref: $base, path: $dirRoadmap) { blobs(first: 100) { pageInfo { hasNextPage } nodes { name path } } }
274
+ reservasTopo: tree(ref: $refReservas) { lastCommit { sha committedDate } }
275
+ reservasDir: tree(ref: $refReservas, path: $dirReservas) { blobs(first: 100) { pageInfo { hasNextPage } nodes { name path } } }
276
+ fabricaTopo: tree(ref: $refFabrica) { lastCommit { sha committedDate } }
277
+ fabricaDir: tree(ref: $refFabrica, path: $dirFabrica) { blobs(first: 100) { pageInfo { hasNextPage } nodes { name path } } }
278
+ } }
279
+ }`;
280
+ /** Os blobs de cada ponta que existe; so as partes pedidas entram na consulta (texto fixo, sem interpolar o pedido). */
281
+ function consultaDeBlobsDoGitlab(partes) {
282
+ const sufixo = { base: 'Base', reservas: 'Reservas', fabrica: 'Fabrica' };
283
+ const declaracoes = ['$path: ID!', ...partes.flatMap((p) => [`$ref${sufixo[p]}: String!`, `$paths${sufixo[p]}: [String!]!`])];
284
+ const campos = partes.map((p) => `${p}: blobs(ref: $ref${sufixo[p]}, paths: $paths${sufixo[p]}, first: 100) ` +
285
+ '{ pageInfo { hasNextPage } nodes { path rawTextBlob } }');
286
+ return `query(${declaracoes.join(', ')}) { project(fullPath: $path) { repository { ${campos.join(' ')} } } }`;
287
+ }
288
+ function lerDoGitlab(forja, pedido, executor, lidoEm) {
289
+ const args = ['api', 'graphql', '--method', 'POST', '--input', '-', ...hostnameDe(forja)];
290
+ let chamadas = 0;
291
+ const graphql = (query, variables) => { chamadas++; return chamarJson(executor, 'glab', args, JSON.stringify({ query, variables })); };
292
+ const repositorio = (json) => {
293
+ const projeto = obj(json) && obj(json.data) ? json.data.project : undefined;
294
+ if (projeto === null)
295
+ throw new NaoEncontrado();
296
+ exigir(obj(projeto) && obj(projeto.repository), 'repositorio');
297
+ return projeto.repository;
298
+ };
299
+ let base = pedido.base ?? null;
300
+ if (!base) {
301
+ const r = graphql(CONSULTA_RAIZ_GITLAB, { path: forja.repo });
302
+ if (!r.ok)
303
+ return r;
304
+ const raiz = repositorio(r.json).rootRef;
305
+ base = typeof raiz === 'string' && raiz ? raiz : null;
306
+ }
307
+ const arvores = graphql(CONSULTA_ARVORES_GITLAB, { path: forja.repo, base: base ?? 'HEAD', dirRoadmap: pedido.dirRoadmap,
308
+ refReservas: pedido.reservas.branch, dirReservas: pedido.reservas.dir, refFabrica: pedido.fabrica.branch, dirFabrica: pedido.fabrica.dir });
309
+ if (!arvores.ok)
310
+ return arvores;
311
+ const repo = repositorio(arvores.json);
312
+ const topo = (chave) => {
313
+ const arvore = repo[chave];
314
+ if (!obj(arvore) || arvore.lastCommit === null || arvore.lastCommit === undefined)
315
+ return null;
316
+ const c = arvore.lastCommit;
317
+ exigir(obj(c) && typeof c.sha === 'string' && SHA.test(c.sha), `${chave}: commit`);
318
+ return { commit: c.sha, dataDoCommit: typeof c.committedDate === 'string' ? c.committedDate : null };
319
+ };
320
+ const cortadas = new Set();
321
+ const cortou = (conexao, chave) => {
322
+ if (obj(conexao.pageInfo) && conexao.pageInfo.hasNextPage === true)
323
+ cortadas.add(chave);
324
+ };
325
+ const caminhos = (chave) => {
326
+ const arvore = repo[chave];
327
+ if (!obj(arvore))
328
+ return null;
329
+ exigir(obj(arvore.blobs) && Array.isArray(arvore.blobs.nodes), `${chave}: blobs`);
330
+ cortou(arvore.blobs, chave);
331
+ return arvore.blobs.nodes.map((n) => {
332
+ exigir(obj(n) && typeof n.path === 'string', `${chave}: caminho`);
333
+ return n.path;
334
+ });
335
+ };
336
+ const [pontaBase, pontaReservas, pontaFabrica] = [base ? topo('baseTopo') : null, topo('reservasTopo'), topo('fabricaTopo')];
337
+ const dirBase = pontaBase ? caminhos('baseDir') : null;
338
+ const dirReservas = pontaReservas ? caminhos('reservasDir') : null, dirFabrica = pontaFabrica ? caminhos('fabricaDir') : null;
339
+ const pedidos = [
340
+ ['base', pontaBase, pontaBase ? [...(dirBase ?? []), pedido.manifesto] : []],
341
+ ['reservas', pontaReservas, dirReservas ?? []],
342
+ ['fabrica', pontaFabrica, dirFabrica ?? []],
343
+ ].filter(([, ponta, paths]) => ponta && paths.length > 0);
344
+ let conteudo = {};
345
+ if (pedidos.length) {
346
+ const sufixo = { base: 'Base', reservas: 'Reservas', fabrica: 'Fabrica' };
347
+ const variaveis = { path: forja.repo };
348
+ for (const [parte, ponta, paths] of pedidos) {
349
+ variaveis[`ref${sufixo[parte]}`] = ponta.commit;
350
+ variaveis[`paths${sufixo[parte]}`] = paths;
351
+ }
352
+ const blobs = graphql(consultaDeBlobsDoGitlab(pedidos.map(([parte]) => parte)), variaveis);
353
+ if (!blobs.ok)
354
+ return blobs;
355
+ conteudo = repositorio(blobs.json);
356
+ }
357
+ const textos = (chave) => {
358
+ const c = conteudo[chave];
359
+ if (c === null || c === undefined)
360
+ return new Map();
361
+ exigir(obj(c) && Array.isArray(c.nodes), `${chave}: conteudo`);
362
+ cortou(c, chave);
363
+ return new Map(c.nodes.map((n) => {
364
+ exigir(obj(n) && typeof n.path === 'string', `${chave}: blob`);
365
+ return [n.path, typeof n.rawTextBlob === 'string' ? n.rawTextBlob : null];
366
+ }));
367
+ };
368
+ const arquivos = (lista, mapa) => lista === null ? null : lista.map((caminho) => ({ caminho, texto: mapa.get(caminho) ?? null }));
369
+ let commits = [];
370
+ let commitsParciais = false;
371
+ if (pontaBase) {
372
+ chamadas++;
373
+ const r = commitsDoGitlab(forja, pontaBase.commit, pedido.desde, executor);
374
+ if (!r.ok)
375
+ return r;
376
+ commits = r.commits;
377
+ commitsParciais = r.parciais;
378
+ }
379
+ const [mapaBase, mapaReservas, mapaFabrica] = [textos('base'), textos('reservas'), textos('fabrica')];
380
+ const parcial = (dir, conteudo) => cortadas.has(dir) || cortadas.has(conteudo);
381
+ return { ok: true, leitura: { forja, lidoEm, chamadas,
382
+ base: pontaBase ? { ref: base ?? 'HEAD', ...pontaBase, arquivos: arquivos(dirBase, mapaBase), parcial: parcial('baseDir', 'base'),
383
+ manifesto: mapaBase.get(pedido.manifesto) ?? null, commits, commitsParciais } : null,
384
+ reservas: pontaReservas ? { ref: pedido.reservas.branch, ...pontaReservas, arquivos: arquivos(dirReservas, mapaReservas),
385
+ parcial: parcial('reservasDir', 'reservas') } : null,
386
+ fabrica: pontaFabrica ? { ref: pedido.fabrica.branch, ...pontaFabrica, arquivos: arquivos(dirFabrica, mapaFabrica),
387
+ parcial: parcial('fabricaDir', 'fabrica') } : null } };
388
+ }
389
+ const CONSULTA_DE_COMMITS_GITHUB = `query($owner: String!, $name: String!, $oid: GitObjectID!, $desde: GitTimestamp!) {
390
+ repository(owner: $owner, name: $name) { object(oid: $oid) { ... on Commit {
391
+ history(since: $desde, first: 100) { pageInfo { hasNextPage } nodes { oid messageHeadline committedDate } } } } }
392
+ }`;
393
+ /**
394
+ * So os commits de um commit da base desde `desde`, quando a janela da primeira leitura ficou curta:
395
+ * o fuso do projeto so se conhece depois de ler o manifesto dele (achado 5 da rodada 2). So consulta.
396
+ */
397
+ function lerCommitsDaForja(forja, pedido, executor = exports.executorPadrao) {
398
+ if (!SHA.test(pedido.commit))
399
+ return erro('forja.resposta-invalida', 'commit da base fora do formato');
400
+ try {
401
+ if (forja.tipo === 'gitlab')
402
+ return commitsDoGitlab(forja, pedido.commit, pedido.desde, executor);
403
+ const [owner, name] = forja.repo.split('/');
404
+ const r = chamarJson(executor, 'gh', ['api', 'graphql', '--method', 'POST', '--input', '-', ...hostnameDe(forja)], JSON.stringify({ query: CONSULTA_DE_COMMITS_GITHUB, variables: { owner, name, oid: pedido.commit, desde: pedido.desde } }));
405
+ if (!r.ok)
406
+ return r;
407
+ const dados = obj(r.json) ? r.json.data : undefined;
408
+ exigir(obj(dados) && obj(dados.repository), 'repositorio');
409
+ const objeto = dados.repository.object;
410
+ exigir(obj(objeto), 'commit');
411
+ return { ok: true, ...commitsDoHistorico(objeto.history, 'commits') };
412
+ }
413
+ catch (e) {
414
+ if (e instanceof RespostaInvalida)
415
+ return erro('forja.resposta-invalida', `resposta fora do formato esperado em ${e.message}`);
416
+ throw e;
417
+ }
418
+ }
419
+ /**
420
+ * Le da forja a base (com `docs/roadmap`, o manifesto e os commits desde `desde`) e as duas branches
421
+ * de estado. So consulta; qualquer falha volta como erro tipado, com o detalhe redigido.
422
+ */
423
+ function lerDaForja(forja, pedido, executor = exports.executorPadrao) {
424
+ const lidoEm = pedido.lidoEm ?? new Date().toISOString();
425
+ try {
426
+ return forja.tipo === 'github' ? lerDoGithub(forja, pedido, executor, lidoEm) : lerDoGitlab(forja, pedido, executor, lidoEm);
427
+ }
428
+ catch (e) {
429
+ if (e instanceof NaoEncontrado)
430
+ return erro('forja.nao-encontrado', `${(0, exports.rotuloDaForja)(forja)} não existe ou não é visível para o login da forja`);
431
+ if (e instanceof RespostaInvalida)
432
+ return erro('forja.resposta-invalida', `resposta fora do formato esperado em ${e.message}`);
433
+ throw e;
434
+ }
435
+ }
package/dist/gates.js CHANGED
@@ -20,6 +20,7 @@ exports.registrarGateLiberado = registrarGateLiberado;
20
20
  exports.descreverMotivo = descreverMotivo;
21
21
  exports.aprovarGateHumano = aprovarGateHumano;
22
22
  exports.aprovacaoHumanaProvada = aprovacaoHumanaProvada;
23
+ exports.reciboHumanoConfere = reciboHumanoConfere;
23
24
  exports.aprovacoesHumanas = aprovacoesHumanas;
24
25
  const ledger_1 = require("./ledger");
25
26
  const modos_1 = require("./modos");
@@ -44,6 +45,7 @@ exports.DESCRICAO_DO_MOTIVO = {
44
45
  'runtime.rate-limited': 'o runtime bateu o limite de uso da assinatura e a fase morreu antes de terminar (bloco B3: entra na fila duravel e e retomada na janela seguinte)',
45
46
  'runtime.quota-exhausted': 'a cota ou os creditos da conta do runtime acabaram (I-33: o perfil sai do rodizio e a fase segue em outro perfil, outro runtime ou na fila)',
46
47
  'runtime.auth-missing': 'a conta do runtime nao esta autenticada (I-33: o perfil nunca recebe despacho ate o login ser refeito pelo proprio CLI)',
48
+ 'runtime.model-unavailable': 'o modelo pedido nao existe ou a conta nao tem acesso a ele (RM-037: o perfil segue no rodizio e a fase vai a outro perfil com o mesmo modelo ou ao fallback do bloco)',
47
49
  'cost.violation': 'o despacho seria redirecionado para provider pago (violacao de custo: e o unico motivo que NUNCA recebe retry automatico)',
48
50
  'tree.blocked': 'a arvore de destino nao esta disponivel para o merge (branch em check-out com alteracao nao commitada, ou worktree que nao pode ser montada)',
49
51
  'lease.busy': 'lease ocupado por outra thread',
@@ -128,7 +130,14 @@ function formaDeAprovacao(e) {
128
130
  * trocados no disco ou com canal trocado no ledger.
129
131
  */
130
132
  function aprovacaoHumanaProvada(raiz, threadId, e) {
131
- if (!formaDeAprovacao(e))
133
+ return formaDeAprovacao(e) && reciboHumanoConfere(raiz, threadId, e);
134
+ }
135
+ /**
136
+ * K3.1: a mesma prova, para qualquer veredito do gate. O dossiê de decisão mostra a resposta do
137
+ * dono só quando o recibo do ingresso confere; aprovar ou recusar não muda o que se reconfere.
138
+ */
139
+ function reciboHumanoConfere(raiz, threadId, e) {
140
+ if (e.tipo !== ledger_1.TIPOS_DE_EVENTO.pausaHumana)
132
141
  return false;
133
142
  if (e.origem === 'telegram' || e.origem === 'native')
134
143
  return (0, hitl_ingress_receipt_1.validarEvidenciaDoIngresso)(raiz, threadId, e);
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CLASSES_QUE_NAO_BLOQUEIAM = exports.JANELA_PADRAO_MIN = void 0;
3
+ exports.MOTIVOS_DO_ORQUESTRADOR = exports.CLASSES_QUE_NAO_BLOQUEIAM = exports.JANELA_PADRAO_MIN = void 0;
4
4
  exports.atoIrreversivelDoItem = atoIrreversivelDoItem;
5
+ exports.quemDecide = quemDecide;
5
6
  exports.classificarItem = classificarItem;
6
7
  exports.contarClassificacoes = contarClassificacoes;
7
8
  /**
@@ -65,6 +66,15 @@ function atoIrreversivelDoItem(item) {
65
66
  }
66
67
  return hitl_contract_1.ATO_IRREVERSIVEL_DO_MOTIVO[item.motivo];
67
68
  }
69
+ exports.MOTIVOS_DO_ORQUESTRADOR = Object.freeze([
70
+ 'runtime.unavailable', 'runtime.silencio', 'runtime.rate-limited', 'artifact.missing',
71
+ 'verify.failed', 'verify.regression', 'verify.timeout', 'verify.sem-veredito',
72
+ 'claims.failed', 'claims.unverifiable', 'ci.failed', 'hitl.formato',
73
+ 'lease.busy', 'vaga.stale', 'conducao.em-andamento', 'concurrency.limite',
74
+ ]);
75
+ function quemDecide(motivo) {
76
+ return exports.MOTIVOS_DO_ORQUESTRADOR.includes(String(motivo ?? '')) ? 'orquestrador' : 'dono';
77
+ }
68
78
  /** Nome curto da sessao, do jeito que o dono ja ve em `ork board` e nos comandos de log. */
69
79
  const sessaoCurta = (id) => id.slice(0, 8);
70
80
  function classificarItem(item, opcoes) {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CONTRATOS_DE_PEDIDO = exports.CAMPOS_DE_GATE = exports.TETOS_HITL_V2 = exports.LETRAS_DE_ALTERNATIVA = exports.CONTRATO_HITL_V2 = exports.ATO_IRREVERSIVEL_DO_MOTIVO = exports.ATOS_IRREVERSIVEIS = exports.CONTRATO_HITL = void 0;
3
+ exports.CONTRATOS_DE_PEDIDO = exports.CAMPOS_DA_DECISAO_NO_MCP = exports.CAMPOS_DA_DECISAO_NO_CLI = exports.CAMPOS_DE_GATE = exports.TETOS_HITL_V2 = exports.LETRAS_DE_ALTERNATIVA = exports.CONTRATO_HITL_V2 = exports.ATO_IRREVERSIVEL_DO_MOTIVO = exports.ATOS_IRREVERSIVEIS = exports.CONTRATO_HITL = void 0;
4
4
  exports.atoDaPausa = atoDaPausa;
5
5
  exports.profundidadeDoModo = profundidadeDoModo;
6
6
  exports.profundidadeDoPedido = profundidadeDoPedido;
@@ -9,6 +9,7 @@ exports.estadoDoPedido = estadoDoPedido;
9
9
  exports.respostaAceitaDoPedido = respostaAceitaDoPedido;
10
10
  exports.validarRespostaHitl = validarRespostaHitl;
11
11
  exports.vereditoDoGate = vereditoDoGate;
12
+ exports.recusaNaSuperficie = recusaNaSuperficie;
12
13
  exports.validarPedidoHitlV2 = validarPedidoHitlV2;
13
14
  exports.validarPedidoHitl = validarPedidoHitl;
14
15
  exports.ehV2 = ehV2;
@@ -216,24 +217,66 @@ function validarComumV2(v) {
216
217
  throw new Error('pedido HITL v2: identificação, modo, instante ou profundidade inválidos');
217
218
  }
218
219
  }
220
+ /**
221
+ * RM-037 (rm037defeito, defeito 4): o que esta errado num campo de linha unica, ou null. A recusa diz
222
+ * o campo e o motivo real: texto presente e acima do teto respondia "exige o que foi decidido", e quem
223
+ * mandou os tres campos nao tinha como saber que o problema era o tamanho.
224
+ */
225
+ function problemaDaLinha(campo, v, teto) {
226
+ if (typeof v !== 'string' || !v.trim())
227
+ return { ausente: true, texto: `falta ${campo}` };
228
+ if (/[\r\n]/.test(v))
229
+ return { ausente: false, texto: `${campo} tem quebra de linha; cada campo é uma linha só` };
230
+ if (/[\x00-\x08\x0b-\x1f\x7f]/.test(v))
231
+ return { ausente: false, texto: `${campo} tem caractere de controle` };
232
+ if (v.length > teto)
233
+ return { ausente: false, texto: `${campo} tem ${v.length} caracteres; o teto é ${teto}` };
234
+ return null;
235
+ }
236
+ /**
237
+ * Os campos da decisao informada como cada superficie os mostra (achado S4 do CHECK): a recusa do contrato
238
+ * usa o nome interno, e quem digitou `--como-mudar` ou mandou `custoAgora` precisa ler o nome que usou.
239
+ */
240
+ exports.CAMPOS_DA_DECISAO_NO_CLI = Object.freeze({
241
+ decidido: '--decidido', porque: '--porque', comoMudar: '--como-mudar', 'custoDeReverter.agora': '--custo-agora',
242
+ 'custoDeReverter.depois': '--custo-depois', 'criterio.referencia': '--criterio',
243
+ });
244
+ exports.CAMPOS_DA_DECISAO_NO_MCP = Object.freeze({
245
+ decidido: 'decidido', porque: 'porque', comoMudar: 'comoMudar', 'custoDeReverter.agora': 'custoAgora',
246
+ 'custoDeReverter.depois': 'custoDepois', 'criterio.referencia': 'criterio.referencia',
247
+ });
248
+ /** Troca o nome interno do campo, onde a recusa o cita, pelo nome da superficie. */
249
+ function recusaNaSuperficie(mensagem, nomes) {
250
+ return mensagem.replace(/(falta |: )(decidido|porque|comoMudar|custoDeReverter\.agora|custoDeReverter\.depois|criterio\.referencia)(?=\)| tem )/g, (_, antes, campo) => `${antes}${nomes[campo] ?? campo}`);
251
+ }
219
252
  function validarDecidido(v) {
220
253
  for (const campo of exports.CAMPOS_DE_GATE) {
221
254
  if (v[campo] !== undefined)
222
255
  throw new Error(`pedido HITL v2: decisão informada não pode trazer ${campo}`);
223
256
  }
224
- if (!linhaUnica(v.decidido, exports.TETOS_HITL_V2.campoDaDecisao) || !linhaUnica(v.porque, exports.TETOS_HITL_V2.campoDaDecisao) ||
225
- !linhaUnica(v.comoMudar, exports.TETOS_HITL_V2.campoDaDecisao)) {
226
- throw new Error('pedido HITL v2: decisão informada exige o que foi decidido, o porquê e como mudar');
257
+ for (const campo of ['decidido', 'porque', 'comoMudar']) {
258
+ const problema = problemaDaLinha(campo, v[campo], exports.TETOS_HITL_V2.campoDaDecisao);
259
+ if (problema?.ausente)
260
+ throw new Error(`pedido HITL v2: decisão informada exige o que foi decidido, o porquê e como mudar (${problema.texto})`);
261
+ if (problema)
262
+ throw new Error(`pedido HITL v2: decisão informada: ${problema.texto}`);
227
263
  }
228
264
  const custo = v.custoDeReverter;
229
- if (!objeto(custo) || !linhaUnica(custo.agora, exports.TETOS_HITL_V2.custo) || !linhaUnica(custo.depois, exports.TETOS_HITL_V2.custo)) {
265
+ const problemasDoCusto = ['agora', 'depois']
266
+ .map(lado => problemaDaLinha(`custoDeReverter.${lado}`, objeto(custo) ? custo[lado] : undefined, exports.TETOS_HITL_V2.custo));
267
+ if (problemasDoCusto.some(p => p?.ausente)) {
230
268
  throw new Error('pedido HITL v2: custo de reverter exige agora e depois, os dois presentes');
231
269
  }
270
+ const doCusto = problemasDoCusto.find(p => p !== null);
271
+ if (doCusto)
272
+ throw new Error(`pedido HITL v2: custo de reverter: ${doCusto.texto}`);
232
273
  const criterio = v.criterio;
233
- if (!objeto(criterio) || !['manifesto', 'ledger', 'medicao'].includes(criterio.tipo) ||
234
- !linhaUnica(criterio.referencia, 300)) {
274
+ const daReferencia = objeto(criterio) ? problemaDaLinha('criterio.referencia', criterio.referencia, 300) : null;
275
+ if (!objeto(criterio) || !['manifesto', 'ledger', 'medicao'].includes(criterio.tipo) || daReferencia?.ausente) {
235
276
  throw new Error('pedido HITL v2: decisão informada exige critério citado e resolvível');
236
277
  }
278
+ if (daReferencia)
279
+ throw new Error(`pedido HITL v2: critério: ${daReferencia.texto}`);
237
280
  // R1, porta fechada: ato irreversivel nunca se qualifica como decisao informada.
238
281
  if (v.irreversivel === true)
239
282
  throw new Error('pedido HITL v2: ato irreversível nunca é decisão informada');