@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
package/dist/policies.js CHANGED
@@ -96,6 +96,17 @@ exports.POLICIES_CONHECIDAS = {
96
96
  descricao: 'avisa quando a branch da thread esta atras da base',
97
97
  },
98
98
  };
99
+ /**
100
+ * Ensaio da 0.5.0: a thread criada sem `--worktree auto` entrega a base para a base, e a correcao
101
+ * repetia o proprio `ork ship`. A thread que ainda nao passou do GO ganha a branch com
102
+ * `ork worktree ensure`. Depois do GO, nao: os commits ja estao na base, a branch nova nasce com eles
103
+ * e o ship sem delta empurraria a base direto, que e o que esta policy barra (CHECK, rodada 1).
104
+ */
105
+ function correcaoSemBranchDaThread(threadId, base) {
106
+ return `entregue a branch da thread (ork/<slug>) com --para ${base || '<base>'}; sem worktree e antes do GO, ` +
107
+ `crie-a com ork worktree ensure ${threadId || '<thread>'} (thread nova: --worktree auto); depois do GO, ` +
108
+ 'os commits ja estao na base e o ship nao os separa: leve-os para a branch da thread antes de entregar';
109
+ }
99
110
  function severidade(bruta) {
100
111
  if (bruta === 'block' || bruta === 'warn' || bruta === 'off')
101
112
  return bruta;
@@ -158,7 +169,7 @@ function avaliarPolicies(manifesto, ctx) {
158
169
  severidade: sev,
159
170
  motivo: 'policy.violation',
160
171
  detalhe: `origem e destino sao a mesma branch ("${de}"): isso e push direto na base, nao merge de thread`,
161
- correcao: 'entregue a partir da branch da thread: ork ship <thread> --para ' + (base || 'main'),
172
+ correcao: correcaoSemBranchDaThread(ctx.threadId, base),
162
173
  });
163
174
  }
164
175
  else if (de && base && de === base) {
@@ -167,7 +178,21 @@ function avaliarPolicies(manifesto, ctx) {
167
178
  severidade: sev,
168
179
  motivo: 'policy.violation',
169
180
  detalhe: `a origem "${de}" e a propria branch base do projeto: nenhuma branch de thread foi usada`,
170
- correcao: 'crie a thread com --worktree auto e entregue a branch ork/<slug>',
181
+ correcao: correcaoSemBranchDaThread(ctx.threadId, base),
182
+ });
183
+ }
184
+ else if (ctx.semDeltaComBaseAFrente === true) {
185
+ // Fatia 2 do ensaio da 0.5.0 (P9): a thread que perdeu a branch depois do GO ganha uma com os commits
186
+ // ja na base; o ship ve nada a mergear e empurraria a base, que e o que esta policy barra.
187
+ const remoto = ctx.remoto ?? 'origin';
188
+ violacoes.push({
189
+ policy: nome,
190
+ severidade: sev,
191
+ motivo: 'policy.violation',
192
+ detalhe: `a branch "${de}" nao traz commit alem de "${para}", e ${para} local tem commit que ${remoto}/${para} nao tem: ` +
193
+ 'o ship empurraria a base direto, sem merge de thread',
194
+ correcao: `leve os commits que estao so em ${para} local para a branch da thread antes de entregar, ou entregue-os ` +
195
+ `pela thread que os criou (conferido em refs/remotes/${remoto}/${para}, sem rede)`,
171
196
  });
172
197
  }
173
198
  continue;
