@orkastery/cli 0.5.1 → 0.5.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.
Files changed (110) hide show
  1. package/adapters/claude-code/agents/ork-master.md +2 -0
  2. package/adapters/claude-code/commands/master.md +8 -0
  3. package/adapters/claude-code/commands/ork.md +9 -0
  4. package/adapters/codex/skills/ork/SKILL.md +18 -3
  5. package/adapters/hermes/README.md +1 -0
  6. package/adapters/hermes/bin/ork-network-status.sh +10 -0
  7. package/adapters/hermes/hermes.plugin.json +1 -0
  8. package/adapters/hermes/hitl-ingress/__init__.py +2 -2
  9. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +6 -5
  10. package/adapters/openclaw/README.md +24 -4
  11. package/adapters/openclaw/dist/hitl-ingress.js +2 -2
  12. package/adapters/openclaw/dist/index.js +46 -11
  13. package/adapters/openclaw/openclaw.plugin.json +5 -1
  14. package/adapters/openclaw/package.json +2 -2
  15. package/adapters/openclaw/src/hitl-ingress.ts +2 -2
  16. package/adapters/openclaw/src/index.ts +49 -11
  17. package/assets/orkmind_bridge.py +117 -9
  18. package/dist/adapters/claude-bg.js +39 -5
  19. package/dist/adapters/codex.js +4 -3
  20. package/dist/agents-md.js +1 -0
  21. package/dist/auditrun.js +5 -1
  22. package/dist/board.js +16 -14
  23. package/dist/branch-de-estado.js +66 -10
  24. package/dist/busca-semantica.js +52 -12
  25. package/dist/canarios-rm057.js +149 -0
  26. package/dist/canarios.js +2 -0
  27. package/dist/ci.js +8 -1
  28. package/dist/claims.js +39 -1
  29. package/dist/company-brain-context.js +158 -22
  30. package/dist/docs.js +48 -9
  31. package/dist/doctor-pulse-cron.js +194 -0
  32. package/dist/doctor-rede.js +149 -0
  33. package/dist/doctor.js +89 -9
  34. package/dist/entrega-pr.js +20 -8
  35. package/dist/esgotamentos.js +181 -0
  36. package/dist/experiencia.js +12 -2
  37. package/dist/fabrica-estado.js +14 -6
  38. package/dist/fabrica-publicar.js +6 -0
  39. package/dist/fechamento.js +20 -66
  40. package/dist/forja.js +31 -7
  41. package/dist/gates.js +5 -1
  42. package/dist/hitl-contract.js +59 -4
  43. package/dist/hitl-curto.js +11 -6
  44. package/dist/hitl-gates.js +5 -0
  45. package/dist/hitl-lote.js +5 -2
  46. package/dist/hitl-resumo.js +9 -0
  47. package/dist/hitl-tempo-parado.js +201 -0
  48. package/dist/hitl-texto-livre.js +5 -5
  49. package/dist/horario.js +27 -0
  50. package/dist/hosts.js +52 -10
  51. package/dist/impedimento.js +205 -0
  52. package/dist/index.js +391 -74
  53. package/dist/indice-vetorial.js +87 -10
  54. package/dist/indice.js +3 -1
  55. package/dist/init.js +17 -0
  56. package/dist/integracoes-locais.js +2 -0
  57. package/dist/intelligence-graph-cli.js +91 -8
  58. package/dist/intelligence-graph-contexto.js +215 -0
  59. package/dist/intelligence-graph-parsers.js +6 -4
  60. package/dist/leases.js +664 -87
  61. package/dist/ledger-stats.js +15 -1
  62. package/dist/ledger.js +3 -0
  63. package/dist/licoes.js +1 -0
  64. package/dist/maestro-runtime.js +4 -1
  65. package/dist/manifest.js +20 -1
  66. package/dist/master-nota.js +14 -5
  67. package/dist/master.js +129 -12
  68. package/dist/mcp-artifacts.js +34 -3
  69. package/dist/mcp-git.js +20 -3
  70. package/dist/mcp-grafo-worker.js +5 -3
  71. package/dist/mcp-grafo.js +50 -4
  72. package/dist/mcp-server.js +14 -1
  73. package/dist/memoria.js +48 -6
  74. package/dist/network-roadmap.js +191 -16
  75. package/dist/ocupacao.js +11 -3
  76. package/dist/orkmind.js +90 -5
  77. package/dist/orquestracao.js +12 -10
  78. package/dist/parado-no-condutor.js +188 -32
  79. package/dist/phase.js +68 -15
  80. package/dist/policies.js +52 -1
  81. package/dist/portfolio.js +4 -1
  82. package/dist/projeto-alvo.js +5 -1
  83. package/dist/prompts.js +36 -1
  84. package/dist/prova-ativacao.js +75 -6
  85. package/dist/pulse-delivery.js +34 -2
  86. package/dist/pulse.js +49 -3
  87. package/dist/ratelimit.js +20 -4
  88. package/dist/recall.js +3 -1
  89. package/dist/rede-adesao.js +399 -0
  90. package/dist/rede-forja.js +289 -0
  91. package/dist/rede-projetos.js +239 -0
  92. package/dist/rede-status.js +336 -0
  93. package/dist/rede.js +980 -0
  94. package/dist/retry.js +69 -2
  95. package/dist/roadmap-status.js +23 -3
  96. package/dist/runtime-profiles.js +22 -1
  97. package/dist/saida-segura.js +58 -0
  98. package/dist/session-watcher-claude.js +113 -23
  99. package/dist/sessoes-contas.js +187 -0
  100. package/dist/sessoes-inventario.js +6 -18
  101. package/dist/sessoes.js +83 -2
  102. package/dist/ship.js +85 -19
  103. package/dist/thread.js +192 -13
  104. package/dist/worktree.js +5 -2
  105. package/eval/fixtures/fx-pedido-colado/caso.json +20 -0
  106. package/package.json +7 -3
  107. package/references/definition-of-done.md +1 -1
  108. package/skills/README.md +1 -1
  109. package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
  110. package/skills/phases/master-metrics/SKILL.md +8 -3
