@orkastery/cli 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/adapters/claude-code/README.md +11 -2
  2. package/adapters/claude-code/commands/onboarding.md +3 -2
  3. package/adapters/openclaw/README.md +11 -0
  4. package/adapters/openclaw/dist/index.js +1 -1
  5. package/adapters/openclaw/openclaw.plugin.json +1 -1
  6. package/adapters/openclaw/package.json +1 -1
  7. package/adapters/openclaw/src/index.ts +1 -1
  8. package/dist/adapters/claude-bg.js +20 -6
  9. package/dist/auditrun.js +1 -1
  10. package/dist/claim-lint.js +3 -1
  11. package/dist/docs.js +71 -3
  12. package/dist/doctor.js +209 -32
  13. package/dist/entrega-pr.js +7 -0
  14. package/dist/fabrica-estado.js +21 -3
  15. package/dist/fix.js +3 -3
  16. package/dist/hitl-resumo.js +24 -1
  17. package/dist/hosts.js +7 -3
  18. package/dist/index.js +42 -14
  19. package/dist/init.js +11 -5
  20. package/dist/integracoes-locais.js +2 -0
  21. package/dist/intelligence-graph-cli.js +181 -27
  22. package/dist/intelligence-graph-contract.js +71 -12
  23. package/dist/intelligence-graph-extract-md.js +41 -23
  24. package/dist/intelligence-graph-extract-ts.js +181 -11
  25. package/dist/intelligence-graph-extract.js +186 -9
  26. package/dist/intelligence-graph-index.js +205 -24
  27. package/dist/intelligence-graph-repo.js +10 -0
  28. package/dist/leases.js +16 -0
  29. package/dist/ledger.js +9 -0
  30. package/dist/licoes.js +5 -4
  31. package/dist/liveness.js +2 -1
  32. package/dist/maestro-runtime.js +3 -0
  33. package/dist/manifest.js +24 -0
  34. package/dist/mcp-artifacts.js +2 -0
  35. package/dist/mcp-git.js +60 -12
  36. package/dist/mcp-grafo-worker.js +91 -0
  37. package/dist/mcp-grafo.js +233 -0
  38. package/dist/mcp-install.js +2 -1
  39. package/dist/mcp-server.js +9 -0
  40. package/dist/mcp-ship.js +2 -0
  41. package/dist/mcp-verify.js +3 -1
  42. package/dist/network-roadmap.js +5 -5
  43. package/dist/ocupacao.js +6 -2
  44. package/dist/onboarding.js +9 -4
  45. package/dist/orquestracao.js +2 -1
  46. package/dist/parado-no-condutor.js +933 -0
  47. package/dist/policies.js +27 -2
  48. package/dist/prova-ativacao.js +345 -0
  49. package/dist/pulse-delivery.js +27 -6
  50. package/dist/pulse.js +53 -3
  51. package/dist/roadmap-status.js +142 -7
  52. package/dist/runtime-context.js +5 -1
  53. package/dist/sessoes-inventario.js +7 -1
  54. package/dist/sessoes.js +4 -1
  55. package/dist/ship.js +25 -1
  56. package/dist/thread.js +21 -2
  57. package/dist/util.js +71 -4
  58. package/dist/verify.js +2 -2
  59. package/dist/worktree.js +29 -6
  60. package/eval/casos/onboarding.json +1 -1
  61. package/package.json +1 -1
  62. package/skills/core/onboarding/SKILL.md +3 -2
  63. package/skills/phases/ship-release/SKILL.md +6 -3
@@ -50,6 +50,15 @@ exige Linux, Python 3/pexpect e OAuth de assinatura em `ANTHROPIC_TOKEN` no arqu
50
50
  permanece fora da fixture. As sessões e os ledgers temporários são descartados;
51
51
  guarde a saída JSON como recibo antes de declarar a prova concluída.
52
52
 
53
+ **Prova de ativação do Maestro (RM-032).** `node core/scripts/prova-ativacao.cjs claude-code`
54
+ instala o plugin e o `.mcp.json` desta cópia num projeto descartável e abre uma sessão nova
55
+ `claude -p "orkastery maestro"` com `--plugin-dir`, `--mcp-config --strict-mcp-config`,
56
+ `--setting-sources project` e `--no-session-persistence`. Usa o login nativo do CLI, sem copiar
57
+ credencial e sem gravar no registro global de plugins. Aprova só quando o modelo chama
58
+ `mcp__orkastery__ork_maestro` (o `ork maestro` pelo Bash é desvio) e o snapshot é do projeto
59
+ descartável; o recibo traz o sha256 dos arquivos globais antes e depois. Modelo padrão: `sonnet`
60
+ (`--modelo` troca). Detalhes no [contrato do snapshot](../../docs/referencia/contratos/maestro-i32.md).
61
+
53
62
  ## O que voce ganha
54
63
 
55
64
  | Voce digita | Voce recebe |
@@ -165,8 +174,8 @@ tres e o `--dry-run` mostra o que ele vai fazer sobre cada um.
165
174
  (`core/`, `phases/`, `reviewers/`, ...) que este catalogo usa. Por isso `plugin.json` declara os
166
175
  caminhos um a um. O teste compara esses caminhos com o catálogo fonte e exige
167
176
  `onboarding`, acrescentada por I15. Neste catálogo, `claude plugin details orkastery`
168
- deve reportar 26 entradas: 18 skills mais oito comandos. O startup registra
169
- separadamente skills e comandos. A prova anterior de T10, com 17 skills, é histórica.
177
+ deve reportar 28 entradas: 20 skills e 8 comandos (a linha `Skills (28)`). O startup registra
178
+ separadamente skills e comandos. As provas anteriores, com 17 e 18 skills, são históricas.
170
179
  Confira também os seis agentes e seis eventos de hooks, sem erros de carregamento.
171
180
 
172
181
  2. **Duas copias do catalogo divergem, e a divergencia so aparece quando ja custou uma entrega.**
@@ -12,8 +12,9 @@ etapas ou validação neste host e não invente respostas em nome do builder.
12
12
 
13
13
  Grave a resposta pública solicitada com `ork onboarding set <etapa> --conteudo <JSON> --por <quem>`.
14
14
  Transporte o JSON como argumento, com escaping de shell adequado; nunca execute o texto recebido.
