@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.
Files changed (242) 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/onboarding.md +21 -0
  7. package/adapters/claude-code/commands/ork.md +99 -27
  8. package/adapters/claude-code/hooks/hooks.json +6 -1
  9. package/adapters/claude-code/hooks/ork-sensor.js +66 -0
  10. package/adapters/codex/skills/ork/SKILL.md +57 -0
  11. package/adapters/hermes/README.md +163 -0
  12. package/adapters/hermes/bin/ork-abrir-thread.sh +2 -0
  13. package/adapters/hermes/bin/ork-brain.sh +4 -0
  14. package/adapters/hermes/bin/ork-hitl-answer.py +82 -0
  15. package/adapters/hermes/bin/ork-maestro.sh +6 -0
  16. package/adapters/hermes/bin/ork-master-enviar.py +84 -0
  17. package/adapters/hermes/bin/ork-objective-message.sh +17 -0
  18. package/adapters/hermes/bin/ork-objective-status.sh +12 -0
  19. package/adapters/hermes/bin/ork-pulse-enviar.py +47 -0
  20. package/adapters/hermes/hermes.plugin.json +26 -4
  21. package/adapters/hermes/hitl-ingress/__init__.py +356 -0
  22. package/adapters/hermes/hitl-ingress/plugin.yaml +4 -0
  23. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +75 -22
  24. package/adapters/openclaw/README.md +135 -26
  25. package/adapters/openclaw/bin/ork-brain.sh +4 -0
  26. package/adapters/openclaw/construir.sh +19 -0
  27. package/adapters/openclaw/dist/hitl-ingress.js +199 -0
  28. package/adapters/openclaw/dist/index.js +437 -0
  29. package/adapters/openclaw/openclaw.plugin.json +39 -134
  30. package/adapters/openclaw/package.json +26 -0
  31. package/adapters/openclaw/src/hitl-ingress.ts +180 -0
  32. package/adapters/openclaw/src/index.ts +458 -0
  33. package/adapters/openclaw/src/tipos-openclaw.d.ts +58 -0
  34. package/adapters/openclaw/tsconfig.json +15 -0
  35. package/assets/docs/markdownlint-cli2.jsonc +22 -0
  36. package/assets/docs/padroes/documentacao-de-produto.md +133 -0
  37. package/assets/docs/padroes/roadmap-de-produto.md +98 -0
  38. package/assets/docs/produto/README.md +14 -0
  39. package/assets/docs/produto/_modelo-feature.md +59 -0
  40. package/assets/docs/roadmap/README.md +14 -0
  41. package/assets/docs/roadmap/_modelo-item.md +73 -0
  42. package/assets/orkmind-native-schema.json +32 -0
  43. package/assets/orkmind_bridge.py +357 -0
  44. package/assets/orkmind_fixture.py +67 -0
  45. package/assets/orkmind_prospective.py +138 -0
  46. package/assets/reference-tariffs-i07.json +23 -0
  47. package/dist/adapters/claude-bg.js +488 -39
  48. package/dist/adapters/codex-controller-sensor.js +449 -0
  49. package/dist/adapters/codex-controller-worker.js +361 -0
  50. package/dist/adapters/codex-controller.js +252 -0
  51. package/dist/adapters/codex-events.js +205 -0
  52. package/dist/adapters/codex-question.js +26 -0
  53. package/dist/adapters/codex-runner.js +133 -0
  54. package/dist/adapters/codex.js +394 -0
  55. package/dist/agents-md.js +84 -0
  56. package/dist/auditoria.js +11 -8
  57. package/dist/auditrun.js +5 -4
  58. package/dist/board.js +173 -22
  59. package/dist/branch-de-estado.js +136 -0
  60. package/dist/canarios-hitl.js +177 -0
  61. package/dist/canarios-i43.js +543 -0
  62. package/dist/canarios-pulse.js +147 -0
  63. package/dist/canarios-sensores.js +129 -0
  64. package/dist/canarios.js +111 -2
  65. package/dist/catalogo.js +9 -0
  66. package/dist/ci.js +216 -0
  67. package/dist/ciclos.js +2 -1
  68. package/dist/claim-lint.js +64 -0
  69. package/dist/claims.js +60 -0
  70. package/dist/company-brain-capture.js +195 -0
  71. package/dist/company-brain-cli.js +123 -0
  72. package/dist/company-brain-client.js +61 -0
  73. package/dist/company-brain-contract.js +166 -0
  74. package/dist/company-brain-journal.js +192 -0
  75. package/dist/company-brain-mcp.js +33 -0
  76. package/dist/company-brain-migration.js +60 -0
  77. package/dist/company-brain-source.js +177 -0
  78. package/dist/company-brain-worker.js +18 -0
  79. package/dist/conducao-texto.js +61 -0
  80. package/dist/conducao.js +876 -0
  81. package/dist/contrato-publico.js +39 -0
  82. package/dist/creation-operation-store.js +251 -0
  83. package/dist/creation-operation.js +148 -0
  84. package/dist/decisao-autonoma.js +183 -0
  85. package/dist/delegation.js +79 -0
  86. package/dist/demo.js +120 -0
  87. package/dist/docs.js +828 -0
  88. package/dist/doctor.js +183 -24
  89. package/dist/entrega-pr.js +117 -0
  90. package/dist/escopo-escrita.js +71 -0
  91. package/dist/estado-thread.js +222 -0
  92. package/dist/evalrunner.js +12 -0
  93. package/dist/fabrica-estado.js +329 -0
  94. package/dist/fabrica-publicar.js +76 -0
  95. package/dist/fix.js +21 -0
  96. package/dist/gates.js +55 -9
  97. package/dist/handoff.js +39 -23
  98. package/dist/hitl-canais.js +333 -0
  99. package/dist/hitl-classificacao.js +131 -0
  100. package/dist/hitl-contract.js +465 -0
  101. package/dist/hitl-estado.js +136 -0
  102. package/dist/hitl-gates.js +554 -0
  103. package/dist/hitl-ingress-receipt.js +382 -0
  104. package/dist/hitl-local-atestado.js +57 -0
  105. package/dist/hitl-local-receipt.js +328 -0
  106. package/dist/hitl-local.js +143 -0
  107. package/dist/hitl-lock.js +139 -0
  108. package/dist/hitl-lote.js +223 -0
  109. package/dist/hitl-native-offer.js +97 -0
  110. package/dist/hitl-native.js +65 -0
  111. package/dist/hitl-presentation.js +209 -0
  112. package/dist/hitl-public-receipt.js +176 -0
  113. package/dist/hitl-resumo.js +209 -0
  114. package/dist/hitl-sessions.js +475 -0
  115. package/dist/hitl.js +711 -0
  116. package/dist/horario.js +269 -0
  117. package/dist/hosts.js +198 -22
  118. package/dist/index.js +1736 -93
  119. package/dist/indice.js +99 -0
  120. package/dist/init.js +28 -3
  121. package/dist/integracoes-locais.js +17 -0
  122. package/dist/leases.js +65 -21
  123. package/dist/ledger-stats.js +272 -0
  124. package/dist/ledger.js +107 -2
  125. package/dist/licoes.js +215 -0
  126. package/dist/liveness.js +218 -0
  127. package/dist/maestro-actions.js +51 -0
  128. package/dist/maestro-authority.js +152 -0
  129. package/dist/maestro-cli.js +97 -0
  130. package/dist/maestro-contract.js +55 -0
  131. package/dist/maestro-discovery.js +132 -0
  132. package/dist/maestro-runtime.js +268 -0
  133. package/dist/maestro-snapshot.js +86 -0
  134. package/dist/maestro-sources.js +224 -0
  135. package/dist/manifest.js +165 -6
  136. package/dist/maquina.js +102 -0
  137. package/dist/master-audit.js +101 -0
  138. package/dist/master-batch.js +43 -0
  139. package/dist/master-digest.js +149 -0
  140. package/dist/master-migracao.js +155 -0
  141. package/dist/master.js +361 -77
  142. package/dist/mcp-artifacts.js +234 -0
  143. package/dist/mcp-git.js +433 -0
  144. package/dist/mcp-install.js +310 -0
  145. package/dist/mcp-maestro.js +25 -0
  146. package/dist/mcp-server.js +469 -0
  147. package/dist/mcp-ship.js +436 -0
  148. package/dist/mcp-verify.js +173 -0
  149. package/dist/memoria-humana.js +207 -0
  150. package/dist/memoria.js +314 -78
  151. package/dist/memory-migration.js +279 -0
  152. package/dist/memory-prospective.js +148 -0
  153. package/dist/modos-migracao.js +191 -0
  154. package/dist/modos.js +151 -15
  155. package/dist/monitor-lock.js +110 -0
  156. package/dist/objective.js +449 -0
  157. package/dist/ocupacao.js +267 -0
  158. package/dist/onboarding.js +303 -0
  159. package/dist/orkmind.js +389 -90
  160. package/dist/orquestracao.js +96 -69
  161. package/dist/phase.js +596 -150
  162. package/dist/playbook-capabilities.js +150 -0
  163. package/dist/playbook-contracts.js +188 -0
  164. package/dist/portfolio-context.js +61 -0
  165. package/dist/portfolio.js +180 -0
  166. package/dist/preflight.js +207 -0
  167. package/dist/process-audit.js +112 -0
  168. package/dist/project-state.js +93 -0
  169. package/dist/prompts.js +25 -5
  170. package/dist/prova-minima.js +112 -0
  171. package/dist/pulse-cadencia.js +163 -0
  172. package/dist/pulse-consentimento.js +260 -0
  173. package/dist/pulse-delivery.js +323 -0
  174. package/dist/pulse-resposta.js +542 -0
  175. package/dist/pulse.js +262 -0
  176. package/dist/ratelimit.js +35 -2
  177. package/dist/recall.js +80 -3
  178. package/dist/redacao-saida.js +45 -0
  179. package/dist/redacao-url.js +66 -0
  180. package/dist/retry.js +637 -115
  181. package/dist/roadmap-reservas.js +243 -0
  182. package/dist/runtime-ambiente.js +38 -0
  183. package/dist/runtime-context.js +215 -0
  184. package/dist/runtime-profiles.js +869 -0
  185. package/dist/runtimes.js +103 -0
  186. package/dist/sandbox.js +9 -0
  187. package/dist/session-events.js +203 -0
  188. package/dist/session-watcher-claude.js +637 -0
  189. package/dist/session-watcher.js +683 -0
  190. package/dist/sessoes-adopt.js +154 -0
  191. package/dist/sessoes-inventario.js +159 -0
  192. package/dist/sessoes.js +16 -35
  193. package/dist/setup.js +691 -0
  194. package/dist/ship.js +150 -15
  195. package/dist/slug.js +1 -1
  196. package/dist/thread-close.js +115 -0
  197. package/dist/thread.js +142 -28
  198. package/dist/tokens.js +1 -1
  199. package/dist/util.js +4 -2
  200. package/dist/verify-sandbox.js +265 -0
  201. package/dist/verify.js +270 -25
  202. package/dist/versao.js +60 -0
  203. package/dist/worktree.js +31 -2
  204. package/dist/write-activation.js +291 -0
  205. package/dist/yaml.js +2 -0
  206. package/eval/casos/onboarding.json +102 -0
  207. package/eval/casos/orkastery-bootstrap.json +35 -1
  208. package/eval/casos/scope-check-capability-map.json +45 -7
  209. package/eval/casos/ship-release.json +2 -2
  210. package/eval/fixtures/b0-slug-e-modos/caso.json +35 -22
  211. package/eval/fixtures/b2-master-log/caso.json +1 -1
  212. package/eval/fixtures/fx-adapter-editado-detectado/caso.json +19 -0
  213. package/eval/fixtures/fx-auto-quiet/caso.json +11 -0
  214. package/eval/fixtures/fx-blanket-approve/caso.json +10 -0
  215. package/eval/fixtures/fx-check-runtime-cruzado/caso.json +20 -0
  216. package/eval/fixtures/fx-codex-dry/caso.json +18 -0
  217. package/eval/fixtures/fx-donewhen-executavel/caso.json +15 -0
  218. package/eval/fixtures/fx-estado-dividido/caso.json +15 -0
  219. package/eval/fixtures/fx-fase-orfa/caso.json +116 -0
  220. package/eval/fixtures/fx-happy/caso.json +6 -4
  221. package/eval/fixtures/fx-hitl-latency/caso.json +20 -0
  222. package/eval/fixtures/fx-indice-reversao/caso.json +25 -0
  223. package/eval/fixtures/fx-listagem-abertas/caso.json +16 -0
  224. package/eval/fixtures/fx-maestro-bootstrap/caso.json +21 -0
  225. package/eval/fixtures/fx-modo-aposentado-escritor/caso.json +21 -0
  226. package/eval/fixtures/fx-modo-aposentado-leitor/caso.json +16 -0
  227. package/eval/fixtures/fx-objective-oscillation/caso.json +14 -0
  228. package/eval/fixtures/fx-omnicanal/caso.json +17 -0
  229. package/eval/fixtures/fx-sensores-runtime/caso.json +17 -0
  230. package/monitor/company-brain.cjs +17 -0
  231. package/monitor/pulse-scope.cjs +45 -0
  232. package/monitor/pulse.cron +18 -0
  233. package/monitor/varredura-pulse.sh +27 -0
  234. package/package.json +25 -4
  235. package/schemas/claims.schema.json +25 -0
  236. package/schemas/company-brain.schema.json +1158 -0
  237. package/schemas/creation-operation.schema.json +365 -0
  238. package/schemas/maestro-snapshot.schema.json +2716 -0
  239. package/skills/core/onboarding/SKILL.md +50 -0
  240. package/skills/core/orkastery-bootstrap/SKILL.md +95 -65
  241. package/skills/core/thread-state/SKILL.md +3 -1
  242. package/skills/governance/scope-check-capability-map/SKILL.md +17 -7
