@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
@@ -8,39 +8,143 @@
8
8
  * O que vale: `claude --bg "<prompt>" --name <slug>` rodando com cwd no diretorio do projeto,
9
9
  * o que deixa a sessao visivel no app Claude e no `claude agents --json`.
10
10
  */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
11
44
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.NOME_ADAPTER = void 0;
45
+ exports.JANELA_CURTA_MAX_MS = exports.NOME_ADAPTER = void 0;
46
+ exports.ambienteDoPerfil = ambienteDoPerfil;
13
47
  exports.disponivel = disponivel;
14
48
  exports.versao = versao;
15
49
  exports.montarComando = montarComando;
16
50
  exports.extrairSessionId = extrairSessionId;
17
51
  exports.listarSessoes = listarSessoes;
52
+ exports.consultarSessoes = consultarSessoes;
53
+ exports.consultarAgentesNativos = consultarAgentesNativos;
18
54
  exports.acharSessao = acharSessao;
19
55
  exports.despachar = despachar;
20
56
  exports.parseRateLimit = parseRateLimit;
57
+ exports.naturezaDoLimite = naturezaDoLimite;
58
+ exports.parseFalhaDeConta = parseFalhaDeConta;
59
+ exports.conferirAuth = conferirAuth;
21
60
  exports.comandoAttach = comandoAttach;
22
61
  exports.limparAnsi = limparAnsi;
23
62
  exports.logs = logs;
63
+ exports.logsDaSessao = logsDaSessao;
24
64
  exports.parar = parar;
25
65
  exports.ocupacaoDeContexto = ocupacaoDeContexto;
26
66
  const util_1 = require("../util");
67
+ const runtime_ambiente_1 = require("../runtime-ambiente");
68
+ const path = __importStar(require("node:path"));
69
+ const runtime_context_1 = require("../runtime-context");
70
+ const runtime_profiles_1 = require("../runtime-profiles");
71
+ function exec(cmd, args, cwd, timeoutMs, ambiente = (0, runtime_ambiente_1.ambienteDeAssinatura)()) {
72
+ return (0, util_1.exec)(cmd, args, cwd, timeoutMs, ambiente);
73
+ }
74
+ /**
75
+ * I-33 (D2): ambiente do filho com o `CLAUDE_CONFIG_DIR` do perfil sobre a assinatura
76
+ * higienizada (sem provider pago). Sem perfil, o ambiente do processo, como antes da I-33.
77
+ * A mesma conta despacha, reverifica, consulta, le logs e para a sessao.
78
+ */
79
+ function ambienteDoPerfil(perfil) {
80
+ if (perfil && (perfil.runtime !== 'claude-bg' || !perfil.configDir))
81
+ throw new Error('runtime.profiles.invalid: perfil sem CLAUDE_CONFIG_DIR no despacho claude-bg');
82
+ return (0, runtime_profiles_1.ambienteComPerfil)((0, runtime_ambiente_1.ambienteDeAssinatura)(), perfil);
83
+ }
27
84
  exports.NOME_ADAPTER = 'claude-bg';
85
+ /** Consultas do perfil interactive; gates e respostas humanas nunca recebem grant. */
86
+ const CONSULTAS_MCP = [
87
+ 'mcp__orkastery__ork_thread_status',
88
+ 'mcp__orkastery__ork_phase_list',
89
+ 'mcp__orkastery__ork_hitl_pending',
90
+ 'mcp__orkastery__ork_observe',
91
+ 'mcp__orkastery__ork_artifact_read',
92
+ 'mcp__orkastery__ork_claims_list',
93
+ ];
94
+ const MUTACOES_WORKTREE = [
95
+ 'mcp__orkastery__ork_artifact_write', 'mcp__orkastery__ork_claim_add',
96
+ 'mcp__orkastery__ork_git_commit', 'mcp__orkastery__ork_verify', 'mcp__orkastery__ork_ship',
97
+ ];
98
+ /** Regras nativas de arquivo, nao sandbox de processos. Edit tambem cobre Write. */
99
+ function permissoesDaWorktree(cwd, permiteEditarProduto) {
100
+ if (!path.isAbsolute(cwd) || cwd === '/' || path.normalize(cwd) !== cwd || !/^[A-Za-z0-9_./-]+$/.test(cwd))
101
+ throw Error('runtime.context.permissions: worktree contem caracteres ambiguos para regras nativas; use caminho sem glob, espacos ou delimitadores');
102
+ const absoluto = '/' + cwd; // Claude exige // para caminho absoluto.
103
+ const deny = [];
104
+ for (const nome of ['.git', '.orkastery', '.claude', '.codex', '.agents', '.env', '.env*']) {
105
+ for (const prefixo of [`${absoluto}/${nome}`, `${absoluto}/**/${nome}`])
106
+ deny.push(`Edit(${prefixo})`, `Edit(${prefixo}/**)`);
107
+ }
108
+ return { allow: [...CONSULTAS_MCP, 'mcp__orkastery__ork_git_status', ...MUTACOES_WORKTREE, ...(permiteEditarProduto ? [`Edit(${absoluto}/**)`] : [])], deny };
109
+ }
28
110
  /** O runtime esta disponivel nesta maquina? */
29
111
  function disponivel() {
30
112
  return (0, util_1.noPath)('claude');
31
113
  }
32
114
  /** Versao do runtime, para o `ork doctor`. */
33
115
  function versao() {
34
- const r = (0, util_1.exec)('claude', ['--version'], undefined, 30000);
116
+ const r = exec('claude', ['--version'], undefined, 30000);
35
117
  return r.ok ? r.stdout.trim() : null;
36
118
  }