@@ -0,0 +1,345 @@
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.ENTRADAS = exports.HOSTS_DA_PROVA = void 0;
37
+ exports.transcriptDoClaude = transcriptDoClaude;
38
+ exports.transcriptDoOpenclaw = transcriptDoOpenclaw;
39
+ exports.extrairSnapshot = extrairSnapshot;
40
+ exports.conferirProva = conferirProva;
41
+ exports.redigir = redigir;
42
+ exports.redigirObjeto = redigirObjeto;
43
+ /**
44
+ * RM-032: conferência da prova de ativação por host, sem host.
45
+ *
46
+ * O roteiro `core/scripts/prova-ativacao.cjs` abre uma sessão nova e não interativa no host
47
+ * (Claude Code ou OpenClaw), diz `orkastery maestro` e entrega aqui o transcript. Esta
48
+ * conferência olha só o que é determinístico: qual ferramenta o modelo chamou, o resultado que
49
+ * ela devolveu (contra o contrato da entrada) e o projeto lido. Da resposta final em texto
50
+ * livre, exige apenas que cite o projeto e não repita o incidente de 29/09 ("roadmap vazio").
51
+ */
52
+ const fs = __importStar(require("node:fs"));
53
+ const path = __importStar(require("node:path"));
54
+ const maestro_contract_1 = require("./maestro-contract");
55
+ exports.HOSTS_DA_PROVA = ['claude-code', 'openclaw'];
56
+ /**
57
+ * As entradas da frase em cada host. No Claude, a tool MCP do servidor `orkastery` fixado no
58
+ * projeto (MAESTRO_HOST_SURFACES em hosts.ts). No OpenClaw 0.5.0 (RM-054, fatia 2), a frase sem
59
+ * projeto vai a `ork_network_roadmap`, a única tool da extensão no perfil `coding`; com projeto
60
+ * nomeado, a `ork_maestro`. `ork maestro` pelo shell do host é desvio: o CLI resolve o projeto
61
+ * pelo diretório atual, a classe de erro do incidente.
62
+ */
63
+ exports.ENTRADAS = {
64
+ 'claude-code': [{ ferramenta: 'mcp__orkastery__ork_maestro', contrato: 'maestro' }],
65
+ openclaw: [{ ferramenta: 'ork_network_roadmap', contrato: 'rede' }, { ferramenta: 'ork_maestro', contrato: 'maestro' }],
66
+ };
67
+ const SHELL_DO_HOST = { 'claude-code': 'Bash', openclaw: 'exec' };
68
+ const ORK_MAESTRO_NO_SHELL = /(^|[\s;&|(/])ork(\s+(--projeto|--project|-p)(\s+|=)\S+)?\s+maestro\b/;
69
+ function linhasJson(texto) {
70
+ const eventos = [];
71
+ for (const linha of texto.split('\n')) {
72
+ const limpa = linha.trim();
73
+ if (!limpa.startsWith('{'))
74
+ continue;
75
+ try {
76
+ eventos.push(JSON.parse(limpa));
77
+ }
78
+ catch { /* linha de log do host, não evento */ }
79
+ }
80
+ return eventos;
81
+ }
82
+ function textoDoConteudo(conteudo) {
83
+ if (typeof conteudo === 'string')
84
+ return conteudo;
85
+ if (!Array.isArray(conteudo))
86
+ return null;
87
+ const partes = conteudo.filter((c) => !!c && typeof c === 'object' && c.type === 'text' && typeof c.text === 'string');
88
+ return partes.length ? partes.map(p => p.text).join('\n') : null;
89
+ }
90
+ function classificar(host, ferramenta, argumentos) {
91
+ const entrada = exports.ENTRADAS[host].find(e => e.ferramenta === ferramenta);
92
+ if (entrada)
93
+ return { via: 'entrada', contrato: entrada.contrato };
94
+ const comando = argumentos?.command;
95
+ if (ferramenta === SHELL_DO_HOST[host] && typeof comando === 'string' && ORK_MAESTRO_NO_SHELL.test(comando))
96
+ return { via: 'shell', contrato: null };
97
+ return null;
98
+ }
99
+ /** Transcript `claude -p --output-format stream-json --verbose`. */
100
+ function transcriptDoClaude(streamJson) {
101
+ const eventos = linhasJson(streamJson);
102
+ let ferramentasExpostas = null;
103
+ let respostaFinal = null;
104
+ const pedidas = new Map();
105
+ const chamadas = [];
106
+ for (const e of eventos) {
107
+ if (e.type === 'system' && e.subtype === 'init' && Array.isArray(e.tools))
108
+ ferramentasExpostas = e.tools.filter((t) => typeof t === 'string');
109
+ const mensagem = e.message;
110
+ const blocos = Array.isArray(mensagem?.content) ? mensagem.content : [];
111
+ if (e.type === 'assistant') {
112
+ for (const b of blocos) {
113
+ if (b?.type !== 'tool_use' || typeof b.name !== 'string' || typeof b.id !== 'string')
114
+ continue;
115
+ const tipo = classificar('claude-code', b.name, b.input);
116
+ if (!tipo)
117
+ continue;
118
+ const chamada = { ferramenta: b.name, ...tipo, argumentos: b.input ?? null, resultado: null, erro: false };
119
+ pedidas.set(b.id, chamada);
120
+ chamadas.push(chamada);
121
+ }
122
+ }
123
+ else if (e.type === 'user') {
124
+ for (const b of blocos) {
125
+ if (b?.type !== 'tool_result' || typeof b.tool_use_id !== 'string')
126
+ continue;
127
+ const chamada = pedidas.get(b.tool_use_id);
128
+ if (!chamada)
129
+ continue;
130
+ chamada.resultado = textoDoConteudo(b.content);
131
+ chamada.erro = b.is_error === true;
132
+ }
133
+ }
134
+ else if (e.type === 'result' && typeof e.result === 'string')
135
+ respostaFinal = e.result;
136
+ }
137
+ return { ferramentasExpostas, chamadas, respostaFinal };
138
+ }
139
+ /**
140
+ * Trajetória do OpenClaw (`openclaw sessions export-trajectory`, `events.jsonl`): eventos
141
+ * `tool.call`/`tool.result` da origem `runtime`. As ferramentas expostas e a resposta final vêm
142
+ * do `openclaw agent --json` (`meta.systemPromptReport.tools.entries`, `payloads[].text`).
143
+ */
144
+ function transcriptDoOpenclaw(eventsJsonl, agenteJson) {
145
+ const pedidas = new Map();
146
+ const chamadas = [];
147
+ for (const e of linhasJson(eventsJsonl)) {
148
+ if (e.source !== 'runtime')
149
+ continue;
150
+ const d = (e.data ?? {});
151
+ if (typeof d.toolCallId !== 'string' || typeof d.name !== 'string')
152
+ continue;
153
+ if (e.type === 'tool.call') {
154
+ const tipo = classificar('openclaw', d.name, d.args);
155
+ if (!tipo)
156
+ continue;
157
+ const chamada = { ferramenta: d.name, ...tipo, argumentos: d.args ?? null, resultado: null, erro: false };
158
+ pedidas.set(d.toolCallId, chamada);
159
+ chamadas.push(chamada);
160
+ }
161
+ else if (e.type === 'tool.result') {
162
+ const chamada = pedidas.get(d.toolCallId);
163
+ if (!chamada)
164
+ continue;
165
+ chamada.resultado = textoDoConteudo(d.result?.content);
166
+ chamada.erro = d.success !== true;
167
+ }
168
+ }
169
+ const agente = (agenteJson ?? {});
170
+ const entradas = agente.meta?.systemPromptReport?.tools?.entries;
171
+ const ferramentasExpostas = Array.isArray(entradas) ? entradas.map(t => t?.name).filter((n) => typeof n === 'string') : null;
172
+ const textos = (agente.payloads ?? []).map(p => p?.text).filter((t) => typeof t === 'string');
173
+ return { ferramentasExpostas, chamadas, respostaFinal: textos.length ? textos.join('\n') : null };
174
+ }
175
+ /** O primeiro objeto `ork.maestro-snapshot/v1` do texto (o host pode juntar stderr ao stdout). */
176
+ function extrairSnapshot(texto) {
177
+ const inicio = texto.indexOf('{"schema":"ork.maestro-snapshot/v1"');
178
+ if (inicio < 0)
179
+ return null;
180
+ let profundidade = 0, emTexto = false, escape = false;
181
+ for (let i = inicio; i < texto.length; i++) {
182
+ const c = texto[i];
183
+ if (emTexto) {
184
+ if (escape)
185
+ escape = false;
186
+ else if (c === '\\')
187
+ escape = true;
188
+ else if (c === '"')
189
+ emTexto = false;
190
+ }
191
+ else if (c === '"')
192
+ emTexto = true;
193
+ else if (c === '{')
194
+ profundidade++;
195
+ else if (c === '}' && --profundidade === 0) {
196
+ try {
197
+ return JSON.parse(texto.slice(inicio, i + 1));
198
+ }
199
+ catch {
200
+ return null;
201
+ }
202
+ }
203
+ }
204
+ return null;
205
+ }
206
+ function expandirHome(raiz, home) {
207
+ return raiz === '~' ? home : raiz.startsWith('~/') ? path.join(home, raiz.slice(2)) : raiz;
208
+ }
209
+ const ROADMAP_VAZIO = /\broadmap\b:?((?:\s+[^\s.!?:;,]+){0,4}?)\s+(vazio|empty)\b/giu;
210
+ const NEGACAO = /(^|[^\p{L}])(n[ãa]o|nunca|jamais|not|never|isn't|doesn't|nem)([^\p{L}]|$)/iu;
211
+ /**
212
+ * A frase que conclui "roadmap vazio". A negação só conta quando governa a conclusão: dentro do
213
+ * trecho ("the roadmap is not empty") ou nas até seis palavras antes dele, sem atravessar
214
+ * pontuação ("zero threads não quer dizer roadmap vazio"). "O roadmap está vazio e não há
215
+ * threads" e "Não há itens: o roadmap está vazio" concluem.
216
+ */
217
+ function concluiRoadmapVazio(texto) {
218
+ for (const m of texto.matchAll(ROADMAP_VAZIO)) {
219
+ const antes = texto.slice(0, m.index).split(/[.!?:;,\n]/).pop().trim().split(/\s+/).slice(-6).join(' ');
220
+ if (!NEGACAO.test(antes) && !NEGACAO.test(m[1]))
221
+ return `${antes} ${m[0]}`.trim();
222
+ }
223
+ return null;
224
+ }
225
+ const canonica = (p) => { try {
226
+ return fs.realpathSync(p);
227
+ }
228
+ catch {
229
+ return path.resolve(p);
230
+ } };
231
+ /** Nome inteiro, não pedaço de palavra: a abreviação `pav` não casa com "pavimento". */
232
+ const nomeia = (texto, nome) => new RegExp(`(^|[^\\p{L}\\p{N}_-])${nome.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\p{L}\\p{N}_-]|$)`, 'iu').test(texto);
233
+ /**
234
+ * O texto de `ork network roadmap` (`textoDoPanoramaDaRede`): a primeira linha diz de onde foi
235
+ * lido, a segunda quais projetos foram consultados (`<nome> (clone em <raiz>)`) e o bloco
236
+ * "Não consultado:" o que ficou de fora.
237
+ */
238
+ function lerPanoramaDaRede(texto) {
239
+ const linhas = texto.split('\n');
240
+ const i = linhas.findIndex(l => /^Panorama da rede lido de /.test(l));
241
+ if (i < 0 || !/^Consultado: /.test(linhas[i + 1] ?? '') || linhas[i + 2] !== 'Não consultado:')
242
+ return null;
243
+ const naoConsultado = [];
244
+ for (const l of linhas.slice(i + 3)) {
245
+ if (!l.startsWith('• '))
246
+ break;
247
+ naoConsultado.push(l.slice(2));
248
+ }
249
+ return { cabecalho: linhas[i], consultado: linhas[i + 1], naoConsultado };
250
+ }
251
+ function conferirProva(host, t, esperado) {
252
+ const conferencias = [];
253
+ const conferir = (id, ok, detalhe) => { conferencias.push({ id, ok, detalhe }); return ok; };
254
+ const nomes = exports.ENTRADAS[host].map(e => e.ferramenta);
255
+ const expostas = nomes.filter(n => t.ferramentasExpostas?.includes(n));
256
+ conferir('entrada.exposta', expostas.length > 0, t.ferramentasExpostas === null ? 'o transcript não lista as ferramentas expostas'
257
+ : expostas.length ? `${expostas.join(', ')} exposta(s) ao modelo`
258
+ : `${nomes.join(' e ')} ausente(s) entre ${t.ferramentasExpostas.length} ferramentas expostas`);
259
+ // A última chamada contratada com resultado é a que vale (uma nova tentativa depois de erro não
260
+ // reprova); qualquer `ork maestro` pelo shell reprova, mesmo ao lado da chamada contratada.
261
+ const contratadas = t.chamadas.filter(c => c.via === 'entrada');
262
+ const contratada = [...contratadas].reverse().find(c => c.resultado !== null && !c.erro) ?? contratadas.at(-1);
263
+ const desvio = t.chamadas.find(c => c.via === 'shell');
264
+ const comando = desvio && JSON.stringify(desvio.argumentos?.command);
265
+ conferir('entrada.chamada', !!contratada && !desvio, contratada && desvio ? `${contratada.ferramenta} chamada, mas também desvio: ${desvio.ferramenta} ${comando}`
266
+ : contratada ? `${contratada.ferramenta} chamada`
267
+ : desvio ? `desvio: ${desvio.ferramenta} ${comando} em vez de ${nomes.join(' ou ')}`
268
+ : `${nomes.join(' ou ')} não foi chamada`);
269
+ let snapshot = null;
270
+ let rede = null;
271
+ if (contratada && conferir('resultado.sem-erro', contratada.resultado !== null && !contratada.erro, contratada.resultado === null ? 'a chamada não teve resultado' : contratada.erro ? `erro: ${contratada.resultado.slice(0, 300)}` : 'resultado sem erro')) {
272
+ if (contratada.contrato === 'maestro') {
273
+ try {
274
+ snapshot = (0, maestro_contract_1.validateMaestroSnapshot)(extrairSnapshot(contratada.resultado));
275
+ conferir('resultado.contrato', true, `${snapshot.schema} validado`);
276
+ }
277
+ catch (e) {
278
+ conferir('resultado.contrato', false, `fora do contrato: ${e.message.slice(0, 300)}`);
279
+ }
280
+ }
281
+ else {
282
+ rede = lerPanoramaDaRede(contratada.resultado);
283
+ conferir('resultado.contrato', rede !== null, rede ? 'texto do ork.network-roadmap/v1: cabeçalho, Consultado e Não consultado'
284
+ : 'fora do contrato: sem o cabeçalho "Panorama da rede lido de", "Consultado:" e "Não consultado:"');
285
+ }
286
+ }
287
+ if (snapshot) {
288
+ const p = snapshot.project;
289
+ const exibida = p.root === undefined || p.root.includes('[caminho privado]') ? null : canonica(expandirHome(p.root, esperado.home));
290
+ const mesmaCopia = p.fingerprint === esperado.projeto.fingerprint && (exibida === null || exibida === canonica(esperado.projeto.raiz));
291
+ conferir('resultado.projeto', p.name === esperado.projeto.nome && mesmaCopia, `leu ${p.name} (${p.origin}) em ${p.root ?? 'raiz não exibida'}, impressão ${p.fingerprint.slice(0, 12)}; ` +
292
+ `esperado ${esperado.projeto.nome}, impressão ${esperado.projeto.fingerprint.slice(0, 12)}`);
293
+ const lacunas = snapshot.notConsulted ?? [];
294
+ conferir('resultado.nao-consultado', lacunas.length > 0, lacunas.length ? `declara o que não leu: ${lacunas.length} item(ns)` : 'notConsulted ausente: zero threads pode virar "roadmap vazio"');
295
+ }
296
+ if (rede) {
297
+ // "Consultado: a (clone em X); b (clone em Y)": vale o projeto esperado na raiz esperada.
298
+ const lidos = [...rede.consultado.replace(/^Consultado: /, '').matchAll(/(?:^|; )(.+?) \(clone em ([^)]+)\)/g)];
299
+ const leu = lidos.some(([, nome, raiz]) => nome === esperado.projeto.nome &&
300
+ canonica(expandirHome(raiz, esperado.home)) === canonica(esperado.projeto.raiz));
301
+ conferir('resultado.projeto', leu, `${rede.consultado.slice(0, 300)}; esperado ${esperado.projeto.nome} (clone em ${esperado.projeto.raiz})`);
302
+ conferir('resultado.nao-consultado', rede.naoConsultado.length > 0, rede.naoConsultado.length ? `declara o que não leu: ${rede.naoConsultado.length} item(ns)` : 'sem o bloco do que não foi consultado');
303
+ }
304
+ const resposta = t.respostaFinal ?? '';
305
+ const citaProjeto = [esperado.projeto.nome, esperado.projeto.id].some(n => nomeia(resposta, n));
306
+ conferir('resposta.cita-projeto', citaProjeto, t.respostaFinal === null ? 'sem resposta final' : citaProjeto ? 'a resposta nomeia o projeto lido' : 'a resposta não nomeia o projeto lido');
307
+ const vazio = concluiRoadmapVazio(resposta);
308
+ conferir('resposta.sem-roadmap-vazio', vazio === null, vazio ? `a resposta conclui "roadmap vazio" a partir do panorama (incidente de 29/09): ${vazio.slice(0, 200)}` : 'sem concluir "roadmap vazio" a partir do panorama');
309
+ return {
310
+ ok: conferencias.every(c => c.ok),
311
+ conferencias,
312
+ snapshot: snapshot && {
313
+ schema: snapshot.schema, projeto: snapshot.project,
314
+ secoes: Object.fromEntries(Object.entries(snapshot.sections).map(([nome, s]) => [nome, s.state])),
315
+ notConsulted: snapshot.notConsulted ?? [],
316
+ },
317
+ rede,
318
+ };
319
+ }
320
+ /**
321
+ * Redação antes de qualquer gravação: o recibo vai ao repositório. Cobre tokens com prefixo
322
+ * conhecido, `Bearer`/`Basic`, variáveis de ambiente com nome de segredo e valores de chaves JSON
323
+ * com nome de segredo. Para objetos, use `redigirObjeto`, que aplica a regra a cada texto antes
324
+ * da serialização (JSON dentro de stdout escaparia depois dela).
325
+ */
326
+ const CHAVE_DE_SEGREDO = /token|secret|password|passwd|api[_-]?key|apikey|credential|authorization|cookie/i;
327
+ function redigir(texto) {
328
+ return texto
329
+ .replace(/\b(sk-ant-[A-Za-z0-9_-]+|sk-or-[A-Za-z0-9_-]+|sk-[A-Za-z0-9_-]{16,}|[sr]k_live_[A-Za-z0-9]{10,}|AIza[0-9A-Za-z_-]{20,})/g, '[REDIGIDO]')
330
+ .replace(/\b(gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{16,}|xox[abprs]-[A-Za-z0-9-]{10,})/g, '[REDIGIDO]')
331
+ .replace(/\b(Bearer|Basic)(\s+)[A-Za-z0-9._~+/-]{8,}=*/gi, '$1$2[REDIGIDO]')
332
+ // O valor para em aspas, barra invertida e espaço: aplicada a JSON, a regra não come o fecho.
333
+ .replace(/\b([A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD)[A-Z0-9_]*\s*=\s*)(?!\d+\b)("[^"\\]*"|'[^'\\]*'|[^\s"'\\]+)/g, '$1[REDIGIDO]')
334
+ .replace(/("[^"]*(?:token|secret|password|passwd|api[_-]?key|apikey|credential|authorization|cookie)[^"]*"\s*:\s*)"(?:[^"\\]|\\.)*"/gi, '$1"[REDIGIDO]"');
335
+ }
336
+ function redigirObjeto(valor) {
337
+ if (typeof valor === 'string')
338
+ return redigir(valor);
339
+ if (Array.isArray(valor))
340
+ return valor.map(v => redigirObjeto(v));
341
+ if (valor && typeof valor === 'object') {
342
+ return Object.fromEntries(Object.entries(valor).map(([k, v]) => [k, typeof v === 'string' && CHAVE_DE_SEGREDO.test(k) ? '[REDIGIDO]' : redigirObjeto(v)]));
343
+ }
344
+ return valor;
345
+ }
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.adquirirLockMonitor = void 0;
36
+ exports.RETEM_PARADO_MS = exports.adquirirLockMonitor = void 0;
37
37
  exports.assinaturaPulse = assinaturaPulse;