@@ -0,0 +1,356 @@
1
+ """Ingresso nativo Hermes: callback síncrono, I/O limitado no loop do host.
2
+
3
+ D12: este host é o canal `hermes`. O canal entra no corpo assinado do envelope, não
4
+ num campo solto: Hermes e OpenClaw compartilham transporte, mas têm chaves e identidades
5
+ próprias; sem o canal dentro do HMAC o recibo perderia essa proveniência.
6
+ """
7
+ import asyncio
8
+ import hashlib
9
+ import hmac
10
+ import json
11
+ import logging
12
+ import os
13
+ import re
14
+ import subprocess
15
+ import time
16
+ from datetime import datetime, timezone
17
+
18
+ CANAL = 'hermes'
19
+ COMMAND = re.compile(r'^/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})$')
20
+ NATIVE_COMMAND = re.compile(r'^/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}))?$')
21
+ # I-41 (GO-FIX 1): as duas formas da resposta ao resumo do pulse, que o dono digita sem barra e
22
+ # sem identificador: "P4EJ a" ao resumo e "1a 2c" as perguntas. As MESMAS fontes vivem no nucleo
23
+ # (GRAMATICA_DO_PULSE, core/src/pulse-resposta.ts) e um teste do nucleo confere que continuam
24
+ # iguais. O codigo comeca por letra e tem digito: conversa comum nao tem esta forma, e ele
25
+ # nunca se confunde com a resposta ao lote, que comeca pelo numero da pergunta.
26
+ PULSE_CONSENT = re.compile(r'^[ \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}$', re.IGNORECASE)
27
+ PULSE_LOTE = re.compile(r'^[ \t]*[0-9]{1,2}[ \t]*[a-zA-Z](?:[ \t,;]*[0-9]{1,2}[ \t]*[a-zA-Z])*[ \t]*[.!]?[ \t]*$')
28
+ # I-50 (RM-039): a tag da cadencia do resumo, sozinha na mensagem. Comeca por '#': nunca se
29
+ # confunde com as outras duas formas, e tag no meio de uma frase continua indo para o assistente.
30
+ PULSE_CADENCIA = re.compile(r'^[ \t]*#OrkPulse(?:On(?:-(?:15|30|60)m)?|Off)[ \t]*[.!]?[ \t]*$', re.IGNORECASE)
31
+ # O endereco do pulse no corpo assinado: nem thread nem pedido. Quem traduz e o nucleo.
32
+ PULSE_ALVO, PULSE_ENDERECO = 'pulse', 'resposta'
33
+ CORE_TIMEOUT = 10
34
+ CONFIRM_TIMEOUT = 3
35
+ MAX_ENTRIES = 1024
36
+ MAX_PENDING = 16
37
+ CONFIRM_TEXT = {
38
+ 'gate': 'Orkastery: resposta ao gate registrada pelo núcleo.',
39
+ 'session': 'Orkastery: resposta entregue à sessão pelo núcleo.',
40
+ }
41
+ logger = logging.getLogger(__name__)
42
+ # Só metadados: nunca guardar resposta, chave, stdout ou exceção nestes registros.
43
+ _entries = {}
44
+ _tasks = set()
45
+
46
+
47
+ def _allowed(name, value):
48
+ return value in {s.strip() for s in os.environ.get(name, '').split(',') if s.strip()}
49
+
50
+
51
+ def _status(entry=None, replay=False):
52
+ effect = entry['effect'] if entry else 'unconfirmed'
53
+ confirmation = entry['confirmation'] if entry else 'not_attempted'
54
+ return {'action': 'skip', 'reason': f'ork.hitl: effect={effect}; confirmation={confirmation}',
55
+ 'effect': effect, 'confirmation': confirmation, 'replay': replay}
56
+
57
+
58
+ def _is_pulse(text):
59
+ return '\x00' not in text and bool(PULSE_CONSENT.fullmatch(text) or PULSE_LOTE.fullmatch(text)
60
+ or PULSE_CADENCIA.fullmatch(text))
61
+
62
+
63
+ def _receipt_ok(receipt, operation, pedido):
64
+ if operation == 'pulse':
65
+ # O texto que volta ao dono e montado pelo nucleo; este adaptador so o transporta.
66
+ return (isinstance(receipt, dict) and receipt.get('ok') is True
67
+ and receipt.get('contrato') == 'ork.pulse-resposta/v1'
68
+ and isinstance(receipt.get('mensagem'), str) and bool(receipt['mensagem'])
69
+ and type(receipt.get('repetida')) is bool)
70
+ states = {'aprovado', 'recusado', 'aguardando'} if operation == 'gate' else {'entregue'}
71
+ return (isinstance(receipt, dict) and receipt.get('ok') is True
72
+ and receipt.get('pedidoId') == pedido and receipt.get('estado') in states
73
+ and type(receipt.get('repetida')) is bool
74
+ and (operation == 'gate' or isinstance(receipt.get('sessionId'), str)
75
+ and bool(receipt['sessionId'])))
76
+
77
+
78
+ def _call_core(argv, envelope, root, env):
79
+ return subprocess.run(argv, input=json.dumps(envelope, ensure_ascii=False), cwd=root,
80
+ env=env, text=True, encoding='utf-8', capture_output=True,
81
+ timeout=CORE_TIMEOUT, check=False)
82
+
83
+
84
+ async def _deliver(entry, msg, operation, pedido, argv, envelope, root, env):
85
+ try:
86
+ # A espera no executor também tem orçamento. Cancelamento não prova rollback.
87
+ entry['effect'] = 'unknown'
88
+ result = await asyncio.wait_for(
89
+ asyncio.to_thread(_call_core, argv, envelope, root, env), CORE_TIMEOUT + 1)
90
+ receipt = json.loads(result.stdout) if result.returncode == 0 else None
91
+ if not _receipt_ok(receipt, operation, pedido):
92
+ return
93
+ entry['effect'] = 'confirmed'
94
+ if receipt['repetida']:
95
+ # Após restart não sabemos se o canal já recebeu. Nunca reenviar às cegas.
96
+ entry['confirmation'] = 'unknown'
97
+ return
98
+ entry['confirmation'] = 'unknown'
99
+ reply = receipt['mensagem'] if operation == 'pulse' else CONFIRM_TEXT[operation]
100
+ sent = await asyncio.wait_for(msg.reply_text(
101
+ reply, do_quote=False, parse_mode=None,
102
+ read_timeout=CONFIRM_TIMEOUT, write_timeout=CONFIRM_TIMEOUT,
103
+ connect_timeout=CONFIRM_TIMEOUT, pool_timeout=CONFIRM_TIMEOUT), CONFIRM_TIMEOUT)
104
+ from telegram import Message
105
+ if (isinstance(sent, Message) and type(sent.message_id) is int and sent.message_id > 0
106
+ and sent.chat.id == msg.chat.id and sent.from_user.id == msg.get_bot().id
107
+ and sent.from_user.is_bot is True and sent.text == reply):
108
+ entry['confirmation'] = 'sent'
109
+ except (Exception, asyncio.CancelledError):
110
+ # Timeout/exceção/recibo inválido: efeito pode existir; canal pode ter recebido.
111
+ # Nunca ecoar detalhes do CLI/PTB, nem transformar incerteza em sucesso.
112
+ pass
113
+ finally:
114
+ entry['done'] = True
115
+ logger.info('ork.hitl: ref=%s effect=%s confirmation=%s',
116
+ entry['ref'], entry['effect'], entry['confirmation'])
117
+
118
+
119
+ def _ingress(*, event, gateway, **_kwargs):
120
+ text = getattr(event, 'text', '')
121
+ if not isinstance(text, str):
122
+ return None
123
+ pulse = not text.startswith('/ork ') and _is_pulse(text)
124
+ if not text.startswith('/ork ') and not pulse:
125
+ return None
126
+ if getattr(getattr(event, 'source', None), 'platform', None) and event.source.platform.value != 'telegram':
127
+ # O resumo do pulse sai pelo Telegram; a resposta a ele so e reconhecida la.
128
+ return None if pulse else _native_ingress(event, gateway, _kwargs.get('session_store'))
129
+ # invoke_hook instalado NÃO resolve coroutine. Todo caminho /ork, e a resposta ao pulse, retorna skip.
130
+ try:
131
+ from gateway.platforms.base import MessageEvent
132
+ from telegram import Message
133
+ if not isinstance(event, MessageEvent) or getattr(event, 'internal', False):
134
+ return _status()
135
+ msg, source = event.raw_message, event.source
136
+ if (not isinstance(msg, Message) or source.platform.value != 'telegram'
137
+ or gateway._is_user_authorized(source) is not True):
138
+ return _status()
139
+ user, chat = str(msg.from_user.id), str(msg.chat.id)
140
+ bot = os.environ.get('ORK_HITL_TELEGRAM_BOT_ID', '')
141
+ if (msg.from_user.is_bot is not False or not bot or str(msg.get_bot().id) != bot
142
+ or user == bot or getattr(msg, 'forward_origin', None) or getattr(msg, 'sender_chat', None)
143
+ or str(source.user_id) != user or str(source.chat_id) != chat
144
+ or type(msg.message_id) is not int or msg.message_id <= 0
145
+ or str(event.message_id) != str(msg.message_id) or msg.text != text
146
+ or not _allowed('ORK_HITL_TELEGRAM_USERS', user)
147
+ or not _allowed('ORK_HITL_TELEGRAM_CHATS', chat)):
148
+ return _status()
149
+ if pulse:
150
+ # O dono respondeu ao resumo ou ao lote. O texto inteiro e a resposta, e o endereco
151
+ # assinado e o do pulse: este adaptador nao conhece pedido e nao escolhe nada.
152
+ operation, thread, pedido, answer = 'pulse', PULSE_ALVO, PULSE_ENDERECO, text
153
+ else:
154
+ match = COMMAND.fullmatch(text)
155
+ if not match or '\x00' in text:
156
+ return _status()
157
+ operation, thread, pedido, answer = match.groups()
158
+ now = datetime.now(timezone.utc)
159
+ age = (now - msg.date).total_seconds()
160
+ # FX1: este canal assina com a SUA chave. ORK_HITL_INGRESS_KEY fica so para o
161
+ # envelope v1 legado, que este adaptador nao produz mais.
162
+ key = os.environ.get('ORK_HITL_INGRESS_KEY_HERMES', '')
163
+ root = os.environ.get('ORK_HITL_ROOT', '')
164
+ if not 0 <= age <= 60 or len(key.encode()) < 32 or not os.path.isabs(root):
165
+ return _status()
166
+ envelope = {'resposta': answer, 'canal': CANAL, 'origem': 'telegram', 'por': f'telegram:{user}',
167
+ 'mensagem': f'telegram:{chat}:{msg.message_id}',
168
+ 'recebidoEm': msg.date.astimezone(timezone.utc).isoformat(timespec='milliseconds').replace('+00:00', 'Z')}
169
+ # FX5: `conta` tem posicao fixa no corpo v2, e e `None` neste canal: a allowlist do
170
+ # Hermes e de usuario e chat, nao de conta. Declarar conta aqui seria recusado pelo
171
+ # nucleo, e omitir a posicao mudaria a aridade do corpo assinado.
172
+ body = ['ork.hitl-answer/v2', thread, pedido, CANAL, None, envelope['origem'], envelope['por'],
173
+ envelope['mensagem'], envelope['recebidoEm'], answer]
174
+ envelope['prova'] = hmac.new(key.encode(), json.dumps(body, ensure_ascii=False, separators=(',', ':')).encode(), hashlib.sha256).hexdigest()
175
+ # Cada subcomando tem a sua flag de stdin: `gate answer` exige --resposta-stdin e
176
+ # `sessions answer` exige --stdin. Mandar a mesma flag para os dois fazia toda
177
+ # resposta de sessao ser recusada pelo proprio CLI, antes de qualquer conferencia.
178
+ if operation == 'pulse':
179
+ argv = [os.environ.get('ORK_BIN', 'ork'), 'pulse', 'responder', '--resposta-stdin',
180
+ '--origem', 'telegram', '--canal', CANAL,
181
+ '--por', envelope['por'], '--mensagem', envelope['mensagem']]
182
+ else:
183
+ argv = [os.environ.get('ORK_BIN', 'ork'), 'gate' if operation == 'gate' else 'sessions',
184
+ 'answer', thread, pedido,
185
+ '--resposta-stdin' if operation == 'gate' else '--stdin',
186
+ '--origem', 'telegram', '--canal', CANAL,
187
+ '--por', envelope['por'], '--mensagem', envelope['mensagem']]
188
+ # Usar o loop que possui o bot HTTP; nunca asyncio.run/thread com esse bot.
189
+ loop = asyncio.get_running_loop()
190
+ identity = (root, bot, chat, str(msg.message_id))
191
+ fingerprint = operation + ':' + envelope['prova']
192
+ previous = _entries.get(identity)
193
+ if previous:
194
+ return _status(previous, replay=True) if hmac.compare_digest(previous['fingerprint'], fingerprint) else _status()
195
+ for identity_old, old in list(_entries.items()):
196
+ if old['done'] and time.monotonic() >= old['expires']:
197
+ del _entries[identity_old]
198
+ if len(_entries) >= MAX_ENTRIES or len(_tasks) >= MAX_PENDING:
199
+ return _status()
200
+ entry = {'fingerprint': fingerprint, 'effect': 'pending', 'confirmation': 'not_attempted',
201
+ 'done': False, 'expires': time.monotonic() + 61,
202
+ 'ref': hashlib.sha256(json.dumps(identity).encode()).hexdigest()[:16]}
203
+ _entries[identity] = entry # Reserva antes de agendar, sem await no callback.
204
+ work = _deliver(entry, msg, operation, pedido, argv, envelope, root, dict(os.environ))
205
+ try:
206
+ task = loop.create_task(work)
207
+ except Exception:
208
+ work.close()
209
+ entry.update(effect='unconfirmed', done=True)
210
+ return _status(entry)
211
+ _tasks.add(task)
212
+ task.add_done_callback(_tasks.discard)
213
+ return _status(entry)
214
+ except Exception:
215
+ pass
216
+ return _status()
217
+
218
+
219
+ def register(ctx):
220
+ ctx.register_hook('pre_gateway_dispatch', _ingress)
221
+
222
+
223
+ async def _native_core(argv, root, envelope=None):
224
+ # Subprocesso assíncrono limitado: não prende executor thread no shutdown.
225
+ proc = await asyncio.create_subprocess_exec(*argv, cwd=root,
226
+ stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL,
227
+ limit=65536)
228
+ try:
229
+ data = None if envelope is None else json.dumps(envelope, ensure_ascii=False).encode()
230
+ out, _ = await asyncio.wait_for(proc.communicate(data), CORE_TIMEOUT)
231
+ if proc.returncode or len(out) > 65536:
232
+ raise ValueError('native.core.unavailable')
233
+ return json.loads(out)
234
+ except BaseException:
235
+ if proc.returncode is None:
236
+ proc.kill()
237
+ await proc.wait()
238
+ raise
239
+
240
+
241
+ async def _native_deliver(entry, msg, match, binding, key, root):
242
+ operation, thread, pedido, answer = match.groups()
243
+ cli = os.environ.get('ORK_BIN', 'ork')
244
+ try:
245
+ compact = lambda v: json.dumps(v, ensure_ascii=False, separators=(',', ':'))
246
+ offer = dict(native=dict(binding, messageId=str(msg.id)),
247
+ recebidoEm=msg.created_at.astimezone(timezone.utc).isoformat(timespec='milliseconds').replace('+00:00', 'Z'))
248
+ body = ['ork.hitl-native-offer/v1', thread, pedido] + [offer['native'][k] for k in
249
+ ['host', 'installationId', 'connectionId', 'sessionId', 'accountId', 'channelId', 'conversationId', 'personId', 'messageId']] + [offer['recebidoEm']]
250
+ offer['prova'] = hmac.new(key.encode(), compact(body).encode(), hashlib.sha256).hexdigest()
251
+ view = await _native_core([cli, 'gate', 'context', thread, pedido, '--native-offer-stdin'], root, offer)
252
+ p = view.get('pedido', {})
253
+ if (p.get('id') != pedido or p.get('thread') != thread or (operation != 'offer' and p.get('alvo', {}).get('tipo') != operation)
254
+ or not any(c.get('canal') == 'hermes' and c.get('transporte') == 'native' and c.get('estado') == 'disponivel' for c in view.get('canais', []))
255
+ or not re.fullmatch('[a-f0-9]{64}', view.get('contexto', ''))
256
+ or not re.fullmatch('[a-f0-9]{64}', view.get('pedidoSha256', ''))):
257
+ return
258
+ if operation == 'offer':
259
+ text = view.get('apresentacao', {}).get('mensagem')
260
+ if not isinstance(text, str):
261
+ return
262
+ # I-35: o prazo chega ao dono no fuso dele; nucleo de outra versao pode nao
263
+ # ter posto o prazo local na mensagem, entao o campo local vai junto.
264
+ prazo_local = view.get('prazoLocal')
265
+ if isinstance(prazo_local, str) and prazo_local and prazo_local not in text:
266
+ text += '\nPrazo (fuso do dono): ' + prazo_local
267
+ import discord
268
+ entry['effect'] = 'presented'
269
+ entry['confirmation'] = 'unknown'
270
+ text += '\nCanal nativo Hermes disponível nesta conversa.'
271
+ sent = await asyncio.wait_for(msg.reply(text, mention_author=False,
272
+ allowed_mentions=discord.AllowedMentions.none()), CONFIRM_TIMEOUT)
273
+ if isinstance(sent, discord.Message) and sent.channel.id == msg.channel.id and sent.content == text:
274
+ entry['confirmation'] = 'sent'
275
+ return
276
+ n = dict(binding, messageId=str(msg.id), context=view['contexto'],
277
+ pedidoSha256=view['pedidoSha256'], expiresAt=p['prazo'])
278
+ identity = [n[k] for k in ['host', 'installationId', 'connectionId', 'sessionId', 'channelId', 'accountId', 'conversationId', 'messageId']]
279
+ envelope = dict(origem='native', canal='hermes', conta=n['accountId'], native=n,
280
+ por='native:hermes:' + n['personId'], mensagem='native:' + hashlib.sha256(compact(identity).encode()).hexdigest(),
281
+ recebidoEm=msg.created_at.astimezone(timezone.utc).isoformat(timespec='milliseconds').replace('+00:00', 'Z'), resposta=answer)
282
+ body = ['ork.hitl-native/v1', thread, pedido] + [n[k] for k in ['host', 'installationId', 'connectionId', 'sessionId',
283
+ 'accountId', 'channelId', 'conversationId', 'personId', 'messageId', 'context', 'pedidoSha256', 'expiresAt']] + [envelope[k] for k in ['origem', 'canal', 'conta', 'por', 'mensagem', 'recebidoEm', 'resposta']]
284
+ envelope['prova'] = hmac.new(key.encode(), compact(body).encode(), hashlib.sha256).hexdigest()
285
+ entry['effect'] = 'unknown'
286
+ receipt = await _native_core([cli, 'gate' if operation == 'gate' else 'sessions', 'answer', thread, pedido,
287
+ '--resposta-stdin' if operation == 'gate' else '--stdin', '--origem', 'native', '--canal', 'hermes',
288
+ '--conta', n['accountId'], '--por', envelope['por'], '--mensagem', envelope['mensagem']], root, envelope)
289
+ if not _receipt_ok(receipt, operation, pedido):
290
+ return
291
+ entry['effect'] = 'confirmed'
292
+ if receipt['repetida']:
293
+ entry['confirmation'] = 'unknown'
294
+ return
295
+ entry['confirmation'] = 'unknown'
296
+ import discord
297
+ sent = await asyncio.wait_for(msg.reply(CONFIRM_TEXT[operation], mention_author=False,
298
+ allowed_mentions=discord.AllowedMentions.none()), CONFIRM_TIMEOUT)
299
+ if isinstance(sent, discord.Message) and sent.channel.id == msg.channel.id and sent.content == CONFIRM_TEXT[operation]:
300
+ entry['confirmation'] = 'sent'
301
+ except (Exception, asyncio.CancelledError):
302
+ pass
303
+ finally:
304
+ entry['done'] = True
305
+ logger.info('ork.hitl.native: ref=%s effect=%s confirmation=%s', entry['ref'], entry['effect'], entry['confirmation'])
306
+
307
+
308
+ def _native_ingress(event, gateway, session_store):
309
+ """Callback Discord do gateway instalado; terminal/ACP sem atestado fica indisponível."""
310
+ try:
311
+ from gateway.platforms.base import MessageEvent
312
+ import discord
313
+ if not isinstance(event, MessageEvent) or getattr(event, 'internal', False):
314
+ return _status()
315
+ msg, source = event.raw_message, event.source
316
+ binding = json.loads(os.environ.get('ORK_HITL_NATIVE_BINDING_HERMES', 'null'))
317
+ key, root = os.environ.get('ORK_HITL_NATIVE_KEY_HERMES', ''), os.environ.get('ORK_HITL_ROOT', '')
318
+ fields = {'host', 'installationId', 'connectionId', 'sessionId', 'accountId', 'channelId', 'conversationId', 'personId'}
319
+ if (not isinstance(binding, dict) or set(binding) != fields or any(not isinstance(v, str) or not v for v in binding.values())
320
+ or binding['host'] != 'hermes' or binding['channelId'] != 'discord' or source.platform.value != 'discord'
321
+ or not isinstance(msg, discord.Message) or source.is_bot is not False or msg.author.bot is not False
322
+ or msg.webhook_id is not None or msg.application_id is not None
323
+ or gateway._is_user_authorized(source) is not True or len(key.encode()) < 32 or not os.path.isabs(root)
324
+ or str(msg.author.id) != source.user_id or source.user_id != binding['personId']
325
+ or str(msg.channel.id) != source.chat_id or source.chat_id != binding['conversationId']
326
+ or str(getattr(msg.guild, 'id', '')) != binding['accountId']
327
+ or str(event.message_id) != str(msg.id) or msg.content != event.text):
328
+ return _status()
329
+ age = (datetime.now(timezone.utc) - msg.created_at).total_seconds()
330
+ if not 0 <= age <= 60:
331
+ return _status()
332
+ # API observada no SessionStore instalado; não cria nem reseta sessão por consulta.
333
+ entries = [s for s in session_store.list_sessions() if s.session_id == binding['sessionId']
334
+ and s.session_key == session_store._generate_session_key(source) and not s.suspended]
335
+ if len(entries) != 1:
336
+ return _status()
337
+ match = NATIVE_COMMAND.fullmatch(event.text)
338
+ if not match or '\x00' in event.text or (match[1] == 'offer' and match[4] is not None) or (match[1] != 'offer' and not match[4]):
339
+ return _status()
340
+ identity = (root, binding['connectionId'], binding['sessionId'], source.chat_id, str(msg.id))
341
+ if identity in _entries:
342
+ return _status(_entries[identity], replay=True)
343
+ for old_id, old in list(_entries.items()):
344
+ if old['done'] and time.monotonic() >= old['expires']:
345
+ del _entries[old_id]
346
+ if len(_entries) >= MAX_ENTRIES or len(_tasks) >= MAX_PENDING:
347
+ return _status()
348
+ entry = dict(effect='pending', confirmation='not_attempted', done=False, expires=time.monotonic() + 61,
349
+ ref=hashlib.sha256(json.dumps(identity).encode()).hexdigest()[:16])
350
+ _entries[identity] = entry
351
+ task = asyncio.get_running_loop().create_task(_native_deliver(entry, msg, match, binding, key, root))
352
+ _tasks.add(task)
353
+ task.add_done_callback(_tasks.discard)
354
+ return _status(entry)
355
+ except Exception:
356
+ return _status()
@@ -0,0 +1,4 @@
1
+ name: orkastery-hitl
2
+ version: 0.1.0
3
+ description: Resposta HITL pelos callbacks autenticados Telegram ou Discord nativo, sem tool do modelo.
4
+ kind: standalone
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: orkastery-devmaster
3
- description: "Roteador do Orkastery no Hermes: le a #TAG de conducao do pedido do builder, chama o `ork` para abrir e conduzir a thread pelas 6 fases, e apresenta os gates ao humano. Zero regra de negocio: quem decide e o humano, quem verifica e o `ork`."
3
+ description: "Reconhece orkastery maestro no Hermes: consulta panorama pelo ork_maestro, conduz demandas autorizadas e apresenta HITL na conversa."
4
4
  bucket: hermes
