@orkastery/cli 0.4.3 → 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.
- package/adapters/README.md +11 -0
- package/adapters/claude-code/.claude-plugin/plugin.json +3 -1
- package/adapters/claude-code/README.md +11 -2
- package/adapters/claude-code/agents/ork-check.md +1 -1
- package/adapters/claude-code/agents/ork-go.md +2 -2
- package/adapters/claude-code/agents/ork-goal.md +1 -1
- package/adapters/claude-code/agents/ork-master.md +1 -1
- package/adapters/claude-code/agents/ork-plan.md +1 -1
- package/adapters/claude-code/agents/ork-ship.md +1 -1
- package/adapters/claude-code/commands/master.md +11 -8
- package/adapters/claude-code/commands/onboarding.md +3 -2
- package/adapters/claude-code/commands/ork.md +8 -0
- package/adapters/claude-code/commands/ship.md +2 -2
- package/adapters/codex/skills/ork/SKILL.md +12 -1
- package/adapters/hermes/README.md +3 -0
- package/adapters/hermes/bin/ork-abrir-thread.sh +17 -2
- package/adapters/hermes/bin/ork-brain.sh +3 -0
- package/adapters/hermes/bin/ork-maestro.sh +4 -1
- package/adapters/hermes/bin/ork-network-roadmap.sh +11 -0
- package/adapters/hermes/bin/ork-roadmap-status.sh +9 -0
- package/adapters/hermes/hermes.plugin.json +2 -0
- package/adapters/hermes/hitl-ingress/__init__.py +36 -3
- package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +19 -20
- package/adapters/openclaw/README.md +45 -2
- package/adapters/openclaw/dist/hitl-ingress.js +37 -3
- package/adapters/openclaw/dist/index.js +93 -21
- package/adapters/openclaw/openclaw.plugin.json +10 -1
- package/adapters/openclaw/package.json +2 -2
- package/adapters/openclaw/src/hitl-ingress.ts +32 -3
- package/adapters/openclaw/src/index.ts +109 -21
- package/assets/docs/padroes/documentacao-de-produto.md +2 -2
- package/assets/docs/padroes/roadmap-de-produto.md +1 -1
- package/assets/docs/produto/_modelo-feature.md +2 -0
- package/assets/orkmind_bridge.py +214 -1
- package/dist/adapters/claude-bg.js +24 -7
- package/dist/auditrun.js +1 -1
- package/dist/board.js +74 -0
- package/dist/branch-de-estado.js +5 -4
- package/dist/busca-semantica.js +161 -0
- package/dist/ci.js +141 -10
- package/dist/claim-lint.js +3 -1
- package/dist/company-brain-cli.js +27 -2
- package/dist/company-brain-context.js +132 -0
- package/dist/company-brain-dossie.js +376 -0
- package/dist/company-brain-mcp.js +4 -1
- package/dist/conducao.js +71 -18
- package/dist/decisao-autonoma.js +42 -0
- package/dist/docs.js +135 -25
- package/dist/doctor.js +242 -32
- package/dist/entrega-pr.js +116 -5
- package/dist/experiencia-instalacao.js +256 -0
- package/dist/experiencia.js +72 -0
- package/dist/fabrica-estado.js +63 -12
- package/dist/fechamento.js +105 -0
- package/dist/fix.js +3 -3
- package/dist/forja.js +435 -0
- package/dist/gates.js +9 -1
- package/dist/hitl-classificacao.js +11 -1
- package/dist/hitl-contract.js +50 -7
- package/dist/hitl-curto.js +158 -0
- package/dist/hitl-gates.js +67 -1
- package/dist/hitl-lote.js +30 -15
- package/dist/hitl-presentation.js +10 -7
- package/dist/hitl-resumo.js +57 -13
- package/dist/hitl-texto-livre.js +150 -0
- package/dist/hosts.js +92 -3
- package/dist/index.js +637 -45
- package/dist/indice-vetorial.js +317 -0
- package/dist/init.js +24 -5
- package/dist/integracoes-locais.js +3 -1
- package/dist/intelligence-benchmark-contract.js +570 -0
- package/dist/intelligence-graph-cli.js +584 -0
- package/dist/intelligence-graph-contract.js +636 -0
- package/dist/intelligence-graph-extract-md.js +487 -0
- package/dist/intelligence-graph-extract-ts.js +980 -0
- package/dist/intelligence-graph-extract.js +546 -0
- package/dist/intelligence-graph-index.js +693 -0
- package/dist/intelligence-graph-parsers.js +327 -0
- package/dist/intelligence-graph-query.js +344 -0
- package/dist/intelligence-graph-repo.js +197 -0
- package/dist/leases.js +100 -0
- package/dist/ledger.js +23 -0
- package/dist/licoes.js +5 -4
- package/dist/liveness.js +2 -1
- package/dist/maestro-cli.js +10 -1
- package/dist/maestro-contract.js +5 -1
- package/dist/maestro-discovery.js +16 -2
- package/dist/maestro-runtime.js +3 -0
- package/dist/maestro-snapshot.js +6 -2
- package/dist/manifest.js +166 -5
- package/dist/master-batch.js +3 -2
- package/dist/master-digest.js +2 -2
- package/dist/master-nota.js +203 -0
- package/dist/master.js +4 -0
- package/dist/mcp-artifacts.js +2 -0
- package/dist/mcp-experiencia.js +164 -0
- package/dist/mcp-git.js +64 -12
- package/dist/mcp-grafo-worker.js +91 -0
- package/dist/mcp-grafo.js +233 -0
- package/dist/mcp-install.js +5 -2
- package/dist/mcp-maestro.js +3 -2
- package/dist/mcp-server.js +93 -5
- package/dist/mcp-ship.js +2 -0
- package/dist/mcp-verify.js +3 -1
- package/dist/memoria.js +147 -1
- package/dist/network-roadmap.js +907 -0
- package/dist/ocupacao.js +43 -3
- package/dist/onboarding.js +122 -11
- package/dist/orkmind.js +165 -9
- package/dist/orquestracao.js +2 -1
- package/dist/parado-no-condutor.js +933 -0
- package/dist/phase.js +166 -13
- package/dist/policies.js +29 -2
- package/dist/projeto-alvo.js +551 -0
- package/dist/prompts.js +1 -1
- package/dist/prova-ativacao.js +345 -0
- package/dist/pulse-consentimento.js +4 -2
- package/dist/pulse-delivery.js +32 -6
- package/dist/pulse-resposta.js +674 -49
- package/dist/pulse.js +74 -3
- package/dist/retry.js +51 -8
- package/dist/roadmap-reservas.js +262 -20
- package/dist/roadmap-status.js +348 -0
- package/dist/runtime-context.js +5 -1
- package/dist/sessoes-inventario.js +7 -1
- package/dist/sessoes.js +4 -1
- package/dist/ship.js +25 -1
- package/dist/thread-close.js +3 -0
- package/dist/thread.js +21 -2
- package/dist/util.js +71 -4
- package/dist/verify.js +6 -5
- package/dist/worktree.js +29 -6
- package/eval/casos/onboarding.json +1 -1
- package/eval/casos/orchestration-experience-pt-br.json +48 -0
- package/eval/casos/orchestration-experience.json +48 -0
- package/package.json +1 -1
- package/schemas/code-artifact-graph.v1.schema.json +581 -0
- package/schemas/graph-benchmark.v1.schema.json +2240 -0
- package/schemas/maestro-snapshot.schema.json +23 -0
- package/skills/README.md +7 -5
- package/skills/core/onboarding/SKILL.md +10 -2
- package/skills/core/orchestration-experience/SKILL.md +113 -0
- package/skills/core/orchestration-experience-pt-br/SKILL.md +112 -0
- package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
- package/skills/phases/go-implementation/SKILL.md +2 -0
- package/skills/phases/ship-release/SKILL.md +6 -3
|
@@ -0,0 +1,933 @@
|
|
|
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.LIMITE_DE_RECENTES = exports.LIMITE_DE_PRS = exports.VALIDADE_DO_RETRATO_MIN = exports.LIMIAR_PARADO_NO_CONDUTOR_MIN = exports.CONTRATO_PRS = void 0;
|
|
37
|
+
exports.nomeDeCheck = nomeDeCheck;
|
|
38
|
+
exports.repositorioDoRemoto = repositorioDoRemoto;
|
|
39
|
+
exports.lerPrsDaForja = lerPrsDaForja;
|
|
40
|
+
exports.gravarRetratoDePrs = gravarRetratoDePrs;
|
|
41
|
+
exports.lerRetratoDePrs = lerRetratoDePrs;
|
|
42
|
+
exports.perguntaAberta = perguntaAberta;
|
|
43
|
+
exports.fimDoTurno = fimDoTurno;
|
|
44
|
+
exports.pendenciaDoDono = pendenciaDoDono;
|
|
45
|
+
exports.esperaDoCondutor = esperaDoCondutor;
|
|
46
|
+
exports.sessaoSemPergunta = sessaoSemPergunta;
|
|
47
|
+
exports.entregasDoProjeto = entregasDoProjeto;
|
|
48
|
+
/**
|
|
49
|
+
* RM-037 (fatia 4): o trabalho parado no condutor.
|
|
50
|
+
*
|
|
51
|
+
* Em 01/10/2026 o condutor parou duas vezes e nada no produto percebeu. De madrugada, tres threads
|
|
52
|
+
* terminaram com o verify verde e ficaram 15 h sem push e sem PR; de dia, PRs ficaram horas com os
|
|
53
|
+
* checks verdes e sem merge, e um PR com o check vermelho ficou 4h38 sem fase nenhuma. O pulse batia de
|
|
54
|
+
* 15 em 15 min e so dizia "Esperando voce": o `human.pending` que o observador grava quando a sessao
|
|
55
|
+
* claude-bg encerra o turno em `blocked` contava como pergunta do dono, e nao havia pergunta nenhuma
|
|
56
|
+
* (12 desses resultados seguiram ate o `ship_done` sem um `human_gate`).
|
|
57
|
+
*
|
|
58
|
+
* Aqui mora, num lugar so, o que e do condutor depois da entrega:
|
|
59
|
+
*
|
|
60
|
+
* - quem e do condutor (`esperaDoCondutor`): o fim de turno num bloco sem pausa ao fim, sem pergunta
|
|
61
|
+
* estruturada aberta. Pausa prevista do modo, escalacao tipada, `hitl_requested` aberto (inclusive
|
|
62
|
+
* vencido que espera), prompt de permissao do hook e menu na tela continuam do dono;
|
|
63
|
+
* - o estado da entrega de cada thread aberta: o git local (a ponta da branch, a copia do remoto que o
|
|
64
|
+
* push desta maquina atualiza e a base) e os PRs da forja, lidos pelo `gh` e guardados como retrato
|
|
65
|
+
* `ork.prs-abertos/v1`;
|
|
66
|
+
* - os casos, cada um com o desde e o proximo passo, alem do limiar. O que sai do dono volta sempre
|
|
67
|
+
* como linha do condutor (CHECK da fatia 4, rodada 1).
|
|
68
|
+
*
|
|
69
|
+
* O pulse le a forja so quando alguma thread tem branch publicada; o status do roadmap le o retrato e
|
|
70
|
+
* nunca a rede. Falha, lista cortada e retrato velho sao "PR nao lido", nunca "sem PR".
|
|
71
|
+
*/
|
|
72
|
+
const fs = __importStar(require("node:fs"));
|
|
73
|
+
const path = __importStar(require("node:path"));
|
|
74
|
+
const node_child_process_1 = require("node:child_process");
|
|
75
|
+
const conducao_1 = require("./conducao");
|
|
76
|
+
const estado_thread_1 = require("./estado-thread");
|
|
77
|
+
const forja_1 = require("./forja");
|
|
78
|
+
const hitl_1 = require("./hitl");
|
|
79
|
+
const hitl_contract_1 = require("./hitl-contract");
|
|
80
|
+
const hitl_classificacao_1 = require("./hitl-classificacao");
|
|
81
|
+
const hitl_gates_1 = require("./hitl-gates");
|
|
82
|
+
const ledger_1 = require("./ledger");
|
|
83
|
+
const modos_1 = require("./modos");
|
|
84
|
+
const ocupacao_1 = require("./ocupacao");
|
|
85
|
+
const session_watcher_claude_1 = require("./session-watcher-claude");
|
|
86
|
+
const thread_1 = require("./thread");
|
|
87
|
+
const util_1 = require("./util");
|
|
88
|
+
const worktree_1 = require("./worktree");
|
|
89
|
+
exports.CONTRATO_PRS = 'ork.prs-abertos/v1';
|
|
90
|
+
/** D4: duas batidas do pulse. Abaixo disto o condutor pode estar agindo. */
|
|
91
|
+
exports.LIMIAR_PARADO_NO_CONDUTOR_MIN = 30;
|
|
92
|
+
/** O retrato de PRs vale por uma hora para quem nao le a rede (o status do roadmap); depois, "PR nao lido". */
|
|
93
|
+
exports.VALIDADE_DO_RETRATO_MIN = 60;
|
|
94
|
+
/** Quantos PRs ABERTOS a leitura pede. Lista cheia e `parcial`: o PR que nao veio nunca vira "sem PR". */
|
|
95
|
+
exports.LIMITE_DE_PRS = 200;
|
|
96
|
+
/** Quantos PRs recentes (de qualquer estado) a segunda leitura pede: so para achar o mesclado e o fechado. */
|
|
97
|
+
exports.LIMITE_DE_RECENTES = 100;
|
|
98
|
+
/** Retrato com data mais a frente do relogio do que isto nao vale (relogio adiantado ou arquivo mexido). */
|
|
99
|
+
const FOLGA_DO_RELOGIO_MIN = 5;
|
|
100
|
+
const ARQUIVO_DOS_PRS = 'prs.json';
|
|
101
|
+
const PRAZO_DO_GH_MS = 30000;
|
|
102
|
+
/** A leitura inteira da forja numa batida: cabe com folga nos 240 s que a varredura da ao subprocesso do pulse. */
|
|
103
|
+
const ORCAMENTO_DA_LEITURA_MS = 60000;
|
|
104
|
+
const CAMPOS_DOS_RECENTES = 'number,state,headRefName,headRefOid,baseRefName,isDraft,isCrossRepository,url,createdAt,mergedAt';
|
|
105
|
+
const CAMPOS_DO_GH = `${CAMPOS_DOS_RECENTES},statusCheckRollup`;
|
|
106
|
+
/** Quantas branches candidatas a "sem PR" sao conferidas uma a uma por batida; o resto fica "PR nao lido". */
|
|
107
|
+
const LIMITE_DE_CANDIDATAS = 10;
|
|
108
|
+
/** A conferencia de uma candidata so comeca com este tempo de sobra no orcamento; com menos, a branch fica sem conferir. */
|
|
109
|
+
const MINIMO_DA_CONFERENCIA_MS = 5000;
|
|
110
|
+
/** As fases que o ciclo despacha antes da entrega: depois de um bloco que para nelas, o passo e despachar, nao publicar. */
|
|
111
|
+
const FASES_ANTES_DA_ENTREGA = ['GOAL', 'PLAN', 'GO', 'CHECK'];
|
|
112
|
+
/** Os modos em que a #TAG ja autoriza o push (`autorizacaoDePush`, `core/src/ship.ts`). */
|
|
113
|
+
const MODOS_COM_PUSH_AUTORIZADO = ['auto', 'maestro'];
|
|
114
|
+
/** O ambiente do `gh`: sem cor, sem prompt e sem host ou repositorio herdados (o host vai no `--repo`). */
|
|
115
|
+
function ambienteDoGh() {
|
|
116
|
+
const env = { ...process.env, GH_PROMPT_DISABLED: '1', NO_COLOR: '1', CLICOLOR: '0', CLICOLOR_FORCE: '0' };
|
|
117
|
+
delete env.GH_HOST;
|
|
118
|
+
delete env.GH_REPO;
|
|
119
|
+
delete env.GH_FORCE_TTY;
|
|
120
|
+
return env;
|
|
121
|
+
}
|
|
122
|
+
const ghPadrao = (args, timeoutMs) => {
|
|
123
|
+
const r = (0, node_child_process_1.spawnSync)('gh', [...args], { encoding: 'utf8', timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024, env: ambienteDoGh() });
|
|
124
|
+
return { status: r.status, stdout: r.stdout ?? '', stderr: r.error ? r.error.message : (r.stderr ?? '') };
|
|
125
|
+
};
|
|
126
|
+
/** Git local sem rede escondida: clone parcial nunca busca objeto durante uma leitura daqui. */
|
|
127
|
+
const git = (raiz, args) => (0, util_1.exec)('git', args, raiz, 60000, { ...process.env, GIT_NO_LAZY_FETCH: '1' });
|
|
128
|
+
const SHA = /^[0-9a-f]{40}$/;
|
|
129
|
+
const BRANCH = /^[A-Za-z0-9._/-]{1,200}$/;
|
|
130
|
+
const REPOSITORIO = /^[A-Za-z0-9_.-]{1,100}\/[A-Za-z0-9_.-]{1,100}$/;
|
|
131
|
+
/** Texto curto vindo de fora: sem controle, num teto. */
|
|
132
|
+
function curto(v, teto = 120) {
|
|
133
|
+
if (typeof v !== 'string')
|
|
134
|
+
return null;
|
|
135
|
+
const limpo = Array.from(v.replace(/\p{Cc}/gu, ' ').replace(/\s+/g, ' ').trim()).slice(0, teto).join('');
|
|
136
|
+
return limpo || null;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* O nome de um check vem do GitHub e e o unico texto livre da forja que chega ao dono (no proximo passo).
|
|
140
|
+
* Fica so letra, numero e pontuacao simples: nada de controle, de formatacao invisivel, de quebra de
|
|
141
|
+
* linha, de `:` (diretiva de canal) nem de `$` (padrao de substituicao).
|
|
142
|
+
*/
|
|
143
|
+
function nomeDeCheck(v) {
|
|
144
|
+
if (typeof v !== 'string')
|
|
145
|
+
return null;
|
|
146
|
+
const limpo = v.replace(/[^\p{L}\p{N} ()[\]._,+/-]+/gu, ' ').replace(/\s+/g, ' ').trim();
|
|
147
|
+
return limpo ? Array.from(limpo).slice(0, 80).join('').trim() : null;
|
|
148
|
+
}
|
|
149
|
+
/** Instante ISO de fora. O GitHub devolve o ano 1 para o que ainda nao aconteceu. */
|
|
150
|
+
function instante(v) {
|
|
151
|
+
if (typeof v !== 'string')
|
|
152
|
+
return null;
|
|
153
|
+
const ms = Date.parse(v);
|
|
154
|
+
return Number.isFinite(ms) && new Date(ms).getUTCFullYear() >= 2000 ? new Date(ms).toISOString() : null;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Um check do rollup, com a chave que diz quando ele se repete (o tipo, o workflow e o nome cru, como o `gh
|
|
158
|
+
* pr checks`: dois checks diferentes com o mesmo nome limpo nunca se fundem) e o inicio que decide o mais novo.
|
|
159
|
+
*/
|
|
160
|
+
function checkDaForja(c) {
|
|
161
|
+
if (!c || typeof c !== 'object')
|
|
162
|
+
return null;
|
|
163
|
+
const o = c;
|
|
164
|
+
const nome = nomeDeCheck(o.name) ?? nomeDeCheck(o.context);
|
|
165
|
+
if (!nome)
|
|
166
|
+
return null;
|
|
167
|
+
// O run do Actions separa o mesmo job disparado por dois eventos; a reexecucao fica no mesmo run.
|
|
168
|
+
const run = /\/actions\/runs\/(\d+)/.exec(String(o.detailsUrl ?? o.targetUrl ?? ''))?.[1] ?? '';
|
|
169
|
+
const chave = JSON.stringify([String(o.__typename ?? ''), String(o.workflowName ?? '').slice(0, 200),
|
|
170
|
+
String(o.name ?? o.context ?? '').slice(0, 200), run]);
|
|
171
|
+
let situacao;
|
|
172
|
+
if (o.__typename === 'StatusContext' || (o.status === undefined && typeof o.state === 'string')) {
|
|
173
|
+
const s = String(o.state ?? '').toUpperCase();
|
|
174
|
+
situacao = s === 'SUCCESS' ? 'verde' : s === 'FAILURE' || s === 'ERROR' ? 'vermelho' : 'pendente';
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const status = String(o.status ?? '').toUpperCase(), conclusao = String(o.conclusion ?? '').toUpperCase();
|
|
178
|
+
situacao = status !== 'COMPLETED' ? 'pendente' : ['SUCCESS', 'NEUTRAL', 'SKIPPED'].includes(conclusao) ? 'verde' : 'vermelho';
|
|
179
|
+
}
|
|
180
|
+
const concluidoEm = situacao === 'pendente' ? null : instante(o.completedAt) ?? instante(o.startedAt);
|
|
181
|
+
return { check: { nome, situacao, concluidoEm }, chave, inicio: instante(o.startedAt) ?? instante(o.completedAt) ?? '' };
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Um PR da resposta do `gh`, ou `null` quando nao e de thread nenhuma daqui (fork, outra base, branch que
|
|
185
|
+
* nao e nome de branch de thread). Formato errado lanca: a leitura inteira vira "nao lido".
|
|
186
|
+
*/
|
|
187
|
+
function prDaForja(bruto, base) {
|
|
188
|
+
if (!bruto || typeof bruto !== 'object')
|
|
189
|
+
throw new Error('PR fora do formato');
|
|
190
|
+
const p = bruto;
|
|
191
|
+
const numero = p.number, head = p.headRefOid;
|
|
192
|
+
const estado = { OPEN: 'aberto', MERGED: 'mesclado', CLOSED: 'fechado' }[String(p.state)];
|
|
193
|
+
if (!Number.isSafeInteger(numero) || numero < 1 || typeof head !== 'string' || !SHA.test(head) || !estado ||
|
|
194
|
+
typeof p.isDraft !== 'boolean')
|
|
195
|
+
throw new Error('PR fora do formato');
|
|
196
|
+
if (p.isCrossRepository === true || (typeof p.baseRefName === 'string' && p.baseRefName !== base) ||
|
|
197
|
+
typeof p.headRefName !== 'string' || !BRANCH.test(p.headRefName))
|
|
198
|
+
return null;
|
|
199
|
+
const rollup = p.statusCheckRollup;
|
|
200
|
+
if (rollup !== null && rollup !== undefined && !Array.isArray(rollup))
|
|
201
|
+
throw new Error('checks fora do formato');
|
|
202
|
+
const porChave = new Map();
|
|
203
|
+
if (estado === 'aberto') {
|
|
204
|
+
for (const [i, c] of (rollup ?? []).entries()) {
|
|
205
|
+
// Check ilegivel nunca vira verde: entra como pendente, e o PR nao sai como "esperando merge".
|
|
206
|
+
const lido = checkDaForja(c) ?? { check: { nome: 'check ilegivel', situacao: 'pendente', concluidoEm: null },
|
|
207
|
+
chave: `ilegivel:${i}`, inicio: '' };
|
|
208
|
+
// O rollup repete o check reexecutado: vale o mais novo da mesma chave, como no `gh pr checks`.
|
|
209
|
+
const atual = porChave.get(lido.chave);
|
|
210
|
+
if (!atual || lido.inicio > atual.inicio)
|
|
211
|
+
porChave.set(lido.chave, lido);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const url = typeof p.url === 'string' && /^https:\/\/[^\s]+$/.test(p.url) ? curto(p.url, 300) : null;
|
|
215
|
+
return { numero: numero, branch: p.headRefName, head, estado, rascunho: p.isDraft, url,
|
|
216
|
+
criadoEm: instante(p.createdAt), mescladoEm: instante(p.mergedAt), checks: [...porChave.values()].map((x) => x.check) };
|
|
217
|
+
}
|
|
218
|
+
/** O repositorio `dono/nome` do remoto, so quando ele e do github.com (o host ancorado, nao so citado no caminho). */
|
|
219
|
+
function repositorioDoRemoto(raiz, remoto) {
|
|
220
|
+
const url = git(raiz, ['remote', 'get-url', remoto]);
|
|
221
|
+
const forja = url.ok ? (0, forja_1.identidadeDaForja)(url.stdout.trim()) : null;
|
|
222
|
+
return forja && forja.tipo === 'github' && forja.host === 'github.com' && REPOSITORIO.test(forja.repo) ? forja.repo : null;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Le os PRs da base do repositorio do remoto em duas chamadas: os abertos (ate `LIMITE_DE_PRS`; e so a
|
|
226
|
+
* lista deles que pode dizer "sem PR") e os recentes de qualquer estado (para achar o mesclado e o fechado).
|
|
227
|
+
* O `gh` usa a autenticacao dele; nenhum token passa por aqui. Remoto que nao e do github.com nao chama nada.
|
|
228
|
+
*/
|
|
229
|
+
function lerPrsDaForja(carregado, opcoes = {}) {
|
|
230
|
+
const lidoEm = opcoes.quando ?? new Date().toISOString();
|
|
231
|
+
const remoto = opcoes.remoto ?? carregado.manifesto.fabrica.remoto;
|
|
232
|
+
const base = carregado.manifesto.worktree.base_branch;
|
|
233
|
+
const repositorio = repositorioDoRemoto(carregado.raiz, remoto);
|
|
234
|
+
if (!repositorio)
|
|
235
|
+
return { ok: false, lidoEm, erro: `o remoto ${remoto} não é um repositório do github.com` };
|
|
236
|
+
const orcamento = opcoes.orcamentoMs ?? ORCAMENTO_DA_LEITURA_MS, fimDaLeitura = Date.now() + orcamento;
|
|
237
|
+
const resta = () => fimDaLeitura - Date.now();
|
|
238
|
+
const pedir = (args) => {
|
|
239
|
+
if (resta() <= 0)
|
|
240
|
+
return `gh pr list: orçamento de ${Math.round(orcamento / 1000)} s da leitura esgotado`;
|
|
241
|
+
const r = (opcoes.executor ?? ghPadrao)(['pr', 'list', `--repo=github.com/${repositorio}`, `--base=${base}`, ...args], Math.min(PRAZO_DO_GH_MS, resta()));
|
|
242
|
+
if (r.status !== 0) {
|
|
243
|
+
const detalhe = curto((0, hitl_1.redigirSegredos)(r.stderr || r.stdout || ''), 160) ?? 'sem detalhe';
|
|
244
|
+
return `gh pr list falhou (${r.status === null ? 'sem código de saída' : `código ${r.status}`}): ${detalhe}`;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const lista = JSON.parse(r.stdout);
|
|
248
|
+
if (!Array.isArray(lista))
|
|
249
|
+
throw new Error('a resposta não é uma lista');
|
|
250
|
+
return lista;
|
|
251
|
+
}
|
|
252
|
+
catch (e) {
|
|
253
|
+
return `resposta do gh pr list fora do formato: ${curto((0, hitl_1.redigirSegredos)(e.message), 120) ?? 'sem detalhe'}`;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
try {
|
|
257
|
+
const porNumero = new Map();
|
|
258
|
+
const juntar = (lista, comChecks) => {
|
|
259
|
+
for (const bruto of lista) {
|
|
260
|
+
const pr = prDaForja(bruto, base);
|
|
261
|
+
if (!pr || porNumero.has(pr.numero))
|
|
262
|
+
continue;
|
|
263
|
+
// S-b do CHECK (rodada 3): a leitura sem rollup nunca diz que o aberto nao tem checks.
|
|
264
|
+
if (!comChecks && pr.estado === 'aberto')
|
|
265
|
+
pr.checks = [{ nome: 'checks não lidos', situacao: 'pendente', concluidoEm: null }];
|
|
266
|
+
porNumero.set(pr.numero, pr);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
const abertos = pedir(['--state=open', `--limit=${exports.LIMITE_DE_PRS}`, `--json=${CAMPOS_DO_GH}`]);
|
|
270
|
+
if (typeof abertos === 'string')
|
|
271
|
+
return { ok: false, lidoEm, erro: abertos };
|
|
272
|
+
juntar(abertos, true);
|
|
273
|
+
const recentes = pedir(['--state=all', `--limit=${exports.LIMITE_DE_RECENTES}`, `--json=${CAMPOS_DOS_RECENTES}`]);
|
|
274
|
+
if (typeof recentes === 'string')
|
|
275
|
+
return { ok: false, lidoEm, erro: recentes };
|
|
276
|
+
juntar(recentes, false);
|
|
277
|
+
// R1 do CHECK (rodada 3): antes de dizer "sem PR", a branch candidata e conferida sozinha (o mesclado velho
|
|
278
|
+
// fica fora dos recentes). Acima do teto da batida, a branch fica sem conferir, e isso e "PR nao lido".
|
|
279
|
+
const achadas = new Set([...porNumero.values()].map((p) => p.branch));
|
|
280
|
+
const faltam = [...new Set(opcoes.candidatas ?? [])].filter((b) => BRANCH.test(b) && !achadas.has(b));
|
|
281
|
+
const semConferir = [];
|
|
282
|
+
for (const [i, branch] of faltam.entries()) {
|
|
283
|
+
// Acima do teto da batida, sem tempo para a conferencia inteira ou com ela falhando, a candidata fica sem conferir
|
|
284
|
+
// ("PR nao lido") e o que ja foi lido vale (sugestao da seguranca na rodada 4 do CHECK).
|
|
285
|
+
if (i >= LIMITE_DE_CANDIDATAS || resta() < MINIMO_DA_CONFERENCIA_MS) {
|
|
286
|
+
semConferir.push(branch);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const daBranch = pedir(['--state=all', `--head=${branch}`, '--limit=5', `--json=${CAMPOS_DOS_RECENTES}`]);
|
|
290
|
+
try {
|
|
291
|
+
if (typeof daBranch === 'string')
|
|
292
|
+
throw new Error(daBranch);
|
|
293
|
+
juntar(daBranch, false);
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
semConferir.push(branch);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return { ok: true, retrato: { contrato: exports.CONTRATO_PRS, lidoEm, repositorio, base, parcial: abertos.length >= exports.LIMITE_DE_PRS,
|
|
300
|
+
...(semConferir.length ? { semConferir } : {}), prs: [...porNumero.values()] } };
|
|
301
|
+
}
|
|
302
|
+
catch (e) {
|
|
303
|
+
return { ok: false, lidoEm, erro: `resposta do gh pr list fora do formato: ${curto((0, hitl_1.redigirSegredos)(e.message), 120) ?? 'sem detalhe'}` };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const dirDoMonitor = (raiz) => path.join((0, estado_thread_1.raizDoEstado)(raiz), '.orkastery', 'monitor');
|
|
307
|
+
/** Grava o retrato de uma leitura que deu certo: arquivo novo exclusivo, so do dono, renomeado por cima. */
|
|
308
|
+
function gravarRetratoDePrs(raiz, retrato) {
|
|
309
|
+
const dir = dirDoMonitor(raiz);
|
|
310
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
311
|
+
const arquivo = path.join(dir, ARQUIVO_DOS_PRS), tmp = `${arquivo}.${process.pid}.${Date.now()}.tmp`;
|
|
312
|
+
try {
|
|
313
|
+
fs.writeFileSync(tmp, JSON.stringify(retrato, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
|
|
314
|
+
fs.renameSync(tmp, arquivo);
|
|
315
|
+
}
|
|
316
|
+
catch (e) {
|
|
317
|
+
try {
|
|
318
|
+
fs.unlinkSync(tmp);
|
|
319
|
+
}
|
|
320
|
+
catch { /* ja saiu */ }
|
|
321
|
+
throw e;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** O ultimo retrato gravado pelo pulse, conferido campo a campo e com os nomes de check limpos de novo. */
|
|
325
|
+
function lerRetratoDePrs(raiz) {
|
|
326
|
+
try {
|
|
327
|
+
const r = JSON.parse(fs.readFileSync(path.join(dirDoMonitor(raiz), ARQUIVO_DOS_PRS), 'utf8'));
|
|
328
|
+
const lidoEm = instante(r.lidoEm);
|
|
329
|
+
if (r.contrato !== exports.CONTRATO_PRS || !lidoEm || typeof r.repositorio !== 'string' || !REPOSITORIO.test(r.repositorio) ||
|
|
330
|
+
typeof r.base !== 'string' || typeof r.parcial !== 'boolean' || !Array.isArray(r.prs) ||
|
|
331
|
+
(r.semConferir !== undefined && !(Array.isArray(r.semConferir) && r.semConferir.every((b) => typeof b === 'string' && BRANCH.test(b)))))
|
|
332
|
+
return null;
|
|
333
|
+
const opcional = (v) => (v === null || v === undefined ? null : instante(v) ?? undefined);
|
|
334
|
+
const prs = [];
|
|
335
|
+
for (const bruto of r.prs) {
|
|
336
|
+
const p = (bruto ?? {});
|
|
337
|
+
const estado = p.estado;
|
|
338
|
+
const criadoEm = opcional(p.criadoEm), mescladoEm = opcional(p.mescladoEm);
|
|
339
|
+
if (!Number.isSafeInteger(p.numero) || p.numero < 1 || typeof p.branch !== 'string' || !BRANCH.test(p.branch) ||
|
|
340
|
+
typeof p.head !== 'string' || !SHA.test(p.head) || !['aberto', 'mesclado', 'fechado'].includes(estado) ||
|
|
341
|
+
typeof p.rascunho !== 'boolean' || !Array.isArray(p.checks) || criadoEm === undefined || mescladoEm === undefined)
|
|
342
|
+
return null;
|
|
343
|
+
const checks = [];
|
|
344
|
+
for (const c of p.checks) {
|
|
345
|
+
const k = (c ?? {});
|
|
346
|
+
const nome = nomeDeCheck(k.nome), concluidoEm = opcional(k.concluidoEm);
|
|
347
|
+
if (!nome || !['verde', 'vermelho', 'pendente'].includes(String(k.situacao)) || concluidoEm === undefined)
|
|
348
|
+
return null;
|
|
349
|
+
checks.push({ nome, situacao: k.situacao, concluidoEm });
|
|
350
|
+
}
|
|
351
|
+
prs.push({ numero: p.numero, branch: p.branch, head: p.head, estado, rascunho: p.rascunho,
|
|
352
|
+
url: typeof p.url === 'string' && /^https:\/\/[^\s]+$/.test(p.url) ? curto(p.url, 300) : null, criadoEm, mescladoEm, checks });
|
|
353
|
+
}
|
|
354
|
+
return { contrato: exports.CONTRATO_PRS, lidoEm, repositorio: r.repositorio, base: r.base, parcial: r.parcial,
|
|
355
|
+
...(Array.isArray(r.semConferir) && r.semConferir.length ? { semConferir: r.semConferir } : {}), prs };
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
// Quem e do condutor (D6).
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
const RESPOSTAS_HITL = ['human_gate', 'session_answered'];
|
|
365
|
+
/**
|
|
366
|
+
* Pergunta estruturada ainda pendente desde `desde`: pedido sem resposta que, vencido, espera ou escala
|
|
367
|
+
* (so o "seguir a recomendada" resolve sozinho). Decisao informada (`decidido`) nao e pergunta. Pedido
|
|
368
|
+
* ilegivel conta como pendente: na duvida, a vez e do dono.
|
|
369
|
+
*/
|
|
370
|
+
function perguntaAberta(eventos, desde, quando) {
|
|
371
|
+
const inicio = Date.parse(desde);
|
|
372
|
+
const respondidos = new Set(eventos.filter((e) => RESPOSTAS_HITL.includes(e.tipo)).map((e) => String(e.pedidoId)));
|
|
373
|
+
// O pedido novo para o mesmo alvo substitui o velho: so o mais novo de cada sessao ou gate conta.
|
|
374
|
+
const porAlvo = new Map();
|
|
375
|
+
for (const e of eventos) {
|
|
376
|
+
if (e.tipo !== 'hitl_requested' || Date.parse(e.ts) < inicio)
|
|
377
|
+
continue;
|
|
378
|
+
const pedido = e.pedido;
|
|
379
|
+
if (!pedido || typeof pedido !== 'object') {
|
|
380
|
+
porAlvo.set(`ilegivel:${String(e.eventId)}`, null);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (pedido.classe === 'decidido')
|
|
384
|
+
continue;
|
|
385
|
+
let alvo;
|
|
386
|
+
try {
|
|
387
|
+
alvo = (0, hitl_contract_1.alvoDoPedido)(pedido);
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
alvo = undefined;
|
|
391
|
+
}
|
|
392
|
+
porAlvo.set(alvo ? JSON.stringify(alvo) : `pedido:${String(pedido.id)}`, pedido);
|
|
393
|
+
}
|
|
394
|
+
return [...porAlvo.values()].some((pedido) => {
|
|
395
|
+
if (!pedido)
|
|
396
|
+
return true;
|
|
397
|
+
if (respondidos.has(String(pedido.id)))
|
|
398
|
+
return false;
|
|
399
|
+
try {
|
|
400
|
+
return (0, hitl_contract_1.estadoDoPedido)(pedido, quando) !== 'seguir-recomendada';
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
/** O carimbo que o radar grava (`ork sessions hitl --registrar`): a leitura da tela, nao um evento da sessao. */
|
|
408
|
+
const doRadar = (e) => e.tipo === 'sessao_bloqueada' && e.fonte === 'ork sessions hitl --registrar';
|
|
409
|
+
/**
|
|
410
|
+
* B1 do CHECK (rodada 5): o SHIP que o observador viu terminar em `done`, com o Stop, sem o `ship_done` no
|
|
411
|
+
* intervalo do despacho. A prova do ork no SHIP e so o `ship_done` (`provaDoOrk`): o que falta e mergear o PR
|
|
412
|
+
* ou registrar a entrega, e isso e do condutor, como o fim de turno em `blocked`. Dos 4 `human.pending` do SHIP
|
|
413
|
+
* nos ledgers de 30/09, 2 tem este formato (ork-rm037noite e ork-pacotedeexpe). O CHECK em `done` sem o veredito
|
|
414
|
+
* e outra coisa, e continua com o dono (A3 da rodada 5, pendente).
|
|
415
|
+
*/
|
|
416
|
+
function shipSemRegistro(e) {
|
|
417
|
+
const { ok: provou } = (e.provaOrk ?? {});
|
|
418
|
+
return e.motivo === 'human.pending' && e.origem === 'sessions.watch' && e.fase === 'SHIP' && e.estadoNativo === 'done' &&
|
|
419
|
+
!!e.stop && provou === false;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* O bloqueio que pede o dono: o prompt de permissao do hook e o que o radar leu com pergunta na tela. O
|
|
423
|
+
* carimbo do radar no fim de turno (`blocked` lido sem menu nem pergunta) nao e prompt nenhum.
|
|
424
|
+
*/
|
|
425
|
+
function bloqueioDoDono(e) {
|
|
426
|
+
return e.tipo === 'sessao_bloqueada' && !(doRadar(e) && e.tipoDeHitl === 'hitl.desconhecido' && !String(e.pergunta ?? '').trim());
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* O ultimo entre bloqueio do dono, destravamento e Stop da sessao, desde o despacho, e bloqueio? R4-1 do CHECK
|
|
430
|
+
* (rodada 4): o carimbo do radar depois de um Stop, sem atividade da sessao no meio, e a leitura da mensagem
|
|
431
|
+
* final (a lista numerada vira "hitl.pergunta"; a palavra "token", "hitl.credencial") e nao conta. O prompt do
|
|
432
|
+
* hook conta sempre.
|
|
433
|
+
*/
|
|
434
|
+
function bloqueioPendente(eventos, sessionId, desde) {
|
|
435
|
+
const inicio = Date.parse(desde);
|
|
436
|
+
let pendente = false, turnoEncerrado = false;
|
|
437
|
+
for (const e of eventos) {
|
|
438
|
+
if (e.sessionId !== sessionId || Date.parse(e.ts) < inicio)
|
|
439
|
+
continue;
|
|
440
|
+
if (e.tipo === 'runtime_stop') {
|
|
441
|
+
pendente = false;
|
|
442
|
+
turnoEncerrado = true;
|
|
443
|
+
}
|
|
444
|
+
else if (e.tipo === 'sessao_destravada')
|
|
445
|
+
pendente = false;
|
|
446
|
+
else if (doRadar(e)) {
|
|
447
|
+
if (bloqueioDoDono(e) && !turnoEncerrado)
|
|
448
|
+
pendente = true;
|
|
449
|
+
}
|
|
450
|
+
else if (atividadeDaSessao(e)) {
|
|
451
|
+
turnoEncerrado = false;
|
|
452
|
+
if (e.tipo === 'sessao_bloqueada')
|
|
453
|
+
pendente = true;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return pendente;
|
|
457
|
+
}
|
|
458
|
+
/** O ultimo despacho de fase da thread, quando ha um. */
|
|
459
|
+
function ultimoDespacho(eventos) {
|
|
460
|
+
for (let i = eventos.length - 1; i >= 0; i--)
|
|
461
|
+
if (eventos[i].tipo === 'phase_dispatch')
|
|
462
|
+
return eventos[i];
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
const texto = (v) => (typeof v === 'string' && v ? v : null);
|
|
466
|
+
/**
|
|
467
|
+
* Atividade da sessao que supera um fim de turno: heartbeat (fora a notificacao de ociosidade), commit, Stop
|
|
468
|
+
* novo e prompt de permissao do hook. O carimbo do radar nao e atividade: so registra o `blocked` que ja
|
|
469
|
+
* estava la, mesmo quando a tela traz lista ou palavra-chave (R4-1 do CHECK, rodada 4).
|
|
470
|
+
*/
|
|
471
|
+
function atividadeDaSessao(e) {
|
|
472
|
+
if (e.tipo === 'runtime_event')
|
|
473
|
+
return !(e.sensor === 'notification' && e.notificationType === 'idle_prompt');
|
|
474
|
+
return e.tipo === 'commit' || e.tipo === 'runtime_stop' || (e.tipo === 'sessao_bloqueada' && !doRadar(e));
|
|
475
|
+
}
|
|
476
|
+
function fimDoTurno(eventos, despacho) {
|
|
477
|
+
const i = eventos.lastIndexOf(despacho);
|
|
478
|
+
const depois = eventos.slice(i + 1);
|
|
479
|
+
const sid = texto(despacho.sessionId);
|
|
480
|
+
const daSessao = (e) => !sid || !e.sessionId || e.sessionId === sid;
|
|
481
|
+
const resultado = [...depois].reverse().find((e) => e.tipo === 'phase_result' && daSessao(e));
|
|
482
|
+
let concluida = false;
|
|
483
|
+
if (resultado) {
|
|
484
|
+
const fase = texto(resultado.fase);
|
|
485
|
+
// O resultado legado (sem classificacao, `ok: true`) tambem e fase concluida.
|
|
486
|
+
concluida = resultado.ok !== false && (resultado.classificacao === 'fase_concluida' ||
|
|
487
|
+
(resultado.classificacao === undefined && resultado.ok === true));
|
|
488
|
+
// A1 (rodada 3) e R4-2 (rodada 4) do CHECK: o dono pode ter respondido na tela e a sessao voltado a trabalhar,
|
|
489
|
+
// depois da fase concluida ou da espera do observador. So vale o fim de turno sem atividade depois; com
|
|
490
|
+
// atividade, decide o Stop correlacionado mais novo, abaixo.
|
|
491
|
+
const retomou = !!sid && eventos.slice(eventos.lastIndexOf(resultado) + 1).some((e) => e.sessionId === sid && atividadeDaSessao(e));
|
|
492
|
+
if (concluida) {
|
|
493
|
+
if (!retomou)
|
|
494
|
+
return { em: resultado.ts, tipo: 'concluida', sessionId: sid, fase, comProva: true };
|
|
495
|
+
}
|
|
496
|
+
else if (resultado.motivo === 'human.pending' && despacho.pausaAoFim === true) {
|
|
497
|
+
// S-a do CHECK (rodada 3): no bloco com pausa ao fim, todo `human.pending` do resultado e a fase entregue ao dono.
|
|
498
|
+
return { em: resultado.ts, tipo: 'pausa-do-bloco', sessionId: sid, fase, comProva: true };
|
|
499
|
+
}
|
|
500
|
+
else if (resultado.motivo === 'human.pending' && resultado.stop && (resultado.estadoNativo === 'blocked' || shipSemRegistro(resultado))) {
|
|
501
|
+
if (!retomou) {
|
|
502
|
+
const { ts: doStop } = resultado.stop;
|
|
503
|
+
const { ok: provou } = (resultado.provaOrk ?? {});
|
|
504
|
+
return { em: instante(doStop) ?? resultado.ts, tipo: 'espera-do-observador', sessionId: sid, fase,
|
|
505
|
+
comProva: typeof provou === 'boolean' ? provou : null };
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
else
|
|
509
|
+
return { em: resultado.ts, tipo: 'outro', sessionId: sid, fase, comProva: null };
|
|
510
|
+
}
|
|
511
|
+
if (sid) {
|
|
512
|
+
const ultimoStop = [...depois].reverse().find((e) => e.tipo === 'runtime_stop' && e.sessionId === sid && e.sensor === 'stop');
|
|
513
|
+
const despachadaEm = texto(ultimoStop?.despachoEm);
|
|
514
|
+
if (ultimoStop && despachadaEm) {
|
|
515
|
+
const stop = (0, session_watcher_claude_1.stopCorrelacionado)(eventos, { sessionId: sid, fase: String(ultimoStop.fase), despachadaEm });
|
|
516
|
+
// Com resultado anterior e sessao retomada, so o Stop depois do resultado encerra o turno novo. A fase que
|
|
517
|
+
// ja tinha concluido continua concluida; o turno novo acabou neste Stop.
|
|
518
|
+
if (stop && (!resultado || Date.parse(stop.ts) > Date.parse(resultado.ts))) {
|
|
519
|
+
return concluida ? { em: stop.ts, tipo: 'concluida', sessionId: sid, fase: texto(resultado?.fase) ?? texto(stop.fase), comProva: true }
|
|
520
|
+
: { em: stop.ts, tipo: 'stop-sem-resultado', sessionId: sid, fase: texto(stop.fase), comProva: null };
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* O que ainda espera o dono depois do ultimo despacho, quando espera: a pausa prevista do bloco sem
|
|
528
|
+
* aprovacao humana, a thread pausada, a pergunta pendente, o prompt de permissao e a escalacao tipada
|
|
529
|
+
* dele, cada um com as regras de destravar do monitor. Nada disso e do condutor (P3).
|
|
530
|
+
*/
|
|
531
|
+
function pendenciaDoDono(t, eventos, quando) {
|
|
532
|
+
if (t.status === 'pausada')
|
|
533
|
+
return 'thread pausada';
|
|
534
|
+
const despacho = ultimoDespacho(eventos);
|
|
535
|
+
if (!despacho)
|
|
536
|
+
return null;
|
|
537
|
+
const depois = eventos.slice(eventos.lastIndexOf(despacho) + 1);
|
|
538
|
+
if (despacho.pausaAoFim === true && !depois.some(ocupacao_1.ehAprovacaoHumana))
|
|
539
|
+
return 'pausa prevista ao fim do bloco';
|
|
540
|
+
if (perguntaAberta(eventos, despacho.ts, quando))
|
|
541
|
+
return 'pergunta aberta';
|
|
542
|
+
const sid = texto(despacho.sessionId);
|
|
543
|
+
if (sid && bloqueioPendente(eventos, sid, despacho.ts))
|
|
544
|
+
return 'prompt de permissão pendente';
|
|
545
|
+
// Aviso da rodada 6 do CHECK: o gate do observador so muda de dono com o fim de turno provado. A sessao que voltou a
|
|
546
|
+
// trabalhar depois do resultado, sem Stop novo, segue do dono, e a thread nao sai tambem como linha do condutor.
|
|
547
|
+
const fim = fimDoTurno(eventos, despacho);
|
|
548
|
+
const turnoEncerrado = !!fim && fim.tipo !== 'outro';
|
|
549
|
+
for (let k = 0; k < depois.length; k++) {
|
|
550
|
+
const e = depois[k];
|
|
551
|
+
if (e.tipo !== 'gate_blocked')
|
|
552
|
+
continue;
|
|
553
|
+
const motivo = String(e.motivo ?? '');
|
|
554
|
+
// O fim de turno que o observador viu em `blocked`, e o SHIP em `done` sem o `ship_done` (B1 da rodada 5), nao sao
|
|
555
|
+
// escalacao: sao justamente o que muda de dono.
|
|
556
|
+
if (turnoEncerrado && motivo === 'human.pending' && e.origem === 'sessions.watch' &&
|
|
557
|
+
(e.estadoNativo === 'blocked' || shipSemRegistro(e)))
|
|
558
|
+
continue;
|
|
559
|
+
const doDono = motivo === 'human.pending' ||
|
|
560
|
+
(hitl_gates_1.MOTIVOS_DE_ESCALACAO_HUMANA.includes(motivo) && (0, hitl_classificacao_1.quemDecide)(motivo) === 'dono');
|
|
561
|
+
const resolvido = depois.slice(k + 1).some((p) => ocupacao_1.EVENTOS_QUE_DESTRAVAM.includes(p.tipo) || (0, ocupacao_1.ehAprovacaoHumana)(p));
|
|
562
|
+
if (doDono && !resolvido)
|
|
563
|
+
return `escalação ${motivo}`;
|
|
564
|
+
}
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
function esperaDoCondutor(t, eventos, quando) {
|
|
568
|
+
if (t.status === 'fechada')
|
|
569
|
+
return null;
|
|
570
|
+
const despacho = ultimoDespacho(eventos);
|
|
571
|
+
if (!despacho || despacho.pausaAoFim !== false)
|
|
572
|
+
return null;
|
|
573
|
+
const fim = fimDoTurno(eventos, despacho);
|
|
574
|
+
if (!fim || fim.tipo === 'outro' || fim.tipo === 'pausa-do-bloco' || pendenciaDoDono(t, eventos, quando))
|
|
575
|
+
return null;
|
|
576
|
+
return { thread: t.id, fase: fim.fase, sessionId: fim.sessionId, fimDoTurnoEm: fim.em, tipo: fim.tipo, comProva: fim.comProva };
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Sessao `blocked` do radar de quem o turno acabou sem pergunta: claude-bg com a tela lida, sem menu, sem
|
|
580
|
+
* pergunta e sem palavra de credencial ou permissao. Sessao nativa (Codex) em `blocked` e pergunta
|
|
581
|
+
* estruturada, e tela nao lida (orcamento) nao prova nada: as duas continuam do dono.
|
|
582
|
+
*/
|
|
583
|
+
function sessaoSemPergunta(s) {
|
|
584
|
+
return s.classe === 'hitl' && s.jobVivo === true && s.kind !== 'codex-controller' && s.tipoDeHitl === 'hitl.desconhecido' &&
|
|
585
|
+
s.alternativas.length === 0 && !s.pergunta?.trim() && !!s.thread?.id;
|
|
586
|
+
}
|
|
587
|
+
const minutosDesde = (desde, quando) => Math.max(0, Math.floor((Date.parse(quando) - Date.parse(desde)) / 60000));
|
|
588
|
+
const curtoDaSessao = (sid) => (sid ?? '').slice(0, 8);
|
|
589
|
+
/** Os refs locais de uma vez: `refs/heads/...` e a copia do remoto, sem rede. */
|
|
590
|
+
function refsLocais(raiz, remoto) {
|
|
591
|
+
const r = git(raiz, ['for-each-ref', '--format=%(refname)%09%(objectname)', 'refs/heads/', `refs/remotes/${remoto}/`]);
|
|
592
|
+
const mapa = new Map();
|
|
593
|
+
if (!r.ok)
|
|
594
|
+
return mapa;
|
|
595
|
+
for (const linha of r.stdout.split('\n')) {
|
|
596
|
+
const [ref, sha] = linha.split('\t');
|
|
597
|
+
if (ref && sha && SHA.test(sha))
|
|
598
|
+
mapa.set(ref, sha);
|
|
599
|
+
}
|
|
600
|
+
return mapa;
|
|
601
|
+
}
|
|
602
|
+
/** Os merges `ship(<thread>)` da base numa leitura so, do mais novo para o mais velho (fica o mais novo). */
|
|
603
|
+
function mergesNaBase(raiz, baseRef) {
|
|
604
|
+
const mapa = new Map();
|
|
605
|
+
if (!baseRef)
|
|
606
|
+
return mapa;
|
|
607
|
+
// Regex basica do git: o `(` e literal. A ref vem qualificada do `for-each-ref`.
|
|
608
|
+
const r = git(raiz, ['log', baseRef, '--first-parent', '--format=%H%x09%cI%x09%s', '--grep=^ship(']);
|
|
609
|
+
if (!r.ok)
|
|
610
|
+
return mapa;
|
|
611
|
+
for (const linha of r.stdout.split('\n')) {
|
|
612
|
+
const [sha, em, assunto] = linha.split('\t');
|
|
613
|
+
const m = /^ship\(([A-Za-z0-9._-]+)\)/.exec(assunto ?? '');
|
|
614
|
+
if (m && sha && SHA.test(sha) && !mapa.has(m[1]))
|
|
615
|
+
mapa.set(m[1], { sha, em: instante(em) });
|
|
616
|
+
}
|
|
617
|
+
return mapa;
|
|
618
|
+
}
|
|
619
|
+
/** Situacao dos checks de um PR aberto e o instante que conta para cada uma. */
|
|
620
|
+
function situacaoDoPr(pr) {
|
|
621
|
+
const maisNovo = (cs) => cs.map((c) => c.concluidoEm).filter((x) => !!x).sort().at(-1) ?? null;
|
|
622
|
+
const vermelhos = pr.checks.filter((c) => c.situacao === 'vermelho');
|
|
623
|
+
if (vermelhos.length) {
|
|
624
|
+
const ultimo = [...vermelhos].sort((a, b) => String(a.concluidoEm).localeCompare(String(b.concluidoEm))).at(-1);
|
|
625
|
+
return { situacao: 'vermelho', desdeEm: maisNovo(vermelhos) ?? pr.criadoEm, vermelho: ultimo.nome, semChecks: false };
|
|
626
|
+
}
|
|
627
|
+
if (!pr.checks.length)
|
|
628
|
+
return { situacao: 'pendente', desdeEm: pr.criadoEm, vermelho: null, semChecks: true };
|
|
629
|
+
if (pr.checks.some((c) => c.situacao === 'pendente'))
|
|
630
|
+
return { situacao: 'pendente', desdeEm: null, vermelho: null, semChecks: false };
|
|
631
|
+
return { situacao: 'verde', desdeEm: maisNovo(pr.checks) ?? pr.criadoEm, vermelho: null, semChecks: false };
|
|
632
|
+
}
|
|
633
|
+
/** Os PRs da branch no retrato: o aberto mais novo; o mesclado na ponta atual; o fechado mais novo. */
|
|
634
|
+
function prsDaBranch(retrato, branch, cabeca) {
|
|
635
|
+
const daBranch = retrato && branch ? retrato.prs.filter((p) => p.branch === branch).sort((a, b) => b.numero - a.numero) : [];
|
|
636
|
+
return {
|
|
637
|
+
aberto: daBranch.find((p) => p.estado === 'aberto') ?? null,
|
|
638
|
+
// Commit novo depois do merge e entrega nova, que ainda nao tem PR.
|
|
639
|
+
mesclado: daBranch.find((p) => p.estado === 'mesclado' && p.head === cabeca) ?? null,
|
|
640
|
+
fechado: daBranch.find((p) => p.estado === 'fechado') ?? null,
|
|
641
|
+
achado: daBranch.length > 0,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
const houveDespachoDepois = (eventos, desde) => eventos.some((e) => e.tipo === 'phase_dispatch' && Date.parse(e.ts) > Date.parse(desde));
|
|
645
|
+
/**
|
|
646
|
+
* A fase que vem depois do que o despacho rodou, quando o ciclo da thread tem uma. No #Auto uma sessao roda o
|
|
647
|
+
* bloco inteiro; nos outros modos o condutor despacha fase a fase dentro do bloco (#Maestro: GOAL e depois
|
|
648
|
+
* PLAN, as duas com o bloco GOAL-PLAN), e pular uma fase pularia a pausa do dono que mora nela.
|
|
649
|
+
*/
|
|
650
|
+
function faseSeguinte(t, despacho) {
|
|
651
|
+
if (!despacho)
|
|
652
|
+
return null;
|
|
653
|
+
const fases = t.fases ?? [];
|
|
654
|
+
const bloco = typeof despacho.bloco === 'string' && despacho.bloco ? despacho.bloco.split('-') : [];
|
|
655
|
+
const rodou = t.modo === 'auto' && bloco.length ? bloco.at(-1) : String(despacho.fase ?? '');
|
|
656
|
+
const i = fases.indexOf(rodou);
|
|
657
|
+
return i >= 0 && i + 1 < fases.length ? fases[i + 1] : null;
|
|
658
|
+
}
|
|
659
|
+
/** A linha das sessoes que sobraram de uma thread: uma linha, com todas elas. */
|
|
660
|
+
function passoDasSobras(ids, fechada) {
|
|
661
|
+
const onde = fechada ? 'da thread fechada' : 'de um despacho anterior';
|
|
662
|
+
return ids.length === 1
|
|
663
|
+
? `encerrar a sessão ${ids[0]}, que sobrou ${onde} (ork sessions stop ${ids[0]})`
|
|
664
|
+
: `encerrar as sessões ${ids.join(' e ')}, que sobraram ${onde} (${ids.map((id) => `ork sessions stop ${id}`).join('; ')})`;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* O estado da entrega de cada thread aberta deste checkout e o que esta parado no condutor (D5), mais as
|
|
668
|
+
* sessoes `blocked` sem pergunta que sobraram. O que o predicado tira do dono volta sempre como linha.
|
|
669
|
+
*/
|
|
670
|
+
function entregasDoProjeto(carregado, opcoes) {
|
|
671
|
+
const raiz = carregado.raiz, quando = opcoes.quando, limiar = opcoes.limiarMin ?? exports.LIMIAR_PARADO_NO_CONDUTOR_MIN;
|
|
672
|
+
const remoto = carregado.manifesto.fabrica.remoto, baseBranch = carregado.manifesto.worktree.base_branch;
|
|
673
|
+
const refs = refsLocais(raiz, remoto);
|
|
674
|
+
// A copia do remoto vem primeiro: a base local desta maquina pode estar parada (o merge e pelo GitHub).
|
|
675
|
+
const baseRef = [`refs/remotes/${remoto}/${baseBranch}`, `refs/heads/${baseBranch}`].find((r) => refs.has(r)) ?? null;
|
|
676
|
+
const merges = mergesNaBase(raiz, baseRef);
|
|
677
|
+
const sessoes = opcoes.sessoes ?? [];
|
|
678
|
+
const threads = [];
|
|
679
|
+
for (const id of (0, thread_1.listarIds)(raiz)) {
|
|
680
|
+
try {
|
|
681
|
+
threads.push((0, thread_1.lerThread)(raiz, id));
|
|
682
|
+
}
|
|
683
|
+
catch { /* thread ilegivel fica de fora */ }
|
|
684
|
+
}
|
|
685
|
+
const fatos = threads.filter((t) => t.status !== 'fechada').map((t) => {
|
|
686
|
+
let eventos = [];
|
|
687
|
+
try {
|
|
688
|
+
eventos = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, t.id));
|
|
689
|
+
}
|
|
690
|
+
catch { /* ledger ilegivel: sem fato, sem caso */ }
|
|
691
|
+
const branch = (0, worktree_1.branchDaWorktree)(t);
|
|
692
|
+
const cabeca = refs.get(`refs/heads/${branch}`) ?? null;
|
|
693
|
+
const remota = refs.get(`refs/remotes/${remoto}/${branch}`) ?? null;
|
|
694
|
+
const comProduto = !!cabeca && !!baseRef && Number(git(raiz, ['rev-list', '--count', `${baseRef}..${cabeca}`]).stdout.trim()) > 0;
|
|
695
|
+
// `merge-base --is-ancestor`: 0 contem, 1 nao contem; outro codigo e git sem resposta, que nunca vira "sem push".
|
|
696
|
+
const contem = remota && cabeca && remota !== cabeca ? git(raiz, ['merge-base', '--is-ancestor', cabeca, remota]).code : null;
|
|
697
|
+
const publicada = !cabeca ? null : !remota ? false : remota === cabeca || contem === 0 ? true : contem === 1 ? false : null;
|
|
698
|
+
const ships = eventos.filter((e) => e.tipo === 'ship_done'), merge = merges.get(t.id) ?? null;
|
|
699
|
+
// R4-4 do CHECK (rodada 4): o `ship_done` cobre a entrega do mesmo head (`shaDe`) ou do mesmo merge; a entrega
|
|
700
|
+
// nova depois dele ainda nao foi registrada. O legado sem o sha cobre tudo, como antes.
|
|
701
|
+
const cobre = (campo, sha) => ships.some((e) => typeof e[campo] !== 'string' || !SHA.test(e[campo]) || e[campo] === sha);
|
|
702
|
+
return { t, eventos, branch: cabeca ? branch : null, cabeca, comProduto, publicada, temRemota: !!remota,
|
|
703
|
+
shipNoLedger: ships.length > 0, entregueNaPonta: cabeca ? cobre('shaDe', cabeca) : ships.length > 0,
|
|
704
|
+
mergeRegistrado: !!merge && cobre('mergeSha', merge.sha), merge };
|
|
705
|
+
});
|
|
706
|
+
// D2: a forja so e lida quando alguma thread tem o que mostrar la: produto numa branch que ja foi ao remoto
|
|
707
|
+
// (inteira, ou com commit novo ainda sem push, que pode ter PR aberto).
|
|
708
|
+
let prs = null;
|
|
709
|
+
const candidatas = fatos.filter((f) => f.comProduto && f.temRemota && f.branch).map((f) => f.branch);
|
|
710
|
+
if (opcoes.lerPrs && candidatas.length)
|
|
711
|
+
prs = opcoes.lerPrs(candidatas);
|
|
712
|
+
// O retrato so vale do repositorio e da base de agora, e lido ha menos de `VALIDADE_DO_RETRATO_MIN`.
|
|
713
|
+
let retrato = null, semRetrato = prs && !prs.ok ? prs.erro : 'sem leitura dos PRs';
|
|
714
|
+
if (prs?.ok) {
|
|
715
|
+
const r = prs.retrato;
|
|
716
|
+
if (r.base !== baseBranch || r.repositorio !== repositorioDoRemoto(raiz, remoto))
|
|
717
|
+
semRetrato = 'retrato de PRs de outro repositório ou base';
|
|
718
|
+
else if (minutosDesde(r.lidoEm, quando) > exports.VALIDADE_DO_RETRATO_MIN)
|
|
719
|
+
semRetrato = 'retrato de PRs velho';
|
|
720
|
+
else if (Date.parse(r.lidoEm) - Date.parse(quando) > FOLGA_DO_RELOGIO_MIN * 60000)
|
|
721
|
+
semRetrato = 'retrato de PRs com data no futuro';
|
|
722
|
+
else
|
|
723
|
+
retrato = r;
|
|
724
|
+
}
|
|
725
|
+
const estados = [], parados = [];
|
|
726
|
+
const doCondutor = { gates: new Set(), sessoes: new Set(), turnosEncerrados: new Set() };
|
|
727
|
+
for (const f of fatos) {
|
|
728
|
+
const { t, eventos } = f;
|
|
729
|
+
const despacho = ultimoDespacho(eventos);
|
|
730
|
+
const fim = despacho ? fimDoTurno(eventos, despacho) : null;
|
|
731
|
+
const terminou = !!fim && fim.tipo !== 'outro';
|
|
732
|
+
const fimEm = terminou ? fim.em : null;
|
|
733
|
+
// A1 do CHECK (rodada 3): o fim de turno vem do ledger (Stop sem atividade depois); a tela da sessao depois
|
|
734
|
+
// dele e a mensagem final, e a lista numerada nela nao e menu. Sessao retomada nao tem fim de turno.
|
|
735
|
+
const espera = esperaDoCondutor(t, eventos, quando);
|
|
736
|
+
if (espera) {
|
|
737
|
+
doCondutor.gates.add(`${t.id}|${espera.fase ?? t.faseAtual}`);
|
|
738
|
+
if (espera.sessionId) {
|
|
739
|
+
doCondutor.sessoes.add(espera.sessionId);
|
|
740
|
+
doCondutor.turnosEncerrados.add(espera.sessionId);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
const conducao = (0, conducao_1.conducaoDaThread)(raiz, t.id, { agora: new Date(quando) });
|
|
744
|
+
// P7: a sessao que ja encerrou o turno nao conduz mais nada, mesmo com o lease de pe.
|
|
745
|
+
const conduzidaAgora = !!conducao && !(conducao.dono.tipo === 'sessao' && terminou && conducao.dono.sessionId === fim.sessionId);
|
|
746
|
+
const doDono = pendenciaDoDono(t, eventos, quando);
|
|
747
|
+
// A2 do CHECK (rodada 3): depois do veredito do dono, o tempo do condutor conta a partir dele.
|
|
748
|
+
const aprovacao = despacho ? eventos.slice(eventos.lastIndexOf(despacho) + 1).filter(ocupacao_1.ehAprovacaoHumana).at(-1) : undefined;
|
|
749
|
+
// A2 do CHECK: a leitura dos PRs so vale depois do fim do turno; antes dele, o turno pode ter mudado o PR.
|
|
750
|
+
const prLido = retrato && (!fimEm || Date.parse(retrato.lidoEm) >= Date.parse(fimEm)) ? retrato : null;
|
|
751
|
+
const daBranch = prsDaBranch(prLido, f.branch, f.cabeca);
|
|
752
|
+
const aberto = daBranch.aberto, s = aberto ? situacaoDoPr(aberto) : null;
|
|
753
|
+
// S1 do CHECK (rodada 2): o head do PR aberto e a ponta local: ja esta no GitHub, mesmo com a ref de rastreio velha.
|
|
754
|
+
const publicada = f.publicada === false && aberto?.head === f.cabeca ? true : f.publicada;
|
|
755
|
+
const precisaDePr = f.comProduto && publicada === true;
|
|
756
|
+
const cortado = !!prLido && !daBranch.achado && (prLido.parcial || !!f.branch && !!prLido.semConferir?.includes(f.branch));
|
|
757
|
+
const usouPr = !!prLido && precisaDePr && !cortado;
|
|
758
|
+
const prNaoLido = !precisaDePr || usouPr ? null : !retrato ? semRetrato : !prLido ? 'retrato de PRs anterior ao fim do turno'
|
|
759
|
+
: prLido.parcial ? `lista de PRs abertos cortada nos ${exports.LIMITE_DE_PRS} mais novos` : 'branch sem conferir nesta batida';
|
|
760
|
+
const proxima = faseSeguinte(t, despacho);
|
|
761
|
+
const antesDaEntrega = !!proxima && FASES_ANTES_DA_ENTREGA.includes(proxima);
|
|
762
|
+
const despachar = `despachar a fase ${proxima} (ork phase run ${t.id} ${proxima} --prompt "<pedido da fase>")`;
|
|
763
|
+
const prSabido = !!prLido && !cortado;
|
|
764
|
+
const autorizacao = MODOS_COM_PUSH_AUTORIZADO.includes(t.modo) ? '' : `, com a autorização de push do dono (${(0, modos_1.tagDoModo)(t.modo)})`;
|
|
765
|
+
// B2 do CHECK: depois de uma fase que terminou, o PR parado volta a ser do condutor, mesmo com despacho no meio.
|
|
766
|
+
const valeDesde = (instanteDoPr) => !houveDespachoDepois(eventos, instanteDoPr) ||
|
|
767
|
+
(!!fimEm && Date.parse(fimEm) >= Date.parse(instanteDoPr));
|
|
768
|
+
const maisTarde = (a, b) => (b && Date.parse(b) > Date.parse(a) ? b : a);
|
|
769
|
+
const { ts: aprovadaEm } = aprovacao ?? { ts: null };
|
|
770
|
+
const fimOuVeredito = fimEm ? maisTarde(fimEm, aprovadaEm) : null;
|
|
771
|
+
let caso = null, desde = null, passo = '', peloGit = false;
|
|
772
|
+
const evidencia = [];
|
|
773
|
+
if (!conduzidaAgora && !doDono) {
|
|
774
|
+
if (f.comProduto && publicada === false && terminou) {
|
|
775
|
+
desde = fimOuVeredito;
|
|
776
|
+
if (antesDaEntrega && !aberto) {
|
|
777
|
+
caso = 'fase-seguinte';
|
|
778
|
+
passo = despachar;
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
caso = 'sem-push';
|
|
782
|
+
// N2 da seguranca (rodada 2): branch que ja foi ao remoto pode ter PR; sem a leitura, nunca "abrir o PR". A4 da
|
|
783
|
+
// rodada 5: publicar e abrir o PR pedem a mesma autorizacao de push que o merge.
|
|
784
|
+
passo = (aberto ? `publicar os commits novos da branch ${f.branch} no PR #${aberto.numero}`
|
|
785
|
+
: f.temRemota && !prSabido ? `publicar os commits novos da branch ${f.branch} (PR não lido)`
|
|
786
|
+
: `publicar a branch ${f.branch} e abrir o PR`) + autorizacao;
|
|
787
|
+
}
|
|
788
|
+
evidencia.push(`refs/heads/${f.branch} tem commit fora de refs/remotes/${remoto}/${f.branch}`);
|
|
789
|
+
}
|
|
790
|
+
else if (aberto && s?.situacao === 'vermelho' && s.desdeEm && valeDesde(s.desdeEm)) {
|
|
791
|
+
caso = 'pr-vermelho';
|
|
792
|
+
desde = maisTarde(s.desdeEm, fimOuVeredito);
|
|
793
|
+
passo = `corrigir o check ${s.vermelho} vermelho do PR #${aberto.numero} e despachar a correção`;
|
|
794
|
+
evidencia.push(`gh pr list: PR #${aberto.numero} com o check ${s.vermelho} vermelho`);
|
|
795
|
+
}
|
|
796
|
+
else if (aberto && s && (s.situacao === 'verde' || s.semChecks) && s.desdeEm && valeDesde(s.desdeEm)) {
|
|
797
|
+
caso = 'pr-verde';
|
|
798
|
+
desde = maisTarde(s.desdeEm, fimOuVeredito);
|
|
799
|
+
passo = `${aberto.rascunho ? `tirar o PR #${aberto.numero} do rascunho e mergear` : `mergear o PR #${aberto.numero}`}` +
|
|
800
|
+
`${s.semChecks ? ', que não tem checks' : ''}${autorizacao}`;
|
|
801
|
+
evidencia.push(s.semChecks ? `gh pr list: PR #${aberto.numero} sem checks e sem merge`
|
|
802
|
+
: `gh pr list: PR #${aberto.numero} com ${aberto.checks.length} check(s) verde(s) e sem merge`);
|
|
803
|
+
}
|
|
804
|
+
else if (!aberto && !f.comProduto && f.merge && !f.mergeRegistrado) {
|
|
805
|
+
caso = 'sem-registro';
|
|
806
|
+
desde = f.merge.em ?? fimEm;
|
|
807
|
+
peloGit = true;
|
|
808
|
+
passo = `registrar a entrega do merge ${f.merge.sha.slice(0, 7)} (ork ship registrar-pr ${t.id})`;
|
|
809
|
+
evidencia.push(`git log: merge ship(${t.id}) ${f.merge.sha.slice(0, 7)} na base, sem ship_done no ledger`);
|
|
810
|
+
}
|
|
811
|
+
else if (!aberto && daBranch.mesclado && !f.entregueNaPonta && (daBranch.mesclado.mescladoEm ?? fimEm)) {
|
|
812
|
+
// N4 do CHECK (rodada 2): PR mesclado sem o assunto ship(<thread>); o `registrar-pr` nao o acha sozinho. Com o
|
|
813
|
+
// merge ship(<thread>) ainda sem registro na base (squash), o `registrar-pr` acha.
|
|
814
|
+
caso = 'sem-registro';
|
|
815
|
+
desde = daBranch.mesclado.mescladoEm ?? fimEm;
|
|
816
|
+
passo = f.merge && !f.mergeRegistrado ? `registrar a entrega do merge ${f.merge.sha.slice(0, 7)} (ork ship registrar-pr ${t.id})`
|
|
817
|
+
: `conferir a entrega do PR #${daBranch.mesclado.numero}, mesclado sem o assunto ship(${t.id}), e registrar o ship_done`;
|
|
818
|
+
evidencia.push(`gh pr list: PR #${daBranch.mesclado.numero} mesclado na ponta da branch, sem ship_done no ledger`);
|
|
819
|
+
}
|
|
820
|
+
else if (!aberto && !daBranch.mesclado && precisaDePr && terminou && usouPr && !f.entregueNaPonta) {
|
|
821
|
+
desde = fimOuVeredito;
|
|
822
|
+
if (antesDaEntrega) {
|
|
823
|
+
caso = 'fase-seguinte';
|
|
824
|
+
passo = despachar;
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
caso = 'sem-pr';
|
|
828
|
+
passo = (daBranch.fechado ? `abrir de novo o PR da branch ${f.branch} (o PR #${daBranch.fechado.numero} foi fechado sem merge)`
|
|
829
|
+
: `abrir o PR da branch ${f.branch}`) + autorizacao;
|
|
830
|
+
}
|
|
831
|
+
evidencia.push(`refs/remotes/${remoto}/${f.branch} contém a ponta`, daBranch.fechado ? `gh pr list: PR #${daBranch.fechado.numero} fechado sem merge` : `gh pr list: nenhum PR de ${f.branch}`);
|
|
832
|
+
}
|
|
833
|
+
else if (terminou && despacho?.pausaAoFim === true && aprovadaEm && proxima && FASES_ANTES_DA_ENTREGA.includes(proxima)) {
|
|
834
|
+
// S-e do CHECK (rodada 3): o dono aprovou a pausa e a fase seguinte nao foi despachada. Rodada 5: vale para
|
|
835
|
+
// qualquer fim de turno do bloco com pausa ao fim, porque no Codex a fase boa chega como `fase_concluida`.
|
|
836
|
+
caso = 'fase-seguinte';
|
|
837
|
+
desde = aprovadaEm;
|
|
838
|
+
passo = despachar;
|
|
839
|
+
evidencia.push('ledger: veredito do dono na pausa prevista, sem despacho depois');
|
|
840
|
+
}
|
|
841
|
+
else if (espera) {
|
|
842
|
+
// O que sai do dono volta sempre: sem caso de entrega, a linha diz o que a thread pede agora.
|
|
843
|
+
desde = espera.fimDoTurnoEm;
|
|
844
|
+
const id = curtoDaSessao(espera.sessionId);
|
|
845
|
+
if (antesDaEntrega && !f.entregueNaPonta && espera.comProva !== false) {
|
|
846
|
+
caso = 'fase-seguinte';
|
|
847
|
+
passo = despachar;
|
|
848
|
+
}
|
|
849
|
+
else {
|
|
850
|
+
caso = 'sessao-sem-pergunta';
|
|
851
|
+
passo = f.shipNoLedger && !f.comProduto ? `fechar o MASTER da thread (ork master ${t.id})`
|
|
852
|
+
: aberto ? `acompanhar os checks do PR #${aberto.numero}, que seguem em andamento`
|
|
853
|
+
: precisaDePr && !usouPr ? `conferir o PR da branch ${f.branch} (PR não lido) e seguir`
|
|
854
|
+
: `ler o fim da sessão ${id} (ork sessions logs ${id}) e seguir a thread`;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (caso && fim && fim.tipo !== 'outro')
|
|
859
|
+
evidencia.push(`fim do turno: ${fim.tipo}`);
|
|
860
|
+
const paradoHaMin = desde ? minutosDesde(desde, quando) : 0;
|
|
861
|
+
const dependeDoPr = caso === 'pr-verde' || caso === 'pr-vermelho' || caso === 'sem-pr' || (caso === 'sem-push' && !!aberto) ||
|
|
862
|
+
(caso === 'sem-registro' && !peloGit);
|
|
863
|
+
const parado = caso && desde && paradoHaMin >= limiar ? {
|
|
864
|
+
thread: t.id, caso, desdeEm: desde, paradoHaMin, proximoPasso: passo, evidencia, branch: f.branch, pr: aberto?.numero ?? null,
|
|
865
|
+
sessionId: espera?.sessionId ?? fim?.sessionId ?? null, prLidoEm: dependeDoPr && prLido ? prLido.lidoEm : null,
|
|
866
|
+
} : null;
|
|
867
|
+
if (parado)
|
|
868
|
+
parados.push(parado);
|
|
869
|
+
const prDaEntrega = aberto ? { numero: aberto.numero, estado: 'aberto', situacao: s?.situacao ?? null,
|
|
870
|
+
checkVermelho: s?.vermelho ?? null, rascunho: aberto.rascunho }
|
|
871
|
+
: daBranch.mesclado && !f.entregueNaPonta ? { numero: daBranch.mesclado.numero, estado: 'mesclado', situacao: null, checkVermelho: null, rascunho: false }
|
|
872
|
+
: null;
|
|
873
|
+
const registrar = `ork ship registrar-pr ${t.id}`;
|
|
874
|
+
const resumo = aberto ? (s?.situacao === 'vermelho' ? `PR #${aberto.numero} aberto com o check ${s.vermelho} vermelho`
|
|
875
|
+
: s?.semChecks ? `PR #${aberto.numero} aberto, sem checks, esperando o merge`
|
|
876
|
+
: s?.situacao === 'verde' ? (aberto.rascunho ? `PR #${aberto.numero} em rascunho, com os checks verdes`
|
|
877
|
+
: `PR #${aberto.numero} com os checks verdes, esperando o merge`)
|
|
878
|
+
: `PR #${aberto.numero} aberto, checks em andamento`)
|
|
879
|
+
: f.comProduto && publicada === false ? 'branch com commits sem push'
|
|
880
|
+
: daBranch.mesclado && !f.entregueNaPonta ? (f.merge && !f.mergeRegistrado ? `PR #${daBranch.mesclado.numero} mesclado, falta registrar a entrega (${registrar})`
|
|
881
|
+
: `PR #${daBranch.mesclado.numero} mesclado sem o assunto ship(${t.id}), falta registrar a entrega`)
|
|
882
|
+
: !f.comProduto && f.merge && !f.mergeRegistrado ? `merge ${f.merge.sha.slice(0, 7)} na base, falta registrar a entrega (${registrar})`
|
|
883
|
+
: precisaDePr ? (!usouPr ? 'branch publicada, PR não lido'
|
|
884
|
+
: daBranch.fechado ? `branch publicada, PR #${daBranch.fechado.numero} fechado sem merge` : 'branch publicada sem PR')
|
|
885
|
+
: null;
|
|
886
|
+
estados.push({ thread: t.id, branch: f.branch, comProduto: f.comProduto, publicada, pr: prDaEntrega,
|
|
887
|
+
prLidoEm: prLido && (usouPr || aberto) ? prLido.lidoEm : null, prNaoLido, conduzidaAgora, espera, parado, resumo });
|
|
888
|
+
}
|
|
889
|
+
// As sessoes `blocked` sem pergunta que sobraram: de thread fechada, ou de despacho antigo de thread aberta.
|
|
890
|
+
// A do despacho corrente fica com o predicado do ledger (`esperaDoCondutor`). Sem o carimbo do radar nao
|
|
891
|
+
// ha desde, e a sessao segue com o dono.
|
|
892
|
+
const sobras = new Map();
|
|
893
|
+
for (const s of sessoes) {
|
|
894
|
+
if (!sessaoSemPergunta(s))
|
|
895
|
+
continue;
|
|
896
|
+
const t = threads.find((x) => x.id === s.thread.id);
|
|
897
|
+
const { bloqueadaDesdeEm: carimbo } = s;
|
|
898
|
+
const desde = instante(carimbo);
|
|
899
|
+
if (!t || !desde)
|
|
900
|
+
continue;
|
|
901
|
+
if (t.status !== 'fechada') {
|
|
902
|
+
let eventos = [];
|
|
903
|
+
try {
|
|
904
|
+
eventos = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, t.id));
|
|
905
|
+
}
|
|
906
|
+
catch {
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
const despachos = eventos.filter((e) => e.tipo === 'phase_dispatch');
|
|
910
|
+
const antiga = texto(despachos.at(-1)?.sessionId) !== s.sessionId && despachos.some((e) => e.sessionId === s.sessionId);
|
|
911
|
+
if (!antiga || pendenciaDoDono(t, eventos, quando))
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
const atual = sobras.get(t.id) ?? { ids: [], sessoes: [], desde, fechada: t.status === 'fechada' };
|
|
915
|
+
atual.ids.push(curtoDaSessao(s.sessionId));
|
|
916
|
+
atual.sessoes.push(s.sessionId);
|
|
917
|
+
if (Date.parse(desde) < Date.parse(atual.desde))
|
|
918
|
+
atual.desde = desde;
|
|
919
|
+
sobras.set(t.id, atual);
|
|
920
|
+
}
|
|
921
|
+
for (const [thread, x] of sobras) {
|
|
922
|
+
for (const sid of x.sessoes)
|
|
923
|
+
doCondutor.sessoes.add(sid);
|
|
924
|
+
// A thread que ja tem a linha dela nao ganha outra: uma linha por thread.
|
|
925
|
+
if (parados.some((p) => p.thread === thread) || minutosDesde(x.desde, quando) < limiar)
|
|
926
|
+
continue;
|
|
927
|
+
parados.push({ thread, caso: 'sessao-sem-pergunta', desdeEm: x.desde, paradoHaMin: minutosDesde(x.desde, quando),
|
|
928
|
+
proximoPasso: passoDasSobras(x.ids, x.fechada), evidencia: ['radar: sessão blocked com a tela lida, sem menu nem pergunta'],
|
|
929
|
+
branch: null, pr: null, sessionId: x.sessoes[0], prLidoEm: null });
|
|
930
|
+
}
|
|
931
|
+
parados.sort((a, b) => a.desdeEm.localeCompare(b.desdeEm) || a.thread.localeCompare(b.thread));
|
|
932
|
+
return { estados, parados, doCondutor, prs };
|
|
933
|
+
}
|