119
+ /** Ferramentas de arquivo negadas no PLAN: o plano sai pelo núcleo, não pelo disco. */
120
+ const ESCRITA_DE_ARQUIVO = ['Edit', 'Write', 'NotebookEdit'];
37
121
  /** Monta a linha de comando exata do despacho (usada tambem pelo `--dry-run`). */
38
122
  function montarComando(pedido) {
39
123
  const args = ['--bg', pedido.prompt, '--name', pedido.nome];
124
+ // I-34 (D6): plan mode em `--bg` desabilita ExitPlanMode, grava o plano em ~/.claude/plans
125
+ // e bloqueia ork_artifact_write. O PLAN roda como sessão comum, sem escrita de arquivo e
126
+ // com o artefato gravado pelo núcleo; o agente `ork-plan` não lista ferramentas MCP.
127
+ const plano = pedido.colaboracao === 'plan';
40
128
  if (pedido.model)
41
129
  args.push('--model', pedido.model);
42
130
  if (pedido.effort)
43
131
  args.push('--effort', pedido.effort);
132
+ if (pedido.contextoRuntime) {
133
+ if (pedido.contextoRuntime.host !== 'claude-code')
134
+ throw Error('runtime.context.invalid: host Claude esperado');
135
+ const contexto = (0, runtime_context_1.validarContextoRuntime)(pedido.contextoRuntime, pedido.cwd);
136
+ // Somente o servidor gerado: nao mesclar MCP de outros escopos nesta sessao filha.
137
+ const permissoes = contexto.permissoesFilho === 'worktree'
138
+ ? permissoesDaWorktree(pedido.cwd, contexto.permiteEditarProduto && !plano) : null;
139
+ // PLAN não implementa: nem commit, nem claim, nem verify, nem ship, em nenhum perfil.
140
+ const allow = plano ? [...CONSULTAS_MCP, 'mcp__orkastery__ork_artifact_write'] : permissoes?.allow ?? [...CONSULTAS_MCP];
141
+ const deny = [...(plano ? ESCRITA_DE_ARQUIVO : []), ...(permissoes?.deny ?? [])];
142
+ args.push('--plugin-dir', contexto.instalacao, '--strict-mcp-config', '--mcp-config', JSON.stringify({ mcpServers: { orkastery: contexto.servidor } }), '--allowedTools', allow.join(','));
143
+ if (deny.length)
144
+ args.push('--disallowedTools', deny.join(','));
145
+ }
146
+ else if (plano)
147
+ args.push('--disallowedTools', ESCRITA_DE_ARQUIVO.join(','));
44
148
  return ['claude', ...args];
45
149
  }
46
150
  /**
@@ -61,25 +165,81 @@ function extrairSessionId(saida) {
61
165
  * em 02/09/2026 na versao 2.1.259, `claude agents --json --cwd <raiz>` devolve lista
62
166
  * vazia mesmo com sessoes vivas naquele diretorio.
63
167
  */