5
5
  roteia: "ork modos --do-pedido | ork thread new | ork phase run | ork ship | ork master"
6
6
  license: MIT
@@ -8,6 +8,20 @@ license: MIT
8
8
 
9
9
  # Orkastery no Hermes
10
10
 
11
+ Mensagem literal `orkastery maestro`: use `ork_maestro` (wrapper de `ork maestro --json`)
12
+ para consultar panorama do projeto atual. Não abra thread por essa consulta. Apresente
13
+ fontes/lacunas e próximas ações; ausência/ambiguidade de projeto exige contexto permitido.
14
+ Em sessão de fase já despachada, siga o bloco recebido sem abrir outra orquestração.
15
+ Ações usam os comandos tipados do núcleo com precondições e readback.
16
+
17
+ Usabilidade HITL tem prioridade máxima: tópicos, recomendação e opções claras, UUID
18
+ interno. Telegram é opcional. Só anuncie ingresso humano disponível quando o callback
19
+ do host estiver homologado e configurado; texto de tool não é identidade humana.
20
+ Sem ingresso no canal escolhido, conserve o pedido pendente e explique o motivo.
21
+ Não altere runtime, provider, perfil filho ou sandbox para contornar falha.
22
+ Horário para o dono sai no fuso dele (`owner.timezone`): use `prazoLocal` e os fatos
23
+ `*Local` do JSON (ex.: `19/09 15:16 (horário de Brasília)`), nunca o ISO em UTC.
24
+
11
25
  ## O que esta skill e
