@orkastery/cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +86 -65
  3. package/adapters/README.md +30 -15
  4. package/adapters/claude-code/.claude-plugin/plugin.json +11 -5
  5. package/adapters/claude-code/README.md +110 -8
  6. package/adapters/claude-code/commands/onboarding.md +21 -0
  7. package/adapters/claude-code/commands/ork.md +99 -27
  8. package/adapters/claude-code/hooks/hooks.json +6 -1
  9. package/adapters/claude-code/hooks/ork-sensor.js +66 -0
  10. package/adapters/codex/skills/ork/SKILL.md +57 -0
  11. package/adapters/hermes/README.md +163 -0
  12. package/adapters/hermes/bin/ork-abrir-thread.sh +2 -0
  13. package/adapters/hermes/bin/ork-brain.sh +4 -0
  14. package/adapters/hermes/bin/ork-hitl-answer.py +82 -0
  15. package/adapters/hermes/bin/ork-maestro.sh +6 -0
  16. package/adapters/hermes/bin/ork-master-enviar.py +84 -0
  17. package/adapters/hermes/bin/ork-objective-message.sh +17 -0
  18. package/adapters/hermes/bin/ork-objective-status.sh +12 -0
  19. package/adapters/hermes/bin/ork-pulse-enviar.py +47 -0
  20. package/adapters/hermes/hermes.plugin.json +26 -4
  21. package/adapters/hermes/hitl-ingress/__init__.py +356 -0
  22. package/adapters/hermes/hitl-ingress/plugin.yaml +4 -0
  23. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +75 -22
  24. package/adapters/openclaw/README.md +135 -26
  25. package/adapters/openclaw/bin/ork-brain.sh +4 -0
  26. package/adapters/openclaw/construir.sh +19 -0
  27. package/adapters/openclaw/dist/hitl-ingress.js +199 -0
  28. package/adapters/openclaw/dist/index.js +437 -0
  29. package/adapters/openclaw/openclaw.plugin.json +39 -134
  30. package/adapters/openclaw/package.json +26 -0
  31. package/adapters/openclaw/src/hitl-ingress.ts +180 -0
  32. package/adapters/openclaw/src/index.ts +458 -0
  33. package/adapters/openclaw/src/tipos-openclaw.d.ts +58 -0
  34. package/adapters/openclaw/tsconfig.json +15 -0
  35. package/assets/docs/markdownlint-cli2.jsonc +22 -0
  36. package/assets/docs/padroes/documentacao-de-produto.md +133 -0
  37. package/assets/docs/padroes/roadmap-de-produto.md +98 -0
  38. package/assets/docs/produto/README.md +14 -0
  39. package/assets/docs/produto/_modelo-feature.md +59 -0
  40. package/assets/docs/roadmap/README.md +14 -0
  41. package/assets/docs/roadmap/_modelo-item.md +73 -0
  42. package/assets/orkmind-native-schema.json +32 -0
  43. package/assets/orkmind_bridge.py +357 -0
  44. package/assets/orkmind_fixture.py +67 -0
  45. package/assets/orkmind_prospective.py +138 -0
  46. package/assets/reference-tariffs-i07.json +23 -0
  47. package/dist/adapters/claude-bg.js +488 -39
  48. package/dist/adapters/codex-controller-sensor.js +449 -0
  49. package/dist/adapters/codex-controller-worker.js +361 -0
  50. package/dist/adapters/codex-controller.js +252 -0
  51. package/dist/adapters/codex-events.js +205 -0
  52. package/dist/adapters/codex-question.js +26 -0
  53. package/dist/adapters/codex-runner.js +133 -0
  54. package/dist/adapters/codex.js +394 -0
  55. package/dist/agents-md.js +84 -0
  56. package/dist/auditoria.js +11 -8
  57. package/dist/auditrun.js +5 -4
  58. package/dist/board.js +173 -22
  59. package/dist/branch-de-estado.js +136 -0
  60. package/dist/canarios-hitl.js +177 -0
  61. package/dist/canarios-i43.js +543 -0
  62. package/dist/canarios-pulse.js +147 -0
  63. package/dist/canarios-sensores.js +129 -0
  64. package/dist/canarios.js +111 -2
  65. package/dist/catalogo.js +9 -0
  66. package/dist/ci.js +216 -0
  67. package/dist/ciclos.js +2 -1
  68. package/dist/claim-lint.js +64 -0
  69. package/dist/claims.js +60 -0
  70. package/dist/company-brain-capture.js +195 -0
  71. package/dist/company-brain-cli.js +123 -0
  72. package/dist/company-brain-client.js +61 -0
  73. package/dist/company-brain-contract.js +166 -0
  74. package/dist/company-brain-journal.js +192 -0
  75. package/dist/company-brain-mcp.js +33 -0
  76. package/dist/company-brain-migration.js +60 -0
  77. package/dist/company-brain-source.js +177 -0
  78. package/dist/company-brain-worker.js +18 -0
  79. package/dist/conducao-texto.js +61 -0
  80. package/dist/conducao.js +876 -0
  81. package/dist/contrato-publico.js +39 -0
  82. package/dist/creation-operation-store.js +251 -0
  83. package/dist/creation-operation.js +148 -0
  84. package/dist/decisao-autonoma.js +183 -0
  85. package/dist/delegation.js +79 -0
  86. package/dist/demo.js +120 -0
  87. package/dist/docs.js +828 -0
  88. package/dist/doctor.js +183 -24
  89. package/dist/entrega-pr.js +117 -0
  90. package/dist/escopo-escrita.js +71 -0
  91. package/dist/estado-thread.js +222 -0
  92. package/dist/evalrunner.js +12 -0
  93. package/dist/fabrica-estado.js +329 -0
  94. package/dist/fabrica-publicar.js +76 -0
  95. package/dist/fix.js +21 -0
  96. package/dist/gates.js +55 -9
  97. package/dist/handoff.js +39 -23
  98. package/dist/hitl-canais.js +333 -0
  99. package/dist/hitl-classificacao.js +131 -0
  100. package/dist/hitl-contract.js +465 -0
  101. package/dist/hitl-estado.js +136 -0
  102. package/dist/hitl-gates.js +554 -0
  103. package/dist/hitl-ingress-receipt.js +382 -0
  104. package/dist/hitl-local-atestado.js +57 -0
  105. package/dist/hitl-local-receipt.js +328 -0
  106. package/dist/hitl-local.js +143 -0
  107. package/dist/hitl-lock.js +139 -0
  108. package/dist/hitl-lote.js +223 -0
  109. package/dist/hitl-native-offer.js +97 -0
  110. package/dist/hitl-native.js +65 -0
  111. package/dist/hitl-presentation.js +209 -0
  112. package/dist/hitl-public-receipt.js +176 -0
  113. package/dist/hitl-resumo.js +209 -0
  114. package/dist/hitl-sessions.js +475 -0
  115. package/dist/hitl.js +711 -0
  116. package/dist/horario.js +269 -0
  117. package/dist/hosts.js +198 -22
  118. package/dist/index.js +1736 -93
  119. package/dist/indice.js +99 -0
  120. package/dist/init.js +28 -3
  121. package/dist/integracoes-locais.js +17 -0
  122. package/dist/leases.js +65 -21
  123. package/dist/ledger-stats.js +272 -0
  124. package/dist/ledger.js +107 -2
  125. package/dist/licoes.js +215 -0
  126. package/dist/liveness.js +218 -0
  127. package/dist/maestro-actions.js +51 -0
  128. package/dist/maestro-authority.js +152 -0
  129. package/dist/maestro-cli.js +97 -0
  130. package/dist/maestro-contract.js +55 -0
  131. package/dist/maestro-discovery.js +132 -0
  132. package/dist/maestro-runtime.js +268 -0
  133. package/dist/maestro-snapshot.js +86 -0
  134. package/dist/maestro-sources.js +224 -0
  135. package/dist/manifest.js +165 -6
  136. package/dist/maquina.js +102 -0
  137. package/dist/master-audit.js +101 -0
  138. package/dist/master-batch.js +43 -0
  139. package/dist/master-digest.js +149 -0
  140. package/dist/master-migracao.js +155 -0
  141. package/dist/master.js +361 -77
  142. package/dist/mcp-artifacts.js +234 -0
  143. package/dist/mcp-git.js +433 -0
  144. package/dist/mcp-install.js +310 -0
  145. package/dist/mcp-maestro.js +25 -0
  146. package/dist/mcp-server.js +469 -0
  147. package/dist/mcp-ship.js +436 -0
  148. package/dist/mcp-verify.js +173 -0
  149. package/dist/memoria-humana.js +207 -0
  150. package/dist/memoria.js +314 -78
  151. package/dist/memory-migration.js +279 -0
  152. package/dist/memory-prospective.js +148 -0
  153. package/dist/modos-migracao.js +191 -0
  154. package/dist/modos.js +151 -15
  155. package/dist/monitor-lock.js +110 -0
  156. package/dist/objective.js +449 -0
  157. package/dist/ocupacao.js +267 -0
  158. package/dist/onboarding.js +303 -0
  159. package/dist/orkmind.js +389 -90
  160. package/dist/orquestracao.js +96 -69
  161. package/dist/phase.js +596 -150
  162. package/dist/playbook-capabilities.js +150 -0
  163. package/dist/playbook-contracts.js +188 -0
  164. package/dist/portfolio-context.js +61 -0
  165. package/dist/portfolio.js +180 -0
  166. package/dist/preflight.js +207 -0
  167. package/dist/process-audit.js +112 -0
  168. package/dist/project-state.js +93 -0
  169. package/dist/prompts.js +25 -5
  170. package/dist/prova-minima.js +112 -0
  171. package/dist/pulse-cadencia.js +163 -0
  172. package/dist/pulse-consentimento.js +260 -0
  173. package/dist/pulse-delivery.js +323 -0
  174. package/dist/pulse-resposta.js +542 -0
  175. package/dist/pulse.js +262 -0
  176. package/dist/ratelimit.js +35 -2
  177. package/dist/recall.js +80 -3
  178. package/dist/redacao-saida.js +45 -0
  179. package/dist/redacao-url.js +66 -0
  180. package/dist/retry.js +637 -115
  181. package/dist/roadmap-reservas.js +243 -0
  182. package/dist/runtime-ambiente.js +38 -0
  183. package/dist/runtime-context.js +215 -0
  184. package/dist/runtime-profiles.js +869 -0
  185. package/dist/runtimes.js +103 -0
  186. package/dist/sandbox.js +9 -0
  187. package/dist/session-events.js +203 -0
  188. package/dist/session-watcher-claude.js +637 -0
  189. package/dist/session-watcher.js +683 -0
  190. package/dist/sessoes-adopt.js +154 -0
  191. package/dist/sessoes-inventario.js +159 -0
  192. package/dist/sessoes.js +16 -35
  193. package/dist/setup.js +691 -0
  194. package/dist/ship.js +150 -15
  195. package/dist/slug.js +1 -1
  196. package/dist/thread-close.js +115 -0
  197. package/dist/thread.js +142 -28
  198. package/dist/tokens.js +1 -1
  199. package/dist/util.js +4 -2
  200. package/dist/verify-sandbox.js +265 -0
  201. package/dist/verify.js +270 -25
  202. package/dist/versao.js +60 -0
  203. package/dist/worktree.js +31 -2
  204. package/dist/write-activation.js +291 -0
  205. package/dist/yaml.js +2 -0
  206. package/eval/casos/onboarding.json +102 -0
  207. package/eval/casos/orkastery-bootstrap.json +35 -1
  208. package/eval/casos/scope-check-capability-map.json +45 -7
  209. package/eval/casos/ship-release.json +2 -2
  210. package/eval/fixtures/b0-slug-e-modos/caso.json +35 -22
  211. package/eval/fixtures/b2-master-log/caso.json +1 -1
  212. package/eval/fixtures/fx-adapter-editado-detectado/caso.json +19 -0
  213. package/eval/fixtures/fx-auto-quiet/caso.json +11 -0
  214. package/eval/fixtures/fx-blanket-approve/caso.json +10 -0
  215. package/eval/fixtures/fx-check-runtime-cruzado/caso.json +20 -0
  216. package/eval/fixtures/fx-codex-dry/caso.json +18 -0
  217. package/eval/fixtures/fx-donewhen-executavel/caso.json +15 -0
  218. package/eval/fixtures/fx-estado-dividido/caso.json +15 -0
  219. package/eval/fixtures/fx-fase-orfa/caso.json +116 -0
  220. package/eval/fixtures/fx-happy/caso.json +6 -4
  221. package/eval/fixtures/fx-hitl-latency/caso.json +20 -0
  222. package/eval/fixtures/fx-indice-reversao/caso.json +25 -0
  223. package/eval/fixtures/fx-listagem-abertas/caso.json +16 -0
  224. package/eval/fixtures/fx-maestro-bootstrap/caso.json +21 -0
  225. package/eval/fixtures/fx-modo-aposentado-escritor/caso.json +21 -0
  226. package/eval/fixtures/fx-modo-aposentado-leitor/caso.json +16 -0
  227. package/eval/fixtures/fx-objective-oscillation/caso.json +14 -0
  228. package/eval/fixtures/fx-omnicanal/caso.json +17 -0
  229. package/eval/fixtures/fx-sensores-runtime/caso.json +17 -0
  230. package/monitor/company-brain.cjs +17 -0
  231. package/monitor/pulse-scope.cjs +45 -0
  232. package/monitor/pulse.cron +18 -0
  233. package/monitor/varredura-pulse.sh +27 -0
  234. package/package.json +25 -4
  235. package/schemas/claims.schema.json +25 -0
  236. package/schemas/company-brain.schema.json +1158 -0
  237. package/schemas/creation-operation.schema.json +365 -0
  238. package/schemas/maestro-snapshot.schema.json +2716 -0
  239. package/skills/core/onboarding/SKILL.md +50 -0
  240. package/skills/core/orkastery-bootstrap/SKILL.md +95 -65
  241. package/skills/core/thread-state/SKILL.md +3 -1
  242. package/skills/governance/scope-check-capability-map/SKILL.md +17 -7
