@orkastery/cli 0.2.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +86 -65
  3. package/adapters/README.md +30 -15
  4. package/adapters/claude-code/.claude-plugin/plugin.json +11 -5
  5. package/adapters/claude-code/README.md +110 -8
  6. package/adapters/claude-code/commands/check.md +4 -3
  7. package/adapters/claude-code/commands/goal.md +3 -2
  8. package/adapters/claude-code/commands/master.md +3 -2
  9. package/adapters/claude-code/commands/onboarding.md +21 -0
  10. package/adapters/claude-code/commands/ork.md +99 -27
  11. package/adapters/claude-code/hooks/hooks.json +6 -1
  12. package/adapters/claude-code/hooks/ork-guard.js +1 -1
  13. package/adapters/claude-code/hooks/ork-sensor.js +66 -0
  14. package/adapters/codex/skills/ork/SKILL.md +57 -0
  15. package/adapters/hermes/README.md +161 -0
  16. package/adapters/hermes/bin/ork-abrir-thread.sh +2 -0
  17. package/adapters/hermes/bin/ork-brain.sh +4 -0
  18. package/adapters/hermes/bin/ork-hitl-answer.py +82 -0
  19. package/adapters/hermes/bin/ork-maestro.sh +6 -0
  20. package/adapters/hermes/bin/ork-master-enviar.py +84 -0
  21. package/adapters/hermes/bin/ork-pulse-enviar.py +47 -0
  22. package/adapters/hermes/hermes.plugin.json +24 -4
  23. package/adapters/hermes/hitl-ingress/__init__.py +356 -0
  24. package/adapters/hermes/hitl-ingress/plugin.yaml +4 -0
  25. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +75 -22
  26. package/adapters/openclaw/README.md +136 -27
  27. package/adapters/openclaw/bin/ork-brain.sh +4 -0
  28. package/adapters/openclaw/construir.sh +19 -0
  29. package/adapters/openclaw/dist/hitl-ingress.js +199 -0
  30. package/adapters/openclaw/dist/index.js +437 -0
  31. package/adapters/openclaw/openclaw.plugin.json +39 -134
  32. package/adapters/openclaw/package.json +26 -0
  33. package/adapters/openclaw/src/hitl-ingress.ts +180 -0
  34. package/adapters/openclaw/src/index.ts +460 -0
  35. package/adapters/openclaw/src/tipos-openclaw.d.ts +58 -0
  36. package/adapters/openclaw/tsconfig.json +15 -0
  37. package/assets/docs/markdownlint-cli2.jsonc +22 -0
  38. package/assets/docs/padroes/documentacao-de-produto.md +133 -0
  39. package/assets/docs/padroes/roadmap-de-produto.md +98 -0
  40. package/assets/docs/produto/README.md +14 -0
  41. package/assets/docs/produto/_modelo-feature.md +59 -0
  42. package/assets/docs/roadmap/README.md +14 -0
  43. package/assets/docs/roadmap/_modelo-item.md +73 -0
  44. package/assets/orkmind-native-schema.json +32 -0
  45. package/assets/orkmind_bridge.py +357 -0
  46. package/assets/orkmind_fixture.py +67 -0
  47. package/assets/orkmind_prospective.py +138 -0
  48. package/assets/reference-tariffs-i07.json +23 -0
  49. package/dist/adapters/claude-bg.js +488 -39
  50. package/dist/adapters/codex-controller-sensor.js +449 -0
  51. package/dist/adapters/codex-controller-worker.js +361 -0
  52. package/dist/adapters/codex-controller.js +252 -0
  53. package/dist/adapters/codex-events.js +205 -0
  54. package/dist/adapters/codex-question.js +26 -0
  55. package/dist/adapters/codex-runner.js +133 -0
  56. package/dist/adapters/codex.js +394 -0
  57. package/dist/agents-md.js +84 -0
  58. package/dist/auditoria.js +12 -9
  59. package/dist/auditrun.js +5 -4
  60. package/dist/board.js +174 -23
  61. package/dist/branch-de-estado.js +136 -0
  62. package/dist/canarios-hitl.js +177 -0
  63. package/dist/canarios-i43.js +543 -0
  64. package/dist/canarios-pulse.js +147 -0
  65. package/dist/canarios-sensores.js +129 -0
  66. package/dist/canarios.js +111 -2
  67. package/dist/catalogo.js +9 -0
  68. package/dist/ci.js +223 -0
  69. package/dist/ciclos.js +2 -1
  70. package/dist/claim-lint.js +64 -0
  71. package/dist/claims.js +60 -0
  72. package/dist/company-brain-capture.js +195 -0
  73. package/dist/company-brain-cli.js +123 -0
  74. package/dist/company-brain-client.js +61 -0
  75. package/dist/company-brain-contract.js +166 -0
  76. package/dist/company-brain-journal.js +192 -0
  77. package/dist/company-brain-mcp.js +33 -0
  78. package/dist/company-brain-migration.js +60 -0
  79. package/dist/company-brain-source.js +177 -0
  80. package/dist/company-brain-worker.js +18 -0
  81. package/dist/conducao-texto.js +61 -0
  82. package/dist/conducao.js +876 -0
  83. package/dist/contrato-publico.js +39 -0
  84. package/dist/creation-operation-store.js +251 -0
  85. package/dist/creation-operation.js +148 -0
  86. package/dist/decisao-autonoma.js +183 -0
  87. package/dist/delegation.js +79 -0
  88. package/dist/demo.js +120 -0
  89. package/dist/docs.js +828 -0
  90. package/dist/doctor.js +184 -25
  91. package/dist/entrega-pr.js +117 -0
  92. package/dist/escopo-escrita.js +71 -0
  93. package/dist/estado-thread.js +222 -0
  94. package/dist/evalrunner.js +12 -0
  95. package/dist/fabrica-estado.js +329 -0
  96. package/dist/fabrica-publicar.js +76 -0
  97. package/dist/fix.js +22 -1
  98. package/dist/gates.js +59 -11
  99. package/dist/handoff.js +39 -23
  100. package/dist/hitl-canais.js +333 -0
  101. package/dist/hitl-classificacao.js +131 -0
  102. package/dist/hitl-contract.js +465 -0
  103. package/dist/hitl-estado.js +136 -0
  104. package/dist/hitl-gates.js +554 -0
  105. package/dist/hitl-ingress-receipt.js +382 -0
  106. package/dist/hitl-local-atestado.js +57 -0
  107. package/dist/hitl-local-receipt.js +328 -0
  108. package/dist/hitl-local.js +143 -0
  109. package/dist/hitl-lock.js +139 -0
  110. package/dist/hitl-lote.js +223 -0
  111. package/dist/hitl-native-offer.js +97 -0
  112. package/dist/hitl-native.js +65 -0
  113. package/dist/hitl-presentation.js +209 -0
  114. package/dist/hitl-public-receipt.js +176 -0
  115. package/dist/hitl-resumo.js +209 -0
  116. package/dist/hitl-sessions.js +475 -0
  117. package/dist/hitl.js +711 -0
  118. package/dist/horario.js +269 -0
  119. package/dist/hosts.js +198 -22
  120. package/dist/index.js +1741 -94
  121. package/dist/indice.js +99 -0
  122. package/dist/init.js +28 -3
  123. package/dist/integracoes-locais.js +17 -0
  124. package/dist/leases.js +65 -21
  125. package/dist/ledger-stats.js +272 -0
  126. package/dist/ledger.js +109 -2
  127. package/dist/licoes.js +221 -0
  128. package/dist/liveness.js +218 -0
  129. package/dist/maestro-actions.js +51 -0
  130. package/dist/maestro-authority.js +152 -0
  131. package/dist/maestro-cli.js +97 -0
  132. package/dist/maestro-contract.js +55 -0
  133. package/dist/maestro-discovery.js +132 -0
  134. package/dist/maestro-runtime.js +268 -0
  135. package/dist/maestro-snapshot.js +86 -0
  136. package/dist/maestro-sources.js +224 -0
  137. package/dist/manifest.js +165 -6
  138. package/dist/maquina.js +102 -0
  139. package/dist/master-audit.js +101 -0
  140. package/dist/master-batch.js +43 -0
  141. package/dist/master-digest.js +149 -0
  142. package/dist/master-migracao.js +155 -0
  143. package/dist/master.js +361 -77
  144. package/dist/mcp-artifacts.js +234 -0
  145. package/dist/mcp-git.js +433 -0
  146. package/dist/mcp-install.js +310 -0
  147. package/dist/mcp-maestro.js +25 -0
  148. package/dist/mcp-server.js +469 -0
  149. package/dist/mcp-ship.js +436 -0
  150. package/dist/mcp-verify.js +173 -0
  151. package/dist/memoria-humana.js +207 -0
  152. package/dist/memoria.js +314 -78
  153. package/dist/memory-migration.js +279 -0
  154. package/dist/memory-prospective.js +148 -0
  155. package/dist/modos-migracao.js +191 -0
  156. package/dist/modos.js +151 -15
  157. package/dist/monitor-lock.js +110 -0
  158. package/dist/objective.js +449 -0
  159. package/dist/ocupacao.js +267 -0
  160. package/dist/onboarding.js +303 -0
  161. package/dist/orkmind.js +389 -90
  162. package/dist/orquestracao.js +96 -69
  163. package/dist/phase.js +612 -151
  164. package/dist/playbook-capabilities.js +150 -0
  165. package/dist/playbook-contracts.js +188 -0
  166. package/dist/policies.js +72 -0
  167. package/dist/portfolio-context.js +61 -0
  168. package/dist/portfolio.js +180 -0
  169. package/dist/preflight.js +207 -0
  170. package/dist/process-audit.js +112 -0
  171. package/dist/project-state.js +93 -0
  172. package/dist/prompts.js +25 -5
  173. package/dist/prova-minima.js +112 -0
  174. package/dist/pulse-cadencia.js +163 -0
  175. package/dist/pulse-consentimento.js +260 -0
  176. package/dist/pulse-delivery.js +323 -0
  177. package/dist/pulse-resposta.js +542 -0
  178. package/dist/pulse.js +262 -0
  179. package/dist/ratelimit.js +35 -2
  180. package/dist/recall.js +80 -3
  181. package/dist/redacao-saida.js +45 -0
  182. package/dist/redacao-url.js +66 -0
  183. package/dist/retry.js +640 -118
  184. package/dist/roadmap-reservas.js +243 -0
  185. package/dist/runtime-ambiente.js +38 -0
  186. package/dist/runtime-context.js +215 -0
  187. package/dist/runtime-profiles.js +869 -0
  188. package/dist/runtimes.js +103 -0
  189. package/dist/sandbox.js +9 -0
  190. package/dist/session-events.js +203 -0
  191. package/dist/session-watcher-claude.js +637 -0
  192. package/dist/session-watcher.js +700 -0
  193. package/dist/sessoes-adopt.js +154 -0
  194. package/dist/sessoes-inventario.js +159 -0
  195. package/dist/sessoes.js +16 -35
  196. package/dist/setup.js +691 -0
  197. package/dist/ship.js +168 -19
  198. package/dist/slug.js +1 -1
  199. package/dist/thread-close.js +115 -0
  200. package/dist/thread.js +142 -28
  201. package/dist/tokens.js +1 -1
  202. package/dist/util.js +4 -2
  203. package/dist/verify-sandbox.js +265 -0
  204. package/dist/verify.js +270 -25
  205. package/dist/versao.js +60 -0
  206. package/dist/worktree.js +31 -2
  207. package/dist/write-activation.js +291 -0
  208. package/dist/yaml.js +5 -1
  209. package/eval/casos/master-metrics.json +1 -1
  210. package/eval/casos/onboarding.json +102 -0
  211. package/eval/casos/orkastery-bootstrap.json +37 -3
  212. package/eval/casos/scope-check-capability-map.json +45 -7
  213. package/eval/casos/ship-release.json +3 -3
  214. package/eval/fixtures/b0-slug-e-modos/caso.json +35 -22
  215. package/eval/fixtures/b2-master-log/caso.json +1 -1
  216. package/eval/fixtures/fx-adapter-editado-detectado/caso.json +19 -0
  217. package/eval/fixtures/fx-auto-quiet/caso.json +11 -0
  218. package/eval/fixtures/fx-blanket-approve/caso.json +10 -0
  219. package/eval/fixtures/fx-check-runtime-cruzado/caso.json +20 -0
  220. package/eval/fixtures/fx-codex-dry/caso.json +18 -0
  221. package/eval/fixtures/fx-donewhen-executavel/caso.json +15 -0
  222. package/eval/fixtures/fx-estado-dividido/caso.json +15 -0
  223. package/eval/fixtures/fx-fase-orfa/caso.json +116 -0
  224. package/eval/fixtures/fx-happy/caso.json +6 -4
  225. package/eval/fixtures/fx-hitl-latency/caso.json +20 -0
  226. package/eval/fixtures/fx-indice-reversao/caso.json +25 -0
  227. package/eval/fixtures/fx-listagem-abertas/caso.json +16 -0
  228. package/eval/fixtures/fx-maestro-bootstrap/caso.json +21 -0
  229. package/eval/fixtures/fx-modo-aposentado-escritor/caso.json +21 -0
  230. package/eval/fixtures/fx-modo-aposentado-leitor/caso.json +16 -0
  231. package/eval/fixtures/fx-objective-oscillation/caso.json +14 -0
  232. package/eval/fixtures/fx-omnicanal/caso.json +17 -0
  233. package/eval/fixtures/fx-sensores-runtime/caso.json +17 -0
  234. package/monitor/company-brain.cjs +17 -0
  235. package/monitor/pulse-scope.cjs +45 -0
  236. package/monitor/pulse.cron +18 -0
  237. package/monitor/varredura-pulse.sh +27 -0
  238. package/package.json +28 -7
  239. package/references/definition-of-done.md +2 -2
  240. package/schemas/claims.schema.json +25 -0
  241. package/schemas/company-brain.schema.json +1158 -0
  242. package/schemas/creation-operation.schema.json +365 -0
  243. package/schemas/maestro-snapshot.schema.json +2716 -0
  244. package/skills/README.md +3 -3
  245. package/skills/core/onboarding/SKILL.md +50 -0
  246. package/skills/core/orkastery-bootstrap/SKILL.md +95 -65
  247. package/skills/core/thread-state/SKILL.md +3 -1
  248. package/skills/governance/decision-triage/SKILL.md +4 -4
  249. package/skills/governance/narrative-guardian/SKILL.md +1 -1
  250. package/skills/governance/roadmap-keeper/SKILL.md +1 -1
  251. package/skills/governance/scope-check-capability-map/SKILL.md +17 -7
  252. package/skills/observability/thread-tracing/SKILL.md +1 -1
  253. package/skills/phases/check-quality/SKILL.md +2 -2
  254. package/skills/phases/goal-definition/SKILL.md +1 -1
  255. package/skills/phases/master-metrics/SKILL.md +9 -7
  256. package/skills/phases/plan-specification/SKILL.md +1 -1
  257. package/skills/phases/ship-release/SKILL.md +1 -1