12
26
 
13
27
  O roteador do Orkastery dentro do Hermes, e **so isso**. Ela nao conduz fase por conta propria,
@@ -23,30 +37,47 @@ mora no `ork`; aqui ficou o roteador.**
23
37
 
24
38
  Sempre que o builder pedir trabalho de produto num repositorio que tem `orkastery.yaml`.
25
39
 
40
+ ## Onboarding do projeto
41
+
42
+ Quando o builder pedir onboarding, execute `ork onboarding` para obter a pauta do núcleo.
43
+ Consulte `ork onboarding show --json` para retomar as etapas pendentes. Conduza a conversa
44
+ pela pauta devolvida e grave cada resposta pública com `ork onboarding set <etapa>
45
+ --conteudo <JSON> --por <quem>`, transportando argumentos como dados. A pauta e a validação
46
+ pertencem ao núcleo; não mantenha lista de etapas no host nem invente respostas.
47
+ Credenciais são somente nomes de variáveis; valores secretos ficam em `~/.hermes/.env`.
48
+ Use `ork onboarding reset [etapa]` quando solicitado e `ork onboarding sync --json` para
49
+ publicação opcional. Apresente o motivo tipado de degradação quando a memória não publicar.
50
+ Onboarding não exige criar thread de produto. Ao concluir, apresente `ork onboarding show --json`.
51
+
26
52
  ## Passo 1: o modo sai do pedido, nao de configuracao
