@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
package/dist/thread.js CHANGED
@@ -48,10 +48,13 @@ exports.gravarThread = gravarThread;
48
48
  exports.idDisponivel = idDisponivel;
49
49
  exports.criarWorktree = criarWorktree;
50
50
  exports.novaThread = novaThread;
51
+ exports.exigirVarianteCompativel = exigirVarianteCompativel;
51
52
  exports.blocoDaThread = blocoDaThread;
52
53
  exports.pausaNaThread = pausaNaThread;
53
54
  exports.pausasDaThread = pausasDaThread;
55
+ exports.canalDaSessao = canalDaSessao;
54
56
  exports.resumoDaThread = resumoDaThread;
57
+ exports.threadsDaListagem = threadsDaListagem;
55
58
  exports.tabelaDeThreads = tabelaDeThreads;
56
59
  exports.exigirFase = exigirFase;
57
60
  const fs = __importStar(require("node:fs"));
@@ -60,15 +63,21 @@ const ciclos_1 = require("./ciclos");
60
63
  const modos_1 = require("./modos");
61
64
  const manifest_1 = require("./manifest");
62
65
  const slug_1 = require("./slug");
66
+ const types_1 = require("./types");
63
67
  const util_1 = require("./util");
64
68
  const ledger_1 = require("./ledger");
69
+ const estado_thread_1 = require("./estado-thread");
70
+ const creation_operation_store_1 = require("./creation-operation-store");
71
+ const horario_1 = require("./horario");
72
+ const maquina_1 = require("./maquina");
73
+ const conducao_texto_1 = require("./conducao-texto");
65
74
  /** Diretorio de threads do projeto. */
66
75
  function dirThreads(raiz) {
67
- return path.join((0, manifest_1.dirEstado)(raiz), 'threads');
76
+ return path.join((0, manifest_1.dirEstado)((0, estado_thread_1.raizDoEstado)(raiz)), 'threads');
68
77
  }
69
78
  /** Diretorio de uma thread. */
70
79
  function dirThread(raiz, id) {
71
- return path.join(dirThreads(raiz), id);
80
+ return (0, estado_thread_1.estadoCanonico)(raiz, id);
72
81
  }
73
82
  /** Caminho do `thread.json`. */
