@orkastery/cli 0.4.3 → 0.5.0

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 (121) hide show
  1. package/adapters/README.md +11 -0
  2. package/adapters/claude-code/.claude-plugin/plugin.json +3 -1
  3. package/adapters/claude-code/agents/ork-check.md +1 -1
  4. package/adapters/claude-code/agents/ork-go.md +2 -2
  5. package/adapters/claude-code/agents/ork-goal.md +1 -1
  6. package/adapters/claude-code/agents/ork-master.md +1 -1
  7. package/adapters/claude-code/agents/ork-plan.md +1 -1
  8. package/adapters/claude-code/agents/ork-ship.md +1 -1
  9. package/adapters/claude-code/commands/master.md +11 -8
  10. package/adapters/claude-code/commands/ork.md +8 -0
  11. package/adapters/claude-code/commands/ship.md +2 -2
  12. package/adapters/codex/skills/ork/SKILL.md +12 -1
  13. package/adapters/hermes/README.md +3 -0
  14. package/adapters/hermes/bin/ork-abrir-thread.sh +17 -2
  15. package/adapters/hermes/bin/ork-brain.sh +3 -0
  16. package/adapters/hermes/bin/ork-maestro.sh +4 -1
  17. package/adapters/hermes/bin/ork-network-roadmap.sh +11 -0
  18. package/adapters/hermes/bin/ork-roadmap-status.sh +9 -0
  19. package/adapters/hermes/hermes.plugin.json +2 -0
  20. package/adapters/hermes/hitl-ingress/__init__.py +36 -3
  21. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +19 -20
  22. package/adapters/openclaw/README.md +34 -2
  23. package/adapters/openclaw/dist/hitl-ingress.js +37 -3
  24. package/adapters/openclaw/dist/index.js +92 -20
  25. package/adapters/openclaw/openclaw.plugin.json +10 -1
  26. package/adapters/openclaw/package.json +2 -2
  27. package/adapters/openclaw/src/hitl-ingress.ts +32 -3
  28. package/adapters/openclaw/src/index.ts +108 -20
  29. package/assets/docs/padroes/documentacao-de-produto.md +2 -2
  30. package/assets/docs/padroes/roadmap-de-produto.md +1 -1
  31. package/assets/docs/produto/_modelo-feature.md +2 -0
  32. package/assets/orkmind_bridge.py +214 -1
  33. package/dist/adapters/claude-bg.js +6 -3
  34. package/dist/board.js +74 -0
  35. package/dist/branch-de-estado.js +5 -4
  36. package/dist/busca-semantica.js +161 -0
  37. package/dist/ci.js +141 -10
  38. package/dist/company-brain-cli.js +27 -2
  39. package/dist/company-brain-context.js +132 -0
  40. package/dist/company-brain-dossie.js +376 -0
  41. package/dist/company-brain-mcp.js +4 -1
  42. package/dist/conducao.js +71 -18
  43. package/dist/decisao-autonoma.js +42 -0
  44. package/dist/docs.js +64 -22
  45. package/dist/doctor.js +33 -0
  46. package/dist/entrega-pr.js +109 -5
  47. package/dist/experiencia-instalacao.js +256 -0
  48. package/dist/experiencia.js +72 -0
  49. package/dist/fabrica-estado.js +43 -10
  50. package/dist/fechamento.js +105 -0
  51. package/dist/forja.js +435 -0
  52. package/dist/gates.js +9 -1
  53. package/dist/hitl-classificacao.js +11 -1
  54. package/dist/hitl-contract.js +50 -7
  55. package/dist/hitl-curto.js +158 -0
  56. package/dist/hitl-gates.js +67 -1
  57. package/dist/hitl-lote.js +30 -15
  58. package/dist/hitl-presentation.js +10 -7
  59. package/dist/hitl-resumo.js +33 -12
  60. package/dist/hitl-texto-livre.js +150 -0
  61. package/dist/hosts.js +86 -1
  62. package/dist/index.js +602 -38
  63. package/dist/indice-vetorial.js +317 -0
  64. package/dist/init.js +13 -0
  65. package/dist/integracoes-locais.js +1 -1
  66. package/dist/intelligence-benchmark-contract.js +570 -0
  67. package/dist/intelligence-graph-cli.js +430 -0
  68. package/dist/intelligence-graph-contract.js +577 -0
  69. package/dist/intelligence-graph-extract-md.js +469 -0
  70. package/dist/intelligence-graph-extract-ts.js +810 -0
  71. package/dist/intelligence-graph-extract.js +369 -0
  72. package/dist/intelligence-graph-index.js +512 -0
  73. package/dist/intelligence-graph-parsers.js +327 -0
  74. package/dist/intelligence-graph-query.js +344 -0
  75. package/dist/intelligence-graph-repo.js +187 -0
  76. package/dist/leases.js +84 -0
  77. package/dist/ledger.js +14 -0
  78. package/dist/maestro-cli.js +10 -1
  79. package/dist/maestro-contract.js +5 -1
  80. package/dist/maestro-discovery.js +16 -2
  81. package/dist/maestro-snapshot.js +6 -2
  82. package/dist/manifest.js +142 -5
  83. package/dist/master-batch.js +3 -2
  84. package/dist/master-digest.js +2 -2
  85. package/dist/master-nota.js +203 -0
  86. package/dist/master.js +4 -0
  87. package/dist/mcp-experiencia.js +164 -0
  88. package/dist/mcp-git.js +4 -0
  89. package/dist/mcp-install.js +3 -1
  90. package/dist/mcp-maestro.js +3 -2
  91. package/dist/mcp-server.js +84 -5
  92. package/dist/memoria.js +147 -1
  93. package/dist/network-roadmap.js +907 -0
  94. package/dist/ocupacao.js +38 -2
  95. package/dist/onboarding.js +113 -7
  96. package/dist/orkmind.js +165 -9
  97. package/dist/phase.js +166 -13
  98. package/dist/policies.js +2 -0
  99. package/dist/projeto-alvo.js +551 -0
  100. package/dist/prompts.js +1 -1
  101. package/dist/pulse-consentimento.js +4 -2
  102. package/dist/pulse-delivery.js +5 -0
  103. package/dist/pulse-resposta.js +674 -49
  104. package/dist/pulse.js +21 -0
  105. package/dist/retry.js +51 -8
  106. package/dist/roadmap-reservas.js +262 -20
  107. package/dist/roadmap-status.js +213 -0
  108. package/dist/thread-close.js +3 -0
  109. package/dist/verify.js +4 -3
  110. package/eval/casos/orchestration-experience-pt-br.json +48 -0
  111. package/eval/casos/orchestration-experience.json +48 -0
  112. package/package.json +1 -1
  113. package/schemas/code-artifact-graph.v1.schema.json +581 -0
  114. package/schemas/graph-benchmark.v1.schema.json +2240 -0
  115. package/schemas/maestro-snapshot.schema.json +23 -0
  116. package/skills/README.md +7 -5
  117. package/skills/core/onboarding/SKILL.md +7 -0
  118. package/skills/core/orchestration-experience/SKILL.md +113 -0
  119. package/skills/core/orchestration-experience-pt-br/SKILL.md +112 -0
  120. package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
  121. package/skills/phases/go-implementation/SKILL.md +2 -0
