@orkastery/cli 0.4.1 → 0.4.3

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.
package/README.md CHANGED
@@ -13,6 +13,9 @@ append-only ledger.
13
13
  it again on an independent runner before the merge.
14
14
  - **You choose how often you are called.** One mode per #TAG, in the request itself, decides
15
15
  where the cycle pauses. Verification is the same in all of them.
16
+ - **Decisions sized for a full day.** Every question `ork` brings you is short, with alternatives
17
+ and one recommendation, so a person running dozens of things at once, including people with
18
+ ADHD, decides in seconds.
16
19
  - **Subscription, never pay-per-token.** Dispatch uses the runtime CLI's own login. `ork` never
17
20
  reads credentials, and the `subscription-only` policy blocks paid providers.
18
21
 
@@ -57,7 +57,6 @@ fallback: usa `ORK_BIN` do ambiente ou o `ork` do PATH.
57
57
  | `ork_claims_add` | Registra alegacao verificavel com o comando que a comprova |
58
58
  | `ork_verify` / `ork_verify_baseline` | Reexecuta no HEAD real; grava a baseline antes do GO |
59
59
  | `ork_worktree_ensure` / `ork_worktree_audit` | Worktree isolada, conferida no proprio git |
60
- | `ork_objective_status` / `ork_objective_message` | Aposentadas com o `ork objective` na I-43: devolvem a recusa tipada `objective.aposentado` |
61
60
  | `ork_portfolio_list` | Lista produtos, projetos e iniciativas canônicos |
62
61
  | `ork_gate_answer` | Resposta humana correlacionada a um gate |
63
62
  | `ork_session_answer` | Resposta humana correlacionada a uma sessão |
@@ -254,25 +254,6 @@ const FERRAMENTAS = [
254
254
  parameters: schema({ thread: { type: 'string' } }),
255
255
  argv: (p) => ['phase', 'list', texto(p, 'thread')],
256
256
  },