15
- Peça somente nomes de variáveis para credenciais. Valores secretos ficam em `~/.hermes/.env`;
16
- não leia esse arquivo nem transporte valores para a conversa, onboarding ou ledger.
15
+ Peça somente nomes de variáveis para credenciais. Valores secretos ficam no ambiente do processo
16
+ ou no cofre do host (no Hermes, `~/.hermes/.env`); não os leia nem transporte valores para a
17
+ conversa, onboarding ou ledger.
17
18
 
18
19
  Reset solicitado usa `ork onboarding reset [etapa]`. Publicação opcional usa
19
20
  `ork onboarding sync --json`; apresente o motivo tipado de degradação sem bloquear a entrevista.
@@ -97,6 +97,17 @@ no incidente de 29/09. As outras tools continuam sob escolha do operador:
97
97
  `tools.alsoAllow: ["orkastery"]` no `openclaw.json` libera todas; allowlist e deny do operador
98
98
  sempre valem.
99
99
 
100
+ ## Prova de ativação do Maestro (RM-032)
101
+
102
+ `node core/scripts/prova-ativacao.cjs openclaw` instala esta extensão por
103
+ `ork adapter install openclaw --dir <estado temporário>`, a raiz global de uma cópia descartável
104
+ (`OPENCLAW_STATE_DIR` e `OPENCLAW_CONFIG_PATH` temporários, segredo do provedor só por SecretRef,
105
+ nada em `~/.openclaw`), e roda `openclaw agent --local` com `orkastery maestro` num workspace sem
106
+ manifesto, o cenário do incidente de 29/09. Aprova quando o modelo chama `ork_network_roadmap`
107
+ (ou `ork_maestro` com projeto) e o resultado é o do projeto descartável. A prova não roda
108
+ `openclaw plugins install --force` nem mexe em `plugins.allow`: o aviso de procedência vai ao
109
+ recibo, e a revisão fica com o dono.
110
+
100
111
  ## A #TAG de conducao
101
112
 
102
113
  O manifesto novo do OpenClaw nao tem campo proprio para isso, entao a regra vive aqui e
@@ -205,7 +205,7 @@ const FERRAMENTAS = [
205
205
  },
