@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,180 @@
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
+ interface NativeEvent {
7
+ content: string; channel: string; commandAuthorized?: boolean; accountId?: string;
8
+ senderId?: string; conversationId?: string; messageId?: string; timestamp?: number;
9
+ sessionKey?: string; senderIsOwner?: boolean;
10
+ }
11
+ interface NativeContext { channelId: string; accountId?: string; senderId?: string; conversationId?: string; messageId?: string; sessionKey?: string }
12
+ interface NativeApi { on: (name: 'inbound_claim', handler: (event: NativeEvent, ctx: NativeContext) => unknown) => void }
13
+
14
+ /**
15
+ * I-41 (GO-FIX 1): the two shapes of an answer to the pulse summary, typed with no slash and no
16
+ * identifier: "P4EJ a" answers the summary and "1a 2c" answers the questions. The SAME sources
17
+ * live in the core (GRAMATICA_DO_PULSE, core/src/pulse-resposta.ts) and a core test checks they
18
+ * stay equal. The code starts with a letter and has a digit: ordinary chat never has this shape,
19
+ * and it never reads as an answer to the batch, which starts with the question number.
20
+ */
21
+ 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');
22
+ 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]*$");
23
+ /** I-50 (RM-039): the pulse cadence tag, alone in the message. It starts with '#', so it never reads as the other two. */
24
+ const PULSE_CADENCIA = new RegExp("^[ \\t]*#OrkPulse(?:On(?:-(?:15|30|60)m)?|Off)[ \\t]*[.!]?[ \\t]*$", 'i');
25
+ /** The pulse address inside the signed body: neither a thread nor a request. The core translates. */
26
+ const PULSE_ALVO = 'pulse', PULSE_ENDERECO = 'resposta';
27
+ export function isPulseAnswer(text: string): boolean {
28
+ return !text.includes('\0') && (PULSE_CONSENT.test(text) || PULSE_LOTE.test(text) || PULSE_CADENCIA.test(text));
29
+ }
30
+
31
+ export function registerHitlIngress(api: NativeApi, bin: string): void {
32
+ api.on('inbound_claim', async (event, ctx) => {
33
+ if (typeof event.content !== 'string') return { handled: false };
34
+ const pulse = !event.content.startsWith('/ork ') && isPulseAnswer(event.content);
35
+ if (!event.content.startsWith('/ork ') && !pulse) return { handled: false };
36
+ const denied = { handled: true, reply: { text: 'Resposta HITL não confirmada. Confira ingresso e ledger do pedido.' } };
37
+ // The pulse summary goes out on Telegram; only there is an answer to it recognized.
38
+ if (event.channel !== 'telegram') return pulse ? { handled: false } : nativeIngress(event, ctx, bin).catch(() => denied);
39
+ try {
40
+ const env = process.env, user = event.senderId ?? '', chat = event.conversationId ?? '', message = event.messageId ?? '';
41
+ const allowed = (key: string, value: string) => (env[key] ?? '').split(',').map(s => s.trim()).filter(Boolean).includes(value);
42
+ const age = Date.now() - Number(event.timestamp);
43
+ if (event.channel !== 'telegram' || ctx.channelId !== 'telegram' || event.commandAuthorized !== true ||
44
+ !env.ORK_HITL_OPENCLAW_ACCOUNT || event.accountId !== env.ORK_HITL_OPENCLAW_ACCOUNT || event.accountId !== ctx.accountId ||
45
+ !env.ORK_HITL_TELEGRAM_BOT_ID || user === env.ORK_HITL_TELEGRAM_BOT_ID ||
46
+ !/^\d+$/.test(user) || !/^-?\d+$/.test(chat) || !/^\d+$/.test(message) ||
47
+ user !== ctx.senderId || chat !== ctx.conversationId || message !== ctx.messageId ||
48
+ !allowed('ORK_HITL_TELEGRAM_USERS', user) || !allowed('ORK_HITL_TELEGRAM_CHATS', chat) ||
49
+ !Number.isFinite(age) || age < 0 || age > 60000) return denied;
50
+ 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);
51
+ // FX1: this channel signs with ITS OWN key. The shared ORK_HITL_INGRESS_KEY is kept
52
+ // for the legacy v1 envelope only, which this adapter no longer produces.
53
+ const key = env.ORK_HITL_INGRESS_KEY_OPENCLAW, root = env.ORK_HITL_ROOT;
54
+ if ((!pulse && !match) || event.content.includes('\0') || !key || Buffer.byteLength(key) < 32 || !root?.startsWith('/')) return denied;
55
+ if (pulse) return pulseIngress(event.content, { user, chat, message, key, root, bin, timestamp: Number(event.timestamp) }, denied);
56
+ const [, operation, thread, pedido, resposta] = match!;
57
+ const canal = 'openclaw';
58
+ // FX5: the authorized account goes INSIDE the signed body. Checking `accountId` only
59
+ // here left the durable receipt proving generic telegram transport and nothing else:
60
+ // nobody reading it later could tell which OpenClaw account the decision came from.
61
+ const conta = env.ORK_HITL_OPENCLAW_ACCOUNT;
62
+ const envelope = { resposta, canal, conta, origem: 'telegram', por: `telegram:${user}`, mensagem: `telegram:${chat}:${message}`,
63
+ recebidoEm: new Date(Number(event.timestamp)).toISOString(), prova: '' };
64
+ envelope.prova = createHmac('sha256', key).update(JSON.stringify(['ork.hitl-answer/v2', thread, pedido, canal, conta,
65
+ envelope.origem, envelope.por, envelope.mensagem, envelope.recebidoEm, resposta])).digest('hex');
66
+ // `gate answer` exige --resposta-stdin; `sessions answer` exige --stdin. A mesma flag
67
+ // para os dois fazia o CLI recusar toda resposta de sessao antes de conferir qualquer coisa.
68
+ const args = [operation === 'gate' ? 'gate' : 'sessions', 'answer', thread, pedido,
69
+ operation === 'gate' ? '--resposta-stdin' : '--stdin',
70
+ '--origem', 'telegram', '--canal', canal, '--conta', conta, '--por', envelope.por, '--mensagem', envelope.mensagem];
71
+ // FX5: `ok` alone was never a receipt. The core answers with the typed verdict, and a
72
+ // reply to the human must only claim what that verdict says: a gate lands in one of
73
+ // three states, a session answer lands `entregue` with the session it reached, and
74
+ // `repetida` says whether this call caused the effect or merely found it already done.
75
+ const estados = operation === 'gate' ? ['aprovado', 'recusado', 'aguardando'] : ['entregue'];
76
+ const ok = await new Promise<boolean>(resolve => {
77
+ const child = execFile(bin, args, { cwd: root, timeout: 10000, maxBuffer: 65536 }, (err, stdout) => {
78
+ try {
79
+ const r = JSON.parse(stdout);
80
+ resolve(!err && r.ok === true && r.pedidoId === pedido && estados.includes(r.estado) &&
81
+ typeof r.repetida === 'boolean' &&
82
+ (operation === 'gate' || typeof r.sessionId === 'string' && r.sessionId.length > 0));
83
+ } catch { resolve(false); }
84
+ });
85
+ child.stdin?.on('error', () => resolve(false));
86
+ child.stdin?.end(JSON.stringify(envelope));
87
+ });
88
+ return ok ? { handled: true, reply: { text: `Resposta recebida pelo núcleo para o pedido ${pedido}.` } } : denied;
89
+ } catch { return denied; }
90
+ });
91
+ }
92
+
93
+ /**
94
+ * I-41 (GO-FIX 1): the owner answered the pulse summary or its questions. The whole text is the
95
+ * answer and the signed address is the pulse's; this adapter knows no request and picks nothing.
96
+ * The reply is the text the core composed, transported as is.
97
+ */
98
+ async function pulseIngress(texto: string, m: { user: string; chat: string; message: string; key: string; root: string;
99
+ bin: string; timestamp: number }, denied: { handled: boolean; reply: { text: string } }) {
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<any>(resolve => {
108
+ const child = execFile(m.bin, args, { cwd: m.root, timeout: 10000, maxBuffer: 65536 }, (err, stdout) => {
109
+ try { resolve(err ? null : JSON.parse(stdout)); } catch { resolve(null); }
110
+ });
111
+ child.stdin?.on('error', () => resolve(null));
112
+ child.stdin?.end(JSON.stringify(envelope));
113
+ });
114
+ if (!receipt || receipt.ok !== true || receipt.contrato !== 'ork.pulse-resposta/v1' ||
115
+ typeof receipt.mensagem !== 'string' || !receipt.mensagem || typeof receipt.repetida !== 'boolean') return denied;
116
+ // Same message already handled: never resend blindly.
117
+ return receipt.repetida ? { handled: true } : { handled: true, reply: { text: receipt.mensagem } };
118
+ }
119
+
120
+ /** SDK instalado: inbound_claim fornece commandAuthorized, senderIsOwner e sessionKey.
121
+ * Somente esta callback usa a chave privada; nenhuma tool registra nativeIngress. */
122
+ async function nativeIngress(event: NativeEvent, ctx: NativeContext, bin: string) {
123
+ const denied = { handled: true, reply: { text: 'Ingresso nativo indisponível ou identidade não comprovada. O pedido permanece pendente.' } };
124
+ const env = process.env, root = env.ORK_HITL_ROOT, key = env.ORK_HITL_NATIVE_KEY_OPENCLAW;
125
+ const binding = JSON.parse(env.ORK_HITL_NATIVE_BINDING_OPENCLAW ?? 'null');
126
+ const age = Date.now() - Number(event.timestamp);
127
+ if (!binding || binding.host !== 'openclaw' || !root?.startsWith('/') || !key || Buffer.byteLength(key) < 32 ||
128
+ event.commandAuthorized !== true || event.senderIsOwner !== true ||
129
+ !event.sessionKey || event.sessionKey !== ctx.sessionKey || event.sessionKey !== binding.sessionId ||
130
+ !event.messageId || event.messageId !== ctx.messageId || !binding.installationId || !binding.connectionId ||
131
+ event.channel !== ctx.channelId || event.channel !== binding.channelId ||
132
+ event.accountId !== ctx.accountId || event.accountId !== binding.accountId ||
133
+ event.senderId !== ctx.senderId || event.senderId !== binding.personId ||
134
+ event.conversationId !== ctx.conversationId || event.conversationId !== binding.conversationId ||
135
+ !Number.isFinite(age) || age < 0 || age > 60000) return denied;
136
+ 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);
137
+ if (!match || event.content.includes('\0') || (match[1] === 'offer' ? match[4] !== undefined : !match[4])) return denied;
138
+ const [, operation, thread, pedido, resposta] = match;
139
+ const call = (args: string[], input?: unknown) => new Promise<any>((resolve, reject) => {
140
+ const child = execFile(bin, args, { cwd: root, timeout: 10000, maxBuffer: 65536 }, (error, stdout) => {
141
+ if (error) { reject(Error('native.core.unavailable')); return; }
142
+ try { resolve(JSON.parse(stdout)); } catch { reject(Error('native.core.invalid')); }
143
+ });
144
+ child.stdin?.on('error', () => reject(Error('native.core.unavailable')));
145
+ if (input !== undefined) child.stdin?.end(JSON.stringify(input));
146
+ });
147
+ const offer = { native: { ...binding, messageId: event.messageId }, recebidoEm: new Date(Number(event.timestamp)).toISOString(), prova: '' };
148
+ const b = offer.native;
149
+ offer.prova = createHmac('sha256', key).update(JSON.stringify(['ork.hitl-native-offer/v1', thread, pedido,
150
+ b.host, b.installationId, b.connectionId, b.sessionId, b.accountId, b.channelId,
151
+ b.conversationId, b.personId, b.messageId, offer.recebidoEm])).digest('hex');
152
+ const view = await call(['gate', 'context', thread, pedido, '--native-offer-stdin'], offer);
153
+ if (view.pedido?.id !== pedido || view.pedido?.thread !== thread || (operation !== 'offer' && view.pedido?.alvo?.tipo !== operation) ||
154
+ !view.canais?.some((c: any) => c.canal === 'openclaw' && c.transporte === 'native' && c.estado === 'disponivel') ||
155
+ !/^[a-f0-9]{64}$/.test(view.contexto) || !/^[a-f0-9]{64}$/.test(view.pedidoSha256)) return denied;
156
+ if (operation === 'offer') {
157
+ if (typeof view.apresentacao?.mensagem !== 'string') return denied;
158
+ // I-35: o prazo chega ao dono no fuso dele; núcleo de outra versão pode não ter posto o
159
+ // prazo local na mensagem, então o campo local vai junto.
160
+ const prazo = typeof view.prazoLocal === 'string' && view.prazoLocal && !view.apresentacao.mensagem.includes(view.prazoLocal)
161
+ ? `\nPrazo (fuso do dono): ${view.prazoLocal}` : '';
162
+ return { handled: true, reply: { text: view.apresentacao.mensagem + prazo + '\nCanal nativo OpenClaw disponível nesta conversa.' } };
163
+ }
164
+ const n = { ...binding, messageId: event.messageId, context: view.contexto,
165
+ pedidoSha256: view.pedidoSha256, expiresAt: view.pedido.prazo };
166
+ const message = 'native:' + createHash('sha256').update(JSON.stringify([n.host, n.installationId, n.connectionId,
167
+ n.sessionId, n.channelId, n.accountId, n.conversationId, n.messageId])).digest('hex');
168
+ const r = { origem: 'native', canal: 'openclaw', conta: n.accountId, native: n, por: `native:openclaw:${n.personId}`,
169
+ mensagem: message, recebidoEm: new Date(Number(event.timestamp)).toISOString(), resposta, prova: '' };
170
+ r.prova = createHmac('sha256', key).update(JSON.stringify(['ork.hitl-native/v1', thread, pedido,
171
+ n.host, n.installationId, n.connectionId, n.sessionId, n.accountId, n.channelId, n.conversationId, n.personId,
172
+ n.messageId, n.context, n.pedidoSha256, n.expiresAt, r.origem, r.canal, r.conta, r.por, r.mensagem, r.recebidoEm, resposta])).digest('hex');
173
+ const receipt = await call([operation === 'gate' ? 'gate' : 'sessions', 'answer', thread, pedido,
174
+ operation === 'gate' ? '--resposta-stdin' : '--stdin', '--origem', 'native', '--canal', 'openclaw',
175
+ '--conta', r.conta, '--por', r.por, '--mensagem', message], r);
176
+ if (receipt.ok !== true || receipt.pedidoId !== pedido || typeof receipt.repetida !== 'boolean' ||
177
+ !(operation === 'gate' ? ['aprovado', 'recusado', 'aguardando'] : ['entregue']).includes(receipt.estado) ||
178
+ (operation === 'session' && !receipt.sessionId)) return denied;
179
+ return receipt.repetida ? { handled: true } : { handled: true, reply: { text: 'Resposta nativa confirmada pelo núcleo.' } };
180
+ }
@@ -0,0 +1,460 @@
1
+ /**
2
+ * Plugin Orkastery para OpenClaw, no formato de extensao 2026.7.1.
3
+ *
4
+ * Zero regra de negocio no host: cada tool monta um VETOR argv e chama o CLI
5
+ * `ork`. O parse da #TAG, a validacao de modo, gates e verificacao vivem no
6
+ * nucleo, com teste. Tool nova que precisa de logica e sinal de que a logica
7
+ * pertence ao `ork`.
8
+ */
9
+ import { registerHitlIngress } from './hitl-ingress.js';
10
+ import { execFile } from 'node:child_process';
11
+ import { defineToolPlugin } from 'openclaw/plugin-sdk/tool-plugin';
12
+
13
+ /**
14
+ * Renderizado pelo `ork adapter install openclaw` para o caminho absoluto do
15
+ * `ork` desta maquina. Se a copia nao passou pelo instalador, o placeholder
16
+ * sobrevive e o fallback abaixo usa ORK_BIN do ambiente ou o `ork` do PATH.
17
+ */
18
+ const ORK_RENDERIZADO = '{{ork_bin}}';
19
+
20
+ /**
21
+ * Abre-chaves duplo montado por concatenacao: o arquivo instalado nao pode
22
+ * conter o par literal fora do placeholder, porque a prova do pitfall 1 e o
23
+ * grep pelo par devolvendo 0 no dist renderizado.
24
+ */
25
+ const MARCA_DE_PLACEHOLDER = '{' + '{';
26
+
27
+ function resolverOrkBin(): string {
28
+ if (!ORK_RENDERIZADO.startsWith(MARCA_DE_PLACEHOLDER)) return ORK_RENDERIZADO;
29
+ const doAmbiente = process.env.ORK_BIN;
30
+ if (doAmbiente && doAmbiente.trim() !== '') return doAmbiente;
31
+ return 'ork';
32
+ }
33
+
34
+ /** Limite generoso para saidas de board/ledger sem estourar o buffer padrao. */
35
+ const MAX_BUFFER = 16 * 1024 * 1024;
36
+
37
+ /**
38
+ * Roda o `ork` com argv em vetor (nunca string de shell: um pedido com espaco,
39
+ * aspas ou quebra de linha chega inteiro em --prompt). Devolve stdout+stderr
40
+ * como texto; exit diferente de zero vira texto prefixado, nunca excecao, para
41
+ * o agente ler o erro tipado do nucleo em vez de um stack trace do host.
42
+ */
43
+ async function rodarOrk(args: string[], signal?: AbortSignal, entrada?: string): Promise<string> {
44
+ const bin = resolverOrkBin();
45
+ const parentEnv = { ...process.env };
46
+ const hasPrivateHitlAuthority = Object.keys(parentEnv).some((name) => /^ORK_HITL_(?:INGRESS_KEY|NATIVE_KEY)/.test(name));
47
+ // O núcleo prepara recibos legados do ORK_HITL_ROOT confiável e deriva a raiz
48
+ // pública antes de sanitizar. Nenhum argumento do modelo chega ao helper.
49
+ let verifiers: string | undefined;
50
+ try {
51
+ verifiers = await new Promise<string | undefined>((resolve, reject) => {
52
+ execFile(bin, ['receipt-verifiers', '--json'], { env: parentEnv, signal, timeout: 5000, maxBuffer: 4096 },
53
+ (error, stdout) => {
54
+ if (error) return reject(error);
55
+ try {
56
+ const value = JSON.parse(stdout);
57
+ if (value !== null && value.schema !== 'ork.hitl-verifiers/v1') throw Error('invalid');
58
+ if (hasPrivateHitlAuthority && (!value || !value.verifiers || !Object.keys(value.verifiers).length)) throw Error('missing');
59
+ resolve(value === null ? undefined : JSON.stringify(value));
60
+ } catch (error) { reject(error); }
61
+ });
62
+ });
63
+ } catch {
64
+ // Sem autoridade HITL não há segredo a transformar: comandos comuns seguem
65
+ // com o ambiente sanitizado. Com autoridade presente, falhamos fechado para
66
+ // nunca entregar ao executor uma sessão incapaz de verificar seus recibos.
67
+ if (hasPrivateHitlAuthority) return '[ork recusou] hitl.receipt.verifiers-unavailable';
68
+ }
69
+ // Tools do modelo não herdam a autoridade da callback humana no mesmo host.
70
+ const env = Object.fromEntries(Object.entries(parentEnv).filter(([name]) => !name.startsWith('ORK_HITL_') && name !== 'ORK_RECEIPT_VERIFIERS'));
71
+ if (verifiers) env.ORK_RECEIPT_VERIFIERS = verifiers;
72
+ // I-36 (D6): o adaptador declara o proprio canal de conducao. Descreve a porta, nao concede autoridade.
73
+ env.ORK_CANAL = 'openclaw';
74
+ return new Promise((resolve) => {
75
+ const filho = execFile(bin, args, { maxBuffer: MAX_BUFFER, signal, env }, (erro, stdout, stderr) => {
76
+ const partes = [stdout, stderr].map((t) => (t ?? '').trim()).filter((t) => t !== '');
77
+ const saida = partes.join('\n');
78
+ if (!erro) {
79
+ resolve(saida === '' ? 'ok (sem saida)' : saida);
80
+ return;
81
+ }
82
+ const codigo = typeof (erro as NodeJS.ErrnoException).code === 'string'
83
+ ? (erro as NodeJS.ErrnoException).code
84
+ : ((erro as unknown as { code?: number }).code ?? 'desconhecido');
85
+ if (codigo === 'ENOENT') {
86
+ resolve(
87
+ `ork nao encontrado em "${bin}". Instale o adapter com ` +
88
+ '`ork adapter install openclaw` (que renderiza o caminho) ou exporte ORK_BIN.'
89
+ );
90
+ return;
91
+ }
92
+ resolve(`[ork saiu com ${String(codigo)}]\n${saida}`);
93
+ });
94
+ if (entrada !== undefined) filho.stdin?.end(entrada);
95
+ });
96
+ }
97
+
98
+ /** Schema de objeto JSON com todas as propriedades obrigatorias. */
99
+ function schema(
100
+ propriedades: Record<string, Record<string, unknown>>
101
+ ): Record<string, unknown> {
102
+ return {
103
+ type: 'object',
104
+ properties: propriedades,
105
+ required: Object.keys(propriedades),
106
+ additionalProperties: false,
107
+ };
108
+ }
109
+
110
+ function texto(params: Record<string, unknown>, chave: string): string {
111
+ const valor = params[chave];
112
+ if (typeof valor === 'string') return valor;
113
+ return String(valor ?? '');
114
+ }
115
+
116
+ /** O gateway fornece o update original; o agente não pode sintetizar evidência humana. */
117
+ function argvDaResposta(p: Record<string, unknown>, alvo: 'gate' | 'sessions'): string[] {
118
+ const update = p.updateTelegram as Record<string, any> | undefined;
119
+ const callback = update?.callback_query;
120
+ const mensagem = callback?.message ?? update?.message;
121
+ const remetente = callback?.from ?? mensagem?.from;
122
+ const usuario = String(remetente?.id ?? '');
123
+ const chat = String(mensagem?.chat?.id ?? '');
124
+ const permitidos = (nome: string) => (process.env[nome] ?? '').split(',').map(v => v.trim()).filter(Boolean);
125
+ if (!usuario || !chat || remetente?.is_bot !== false || mensagem?.forward_origin ||
126
+ !permitidos('ORK_HITL_TELEGRAM_USERS').includes(usuario) ||
127
+ !permitidos('ORK_HITL_TELEGRAM_CHATS').includes(chat)) {
128
+ throw new Error('origem humana não autorizada pelo gateway');
129
+ }
130
+ const thread = texto(p, 'thread'), pedido = texto(p, 'pedido');
131
+ if (![thread, pedido].every(v => /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(v))) {
132
+ throw new Error('thread ou pedido inválido');
133
+ }
134
+ let resposta: unknown, referencia: unknown;
135
+ if (callback) {
136
+ const prefixo = `ork:${thread}:${pedido}:`;
137
+ if (typeof callback.data !== 'string' || !callback.data.startsWith(prefixo)) {
138
+ throw new Error('callback não corresponde ao pedido');
139
+ }
140
+ resposta = callback.data.slice(prefixo.length);
141
+ if (!/^[1-9][0-9]?$/.test(String(resposta))) throw new Error('opção inválida');
142
+ referencia = callback.id;
143
+ } else {
144
+ const origem = mensagem?.reply_to_message?.text;
145
+ if (typeof origem !== 'string' || !origem.split('\n').includes(`ork-hitl ${thread} ${pedido}`)) {
146
+ throw new Error('responda à mensagem do pedido HITL');
147
+ }
148
+ resposta = mensagem.text;
149
+ referencia = mensagem.message_id;
150
+ }
151
+ if (typeof resposta !== 'string' || !resposta.trim() || resposta.length > 4096 ||
152
+ resposta.includes('\0') || !referencia) throw new Error('resposta ou mensagem inválida');
153
+ return [alvo, 'answer', thread, pedido, '--resposta-stdin', '--por', `telegram:${usuario}`,
154
+ '--mensagem', `telegram:${chat}:${referencia}`, '--origem', 'telegram'];
155
+ }
156
+
157
+ /** Catálogo em paridade nome a nome com o manifesto; aprovação genérica legada aposentada. */
158
+ interface FerramentaOrk {
159
+ name: string;
160
+ description: string;
161
+ parameters: Record<string, unknown>;
162
+ argv: (params: Record<string, unknown>) => string[];
163
+ entrada?: (params: Record<string, unknown>) => string;
164
+ }
165
+
166
+ const FERRAMENTAS: FerramentaOrk[] = [
167
+ {
168
+ name: 'ork_maestro',
169
+ description: 'Ao receber a frase exata orkastery maestro, consulte o panorama somente leitura. Não cria thread. Apresente fontes, lacunas e HITL com recomendação e opções claras; horários para o dono vêm dos campos *Local (fuso do dono), nunca do ISO. Ação posterior exige operação autorizada e readback.',
170
+ parameters: { type: 'object', additionalProperties: false, properties: {
171
+ thread: { type: 'string' }, section: { type: 'string', enum: ['portfolio','demands','threads','sessions','blockers','leases','retries','hitl','ship','master','nextActions'] },
172
+ offset: { type: 'integer', minimum: 0, maximum: 100000 },
173
+ } },
174
+ argv: p => {
175
+ if (Object.keys(p).some(k => !['thread', 'section', 'offset'].includes(k)) ||
176
+ (p.thread !== undefined && (typeof p.thread !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(p.thread))) ||
177
+ (p.section === undefined) !== (p.offset === undefined) ||
178
+ (p.offset !== undefined && (!Number.isSafeInteger(p.offset) || Number(p.offset) < 0 || Number(p.offset) > 100000)))
179
+ throw Error('maestro.arguments.invalid');
180
+ return ['maestro', '--json', ...(p.thread === undefined ? [] : ['--thread', String(p.thread)]),
181
+ ...(p.section === undefined ? [] : ['--section', String(p.section), '--offset', String(p.offset)])];
182
+ },
183
+ },
184
+ {
185
+ name: 'ork_onboarding',
186
+ description: 'Consulta a pauta do núcleo e registra respostas públicas. Credenciais somente como nomes de variáveis; valores ficam em ~/.hermes/.env. Sync publica opcionalmente e relata degradação.',
187
+ parameters: {
188
+ type: 'object', additionalProperties: false, required: ['acao'],
189
+ properties: {
190
+ acao: { type: 'string', enum: ['show', 'set', 'reset', 'sync'] },
191
+ etapa: { type: 'string', description: 'Etapa devolvida pela pauta do CLI; omita para reset total.' },
192
+ conteudo: { type: 'string', description: 'JSON público serializado; nunca valores secretos.' },
193
+ por: { type: 'string', description: 'Autoria da resposta; omitido usa owner.' },
194
+ },
195
+ },
196
+ argv: (p) => ['onboarding', texto(p, 'acao'),
197
+ ...(p.etapa === undefined ? [] : [texto(p, 'etapa')]),
198
+ ...(p.conteudo === undefined ? [] : ['--conteudo', texto(p, 'conteudo')]),
199
+ ...(p.por === undefined ? [] : ['--por', texto(p, 'por')]), '--json'],
200
+ },
201
+ {
202
+ name: 'ork_doctor',
203
+ description:
204
+ 'O que vale nesta maquina agora: runtime, assinatura, provider de custo. Sai diferente de zero quando o despacho nao vale.',
205
+ parameters: schema({}),
206
+ argv: () => ['doctor'],
207
+ },
208
+ {
209
+ name: 'ork_modo_do_pedido',
210
+ description:
211
+ 'Le a #TAG de conducao do pedido do builder (#Classic, #Maestro, #Auto, #Fast) e devolve o modo. #TAG aposentada (#Look, #Ork) recebe recusa tipada modo.aposentado, com saida != 0, em vez de virar o default em silencio. Sem tag, devolve o conduction.default_mode do manifesto. Use SEMPRE este tool em vez de reconhecer a tag no host.',
212
+ parameters: schema({
213
+ pedido: { type: 'string', description: 'O texto inteiro do pedido do builder' },
214
+ }),
215
+ argv: (p) => ['modos', '--do-pedido', texto(p, 'pedido'), '--json'],
216
+ },
217
+ {
218
+ name: 'ork_thread_new',
219
+ description:
220
+ 'Cria a thread, o slug de 3 partes e o ledger. O modo vem de ork_modo_do_pedido; o nucleo valida contra conduction.allowed_modes e recusa o que o projeto nao permite.',
221
+ parameters: schema({
222
+ nome: { type: 'string', description: 'Nome curto da demanda' },
223
+ modo: {
224
+ type: 'string',
225
+ description: 'classic|maestro|auto|fast, vindo de ork_modo_do_pedido',
226
+ },
227
+ }),
228
+ argv: (p) => [
229
+ 'thread',
230
+ 'new',
231
+ texto(p, 'nome'),
232
+ '--mode',
233
+ texto(p, 'modo'),
234
+ '--worktree',
235
+ 'auto',
236
+ ],
237
+ },
238
+ {
239
+ name: 'ork_thread_status',
240
+ description: 'Estado da thread lido do disco e cruzado com o runtime real.',
241
+ parameters: schema({ thread: { type: 'string' } }),
242
+ argv: (p) => ['thread', 'status', texto(p, 'thread')],
243
+ },
244
+ {
245
+ name: 'ork_phase_run',
246
+ description:
247
+ 'Despacha uma fase (GOAL PLAN GO CHECK SHIP MASTER) pelo runtime adapter, gravando o prompt exato com sha256 no ledger.',
248
+ parameters: schema({
249
+ thread: { type: 'string' },
250
+ fase: { type: 'string', enum: ['GOAL', 'PLAN', 'GO', 'CHECK', 'SHIP', 'MASTER'] },
251
+ prompt: { type: 'string', description: 'O pedido do builder para esta fase' },
252
+ }),
253
+ argv: (p) => [
254
+ 'phase',
255
+ 'run',
256
+ texto(p, 'thread'),
257
+ texto(p, 'fase'),
258
+ '--prompt',
259
+ texto(p, 'prompt'),
260
+ ],
261
+ },
262
+ {
263
+ name: 'ork_phase_list',
264
+ description: 'O ledger da thread, evento a evento, com quem decidiu e a evidencia.',
265
+ parameters: schema({ thread: { type: 'string' } }),
266
+ argv: (p) => ['phase', 'list', texto(p, 'thread')],
267
+ },
268
+ {
269
+ name: 'ork_objective_status',
270
+ description:
271
+ 'Aposentado na I-43 junto com ork objective: devolve a recusa tipada objective.aposentado, com saida != 0. Estado de uma entrega: ork_thread_status. Ciclos ligados a um produto, projeto ou iniciativa: ork portfolio inspect <id> --json.',
272
+ parameters: schema({ objective: { type: 'string', description: 'ID obj-* do ticket' } }),
273
+ argv: (p) => ['objective', 'status', texto(p, 'objective'), '--json'],
274
+ },
275
+ {
276
+ name: 'ork_objective_message',
277
+ description:
278
+ 'Aposentado na I-43 junto com ork objective: devolve a recusa tipada objective.aposentado, com saida != 0, e nao grava nada. Resposta do dono a uma pausa vai por ork_gate_answer ou ork_session_answer.',
279
+ parameters: schema({
280
+ objective: { type: 'string', description: 'ID obj-* do ticket' },
281
+ author: { type: 'string', description: 'Identidade fornecida pelo gateway autenticado' },
282
+ message: { type: 'string', description: 'Texto integral recebido do dono' },
283
+ }),
284
+ argv: (p) => [
285
+ 'objective', 'message', texto(p, 'objective'),
286
+ '--text', texto(p, 'message'), '--por', texto(p, 'author'), '--role', 'human',
287
+ ],
288
+ },
289
+ {
290
+ name: 'ork_portfolio_list',
291
+ description: 'Lista produtos, projetos e iniciativas do catálogo canônico usado pelo Kanban.',
292
+ parameters: schema({}),
293
+ argv: () => ['portfolio', 'list', '--json'],
294
+ },
295
+ {
296
+ name: 'ork_brain_status',
297
+ description: 'Confere contrato, disponibilidade e tenant do Company Brain sem aceitar identidade ou DSN nos argumentos.',
298
+ parameters: schema({ thread: { type: 'string' } }),
299
+ argv: (p) => ['brain', 'status', '--thread', texto(p, 'thread')],
300
+ },
301
+ {
302
+ name: 'ork_brain_query',
303
+ description: 'Consulta a ontologia do Company Brain pela identidade autenticada no transporte.',
304
+ parameters: schema({
305
+ thread: { type: 'string' },
306
+ kinds: { type: 'string', description: 'prod,proj,init,assertion' },
307
+ }),
308
+ argv: (p) => ['brain', 'query', '--thread', texto(p, 'thread'), '--kinds', texto(p, 'kinds'), '--limit', '50'],
309
+ },
310
+ {
311
+ name: 'ork_brain_get',
312
+ description: 'Lê uma entidade exata do Company Brain; ausência e retenção permanecem explícitas.',
313
+ parameters: schema({ thread: { type: 'string' }, id: { type: 'string' } }),
314
+ argv: (p) => ['brain', 'get', texto(p, 'id'), '--thread', texto(p, 'thread')],
315
+ },
316
+ {
317
+ name: 'ork_claims_add',
318
+ description:
319
+ 'Registra uma alegacao verificavel com o comando que a comprova. Toda citacao de arquivo ou teste vira claim.',
320
+ parameters: schema({
321
+ thread: { type: 'string' },
322
+ arquivo: { type: 'string' },
323
+ alegacao: { type: 'string' },
324
+ comando: { type: 'string' },
325
+ }),
326
+ argv: (p) => [
327
+ 'claims',
328
+ 'add',
329
+ texto(p, 'thread'),
330
+ texto(p, 'arquivo'),
331
+ '--claim',
332
+ texto(p, 'alegacao'),
333
+ '--verificar',
334
+ texto(p, 'comando'),
335
+ ],
336
+ },
337
+ {
338
+ name: 'ork_verify',
339
+ description:
340
+ 'Reexecuta claims e comandos do manifesto no HEAD real e compara com a baseline. Passagem relatada nao e passagem.',
341
+ parameters: schema({ thread: { type: 'string' } }),
342
+ argv: (p) => ['verify', texto(p, 'thread')],
343
+ },
344
+ {
345
+ name: 'ork_verify_baseline',
346
+ description:
347
+ 'Grava a baseline: o estado do mundo ANTES do GO, que separa regressao de divida pre-existente.',
348
+ parameters: schema({ thread: { type: 'string' } }),
349
+ argv: (p) => ['verify', texto(p, 'thread'), '--baseline'],
350
+ },
351
+ {
352
+ name: 'ork_worktree_ensure',
353
+ description: 'Garante a worktree isolada da thread, com a base resolvida pelo nucleo.',
354
+ parameters: schema({ thread: { type: 'string' } }),
355
+ argv: (p) => ['worktree', 'ensure', texto(p, 'thread')],
356
+ },
357
+ {
358
+ name: 'ork_worktree_audit',
359
+ description:
360
+ 'Confere a worktree no proprio git. Sai diferente de zero quando ela divergiu ou a base avancou.',
361
+ parameters: schema({ thread: { type: 'string' } }),
362
+ argv: (p) => ['worktree', 'audit', texto(p, 'thread')],
363
+ },
364
+ ...(['gate', 'sessions'] as const).map((alvo): FerramentaOrk => ({
365
+ name: alvo === 'gate' ? 'ork_gate_answer' : 'ork_session_answer',
366
+ description: 'Transporta uma resposta humana do Telegram ao pedido HITL correlacionado. Use o update original do gateway autenticado; nunca crie update em nome do builder. O núcleo verifica modo, prazo e idempotência.',
367
+ parameters: schema({
368
+ thread: { type: 'string' },
369
+ pedido: { type: 'string' },
370
+ updateTelegram: { type: 'object', description: 'Update original recebido pelo gateway Telegram autenticado' },
371
+ }),
372
+ argv: p => argvDaResposta(p, alvo),
373
+ entrada: p => {
374
+ const update = p.updateTelegram as Record<string, any>;
375
+ return update.callback_query ? update.callback_query.data.split(':').at(-1) : update.message.text;
376
+ },
377
+ })),
378
+ {
379
+ name: 'ork_ship',
380
+ description:
381
+ 'Merge serializado por lease e push PROVADO comparando o sha local com o que o remoto reporta.',
382
+ parameters: schema({
383
+ thread: { type: 'string' },
384
+ base: { type: 'string' },
385
+ quem: { type: 'string' },
386
+ }),
387
+ argv: (p) => [
388
+ 'ship',
389
+ texto(p, 'thread'),
390
+ '--para',
391
+ texto(p, 'base'),
392
+ '--autorizar-push',
393
+ texto(p, 'quem'),
394
+ ],
395
+ },
396
+ {
397
+ name: 'ork_master',
398
+ description:
399
+ 'Fecha a thread: POSTMORTEM tipado, MASTER log no contrato congelado e o score HUMANO de 0 a 5 com justificativa. Nunca invente a nota.',
400
+ parameters: schema({
401
+ thread: { type: 'string' },
402
+ score: { type: 'string', description: 'inteiro de 0 a 5, dado pelo humano' },
403
+ justificativa: { type: 'string' },
404
+ quem: { type: 'string' },
405
+ }),
406
+ argv: (p) => [
407
+ 'master',
408
+ texto(p, 'thread'),
409
+ '--score',
410
+ texto(p, 'score'),
411
+ '--justificativa',
412
+ texto(p, 'justificativa'),
413
+ '--por',
414
+ texto(p, 'quem'),
415
+ ],
416
+ },
417
+ {
418
+ name: 'ork_board',
419
+ description:
420
+ 'Todas as threads em uma visao, e o escalonador dizendo quem avanca agora e quem espera.',
421
+ parameters: schema({}),
422
+ argv: () => ['board', 'plan'],
423
+ },
424
+ {
425
+ name: 'ork_master_batch',
426
+ description:
427
+ 'Todas as entregas em JSON, com o indice derivado do ledger, as ja pontuadas e as aceitas por omissao. Roda ork master --todas; o nome e da antiga fila de score (ork master --batch), aposentada na I-43.',
428
+ parameters: schema({}),
429
+ argv: () => ['master', '--todas', '--json'],
430
+ },
431
+ ];
432
+
433
+ const plugin = defineToolPlugin({
434
+ id: 'orkastery',
435
+ name: 'Orkastery',
436
+ description:
437
+ 'Conducao de looping threads em 6 fases pelo nucleo `ork`, exposta ao OpenClaw como tools `ork_*`. Zero regra de negocio no host: cada tool e uma chamada de CLI.',
438
+ tools: (tool) =>
439
+ FERRAMENTAS.map((f) =>
440
+ tool({
441
+ name: f.name,
442
+ description: f.description,
443
+ parameters: f.parameters,
444
+ execute: (params, _config, contexto) => {
445
+ try { return rodarOrk(f.argv(params), contexto.signal, f.entrada?.(params)); }
446
+ catch { return '[ork recusou] resposta humana não confirmada; confira origem e correlação do pedido'; }
447
+ },
448
+ })
449
+ ),
450
+ });
451
+
452
+ // Preserve SDK metadata and its tool registration; add only a native ingress hook.
453
+ const entry = plugin as { register?: (api: unknown) => unknown };
454
+ const registerTools = entry.register;
455
+ entry.register = (api: unknown) => {
456
+ const result = registerTools?.(api);
457
+ registerHitlIngress(api as Parameters<typeof registerHitlIngress>[0], resolverOrkBin());
458
+ return result;
459
+ };
460
+ export default plugin;