38
38
  exports.assinaturaDoResumo = assinaturaDoResumo;
39
39
  exports.mensagemPulse = mensagemPulse;
@@ -63,6 +63,8 @@ const pulse_cadencia_1 = require("./pulse-cadencia");
63
63
  // O lock dos monitores mudou de modulo (I-41, GO-FIX 1); quem o importava daqui continua importando.
64
64
  var monitor_lock_2 = require("./monitor-lock");
65
65
  Object.defineProperty(exports, "adquirirLockMonitor", { enumerable: true, get: function () { return monitor_lock_2.adquirirLockMonitor; } });
66
+ /** RM-037 (fatia 4): por quanto tempo a parada ja informada continua sabida depois de sumir de uma batida. */
67
+ exports.RETEM_PARADO_MS = 6 * 60 * 60 * 1000;
66
68
  /**
67
69
  * I-41 (D9): a assinatura de deduplicacao NAO inclui `apresentacao`.
68
70
  *
@@ -183,9 +185,24 @@ function varrerPulse(opcoes) {
183
185
  const esperasFora = outras.flatMap(m => m.esperando.map(e => `maquina:${m.maquina}:${e.thread}:${e.fase}`));
184
186
  for (const k of esperasFora)
185
187
  presentes.add(k);
186
- vistas = Object.fromEntries(Object.entries(vistas).filter(([id]) => presentes.has(id)));
188
+ // RM-037 (fatia 4, D7): o trabalho parado no condutor e noticia uma vez por thread e caso, como a
189
+ // decisao informada: sai na janela da cadencia do dono e nunca abre pedido.
190
+ const parados = pulse.paradoNoCondutor ?? [];
191
+ const chaveDoParado = (p) => `parado:${p.thread}:${p.caso}`;
192
+ for (const p of parados)
193
+ presentes.add(chaveDoParado(p));
194
+ // A parada que some por uma batida (gh que falhou, um `ork verify` do condutor) e volta nao e noticia
195
+ // nova: a marca fica guardada por `RETEM_PARADO_MS` depois da ultima vez que a parada apareceu.
196
+ const guardada = (id, valor) => id.startsWith('parado:') &&
197
+ Date.parse(quando) - Number(valor.split('|')[1]) < exports.RETEM_PARADO_MS;
198
+ vistas = Object.fromEntries(Object.entries(vistas).filter(([id, valor]) => presentes.has(id) || guardada(id, valor)));
187
199
  const esperaNovaFora = esperasFora.some(k => vistas[k] === undefined);
188
200
  const decisoesNovas = decisoes.filter(d => vistas[chaveDaDecisao(d.id)] === undefined);
201
+ const paradosNovos = parados.filter(p => vistas[chaveDoParado(p)] === undefined);
202
+ // A marca guarda o instante em milissegundos: e dado de maquina, nao texto para o dono.
203
+ for (const p of parados)
204
+ if (vistas[chaveDoParado(p)] !== undefined)
205
+ vistas[chaveDoParado(p)] = `informada|${Date.parse(quando)}`;
189
206
  // I-41 (GO-FIX 1): a thread que ja recebeu a pergunta e ainda nao respondeu nao e noticia. A
190
207
  // mudanca no item dela (o pedido que o sim do dono abriu) e obra nossa, e avisar sobre ela seria
191
208
  // mandar ao dono um resumo sobre a pergunta que ele acabou de receber.
@@ -205,7 +222,8 @@ function varrerPulse(opcoes) {
205
222
  !config.argumentos.every(a => typeof a === 'string') || !config.argumentos.some(a => a.includes('{{mensagem}}'))) {
206
223
  throw new Error('transporte requer executavel e argumentos com {{mensagem}}');
207
224
  }
208
- const r = (0, node_child_process_1.spawnSync)(config.executavel, config.argumentos.map(a => a.replaceAll('{{mensagem}}', mensagem)), { cwd: raiz, encoding: 'utf8', input: mensagem, timeout: 30000, maxBuffer: 1024 * 1024 });
225
+ // A mensagem entra como texto: `$&` e `$'` nela nao sao padrao de substituicao (CHECK da RM-037, fatia 4).
226
+ const r = (0, node_child_process_1.spawnSync)(config.executavel, config.argumentos.map(a => a.replaceAll('{{mensagem}}', () => mensagem)), { cwd: raiz, encoding: 'utf8', input: mensagem, timeout: 30000, maxBuffer: 1024 * 1024 });
209
227
  entregue = r.status === 0;
210
228
  }
211
229
  if (!entregue)
@@ -229,12 +247,13 @@ function varrerPulse(opcoes) {
229
247
  // I-50: pergunta nova sai na hora; o resto (status, decisoes informadas) espera a janela da
230
248
  // cadencia do dono, e a novidade que esperou continua nao vista ate sair.
231
249
  const noPrazo = !opcoes.comCadencia || (0, pulse_cadencia_1.janelaAberta)(cadencia, (0, pulse_cadencia_1.lerUltimoResumo)(opcoes.raiz, dir), quando);
232
- const adiada = !conjuntoNovo && !esperaNovaFora && (novas > 0 || decisoesNovas.length > 0) && !noPrazo;
233
- if (conjuntoNovo || esperaNovaFora || (noPrazo && (novas > 0 || decisoesNovas.length > 0))) {
250
+ const informacaoNova = novas > 0 || decisoesNovas.length > 0 || paradosNovos.length > 0;
251
+ const adiada = !conjuntoNovo && !esperaNovaFora && informacaoNova && !noPrazo;
252
+ if (conjuntoNovo || esperaNovaFora || (noPrazo && informacaoNova)) {
234
253
  const guardadas = anterior ? fila.candidatos.filter(c => anterior.candidatos.some(a => a.thread === c.thread)).length : 0;
235
254
  const resumo = (0, hitl_resumo_1.resumirHitl)(itens, { quando, janelaMin, prontas: fila.candidatos.length, acumuladas: guardadas,
236
255
  consertos: fila.consertos, atoDoItem: i => fila.atos.get(`${i.thread}|${i.fase}`),
237
- decisoes: decisoesNovas, acimaDoLimiar: pulse.acimaDoLimiar ?? [], outrasMaquinas: outras });
256
+ decisoes: decisoesNovas, acimaDoLimiar: pulse.acimaDoLimiar ?? [], outrasMaquinas: outras, paradosNoCondutor: parados });
238
257
  // Pedir licenca para mandar zero perguntas era o defeito: sem pergunta, nao ha codigo.
239
258
  // I-50: cadencia curta nao encurta o prazo do dono para responder ao resumo (minimo de 60 min).
240
259
  const consentimento = fila.candidatos.length ? (0, monitor_lock_1.comLockDaConversa)(dir, () => (0, pulse_consentimento_1.abrirConsentimento)(raiz, { quando,
@@ -252,6 +271,8 @@ function varrerPulse(opcoes) {
252
271
  vistas[chaveDaDecisao(d.id)] = 'informada';
253
272
  for (const k of esperasFora)
254
273
  vistas[k] = 'informada';
274
+ for (const p of parados)
275
+ vistas[chaveDoParado(p)] = `informada|${Date.parse(quando)}`;
255
276
  (0, pulse_cadencia_1.gravarUltimoResumo)(opcoes.raiz, quando, dir);
256
277
  }
257
278
  gravarCache(cache, vistas);
package/dist/pulse.js CHANGED
@@ -26,6 +26,8 @@ const decisao_autonoma_1 = require("./decisao-autonoma");
26
26
  const docs_1 = require("./docs");
27
27
  const conducao_1 = require("./conducao");
28
28
  const conducao_texto_1 = require("./conducao-texto");
29
+ const hitl_resumo_1 = require("./hitl-resumo");
30
+ const parado_no_condutor_1 = require("./parado-no-condutor");
29
31
  exports.CONTRATO_PULSE = 'ork.pulse/v1';
30
32
  /** Uma semana: o resumo cita a decisao uma vez, e ela continua lida ate sair da janela. */