@@ -0,0 +1,869 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.COLUNAS_DE_PERFIS = exports.POLITICA_PADRAO = exports.ARQUIVO_CONTAS = exports.CONTRATO_CONTAS = exports.ESTADOS_DE_PERFIL = exports.RUNTIMES_COM_PERFIL = exports.VARIAVEL_DO_PERFIL = exports.ARQUIVO_PERFIS = exports.CONTRATO_PERFIS = void 0;
37
+ exports.runtimeComPerfil = runtimeComPerfil;
38
+ exports.validarDiretorioDePerfil = validarDiretorioDePerfil;
39
+ exports.validarIdDePerfil = validarIdDePerfil;
40
+ exports.diretorioDoPerfil = diretorioDoPerfil;
41
+ exports.validarPerfilDeDespacho = validarPerfilDeDespacho;
42
+ exports.perfilDeDespacho = perfilDeDespacho;
43
+ exports.diretorioImplicito = diretorioImplicito;
44
+ exports.diretorioEfetivo = diretorioEfetivo;
45
+ exports.ambienteComPerfil = ambienteComPerfil;
46
+ exports.perfilDoRegistro = perfilDoRegistro;
47
+ exports.pastaPrivada = pastaPrivada;
48
+ exports.caminhoDoStore = caminhoDoStore;
49
+ exports.validarStore = validarStore;
50
+ exports.lerPerfis = lerPerfis;
51
+ exports.atualizarPerfis = atualizarPerfis;
52
+ exports.caminhoRealDoPerfil = caminhoRealDoPerfil;
53
+ exports.prepararDiretorioDoPerfil = prepararDiretorioDoPerfil;
54
+ exports.adicionarPerfil = adicionarPerfil;
55
+ exports.desativarPerfil = desativarPerfil;
56
+ exports.reativarPerfil = reativarPerfil;
57
+ exports.marcarFalhaDePerfil = marcarFalhaDePerfil;
58
+ exports.registrarConferenciaInconclusiva = registrarConferenciaInconclusiva;
59
+ exports.registrarUsoDePerfil = registrarUsoDePerfil;
60
+ exports.pastaDasContas = pastaDasContas;
61
+ exports.chaveDaConta = chaveDaConta;
62
+ exports.lerContasCompartilhadas = lerContasCompartilhadas;
63
+ exports.publicarEstadoDaConta = publicarEstadoDaConta;
64
+ exports.aplicarContasCompartilhadas = aplicarContasCompartilhadas;
65
+ exports.lerPerfisComContas = lerPerfisComContas;
66
+ exports.perfisDoRuntime = perfisDoRuntime;
67
+ exports.perfilDisponivel = perfilDisponivel;
68
+ exports.politicaDeRotacao = politicaDeRotacao;
69
+ exports.proximoPerfilDisponivel = proximoPerfilDisponivel;
70
+ exports.prazoDoRuntime = prazoDoRuntime;
71
+ exports.prazoDaFila = prazoDaFila;
72
+ exports.menorEsgotadoAte = menorEsgotadoAte;
73
+ exports.linhasDePerfis = linhasDePerfis;
74
+ exports.perfisComHorario = perfisComHorario;
75
+ /**
76
+ * I-33 (D3): perfis de conta por runtime, contrato `ork.runtime-profiles/v1`.
77
+ *
78
+ * Um perfil aponta o DIRETORIO onde o proprio CLI do runtime guarda configuracao e login
79
+ * (`CLAUDE_CONFIG_DIR` no claude-bg, `CODEX_HOME` no codex). O `ork` nunca le, copia nem
80
+ * migra o que o CLI grava la dentro: o login e feito pelo proprio CLI com o env do perfil, e
81
+ * o store guarda so identidade, diretorio e estado de uso. Nenhum segredo entra aqui.
82
+ *
83
+ * O arquivo mora em `<estado>/.orkastery/private/runtime-profiles.json` (diretorio 0700,
84
+ * arquivo 0600, dono igual ao uid do processo), com a mesma validacao do recibo local de
85
+ * HITL: modo exato, sem link simbolico, realpath conferido e identidade do diretorio
86
+ * reconferida depois de abrir o arquivo. Sem store, nao ha perfil: o despacho segue com o
87
+ * ambiente do processo, exatamente como antes (P8 do GOAL).
88
+ */
89
+ const fs = __importStar(require("node:fs"));
90
+ const os = __importStar(require("node:os"));
91
+ const path = __importStar(require("node:path"));
92
+ const node_crypto_1 = require("node:crypto");
93
+ const estado_thread_1 = require("./estado-thread");
94
+ const horario_1 = require("./horario");
95
+ exports.CONTRATO_PERFIS = 'ork.runtime-profiles/v1';
96
+ exports.ARQUIVO_PERFIS = 'runtime-profiles.json';
97
+ /** Runtimes homologados que aceitam perfil, e a variavel que cada um le. */
98
+ exports.VARIAVEL_DO_PERFIL = { 'claude-bg': 'CLAUDE_CONFIG_DIR', codex: 'CODEX_HOME' };
99
+ exports.RUNTIMES_COM_PERFIL = Object.keys(exports.VARIAVEL_DO_PERFIL);
100
+ /**
101
+ * I-33 (D13): `provider-pago` e o perfil cujo proprio CLI diz que o login e por API key,
102
+ * `api_key_helper`, Console ou provider de nuvem, e nao pela assinatura. Nunca recebe despacho;
103
+ * so volta ao rodizio quando `ork accounts check` confere login de assinatura.
104
+ */
105
+ exports.ESTADOS_DE_PERFIL = ['ativo', 'esgotado', 'sem-auth', 'provider-pago', 'desativado'];
106
+ const ID_DE_PERFIL = /^[a-z0-9][a-z0-9._-]{0,62}$/i;
107
+ const LIMITE_STORE_BYTES = 256 * 1024;
108
+ const LIMITE_DETALHE = 200;
109
+ const uid = () => {
110
+ const atual = process.getuid?.();
111
+ if (atual === undefined)
112
+ throw new Error('runtime.profiles.invalid: uid do processo indisponivel');
113
+ return atual;
114
+ };
115
+ function runtimeComPerfil(nome) {
116
+ return Object.prototype.hasOwnProperty.call(exports.VARIAVEL_DO_PERFIL, nome);
117
+ }
118
+ /** Caminho absoluto, normalizado, sem controle e sem ser a raiz: e so isso que o perfil aceita. */
119
+ function validarDiretorioDePerfil(dir) {
120
+ if (typeof dir !== 'string' || dir.length === 0 || dir.length > 1024 || /\p{Cc}/u.test(dir) ||
121
+ !path.isAbsolute(dir) || path.normalize(dir) !== dir || dir === '/' || dir.endsWith('/'))
122
+ throw new Error('runtime.profiles.invalid: diretorio do perfil precisa ser caminho absoluto normalizado');
123
+ return dir;
124
+ }
125
+ function validarIdDePerfil(id) {
126
+ if (typeof id !== 'string' || !ID_DE_PERFIL.test(id))
127
+ throw new Error('runtime.profiles.invalid: id do perfil aceita letras, digitos, ponto, hifen e sublinhado (ate 63)');
128
+ return id;
129
+ }
130
+ /** Diretorio do perfil, pelo campo do seu runtime. */
131
+ function diretorioDoPerfil(p) {
132
+ const dir = p.runtime === 'claude-bg' ? p.configDir : p.codexHome;
133
+ return validarDiretorioDePerfil(dir);
134
+ }
135
+ /** Valida um perfil de despacho vindo do ledger, do thread.json ou do pedido da fila. */
136
+ function validarPerfilDeDespacho(v) {
137
+ if (!v || typeof v !== 'object' || Array.isArray(v))
138
+ throw new Error('runtime.profiles.invalid: perfil de despacho ausente');
139
+ const o = v;
140
+ const runtime = o.runtime;
141
+ if (typeof runtime !== 'string' || !runtimeComPerfil(runtime))
142
+ throw new Error('runtime.profiles.invalid: runtime do perfil desconhecido');
143
+ const campo = runtime === 'claude-bg' ? 'configDir' : 'codexHome';
144
+ const outro = runtime === 'claude-bg' ? 'codexHome' : 'configDir';
145
+ if (o[outro] !== undefined || Object.keys(o).some(k => !['id', 'runtime', campo].includes(k)))
146
+ throw new Error('runtime.profiles.invalid: perfil de despacho com campo inesperado');
147
+ const id = validarIdDePerfil(o.id), dir = validarDiretorioDePerfil(o[campo]);
148
+ return runtime === 'claude-bg' ? { id, runtime, configDir: dir } : { id, runtime, codexHome: dir };
149
+ }
150
+ function perfilDeDespacho(p) {
151
+ return p.runtime === 'claude-bg' ? { id: p.id, runtime: p.runtime, configDir: diretorioDoPerfil(p) }
152
+ : { id: p.id, runtime: p.runtime, codexHome: diretorioDoPerfil(p) };
153
+ }
154
+ /**
155
+ * Diretorio implicito de quem despacha sem perfil: o do ambiente informado, ou o padrao do
156
+ * CLI na HOME. E o comportamento anterior a I-33, agora com um unico domicilio.
157
+ */
158
+ function diretorioImplicito(runtime, env = process.env) {
159
+ const valor = (env[exports.VARIAVEL_DO_PERFIL[runtime]] ?? '').trim();
160
+ return valor !== '' ? valor : path.join(os.homedir(), runtime === 'claude-bg' ? '.claude' : '.codex');
161
+ }
162
+ /** Diretorio efetivo: o do perfil registrado, ou o implicito quando a sessao nao tem perfil. */
163
+ function diretorioEfetivo(runtime, perfil, env = process.env) {
164
+ if (!perfil)
165
+ return diretorioImplicito(runtime, env);
166
+ if (perfil.runtime !== runtime)
167
+ throw new Error('runtime.profiles.invalid: perfil de outro runtime');
168
+ return diretorioDoPerfil(perfil);
169
+ }
170
+ /**
171
+ * Ambiente do filho com o perfil aplicado sobre uma base ja higienizada (sem provider pago).
172
+ * Sem perfil devolve a base intacta. Nunca le nada de dentro do diretorio do perfil.
173
+ */
174
+ function ambienteComPerfil(base, perfil) {
175
+ const env = { ...base };
176
+ if (perfil)
177
+ env[exports.VARIAVEL_DO_PERFIL[perfil.runtime]] = diretorioDoPerfil(perfil);
178
+ return env;
179
+ }
180
+ /**
181
+ * D4: o perfil gravado no registro da sessao. O `session_sensor_registered` do despacho vence;
182
+ * sem ele, o `phase_dispatch` da sessao. Ausente devolve `null` (sessao sem perfil, ambiente do
183
+ * processo); presente e invalido LANCA, porque cair para o env do processo seria observar a
184
+ * sessao pela conta errada, que e o risco R2.
185
+ */
186
+ function perfilDoRegistro(eventos, sessao) {
187
+ const doSensor = eventos.filter(e => e.tipo === 'session_sensor_registered' && e.sessionId === sessao.sessionId &&
188
+ (sessao.despachadaEm === undefined || e.despachoEm === sessao.despachadaEm)).at(-1);
189
+ const doDespacho = eventos.filter(e => e.tipo === 'phase_dispatch' && e.sessionId === sessao.sessionId).at(-1);
190
+ const bruto = doSensor?.perfil !== undefined ? doSensor.perfil : doDespacho?.perfil;
191
+ if (bruto === undefined || bruto === null)
192
+ return null;
193
+ const perfil = validarPerfilDeDespacho(bruto);
194
+ if (doSensor?.perfil !== undefined && doDespacho?.perfil !== undefined &&
195
+ JSON.stringify(validarPerfilDeDespacho(doDespacho.perfil)) !== JSON.stringify(perfil))
196
+ throw new Error('runtime.profiles.invalid: perfil do sensor diverge do despacho');
197
+ return perfil;
198
+ }
199
+ // ---------------------------------------------------------------------------
200
+ // Armazenamento privado.
201
+ // ---------------------------------------------------------------------------
202
+ function pastaPrivada(raiz) {
203
+ return path.join((0, estado_thread_1.raizDoEstado)(raiz), '.orkastery', 'private');
204
+ }
205
+ function caminhoDoStore(raiz) {
206
+ return path.join(pastaPrivada(raiz), exports.ARQUIVO_PERFIS);
207
+ }
208
+ function validarPasta(pasta) {
209
+ const st = fs.lstatSync(pasta);
210
+ if (!st.isDirectory() || st.isSymbolicLink() || st.uid !== uid() || (st.mode & 0o777) !== 0o700 ||
211
+ fs.realpathSync(pasta) !== pasta)
212
+ throw new Error('runtime.profiles.invalid: pasta privada precisa ser 0700 do proprio usuario');
213
+ return st;
214
+ }
215
+ function garantirPasta(raiz) {
216
+ const pasta = pastaPrivada(raiz);
217
+ const estado = path.dirname(pasta);
218
+ const stEstado = fs.lstatSync(estado);
219
+ if (!stEstado.isDirectory() || stEstado.isSymbolicLink() || fs.realpathSync(estado) !== estado)
220
+ throw new Error('runtime.profiles.invalid: diretorio de estado invalido');
221
+ try {
222
+ fs.mkdirSync(pasta, { mode: 0o700 });
223
+ fs.chmodSync(pasta, 0o700);
224
+ }
225
+ catch (e) {
226
+ if (e.code !== 'EEXIST')
227
+ throw e;
228
+ }
229
+ return { pasta, st: validarPasta(pasta) };
230
+ }
231
+ function storeVazio() { return { contrato: exports.CONTRATO_PERFIS, perfis: [] }; }
232
+ function data(v, campo) {
233
+ if (v === null)
234
+ return null;
235
+ if (typeof v !== 'string' || !Number.isFinite(Date.parse(v)))
236
+ throw new Error(`runtime.profiles.invalid: ${campo} invalido`);
237
+ return v;
238
+ }
239
+ function validarPerfil(v) {
240
+ if (!v || typeof v !== 'object' || Array.isArray(v))
241
+ throw new Error('runtime.profiles.invalid: perfil invalido');
242
+ const o = v;
243
+ const despacho = validarPerfilDeDespacho({ id: o.id, runtime: o.runtime,
244
+ ...(o.configDir !== undefined ? { configDir: o.configDir } : {}), ...(o.codexHome !== undefined ? { codexHome: o.codexHome } : {}) });
245
+ const permitidos = ['id', 'runtime', 'configDir', 'codexHome', 'estado', 'esgotadoAte', 'ultimaFalha', 'ultimoUso', 'criadoEm'];
246
+ if (Object.keys(o).some(k => !permitidos.includes(k)))
247
+ throw new Error('runtime.profiles.invalid: campo inesperado no perfil');
248
+ if (!exports.ESTADOS_DE_PERFIL.includes(o.estado))
249
+ throw new Error('runtime.profiles.invalid: estado do perfil desconhecido');
250
+ let ultimaFalha = null;
251
+ if (o.ultimaFalha !== null) {
252
+ const f = o.ultimaFalha;
253
+ if (!f || typeof f !== 'object' || typeof f.motivo !== 'string' || typeof f.detalhe !== 'string' ||
254
+ f.detalhe.length > LIMITE_DETALHE || Object.keys(f).some(k => !['motivo', 'em', 'detalhe'].includes(k)))
255
+ throw new Error('runtime.profiles.invalid: ultimaFalha invalida');
256
+ ultimaFalha = { motivo: f.motivo, em: data(f.em, 'ultimaFalha.em'), detalhe: f.detalhe };
257
+ }
258
+ return { ...despacho, estado: o.estado, esgotadoAte: data(o.esgotadoAte, 'esgotadoAte'),
259
+ ultimaFalha, ultimoUso: data(o.ultimoUso, 'ultimoUso'), criadoEm: data(o.criadoEm, 'criadoEm') };
260
+ }
261
+ /** Conteudo validado; ids unicos e diretorio unico por runtime. */
262
+ function validarStore(v) {
263
+ if (!v || typeof v !== 'object' || Array.isArray(v))
264
+ throw new Error('runtime.profiles.invalid: store invalido');
265
+ const o = v;
266
+ if (o.contrato !== exports.CONTRATO_PERFIS || !Array.isArray(o.perfis) || Object.keys(o).sort().join(',') !== 'contrato,perfis')
267
+ throw new Error(`runtime.profiles.invalid: contrato ${exports.CONTRATO_PERFIS} esperado`);
268
+ const perfis = o.perfis.map(validarPerfil);
269
+ if (new Set(perfis.map(p => p.id)).size !== perfis.length)
270
+ throw new Error('runtime.profiles.invalid: id de perfil repetido');
271
+ if (new Set(perfis.map(p => `${p.runtime}|${diretorioDoPerfil(p)}`)).size !== perfis.length)
272
+ throw new Error('runtime.profiles.invalid: dois perfis do mesmo runtime no mesmo diretorio');
273
+ return { contrato: exports.CONTRATO_PERFIS, perfis };
274
+ }
275
+ /** Le o store. Sem pasta privada ou sem arquivo: store vazio (nenhum perfil configurado). */
276
+ function lerPerfis(raiz) {
277
+ const pasta = pastaPrivada(raiz);
278
+ let pastaSt;
279
+ try {
280
+ pastaSt = validarPasta(pasta);
281
+ }
282
+ catch (e) {
283
+ if (e.code === 'ENOENT')
284
+ return storeVazio();
285
+ throw e;
286
+ }
287
+ let fd;
288
+ try {
289
+ fd = fs.openSync(path.join(pasta, exports.ARQUIVO_PERFIS), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
290
+ }
291
+ catch (e) {
292
+ if (e.code === 'ENOENT')
293
+ return storeVazio();
294
+ throw new Error('runtime.profiles.invalid: arquivo de perfis ilegivel ou link simbolico');
295
+ }
296
+ try {
297
+ const st = fs.fstatSync(fd), atual = fs.lstatSync(pasta);
298
+ if (!st.isFile() || st.nlink !== 1 || st.uid !== uid() || (st.mode & 0o777) !== 0o600 || st.size > LIMITE_STORE_BYTES ||
299
+ atual.dev !== pastaSt.dev || atual.ino !== pastaSt.ino)
300
+ throw new Error('runtime.profiles.invalid: arquivo de perfis precisa ser 0600 do proprio usuario');
301
+ let bruto;
302
+ try {
303
+ bruto = JSON.parse(fs.readFileSync(fd, 'utf8'));
304
+ }
305
+ catch {
306
+ throw new Error('runtime.profiles.invalid: arquivo de perfis com JSON invalido');
307
+ }
308
+ return validarStore(bruto);
309
+ }
310
+ finally {
311
+ fs.closeSync(fd);
312
+ }
313
+ }
314
+ function gravarStore(pasta, pastaSt, store) {
315
+ gravarArquivoPrivado(pasta, pastaSt, exports.ARQUIVO_PERFIS, JSON.stringify(validarStore(store), null, 2) + '\n');
316
+ }
317
+ /** Gravacao atomica de um arquivo 0600 numa pasta privada ja conferida (temp, fsync, rename). */
318
+ function gravarArquivoPrivado(pasta, pastaSt, arquivo, conteudo) {
319
+ const temp = path.join(pasta, `.${arquivo}.${(0, node_crypto_1.randomUUID)()}.tmp`);
320
+ const fd = fs.openSync(temp, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
321
+ try {
322
+ // O umask so tira bits; o modo exato e imposto aqui para a leitura nao reprovar depois.
323
+ fs.fchmodSync(fd, 0o600);
324
+ fs.writeFileSync(fd, conteudo);
325
+ fs.fsyncSync(fd);
326
+ }
327
+ finally {
328
+ fs.closeSync(fd);
329
+ }
330
+ const atual = fs.lstatSync(pasta);
331
+ if (atual.dev !== pastaSt.dev || atual.ino !== pastaSt.ino) {
332
+ fs.rmSync(temp, { force: true });
333
+ throw new Error('runtime.profiles.invalid: pasta privada trocada durante a gravacao');
334
+ }
335
+ fs.renameSync(temp, path.join(pasta, arquivo));
336
+ const dfd = fs.openSync(pasta, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY);
337
+ try {
338
+ fs.fsyncSync(dfd);
339
+ }
340
+ finally {
341
+ fs.closeSync(dfd);
342
+ }
343
+ }
344
+ const PRAZO_DO_LOCK_MS = 15000;
345
+ const LOCK_SEM_DONO_VELHO_MS = 10000;
346
+ /** Conteudo do arquivo de dono do lock, ou `null` quando ele ainda nao existe ou ja saiu. */
347
+ function lerDono(dono) {
348
+ try {
349
+ return fs.readFileSync(dono, 'utf8');
350
+ }
351
+ catch (e) {
352
+ if (e.code === 'ENOENT')
353
+ return null;
354
+ throw e;
355
+ }
356
+ }
357
+ /**
358
+ * D15 (A5): quebra do lock velho sem apagar lock vivo de outro processo. O diretorio e renomeado
359
+ * para um nome unico (so um processo ganha o `rename`); se o dono que foi junto nao e o morto que
360
+ * foi observado, o lock era de um dono novo e volta ao lugar.
361
+ */
362
+ function quebrarLockVelho(lock, donoObservado) {
363
+ const velho = `${lock}.velho.${(0, node_crypto_1.randomUUID)()}`;
364
+ try {
365
+ fs.renameSync(lock, velho);
366
+ }
367
+ catch (e) {
368
+ if (e.code === 'ENOENT')
369
+ return;
370
+ throw e;
371
+ }
372
+ const levado = lerDono(path.join(velho, 'pid'));
373
+ if (levado !== donoObservado) {
374
+ try {
375
+ fs.renameSync(velho, lock);
376
+ return;
377
+ }
378
+ catch { /* um terceiro ja tomou o lugar: o renomeado e descartado */ }
379
+ }
380
+ fs.rmSync(path.join(velho, 'pid'), { force: true });
381
+ try {
382
+ fs.rmdirSync(velho);
383
+ }
384
+ catch { /* resto alheio fica para inspecao */ }
385
+ }
386
+ /**
387
+ * Exclusao curta entre processos `ork` que atualizam o store ao mesmo tempo. D15 (A5): o dono pode
388
+ * liberar o lock a qualquer instante entre as leituras de quem espera; `ENOENT` nessas leituras quer
389
+ * dizer "livre agora" e volta ao `mkdir`, nunca vira erro da atualizacao. A espera tem jitter para
390
+ * nao formar comboio e prazo folgado; so o prazo estourado vira `runtime.profiles.busy`.
391
+ */
392
+ function comLockDoStore(pasta, executar, arquivo = exports.ARQUIVO_PERFIS) {
393
+ const lock = path.join(pasta, `${arquivo}.lock`), dono = path.join(lock, 'pid');
394
+ const prazo = Date.now() + PRAZO_DO_LOCK_MS, pausa = new Int32Array(new SharedArrayBuffer(4));
395
+ for (;;) {
396
+ try {
397
+ fs.mkdirSync(lock, { mode: 0o700 });
398
+ break;
399
+ }
400
+ catch (e) {
401
+ if (e.code !== 'EEXIST')
402
+ throw e;
403
+ }
404
+ const bruto = lerDono(dono);
405
+ let mtimeMs;
406
+ try {
407
+ mtimeMs = fs.statSync(lock).mtimeMs;
408
+ }
409
+ catch (e) {
410
+ if (e.code === 'ENOENT')
411
+ continue;
412
+ throw e;
413
+ }
414
+ const pid = bruto === null ? null : Number(bruto);
415
+ let vivo = true;
416
+ if (pid !== null && Number.isInteger(pid) && pid > 0) {
417
+ try {
418
+ process.kill(pid, 0);
419
+ }
420
+ catch (erro) {
421
+ vivo = erro.code !== 'ESRCH';
422
+ }
423
+ }
424
+ else
425
+ vivo = Date.now() - mtimeMs < LOCK_SEM_DONO_VELHO_MS;
426
+ if (!vivo) {
427
+ quebrarLockVelho(lock, bruto);
428
+ continue;
429
+ }
430
+ if (Date.now() >= prazo)
431
+ throw new Error('runtime.profiles.busy: store de perfis ocupado; tente novamente');
432
+ Atomics.wait(pausa, 0, 0, 2 + Math.floor(Math.random() * 18));
433
+ }
434
+ try {
435
+ fs.writeFileSync(dono, String(process.pid), { flag: 'wx', mode: 0o600 });
436
+ }
437
+ catch (e) {
438
+ try {
439
+ fs.rmdirSync(lock);
440
+ }
441
+ catch { /* lock ja tomado */ }
442
+ throw e;
443
+ }
444
+ try {
445
+ return executar();
446
+ }
447
+ finally {
448
+ // So libera o lock que ainda e deste processo.
449
+ if (lerDono(dono) === String(process.pid)) {
450
+ fs.rmSync(dono, { force: true });
451
+ try {
452
+ fs.rmdirSync(lock);
453
+ }
454
+ catch { /* ja removido */ }
455
+ }
456
+ }
457
+ }
458
+ /** Leitura, mutacao e gravacao sob o lock do store. A mutacao recebe uma copia. */
459
+ function atualizarPerfis(raiz, mutar) {
460
+ const { pasta, st } = garantirPasta(raiz);
461
+ return comLockDoStore(pasta, () => {
462
+ const store = lerPerfis(raiz);
463
+ const copia = { contrato: exports.CONTRATO_PERFIS, perfis: store.perfis.map(p => ({ ...p })) };
464
+ const r = mutar(copia);
465
+ gravarStore(pasta, st, copia);
466
+ return r;
467
+ });
468
+ }
469
+ function exigirPerfil(store, id) {
470
+ const p = store.perfis.find(x => x.id === id);
471
+ if (!p)
472
+ throw new Error(`runtime.profiles.unknown: perfil "${id}" nao existe (veja: ork accounts list)`);
473
+ return p;
474
+ }
475
+ /**
476
+ * A7: identidade do diretorio pelo caminho REAL, resolvendo links do trecho que ja existe (o resto
477
+ * do caminho, ainda nao criado, entra como esta). Dois perfis que chegam a mesma conta por um link
478
+ * simbolico sao o mesmo perfil.
479
+ */
480
+ function caminhoRealDoPerfil(dir) {
481
+ const faltando = [];
482
+ let atual = dir;
483
+ for (;;) {
484
+ try {
485
+ return path.join(fs.realpathSync(atual), ...faltando.reverse());
486
+ }
487
+ catch (e) {
488
+ const codigo = e.code;
489
+ if (codigo !== 'ENOENT' && codigo !== 'ENOTDIR')
490
+ return path.join(atual, ...faltando.reverse());
491
+ const pai = path.dirname(atual);
492
+ if (pai === atual)
493
+ return path.join(atual, ...faltando.reverse());
494
+ faltando.push(path.basename(atual));
495
+ atual = pai;
496
+ }
497
+ }
498
+ }
499
+ /**
500
+ * A7: cria (0700) ou confere o diretorio de um perfil ANTES de grava-lo: diretorio de verdade, do
501
+ * proprio usuario e com leitura e escrita. Diretorio de outro usuario ou inacessivel nunca vira perfil.
502
+ */
503
+ function prepararDiretorioDoPerfil(dir) {
504
+ validarDiretorioDePerfil(dir);
505
+ try {
506
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
507
+ }
508
+ catch (e) {
509
+ throw new Error(`runtime.profiles.invalid: diretorio do perfil inacessivel (${e.code ?? 'erro'})`);
510
+ }
511
+ let st;
512
+ try {
513
+ st = fs.statSync(dir);
514
+ }
515
+ catch (e) {
516
+ throw new Error(`runtime.profiles.invalid: diretorio do perfil inacessivel (${e.code ?? 'erro'})`);
517
+ }
518
+ if (!st.isDirectory())
519
+ throw new Error('runtime.profiles.invalid: o caminho do perfil nao e diretorio');
520
+ if (st.uid !== uid())
521
+ throw new Error('runtime.profiles.invalid: diretorio do perfil pertence a outro usuario');
522
+ try {
523
+ fs.accessSync(dir, fs.constants.R_OK | fs.constants.W_OK | fs.constants.X_OK);
524
+ }
525
+ catch {
526
+ throw new Error('runtime.profiles.invalid: diretorio do perfil sem leitura e escrita para este usuario');
527
+ }
528
+ }
529
+ function adicionarPerfil(raiz, novo, agoraIso = new Date().toISOString()) {
530
+ if (!runtimeComPerfil(novo.runtime))
531
+ throw new Error(`runtime.profiles.invalid: runtime "${novo.runtime}" nao aceita perfil`);
532
+ const runtime = novo.runtime;
533
+ const dir = validarDiretorioDePerfil(novo.dir);
534
+ const perfil = { id: validarIdDePerfil(novo.id), runtime,
535
+ ...(runtime === 'claude-bg' ? { configDir: dir } : { codexHome: dir }),
536
+ estado: 'ativo', esgotadoAte: null, ultimaFalha: null, ultimoUso: null, criadoEm: agoraIso };
537
+ return atualizarPerfis(raiz, store => {
538
+ const existente = store.perfis.find(p => p.id === perfil.id);
539
+ if (existente) {
540
+ // A9: `remove` desativa; o mesmo `add` (mesmo id, runtime e diretorio) devolve o perfil ao rodizio.
541
+ if (existente.estado === 'desativado' && existente.runtime === runtime && diretorioDoPerfil(existente) === dir) {
542
+ existente.estado = 'ativo';
543
+ existente.esgotadoAte = null;
544
+ return { ...existente };
545
+ }
546
+ throw new Error(`runtime.profiles.invalid: perfil "${perfil.id}" ja existe` +
547
+ (existente.estado === 'desativado' ? ` (desativado em ${diretorioDoPerfil(existente)}; reative com o mesmo --runtime e --dir)` : ''));
548
+ }
549
+ if (store.perfis.some(p => p.runtime === runtime && diretorioDoPerfil(p) === dir))
550
+ throw new Error(`runtime.profiles.invalid: ja ha perfil ${runtime} em ${dir}`);
551
+ // A7: o mesmo diretorio por outro caminho (link simbolico) e a mesma conta, nao outro perfil.
552
+ const real = caminhoRealDoPerfil(dir);
553
+ const mesmo = store.perfis.find(p => p.runtime === runtime && caminhoRealDoPerfil(diretorioDoPerfil(p)) === real);
554
+ if (mesmo)
555
+ throw new Error(`runtime.profiles.invalid: ${dir} e o mesmo diretorio do perfil "${mesmo.id}" (${real})`);
556
+ store.perfis.push(perfil);
557
+ return perfil;
558
+ });
559
+ }
560
+ /** `remove` desativa: o diretorio e o login do CLI ficam onde estao. */
561
+ function desativarPerfil(raiz, id) {
562
+ return atualizarPerfis(raiz, store => {
563
+ const p = exigirPerfil(store, id);
564
+ p.estado = 'desativado';
565
+ return { ...p };
566
+ });
567
+ }
568
+ /** Volta ao rodizio (login de assinatura conferido ou prazo de cota vencido). Perfil desativado so volta por `add`. */
569
+ function reativarPerfil(raiz, id) {
570
+ const p = atualizarPerfis(raiz, store => {
571
+ const p = exigirPerfil(store, id);
572
+ if (p.estado === 'desativado')
573
+ throw new Error(`runtime.profiles.invalid: perfil "${id}" esta desativado`);
574
+ p.estado = 'ativo';
575
+ p.esgotadoAte = null;
576
+ return { ...p };
577
+ });
578
+ // I-49: login conferido ou prazo vencido vale para a conta, entao sai do registro compartilhado.
579
+ publicarEstadoDaConta(p, (0, estado_thread_1.raizDoEstado)(raiz));
580
+ return p;
581
+ }
582
+ /**
583
+ * D5: marca a falha de conta. Cota esgotada sai do rodizio ate `esgotadoAte`; auth ausente
584
+ * sai sem prazo, ate o login ser refeito e conferido.
585
+ */
586
+ function marcarFalhaDePerfil(raiz, id, falha) {
587
+ if (falha.estado === 'esgotado' && (falha.esgotadoAte === null || !Number.isFinite(Date.parse(falha.esgotadoAte))))
588
+ throw new Error('runtime.profiles.invalid: cota esgotada exige esgotadoAte');
589
+ const marcado = atualizarPerfis(raiz, store => {
590
+ const p = exigirPerfil(store, id);
591
+ if (p.estado !== 'desativado')
592
+ p.estado = falha.estado;
593
+ p.esgotadoAte = falha.estado === 'esgotado' ? falha.esgotadoAte : null;
594
+ p.ultimaFalha = { motivo: falha.motivo, em: falha.em ?? new Date().toISOString(), detalhe: detalheCurto(falha.detalhe) };
595
+ return { ...p };
596
+ });
597
+ // I-49: a falha e da CONTA; as outras fabricas do mesmo usuario passam a enxerga-la.
598
+ if (marcado.estado !== 'desativado')
599
+ publicarEstadoDaConta(marcado, (0, estado_thread_1.raizDoEstado)(raiz));
600
+ return marcado;
601
+ }
602
+ const detalheCurto = (detalhe) => Array.from(detalhe.replace(/\p{Cc}/gu, ' ')).slice(0, LIMITE_DETALHE).join('');
603
+ /**
604
+ * A14: conferencia de login INCONCLUSIVA (timeout, resposta ilegivel, binario ausente). Nao e prova
605
+ * de login perdido: so a `ultimaFalha` registra o fato, o estado nao muda, e o proximo despacho
606
+ * confere de novo antes de usar o perfil.
607
+ */
608
+ function registrarConferenciaInconclusiva(raiz, id, detalhe, em = new Date().toISOString()) {
609
+ return atualizarPerfis(raiz, store => {
610
+ const p = exigirPerfil(store, id);
611
+ p.ultimaFalha = { motivo: 'runtime.unavailable', em, detalhe: detalheCurto(`conferencia de login inconclusiva: ${detalhe}`) };
612
+ return { ...p };
613
+ });
614
+ }
615
+ /** Uso bem sucedido: carimba o instante e devolve ao rodizio o perfil cujo prazo de cota venceu. */
616
+ function registrarUsoDePerfil(raiz, id, em = new Date().toISOString()) {
617
+ const p = atualizarPerfis(raiz, store => {
618
+ const p = exigirPerfil(store, id);
619
+ p.ultimoUso = em;
620
+ if (p.estado === 'esgotado' && p.esgotadoAte !== null && Date.parse(p.esgotadoAte) <= Date.parse(em)) {
621
+ p.estado = 'ativo';
622
+ p.esgotadoAte = null;
623
+ }
624
+ return { ...p };
625
+ });
626
+ // I-49: uso bem sucedido prova que a conta responde; se outro projeto a marcou, a marca sai.
627
+ if (p.estado === 'ativo' && lerContasCompartilhadas().some((c) => { try {
628
+ return c.chave === chaveDaConta(p);
629
+ }
630
+ catch {
631
+ return false;
632
+ } })) {
633
+ publicarEstadoDaConta(p, (0, estado_thread_1.raizDoEstado)(raiz), Date.parse(em));
634
+ }
635
+ return p;
636
+ }
637
+ // ---------------------------------------------------------------------------
638
+ // I-49 (RM-040): estado da CONTA compartilhado entre os projetos do mesmo usuario.
639
+ // ---------------------------------------------------------------------------
640
+ /**
641
+ * O store de perfis e por projeto, mas a conta e do usuario: tres fabricas na mesma maquina
642
+ * apontam para o mesmo `CLAUDE_CONFIG_DIR`. Quando uma delas ve a cota esgotada, o login perdido
643
+ * ou a credencial paga, as outras precisam saber antes de bater na mesma parede.
644
+ *
645
+ * O registro mora em `~/.orkastery/private/contas.json`, com as mesmas garantias do store (pasta
646
+ * 0700, arquivo 0600 do proprio usuario, sem link simbolico, gravacao atomica sob lock). A chave
647
+ * e o runtime mais o caminho REAL do diretorio do perfil: o id do perfil e do projeto, a conta e
648
+ * do diretorio. Nenhum segredo entra aqui, como no store.
649
+ *
650
+ * O registro so escurece: ele nunca devolve ao rodizio um perfil que o projeto marcou fora, e
651
+ * leitura com problema vale registro vazio (o despacho segue como antes da I-49).
652
+ */
653
+ exports.CONTRATO_CONTAS = 'ork.contas-compartilhadas/v1';
654
+ exports.ARQUIVO_CONTAS = 'contas.json';
655
+ const LIMITE_CONTAS_BYTES = 64 * 1024;
656
+ const ESTADOS_COMPARTILHADOS = ['esgotado', 'sem-auth', 'provider-pago'];
657
+ function pastaDasContas() {
658
+ // Testes e o `ork eval` apontam para uma pasta propria, para nunca tocar o registro do usuario.
659
+ const isolada = process.env.ORK_CONTAS_DIR;
660
+ if (isolada && path.isAbsolute(isolada))
661
+ return isolada;
662
+ // Pelo caminho real do home: a pasta privada so vale sem link simbolico no caminho.
663
+ let home = os.homedir();
664
+ try {
665
+ home = fs.realpathSync(home);
666
+ }
667
+ catch { /* home ausente: a leitura vale vazio */ }
668
+ return path.join(home, '.orkastery', 'private');
669
+ }
670
+ function chaveDaConta(p) {
671
+ return `${p.runtime}:${caminhoRealDoPerfil(diretorioDoPerfil(p))}`;
672
+ }
673
+ function validarRegistroDeContas(v) {
674
+ if (!v || typeof v !== 'object' || v.contrato !== exports.CONTRATO_CONTAS ||
675
+ !Array.isArray(v.contas))
676
+ throw new Error('runtime.profiles.invalid: registro de contas invalido');
677
+ const contas = v.contas.map((c) => {
678
+ const o = c;
679
+ if (!o || typeof o.chave !== 'string' || !ESTADOS_COMPARTILHADOS.includes(String(o.estado)) ||
680
+ typeof o.motivo !== 'string' || typeof o.detalhe !== 'string' || typeof o.projeto !== 'string' || typeof o.em !== 'string')
681
+ throw new Error('runtime.profiles.invalid: conta compartilhada invalida');
682
+ return { chave: o.chave, estado: o.estado, esgotadoAte: data(o.esgotadoAte ?? null, 'esgotadoAte'),
683
+ motivo: o.motivo, detalhe: detalheCurto(o.detalhe), em: data(o.em, 'em'), projeto: o.projeto };
684
+ });
685
+ return { contrato: exports.CONTRATO_CONTAS, contas };
686
+ }
687
+ /** Le o registro. Ausente, ilegivel ou com dono e modo errados vale vazio: o registro so escurece. */
688
+ function lerContasCompartilhadas() {
689
+ const pasta = pastaDasContas();
690
+ try {
691
+ const pastaSt = validarPasta(pasta);
692
+ const fd = fs.openSync(path.join(pasta, exports.ARQUIVO_CONTAS), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
693
+ try {
694
+ const st = fs.fstatSync(fd), atual = fs.lstatSync(pasta);
695
+ if (!st.isFile() || st.nlink !== 1 || st.uid !== uid() || (st.mode & 0o777) !== 0o600 || st.size > LIMITE_CONTAS_BYTES ||
696
+ atual.dev !== pastaSt.dev || atual.ino !== pastaSt.ino)
697
+ return [];
698
+ return validarRegistroDeContas(JSON.parse(fs.readFileSync(fd, 'utf8'))).contas;
699
+ }
700
+ finally {
701
+ fs.closeSync(fd);
702
+ }
703
+ }
704
+ catch {
705
+ return [];
706
+ }
707
+ }
708
+ function garantirPastaDasContas() {
709
+ const pasta = pastaDasContas();
710
+ fs.mkdirSync(pasta, { recursive: true, mode: 0o700 });
711
+ fs.chmodSync(pasta, 0o700);
712
+ return { pasta, st: validarPasta(pasta) };
713
+ }
714
+ /** O diretorio da conta ainda existe? Entrada de diretorio apagado (sandbox, conta removida) sai. */
715
+ function contaAindaExiste(c) {
716
+ const dir = c.chave.slice(c.chave.indexOf(':') + 1);
717
+ return path.isAbsolute(dir) && fs.existsSync(dir);
718
+ }
719
+ /**
720
+ * Grava no registro o estado que o projeto acabou de ver na conta do perfil: falha de conta
721
+ * entra (ou substitui a anterior), perfil de volta ao rodizio sai. Entradas com prazo vencido
722
+ * saem junto. Melhor esforco: o registro nunca derruba a operacao do projeto.
723
+ */
724
+ function publicarEstadoDaConta(p, projeto, agoraMs = Date.now()) {
725
+ try {
726
+ const chave = chaveDaConta(p);
727
+ const { pasta, st } = garantirPastaDasContas();
728
+ comLockDoStore(pasta, () => {
729
+ const vivas = lerContasCompartilhadas().filter((c) => c.chave !== chave && contaAindaExiste(c) &&
730
+ !(c.estado === 'esgotado' && c.esgotadoAte !== null && Date.parse(c.esgotadoAte) <= agoraMs));
731
+ if (ESTADOS_COMPARTILHADOS.includes(p.estado) && !(p.estado === 'esgotado' && p.esgotadoAte === null)) {
732
+ vivas.push({ chave, estado: p.estado, esgotadoAte: p.estado === 'esgotado' ? p.esgotadoAte : null,
733
+ motivo: p.ultimaFalha?.motivo ?? 'runtime.unavailable', detalhe: p.ultimaFalha?.detalhe ?? '',
734
+ em: p.ultimaFalha?.em ?? new Date(agoraMs).toISOString(), projeto });
735
+ }
736
+ gravarArquivoPrivado(pasta, st, exports.ARQUIVO_CONTAS, JSON.stringify({ contrato: exports.CONTRATO_CONTAS, contas: vivas }, null, 2) + '\n');
737
+ }, exports.ARQUIVO_CONTAS);
738
+ }
739
+ catch { /* o registro compartilhado e melhor esforco: o store do projeto ja foi gravado */ }
740
+ }
741
+ /**
742
+ * O store do projeto com o que as outras fabricas viram na mesma conta. So escurece: perfil
743
+ * desativado ou com credencial paga continua como esta; esgotado local com prazo maior mantem o
744
+ * dele. Entrada vencida nao vale.
745
+ */
746
+ function aplicarContasCompartilhadas(store, contas, agoraMs = Date.now()) {
747
+ if (contas.length === 0)
748
+ return store;
749
+ const porChave = new Map(contas.map((c) => [c.chave, c]));
750
+ return { contrato: store.contrato, perfis: store.perfis.map((p) => {
751
+ if (p.estado === 'desativado' || p.estado === 'provider-pago')
752
+ return p;
753
+ let c;
754
+ try {
755
+ c = porChave.get(chaveDaConta(p));
756
+ }
757
+ catch {
758
+ return p;
759
+ }
760
+ if (!c)
761
+ return p;
762
+ const falha = { motivo: c.motivo, em: c.em, detalhe: detalheCurto(`visto em outro projeto: ${c.detalhe}`) };
763
+ if (c.estado === 'esgotado') {
764
+ const ate = c.esgotadoAte === null ? NaN : Date.parse(c.esgotadoAte);
765
+ if (!(ate > agoraMs))
766
+ return p;
767
+ if (p.estado === 'esgotado' && p.esgotadoAte !== null && Date.parse(p.esgotadoAte) >= ate)
768
+ return p;
769
+ if (p.estado === 'sem-auth')
770
+ return p;
771
+ return { ...p, estado: 'esgotado', esgotadoAte: c.esgotadoAte, ultimaFalha: falha };
772
+ }
773
+ return { ...p, estado: c.estado, esgotadoAte: null, ultimaFalha: falha };
774
+ }) };
775
+ }
776
+ /** Os perfis que o despacho enxerga: o store do projeto com o estado compartilhado das contas. */
777
+ function lerPerfisComContas(raiz, agoraMs = Date.now()) {
778
+ return aplicarContasCompartilhadas(lerPerfis(raiz), lerContasCompartilhadas(), agoraMs);
779
+ }
780
+ // ---------------------------------------------------------------------------
781
+ // Escolha do perfil (leitura pura do store).
782
+ // ---------------------------------------------------------------------------
783
+ function perfisDoRuntime(store, runtime) {
784
+ return store.perfis.filter(p => p.runtime === runtime);
785
+ }
786
+ /** Ativo, ou esgotado com o prazo ja vencido. `sem-auth`, `provider-pago` e `desativado` nunca recebem despacho. */
787
+ function perfilDisponivel(p, agoraMs = Date.now()) {
788
+ if (p.estado === 'ativo')
789
+ return true;
790
+ return p.estado === 'esgotado' && p.esgotadoAte !== null && Date.parse(p.esgotadoAte) <= agoraMs;
791
+ }
792
+ /** Defaults da D16: por cota ligada (decisao do dono em 19/09/2026) e por login perdido ligada. */
793
+ exports.POLITICA_PADRAO = { mesmoRuntimePorCota: true, mesmoRuntimePorAuth: true };
794
+ function politicaDeRotacao(manifesto) {
795
+ const r = manifesto.runtime_profiles;
796
+ return { mesmoRuntimePorCota: r?.rotate_same_runtime_on_quota !== false,
797
+ mesmoRuntimePorAuth: r?.rotate_same_runtime_on_auth !== false };
798
+ }
799
+ /**
800
+ * D14: o perfil da vez do runtime, na ordem do store. `desativado` e `provider-pago` nunca foram
801
+ * elegiveis e sao pulados. `sem-auth` so e pulado com a troca por login ligada. Perfil esgotado
802
+ * (ou ja tentado nesta rotacao) so e pulado com a troca por cota ligada: desligada, ele SEGURA o
803
+ * runtime ate o prazo, e nenhum outro perfil do mesmo runtime o substitui automaticamente.
804
+ */
805
+ function proximoPerfilDisponivel(store, runtime, politica, opcoes = {}) {
806
+ const agoraMs = opcoes.agoraMs ?? Date.now();
807
+ for (const p of perfisDoRuntime(store, runtime)) {
808
+ if (p.estado === 'desativado' || p.estado === 'provider-pago')
809
+ continue;
810
+ if (p.estado === 'sem-auth') {
811
+ if (politica.mesmoRuntimePorAuth)
812
+ continue;
813
+ return null;
814
+ }
815
+ if (!(opcoes.excluir ?? []).includes(p.id) && perfilDisponivel(p, agoraMs))
816
+ return p;
817
+ if (!politica.mesmoRuntimePorCota)
818
+ return null;
819
+ }
820
+ return null;
821
+ }
822
+ /**
823
+ * D14: quando o runtime volta a ter perfil da vez, pela mesma regra da escolha. Com a troca por
824
+ * cota desligada, e o prazo do perfil que segura o runtime; ligada, o menor prazo entre os
825
+ * esgotados. Sem prazo (so login ausente, ou perfil ainda disponivel), `null`.
826
+ */
827
+ function prazoDoRuntime(store, runtime, politica, agoraMs = Date.now()) {
828
+ const prazos = [];
829
+ for (const p of perfisDoRuntime(store, runtime)) {
830
+ if (p.estado === 'desativado' || p.estado === 'provider-pago')
831
+ continue;
832
+ if (p.estado === 'sem-auth') {
833
+ if (politica.mesmoRuntimePorAuth)
834
+ continue;
835
+ break;
836
+ }
837
+ if (p.estado === 'esgotado' && p.esgotadoAte !== null && Date.parse(p.esgotadoAte) > agoraMs)
838
+ prazos.push(p.esgotadoAte);
839
+ if (!politica.mesmoRuntimePorCota)
840
+ break;
841
+ }
842
+ return prazos.sort()[0] ?? null;
843
+ }
844
+ /** Menor prazo entre os runtimes informados, pela politica da D14: prazo da fila duravel (P4). */
845
+ function prazoDaFila(store, runtimes, politica, agoraMs = Date.now()) {
846
+ return runtimes.map(r => prazoDoRuntime(store, r, politica, agoraMs)).filter((p) => p !== null).sort()[0] ?? null;
847
+ }
848
+ /** Menor `esgotadoAte` futuro entre os perfis dos runtimes informados, sem politica (leitura do store). */
849
+ function menorEsgotadoAte(store, runtimes, agoraMs = Date.now()) {
850
+ const prazos = store.perfis.filter(p => runtimes.includes(p.runtime) && p.estado === 'esgotado' && p.esgotadoAte !== null &&
851
+ Date.parse(p.esgotadoAte) > agoraMs).map(p => p.esgotadoAte).sort();
852
+ return prazos[0] ?? null;
853
+ }
854
+ /**
855
+ * Linhas da tabela de `ork accounts list` e `ork accounts check`: sem segredo, porque o store
856
+ * nao guarda nenhum. I-35: a tabela e texto para pessoa, entao as tres colunas de horario saem
857
+ * no fuso do dono; o store e a saida `--json` continuam em UTC ISO. Quem imprime diz o fuso uma
858
+ * vez, pela `legendaDoFuso()`, quando `perfisComHorario` acusa alguma coluna preenchida.
859
+ */
860
+ function linhasDePerfis(store) {
861
+ return store.perfis.map(p => [p.id, p.runtime, diretorioDoPerfil(p), p.estado,
862
+ p.esgotadoAte ? (0, horario_1.formatarDataHora)(p.esgotadoAte) : '-', p.ultimoUso ? (0, horario_1.formatarDataHora)(p.ultimoUso) : '-',
863
+ p.ultimaFalha ? `${p.ultimaFalha.motivo} em ${(0, horario_1.formatarDataHora)(p.ultimaFalha.em)}` : '-']);
864
+ }
865
+ /** True quando a tabela mostra algum horario, e a mensagem precisa dizer o fuso uma vez. */
866
+ function perfisComHorario(store) {
867
+ return store.perfis.some(p => p.esgotadoAte !== null || p.ultimoUso !== null || p.ultimaFalha !== null);
868
+ }
869
+ exports.COLUNAS_DE_PERFIS = ['PERFIL', 'RUNTIME', 'DIRETORIO', 'ESTADO', 'ESGOTADO ATE', 'ULTIMO USO', 'ULTIMA FALHA'];