@@ -0,0 +1,187 @@
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.pidVivo = pidVivo;
37
+ exports.ehFantasma = ehFantasma;
38
+ exports.contasDeSessoes = contasDeSessoes;
39
+ exports.consultarContas = consultarContas;
40
+ exports.estadosNasContas = estadosNasContas;
41
+ exports.sessoesVivasPorPerfil = sessoesVivasPorPerfil;
42
+ /**
43
+ * RM-056 (D4, D5): as sessoes desta maquina em TODAS as contas que o `ork` conhece.
44
+ *
45
+ * O `claude agents` e o `~/.codex/sessions` so respondem pela conta do ambiente de quem consulta:
46
+ * com perfis (`ork accounts`), uma sessao despachada pelo perfil X so aparece consultando com o
47
+ * `CLAUDE_CONFIG_DIR`/`CODEX_HOME` de X. Aqui a consulta passa pela conta do processo e por cada
48
+ * perfil do store, sem repetir diretorio, e cada sessao volta com o id do perfil dela (nunca o
49
+ * diretorio nem nada de dentro dele).
50
+ *
51
+ * Fantasma (D5): sessao claude-bg com estado nao terminal sem pid, ou com pid sem processo. O
52
+ * `claude agents` guarda o registro, mas nao ha quem trabalhe: ela nao ocupa vaga, e
53
+ * `ork sessions limpar-fantasmas` solta o vinculo do `ork` sem tocar no runtime.
54
+ */
55
+ const fs = __importStar(require("node:fs"));
56
+ const path = __importStar(require("node:path"));
57
+ const claude_bg_1 = require("./adapters/claude-bg");
58
+ const codex_1 = require("./adapters/codex");
59
+ const runtime_profiles_1 = require("./runtime-profiles");
60
+ const TERMINAIS = ['done', 'completed', 'exited', 'stopped', 'failed'];
61
+ const estadoDe = (s) => String(s.state ?? s.status ?? '').trim().toLowerCase();
62
+ /** O processo existe? `EPERM` e processo de outro usuario: existe. */
63
+ function pidVivo(pid) {
64
+ if (typeof pid !== 'number' || !Number.isSafeInteger(pid) || pid <= 0)
65
+ return false;
66
+ try {
67
+ process.kill(pid, 0);
68
+ return true;
69
+ }
70
+ catch (e) {
71
+ return e.code === 'EPERM';
72
+ }
73
+ }
74
+ /** D5: fantasma e so claude-bg (o codex nao expoe pid na fonte), em estado nao terminal. */
75
+ function ehFantasma(runtime, s, vivo = pidVivo) {
76
+ return runtime === 'claude-bg' && !TERMINAIS.includes(estadoDe(s)) && !vivo(s.pid);
77
+ }
78
+ const real = (dir) => { try {
79
+ return fs.realpathSync(dir);
80
+ }
81
+ catch {
82
+ return path.resolve(dir);
83
+ } };
84
+ /**
85
+ * D4: as contas a consultar. Os perfis do store vem primeiro (inclusive desativado, que ainda pode
86
+ * ter sessao viva), para a sessao levar o id do perfil quando a conta do processo e a mesma pasta.
87
+ */
88
+ function contasDeSessoes(raiz, env = process.env) {
89
+ const contas = [], fontes = [];
90
+ const vistos = new Set();
91
+ const incluir = (runtime, perfil, dir) => {
92
+ const chave = `${runtime}:${real(dir)}`;
93
+ if (vistos.has(chave))
94
+ return;
95
+ vistos.add(chave);
96
+ contas.push({ runtime, perfil, dir });
97
+ };
98
+ if (raiz) {
99
+ try {
100
+ for (const p of (0, runtime_profiles_1.lerPerfis)(raiz).perfis) {
101
+ const d = (0, runtime_profiles_1.perfilDeDespacho)(p);
102
+ incluir(d.runtime, d, (d.configDir ?? d.codexHome));
103
+ }
104
+ }
105
+ catch (e) {
106
+ fontes.push({ origem: 'perfis de conta (ork accounts)', ok: false, detalhe: e.message });
107
+ }
108
+ }
109
+ for (const runtime of runtime_profiles_1.RUNTIMES_COM_PERFIL)
110
+ incluir(runtime, null, (0, runtime_profiles_1.diretorioImplicito)(runtime, env));
111
+ return { contas, fontes };
112
+ }
113
+ const rotulo = (c) => c.perfil ? `perfil ${c.perfil.id}` : 'conta do processo';
114
+ /**
115
+ * D4: as sessoes de todas as contas. Consulta que falha vira fonte `ok: false`, nunca "zero
116
+ * sessoes". `staleMin` e a janela de atividade que torna viva uma sessao codex (D3).
117
+ */
118
+ function consultarContas(raiz, opcoes = {}) {
119
+ const base = opcoes.contas ? { contas: opcoes.contas, fontes: [] } : contasDeSessoes(raiz);
120
+ const fontes = [...base.fontes];
121
+ const sessoes = [];
122
+ const agoraMs = opcoes.agoraMs ?? Date.now();
123
+ const janelaMs = (opcoes.staleMin ?? 30) * 60_000;
124
+ for (const conta of base.contas) {
125
+ const perfil = conta.perfil?.id ?? null;
126
+ if (conta.runtime === 'claude-bg') {
127
+ let ambiente;
128
+ try {
129
+ ambiente = (0, claude_bg_1.ambienteDoPerfil)(conta.perfil);
130
+ }
131
+ catch (e) {
132
+ fontes.push({ origem: `claude agents (${rotulo(conta)})`, ok: false, detalhe: e.message });
133
+ continue;
134
+ }
135
+ const r = (0, claude_bg_1.consultarSessoes)(undefined, opcoes.todas, ambiente);
136
+ const origem = `claude agents --json${opcoes.todas ? ' --all' : ''} (${rotulo(conta)})`;
137
+ // Fatia 2 do ensaio da 0.5.0 (P1): só o ENOENT antes da consulta prova a fonte ausente; qualquer
138
+ // outra falha do `claude` continua deixando o inventário incompleto.
139
+ fontes.push(r.ausente
140
+ ? { origem, ok: true, ausente: true, detalhe: `${r.detalhe}: nenhuma sessão claude-bg a listar`,
141
+ correcao: 'para despachar e listar pelo claude-bg, instale o Claude Code e garanta `claude` no PATH' }
142
+ : { origem, ok: r.ok, detalhe: r.detalhe });
143
+ for (const s of r.sessoes) {
144
+ const fantasma = ehFantasma('claude-bg', s);
145
+ sessoes.push({ ...s, runtime: 'claude-bg', perfil, fantasma, viva: !fantasma && !TERMINAIS.includes(estadoDe(s)) });
146
+ }
147
+ }
148
+ else {
149
+ const r = (0, codex_1.consultarRollouts)(opcoes.todas, conta.dir);
150
+ // A conta do processo segue com o caminho de antes; a do perfil vai pelo id, nunca pela pasta.
151
+ fontes.push(...(conta.perfil ? r.resultadosFontes.map(f => ({ ...f,
152
+ origem: `codex (${rotulo(conta)}): ${path.relative(conta.dir, f.origem)}` })) : r.resultadosFontes));
153
+ for (const s of r.sessoes) {
154
+ const recente = typeof s.atividadeEm === 'number' && agoraMs - s.atividadeEm <= janelaMs;
155
+ sessoes.push({ ...s, runtime: 'codex', perfil, fantasma: false, viva: estadoDe(s) !== 'completed' && recente });
156
+ }
157
+ }
158
+ }
159
+ return { sessoes, fontes, contas: base.contas };
160
+ }
161
+ /**
162
+ * D7: o mapa sessionId -> estado que o escalonador e o monitor usam, agora de todas as contas.
163
+ * Fantasma vira `fantasma` (nem `working` nem `blocked`: nao ocupa vaga nem pede o dono); codex
164
+ * vivo vira `working`. `null` ("runtime nao consultado") quando nenhuma conta claude-bg respondeu e
165
+ * nenhuma sessao codex apareceu: um mapa vazio ali diria "nada rodando" sem ter olhado.
166
+ */
167
+ function estadosNasContas(raiz, opcoes = {}) {
168
+ const r = consultarContas(raiz, opcoes);
169
+ const claude = r.fontes.filter(f => f.origem.startsWith('claude agents'));
170
+ if (claude.length > 0 && !claude.some(f => f.ok) && !r.sessoes.some(s => s.runtime === 'codex'))
171
+ return null;
172
+ return new Map(r.sessoes.map(s => [s.sessionId,
173
+ s.fantasma ? 'fantasma' : s.runtime === 'codex' ? (s.viva ? 'working' : estadoDe(s)) : estadoDe(s)]));
174
+ }
175
+ /**
176
+ * RM-056 (D3): sessoes vivas de cada perfil do runtime nesta maquina, a carga do rodizio por
177
+ * carga. So as contas dos perfis sao consultadas (a do processo nao recebe despacho com perfis).
178
+ */
179
+ function sessoesVivasPorPerfil(raiz, runtime, staleMin, agoraMs) {
180
+ const contas = contasDeSessoes(raiz).contas.filter(c => c.runtime === runtime && c.perfil);
181
+ const carga = new Map(contas.map(c => [c.perfil.id, 0]));
182
+ for (const s of consultarContas(raiz, { contas, staleMin, agoraMs }).sessoes) {
183
+ if (s.viva && s.perfil)
184
+ carga.set(s.perfil, (carga.get(s.perfil) ?? 0) + 1);
185
+ }
186
+ return carga;
187
+ }
@@ -38,9 +38,8 @@ exports.inventariarSessoes = inventariarSessoes;
38
38
  const fs = __importStar(require("node:fs"));