206
206
  {
207
207
  name: 'ork_onboarding',
208
- description: 'Consulta a pauta do núcleo e registra respostas públicas. Credenciais somente como nomes de variáveis; valores ficam em ~/.hermes/.env. Sync publica opcionalmente e relata degradação.',
208
+ description: 'Consulta a pauta do núcleo e registra respostas públicas. Credenciais somente como nomes de variáveis; valores ficam no ambiente do processo ou no cofre do host (no Hermes, ~/.hermes/.env). Sync publica opcionalmente e relata degradação.',
209
209
  parameters: {
210
210
  type: 'object', additionalProperties: false, required: ['acao'],
211
211
  properties: {
@@ -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.5.0",
5
+ "version": "0.5.1",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "properties": {},
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orkastery/openclaw-plugin",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Plugin Orkastery para OpenClaw: 27 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,
@@ -220,7 +220,7 @@ const FERRAMENTAS: FerramentaOrk[] = [
220
220
  },
221
221
  {
222
222
  name: 'ork_onboarding',
223
- description: 'Consulta a pauta do núcleo e registra respostas públicas. Credenciais somente como nomes de variáveis; valores ficam em ~/.hermes/.env. Sync publica opcionalmente e relata degradação.',
223
+ description: 'Consulta a pauta do núcleo e registra respostas públicas. Credenciais somente como nomes de variáveis; valores ficam no ambiente do processo ou no cofre do host (no Hermes, ~/.hermes/.env). Sync publica opcionalmente e relata degradação.',
224
224
  parameters: {
225
225
  type: 'object', additionalProperties: false, required: ['acao'],
226
226
  properties: {
@@ -71,6 +71,7 @@ const runtime_ambiente_1 = require("../runtime-ambiente");
71
71
  const path = __importStar(require("node:path"));
72
72
  const runtime_context_1 = require("../runtime-context");
73
73
  const runtime_profiles_1 = require("../runtime-profiles");
74
+ const mcp_grafo_1 = require("../mcp-grafo");
74
75
  function exec(cmd, args, cwd, timeoutMs, ambiente = (0, runtime_ambiente_1.ambienteDeAssinatura)()) {
75
76
  return (0, util_1.exec)(cmd, args, cwd, timeoutMs, ambiente);
76
77
  }
@@ -107,8 +108,14 @@ const MUTACOES_WORKTREE = [
107
108
  'mcp__orkastery__ork_artifact_write', 'mcp__orkastery__ork_claim_add', 'mcp__orkastery__ork_decision_record',
108
109
  'mcp__orkastery__ork_git_commit', 'mcp__orkastery__ork_verify', 'mcp__orkastery__ork_ship',
109
110
  ];
111
+ /**
112
+ * RM-031 KG5 (D7): as consultas do grafo, de leitura, so com a flag do manifesto da raiz; sem ela, nada muda.
113
+ * Montada na chamada: o `mcp-grafo` chega a este modulo pelo `mcp-git`, e no ciclo de imports o valor pode
114
+ * ainda nao existir quando este modulo carrega.
115
+ */
116
+ const consultasDoGrafoMcp = () => mcp_grafo_1.TOOLS_DO_GRAFO.map((nome) => `mcp__orkastery__${nome}`);
110
117
  /** Regras nativas de arquivo, nao sandbox de processos. Edit tambem cobre Write. */
111
- function permissoesDaWorktree(cwd, permiteEditarProduto) {
118
+ function permissoesDaWorktree(cwd, permiteEditarProduto, consultas) {
112
119
  if (!path.isAbsolute(cwd) || cwd === '/' || path.normalize(cwd) !== cwd || !/^[A-Za-z0-9_./-]+$/.test(cwd))
113
120
  throw Error('runtime.context.permissions: worktree contem caracteres ambiguos para regras nativas; use caminho sem glob, espacos ou delimitadores');
114
121
  const absoluto = '/' + cwd; // Claude exige // para caminho absoluto.
@@ -117,7 +124,7 @@ function permissoesDaWorktree(cwd, permiteEditarProduto) {
117
124
  for (const prefixo of [`${absoluto}/${nome}`, `${absoluto}/**/${nome}`])
118
125
  deny.push(`Edit(${prefixo})`, `Edit(${prefixo}/**)`);
119
126
  }
120
- return { allow: [...CONSULTAS_MCP, 'mcp__orkastery__ork_git_status', ...MUTACOES_WORKTREE, ...(permiteEditarProduto ? [`Edit(${absoluto}/**)`] : [])], deny };
127
+ return { allow: [...consultas, 'mcp__orkastery__ork_git_status', ...MUTACOES_WORKTREE, ...(permiteEditarProduto ? [`Edit(${absoluto}/**)`] : [])], deny };
121
128
  }
122
129
  /** O runtime esta disponivel nesta maquina? */
123
130
  function disponivel() {
@@ -152,13 +159,14 @@ function montarComando(pedido) {
152
159
  if (pedido.contextoRuntime.host !== 'claude-code')
153
160
  throw Error('runtime.context.invalid: host Claude esperado');
154
161
  const contexto = (0, runtime_context_1.validarContextoRuntime)(pedido.contextoRuntime, pedido.cwd);
162
+ const consultas = [...CONSULTAS_MCP, ...(contexto.grafoMcp ? consultasDoGrafoMcp() : [])];
155
163
  // Somente o servidor gerado: nao mesclar MCP de outros escopos nesta sessao filha.
156
164
  const permissoes = contexto.permissoesFilho === 'worktree'
157
- ? permissoesDaWorktree(pedido.cwd, contexto.permiteEditarProduto && !plano) : null;
165
+ ? permissoesDaWorktree(pedido.cwd, contexto.permiteEditarProduto && !plano, consultas) : null;
158
166
  // PLAN não implementa: nem commit, nem claim, nem verify, nem ship, em nenhum perfil. Decidir não é
159
167
  // implementar, e o PLAN é onde as decisões nascem (RM-037, achado S7 do CHECK): a decisão autônoma fica.
160
- const allow = plano ? [...CONSULTAS_MCP, 'mcp__orkastery__ork_artifact_write', 'mcp__orkastery__ork_decision_record']
161
- : permissoes?.allow ?? [...CONSULTAS_MCP];
168
+ const allow = plano ? [...consultas, 'mcp__orkastery__ork_artifact_write', 'mcp__orkastery__ork_decision_record']
169
+ : permissoes?.allow ?? consultas;
162
170
  const deny = [...(plano ? ESCRITA_DE_ARQUIVO : []), ...(permissoes?.deny ?? [])];
163
171
  args.push('--plugin-dir', contexto.instalacao, '--strict-mcp-config', '--mcp-config', JSON.stringify({ mcpServers: { orkastery: contexto.servidor } }), '--allowedTools', allow.join(','));
164
172
  if (deny.length)
@@ -189,12 +197,18 @@ function extrairSessionId(saida) {
189
197
  function listarSessoes(cwd, todas = false, ambiente = (0, runtime_ambiente_1.ambienteDeAssinatura)()) {
190
198
  return consultarSessoes(cwd, todas, ambiente).sessoes;
191
199
  }
192
- /** Falha de consulta não pode ser confundida com uma máquina sem sessões. */
200
+ /**
201
+ * Falha de consulta não pode ser confundida com uma máquina sem sessões. O binário ausente segue
202
+ * falha aqui (o observador e o pulse não concluem nada dele); só o inventário o trata como fonte
203
+ * ausente, com a correção.
204
+ */
193
205
  function consultarSessoes(cwd, todas = false, ambiente = (0, runtime_ambiente_1.ambienteDeAssinatura)()) {
194
206
  const args = ['agents', '--json'];
195
207
  if (todas)
196
208
  args.push('--all');
197
209
  const r = exec('claude', args, undefined, 60000, ambiente);
210
+ if (r.error === 'ENOENT')
211
+ return { ok: false, ausente: true, sessoes: [], detalhe: 'binário `claude` fora do PATH' };
198
212
  if (!r.ok)
199
213
  return { ok: false, sessoes: [], detalhe: `claude agents falhou (código ${r.code})` };
200
214
  let dados;
package/dist/auditrun.js CHANGED
@@ -798,7 +798,7 @@ function textoDaRodada(raiz, id) {
798
798
  L.push('');
799
799
  if (r.veredito) {
800
800
  L.push(` Claims do auditor: ${r.veredito.ok ? 'SUSTENTADAS' : 'REPROVADAS'} ` +
801
- `(motivos: ${r.veredito.motivos.join(', ') || 'nenhum'}) no HEAD ${r.veredito.commit.slice(0, 8)}`);
801
+ `(motivos: ${r.veredito.motivos.join(', ') || 'nenhum'}) no HEAD ${(0, util_1.shaCurto)(r.veredito.commit)}`);
802
802
  }
803
803
  else {
804
804
  L.push(` Claims do auditor: NAO verificadas. Rode: ork audit verify ${r.id}`);
@@ -20,7 +20,9 @@ exports.analisarComando = analisarComando;
20
20
  exports.analisarComandos = analisarComandos;
21
21
  exports.linhaDoLintDeClaim = linhaDoLintDeClaim;
22
22
  const CORRECAO = {
23
- 'suite-inteira': 'use `npm --prefix core run test:ci` (a suite hermetica) ou `node --test` no arquivo do teste',
23
+ // Fatia 2 do ensaio da 0.5.0 (P6): a correcao vale para qualquer projeto; o script do Orkastery saiu daqui.
24
+ 'suite-inteira': 'rode só o teste da claim (`node --test` no arquivo do teste, ou o comando do projeto que roda um arquivo) ' +
25
+ 'ou um script hermético do projeto',
24
26
  'sha-intermediario': 'ancore na base (`git diff "$(git merge-base origin/main HEAD)" HEAD`) ou prove ancestralidade com `git merge-base --is-ancestor <sha> HEAD`',
25
27
  'contagem-de-commits': 'use `git merge-base --is-ancestor <commit> HEAD`: contagem de commits quebra com rebase e merge',
26
28
  };
package/dist/docs.js CHANGED
@@ -46,6 +46,7 @@ exports.renderizarEstado = renderizarEstado;
46
46
  exports.textoDaVerificacao = textoDaVerificacao;
47
47
  exports.escreverYaml = escreverYaml;
48
48
  exports.mergeDaThread = mergeDaThread;
49
+ exports.mergesDaThreadNoGit = mergesDaThreadNoGit;
49
50
  exports.escopoPadraoDoSync = escopoPadraoDoSync;
50
51
  exports.sincronizarDocs = sincronizarDocs;
51
52
  exports.tabelaDoRoadmap = tabelaDoRoadmap;
@@ -262,9 +263,12 @@ function verificarDocs(raiz, opcoes = {}) {
262
263
  verificarFontes(raiz, d, comandos, achados);
263
264
  if (d.tipo === 'roadmap') {
264
265
  verificarEstadoDoRoadmap(raiz, d, base, achados);
266
+ if (base)
267
+ verificarMergeDaThread(raiz, d, base, achados, opcoes.pr === true);
265
268
  verificarTabelasDeEstado(d, achados);
266
269
  }
267
270
  }
271
+ verificarIndices(raiz, docs, achados, opcoes.pr === true);
268
272
  return { docs, achados };
269
273
  }
270
274
  function verificarChaves(d, achados) {
@@ -564,6 +568,57 @@ function verificarEstadoDoRoadmap(raiz, d, base, achados) {
564
568
  }
565
569
  }
566
570
  }
571
+ /**
572
+ * RM-037 (fatia 3, defeito 1): o outro sentido da paridade com o git. Depois do merge do PR, o
573
+ * frontmatter da RM-049 (#26) e da RM-051 (#33) seguiu em "Branch criada", e o verificador dizia OK:
574
+ * ele so conferia que `Mesclado` tem o commit na base. O merge desta fabrica e pelo GitHub, sem o
575
+ * `ork ship` no caminho, entao a prova mora aqui, no job que o CI obrigatorio roda. A regra usa o
576
+ * mesmo `ship(<thread>)` do `sincronizarDocs`: tudo o que ela acusa o `sincronizar` corrige.
577
+ */
578
+ function verificarMergeDaThread(raiz, d, base, achados, pr = false) {
579
+ const sdlc = ehMapa(d.dados.sdlc) ? d.dados.sdlc : null;
580
+ const thread = sdlc && typeof sdlc.thread === 'string' ? sdlc.thread.trim() : '';
581
+ if (!thread)
582
+ return;
583
+ const merges = mergesDaThreadNoGit(raiz, thread, base);
584
+ if (merges.length === 0)
585
+ return;
586
+ const codigo = String((ehMapa(d.dados.estado) ? d.dados.estado.codigo : null) ?? '');
587
+ if (codigo === 'Mesclado')
588
+ return;
589
+ achados.push((pr ? aviso : erro)(d.arquivo, d.id, 'docs.paridade.merge', `a thread ${thread} entrou na ${base} pelo merge ${merges[0].slice(0, 7)} (ship(${thread})), e o estado.codigo diz "${codigo || 'vazio'}"` +
590
+ (pr ? ' (no PR, aviso: o push da main reprova)' : ''), `abra um PR de docs sobre a ${base} atualizada com ork docs sincronizar --escrever --so ${d.id} (o item e os índices); ` +
591
+ `se o item tem outra fatia em curso, aponte sdlc.thread para a thread dela`));
592
+ }
593
+ /** Os indices gerados (`docs/roadmap/README.md`, `docs/produto/README.md`) e o que o frontmatter diz. */
594
+ const INDICES_GERADOS = [
595
+ [`${exports.DIR_ROADMAP}/README.md`, (docs) => tabelaDoRoadmap(docs)],
596
+ [`${exports.DIR_PRODUTO}/README.md`, (docs) => tabelaDoProduto(docs)],
597
+ ];
598
+ /**
599
+ * RM-037 (fatia 3, defeito 1): o indice e uma vista do frontmatter, como as tabelas do corpo, e mentia
600
+ * do mesmo jeito: sem a RM-051 e com a RM-031 de antes. Divergente reprova; vazio (o modelo recem-copiado
601
+ * pelo `ork docs init`) so avisa; sem os marcadores, nao ha indice gerado para conferir.
602
+ */
603
+ function verificarIndices(raiz, docs, achados, pr = false) {
604
+ for (const [arquivo, gerar] of INDICES_GERADOS) {
605
+ const abs = path.join(raiz, arquivo);
606
+ if (!fs.existsSync(abs))
607
+ continue;
608
+ // Checkout com CRLF (Windows com core.autocrlf) nao e divergencia: as paginas tambem normalizam.
609
+ const atual = blocoEntre(fs.readFileSync(abs, 'utf8').replace(/\r\n/g, '\n'), INICIO, FIM);
610
+ if (atual === null)
611
+ continue;
612
+ if (atual === '') {
613
+ if (docs.length > 0) {
614
+ achados.push(aviso(arquivo, undefined, 'docs.paridade.indice', 'o índice ainda não foi gerado', 'rode ork docs sincronizar --escrever'));
615
+ }
616
+ }
617
+ else if (atual !== gerar(docs)) {
618
+ achados.push((pr ? aviso : erro)(arquivo, undefined, 'docs.paridade.indice', `o índice diverge do frontmatter das páginas${pr ? ' (no PR, aviso: o push da main reprova)' : ''}`, 'o índice é gerado do frontmatter: rode ork docs sincronizar --escrever e commite o índice'));
619
+ }
620
+ }
621
+ }
567
622
  // --------------------------------------------------------------------------------------------
568
623
  // Saida para humano (TDAH) e para agente
569
624
  function textoDaVerificacao(docs, achados) {
@@ -666,13 +721,26 @@ function mergeDaThread(raiz, thread, base) {
666
721
  return { sha: String(ship.mergeSha).slice(0, 7), fonte: 'ledger ship_done' };
667
722
  }
668
723
  catch { /* thread sem estado nesta maquina: cai para o git */ }
669
- // Sem `-n 1`: o git corta antes de inverter, e `--reverse -n 1` devolveria o mais recente.
670
- const log = git(raiz, ['log', base, '--reverse', '--format=%H', '--fixed-strings', `--grep=ship(${thread})`]);
671
- const primeiro = log.ok ? log.saida.split('\n')[0] : '';
724
+ const primeiro = mergesDaThreadNoGit(raiz, thread, base)[0];
672
725
  if (primeiro)
673
726
  return { sha: primeiro.slice(0, 7), fonte: `git log ${base}` };
674
727
  return null;
675
728
  }
729
+ /**
730
+ * Os commits `ship(<thread>)` da base, do mais velho ao mais novo, so pelo git (o CI nao tem ledger).
731
+ * RM-037 (fatia 3): um lugar so para o `sincronizarDocs` e o `verificarDocs` acharem o mesmo merge.
732
+ */
733
+ function mergesDaThreadNoGit(raiz, thread, base) {
734
+ // Sem `-n 1`: o git corta antes de inverter, e `--reverse -n 1` devolveria o mais recente.
735
+ // GO-FIX 2 (achado 4 do CHECK): so a linha de primeiro pai da base e o assunto que COMECA com
736
+ // `ship(<thread>)`, como no `mergeDaEntrega`. Commit que cita o merge no corpo, ou o Revert dele, nao e merge.
737
+ const log = git(raiz, ['log', base, '--first-parent', '--reverse', '--format=%H%x09%s', '--fixed-strings', `--grep=ship(${thread})`]);
738
+ if (!log.ok)
739
+ return [];
740
+ return log.saida.split('\n').map((l) => l.split('\t'))
741
+ .filter(([sha, assunto]) => !!sha && typeof assunto === 'string' && assunto.startsWith(`ship(${thread})`))
742
+ .map(([sha]) => sha);
743
+ }
676
744
  function temBranch(raiz, thread) {
677
745
  const r = git(raiz, ['for-each-ref', '--format=%(refname:short)', `refs/heads/ork/${thread}-*`, `refs/remotes/origin/ork/${thread}-*`]);
678
746
  return r.ok && r.saida.length > 0;
package/dist/doctor.js CHANGED
@@ -39,9 +39,14 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  };
40
40
  })();
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.TETO_DO_DONO_DO_GIT = void 0;
42
43
  exports.checarContas = checarContas;
43
44
  exports.checarChaveDeEmbedding = checarChaveDeEmbedding;
45
+ exports.checarDonoDoGit = checarDonoDoGit;
44
46
  exports.checarOnboarding = checarOnboarding;
47
+ exports.blocosDoRuntime = blocosDoRuntime;
48
+ exports.checarRuntimeClaude = checarRuntimeClaude;
49
+ exports.checarDespachoPeloCodex = checarDespachoPeloCodex;
45
50
  exports.checar = checar;
46
51
  exports.relatorio = relatorio;
47
52
  exports.doctor = doctor;
@@ -63,6 +68,7 @@ const project_state_1 = require("./project-state");
63
68
  const runtime_ambiente_1 = require("./runtime-ambiente");
64
69
  const runtime_profiles_1 = require("./runtime-profiles");
65
70
  const preflight_1 = require("./preflight");
71
+ const setup_1 = require("./setup");
66
72
  /**
67
73
  * I-33 (D7): check "contas por runtime". Cada perfil ativo tem o login conferido pelo proprio
68
74
  * CLI com o env dele (`claude auth status`, `codex login status`); o doctor so le e relata, sem
@@ -148,6 +154,81 @@ function checarChaveDeEmbedding(carregado, env = process.env) {
148
154
  return { nome, nivel: 'warn', detalhe: `${variavel} ausente do ambiente: a busca por significado usa o fallback local ou cai para FTS; o regime nao muda`,
149
155
  correcao: `exporte ${variavel} com a chave dedicada ao Orkastery (com limite de credito no painel do provider)` };
150
156
  }
157
+ /** Acima disso a conferencia do dono do `.git` fica parcial (aviso), para o doctor nao pesar. */
158
+ exports.TETO_DO_DONO_DO_GIT = 200_000;
159
+ /**
160
+ * RM-037 (fatia 3, defeito 7): git rodado como root no repositorio do dono deixa objeto, pasta e
161
+ * `FETCH_HEAD` com outro dono, e o git do dono deixa de gravar neles: o fetch travou em 29/09 e em
162
+ * 01/10. O dono do repositorio e o dono do diretorio comum do git. O check acusa com a contagem, ate
163
+ * tres exemplos e o `chown` exato; o doctor nunca roda nada. Fora de repositorio git, `null`.
164
+ */
165
+ function checarDonoDoGit(dirInicial, opcoes = {}) {
166
+ const nome = 'dono do .git';
167
+ const comum = (0, util_1.exec)('git', ['rev-parse', '--git-common-dir'], dirInicial);
168
+ if (!comum.ok || !comum.stdout.trim())
169
+ return null;
170
+ const dirComum = path.resolve(dirInicial, comum.stdout.trim());
171
+ const lstat = opcoes.lstat ?? ((p) => fs.lstatSync(p));
172
+ let raiz;
173
+ try {
174
+ raiz = lstat(dirComum);
175
+ }
176
+ catch {
177
+ return null;
178
+ }
179
+ const teto = opcoes.teto ?? exports.TETO_DO_DONO_DO_GIT;
180
+ const relativo = (f) => path.relative(path.dirname(dirComum), f);
181
+ // A correcao sai pronta para copiar: caminho com caractere fora do conjunto seguro vai entre aspas simples.
182
+ const noShell = (s) => /^[A-Za-z0-9._/-]+$/.test(s) ? s : `'${s.replace(/'/g, "'\\''")}'`;
183
+ const estranhos = [], uids = new Set();
184
+ let total = 0, vistos = 0, parcial = false;
185
+ const pilha = [dirComum];
186
+ while (pilha.length > 0 && !parcial) {
187
+ const atual = pilha.pop();
188
+ let nomes;
189
+ // Pasta que o dono nao consegue listar ja aparece pelo dono dela, na entrada de cima.
190
+ try {
191
+ nomes = fs.readdirSync(atual);
192
+ }
193
+ catch {
194
+ continue;
195
+ }
196
+ for (const n of nomes) {
197
+ if (++vistos > teto) {
198
+ parcial = true;
199
+ break;
200
+ }
201
+ const f = path.join(atual, n);
202
+ let st;
203
+ try {
204
+ st = lstat(f);
205
+ }
206
+ catch {
207
+ continue;
208
+ }
209
+ if (st.uid !== raiz.uid) {
210
+ total++;
211
+ uids.add(st.uid);
212
+ if (estranhos.length < 3)
213
+ estranhos.push(relativo(f));
214
+ }
215
+ if (st.isDirectory())
216
+ pilha.push(f);
217
+ }
218
+ }
219
+ if (total > 0) {
220
+ return { nome, nivel: 'fail',
221
+ detalhe: `${total} entrada(s) de ${relativo(dirComum)} com outro dono (uid ${[...uids].sort((a, b) => a - b).join(', ')}; o do ` +
222
+ `repositorio e ${raiz.uid}): ${estranhos.join(', ')}${total > estranhos.length ? ', ...' : ''}; o git do dono nao grava nelas, ` +
223
+ `e o fetch e o commit falham${parcial ? ` (conferencia parcial: mais de ${teto} entradas)` : ''}`,
224
+ correcao: `sudo chown -R ${raiz.uid}:${raiz.gid} ${noShell(dirComum)} (o ork nao roda isso sozinho)` };
225
+ }
226
+ if (parcial) {
227
+ return { nome, nivel: 'warn', detalhe: `mais de ${teto} entradas em ${relativo(dirComum)}: conferencia parcial, sem outro dono ate aqui`,
228
+ correcao: `confira o resto com: find ${noShell(dirComum)} -not -uid ${raiz.uid}` };
229
+ }
230
+ return { nome, nivel: 'ok', detalhe: `${vistos} entradas de ${relativo(dirComum)} com o dono do repositorio (uid ${raiz.uid})` };
231
+ }
151
232
  /** Checks locais; ler a entrevista não abre driver, banco, runtime ou rede. */
152
233
  function checarOnboarding(carregado) {
153
234
  const estado = (0, onboarding_1.lerOnboarding)(carregado.raiz);
@@ -163,9 +244,15 @@ function checarOnboarding(carregado) {
163
244
  checks.push({ nome: 'onboarding memoria', nivel: 'warn', detalhe: `entrevista escolheu ${modo}; manifesto declara ${carregado.manifesto.memory.mode}`,
164
245
  correcao: `revise memory.mode: ${modo} em orkastery.yaml; a entrevista não altera o manifesto` });
165
246
  }
166
- // I-35: o fuso respondido na etapa maestro orienta owner.timezone, sem editar o manifesto.
247
+ // I-35: o fuso respondido na etapa maestro orienta owner.timezone, sem editar o manifesto. Com
248
+ // `owner.timezone` na mesma resposta (gravado no manifesto pelo `onboarding set`), vale ele: no
249
+ // ensaio da 0.5.0, o `fuso` legado da resposta anterior mandava desfazer a escolha explicita.
167
250
  const maestro = estado.etapas.maestro?.conteudo;
168
- const fusoDaEntrevista = maestro && typeof maestro === 'object' && !Array.isArray(maestro) ? (0, horario_1.normalizarFuso)(maestro.fuso) : undefined;
251
+ const owner = maestro && typeof maestro === 'object' && !Array.isArray(maestro) ? maestro.owner : undefined;
252
+ const fusoDoOwner = owner && typeof owner === 'object' && !Array.isArray(owner) ? owner.timezone : undefined;
253
+ // Owner invalido (so por edicao a mao: o `onboarding set` recusa) nao cala o fuso legado.
254
+ const fusoDaEntrevista = maestro && typeof maestro === 'object' && !Array.isArray(maestro)
255
+ ? (0, horario_1.normalizarFuso)(fusoDoOwner) ?? (0, horario_1.normalizarFuso)(maestro.fuso) : undefined;
169
256
  if (fusoDaEntrevista && fusoDaEntrevista !== carregado.manifesto.owner?.timezone) {
170
257
  checks.push({ nome: 'onboarding fuso', nivel: 'warn',
171
258
  detalhe: `entrevista informou ${fusoDaEntrevista}; manifesto declara ${carregado.manifesto.owner?.timezone ?? `${horario_1.CHAVE_DO_FUSO} ausente`}`,
@@ -173,6 +260,97 @@ function checarOnboarding(carregado) {
173
260
  }
174
261
  return checks;
175
262
  }
263
+ /**
264
+ * Fatia 2 do ensaio da 0.5.0 (P1): por qual runtime cada bloco dos modos de
265
+ * `conduction.allowed_modes` despacha, pelo mesmo criterio do despacho sem opcao de CLI
266
+ * (`resolverDespacho`: o bloco do setup vence `runtime.adapter`). Quem passou os blocos para o
267
+ * Codex nao precisa do `claude`. Setup ilegivel devolve `null`: quem chama fica no lado seguro.
268
+ */
269
+ function blocosDoRuntime(carregado, runtime) {
270
+ const { manifesto } = carregado;
271
+ const r = { principal: [], fallback: [] };
272
+ try {
273
+ const setup = (0, setup_1.lerSetup)(carregado.raiz);
274
+ for (const modo of manifesto.conduction.allowed_modes) {
275
+ modos_1.MODOS[modo].blocos.forEach((b, i) => {
276
+ const doBloco = (0, setup_1.configDoBloco)(setup, modo, b.fases[0]);
277
+ const bloco = { tag: modos_1.MODOS[modo].tag, bloco: i + 1 };
278
+ if ((doBloco?.runtime ?? manifesto.runtime.adapter) === runtime)
279
+ r.principal.push(bloco);
280
+ else if (doBloco?.fallback?.some((f) => f.split(':')[0] === runtime))
281
+ r.fallback.push(bloco);
282
+ });
283
+ }
284
+ }
285
+ catch {
286
+ return null;
287
+ }
288
+ return r;
289
+ }
290
+ /** `#Classic 1, 2; #Auto 1`: os blocos agrupados por modo, na ordem dos modos permitidos. */
291
+ function textoDosBlocos(blocos) {
292
+ const porModo = new Map();
293
+ for (const b of blocos)
294
+ porModo.set(b.tag, [...(porModo.get(b.tag) ?? []), b.bloco]);
295
+ return [...porModo].map(([tag, numeros]) => `${tag} ${numeros.join(', ')}`).join('; ');
296
+ }
297
+ /**
298
+ * Fatia 2 do ensaio da 0.5.0 (P1): o `claude` so e obrigatorio quando algum bloco de modo permitido
299
+ * despacha por ele. Sem manifesto, com manifesto invalido ou com setup ilegivel vale o padrao (todo
300
+ * bloco no claude-bg), e a falta reprova como antes.
301
+ */
302
+ function checarRuntimeClaude(carregado, claude, versao) {
303
+ const nome = 'runtime claude-bg';
304
+ if (claude)
305
+ return { nome, nivel: 'ok', detalhe: `${claude} (${versao ?? 'versao desconhecida'})` };
306
+ const blocos = carregado && carregado.erros.length === 0 ? blocosDoRuntime(carregado, 'claude-bg') : null;
307
+ if (blocos && blocos.principal.length === 0) {
308
+ // CHECK, rodada 1 (S6): as fases nao precisam dele, mas o `ork audit run` despacha sempre pelo claude-bg.
309
+ return { nome, nivel: 'warn',
310
+ detalhe: 'binario `claude` fora do PATH (opcional para as fases: nenhum bloco dos modos permitidos despacha pelo claude-bg' +
311
+ (blocos.fallback.length
312
+ ? `; o fallback de ${blocos.fallback.length} bloco(s) cai nele e falharia: ${textoDosBlocos(blocos.fallback)}` : '') +
313
+ '; o ork audit run ainda despacha por ele)',
314
+ correcao: 'para despachar pelo claude-bg (e rodar ork audit run), instale o Claude Code e garanta `claude` no PATH' };
315
+ }
316
+ return { nome, nivel: 'fail',
317
+ detalhe: 'binario `claude` fora do PATH' + (blocos
318
+ ? `; ${blocos.principal.length} bloco(s) dos modos permitidos despacham por ele (${textoDosBlocos(blocos.principal)})` : ''),
319
+ correcao: 'instale o Claude Code e garanta `claude` no PATH; so com o Codex, passe cada bloco para ele ' +
320
+ '(ork setup <modo> --bloco N --runtime codex --model <modelo>) ou tire o modo de conduction.allowed_modes ' +
321
+ '(mantenha o conduction.default_mode entre os permitidos)' };
322
+ }
323
+ /**
324
+ * O despacho pelo Codex quando o projeto o elege: por `runtime.adapter: codex`, como antes, ou, desde
325
+ * a fatia 2 do ensaio da 0.5.0 (P1), por bloco de modo permitido. A falta do `codex`, ou um sandbox
326
+ * que nao executa, deixa de ser aviso: e exatamente o que bloquearia a fase. Sem nenhum dos dois, o
327
+ * check nao sai.
328
+ */
329
+ function checarDespachoPeloCodex(carregado, codex, sonda) {
330
+ const pelaAdapter = carregado.manifesto.runtime.adapter === 'codex';
331
+ const blocos = blocosDoRuntime(carregado, 'codex')?.principal ?? [];
332
+ if (!pelaAdapter && blocos.length === 0)
333
+ return null;
334
+ const origem = [pelaAdapter ? 'runtime.adapter: codex' : '',
335
+ blocos.length ? `${blocos.length} bloco(s) dos modos permitidos no codex (${textoDosBlocos(blocos)})` : '']
336
+ .filter(Boolean).join(' e ');
337
+ const sandboxConfigurado = carregado.manifesto.runtime.sandbox;
338
+ const sandboxQuebrado = sonda !== null && !sonda.ok && sandboxConfigurado !== 'danger-full-access';
339
+ return {
340
+ nome: 'despacho pelo codex',
341
+ nivel: !codex ? 'fail' : sandboxQuebrado ? 'fail' : 'ok',
342
+ detalhe: !codex
343
+ ? `${origem}, mas o binario \`codex\` esta fora do PATH`
344
+ : sandboxQuebrado
345
+ ? `${origem} com sandbox "${sandboxConfigurado}", mas o sandbox nao executa nesta maquina (o agente narraria sucesso sem rodar nada)`
346
+ : `${origem} com sandbox "${sandboxConfigurado}"`,
347
+ correcao: !codex
348
+ ? 'instale o Codex CLI e autentique com `codex login` (assinatura, nunca API key)'
349
+ : sandboxQuebrado
350
+ ? 'instale o bubblewrap do sistema, ou declare runtime.sandbox: danger-full-access ciente do risco'
351
+ : undefined,
352
+ };
353
+ }
176
354
  /** Roda todos os checks a partir do diretorio informado. */
177
355
  function checar(dirInicial = process.cwd(), nomesHerdados = (0, runtime_ambiente_1.nomesDeProviderAtivos)()) {
178
356
  const checks = [];
@@ -192,31 +370,43 @@ function checar(dirInicial = process.cwd(), nomesHerdados = (0, runtime_ambiente
192
370
  });
193
371
  const repo = (0, util_1.exec)('git', ['rev-parse', '--is-inside-work-tree'], dirInicial);
194
372
  const dentroDeRepo = repo.ok && repo.stdout.trim() === 'true';
195
- const branch = (0, util_1.exec)('git', ['rev-parse', '--abbrev-ref', 'HEAD'], dirInicial);
373
+ // Ensaio da 0.5.0: sem commit, o `rev-parse --abbrev-ref` respondia "HEAD"; a branch vem do
374
+ // `symbolic-ref`, e a falta de commit fica dita.
375
+ const semCommit = dentroDeRepo && !(0, util_1.exec)('git', ['rev-parse', '--verify', '--quiet', 'HEAD'], dirInicial).ok;
376
+ const branch = dentroDeRepo ? (0, util_1.branchDoHead)(dirInicial) ?? (0, util_1.exec)('git', ['rev-parse', '--abbrev-ref', 'HEAD'], dirInicial).stdout.trim() : '';
196
377
  checks.push({
197
378
  nome: 'repositorio',
198
379
  nivel: dentroDeRepo ? 'ok' : 'fail',
199
- detalhe: dentroDeRepo ? `branch ${branch.stdout.trim()}` : 'fora de um repositorio git',
380
+ detalhe: dentroDeRepo ? `branch ${branch}${semCommit ? ' (sem commit)' : ''}` : 'fora de um repositorio git',
200
381
  correcao: dentroDeRepo ? undefined : 'rode o ork dentro de um repositorio git',
201
382
  });
383
+ if (dentroDeRepo) {
384
+ const dono = checarDonoDoGit(dirInicial);
385
+ if (dono)
386
+ checks.push(dono);
387
+ }
388
+ // Fatia 2 do ensaio da 0.5.0 (P1): o manifesto e lido antes dos runtimes, porque e o setup dos
389
+ // modos permitidos que diz se o `claude` e obrigatorio; a ordem das linhas do relatorio nao muda.
390
+ const carregado = (0, manifest_1.carregarManifesto)(dirInicial);
202
391
  const claude = adapter.disponivel();
203
392
  const v = claude ? adapter.versao() : null;
204
- checks.push({
205
- nome: 'runtime claude-bg',
206
- nivel: claude ? 'ok' : 'fail',
207
- detalhe: claude ? `${claude} (${v ?? 'versao desconhecida'})` : 'binario `claude` fora do PATH',
208
- correcao: claude ? undefined : 'instale o Claude Code e garanta `claude` no PATH',
209
- });
393
+ checks.push(checarRuntimeClaude(carregado, claude, v));
210
394
  // Segundo runtime homologado. Ausente e `warn`, nao `fail`: o claude-bg segue sendo o
211
395
  // padrao, e um projeto que nunca pediu codex nao pode ficar bloqueado por ele.
212
396
  const codex = codexAdapter.disponivel();
213
397
  const codexVersao = codex ? codexAdapter.versao() : null;
398
+ // CHECK, rodada 1 (S6): com o projeto despachando pelo codex, a falta dele nao e "opcional"; quem
399
+ // reprova e o check `despacho pelo codex`, mais abaixo.
400
+ const usaCodex = !!carregado && carregado.erros.length === 0 && (carregado.manifesto.runtime.adapter === 'codex' ||
401
+ (blocosDoRuntime(carregado, 'codex')?.principal.length ?? 0) > 0);
214
402
  checks.push({
215
403
  nome: 'runtime codex',
216
404
  nivel: codex ? 'ok' : 'warn',
217
405
  detalhe: codex
218
406
  ? `${codex} (${codexVersao ?? 'versao desconhecida'})`
219
- : 'binario `codex` fora do PATH (opcional: claude-bg e o runtime padrao)',
407
+ : usaCodex
408
+ ? 'binario `codex` fora do PATH (o projeto despacha por ele: veja despacho pelo codex)'
409
+ : 'binario `codex` fora do PATH (opcional: claude-bg e o runtime padrao)',
220
410
  correcao: codex
221
411
  ? undefined
222
412
  : 'para despachar pelo codex, instale o Codex CLI e autentique com `codex login`',
@@ -238,7 +428,6 @@ function checar(dirInicial = process.cwd(), nomesHerdados = (0, runtime_ambiente
238
428
  'nesta maquina) ou declare runtime.sandbox: danger-full-access ciente do risco',
239
429
  });
240
430
  }
241
- const carregado = (0, manifest_1.carregarManifesto)(dirInicial);
242
431
  if (!carregado) {
243
432
  checks.push({
244
433
  nome: 'manifesto',
@@ -285,26 +474,11 @@ function checar(dirInicial = process.cwd(), nomesHerdados = (0, runtime_ambiente
285
474
  : `${fuso.fuso} (fuso do sistema; ${horario_1.CHAVE_DO_FUSO} nao configurado)`),
286
475
  correcao: fuso.aviso ? `corrija ${horario_1.CHAVE_DO_FUSO} com um nome IANA` : undefined,
287
476
  });
288
- // Quando o manifesto ELEGE o codex como runtime de despacho, a ausencia dele (ou um
477
+ // Quando o manifesto ou um bloco de modo permitido ELEGE o codex, a ausencia dele (ou um
289
478
  // sandbox que nao executa) deixa de ser aviso: e exatamente o que bloquearia a fase.
290
- if (carregado.manifesto.runtime.adapter === 'codex') {
291
- const sandboxConfigurado = carregado.manifesto.runtime.sandbox;
292
- const sandboxQuebrado = sondaDoCodex !== null && !sondaDoCodex.ok && sandboxConfigurado !== 'danger-full-access';
293
- checks.push({
294
- nome: 'despacho pelo codex',
295
- nivel: !codex ? 'fail' : sandboxQuebrado ? 'fail' : 'ok',
296
- detalhe: !codex
297
- ? 'runtime.adapter: codex, mas o binario `codex` esta fora do PATH'
298
- : sandboxQuebrado
299
- ? `runtime.adapter: codex com sandbox "${sandboxConfigurado}", mas o sandbox nao executa nesta maquina (o agente narraria sucesso sem rodar nada)`
300
- : `runtime.adapter: codex com sandbox "${sandboxConfigurado}"`,
301
- correcao: !codex
302
- ? 'instale o Codex CLI e autentique com `codex login` (assinatura, nunca API key)'
303
- : sandboxQuebrado
304
- ? 'instale o bubblewrap do sistema, ou declare runtime.sandbox: danger-full-access ciente do risco'
305
- : undefined,
306
- });
307
- }
479
+ const despachoPeloCodex = checarDespachoPeloCodex(carregado, codex, sondaDoCodex);
480
+ if (despachoPeloCodex)
481
+ checks.push(despachoPeloCodex);
308
482
  const politica = carregado.manifesto.runtime.provider_policy;
309
483
  const soAssinatura = politica === 'subscription-only';
310
484
  const redirecionam = ENVS_QUE_REDIRECIONAM.filter(e => nomesHerdados.includes(e));
@@ -385,12 +559,15 @@ function checar(dirInicial = process.cwd(), nomesHerdados = (0, runtime_ambiente
385
559
  checks.push(...(0, preflight_1.sondasDeAmbiente)(carregado.raiz));
386
560
  {
387
561
  const inventario = (0, sessoes_inventario_1.inventariarSessoes)(carregado.raiz, { global: true, todas: true });
562
+ // Fatia 2 do ensaio da 0.5.0 (P1): binario ausente e fonte ausente, dita no detalhe.
563
+ const ausentes = inventario.fontes.filter((f) => f.ausente).map((f) => f.origem);
388
564
  checks.push({
389
565
  nome: 'governanca de sessoes',
390
566
  nivel: inventario.ok && inventario.semThread === 0 ? 'ok' : 'warn',
391
567
  detalhe: `${inventario.escopo.usuario}, Claude e Codex, global com histórico: ${inventario.total} sessões; ` +
392
568
  `${inventario.semThread} sem thread; ${inventario.ambiguas} ambíguas; ` +
393
- `inventário ${inventario.ok ? 'válido' : 'INCOMPLETO (zero não comprovado)'}`,
569
+ `inventário ${inventario.ok ? 'válido' : 'INCOMPLETO (zero não comprovado)'}` +
570
+ (ausentes.length ? `; fonte ausente: ${ausentes.join(', ')}` : ''),
394
571
  correcao: inventario.ok && inventario.semThread === 0 ? undefined :
395
572
  'ork sessions --global --all --json; adote identidades sem vínculo com ork sessions adopt <id>',
396
573
  });