27
53
 
28
54
  ```bash
29
55
  MODO=$(ork modos --do-pedido "<pedido inteiro do builder>")
30
56
  ```
31
57
 
32
- O comando usa a mesma funcao do nucleo que reconhece `#Look`, `#Ork`, `#Classic`, `#Maestro` e
33
- `#Auto`. Sem tag no texto vale o `conduction.default_mode` do manifesto. **Nao reimplemente esse
34
- parse aqui**, e nao valide o modo: `conduction.allowed_modes` e conferido dentro do
35
- `ork thread new`, que recusa com erro tipado quando o projeto nao permite o modo pedido.
58
+ O comando usa a mesma funcao do nucleo que reconhece `#Classic`, `#Maestro`, `#Auto` e `#Fast`.
59
+ Sem tag no texto vale o `conduction.default_mode` do manifesto. **Nao reimplemente esse parse
60
+ aqui**, e nao valide o modo: `ork thread new` confere `conduction.allowed_modes` e recusa com erro tipado.
61
+
62
+ `#Look` e `#Ork` foram aposentados pela I-43. Um pedido com uma dessas tags faz o comando
63
+ sair com codigo != 0 e a recusa tipada `modo.aposentado`, que ja nomeia o substituto vivo.
64
+ **Repasse a recusa ao builder como ela veio**: nao traduza para outro modo e nao siga com o
65
+ default, porque trocar o regime de supervisao pelas costas de quem pediu e o defeito que a
66
+ recusa existe para impedir.
36
67
 