39
39
  const path = __importStar(require("node:path"));
40
40
  const os = __importStar(require("node:os"));
41
- const claude_bg_1 = require("./adapters/claude-bg");
42
- const codex_1 = require("./adapters/codex");
43
41
  const estado_thread_1 = require("./estado-thread");
42
+ const sessoes_contas_1 = require("./sessoes-contas");
44
43
  const thread_1 = require("./thread");
45
44
  /** Projeto canônico mais próximo de cada cwd declarado, sem varrer o HOME inteiro. */
46
45
  function raizesDeRegistros(cwds) {
@@ -71,22 +70,10 @@ function raizesDeRegistros(cwds) {
71
70
  }
72
71
  function inventariarSessoes(raiz, opcoes = {}) {
73
72
  const canonica = (0, estado_thread_1.raizDoEstado)(raiz);
74
- const claude = (0, claude_bg_1.consultarSessoes)(undefined, opcoes.todas);
75
- const codex = (0, codex_1.consultarRollouts)(opcoes.todas);
76
- const origemClaude = `claude agents --json${opcoes.todas ? ' --all' : ''}`;
77
- // Fatia 2 do ensaio da 0.5.0 (P1): como o diretório opcional do Codex, só o ENOENT antes da consulta
78
- // prova a fonte ausente; qualquer outra falha do `claude` continua deixando o inventário incompleto.
79
- const fontes = [
80
- claude.ausente
81
- ? { origem: origemClaude, ok: true, ausente: true, detalhe: `${claude.detalhe}: nenhuma sessão claude-bg a listar`,
82
- correcao: 'para despachar e listar pelo claude-bg, instale o Claude Code e garanta `claude` no PATH' }
83
- : { origem: origemClaude, ok: claude.ok, detalhe: claude.detalhe },
84
- ...codex.resultadosFontes,
85
- ];
86
- const todas = [
87
- ...claude.sessoes.map(s => ({ ...s, runtime: 'claude-bg', vinculos: [] })),
88
- ...codex.sessoes.map(s => ({ ...s, runtime: 'codex', vinculos: [] })),
89
- ];
73
+ // RM-056 (D4): a conta do processo e cada perfil do store, sem repetir diretorio.
74
+ const contas = (0, sessoes_contas_1.consultarContas)(canonica, { todas: opcoes.todas });
75
+ const fontes = contas.fontes;
76
+ const todas = contas.sessoes.map(s => ({ ...s, vinculos: [] }));
90
77
  // O cruzamento usa o universo global mesmo quando a apresentação é local.
91
78
  try {
92
79
  const projetos = raizesDeRegistros([canonica, ...todas.map(s => s.cwd)]);
@@ -161,5 +148,6 @@ function inventariarSessoes(raiz, opcoes = {}) {
161
148
  escopo: { usuario: os.userInfo().username, global: !!opcoes.global, historico: !!opcoes.todas, raiz: canonica },
162
149
  fontes, sessoes, total: sessoes.length,
163
150
  semThread: sessoes.filter(s => s.vinculos.length === 0).length, ambiguas,
151
+ fantasmas: sessoes.filter(s => s.fantasma).length,
164
152
  };
165
153
  }
package/dist/sessoes.js CHANGED
@@ -44,19 +44,29 @@ exports.tabelaDeSessoes = tabelaDeSessoes;
44
44
  exports.logsDaSessao = logsDaSessao;
45
45
  exports.pararSessao = pararSessao;
46
46
  exports.comandoDeAttach = comandoDeAttach;
47
+ exports.limparFantasmas = limparFantasmas;
48
+ exports.sessoesSemFim = sessoesSemFim;
49
+ exports.textoDaLimpeza = textoDaLimpeza;
47
50
  const adapter = __importStar(require("./adapters/claude-bg"));
48
51
  const sessoes_inventario_1 = require("./sessoes-inventario");
49
52
  const runtime_profiles_1 = require("./runtime-profiles");
50
53
  const util_1 = require("./util");
54
+ const estado_thread_1 = require("./estado-thread");
55
+ const ledger_1 = require("./ledger");
56
+ const thread_1 = require("./thread");
51
57
  function textoDoInventario(r) {
52
58
  const linhas = [
53
59
  `Inventário ${r.escopo.global ? 'global' : 'do projeto'} da conta ${r.escopo.usuario}; histórico: ${r.escopo.historico ? 'incluído' : 'omitido'}.`,
54
60
  `Fontes: ${r.fontes.map(f => `${f.origem} (${f.ausente ? 'ausente' : f.ok ? 'ok' : 'FALHA'})`).join('; ')}`,
55
- `Total: ${r.total}; sem thread: ${r.semThread}; ambíguas: ${r.ambiguas}; consulta: ${r.ok ? 'válida' : 'INCOMPLETA'}.`,
61
+ `Total: ${r.total}; sem thread: ${r.semThread}; ambíguas: ${r.ambiguas}; fantasmas: ${r.fantasmas}; consulta: ${r.ok ? 'válida' : 'INCOMPLETA'}.`,
56
62
  ];
63
+ // RM-056 (D4): a coluna PERFIL e o id do perfil (`processo` sem perfil), nunca o diretorio da conta.
57
64
  if (r.sessoes.length)
58
- linhas.push((0, util_1.tabela)(['ID', 'RUNTIME', 'ESTADO', 'THREAD/FASE'], r.sessoes.map(s => [s.sessionId, s.runtime, s.state ?? s.status ?? 'unknown',
65
+ linhas.push((0, util_1.tabela)(['ID', 'RUNTIME', 'PERFIL', 'ESTADO', 'THREAD/FASE'], r.sessoes.map(s => [s.sessionId, s.runtime, s.perfil ?? 'processo',
66
+ `${s.state ?? s.status ?? 'unknown'}${s.fantasma ? ' (fantasma)' : ''}`,
59
67
  s.vinculos.map(v => `${v.thread}/${v.fase}`).join(', ') || '(fora do ork)'])));
68
+ if (r.fantasmas > 0)
69
+ linhas.push('Fantasma: registro sem processo vivo, não ocupa vaga; `ork sessions limpar-fantasmas` solta o vínculo do ork sem tocar no runtime.');
60
70
  for (const f of r.fontes.filter(f => !f.ok))
61
71
  linhas.push(`Falha: ${f.detalhe}`);
62
72
  // Fatia 2 do ensaio da 0.5.0 (P1): a fonte ausente nao e falha, mas diz o que falta e como ter.
@@ -120,3 +130,74 @@ function comandoDeAttach(chave, raiz) {
120
130
  const { sessao, perfil, configDir } = l.achada;
121
131
  return { texto: adapter.comandoAttach(sessao.sessionId, perfil ? configDir : null), codigo: 0 };
122
132
  }
133
+ const ENCERRAM_A_SESSAO = ['phase_result', 'sessao_morta', 'session_superseded'];
134
+ /**
135
+ * RM-056 (D6): solta o vinculo do `ork` com cada sessao fantasma (sem pid e sem processo) deste
136
+ * projeto, gravando `sessao_morta` no ledger da thread: e o evento que libera a conducao
137
+ * (`fimDaSessao`) e a vaga. Nunca chama o runtime: o registro no `claude agents` fica onde esta.
138
+ */
139
+ function limparFantasmas(raiz, opcoes = {}) {
140
+ const canonica = (0, estado_thread_1.raizDoEstado)(raiz);
141
+ const inv = (0, sessoes_inventario_1.inventariarSessoes)(canonica, { global: true });
142
+ const itens = [];
143
+ for (const s of inv.sessoes.filter(x => x.fantasma)) {
144
+ // Fechamento (RM-056, ao fechar): so os vinculos da thread que fecha; o resto fica como estava.
145
+ const vinculos = opcoes.thread ? s.vinculos.filter(v => v.thread === opcoes.thread && v.raiz === canonica) : s.vinculos;
146
+ if (opcoes.thread && vinculos.length === 0)
147
+ continue;
148
+ if (vinculos.length === 0) {
149
+ itens.push({ sessionId: s.sessionId, perfil: s.perfil, thread: null, fase: null, acao: 'sem-thread' });
150
+ continue;
151
+ }
152
+ for (const v of vinculos) {
153
+ const base = { sessionId: s.sessionId, perfil: s.perfil, thread: v.thread, fase: v.fase };
154
+ if (v.raiz !== canonica) {
155
+ itens.push({ ...base, acao: 'outro-projeto' });
156
+ continue;
157
+ }
158
+ const dir = (0, thread_1.dirThread)(canonica, v.thread);
159
+ if ((0, ledger_1.lerLedger)(dir).some(e => ENCERRAM_A_SESSAO.includes(e.tipo) && e.sessionId === s.sessionId)) {
160
+ itens.push({ ...base, acao: 'ja-solto' });
161
+ continue;
162
+ }
163
+ if (!opcoes.dryRun)
164
+ (0, ledger_1.registrar)(dir, v.thread, 'sessao_morta', {
165
+ fase: v.fase, sessionId: s.sessionId, runtime: s.runtime, ...(s.perfil ? { perfilId: s.perfil } : {}),
166
+ origem: opcoes.origem ?? 'sessions.limpar-fantasmas', estadoNoRuntime: s.state ?? s.status ?? null,
167
+ evidencia: 'claude agents lista a sessao sem pid ou com pid sem processo (fantasma, RM-056 D5)',
168
+ razao: 'solta o vinculo do ork (conducao e vaga) sem tocar no runtime: claude stop e claude rm nao acham o job',
169
+ });
170
+ itens.push({ ...base, acao: 'solto' });
171
+ }
172
+ }
173
+ return { ok: inv.ok, dryRun: opcoes.dryRun === true, itens, falhas: inv.fontes.filter(f => !f.ok).map(f => `${f.origem}: ${f.detalhe}`) };
174
+ }
175
+ /**
176
+ * RM-056 (ao fechar): as sessoes registradas na thread que o ledger dela ainda nao encerra. E o filtro
177
+ * barato do fechamento: so com alguma delas o `liberarAoFechar` consulta o inventario (o `claude agents`
178
+ * de cada conta). Sessao sem fim pode estar viva; quem decide se e fantasma e o inventario.
179
+ */
180
+ function sessoesSemFim(raiz, threadId) {
181
+ const canonica = (0, estado_thread_1.raizDoEstado)(raiz);
182
+ const t = (0, thread_1.lerThread)(canonica, threadId);
183
+ const eventos = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(canonica, threadId));
184
+ const encerradas = new Set(eventos.filter(e => ENCERRAM_A_SESSAO.includes(e.tipo)).map(e => String(e.sessionId ?? '')));
185
+ const ids = (Array.isArray(t.sessoes) ? t.sessoes : []).map(s => s?.sessionId).filter((id) => typeof id === 'string');
186
+ return [...new Set(ids)].filter(id => !encerradas.has(id));
187
+ }
188
+ function textoDaLimpeza(r) {
189
+ if (r.itens.length === 0)
190
+ return `Nenhuma sessão fantasma${r.ok ? '' : ' nas contas que responderam'}.` +
191
+ r.falhas.map(f => `\nFalha: ${f}`).join('');
192
+ const verbo = {
193
+ solto: r.dryRun ? 'soltaria (sessao_morta no ledger)' : 'solto: sessao_morta no ledger',
194
+ 'ja-solto': 'já encerrada no ledger', 'sem-thread': 'fora do ork: nada a soltar',
195
+ 'outro-projeto': 'vínculo de outro projeto: rode lá',
196
+ };
197
+ return [
198
+ `Sessões fantasma (sem pid e sem processo)${r.dryRun ? ', ensaio: nada foi gravado' : ''}. O runtime não foi tocado.`,
199
+ (0, util_1.tabela)(['ID', 'PERFIL', 'THREAD/FASE', 'AÇÃO'], r.itens.map(i => [i.sessionId, i.perfil ?? 'processo',
200
+ i.thread ? `${i.thread}/${i.fase}` : '-', verbo[i.acao]])),
201
+ ...r.falhas.map(f => `Falha: ${f}`),
202
+ ].join('\n');
203
+ }
package/dist/ship.js CHANGED
@@ -50,6 +50,7 @@ var __importStar = (this && this.__importStar) || (function () {
50
50
  };
51
51
  })();
52
52
  Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.commitQueIncorporou = commitQueIncorporou;
53
54
  exports.autorizacaoDePush = autorizacaoDePush;
54
55
  exports.arvoreComBranch = arvoreComBranch;
55
56
  exports.prepararArvore = prepararArvore;
@@ -62,6 +63,7 @@ exports.ship = ship;
62
63
  exports.textoDoShip = textoDoShip;
63
64
  const fs = __importStar(require("node:fs"));
64
65
  const path = __importStar(require("node:path"));
66
+ const node_child_process_1 = require("node:child_process");
65
67
  const gates_1 = require("./gates");
66
68
  const leases_1 = require("./leases");
67
69
  const ledger_1 = require("./ledger");
@@ -76,6 +78,45 @@ const contrato_publico_1 = require("./contrato-publico");
76
78
  const prova_minima_1 = require("./prova-minima");
77
79
  const horario_1 = require("./horario");
78
80
  const fabrica_publicar_1 = require("./fabrica-publicar");
81
+ const branch_de_estado_1 = require("./branch-de-estado");
82
+ /**
83
+ * RM-037 (fatia 5, A1): o commit de primeiro pai da base que incorporou o `shaDe`: o merge que trouxe a branch, ou
84
+ * o proprio `shaDe` quando a base avancou por fast-forward ate ele. Com a branch ja incorporada, a ponta da base e
85
+ * outra coisa: no `ship_done` da ork-docsusuarios (27/09/2026), o unico do `ork ship` com `jaIncorporado` nos ledgers
86
+ * deste projeto, ela era o merge de outra thread. O que trouxe a branch e o commit mais velho do primeiro pai da base
87
+ * que descende do `shaDe`. GO-FIX (rodada 1 do CHECK): `--first-parent` junto com `--ancestry-path` so propaga a
88
+ * descendencia pelos commits de primeiro pai, e o merge do PR que recebeu "Update branch" no GitHub (o pai dele e um
89
+ * merge que descende do `shaDe`, nao o `shaDe`) sumia da lista; agora sao duas listas e a intersecao. `null` quando o
90
+ * git nao responde.
91
+ */
92
+ function commitQueIncorporou(raiz, shaDe, ponta, passos = []) {
93
+ if (shaDe === ponta)
94
+ return shaDe;
95
+ // `passos` recebe os comandos que de fato rodaram, na ordem: e a evidencia do ship (sugestao da conferencia da rodada 2).
96
+ const curto = (sha) => sha.slice(0, 8);
97
+ // As listas crescem com o historico entre a branch e a ponta: o buffer vai alem do 1 MiB padrao do spawnSync.
98
+ const listar = (args) => {
99
+ passos.push(`git ${args.map((a) => a.replace(/^([0-9a-f]{40,64})\.\.([0-9a-f]{40,64})$/, (_, x, y) => `${curto(x)}..${curto(y)}`)).join(' ')}`);
100
+ const r = (0, node_child_process_1.spawnSync)('git', args, { cwd: raiz, encoding: 'utf8', timeout: 120000, maxBuffer: 256 * 1024 * 1024 });
101
+ return r.status === 0 && !r.error ? r.stdout.split('\n').filter((l) => /^[0-9a-f]{40,64}$/.test(l)) : null;
102
+ };
103
+ const primeiroPai = listar(['rev-list', '--first-parent', `${shaDe}..${ponta}`]);
104
+ const descendentes = listar(['rev-list', '--ancestry-path', `${shaDe}..${ponta}`]);
105
+ if (!primeiroPai || !descendentes)
106
+ return null;
107
+ const doShaDe = new Set(descendentes);
108
+ // Do mais velho para o mais novo: o primeiro de primeiro pai que descende do `shaDe`.
109
+ const maisVelho = [...primeiroPai].reverse().find((c) => doShaDe.has(c));
110
+ if (!maisVelho)
111
+ return null;
112
+ // O primeiro pai dele e o proprio `shaDe`: a branch ja estava no primeiro pai da base (fast-forward). Sem a resposta
113
+ // do git, nada e gravado: devolver o mais velho poria no `mergeSha` o filho do `shaDe` (sugestao da rodada 2).
114
+ passos.push(`git rev-parse ${curto(maisVelho)}^1`);
115
+ const pai = (0, util_1.exec)('git', ['rev-parse', `${maisVelho}^1`], raiz);
116
+ if (!pai.ok)
117
+ return null;
118
+ return pai.stdout.trim() === shaDe ? shaDe : maisVelho;
119
+ }
79
120
  /** Os caminhos de contrato publico que a entrega traz sobre a base (diff desde o merge-base). */
80
121
  function contratosDaEntrega(raiz, shaPara, shaDe) {
81
122
  const base = (0, util_1.exec)('git', ['merge-base', shaPara, shaDe], raiz);
@@ -185,7 +226,7 @@ function prepararArvore(raiz, para) {
185
226
  }
186
227
  const dir = dirTemporarioDeShip(raiz, para);
187
228
  fs.mkdirSync(path.dirname(dir), { recursive: true });
188
- const r = (0, util_1.exec)('git', ['worktree', 'add', dir, para], raiz);
229
+ const r = (0, util_1.exec)('git', ['worktree', 'add', '--', dir, para], raiz);
189
230
  if (!r.ok) {
190
231
  throw new Error(`nao foi possivel montar a arvore de merge de "${para}": ${(r.stderr || r.stdout).trim()}`);
191
232
  }
@@ -211,7 +252,8 @@ function shaDaRef(dir, ref) {
211
252
  * codigo de saida do `git push` nao prova que o remoto ficou com o commit certo.
212
253
  */
213
254
  function shaNoRemoto(dir, remoto, branch) {
214
- const r = (0, util_1.exec)('git', ['ls-remote', remoto, `refs/heads/${branch}`], dir, 120000);
255
+ // RM-047: so nome de remoto chega ao git, e depois do `--` (nunca vira opcao nem transporte).
256
+ const r = (0, util_1.exec)('git', ['ls-remote', '--', (0, branch_de_estado_1.exigirRemoto)(remoto, 'ship'), `refs/heads/${branch}`], dir, 120000);
215
257
  if (!r.ok)
216
258
  return null;
217
259
  const linha = r.stdout.split('\n').find((l) => l.trim() !== '');
@@ -222,7 +264,7 @@ function shaNoRemoto(dir, remoto, branch) {
222
264
  }
223
265
  /** O remoto esta configurado neste repositorio? */
224
266
  function remotoConfigurado(dir, remoto) {
225
- return (0, util_1.exec)('git', ['remote', 'get-url', remoto], dir).ok;
267
+ return (0, util_1.exec)('git', ['remote', 'get-url', '--', (0, branch_de_estado_1.exigirRemoto)(remoto, 'ship')], dir).ok;
226
268
  }
227
269
  function baseDoResultado(thread, de, opcoes) {
228
270
  return {
@@ -237,6 +279,7 @@ function baseDoResultado(thread, de, opcoes) {
237
279
  shaDe: '',
238
280
  shaParaAntes: '',
239
281
  mergeSha: null,
282
+ pontaDaBase: null,
240
283
  jaIncorporado: false,
241
284
  remoto: opcoes.remoto ?? 'origin',
242
285
  shaRemoto: null,
@@ -329,7 +372,10 @@ function ship(carregado, threadId, opcoes) {
329
372
  const dir = (0, thread_1.dirThread)(raiz, threadId);
330
373
  const de = opcoes.de ?? thread.base.branch;
331
374
  const para = opcoes.para;
332
- const remoto = opcoes.remoto ?? 'origin';
375
+ // RM-047: o `--remoto` vem da linha de comando. Antes de qualquer git (inclusive a leitura da reversao),
376
+ // so nome de remoto passa; o resto recusa com `ship.remoto-invalido`, e o ship nao entrega sem push provado
377
+ // por um remoto que nunca existiu.
378
+ const remoto = (0, branch_de_estado_1.exigirRemoto)(opcoes.remoto ?? 'origin', 'ship');
333
379
  const r = baseDoResultado(thread, de, opcoes);
334
380
  // I-43 (T7): antes de qualquer coisa, se a entrega ANTERIOR desta thread foi
335
381
  // revertida na base, isso vira `rollback_done`. E o insumo de maior peso do indice
@@ -449,7 +495,7 @@ function ship(carregado, threadId, opcoes) {
449
495
  }
450
496
  // 4. Lease `main-tree`: so uma thread mergeia por vez.
451
497
  if (r.dryRun) {
452
- const ocupado = (0, leases_1.lerLease)(raiz, leases_1.LEASE_MAIN_TREE);
498
+ const ocupado = (0, leases_1.leasesColidentes)(raiz, leases_1.LEASE_MAIN_TREE)[0] ?? null;
453
499
  r.leaseOcupadoPor = ocupado;
454
500
  // O ensaio tambem responde ONDE o merge aconteceria e se aquela arvore esta limpa:
455
501
  // descobrir isso so na hora do merge real seria descobrir tarde demais.
@@ -458,11 +504,17 @@ function ship(carregado, threadId, opcoes) {
458
504
  ? `${dona} (arvore que tem ${para} em check-out; ${temAlteracaoNaoCommitada(dona) ? 'SUJA, o ship reprovaria com tree.blocked' : 'limpa'})`
459
505
  : `worktree temporaria em ${path.join((0, manifest_1.dirEstado)(raiz), 'tmp')} (ninguem tem ${para} em check-out)`;
460
506
  r.detalhe = `arvore de merge: ${arvoreDoMerge}`;
507
+ // Ensaio de 03/10 (R5): sem remoto (ou com --sem-push), o ensaio diz o mesmo que o ship real, e nao
508
+ // promete um push e uma prova que nao vao acontecer.
509
+ const semPushNoReal = opcoes.semPush ? 'push nao executado por --sem-push (merge local concluido)'
510
+ : !remotoConfigurado(raiz, remoto) ? `remoto "${remoto}" nao configurado: merge local concluido, sem push a provar` : null;
461
511
  r.passos = [
462
512
  `arvore de merge: ${arvoreDoMerge}`,
463
513
  `git merge --no-ff ${de} -m "<mensagem de ship>"`,
464
- `git push ${remoto} ${para}`,
465
- `git ls-remote ${remoto} refs/heads/${para} (prova do push)`,
514
+ ...(semPushNoReal ? [`sem push: ${semPushNoReal}`] : [
515
+ `git push ${remoto} ${para}`,
516
+ `git ls-remote ${remoto} refs/heads/${para} (prova do push)`,
517
+ ]),
466
518
  ];
467
519
  (0, ledger_1.registrar)(dir, threadId, ledger_1.TIPOS_DE_EVENTO.shipIniciado, {
468
520
  de,
@@ -493,11 +545,11 @@ function ship(carregado, threadId, opcoes) {
493
545
  posicao: aquisicao.posicaoNaFila,
494
546
  naFrente: (0, leases_1.esperandoPor)(raiz, leases_1.LEASE_MAIN_TREE).length,
495
547
  bloqueadaPor: dono?.thread ?? '(desconhecida)',
496
- motivo: 'lease.busy',
548
+ motivo: aquisicao.motivo ?? 'lease.busy',
497
549
  });
498
- return bloquear('lease.busy', `lease ${leases_1.LEASE_MAIN_TREE} esta com a thread ${dono?.thread ?? '(desconhecida)'} ` +
550
+ return bloquear(aquisicao.motivo ?? 'lease.busy', aquisicao.falhaRetomada ? aquisicao.detalhe : `lease ${leases_1.LEASE_MAIN_TREE} esta com a thread ${dono?.thread ?? '(desconhecida)'} ` +
499
551
  `desde ${dono?.adquiridoEm ?? '?'} (${dono?.motivo ?? 'sem motivo'}); ` +
500
- `esta thread entrou na merge queue na posicao ${aquisicao.posicaoNaFila}`, `espere a vez na fila (ork lease list), ou libere com: ork lease release ${leases_1.LEASE_MAIN_TREE} --forcar`);
552
+ `esta thread entrou na merge queue na posicao ${aquisicao.posicaoNaFila}`, aquisicao.correcao);
501
553
  }
502
554
  r.lease = aquisicao.lease;
503
555
  (0, ledger_1.registrar)(dir, threadId, ledger_1.TIPOS_DE_EVENTO.leaseAdquirido, {
@@ -532,6 +584,9 @@ function ship(carregado, threadId, opcoes) {
532
584
  const jaIncorporado = (0, util_1.exec)('git', ['merge-base', '--is-ancestor', shaDe, shaPara], raiz).ok;
533
585
  r.jaIncorporado = jaIncorporado;
534
586
  let mergeSha = shaPara;
587
+ // RM-037 (fatia 5, A1): a ponta que o push prova. Com o merge feito aqui, e ele; com a branch ja incorporada, a
588
+ // ponta de antes, que pode trazer outros merges depois do que entregou esta thread.
589
+ let pontaDaBase = shaPara;
535
590
  if (!jaIncorporado) {
536
591
  const mensagem = opcoes.mensagem ??
537
592
  `ship(${threadId}): merge de ${de} em ${para}\n\n` +
@@ -539,7 +594,8 @@ function ship(carregado, threadId, opcoes) {
539
594
  `modo: ${(0, modos_1.tagDoModo)(thread.modo)}\n` +
540
595
  `autorizado por: ${autorizacao.por} (${autorizacao.tipo})\n` +
541
596
  `verificado em: HEAD ${verificacao.commit}\n`;
542
- const merge = (0, util_1.exec)('git', ['merge', '--no-ff', de, '-m', mensagem], arvore.dir, 300000);
597
+ // RM-047: o merge e do sha verificado, e nao do nome que veio de `--de` (nome nunca vira opcao do git).
598
+ const merge = (0, util_1.exec)('git', ['merge', '--no-ff', '-m', mensagem, shaDe], arvore.dir, 300000);
543
599
  passos.push(`git merge --no-ff ${de}`);
544
600
  if (!merge.ok) {
545
601
  (0, util_1.exec)('git', ['merge', '--abort'], arvore.dir);
@@ -551,6 +607,7 @@ function ship(carregado, threadId, opcoes) {
551
607
  return bloquear('artifact.missing', 'nao foi possivel ler o HEAD apos o merge', 'confira o repositorio');
552
608
  }
553
609
  mergeSha = novo;
610
+ pontaDaBase = novo;
554
611
  // Verificacao independente do merge: a origem precisa ser ancestral do resultado.
555
612
  const incorporou = (0, util_1.exec)('git', ['merge-base', '--is-ancestor', shaDe, mergeSha], arvore.dir).ok;
556
613
  passos.push(`git merge-base --is-ancestor ${shaDe.slice(0, 8)} ${mergeSha.slice(0, 8)}`);
@@ -559,9 +616,16 @@ function ship(carregado, threadId, opcoes) {
559
616
  }
560
617
  }
561
618
  else {
562
- passos.push(`(nada a mergear: ${de} ja e ancestral de ${para})`);
619
+ // RM-037 (fatia 5, A1): o merge que trouxe a branch, e nao a ponta da base.
620
+ const incorporadoEm = commitQueIncorporou(raiz, shaDe, shaPara, passos);
621
+ if (!incorporadoEm) {
622
+ return bloquear('artifact.missing', `${de} ja e ancestral de ${para}, mas o git nao achou o commit de primeiro pai que o incorporou`, 'confira o repositorio: o ship_done precisa do merge que entregou a thread');
623
+ }
624
+ mergeSha = incorporadoEm;
625
+ passos.push(`(nada a mergear: ${de} ja e ancestral de ${para}, incorporado em ${mergeSha.slice(0, 8)})`);
563
626
  }
564
627
  r.mergeSha = mergeSha;
628
+ r.pontaDaBase = pontaDaBase;
565
629
  opcoes.revalidarGit?.('push', arvore.dir);
566
630
  // 6. Push PROVADO por ls-remote.
567
631
  const temRemoto = remotoConfigurado(arvore.dir, remoto);
@@ -575,8 +639,8 @@ function ship(carregado, threadId, opcoes) {
575
639
  else {
576
640
  const existeNoRemoto = shaNoRemoto(arvore.dir, remoto, para) !== null;
577
641
  const argsPush = existeNoRemoto
578
- ? ['push', remoto, `${para}:refs/heads/${para}`]
579
- : ['push', '-u', remoto, `${para}:refs/heads/${para}`];
642
+ ? ['push', '--', remoto, `refs/heads/${para}:refs/heads/${para}`]
643
+ : ['push', '-u', '--', remoto, `refs/heads/${para}:refs/heads/${para}`];
580
644
  const push = (0, util_1.exec)('git', argsPush, arvore.dir, 300000);
581
645
  passos.push(`git ${argsPush.join(' ')}`);
582
646
  if (!push.ok) {
@@ -587,10 +651,10 @@ function ship(carregado, threadId, opcoes) {
587
651
  const shaRemoto = shaNoRemoto(arvore.dir, remoto, para);
588
652
  passos.push(`git ls-remote ${remoto} refs/heads/${para}`);
589
653
  r.shaRemoto = shaRemoto;
590
- r.pushVerificado = shaRemoto !== null && shaRemoto === mergeSha;
654
+ r.pushVerificado = shaRemoto !== null && shaRemoto === pontaDaBase;
591
655
  if (!r.pushVerificado) {
592
656
  r.passos = passos;
593
- return bloquear('runtime.unavailable', `ls-remote devolveu ${shaRemoto ?? '(nada)'} e o destino local esta em ${mergeSha}: ` +
657
+ return bloquear('runtime.unavailable', `ls-remote devolveu ${shaRemoto ?? '(nada)'} e o destino local esta em ${pontaDaBase}: ` +
594
658
  'o push nao ficou provado', 'rode o ship de novo e confira o remoto');
595
659
  }
596
660
  }
@@ -609,6 +673,8 @@ function ship(carregado, threadId, opcoes) {
609
673
  shaDe,
610
674
  shaParaAntes: shaPara,
611
675
  mergeSha,
676
+ // RM-037 (fatia 5, A1): a ponta da base vai em campo proprio; o `mergeSha` e o merge que entregou a thread.
677
+ pontaDaBase,
612
678
  jaIncorporado,
613
679
  remoto: r.remoto,
614
680
  shaRemoto: r.shaRemoto,
@@ -676,7 +742,7 @@ function textoDoShip(r) {
676
742
  `${v.comandos.filter((c) => c.ok).length}/${v.comandos.length} comandos)`);
677
743
  }
678
744
  if (r.ci) {
679
- linhas.push(` ci ${r.ci.required ? `${r.ci.state.toUpperCase()} ${r.ci.context} em ${r.ci.sha.slice(0, 8)}` : 'não exigido'}`);
745
+ linhas.push(` ci ${r.ci.required ? `${r.ci.state.toUpperCase()} ${r.ci.context} em ${r.ci.sha.slice(0, 8)}` : 'não exigido'}`);
680
746
  }
681
747
  linhas.push(...(0, policies_1.linhasDeAviso)(r.violacoes ?? []));
682
748
  if (r.leaseOcupadoPor) {
@@ -705,11 +771,11 @@ function textoDoShip(r) {
705
771
  }
706
772
  linhas.push('');
707
773
  linhas.push(r.jaIncorporado
708
- ? `Nada a mergear: ${r.de} ja estava em ${r.para}.`
774
+ ? `Nada a mergear: ${r.de} ja estava em ${r.para}, incorporado em ${r.mergeSha}.`
709
775
  : `Merge --no-ff concluido: ${r.mergeSha}`);
710
776
  if (r.pushVerificado) {
711
777
  linhas.push(`Push PROVADO: ls-remote de ${r.remoto}/${r.para} devolveu ${r.shaRemoto}`);
712
- linhas.push(` e o destino local esta no mesmo sha: ${r.mergeSha}`);
778
+ linhas.push(` e o destino local esta no mesmo sha: ${r.pontaDaBase ?? r.mergeSha}`);
713
779
  }
714
780
  else {
715
781
  linhas.push(`Push nao provado: ${local(r.detalhe) || 'sem remoto configurado'}`);