@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,876 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ErroDeConducao = exports.ESPERA_PADRAO_MS = exports.PRAZO_DA_RESERVA_MS = exports.PRAZO_DE_SESSAO_PADRAO_MS = exports.MARGEM_DO_PRAZO_MS = exports.ENV_THREAD_DO_DESPACHO = exports.ENV_IDENTIDADE_DE_DESPACHO = exports.ENV_CANAL = exports.ERRO_CANAL_DESCONHECIDO = exports.CANAIS_DE_CONDUCAO = exports.conducaoDoLease = void 0;
37
+ exports.validarCanal = validarCanal;
38
+ exports.canalDoProcesso = canalDoProcesso;
39
+ exports.ambienteDaConducao = ambienteDaConducao;
40
+ exports.identidadeDoAmbiente = identidadeDoAmbiente;
41
+ exports.prazoDaVerificacao = prazoDaVerificacao;
42
+ exports.nomeDaConducao = nomeDaConducao;
43
+ exports.fimDaSessao = fimDaSessao;
44
+ exports.conducaoDosDados = conducaoDosDados;
45
+ exports.conducaoDaThread = conducaoDaThread;
46
+ exports.dormir = dormir;
47
+ exports.vidaDoProcesso = vidaDoProcesso;
48
+ exports.tomarConducao = tomarConducao;
49
+ exports.registrarConducaoDaSessao = registrarConducaoDaSessao;
50
+ exports.comandoDeEsperar = comandoDeEsperar;
51
+ exports.recusaDeConducao = recusaDeConducao;
52
+ exports.textoDoPedidoRepetido = textoDoPedidoRepetido;
53
+ exports.registrarRecusa = registrarRecusa;
54
+ exports.comConducao = comConducao;
55
+ exports.esperarConducaoLivre = esperarConducaoLivre;
56
+ exports.liberarSeOrfa = liberarSeOrfa;
57
+ exports.assumirConducao = assumirConducao;
58
+ exports.textoDoStatusDaConducao = textoDoStatusDaConducao;
59
+ /**
60
+ * I-36 (RM-036): conducao multicanal do Maestro no nucleo.
61
+ *
62
+ * Existe UM Maestro, o dono, e varios canais por onde ele conduz a mesma fabrica: o Claude Code,
63
+ * o Hermes, o OpenClaw, o Codex, o MCP e o CLI. Um pedido vindo de outro canal e o mesmo dono
64
+ * chegando por outra porta, e nunca um intruso a ser derrubado. O que o nucleo garante e que dois
65
+ * pedidos nunca EXECUTEM juntos na mesma worktree: em 19/09/2026 duas sessoes de canais diferentes
66
+ * rodaram build e teste ao mesmo tempo na worktree da mesma thread, o `verify` gravou `code: -1`
67
+ * como reprovacao, e o unico jeito de destravar foi matar a sessao do outro canal.
68
+ *
69
+ * O recurso e o lease `exec:<thread>`, no estado canonico do projeto (D1). Quem conduz fica nele:
70
+ * canal, sessao ou processo, fase, desde quando e o sha do prompt. O segundo pedido e recusado na
71
+ * hora com `conducao.em-andamento` e as tres acoes (esperar, acompanhar, assumir), com `--esperar`
72
+ * como opcao (D2, decisao do dono em 27/09/2026). Vida se prova pelo kernel ou pelo runtime, nunca
73
+ * por PID ou prazo sozinhos (D7): liberar pelo prazo o que ainda roda e exatamente o incidente.
74
+ */
75
+ const fs = __importStar(require("node:fs"));
76
+ const path = __importStar(require("node:path"));
77
+ const node_crypto_1 = require("node:crypto");
78
+ const node_child_process_1 = require("node:child_process");
79
+ const codex_runner_1 = require("./adapters/codex-runner");
80
+ const conducao_texto_1 = require("./conducao-texto");
81
+ var conducao_texto_2 = require("./conducao-texto");
82
+ Object.defineProperty(exports, "conducaoDoLease", { enumerable: true, get: function () { return conducao_texto_2.conducaoDoLease; } });
83
+ const leases_1 = require("./leases");
84
+ const ledger_1 = require("./ledger");
85
+ const maquina_1 = require("./maquina");
86
+ const thread_1 = require("./thread");
87
+ const horario_1 = require("./horario");
88
+ const util_1 = require("./util");
89
+ // ---------------------------------------------------------------------------
90
+ // Canais (D5, D6): registro fechado, declarado pela borda.
91
+ // ---------------------------------------------------------------------------
92
+ /** O registro fechado de canais: de onde o pedido chega e como a borda o declara. */
93
+ exports.CANAIS_DE_CONDUCAO = {
94
+ 'claude-code': {
95
+ origem: 'sessao interativa do Claude Code',
96
+ declaracao: 'servidor MCP iniciado com --host claude-code; no CLI, a variavel CLAUDECODE=1 que o proprio Claude Code exporta',
97
+ },
98
+ hermes: {
99
+ origem: 'assistente Hermes',
100
+ declaracao: 'HERMES_HOME, definido pelo servico do gateway, ou --canal hermes nos binarios do adaptador',
101
+ },
102
+ openclaw: {
103
+ origem: 'plugin OpenClaw',
104
+ declaracao: 'ORK_CANAL=openclaw no ambiente que o adaptador monta para cada chamada do ork',
105
+ },
106
+ codex: {
107
+ origem: 'sessao do Codex',
108
+ declaracao: 'servidor MCP iniciado com --host codex',
109
+ },
110
+ mcp: {
111
+ origem: 'cliente MCP sem host homologado',
112
+ declaracao: 'reservado: o servidor MCP atual exige host homologado e declara o canal dele',
113
+ },
114
+ cli: {
115
+ origem: 'terminal, ork chamado direto',
116
+ declaracao: 'padrao de quem nao declara canal',
117
+ },
118
+ };
119
+ /** Codigo tipado da recusa de canal fora do registro. */
120
+ exports.ERRO_CANAL_DESCONHECIDO = 'conducao.canal-desconhecido';
121
+ /** Canal fora do registro e recusado com motivo tipado, nunca adivinhado (T9). */
122
+ function validarCanal(bruto) {
123
+ const canal = bruto.trim();
124
+ if (!Object.hasOwn(exports.CANAIS_DE_CONDUCAO, canal)) {
125
+ throw new Error(`${exports.ERRO_CANAL_DESCONHECIDO}: "${bruto}" nao e um canal de conducao homologado ` +
126
+ `(${Object.keys(exports.CANAIS_DE_CONDUCAO).join(', ')})`);
127
+ }
128
+ return canal;
129
+ }
130
+ /** Variavel com que um host ou adaptador declara o proprio canal. */
131
+ exports.ENV_CANAL = 'ORK_CANAL';
132
+ /**
133
+ * O canal deste processo (D6): `--canal`, depois `ORK_CANAL`, depois o que o proprio host exporta
134
+ * (`CLAUDECODE=1` do Claude Code, `HERMES_HOME` do gateway do Hermes), e por fim `cli`. O canal
135
+ * descreve a porta e nao concede autoridade: nenhum gate e nenhuma permissao dependem dele.
136
+ */
137
+ function canalDoProcesso(explicito, ambiente = process.env) {
138
+ if (typeof explicito === 'string' && explicito.trim() !== '')
139
+ return validarCanal(explicito);
140
+ const declarado = (ambiente[exports.ENV_CANAL] ?? '').trim();
141
+ if (declarado)
142
+ return validarCanal(declarado);
143
+ if (ambiente.CLAUDECODE === '1')
144
+ return 'claude-code';
145
+ if ((ambiente.HERMES_HOME ?? '').trim())
146
+ return 'hermes';
147
+ return 'cli';
148
+ }
149
+ // ---------------------------------------------------------------------------
150
+ // Identidade de despacho (D4): a reentrada da sessao que ja conduz.
151
+ // ---------------------------------------------------------------------------
152
+ /** Identidade do despacho no ambiente da sessao filha, para as chamadas do CLI de dentro dela. */
153
+ exports.ENV_IDENTIDADE_DE_DESPACHO = 'ORK_DISPATCH_ID';
154
+ exports.ENV_THREAD_DO_DESPACHO = 'ORK_DISPATCH_THREAD';
155
+ /** O que o despacho poe no ambiente da sessao filha: identidade, thread e canal de origem. */
156
+ function ambienteDaConducao(identidade, threadId, canal) {
157
+ return { [exports.ENV_IDENTIDADE_DE_DESPACHO]: identidade, [exports.ENV_THREAD_DO_DESPACHO]: threadId, [exports.ENV_CANAL]: canal };
158
+ }
159
+ /** A identidade de despacho que este processo herdou, quando ela e desta thread. */
160
+ function identidadeDoAmbiente(threadId, ambiente = process.env) {
161
+ const id = (ambiente[exports.ENV_IDENTIDADE_DE_DESPACHO] ?? '').trim();
162
+ const thread = (ambiente[exports.ENV_THREAD_DO_DESPACHO] ?? '').trim();
163
+ return /^[a-f0-9-]{36}$/.test(id) && thread === threadId ? id : null;
164
+ }
165
+ // ---------------------------------------------------------------------------
166
+ // Prazos (D3): derivados do teto real da operacao; metadado, nunca prova.
167
+ // ---------------------------------------------------------------------------
168
+ /** Folga sobre o teto: o `timeout(1)` ainda da 15 s de `--kill-after` depois do prazo. */
169
+ exports.MARGEM_DO_PRAZO_MS = 60_000;
170
+ /** Sessao despachada sem limite de duracao no setup do bloco. */
171
+ exports.PRAZO_DE_SESSAO_PADRAO_MS = 12 * 60 * 60 * 1000;
172
+ /** Reserva de quem assumiu a conducao: curta, porque nao e execucao. */
173
+ exports.PRAZO_DA_RESERVA_MS = 15 * 60 * 1000;
174
+ /** Espera padrao de `--esperar` sem valor. */
175
+ exports.ESPERA_PADRAO_MS = 30 * 60 * 1000;
176
+ /** O prazo do lease de uma verificacao: o teto de cada comando vezes quantos rodam, com folga. */
177
+ function prazoDaVerificacao(prazoPorComandoMs, comandos) {
178
+ return prazoPorComandoMs * Math.max(1, comandos) + exports.MARGEM_DO_PRAZO_MS;
179
+ }
180
+ // ---------------------------------------------------------------------------
181
+ // Leitura unica (T2, D9).
182
+ // ---------------------------------------------------------------------------
183
+ /** Nome canonico do lease de execucao da thread. */
184
+ function nomeDaConducao(threadId) {
185
+ return `exec:${threadId}`;
186
+ }
187
+ /** Estados em que o runtime ja deu a sessao por encerrada. */
188
+ const TERMINAIS = ['done', 'completed', 'exited', 'stopped', 'failed'];
189
+ /**
190
+ * O evento que prova que a sessao dona do lease acabou: o resultado da fase dela (a prova da I-34
191
+ * que o observador grava), a morte registrada, a superacao, ou a thread fechada. So conta evento
192
+ * posterior ao inicio da conducao.
193
+ */
194
+ function fimDaSessao(eventos, lease) {
195
+ const dono = lease.conducao?.dono;
196
+ if (!dono || dono.tipo !== 'sessao')
197
+ return null;
198
+ const desde = Date.parse(lease.adquiridoEm);
199
+ return eventos.find((e) => Date.parse(e.ts) >= desde && ((['phase_result', 'sessao_morta', 'session_superseded'].includes(e.tipo) && e.sessionId === dono.sessionId) ||
200
+ ['thread_closed_admin', 'master_done'].includes(e.tipo))) ?? null;
201
+ }
202
+ /**
203
+ * A leitura pura, para quem so tem os dados (o snapshot do Maestro le por um leitor confinado).
204
+ * Sessao encerrada no ledger e reserva vencida saem da leitura; processo sem prova de vida so sai
205
+ * quando a prova de processo diz `morto`.
206
+ */
207
+ function conducaoDosDados(lease, eventos, opcoes = {}) {
208
+ if (!lease)
209
+ return null;
210
+ const atual = (0, conducao_texto_1.conducaoDoLease)(lease);
211
+ if (!atual)
212
+ return null;
213
+ if (atual.dono.tipo === 'reserva')
214
+ return (0, leases_1.expirado)(lease, opcoes.agora) ? null : atual;
215
+ if (atual.dono.tipo === 'processo')
216
+ return opcoes.vidaDoProcesso === 'morto' ? null : atual;
217
+ return fimDaSessao(eventos, lease) ? null : atual;
218
+ }
219
+ /**
220
+ * "Quem conduz agora" (T2): a UNICA fonte. Thread status, board, monitor, pulse e os hosts leem
221
+ * daqui, e nenhuma superficie recalcula. Nao escreve nada: liberar e trabalho de quem toma.
222
+ */
223
+ function conducaoDaThread(raiz, threadId, opcoes = {}) {
224
+ const lease = (0, leases_1.lerLease)(raiz, nomeDaConducao(threadId));
225
+ if (!lease?.conducao)
226
+ return null;
227
+ const vida = lease.conducao.dono.tipo === 'processo' ? vidaDoProcesso(raiz, threadId, lease) : undefined;
228
+ let eventos = [];
229
+ try {
230
+ eventos = lease.conducao.dono.tipo === 'sessao' ? (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, threadId)) : [];
231
+ }
232
+ catch { /* ledger ilegivel nao prova fim de sessao */ }
233
+ let fechada = false;
234
+ try {
235
+ fechada = (0, thread_1.lerThread)(raiz, threadId).status === 'fechada';
236
+ }
237
+ catch { /* thread ilegivel segue como esta */ }
238
+ if (fechada && lease.conducao.dono.tipo !== 'processo')
239
+ return null;
240
+ return conducaoDosDados(lease, eventos, { agora: opcoes.agora, vidaDoProcesso: vida });
241
+ }
242
+ // ---------------------------------------------------------------------------
243
+ // Exclusao do kernel (D7).
244
+ // ---------------------------------------------------------------------------
245
+ const FLOCK = '/usr/bin/flock';
246
+ const pausa = new Int32Array(new SharedArrayBuffer(4));
247
+ /** Espera sincrona sem ocupar CPU. */
248
+ function dormir(ms) {
249
+ if (ms > 0)
250
+ Atomics.wait(pausa, 0, 0, Math.ceil(ms));
251
+ }
252
+ /** Arquivo da trava de kernel da thread: nunca apagado, como o lock HITL. */
253
+ function caminhoDaTrava(raiz, threadId) {
254
+ return path.join((0, thread_1.dirThread)(raiz, threadId), '.conducao.lock');
255
+ }
256
+ function kernelDisponivel() {
257
+ return process.platform === 'linux' && fs.existsSync(FLOCK);
258
+ }
259
+ /**
260
+ * Tenta a trava exclusiva do kernel. O `flock` ajudante tranca a descricao de arquivo que este
261
+ * processo segura; ao sair, a trava continua do processo, e morre com ele. Devolve o descritor, ou
262
+ * null quando outro processo vivo segura a trava.
263
+ */
264
+ function tentarTrava(raiz, threadId) {
265
+ const arquivo = caminhoDaTrava(raiz, threadId);
266
+ const fd = fs.openSync(arquivo, fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_NOFOLLOW, 0o600);
267
+ if (!kernelDisponivel())
268
+ return fd;
269
+ const r = (0, node_child_process_1.spawnSync)(FLOCK, ['--exclusive', '--nonblock', '3'], { stdio: ['ignore', 'ignore', 'ignore', fd], timeout: 5000 });
270
+ if (r.status === 0 && !r.error)
271
+ return fd;
272
+ fs.closeSync(fd);
273
+ return null;
274
+ }
275
+ /**
276
+ * Prova de vida de um dono processo. No Linux, a trava do kernel: se ela esta livre, o dono nao
277
+ * existe mais (morreu, ou a maquina reiniciou). Fora do Linux, sem kernel lock, vale a identidade do
278
+ * processo (pid e instante de inicio), ou `desconhecido`.
279
+ */
280
+ function vidaDoProcesso(raiz, threadId, lease) {
281
+ const dono = lease.conducao?.dono;
282
+ if (!dono || dono.tipo !== 'processo')
283
+ return 'desconhecido';
284
+ if (dono.maquina !== (0, maquina_1.nomeDaMaquina)())
285
+ return 'desconhecido';
286
+ if (!kernelDisponivel()) {
287
+ try {
288
+ process.kill(dono.pid, 0);
289
+ }
290
+ catch {
291
+ return 'morto';
292
+ }
293
+ const atual = (0, codex_runner_1.identidadeProcesso)(dono.pid);
294
+ return atual && atual.inicio === dono.inicio ? 'vivo' : 'desconhecido';
295
+ }
296
+ let fd = null;
297
+ try {
298
+ fd = tentarTrava(raiz, threadId);
299
+ }
300
+ catch {
301
+ return 'desconhecido';
302
+ }
303
+ if (fd === null)
304
+ return 'vivo';
305
+ fs.closeSync(fd);
306
+ return 'morto';
307
+ }
308
+ /** Conducoes que ESTE processo segura, pela chave canonica: a reentrada no mesmo processo. */
309
+ const emCurso = new Map();
310
+ function chave(raiz, threadId) {
311
+ return `${(0, leases_1.caminhoLease)(raiz, nomeDaConducao(threadId))}`;
312
+ }
313
+ /** A consulta padrao: o controle nativo do runtime (claude-bg ou o controller do codex). */
314
+ const consultaNativa = (dono, raiz, threadId) => {
315
+ try {
316
+ // Carregado na hora: o modulo de sessoes puxa o HITL inteiro, que nao pertence a leitura.
317
+ const { controleNativo } = require('./hitl-sessions');
318
+ const r = controleNativo(dono.runtime, raiz, threadId, dono.sessionId).consultar();
319
+ if (!r.ok)
320
+ return { ok: false, estado: null, detalhe: 'o runtime nao respondeu a consulta' };
321
+ const achadas = r.sessoes.filter((s) => s.sessionId === dono.sessionId);
322
+ return { ok: true, estado: achadas[0]?.estado ?? null, detalhe: achadas.length ? `estado ${achadas[0].estado}` : 'sessao ausente' };
323
+ }
324
+ catch (e) {
325
+ return { ok: false, estado: null, detalhe: e.message };
326
+ }
327
+ };
328
+ /**
329
+ * A sessao dona acabou? Primeiro o ledger (a prova da I-34, barata); depois, se pedido, o runtime.
330
+ * Sessao ausente so prova fim quando a consulta foi feita na conta dela (sem perfil, a do processo).
331
+ * O estado visto no runtime volta junto: sessao `blocked` espera alguem e nao executa nada.
332
+ */
333
+ function situacaoDaSessao(raiz, threadId, lease, pedido) {
334
+ let estadoNoRuntime = null;
335
+ const fim = provaDeFimDaSessao(raiz, threadId, lease, pedido, (estado) => { estadoNoRuntime = estado; });
336
+ return { fim, estadoNoRuntime };
337
+ }
338
+ function provaDeFimDaSessao(raiz, threadId, lease, pedido, anotarEstado = () => undefined) {
339
+ const dono = lease.conducao?.dono;
340
+ if (!dono || dono.tipo !== 'sessao')
341
+ return null;
342
+ let eventos = [];
343
+ try {
344
+ eventos = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, threadId));
345
+ }
346
+ catch { /* sem ledger, so o runtime prova */ }
347
+ const fim = fimDaSessao(eventos, lease);
348
+ if (fim)
349
+ return { motivo: 'sessao-encerrada', prova: `ledger: ${fim.tipo}${fim.eventId ? ` ${String(fim.eventId)}` : ''}`, provaEm: fim.ts };
350
+ try {
351
+ if ((0, thread_1.lerThread)(raiz, threadId).status === 'fechada')
352
+ return { motivo: 'sessao-encerrada', prova: 'thread fechada' };
353
+ }
354
+ catch { /* thread ilegivel nao prova nada */ }
355
+ if (pedido.consultarRuntime === false)
356
+ return null;
357
+ const r = (pedido.consultarSessao ?? consultaNativa)(dono, raiz, threadId);
358
+ if (!r.ok)
359
+ return null;
360
+ anotarEstado(r.estado);
361
+ if (r.estado && TERMINAIS.includes(r.estado))
362
+ return { motivo: 'sessao-orfa', prova: `runtime ${dono.runtime}: ${r.detalhe}` };
363
+ if (r.estado === null && !dono.perfil)
364
+ return { motivo: 'sessao-orfa', prova: `runtime ${dono.runtime}: ${r.detalhe}` };
365
+ return null;
366
+ }
367
+ function dadosDoPedido(pedido, identidade, dono) {
368
+ return {
369
+ contrato: 'ork.conducao/v1',
370
+ canal: pedido.canal,
371
+ correlacao: pedido.correlacao ?? null,
372
+ operacao: pedido.operacao,
373
+ fase: pedido.fase ?? null,
374
+ promptSha256: pedido.promptSha256 ?? null,
375
+ identidade,
376
+ dono,
377
+ };
378
+ }
379
+ function donoProcesso() {
380
+ const id = process.platform === 'linux' ? (0, codex_runner_1.identidadeProcesso)(process.pid) : null;
381
+ return { tipo: 'processo', pid: process.pid, inicio: id?.inicio ?? '', bootId: id?.boot ?? '', maquina: (0, maquina_1.nomeDaMaquina)() };
382
+ }
383
+ /** Grava a liberacao com a prova e apaga o lease (quem chama segura a trava do kernel). */
384
+ function liberarComProva(raiz, threadId, lease, tipo, dados) {
385
+ try {
386
+ fs.unlinkSync((0, leases_1.caminhoLease)(raiz, lease.nome));
387
+ }
388
+ catch { /* ja saiu */ }
389
+ const anterior = (0, conducao_texto_1.conducaoDoLease)(lease);
390
+ (0, ledger_1.registrar)((0, thread_1.dirThread)(raiz, threadId), threadId, tipo, {
391
+ lease: lease.nome,
392
+ anterior: anterior ? { canal: anterior.canal, sessao: anterior.sessao, fase: anterior.fase, desde: anterior.desde,
393
+ operacao: anterior.operacao, dono: anterior.dono, promptSha256: anterior.promptSha256 } : null,
394
+ ...dados,
395
+ });
396
+ }
397
+ /**
398
+ * Com a trava do kernel na mao: o lease que estiver ai ainda vale? Dono processo nao vale (a trava
399
+ * estava livre, entao ele morreu). Dono sessao vale ate a prova de fim. Reserva vale no prazo, e para
400
+ * o mesmo canal vira a conducao dele.
401
+ */
402
+ function avaliarComTrava(raiz, threadId, pedido, opcoes = { usarReserva: true }) {
403
+ const lease = (0, leases_1.lerLease)(raiz, nomeDaConducao(threadId));
404
+ if (!lease)
405
+ return null;
406
+ const atual = (0, conducao_texto_1.conducaoDoLease)(lease);
407
+ if (!atual) {
408
+ liberarComProva(raiz, threadId, lease, ledger_1.TIPOS_DE_EVENTO.conducaoOrfaLiberada, { motivo: 'lease-invalido', prova: 'lease exec sem dados de conducao validos' });
409
+ return null;
410
+ }
411
+ if (atual.dono.tipo === 'processo') {
412
+ const d = atual.dono;
413
+ // Sem kernel lock (fora do Linux), segurar a "trava" nao prova nada: vale a identidade do processo.
414
+ if (!kernelDisponivel() && vidaDoProcesso(raiz, threadId, lease) !== 'morto')
415
+ return { ok: false, idempotente: false, atual };
416
+ liberarComProva(raiz, threadId, lease, ledger_1.TIPOS_DE_EVENTO.conducaoOrfaLiberada, {
417
+ motivo: 'processo-morto',
418
+ prova: kernelDisponivel()
419
+ ? `trava do kernel livre: o processo ${d.pid} (boot ${d.bootId.slice(0, 8) || '?'}) nao a segura mais`
420
+ : `processo ${d.pid} ausente`,
421
+ });
422
+ return null;
423
+ }
424
+ if (atual.dono.tipo === 'reserva') {
425
+ if ((0, leases_1.expirado)(lease)) {
426
+ liberarComProva(raiz, threadId, lease, ledger_1.TIPOS_DE_EVENTO.conducaoLiberada, { motivo: 'reserva-vencida', prova: `reserva de ${atual.dono.por} venceu em ${(0, horario_1.formatarDataHoraRotulada)(lease.expiraEm)}` });
427
+ return null;
428
+ }
429
+ if (opcoes.usarReserva && atual.canal === pedido.canal) {
430
+ liberarComProva(raiz, threadId, lease, ledger_1.TIPOS_DE_EVENTO.conducaoLiberada, { motivo: 'reserva-usada', prova: `pedido do mesmo canal ${pedido.canal} de quem assumiu` });
431
+ return null;
432
+ }
433
+ return { ok: false, idempotente: false, atual };
434
+ }
435
+ const { fim, estadoNoRuntime } = situacaoDaSessao(raiz, threadId, lease, pedido);
436
+ if (fim) {
437
+ liberarComProva(raiz, threadId, lease, fim.motivo === 'sessao-encerrada'
438
+ ? ledger_1.TIPOS_DE_EVENTO.conducaoLiberada : ledger_1.TIPOS_DE_EVENTO.conducaoOrfaLiberada, fim);
439
+ return null;
440
+ }
441
+ const despacho = pedido.operacao === 'phase.run' || pedido.operacao === 'retry.run';
442
+ const mesmaFase = atual.fase === (pedido.fase ?? null);
443
+ const mesmoPedido = despacho && mesmaFase && !!pedido.promptSha256 && atual.promptSha256 === pedido.promptSha256;
444
+ // A sucessora (fluxo de superacao das sessoes HITL): a sessao `blocked` espera alguem e nao executa;
445
+ // o despacho seguinte da MESMA fase a sucede, e a superacao a encerra pelo controle do runtime.
446
+ if (despacho && mesmaFase && !mesmoPedido && estadoNoRuntime === 'blocked') {
447
+ liberarComProva(raiz, threadId, lease, ledger_1.TIPOS_DE_EVENTO.conducaoLiberada, {
448
+ motivo: 'sessao-bloqueada-sucedida',
449
+ prova: `runtime ${atual.dono.tipo === 'sessao' ? atual.dono.runtime : '?'}: estado blocked; o despacho seguinte da fase ${atual.fase} a sucede`,
450
+ });
451
+ return null;
452
+ }
453
+ return { ok: false, idempotente: mesmoPedido, atual };
454
+ }
455
+ /**
456
+ * Toma a conducao da thread para esta operacao, ou diz quem a segura.
457
+ *
458
+ * Reentra quando este mesmo processo ja conduz, ou quando a identidade de despacho do pedido e a de
459
+ * quem conduz (a sessao de CHECK que roda `ork verify` dentro da propria fase, R3). Sem reentrada, a
460
+ * trava do kernel serializa a decisao e o lease registra quem conduz.
461
+ */
462
+ function tomarConducao(raiz, threadId, pedido) {
463
+ const k = chave(raiz, threadId);
464
+ const propria = emCurso.get(k);
465
+ if (propria && !propria.convertida) {
466
+ propria.profundidade++;
467
+ return reentrada(propria.identidade, () => { propria.profundidade--; });
468
+ }
469
+ const existente = (0, leases_1.lerLease)(raiz, nomeDaConducao(threadId));
470
+ if (pedido.identidade && existente?.conducao?.identidade === pedido.identidade) {
471
+ return reentrada(pedido.identidade, () => undefined);
472
+ }
473
+ let fd = null;
474
+ for (let tentativa = 0; tentativa < 3 && fd === null; tentativa++) {
475
+ fd = tentarTrava(raiz, threadId);
476
+ if (fd === null)
477
+ dormir(150);
478
+ }
479
+ if (fd === null) {
480
+ // Um processo vivo segura a trava: ou conduz, ou esta gravando o lease agora mesmo.
481
+ let atual = null;
482
+ for (let i = 0; i < 5 && !atual; i++) {
483
+ const lease = (0, leases_1.lerLease)(raiz, nomeDaConducao(threadId));
484
+ atual = lease ? (0, conducao_texto_1.conducaoDoLease)(lease) : null;
485
+ if (!atual)
486
+ dormir(200);
487
+ }
488
+ return { ok: false, idempotente: false, atual };
489
+ }
490
+ try {
491
+ const ocupada = avaliarComTrava(raiz, threadId, pedido);
492
+ if (ocupada) {
493
+ fs.closeSync(fd);
494
+ return ocupada;
495
+ }
496
+ const identidade = pedido.identidade ?? (0, node_crypto_1.randomUUID)();
497
+ const lease = {
498
+ nome: nomeDaConducao(threadId),
499
+ thread: threadId,
500
+ motivo: `${conducao_texto_1.NOME_DA_OPERACAO[pedido.operacao]} pelo canal ${pedido.canal}`,
501
+ pid: process.pid,
502
+ adquiridoEm: (0, util_1.agora)(),
503
+ expiraEm: new Date(Date.now() + pedido.prazoMs).toISOString(),
504
+ conducao: dadosDoPedido(pedido, identidade, donoProcesso()),
505
+ };
506
+ (0, leases_1.regravarLease)(raiz, lease);
507
+ const estado = { identidade, profundidade: 0, lease, fd, convertida: false };
508
+ emCurso.set(k, estado);
509
+ return {
510
+ ok: true,
511
+ reentrada: false,
512
+ identidade,
513
+ renovar: (prazoDoProximoMs, agoraMs = Date.now()) => renovar(raiz, threadId, estado, prazoDoProximoMs, agoraMs),
514
+ converterEmSessao: (dados) => converter(raiz, k, estado, dados),
515
+ liberar: () => liberarPropria(raiz, k, estado),
516
+ };
517
+ }
518
+ catch (e) {
519
+ try {
520
+ fs.closeSync(fd);
521
+ }
522
+ catch { /* ja fechado */ }
523
+ throw e;
524
+ }
525
+ }
526
+ function reentrada(identidade, sair) {
527
+ let saiu = false;
528
+ return {
529
+ ok: true, reentrada: true, identidade,
530
+ renovar: () => undefined,
531
+ converterEmSessao: () => undefined,
532
+ liberar: () => { if (!saiu) {
533
+ saiu = true;
534
+ sair();
535
+ } },
536
+ };
537
+ }
538
+ function renovar(raiz, threadId, estado, prazoDoProximoMs, agoraMs) {
539
+ if (estado.convertida)
540
+ return;
541
+ const falta = Date.parse(estado.lease.expiraEm) - agoraMs;
542
+ if (falta >= prazoDoProximoMs + exports.MARGEM_DO_PRAZO_MS)
543
+ return;
544
+ const c = estado.lease.conducao;
545
+ const renovacoes = (c.renovacoes ?? 0) + 1;
546
+ estado.lease = { ...estado.lease, expiraEm: new Date(agoraMs + prazoDoProximoMs + exports.MARGEM_DO_PRAZO_MS).toISOString(),
547
+ conducao: { ...c, renovacoes, renovadoEm: new Date(agoraMs).toISOString() } };
548
+ (0, leases_1.regravarLease)(raiz, estado.lease);
549
+ (0, ledger_1.registrar)((0, thread_1.dirThread)(raiz, threadId), threadId, ledger_1.TIPOS_DE_EVENTO.conducaoRenovada, {
550
+ lease: estado.lease.nome, operacao: c.operacao, canal: c.canal, expiraEm: estado.lease.expiraEm, renovacoes,
551
+ razao: 'o prazo que faltava nao cobria o proximo comando: renovado com a operacao viva',
552
+ });
553
+ }
554
+ function converter(raiz, k, estado, dados) {
555
+ if (estado.convertida)
556
+ return;
557
+ const c = estado.lease.conducao;
558
+ estado.lease = {
559
+ ...estado.lease,
560
+ expiraEm: new Date(Date.now() + dados.prazoMs).toISOString(),
561
+ conducao: { ...c, dono: { tipo: 'sessao', sessionId: dados.sessionId, runtime: dados.runtime, perfil: dados.perfil,
562
+ maquina: (0, maquina_1.nomeDaMaquina)() } },
563
+ };
564
+ (0, leases_1.regravarLease)(raiz, estado.lease);
565
+ estado.convertida = true;
566
+ emCurso.delete(k);
567
+ try {
568
+ fs.closeSync(estado.fd);
569
+ }
570
+ catch { /* ja fechado */ }
571
+ }
572
+ function liberarPropria(raiz, k, estado) {
573
+ if (estado.convertida)
574
+ return;
575
+ if (estado.profundidade > 0) {
576
+ estado.profundidade--;
577
+ return;
578
+ }
579
+ const atual = (0, leases_1.lerLease)(raiz, estado.lease.nome);
580
+ if (atual?.conducao?.identidade === estado.identidade && atual.conducao.dono.tipo === 'processo') {
581
+ try {
582
+ fs.unlinkSync((0, leases_1.caminhoLease)(raiz, estado.lease.nome));
583
+ }
584
+ catch { /* ja saiu */ }
585
+ }
586
+ emCurso.delete(k);
587
+ try {
588
+ fs.closeSync(estado.fd);
589
+ }
590
+ catch { /* ja fechado */ }
591
+ }
592
+ /**
593
+ * Registra a conducao da sessao de um despacho que nao tomou o lease antes (chamada direta de
594
+ * `concluirDespacho`). So grava quando nao ha conducao nenhuma: nunca escreve por cima de outra.
595
+ */
596
+ function registrarConducaoDaSessao(raiz, threadId, pedido, sessao) {
597
+ const t = tomarConducao(raiz, threadId, { ...pedido, consultarRuntime: false });
598
+ if (!t.ok)
599
+ return false;
600
+ if (t.reentrada) {
601
+ t.liberar();
602
+ return false;
603
+ }
604
+ t.converterEmSessao({ ...sessao, prazoMs: pedido.prazoMs });
605
+ return true;
606
+ }
607
+ /** O comando exato de repetir o pedido esperando a vez. */
608
+ function comandoDeEsperar(threadId, pedido) {
609
+ switch (pedido.operacao) {
610
+ case 'phase.run': return `ork phase run ${threadId} ${pedido.fase ?? '<FASE>'} --prompt "<o mesmo pedido>" --esperar 30`;
611
+ case 'retry.run': return `ork retry run ${threadId} --esperar 30`;
612
+ case 'baseline': return `ork verify ${threadId} --baseline --esperar 30`;
613
+ case 'fix.open': return `ork fix open ${threadId} --esperar 30`;
614
+ case 'fix.reverify': return `ork fix reverify ${threadId} --esperar 30`;
615
+ default: return `ork verify ${threadId} --esperar 30`;
616
+ }
617
+ }
618
+ /** Como acompanhar quem conduz: o status da conducao e, se for sessao, os logs dela. */
619
+ function comandoDeAcompanhar(threadId, atual) {
620
+ const base = `ork conducao status ${threadId}`;
621
+ return atual?.sessao ? `${base} && ork sessions logs ${atual.sessao.slice(0, 8)}` : base;
622
+ }
623
+ /**
624
+ * A recusa, em estrutura e em texto. O dono pediu que ela explique sempre, para um humano, o que
625
+ * aconteceu e o que fazer: o texto sai daqui e de lugar nenhum mais, e o JSON e a mesma estrutura.
626
+ */
627
+ function recusaDeConducao(threadId, atual, pedido, opcoes = {}) {
628
+ const conducao = atual
629
+ ? { ...atual, desdeLocal: (0, horario_1.formatarDataHoraRotulada)(atual.desde, opcoes), linha: (0, conducao_texto_1.linhaDeConducao)(atual, opcoes) }
630
+ : null;
631
+ const acoes = [
632
+ { acao: 'esperar', comando: comandoDeEsperar(threadId, pedido),
633
+ explicacao: 'Esperar a vez: repete este pedido e segue sozinho quando a conducao atual terminar (ate 30 min).' },
634
+ { acao: 'acompanhar', comando: comandoDeAcompanhar(threadId, atual),
635
+ explicacao: 'Acompanhar quem conduz, sem mexer em nada.' },
636
+ { acao: 'assumir', comando: `ork conducao assumir ${threadId} --por <voce> --motivo "<por que>"`,
637
+ explicacao: 'Assumir a conducao: encerra a atual pelo proprio runtime e registra no ledger quem assumiu, de qual canal e por que.' },
638
+ ];
639
+ const linhas = [
640
+ `Pedido nao executado: a thread ${threadId} ja esta sendo conduzida agora, e duas execucoes na mesma worktree ` +
641
+ 'se corrompem (foi o que aconteceu em 19/09/2026).',
642
+ ];
643
+ if (conducao) {
644
+ linhas.push(` Quem conduz: canal ${conducao.canal}, ${(0, conducao_texto_1.quemConduz)(conducao)}, fase ${conducao.fase ?? '-'}, desde ${conducao.desdeLocal}.`);
645
+ if (conducao.promptSha256)
646
+ linhas.push(` Pedido em curso: prompt sha256 ${conducao.promptSha256.slice(0, 12)}.`);
647
+ }
648
+ else {
649
+ linhas.push(' Outra operacao esta tomando a conducao neste instante; o registro dela ainda nao apareceu.');
650
+ }
651
+ linhas.push(` Pedido recusado: ${conducao_texto_1.NOME_DA_OPERACAO[pedido.operacao]}${pedido.fase ? ` da fase ${pedido.fase}` : ''}, pelo canal ${pedido.canal}.`);
652
+ linhas.push('O que voce pode fazer:');
653
+ acoes.forEach((a, i) => { linhas.push(` ${i + 1}. ${a.explicacao}`); linhas.push(` ${a.comando}`); });
654
+ return {
655
+ contrato: 'ork.conducao-recusa/v1',
656
+ motivo: 'conducao.em-andamento',
657
+ thread: threadId,
658
+ conducao,
659
+ pedido: { canal: pedido.canal, operacao: pedido.operacao, fase: pedido.fase ?? null, promptSha256: pedido.promptSha256 ?? null },
660
+ acoes,
661
+ texto: linhas.join('\n'),
662
+ };
663
+ }
664
+ /**
665
+ * O pedido repetido (T14): a mesma fase com o mesmo prompt de quem ja conduz. Nada foi despachado,
666
+ * e o texto diz onde acompanhar a sessao que ja atende o pedido.
667
+ */
668
+ function textoDoPedidoRepetido(threadId, atual, opcoes = {}) {
669
+ return [
670
+ `Pedido ja em andamento: a fase ${atual.fase ?? '-'} da thread ${threadId} roda com este mesmo prompt ` +
671
+ `(sha256 ${(atual.promptSha256 ?? '').slice(0, 12)}) desde ${(0, horario_1.formatarDataHoraRotulada)(atual.desde, opcoes)}. ` +
672
+ 'Nenhuma sessao nova foi aberta e nenhuma cota foi gasta.',
673
+ ` ${(0, conducao_texto_1.linhaDeConducao)(atual, opcoes)}`,
674
+ ` acompanhar: ${comandoDeAcompanhar(threadId, atual)}`,
675
+ ].join('\n');
676
+ }
677
+ /** A recusa tipada como erro, para quem nao tem um resultado estruturado onde devolve-la. */
678
+ class ErroDeConducao extends Error {
679
+ recusa;
680
+ constructor(recusa) {
681
+ super(`conducao.em-andamento: ${recusa.texto}`);
682
+ this.name = 'ErroDeConducao';
683
+ this.recusa = recusa;
684
+ }
685
+ }
686
+ exports.ErroDeConducao = ErroDeConducao;
687
+ /** Grava a recusa no ledger: o segundo pedido deixa rastro, que era o que faltava em 19/09. */
688
+ function registrarRecusa(raiz, recusa) {
689
+ try {
690
+ (0, ledger_1.registrar)((0, thread_1.dirThread)(raiz, recusa.thread), recusa.thread, ledger_1.TIPOS_DE_EVENTO.conducaoRecusada, {
691
+ motivo: recusa.motivo,
692
+ pedido: recusa.pedido,
693
+ conducao: recusa.conducao ? { canal: recusa.conducao.canal, sessao: recusa.conducao.sessao, fase: recusa.conducao.fase,
694
+ desde: recusa.conducao.desde, operacao: recusa.conducao.operacao, promptSha256: recusa.conducao.promptSha256 } : null,
695
+ });
696
+ }
697
+ catch { /* a recusa ao pedido vale mesmo sem o registro */ }
698
+ }
699
+ /**
700
+ * Roda a operacao de processo sob a conducao da thread (T4, T5). Sem a conducao, nada executa: a
701
+ * recusa sai como `ErroDeConducao`. Com `esperarMs`, espera a vez por sondagem ate o prazo.
702
+ */
703
+ function comConducao(raiz, threadId, pedido, executar) {
704
+ const prazo = pedido.esperarMs && pedido.esperarMs > 0 ? Date.now() + pedido.esperarMs : null;
705
+ for (;;) {
706
+ const t = tomarConducao(raiz, threadId, pedido);
707
+ if (t.ok) {
708
+ try {
709
+ return executar(t);
710
+ }
711
+ finally {
712
+ t.liberar();
713
+ }
714
+ }
715
+ if (!prazo || Date.now() >= prazo) {
716
+ const recusa = recusaDeConducao(threadId, t.atual, pedido);
717
+ registrarRecusa(raiz, recusa);
718
+ throw new ErroDeConducao(recusa);
719
+ }
720
+ dormir(Math.min(2000, prazo - Date.now()));
721
+ }
722
+ }
723
+ /** Espera, sem tomar nada, ate nao haver conducao ou acabar o prazo. Devolve se liberou. */
724
+ function esperarConducaoLivre(raiz, threadId, esperarMs) {
725
+ const prazo = Date.now() + esperarMs;
726
+ for (;;) {
727
+ if (!conducaoDaThread(raiz, threadId))
728
+ return true;
729
+ if (Date.now() >= prazo)
730
+ return false;
731
+ dormir(Math.min(2000, prazo - Date.now()));
732
+ }
733
+ }
734
+ // ---------------------------------------------------------------------------
735
+ // Recuperacao de orfa (T16) e handoff (T15).
736
+ // ---------------------------------------------------------------------------
737
+ /**
738
+ * Libera a conducao que nao tem mais dono vivo, com a prova (D7). Nao conclui fase: a liberacao
739
+ * so apaga o lease e registra; `phase_result` continua sendo gravado so pela prova da I-34.
740
+ */
741
+ function liberarSeOrfa(raiz, threadId, opcoes = {}) {
742
+ const antes = (0, leases_1.lerLease)(raiz, nomeDaConducao(threadId));
743
+ if (!antes)
744
+ return { liberada: false, detalhe: 'nenhuma conducao registrada' };
745
+ const fd = tentarTrava(raiz, threadId);
746
+ if (fd === null)
747
+ return { liberada: false, detalhe: 'um processo vivo segura a conducao' };
748
+ try {
749
+ const ocupada = avaliarComTrava(raiz, threadId, { canal: 'cli', operacao: 'verify', prazoMs: 0, ...opcoes }, { usarReserva: false });
750
+ return ocupada ? { liberada: false, detalhe: 'a conducao tem dono vivo' } : { liberada: true, detalhe: 'conducao sem dono liberada, com prova no ledger' };
751
+ }
752
+ finally {
753
+ fs.closeSync(fd);
754
+ }
755
+ }
756
+ /** O estado da worktree no handoff: o que ficou alterado, para quem assume saber. */
757
+ function estadoDaWorktree(raiz, threadId) {
758
+ try {
759
+ const t = (0, thread_1.lerThread)(raiz, threadId);
760
+ const dir = t.worktree ?? raiz;
761
+ const head = (0, util_1.exec)('git', ['rev-parse', 'HEAD'], dir);
762
+ const st = (0, util_1.exec)('git', ['status', '--porcelain'], dir);
763
+ return { head: head.ok ? head.stdout.trim() : null, alterados: st.ok ? st.stdout.split('\n').filter(Boolean).slice(0, 40) : [] };
764
+ }
765
+ catch {
766
+ return { head: null, alterados: [] };
767
+ }
768
+ }
769
+ const controleNativoDoHandoff = (runtime, raiz, threadId, sessionId) => {
770
+ const { controleNativo } = require('./hitl-sessions');
771
+ const ctl = controleNativo(runtime, raiz, threadId, sessionId);
772
+ return {
773
+ consultar: () => ctl.consultar(),
774
+ parar: (s) => ctl.parar(s),
775
+ };
776
+ };
777
+ /**
778
+ * `ork conducao assumir` (T15): encerra a conducao atual pelo mecanismo suportado do runtime, toma
779
+ * uma reserva curta para o canal de quem assumiu e registra quem, de qual canal e por que. Processo
780
+ * local vivo nao e encerrado por sinal (D8): o handoff recusa e diz como esperar.
781
+ */
782
+ function assumirConducao(raiz, threadId, opcoes) {
783
+ const por = opcoes.por.trim(), motivo = opcoes.motivo.trim();
784
+ if (!por)
785
+ throw new Error('conducao.assumir: informe --por <quem assume>');
786
+ if (!motivo)
787
+ throw new Error('conducao.assumir: informe --motivo; handoff sem razao registrada e o mesmo kill de antes, com outro nome');
788
+ const dir = (0, thread_1.dirThread)(raiz, threadId);
789
+ (0, thread_1.lerThread)(raiz, threadId);
790
+ liberarSeOrfa(raiz, threadId, { consultarSessao: opcoes.consultarSessao });
791
+ const fd = tentarTrava(raiz, threadId);
792
+ if (fd === null) {
793
+ const atual = conducaoDaThread(raiz, threadId);
794
+ return { ok: false, thread: threadId, anterior: atual, mecanismo: 'nenhum',
795
+ detalhe: `a conducao atual e um processo local vivo (${atual ? (0, conducao_texto_1.quemConduz)(atual) : 'gravando o lease'}), que termina sozinho. ` +
796
+ `O handoff nao envia sinal ao processo: espere com ${atual ? comandoDeEsperar(threadId, { operacao: atual.operacao, fase: atual.fase }) : '--esperar'}, ` +
797
+ 'ou encerre-o no canal de onde ele saiu.' };
798
+ }
799
+ try {
800
+ const lease = (0, leases_1.lerLease)(raiz, nomeDaConducao(threadId));
801
+ const anterior = lease ? (0, conducao_texto_1.conducaoDoLease)(lease) : null;
802
+ let mecanismo = 'nenhum: nao havia conducao em andamento';
803
+ let prova = 'lease exec ausente';
804
+ if (lease && anterior?.dono.tipo === 'sessao') {
805
+ const dono = anterior.dono;
806
+ const ctl = (opcoes.controle ?? controleNativoDoHandoff)(dono.runtime, raiz, threadId, dono.sessionId);
807
+ const antes = ctl.consultar();
808
+ if (!antes.ok) {
809
+ return { ok: false, thread: threadId, anterior, mecanismo: 'nenhum',
810
+ detalhe: 'runtime.unavailable: nao consegui consultar a sessao no runtime; nada foi encerrado e a conducao segue com ela' };
811
+ }
812
+ const s = antes.sessoes.find((x) => x.sessionId === dono.sessionId);
813
+ if (s && !TERMINAIS.includes(s.estado)) {
814
+ if (!ctl.parar(s)) {
815
+ return { ok: false, thread: threadId, anterior, mecanismo: `parar ${dono.runtime}`,
816
+ detalhe: `runtime.unavailable: o runtime ${dono.runtime} nao confirmou o encerramento; a conducao segue com a sessao ${dono.sessionId.slice(0, 8)}` };
817
+ }
818
+ const depois = ctl.consultar();
819
+ const ainda = depois.ok ? depois.sessoes.find((x) => x.sessionId === dono.sessionId) : s;
820
+ if (!depois.ok || (ainda && !TERMINAIS.includes(ainda.estado))) {
821
+ return { ok: false, thread: threadId, anterior, mecanismo: `parar ${dono.runtime}`,
822
+ detalhe: 'runtime.unavailable: a consulta depois do encerramento nao confirma a sessao parada; o lease foi mantido' };
823
+ }
824
+ mecanismo = `parar pelo runtime ${dono.runtime} (controle homologado)`;
825
+ prova = `runtime: ${s.estado} -> ${ainda?.estado ?? 'ausente'}`;
826
+ }
827
+ else {
828
+ mecanismo = 'nenhum: a sessao ja tinha terminado';
829
+ prova = `runtime: ${s?.estado ?? 'sessao ausente'}`;
830
+ }
831
+ }
832
+ else if (lease && anterior?.dono.tipo === 'reserva') {
833
+ mecanismo = `reserva de ${anterior.dono.por} substituida`;
834
+ prova = `reserva do canal ${anterior.canal}`;
835
+ }
836
+ if (lease) {
837
+ try {
838
+ fs.unlinkSync((0, leases_1.caminhoLease)(raiz, lease.nome));
839
+ }
840
+ catch { /* ja saiu */ }
841
+ }
842
+ const reserva = {
843
+ nome: nomeDaConducao(threadId), thread: threadId, motivo: `reserva de ${por} ao assumir: ${motivo}`, pid: process.pid,
844
+ adquiridoEm: (0, util_1.agora)(), expiraEm: new Date(Date.now() + exports.PRAZO_DA_RESERVA_MS).toISOString(),
845
+ conducao: { contrato: 'ork.conducao/v1', canal: opcoes.canal, correlacao: null, operacao: 'handoff', fase: null,
846
+ promptSha256: null, identidade: (0, node_crypto_1.randomUUID)(), dono: { tipo: 'reserva', por, maquina: (0, maquina_1.nomeDaMaquina)() } },
847
+ };
848
+ (0, leases_1.regravarLease)(raiz, reserva);
849
+ (0, ledger_1.registrar)(dir, threadId, ledger_1.TIPOS_DE_EVENTO.conducaoAssumida, {
850
+ por, canal: opcoes.canal, motivo, mecanismo, prova,
851
+ anterior: anterior ? { canal: anterior.canal, sessao: anterior.sessao, fase: anterior.fase, desde: anterior.desde,
852
+ operacao: anterior.operacao, promptSha256: anterior.promptSha256 } : null,
853
+ reservaAte: reserva.expiraEm,
854
+ worktree: estadoDaWorktree(raiz, threadId),
855
+ });
856
+ return { ok: true, thread: threadId, anterior, mecanismo,
857
+ detalhe: `conducao assumida por ${por} pelo canal ${opcoes.canal}; a proxima operacao desse canal usa a reserva ate ` +
858
+ `${(0, horario_1.formatarDataHoraRotulada)(reserva.expiraEm)}` };
859
+ }
860
+ finally {
861
+ fs.closeSync(fd);
862
+ }
863
+ }
864
+ /** Texto de `ork conducao status`. */
865
+ function textoDoStatusDaConducao(threadId, atual) {
866
+ if (!atual)
867
+ return `Thread ${threadId}: ninguem conduz agora. Qualquer canal pode despachar ou verificar.`;
868
+ const linhas = [`Thread ${threadId}: ${(0, conducao_texto_1.linhaDeConducao)(atual)}`];
869
+ if (atual.promptSha256)
870
+ linhas.push(` prompt sha256 ${atual.promptSha256.slice(0, 12)}`);
871
+ linhas.push(` prazo do lease (metadado, nao prova): ${(0, horario_1.formatarDataHoraRotulada)(atual.expiraEm)}`);
872
+ if (atual.sessao)
873
+ linhas.push(` acompanhar: ork sessions logs ${atual.sessao.slice(0, 8)}`);
874
+ linhas.push(` assumir: ork conducao assumir ${threadId} --por <voce> --motivo "<por que>"`);
875
+ return linhas.join('\n');
876
+ }