package/dist/phase.js CHANGED
@@ -1,12 +1,4 @@
1
1
  "use strict";
2
- /**
3
- * `ork phase run|list`: despacho de fase pelo runtime adapter e leitura do ledger.
4
- *
5
- * O prompt e montado pelo `ork` (nomenclatura das fases + modo de conducao + contexto da
6
- * thread + pedido do builder), gravado com hash sha256 no diretorio da thread e so entao
7
- * despachado. Depois do despacho o `ork` RE-VERIFICA no runtime que a sessao existe:
8
- * self-report de despacho nao vale como evidencia.
9
- */
10
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
3
  if (k2 === undefined) k2 = k;
12
4
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -42,6 +34,11 @@ var __importStar = (this && this.__importStar) || (function () {
42
34
  })();
43
35
  Object.defineProperty(exports, "__esModule", { value: true });
44
36
  exports.REGRAS_DE_EVIDENCIA = exports.CONTRATO_DAS_FASES = void 0;
37
+ exports.escolherPerfil = escolherPerfil;
38
+ exports.perfilParaDespacho = perfilParaDespacho;
39
+ exports.runtimeCruzadoParaDespacho = runtimeCruzadoParaDespacho;
40
+ exports.estadoParaDespacho = estadoParaDespacho;
41
+ exports.fasesDoCicloNoPrompt = fasesDoCicloNoPrompt;
45
42
  exports.valoresDoPrompt = valoresDoPrompt;
46
43
  exports.montarPrompt = montarPrompt;
47
44
  exports.montarPromptComMemoria = montarPromptComMemoria;
@@ -50,16 +47,33 @@ exports.slugDaSessao = slugDaSessao;
50
47
  exports.resolverDespacho = resolverDespacho;
51
48
  exports.dadosDoDespacho = dadosDoDespacho;
52
49
  exports.concluirDespacho = concluirDespacho;
50
+ exports.prazoDaSessao = prazoDaSessao;
53
51
  exports.rodarFase = rodarFase;
54
52
  exports.tabelaDoLedger = tabelaDoLedger;
55
53
  exports.descreverSlug = descreverSlug;
54
+ const hitl_gates_1 = require("./hitl-gates");
55
+ const runtime_context_1 = require("./runtime-context");
56
+ /**
57
+ * `ork phase run|list`: despacho de fase pelo runtime adapter e leitura do ledger.
58
+ *
59
+ * O prompt e montado pelo `ork` (nomenclatura das fases + modo de conducao + contexto da
60
+ * thread + pedido do builder), gravado com hash sha256 no diretorio da thread e so entao
61
+ * despachado. Depois do despacho o `ork` RE-VERIFICA no runtime que a sessao existe:
62
+ * self-report de despacho nao vale como evidencia.
63
+ */
56
64
  const node_crypto_1 = require("node:crypto");
65
+ const fs = __importStar(require("node:fs"));
57
66
  const path = __importStar(require("node:path"));
58
- const adapter = __importStar(require("./adapters/claude-bg"));
67
+ const session_watcher_1 = require("./session-watcher");
68
+ const session_watcher_claude_1 = require("./session-watcher-claude");
69
+ const runtimes_1 = require("./runtimes");
70
+ const setup_1 = require("./setup");
59
71
  const modos_1 = require("./modos");
60
72
  const gates_1 = require("./gates");
61
73
  const policies_1 = require("./policies");
62
74
  const ratelimit_1 = require("./ratelimit");
75
+ const claude_bg_1 = require("./adapters/claude-bg");
76
+ const codex_1 = require("./adapters/codex");
63
77
  const memoria_1 = require("./memoria");
64
78
  const prompts_1 = require("./prompts");
65
79
  const slug_1 = require("./slug");
@@ -67,6 +81,145 @@ const thread_1 = require("./thread");
67
81
  const types_1 = require("./types");
68
82
  const util_1 = require("./util");
69
83
  const ledger_1 = require("./ledger");
