@orkastery/cli 0.2.0 → 0.3.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.
- package/LICENSE +1 -1
- package/README.md +86 -65
- package/adapters/README.md +30 -15
- package/adapters/claude-code/.claude-plugin/plugin.json +11 -5
- package/adapters/claude-code/README.md +110 -8
- package/adapters/claude-code/commands/onboarding.md +21 -0
- package/adapters/claude-code/commands/ork.md +99 -27
- package/adapters/claude-code/hooks/hooks.json +6 -1
- package/adapters/claude-code/hooks/ork-sensor.js +66 -0
- package/adapters/codex/skills/ork/SKILL.md +57 -0
- package/adapters/hermes/README.md +163 -0
- package/adapters/hermes/bin/ork-abrir-thread.sh +2 -0
- package/adapters/hermes/bin/ork-brain.sh +4 -0
- package/adapters/hermes/bin/ork-hitl-answer.py +82 -0
- package/adapters/hermes/bin/ork-maestro.sh +6 -0
- package/adapters/hermes/bin/ork-master-enviar.py +84 -0
- package/adapters/hermes/bin/ork-objective-message.sh +17 -0
- package/adapters/hermes/bin/ork-objective-status.sh +12 -0
- package/adapters/hermes/bin/ork-pulse-enviar.py +47 -0
- package/adapters/hermes/hermes.plugin.json +26 -4
- package/adapters/hermes/hitl-ingress/__init__.py +356 -0
- package/adapters/hermes/hitl-ingress/plugin.yaml +4 -0
- package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +75 -22
- package/adapters/openclaw/README.md +135 -26
- package/adapters/openclaw/bin/ork-brain.sh +4 -0
- package/adapters/openclaw/construir.sh +19 -0
- package/adapters/openclaw/dist/hitl-ingress.js +199 -0
- package/adapters/openclaw/dist/index.js +437 -0
- package/adapters/openclaw/openclaw.plugin.json +39 -134
- package/adapters/openclaw/package.json +26 -0
- package/adapters/openclaw/src/hitl-ingress.ts +180 -0
- package/adapters/openclaw/src/index.ts +458 -0
- package/adapters/openclaw/src/tipos-openclaw.d.ts +58 -0
- package/adapters/openclaw/tsconfig.json +15 -0
- package/assets/docs/markdownlint-cli2.jsonc +22 -0
- package/assets/docs/padroes/documentacao-de-produto.md +133 -0
- package/assets/docs/padroes/roadmap-de-produto.md +98 -0
- package/assets/docs/produto/README.md +14 -0
- package/assets/docs/produto/_modelo-feature.md +59 -0
- package/assets/docs/roadmap/README.md +14 -0
- package/assets/docs/roadmap/_modelo-item.md +73 -0
- package/assets/orkmind-native-schema.json +32 -0
- package/assets/orkmind_bridge.py +357 -0
- package/assets/orkmind_fixture.py +67 -0
- package/assets/orkmind_prospective.py +138 -0
- package/assets/reference-tariffs-i07.json +23 -0
- package/dist/adapters/claude-bg.js +488 -39
- package/dist/adapters/codex-controller-sensor.js +449 -0
- package/dist/adapters/codex-controller-worker.js +361 -0
- package/dist/adapters/codex-controller.js +252 -0
- package/dist/adapters/codex-events.js +205 -0
- package/dist/adapters/codex-question.js +26 -0
- package/dist/adapters/codex-runner.js +133 -0
- package/dist/adapters/codex.js +394 -0
- package/dist/agents-md.js +84 -0
- package/dist/auditoria.js +11 -8
- package/dist/auditrun.js +5 -4
- package/dist/board.js +173 -22
- package/dist/branch-de-estado.js +136 -0
- package/dist/canarios-hitl.js +177 -0
- package/dist/canarios-i43.js +543 -0
- package/dist/canarios-pulse.js +147 -0
- package/dist/canarios-sensores.js +129 -0
- package/dist/canarios.js +111 -2
- package/dist/catalogo.js +9 -0
- package/dist/ci.js +216 -0
- package/dist/ciclos.js +2 -1
- package/dist/claim-lint.js +64 -0
- package/dist/claims.js +60 -0
- package/dist/company-brain-capture.js +195 -0
- package/dist/company-brain-cli.js +123 -0
- package/dist/company-brain-client.js +61 -0
- package/dist/company-brain-contract.js +166 -0
- package/dist/company-brain-journal.js +192 -0
- package/dist/company-brain-mcp.js +33 -0
- package/dist/company-brain-migration.js +60 -0
- package/dist/company-brain-source.js +177 -0
- package/dist/company-brain-worker.js +18 -0
- package/dist/conducao-texto.js +61 -0
- package/dist/conducao.js +876 -0
- package/dist/contrato-publico.js +39 -0
- package/dist/creation-operation-store.js +251 -0
- package/dist/creation-operation.js +148 -0
- package/dist/decisao-autonoma.js +183 -0
- package/dist/delegation.js +79 -0
- package/dist/demo.js +120 -0
- package/dist/docs.js +828 -0
- package/dist/doctor.js +183 -24
- package/dist/entrega-pr.js +117 -0
- package/dist/escopo-escrita.js +71 -0
- package/dist/estado-thread.js +222 -0
- package/dist/evalrunner.js +12 -0
- package/dist/fabrica-estado.js +329 -0
- package/dist/fabrica-publicar.js +76 -0
- package/dist/fix.js +21 -0
- package/dist/gates.js +55 -9
- package/dist/handoff.js +39 -23
- package/dist/hitl-canais.js +333 -0
- package/dist/hitl-classificacao.js +131 -0
- package/dist/hitl-contract.js +465 -0
- package/dist/hitl-estado.js +136 -0
- package/dist/hitl-gates.js +554 -0
- package/dist/hitl-ingress-receipt.js +382 -0
- package/dist/hitl-local-atestado.js +57 -0
- package/dist/hitl-local-receipt.js +328 -0
- package/dist/hitl-local.js +143 -0
- package/dist/hitl-lock.js +139 -0
- package/dist/hitl-lote.js +223 -0
- package/dist/hitl-native-offer.js +97 -0
- package/dist/hitl-native.js +65 -0
- package/dist/hitl-presentation.js +209 -0
- package/dist/hitl-public-receipt.js +176 -0
- package/dist/hitl-resumo.js +209 -0
- package/dist/hitl-sessions.js +475 -0
- package/dist/hitl.js +711 -0
- package/dist/horario.js +269 -0
- package/dist/hosts.js +198 -22
- package/dist/index.js +1736 -93
- package/dist/indice.js +99 -0
- package/dist/init.js +28 -3
- package/dist/integracoes-locais.js +17 -0
- package/dist/leases.js +65 -21
- package/dist/ledger-stats.js +272 -0
- package/dist/ledger.js +107 -2
- package/dist/licoes.js +215 -0
- package/dist/liveness.js +218 -0
- package/dist/maestro-actions.js +51 -0
- package/dist/maestro-authority.js +152 -0
- package/dist/maestro-cli.js +97 -0
- package/dist/maestro-contract.js +55 -0
- package/dist/maestro-discovery.js +132 -0
- package/dist/maestro-runtime.js +268 -0
- package/dist/maestro-snapshot.js +86 -0
- package/dist/maestro-sources.js +224 -0
- package/dist/manifest.js +165 -6
- package/dist/maquina.js +102 -0
- package/dist/master-audit.js +101 -0
- package/dist/master-batch.js +43 -0
- package/dist/master-digest.js +149 -0
- package/dist/master-migracao.js +155 -0
- package/dist/master.js +361 -77
- package/dist/mcp-artifacts.js +234 -0
- package/dist/mcp-git.js +433 -0
- package/dist/mcp-install.js +310 -0
- package/dist/mcp-maestro.js +25 -0
- package/dist/mcp-server.js +469 -0
- package/dist/mcp-ship.js +436 -0
- package/dist/mcp-verify.js +173 -0
- package/dist/memoria-humana.js +207 -0
- package/dist/memoria.js +314 -78
- package/dist/memory-migration.js +279 -0
- package/dist/memory-prospective.js +148 -0
- package/dist/modos-migracao.js +191 -0
- package/dist/modos.js +151 -15
- package/dist/monitor-lock.js +110 -0
- package/dist/objective.js +449 -0
- package/dist/ocupacao.js +267 -0
- package/dist/onboarding.js +303 -0
- package/dist/orkmind.js +389 -90
- package/dist/orquestracao.js +96 -69
- package/dist/phase.js +596 -150
- package/dist/playbook-capabilities.js +150 -0
- package/dist/playbook-contracts.js +188 -0
- package/dist/portfolio-context.js +61 -0
- package/dist/portfolio.js +180 -0
- package/dist/preflight.js +207 -0
- package/dist/process-audit.js +112 -0
- package/dist/project-state.js +93 -0
- package/dist/prompts.js +25 -5
- package/dist/prova-minima.js +112 -0
- package/dist/pulse-cadencia.js +163 -0
- package/dist/pulse-consentimento.js +260 -0
- package/dist/pulse-delivery.js +323 -0
- package/dist/pulse-resposta.js +542 -0
- package/dist/pulse.js +262 -0
- package/dist/ratelimit.js +35 -2
- package/dist/recall.js +80 -3
- package/dist/redacao-saida.js +45 -0
- package/dist/redacao-url.js +66 -0
- package/dist/retry.js +637 -115
- package/dist/roadmap-reservas.js +243 -0
- package/dist/runtime-ambiente.js +38 -0
- package/dist/runtime-context.js +215 -0
- package/dist/runtime-profiles.js +869 -0
- package/dist/runtimes.js +103 -0
- package/dist/sandbox.js +9 -0
- package/dist/session-events.js +203 -0
- package/dist/session-watcher-claude.js +637 -0
- package/dist/session-watcher.js +683 -0
- package/dist/sessoes-adopt.js +154 -0
- package/dist/sessoes-inventario.js +159 -0
- package/dist/sessoes.js +16 -35
- package/dist/setup.js +691 -0
- package/dist/ship.js +150 -15
- package/dist/slug.js +1 -1
- package/dist/thread-close.js +115 -0
- package/dist/thread.js +142 -28
- package/dist/tokens.js +1 -1
- package/dist/util.js +4 -2
- package/dist/verify-sandbox.js +265 -0
- package/dist/verify.js +270 -25
- package/dist/versao.js +60 -0
- package/dist/worktree.js +31 -2
- package/dist/write-activation.js +291 -0
- package/dist/yaml.js +2 -0
- package/eval/casos/onboarding.json +102 -0
- package/eval/casos/orkastery-bootstrap.json +35 -1
- package/eval/casos/scope-check-capability-map.json +45 -7
- package/eval/casos/ship-release.json +2 -2
- package/eval/fixtures/b0-slug-e-modos/caso.json +35 -22
- package/eval/fixtures/b2-master-log/caso.json +1 -1
- package/eval/fixtures/fx-adapter-editado-detectado/caso.json +19 -0
- package/eval/fixtures/fx-auto-quiet/caso.json +11 -0
- package/eval/fixtures/fx-blanket-approve/caso.json +10 -0
- package/eval/fixtures/fx-check-runtime-cruzado/caso.json +20 -0
- package/eval/fixtures/fx-codex-dry/caso.json +18 -0
- package/eval/fixtures/fx-donewhen-executavel/caso.json +15 -0
- package/eval/fixtures/fx-estado-dividido/caso.json +15 -0
- package/eval/fixtures/fx-fase-orfa/caso.json +116 -0
- package/eval/fixtures/fx-happy/caso.json +6 -4
- package/eval/fixtures/fx-hitl-latency/caso.json +20 -0
- package/eval/fixtures/fx-indice-reversao/caso.json +25 -0
- package/eval/fixtures/fx-listagem-abertas/caso.json +16 -0
- package/eval/fixtures/fx-maestro-bootstrap/caso.json +21 -0
- package/eval/fixtures/fx-modo-aposentado-escritor/caso.json +21 -0
- package/eval/fixtures/fx-modo-aposentado-leitor/caso.json +16 -0
- package/eval/fixtures/fx-objective-oscillation/caso.json +14 -0
- package/eval/fixtures/fx-omnicanal/caso.json +17 -0
- package/eval/fixtures/fx-sensores-runtime/caso.json +17 -0
- package/monitor/company-brain.cjs +17 -0
- package/monitor/pulse-scope.cjs +45 -0
- package/monitor/pulse.cron +18 -0
- package/monitor/varredura-pulse.sh +27 -0
- package/package.json +25 -4
- package/schemas/claims.schema.json +25 -0
- package/schemas/company-brain.schema.json +1158 -0
- package/schemas/creation-operation.schema.json +365 -0
- package/schemas/maestro-snapshot.schema.json +2716 -0
- package/skills/core/onboarding/SKILL.md +50 -0
- package/skills/core/orkastery-bootstrap/SKILL.md +95 -65
- package/skills/core/thread-state/SKILL.md +3 -1
- package/skills/governance/scope-check-capability-map/SKILL.md +17 -7
|
@@ -1,18 +1,48 @@
|
|
|
1
1
|
# Adaptador OpenClaw
|
|
2
2
|
|
|
3
|
-
O OpenClaw recebe o Orkastery como
|
|
4
|
-
|
|
3
|
+
O OpenClaw recebe o Orkastery como uma **extensao** no formato 2026.7.1: um pacote com
|
|
4
|
+
`package.json` (`openclaw.extensions: ["./dist/index.js"]`), um entry JS que registra as
|
|
5
|
+
**21 tools `ork_*`** via `defineToolPlugin` do SDK do proprio OpenClaw, e o manifesto
|
|
6
|
+
`openclaw.plugin.json` gerado (`id`, `activation`, `contracts.tools`). Cada tool e uma
|
|
7
|
+
chamada de CLI do `ork`, sem regra de negocio no host.
|
|
8
|
+
|
|
9
|
+
## Estrutura do pacote
|
|
10
|
+
|
|
11
|
+
| Arquivo | Papel |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `package.json` | `openclaw.extensions` aponta o entry; sem dependencia de runtime |
|
|
14
|
+
| `dist/index.js` | Entry construido; unico arquivo com o placeholder `{{ork_bin}}` |
|
|
15
|
+
| `openclaw.plugin.json` | Manifesto GERADO por `openclaw plugins build` (nao editar na mao) |
|
|
16
|
+
| `src/index.ts` | Fonte TS; `construir.sh` compila com o tsc do core |
|
|
17
|
+
| `bin/ork-abrir-thread.sh` | Atalho de abertura de thread sem reimplementar o parse da #TAG |
|
|
18
|
+
|
|
19
|
+
O entry importa `openclaw/plugin-sdk/tool-plugin`, que o loader do OpenClaw resolve por
|
|
20
|
+
alias: o pacote nao precisa de `node_modules` no destino. Os parametros das tools sao JSON
|
|
21
|
+
Schema literal (sem typebox). Se uma versao futura do `openclaw plugins validate` exigir
|
|
22
|
+
typebox, adicione-o em `dependencies` e rode `npm install` no destino.
|
|
5
23
|
|
|
6
24
|
## Instalacao
|
|
7
25
|
|
|
8
26
|
```bash
|
|
9
|
-
ork adapter install openclaw # instala em <projeto>/.openclaw
|
|
10
|
-
ork adapter install openclaw --dir ~/.openclaw # instala para a maquina
|
|
27
|
+
ork adapter install openclaw # instala em <projeto>/.openclaw/extensions/orkastery
|
|
28
|
+
ork adapter install openclaw --dir ~/.openclaw # instala para a maquina, no source root global
|
|
11
29
|
ork adapter install openclaw --dry-run # lista o que seria escrito
|
|
12
30
|
```
|
|
13
31
|
|
|
14
|
-
O instalador renderiza `{{ork_bin}}` com o caminho absoluto do `ork`
|
|
15
|
-
`INSTALADO.json` com a origem e o sha256 de cada arquivo.
|
|
32
|
+
O instalador renderiza `{{ork_bin}}` no `dist/index.js` com o caminho absoluto do `ork`
|
|
33
|
+
desta maquina e grava `INSTALADO.json` com a origem e o sha256 de cada arquivo.
|
|
34
|
+
|
|
35
|
+
**Registro no OpenClaw.** Com `--dir ~/.openclaw`, o destino
|
|
36
|
+
`~/.openclaw/extensions/orkastery` ja e o source root `global` que o
|
|
37
|
+
`openclaw plugins list` varre: reinicie o gateway e confira. Alternativas equivalentes:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
openclaw plugins install ~/.openclaw/extensions/orkastery # registra explicitamente
|
|
41
|
+
# ou aponte plugins.load.paths para o diretorio instalado na config do OpenClaw
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Se a copia nao passou pelo instalador (placeholder nao renderizado), o entry cai no
|
|
45
|
+
fallback: usa `ORK_BIN` do ambiente ou o `ork` do PATH.
|
|
16
46
|
|
|
17
47
|
## As tools
|
|
18
48
|
|
|
@@ -27,37 +57,116 @@ O instalador renderiza `{{ork_bin}}` com o caminho absoluto do `ork` desta maqui
|
|
|
27
57
|
| `ork_claims_add` | Registra alegacao verificavel com o comando que a comprova |
|
|
28
58
|
| `ork_verify` / `ork_verify_baseline` | Reexecuta no HEAD real; grava a baseline antes do GO |
|
|
29
59
|
| `ork_worktree_ensure` / `ork_worktree_audit` | Worktree isolada, conferida no proprio git |
|
|
30
|
-
| `
|
|
60
|
+
| `ork_objective_status` | Retoma o ticket canônico compartilhado com o Kanban |
|
|
61
|
+
| `ork_objective_message` | Preserva no ticket uma mensagem do canal autenticado |
|
|
62
|
+
| `ork_portfolio_list` | Lista produtos, projetos e iniciativas canônicos |
|
|
63
|
+
| `ork_gate_answer` | Resposta humana correlacionada a um gate |
|
|
64
|
+
| `ork_session_answer` | Resposta humana correlacionada a uma sessão |
|
|
31
65
|
| `ork_ship` | Merge serializado por lease e push provado contra o remoto |
|
|
32
66
|
| `ork_master` | POSTMORTEM tipado e o score HUMANO de 0 a 5 |
|
|
33
67
|
| `ork_board` / `ork_master_batch` | Escalonador e fila de score |
|
|
34
68
|
|
|
35
69
|
## A #TAG de conducao
|
|
36
70
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
71
|
+
O manifesto novo do OpenClaw nao tem campo proprio para isso, entao a regra vive aqui e
|
|
72
|
+
nas descricoes das tools: `ork_modo_do_pedido` devolve JSON com o modo, a tag, se veio do
|
|
73
|
+
pedido ou do `conduction.default_mode`, e quantas pausas humanas aquele modo tem. O host
|
|
74
|
+
passa o resultado a `ork_thread_new` e **nao decide nada**: `conduction.allowed_modes` e
|
|
75
|
+
conferido dentro do `ork thread new`, que recusa com erro tipado quando o projeto nao
|
|
76
|
+
permite o modo.
|
|
41
77
|
|
|
42
78
|
## Os 3 pitfalls de instalacao
|
|
43
79
|
|
|
44
|
-
1. **Placeholder `{{ork_bin}}` que fica no arquivo.** O
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`grep -c '{{' <destino>/
|
|
80
|
+
1. **Placeholder `{{ork_bin}}` que fica no arquivo.** O unico placeholder renderizavel
|
|
81
|
+
vive no `dist/index.js`. Nao renderizado, o entry cai no fallback (`ORK_BIN` ou o `ork`
|
|
82
|
+
do PATH); sem fallback possivel, toda tool viraria "comando nao encontrado", e a sessao
|
|
83
|
+
so descobriria depois de ja ter aberto thread na cabeca do agente. Confira com
|
|
84
|
+
`grep -c '{{' <destino>/extensions/orkastery/dist/index.js`, que precisa devolver `0`.
|
|
85
|
+
|
|
86
|
+
2. **Argumento com espaco quebrado em varios argumentos.** Cada tool monta o comando como
|
|
87
|
+
**vetor de argv** e chama o `ork` sem shell, exatamente para que um pedido com espaco,
|
|
88
|
+
aspas ou quebra de linha chegue inteiro em `--prompt`. Se voce adaptar uma tool,
|
|
89
|
+
mantenha o vetor: uma string de shell aqui vira injecao de comando com o texto do
|
|
90
|
+
builder dentro.
|
|
91
|
+
|
|
92
|
+
3. **Tool que reimplementa regra do nucleo.** A tentacao e criar uma tool "esperta" que
|
|
93
|
+
decide o modo, monta o prompt ou escolhe a base. Toda tool deste plugin e uma chamada
|
|
94
|
+
de CLI e nada mais; a decisao que parece faltar aqui ja existe do outro lado, com
|
|
95
|
+
teste. Tool nova que precisa de logica e sinal de que a logica pertence ao `ork`.
|
|
49
96
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
97
|
+
## Como reconstruir
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
bash adapters/openclaw/construir.sh # tsc do core compila src/ para dist/
|
|
101
|
+
cd adapters/openclaw && openclaw plugins build --entry ./dist/index.js # regenera o manifesto
|
|
102
|
+
cd adapters/openclaw && openclaw plugins validate --entry ./dist/index.js # Plugin orkastery is valid.
|
|
103
|
+
```
|
|
54
104
|
|
|
55
|
-
|
|
56
|
-
modo, monta o prompt ou escolhe a base. Toda tool deste plugin e uma chamada de CLI e nada
|
|
57
|
-
mais; a decisao que parece faltar aqui ja existe do outro lado, com teste. Tool nova que precisa
|
|
58
|
-
de logica e sinal de que a logica pertence ao `ork`.
|
|
105
|
+
O `dist/` fica commitado: o instalador copia arquivos do repositorio, nao roda build.
|
|
59
106
|
|
|
60
107
|
## O que este adaptador nao faz
|
|
61
108
|
|
|
62
|
-
Nao escreve codigo de produto, nao decide gate e nao valida modo. Ele traduz intencao em
|
|
63
|
-
`ork`, apresenta o resultado ao humano e registra o que o humano decidiu.
|
|
109
|
+
Nao escreve codigo de produto, nao decide gate e nao valida modo. Ele traduz intencao em
|
|
110
|
+
chamada de `ork`, apresenta o resultado ao humano e registra o que o humano decidiu.
|
|
111
|
+
|
|
112
|
+
## Contas por runtime (I-33)
|
|
113
|
+
|
|
114
|
+
Este host tem superfície CLI, e a paridade com os hosts MCP vem pelos mesmos comandos do
|
|
115
|
+
núcleo; a superfície MCP de `accounts` não existe neste ciclo (o `add` é interativo e local, e
|
|
116
|
+
a leitura de estado já sai de `ork_observe`).
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
ork accounts list [--json] # perfis por runtime, sem segredo
|
|
120
|
+
ork accounts add <id> --runtime claude-bg|codex --dir <diretorio>
|
|
121
|
+
ork accounts check [<id>] # confere o login pelo proprio CLI
|
|
122
|
+
ork accounts remove <id> # desativa; diretorio e login ficam
|
|
123
|
+
ork setup <modo> --bloco N --fallback codex:<modelo>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
O login roda no terminal do operador, pelo próprio CLI do runtime com o env do perfil; o
|
|
127
|
+
adaptador nunca recebe, guarda ou repassa credencial, e a conversa nunca é canal de login.
|
|
128
|
+
Quando uma conta esgota a cota, o crédito ou o limite do plano, ou perde o login, o
|
|
129
|
+
`ork retry run` rotaciona e grava `runtime_profile_rotated`; o host só apresenta o evento e a
|
|
130
|
+
pendência humana quando houver. A troca para outro perfil do mesmo runtime por esgotamento vem
|
|
131
|
+
ligada por padrão desde a decisão do dono de 19/09/2026
|
|
132
|
+
(`runtime_profiles.rotate_same_runtime_on_quota: true`; `false` desliga), vale só para perfis
|
|
133
|
+
que o operador cadastrou e autenticou pelo CLI oficial, para contas que ele tem direito de usar
|
|
134
|
+
sob os termos do provedor, e nunca acontece no rate limit comum, que espera a janela na fila
|
|
135
|
+
(ver `SECURITY.md` e `docs/guias/verificacao.md` na raiz do repositório).
|
|
136
|
+
|
|
137
|
+
## Respostas HITL
|
|
138
|
+
|
|
139
|
+
`ork_gate_answer` e `ork_session_answer` recebem `thread`, `pedido` e `updateTelegram` original do gateway autenticado. Configure `ORK_HITL_TELEGRAM_USERS` e `ORK_HITL_TELEGRAM_CHATS` com os ids permitidos, separados por vírgula. A mensagem responde ao alerta com a linha `ork-hitl <thread> <pedido>`; callback contém `ork:<thread>:<pedido>:<opção>`. Bots, origem não autorizada e correlação incorreta são recusados antes do CLI.
|
|
140
|
+
|
|
141
|
+
O gateway autenticado é a fronteira de confiança: conteúdo de conversa ou JSON criado por agente não comprova resposta humana. `ork_gate_approve` foi retirado do catálogo; migre o host para os comandos de resposta. Expiração nunca aprova, e delegação não pode aprovar push nem score. O núcleo fornece a profundidade da apresentação.
|
|
142
|
+
|
|
143
|
+
Verificação: `sh adapters/openclaw/construir.sh && node --test adapters/openclaw/test/hitl-answer.test.mjs`.
|
|
144
|
+
# Entrada Maestro
|
|
145
|
+
|
|
146
|
+
O callback `inbound_claim` possui ingresso adicional `native` sem Telegram quando
|
|
147
|
+
o SDK comprova `commandAuthorized`, `senderIsOwner`, sessão e identidade do contexto.
|
|
148
|
+
Bindings/chave privados `ORK_HITL_NATIVE_BINDING_OPENCLAW`/`ORK_HITL_NATIVE_KEY_OPENCLAW`
|
|
149
|
+
pertencem ao processo do gateway e ao núcleo, nunca ao modelo. Ausência de campo,
|
|
150
|
+
conta/canal/sessão divergente ou recibo inválido mantém o pedido pendente. A resposta
|
|
151
|
+
usa `/ork gate|session <thread> <pedido> <resposta>` na mesma conversa. Replay não
|
|
152
|
+
reenvia confirmação. A homologação live exige instalação/ativação pós-SHIP.
|
|
153
|
+
|
|
154
|
+
`orkastery maestro` roteia para `ork_maestro`, consulta JSON do núcleo com argv sem
|
|
155
|
+
shell. O panorama não abre trabalho; fonte indisponível permanece explícita.
|
|
156
|
+
Sessão filha executa o bloco recebido. A apresentação HITL prioriza tópicos,
|
|
157
|
+
recomendação e opções rotuladas, sem UUID exposto. Telegram é opcional; ingresso
|
|
158
|
+
nativo adicional só fica disponível após prova do callback e configuração do host.
|
|
159
|
+
Instalação fixture/SDK simulado não comprova ativação em uma sessão real.
|
|
160
|
+
|
|
161
|
+
Na callback autenticada, `/ork offer <thread> <pedido>` consulta a pergunta e a
|
|
162
|
+
oferta nativa sem responder ao pedido. O gateway assina uma prova efêmera da
|
|
163
|
+
mensagem e chama `gate context --native-offer-stdin`; só uma prova válida para
|
|
164
|
+
aquele host, sessão e pedido anuncia disponibilidade na conversa. Configuração
|
|
165
|
+
isolada, prova vencida ou texto produzido pelo modelo não demonstram callback.
|
|
166
|
+
|
|
167
|
+
Antes de executar tools, o helper fixo `ork receipt-verifiers --json` autentica e
|
|
168
|
+
prepara provas públicas dos recibos legados no projeto de `ORK_HITL_ROOT`, definido
|
|
169
|
+
pelo gateway. Os bytes dos recibos e o ledger permanecem preservados. Só os
|
|
170
|
+
verificadores públicos seguem ao executor; variáveis `ORK_HITL_*` são removidas.
|
|
171
|
+
Autoridade presente com projeto, preparação ou verificadores indisponíveis recusa
|
|
172
|
+
a execução da tool (`hitl.receipt.verifiers-unavailable`).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Compila o entry TS do plugin para dist/ usando o tsc do core.
|
|
3
|
+
#
|
|
4
|
+
# Script de DESENVOLVIMENTO, rodado no repositorio do Orkastery; o dist/ gerado
|
|
5
|
+
# fica commitado porque o `ork adapter install openclaw` copia arquivos do
|
|
6
|
+
# repo, nao roda build. No diretorio instalado este script nao funciona (nao ha
|
|
7
|
+
# core/ do lado) e nao precisa: o que roda la e o dist/index.js.
|
|
8
|
+
|
|
9
|
+
set -eu
|
|
10
|
+
|
|
11
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
12
|
+
TSC="$DIR/../../core/node_modules/.bin/tsc"
|
|
13
|
+
|
|
14
|
+
if [ ! -x "$TSC" ]; then
|
|
15
|
+
echo "tsc nao encontrado em $TSC; rode npm ci em core/ antes" >&2
|
|
16
|
+
exit 2
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
exec "$TSC" -p "$DIR/tsconfig.json"
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/** Native inbound_claim hook only. Never register this callback as a model tool. */
|
|
2
|
+
/** D12: this host is the `openclaw` channel, and the channel is signed, not labelled. */
|
|
3
|
+
import { createHash, createHmac } from 'node:crypto';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
/**
|
|
6
|
+
* I-41 (GO-FIX 1): the two shapes of an answer to the pulse summary, typed with no slash and no
|
|
7
|
+
* identifier: "P4EJ a" answers the summary and "1a 2c" answers the questions. The SAME sources
|
|
8
|
+
* live in the core (GRAMATICA_DO_PULSE, core/src/pulse-resposta.ts) and a core test checks they
|
|
9
|
+
* stay equal. The code starts with a letter and has a digit: ordinary chat never has this shape,
|
|
10
|
+
* and it never reads as an answer to the batch, which starts with the question number.
|
|
11
|
+
*/
|
|
12
|
+
const PULSE_CONSENT = new RegExp("^[ \\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}$", 'i');
|
|
13
|
+
const PULSE_LOTE = new RegExp("^[ \\t]*[0-9]{1,2}[ \\t]*[a-zA-Z](?:[ \\t,;]*[0-9]{1,2}[ \\t]*[a-zA-Z])*[ \\t]*[.!]?[ \\t]*$");
|
|
14
|
+
/** I-50 (RM-039): the pulse cadence tag, alone in the message. It starts with '#', so it never reads as the other two. */
|
|
15
|
+
const PULSE_CADENCIA = new RegExp("^[ \\t]*#OrkPulse(?:On(?:-(?:15|30|60)m)?|Off)[ \\t]*[.!]?[ \\t]*$", 'i');
|
|
16
|
+
/** The pulse address inside the signed body: neither a thread nor a request. The core translates. */
|
|
17
|
+
const PULSE_ALVO = 'pulse', PULSE_ENDERECO = 'resposta';
|
|
18
|
+
export function isPulseAnswer(text) {
|
|
19
|
+
return !text.includes('\0') && (PULSE_CONSENT.test(text) || PULSE_LOTE.test(text) || PULSE_CADENCIA.test(text));
|
|
20
|
+
}
|
|
21
|
+
export function registerHitlIngress(api, bin) {
|
|
22
|
+
api.on('inbound_claim', async (event, ctx) => {
|
|
23
|
+
if (typeof event.content !== 'string')
|
|
24
|
+
return { handled: false };
|
|
25
|
+
const pulse = !event.content.startsWith('/ork ') && isPulseAnswer(event.content);
|
|
26
|
+
if (!event.content.startsWith('/ork ') && !pulse)
|
|
27
|
+
return { handled: false };
|
|
28
|
+
const denied = { handled: true, reply: { text: 'Resposta HITL não confirmada. Confira ingresso e ledger do pedido.' } };
|
|
29
|
+
// The pulse summary goes out on Telegram; only there is an answer to it recognized.
|
|
30
|
+
if (event.channel !== 'telegram')
|
|
31
|
+
return pulse ? { handled: false } : nativeIngress(event, ctx, bin).catch(() => denied);
|
|
32
|
+
try {
|
|
33
|
+
const env = process.env, user = event.senderId ?? '', chat = event.conversationId ?? '', message = event.messageId ?? '';
|
|
34
|
+
const allowed = (key, value) => (env[key] ?? '').split(',').map(s => s.trim()).filter(Boolean).includes(value);
|
|
35
|
+
const age = Date.now() - Number(event.timestamp);
|
|
36
|
+
if (event.channel !== 'telegram' || ctx.channelId !== 'telegram' || event.commandAuthorized !== true ||
|
|
37
|
+
!env.ORK_HITL_OPENCLAW_ACCOUNT || event.accountId !== env.ORK_HITL_OPENCLAW_ACCOUNT || event.accountId !== ctx.accountId ||
|
|
38
|
+
!env.ORK_HITL_TELEGRAM_BOT_ID || user === env.ORK_HITL_TELEGRAM_BOT_ID ||
|
|
39
|
+
!/^\d+$/.test(user) || !/^-?\d+$/.test(chat) || !/^\d+$/.test(message) ||
|
|
40
|
+
user !== ctx.senderId || chat !== ctx.conversationId || message !== ctx.messageId ||
|
|
41
|
+
!allowed('ORK_HITL_TELEGRAM_USERS', user) || !allowed('ORK_HITL_TELEGRAM_CHATS', chat) ||
|
|
42
|
+
!Number.isFinite(age) || age < 0 || age > 60000)
|
|
43
|
+
return denied;
|
|
44
|
+
const match = /^\/ork (gate|session) ([a-zA-Z0-9][a-zA-Z0-9._-]{0,79}) ([a-zA-Z0-9][a-zA-Z0-9._-]{0,79}) ([\s\S]{1,4096})$/.exec(event.content);
|
|
45
|
+
// FX1: this channel signs with ITS OWN key. The shared ORK_HITL_INGRESS_KEY is kept
|
|
46
|
+
// for the legacy v1 envelope only, which this adapter no longer produces.
|
|
47
|
+
const key = env.ORK_HITL_INGRESS_KEY_OPENCLAW, root = env.ORK_HITL_ROOT;
|
|
48
|
+
if ((!pulse && !match) || event.content.includes('\0') || !key || Buffer.byteLength(key) < 32 || !root?.startsWith('/'))
|
|
49
|
+
return denied;
|
|
50
|
+
if (pulse)
|
|
51
|
+
return pulseIngress(event.content, { user, chat, message, key, root, bin, timestamp: Number(event.timestamp) }, denied);
|
|
52
|
+
const [, operation, thread, pedido, resposta] = match;
|
|
53
|
+
const canal = 'openclaw';
|
|
54
|
+
// FX5: the authorized account goes INSIDE the signed body. Checking `accountId` only
|
|
55
|
+
// here left the durable receipt proving generic telegram transport and nothing else:
|
|
56
|
+
// nobody reading it later could tell which OpenClaw account the decision came from.
|
|
57
|
+
const conta = env.ORK_HITL_OPENCLAW_ACCOUNT;
|
|
58
|
+
const envelope = { resposta, canal, conta, origem: 'telegram', por: `telegram:${user}`, mensagem: `telegram:${chat}:${message}`,
|
|
59
|
+
recebidoEm: new Date(Number(event.timestamp)).toISOString(), prova: '' };
|
|
60
|
+
envelope.prova = createHmac('sha256', key).update(JSON.stringify(['ork.hitl-answer/v2', thread, pedido, canal, conta,
|
|
61
|
+
envelope.origem, envelope.por, envelope.mensagem, envelope.recebidoEm, resposta])).digest('hex');
|
|
62
|
+
// `gate answer` exige --resposta-stdin; `sessions answer` exige --stdin. A mesma flag
|
|
63
|
+
// para os dois fazia o CLI recusar toda resposta de sessao antes de conferir qualquer coisa.
|
|
64
|
+
const args = [operation === 'gate' ? 'gate' : 'sessions', 'answer', thread, pedido,
|
|
65
|
+
operation === 'gate' ? '--resposta-stdin' : '--stdin',
|
|
66
|
+
'--origem', 'telegram', '--canal', canal, '--conta', conta, '--por', envelope.por, '--mensagem', envelope.mensagem];
|
|
67
|
+
// FX5: `ok` alone was never a receipt. The core answers with the typed verdict, and a
|
|
68
|
+
// reply to the human must only claim what that verdict says: a gate lands in one of
|
|
69
|
+
// three states, a session answer lands `entregue` with the session it reached, and
|
|
70
|
+
// `repetida` says whether this call caused the effect or merely found it already done.
|
|
71
|
+
const estados = operation === 'gate' ? ['aprovado', 'recusado', 'aguardando'] : ['entregue'];
|
|
72
|
+
const ok = await new Promise(resolve => {
|
|
73
|
+
const child = execFile(bin, args, { cwd: root, timeout: 10000, maxBuffer: 65536 }, (err, stdout) => {
|
|
74
|
+
try {
|
|
75
|
+
const r = JSON.parse(stdout);
|
|
76
|
+
resolve(!err && r.ok === true && r.pedidoId === pedido && estados.includes(r.estado) &&
|
|
77
|
+
typeof r.repetida === 'boolean' &&
|
|
78
|
+
(operation === 'gate' || typeof r.sessionId === 'string' && r.sessionId.length > 0));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
resolve(false);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
child.stdin?.on('error', () => resolve(false));
|
|
85
|
+
child.stdin?.end(JSON.stringify(envelope));
|
|
86
|
+
});
|
|
87
|
+
return ok ? { handled: true, reply: { text: `Resposta recebida pelo núcleo para o pedido ${pedido}.` } } : denied;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return denied;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* I-41 (GO-FIX 1): the owner answered the pulse summary or its questions. The whole text is the
|
|
96
|
+
* answer and the signed address is the pulse's; this adapter knows no request and picks nothing.
|
|
97
|
+
* The reply is the text the core composed, transported as is.
|
|
98
|
+
*/
|
|
99
|
+
async function pulseIngress(texto, m, denied) {
|
|
100
|
+
const canal = 'openclaw', conta = process.env.ORK_HITL_OPENCLAW_ACCOUNT ?? '';
|
|
101
|
+
const envelope = { resposta: texto, canal, conta, origem: 'telegram', por: `telegram:${m.user}`,
|
|
102
|
+
mensagem: `telegram:${m.chat}:${m.message}`, recebidoEm: new Date(m.timestamp).toISOString(), prova: '' };
|
|
103
|
+
envelope.prova = createHmac('sha256', m.key).update(JSON.stringify(['ork.hitl-answer/v2', PULSE_ALVO, PULSE_ENDERECO, canal, conta,
|
|
104
|
+
envelope.origem, envelope.por, envelope.mensagem, envelope.recebidoEm, texto])).digest('hex');
|
|
105
|
+
const args = ['pulse', 'responder', '--resposta-stdin', '--origem', 'telegram', '--canal', canal, '--conta', conta,
|
|
106
|
+
'--por', envelope.por, '--mensagem', envelope.mensagem];
|
|
107
|
+
const receipt = await new Promise(resolve => {
|
|
108
|
+
const child = execFile(m.bin, args, { cwd: m.root, timeout: 10000, maxBuffer: 65536 }, (err, stdout) => {
|
|
109
|
+
try {
|
|
110
|
+
resolve(err ? null : JSON.parse(stdout));
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
resolve(null);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
child.stdin?.on('error', () => resolve(null));
|
|
117
|
+
child.stdin?.end(JSON.stringify(envelope));
|
|
118
|
+
});
|
|
119
|
+
if (!receipt || receipt.ok !== true || receipt.contrato !== 'ork.pulse-resposta/v1' ||
|
|
120
|
+
typeof receipt.mensagem !== 'string' || !receipt.mensagem || typeof receipt.repetida !== 'boolean')
|
|
121
|
+
return denied;
|
|
122
|
+
// Same message already handled: never resend blindly.
|
|
123
|
+
return receipt.repetida ? { handled: true } : { handled: true, reply: { text: receipt.mensagem } };
|
|
124
|
+
}
|
|
125
|
+
/** SDK instalado: inbound_claim fornece commandAuthorized, senderIsOwner e sessionKey.
|
|
126
|
+
* Somente esta callback usa a chave privada; nenhuma tool registra nativeIngress. */
|
|
127
|
+
async function nativeIngress(event, ctx, bin) {
|
|
128
|
+
const denied = { handled: true, reply: { text: 'Ingresso nativo indisponível ou identidade não comprovada. O pedido permanece pendente.' } };
|
|
129
|
+
const env = process.env, root = env.ORK_HITL_ROOT, key = env.ORK_HITL_NATIVE_KEY_OPENCLAW;
|
|
130
|
+
const binding = JSON.parse(env.ORK_HITL_NATIVE_BINDING_OPENCLAW ?? 'null');
|
|
131
|
+
const age = Date.now() - Number(event.timestamp);
|
|
132
|
+
if (!binding || binding.host !== 'openclaw' || !root?.startsWith('/') || !key || Buffer.byteLength(key) < 32 ||
|
|
133
|
+
event.commandAuthorized !== true || event.senderIsOwner !== true ||
|
|
134
|
+
!event.sessionKey || event.sessionKey !== ctx.sessionKey || event.sessionKey !== binding.sessionId ||
|
|
135
|
+
!event.messageId || event.messageId !== ctx.messageId || !binding.installationId || !binding.connectionId ||
|
|
136
|
+
event.channel !== ctx.channelId || event.channel !== binding.channelId ||
|
|
137
|
+
event.accountId !== ctx.accountId || event.accountId !== binding.accountId ||
|
|
138
|
+
event.senderId !== ctx.senderId || event.senderId !== binding.personId ||
|
|
139
|
+
event.conversationId !== ctx.conversationId || event.conversationId !== binding.conversationId ||
|
|
140
|
+
!Number.isFinite(age) || age < 0 || age > 60000)
|
|
141
|
+
return denied;
|
|
142
|
+
const match = /^\/ork (offer|gate|session) ([a-zA-Z0-9][a-zA-Z0-9._-]{0,79}) ([a-zA-Z0-9][a-zA-Z0-9._-]{0,79})(?: ([\s\S]{1,4096}))?$/.exec(event.content);
|
|
143
|
+
if (!match || event.content.includes('\0') || (match[1] === 'offer' ? match[4] !== undefined : !match[4]))
|
|
144
|
+
return denied;
|
|
145
|
+
const [, operation, thread, pedido, resposta] = match;
|
|
146
|
+
const call = (args, input) => new Promise((resolve, reject) => {
|
|
147
|
+
const child = execFile(bin, args, { cwd: root, timeout: 10000, maxBuffer: 65536 }, (error, stdout) => {
|
|
148
|
+
if (error) {
|
|
149
|
+
reject(Error('native.core.unavailable'));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
resolve(JSON.parse(stdout));
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
reject(Error('native.core.invalid'));
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
child.stdin?.on('error', () => reject(Error('native.core.unavailable')));
|
|
160
|
+
if (input !== undefined)
|
|
161
|
+
child.stdin?.end(JSON.stringify(input));
|
|
162
|
+
});
|
|
163
|
+
const offer = { native: { ...binding, messageId: event.messageId }, recebidoEm: new Date(Number(event.timestamp)).toISOString(), prova: '' };
|
|
164
|
+
const b = offer.native;
|
|
165
|
+
offer.prova = createHmac('sha256', key).update(JSON.stringify(['ork.hitl-native-offer/v1', thread, pedido,
|
|
166
|
+
b.host, b.installationId, b.connectionId, b.sessionId, b.accountId, b.channelId,
|
|
167
|
+
b.conversationId, b.personId, b.messageId, offer.recebidoEm])).digest('hex');
|
|
168
|
+
const view = await call(['gate', 'context', thread, pedido, '--native-offer-stdin'], offer);
|
|
169
|
+
if (view.pedido?.id !== pedido || view.pedido?.thread !== thread || (operation !== 'offer' && view.pedido?.alvo?.tipo !== operation) ||
|
|
170
|
+
!view.canais?.some((c) => c.canal === 'openclaw' && c.transporte === 'native' && c.estado === 'disponivel') ||
|
|
171
|
+
!/^[a-f0-9]{64}$/.test(view.contexto) || !/^[a-f0-9]{64}$/.test(view.pedidoSha256))
|
|
172
|
+
return denied;
|
|
173
|
+
if (operation === 'offer') {
|
|
174
|
+
if (typeof view.apresentacao?.mensagem !== 'string')
|
|
175
|
+
return denied;
|
|
176
|
+
// I-35: o prazo chega ao dono no fuso dele; núcleo de outra versão pode não ter posto o
|
|
177
|
+
// prazo local na mensagem, então o campo local vai junto.
|
|
178
|
+
const prazo = typeof view.prazoLocal === 'string' && view.prazoLocal && !view.apresentacao.mensagem.includes(view.prazoLocal)
|
|
179
|
+
? `\nPrazo (fuso do dono): ${view.prazoLocal}` : '';
|
|
180
|
+
return { handled: true, reply: { text: view.apresentacao.mensagem + prazo + '\nCanal nativo OpenClaw disponível nesta conversa.' } };
|
|
181
|
+
}
|
|
182
|
+
const n = { ...binding, messageId: event.messageId, context: view.contexto,
|
|
183
|
+
pedidoSha256: view.pedidoSha256, expiresAt: view.pedido.prazo };
|
|
184
|
+
const message = 'native:' + createHash('sha256').update(JSON.stringify([n.host, n.installationId, n.connectionId,
|
|
185
|
+
n.sessionId, n.channelId, n.accountId, n.conversationId, n.messageId])).digest('hex');
|
|
186
|
+
const r = { origem: 'native', canal: 'openclaw', conta: n.accountId, native: n, por: `native:openclaw:${n.personId}`,
|
|
187
|
+
mensagem: message, recebidoEm: new Date(Number(event.timestamp)).toISOString(), resposta, prova: '' };
|
|
188
|
+
r.prova = createHmac('sha256', key).update(JSON.stringify(['ork.hitl-native/v1', thread, pedido,
|
|
189
|
+
n.host, n.installationId, n.connectionId, n.sessionId, n.accountId, n.channelId, n.conversationId, n.personId,
|
|
190
|
+
n.messageId, n.context, n.pedidoSha256, n.expiresAt, r.origem, r.canal, r.conta, r.por, r.mensagem, r.recebidoEm, resposta])).digest('hex');
|
|
191
|
+
const receipt = await call([operation === 'gate' ? 'gate' : 'sessions', 'answer', thread, pedido,
|
|
192
|
+
operation === 'gate' ? '--resposta-stdin' : '--stdin', '--origem', 'native', '--canal', 'openclaw',
|
|
193
|
+
'--conta', r.conta, '--por', r.por, '--mensagem', message], r);
|
|
194
|
+
if (receipt.ok !== true || receipt.pedidoId !== pedido || typeof receipt.repetida !== 'boolean' ||
|
|
195
|
+
!(operation === 'gate' ? ['aprovado', 'recusado', 'aguardando'] : ['entregue']).includes(receipt.estado) ||
|
|
196
|
+
(operation === 'session' && !receipt.sessionId))
|
|
197
|
+
return denied;
|
|
198
|
+
return receipt.repetida ? { handled: true } : { handled: true, reply: { text: 'Resposta nativa confirmada pelo núcleo.' } };
|
|
199
|
+
}
|