74
83
  function caminhoThread(raiz, id) {
@@ -96,7 +105,7 @@ function lerThread(raiz, id) {
96
105
  /** Grava a thread, sempre atualizando o carimbo de alteracao. */
97
106
  function gravarThread(raiz, thread) {
98
107
  thread.atualizadaEm = (0, util_1.agora)();
99
- (0, util_1.gravarJson)(caminhoThread(raiz, thread.id), thread);
108
+ (0, creation_operation_store_1.writeCreationJson)(caminhoThread(raiz, thread.id), thread);
100
109
  }
101
110
  /** Carimba a base da thread: branch e commit reais no momento da criacao. */
102
111
  function carimbarBase(raiz) {
@@ -148,10 +157,33 @@ function criarWorktree(carregado, id, slug, opcoes = {}) {
148
157
  if (!criada) {
149
158
  throw new Error(`worktree nao apareceu em \`git worktree list\` apos a criacao: ${dir}`);
150
159
  }
160
+ (0, estado_thread_1.vincularEstado)(raiz, id, dir);
151
161
  return { dir, branch };
152
162
  }
153
163
  /** Cria a thread: valida modo contra o manifesto, gera o slug e carimba a base. */
154
164
  function novaThread(carregado, opcoes) {
165
+ if (opcoes.dryRun)
166
+ return criarThreadSerializada(carregado, opcoes);
167
+ return (0, creation_operation_store_1.withCreationLock)((0, estado_thread_1.raizDoEstado)(carregado.raiz), 'threads', () => criarThreadSerializada(carregado, opcoes));
168
+ }
169
+ /**
170
+ * I-42 (D12): variante que redesenha blocos nao cabe num modo de bloco unico e parcial.
171
+ *
172
+ * Regra por desenho, nao por nome: `#Fast` tem um bloco so, com uma fase, e `gap` sobre ele
173
+ * apagaria o ciclo inteiro, enquanto `goal-plan` seria um no-op silencioso. A recusa sai antes
174
+ * de aplicar a variante, com o par variante-modo na mensagem. O `#Auto`, que tambem tem um bloco
175
+ * so mas percorre as seis fases, continua aceitando as duas, porque nelas ha o que redesenhar.
176
+ */
177
+ function exigirVarianteCompativel(variante, def) {
178
+ if (!(0, ciclos_1.definicaoDaVariante)(variante).mudaBlocos)
179
+ return;
180
+ const [unico, ...resto] = def.blocos;
181
+ if (unico && resto.length === 0 && unico.fases.length < types_1.FASES.length) {
182
+ throw new Error(`o ciclo ${variante} redesenha os blocos e o modo ${def.tag} tem um bloco so ` +
183
+ `(${unico.fases.join('-')}): crie a thread sem --ciclo ou escolha um modo de ciclo completo`);
184
+ }
185
+ }
186
+ function criarThreadSerializada(carregado, opcoes) {
155
187
  const { raiz, manifesto } = carregado;
156
188
  const permitidos = manifesto.conduction.allowed_modes;
157
189
  if (!permitidos.includes(opcoes.modo)) {
@@ -167,6 +199,8 @@ function novaThread(carregado, opcoes) {
167
199
  const variante = opcoes.variante ?? null;
168
200
  // A variante muda o desenho dos blocos ANTES do slug: a parte 3 do slug vem do
169
201
  // primeiro bloco, entao um ciclo `goal-plan` ja nasce com o slug `f12`.
202
+ if (variante)
203
+ exigirVarianteCompativel(variante, def);
170
204
  const blocos = (0, ciclos_1.aplicarVariante)(def.blocos, variante);
171
205
  if (variante) {
172
206
  const dv = (0, ciclos_1.definicaoDaVariante)(variante);
@@ -183,15 +217,42 @@ function novaThread(carregado, opcoes) {
183
217
  else if (opcoes.branch) {
184
218
  throw new Error('--branch so vale com --ciclo merge-branch');
185
219
  }
186
- const { id, assunto } = opcoes.dryRun
187
- ? { id: `${abbrev}-${assuntoBruto}`, assunto: assuntoBruto }
188
- : idDisponivel(raiz, abbrev, assuntoBruto);
220
+ const reservation = opcoes.reservation;
221
+ const operation = reservation ? (0, creation_operation_store_1.readCreationOperation)(raiz, reservation.operationId, reservation.principal) : null;
222
+ if (operation && (!operation.reserved.threadIds.includes(reservation.threadId)
223
+ || !reservation.threadId.startsWith(abbrev + '-') || !operation.reserved.ticketId
224
+ || operation.request.mode !== opcoes.modo || ['compensating', 'compensated', 'compensation_failed'].includes(operation.state)
225
+ || opcoes.criarWorktree || opcoes.worktree || opcoes.variante || opcoes.slug || opcoes.dryRun)) {
226
+ throw new Error('creation.conflict: reserva de thread incompatível');
227
+ }
228
+ const origin = operation ? { operationId: operation.operationId, ticketId: operation.reserved.ticketId, requestHash: operation.requestHash } : undefined;
229
+ if (reservation && fs.existsSync(caminhoThread(raiz, reservation.threadId))) {
230
+ const existing = lerThread(raiz, reservation.threadId);
231
+ if (existing.creationOrigin?.operationId !== origin.operationId || existing.creationOrigin?.ticketId !== origin.ticketId
232
+ || existing.creationOrigin?.requestHash !== origin.requestHash || existing.modo !== opcoes.modo || existing.nome !== opcoes.nome) {
233
+ throw new Error('creation.conflict: thread reservada pertence a outra intenção');
234
+ }
235
+ if (!(0, ledger_1.lerLedger)(dirThread(raiz, existing.id)).some((event) => event.tipo === ledger_1.TIPOS_DE_EVENTO.threadCriada)) {
236
+ (0, ledger_1.registrar)(dirThread(raiz, existing.id), existing.id, ledger_1.TIPOS_DE_EVENTO.threadCriada, { creationOrigin: origin, recovered: true });
237
+ }
238
+ return { thread: existing, gravada: false };
239
+ }
240
+ if (reservation && fs.existsSync(dirThread(raiz, reservation.threadId))
241
+ && fs.readdirSync(dirThread(raiz, reservation.threadId)).some((name) => !/^\.thread\.json\..+\.tmp$/.test(name))) {
242
+ throw new Error('creation.conflict: diretório reservado sem origem confirmada');
243
+ }
244
+ const { id, assunto } = reservation
245
+ ? { id: reservation.threadId, assunto: reservation.threadId.slice(abbrev.length + 1) }
246
+ : opcoes.dryRun
247
+ ? { id: `${abbrev}-${assuntoBruto}`, assunto: assuntoBruto }
248
+ : idDisponivel(raiz, abbrev, assuntoBruto);
189
249
  const slug = opcoes.slug ?? (0, slug_1.montarSlug)(abbrev, assunto, blocos[0].slugFases);
190
250
  // O override `--slug` do builder tambem passa pela regex canonica.
191
251
  if (!(0, slug_1.slugValido)(slug)) {
192
252
  throw new Error(`slug invalido: "${slug}" (regex canonica: ${slug_1.REGEX_SLUG.source})`);
193
253
  }
194
254
  const thread = {
255
+ ...(origin ? { creationOrigin: origin } : {}),
195
256
  id,
196
257
  slug,
197
258
  nome: opcoes.nome,
@@ -204,9 +265,13 @@ function novaThread(carregado, opcoes) {
204
265
  criadaEm: (0, util_1.agora)(),
205
266
  atualizadaEm: (0, util_1.agora)(),
206
267
  projeto: { name: manifesto.project.name, abbrev },
268
+ maquina: (0, maquina_1.nomeDaMaquina)(),
269
+ ...(opcoes.roadmap ? { roadmap: opcoes.roadmap } : {}),
207
270
  base: carimbarBase(raiz),
208
271
  worktree: opcoes.worktree ?? null,
209
272
  sessoes: [],
273
+ ...(opcoes.exigeRuntimeDiferente === true ? { exigeRuntimeDiferente: true } : {}),
274
+ ...(opcoes.doneWhen && opcoes.doneWhen.length > 0 ? { doneWhen: opcoes.doneWhen } : {}),
210
275
  decisoes: [],
211
276
  claims: [],
212
277
  leases: [],
@@ -235,6 +300,7 @@ function novaThread(carregado, opcoes) {
235
300
  }
236
301
  gravarThread(raiz, thread);
237
302
  (0, ledger_1.registrar)(dirThread(raiz, id), id, ledger_1.TIPOS_DE_EVENTO.threadCriada, {
303
+ ...(origin ? { creationOrigin: origin } : {}),
238
304
  slug,
239
305
  modo: opcoes.modo,
240
306
  tag: def.tag,
@@ -245,6 +311,8 @@ function novaThread(carregado, opcoes) {
245
311
  worktree: thread.worktree,
246
312
  variante,
247
313
  branchDeOrigem: opcoes.branch ?? null,
314
+ maquina: thread.maquina,
315
+ ...(thread.roadmap ? { roadmap: thread.roadmap } : {}),
248
316
  });
249
317
  if (worktreeCriada) {
250
318
  (0, ledger_1.registrar)(dirThread(raiz, id), id, ledger_1.TIPOS_DE_EVENTO.worktreeCriada, {
@@ -280,48 +348,94 @@ function pausasDaThread(thread) {
280
348
  return thread.blocos.filter((b) => b.pausa).length;
281
349
  }
282
350
  /** Texto do resumo de uma thread recem-criada ou consultada. */
283
- function resumoDaThread(thread) {
351
+ /** I-36 (T10): o canal de origem de uma sessao; a de antes do campo le como `desconhecido`, nunca `cli`. */
352
+ function canalDaSessao(s) {
353
+ return s.canal ?? 'desconhecido';
354
+ }
355
+ function resumoDaThread(thread, conducao) {
284
356
  const def = (0, modos_1.definicaoDoModo)(thread.modo);
285
357
  const linhas = [];
286
358
  linhas.push(` id ${thread.id}`);
287
359
  linhas.push(` slug ${thread.slug}`);
288
360
  linhas.push(` nome ${thread.nome}`);
289
- linhas.push(` modo ${def.tag} (${pausasDaThread(thread)} pausas humanas)`);
361
+ linhas.push(` modo ${def.tag} (${pausasDaThread(thread)} pausas humanas previstas)`);
290
362
  linhas.push(` ciclo ${thread.variante ?? 'padrao do modo'}`);
291
363
  linhas.push(` fase ${thread.faseAtual}`);
292
364
  linhas.push(` status ${thread.status}`);
293
365
  linhas.push(` base ${thread.base.branch} @ ${thread.base.commit.slice(0, 8)}`);
294
366
  linhas.push(` worktree ${thread.worktree ?? '(raiz do projeto)'}`);
367
+ // I-36 (T17): quem passa a leitura da conducao ve a linha unica; sem ela o resumo fica como era.
368
+ if (conducao !== undefined)
369
+ linhas.push(` conducao ${conducao ? (0, conducao_texto_1.linhaDeConducao)(conducao) : 'ninguem conduz agora'}`);
370
+ // I-43 (D4, viga a): a exigencia so aparece quando existe, para nao poluir o resumo
371
+ // das threads que nao a declararam.
372
+ if (thread.exigeRuntimeDiferente === true) {
373
+ linhas.push(' validacao CHECK em runtime diferente do GO (exigido pela thread)');
374
+ }
375
+ // I-43 (D4, viga b): o criterio aparece COM o comando. Mostrar so o criterio seria
376
+ // reproduzir a prosa do envelope numa tela nova.
377
+ if (thread.doneWhen && thread.doneWhen.length > 0) {
378
+ linhas.push(` pronto quando (${thread.doneWhen.length} criterio(s) executaveis)`);
379
+ for (const d of thread.doneWhen)
380
+ linhas.push(` ${d.criterio}\n $ ${d.comando}`);
381
+ }
295
382
  if (thread.score) {
296
383
  linhas.push(` score ${thread.score.valor}/5 por ${thread.score.avaliadoPor} ` +
297
- `(${thread.score.regime}) em ${thread.score.avaliadoEm}`);
384
+ `(${thread.score.regime}) em ${(0, horario_1.formatarDataHoraRotulada)(thread.score.avaliadoEm)}`);
298
385
  }
299
- linhas.push(' blocos');
386
+ linhas.push(' blocos previstos');
300
387
  for (const b of thread.blocos) {
301
- const marca = b.pausa ? `pausa: ${b.pausaSobre}` : 'sem pausa (decisao autonoma no ledger)';
388
+ const marca = b.pausa ? `pausa prevista: ${b.pausaSobre}` : 'sem pausa humana prevista';
302
389
  linhas.push(` ${b.slugFases.padEnd(6)} ${b.fases.join('-').padEnd(18)} ${marca}`);
303
390
  }
304
391
  return linhas.join('\n');
305
392
  }
393
+ /**
394
+ * As threads de `ork thread list`, ja filtradas.
395
+ *
396
+ * I-43 (D8): o DEFAULT mostra o que NAO esta fechado, e nao "so as abertas".
397
+ * O GOAL pediu "so abertas", e a medida corrigiu o pedido: existe thread `pausada`
398
+ * em disco, e um default que a esconde esconde justamente a que mais pede atencao.
399
+ *
400
+ * O custo medido era o default: 137 linhas para as 31 que interessavam, e 71 delas
401
+ * (52 por cento) eram `Sessao adotada claude-bg <uuid>`, lixo do mecanismo de adocao.
402
+ * Quem pagava era quem nao sabia que existia flag.
403
+ */
404
+ function threadsDaListagem(raiz, opcoes = {}) {
405
+ return listarIds(raiz)
406
+ .map((id) => lerThread(raiz, id))
407
+ .filter((t) => opcoes.todas === true || t.status !== 'fechada')
408
+ .map((t) => ({
409
+ id: t.id,
410
+ slug: t.slug,
411
+ modo: String(t.modo),
412
+ tag: (0, modos_1.tagDoModo)(t.modo),
413
+ fase: t.faseAtual,
414
+ status: t.status,
415
+ pausas: pausasDaThread(t),
416
+ sessoes: t.sessoes.length,
417
+ }));
418
+ }
306
419
  /** Tabela de `ork thread list`. */
307
- function tabelaDeThreads(raiz) {
308
- const ids = listarIds(raiz);
309
- if (ids.length === 0) {
310
- return 'Nenhuma thread em .orkastery/threads. Crie uma com: ork thread new "<nome>" --modo default';
420
+ function tabelaDeThreads(raiz, opcoes = {}) {
421
+ const linhas = threadsDaListagem(raiz, opcoes);
422
+ if (linhas.length === 0) {
423
+ // A divida paga aqui: a mensagem sugeria `--modo default`, um modo que
424
+ // `parseModo` recusa, e e a origem provavel das 3 threads com `modo: default`
425
+ // gravadas em disco. A sugestao agora e um modo que existe.
426
+ const vazio = opcoes.todas === true
427
+ ? 'Nenhuma thread em .orkastery/threads.'
428
+ : 'Nenhuma thread aberta ou pausada (use --todas para ver as fechadas).';
429
+ return `${vazio} Crie uma com: ork thread new "<nome>" --modo ${modos_1.ORDEM_DOS_MODOS[0]}`;
311
430
  }
312
- const linhas = ids.map((id) => {
313
- const t = lerThread(raiz, id);
314
- return [
315
- t.id,
316
- t.slug,
317
- (0, modos_1.tagDoModo)(t.modo),
318
- t.faseAtual,
319
- t.status,
320
- String(pausasDaThread(t)),
321
- String(t.sessoes.length),
322
- ];
323
- });
324
- return (0, util_1.tabela)(['ID', 'SLUG', 'MODO', 'FASE', 'STATUS', 'PAUSAS', 'SESSOES'], linhas);
431
+ const corpo = linhas.map((l) => [
432
+ l.id, l.slug, l.tag, l.fase, l.status, String(l.pausas), String(l.sessoes),
433
+ ]);
434
+ const t = (0, util_1.tabela)(['ID', 'SLUG', 'MODO', 'FASE', 'STATUS', 'PAUSAS', 'SESSOES'], corpo);
435
+ if (opcoes.todas === true)
436
+ return t;
437
+ const fechadas = listarIds(raiz).length - linhas.length;
438
+ return fechadas > 0 ? `${t}\n\n ${fechadas} fechada(s) omitida(s). Use --todas para ver todas.` : t;
325
439
  }
326
440
  /** Fase valida e pertencente ao ciclo da thread. */
327
441
  function exigirFase(thread, bruto) {
package/dist/tokens.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * `ork gate next`: o gate de tokens (visao unificada, secao 3.6).
3
+ * `ork gate next`: o gate de tokens (guia: docs/guias/memoria-e-handoff.md).
4
4
  *
5
5
  * Ao fim de cada fase, ANTES de despachar o proximo passo, o `ork` mede a lotacao da
6
6
  * janela da sessao atual e decide entre continuar na MESMA sessao ou abrir uma NOVA.
package/dist/util.js CHANGED
@@ -50,18 +50,20 @@ const node_child_process_1 = require("node:child_process");
50
50
  const fs = __importStar(require("node:fs"));
51
51
  const path = __importStar(require("node:path"));
52
52
  /** Executa um comando capturando saida, sem shell (argumentos vao como array). */
53
- function exec(cmd, args, cwd, timeoutMs = 120000) {
53
+ function exec(cmd, args, cwd, timeoutMs = 120000, env = process.env) {
54
54
  const r = (0, node_child_process_1.spawnSync)(cmd, args, {
55
55
  cwd,
56
56
  encoding: 'utf8',
57
57
  timeout: timeoutMs,
58
- env: process.env,
58
+ env,
59
59
  });
60
60
  return {
61
61
  ok: r.status === 0,
62
62
  code: r.status ?? -1,
63
63
  stdout: (r.stdout ?? '').toString(),
64
64
  stderr: (r.stderr ?? '').toString(),
65
+ signal: r.signal ?? null,
66
+ ...(r.error ? { error: r.error.code ?? r.error.message } : {}),
65
67
  };
66
68
  }
67
69
  /** O binario esta no PATH? */
@@ -0,0 +1,265 @@
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.criarExecutorSandbox = criarExecutorSandbox;
37
+ /** VERIFY interno sob sandbox Codex local, sem modelo e sem fallback não confinado.
38
+ * Contrato de escrita/rede, não de confidencialidade de leitura do filesystem.
39
+ * API síncrona: timeout rígido; não promete cancelamento reativo por AbortSignal.
40
+ */
41
+ const fs = __importStar(require("node:fs"));
42
+ const os = __importStar(require("node:os"));
43
+ const path = __importStar(require("node:path"));
44
+ const node_child_process_1 = require("node:child_process");
45
+ const node_url_1 = require("node:url");
46
+ const verify_1 = require("./verify");
47
+ const MAX_OUTPUT = 1024 * 1024;
48
+ /** Supervisor Linux fixo: subreaper recolhe também daemons/setsid.
49
+ * Nunca executa código de claim fora do sandbox. Python é infraestrutura local, sem pacotes. */
50
+ const SUPERVISOR = String.raw `
51
+ import ctypes, errno, json, os, selectors, signal, subprocess, sys, time
52
+ p = json.load(sys.stdin)
53
+ libc = ctypes.CDLL(None, use_errno=True)
54
+ if libc.prctl(36, 1, 0, 0, 0) != 0:
55
+ raise OSError(ctypes.get_errno(), 'PR_SET_CHILD_SUBREAPER falhou')
56
+ state = {'failure': None}
57
+ def interrupted(_sig, _frame): state['failure'] = 'supervisor interrompido'
58
+ signal.signal(signal.SIGTERM, interrupted)
59
+ def cleanup():
60
+ deadline = time.monotonic() + 3
61
+ while True:
62
+ with open('/proc/self/task/' + str(os.getpid()) + '/children') as f:
63
+ children = [int(x) for x in f.read().split()]
64
+ # Filhos vivos ou zombies ainda não reaped: seus PIDs não podem ser reutilizados.
65
+ for pid in children:
66
+ try: os.kill(pid, signal.SIGKILL)
67
+ except ProcessLookupError: pass
68
+ try:
69
+ while True:
70
+ pid, _status = os.waitpid(-1, os.WNOHANG)
71
+ if pid == 0: break
72
+ except ChildProcessError: return True
73
+ if time.monotonic() >= deadline: return False
74
+ time.sleep(.005)
75
+ stdout = bytearray(); stderr = bytearray(); code = -1
76
+ try:
77
+ child = subprocess.Popen([p['bin'], *p['args']], cwd=p['cwd'], env=p['env'],
78
+ stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, start_new_session=True)
79
+ selector = selectors.DefaultSelector()
80
+ selector.register(child.stdout, selectors.EVENT_READ, stdout)
81
+ selector.register(child.stderr, selectors.EVENT_READ, stderr)
82
+ deadline = time.monotonic() + p['timeoutMs']/1000
83
+ while True:
84
+ if state['failure']: break
85
+ if time.monotonic() >= deadline:
86
+ state['failure'] = 'timeout do sandbox'; break
87
+ if child.poll() is not None and not selector.get_map():
88
+ code = child.returncode; break
89
+ for key, _ in selector.select(.025):
90
+ data = os.read(key.fileobj.fileno(), 65536)
91
+ if not data: selector.unregister(key.fileobj); continue
92
+ if len(stdout)+len(stderr)+len(data) > p['maxOutput']:
93
+ state['failure'] = 'limite de saída excedido'; break
94
+ key.data.extend(data)
95
+ except Exception as e: state['failure'] = 'sandbox indisponível: ' + str(e)
96
+ finally:
97
+ cleanup_complete = cleanup()
98
+ if not cleanup_complete: state['failure'] = 'descendente do sandbox não encerrou no prazo'
99
+ print(json.dumps({'code': -1 if state['failure'] else code,
100
+ 'stdout': stdout.decode(errors='replace'), 'stderr': stderr.decode(errors='replace'), 'failure': state['failure'], 'cleanupComplete': cleanup_complete}))
101
+ `;
102
+ /** Somente WT e leaf scratch privado são graváveis; configuração e parent continuam readonly. */
103
+ function estadoSandbox(worktree, scratch) {
104
+ const entries = [
105
+ { path: { type: 'special', value: { kind: 'root' } }, access: 'read' },
106
+ { path: { type: 'path', path: worktree }, access: 'write' },
107
+ { path: { type: 'path', path: scratch }, access: 'write' },
108
+ ];
109
+ // Metadados/configuração de host e estado canônico não pertencem ao comando testado.
110
+ for (const nome of ['.git', '.orkastery', '.codex', '.claude', '.agents']) {
111
+ const alvo = path.join(worktree, nome);
112
+ entries.push({ path: { type: 'path', path: alvo }, access: 'read' });
113
+ }
114
+ return {
115
+ permissionProfile: { type: 'managed', file_system: { type: 'restricted', entries }, network: 'restricted' },
116
+ sandboxCwd: (0, node_url_1.pathToFileURL)(worktree).href,
117
+ useLegacyLandlock: false,
118
+ };
119
+ }
120
+ function criarExecutorSandbox(opcoes) {
121
+ if (process.platform !== 'linux')
122
+ throw new Error('executor VERIFY sandbox disponível somente em Linux');
123
+ if (!path.isAbsolute(opcoes.worktree))
124
+ throw new Error('worktree deve ser absoluta');
125
+ const worktree = fs.realpathSync(opcoes.worktree);
126
+ if (!fs.statSync(worktree).isDirectory() || worktree === path.parse(worktree).root)
127
+ throw new Error('worktree inválida');
128
+ const original = fs.statSync(worktree);
129
+ const timeoutMs = opcoes.timeoutMs ?? 60_000;
130
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > verify_1.TIMEOUT_VERIFY_MS)
131
+ throw new Error('timeout sandbox fora do limite');
132
+ // Binário local aprovado da instalação; não procuramos executável fornecido pela claim.
133
+ const bin = fs.realpathSync('/usr/bin/codex');
134
+ const binStat = fs.statSync(bin);
135
+ const isolamento = fs.mkdtempSync(path.join(os.tmpdir(), 'ork-verify-config-'));
136
+ if (fs.realpathSync(isolamento) === worktree || fs.realpathSync(isolamento).startsWith(worktree + path.sep)) {
137
+ fs.rmdirSync(isolamento);
138
+ throw Error('scratch VERIFY deve ficar fora da worktree');
139
+ }
140
+ const isolamentoStat = fs.lstatSync(isolamento);
141
+ const identidadeIgual = (file, original) => {
142
+ const st = fs.lstatSync(file, { throwIfNoEntry: false });
143
+ return !!st && st.isDirectory() && !st.isSymbolicLink() && st.dev === original.dev && st.ino === original.ino && st.uid === original.uid;
144
+ };
145
+ const codexHome = path.join(isolamento, 'codex');
146
+ fs.mkdirSync(codexHome);
147
+ fs.writeFileSync(path.join(codexHome, 'config.toml'), '');
148
+ let fechado = false, consumidoresEncerrados = true, erroCleanup = '';
149
+ const fechar = () => {
150
+ if (fechado) {
151
+ if (erroCleanup)
152
+ throw Error(erroCleanup);
153
+ return;
154
+ }
155
+ if (erroCleanup)
156
+ throw Error(erroCleanup);
157
+ if (!consumidoresEncerrados)
158
+ throw Error('cleanup incompleto: consumidores não comprovadamente encerrados; isolamento preservado: ' + isolamento);
159
+ if (!identidadeIgual(isolamento, isolamentoStat))
160
+ throw Error('cleanup incompleto: isolamento substituído; preservado');
161
+ fechado = true;
162
+ fs.rmSync(isolamento, { recursive: true, force: false });
163
+ };
164
+ const executar = (nome, comando, cwd) => {
165
+ if (fechado)
166
+ throw new Error('executor sandbox fechado');
167
+ if (erroCleanup || !consumidoresEncerrados)
168
+ throw Error(erroCleanup || 'cleanup incompleto: consumidor pendente');
169
+ const atual = fs.statSync(worktree);
170
+ const atualBin = fs.statSync(bin);
171
+ if (fs.realpathSync(cwd) !== worktree || atual.dev !== original.dev || atual.ino !== original.ino ||
172
+ atualBin.dev !== binStat.dev || atualBin.ino !== binStat.ino)
173
+ throw new Error('escopo/binário do executor sandbox mudou');
174
+ if (!identidadeIgual(isolamento, isolamentoStat))
175
+ throw Error('isolamento sandbox mudou');
176
+ const scratch = fs.mkdtempSync(path.join(isolamento, 'scratch-'));
177
+ const scratchStat = fs.lstatSync(scratch);
178
+ const criados = [];
179
+ const home = path.join(scratch, 'home');
180
+ const tmp = path.join(scratch, 'tmp');
181
+ fs.mkdirSync(home);
182
+ fs.mkdirSync(tmp);
183
+ try {
184
+ // Diretórios vazios não aparecem no Git. Não criar arquivos sintéticos nem esconder untracked.
185
+ for (const nome of ['.git', '.orkastery', '.codex', '.claude', '.agents']) {
186
+ const file = path.join(worktree, nome);
187
+ let st = fs.lstatSync(file, { throwIfNoEntry: false });
188
+ if (!st) {
189
+ fs.mkdirSync(file, { mode: 0o700 });
190
+ st = fs.lstatSync(file);
191
+ criados.push({ file, stat: st });
192
+ }
193
+ if (st.isSymbolicLink() || !(st.isDirectory() || (nome === '.git' && st.isFile() && st.nlink === 1)))
194
+ throw Error('metadado protegido inesperado; objeto preservado: ' + nome);
195
+ }
196
+ for (const item of criados)
197
+ if (!identidadeIgual(item.file, item.stat))
198
+ throw Error('metadado temporário mudou antes do lançamento');
199
+ const env = { PATH: '/usr/local/bin:/usr/bin:/bin', HOME: home, CODEX_HOME: codexHome,
200
+ TMPDIR: tmp, TMP: tmp, TEMP: tmp, LANG: 'C.UTF-8', CI: '1',
201
+ XDG_CACHE_HOME: path.join(home, '.cache'), npm_config_cache: path.join(home, '.npm') };
202
+ const args = ['sandbox', '--sandbox-state-json', JSON.stringify(estadoSandbox(worktree, scratch)),
203
+ '--sandbox-state-disable-network', '/bin/bash', '-c', comando];
204
+ consumidoresEncerrados = false;
205
+ const r = (0, node_child_process_1.spawnSync)('/usr/bin/python3', ['-c', SUPERVISOR], {
206
+ input: JSON.stringify({ bin, args, cwd: worktree, env, timeoutMs, maxOutput: MAX_OUTPUT }),
207
+ cwd: isolamento, env, encoding: 'utf8', maxBuffer: 4 * MAX_OUTPUT,
208
+ timeout: timeoutMs + 5_000, killSignal: 'SIGTERM',
209
+ });
210
+ let resultado;
211
+ try {
212
+ if (r.error || r.status !== 0)
213
+ throw new Error(r.error?.message ?? `supervisor saiu ${r.status}`);
214
+ const v = JSON.parse(r.stdout);
215
+ if (!v || typeof v !== 'object')
216
+ throw new Error('resposta inválida');
217
+ const x = v;
218
+ if (!Number.isInteger(x.code) || typeof x.stdout !== 'string' || typeof x.stderr !== 'string' ||
219
+ !(x.failure === null || typeof x.failure === 'string') || typeof x.cleanupComplete !== 'boolean')
220
+ throw new Error('resposta inválida');
221
+ resultado = x;
222
+ consumidoresEncerrados = resultado.cleanupComplete;
223
+ }
224
+ catch (e) {
225
+ resultado = { code: -1, stdout: '', stderr: r.stderr ?? '', failure: `falha fechada do sandbox: ${String(e)}`, cleanupComplete: false };
226
+ }
227
+ const stderr = resultado.stderr + (resultado.failure ? '\n' + resultado.failure : '');
228
+ const resumo = (resultado.stdout + '\n' + stderr).trim().split('\n').slice(-10).join('\n').slice(-800);
229
+ return { nome, comando, ok: resultado.code === 0 && !resultado.failure, code: resultado.code,
230
+ resumo, stdout: resultado.stdout, stderr, executor: 'codex-sandbox' };
231
+ }
232
+ finally {
233
+ if (!consumidoresEncerrados) {
234
+ erroCleanup = 'cleanup incompleto: consumidores não comprovadamente encerrados; temporários preservados: ' + isolamento;
235
+ }
236
+ else {
237
+ const erros = [];
238
+ for (const item of criados.reverse()) {
239
+ try {
240
+ if (!identidadeIgual(item.file, item.stat))
241
+ throw Error('identidade mudou');
242
+ // rmdir é a checagem atômica de vazio. Nunca recursive/force nos paths da WT.
243
+ fs.rmdirSync(item.file);
244
+ }
245
+ catch {
246
+ erros.push(path.basename(item.file));
247
+ }
248
+ }
249
+ try {
250
+ if (!identidadeIgual(isolamento, isolamentoStat) || !identidadeIgual(scratch, scratchStat))
251
+ throw Error('scratch mudou');
252
+ fs.rmSync(scratch, { recursive: true, force: false });
253
+ }
254
+ catch {
255
+ erros.push('scratch');
256
+ }
257
+ if (erros.length)
258
+ erroCleanup = 'cleanup incompleto: objetos substituídos ou não vazios preservados: ' + erros.join(', ') + '; isolamento preservado: ' + isolamento;
259
+ }
260
+ if (erroCleanup)
261
+ throw Error(erroCleanup);
262
+ }
263
+ };
264
+ return { executar, fechar };
265
+ }