84
+ const estado_thread_1 = require("./estado-thread");
85
+ const horario_1 = require("./horario");
86
+ const runtime_profiles_1 = require("./runtime-profiles");
87
+ const maquina_1 = require("./maquina");
88
+ const fabrica_publicar_1 = require("./fabrica-publicar");
89
+ const conducao_1 = require("./conducao");
90
+ /**
91
+ * I-33 (D2, D4, D14): o perfil do despacho. Sem perfis do runtime no store, nenhum (P8). Com
92
+ * perfis, o pedido explicito ou o perfil da vez na ordem do store pela politica de rotacao do
93
+ * manifesto (sem ela, a padrao da D16); perfil esgotado, sem auth, pago ou desativado nunca
94
+ * recebe despacho.
95
+ */
96
+ function escolherPerfil(raiz, runtime, opcoes = {}) {
97
+ // I-49: o despacho enxerga o estado que as outras fabricas viram na mesma conta.
98
+ const store = (0, runtime_profiles_1.lerPerfisComContas)(raiz, opcoes.agoraMs);
99
+ const doRuntime = (0, runtime_profiles_1.perfisDoRuntime)(store, runtime);
100
+ if (opcoes.perfil !== undefined) {
101
+ const pedido = doRuntime.find(p => p.id === opcoes.perfil);
102
+ if (!pedido)
103
+ return { perfil: null, configurados: doRuntime.length, erro: `perfil "${opcoes.perfil}" nao existe para o runtime ${runtime}` };
104
+ if (!(0, runtime_profiles_1.perfilDisponivel)(pedido, opcoes.agoraMs))
105
+ return { perfil: null, configurados: doRuntime.length,
106
+ erro: `perfil "${pedido.id}" esta ${pedido.estado}${pedido.esgotadoAte ? ` ate ${pedido.esgotadoAte}` : ''}` };
107
+ return { perfil: (0, runtime_profiles_1.perfilDeDespacho)(pedido), configurados: doRuntime.length };
108
+ }
109
+ if (doRuntime.length === 0)
110
+ return { perfil: null, configurados: 0 };
111
+ const proximo = (0, runtime_profiles_1.proximoPerfilDisponivel)(store, runtime, opcoes.politica ?? runtime_profiles_1.POLITICA_PADRAO, opcoes);
112
+ if (proximo)
113
+ return { perfil: (0, runtime_profiles_1.perfilDeDespacho)(proximo), configurados: doRuntime.length };
114
+ const agoraMs = opcoes.agoraMs ?? Date.now();
115
+ // Motivo do bloqueio pelo estado dos perfis: com algum esgotado ha prazo (fila); so sem-auth, login;
116
+ // so provider pago (D13), custo: `cost.violation` nunca recebe retry automatico.
117
+ const motivo = doRuntime.some(p => p.estado === 'esgotado' && p.esgotadoAte !== null && Date.parse(p.esgotadoAte) > agoraMs)
118
+ ? 'runtime.quota-exhausted' : doRuntime.some(p => p.estado === 'sem-auth') ? 'runtime.auth-missing'
119
+ : doRuntime.some(p => p.estado === 'provider-pago') ? 'cost.violation' : 'runtime.unavailable';
120
+ return { perfil: null, configurados: doRuntime.length, motivo,
121
+ erro: `nenhum perfil disponivel do runtime ${runtime} (${doRuntime.map(p => `${p.id}: ${p.estado}`).join(', ')})` };
122
+ }
123
+ /**
124
+ * I-33 (D7, D13): preflight de auth antes do despacho. O perfil escolhido tem o login conferido
125
+ * pelo proprio CLI com o env dele; reprovado, vira `sem-auth` no store (ou `provider-pago`, quando
126
+ * o login e por API key, helper, Console ou nuvem), a troca vai ao ledger (`runtime_profile_rotated`)
127
+ * e o proximo perfil do mesmo runtime e conferido. Perfil sem login de assinatura nunca recebe
128
+ * despacho. Sem perfil (ambiente do processo) ou em ensaio, nada e conferido.
129
+ */
130
+ function perfilParaDespacho(carregado, thread, fase, runtime, opcoes) {
131
+ const { raiz } = carregado;
132
+ const excluir = [...(opcoes.excluir ?? [])];
133
+ const politica = (0, runtime_profiles_1.politicaDeRotacao)(carregado.manifesto);
134
+ for (;;) {
135
+ const escolha = escolherPerfil(raiz, runtime, { perfil: opcoes.perfil, excluir, politica });
136
+ if (escolha.erro || !escolha.perfil || opcoes.dryRun)
137
+ return escolha;
138
+ const auth = runtime === 'claude-bg' ? (0, claude_bg_1.conferirAuth)(escolha.perfil) : runtime === 'codex' ? (0, codex_1.conferirAuth)(escolha.perfil)
139
+ : { ok: false, detalhe: `runtime ${runtime} sem conferencia de login` };
140
+ if (auth.ok)
141
+ return escolha;
142
+ let motivo, razao;
143
+ if (auth.transitorio) {
144
+ // A14: conferencia inconclusiva nao marca sem-auth: o perfil fica fora so deste despacho.
145
+ (0, runtime_profiles_1.registrarConferenciaInconclusiva)(raiz, escolha.perfil.id, auth.detalhe);
146
+ motivo = 'runtime.unavailable';
147
+ razao = 'a conferencia de login nao concluiu (A14): o perfil fica fora so deste despacho e e conferido de novo no proximo';
148
+ }
149
+ else if (auth.pago) {
150
+ // D13: login valido, mas cobrado por token. O perfil sai do rodizio com estado proprio e nunca despacha.
151
+ (0, runtime_profiles_1.marcarFalhaDePerfil)(raiz, escolha.perfil.id, { estado: 'provider-pago', esgotadoAte: null, motivo: 'cost.violation', detalhe: auth.detalhe });
152
+ motivo = 'cost.violation';
153
+ razao = 'o preflight achou login por provider pago (D13): perfil de API key, helper, Console ou nuvem nunca recebe despacho';
154
+ }
155
+ else {
156
+ const falha = { motivo: 'runtime.auth-missing', resetEm: null, fonte: 'sem-horario', trecho: auth.detalhe };
157
+ (0, ratelimit_1.marcarContaDaFalha)(carregado, escolha.perfil, falha);
158
+ motivo = falha.motivo;
159
+ razao = 'o preflight de auth reprovou o perfil antes do despacho (D7): perfil sem login nunca recebe despacho';
160
+ }
161
+ excluir.push(escolha.perfil.id);
162
+ const proximo = opcoes.perfil === undefined ? escolherPerfil(raiz, runtime, { excluir, politica }).perfil : null;
163
+ (0, ledger_1.registrar)((0, thread_1.dirThread)(raiz, thread.id), thread.id, ledger_1.TIPOS_DE_EVENTO.perfilRotacionado, {
164
+ fase, motivo, origem: `${opcoes.origem}.preflight`,
165
+ de: { runtime, perfil: escolha.perfil.id }, para: proximo ? { runtime, perfil: proximo.id } : null,
166
+ evidencia: auth.detalhe, razao,
167
+ autorizadoPor: 'politica de rotacao da I-33 (D5, D7, D13), igual em todos os modos',
168
+ });
169
+ if (opcoes.perfil !== undefined)
170
+ return { perfil: null, configurados: escolha.configurados, motivo,
171
+ erro: `perfil "${opcoes.perfil}" ${auth.transitorio ? 'com conferencia de login inconclusiva' : auth.pago ? 'autenticado por provider pago' : 'sem login conferido'}: ${auth.detalhe}` };
172
+ }
173
+ }
174
+ /**
175
+ * O CHECK desta thread pode rodar neste runtime? (I-43, D4, viga a)
176
+ *
177
+ * A regra: quem VALIDA nao pode ser quem EXECUTOU. Ela vinha do Objective Envelope,
178
+ * onde valia so na criacao e para tres threads que nunca rodaram nada; aqui ela passa
179
+ * a valer no despacho de qualquer thread que a exija.
180
+ *
181
+ * Compara o runtime do CHECK com os runtimes das sessoes de GO JA REGISTRADAS em
182
+ * `thread.sessoes`, que ja carregam `runtime` e ja aparecem em `ork thread status`.
183
+ * Nenhum dado novo precisa ser coletado: o que faltava era a regra, nao o dado.
184
+ *
185
+ * Sem sessao de GO registrada nao ha o que cruzar, e o despacho passa: inventar
186
+ * impedimento sobre um GO que nao aconteceu seria barrar por suposicao.
187
+ */
188
+ function runtimeCruzadoParaDespacho(thread, fase, runtime) {
189
+ if (thread.exigeRuntimeDiferente !== true || fase !== 'CHECK')
190
+ return null;
191
+ const doGo = [...new Set(thread.sessoes.filter((s) => s.fase === 'GO').map((s) => s.runtime))];
192
+ if (doGo.length === 0)
193
+ return null;
194
+ if (!doGo.includes(runtime))
195
+ return null;
196
+ return `runtime.autoconferencia: o CHECK precisa de runtime diferente do GO, e "${runtime}" `
197
+ + `foi o runtime do GO desta thread (${doGo.join(', ')}). `
198
+ + `Despache o CHECK em outro runtime, ou tire a exigencia da thread.`;
199
+ }
200
+ /**
201
+ * I-43 (T11): o ramo do Objective Envelope saiu daqui.
202
+ *
203
+ * Ele era a UNICA consulta do produto ao envelope, e dependia de `thread.creationOrigin`.
204
+ * A medida: das 137 threads do projeto, ZERO tinham `creationOrigin`. O ramo nunca
205
+ * disparou para thread nenhuma, e a "unica pausa planejada do #Maestro" nao bloqueava
206
+ * o despacho nem das tres threads que o proprio envelope criou.
207
+ *
208
+ * O que ele PRETENDIA garantir (que ninguem despacha sem validacao combinada) continua
209
+ * garantido, e agora de verdade, por `runtimeCruzadoParaDespacho` logo acima e pelos
210
+ * criterios de pronto executaveis que `ork verify` cobra.
211
+ */
212
+ function estadoParaDespacho(raiz, thread, dryRun = false) {
213
+ if (!thread.worktree)
214
+ return null;
215
+ try {
216
+ (0, estado_thread_1.vincularEstado)(raiz, thread.id, thread.worktree, dryRun);
217
+ return null;
218
+ }
219
+ catch (e) {
220
+ return e.message;
221
+ }
222
+ }
70
223
  /** O que cada fase canonica entrega. Vai no cabecalho do prompt de toda sessao. */