31
33
  exports.JANELA_DAS_DECISOES_MS = 7 * 24 * 60 * 60 * 1000;
@@ -47,6 +49,17 @@ function apresentacaoCurtaDoItem(pedido, desde, quando) {
47
49
  function comporPulse(carregado, entrada) {
48
50
  const { radar, monitor, batch, orfas } = entrada, quando = radar.consultadoEm;
49
51
  const humanos = [], automaticas = [];
52
+ // RM-037 (fatia 4): o fim de turno sem pergunta e do condutor. Ele sai da fila do dono aqui e volta
53
+ // como linha propria (`paradoNoCondutor`); leitura que falha nao esconde nada do dono.
54
+ let entregas = entrada.entregas;
55
+ if (!entregas) {
56
+ try {
57
+ entregas = (0, parado_no_condutor_1.entregasDoProjeto)(carregado, { quando, sessoes: radar.sessoes });
58
+ }
59
+ catch {
60
+ entregas = undefined;
61
+ }
62
+ }
50
63
  const registros = new Map();
51
64
  const ehRegistro = (id) => {
52
65
  if (!id)
@@ -79,6 +92,11 @@ function comporPulse(carregado, entrada) {
79
92
  };
80
93
  const TERMINAIS = ['failed', 'done', 'stopped', 'completed'];
81
94
  for (const s of radar.sessoes.filter(s => s.precisaDeHumano)) {
95
+ // RM-037 (fatia 4): sessao `blocked` de quem o turno acabou sem pergunta e do condutor: a que o ledger provou
96
+ // (Stop sem atividade depois; a lista numerada da mensagem final nao e menu) e a sobra sem menu na tela.
97
+ if (entregas?.doCondutor.sessoes.has(s.sessionId) &&
98
+ ((0, parado_no_condutor_1.sessaoSemPergunta)(s) || entregas.doCondutor.turnosEncerrados.has(s.sessionId)))
99
+ continue;
82
100
  // Job morto ou estado terminal é história. Bloqueio vivo ou desconhecido continua visível.
83
101
  if (ehRegistro(s.thread?.id ?? null) && s.jobVivo !== true &&
84
102
  (s.jobVivo === false || TERMINAIS.includes(s.estadoBruto)))
@@ -123,6 +141,9 @@ function comporPulse(carregado, entrada) {
123
141
  for (const p of l.paradas) {
124
142
  if (VERIFICACAO.includes(p.motivo) && jaMesclada(l.thread))
125
143
  continue;
144
+ // RM-037 (fatia 4): o `human.pending` que o observador grava no fim de turno sem pergunta nao e do dono.
145
+ if (p.motivo === 'human.pending' && p.fonte === 'ledger' && entregas?.doCondutor.gates.has(`${l.thread}|${p.fase}`))
146
+ continue;
126
147
  if (ehRegistro(l.thread)) {
127
148
  if (p.fonte === 'escalonador' || ['lease.busy', 'conducao.em-andamento', 'concurrency.limite', 'vaga.stale', 'runtime.silencio'].includes(p.motivo))
128
149
  continue;
@@ -216,7 +237,8 @@ function comporPulse(carregado, entrada) {
216
237
  }
217
238
  }
218
239
  return { contrato: exports.CONTRATO_PULSE, consultadoEm: quando, runtime: { ok: radar.runtimeConsultado, detalhe: radar.runtimeDetalhe },
219
- precisaDeHumanoAgora: h, acoesAutomaticas: a, resumo: { humanos: h.length, automaticas: a.length, scores: batch.length, fasesOrfas: orfas.length } };
240
+ precisaDeHumanoAgora: h, acoesAutomaticas: a, resumo: { humanos: h.length, automaticas: a.length, scores: batch.length, fasesOrfas: orfas.length },
241
+ ...(entregas?.parados.length ? { paradoNoCondutor: entregas.parados } : {}) };
220
242
  }
221
243
  function montarPulse(carregado, opcoes = {}) {
222
244
  if (opcoes.registrar)
@@ -243,7 +265,27 @@ function montarPulse(carregado, opcoes = {}) {
243
265
  }
244
266
  }
245
267
  const monitor = (0, orquestracao_1.montarMonitor)(carregado, { agora: quando, estados });
246
- const pulse = comporPulse(carregado, { radar, monitor, orfas, batch: (0, master_1.pendentesDeScore)(carregado.raiz) });
268
+ // RM-037 (fatia 4, D2): a forja so e lida quando alguma thread tem branch publicada; a leitura boa vira o
269
+ // retrato que o status do roadmap le sem rede. Falha de leitura e diagnostico, nunca "sem PR".
270
+ let entregas;
271
+ try {
272
+ entregas = (0, parado_no_condutor_1.entregasDoProjeto)(carregado, { quando, sessoes: radar.sessoes, lerPrs: (candidatas) => {
273
+ const leitura = (0, parado_no_condutor_1.lerPrsDaForja)(carregado, { quando, executor: opcoes.executorDoGh, candidatas });
274
+ if (leitura.ok) {
275
+ try {
276
+ (0, parado_no_condutor_1.gravarRetratoDePrs)(carregado.raiz, leitura.retrato);
277
+ }
278
+ catch { /* o retrato e economia do status */ }
279
+ }
280
+ return leitura;
281
+ } });
282
+ if (entregas.prs && !entregas.prs.ok)
283
+ diagnosticos.push(`prs.nao-lidos: ${entregas.prs.erro.slice(0, 160)}`);
284
+ }
285
+ catch (e) {
286
+ diagnosticos.push(`entregas.indisponiveis: ${e.message.slice(0, 120)}`);
287
+ }
288
+ const pulse = comporPulse(carregado, { radar, monitor, orfas, batch: (0, master_1.pendentesDeScore)(carregado.raiz), ...(entregas ? { entregas } : {}) });
247
289
  const conducoes = monitor.linhas.filter((l) => l.conducao).map((l) => ({ thread: l.thread, linha: (0, conducao_texto_1.linhaDeConducao)(l.conducao, { agora: quando }) }));
248
290
  if (conducoes.length > 0)
249
291
  pulse.conducoes = conducoes;
@@ -264,8 +306,10 @@ function textoDoPulse(p) {
264
306
  // I-35: o pulse guarda ISO; o texto ao dono sai no fuso dele, rotulado no cabeçalho.
265
307
  const local = (texto) => (0, horario_1.localizarTexto)(texto, { agora: p.consultadoEm });
266
308
  const linhas = [`Pulse (${p.contrato}) ${(0, horario_1.formatarDataHoraRotulada)(p.consultadoEm, { agora: p.consultadoEm })}`, `Precisa de humano agora: ${p.resumo.humanos}`, `${p.resumo.scores} entrega(s) sem nota, aceitas por padrão com registro`];
267
- if (p.runtime.ok && p.runtime.detalhe.includes('liveness.snapshot.invalid'))
309
+ // RM-037 (fatia 4): PR nao lido tambem e dito, para a falta de linha do condutor nao parecer "nada parado".
310
+ if (p.runtime.ok && ['liveness.snapshot.invalid', 'prs.nao-lidos', 'entregas.indisponiveis'].some(d => p.runtime.detalhe.includes(d))) {
268
311
  linhas.push(`[diagnostico] ${local(p.runtime.detalhe)}`);
312
+ }
269
313
  if (!p.runtime.ok)
270
314
  linhas.push(`[consulta incompleta] ${local(p.runtime.detalhe)}`);
271
315
  for (const i of p.precisaDeHumanoAgora) {
@@ -276,6 +320,12 @@ function textoDoPulse(p) {
276
320
  for (const c of p.conducoes)
277
321
  linhas.push(`${c.thread}: ${c.linha}`);
278
322
  }
323
+ // RM-037 (fatia 4): o que espera o condutor, uma linha por thread; nao e pergunta ao dono.
324
+ if (p.paradoNoCondutor?.length) {
325
+ linhas.push('', `Parado no condutor: ${p.paradoNoCondutor.length}`);
326
+ for (const x of p.paradoNoCondutor)
327
+ linhas.push((0, hitl_resumo_1.linhaDoParadoNoCondutor)(x, { agora: p.consultadoEm }));
328
+ }
279
329
  linhas.push('', `Ações automáticas pendentes: ${p.resumo.automaticas}`);
280
330
  for (const i of p.acoesAutomaticas)
281
331
  linhas.push(`${i.thread}: ${i.motivo}; ${i.comandoResposta}`);