37
68
  | #TAG | Pausas | Quando o builder escolhe |
38
69
  |---|---|---|
39
- | `#Look` | 6 | Risco alto, passo irreversivel, dominio novo |
40
- | `#Ork` | 5 | Veredito separado da execucao e da evidencia |
41
- | `#Classic` | 3 | O padrao: premissas delicadas, entrega solta, score em batch |
42
- | `#Maestro` | 1 | Solucao clara e agil, score em batch |
70
+ | `#Classic` | 3 | O padrao do `ork init`: premissas delicadas |
71
+ | `#Maestro` | 1 | Solucao clara e agil: uma pausa para as premissas |
43
72
  | `#Auto` | 0 | Docs, estudos, configuracoes, auditorias |
73
+ | `#Fast` | 0 | Pedido pequeno e claro: so a GO; o push pede autorizacao |
44
74
 
45
75
  ## Passo 2: antes de abrir, confira a maquina
46
76
 
47
77
  ```bash
48
78
  ork doctor # sai != 0 quando o despacho nao vale (custo, runtime, ambiente)
49
79
  ork board plan # quem avanca agora, quem espera e por que
80
+ ork brain status # contrato Company Brain e tenant efetivo
50
81
  ```
51
82
 
52
83
  Um `fail` do doctor para o ciclo antes de ele abrir. Isso e economia, nao obstaculo.