71
224
  exports.CONTRATO_DAS_FASES = {
72
225
  GOAL: 'GOAL (F1): objetivo verificavel, impact map, criterios de sucesso e claims com comando de verificacao. Nao implementa.',
@@ -82,6 +235,21 @@ exports.REGRAS_DE_EVIDENCIA = [
82
235
  'Trabalhe apenas no diretorio de trabalho indicado acima.',
83
236
  'Passos irreversiveis (push, merge, delecao) so acontecem quando o modo autoriza.',
84
237
  ];
238
+ /**
239
+ * I-42 (D11): as fases que o cabecalho do prompt descreve.
240
+ *
241
+ * Regra por desenho de bloco, nunca por nome de modo: quando o ciclo e um bloco so e esse bloco
242
+ * nao percorre as seis fases, o prompt descreve so as fases dele. As outras cinco linhas seriam
243
+ * cerimonia de um ciclo que a sessao nao vai percorrer. Bloco unico com as seis fases e ciclo de
244
+ * varios blocos continuam com as seis linhas, byte a byte.
245
+ */
246
+ function fasesDoCicloNoPrompt(thread) {
247
+ const [unico, ...resto] = thread.blocos;
248
+ if (unico && resto.length === 0 && unico.fases.length < types_1.FASES.length) {
249
+ return types_1.FASES.filter((f) => unico.fases.includes(f));
250
+ }
251
+ return types_1.FASES;
252
+ }
85
253
  /**
86
254
  * Os valores que o template de fase recebe.
87
255
  *
@@ -98,7 +266,7 @@ function valoresDoPrompt(thread, fase, pedido, memoriaInjetada = '') {
98
266
  thread: thread.id,
99
267
  bloco: bloco.fases.join('-'),
100
268
  nome: thread.nome,
101
- ciclo_canonico: types_1.FASES.map((f) => `${bloco.fases.includes(f) ? '>' : ' '} ${exports.CONTRATO_DAS_FASES[f]}`).join('\n'),
269
+ ciclo_canonico: fasesDoCicloNoPrompt(thread).map((f) => `${bloco.fases.includes(f) ? '>' : ' '} ${exports.CONTRATO_DAS_FASES[f]}`).join('\n'),
102
270
  tag: def.tag,
103
271
  blocos: thread.blocos.map((b) => b.fases.join('-')).join(' / '),
104
272
  pausas: String(thread.blocos.filter((b) => b.pausa).length),
@@ -150,6 +318,16 @@ function montarPromptComMemoria(carregado, thread, fase, pedido, memoria) {
150
318
  function hashDoPrompt(prompt) {
151
319
  return (0, node_crypto_1.createHash)('sha256').update(prompt, 'utf8').digest('hex');
152
320
  }
321
+ function schemaDeClaimsDoPacote() {
322
+ const candidatos = [
323
+ path.join(__dirname, '..', 'schemas', 'claims.schema.json'),
324
+ path.join(__dirname, '..', '..', 'schemas', 'claims.schema.json'),
325
+ ];
326
+ const arquivo = candidatos.find(c => fs.existsSync(c));
327
+ if (!arquivo)
328
+ throw new Error('runtime.unavailable: schema de claims nao encontrado no pacote');
329
+ return JSON.parse(fs.readFileSync(arquivo, 'utf8'));
330
+ }
153
331
  /** Slug da sessao que conduz esta fase, rotacionando quando o slug ja foi usado na thread. */
154
332
  function slugDaSessao(thread, fase) {
155
333
  const bloco = (0, thread_1.blocoDaThread)(thread, fase);
@@ -160,18 +338,28 @@ function slugDaSessao(thread, fase) {
160
338
  return (0, slug_1.proximaRotacao)(base, usados);
161
339
  }
162
340
  /**
163
- * Modelo e esforco EFETIVAMENTE usados num despacho, resolvidos numa funcao so.
341
+ * Runtime, modelo e esforco EFETIVAMENTE usados num despacho, resolvidos numa funcao so.
342
+ *
343
+ * Precedencia (decisao D4 da thread ork-homologarcod): opcao explicita do CLI > setup do
344
+ * bloco (feature #setup, `.orkastery/setup.json`) > runtime do manifesto.
164
345
  *
165
346
  * Regra do esforco: quem informa SO o modelo esta pedindo o despacho mais capaz que
166
- * existe, entao o esforco assume `high` em vez de herdar o do manifesto. Quem informa o
167
- * esforco manda, e sem nenhum dos dois vale o manifesto. Resolver isso aqui, e nao no
168
- * ponto de chamada, e o que permite gravar o par no ledger como FATO: o mesmo valor que
169
- * foi para o runtime adapter e o que fica carimbado no evento.
347
+ * existe, entao o esforco assume `high` em vez de herdar o do setup ou do manifesto.
348
+ * Quem informa o esforco manda. Resolver isso aqui, e nao no ponto de chamada, e o que
349
+ * permite gravar o trio no ledger como FATO: o mesmo valor que foi para o runtime
350
+ * adapter e o que fica carimbado no evento.
170
351
  */
171
- function resolverDespacho(manifesto, opcoes) {
172
- const model = opcoes.model ?? manifesto.runtime.model;
173
- const effort = opcoes.effort ?? (opcoes.model !== undefined ? 'high' : manifesto.runtime.effort);
174
- return { model, effort };
352
+ function resolverDespacho(manifesto, opcoes, doBloco) {
353
+ const origem = doBloco?.runtime ?? manifesto.runtime.adapter;
354
+ const runtime = opcoes.runtime ?? origem;
355
+ if ((0, runtimes_1.runtimeConhecido)(runtime) && runtime !== origem && !opcoes.model?.trim())
356
+ throw new Error(`setup.model.required: ao trocar runtime de ${origem} para ${runtime}, informe --runtime ${runtime} --model <modelo> para este despacho; o setup do projeto permanece intacto.`);
357
+ const model = opcoes.model ?? doBloco?.model ?? manifesto.runtime.model;
358
+ if (!model.trim())
359
+ throw new Error(`setup.model.required: modelo pendente para ${runtime}; informe --model <modelo> neste despacho ou complete o bloco em ork setup.`);
360
+ const effort = opcoes.effort ??
361
+ (opcoes.model !== undefined ? 'high' : doBloco?.effort ?? manifesto.runtime.effort);
362
+ return { runtime, model, effort };
175
363
  }
176
364
  /** O comando com o prompt mascarado pelo hash: o ledger nao guarda o texto inteiro. */
177
365
  function comandoMascarado(comando, sha) {
@@ -194,6 +382,12 @@ function dadosDoDespacho(ctx, comando) {
194
382
  promptPath: path.relative(ctx.raiz, ctx.promptPath),
195
383
  promptSha256: ctx.promptSha256,
196
384
  origem: ctx.origem,
385
+ // I-36 (T10): a porta por onde o dono chegou. `runtime` e o motor; `canal` e a porta.
386
+ ...(ctx.canal ? { canal: ctx.canal } : {}),
387
+ ...(ctx.correlacao ? { correlacao: ctx.correlacao } : {}),
388
+ ...(ctx.identidade ? { identidade: ctx.identidade } : {}),
389
+ // I-33 (D4): so identidade e diretorio do perfil; o store guarda o resto e nada e segredo.
390
+ ...(ctx.perfil ? { perfil: ctx.perfil } : {}),
197
391
  comando: comandoMascarado(comando, ctx.promptSha256),
198
392
  };
199
393
  }
@@ -201,27 +395,36 @@ function dadosDoDespacho(ctx, comando) {
201
395
  * Fecha um despacho BEM SUCEDIDO: ledger, decisao autonoma ou pausa prevista, e a
202
396
  * sessao gravada no `thread.json`. Usada pelo `ork phase run` e pela retomada do B3.
203
397
  */
204
- function concluirDespacho(ctx, sessionId, verificada, comando) {
398
+ function concluirDespacho(ctx, sessionId, verificada, comando, fonteVerificacao, controlador,
399
+ /** Variante histórica do supervisor; a variante do controller não tem log próprio. */
400
+ sensores) {
205
401
  const dir = (0, thread_1.dirThread)(ctx.raiz, ctx.thread.id);
206
402
  const comum = dadosDoDespacho(ctx, comando);
207
403
  const bloco = (0, thread_1.blocoDaThread)(ctx.thread, ctx.fase);
208
404
  const pausaAoFim = (0, thread_1.pausaNaThread)(ctx.thread, ctx.fase);
209
- (0, ledger_1.registrar)(dir, ctx.thread.id, ledger_1.TIPOS_DE_EVENTO.faseDespachada, { ...comum, sessionId });
405
+ // I-51 (RM-047): a maquina que despachou. So o evento de conducao; os de HITL nao mudam.
406
+ (0, ledger_1.registrar)(dir, ctx.thread.id, ledger_1.TIPOS_DE_EVENTO.faseDespachada, { ...comum, sessionId, ...(controlador ? { controlador } : {}),
407
+ maquina: (0, maquina_1.nomeDaMaquina)() });
210
408
  (0, ledger_1.registrar)(dir, ctx.thread.id, ledger_1.TIPOS_DE_EVENTO.despachoVerificado, {
211
409
  fase: ctx.fase,
212
410
  slug: ctx.slug,
213
411
  sessionId,
214
- fonte: 'claude agents --json',
412
+ // A fonte e do runtime que despachou: e ela que diz ONDE conferir a sessao depois.
413
+ fonte: fonteVerificacao ?? (0, runtimes_1.resolverRuntime)(ctx.runtime).fonteVerificacao,
215
414
  encontrada: verificada,
216
415
  });
217
416
  if (!pausaAoFim) {
417
+ const tag = (0, modos_1.definicaoDoModo)(ctx.thread.modo).tag;
218
418
  (0, ledger_1.registrar)(dir, ctx.thread.id, ledger_1.TIPOS_DE_EVENTO.decisaoAutonoma, {
219
419
  fase: ctx.fase,
220
420
  slug: ctx.slug,
221
421
  sessionId,
222
422
  decisao: `bloco ${bloco.fases.join('-')} segue sem pausa humana`,
223
- autorizadoPor: `#TAG ${(0, modos_1.definicaoDoModo)(ctx.thread.modo).tag} no pedido do builder`,
423
+ // I-41 (GO-FIX 1, D11): o rastro tipado. `autorizadoPor` fica pelo que ja o lia.
424
+ quemDecidiu: `o builder, pela #TAG ${tag} no pedido`,
425
+ autorizadoPor: `#TAG ${tag} no pedido do builder`,
224
426
  evidencia: `prompt sha256 ${ctx.promptSha256}`,
427
+ razao: `o modo ${tag} não prevê pausa humana ao fim do bloco ${bloco.fases.join('-')}`,
225
428
  });
226
429
  }
227
430
  else {
@@ -234,6 +437,7 @@ function concluirDespacho(ctx, sessionId, verificada, comando) {
234
437
  });
235
438
  }
236
439
  const sessao = {
440
+ ...(controlador ? { controlador } : {}),
237
441
  slug: ctx.slug,
238
442
  fase: ctx.fase,
239
443
  bloco: bloco.fases.join('-'),
@@ -243,23 +447,110 @@ function concluirDespacho(ctx, sessionId, verificada, comando) {
243
447
  promptPath: path.relative(ctx.raiz, ctx.promptPath),
244
448
  promptSha256: ctx.promptSha256,
245
449
  verificada,
450
+ ...(ctx.perfil ? { perfil: ctx.perfil } : {}),
451
+ ...(ctx.canal ? { canal: ctx.canal } : {}),
452
+ ...(ctx.correlacao ? { correlacao: ctx.correlacao } : {}),
246
453
  };
247
454
  ctx.thread.sessoes.push(sessao);
455
+ // `ultimoUso` e informativo: store indisponivel nao desfaz um despacho ja provado.
456
+ if (ctx.perfil) {
457
+ try {
458
+ (0, runtime_profiles_1.registrarUsoDePerfil)(ctx.raiz, ctx.perfil.id, sessao.despachadaEm);
459
+ }
460
+ catch { /* informativo */ }
461
+ }
248
462
  ctx.thread.faseAtual = ctx.fase;
249
463
  (0, thread_1.gravarThread)(ctx.raiz, ctx.thread);
464
+ // I-36 (T7): a conducao deixa de ser do ato de despachar e passa a ser da janela da sessao.
465
+ vincularConducaoDaSessao(ctx, sessionId);
466
+ // I-51 (RM-047): as outras maquinas veem a fase nova; so com a fabrica compartilhada.
467
+ (0, fabrica_publicar_1.publicarEmSegundoPlano)(ctx.raiz);
468
+ if (ctx.runtime === 'codex' || ctx.runtime === 'claude-bg')
469
+ registrarObservacao(ctx, sessao, dir, controlador, sensores);
250
470
  return sessao;
251
471
  }
472
+ /**
473
+ * I-36 (T7): o lease de execucao sobrevive ao `ork phase run`, porque quem executa e a sessao. Ele
474
+ * so sai quando a fase termina (a prova da I-34 no ledger) ou com prova de abandono (D7).
475
+ */
476
+ function vincularConducaoDaSessao(ctx, sessionId) {
477
+ const sessao = { sessionId, runtime: ctx.runtime, perfil: ctx.perfil?.id ?? null };
478
+ const prazoMs = ctx.prazoDaSessaoMs ?? conducao_1.PRAZO_DE_SESSAO_PADRAO_MS;
479
+ if (ctx.conducao) {
480
+ ctx.conducao.converterEmSessao({ ...sessao, prazoMs });
481
+ return;
482
+ }
483
+ try {
484
+ (0, conducao_1.registrarConducaoDaSessao)(ctx.raiz, ctx.thread.id, { canal: ctx.canal ?? 'cli', correlacao: ctx.correlacao ?? null,
485
+ operacao: ctx.origem.startsWith('retry') ? 'retry.run' : 'phase.run', fase: ctx.fase, promptSha256: ctx.promptSha256,
486
+ identidade: ctx.identidade?.dispatchId ?? null, prazoMs }, sessao);
487
+ }
488
+ catch { /* a conducao protege a worktree; um despacho ja provado nao se desfaz por falta dela */ }
489
+ }
490
+ /** I-36: o prazo do lease de uma sessao do bloco: o limite de duracao dele, ou o padrao. */
491
+ function prazoDaSessao(limites) {
492
+ return limites?.duracaoMs ? limites.duracaoMs + conducao_1.MARGEM_DO_PRAZO_MS : conducao_1.PRAZO_DE_SESSAO_PADRAO_MS;
493
+ }
494
+ /**
495
+ * D4: registro da fonte e arranque do observador no ponto comum a `phase` e `retry`.
496
+ * Sem CLI manual e sem segundo runtime: quem despacha e prova a sessao inicia a observacao.
497
+ */
498
+ function registrarObservacao(ctx, sessao, dir, controlador, sensores) {
499
+ const mesmoDespacho = (e) => e.sessionId === sessao.sessionId && e.despachoEm === sessao.despachadaEm;
500
+ try {
501
+ // I-34: claude-bg observa pela fonte nativa; no PLAN o registro guarda o sha de base do artefato.
502
+ // I-33 (N1): o GO codex registra o HEAD do despacho, como a fonte claude-bg; sem ele a D11c nao ve commit pelo shell.
503
+ const head = ctx.runtime === 'codex' && ctx.fase === 'GO' ? { head: ctx.headNoDespacho ?? null } : {};
504
+ const fonte = ctx.runtime === 'claude-bg' ? (0, session_watcher_claude_1.fonteClaudeDoDespacho)(ctx.raiz, ctx.thread.id, ctx.fase, ctx.cwd)
505
+ : sensores?.logPath ? { logPath: sensores.logPath, processoPath: sensores.processoPath, reciboPath: sensores.reciboPath, ...head }
506
+ : controlador ? { controlador, cwd: ctx.cwd, ...head } : null;
507
+ if (!fonte)
508
+ return;
509
+ (0, session_watcher_1.validarFonteWatcher)(ctx.raiz, sessao.sessionId, fonte);
510
+ const eventos = (0, ledger_1.lerLedger)(dir);
511
+ if (!eventos.some(e => e.tipo === 'session_sensor_registered' && mesmoDespacho(e))) {
512
+ (0, ledger_1.registrar)(dir, ctx.thread.id, 'session_sensor_registered', { fase: ctx.fase,
513
+ sessionId: sessao.sessionId, despachoEm: sessao.despachadaEm, ...fonte, ...(ctx.perfil ? { perfil: ctx.perfil } : {}) });
514
+ }
515
+ // A primitiva verifica identidade/prontidao e ignora registros antigos sem prova.
516
+ (0, session_watcher_1.iniciarWatcher)(ctx.raiz, sessao.sessionId);
517
+ }
518
+ catch {
519
+ (0, ledger_1.registrar)(dir, ctx.thread.id, 'session_watcher_error', { fase: ctx.fase, sessionId: sessao.sessionId,
520
+ despachoEm: sessao.despachadaEm, motivo: 'runtime.unavailable', erro: 'fonte ou prontidao do watcher invalida',
521
+ origem: 'phase.dispatch', ...(controlador ? { controlador } : {}) });
522
+ }
523
+ }
252
524
  /** Despacha a fase pelo runtime adapter e registra tudo no ledger da thread. */
253
525
  function rodarFase(carregado, threadId, opcoes) {
526
+ const rodar = () => opcoes.dryRun ? rodarFaseSobLock(carregado, threadId, opcoes) :
527
+ (0, hitl_gates_1.comLockHitl)(carregado.raiz, threadId, () => rodarFaseSobLock(carregado, threadId, opcoes));
528
+ // I-36 (D2): `--esperar` espera a vez FORA do lock HITL (que serializa respostas do dono) e so
529
+ // depois repete o pedido. Sem ele, a recusa sai na hora.
530
+ const prazo = opcoes.esperarMs && opcoes.esperarMs > 0 && !opcoes.dryRun ? Date.now() + opcoes.esperarMs : null;
531
+ for (;;) {
532
+ if (prazo)
533
+ (0, conducao_1.esperarConducaoLivre)(carregado.raiz, threadId, Math.max(0, prazo - Date.now()));
534
+ const r = rodar();
535
+ if (r.motivo !== 'conducao.em-andamento' || !prazo || Date.now() >= prazo)
536
+ return r;
537
+ }
538
+ }
539
+ function rodarFaseSobLock(carregado, threadId, opcoes) {
254
540
  const { raiz, manifesto } = carregado;
255
541
  const thread = (0, thread_1.lerThread)(raiz, threadId);
256
542
  const fase = opcoes.fase;
257
543
  const dir = (0, thread_1.dirThread)(raiz, thread.id);
258
544
  const slug = slugDaSessao(thread, fase);
259
- // Resolve UMA vez o par modelo/esforco: o mesmo objeto vai ao runtime adapter, ao
260
- // ledger e a resposta do CLI. Resolver duas vezes e o caminho curto para o ledger
261
- // dizer um modelo e o `claude --bg` receber outro.
262
- const { model, effort } = resolverDespacho(manifesto, opcoes);
545
+ // Resolve UMA vez o trio runtime/modelo/esforco: o mesmo objeto vai ao runtime adapter,
546
+ // ao ledger e a resposta do CLI. Resolver duas vezes e o caminho curto para o ledger
547
+ // dizer um modelo e o runtime receber outro. O setup do bloco (feature #setup) entra
548
+ // aqui: cada bloco de cada modo pode abrir a sessao com runtime/modelo/esforco proprios.
549
+ const setup = (0, setup_1.lerSetup)(raiz);
550
+ const doBloco = (0, setup_1.configDoBloco)(setup, thread.modo, fase);
551
+ const { runtime, model, effort } = resolverDespacho(manifesto, opcoes, doBloco);
552
+ const limites = (0, setup_1.limitesDoBloco)(manifesto, setup, thread.modo, fase);
553
+ const rt = (0, runtimes_1.resolverRuntime)(runtime);
263
554
  // Bloco B6: a memoria entra ANTES do hash. O prompt que o ledger carimba e o mesmo que
264
555
  // o agente recebe, memoria injetada inclusa.
265
556
  const memoria = opcoes.memoria ?? (0, memoria_1.abrirMemoria)(carregado);
@@ -296,6 +587,7 @@ function rodarFase(carregado, threadId, opcoes) {
296
587
  verificada: false,
297
588
  pausaAoFim: (0, thread_1.pausaNaThread)(thread, fase),
298
589
  dryRun: opcoes.dryRun === true,
590
+ runtime,
299
591
  model,
300
592
  effort,
301
593
  bloqueado: true,
@@ -304,173 +596,327 @@ function rodarFase(carregado, threadId, opcoes) {
304
596
  erro: detalhe,
305
597
  };
306
598
  }
307
- (0, util_1.gravar)(promptPath, prompt);
308
- // A injecao e evidencia: o ledger diz quantos itens entraram, de que colecao e de onde,
309
- // para que "o prompt tinha a decisao D2" seja verificavel depois sem reler o prompt.
310
- if (injecao.itens.length > 0) {
311
- (0, ledger_1.registrar)(dir, thread.id, ledger_1.TIPOS_DE_EVENTO.memoriaInjetada, {
312
- fase,
313
- slug,
314
- regime: injecao.regime,
315
- decisoes: injecao.decisoes,
316
- licoes: injecao.licoes,
317
- regras: injecao.regras,
318
- itens: injecao.itens.map((i) => `${i.id}:${i.colecao}:${i.location}`),
319
- promptSha256: sha,
320
- });
599
+ // I-43 (D4, viga a): a validacao por runtime diferente e cobrada no MESMO portao
600
+ // por onde todo despacho ja passa. Trocar um portao por outro no mesmo lugar e a
601
+ // mudanca que o CHECK consegue conferir lendo um diff.
602
+ const erroDeRuntime = runtimeCruzadoParaDespacho(thread, fase, runtime);
603
+ if (erroDeRuntime) {
604
+ (0, gates_1.registrarGateBloqueado)(dir, thread.id, { gate: 'phase.dispatch', motivo: 'runtime.autoconferencia',
605
+ modo: thread.modo, detalhe: erroDeRuntime, fase, slug,
606
+ correcao: `ork phase run ${thread.id} CHECK --runtime <outro> --prompt "<pedido>"` });
607
+ return { thread, slug, promptPath, promptSha256: sha, comando: [], sessionId: null,
608
+ verificada: false, pausaAoFim: (0, thread_1.pausaNaThread)(thread, fase), dryRun: opcoes.dryRun === true,
609
+ runtime, model, effort, bloqueado: true, motivo: 'runtime.autoconferencia', violacoes, erro: erroDeRuntime };
321
610
  }
322
- if (memoria.estado.motivo && memoria.estado.pedido === 'orkmind') {
323
- (0, ledger_1.registrar)(dir, thread.id, ledger_1.TIPOS_DE_EVENTO.memoriaDegradada, {
324
- fase,
325
- slug,
326
- pedido: memoria.estado.pedido,
327
- efetivo: memoria.estado.efetivo,
328
- motivo: memoria.estado.motivo,
329
- detalhe: memoria.estado.detalhe,
330
- correcao: memoria.estado.correcao,
331
- });
611
+ const erroDeEstado = estadoParaDespacho(raiz, thread, opcoes.dryRun);
612
+ if (erroDeEstado) {
613
+ (0, gates_1.registrarGateBloqueado)(dir, thread.id, { gate: 'phase.dispatch', motivo: 'tree.blocked',
614
+ modo: thread.modo, detalhe: erroDeEstado, correcao: `ork worktree sync ${thread.id}`, fase, slug });
615
+ return { thread, slug, promptPath, promptSha256: sha, comando: [], sessionId: null,
616
+ verificada: false, pausaAoFim: (0, thread_1.pausaNaThread)(thread, fase), dryRun: opcoes.dryRun === true,
617
+ runtime, model, effort, bloqueado: true, motivo: 'tree.blocked', violacoes, erro: erroDeEstado };
332
618
  }
333
- const cwd = thread.worktree ?? raiz;
334
- const pausaAoFim = (0, thread_1.pausaNaThread)(thread, fase);
335
- const resultado = adapter.despachar({
336
- prompt,
337
- nome: slug,
338
- cwd,
339
- model,
340
- effort,
341
- dryRun: opcoes.dryRun,
342
- });
343
- const ctx = {
344
- raiz,
345
- thread,
346
- fase,
347
- slug,
348
- promptPath,
349
- promptSha256: sha,
350
- runtime: manifesto.runtime.adapter,
351
- cwd,
352
- model,
353
- effort,
354
- origem: 'phase.run',
619
+ // I-36 (T7, T14): validado o pedido, a conducao da thread, antes de tocar a worktree. A mesma fase com o
620
+ // mesmo prompt de quem conduz devolve a sessao em andamento sem chamar o adapter; outro pedido recebe a recusa.
621
+ const identidade = (0, runtime_context_1.novaIdentidadeDeDespacho)(thread.id, fase);
622
+ const canal = opcoes.canal ?? (0, conducao_1.canalDoProcesso)();
623
+ const pedidoDeConducao = { canal, correlacao: opcoes.correlacao ?? null, operacao: 'phase.run', fase,
624
+ promptSha256: sha, identidade: identidade.dispatchId, prazoMs: prazoDaSessao(limites) };
625
+ const ocupadaPor = (ocupada, gravarNoLedger) => {
626
+ const base = { thread, slug, promptPath, promptSha256: sha, comando: [], verificada: false,
627
+ pausaAoFim: (0, thread_1.pausaNaThread)(thread, fase), dryRun: opcoes.dryRun === true, runtime, model, effort, violacoes: [] };
628
+ const atual = ocupada.atual;
629
+ if (ocupada.idempotente && atual) {
630
+ if (gravarNoLedger)
631
+ (0, ledger_1.registrar)(dir, thread.id, ledger_1.TIPOS_DE_EVENTO.despachoIdempotente, { fase, promptSha256: sha, canal,
632
+ sessionId: atual.sessao, conducao: { canal: atual.canal, desde: atual.desde },
633
+ razao: 'mesma fase e mesmo prompt de quem ja conduz: a sessao em andamento atende o pedido, sem sessao nova e sem cota' });
634
+ return { ...base, sessionId: atual.sessao, bloqueado: false, motivo: null, idempotente: true, conducao: atual };
635
+ }
636
+ const recusa = (0, conducao_1.recusaDeConducao)(thread.id, atual, pedidoDeConducao);
637
+ if (gravarNoLedger)
638
+ (0, conducao_1.registrarRecusa)(raiz, recusa);
639
+ return { ...base, sessionId: null, bloqueado: true, motivo: 'conducao.em-andamento', erro: recusa.texto, recusa,
640
+ ...(atual ? { conducao: atual } : {}) };
355
641
  };
356
- const comum = dadosDoDespacho(ctx, resultado.comando);
642
+ let conducao = null;
357
643
  if (opcoes.dryRun) {
358
- return {
644
+ // O ensaio nao toma nada, mas diz a verdade: com conducao em andamento, o despacho real seria recusado.
645
+ const atual = (0, conducao_1.conducaoDaThread)(raiz, thread.id);
646
+ if (atual)
647
+ return ocupadaPor({ ok: false, idempotente: atual.fase === fase && atual.promptSha256 === sha, atual }, false);
648
+ }
649
+ else {
650
+ const tomada = (0, conducao_1.tomarConducao)(raiz, thread.id, pedidoDeConducao);
651
+ if (!tomada.ok)
652
+ return ocupadaPor(tomada, true);
653
+ conducao = tomada;
654
+ }
655
+ try {
656
+ const cwd = thread.worktree ?? raiz;
657
+ let contextoRuntime;
658
+ try {
659
+ contextoRuntime = (0, runtime_context_1.contextoDoProjeto)(raiz, runtime, cwd, thread.id);
660
+ }
661
+ catch (e) {
662
+ const erro = `runtime.unavailable: ${e.message}`;
663
+ (0, gates_1.registrarGateBloqueado)(dir, thread.id, { gate: 'phase.dispatch', motivo: 'runtime.unavailable',
664
+ modo: thread.modo, detalhe: erro, fase, slug });
665
+ return { thread, slug, promptPath, promptSha256: sha, comando: [], sessionId: null,
666
+ verificada: false, pausaAoFim: (0, thread_1.pausaNaThread)(thread, fase), dryRun: opcoes.dryRun === true,
667
+ runtime, model, effort, bloqueado: true, motivo: 'runtime.unavailable', violacoes, erro };
668
+ }
669
+ if (contextoRuntime)
670
+ contextoRuntime.identidade = identidade;
671
+ (0, util_1.gravar)(promptPath, prompt);
672
+ // I-33 (D2, D4, D7): o perfil sai do store com o login conferido. Sem perfil disponivel, o
673
+ // despacho nao sai pela conta errada: o prompt ja gravado fica registrado para a rotacao do
674
+ // retry (outro runtime da ordem de fallback, ou a fila), com o motivo tipado.
675
+ let escolha;
676
+ try {
677
+ escolha = perfilParaDespacho(carregado, thread, fase, runtime, { perfil: opcoes.perfil, dryRun: opcoes.dryRun, origem: 'phase.run' });
678
+ }
679
+ catch (e) {
680
+ escolha = { perfil: null, configurados: 0, erro: e.message };
681
+ }
682
+ if (escolha.erro) {
683
+ const motivo = escolha.motivo ?? 'runtime.unavailable';
684
+ const erro = `${motivo}: ${escolha.erro}`;
685
+ if (!opcoes.dryRun) {
686
+ const semPerfil = { identidade, raiz, thread, fase, slug, promptPath, promptSha256: sha, runtime, cwd, model, effort,
687
+ origem: 'phase.run', canal, correlacao: opcoes.correlacao ?? null };
688
+ (0, ledger_1.registrar)(dir, thread.id, ledger_1.TIPOS_DE_EVENTO.despachoFalhou, { ...dadosDoDespacho(semPerfil, []), erro });
689
+ (0, gates_1.registrarGateBloqueado)(dir, thread.id, { gate: 'phase.dispatch', motivo, modo: thread.modo, detalhe: erro,
690
+ correcao: motivo === 'cost.violation'
691
+ ? 'refaca o login do perfil pela assinatura (claude auth login --claudeai ou codex login com ChatGPT) e rode ork accounts check; perfil de provider pago nunca despacha'
692
+ : 'ork accounts list mostra o estado de cada perfil; ork retry run segue pela ordem de fallback ou pela fila',
693
+ fase, slug, runtime, origem: 'phase.run' });
694
+ }
695
+ return { thread, slug, promptPath, promptSha256: sha, comando: [], sessionId: null,
696
+ verificada: false, pausaAoFim: (0, thread_1.pausaNaThread)(thread, fase), dryRun: opcoes.dryRun === true,
697
+ runtime, model, effort, bloqueado: true, motivo, violacoes, erro };
698
+ }
699
+ const perfil = escolha.perfil ?? undefined;
700
+ // A injecao e evidencia: o ledger diz quantos itens entraram, de que colecao e de onde,
701
+ // para que "o prompt tinha a decisao D2" seja verificavel depois sem reler o prompt.
702
+ if (injecao.itens.length > 0) {
703
+ (0, ledger_1.registrar)(dir, thread.id, ledger_1.TIPOS_DE_EVENTO.memoriaInjetada, {
704
+ fase,
705
+ slug,
706
+ regime: injecao.regime,
707
+ decisoes: injecao.decisoes,
708
+ licoes: injecao.licoes,
709
+ regras: injecao.regras,
710
+ itens: injecao.itens.map((i) => `${i.id}:${i.colecao}:${i.location}`),
711
+ promptSha256: sha,
712
+ });
713
+ }
714
+ if (memoria.estado.motivo && memoria.estado.pedido === 'orkmind') {
715
+ (0, ledger_1.registrar)(dir, thread.id, ledger_1.TIPOS_DE_EVENTO.memoriaDegradada, {
716
+ fase,
717
+ slug,
718
+ pedido: memoria.estado.pedido,
719
+ efetivo: memoria.estado.efetivo,
720
+ motivo: memoria.estado.motivo,
721
+ detalhe: memoria.estado.detalhe,
722
+ correcao: memoria.estado.correcao,
723
+ });
724
+ }
725
+ const pausaAoFim = (0, thread_1.pausaNaThread)(thread, fase);
726
+ const claimsSchema = fase === 'GO' && runtime === 'codex'
727
+ ? schemaDeClaimsDoPacote()
728
+ : undefined;
729
+ // I-33 (N1): lido antes de a sessao existir; depois dela, um commit rapido ja estaria no HEAD.
730
+ const headNoDespacho = fase === 'GO' && runtime === 'codex' && !opcoes.dryRun ? (0, session_watcher_claude_1.headDaWorktree)(cwd) : undefined;
731
+ const resultado = rt.despachar({
732
+ prompt,
733
+ nome: slug,
734
+ cwd,
735
+ model,
736
+ effort,
737
+ dryRun: opcoes.dryRun,
738
+ sandbox: manifesto.runtime.sandbox,
739
+ logDir: path.join(dir, 'sessoes'),
740
+ vinculo: { thread: thread.id, fase, promptSha256: sha },
741
+ contextoRuntime,
742
+ ...(perfil ? { perfil } : {}),
743
+ ...(fase === 'PLAN' ? { colaboracao: 'plan' } : {}),
744
+ ...(claimsSchema ? { outputSchema: claimsSchema } : {}),
745
+ // `thread.base.branch` vira a branch fonte quando a thread ganha worktree
746
+ // (`ork/<slug>`). O review nativo precisa comparar essa fonte com a base de
747
+ // integração do projeto; usar a própria fonte produz um diff vazio.
748
+ ...(fase === 'CHECK' && runtime === 'codex' ? { reviewBaseBranch: manifesto.worktree.base_branch } : {}),
749
+ ...(limites ? { duracaoMaximaMs: limites.duracaoMs } : {}),
750
+ // I-36 (D4): a sessao filha herda a identidade do despacho e o canal; e assim que ela reentra.
751
+ ambienteExtra: (0, conducao_1.ambienteDaConducao)(identidade.dispatchId, thread.id, canal),
752
+ });
753
+ const ctx = {
754
+ identidade,
755
+ canal,
756
+ correlacao: opcoes.correlacao ?? null,
757
+ ...(conducao ? { conducao } : {}),
758
+ prazoDaSessaoMs: pedidoDeConducao.prazoMs,
759
+ raiz,
359
760
  thread,
761
+ fase,
360
762
  slug,
361
763
  promptPath,
362
764
  promptSha256: sha,
363
- comando: resultado.comando,
364
- sessionId: null,
365
- verificada: false,
366
- pausaAoFim,
367
- dryRun: true,
368
- // O ensaio NAO toca no ledger (a garantia do B0 continua de pe), mas devolve o
369
- // par resolvido: e assim que o `--dry-run` mostra o modelo que seria despachado.
765
+ runtime,
766
+ cwd,
370
767
  model,
371
768
  effort,
372
- bloqueado: false,
373
- motivo: null,
374
- violacoes,
769
+ origem: 'phase.run',
770
+ ...(perfil ? { perfil } : {}),
771
+ ...(headNoDespacho !== undefined ? { headNoDespacho } : {}),
375
772
  };
376
- }
377
- if (!resultado.ok || !resultado.sessionId) {
378
- const erro = resultado.erro ?? 'sem sessionId na saida do runtime';
379
- (0, ledger_1.registrar)(dir, thread.id, ledger_1.TIPOS_DE_EVENTO.despachoFalhou, {
380
- ...comum,
381
- erro,
382
- stderr: resultado.stderr.slice(0, 500),
383
- });
384
- // Bloco B3: rate limit nao e falha seca. A fase morreu com HORA de volta, entao ela
385
- // vira pedido na fila duravel em vez de virar tarefa da memoria de alguem. O prompt
386
- // ja esta gravado em disco com o sha256 acima, e e ele que sera redespachado.
387
- const sinal = resultado.rateLimit ?? null;
388
- if (sinal) {
389
- (0, ledger_1.registrar)(dir, thread.id, ledger_1.TIPOS_DE_EVENTO.rateLimitDetectado, {
390
- fase,
391
- slug,
392
- fonte: sinal.fonte,
393
- resetEm: sinal.resetEm,
394
- trecho: sinal.trecho,
395
- evidencia: 'stderr real do runtime adapter, nao relato do agente',
396
- });
397
- const pedido = (0, ratelimit_1.enfileirar)(carregado, {
398
- thread: thread.id,
399
- fase,
773
+ const comum = dadosDoDespacho(ctx, resultado.comando);
774
+ if (opcoes.dryRun) {
775
+ return {
776
+ thread,
400
777
  slug,
401
778
  promptPath,
402
779
  promptSha256: sha,
403
- cwd,
780
+ comando: resultado.comando,
781
+ sessionId: null,
782
+ verificada: false,
783
+ pausaAoFim,
784
+ dryRun: true,
785
+ // O ensaio NAO toca no ledger (a garantia do B0 continua de pe), mas devolve o
786
+ // par resolvido: e assim que o `--dry-run` mostra o modelo que seria despachado.
787
+ runtime,
404
788
  model,
405
789
  effort,
406
- sinal,
407
- detalhe: erro,
408
- });
409
- (0, ledger_1.registrar)(dir, thread.id, ledger_1.TIPOS_DE_EVENTO.rateLimitEnfileirado, {
410
- fase,
411
- slug,
412
- pedido: pedido.id,
413
- liberaEm: pedido.liberaEm,
414
- janelaEstimada: pedido.janelaEstimada,
415
- fonte: sinal.fonte,
416
- promptSha256: sha,
417
- correcao: `ork retry resume (ou aguarde ate ${pedido.liberaEm})`,
790
+ bloqueado: false,
791
+ motivo: null,
792
+ violacoes,
793
+ };
794
+ }
795
+ if (!resultado.ok || !resultado.sessionId) {
796
+ const erro = resultado.erro ?? 'sem sessionId na saida do runtime';
797
+ (0, ledger_1.registrar)(dir, thread.id, ledger_1.TIPOS_DE_EVENTO.despachoFalhou, {
798
+ ...comum,
799
+ controlador: resultado.controlador,
800
+ erro,
801
+ stderr: resultado.stderr.slice(0, 500),
418
802
  });
803
+ // I-33 (D1, D5): a CONTA recusou o despacho. Nada rodou, entao nada foi produzido: o perfil
804
+ // sai do rodizio e o gate tipado fica para a rotacao do retry, que redespacha o mesmo prompt.
805
+ const falha = resultado.falhaDeConta ?? null;
806
+ if (falha) {
807
+ try {
808
+ (0, ratelimit_1.marcarContaDaFalha)(carregado, perfil, falha);
809
+ }
810
+ catch { /* o gate abaixo continua registrando a falha */ }
811
+ (0, gates_1.registrarGateBloqueado)(dir, thread.id, { gate: 'phase.dispatch', motivo: falha.motivo, modo: thread.modo, detalhe: erro,
812
+ correcao: 'ork retry run rotaciona para o proximo perfil, para o runtime de fallback ou para a fila', fase, slug,
813
+ runtime, origem: 'phase.run', falhaDeConta: falha, ...(perfil ? { perfil } : {}),
814
+ evidencia: 'saida real do runtime adapter no despacho, nao relato do agente' });
815
+ return { thread, slug, promptPath, promptSha256: sha, comando: resultado.comando, controlador: resultado.controlador,
816
+ sessionId: null, verificada: false, pausaAoFim, dryRun: false, runtime, model, effort, bloqueado: false,
817
+ motivo: falha.motivo, violacoes, naFila: null, erro };
818
+ }
819
+ // Bloco B3: rate limit nao e falha seca. A fase morreu com HORA de volta, entao ela
820
+ // vira pedido na fila duravel em vez de virar tarefa da memoria de alguem. O prompt
821
+ // ja esta gravado em disco com o sha256 acima, e e ele que sera redespachado.
822
+ const sinal = resultado.rateLimit ?? null;
823
+ if (sinal) {
824
+ (0, ledger_1.registrar)(dir, thread.id, ledger_1.TIPOS_DE_EVENTO.rateLimitDetectado, {
825
+ fase,
826
+ slug,
827
+ fonte: sinal.fonte,
828
+ resetEm: sinal.resetEm,
829
+ trecho: sinal.trecho,
830
+ evidencia: 'stderr real do runtime adapter, nao relato do agente',
831
+ });
832
+ const pedido = (0, ratelimit_1.enfileirar)(carregado, {
833
+ thread: thread.id,
834
+ fase,
835
+ slug,
836
+ promptPath,
837
+ promptSha256: sha,
838
+ cwd,
839
+ model,
840
+ effort,
841
+ sinal,
842
+ detalhe: erro,
843
+ });
844
+ (0, ledger_1.registrar)(dir, thread.id, ledger_1.TIPOS_DE_EVENTO.rateLimitEnfileirado, {
845
+ fase,
846
+ slug,
847
+ pedido: pedido.id,
848
+ liberaEm: pedido.liberaEm,
849
+ janelaEstimada: pedido.janelaEstimada,
850
+ fonte: sinal.fonte,
851
+ promptSha256: sha,
852
+ correcao: `ork retry resume (ou aguarde ate ${pedido.liberaEm})`,
853
+ });
854
+ return {
855
+ thread,
856
+ slug,
857
+ promptPath,
858
+ promptSha256: sha,
859
+ comando: resultado.comando,
860
+ controlador: resultado.controlador,
861
+ sessionId: null,
862
+ verificada: false,
863
+ pausaAoFim,
864
+ dryRun: false,
865
+ runtime,
866
+ model,
867
+ effort,
868
+ bloqueado: false,
869
+ motivo: 'runtime.rate-limited',
870
+ violacoes,
871
+ naFila: pedido,
872
+ erro,
873
+ };
874
+ }
419
875
  return {
420
876
  thread,
421
877
  slug,
422
878
  promptPath,
423
879
  promptSha256: sha,
424
880
  comando: resultado.comando,
881
+ controlador: resultado.controlador,
425
882
  sessionId: null,
426
883
  verificada: false,
427
884
  pausaAoFim,
428
885
  dryRun: false,
886
+ runtime,
429
887
  model,
430
888
  effort,
431
889
  bloqueado: false,
432
- motivo: 'runtime.rate-limited',
890
+ motivo: 'runtime.unavailable',
433
891
  violacoes,
434
- naFila: pedido,
892
+ naFila: null,
435
893
  erro,
436
894
  };
437
895
  }
896
+ concluirDespacho(ctx, resultado.sessionId, resultado.verificada, resultado.comando, rt.fonteVerificacao, resultado.controlador, { logPath: resultado.logPath, processoPath: resultado.processoPath, reciboPath: resultado.reciboPath });
438
897
  return {
439
898
  thread,
440
899
  slug,
441
900
  promptPath,
442
901
  promptSha256: sha,
443
902
  comando: resultado.comando,
444
- sessionId: null,
445
- verificada: false,
903
+ sessionId: resultado.sessionId,
904
+ controlador: resultado.controlador,
905
+ verificada: resultado.verificada,
446
906
  pausaAoFim,
447
907
  dryRun: false,
908
+ runtime,
448
909
  model,
449
910
  effort,
450
911
  bloqueado: false,
451
- motivo: 'runtime.unavailable',
912
+ motivo: null,
452
913
  violacoes,
453
- naFila: null,
454
- erro,
455
914
  };
456
915
  }
457
- concluirDespacho(ctx, resultado.sessionId, resultado.verificada, resultado.comando);
458
- return {
459
- thread,
460
- slug,
461
- promptPath,
462
- promptSha256: sha,
463
- comando: resultado.comando,
464
- sessionId: resultado.sessionId,
465
- verificada: resultado.verificada,
466
- pausaAoFim,
467
- dryRun: false,
468
- model,
469
- effort,
470
- bloqueado: false,
471
- motivo: null,
472
- violacoes,
473
- };
916
+ finally {
917
+ // Despacho que nao virou sessao devolve a conducao; o que virou ja a entregou a sessao.
918
+ conducao?.liberar();
919
+ }
474
920
  }
475
921
  /** Tabela de `ork phase list`: o historico do ledger da thread. */
476
922
  function tabelaDoLedger(raiz, threadId) {
@@ -523,7 +969,7 @@ function tabelaDoLedger(raiz, threadId) {
523
969
  if (e.cobertura)
524
970
  detalhe.push(`cobertura ${String(e.cobertura)}`);
525
971
  if (e.liberaEm)
526
- detalhe.push(`libera em ${String(e.liberaEm)}`);
972
+ detalhe.push(`libera em ${(0, horario_1.formatarDataHora)(String(e.liberaEm))}`);
527
973
  if (e.tentativa !== undefined && e.limite !== undefined) {
528
974
  detalhe.push(`tentativa ${String(e.tentativa)}/${String(e.limite)}`);
529
975
  }
@@ -555,12 +1001,12 @@ function tabelaDoLedger(raiz, threadId) {
555
1001
  if (e.erro)
556
1002
  detalhe.push(`erro: ${String(e.erro)}`);
557
1003
  if (e.detalhe && !e.decisao)
558
- detalhe.push(String(e.detalhe).slice(0, 90));
1004
+ detalhe.push((0, horario_1.localizarTexto)(String(e.detalhe)).slice(0, 90));
559
1005
  if (e.modo && !e.fase)
560
1006
  detalhe.push(`modo ${String(e.modo)}`);
561
- return [e.ts.replace('T', ' ').slice(0, 19), e.tipo, detalhe.join(' | ')];
1007
+ return [(0, horario_1.formatarDataHora)(e.ts, { segundos: true }), e.tipo, detalhe.join(' | ')];
562
1008
  });
563
- return (0, util_1.tabela)(['QUANDO', 'EVENTO', 'DETALHE'], linhas);
1009
+ return `${(0, util_1.tabela)(['QUANDO', 'EVENTO', 'DETALHE'], linhas)}\n${(0, horario_1.legendaDoFuso)()}`;
564
1010
  }
565
1011
  /** Slug legivel por humano de uma sessao (usado nas mensagens do CLI). */
566
1012
  function descreverSlug(slug) {