@@ -0,0 +1,907 @@
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.ErroDoPedidoDeProjeto = exports.SAIDA_DO_PEDIDO = exports.LIMIAR_SEM_BATIDA_MS = exports.CONTRATO_REGISTRO_DE_PROJETOS = exports.CONTRATO_PANORAMA_DA_REDE = void 0;
37
+ exports.projetosConhecidos = projetosConhecidos;
38
+ exports.resolverProjeto = resolverProjeto;
39
+ exports.montarPanoramaDaRede = montarPanoramaDaRede;
40
+ exports.textoDoPanoramaDaRede = textoDoPanoramaDaRede;
41
+ /**
42
+ * RM-054 (fatia 1): o roadmap da rede, de qualquer diretorio.
43
+ *
44
+ * Em 29/09/2026 o dono pediu pelo Telegram o status do roadmap do orkastery e recebeu o panorama
45
+ * do cwd do gateway (projeto `workspace`, 0 threads), lido como "roadmap vazio". O roadmap real
46
+ * tinha 13 itens reservados e 10 threads na `vps`. Este modulo junta, para cada projeto pedido:
47
+ *
48
+ * - o status report do RM-048 (`ork.roadmap-status/v1`), com as threads de TODAS as maquinas;
49
+ * - as reservas (`ork/roadmap-reservas`);
50
+ * - as threads de cada maquina (`ork/fabrica-estado`), com a idade da ultima batida;
51
+ * - a fonte e o horario de cada parte, e o que ficou sem ler como lacuna tipada.
52
+ *
53
+ * Com clone nesta maquina, o roadmap vem de `docs/roadmap` da BASE REMOTA (D1 do PLAN): toda
54
+ * maquina ve o mesmo roadmap, qualquer que seja a branch do checkout. As branches de estado vem
55
+ * pelo fetch de sempre, e esta maquina entra pelo estado local fresco, no lugar do retrato
56
+ * publicado dela (D6). Sem clone, tudo vem da forja, so com consulta (`forja.ts`).
57
+ *
58
+ * As lacunas usam o vocabulario da rede (RM-053) e nunca viram "vazio": o que nao foi lido e dito
59
+ * com o tipo e o que fazer. `naoConsultado` diz o que esta leitura nao olhou. Leitura pura: nada e
60
+ * gravado no estado do `ork` nem na forja; o unico efeito e o `git fetch` de sempre nas refs
61
+ * remotas do clone, e `semRemoto` o desliga.
62
+ *
63
+ * Na fatia 2, os hosts chegam aqui por dois modos: `host` (gateway sem cwd de projeto, a regra da
64
+ * RM-052 no `--projeto` proprio do `network`) e `fixado` (o servidor MCP, que le so o projeto dele).
65
+ */
66
+ const node_child_process_1 = require("node:child_process");
67
+ const fs = __importStar(require("node:fs"));
68
+ const os = __importStar(require("node:os"));
69
+ const path = __importStar(require("node:path"));
70
+ const branch_de_estado_1 = require("./branch-de-estado");
71
+ const docs_1 = require("./docs");
72
+ const fabrica_estado_1 = require("./fabrica-estado");
73
+ const forja_1 = require("./forja");
74
+ const horario_1 = require("./horario");
75
+ const estado_thread_1 = require("./estado-thread");
76
+ const manifest_1 = require("./manifest");
77
+ const maquina_1 = require("./maquina");
78
+ const modos_1 = require("./modos");
79
+ const projeto_alvo_1 = require("./projeto-alvo");
80
+ const roadmap_reservas_1 = require("./roadmap-reservas");
81
+ const roadmap_status_1 = require("./roadmap-status");
82
+ const thread_1 = require("./thread");
83
+ const versao_1 = require("./versao");
84
+ const yaml_1 = require("./yaml");
85
+ exports.CONTRATO_PANORAMA_DA_REDE = 'ork.network-roadmap/v1';
86
+ /** O registro de projetos da RM-052, lido como ela o grava. */
87
+ exports.CONTRATO_REGISTRO_DE_PROJETOS = 'ork.projetos/v1';
88
+ /** Maquina sem retrato novo ha mais disto esta sem batida: o mesmo limiar da rede (RM-053, `SEM_BATIDA_MS`). */
89
+ exports.LIMIAR_SEM_BATIDA_MS = 3 * 60 * 60 * 1000;
90
+ /** A recusa de projeto sai com o mesmo codigo da RM-052. */
91
+ exports.SAIDA_DO_PEDIDO = 4;
92
+ /** A recusa do `--projeto`, com os mesmos codigos da RM-052: o nucleo nunca chuta. */
93
+ class ErroDoPedidoDeProjeto extends Error {
94
+ codigo;
95
+ detalhe;
96
+ candidatos;
97
+ correcao;
98
+ constructor(codigo, detalhe, candidatos, correcao) {
99
+ super(`${codigo}: ${detalhe}`);
100
+ this.codigo = codigo;
101
+ this.detalhe = detalhe;
102
+ this.candidatos = candidatos;
103
+ this.correcao = correcao;
104
+ this.name = 'ErroDoPedidoDeProjeto';
105
+ }
106
+ get recusa() {
107
+ return { erro: this.codigo, detalhe: this.detalhe, candidatos: this.candidatos, correcao: this.correcao };
108
+ }
109
+ get texto() {
110
+ return [`${this.codigo}: ${this.detalhe}`, ...(this.candidatos.length ? ['Candidatos:', ...this.candidatos.map((c) => ` • ${c}`)] : []),
111
+ this.correcao].join('\n');
112
+ }
113
+ }
114
+ exports.ErroDoPedidoDeProjeto = ErroDoPedidoDeProjeto;
115
+ const lacuna = (tipo, parte, alvo, detalhe, correcao) => ({ tipo, parte, ...(alvo ? { alvo } : {}), detalhe, correcao });
116
+ // ---------------------------------------------------------------------------
117
+ // Os projetos: o do cwd, o do registro da RM-052 e o do pedido.
118
+ // ---------------------------------------------------------------------------
119
+ /**
120
+ * Nome de remoto do git, como `origin`. O valor vem do manifesto versionado (`fabrica.remoto`), e um
121
+ * valor que comece com `-` viraria opcao do `git fetch` (`--upload-pack=...`): so este formato chega
122
+ * ao git (achado 4 do CHECK).
123
+ */
124
+ const REMOTO = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
125
+ /**
126
+ * Nome de branch base, como `main`. Tambem vem do manifesto (`worktree.base_branch`), e o `git log`
127
+ * da base o recebia cru: `--output=<arquivo>` gravava um arquivo (rodada 2 do CHECK).
128
+ */
129
+ const baseValida = (base) => typeof base === 'string' && /^[A-Za-z0-9][A-Za-z0-9._/-]{0,99}$/.test(base) && !base.includes('..');
130
+ function projetoDoClone(c, origem) {
131
+ const remoto = c.manifesto.fabrica.remoto;
132
+ const url = REMOTO.test(remoto) ? (0, branch_de_estado_1.git)(c.raiz, ['remote', 'get-url', remoto]) : null;
133
+ return { nome: c.manifesto.project.name, forja: url?.ok ? (0, forja_1.identidadeDaForja)(url.stdout.trim()) : null, raiz: c.raiz,
134
+ base: c.manifesto.worktree.base_branch, remoto, origem };
135
+ }
136
+ function mesmoProjeto(a, b) {
137
+ if ((0, forja_1.mesmaForja)(a.forja, b.forja))
138
+ return true;
139
+ if (!a.raiz || !b.raiz)
140
+ return false;
141
+ try {
142
+ return (0, estado_thread_1.raizDoEstado)(a.raiz) === (0, estado_thread_1.raizDoEstado)(b.raiz);
143
+ }
144
+ catch {
145
+ return false;
146
+ }
147
+ }
148
+ const obj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
149
+ /**
150
+ * O registro da RM-052 (`~/.orkastery/projetos.json`, `ork.projetos/v1`): `projetos` como lista ou
151
+ * mapa por nome; de cada um, `nome`, `raiz` ou `caminho`, e `remoto`. Entrada que nao se le vira
152
+ * lacuna; arquivo ausente vai para o nao consultado.
153
+ */
154
+ function projetosDoRegistro(arquivo) {
155
+ const lacunas = [];
156
+ const invalido = (detalhe) => lacuna('registro.invalido', 'registro', arquivo, detalhe, 'confira o registro com `ork projetos` (RM-052) ou peça o projeto com --projeto');
157
+ let bruto;
158
+ try {
159
+ bruto = JSON.parse(fs.readFileSync(arquivo, 'utf8'));
160
+ }
161
+ catch (e) {
162
+ if (e.code === 'ENOENT')
163
+ return { projetos: [], lacunas, lido: false };
164
+ return { projetos: [], lacunas: [invalido('o registro de projetos não é um JSON legível')], lido: true };
165
+ }
166
+ if (!obj(bruto) || (bruto.contrato !== undefined && bruto.contrato !== exports.CONTRATO_REGISTRO_DE_PROJETOS)) {
167
+ return { projetos: [], lacunas: [invalido(`o registro não está no contrato ${exports.CONTRATO_REGISTRO_DE_PROJETOS}`)], lido: true };
168
+ }
169
+ const lista = Array.isArray(bruto.projetos) ? bruto.projetos
170
+ : obj(bruto.projetos) ? Object.entries(bruto.projetos).map(([nome, v]) => (obj(v) ? { nome, ...v } : v)) : null;
171
+ if (!lista)
172
+ return { projetos: [], lacunas: [invalido('o registro não traz a lista de projetos')], lido: true };
173
+ const projetos = [];
174
+ for (const e of lista) {
175
+ const nome = obj(e) && typeof e.nome === 'string' ? e.nome.trim() : '';
176
+ const raiz = obj(e) ? [e.raiz, e.caminho].find((v) => typeof v === 'string' && path.isAbsolute(v)) : undefined;
177
+ const remoto = obj(e) && typeof e.remoto === 'string' ? e.remoto : null;
178
+ let c = null;
179
+ try {
180
+ c = raiz && fs.existsSync(raiz) ? (0, manifest_1.carregarManifesto)(raiz) : null;
181
+ }
182
+ catch {
183
+ c = null;
184
+ }
185
+ const forja = remoto ? (0, forja_1.identidadeDaForja)(remoto) : null;
186
+ if (c)
187
+ projetos.push(projetoDoClone(c, 'registro'));
188
+ else if (nome && forja)
189
+ projetos.push({ nome, forja, raiz: null, base: null, remoto: 'origin', origem: 'registro' });
190
+ else
191
+ lacunas.push(invalido(`entrada ${nome || '(sem nome)'} sem clone nesta máquina e sem remoto de forja reconhecido`));
192
+ }
193
+ return { projetos, lacunas, lido: true };
194
+ }
195
+ /**
196
+ * Os projetos que esta maquina conhece, sem repetir: o do cwd primeiro, depois o registro. Com
197
+ * `host` (D-G6 da fatia 2), o cwd do gateway nao e projeto de ninguem: o projeto dele so entra pelo
198
+ * registro, e fora dele vai ao "nao consultado" dizendo por que. Foi ele que respondeu em 29/09.
199
+ */
200
+ function projetosConhecidos(opcoes = {}) {
201
+ const cwd = opcoes.cwd ?? process.cwd();
202
+ const candidatos = [];
203
+ const doCwd = (0, manifest_1.carregarManifesto)(cwd);
204
+ const arquivo = opcoes.registro ?? path.join((0, maquina_1.pastaDoUsuario)(), 'projetos.json');
205
+ const registro = projetosDoRegistro(arquivo);
206
+ const naoConsultado = registro.lido ? [] : [`registro de projetos desta máquina (RM-052): ${arquivo} ausente`];
207
+ if (doCwd && !opcoes.host)
208
+ candidatos.push(projetoDoClone(doCwd, 'cwd'));
209
+ else if (doCwd) {
210
+ const doGateway = projetoDoClone(doCwd, 'cwd');
211
+ if (!registro.projetos.some((p) => mesmoProjeto(p, doGateway))) {
212
+ naoConsultado.push(`diretório do host (${raizParaExibir(doCwd.raiz)}): o projeto ${doGateway.nome} está fora do registro desta ` +
213
+ 'máquina, e o ork não lê projeto pelo diretório do gateway (RM-052); para entrar, `ork projetos registrar <caminho>`');
214
+ }
215
+ }
216
+ candidatos.push(...registro.projetos);
217
+ const projetos = [];
218
+ for (const p of candidatos) {
219
+ const i = projetos.findIndex((q) => mesmoProjeto(q, p));
220
+ if (i < 0)
221
+ projetos.push(p);
222
+ else if (!projetos[i].raiz && p.raiz)
223
+ projetos[i] = p;
224
+ }
225
+ return { projetos, lacunas: registro.lacunas, naoConsultado };
226
+ }
227
+ const rotuloDoProjeto = (p) => [p.nome, p.forja ? (0, forja_1.rotuloDaForja)(p.forja) : null, p.raiz ? raizParaExibir(p.raiz) : null].filter(Boolean).join(' · ');
228
+ function raizParaExibir(raiz) {
229
+ try {
230
+ return (0, estado_thread_1.raizDoEstado)(raiz);
231
+ }
232
+ catch {
233
+ return raiz;
234
+ }
235
+ }
236
+ /** Caminho explicito: absoluto, `./`, `../` ou `~/`. Nome sozinho e sempre nome (achado 2 do CHECK). */
237
+ const ehCaminho = (t) => path.isAbsolute(t) || /^(?:\.{1,2}|~)(?:\/|$)/.test(t);
238
+ /** As forjas publicas que o host pode pedir sem registro, cada uma com o tipo dela. */
239
+ const FORJAS_PUBLICAS = Object.freeze({ 'github.com': 'github', 'gitlab.com': 'gitlab' });
240
+ /**
241
+ * D-G4 (fatia 2): no host, o `--projeto` do `network` e o nome de um projeto (RM-052) ou a forja
242
+ * (`github:dono/repo`, `gitlab:grupo/repo`). Caminho apontaria o nucleo para qualquer pasta (D4 da
243
+ * RM-052) e URL pode levar credencial: nenhum dos dois entra, e o texto pedido nao volta na recusa.
244
+ * A forja tambem so vale num host conhecido (GO-FIX 1 do CHECK): `github.com`, `gitlab.com` ou o
245
+ * host de um projeto do registro. Com host livre, o texto do modelo levaria o `gh`/`glab` a qualquer
246
+ * servidor (`gitlab:169.254.169.254/a/b`), e um `GITLAB_TOKEN` do ambiente iria junto.
247
+ */
248
+ function exigirPedidoDoHost(pedido, conhecidos) {
249
+ const t = pedido.trim();
250
+ if (projeto_alvo_1.PADRAO_DO_NOME_DE_PROJETO.test(t))
251
+ return;
252
+ const candidatos = conhecidos.map(rotuloDoProjeto);
253
+ const forja = /^(?:github|gitlab):/i.test(t) ? (0, forja_1.forjaDoArgumento)(t) : null;
254
+ if (!forja) {
255
+ throw new ErroDoPedidoDeProjeto('projeto.desconhecido', 'no host, --projeto é o nome de um projeto registrado ou a forja (github:dono/repo, gitlab:grupo/repo); caminho e URL não são aceitos', candidatos, 'peça pelo nome de `ork projetos` (ex.: orkastery) ou por github:dono/repo');
256
+ }
257
+ const publica = FORJAS_PUBLICAS[forja.host] === forja.tipo;
258
+ if (publica || conhecidos.some((p) => p.forja && p.forja.host === forja.host && p.forja.tipo === forja.tipo))
259
+ return;
260
+ throw new ErroDoPedidoDeProjeto('projeto.desconhecido', 'no host, a forja pedida precisa ser github.com, gitlab.com ou a de um projeto registrado nesta máquina', candidatos, 'peça por github:dono/repo ou gitlab:grupo/repo; grupo com ponto no GitLab vai com o host: gitlab:gitlab.com/grupo/repo');
261
+ }
262
+ /**
263
+ * D9: o `--projeto`. `github:`/`gitlab:`/URL, caminho explicito com manifesto, ou o nome (ou
264
+ * `dono/repo`) de um projeto conhecido. Uma pasta do cwd com o mesmo nome nao toma o pedido: o nome
265
+ * so vira caminho escrito como caminho. Ambiguo ou desconhecido recusa com os candidatos.
266
+ */
267
+ function resolverProjeto(pedido, conhecidos, cwd = process.cwd()) {
268
+ const candidatos = conhecidos.map(rotuloDoProjeto);
269
+ const forja = (0, forja_1.forjaDoArgumento)(pedido);
270
+ if (forja) {
271
+ const conhecido = conhecidos.find((p) => (0, forja_1.mesmaForja)(p.forja, forja));
272
+ return conhecido ? { ...conhecido, origem: 'argumento' }
273
+ : { nome: forja.repo.split('/').pop(), forja, raiz: null, base: null, remoto: 'origin', origem: 'argumento' };
274
+ }
275
+ const texto = pedido.trim();
276
+ if (ehCaminho(texto)) {
277
+ const caminho = path.resolve(cwd, texto.replace(/^~(?=$|\/)/, os.homedir()));
278
+ if (!fs.existsSync(caminho) || !fs.statSync(caminho).isDirectory()) {
279
+ throw new ErroDoPedidoDeProjeto('projeto.desconhecido', `${caminho} não existe nesta máquina`, candidatos, 'confira o caminho do clone, ou peça pela forja: --projeto github:dono/repo');
280
+ }
281
+ const c = (0, manifest_1.carregarManifesto)(caminho);
282
+ if (!c) {
283
+ throw new ErroDoPedidoDeProjeto('projeto.sem-manifesto', `${caminho} não tem ${manifest_1.NOME_MANIFESTO}`, candidatos, 'rode `ork init` nesse clone, ou peça pela forja: --projeto github:dono/repo');
284
+ }
285
+ // O caminho pedido e a raiz do projeto, nunca uma pasta dentro de outro (rodada 2 do CHECK).
286
+ const real = (x) => { try {
287
+ return fs.realpathSync(x);
288
+ }
289
+ catch {
290
+ return path.resolve(x);
291
+ } };
292
+ if (real(c.raiz) !== real(caminho)) {
293
+ throw new ErroDoPedidoDeProjeto('projeto.sem-manifesto', `${caminho} não é a raiz de um projeto: o manifesto mais próximo está em ${c.raiz}`, candidatos, `se é esse o projeto, peça --projeto ${c.raiz}`);
294
+ }
295
+ return projetoDoClone(c, 'argumento');
296
+ }
297
+ const alvo = texto.toLowerCase();
298
+ const achados = conhecidos.filter((p) => p.nome.toLowerCase() === alvo ||
299
+ (p.forja && (p.forja.repo.toLowerCase() === alvo || p.forja.repo.split('/').pop().toLowerCase() === alvo)));
300
+ if (achados.length === 1)
301
+ return { ...achados[0], origem: 'argumento' };
302
+ if (achados.length > 1) {
303
+ throw new ErroDoPedidoDeProjeto('projeto.ambiguo', `"${pedido}" casa ${achados.length} projetos conhecidos`, achados.map(rotuloDoProjeto), 'peça pelo caminho do clone ou por github:dono/repo');
304
+ }
305
+ throw new ErroDoPedidoDeProjeto('projeto.desconhecido', `"${pedido}" não é um projeto conhecido nesta máquina`, candidatos, 'peça por github:dono/repo, por gitlab:grupo/repo ou pelo caminho do clone escrito como caminho (./pasta ou absoluto)');
306
+ }
307
+ // ---------------------------------------------------------------------------
308
+ // Leitura: fatos comuns ao clone e a forja.
309
+ // ---------------------------------------------------------------------------
310
+ /** O inicio do dia do dono, com uma hora de folga para a virada do horario de verao. */
311
+ function inicioDoDia(quando, fuso) {
312
+ const p = (0, horario_1.partesLocais)(quando, fuso);
313
+ const decorrido = ((Number(p.hora) * 60 + Number(p.minuto)) * 60 + Number(p.segundo)) * 1000;
314
+ return new Date(Date.parse(quando) - decorrido - 60 * 60 * 1000).toISOString();
315
+ }
316
+ /** D8: as threads entregues hoje, pelo merge `ship(<thread>)` na base com a data de hoje no fuso do dono. */
317
+ function entreguesHoje(commits, quando, fuso) {
318
+ const hoje = (0, horario_1.dataLocal)(quando, fuso), ids = new Set();
319
+ for (const c of commits) {
320
+ const m = /^ship\(([A-Za-z0-9._-]+)\)/.exec(c.assunto);
321
+ if (m && Number.isFinite(Date.parse(c.data)) && (0, horario_1.dataLocal)(c.data, fuso) === hoje)
322
+ ids.add(m[1]);
323
+ }
324
+ return ids;
325
+ }
326
+ function docsDosArquivos(arquivos, lacunas) {
327
+ const docs = [];
328
+ for (const a of arquivos) {
329
+ if (!(0, docs_1.ehPaginaDeDocs)(path.posix.basename(a.caminho)))
330
+ continue;
331
+ if (a.texto === null) {
332
+ lacunas.push(lacuna('roadmap.pagina-invalida', 'roadmap', a.caminho, 'página binária ou grande demais para ler: ficou de fora', 'confira o arquivo na base do projeto'));
333
+ continue;
334
+ }
335
+ const lido = (0, docs_1.documentoDeTexto)(a.caminho, a.texto);
336
+ if ('doc' in lido)
337
+ docs.push(lido.doc);
338
+ else
339
+ lacunas.push(lacuna('roadmap.pagina-invalida', 'roadmap', a.caminho, `${lido.achado.mensagem}: ficou de fora`, 'rode `ork docs verificar` no projeto'));
340
+ }
341
+ return docs;
342
+ }
343
+ function jsonsDosArquivos(arquivos, valido, invalido, lacunas) {
344
+ const saida = [];
345
+ for (const a of arquivos) {
346
+ if (!a.caminho.endsWith('.json'))
347
+ continue;
348
+ let v;
349
+ try {
350
+ v = a.texto === null ? undefined : JSON.parse(a.texto);
351
+ }
352
+ catch {
353
+ v = undefined;
354
+ }
355
+ if (valido(v))
356
+ saida.push(v);
357
+ else
358
+ lacunas.push(invalido(a.caminho));
359
+ }
360
+ return saida;
361
+ }
362
+ const reservasDosArquivos = (arquivos, lacunas) => jsonsDosArquivos(arquivos, roadmap_reservas_1.reservaValida, (c) => lacuna('reserva.invalida', 'reservas', c, 'reserva ilegível ou de outro contrato: ficou de fora', 'confira a branch ork/roadmap-reservas'), lacunas)
363
+ .sort((a, b) => a.item.localeCompare(b.item));
364
+ const retratosDosArquivos = (arquivos, lacunas) => jsonsDosArquivos(arquivos, retratoLegivel, (c) => lacuna('retrato.invalido', 'fabrica', c, 'retrato ilegível ou de outro contrato: a máquina ficou de fora', 'a máquina republica com `ork fabrica publicar --forcar`'), lacunas)
365
+ .sort((a, b) => a.maquina.localeCompare(b.maquina));
366
+ /** Um campo de texto do manifesto lido da base pela forja (`secao.chave`), ou null. */
367
+ function campoDoManifesto(texto, secao, chave) {
368
+ if (!texto)
369
+ return null;
370
+ try {
371
+ const dados = (0, yaml_1.lerYaml)(texto);
372
+ const mapa = dados && typeof dados === 'object' && !Array.isArray(dados) ? dados[secao] : undefined;
373
+ const valor = mapa && typeof mapa === 'object' && !Array.isArray(mapa) ? mapa[chave] : undefined;
374
+ return typeof valor === 'string' && valor.trim() ? valor.trim() : null;
375
+ }
376
+ catch {
377
+ return null;
378
+ }
379
+ }
380
+ /** O nome do projeto no manifesto lido, para o titulo dizer o nome certo. */
381
+ const nomeDoManifesto = (texto) => campoDoManifesto(texto, 'project', 'name');
382
+ /** A branch base do manifesto lido (achado 7 do CHECK), so com formato de nome de branch. */
383
+ function baseDoManifesto(texto) {
384
+ const base = campoDoManifesto(texto, 'worktree', 'base_branch');
385
+ return baseValida(base) ? base : null;
386
+ }
387
+ const pontuar = (texto) => {
388
+ const limpo = String(texto).trim();
389
+ return /[.?!]$/.test(limpo) ? limpo : `${limpo}.`;
390
+ };
391
+ /**
392
+ * O retrato lido de fora so vale inteiro: alem do contrato, os tipos que o panorama usa e uma batida
393
+ * legivel. Pergunta que nao e texto derrubava o comando, e `publicadoEm` ilegivel nunca virava
394
+ * "sem batida" (achado 6 do CHECK).
395
+ */
396
+ function retratoLegivel(v) {
397
+ if (!(0, fabrica_estado_1.estadoValido)(v))
398
+ return false;
399
+ const textoOuNulo = (x) => x === null || x === undefined || typeof x === 'string';
400
+ return typeof v.por === 'string' && Number.isFinite(Date.parse(v.publicadoEm)) && v.threads.every((t) => typeof t.status === 'string' && typeof t.modo === 'string' && typeof t.esperaVoce === 'boolean' &&
401
+ textoOuNulo(t.roadmap) && textoOuNulo(t.pergunta) && textoOuNulo(t.entregue) && textoOuNulo(t.paradaDesde) &&
402
+ textoOuNulo(t.runtime) && textoOuNulo(t.modelo) && textoOuNulo(t.esforco));
403
+ }
404
+ function fatosDoRetrato(m, entregues, itemDaReserva) {
405
+ return m.threads.map((t) => ({
406
+ id: t.id, roadmap: t.roadmap ?? itemDaReserva.get(t.id) ?? null, aberta: t.status !== 'fechada', fase: t.fase,
407
+ entregueHoje: () => entregues.has(t.id),
408
+ espera: () => t.esperaVoce ? { thread: t.id, pergunta: pontuar(t.pergunta ?? 'o veredito da pausa'), maquina: m.maquina } : undefined,
409
+ maquina: m.maquina,
410
+ }));
411
+ }
412
+ function fatosDaMaquinaLocal(raiz, quando, fuso, maquina, entregues, itemDaReserva) {
413
+ return (0, roadmap_status_1.fatosLocais)(raiz, quando, fuso).map((f) => ({
414
+ ...f, roadmap: f.roadmap ?? itemDaReserva.get(f.id) ?? null, maquina,
415
+ entregueHoje: () => entregues.has(f.id) || f.entregueHoje(),
416
+ espera: () => { const e = f.espera(); return e ? { ...e, maquina } : undefined; },
417
+ }));
418
+ }
419
+ function maquinasDoPanorama(retratos, local, ctx, lacunas) {
420
+ const agora = Date.parse(ctx.quando);
421
+ const saida = [];
422
+ for (const m of retratos) {
423
+ if (local && m.maquina === local.maquina)
424
+ continue;
425
+ const idadeMs = Math.max(0, agora - Date.parse(m.publicadoEm));
426
+ const semBatida = idadeMs > exports.LIMIAR_SEM_BATIDA_MS;
427
+ if (semBatida) {
428
+ lacunas.push(lacuna('maquina.sem-batida', 'fabrica', m.maquina, `${m.maquina} sem retrato novo há ${(0, horario_1.duracaoCurta)(Math.floor(idadeMs / 60000))} (último em ${(0, horario_1.formatarDataHora)(m.publicadoEm, { fuso: ctx.fuso })}): ` +
429
+ 'as threads dela podem ter andado', `confira a ${m.maquina}: ela publica ao criar thread, despachar fase, entregar e a cada batida do pulse`));
430
+ }
431
+ saida.push({ maquina: m.maquina, por: m.por, publicadoEm: m.publicadoEm, idadeMin: Math.floor(idadeMs / 60000), semBatida,
432
+ estaMaquina: m.maquina === ctx.maquina, origem: 'retrato', versaoOrk: m.versaoOrk ?? null,
433
+ ativas: m.threads.filter((t) => !t.entregue), entreguesSemMaster: m.threads.filter((t) => t.entregue).length });
434
+ }
435
+ if (local) {
436
+ saida.push({ maquina: local.maquina, por: local.por, publicadoEm: local.publicadoEm, idadeMin: 0, semBatida: false, estaMaquina: true,
437
+ origem: 'estado-local', versaoOrk: local.versaoOrk, ativas: local.threads.filter((t) => !t.entregue),
438
+ entreguesSemMaster: local.threads.filter((t) => t.entregue).length });
439
+ }
440
+ return saida.sort((a, b) => a.maquina.localeCompare(b.maquina));
441
+ }
442
+ function montarProjeto(p, d, ctx, fontes, lacunas) {
443
+ const itemDaReserva = new Map((d.reservas ?? []).filter((r) => r.thread).map((r) => [r.thread, r.item]));
444
+ const entregues = entreguesHoje(d.commits, ctx.quando, ctx.fuso);
445
+ const fatos = [], ids = new Set();
446
+ const somar = (f) => { if (!ids.has(f.id)) {
447
+ ids.add(f.id);
448
+ fatos.push(f);
449
+ } };
450
+ if (d.local)
451
+ fatosDaMaquinaLocal(d.local.raiz, ctx.quando, ctx.fuso, d.local.retrato.maquina, entregues, itemDaReserva).forEach(somar);
452
+ for (const m of d.retratos ?? [])
453
+ if (!d.local || m.maquina !== d.local.retrato.maquina)
454
+ fatosDoRetrato(m, entregues, itemDaReserva).forEach(somar);
455
+ // Entregue hoje e ja fechada: nao esta em retrato nenhum, mas o merge na base e o fato do dia.
456
+ for (const id of entregues)
457
+ somar({ id, roadmap: itemDaReserva.get(id) ?? null, aberta: false, fase: 'SHIP', entregueHoje: () => true,
458
+ espera: () => undefined });
459
+ const maquinas = d.retratos || d.local ? maquinasDoPanorama(d.retratos ?? [], d.local?.retrato ?? null, ctx, lacunas) : null;
460
+ return {
461
+ projeto: { nome: d.nome, forja: p.forja ? (0, forja_1.rotuloDaForja)(p.forja) : null, clone: p.raiz ? raizParaExibir(p.raiz) : null,
462
+ base: p.base, origem: p.origem, fuso: ctx.fuso },
463
+ roadmap: d.docs ? (0, roadmap_status_1.montarStatusDeFatos)(d.docs, fatos, { quando: ctx.quando, projeto: d.nome }) : null,
464
+ reservas: d.reservas, maquinas, fontes, lacunas,
465
+ };
466
+ }
467
+ // ---------------------------------------------------------------------------
468
+ // Leitura pelo clone.
469
+ // ---------------------------------------------------------------------------
470
+ /**
471
+ * Os blobs de um diretorio da ponta, lidos de uma vez: `ls-tree -z` (o caminho vem cru, e nome
472
+ * nao-ASCII nao ganha aspas, achado 3 do CHECK) e um `cat-file --batch` para todos (achado 9).
473
+ * null: o diretorio nao existe na ponta.
474
+ */
475
+ function arquivosDaPonta(raiz, ponta, dir) {
476
+ const ls = (0, node_child_process_1.spawnSync)('git', ['ls-tree', '-z', ponta, '--', `${dir}/`], { cwd: raiz, encoding: 'utf8', timeout: 60000,
477
+ maxBuffer: 16 * 1024 * 1024 });
478
+ if (ls.status !== 0 || !ls.stdout)
479
+ return null;
480
+ const blobs = ls.stdout.split('\0').map((l) => /^\d+ blob ([0-9a-f]+)\t([\s\S]+)$/.exec(l))
481
+ .filter((m) => !!m).map((m) => ({ sha: m[1], caminho: m[2] }));
482
+ if (!blobs.length)
483
+ return [];
484
+ const cat = (0, node_child_process_1.spawnSync)('git', ['cat-file', '--batch'], { cwd: raiz, input: blobs.map((b) => b.sha).join('\n') + '\n', timeout: 60000,
485
+ maxBuffer: 64 * 1024 * 1024 });
486
+ const textos = new Map();
487
+ if (cat.status === 0 && Buffer.isBuffer(cat.stdout)) {
488
+ const saida = cat.stdout;
489
+ for (let pos = 0; pos < saida.length;) {
490
+ const fim = saida.indexOf(0x0a, pos);
491
+ if (fim < 0)
492
+ break;
493
+ const cabeca = /^([0-9a-f]+) blob (\d+)$/.exec(saida.subarray(pos, fim).toString('utf8'));
494
+ if (!cabeca) {
495
+ pos = fim + 1;
496
+ continue;
497
+ }
498
+ const tamanho = Number(cabeca[2]);
499
+ textos.set(cabeca[1], saida.subarray(fim + 1, fim + 1 + tamanho).toString('utf8'));
500
+ pos = fim + 1 + tamanho + 1;
501
+ }
502
+ }
503
+ return blobs.map((b) => ({ caminho: b.caminho, texto: textos.get(b.sha) ?? null }));
504
+ }
505
+ function commitsDoClone(raiz, ponta, desde) {
506
+ const r = (0, branch_de_estado_1.git)(raiz, ['log', ponta, `--since=${desde}`, '--format=%H%x09%cI%x09%s']);
507
+ if (!r.ok)
508
+ return [];
509
+ return r.stdout.split('\n').filter(Boolean).map((l) => {
510
+ const [commit, data, ...assunto] = l.split('\t');
511
+ return { commit, data, assunto: assunto.join('\t') };
512
+ });
513
+ }
514
+ /**
515
+ * O retrato desta maquina quando o completo nao se monta: um thread.json que nao abre derrubava o
516
+ * `retratoDaMaquina` inteiro, e todas as threads dela sumiam da rede. Aqui entram as legiveis, com a
517
+ * espera do dono do RM-048, e a conta das que ficaram de fora (achado 1 da rodada 2 do CHECK).
518
+ */
519
+ function retratoTolerante(c, ctx, remoto) {
520
+ const raiz = c.raiz;
521
+ const entregas = (0, fabrica_estado_1.entregasNaBase)(raiz, c.manifesto.worktree.base_branch, remoto);
522
+ const threads = [];
523
+ let ilegiveis = 0;
524
+ for (const id of (0, thread_1.listarIds)(raiz)) {
525
+ try {
526
+ const t = (0, thread_1.lerThread)(raiz, id);
527
+ if (t.status === 'fechada')
528
+ continue;
529
+ const entregue = entregas.get(t.id) ?? null;
530
+ let espera;
531
+ try {
532
+ espera = entregue ? undefined : (0, roadmap_status_1.esperaDoDono)(raiz, t, ctx.quando);
533
+ }
534
+ catch {
535
+ espera = undefined;
536
+ }
537
+ threads.push({ id: t.id, nome: t.nome, modo: (0, modos_1.tagDoModo)(t.modo), fase: t.faseAtual, status: t.status, roadmap: t.roadmap ?? null,
538
+ branch: t.base?.branch ?? null, atualizadaEm: t.atualizadaEm ?? null, entregue, esperaVoce: !!espera,
539
+ pergunta: espera?.pergunta ?? null, paradaDesde: null, ...(0, fabrica_estado_1.despachoDaThread)((0, thread_1.dirThread)(raiz, t.id)) });
540
+ }
541
+ catch {
542
+ ilegiveis++;
543
+ }
544
+ }
545
+ const eu = (0, roadmap_reservas_1.quemSouEu)(raiz, { maquina: ctx.maquina });
546
+ return { ilegiveis, retrato: { contrato: fabrica_estado_1.CONTRATO_MAQUINA, maquina: eu.maquina, por: eu.por, projeto: c.manifesto.project.name,
547
+ versaoOrk: versao_1.VERSAO_DO_ORK, publicadoEm: ctx.quando, threads: threads.sort((a, b) => a.id.localeCompare(b.id)) } };
548
+ }
549
+ /**
550
+ * A espera do dono das threads desta maquina pela mesma definicao do roadmap (RM-048), no retrato
551
+ * completo e no tolerante: o monitor do retrato publicado usa outra regra, e a secao de maquinas
552
+ * contradizia o "O que precisa de voce" do mesmo panorama (rodada 3 do CHECK).
553
+ */
554
+ function comEsperaDoRoadmap(r, raiz, ctx) {
555
+ const fatos = new Map((0, roadmap_status_1.fatosLocais)(raiz, ctx.quando, ctx.fuso).map((f) => [f.id, f]));
556
+ return { ...r, threads: r.threads.map((t) => {
557
+ if (t.entregue)
558
+ return t;
559
+ let e;
560
+ try {
561
+ e = fatos.get(t.id)?.espera();
562
+ }
563
+ catch {
564
+ return t;
565
+ }
566
+ return { ...t, esperaVoce: !!e, pergunta: e ? e.pergunta : null, paradaDesde: e ? t.paradaDesde : null };
567
+ }) };
568
+ }
569
+ function lerProjetoDoClone(p, geral) {
570
+ const raiz = p.raiz, remoto = p.remoto, base = p.base ?? 'main';
571
+ const fontes = [], lacunas = [];
572
+ const carregado = (0, manifest_1.carregarManifesto)(raiz);
573
+ // O fuso do dono deste projeto vale para o dia e os horarios dele (achado 5 do CHECK).
574
+ const ctx = { ...geral, fuso: carregado?.manifesto.owner?.timezone ?? geral.fuso };
575
+ if (!REMOTO.test(remoto)) {
576
+ lacunas.push(lacuna('projeto.remoto-invalido', 'projeto', raizParaExibir(raiz), `fabrica.remoto do manifesto (${JSON.stringify(remoto).slice(0, 60)}) não é nome de remoto do git: nada foi lido pelo git`, 'corrija fabrica.remoto no orkastery.yaml (padrão: origin)'));
577
+ return montarProjeto(p, { nome: p.nome, docs: null, reservas: null, retratos: null, local: null, commits: [] }, ctx, fontes, lacunas);
578
+ }
579
+ if (!baseValida(p.base ?? 'main') || (carregado && !baseValida(carregado.manifesto.worktree.base_branch))) {
580
+ lacunas.push(lacuna('projeto.base-invalida', 'projeto', raizParaExibir(raiz), `worktree.base_branch do manifesto (${JSON.stringify(carregado?.manifesto.worktree.base_branch ?? p.base).slice(0, 60)}) ` +
581
+ 'não é nome de branch: nada foi lido pelo git', 'corrija worktree.base_branch no orkastery.yaml (padrão: main)'));
582
+ return montarProjeto(p, { nome: p.nome, docs: null, reservas: null, retratos: null, local: null, commits: [] }, ctx, fontes, lacunas);
583
+ }
584
+ // Uma tentativa de rede por projeto: remoto que nao respondeu nao segura as outras partes (achado 9 do CHECK).
585
+ let semRede = ctx.semRemoto ? 'opcao' : null;
586
+ const ler = (nome, prefixo, prazo) => {
587
+ if (semRede)
588
+ return { ponta: (0, branch_de_estado_1.pontaLocal)(raiz, remoto, nome), atualizado: false, tentou: false };
589
+ const r = (0, branch_de_estado_1.buscarBranch)(raiz, remoto, nome, prefixo, prazo);
590
+ if (!r.atualizado)
591
+ semRede = 'falhou';
592
+ return { ...r, tentou: true };
593
+ };
594
+ const porque = (tentou) => semRede === 'opcao' ? ' (--sem-remoto)'
595
+ : tentou ? `: a leitura de ${remoto} falhou` : `: a leitura de ${remoto} falhou antes, e não houve nova tentativa`;
596
+ const correcao = () => semRede === 'opcao' ? 'rode sem --sem-remoto para ler o remoto' : `confira a rede e o acesso a ${remoto} (git fetch)`;
597
+ const dataDe = (sha) => (0, branch_de_estado_1.git)(raiz, ['show', '-s', '--format=%cI', sha]).stdout.trim() || null;
598
+ /** Uma branch lida do clone: a fonte citada, e a lacuna quando a leitura nao e nova ou nao houve. */
599
+ const branch = (parte, nome, prefixo, prazo) => {
600
+ const r = ler(nome, prefixo, prazo), onde = `${remoto}/${nome}`;
601
+ if (r.ponta) {
602
+ const dataDoCommit = dataDe(r.ponta);
603
+ fontes.push({ parte, origem: 'clone', onde, ref: nome, commit: r.ponta, dataDoCommit, lidoEm: ctx.quando, atualizado: r.atualizado, existe: true });
604
+ if (!r.atualizado) {
605
+ lacunas.push(lacuna(`${parte}.sem-leitura`, parte, onde, `sem leitura nova de ${onde}${porque(r.tentou)}; ` +
606
+ `vale a última cópia desta máquina, commit ${r.ponta.slice(0, 7)} de ${(0, horario_1.formatarDataHora)(dataDoCommit, { fuso: ctx.fuso })}`, correcao()));
607
+ }
608
+ return r.ponta;
609
+ }
610
+ if (r.atualizado) {
611
+ fontes.push({ parte, origem: 'clone', onde, ref: nome, commit: null, dataDoCommit: null, lidoEm: ctx.quando, atualizado: true, existe: false });
612
+ if (parte === 'roadmap') {
613
+ lacunas.push(lacuna('roadmap.sem-base', 'roadmap', onde, `a branch base ${nome} não existe em ${remoto}`, 'confira worktree.base_branch no orkastery.yaml'));
614
+ }
615
+ else if (parte === 'reservas') {
616
+ lacunas.push(lacuna('reservas.sem-branch', 'reservas', onde, `nenhuma reserva feita ainda: a branch ${nome} não existe em ${remoto}`, 'a primeira reserva a cria: `ork roadmap pegar RM-NNN`'));
617
+ }
618
+ else {
619
+ lacunas.push(lacuna('fabrica.sem-branch', 'fabrica', onde, `nenhuma máquina publicou ainda: a branch ${nome} não existe em ${remoto}`, 'cada máquina entra com `ork fabrica entrar`'));
620
+ }
621
+ return null;
622
+ }
623
+ lacunas.push(lacuna(`${parte}.sem-leitura`, parte, onde, `sem leitura de ${onde}${porque(r.tentou)}; esta máquina não tem cópia dela`, correcao()));
624
+ return null;
625
+ };
626
+ let docs = null, commits = [];
627
+ const pontaBase = branch('roadmap', base, 'rede', 30000);
628
+ if (pontaBase) {
629
+ docs = docsDosArquivos(arquivosDaPonta(raiz, pontaBase, docs_1.DIR_ROADMAP) ?? [], lacunas);
630
+ if (!docs.some((d) => d.tipo === 'roadmap')) {
631
+ lacunas.push(lacuna('roadmap.sem-itens', 'roadmap', `${remoto}/${base}`, `nenhuma página de item em ${docs_1.DIR_ROADMAP} na base ${base}`, 'o projeto ainda não tem roadmap como código: `ork docs init` e o modelo docs/roadmap/_modelo-item.md'));
632
+ }
633
+ commits = commitsDoClone(raiz, pontaBase, inicioDoDia(ctx.quando, ctx.fuso));
634
+ }
635
+ const pontaReservas = branch('reservas', roadmap_reservas_1.BRANCH_DE_RESERVAS, 'roadmap', 15000);
636
+ const reservas = pontaReservas ? reservasDosArquivos(arquivosDaPonta(raiz, pontaReservas, roadmap_reservas_1.DIR_DE_RESERVAS) ?? [], lacunas)
637
+ : fontes.some((f) => f.parte === 'reservas' && !f.existe) ? [] : null;
638
+ const pontaFabrica = branch('fabrica', fabrica_estado_1.BRANCH_DA_FABRICA, 'fabrica', 15000);
639
+ const retratos = pontaFabrica ? retratosDosArquivos(arquivosDaPonta(raiz, pontaFabrica, fabrica_estado_1.DIR_DA_FABRICA) ?? [], lacunas)
640
+ : fontes.some((f) => f.parte === 'fabrica' && !f.existe) ? [] : null;
641
+ // D6: esta maquina pelo estado local, lido agora. Estado que nao se le (thread.json corrompido) vira
642
+ // lacuna, e vale o retrato publicado dela, quando ha (achado 1 do CHECK).
643
+ let local = null;
644
+ if (carregado) {
645
+ try {
646
+ local = { raiz, retrato: (0, fabrica_estado_1.retratoDaMaquina)(carregado, { agora: ctx.quando, maquina: ctx.maquina, remoto }) };
647
+ fontes.push({ parte: 'estado-local', origem: 'estado-local', onde: path.join(raizParaExibir(raiz), '.orkastery'), ref: null, commit: null,
648
+ dataDoCommit: null, lidoEm: ctx.quando, atualizado: true, existe: true });
649
+ }
650
+ catch (e) {
651
+ const motivo = (0, forja_1.detalheSeguro)(e?.message ?? String(e)), onde = path.join(raizParaExibir(raiz), '.orkastery');
652
+ const correcao = 'confira as threads desta máquina com `ork thread list` e o thread.json que não abre';
653
+ try {
654
+ const t = retratoTolerante(carregado, ctx, remoto);
655
+ local = { raiz, retrato: t.retrato };
656
+ fontes.push({ parte: 'estado-local', origem: 'estado-local', onde, ref: null, commit: null, dataDoCommit: null, lidoEm: ctx.quando,
657
+ atualizado: true, existe: true });
658
+ lacunas.push(lacuna('estado-local.sem-leitura', 'estado-local', onde, `o retrato local completo não se montou (${motivo}): ` +
659
+ `${ctx.maquina} entra com as threads legíveis, e ${t.ilegiveis} ilegível(is) ficou(aram) de fora`, correcao));
660
+ }
661
+ catch {
662
+ lacunas.push(lacuna('estado-local.sem-leitura', 'estado-local', onde, `o estado local de ${ctx.maquina} não se leu (${motivo}): vale o retrato publicado dela, quando há`, correcao));
663
+ }
664
+ }
665
+ }
666
+ if (local)
667
+ local = { raiz, retrato: comEsperaDoRoadmap(local.retrato, raiz, ctx) };
668
+ return montarProjeto(p, { nome: p.nome, docs, reservas, retratos, local, commits }, ctx, fontes, lacunas);
669
+ }
670
+ // ---------------------------------------------------------------------------
671
+ // Leitura pela forja, sem clone.
672
+ // ---------------------------------------------------------------------------
673
+ const CORRECAO_DA_FORJA = {
674
+ 'forja.ausente': (cli) => `instale ${cli} e faça login (${cli} auth login); gateway e cron têm PATH curto`,
675
+ 'forja.sem-login': (cli) => `rode ${cli} auth login nesta máquina; o ork nunca lê nem copia token`,
676
+ 'forja.nao-encontrado': () => 'confira o nome (github:dono/repo) e se o login da forja enxerga o repositório',
677
+ 'forja.tempo-esgotado': () => 'tente de novo: a forja não respondeu a tempo',
678
+ 'forja.inacessivel': () => 'confira a rede e o status da forja',
679
+ 'forja.resposta-invalida': () => 'a forja respondeu fora do formato esperado: registre o caso no RM-054',
680
+ };
681
+ function lerProjetoDaForja(p, geral) {
682
+ const forja = p.forja, rotulo = (0, forja_1.rotuloDaForja)(forja), cli = forja.tipo === 'github' ? 'gh' : 'glab';
683
+ const fontes = [];
684
+ const lacunas = [lacuna('projeto.sem-clone', 'projeto', rotulo, `sem clone de ${p.nome} nesta máquina: lido da forja, só consulta`, 'para somar as threads desta máquina, trabalhe num clone do projeto')];
685
+ const semLeitura = (d = {}) => montarProjeto(p, { nome: p.nome, docs: null, reservas: null, retratos: null,
686
+ local: null, commits: [], ...d }, geral, fontes, lacunas);
687
+ if (geral.semRemoto) {
688
+ lacunas.push(lacuna('forja.nao-consultada', 'forja', rotulo, '--sem-remoto: a forja não foi consultada e não há clone nesta máquina', 'rode sem --sem-remoto'));
689
+ return semLeitura();
690
+ }
691
+ const desde = inicioDoDia(geral.quando, geral.fuso);
692
+ const ler = (base) => (0, forja_1.lerDaForja)(forja, { base, desde, dirRoadmap: docs_1.DIR_ROADMAP, manifesto: manifest_1.NOME_MANIFESTO,
693
+ reservas: { branch: roadmap_reservas_1.BRANCH_DE_RESERVAS, dir: roadmap_reservas_1.DIR_DE_RESERVAS }, fabrica: { branch: fabrica_estado_1.BRANCH_DA_FABRICA, dir: fabrica_estado_1.DIR_DA_FABRICA },
694
+ lidoEm: geral.quando }, geral.executor);
695
+ let baseUsada = p.base;
696
+ let r = ler(p.base);
697
+ // Sem clone, a base e a do manifesto lido: se ele aponta outra branch, a leitura e refeita por ela.
698
+ const baseDoProjeto = r.ok && !p.base ? baseDoManifesto(r.leitura.base?.manifesto) : null;
699
+ if (r.ok && baseDoProjeto && r.leitura.base && baseDoProjeto !== r.leitura.base.ref) {
700
+ baseUsada = baseDoProjeto;
701
+ r = ler(baseDoProjeto);
702
+ }
703
+ if (!r.ok) {
704
+ lacunas.push(lacuna(r.erro.codigo, 'forja', rotulo, r.erro.detalhe, CORRECAO_DA_FORJA[r.erro.codigo](cli)));
705
+ return semLeitura();
706
+ }
707
+ const l = r.leitura;
708
+ const nome = nomeDoManifesto(l.base?.manifesto) ?? p.nome;
709
+ // A base que valeu vai para o projeto e para o texto, inclusive quando ela nao existe na forja.
710
+ const projeto = { ...p, base: baseUsada ?? l.base?.ref ?? null };
711
+ // O fuso do dono deste projeto vale para o dia e os horarios dele (achado 5 do CHECK). Ele so se
712
+ // conhece depois de ler o manifesto: se o dia dele comecou antes da janela pedida, os commits sao
713
+ // relidos desde o comeco do dia dele (achado 5 da rodada 2).
714
+ const ctx = { ...geral, fuso: (0, horario_1.normalizarFuso)(campoDoManifesto(l.base?.manifesto, 'owner', 'timezone')) ?? geral.fuso };
715
+ let commits = l.base?.commits ?? [], commitsParciais = l.base?.commitsParciais ?? false;
716
+ const inicioNoProjeto = inicioDoDia(ctx.quando, ctx.fuso);
717
+ if (l.base && Date.parse(inicioNoProjeto) < Date.parse(desde)) {
718
+ const h = (0, forja_1.lerCommitsDaForja)(forja, { commit: l.base.commit, desde: inicioNoProjeto }, geral.executor);
719
+ if (h.ok) {
720
+ commits = h.commits;
721
+ commitsParciais = h.parciais;
722
+ }
723
+ else {
724
+ lacunas.push(lacuna('roadmap.entregas-parciais', 'roadmap', `${rotulo}@${l.base.ref}`, `os commits desde o começo do dia no fuso do projeto (${ctx.fuso}) não vieram (${r.leitura.forja.host}: ${h.erro.codigo}): ` +
725
+ '"Entregue hoje" pode ter ficado incompleto', 'confira as entregas do dia no clone'));
726
+ }
727
+ }
728
+ for (const [parte, ponta] of [['roadmap', l.base], ['reservas', l.reservas], ['fabrica', l.fabrica]]) {
729
+ if (ponta?.parcial) {
730
+ lacunas.push(lacuna('forja.leitura-parcial', parte, `${rotulo}@${ponta.ref}`, 'a forja cortou a listagem em 100 arquivos: o resto não foi lido', 'leia pelo clone do projeto, que não tem esse corte'));
731
+ }
732
+ }
733
+ const fonte = (parte, ponta, ref) => {
734
+ fontes.push({ parte, origem: 'forja', onde: `${rotulo}@${ponta?.ref ?? ref}`, ref: ponta?.ref ?? ref, commit: ponta?.commit ?? null,
735
+ dataDoCommit: ponta?.dataDoCommit ?? null, lidoEm: l.lidoEm, atualizado: true, existe: !!ponta });
736
+ };
737
+ let docs = null;
738
+ fonte('roadmap', l.base, baseUsada ?? 'branch padrão');
739
+ if (l.base) {
740
+ docs = docsDosArquivos(l.base.arquivos ?? [], lacunas);
741
+ if (!docs.some((d) => d.tipo === 'roadmap')) {
742
+ lacunas.push(lacuna('roadmap.sem-itens', 'roadmap', `${rotulo}@${l.base.ref}`, `nenhuma página de item em ${docs_1.DIR_ROADMAP} na base ${l.base.ref}`, 'o projeto ainda não tem roadmap como código: `ork docs init` e o modelo docs/roadmap/_modelo-item.md'));
743
+ }
744
+ if (commitsParciais) {
745
+ lacunas.push(lacuna('roadmap.entregas-parciais', 'roadmap', `${rotulo}@${l.base.ref}`, 'a base teve mais de 100 commits hoje: "Entregue hoje" pode ter ficado incompleto', 'confira as entregas do dia no clone'));
746
+ }
747
+ }
748
+ else {
749
+ lacunas.push(lacuna('roadmap.sem-base', 'roadmap', rotulo, `${rotulo} não tem a branch ${baseUsada ?? 'padrão'}`, 'confira worktree.base_branch no orkastery.yaml do projeto'));
750
+ }
751
+ fonte('reservas', l.reservas, roadmap_reservas_1.BRANCH_DE_RESERVAS);
752
+ if (!l.reservas) {
753
+ lacunas.push(lacuna('reservas.sem-branch', 'reservas', `${rotulo}@${roadmap_reservas_1.BRANCH_DE_RESERVAS}`, `nenhuma reserva feita ainda: a branch ${roadmap_reservas_1.BRANCH_DE_RESERVAS} não existe em ${rotulo}`, 'a primeira reserva a cria: `ork roadmap pegar RM-NNN`'));
754
+ }
755
+ fonte('fabrica', l.fabrica, fabrica_estado_1.BRANCH_DA_FABRICA);
756
+ if (!l.fabrica) {
757
+ lacunas.push(lacuna('fabrica.sem-branch', 'fabrica', `${rotulo}@${fabrica_estado_1.BRANCH_DA_FABRICA}`, `nenhuma máquina publicou ainda: a branch ${fabrica_estado_1.BRANCH_DA_FABRICA} não existe em ${rotulo}`, 'cada máquina entra com `ork fabrica entrar`'));
758
+ }
759
+ return montarProjeto(projeto, { nome, docs,
760
+ reservas: l.reservas ? reservasDosArquivos(l.reservas.arquivos ?? [], lacunas) : [],
761
+ retratos: l.fabrica ? retratosDosArquivos(l.fabrica.arquivos ?? [], lacunas) : [],
762
+ local: null, commits }, ctx, fontes, lacunas);
763
+ }
764
+ // ---------------------------------------------------------------------------
765
+ // O panorama.
766
+ // ---------------------------------------------------------------------------
767
+ /**
768
+ * Um projeto, isolado dos outros: o que o derruba vira lacuna dele, e os outros seguem (achado 1 do
769
+ * CHECK). O detalhe passa pela mesma redacao do erro da forja.
770
+ */
771
+ function lerProjeto(p, ctx) {
772
+ const vazio = (l) => ({
773
+ projeto: { nome: p.nome, forja: p.forja ? (0, forja_1.rotuloDaForja)(p.forja) : null, clone: p.raiz ? raizParaExibir(p.raiz) : null, base: p.base,
774
+ origem: p.origem, fuso: ctx.fuso },
775
+ roadmap: null, reservas: null, maquinas: null, fontes: [], lacunas: [l]
776
+ });
777
+ try {
778
+ if (p.raiz)
779
+ return lerProjetoDoClone(p, ctx);
780
+ if (p.forja)
781
+ return lerProjetoDaForja(p, ctx);
782
+ return vazio(lacuna('projeto.sem-fonte', 'projeto', p.nome, 'sem clone nesta máquina e sem remoto de forja reconhecido', 'peça por github:dono/repo'));
783
+ }
784
+ catch (e) {
785
+ return vazio(lacuna('projeto.sem-leitura', 'projeto', p.raiz ? raizParaExibir(p.raiz) : p.nome, `a leitura de ${p.nome} parou: ${(0, forja_1.detalheSeguro)(e?.message ?? String(e))}`, 'confira o clone e o estado dele no próprio projeto; os outros projetos seguiram'));
786
+ }
787
+ }
788
+ /**
789
+ * `ork network roadmap`: os projetos pedidos (ou todos os conhecidos), cada um com roadmap, reservas,
790
+ * threads por maquina, fontes e lacunas. Recusa o `--projeto` ambiguo ou desconhecido.
791
+ */
792
+ function montarPanoramaDaRede(opcoes = {}) {
793
+ const quando = opcoes.quando ?? new Date().toISOString();
794
+ const cwd = opcoes.cwd ?? process.cwd();
795
+ const ctx = { quando, maquina: (0, maquina_1.nomeDaMaquina)(opcoes.maquina), semRemoto: opcoes.semRemoto === true, executor: opcoes.executor,
796
+ fuso: (0, horario_1.fusoDoDono)().fuso };
797
+ const naoConsultado = [
798
+ 'rede por pessoa (RM-053, ork.rede-status/v1): não lida nesta versão; as máquinas vêm da branch ork/fabrica-estado de cada projeto',
799
+ ];
800
+ const lacunas = [];
801
+ let alvos;
802
+ if (opcoes.fixado !== undefined) {
803
+ // D-G5 (fatia 2): o servidor MCP le so o projeto dele, em todas as maquinas; o registro nao e lido.
804
+ const c = (0, manifest_1.carregarManifesto)(opcoes.fixado);
805
+ if (!c) {
806
+ throw new ErroDoPedidoDeProjeto('projeto.sem-manifesto', `a raiz fixada não tem ${manifest_1.NOME_MANIFESTO}`, [], 'confira a raiz fixada na instalação do servidor MCP (ork mcp install --project <raiz>)');
807
+ }
808
+ alvos = [projetoDoClone(c, 'instalacao')];
809
+ naoConsultado.push('outros projetos desta máquina: este servidor MCP lê só o projeto fixado na instalação; ' +
810
+ 'os outros vêm do CLI `ork network roadmap`');
811
+ }
812
+ else {
813
+ const conhecidos = projetosConhecidos({ cwd, registro: opcoes.registro, host: opcoes.host });
814
+ naoConsultado.push(...conhecidos.naoConsultado);
815
+ lacunas.push(...conhecidos.lacunas);
816
+ if (opcoes.pedido !== undefined) {
817
+ if (opcoes.host)
818
+ exigirPedidoDoHost(opcoes.pedido, conhecidos.projetos);
819
+ const alvo = resolverProjeto(opcoes.pedido, conhecidos.projetos, cwd);
820
+ alvos = [alvo];
821
+ for (const p of conhecidos.projetos)
822
+ if (!mesmoProjeto(p, alvo))
823
+ naoConsultado.push(`projeto ${p.nome}: fora do pedido (--projeto)`);
824
+ }
825
+ else {
826
+ alvos = conhecidos.projetos;
827
+ }
828
+ }
829
+ if (alvos.length === 0) {
830
+ lacunas.push(lacuna('rede.sem-projeto', 'rede', undefined, opcoes.host
831
+ ? 'nenhum projeto conhecido nesta máquina: o registro está vazio, e o diretório do host não conta como projeto'
832
+ : 'nenhum projeto conhecido nesta máquina: sem manifesto no diretório atual e sem registro', opcoes.host ? 'peça o projeto pela forja (--projeto github:dono/repo) ou registre o clone com `ork projetos registrar <caminho>`'
833
+ : 'peça o projeto: --projeto <caminho do clone> ou --projeto github:dono/repo'));
834
+ }
835
+ const projetos = alvos.map((p) => lerProjeto(p, ctx));
836
+ return { contrato: exports.CONTRATO_PANORAMA_DA_REDE, consultadoEm: quando, maquina: ctx.maquina, pedido: opcoes.pedido ?? null,
837
+ limiarSemBatidaMin: exports.LIMIAR_SEM_BATIDA_MS / 60000, fuso: projetos[0]?.projeto.fuso ?? ctx.fuso, projetos, naoConsultado, lacunas };
838
+ }
839
+ // ---------------------------------------------------------------------------
840
+ // O texto.
841
+ // ---------------------------------------------------------------------------
842
+ function descreverProjeto(x) {
843
+ return `${x.nome} (${[x.forja, x.clone ? `clone em ${x.clone}` : 'sem clone nesta máquina'].filter(Boolean).join(', ')})`;
844
+ }
845
+ function linhaDaFonte(f, quando, fuso) {
846
+ const parte = { roadmap: 'roadmap', reservas: 'reservas', fabrica: 'fábrica', 'estado-local': 'esta máquina' }[f.parte];
847
+ const lido = f.lidoEm === quando ? 'lido agora' : `lido ${(0, horario_1.formatarDataHora)(f.lidoEm, { fuso })}`;
848
+ if (f.origem === 'estado-local')
849
+ return `• ${parte}: estado local em ${f.onde}, ${lido}`;
850
+ if (!f.existe)
851
+ return `• ${parte}: ${f.onde} não existe, ${lido}`;
852
+ const alvo = f.parte === 'roadmap' ? `${docs_1.DIR_ROADMAP} em ${f.onde}` : f.onde;
853
+ return `• ${parte}: ${alvo} @ ${(f.commit ?? '').slice(0, 7)} (commit de ${(0, horario_1.formatarDataHora)(f.dataDoCommit, { fuso })}), ` +
854
+ (f.atualizado ? lido : 'última cópia desta máquina, sem leitura nova');
855
+ }
856
+ function linhasDasMaquinas(x, fuso) {
857
+ if (x.maquinas === null)
858
+ return ['• não lidas: veja as lacunas'];
859
+ if (x.maquinas.length === 0)
860
+ return ['• nenhuma máquina publicou em ork/fabrica-estado'];
861
+ return x.maquinas.flatMap((m) => {
862
+ const quem = `${m.maquina}${m.estaMaquina ? ' (esta máquina)' : ''}`;
863
+ const batida = m.origem === 'estado-local' ? 'estado local lido agora'
864
+ : `retrato de ${(0, horario_1.formatarDataHora)(m.publicadoEm, { fuso })} (há ${(0, horario_1.duracaoCurta)(m.idadeMin)})${m.semBatida ? ', SEM BATIDA' : ''}`;
865
+ const cabeca = `• ${quem}: ${m.ativas.length} ativa(s)${m.entreguesSemMaster ? `, ${m.entreguesSemMaster} entregue(s) sem MASTER` : ''}, ${batida}`;
866
+ // RM-037 (rm037noite, defeito 4): o runtime e o modelo de cada thread, quando o retrato os traz.
867
+ return [cabeca, ...m.ativas.map((t) => ` ${t.id} · ${t.modo} · ${t.fase} · ${t.roadmap ?? 'sem item'}` +
868
+ (t.runtime || t.modelo ? ` · ${(0, fabrica_estado_1.runtimeDaThread)(t)}` : '') +
869
+ (t.esperaVoce ? ` · espera você: ${t.pergunta ?? 'veredito'}` : ''))];
870
+ });
871
+ }
872
+ function linhasDasReservas(x, fuso) {
873
+ if (x.reservas === null)
874
+ return ['• não lidas: veja as lacunas'];
875
+ if (x.reservas.length === 0)
876
+ return ['• nenhuma reserva em ork/roadmap-reservas'];
877
+ return x.reservas.map((r) => ` ${r.item} · ${r.maquina} · ${r.thread ?? 'sem thread'} · desde ${(0, horario_1.formatarDataHora)(r.desdeEm, { fuso })}`);
878
+ }
879
+ const linhaDaLacuna = (l) => `• ${l.tipo}${l.alvo ? ` (${l.alvo})` : ''}: ${l.detalhe}. O que fazer: ${l.correcao}.`;
880
+ /** O texto de `ork network roadmap`: o consultado e o nao consultado no alto, e cada parte com a fonte e a hora. */
881
+ function textoDoPanoramaDaRede(p) {
882
+ if (p.contrato !== exports.CONTRATO_PANORAMA_DA_REDE)
883
+ throw new Error('panorama da rede: contrato inválido');
884
+ const h = (0, horario_1.partesLocais)(p.consultadoEm, p.fuso);
885
+ const linhas = [
886
+ `Panorama da rede lido de ${p.maquina} (${h.dia}/${h.mes}, ${h.hora}:${h.minuto})`,
887
+ p.projetos.length ? `Consultado: ${p.projetos.map((x) => descreverProjeto(x.projeto)).join('; ')}` : 'Consultado: nenhum projeto.',
888
+ 'Não consultado:', ...(p.naoConsultado.length ? p.naoConsultado.map((n) => `• ${n}`) : ['• nada fora do pedido']),
889
+ ];
890
+ if (p.lacunas.length)
891
+ linhas.push('', 'Lacunas da consulta', ...p.lacunas.map(linhaDaLacuna));
892
+ for (const x of p.projetos) {
893
+ // Cada projeto no fuso do dono dele (achado 5 da rodada 2): a legenda do bloco diz quando difere.
894
+ const fuso = x.projeto.fuso;
895
+ linhas.push('', '────────', '');
896
+ linhas.push(x.roadmap ? (0, roadmap_status_1.textoDoStatusDoRoadmap)(x.roadmap, fuso)
897
+ : `Roadmap do ${x.projeto.nome}: não lido (${x.lacunas.map((l) => l.tipo).filter((t) => t !== 'projeto.sem-clone').join(', ') || 'sem fonte'}).`);
898
+ linhas.push('', 'Threads por máquina', ...linhasDasMaquinas(x, fuso));
899
+ linhas.push('', 'Reservas', ...linhasDasReservas(x, fuso));
900
+ linhas.push('', 'Fontes', ...(x.fontes.length ? x.fontes.map((f) => linhaDaFonte(f, p.consultadoEm, fuso)) : ['• nenhuma fonte lida']));
901
+ linhas.push('', 'Lacunas', ...(x.lacunas.length ? x.lacunas.map(linhaDaLacuna) : ['• nenhuma: todas as fontes foram lidas agora']));
902
+ if (fuso !== p.fuso)
903
+ linhas.push('', `Neste projeto: ${(0, horario_1.legendaDoFuso)(fuso)}`);
904
+ }
905
+ linhas.push('', (0, horario_1.legendaDoFuso)(p.fuso));
906
+ return linhas.join('\n');
907
+ }