@@ -63,33 +94,54 @@ ork phase run <thread> GO --prompt "<uma tarefa do PLAN por vez>"
63
94
  ork verify <thread>
64
95
  ork phase run <thread> CHECK --prompt "<escopo da verificacao>"
65
96
  ork ship <thread> --para main --autorizar-push "<quem>"
66
- ork master <thread> --score <0-5> --justificativa "<texto>" --por "<quem>"
97
+ ork master <thread>
67
98
  ```
68
99
 
100
+ Quando o dono pedir estado do Company Brain, use o binário instalado `ork_brain` para
101
+ `status`, `query` ou `get`. A identidade vem exclusivamente do login autenticado do
102
+ OrkMind; nunca aceite principal, DSN ou raiz vindos da conversa. Consultas devem levar a
103
+ thread explícita e continuar somente leitura.
104
+
69
105
  Cada `phase run` grava o prompt exato com sha256 e registra no ledger. O `ork` reverifica no
70
106
  runtime que a sessao existe: self-report de despacho nao vale como evidencia.
71
107
 
72
108
  ## Passo 4: apresentar os gates ao humano
73
109
 
74
110
  O Hermes e o podio: e aqui que o builder ve a evidencia e da o veredito. Quando o bloco pausa,
75
- mostre o que o `ork` produziu e espere. A liberacao vira evento:
111
+ mostre o que o `ork` produziu e espere. A resposta entra pelo gateway autenticado como update Telegram original, com o remetente e o chat validados por allowlist. Use `ork_gate_answer` ou `ork_session_answer`, preservando o id do pedido e a referência da mensagem. Nunca sintetize um update nem use o nome do builder em uma aprovação criada pelo agente.
76
112
 
77
113
  ```bash