257
- {
258
- name: 'ork_objective_status',
259
- description: 'Aposentado na I-43 junto com ork objective: devolve a recusa tipada objective.aposentado, com saida != 0. Estado de uma entrega: ork_thread_status. Ciclos ligados a um produto, projeto ou iniciativa: ork portfolio inspect <id> --json.',
260
- parameters: schema({ objective: { type: 'string', description: 'ID obj-* do ticket' } }),
261
- argv: (p) => ['objective', 'status', texto(p, 'objective'), '--json'],
262
- },
263
- {
264
- name: 'ork_objective_message',
265
- description: 'Aposentado na I-43 junto com ork objective: devolve a recusa tipada objective.aposentado, com saida != 0, e nao grava nada. Resposta do dono a uma pausa vai por ork_gate_answer ou ork_session_answer.',
266
- parameters: schema({
267
- objective: { type: 'string', description: 'ID obj-* do ticket' },
268
- author: { type: 'string', description: 'Identidade fornecida pelo gateway autenticado' },
269
- message: { type: 'string', description: 'Texto integral recebido do dono' },
270
- }),
271
- argv: (p) => [
272
- 'objective', 'message', texto(p, 'objective'),
273
- '--text', texto(p, 'message'), '--por', texto(p, 'author'), '--role', 'human',
274
- ],
275
- },
276
257
  {
277
258
  name: 'ork_portfolio_list',
278
259
  description: 'Lista produtos, projetos e iniciativas do catálogo canônico usado pelo Kanban.',
@@ -2,7 +2,7 @@
2
2
  "id": "orkastery",
3
3
  "name": "Orkastery",
4
4
  "description": "Conducao de looping threads em 6 fases pelo nucleo `ork`, exposta ao OpenClaw como tools `ork_*`. Zero regra de negocio no host: cada tool e uma chamada de CLI.",
5
- "version": "0.4.1",
5
+ "version": "0.4.3",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "properties": {},
@@ -21,8 +21,6 @@
21
21
  "ork_thread_status",
22
22
  "ork_phase_run",
23
23
  "ork_phase_list",
24
- "ork_objective_status",
25
- "ork_objective_message",
26
24
  "ork_portfolio_list",
27
25
  "ork_brain_status",
28
26
  "ork_brain_query",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@orkastery/openclaw-plugin",
3
- "version": "0.4.1",
4
- "description": "Plugin Orkastery para OpenClaw: 25 tools ork_* que traduzem intencao em chamada de CLI do nucleo `ork`. Zero regra de negocio no host.",
3
+ "version": "0.4.3",
4
+ "description": "Plugin Orkastery para OpenClaw: 23 tools ork_* que traduzem intencao em chamada de CLI do nucleo `ork`. Zero regra de negocio no host.",
5
5
  "type": "module",
6
6
  "private": true,
7
7
  "license": "MIT",
@@ -265,27 +265,6 @@ const FERRAMENTAS: FerramentaOrk[] = [
265
265
  parameters: schema({ thread: { type: 'string' } }),
266
266
  argv: (p) => ['phase', 'list', texto(p, 'thread')],
267
267
  },
268
- {
269
- name: 'ork_objective_status',
270
- description:
271
- 'Aposentado na I-43 junto com ork objective: devolve a recusa tipada objective.aposentado, com saida != 0. Estado de uma entrega: ork_thread_status. Ciclos ligados a um produto, projeto ou iniciativa: ork portfolio inspect <id> --json.',
272
- parameters: schema({ objective: { type: 'string', description: 'ID obj-* do ticket' } }),
273
- argv: (p) => ['objective', 'status', texto(p, 'objective'), '--json'],
274
- },
275
- {
276
- name: 'ork_objective_message',
277
- description:
278
- 'Aposentado na I-43 junto com ork objective: devolve a recusa tipada objective.aposentado, com saida != 0, e nao grava nada. Resposta do dono a uma pausa vai por ork_gate_answer ou ork_session_answer.',
279
- parameters: schema({
280
- objective: { type: 'string', description: 'ID obj-* do ticket' },
281
- author: { type: 'string', description: 'Identidade fornecida pelo gateway autenticado' },
282
- message: { type: 'string', description: 'Texto integral recebido do dono' },
283
- }),
284
- argv: (p) => [
285
- 'objective', 'message', texto(p, 'objective'),
286
- '--text', texto(p, 'message'), '--por', texto(p, 'author'), '--role', 'human',
287
- ],
288
- },
289
268
  {
290
269
  name: 'ork_portfolio_list',
291
270
  description: 'Lista produtos, projetos e iniciativas do catálogo canônico usado pelo Kanban.',
@@ -44,6 +44,7 @@ var __importStar = (this && this.__importStar) || (function () {
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.JANELA_CURTA_MAX_MS = exports.NOME_ADAPTER = void 0;
46
46
  exports.ambienteDoPerfil = ambienteDoPerfil;
47
+ exports.semContextoDeDespacho = semContextoDeDespacho;
47
48
  exports.disponivel = disponivel;
48
49
  exports.versao = versao;
49
50
  exports.montarComando = montarComando;
@@ -62,6 +63,8 @@ exports.limparAnsi = limparAnsi;
62
63
  exports.logs = logs;
63
64
  exports.logsDaSessao = logsDaSessao;
64
65
  exports.parar = parar;
66
+ exports.pararAchada = pararAchada;
67
+ exports.acharSessaoNasContas = acharSessaoNasContas;
65
68
  exports.ocupacaoDeContexto = ocupacaoDeContexto;
66
69
  const util_1 = require("../util");
67
70
  const runtime_ambiente_1 = require("../runtime-ambiente");
@@ -82,6 +85,14 @@ function ambienteDoPerfil(perfil) {
82
85
  return (0, runtime_profiles_1.ambienteComPerfil)((0, runtime_ambiente_1.ambienteDeAssinatura)(), perfil);
83
86
  }
84
87
  exports.NOME_ADAPTER = 'claude-bg';
88
+ /** RM-037 (defeitosdeco D-4): o ambiente do `claude --bg` sem canal nem chave do contexto do despacho. */
89
+ function semContextoDeDespacho(base, contexto) {
90
+ const env = { ...base };
91
+ delete env.ORK_CANAL;
92
+ for (const chave of Object.keys(contexto ?? {}))
93
+ delete env[chave];
94
+ return env;
95
+ }
85
96
  /** Consultas do perfil interactive; gates e respostas humanas nunca recebem grant. */
86
97
  const CONSULTAS_MCP = [
87
98
  'mcp__orkastery__ork_thread_status',
@@ -129,6 +140,13 @@ function montarComando(pedido) {
129
140
  args.push('--model', pedido.model);
130
141
  if (pedido.effort)
131
142
  args.push('--effort', pedido.effort);
143
+ // RM-037 (defeitosdeco D-4): o contexto do despacho (identidade, thread, canal) chega POR SESSAO.
144
+ // O `claude --bg` entrega a sessao a um processo reserva do daemon da conta, e o daemon guarda o
145
+ // ambiente de quem o iniciou: pelo ambiente, a sessao nascia com a identidade de outro despacho.
146
+ // Medido em 28/09/2026 (2.1.284): o `env` de `--settings` chega ao Bash da sessao e vence o herdado.
147
+ const doDespacho = pedido.ambienteExtra ?? {};
148
+ if (Object.keys(doDespacho).length)
149
+ args.push('--settings', JSON.stringify({ env: doDespacho }));
132
150
  if (pedido.contextoRuntime) {
133
151
  if (pedido.contextoRuntime.host !== 'claude-code')
134
152
  throw Error('runtime.context.invalid: host Claude esperado');
@@ -255,8 +273,10 @@ function acharSessao(chave, sessoes) {
255
273
  function despachar(pedido) {
256
274
  const comando = montarComando(pedido);
257
275
  let ambiente;
276
+ // D-4: nada do contexto do despacho vai ao ambiente do processo `claude`, para um daemon novo nunca
277
+ // nascer com ele; a identidade herdada ja sai em `ambienteDeAssinatura`, e o canal sai aqui.
258
278
  try {
259
- ambiente = { ...ambienteDoPerfil(pedido.perfil), ...(pedido.ambienteExtra ?? {}) };
279
+ ambiente = semContextoDeDespacho(ambienteDoPerfil(pedido.perfil), pedido.ambienteExtra);
260
280
  }
261
281
  catch (e) {
262
282
  return { ok: false, comando, sessionId: null, verificada: false, stdout: '', stderr: '', erro: e.message };
@@ -497,6 +517,13 @@ const FRASES_DE_AUTH_AUSENTE = [
497
517
  * so vale como auth ausente quando o mesmo texto nao traz frase de cota: a cota, mais especifica, vence.
498
518
  */
499
519
  const FRASES_AMPLAS_DE_AUTH = [/\bcodex login\b/i];
520
+ /**
521
+ * RM-037 (defeitosdeco D-6): modelo inexistente ou sem acesso na conta. Medido na transcricao da
522
+ * sessao af32834f (28/09/2026, `fable-5-1` num perfil de conta sem esse modelo): `error: "model_not_found"` e o texto
523
+ * "There's an issue with the selected model (fable-5-1). It may not exist or you may not have access
524
+ * to it". Frase de sobrecarga ("model is overloaded") nao entra: e falha transitoria da infra.
525
+ */
526
+ const FRASES_DE_MODELO_INACESSIVEL = [/\bmodel_not_found\b/i, /issue with the selected model/i];
500
527
  /**
501
528
  * I-33 (D16): CRITERIO UNICO entre ESGOTAMENTO da conta e RATE LIMIT comum. Decisao do dono em
502
529
  * 19/09/2026 (opcao a do A3 do CHECK aa279e17): esgotamento de cota, credito ou limite do plano
@@ -593,6 +620,9 @@ function naturezaDoLimite(saida, agoraMs = Date.now()) {
593
620
  }
594
621
  function parseFalhaDeConta(saida, agoraMs = Date.now()) {
595
622
  const texto = saida ?? '';
623
+ const modelo = FRASES_DE_MODELO_INACESSIVEL.find((r) => r.test(texto));
624
+ if (modelo)
625
+ return { motivo: 'runtime.model-unavailable', resetEm: null, fonte: 'sem-horario', trecho: trechoDa(texto, modelo) };
596
626
  const auth = FRASES_DE_AUTH_AUSENTE.find((r) => r.test(texto));
597
627
  if (auth)
598
628
  return { motivo: 'runtime.auth-missing', resetEm: null, fonte: 'sem-horario', trecho: trechoDa(texto, auth) };
@@ -645,9 +675,15 @@ function conferirAuth(perfil) {
645
675
  }
646
676
  return { ok: true, detalhe: `claude auth status: loggedIn (${metodo})` };
647
677
  }
648
- /** Comando de anexar a sessao (precisa de TTY, entao o CLI imprime em vez de executar). */
649
- function comandoAttach(sessionId) {
650
- return `claude attach ${sessionId}`;
678
+ /**
679
+ * Comando de anexar a sessao (precisa de TTY, entao o CLI imprime em vez de executar). RM-037
680
+ * (defeitosdeco D-2): sessao de um perfil so e anexada com o `CLAUDE_CONFIG_DIR` daquela conta.
681
+ */
682
+ function comandoAttach(sessionId, configDir) {
683
+ if (!configDir)
684
+ return `claude attach ${sessionId}`;
685
+ const dir = /^[A-Za-z0-9_./-]+$/.test(configDir) ? configDir : `'${configDir.replace(/'/g, `'\\''`)}'`;
686
+ return `CLAUDE_CONFIG_DIR=${dir} claude attach ${sessionId}`;
651
687
  }
652
688
  /**
653
689
  * Transforma o dump de tela que o `claude logs` devolve em texto legivel.
@@ -736,10 +772,50 @@ function logsDaSessao(id, linhas = 60, ambiente = (0, runtime_ambiente_1.ambient
736
772
  /** Para uma sessao em background (o `claude stop` tambem trabalha com o id curto). */
737
773
  function parar(chave, ambiente = (0, runtime_ambiente_1.ambienteDeAssinatura)()) {
738
774
  const sessao = acharSessao(chave, listarSessoes(undefined, true, ambiente));
739
- const id = sessao?.id ?? (sessao?.sessionId ?? chave).slice(0, 8);
740
- const r = exec('claude', ['stop', id], undefined, 60000, ambiente);
775
+ return pararAchada(sessao ?? { sessionId: chave }, ambiente);
776
+ }
777
+ /** Para a sessao ja achada numa conta, com o ambiente dessa conta, sem listar de novo. */
778
+ function pararAchada(sessao, ambiente = (0, runtime_ambiente_1.ambienteDeAssinatura)()) {
779
+ const r = exec('claude', ['stop', sessao.id ?? sessao.sessionId.slice(0, 8)], undefined, 60000, ambiente);
741
780
  return { ok: r.ok, texto: limparAnsi(r.stdout + r.stderr).trim() };
742
781
  }
782
+ /**
783
+ * RM-037 (defeitosdeco D-2): o `claude agents` so lista a sessao para a conta que a despachou, entao
784
+ * uma chave e procurada no ambiente do processo e em cada perfil claude-bg informado, sem repetir
785
+ * diretorio efetivo. Consulta que falha nao vira "nao encontrada": vai para `falhas`.
786
+ */
787
+ function acharSessaoNasContas(chave, perfis) {
788
+ const achadas = [], falhas = [], vistos = new Set();
789
+ // GO-FIX (R5a): cada consulta pode custar dezenas de segundos sob carga. UUID completo e unico em
790
+ // qualquer conta, entao a busca para na primeira que acha; prefixo ou nome consulta todas, para
791
+ // recusar a chave ambigua.
792
+ const unica = UUID_CLAUDE.test(chave);
793
+ for (const perfil of [null, ...perfis.filter(p => p.runtime === 'claude-bg')]) {
794
+ let ambiente, configDir;
795
+ try {
796
+ ambiente = ambienteDoPerfil(perfil);
797
+ configDir = path.resolve((0, runtime_profiles_1.diretorioEfetivo)('claude-bg', perfil, ambiente));
798
+ }
799
+ catch (e) {
800
+ falhas.push(`${perfil?.id ?? 'processo'}: ${e.message}`);
801
+ continue;
802
+ }
803
+ if (vistos.has(configDir))
804
+ continue;
805
+ vistos.add(configDir);
806
+ const consulta = consultarSessoes(undefined, true, ambiente);
807
+ if (!consulta.ok) {
808
+ falhas.push(`${perfil?.id ?? 'processo'}: ${consulta.detalhe}`);
809
+ continue;
810
+ }
811
+ const sessao = acharSessao(chave, consulta.sessoes);
812
+ if (sessao)
813
+ achadas.push({ sessao, perfil, configDir, ambiente });
814
+ if (sessao && unica)
815
+ break;
816
+ }
817
+ return { achadas, falhas };
818
+ }
743
819
  /**
744
820
  * Ocupacao da janela de contexto de uma sessao, entre 0 e 1.
745
821
  *
@@ -70,23 +70,47 @@ function conferirDiretorioDaFonte(dir, dirSessoes, uid) {
70
70
  }
71
71
  return real;
72
72
  }
73
+ /**
74
+ * Quantas vezes a conferência lstat/abertura se repete quando o inode muda entre as duas. O worker
75
+ * grava o state.json por arquivo temporário e rename: cada gravação é um inode novo, e sob carga
76
+ * ela cai entre o lstat e o open. Só se aceita descritor cujo inode é o conferido; o limite impede
77
+ * laço sem fim contra quem troca o arquivo sem parar.
78
+ */
79
+ const TENTATIVAS_DE_INODE = 5;
73
80
  /** Abre sem seguir link, confere descritor/inode, modo, dono e limite antes de decodificar. */
74
81
  function lerMetadado(dir, nome, uid, obrigatorio = true) {
75
82
  const file = path.join(dir, nome);
76
- let antes;
77
- try {
78
- antes = fs.lstatSync(file);
79
- }
80
- catch (e) {
81
- if (e.code === 'ENOENT' && !obrigatorio)
82
- return null;
83
- return recusar(`metadado ${nome} de controller ausente`);
83
+ for (let tentativa = 1;; tentativa++) {
84
+ let antes;
85
+ try {
86
+ antes = fs.lstatSync(file);
87
+ }
88
+ catch (e) {
89
+ if (e.code === 'ENOENT' && !obrigatorio)
90
+ return null;
91
+ return recusar(`metadado ${nome} de controller ausente`);
92
+ }
93
+ if (!antes.isFile() || antes.isSymbolicLink())
94
+ recusar(`metadado ${nome} de controller não é arquivo regular`);
95
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
96
+ let st;
97
+ try {
98
+ st = fs.fstatSync(fd);
99
+ }
100
+ catch (e) {
101
+ fs.closeSync(fd);
102
+ throw e;
103
+ }
104
+ if (st.isFile() && (st.ino !== antes.ino || st.dev !== antes.dev) && tentativa < TENTATIVAS_DE_INODE) {
105
+ // Substituído por rename entre o lstat e o open: confere e abre de novo, do zero.
106
+ fs.closeSync(fd);
107
+ continue;
108
+ }
109
+ return lerDescritorConferido(fd, st, antes, nome, uid);
84
110
  }
85
- if (!antes.isFile() || antes.isSymbolicLink())
86
- recusar(`metadado ${nome} de controller não é arquivo regular`);
87
- const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
111
+ }
112
+ function lerDescritorConferido(fd, st, antes, nome, uid) {
88
113
  try {
89
- const st = fs.fstatSync(fd);
90
114
  if (!st.isFile() || st.ino !== antes.ino || st.dev !== antes.dev)
91
115
  recusar(`descritor de ${nome} divergente do inode conferido`);
92
116
  if (st.uid !== uid)
@@ -395,11 +419,14 @@ function registrarFonteController(dir, esperado) {
395
419
  }
396
420
  /** Releitura da fonte fixada. Nunca descobre outro processo, nem aceita override de rollout. */
397
421
  function lerSnapshotController(fonte, opcoes = {}) {
398
- const agora = opcoes.agoraMs ?? Date.now();
399
422
  const real = conferirDiretorioDaFonte(fonte.dir, path.dirname(fonte.dir), fonte.uid);
400
423
  if (real !== fonte.dir)
401
424
  recusar('fonte de controller mudou de caminho real');
402
425
  const estado = conferirEstado(lerMetadado(real, 'state.json', fonte.uid), fonte);
426
+ // O relógio de referência é o do fim da leitura. O worker grava o close quando o runtime fecha,
427
+ // e isso pode acontecer entre o início desta releitura e a leitura do state.json: esse close não
428
+ // está no futuro. Com o relógio lido antes, ele era recusado e o watcher morria sem phase_result.
429
+ const agora = opcoes.agoraMs ?? Date.now();
403
430
  if (!mesmaIdentidade(identidadeValida(estado.processoController, fonte.uid), fonte.processoController))
404
431
  recusar('identidade do controller divergente da registrada');
405
432
  if (!mesmaIdentidade(identidadeValida(estado.processoRuntime, fonte.uid), fonte.processoRuntime))
@@ -12,7 +12,9 @@ const falha = () => ({ classificacao: 'gate_blocked', motivo: 'runtime.unavailab
12
12
  const sucesso = () => ({ classificacao: 'fase_concluida', motivo: null });
13
13
  const humano = () => ({ classificacao: 'human.pending', motivo: 'human.pending' });
14
14
  /** I-33 (D12): motivos da conta so saem de erro estruturado do runtime (`falhaDeContaDoErroCodex`). */
15
- const MOTIVOS_DA_CONTA = ['runtime.quota-exhausted', 'runtime.auth-missing'];
15
+ const MOTIVOS_DA_CONTA = ['runtime.quota-exhausted', 'runtime.auth-missing',
16
+ // GO-FIX (R3, RM-037): o modelo inacessivel tambem so vale do erro estruturado, nunca do texto do agente.
17
+ 'runtime.model-unavailable'];
16
18
  function motivo(v) {
17
19
  if (v === undefined || v === null)
18
20
  return null;
package/dist/conducao.js CHANGED
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.ErroDeConducao = exports.ESPERA_PADRAO_MS = exports.PRAZO_DA_RESERVA_MS = exports.PRAZO_DE_SESSAO_PADRAO_MS = exports.MARGEM_DO_PRAZO_MS = exports.ENV_THREAD_DO_DESPACHO = exports.ENV_IDENTIDADE_DE_DESPACHO = exports.ENV_CANAL = exports.ERRO_CANAL_DESCONHECIDO = exports.CANAIS_DE_CONDUCAO = exports.conducaoDoLease = void 0;
36
+ exports.ErroDeConducao = exports.ESPERA_PADRAO_MS = exports.PRAZO_DA_RESERVA_MS = exports.PRAZO_DE_SESSAO_PADRAO_MS = exports.MARGEM_DO_PRAZO_MS = exports.ENV_SESSAO_CLAUDE = exports.ENV_THREAD_DO_DESPACHO = exports.ENV_IDENTIDADE_DE_DESPACHO = exports.ENV_CANAL = exports.ERRO_CANAL_DESCONHECIDO = exports.CANAIS_DE_CONDUCAO = exports.conducaoDoLease = void 0;
37
37
  exports.validarCanal = validarCanal;
38
38
  exports.canalDoProcesso = canalDoProcesso;
39
39
  exports.ambienteDaConducao = ambienteDaConducao;
@@ -86,6 +86,9 @@ const maquina_1 = require("./maquina");
86
86
  const thread_1 = require("./thread");
87
87
  const horario_1 = require("./horario");
88
88
  const util_1 = require("./util");
89
+ const runtime_ambiente_1 = require("./runtime-ambiente");
90
+ Object.defineProperty(exports, "ENV_IDENTIDADE_DE_DESPACHO", { enumerable: true, get: function () { return runtime_ambiente_1.ENV_IDENTIDADE_DE_DESPACHO; } });
91
+ Object.defineProperty(exports, "ENV_THREAD_DO_DESPACHO", { enumerable: true, get: function () { return runtime_ambiente_1.ENV_THREAD_DO_DESPACHO; } });
89
92
  // ---------------------------------------------------------------------------
90
93
  // Canais (D5, D6): registro fechado, declarado pela borda.
91
94
  // ---------------------------------------------------------------------------
@@ -146,21 +149,51 @@ function canalDoProcesso(explicito, ambiente = process.env) {
146
149
  return 'hermes';
147
150
  return 'cli';
148
151
  }
149
- // ---------------------------------------------------------------------------
150
- // Identidade de despacho (D4): a reentrada da sessao que ja conduz.
151
- // ---------------------------------------------------------------------------
152
- /** Identidade do despacho no ambiente da sessao filha, para as chamadas do CLI de dentro dela. */
153
- exports.ENV_IDENTIDADE_DE_DESPACHO = 'ORK_DISPATCH_ID';
154
- exports.ENV_THREAD_DO_DESPACHO = 'ORK_DISPATCH_THREAD';
155
152
  /** O que o despacho poe no ambiente da sessao filha: identidade, thread e canal de origem. */
156
153
  function ambienteDaConducao(identidade, threadId, canal) {
157
- return { [exports.ENV_IDENTIDADE_DE_DESPACHO]: identidade, [exports.ENV_THREAD_DO_DESPACHO]: threadId, [exports.ENV_CANAL]: canal };
154
+ return { [runtime_ambiente_1.ENV_IDENTIDADE_DE_DESPACHO]: identidade, [runtime_ambiente_1.ENV_THREAD_DO_DESPACHO]: threadId, [exports.ENV_CANAL]: canal };
158
155
  }
159
- /** A identidade de despacho que este processo herdou, quando ela e desta thread. */
160
- function identidadeDoAmbiente(threadId, ambiente = process.env) {
161
- const id = (ambiente[exports.ENV_IDENTIDADE_DE_DESPACHO] ?? '').trim();
162
- const thread = (ambiente[exports.ENV_THREAD_DO_DESPACHO] ?? '').trim();
163
- return /^[a-f0-9-]{36}$/.test(id) && thread === threadId ? id : null;
156
+ /** Variavel que o Claude Code poe em cada sessao, com o UUID dela; nao vem do daemon. */
157
+ exports.ENV_SESSAO_CLAUDE = 'CLAUDE_CODE_SESSION_ID';
158
+ /**
159
+ * A identidade de despacho deste processo, quando ela e desta thread.
160
+ *
161
+ * RM-037 (defeitosdeco D-4): dentro de uma sessao Claude Code, o ambiente pode trazer o par de OUTRO
162
+ * despacho (o daemon do `claude --bg` guardava o do primeiro despacho da conta e o passava as sessoes
163
+ * reserva). Com `raiz`, a sessao se reconhece pelo ledger: vale o ultimo `phase_dispatch` desta thread
164
+ * com o `sessionId` dela. O par do ambiente so vale se bater com esse despacho; par que aponta para um
165
+ * despacho claude-bg de outra sessao e recusado, para nunca reentrar a conducao de quem nao e. Fora de
166
+ * sessao Claude, ou com despacho codex (processo proprio por despacho), vale o ambiente, como antes.
167
+ */
168
+ function identidadeDoAmbiente(threadId, ambiente = process.env, raiz) {
169
+ const id = (ambiente[runtime_ambiente_1.ENV_IDENTIDADE_DE_DESPACHO] ?? '').trim();
170
+ const thread = (ambiente[runtime_ambiente_1.ENV_THREAD_DO_DESPACHO] ?? '').trim();
171
+ const doAmbiente = /^[a-f0-9-]{36}$/.test(id) && thread === threadId ? id : null;
172
+ const sessao = (ambiente[exports.ENV_SESSAO_CLAUDE] ?? '').trim();
173
+ if (!raiz || !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(sessao))
174
+ return doAmbiente;
175
+ let eventos;
176
+ try {
177
+ eventos = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, threadId));
178
+ }
179
+ catch {
180
+ return doAmbiente;
181
+ }
182
+ const despachoId = (e) => e.identidade?.dispatchId;
183
+ const despachos = eventos.filter(e => e.tipo === ledger_1.TIPOS_DE_EVENTO.faseDespachada);
184
+ const doPar = doAmbiente ? despachos.filter(e => despachoId(e) === doAmbiente).at(-1) : undefined;
185
+ // GO-FIX (R2 do CHECK): o codex despachado de dentro de uma sessao Claude herda o
186
+ // CLAUDE_CODE_SESSION_ID dela, mas o par do ambiente e do proprio processo codex, por despacho.
187
+ if (doPar?.runtime === 'codex')
188
+ return doAmbiente;
189
+ const daSessao = despachos.filter(e => e.sessionId === sessao && typeof despachoId(e) === 'string').at(-1);
190
+ if (daSessao)
191
+ return String(despachoId(daSessao));
192
+ if (!doAmbiente)
193
+ return null;
194
+ if (doPar && (doPar.runtime ?? 'claude-bg') === 'claude-bg' && typeof doPar.sessionId === 'string' && doPar.sessionId !== sessao)
195
+ return null;
196
+ return doAmbiente;
164
197
  }
165
198
  // ---------------------------------------------------------------------------
166
199
  // Prazos (D3): derivados do teto real da operacao; metadado, nunca prova.
package/dist/fix.js CHANGED
@@ -196,7 +196,7 @@ function sobConducao(carregado, threadId, operacao, conducao, executar) {
196
196
  canal: conducao.canal ?? (0, conducao_1.canalDoProcesso)(),
197
197
  correlacao: conducao.correlacao ?? null,
198
198
  operacao,
199
- identidade: conducao.identidade ?? (0, conducao_1.identidadeDoAmbiente)(threadId),
199
+ identidade: conducao.identidade ?? (0, conducao_1.identidadeDoAmbiente)(threadId, process.env, carregado.raiz),
200
200
  prazoMs: (0, conducao_1.prazoDaVerificacao)(carregado.manifesto.verify.timeout_ms ?? 10 * 60 * 1000, 8),
201
201
  esperarMs: conducao.esperarMs,
202
202
  }, executar);
package/dist/gates.js CHANGED
@@ -44,6 +44,7 @@ exports.DESCRICAO_DO_MOTIVO = {
44
44
  'runtime.rate-limited': 'o runtime bateu o limite de uso da assinatura e a fase morreu antes de terminar (bloco B3: entra na fila duravel e e retomada na janela seguinte)',
45
45
  'runtime.quota-exhausted': 'a cota ou os creditos da conta do runtime acabaram (I-33: o perfil sai do rodizio e a fase segue em outro perfil, outro runtime ou na fila)',
46
46
  'runtime.auth-missing': 'a conta do runtime nao esta autenticada (I-33: o perfil nunca recebe despacho ate o login ser refeito pelo proprio CLI)',
47
+ 'runtime.model-unavailable': 'o modelo pedido nao existe ou a conta nao tem acesso a ele (RM-037: o perfil segue no rodizio e a fase vai a outro perfil com o mesmo modelo ou ao fallback do bloco)',
47
48
  'cost.violation': 'o despacho seria redirecionado para provider pago (violacao de custo: e o unico motivo que NUNCA recebe retry automatico)',
48
49
  'tree.blocked': 'a arvore de destino nao esta disponivel para o merge (branch em check-out com alteracao nao commitada, ou worktree que nao pode ser montada)',
49
50
  'lease.busy': 'lease ocupado por outra thread',
package/dist/index.js CHANGED
@@ -144,6 +144,8 @@ const licoes_1 = require("./licoes");
144
144
  const demo_1 = require("./demo");
145
145
  const entrega_pr_1 = require("./entrega-pr");
146
146
  /** A versao publicada em `@orkastery/cli`, lida do package.json (`versao.ts`). */
147
+ // Antes de qualquer arquivo ou despacho: sem escrita de grupo nem de outros, que o sensor recusaria.
148
+ (0, preflight_1.endurecerUmask)();
147
149
  const VERSAO = versao_1.VERSAO_DO_ORK;
148
150
  /** Parse simples de argumentos: `--chave valor`, `--chave=valor` e flags booleanas. */
149
151
  function parseArgs(argv) {
@@ -1219,17 +1221,17 @@ function comandoSessions(args) {
1219
1221
  }
1220
1222
  if (sub === 'logs') {
1221
1223
  const n = Number(texto(args.opcoes.linhas) ?? '60');
1222
- const r = (0, sessoes_2.logsDaSessao)(chave, Number.isFinite(n) ? n : 60);
1224
+ const r = (0, sessoes_2.logsDaSessao)(chave, Number.isFinite(n) ? n : 60, carregado ? raiz : null);
1223
1225
  console.log(r.texto);
1224
1226
  return r.codigo;
1225
1227
  }
1226
1228
  if (sub === 'stop') {
1227
- const r = (0, sessoes_2.pararSessao)(chave);
1229
+ const r = (0, sessoes_2.pararSessao)(chave, carregado ? raiz : null);
1228
1230
  console.log(r.texto);
1229
1231
  return r.codigo;
1230
1232
  }
1231
1233
  if (sub === 'attach') {
1232
- const r = (0, sessoes_2.comandoDeAttach)(chave);
1234
+ const r = (0, sessoes_2.comandoDeAttach)(chave, carregado ? raiz : null);
1233
1235
  console.log(r.texto);
1234
1236
  return r.codigo;
1235
1237
  }
package/dist/mcp-git.js CHANGED
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.HOOKS_ESTRITOS = exports.HOOKS_DO_COMMIT = exports.HOOKS_INERTES_DO_COMMIT = void 0;
36
37
  exports.estadoGitMcp = estadoGitMcp;
37
38
  exports.commitMcp = commitMcp;
38
39
  exports.perfilGitMcp = perfil;
@@ -41,8 +42,8 @@ exports.caminhoFisicoGitMcp = fisico;
41
42
  exports.metadadosGitMcp = arvoreMetadados;
42
43
  exports.configuracaoPassivaGitMcp = configPassiva;
43
44
  exports.ambienteGitMcp = ambiente;
44
- /** Commit estreito no núcleo instalado. Repositórios com hooks/helpers ficam
45
- * explicitamente indisponíveis; nenhum hook ou configuração é desativado. */
45
+ /** Commit estreito no núcleo instalado. Repositórios com hooks/helpers que as operações
46
+ * executariam ficam explicitamente indisponíveis; nenhum hook ou configuração é desativado. */
46
47
  const fs = __importStar(require("node:fs"));
47
48
  const path = __importStar(require("node:path"));
48
49
  const node_crypto_1 = require("node:crypto");
@@ -130,6 +131,10 @@ function configPassiva(chave, valor, credenciaisInertes = false) {
130
131
  return valor.length <= 8192 && !valor.includes('\0');
131
132
  if (['user.name', 'user.email'].includes(chave))
132
133
  return !!valor.trim() && valor.length <= 512 && !/[\x00-\x1f]/.test(valor);
134
+ // RM-037 (defeitosdeco D-3): a seção do próprio produto (por exemplo `orkastery.cortefeito`, lida
135
+ // pela trava do corte no pre-push). O Git nunca lê essa seção, então ela não executa nada.
136
+ if (/^orkastery\.[a-z0-9-]+$/.test(chave))
137
+ return valor.length <= 512 && !/[\x00-\x1f\x7f]/.test(valor);
133
138
  if (['core.filemode', 'core.logallrefupdates', 'core.ignorecase', 'core.precomposeunicode'].includes(chave))
134
139
  return /^(true|false)$/.test(valor);
135
140
  if (chave === 'core.repositoryformatversion')
@@ -195,7 +200,22 @@ function atributosPassivos(wt, id, selecionados) {
195
200
  }
196
201
  return resultados.join('\0');
197
202
  }
198
- function perfil(wt, comum, gitdir, id, selecionados) {
203
+ /**
204
+ * RM-037 (defeitosdeco D-3): hooks que os comandos Git do status e do commit (`config`, `ls-files`,
205
+ * `check-attr`, `rev-parse`, `symbolic-ref`, `diff`, `diff-tree`, `add` e `commit` sem `--amend`)
206
+ * nunca invocam. Lista positiva: nome fora dela (`pre-commit`, `commit-msg`, `reference-transaction`,
207
+ * `post-index-change`, nome que o Git criar amanhã) continua recusado, e nada é desativado.
208
+ */
209
+ exports.HOOKS_INERTES_DO_COMMIT = new Set(['pre-push', 'pre-rebase', 'post-checkout', 'post-merge',
210
+ 'pre-receive', 'update', 'proc-receive', 'post-receive', 'post-update', 'push-to-checkout', 'applypatch-msg',
211
+ 'pre-applypatch', 'post-applypatch', 'sendemail-validate', 'pre-merge-commit', 'post-rewrite',
212
+ 'p4-changelist', 'p4-prepare-changelist', 'p4-post-changelist', 'p4-pre-submit']);
213
+ exports.HOOKS_DO_COMMIT = { inertes: exports.HOOKS_INERTES_DO_COMMIT,
214
+ correcao: 'o commit do MCP o executaria; commite pelo shell da worktree, onde o hook roda como sempre' };
215
+ /** Padrão estrito (SHIP): merge e push executariam qualquer hook, inclusive o pre-push. */
216
+ exports.HOOKS_ESTRITOS = { inertes: new Set(),
217
+ correcao: 'o SHIP do MCP o executaria (merge ou push); entregue por PR com o bundle do ork ci prepare, ou pelo ork ship do CLI' };
218
+ function perfil(wt, comum, gitdir, id, selecionados, hooksDaOperacao = exports.HOOKS_ESTRITOS) {
199
219
  fisico(path.dirname(comum), comum);
200
220
  fisico(comum, gitdir);
201
221
  for (const nome of ['objects', 'refs', 'logs', 'info'])
@@ -224,9 +244,11 @@ function perfil(wt, comum, gitdir, id, selecionados) {
224
244
  if (fs.lstatSync(hooks, { throwIfNoEntry: false })) {
225
245
  fisico(base, hooks);
226
246
  for (const nome of fs.readdirSync(hooks)) {
227
- const f = path.join(hooks, nome), s = fs.lstatSync(f);
228
- if (!s.isFile() || s.isSymbolicLink() || (!nome.endsWith('.sample') && (s.mode & 0o111) !== 0))
229
- falha('execution-profile.unsupported: hooks preservados');
247
+ const f = path.join(hooks, nome), s = fs.lstatSync(f), rotulo = nome.replace(/[^A-Za-z0-9._-]/g, '?').slice(0, 64);
248
+ if (!s.isFile() || s.isSymbolicLink())
249
+ falha(`execution-profile.unsupported: hooks preservados (${rotulo} nao e arquivo regular)`);
250
+ if (!nome.endsWith('.sample') && (s.mode & 0o111) !== 0 && !hooksDaOperacao.inertes.has(nome))
251
+ falha(`execution-profile.unsupported: hooks preservados (${rotulo}); ${hooksDaOperacao.correcao}`);
230
252
  }
231
253
  }
232
254
  }
@@ -259,7 +281,7 @@ function estadoGitMcp(raiz, threadId) {
259
281
  if (!match)
260
282
  falha('worktree.invalid');
261
283
  const comum = path.join(raiz, '.git'), gitdir = path.resolve(wt, match[1].trim());
262
- perfil(wt, comum, gitdir, t.id, []);
284
+ perfil(wt, comum, gitdir, t.id, [], exports.HOOKS_DO_COMMIT);
263
285
  if ((0, estado_thread_1.raizDoEstado)(wt) !== raiz)
264
286
  falha('worktree.invalid');
265
287
  const branch = (0, worktree_1.branchDaWorktree)(t), destino = c.manifesto.worktree.base_branch;
@@ -300,10 +322,10 @@ function executar(raiz, p) {
300
322
  fisico(comum, gitdir);
301
323
  if ((0, estado_thread_1.raizDoEstado)(wt) !== raiz)
302
324
  falha('worktree.invalid');
303
- const originalPerfil = perfil(wt, comum, gitdir, t.id, p.paths);
325
+ const originalPerfil = perfil(wt, comum, gitdir, t.id, p.paths, exports.HOOKS_DO_COMMIT);
304
326
  const conferir = () => {
305
327
  (0, mcp_artifacts_1.validarArquivosEstadoMcp)(raiz, t.id);
306
- if (perfil(wt, comum, gitdir, t.id, p.paths) !== originalPerfil)
328
+ if (perfil(wt, comum, gitdir, t.id, p.paths, exports.HOOKS_DO_COMMIT) !== originalPerfil)
307
329
  falha('execution-profile.changed');
308
330
  if (git(wt, ['rev-parse', 'HEAD']).trim() !== p.expectedHead)
309
331
  falha('head.stale');
@@ -344,7 +366,7 @@ function executar(raiz, p) {
344
366
  git(wt, ['commit', '-m', p.mensagem, '--', ...p.paths]);
345
367
  commit = git(wt, ['rev-parse', 'HEAD']).trim();
346
368
  });
347
- if (perfil(wt, comum, gitdir, t.id, p.paths) !== originalPerfil)
369
+ if (perfil(wt, comum, gitdir, t.id, p.paths, exports.HOOKS_DO_COMMIT) !== originalPerfil)
348
370
  falha('execution-profile.changed');
349
371
  estadoAuditado = (0, estado_thread_1.auditarEstado)(raiz, t.id, wt).nivel === 'ok';
350
372
  if (!estadoAuditado)
package/dist/preflight.js CHANGED
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.endurecerUmask = endurecerUmask;
36
37
  exports.umaskDoProcesso = umaskDoProcesso;
37
38
  exports.sondasDeAmbiente = sondasDeAmbiente;
38
39
  exports.checkDeContas = checkDeContas;
@@ -61,6 +62,18 @@ const sensoresPadrao = {
61
62
  sandboxCodex: () => (0, codex_2.sondarSandbox)().ok,
62
63
  auth: authPadrao,
63
64
  };
65
+ /**
66
+ * O sensor recusa fonte com escrita de grupo ou de outros (bits 0o022). O ork acrescenta esses bits
67
+ * ao umask do proprio processo, sem nunca afrouxar um umask mais restrito (077 continua 077): tudo o
68
+ * que ele cria, e tudo o que ele despacha (claude, codex), nasce sem escrita alheia, qualquer que
69
+ * seja o umask do shell de quem roda. Devolve o umask anterior e o que ficou valendo.
70
+ */
71
+ function endurecerUmask() {
72
+ const anterior = process.umask(0o077);
73
+ const atual = anterior | 0o022;
74
+ process.umask(atual);
75
+ return { anterior, atual };
76
+ }
64
77
  /** umask efetivo do processo, lido de /proc (Linux) para nao usar o getter deprecado. */
65
78
  function umaskDoProcesso() {
66
79
  try {
package/dist/ratelimit.js CHANGED
@@ -144,7 +144,7 @@ function enfileirar(carregado, novo, agoraMs = Date.now()) {
144
144
  * ela, a janela padrao do manifesto (estimativa declarada, nunca chute). Auth ausente: sem prazo.
145
145
  */
146
146
  function prazoDaConta(manifesto, falha, agoraMs = Date.now()) {
147
- if (falha.motivo === 'runtime.auth-missing')
147
+ if (falha.motivo !== 'runtime.quota-exhausted')
148
148
  return null;
149
149
  // A10: horario dito que ja passou nao devolve o perfil ao rodizio na hora: vale a janela padrao.
150
150
  if (falha.resetEm !== null && Date.parse(falha.resetEm) > agoraMs)
@@ -153,7 +153,8 @@ function prazoDaConta(manifesto, falha, agoraMs = Date.now()) {
153
153
  }
154
154
  /** Marca no store o perfil que falhou pela conta. Sem perfil (ambiente do processo), nada a marcar. */
155
155
  function marcarContaDaFalha(carregado, perfil, falha, agoraMs = Date.now()) {
156
- if (!perfil)
156
+ // RM-037 (defeitosdeco D-6): modelo inacessivel nao e falha da conta inteira; o perfil fica no rodizio.
157
+ if (!perfil || falha.motivo === 'runtime.model-unavailable')
157
158
  return null;
158
159
  const esgotado = falha.motivo === 'runtime.quota-exhausted';
159
160
  return (0, runtime_profiles_1.marcarFalhaDePerfil)(carregado.raiz, perfil.id, { estado: esgotado ? 'esgotado' : 'sem-auth',
package/dist/retry.js CHANGED
@@ -93,6 +93,7 @@ const policies_1 = require("./policies");
93
93
  const ratelimit_1 = require("./ratelimit");
94
94
  const session_watcher_claude_1 = require("./session-watcher-claude");
95
95
  const runtime_profiles_1 = require("./runtime-profiles");
96
+ const modos_1 = require("./modos");
96
97
  const thread_1 = require("./thread");
97
98
  const tokens_1 = require("./tokens");
98
99
  const util_1 = require("./util");
@@ -241,6 +242,15 @@ exports.POLITICA_DE_RETRY = {
241
242
  porque: 'a conta do runtime perdeu a autenticacao: o perfil nunca mais recebe despacho ate o login ser refeito pelo proprio CLI, e o MESMO prompt segue no proximo perfil ou no runtime de fallback',
242
243
  correcao: 'ork accounts check confere o login de cada perfil; ork retry run <thread> rotaciona o prompt ja gravado',
243
244
  },
245
+ // RM-037 (defeitosdeco D-6): o modelo e que falta, nao a conta. Repetir o mesmo modelo na mesma conta
246
+ // so repete a recusa; o perfil continua no rodizio para os outros modelos.
247
+ 'runtime.model-unavailable': {
248
+ motivo: 'runtime.model-unavailable',
249
+ acao: 'reexecutar',
250
+ automatica: true,
251
+ porque: 'o modelo pedido nao existe ou a conta nao tem acesso a ele: o MESMO prompt, com o mesmo sha256, segue no proximo perfil do mesmo runtime com o mesmo modelo e depois no fallback do bloco, nunca de novo no par que ja recusou',
252
+ correcao: 'ork retry run <thread> troca o destino; sem destino, ork setup <modo> --bloco N --model <modelo acessivel> ou --fallback runtime:modelo',
253
+ },
244
254
  'tree.blocked': {
245
255
  motivo: 'tree.blocked',
246
256
  acao: 'sincronizar-worktree',
@@ -1218,6 +1228,9 @@ function executarRetry(carregado, threadId, opcoes = {}) {
1218
1228
  if (plano.motivo === 'runtime.quota-exhausted' || plano.motivo === 'runtime.auth-missing') {
1219
1229
  return rotacionarConta(carregado, thread, plano, opcoes, vazio, registrarTentativa);
1220
1230
  }
1231
+ if (plano.motivo === 'runtime.model-unavailable') {
1232
+ return trocarDestinoDoModelo(carregado, thread, plano, opcoes, vazio, registrarTentativa);
1233
+ }
1221
1234
  // `reexecutar` e `escalar-esforco`: o MESMO prompt volta ao runtime.
1222
1235
  const falho = ultimoDespachoFalho(raiz, threadId, plano.fase, plano.motivo === 'runtime.silencio');
1223
1236
  if (!falho || typeof falho.promptPath !== 'string' || typeof falho.promptSha256 !== 'string') {
@@ -1337,7 +1350,10 @@ function rotacionarConta(carregado, thread, plano, opcoes, vazio, registrarTenta
1337
1350
  else
1338
1351
  implicitos.push(alvo.runtime);
1339
1352
  atual = { runtime: alvo.runtime, perfil: alvo.perfil };
1340
- falhaAtual = ultimo.falhaDeConta ?? falhaAtual;
1353
+ // GO-FIX (R4b): destino que recusou o MODELO sai da rotacao, mas a falha que manda (e o motivo da
1354
+ // fila) continua sendo a da conta; o modelo inacessivel tem troca propria (trocarDestinoDoModelo).
1355
+ if (ultimo.falhaDeConta?.motivo !== 'runtime.model-unavailable')
1356
+ falhaAtual = ultimo.falhaDeConta ?? falhaAtual;
1341
1357
  if (tentativas >= plano.limite) {
1342
1358
  return escalar('limite de tentativas na rotacao', `${tentativas} tentativa(s) de rotacao por ${motivo} em ${fase}, no limite de ` +
1343
1359
  `${plano.limite} do manifesto: escalacao tipada pausa qualquer modo`);
@@ -1369,6 +1385,168 @@ function rotacionarConta(carregado, thread, plano, opcoes, vazio, registrarTenta
1369
1385
  registrarTentativa(true, `fila duravel: pedido ${pedido.id} libera em ${pedido.liberaEm}`);
1370
1386
  return { ...vazio, executada: true, fila: [pedido], redespacho: ultimo, detalhe: `pedido ${pedido.id} na fila ate ${pedido.liberaEm}` };
1371
1387
  }
1388
+ /** RM-037 (defeitosdeco D-6): chave de um destino (runtime, perfil, modelo) que recusou o modelo. */
1389
+ function chaveDoModelo(runtime, perfil, model) {
1390
+ return `${runtime}|${perfil ?? ''}|${model ?? ''}`;
1391
+ }
1392
+ /**
1393
+ * D-6: todo destino que ja recusou o modelo nesta fase, lido do ledger. O gate do observador traz o
1394
+ * `sessionId` (o modelo vem do `phase_dispatch` dele); o do despacho sincrono, o ultimo
1395
+ * `phase_dispatch_failed` da fase antes dele.
1396
+ */
1397
+ function destinosQueRecusaramOModelo(eventos, fase) {
1398
+ const chaves = [];
1399
+ const chaveDo = (origem) => {
1400
+ if (typeof origem.runtime !== 'string')
1401
+ return null;
1402
+ let perfil = null;
1403
+ try {
1404
+ perfil = perfilDoEvento(origem);
1405
+ }
1406
+ catch { /* perfil ilegivel nao exclui ninguem */ }
1407
+ return chaveDoModelo(origem.runtime, perfil?.id ?? null, typeof origem.model === 'string' ? origem.model : null);
1408
+ };
1409
+ eventos.forEach((e, i) => {
1410
+ // GO-FIX (R4a): a recusa sincrona do redespacho (nada rodou) so grava `phase_dispatch_failed`.
1411
+ if (e.tipo === ledger_1.TIPOS_DE_EVENTO.despachoFalhou && e.fase === fase && String(e.erro ?? '').startsWith('runtime.model-unavailable')) {
1412
+ const chave = chaveDo(e);
1413
+ if (chave)
1414
+ chaves.push(chave);
1415
+ return;
1416
+ }
1417
+ if (e.tipo !== ledger_1.TIPOS_DE_EVENTO.gateBloqueado || e.fase !== fase || e.motivo !== 'runtime.model-unavailable')
1418
+ return;
1419
+ const origem = typeof e.sessionId === 'string'
1420
+ ? eventos.filter(d => d.tipo === ledger_1.TIPOS_DE_EVENTO.faseDespachada && d.sessionId === e.sessionId).at(-1)
1421
+ : eventos.slice(0, i).filter(d => d.tipo === ledger_1.TIPOS_DE_EVENTO.despachoFalhou && d.fase === fase).at(-1);
1422
+ const chave = origem ? chaveDo(origem) : null;
1423
+ if (chave)
1424
+ chaves.push(chave);
1425
+ });
1426
+ return chaves;
1427
+ }
1428
+ /**
1429
+ * D-6: o proximo destino do MESMO prompt depois de `runtime.model-unavailable`. Primeiro outro perfil do
1430
+ * mesmo runtime com o MESMO modelo (outra conta pode ter acesso), depois cada runtime da ordem de
1431
+ * fallback do bloco com o modelo dele. Perfil fora do rodizio (esgotado, sem login, desativado, pago)
1432
+ * nao entra, e nenhum destino que ja recusou volta. Sem destino, `null`.
1433
+ */
1434
+ function proximoDestinoDoModelo(raiz, runtime, model, fallback, recusados) {
1435
+ const store = (0, runtime_profiles_1.lerPerfisComContas)(raiz);
1436
+ const utilizavel = (p) => !['desativado', 'provider-pago', 'sem-auth'].includes(p.estado) && (0, runtime_profiles_1.perfilDisponivel)(p);
1437
+ const mesmo = (0, runtime_profiles_1.perfisDoRuntime)(store, runtime).find(p => utilizavel(p) && !recusados.has(chaveDoModelo(runtime, p.id, model)));
1438
+ if (mesmo)
1439
+ return { runtime, perfil: (0, runtime_profiles_1.perfilDeDespacho)(mesmo), model, effort: null };
1440
+ for (const f of fallback) {
1441
+ const perfis = (0, runtime_profiles_1.perfisDoRuntime)(store, f.runtime);
1442
+ if (perfis.length) {
1443
+ const p = perfis.find(x => utilizavel(x) && !recusados.has(chaveDoModelo(f.runtime, x.id, f.model)));
1444
+ if (p)
1445
+ return { runtime: f.runtime, perfil: (0, runtime_profiles_1.perfilDeDespacho)(p), model: f.model, effort: f.effort ?? null };
1446
+ continue;
1447
+ }
1448
+ if (!recusados.has(chaveDoModelo(f.runtime, null, f.model)))
1449
+ return { runtime: f.runtime, perfil: null, model: f.model, effort: f.effort ?? null };
1450
+ }
1451
+ return null;
1452
+ }
1453
+ /** D-6: a correcao exata quando nenhum destino tem o modelo: o bloco do setup que conduz a fase. */
1454
+ function correcaoDoModelo(thread, fase) {
1455
+ const indice = modos_1.MODOS[thread.modo]?.blocos.findIndex(b => b.fases.includes(fase)) ?? -1;
1456
+ const bloco = indice >= 0 ? String(indice + 1) : 'N';
1457
+ return `ork setup ${thread.modo} --bloco ${bloco} --model <modelo acessivel>, ou declare no mesmo bloco --fallback runtime:modelo`;
1458
+ }
1459
+ /**
1460
+ * RM-037 (defeitosdeco D-6): troca de destino depois de `runtime.model-unavailable`. O modelo pedido nao
1461
+ * existe ou a conta nao tem acesso a ele (`model_not_found` na transcricao); o perfil continua no
1462
+ * rodizio e o MESMO prompt segue no proximo destino (`proximoDestinoDoModelo`). Producao no intervalo
1463
+ * escala ao humano, como na rotacao por conta. Recusa do modelo no redespacho (nada rodou) continua a
1464
+ * troca na mesma chamada, dentro do limite de tentativas. Sem destino, o humano recebe a correcao exata.
1465
+ */
1466
+ function trocarDestinoDoModelo(carregado, thread, plano, opcoes, vazio, registrarTentativa) {
1467
+ const { raiz, manifesto } = carregado;
1468
+ const dir = (0, thread_1.dirThread)(raiz, thread.id);
1469
+ const fase = plano.fase;
1470
+ const motivo = 'runtime.model-unavailable';
1471
+ const eventos = (0, ledger_1.lerLedger)(dir);
1472
+ const gate = ultimaReprovacao(raiz, thread.id);
1473
+ const despacho = despachoDaFalha(eventos, gate, fase);
1474
+ const escalar = (razao, diagnostico) => {
1475
+ if (!opcoes.dryRun) {
1476
+ (0, gates_1.registrarGateBloqueado)(dir, thread.id, { gate: 'phase.dispatch', motivo: 'human.pending', modo: thread.modo, detalhe: diagnostico,
1477
+ diagnostico, correcao: `ork gate request ${thread.id}`, fase, origem: 'retry.modelo', pausaQualquerModo: true });
1478
+ (0, ledger_1.registrar)(dir, thread.id, ledger_1.TIPOS_DE_EVENTO.retryEscalado, { fase, motivo, origem: 'retry.modelo', acao: plano.acao,
1479
+ tentativas: plano.tentativas, limite: plano.limite, modo: thread.modo, razao });
1480
+ }
1481
+ return { ...vazio, detalhe: diagnostico };
1482
+ };
1483
+ if (!despacho || typeof despacho.promptPath !== 'string' || typeof despacho.promptSha256 !== 'string') {
1484
+ const detalhe = `nao ha despacho com prompt gravado para ${motivo} em ${fase}: rode a fase de novo com ork phase run`;
1485
+ registrarTentativa(false, detalhe);
1486
+ return { ...vazio, detalhe };
1487
+ }
1488
+ const runtime = typeof despacho.runtime === 'string' ? despacho.runtime : manifesto.runtime.adapter;
1489
+ const model = typeof despacho.model === 'string' ? despacho.model : null;
1490
+ const rotulo = model ?? '(padrao do runtime)';
1491
+ let perfil;
1492
+ try {
1493
+ perfil = typeof gate?.sessionId === 'string'
1494
+ ? (0, runtime_profiles_1.perfilDoRegistro)(eventos, { sessionId: gate.sessionId, despachadaEm: typeof gate.despachoEm === 'string' ? gate.despachoEm : undefined })
1495
+ : perfilDoEvento(despacho);
1496
+ }
1497
+ catch (e) {
1498
+ return escalar('perfil registrado invalido', `troca de destino recusada: ${e.message}`);
1499
+ }
1500
+ if (typeof gate?.sessionId === 'string' && typeof gate.despachoEm === 'string') {
1501
+ const producao = producaoNoIntervalo(raiz, thread, { sessionId: gate.sessionId, fase, despachadaEm: gate.despachoEm }, eventos);
1502
+ if (producao.produziu) {
1503
+ return escalar('producao parcial com modelo inacessivel', `producao parcial com o modelo ${rotulo} inacessivel: ${producao.evidencia}; ` +
1504
+ 'a sessao nao e redespachada sobre a worktree alterada e a fase nao conclui sem o humano');
1505
+ }
1506
+ }
1507
+ const recusados = new Set([...destinosQueRecusaramOModelo(eventos, fase), chaveDoModelo(runtime, perfil?.id ?? null, model)]);
1508
+ const doBloco = (0, setup_1.configDoBloco)((0, setup_1.lerSetup)(raiz), thread.modo, fase);
1509
+ const fallback = alvosDeFallback(doBloco, doBloco?.runtime ?? manifesto.runtime.adapter);
1510
+ let atual = { runtime, perfil, model };
1511
+ let tentativas = plano.tentativas;
1512
+ for (;;) {
1513
+ const alvo = proximoDestinoDoModelo(raiz, runtime, model, fallback, recusados);
1514
+ if (!alvo)
1515
+ break;
1516
+ if (!opcoes.dryRun)
1517
+ (0, ledger_1.registrar)(dir, thread.id, ledger_1.TIPOS_DE_EVENTO.perfilRotacionado, { fase, motivo, origem: 'retry.modelo',
1518
+ de: { runtime: atual.runtime, perfil: atual.perfil?.id ?? null, model: atual.model },
1519
+ para: { runtime: alvo.runtime, perfil: alvo.perfil?.id ?? null, model: alvo.model },
1520
+ razao: alvo.runtime === runtime ? 'outro perfil do mesmo runtime com o mesmo modelo' : 'fallback de runtime pela ordem do bloco',
1521
+ promptSha256: despacho.promptSha256, modo: thread.modo,
1522
+ autorizadoPor: `politica de retry da RM-037 (D-6), bloco sem pausa ou delegado no modo ${thread.modo}` });
1523
+ const mudouRuntime = alvo.runtime !== runtime;
1524
+ const r = redespachar(carregado, thread, fase, despacho.promptPath, despacho.promptSha256, {
1525
+ runtime: alvo.runtime, perfil: alvo.perfil?.id ?? null, model: alvo.model,
1526
+ effort: mudouRuntime ? alvo.effort ?? (typeof despacho.effort === 'string' ? despacho.effort : null) : plano.effort,
1527
+ origem: 'retry.modelo', dryRun: opcoes.dryRun,
1528
+ });
1529
+ if (opcoes.dryRun)
1530
+ return { ...vazio, redespacho: r, detalhe: `ensaio: o mesmo prompt iria a ${alvo.runtime}` +
1531
+ `${alvo.perfil ? ` (perfil ${alvo.perfil.id})` : ''} com o modelo ${alvo.model ?? '(padrao do runtime)'}; ${r.detalhe}` };
1532
+ registrarTentativa(r.ok, r.detalhe);
1533
+ tentativas += 1;
1534
+ if (r.ok)
1535
+ return { ...vazio, executada: true, redespacho: r, detalhe: r.detalhe };
1536
+ // So a recusa do MODELO no redespacho (nada rodou) continua a troca; outra falha para aqui.
1537
+ if (r.falhaDeConta?.motivo !== 'runtime.model-unavailable')
1538
+ return { ...vazio, redespacho: r, detalhe: r.detalhe };
1539
+ recusados.add(chaveDoModelo(alvo.runtime, alvo.perfil?.id ?? null, alvo.model));
1540
+ atual = { runtime: alvo.runtime, perfil: alvo.perfil, model: alvo.model };
1541
+ if (tentativas >= plano.limite) {
1542
+ return escalar('limite de tentativas na troca de modelo', `${tentativas} tentativa(s) de troca por ${motivo} em ${fase}, no limite de ` +
1543
+ `${plano.limite} do manifesto: escalacao tipada pausa qualquer modo`);
1544
+ }
1545
+ }
1546
+ const tentados = [...recusados].map(k => { const [rt, p, m] = k.split('|'); return `${rt}${p ? `/${p}` : ''}:${m || '(padrao)'}`; });
1547
+ return escalar('modelo inacessivel sem outro destino', `o modelo ${rotulo} nao esta acessivel em ${tentados.join(', ')} e o bloco ` +
1548
+ `nao tem outro destino: ${correcaoDoModelo(thread, fase)}`);
1549
+ }
1372
1550
  /** Texto de `ork retry plan`. */
1373
1551
  function textoDoPlano(p) {
1374
1552
  const linhas = [];
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ENVS_DE_PROVIDER_PAGO = void 0;
3
+ exports.ENV_THREAD_DO_DESPACHO = exports.ENV_IDENTIDADE_DE_DESPACHO = exports.ENVS_DE_PROVIDER_PAGO = void 0;
4
4
  exports.nomesDeProviderAtivos = nomesDeProviderAtivos;
5
5
  exports.ambienteDeAssinatura = ambienteDeAssinatura;
6
6
  const hitl_public_receipt_1 = require("./hitl-public-receipt");
@@ -20,6 +20,13 @@ exports.ENVS_DE_PROVIDER_PAGO = [
20
20
  function nomesDeProviderAtivos(base = process.env) {
21
21
  return exports.ENVS_DE_PROVIDER_PAGO.filter(nome => (base[nome] ?? '').trim() !== '');
22
22
  }
23
+ /**
24
+ * Identidade do despacho no ambiente da sessao filha (I-36, D4). RM-037 (defeitosdeco D-4): o domicilio
25
+ * dos nomes e aqui porque nenhum filho de runtime herda a identidade de quem despachou; o despacho
26
+ * acrescenta a sua explicitamente, e o `claude --bg` a leva por sessao em `--settings`.
27
+ */
28
+ exports.ENV_IDENTIDADE_DE_DESPACHO = 'ORK_DISPATCH_ID';
29
+ exports.ENV_THREAD_DO_DESPACHO = 'ORK_DISPATCH_THREAD';
23
30
  /** Copia, sem alterar o ambiente em que as policies ainda precisam ser avaliadas. */
24
31
  function ambienteDeAssinatura(base = process.env) {
25
32
  const env = { ...base };
@@ -31,6 +38,10 @@ function ambienteDeAssinatura(base = process.env) {
31
38
  for (const nome of Object.keys(env))
32
39
  if (nome.startsWith('ORK_HITL_'))
33
40
  delete env[nome];
41
+ // RM-037 (defeitosdeco D-4): a identidade herdada e de OUTRO despacho (o daemon do `claude --bg`
42
+ // guardava a do primeiro despacho e a passava a todas as sessoes reserva da conta).
43
+ delete env[exports.ENV_IDENTIDADE_DE_DESPACHO];
44
+ delete env[exports.ENV_THREAD_DO_DESPACHO];
34
45
  delete env[hitl_public_receipt_1.ENV_HITL_VERIFIERS];
35
46
  if (publicVerifiers)
36
47
  env[hitl_public_receipt_1.ENV_HITL_VERIFIERS] = publicVerifiers;
@@ -158,6 +158,20 @@ function classificarSessaoClaude(e) {
158
158
  : { terminal: true, classificacao: 'fase_concluida', motivo: null, fonte: `${sinal}; ${prova.fonte}`, conclusaoNativa, stop };
159
159
  return { ...c, dependeDoStop: true };
160
160
  };
161
+ // RM-037 (defeitosdeco D-1): turno encerrado pelo Stop e sessão viva em `blocked` é o Claude Code
162
+ // dizendo que o texto final pediu resposta. Nunca conclui direto, e a pausa humana abre sempre
163
+ // (`human.pending`): GO-FIX (R6 do CHECK), sem a prova o motivo também é o humano, porque um
164
+ // `artifact.missing` automático abriria correção sobre uma sessão viva que espera resposta.
165
+ const esperaHumana = () => {
166
+ const sinal = 'Stop correlacionado e sessão viva à espera humana (blocked)';
167
+ const prova = e.prova();
168
+ const c = !prova.ok
169
+ ? { ...bloqueio(`${sinal}; sem prova do ork: ${prova.fonte}; o humano decide`, 'human.pending'),
170
+ diagnostico: `a sessão espera resposta humana (blocked) e a fase ainda não tem a prova do ork: ${prova.fonte}` }
171
+ : { ...bloqueio(`${sinal}; ${prova.fonte}; o humano decide`, 'human.pending'),
172
+ diagnostico: 'a fase tem a prova do ork e a sessão espera resposta humana (blocked): o humano decide' };
173
+ return { ...c, dependeDoStop: true };
174
+ };
161
175
  const venceu = (desde) => agora - desde >= e.limiteMs;
162
176
  // Sem prova para concluir nem para bloquear: `runtime.unavailable` reexecutaria o mesmo prompt
163
177
  // sobre uma fase que pode ter terminado. O observador encerra sem gate e o radar de liveness
@@ -183,6 +197,8 @@ function classificarSessaoClaude(e) {
183
197
  delete cursor.doneDesde;
184
198
  if (estado !== 'working' && estado !== 'blocked')
185
199
  delete cursor.mortoDesde;
200
+ if (estado !== 'blocked')
201
+ delete cursor.bloqueadoDesde;
186
202
  if (['done', 'failed', 'stopped', 'working', 'blocked'].includes(estado ?? ''))
187
203
  delete cursor.desconhecidoDesde;
188
204
  switch (estado) {
@@ -220,6 +236,16 @@ function classificarSessaoClaude(e) {
220
236
  return bloqueio(`processo da sessão morreu sem terminal nativo nem Stop correlacionado (estado ${estado})`);
221
237
  }
222
238
  delete cursor.mortoDesde;
239
+ // D-1: a segunda leitura, com pelo menos um intervalo do observador, separa o `blocked` estável
240
+ // de um instante de transição logo depois do Stop. `status` ocupado ainda é trabalho.
241
+ const ocupado = ['busy', 'running', 'working'].includes(r.status ?? '');
242
+ if (estado === 'blocked' && stop && !bloqueioPendente(e.eventos, e.sessao) && !ocupado) {
243
+ cursor.bloqueadoDesde ??= agora;
244
+ if (agora - cursor.bloqueadoDesde < exports.INTERVALO_WATCH_CLAUDE_MS)
245
+ return espera('turno encerrado pelo Stop com a sessão à espera humana (blocked); aguardando a segunda observação');
246
+ return esperaHumana();
247
+ }
248
+ delete cursor.bloqueadoDesde;
223
249
  return espera(estado === 'blocked' ? 'sessão bloqueada à espera humana' : 'sessão trabalhando');
224
250
  }
225
251
  default:
@@ -183,6 +183,11 @@ function comLockWatcher(dir, action) {
183
183
  }
184
184
  }
185
185
  }
186
+ /**
187
+ * `agoraMs` só vem quando o relógio é injetado. Sem ele, a referência de "evento do futuro" é o
188
+ * relógio depois da leitura: a linha que o runtime anexou durante a observação não é futura, e
189
+ * descartá-la perdia o terminal do turno.
190
+ */
186
191
  function lerFonte(file, anterior, agoraMs, sessionId, fixada) {
187
192
  const fd = fs.openSync(file, fixada
188
193
  ? fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK : 'r');
@@ -206,6 +211,7 @@ function lerFonte(file, anterior, agoraMs, sessionId, fixada) {
206
211
  let offset = mesmo ? anterior.offset : 0;
207
212
  const buffer = Buffer.alloc(Math.min(exports.BLOCO_WATCH_BYTES, stat.size - offset));
208
213
  const n = fs.readSync(fd, buffer, 0, buffer.length, offset);
214
+ const agora = agoraMs ?? Date.now();
209
215
  const fim = buffer.subarray(0, n).lastIndexOf(10);
210
216
  let terminal = mesmo ? anterior.terminal : null;
211
217
  if (fim >= 0) {
@@ -221,7 +227,7 @@ function lerFonte(file, anterior, agoraMs, sessionId, fixada) {
221
227
  (e?.type === 'session_meta' && e.payload?.id !== sessionId))
222
228
  throw new Error('log/rollout pertence a outra sessão');
223
229
  }
224
- for (const t of parser.push(buffer.subarray(0, fim + 1), agoraMs))
230
+ for (const t of parser.push(buffer.subarray(0, fim + 1), agora))
225
231
  terminal = t;
226
232
  offset += fim + 1;
227
233
  }
@@ -235,7 +241,7 @@ function lerFonte(file, anterior, agoraMs, sessionId, fixada) {
235
241
  const parcial = n < exports.BLOCO_WATCH_BYTES && stat.size > offset;
236
242
  return { cursor: { file, ino: stat.ino, tamanho: stat.size, offset, parser: parser.estado, terminal, parcial },
237
243
  cresceu: parser.eventosValidos > 0,
238
- quando: Math.min(agoraMs, stat.mtimeMs), bytes: n, temMais: stat.size > offset };
244
+ quando: Math.min(agora, stat.mtimeMs), bytes: n, temMais: stat.size > offset };
239
245
  }
240
246
  finally {
241
247
  fs.closeSync(fd);
@@ -352,7 +358,8 @@ function observarSessao(carregado, sessionId, opcoes = {}) {
352
358
  fixacao: path.join(dir, 'sessoes', `watcher-source-${chave}.json`),
353
359
  vinculo: { thread: thread.id, fase: sessao.fase, promptSha256: sessao.promptSha256 }
354
360
  });
355
- snapshot = (0, codex_controller_sensor_1.lerSnapshotController)(fonteControlador, { agoraMs: now });
361
+ // Relógio injetado vale como está; o real é lido pelo sensor depois do state.json.
362
+ snapshot = (0, codex_controller_sensor_1.lerSnapshotController)(fonteControlador, opcoes.agoraMs === undefined ? {} : { agoraMs: now });
356
363
  }
357
364
  const file = path.join(pasta, 'cursor.json');
358
365
  const cursor = lerJson(file) ?? { schema: 'ork.session-cursor/v1', sessionId,
@@ -370,7 +377,7 @@ function observarSessao(carregado, sessionId, opcoes = {}) {
370
377
  continue;
371
378
  let leitura;
372
379
  try {
373
- leitura = lerFonte(fonte, cursor.fontes[nome], now, sessionId, nome === 'rollout' ? fonteControlador ?? undefined : undefined);
380
+ leitura = lerFonte(fonte, cursor.fontes[nome], opcoes.agoraMs === undefined ? undefined : now, sessionId, nome === 'rollout' ? fonteControlador ?? undefined : undefined);
374
381
  }
375
382
  catch (e) {
376
383
  if (nome !== 'rollout' || e.message !== 'log/rollout pertence a outra sessão')
package/dist/sessoes.js CHANGED
@@ -46,6 +46,7 @@ exports.pararSessao = pararSessao;
46
46
  exports.comandoDeAttach = comandoDeAttach;
47
47
  const adapter = __importStar(require("./adapters/claude-bg"));
48
48
  const sessoes_inventario_1 = require("./sessoes-inventario");
49
+ const runtime_profiles_1 = require("./runtime-profiles");
49
50
  const util_1 = require("./util");
50
51
  function textoDoInventario(r) {
51
52
  const linhas = [
@@ -63,23 +64,56 @@ function textoDoInventario(r) {
63
64
  function tabelaDeSessoes(raiz, todas = false) {
64
65
  return textoDoInventario((0, sessoes_inventario_1.inventariarSessoes)(raiz, { todas }));
65
66
  }
66
- /** Logs de uma sessao pelo id curto, id completo ou nome. */
67
- function logsDaSessao(chave, linhas) {
68
- const r = adapter.logs(chave, linhas);
67
+ /**
68
+ * RM-037 (defeitosdeco D-2): a sessao pelo id curto, id completo ou nome, procurada no ambiente do
69
+ * processo e em cada perfil claude-bg do projeto (inclusive desativado, que ainda pode ter sessao
70
+ * viva). So uma conta pode responder; duas viram recusa, nenhuma mantem a mensagem de antes.
71
+ */
72
+ function localizarSessao(chave, raiz) {
73
+ let perfis = [];
74
+ const falhas = [];
75
+ if (raiz) {
76
+ try {
77
+ perfis = (0, runtime_profiles_1.lerPerfis)(raiz).perfis.filter(p => p.runtime === 'claude-bg').map(runtime_profiles_1.perfilDeDespacho);
78
+ }
79
+ catch (e) {
80
+ falhas.push(`perfis: ${e.message}`);
81
+ }
82
+ }
83
+ const r = adapter.acharSessaoNasContas(chave, perfis);
84
+ falhas.push(...r.falhas);
85
+ if (r.achadas.length === 1)
86
+ return { achada: r.achadas[0] };
87
+ if (r.achadas.length > 1) {
88
+ const contas = r.achadas.map(a => `${a.perfil?.id ?? 'processo'} (${a.sessao.sessionId})`).join(', ');
89
+ return { texto: `sessao "${chave}" encontrada em mais de uma conta: ${contas}; use o id completo`, codigo: 1 };
90
+ }
91
+ return { texto: `sessao "${chave}" nao encontrada no runtime` + (falhas.length ? `; contas sem resposta: ${falhas.join('; ')}` : ''),
92
+ codigo: 1 };
93
+ }
94
+ /** Logs de uma sessao pelo id curto, id completo ou nome, lidos na conta onde ela esta. */
95
+ function logsDaSessao(chave, linhas, raiz) {
96
+ const l = localizarSessao(chave, raiz);
97
+ if (!('achada' in l))
98
+ return l;
99
+ const { sessao, ambiente } = l.achada;
100
+ const r = adapter.logsDaSessao(sessao.id ?? sessao.sessionId.slice(0, 8), linhas, ambiente);
69
101
  return { texto: r.texto, codigo: r.ok ? 0 : 1 };
70
102
  }
71
- /** Para uma sessao pelo id curto, id completo ou nome. */
72
- function pararSessao(chave) {
73
- const s = adapter.acharSessao(chave);
74
- if (!s)
75
- return { texto: `sessao "${chave}" nao encontrada no runtime`, codigo: 1 };
76
- const r = adapter.parar(s.id ?? s.sessionId);
77
- return { texto: r.texto || `sessao ${s.sessionId} parada`, codigo: r.ok ? 0 : 1 };
103
+ /** Para uma sessao pelo id curto, id completo ou nome, com o `CLAUDE_CONFIG_DIR` da conta dela. */
104
+ function pararSessao(chave, raiz) {
105
+ const l = localizarSessao(chave, raiz);
106
+ if (!('achada' in l))
107
+ return l;
108
+ const { sessao, perfil, ambiente } = l.achada;
109
+ const r = adapter.pararAchada(sessao, ambiente);
110
+ return { texto: r.texto || `sessao ${sessao.sessionId} parada${perfil ? ` (perfil ${perfil.id})` : ''}`, codigo: r.ok ? 0 : 1 };
78
111
  }
79
112
  /** Comando de attach (precisa de TTY, entao o CLI imprime em vez de executar). */
80
- function comandoDeAttach(chave) {
81
- const s = adapter.acharSessao(chave);
82
- if (!s)
83
- return { texto: `sessao "${chave}" nao encontrada no runtime`, codigo: 1 };
84
- return { texto: adapter.comandoAttach(s.sessionId), codigo: 0 };
113
+ function comandoDeAttach(chave, raiz) {
114
+ const l = localizarSessao(chave, raiz);
115
+ if (!('achada' in l))
116
+ return l;
117
+ const { sessao, perfil, configDir } = l.achada;
118
+ return { texto: adapter.comandoAttach(sessao.sessionId, perfil ? configDir : null), codigo: 0 };
85
119
  }
package/dist/verify.js CHANGED
@@ -191,7 +191,7 @@ function pedidoDoVerify(carregado, threadId, operacao, comandos, conducao) {
191
191
  canal: conducao.canal ?? (0, conducao_1.canalDoProcesso)(),
192
192
  correlacao: conducao.correlacao ?? null,
193
193
  operacao,
194
- identidade: conducao.identidade ?? (0, conducao_1.identidadeDoAmbiente)(threadId),
194
+ identidade: conducao.identidade ?? (0, conducao_1.identidadeDoAmbiente)(threadId, process.env, carregado.raiz),
195
195
  prazoMs: (0, conducao_1.prazoDaVerificacao)(carregado.manifesto.verify.timeout_ms ?? exports.TIMEOUT_VERIFY_MS, comandos),
196
196
  esperarMs: conducao.esperarMs,
197
197
  };
package/dist/worktree.js CHANGED
@@ -52,6 +52,7 @@ exports.worktreesDoGit = worktreesDoGit;
52
52
  exports.registradaNoGit = registradaNoGit;
53
53
  exports.arvoreLimpa = arvoreLimpa;
54
54
  exports.garantirWorktree = garantirWorktree;
55
+ exports.historiaPropria = historiaPropria;
55
56
  exports.sincronizarWorktree = sincronizarWorktree;
56
57
  exports.auditarWorktree = auditarWorktree;
57
58
  exports.textoDoAudit = textoDoAudit;
@@ -189,6 +190,34 @@ function garantirWorktree(carregado, id) {
189
190
  correcao: '',
190
191
  };
191
192
  }
193
+ /**
194
+ * RM-037 (defeitosdeco D-5): o que a branch tem de proprio. Bases conhecidas da thread sao o
195
+ * `base.commit` carimbado na criacao e cada `shaBase` que um sync ja incorporou (lidos do ledger);
196
+ * commit que o git nao tem mais nao entra. `proprios` conta o que o HEAD tem fora delas e da base
197
+ * atual (`null` sem nenhuma base conhecida); `pontoDePartida` e a base conhecida mais nova que e
198
+ * ancestral do HEAD; `semAncestral` diz que HEAD e base atual nao tem historia em comum.
199
+ */
200
+ function historiaPropria(raiz, thread, dir, shaBase) {
201
+ const git = (...args) => (0, util_1.exec)('git', args, dir);
202
+ const sincronizadas = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, thread.id))
203
+ .filter(e => e.tipo === ledger_1.TIPOS_DE_EVENTO.worktreeSincronizada && typeof e.shaBase === 'string' && e.shaBase !== '')
204
+ .map(e => String(e.shaBase));
205
+ // GO-FIX (R1): no ciclo `merge-branch`, `base.commit` e a ponta da branch que ja existia, com os
206
+ // commits proprios dela. Nao e base: vale so o ponto onde ela sai da base atual, quando existe.
207
+ const inicio = thread.base?.commit;
208
+ const valido = (sha) => typeof sha === 'string' && /^[a-f0-9]{40,64}$/.test(sha) &&
209
+ git('cat-file', '-e', `${sha}^{commit}`).ok;
210
+ const pontoDaCriacao = thread.variante !== 'merge-branch' ? inicio
211
+ : valido(inicio) ? (git('merge-base', inicio, shaBase).stdout.trim() || null) : null;
212
+ const conhecidas = [...new Set([pontoDaCriacao, ...sincronizadas])].filter(valido);
213
+ const semAncestral = git('merge-base', 'HEAD', shaBase).code === 1;
214
+ const pontoDePartida = [...conhecidas].reverse().find(sha => git('merge-base', '--is-ancestor', sha, 'HEAD').ok) ?? null;
215
+ if (!conhecidas.length)
216
+ return { proprios: null, pontoDePartida, semAncestral };
217
+ const contagem = git('rev-list', '--count', 'HEAD', '--not', ...conhecidas, shaBase);
218
+ const proprios = contagem.ok && /^\d+$/.test(contagem.stdout.trim()) ? Number(contagem.stdout.trim()) : null;
219
+ return { proprios, pontoDePartida, semAncestral };
220
+ }
192
221
  /**
193
222
  * Sincroniza a branch da thread com a base ATUAL (rebase), quando a base avancou.
194
223
  *
@@ -287,6 +316,54 @@ function sincronizarWorktree(carregado, id, opcoes = {}) {
287
316
  correcao: '',
288
317
  };
289
318
  }
319
+ // RM-037 (defeitosdeco D-5): depois do corte de 27/09 a base tem raiz nova. Branch sem commit
320
+ // proprio nao e rebasada: o rebase sem ancestral comum reaplicaria a historia antiga inteira. Ela
321
+ // e recriada no SHA da base com `reset --keep`, que aborta se houvesse mudanca local a perder (a
322
+ // arvore ja foi conferida limpa). Com commit proprio e sem ancestral comum, a branch fica como esta
323
+ // e a correcao exata reaplica so o que e dela.
324
+ const historia = historiaPropria(raiz, thread, dir, shaBase);
325
+ if (historia.proprios === 0) {
326
+ passos.push(`git reset --keep ${shaBase}`);
327
+ if (opcoes.dryRun) {
328
+ return { ...vazio, shaBase, shaAntes, shaDepois: shaAntes, ok: true, jaAtualizada: false, rebaseFeito: false, recriada: false,
329
+ motivo: null, detalhe: `a branch ${branch} nao tem commit proprio: o sync real a recriaria na base ${base} atual, sem rebase`,
330
+ correcao: '' };
331
+ }
332
+ let recriacao;
333
+ try {
334
+ recriacao = (0, estado_thread_1.comEstadoParaGit)(raiz, id, dir, () => (0, util_1.exec)('git', ['reset', '--keep', shaBase], dir));
335
+ }
336
+ catch (e) {
337
+ return { ...vazio, shaBase, shaAntes, ok: false, jaAtualizada: false, rebaseFeito: false,
338
+ motivo: 'tree.blocked', detalhe: e.message,
339
+ correcao: 'inspecione a cópia local e a fonte canônica preservadas antes de repetir o sync' };
340
+ }
341
+ const shaDepois = (0, util_1.exec)('git', ['rev-parse', 'HEAD'], dir).stdout.trim();
342
+ if (!recriacao.ok || shaDepois !== shaBase) {
343
+ return { ...vazio, shaBase, shaAntes, shaDepois, ok: false, jaAtualizada: false, rebaseFeito: false, motivo: 'tree.blocked',
344
+ detalhe: `a recriacao da branch ${branch} na base falhou: ${(recriacao.stderr || recriacao.stdout).trim().split('\n').slice(-2).join(' | ')}`,
345
+ correcao: `confira a worktree ${dir} e rode de novo: ork worktree sync ${id}` };
346
+ }
347
+ const detalhe = historia.semAncestral
348
+ ? `branch sem commit proprio recriada a partir da base ${base} atual; a base foi reescrita e nao tinha historia em comum com ela`
349
+ : `branch sem commit proprio recriada a partir da base ${base} atual`;
350
+ (0, ledger_1.registrar)((0, thread_1.dirThread)(raiz, id), id, ledger_1.TIPOS_DE_EVENTO.worktreeSincronizada, { dir, branch, base, shaBase, shaAntes, shaDepois,
351
+ rebaseFeito: false, recriada: true, semAncestral: historia.semAncestral, detalhe });
352
+ return { ...vazio, shaBase, shaAntes, shaDepois, ok: true, jaAtualizada: false, rebaseFeito: false, recriada: true,
353
+ motivo: null, detalhe, correcao: '' };
354
+ }
355
+ if (historia.semAncestral) {
356
+ const onto = historia.pontoDePartida
357
+ ? `git -C ${dir} rebase --onto ${shaBase} ${historia.pontoDePartida}`
358
+ : `git -C ${dir} rebase --onto ${shaBase} <commit onde a branch da thread comecou>`;
359
+ const detalhe = `a base ${base} foi reescrita e nao tem historia em comum com a branch ${branch}, que tem ` +
360
+ `${historia.proprios ?? 'um numero desconhecido de'} commit(s) proprio(s): o rebase reaplicaria a historia antiga inteira`;
361
+ if (!opcoes.dryRun)
362
+ (0, ledger_1.registrar)((0, thread_1.dirThread)(raiz, id), id, ledger_1.TIPOS_DE_EVENTO.worktreeSincronizada, { dir, branch, base, shaBase, shaAntes,
363
+ rebaseFeito: false, motivo: 'tree.blocked', semAncestral: true, detalhe });
364
+ return { ...vazio, shaBase, shaAntes, shaDepois: shaAntes, ok: false, jaAtualizada: false, rebaseFeito: false,
365
+ motivo: 'tree.blocked', detalhe, correcao: `reaplique so os commits da thread sobre a base nova: ${onto}` };
366
+ }
290
367
  if (opcoes.dryRun) {
291
368
  passos.push(`git rebase ${shaBase}`);
292
369
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orkastery/cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "A software factory of AI agents that proves its work: parallel threads, verified results, and short decisions only when they matter.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/orkastery/orkastery#readme",