64
- function listarSessoes(cwd, todas = false) {
168
+ function listarSessoes(cwd, todas = false, ambiente = (0, runtime_ambiente_1.ambienteDeAssinatura)()) {
169
+ return consultarSessoes(cwd, todas, ambiente).sessoes;
170
+ }
171
+ /** Falha de consulta não pode ser confundida com uma máquina sem sessões. */
172
+ function consultarSessoes(cwd, todas = false, ambiente = (0, runtime_ambiente_1.ambienteDeAssinatura)()) {
65
173
  const args = ['agents', '--json'];
66
174
  if (todas)
67
175
  args.push('--all');
68
- const r = (0, util_1.exec)('claude', args, undefined, 60000);
176
+ const r = exec('claude', args, undefined, 60000, ambiente);
69
177
  if (!r.ok)
70
- return [];
178
+ return { ok: false, sessoes: [], detalhe: `claude agents falhou (código ${r.code})` };
71
179
  let dados;
72
180
  try {
73
181
  const bruto = JSON.parse(r.stdout);
74
- dados = Array.isArray(bruto) ? bruto : [];
182
+ if (!Array.isArray(bruto) || bruto.some(s => !s || typeof s !== 'object' ||
183
+ typeof s.sessionId !== 'string' || !/^[a-f0-9-]{8,}$/i.test(s.sessionId) ||
184
+ typeof s.cwd !== 'string' || !path.isAbsolute(s.cwd) ||
185
+ (s.state !== undefined && typeof s.state !== 'string') ||
186
+ (s.status !== undefined && typeof s.status !== 'string')) ||
187
+ new Set(bruto.map(s => s.sessionId)).size !== bruto.length)
188
+ throw new Error('formato inválido');
189
+ dados = bruto;
75
190
  }
76
191
  catch {
77
- return [];
192
+ return { ok: false, sessoes: [], detalhe: 'claude agents respondeu JSON inválido' };
78
193
  }
79
194
  if (!cwd)
80
- return dados;
195
+ return { ok: true, sessoes: dados, detalhe: '' };
81
196
  const raiz = cwd.replace(/\/$/, '');
82
- return dados.filter((s) => s.cwd === raiz || (s.cwd ?? '').startsWith(raiz + '/'));
197
+ return { ok: true, sessoes: dados.filter((s) => s.cwd === raiz || (s.cwd ?? '').startsWith(raiz + '/')), detalhe: '' };
198
+ }
199
+ const UUID_CLAUDE = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
200
+ const LIMITE_CONSULTA_BYTES = 1024 * 1024;
201
+ /**
202
+ * I-34 (D5): fonte nativa da conclusão claude-bg. Sem `--cwd`: medido em 19/09/2026 na
203
+ * versão 2.1.278, `--cwd <worktree>` devolveu `[]` com a sessão viva ali. `--all` traz
204
+ * sessões de todos os projetos, então só `sessionId` UUID e `cwd` absoluto são exigidos por
205
+ * registro: campo de tipo inesperado vira ausente (estado ausente não conclui nada) e registro
206
+ * sem identidade é descartado. `sessionId` duplicado invalida a consulta inteira.
207
+ */
208
+ function consultarAgentesNativos(agoraMs, ambiente = (0, runtime_ambiente_1.ambienteDeAssinatura)()) {
209
+ // I-33 (D4): o registro nativo e da conta; o observador passa o env do perfil da sessao.
210
+ const consultadoEm = new Date(agoraMs ?? Date.now()).toISOString();
211
+ const falha = (detalhe) => ({ ok: false, registros: [], detalhe, consultadoEm });
212
+ const r = exec('claude', ['agents', '--json', '--all'], undefined, 60000, ambiente);
213
+ if (!r.ok)
214
+ return falha(`claude agents falhou (código ${r.code})`);
215
+ if (Buffer.byteLength(r.stdout) > LIMITE_CONSULTA_BYTES)
216
+ return falha('claude agents excedeu 1 MiB');
217
+ const texto = (v) => typeof v === 'string' ? v : undefined;
218
+ let bruto;
219
+ try {
220
+ bruto = JSON.parse(r.stdout);
221
+ }
222
+ catch {
223
+ return falha('claude agents respondeu JSON inválido');
224
+ }
225
+ if (!Array.isArray(bruto))
226
+ return falha('claude agents respondeu JSON inválido');
227
+ const registros = [];
228
+ let descartados = 0;
229
+ for (const s of bruto) {
230
+ if (!s || typeof s !== 'object' || Array.isArray(s) || typeof s.sessionId !== 'string' || !UUID_CLAUDE.test(s.sessionId) ||
231
+ typeof s.cwd !== 'string' || !path.isAbsolute(s.cwd)) {
232
+ descartados++;
233
+ continue;
234
+ }
235
+ registros.push({ sessionId: s.sessionId, cwd: s.cwd, id: texto(s.id), name: texto(s.name), kind: texto(s.kind),
236
+ state: texto(s.state), status: texto(s.status),
237
+ pid: Number.isSafeInteger(s.pid) && s.pid > 0 ? s.pid : undefined,
238
+ startedAt: typeof s.startedAt === 'number' && Number.isFinite(s.startedAt) ? s.startedAt : undefined });
239
+ }
240
+ if (new Set(registros.map(s => s.sessionId)).size !== registros.length)
241
+ return falha('claude agents repetiu sessionId');
242
+ return { ok: true, registros, detalhe: descartados ? `${descartados} registro(s) sem identidade descartado(s)` : '', consultadoEm };
83
243
  }
84
244
  /** Procura uma sessao pelo id curto, id completo ou nome. */
85
245
  function acharSessao(chave, sessoes) {
@@ -94,6 +254,13 @@ function acharSessao(chave, sessoes) {
94
254
  */
95
255
  function despachar(pedido) {
96
256
  const comando = montarComando(pedido);
257
+ let ambiente;
258
+ try {
259
+ ambiente = { ...ambienteDoPerfil(pedido.perfil), ...(pedido.ambienteExtra ?? {}) };
260
+ }
261
+ catch (e) {
262
+ return { ok: false, comando, sessionId: null, verificada: false, stdout: '', stderr: '', erro: e.message };
263
+ }
97
264
  if (pedido.dryRun) {
98
265
  return { ok: true, comando, sessionId: null, verificada: false, stdout: '', stderr: '' };
99
266
  }
@@ -108,13 +275,19 @@ function despachar(pedido) {
108
275
  erro: 'binario `claude` nao encontrado no PATH',
109
276
  };
110
277
  }
111
- const r = (0, util_1.exec)(comando[0], comando.slice(1), pedido.cwd, 180000);
278
+ const r = exec(comando[0], comando.slice(1), pedido.cwd, 180000, ambiente);
112
279
  const saida = r.stdout + '\n' + r.stderr;
113
280
  const sessionId = extrairSessionId(saida);
114
281
  if (!r.ok || !sessionId) {
115
282
  // Bloco B3: rate limit nao e "runtime indisponivel". Ele tem horario de reset, e por
116
283
  // isso volta tipado daqui para virar pedido na fila duravel em vez de falha seca.
117
- const rateLimit = parseRateLimit(saida);
284
+ // I-33 (D1): falha da CONTA vem antes, porque e mais especifica e pede outro perfil.
285
+ // N6 (P8): sem perfil nada muda; o esgotamento que a leitura da baseline ja lia como rate limit
286
+ // ("Claude AI usage limit reached|<epoch>") segue direto para a fila do B3, como antes da I-33.
287
+ const lida = parseFalhaDeConta(saida);
288
+ const legado = parseRateLimit(saida);
289
+ const falhaDeConta = lida?.motivo === 'runtime.quota-exhausted' && !pedido.perfil && legado ? null : lida;
290
+ const rateLimit = falhaDeConta ? null : legado;
118
291
  return {
119
292
  ok: false,
120
293
  comando,
@@ -123,13 +296,18 @@ function despachar(pedido) {
123
296
  stdout: r.stdout,
124
297
  stderr: r.stderr,
125
298
  rateLimit,
126
- erro: rateLimit
127
- ? `limite de uso do runtime atingido: ${rateLimit.trecho}`
128
- : `despacho falhou (code ${r.code})`,
299
+ falhaDeConta,
300
+ erro: falhaDeConta
301
+ ? `${falhaDeConta.motivo}: ${falhaDeConta.trecho}`
302
+ : rateLimit
303
+ ? `limite de uso do runtime atingido: ${rateLimit.trecho}`
304
+ : `despacho falhou (code ${r.code})`,
129
305
  };
130
306
  }
131
- const achada = acharSessao(sessionId);
132
- const verificada = achada !== null || acharSessao(pedido.nome) !== null;
307
+ // I-33 (D4): a reverificacao consulta a MESMA conta que despachou.
308
+ const sessoes = listarSessoes(undefined, true, ambiente);
309
+ const achada = acharSessao(sessionId, sessoes);
310
+ const verificada = achada !== null || acharSessao(pedido.nome, sessoes) !== null;
133
311
  return {
134
312
  ok: true,
135
313
  comando,
@@ -177,31 +355,75 @@ function casarIso(texto) {
177
355
  return null;
178
356
  return { resetEm: data.toISOString(), trecho: m[0].trim() };
179
357
  }
358
+ /** N5: uma parte da duracao (`4 days`, `3h`, `20m`, `1.5s`, `20ms`), com a unidade fechada sem exigir `\b`. */
359
+ const PARTE_DA_DURACAO = /(\d+(?:\.\d+)?)\s*(milliseconds?|ms|days?|d|hours?|hrs?|h|minutes?|mins?|m|seconds?|secs?|s)(?![a-z])/gi;
360
+ function segundosDaUnidade(unidade) {
361
+ const u = unidade.toLowerCase();
362
+ if (u === 'ms' || u.startsWith('milli'))
363
+ return 0.001;
364
+ if (u.startsWith('d'))
365
+ return 86400;
366
+ if (u.startsWith('h'))
367
+ return 3600;
368
+ if (u.startsWith('m'))
369
+ return 60;
370
+ return 1;
371
+ }
180
372
  function casarDuracao(texto, agoraMs) {
181
373
  const m = texto.match(
182
- // A unidade aceita plural (`25 minutes`): fechar em `\b` sem o `s?` fazia o padrao
183
- // inteiro falhar justamente na forma que o runtime mais usa.
184
- /(?:try again|retry|reset[a-z]*|available|wait)[^\n]{0,20}?\bin\b\s*(\d+)\s*(seconds?|secs?|minutes?|mins?|hours?|hrs?|s|m|h)\b|retry[- ]after[:\s]+(\d+)/i);
374
+ // A unidade aceita plural (`25 minutes`) e, desde o N5, dias e partes compostas nas formas reais
375
+ // dos runtimes: `4 days 3 hours`, `2d 5h 30m` (Claude Code), `1m30s` e `20ms` (API da OpenAI).
376
+ /(?:try again|retry|reset[a-z]*|available|wait)[^\n]{0,20}?\bin\b\s*((?:\d+(?:\.\d+)?\s*(?:milliseconds?|ms|days?|d|hours?|hrs?|h|minutes?|mins?|m|seconds?|secs?|s)(?![a-z])[\s,]*(?:and\s+)?)+)|retry[- ]after[:\s]+(\d+)/i);
185
377
  if (!m)
186
378
  return null;
187
- if (m[3] !== undefined) {
379
+ if (m[2] !== undefined) {
188
380
  // `Retry-After: 3600` e sempre em segundos.
189
381
  return {
190
- resetEm: new Date(agoraMs + Number(m[3]) * 1000).toISOString(),
382
+ resetEm: new Date(agoraMs + Number(m[2]) * 1000).toISOString(),
191
383
  trecho: m[0].trim(),
192
384
  };
193
385
  }
194
- const valor = Number(m[1]);
195
- const unidade = (m[2] ?? '').toLowerCase();
196
- const segundos = unidade.startsWith('h')
197
- ? valor * 3600
198
- : unidade.startsWith('m')
199
- ? valor * 60
200
- : valor;
201
- return { resetEm: new Date(agoraMs + segundos * 1000).toISOString(), trecho: m[0].trim() };
386
+ let segundos = 0;
387
+ for (const parte of m[1].matchAll(PARTE_DA_DURACAO))
388
+ segundos += Number(parte[1]) * segundosDaUnidade(parte[2]);
389
+ return { resetEm: new Date(agoraMs + Math.round(segundos * 1000)).toISOString(), trecho: m[0].trim().replace(/[\s,]+$/, '') };
390
+ }
391
+ const MESES = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
392
+ /**
393
+ * N5: data e hora de relogio local nas formas reais. Codex: `or try again at Sep 22nd, 2026 3:05 PM`
394
+ * (`%b %-d` com sufixo ordinal, `, %Y %-I:%M %p`). Claude Code, quando o reset passa de 24 h:
395
+ * `resets Sep 22, 3pm (<nome IANA do fuso>)` e, em outro ano, `resets Sep 22, 2027, 3:05pm`. Sem ano,
396
+ * vale o ano corrente, ou o seguinte quando a data ja ficou mais de um dia para tras. O fuso entre
397
+ * parenteses nao entra na regex nem no calculo (a hora e lida no relogio desta maquina), e por isso
398
+ * fica como placeholder: o nome literal de fuso mora so em `core/src/horario.ts` (I-35).
399
+ */
400
+ function casarData(texto, agoraMs) {
401
+ const m = texto.match(/(?:try again at|reset[a-z]*(?:\s+at)?)\s+(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\.?\s+(\d{1,2})(?:st|nd|rd|th)?,?\s*(?:(\d{4}),?\s*)?(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*([ap]\.?m\.?)?(?![a-z0-9])/i);
402
+ if (!m)
403
+ return null;
404
+ const mes = MESES.indexOf(m[1].toLowerCase());
405
+ const dia = Number(m[2]);
406
+ let hora = Number(m[4]);
407
+ const minuto = Number(m[5] ?? '0');
408
+ const sufixo = (m[6] ?? '').toLowerCase().replace(/\./g, '');
409
+ if (sufixo === 'pm' && hora < 12)
410
+ hora += 12;
411
+ if (sufixo === 'am' && hora === 12)
412
+ hora = 0;
413
+ if (dia < 1 || dia > 31 || hora > 23 || minuto > 59)
414
+ return null;
415
+ const base = new Date(agoraMs);
416
+ const ano = m[3] !== undefined ? Number(m[3]) : base.getFullYear();
417
+ const alvo = new Date(ano, mes, dia, hora, minuto, 0, 0);
418
+ if (alvo.getMonth() !== mes)
419
+ return null;
420
+ if (m[3] === undefined && alvo.getTime() < agoraMs - 24 * 3600 * 1000)
421
+ alvo.setFullYear(ano + 1);
422
+ return { resetEm: alvo.toISOString(), trecho: m[0].trim() };
202
423
  }
203
424
  function casarRelogio(texto, agoraMs) {
204
- const m = texto.match(/reset[a-z]*\s*(?:at|em|as)?\s*(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\b/i);
425
+ // N5: o codex diz so a hora quando o reset e no mesmo dia (`or try again at 3:05 PM`).
426
+ const m = texto.match(/(?:reset[a-z]*\s*(?:at|em|as)?|try again at)\s*(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\b/i);
205
427
  if (!m)
206
428
  return null;
207
429
  let hora = Number(m[1]);
@@ -239,6 +461,9 @@ function parseRateLimit(saida, agoraMs = Date.now()) {
239
461
  const iso = casarIso(texto);
240
462
  if (iso)
241
463
  return { resetEm: iso.resetEm, fonte: 'iso', trecho: iso.trecho };
464
+ const data = casarData(texto, agoraMs);
465
+ if (data)
466
+ return { resetEm: data.resetEm, fonte: 'relogio', trecho: data.trecho };
242
467
  const duracao = casarDuracao(texto, agoraMs);
243
468
  if (duracao)
244
469
  return { resetEm: duracao.resetEm, fonte: 'duracao', trecho: duracao.trecho };
@@ -248,6 +473,178 @@ function parseRateLimit(saida, agoraMs = Date.now()) {
248
473
  const linha = texto.split('\n').find((l) => frase.test(l)) ?? texto;
249
474
  return { resetEm: null, fonte: 'sem-horario', trecho: linha.trim().slice(0, 200) };
250
475
  }
476
+ /**
477
+ * I-33 (D1): falha da CONTA na saida real do runtime, reconhecida por frase, nunca por codigo de
478
+ * saida. Auth ausente vem primeiro: sem login, nada mais da saida importa. Cota esgotada usa os
479
+ * mesmos padroes de horario do rate limit; sem horario, `resetEm` fica `null` e o prazo e da
480
+ * janela padrao do manifesto, declarado como estimado por quem marca o perfil. O que e cota
481
+ * esgotada e o que e rate limit comum sai do criterio unico da D16 (`naturezaDoLimite`).
482
+ */
483
+ const FRASES_DE_AUTH_AUSENTE = [
484
+ /\bnot logged in\b/i,
485
+ /please run \/login/i,
486
+ /oauth token (?:has )?expired/i,
487
+ /\bauthentication_error\b/i,
488
+ // D15 (A6): o erro real do Claude Code quando o refresh do OAuth falha (codigo e frase).
489
+ /\bauthentication_failed\b/i,
490
+ /oauth session expired/i,
491
+ /\btoken_expired\b/i,
492
+ /refresh[_ ]token[^\n]{0,40}\b(?:expired|revoked|invalid|reused)\b/i,
493
+ /\b401\b[^\n]{0,40}unauthori[sz]ed|unauthori[sz]ed[^\n]{0,40}\b401\b/i,
494
+ ];
495
+ /**
496
+ * A11: mencao ampla ao comando de login do codex. Aparece tambem em dica e em texto de ajuda, entao
497
+ * so vale como auth ausente quando o mesmo texto nao traz frase de cota: a cota, mais especifica, vence.
498
+ */
499
+ const FRASES_AMPLAS_DE_AUTH = [/\bcodex login\b/i];
500
+ /**
501
+ * I-33 (D16): CRITERIO UNICO entre ESGOTAMENTO da conta e RATE LIMIT comum. Decisao do dono em
502
+ * 19/09/2026 (opcao a do A3 do CHECK aa279e17): esgotamento de cota, credito ou limite do plano
503
+ * tira o perfil do rodizio e o MESMO prompt pode seguir no proximo perfil ativo do mesmo runtime;
504
+ * rate limit comum de curta janela NUNCA troca de perfil, espera a janela. Todo classificador de
505
+ * falha da conta passa por aqui: despacho claude-bg e codex, transcricao do Claude Code e erro
506
+ * terminal do turno codex (`parseFalhaDeConta`). Regras, na ordem, pela frase do runtime:
507
+ *
508
+ * 1. `esgotamento-explicito`: o provedor diz que a conta ficou sem cota, credito ou saldo
509
+ * (`usage_limit_exceeded`, `insufficient_quota`, "out of credits", "spend cap"). Esgotamento,
510
+ * com ou sem prazo, mesmo que a resposta venha como HTTP 429.
511
+ * 2. `janela-curta`: o provedor diz que o limite e temporario e nao e a cota ("not your usage
512
+ * limit", "Request rejected (429)", "Too Many Requests", `rate_limit_exceeded`,
513
+ * `rate_limit_error`, overloaded, 529, requisicoes ou tokens por minuto). Rate limit.
514
+ * 3. `limite-do-plano`: frase da janela da assinatura ("usage limit reached", "You've hit your
515
+ * ... limit", "You've reached your ... limit", "plan limit"). Esgotamento quando o prazo dito
516
+ * esta a `JANELA_CURTA_MAX_MS` ou mais, ou quando o runtime nao diz prazo futuro (a janela do
517
+ * plano e de horas ou dias por construcao); rate limit quando o prazo dito e mais curto.
518
+ * 4. `generico`: qualquer outro sinal de limite reconhecido por `parseRateLimit` (`429`, "rate
519
+ * limit", `rate_limit`, "too many requests", "quota exceeded"). Rate limit: na duvida, nao troca
520
+ * de perfil. O Retry-After so conta junto de um desses sinais, como prazo.
521
+ */
522
+ exports.JANELA_CURTA_MAX_MS = 15 * 60 * 1000;
523
+ const FRASES_DE_ESGOTAMENTO = [
524
+ /\busage_limit_exceeded\b/i,
525
+ /\busage_limit_reached\b/i,
526
+ /\bout of credits\b/i,
527
+ /\binsufficient_quota\b/i,
528
+ /exceeded your current quota/i,
529
+ /credit balance is too low/i,
530
+ // Codigo de erro de API do Claude Code quando a conta fica sem credito (I-33, D12).
531
+ /\bbilling_error\b/i,
532
+ /\bspend (?:cap|limit)\b/i,
533
+ /\bworkspace credit limit\b/i,
534
+ /\bout of extra usage\b/i,
535
+ /\brequires usage credits\b/i,
536
+ ];
537
+ const FRASES_DE_JANELA_CURTA = [
538
+ /not your usage limit/i,
539
+ /temporarily limiting requests/i,
540
+ /request rejected \(429\)/i,
541
+ /temporary capacity issue/i,
542
+ /too many requests/i,
543
+ /\brate_limit_exceeded\b/i,
544
+ /\brate_limit_error\b/i,
545
+ /\bserver_overloaded\b/i,
546
+ /\boverloaded(?:_error)?\b/i,
547
+ /\b529\b/,
548
+ /experiencing high load/i,
549
+ /\b(?:requests|tokens) per min(?:ute)?\b/i,
550
+ ];
551
+ const FRASES_DO_LIMITE_DO_PLANO = [
552
+ /usage limits? (?:reached|exceeded)/i,
553
+ /\bhit your (?:[a-z0-9-]+ )?(?:usage )?limit\b/i,
554
+ /\breached your (?:[a-z0-9-]+ )?(?:usage )?limit\b/i,
555
+ /\bplan limit\b/i,
556
+ /out of (?:usage|weekly|session) limit/i,
557
+ ];
558
+ /** N4: a linha da frase, limitada a 200 caracteres; em linha longa, a janela comeca perto da frase. */
559
+ function trechoDa(texto, frase) {
560
+ const linha = (texto.split('\n').find((l) => frase.test(l)) ?? texto).trim();
561
+ if (linha.length <= 200)
562
+ return linha;
563
+ const inicio = Math.max(0, (linha.match(frase)?.index ?? 0) - 40);
564
+ return linha.slice(inicio, inicio + 200).trim();
565
+ }
566
+ /** Horario de volta dito pelo runtime, do padrao mais preciso para o menos; nenhum inventado. */
567
+ function resetDito(texto, agoraMs) {
568
+ // N5: data e hora de relogio local (`Sep 22nd, 2026 3:05 PM`) e relogio; a fonte das duas e `relogio`.
569
+ const padroes = [['epoch', casarEpoch(texto)], ['iso', casarIso(texto)], ['relogio', casarData(texto, agoraMs)],
570
+ ['duracao', casarDuracao(texto, agoraMs)], ['relogio', casarRelogio(texto, agoraMs)]];
571
+ const achado = padroes.find(([, r]) => r !== null);
572
+ return achado ? { resetEm: achado[1].resetEm, fonte: achado[0] } : { resetEm: null, fonte: 'sem-horario' };
573
+ }
574
+ /** D16: a natureza do limite na saida do runtime, pelas quatro regras acima; `null` sem sinal de limite. */
575
+ function naturezaDoLimite(saida, agoraMs = Date.now()) {
576
+ const texto = saida ?? '';
577
+ const explicito = FRASES_DE_ESGOTAMENTO.find((r) => r.test(texto));
578
+ if (explicito)
579
+ return { natureza: 'esgotamento', regra: 'esgotamento-explicito', ...resetDito(texto, agoraMs), trecho: trechoDa(texto, explicito) };
580
+ const curta = FRASES_DE_JANELA_CURTA.find((r) => r.test(texto));
581
+ if (curta)
582
+ return { natureza: 'rate-limit', regra: 'janela-curta', ...resetDito(texto, agoraMs), trecho: trechoDa(texto, curta) };
583
+ const plano = FRASES_DO_LIMITE_DO_PLANO.find((r) => r.test(texto));
584
+ if (plano) {
585
+ const reset = resetDito(texto, agoraMs);
586
+ const horizonte = reset.resetEm === null ? null : Date.parse(reset.resetEm) - agoraMs;
587
+ const curto = horizonte !== null && horizonte > 0 && horizonte < exports.JANELA_CURTA_MAX_MS;
588
+ return { natureza: curto ? 'rate-limit' : 'esgotamento', regra: 'limite-do-plano', ...reset, trecho: trechoDa(texto, plano) };
589
+ }
590
+ const generico = parseRateLimit(texto, agoraMs);
591
+ return generico ? { natureza: 'rate-limit', regra: 'generico', resetEm: generico.resetEm, fonte: generico.fonte, trecho: generico.trecho }
592
+ : null;
593
+ }
594
+ function parseFalhaDeConta(saida, agoraMs = Date.now()) {
595
+ const texto = saida ?? '';
596
+ const auth = FRASES_DE_AUTH_AUSENTE.find((r) => r.test(texto));
597
+ if (auth)
598
+ return { motivo: 'runtime.auth-missing', resetEm: null, fonte: 'sem-horario', trecho: trechoDa(texto, auth) };
599
+ const limite = naturezaDoLimite(texto, agoraMs);
600
+ if (limite?.natureza === 'esgotamento') {
601
+ return { motivo: 'runtime.quota-exhausted', resetEm: limite.resetEm, fonte: limite.fonte, trecho: limite.trecho };
602
+ }
603
+ const ampla = FRASES_AMPLAS_DE_AUTH.find((r) => r.test(texto));
604
+ return ampla ? { motivo: 'runtime.auth-missing', resetEm: null, fonte: 'sem-horario', trecho: trechoDa(texto, ampla) } : null;
605
+ }
606
+ /** Campo textual do `auth status`, sem controle e curto, para o detalhe legivel. */
607
+ const campoDeAuth = (v) => typeof v === 'string' ? v.replace(/[^a-zA-Z0-9_.-]/g, '').slice(0, 32) : '';
608
+ /**
609
+ * I-33 (D7, D13): `claude auth status --json` com o env do perfil. Aprova so login de ASSINATURA:
610
+ * `loggedIn: true`, `authMethod` `claude.ai`, sem `apiKeySource` e com provider `firstParty`.
611
+ * API key, `api_key_helper`, Console ou provider de nuvem cobram por token: `pago`, nunca despacha.
612
+ * O `configDirectory` informado precisa ser o do perfil, prova de que o env foi aplicado. O `ork`
613
+ * nunca abre nada dentro do diretorio: quem responde e o CLI.
614
+ */
615
+ function conferirAuth(perfil) {
616
+ let ambiente;
617
+ try {
618
+ ambiente = ambienteDoPerfil(perfil);
619
+ }
620
+ catch (e) {
621
+ return { ok: false, detalhe: e.message };
622
+ }
623
+ const r = exec('claude', ['auth', 'status', '--json'], undefined, 30000, ambiente);
624
+ // A14: sem codigo de saida (timeout, sinal ou binario ausente) ou sem JSON, a conferencia e inconclusiva.
625
+ if (r.code === -1)
626
+ return { ok: false, transitorio: true, detalhe: 'claude auth status sem resposta (timeout, sinal ou binario ausente)' };
627
+ let dados;
628
+ try {
629
+ dados = JSON.parse(r.stdout);
630
+ }
631
+ catch {
632
+ return { ok: false, transitorio: true, detalhe: `claude auth status sem JSON legivel (codigo ${r.code})` };
633
+ }
634
+ if (!dados || typeof dados !== 'object')
635
+ return { ok: false, transitorio: true, detalhe: 'claude auth status sem JSON legivel' };
636
+ if (perfil && typeof dados.configDirectory === 'string' && dados.configDirectory !== perfil.configDir)
637
+ return { ok: false, detalhe: 'claude auth status respondeu por outro diretorio de configuracao; perfil nao aplicado' };
638
+ if (dados.loggedIn !== true)
639
+ return { ok: false, detalhe: 'claude auth status: loggedIn false' };
640
+ const metodo = campoDeAuth(dados.authMethod), fonte = campoDeAuth(dados.apiKeySource), provedor = campoDeAuth(dados.apiProvider);
641
+ if (metodo !== 'claude.ai' || fonte !== '' || (provedor !== '' && provedor !== 'firstParty')) {
642
+ const partes = [`authMethod ${metodo || 'ausente'}`, ...(fonte ? [`apiKeySource ${fonte}`] : []),
643
+ ...(provedor && provedor !== 'firstParty' ? [`apiProvider ${provedor}`] : [])];
644
+ return { ok: false, pago: true, detalhe: `claude auth status: provider pago (${partes.join(', ')}); so a assinatura claude.ai despacha` };
645
+ }
646
+ return { ok: true, detalhe: `claude auth status: loggedIn (${metodo})` };
647
+ }
251
648
  /** Comando de anexar a sessao (precisa de TTY, entao o CLI imprime em vez de executar). */
252
649
  function comandoAttach(sessionId) {
253
650
  return `claude attach ${sessionId}`;
@@ -259,13 +656,58 @@ function comandoAttach(sessionId) {
259
656
  * nao por quebra de linha. Por isso o movimento vira quebra ANTES de as demais
260
657
  * sequencias ANSI serem removidas, senao a saida inteira colapsa numa linha so.
261
658
  */
659
+ /**
660
+ * Marcador temporario de um CHA (`ESC[<n>G`), resolvido em espacos no fim da limpeza.
661
+ *
662
+ * Fica fora da faixa de controle que a limpeza apaga (`\u0000-\u001f`), entao ele
663
+ * atravessa os passos seguintes intacto.
664
+ */
665
+ const MARCA_DE_COLUNA = '\u0091';
666
+ const MARCA_CASADA = new RegExp(`${MARCA_DE_COLUNA}(\\d+)${MARCA_DE_COLUNA}`, 'g');
667
+ /**
668
+ * Repoe os espacos que o CHA desenhava, linha a linha.
669
+ *
670
+ * O terminal anda ate a coluna pedida preenchendo com branco; quem le o log so ve a
671
+ * sequencia sumir. Quando a coluna alvo ja passou (o runtime reescrevendo o que estava
672
+ * na tela), um unico espaco separa os pedacos: e melhor errar o alinhamento do que
673
+ * colar duas palavras que nunca foram uma so.
674
+ */
675
+ function resolverColunas(texto) {
676
+ if (!texto.includes(MARCA_DE_COLUNA))
677
+ return texto;
678
+ return texto
679
+ .split('\n')
680
+ .map((linha) => {
681
+ // `split` com grupo de captura intercala texto e coluna: [txt, col, txt, col, txt].
682
+ const partes = linha.split(MARCA_CASADA);
683
+ let saida = partes[0] ?? '';
684
+ for (let i = 1; i < partes.length; i += 2) {
685
+ const coluna = Number(partes[i]);
686
+ if (Number.isFinite(coluna) && coluna - 1 > saida.length) {
687
+ saida += ' '.repeat(coluna - 1 - saida.length);
688
+ }
689
+ else if (saida !== '' && !saida.endsWith(' ')) {
690
+ saida += ' ';
691
+ }
692
+ saida += partes[i + 1] ?? '';
693
+ }
694
+ return saida;
695
+ })
696
+ .join('\n');
697
+ }
262
698
  function limparAnsi(bruto) {
263
- return bruto
699
+ return resolverColunas(bruto
264
700
  .replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g, '')
265
701
  .replace(/\u001b\[[0-9;]*[HfABEd]/g, '\n')
702
+ // O CHA (`ESC[<n>G`) posiciona o cursor numa coluna em vez de escrever espacos, e
703
+ // o `claude logs` desenha a tela INTEIRA assim. Engoli-lo junto com o resto das
704
+ // sequencias colava as palavras (`2newMCPserversfoundinthisproject`), o que
705
+ // apagava a pergunta que o radar poe na frente do humano e matava todo o
706
+ // reconhecimento por frase que separa credencial de permissao.
707
+ .replace(/\u001b\[([0-9]*)G/g, (_m, n) => `${MARCA_DE_COLUNA}${n || '1'}${MARCA_DE_COLUNA}`)
266
708
  .replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, '')
267
709
  .replace(/\u001b[()][A-Za-z0-9]/g, '')
268
- .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, '')
710
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, ''))
269
711
  .replace(/[ \t]+$/gm, '')
270
712
  .replace(/\n{3,}/g, '\n\n');
271
713
  }
@@ -276,19 +718,26 @@ function limparAnsi(bruto) {
276
718
  * nao o UUID completo: passar o UUID responde "No job matching". Por isso a chave e
277
719
  * resolvida antes, e a saida (dump de tela com ANSI) e limpa aqui.
278
720
  */
279
- function logs(chave, linhas = 60) {
280
- const sessao = acharSessao(chave);
281
- const id = sessao?.id ?? (sessao?.sessionId ?? chave).slice(0, 8);
282
- const r = (0, util_1.exec)('claude', ['logs', id], undefined, 60000);
721
+ function logs(chave, linhas = 60, ambiente = (0, runtime_ambiente_1.ambienteDeAssinatura)()) {
722
+ const sessao = acharSessao(chave, listarSessoes(undefined, true, ambiente));
723
+ return logsDaSessao(sessao?.id ?? (sessao?.sessionId ?? chave).slice(0, 8), linhas, ambiente);
724
+ }
725
+ /**
726
+ * I-45: a tela de uma sessao cujo id curto o chamador ja tem. `logs` lista todas as sessoes antes
727
+ * de ler (`claude agents --json --all`, 57 s nesta VPS em 25/09/2026) so para achar esse id; o radar
728
+ * ja tem a lista na mao, e pagar a listagem de novo por sessao parada era o grosso da varredura.
729
+ */
730
+ function logsDaSessao(id, linhas = 60, ambiente = (0, runtime_ambiente_1.ambienteDeAssinatura)()) {
731
+ const r = exec('claude', ['logs', id], undefined, 60000, ambiente);
283
732
  const bruto = limparAnsi(r.ok ? r.stdout : r.stdout + r.stderr);
284
733
  const uteis = bruto.split('\n').filter((l) => l.trim() !== '');
285
734
  return { ok: r.ok, texto: uteis.slice(-linhas).join('\n') };
286
735
  }
287
736
  /** Para uma sessao em background (o `claude stop` tambem trabalha com o id curto). */
288
- function parar(chave) {
289
- const sessao = acharSessao(chave);
737
+ function parar(chave, ambiente = (0, runtime_ambiente_1.ambienteDeAssinatura)()) {
738
+ const sessao = acharSessao(chave, listarSessoes(undefined, true, ambiente));
290
739
  const id = sessao?.id ?? (sessao?.sessionId ?? chave).slice(0, 8);
291
- const r = (0, util_1.exec)('claude', ['stop', id], undefined, 60000);
740
+ const r = exec('claude', ['stop', id], undefined, 60000, ambiente);
292
741
  return { ok: r.ok, texto: limparAnsi(r.stdout + r.stderr).trim() };
293
742
  }
294
743
  /**