78
- ork gate approve <thread> <objetivo|premissas|evidencias|push|score> --por "<quem>"
114
+ python3 adapters/hermes/bin/ork-hitl-answer.py ork_gate_answer <thread> <pedido> < update.json
115
+ python3 adapters/hermes/bin/ork-hitl-answer.py ork_session_answer <thread> <pedido> < update.json
79
116
  ```
80
117
 
81
- Quando o bloco nao pausa (`#Maestro`, `#Auto`), o `ork` registra a decisao autonoma com quem
82
- decidiu, com que evidencia e por que. **O modo afrouxa a pausa, NUNCA a verificacao.**
118
+ `ORK_HITL_TELEGRAM_USERS` e `ORK_HITL_TELEGRAM_CHATS` contêm os ids permitidos, separados por vírgula. Sem configuração o comando recusa. A profundidade já vem do núcleo. Expiração só espera ou escala, nunca aprova. Push e score não são delegáveis.
83
119
 
84
- ## Passo 5: o score
120
+ ### Atencao em camadas (I-41) e os formatos que o ingresso aceita
85
121
 
86
- O score de 0 a 5 e humano em todos os modos. Nos modos sem pausa de MASTER, ele vai para a fila:
122
+ O nucleo monta e o Hermes so transporta: (1) UM resumo recorrente com as contagens e
123
+ `Posso te mandar as perguntas agora?`; (2) o dono responde com o codigo e a letra (`K7QX a`);
124
+ (3) com o sim, ate cinco objetivas a–d com uma recomendada, respondidas numa linha (`1a 2c 3b`).
125
+ Decisao obvia chega tomada e informada no resumo (`ork decisao registrar`). Cadencia (I-50): so a tag na mensagem (`#OrkPulseOff` 8h, `#OrkPulseOn` 2h, `#OrkPulseOn-15m`, `-30m`, `-60m`); pedida no meio da frase, devolva a tag exata para o dono mandar sozinha.
87
126
 
88
- ```bash
89
- ork master --batch
90
- ```
127
+ O plugin `orkastery-hitl` so registra esses tres formatos e `/ork gate|session <thread> <pedido> <resposta>`,
128
+ em texto puro, da allowlist, com menos de 60 s. "1. Aprovado" em conversa **nao registra nada**:
129
+ devolva na hora a linha exata, com o id atual de `ork gate request <thread>` (o id muda a cada
130
+ renovacao). **Voce nunca responde pelo dono**: `ork gate answer` do terminal falha com
131
+ `proveniencia em argv diverge do envelope`, e essa e a defesa funcionando.
132
+
133
+ Quando o bloco nao pausa (`#Maestro`, `#Auto`, `#Fast`), o `ork` registra a decisao autonoma com quem
134
+ decidiu, com que evidencia e por que. **O modo afrouxa a pausa, NUNCA a verificacao.**
135
+
136
+ ## Passo 5: a entrega e o MASTER
137
+
138
+ O MASTER fecha com POSTMORTEM tipado e indice derivado do ledger; ninguem digita nota para a fila
139
+ andar. `ork master` mostra as entregas; `ork master --aceitar-omissao` aceita as pendentes com
140
+ registro; a nota humana sobrescreve. `ork master --batch` saiu na I-43 (`master.fila-aposentada`).
91
141
 
92
- Fila que ninguem passa e score que nao existe. Traga a fila ao builder.
142
+ Memoria "degradada" tem dois portoes: a variavel da DSN no ambiente do processo
143
+ (`memory_degraded` com `dsn.env-ausente`) e a ativacao de escrita com aceite humano
144
+ (`write.activation.disabled`). Diagnostico: `ork memory status` e `ork doctor`.
93
145
 
94
146
  ## Racionalizacoes comuns
95
147
 
@@ -99,7 +151,8 @@ Fila que ninguem passa e score que nao existe. Traga a fila ao builder.
99
151
  | "Guardo as regras da metodologia nesta skill" | Metodologia em prosa e metodologia que ninguem verifica. Ela mora no `ork`; aqui fica o roteamento. |
100
152
  | "Valido o modo antes de chamar o `ork`" | Validacao duplicada e validacao que vai divergir. `conduction.allowed_modes` e do nucleo. |
101
153
  | "O builder confia, pulo o gate desta vez" | Gate pulado e gate que nao existe. Se o modo nao pausa, o `ork` registra a decisao autonoma; se pausa, ele espera. |
102
- | "Score eu estimo pelo resultado" | O score e do humano em todos os modos. Estimar nota destroi a unica metrica de conducao do produto. |
154
+ | "Score eu estimo pelo resultado" | O indice sai do ledger e a nota, quando existe, e do humano. Estimar nota destroi a metrica de conducao. |
155
+ | "O dono respondeu na conversa, eu registro" | So o ingresso registra, com prova HMAC do update. Devolva a linha exata para ele mandar. |
103
156
 
104
157
  ## Bandeiras vermelhas
105
158
 
@@ -107,7 +160,7 @@ Fila que ninguem passa e score que nao existe. Traga a fila ao builder.
107
160
  - Chamada de `git` direta em vez de `ork ship`.
108
161
  - Modo escolhido por configuracao do host, e nao pela #TAG do pedido.
109
162
  - Thread aberta com `ork doctor` em `fail`.
110
- - Fila de `ork master --batch` crescendo sem ninguem passar nela.
163
+ - Resposta do dono "registrada" pelo agente, sem o update autenticado do canal.
111
164
 
112
165
  ## Verificacao antes de responder ao builder
113
166