@orkastery/cli 0.5.0 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/adapters/claude-code/README.md +11 -2
  2. package/adapters/claude-code/agents/ork-master.md +2 -0
  3. package/adapters/claude-code/commands/master.md +8 -0
  4. package/adapters/claude-code/commands/onboarding.md +3 -2
  5. package/adapters/claude-code/commands/ork.md +9 -0
  6. package/adapters/codex/skills/ork/SKILL.md +18 -3
  7. package/adapters/hermes/README.md +1 -0
  8. package/adapters/hermes/bin/ork-network-status.sh +10 -0
  9. package/adapters/hermes/hermes.plugin.json +1 -0
  10. package/adapters/hermes/hitl-ingress/__init__.py +2 -2
  11. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +6 -5
  12. package/adapters/openclaw/README.md +35 -4
  13. package/adapters/openclaw/dist/hitl-ingress.js +2 -2
  14. package/adapters/openclaw/dist/index.js +47 -12
  15. package/adapters/openclaw/openclaw.plugin.json +5 -1
  16. package/adapters/openclaw/package.json +2 -2
  17. package/adapters/openclaw/src/hitl-ingress.ts +2 -2
  18. package/adapters/openclaw/src/index.ts +50 -12
  19. package/assets/orkmind_bridge.py +117 -9
  20. package/dist/adapters/claude-bg.js +59 -11
  21. package/dist/adapters/codex.js +4 -3
  22. package/dist/agents-md.js +1 -0
  23. package/dist/auditrun.js +6 -2
  24. package/dist/board.js +16 -14
  25. package/dist/branch-de-estado.js +66 -10
  26. package/dist/busca-semantica.js +52 -12
  27. package/dist/canarios-rm057.js +149 -0
  28. package/dist/canarios.js +2 -0
  29. package/dist/ci.js +8 -1
  30. package/dist/claim-lint.js +3 -1
  31. package/dist/claims.js +39 -1
  32. package/dist/company-brain-context.js +158 -22
  33. package/dist/docs.js +110 -3
  34. package/dist/doctor-pulse-cron.js +194 -0
  35. package/dist/doctor-rede.js +149 -0
  36. package/dist/doctor.js +294 -37
  37. package/dist/entrega-pr.js +27 -8
  38. package/dist/esgotamentos.js +181 -0
  39. package/dist/experiencia.js +12 -2
  40. package/dist/fabrica-estado.js +35 -9
  41. package/dist/fabrica-publicar.js +6 -0
  42. package/dist/fechamento.js +20 -66
  43. package/dist/fix.js +3 -3
  44. package/dist/forja.js +31 -7
  45. package/dist/gates.js +5 -1
  46. package/dist/hitl-contract.js +59 -4
  47. package/dist/hitl-curto.js +11 -6
  48. package/dist/hitl-gates.js +5 -0
  49. package/dist/hitl-lote.js +5 -2
  50. package/dist/hitl-resumo.js +33 -1
  51. package/dist/hitl-tempo-parado.js +201 -0
  52. package/dist/hitl-texto-livre.js +5 -5
  53. package/dist/horario.js +27 -0
  54. package/dist/hosts.js +59 -13
  55. package/dist/impedimento.js +205 -0
  56. package/dist/index.js +433 -88
  57. package/dist/indice-vetorial.js +87 -10
  58. package/dist/indice.js +3 -1
  59. package/dist/init.js +28 -5
  60. package/dist/integracoes-locais.js +4 -0
  61. package/dist/intelligence-graph-cli.js +267 -30
  62. package/dist/intelligence-graph-contexto.js +215 -0
  63. package/dist/intelligence-graph-contract.js +71 -12
  64. package/dist/intelligence-graph-extract-md.js +41 -23
  65. package/dist/intelligence-graph-extract-ts.js +181 -11
  66. package/dist/intelligence-graph-extract.js +186 -9
  67. package/dist/intelligence-graph-index.js +205 -24
  68. package/dist/intelligence-graph-parsers.js +6 -4
  69. package/dist/intelligence-graph-repo.js +10 -0
  70. package/dist/leases.js +680 -87
  71. package/dist/ledger-stats.js +15 -1
  72. package/dist/ledger.js +12 -0
  73. package/dist/licoes.js +6 -4
  74. package/dist/liveness.js +2 -1
  75. package/dist/maestro-runtime.js +7 -1
  76. package/dist/manifest.js +44 -1
  77. package/dist/master-nota.js +14 -5
  78. package/dist/master.js +129 -12
  79. package/dist/mcp-artifacts.js +36 -3
  80. package/dist/mcp-git.js +80 -15
  81. package/dist/mcp-grafo-worker.js +93 -0
  82. package/dist/mcp-grafo.js +279 -0
  83. package/dist/mcp-install.js +2 -1
  84. package/dist/mcp-server.js +23 -1
  85. package/dist/mcp-ship.js +2 -0
  86. package/dist/mcp-verify.js +3 -1
  87. package/dist/memoria.js +48 -6
  88. package/dist/network-roadmap.js +195 -20
  89. package/dist/ocupacao.js +17 -5
  90. package/dist/onboarding.js +9 -4
  91. package/dist/orkmind.js +90 -5
  92. package/dist/orquestracao.js +14 -11
  93. package/dist/parado-no-condutor.js +1089 -0
  94. package/dist/phase.js +68 -15
  95. package/dist/policies.js +79 -3
  96. package/dist/portfolio.js +4 -1
  97. package/dist/projeto-alvo.js +5 -1
  98. package/dist/prompts.js +36 -1
  99. package/dist/prova-ativacao.js +414 -0
  100. package/dist/pulse-delivery.js +60 -7
  101. package/dist/pulse.js +99 -3
  102. package/dist/ratelimit.js +20 -4
  103. package/dist/recall.js +3 -1
  104. package/dist/rede-adesao.js +399 -0
  105. package/dist/rede-forja.js +289 -0
  106. package/dist/rede-projetos.js +239 -0
  107. package/dist/rede-status.js +336 -0
  108. package/dist/rede.js +980 -0
  109. package/dist/retry.js +69 -2
  110. package/dist/roadmap-status.js +162 -7
  111. package/dist/runtime-context.js +5 -1
  112. package/dist/runtime-profiles.js +22 -1
  113. package/dist/saida-segura.js +58 -0
  114. package/dist/session-watcher-claude.js +113 -23
  115. package/dist/sessoes-contas.js +187 -0
  116. package/dist/sessoes-inventario.js +6 -12
  117. package/dist/sessoes.js +87 -3
  118. package/dist/ship.js +110 -20
  119. package/dist/thread.js +212 -14
  120. package/dist/util.js +71 -4
  121. package/dist/verify.js +2 -2
  122. package/dist/worktree.js +34 -8
  123. package/eval/casos/onboarding.json +1 -1
  124. package/eval/fixtures/fx-pedido-colado/caso.json +20 -0
  125. package/package.json +7 -3
  126. package/references/definition-of-done.md +1 -1
  127. package/skills/README.md +1 -1
  128. package/skills/core/onboarding/SKILL.md +3 -2
  129. package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
  130. package/skills/phases/master-metrics/SKILL.md +8 -3
  131. package/skills/phases/ship-release/SKILL.md +6 -3
@@ -1,12 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CONTEXT_SCHEMA = void 0;
3
+ exports.SERVER_CONTEXT_SCHEMA = exports.CONTEXT_SCHEMA = void 0;
4
4
  exports.buildContext = buildContext;
5
5
  const portfolio_1 = require("./portfolio");
6
6
  const company_brain_client_1 = require("./company-brain-client");
7
7
  const company_brain_contract_1 = require("./company-brain-contract");
8
8
  const company_brain_source_1 = require("./company-brain-source");
9
9
  exports.CONTEXT_SCHEMA = 'ork.brain-context/v1';
10
+ exports.SERVER_CONTEXT_SCHEMA = 'orkmind.company-brain-context/v1';
11
+ const SEM_MODO_CONTEXT = 'brain.selection.context-unsupported';
12
+ const SERVIDOR_INVALIDO = 'brain.context.server-invalid';
10
13
  const ID = /^(prod|proj|init)-[a-z0-9][a-z0-9-]{2,47}$/;
11
14
  const ORDEM = { prod: 0, proj: 1, init: 2 };
12
15
  const LIMITE = 1000;
@@ -27,25 +30,122 @@ function citacaoDe(source) {
27
30
  return null;
28
31
  return { instance, source_ref, source_hash, source_version, location };
29
32
  }
33
+ /** O servidor exige também `authority`: o pacote dele só traz item com a citação inteira; a consulta exige o mesmo. */
34
+ const citadoPeloServidor = (source) => citacaoDe(source) !== null && typeof source.authority === 'string' && source.authority !== '';
30
35
  /**
31
- * Monta o pacote dos ids pedidos mais a cadeia de pais. A fonte canônica é o portfólio local;
32
- * o Brain é projeção, e a diferença entre os dois sai como frescor e lacuna, nunca é corrigida aqui.
36
+ * Confere um pacote `orkmind.company-brain-context/v1` contra o lote pedido: schema, tenant, pedido,
37
+ * digest recalculado, citação inteira, ordem, fecho de pais e destino de cada id pedido.
33
38
  */
34
- function buildContext(c, ids, transport, thread) {
35
- const tenant = c.manifesto.memory.tenant;
36
- const pedido = [...new Set(ids)].sort(comparar);
37
- if (!pedido.length)
38
- throw Error('brain.context.ids-required');
39
- if (pedido.some(id => !ID.test(id)))
40
- throw Error('brain.context.id-invalid');
41
- if (pedido.length > LIMITE)
42
- throw Error('brain.context.too-many');
43
- const fonte = new Map((0, company_brain_source_1.portfolioEntities)((0, portfolio_1.readPortfolio)(c.raiz), { tenant, instance: c.manifesto.project.name, thread, aclRef: 'ork-factory' }).map(e => [e.id, e]));
44
- const base = { schema: exports.CONTEXT_SCHEMA, tenant, contrato: company_brain_contract_1.CONTRACT_HASH, thread, pedido };
45
- const encerrar = (r) => ({ ...base, state: r.state, ...(r.error ? { error: r.error } : {}), itens: [], lacunas: [], digest: null, consultadoEm: new Date().toISOString() });
39
+ function conferirPacoteDoServidor(r, tenant, lote) {
40
+ const ctx = r.context;
41
+ if (!['ok', 'empty'].includes(r.state) || !ctx || typeof ctx !== 'object' || Array.isArray(ctx))
42
+ return false;
43
+ if (Object.keys(ctx).sort().join(',') !== 'digest,gaps,items,requested,schema,tenant_id')
44
+ return false;
45
+ const { digest: recebido, ...corpo } = ctx;
46
+ if (ctx.schema !== exports.SERVER_CONTEXT_SCHEMA || ctx.tenant_id !== tenant || (0, company_brain_contract_1.canonical)(ctx.requested) !== (0, company_brain_contract_1.canonical)(lote) ||
47
+ !Array.isArray(ctx.items) || !Array.isArray(ctx.gaps) || typeof recebido !== 'string' || (0, company_brain_contract_1.digest)(corpo) !== recebido)
48
+ return false;
49
+ if (r.state !== (ctx.items.length ? 'ok' : 'empty'))
50
+ return false;
51
+ const pedidos = new Set(lote), vistos = new Map(), codigos = new Map();
52
+ for (const g of ctx.gaps) {
53
+ if (!g || typeof g.id !== 'string' || typeof g.code !== 'string' || !ID.test(g.id))
54
+ return false;
55
+ if (!codigos.has(g.id))
56
+ codigos.set(g.id, new Set());
57
+ codigos.get(g.id).add(g.code);
58
+ }
59
+ for (const item of ctx.items) {
60
+ if (!item || typeof item.id !== 'string' || !ID.test(item.id) || vistos.has(item.id))
61
+ return false;
62
+ if (item.state === 'withheld') {
63
+ if (Object.keys(item).length !== 2)
64
+ return false;
65
+ }
66
+ else if (item.state !== 'ok' || !item.entity || typeof item.entity !== 'object' || item.entity.kind !== kindDe(item.id) ||
67
+ !Number.isInteger(item.entity.version) || !citadoPeloServidor(item.entity.source))
68
+ return false;
69
+ vistos.set(item.id, item);
70
+ }
71
+ const ordem = ctx.items.map((i) => i.id);
72
+ const ordenado = [...ordem].sort((a, b) => ORDEM[kindDe(a)] - ORDEM[kindDe(b)] || comparar(a, b));
73
+ if ((0, company_brain_contract_1.canonical)(ordem) !== (0, company_brain_contract_1.canonical)(ordenado))
74
+ return false;
75
+ // Fecho: todo item fora do pedido é pai, um kind acima, de um item citado do pacote.
76
+ const paisCitados = new Set();
77
+ for (const item of ctx.items)
78
+ if (item.state === 'ok') {
79
+ const parent = item.entity.parent_id;
80
+ if (typeof parent === 'string' && ID.test(parent) && ORDEM[kindDe(parent)] < ORDEM[kindDe(item.id)])
81
+ paisCitados.add(parent);
82
+ }
83
+ for (const id of vistos.keys())
84
+ if (!pedidos.has(id) && !paisCitados.has(id))
85
+ return false;
86
+ // Destino: todo id pedido sai como item ou como lacuna de desconhecido ou de citação incompleta.
87
+ for (const id of lote) {
88
+ const c = codigos.get(id);
89
+ if (!vistos.has(id) && !(c?.has('entity.unknown') || c?.has('citation.incomplete')))
90
+ return false;
91
+ if (vistos.has(id) && (c?.has('entity.unknown') || c?.has('citation.incomplete')))
92
+ return false;
93
+ }
94
+ return true;
95
+ }
96
+ /**
97
+ * B4.2: resolve o escopo pelo modo `context` do servidor. Devolve null quando o OrkMind não tem o modo
98
+ * (o chamador usa a consulta), ou a resposta que encerra o pacote.
99
+ */
100
+ function resolverPeloServidor(transport, tenant, pedido, fonte, pai) {
101
+ const escopo = new Set(pedido), brain = new Map(), retidos = new Set(), semCitacao = new Set();
102
+ let primeiro = true;
103
+ for (let pendentes = pedido; pendentes.length;) {
104
+ const resolvidosNaVolta = [];
105
+ for (let inicio = 0; inicio < pendentes.length; inicio += LIMITE) {
106
+ const lote = pendentes.slice(inicio, inicio + LIMITE);
107
+ const r = transport({ schema: company_brain_client_1.BRAIN_API, operation: 'query', payload: (0, company_brain_contract_1.validateContract)({ schema: 'orkmind.company-brain-selection/v1',
108
+ tenant_id: tenant, facets: { ids: lote, kinds: [], workspace_ids: [], source_instances: [] }, mode: 'context',
109
+ limit: lote.length, offset: 0 }) });
110
+ if (r.state === 'unavailable' && r.error === SEM_MODO_CONTEXT)
111
+ return primeiro ? null : { state: 'conflict', error: SERVIDOR_INVALIDO };
112
+ primeiro = false;
113
+ if (falha(r.state))
114
+ return r;
115
+ if (!conferirPacoteDoServidor(r, tenant, lote))
116
+ return { state: 'conflict', error: SERVIDOR_INVALIDO };
117
+ const ctx = r.context;
118
+ for (const g of ctx.gaps)
119
+ if (g.code === 'citation.incomplete')
120
+ semCitacao.add(g.id);
121
+ for (const item of ctx.items) {
122
+ // Pai que outro lote ou volta já resolveu vale uma vez só: a primeira resposta fica.
123
+ if (escopo.has(item.id) && !lote.includes(item.id))
124
+ continue;
125
+ escopo.add(item.id);
126
+ resolvidosNaVolta.push(item.id);
127
+ if (item.state === 'withheld')
128
+ retidos.add(item.id);
129
+ else
130
+ brain.set(item.id, { ...item.entity, id: item.id });
131
+ }
132
+ resolvidosNaVolta.push(...lote);
133
+ }
134
+ // O servidor fecha os pais que o Brain conhece; os que só a fonte local conhece vão na próxima volta.
135
+ const novos = new Set();
136
+ for (const id of resolvidosNaVolta)
137
+ if (!retidos.has(id))
138
+ for (const parent of [pai(id, fonte.get(id)?.parent_id), pai(id, brain.get(id)?.parent_id)])
139
+ if (parent && !escopo.has(parent))
140
+ novos.add(parent);
141
+ novos.forEach(id => escopo.add(id));
142
+ pendentes = [...novos].sort(comparar);
143
+ }
144
+ return { brain, retidos, semCitacao, escopo };
145
+ }
146
+ /** B4.1: o caminho anterior ao modo `context`, por `query` e `get`, para um OrkMind sem o modo. */
147
+ function resolverPelaConsulta(transport, tenant, pedido, fonte, pai) {
46
148
  const escopo = new Set(pedido), brain = new Map(), retidos = new Set();
47
- // O pai só entra por um filho visível e tem kind acima dele: init, proj e prod fecham em três voltas.
48
- const pai = (id, parent) => typeof parent === 'string' && ID.test(parent) && ORDEM[kindDe(parent)] < ORDEM[kindDe(id)] ? parent : null;
49
149
  let gets = 0;
50
150
  for (let pendentes = pedido; pendentes.length;) {
51
151
  for (let inicio = 0; inicio < pendentes.length; inicio += LIMITE) {
@@ -54,7 +154,7 @@ function buildContext(c, ids, transport, thread) {
54
154
  tenant_id: tenant, facets: { ids: lote, kinds: [], workspace_ids: [], source_instances: [] }, mode: 'selection',
55
155
  limit: lote.length, offset: 0 }) });
56
156
  if (falha(r.state))
57
- return encerrar(r);
157
+ return r;
58
158
  const itens = Array.isArray(r.items) ? r.items : [];
59
159
  for (const item of itens)
60
160
  if (item?.state === 'ok' && doLote.has(item.entity?.id))
@@ -67,10 +167,10 @@ function buildContext(c, ids, transport, thread) {
67
167
  else if (retidosNoLote)
68
168
  for (const id of faltantes) {
69
169
  if (++gets > LIMITE_DE_GET)
70
- return encerrar({ state: 'unavailable', error: 'brain.context.get-limit' });
170
+ return { state: 'unavailable', error: 'brain.context.get-limit' };
71
171
  const g = transport({ schema: company_brain_client_1.BRAIN_API, operation: 'get', payload: { tenant_id: tenant, id } });
72
172
  if (falha(g.state))
73
- return encerrar(g);
173
+ return g;
74
174
  if (g.state === 'withheld')
75
175
  retidos.add(id);
76
176
  else if (g.state === 'ok' && g.entity?.id === id)
@@ -87,6 +187,35 @@ function buildContext(c, ids, transport, thread) {
87
187
  }
88
188
  pendentes = novos.sort(comparar);
89
189
  }
190
+ return { brain, retidos, semCitacao: new Set(), escopo };
191
+ }
192
+ /**
193
+ * Monta o pacote dos ids pedidos mais a cadeia de pais. A fonte canônica é o portfólio local;
194
+ * o Brain é projeção, e a diferença entre os dois sai como frescor e lacuna, nunca é corrigida aqui.
195
+ */
196
+ function buildContext(c, ids, transport, thread) {
197
+ const tenant = c.manifesto.memory.tenant;
198
+ const pedido = [...new Set(ids)].sort(comparar);
199
+ if (!pedido.length)
200
+ throw Error('brain.context.ids-required');
201
+ if (pedido.some(id => !ID.test(id)))
202
+ throw Error('brain.context.id-invalid');
203
+ if (pedido.length > LIMITE)
204
+ throw Error('brain.context.too-many');
205
+ const fonte = new Map((0, company_brain_source_1.portfolioEntities)((0, portfolio_1.readPortfolio)(c.raiz), { tenant, instance: c.manifesto.project.name, thread, aclRef: 'ork-factory' }).map(e => [e.id, e]));
206
+ const base = { schema: exports.CONTEXT_SCHEMA, tenant, contrato: company_brain_contract_1.CONTRACT_HASH, thread, pedido };
207
+ // O pai só entra por um filho visível e tem kind acima dele: init, proj e prod fecham em três voltas.
208
+ const pai = (id, parent) => typeof parent === 'string' && ID.test(parent) && ORDEM[kindDe(parent)] < ORDEM[kindDe(id)] ? parent : null;
209
+ const doServidor = resolverPeloServidor(transport, tenant, pedido, fonte, pai);
210
+ const caminho = doServidor === null ? 'consulta' : 'servidor';
211
+ const encerrar = (r) => ({ ...base, state: r.state, ...(r.error ? { error: r.error } : {}), itens: [], lacunas: [],
212
+ digest: null, consultadoEm: new Date().toISOString(), caminho });
213
+ if (doServidor && 'state' in doServidor)
214
+ return encerrar(doServidor);
215
+ const resolvido = doServidor ?? resolverPelaConsulta(transport, tenant, pedido, fonte, pai);
216
+ if ('state' in resolvido)
217
+ return encerrar(resolvido);
218
+ const { escopo, brain, retidos, semCitacao } = resolvido;
90
219
  const itens = [], lacunas = [];
91
220
  const lacuna = (id, codigo) => lacunas.push({ id, codigo });
92
221
  for (const id of escopo) {
@@ -95,12 +224,19 @@ function buildContext(c, ids, transport, thread) {
95
224
  lacuna(id, 'brain.retido');
96
225
  continue;
97
226
  }
227
+ // Sem citação no Brain vale como no caminho da consulta: o Brain tem a entidade, mas ela não é citável.
228
+ if (semCitacao.has(id)) {
229
+ lacuna(id, 'citacao.incompleta');
230
+ continue;
231
+ }
98
232
  const b = brain.get(id), f = fonte.get(id);
99
233
  if (!b && !f) {
100
234
  lacuna(id, 'entidade.desconhecida');
101
235
  continue;
102
236
  }
103
- const entidade = b ?? f, citacao = citacaoDe(entidade.source);
237
+ // Suspeitas da revisao de 03/10: a citacao exige `authority` nos dois caminhos, como o servidor (o
238
+ // schema do Source a exige); sem isso, um Brain fora do contrato dava pacote e digest diferentes por caminho.
239
+ const entidade = b ?? f, citacao = citadoPeloServidor(entidade.source) ? citacaoDe(entidade.source) : null;
104
240
  if (!citacao) {
105
241
  lacuna(id, 'citacao.incompleta');
106
242
  continue;
@@ -128,5 +264,5 @@ function buildContext(c, ids, transport, thread) {
128
264
  lacunas.sort((a, b) => comparar(a.id, b.id) || comparar(a.codigo, b.codigo));
129
265
  // O digest cobre o que foi dito e de onde veio; o horário da consulta fica fora para o pacote ser reproduzível.
130
266
  const corpo = { ...base, state: (itens.length ? 'ok' : 'empty'), itens, lacunas };
131
- return { ...corpo, digest: (0, company_brain_contract_1.digest)(corpo), consultadoEm: new Date().toISOString() };
267
+ return { ...corpo, digest: (0, company_brain_contract_1.digest)(corpo), consultadoEm: new Date().toISOString(), caminho };
132
268
  }
package/dist/docs.js CHANGED
@@ -43,9 +43,11 @@ exports.comandosDaAjuda = comandosDaAjuda;
43
43
  exports.verificarDocs = verificarDocs;
44
44
  exports.renderizarRelance = renderizarRelance;
45
45
  exports.renderizarEstado = renderizarEstado;
46
+ exports.docsPendentesDoMerge = docsPendentesDoMerge;
46
47
  exports.textoDaVerificacao = textoDaVerificacao;
47
48
  exports.escreverYaml = escreverYaml;
48
49
  exports.mergeDaThread = mergeDaThread;
50
+ exports.mergesDaThreadNoGit = mergesDaThreadNoGit;
49
51
  exports.escopoPadraoDoSync = escopoPadraoDoSync;
50
52
  exports.sincronizarDocs = sincronizarDocs;
51
53
  exports.tabelaDoRoadmap = tabelaDoRoadmap;
@@ -262,9 +264,12 @@ function verificarDocs(raiz, opcoes = {}) {
262
264
  verificarFontes(raiz, d, comandos, achados);
263
265
  if (d.tipo === 'roadmap') {
264
266
  verificarEstadoDoRoadmap(raiz, d, base, achados);
267
+ if (base)
268
+ verificarMergeDaThread(raiz, d, base, achados, opcoes.pr === true);
265
269
  verificarTabelasDeEstado(d, achados);
266
270
  }
267
271
  }
272
+ verificarIndices(raiz, docs, achados, opcoes.pr === true);
268
273
  return { docs, achados };
269
274
  }
270
275
  function verificarChaves(d, achados) {
@@ -564,6 +569,95 @@ function verificarEstadoDoRoadmap(raiz, d, base, achados) {
564
569
  }
565
570
  }
566
571
  }
572
+ /**
573
+ * RM-037 (fatia 3, defeito 1): o outro sentido da paridade com o git. Depois do merge do PR, o
574
+ * frontmatter da RM-049 (#26) e da RM-051 (#33) seguiu em "Branch criada", e o verificador dizia OK:
575
+ * ele so conferia que `Mesclado` tem o commit na base. O merge desta fabrica e pelo GitHub, sem o
576
+ * `ork ship` no caminho, entao a prova mora aqui, no job que o CI obrigatorio roda. A regra usa o
577
+ * mesmo `ship(<thread>)` do `sincronizarDocs`: tudo o que ela acusa o `sincronizar` corrige.
578
+ */
579
+ function verificarMergeDaThread(raiz, d, base, achados, pr = false) {
580
+ const pendente = pendenciaDeMerge(raiz, d, base);
581
+ if (!pendente)
582
+ return;
583
+ const { thread, merges, codigo } = pendente;
584
+ achados.push((pr ? aviso : erro)(d.arquivo, d.id, 'docs.paridade.merge', `a thread ${thread} entrou na ${base} pelo merge ${merges[0].slice(0, 7)} (ship(${thread})), e o estado.codigo diz "${codigo || 'vazio'}"` +
585
+ (pr ? ' (no PR, aviso: o push da main reprova)' : ''), `abra um PR de docs sobre a ${base} atualizada com ork docs sincronizar --escrever --so ${d.id} (o item e os índices); ` +
586
+ `se o item tem outra fatia em curso, aponte sdlc.thread para a thread dela`));
587
+ }
588
+ /**
589
+ * A regra do `docs.paridade.merge`, isolada para o `ork ship registrar-pr` (RM-044, A3): a pagina cujo
590
+ * `sdlc.thread` ja tem merge `ship(<thread>)` na base e cujo `estado.codigo` ainda nao diz `Mesclado`.
591
+ * `thread`, quando dada, restringe a pagina a essa thread.
592
+ */
593
+ function pendenciaDeMerge(raiz, d, base, thread) {
594
+ const sdlc = ehMapa(d.dados.sdlc) ? d.dados.sdlc : null;
595
+ const daPagina = sdlc && typeof sdlc.thread === 'string' ? sdlc.thread.trim() : '';
596
+ if (!daPagina || (thread !== undefined && daPagina !== thread))
597
+ return null;
598
+ const codigo = String((ehMapa(d.dados.estado) ? d.dados.estado.codigo : null) ?? '');
599
+ if (codigo === 'Mesclado')
600
+ return null;
601
+ const merges = mergesDaThreadNoGit(raiz, daPagina, base);
602
+ if (merges.length === 0)
603
+ return null;
604
+ return { thread: daPagina, merges, codigo };
605
+ }
606
+ /**
607
+ * RM-044 (A3): as paginas do roadmap, lidas da `ref` (a base remota depois do fetch, nao a copia local,
608
+ * que pode estar atras), que a regra `docs.paridade.merge` vai acusar por causa da `thread`. Nao grava
609
+ * nada: so lista. Ref ilegivel ou sem `docs/roadmap` devolve lista vazia.
610
+ */
611
+ function docsPendentesDoMerge(raiz, thread, ref) {
612
+ const ls = git(raiz, ['ls-tree', '--name-only', `${ref}:${exports.DIR_ROADMAP}`]);
613
+ if (!ls.ok || !ls.saida)
614
+ return [];
615
+ const pendentes = [];
616
+ for (const nome of ls.saida.split('\n').filter(ehPaginaDeDocs).sort()) {
617
+ const arquivo = `${exports.DIR_ROADMAP}/${nome}`;
618
+ const blob = git(raiz, ['show', `${ref}:${arquivo}`]);
619
+ if (!blob.ok)
620
+ continue;
621
+ const lido = documentoDeTexto(arquivo, `${blob.saida}\n`);
622
+ if (!('doc' in lido) || lido.doc.tipo !== 'roadmap')
623
+ continue;
624
+ const p = pendenciaDeMerge(raiz, lido.doc, ref, thread);
625
+ if (!p)
626
+ continue;
627
+ pendentes.push({ id: lido.doc.id, arquivo, codigo: p.codigo, merge: p.merges[0],
628
+ comando: `ork docs sincronizar --escrever --so ${lido.doc.id}` });
629
+ }
630
+ return pendentes;
631
+ }
632
+ /** Os indices gerados (`docs/roadmap/README.md`, `docs/produto/README.md`) e o que o frontmatter diz. */
633
+ const INDICES_GERADOS = [
634
+ [`${exports.DIR_ROADMAP}/README.md`, (docs) => tabelaDoRoadmap(docs)],
635
+ [`${exports.DIR_PRODUTO}/README.md`, (docs) => tabelaDoProduto(docs)],
636
+ ];
637
+ /**
638
+ * RM-037 (fatia 3, defeito 1): o indice e uma vista do frontmatter, como as tabelas do corpo, e mentia
639
+ * do mesmo jeito: sem a RM-051 e com a RM-031 de antes. Divergente reprova; vazio (o modelo recem-copiado
640
+ * pelo `ork docs init`) so avisa; sem os marcadores, nao ha indice gerado para conferir.
641
+ */
642
+ function verificarIndices(raiz, docs, achados, pr = false) {
643
+ for (const [arquivo, gerar] of INDICES_GERADOS) {
644
+ const abs = path.join(raiz, arquivo);
645
+ if (!fs.existsSync(abs))
646
+ continue;
647
+ // Checkout com CRLF (Windows com core.autocrlf) nao e divergencia: as paginas tambem normalizam.
648
+ const atual = blocoEntre(fs.readFileSync(abs, 'utf8').replace(/\r\n/g, '\n'), INICIO, FIM);
649
+ if (atual === null)
650
+ continue;
651
+ if (atual === '') {
652
+ if (docs.length > 0) {
653
+ achados.push(aviso(arquivo, undefined, 'docs.paridade.indice', 'o índice ainda não foi gerado', 'rode ork docs sincronizar --escrever'));
654
+ }
655
+ }
656
+ else if (atual !== gerar(docs)) {
657
+ achados.push((pr ? aviso : erro)(arquivo, undefined, 'docs.paridade.indice', `o índice diverge do frontmatter das páginas${pr ? ' (no PR, aviso: o push da main reprova)' : ''}`, 'o índice é gerado do frontmatter: rode ork docs sincronizar --escrever e commite o índice'));
658
+ }
659
+ }
660
+ }
567
661
  // --------------------------------------------------------------------------------------------
568
662
  // Saida para humano (TDAH) e para agente
569
663
  function textoDaVerificacao(docs, achados) {
@@ -666,13 +760,26 @@ function mergeDaThread(raiz, thread, base) {
666
760
  return { sha: String(ship.mergeSha).slice(0, 7), fonte: 'ledger ship_done' };
667
761
  }
668
762
  catch { /* thread sem estado nesta maquina: cai para o git */ }
669
- // Sem `-n 1`: o git corta antes de inverter, e `--reverse -n 1` devolveria o mais recente.
670
- const log = git(raiz, ['log', base, '--reverse', '--format=%H', '--fixed-strings', `--grep=ship(${thread})`]);
671
- const primeiro = log.ok ? log.saida.split('\n')[0] : '';
763
+ const primeiro = mergesDaThreadNoGit(raiz, thread, base)[0];
672
764
  if (primeiro)
673
765
  return { sha: primeiro.slice(0, 7), fonte: `git log ${base}` };
674
766
  return null;
675
767
  }
768
+ /**
769
+ * Os commits `ship(<thread>)` da base, do mais velho ao mais novo, so pelo git (o CI nao tem ledger).
770
+ * RM-037 (fatia 3): um lugar so para o `sincronizarDocs` e o `verificarDocs` acharem o mesmo merge.
771
+ */
772
+ function mergesDaThreadNoGit(raiz, thread, base) {
773
+ // Sem `-n 1`: o git corta antes de inverter, e `--reverse -n 1` devolveria o mais recente.
774
+ // GO-FIX 2 (achado 4 do CHECK): so a linha de primeiro pai da base e o assunto que COMECA com
775
+ // `ship(<thread>)`, como no `mergeDaEntrega`. Commit que cita o merge no corpo, ou o Revert dele, nao e merge.
776
+ const log = git(raiz, ['log', base, '--first-parent', '--reverse', '--format=%H%x09%s', '--fixed-strings', `--grep=ship(${thread})`]);
777
+ if (!log.ok)
778
+ return [];
779
+ return log.saida.split('\n').map((l) => l.split('\t'))
780
+ .filter(([sha, assunto]) => !!sha && typeof assunto === 'string' && assunto.startsWith(`ship(${thread})`))
781
+ .map(([sha]) => sha);
782
+ }
676
783
  function temBranch(raiz, thread) {
677
784
  const r = git(raiz, ['for-each-ref', '--format=%(refname:short)', `refs/heads/ork/${thread}-*`, `refs/remotes/origin/ork/${thread}-*`]);
678
785
  return r.ok && r.saida.length > 0;
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ /**
3
+ * RM-039 (B6): o `ork doctor` acusa o pulse que bate devagar demais.
4
+ *
5
+ * A cadencia por tag (#OrkPulseOn-15m e as outras) so vale inteira com a varredura do cron de 15
6
+ * em 15 minutos. A instalacao antiga fica em `0 * * * *`, e nada avisava. Com o transporte do
7
+ * pulse configurado (`.orkastery/monitor/pulse-host.json`), o doctor le o `crontab -l` e avisa,
8
+ * sem bloquear, quando a linha da varredura falta ou bate mais devagar que o template
9
+ * `monitor/pulse.cron`. O doctor nunca edita o crontab: a correcao traz a linha pronta.
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.COMANDO_DO_TEMPLATE = exports.SCRIPT_DA_VARREDURA = exports.BATIDA_DO_TEMPLATE = void 0;
46
+ exports.lerCrontabDoSistema = lerCrontabDoSistema;
47
+ exports.intervaloEmMinutos = intervaloEmMinutos;
48
+ exports.linhasDaVarredura = linhasDaVarredura;
49
+ exports.scriptDaVarreduraDoOrk = scriptDaVarreduraDoOrk;
50
+ exports.comandoDaVarredura = comandoDaVarredura;
51
+ exports.checarCronDoPulse = checarCronDoPulse;
52
+ const fs = __importStar(require("node:fs"));
53
+ const path = __importStar(require("node:path"));
54
+ const util_1 = require("./util");
55
+ /** A batida do template `monitor/pulse.cron` (I-50): a menor cadencia das tags. */
56
+ exports.BATIDA_DO_TEMPLATE = '*/15 * * * *';
57
+ /** O script que o template agenda; a linha do crontab e achada por ele. */
58
+ exports.SCRIPT_DA_VARREDURA = 'varredura-pulse.sh';
59
+ /** Comando do template antes da troca do caminho pelo checkout de quem instala. */
60
+ exports.COMANDO_DO_TEMPLATE = '/CAMINHO/orkastery/monitor/varredura-pulse.sh';
61
+ /** Le o crontab do usuario do processo, sem shell e com prazo. */
62
+ function lerCrontabDoSistema() {
63
+ const r = (0, util_1.exec)('crontab', ['-l'], undefined, 5000);
64
+ if (r.ok)
65
+ return { ok: true, texto: r.stdout };
66
+ if (r.error === 'ENOENT')
67
+ return { ok: false, motivo: 'comando crontab ausente nesta maquina' };
68
+ const msg = (r.stderr || r.stdout).trim().split('\n')[0] ?? '';
69
+ return { ok: false, motivo: msg ? `crontab -l falhou: ${msg}` : `crontab -l saiu ${r.code}` };
70
+ }
71
+ const MACROS = { '@hourly': 60, '@daily': 1440, '@midnight': 1440, '@weekly': 10080,
72
+ '@monthly': 43200, '@yearly': 525600, '@annually': 525600 };
73
+ /**
74
+ * Maior intervalo, em minutos, entre duas batidas da linha (so os campos de minuto e hora; dia,
75
+ * mes e dia da semana restritos contam como mais devagar que uma hora). `null` quando a expressao
76
+ * nao e reconhecida: o doctor entao avisa pela linha, sem adivinhar.
77
+ */
78
+ function intervaloEmMinutos(expressao) {
79
+ const campos = expressao.trim().split(/\s+/);
80
+ if (campos.length === 1 && campos[0].startsWith('@'))
81
+ return MACROS[campos[0]] ?? null;
82
+ if (campos.length !== 5)
83
+ return null;
84
+ const [minuto, hora, dia, mes, semana] = campos;
85
+ const minutos = valoresDoCampo(minuto, 0, 59);
86
+ if (!minutos)
87
+ return null;
88
+ if (hora !== '*' || dia !== '*' || mes !== '*' || semana !== '*')
89
+ return Math.max(60, maiorLacuna(minutos, 60));
90
+ return maiorLacuna(minutos, 60);
91
+ }
92
+ function valoresDoCampo(campo, min, max) {
93
+ const valores = new Set();
94
+ for (const parte of campo.split(',')) {
95
+ const m = /^(\*|(\d+)(?:-(\d+))?)(?:\/(\d+))?$/.exec(parte);
96
+ if (!m)
97
+ return null;
98
+ const inicio = m[1] === '*' ? min : Number(m[2]);
99
+ const fim = m[1] === '*' ? max : m[3] !== undefined ? Number(m[3]) : m[4] !== undefined ? max : inicio;
100
+ const passo = m[4] !== undefined ? Number(m[4]) : 1;
101
+ if (passo < 1 || inicio < min || fim > max || inicio > fim)
102
+ return null;
103
+ for (let v = inicio; v <= fim; v += passo)
104
+ valores.add(v);
105
+ }
106
+ return [...valores].sort((a, b) => a - b);
107
+ }
108
+ /** Maior distancia entre dois valores consecutivos, contando a volta do ciclo. */
109
+ function maiorLacuna(valores, ciclo) {
110
+ let maior = 0;
111
+ for (let i = 0; i < valores.length; i++) {
112
+ const proximo = i + 1 < valores.length ? valores[i + 1] : valores[0] + ciclo;
113
+ maior = Math.max(maior, proximo - valores[i]);
114
+ }
115
+ return maior;
116
+ }
117
+ /** Separa a expressao de horario do comando numa linha do crontab. */
118
+ function dividirLinha(linha) {
119
+ const t = linha.trim();
120
+ if (t.startsWith('@')) {
121
+ const m = /^(@\S+)\s+(.+)$/.exec(t);
122
+ return m ? { quando: m[1], comando: m[2] } : null;
123
+ }
124
+ const m = /^(\S+\s+\S+\s+\S+\s+\S+\s+\S+)\s+(.+)$/.exec(t);
125
+ return m ? { quando: m[1], comando: m[2] } : null;
126
+ }
127
+ /** Linhas ativas (nao comentario, nao variavel) que agendam a varredura do pulse. */
128
+ function linhasDaVarredura(crontab) {
129
+ return crontab.split('\n').map(l => l.trim())
130
+ .filter(l => l && !l.startsWith('#') && !/^[A-Za-z_][A-Za-z0-9_]*\s*=/.test(l) && l.includes(exports.SCRIPT_DA_VARREDURA));
131
+ }
132
+ /** Aspas de shell so quando o caminho precisa (a linha vai colada no crontab, que roda por `sh`). */
133
+ function citar(caminho) {
134
+ return /^[A-Za-z0-9_./:@+-]+$/.test(caminho) ? caminho : `'${caminho.replace(/'/g, `'\\''`)}'`;
135
+ }
136
+ /**
137
+ * O script da varredura que este `ork` traz: no pacote do npm, `monitor/` fica ao lado de `dist/`; no
138
+ * checkout, na raiz do repositorio, dois niveis acima de `core/dist/`. `null` quando nenhum existe.
139
+ */
140
+ function scriptDaVarreduraDoOrk(dirDoDist = __dirname) {
141
+ for (const dir of [path.join(dirDoDist, '..', 'monitor'), path.join(dirDoDist, '..', '..', 'monitor')]) {
142
+ const script = path.join(dir, exports.SCRIPT_DA_VARREDURA);
143
+ if (fs.existsSync(script))
144
+ return path.resolve(script);
145
+ }
146
+ return null;
147
+ }
148
+ /**
149
+ * O comando que a linha nova do crontab roda. Ensaio de 03/10/2026 (RM-049): a correcao apontava
150
+ * `<projeto>/monitor/varredura-pulse.sh`, que so existe no checkout do proprio Orkastery; quem instalou
151
+ * pelo npm recebia um caminho que nao existe. Quando o projeto nao traz o script, a linha usa o do `ork`
152
+ * instalado e diz o projeto por `ORK_PULSE_PROJECT` (o cron nao roda no diretorio do projeto).
153
+ */
154
+ function comandoDaVarredura(raizDoProjeto, scriptDoOrk = scriptDaVarreduraDoOrk()) {
155
+ const doProjeto = path.join(raizDoProjeto, 'monitor', exports.SCRIPT_DA_VARREDURA);
156
+ if (fs.existsSync(doProjeto) || !scriptDoOrk)
157
+ return citar(doProjeto);
158
+ return `ORK_PULSE_PROJECT=${citar(raizDoProjeto)} ${citar(scriptDoOrk)}`;
159
+ }
160
+ /**
161
+ * Check "cadencia do pulse no cron". `null` quando o projeto nao tem transporte do pulse: sem
162
+ * `pulse-host.json` nao ha resumo para entregar, e o doctor nao fala de cron.
163
+ */
164
+ function checarCronDoPulse(dirMonitor, raizDoCheckout, ler = lerCrontabDoSistema, scriptDoOrk = scriptDaVarreduraDoOrk()) {
165
+ if (!fs.existsSync(path.join(dirMonitor, 'pulse-host.json')))
166
+ return null;
167
+ const nome = 'cadencia do pulse no cron';
168
+ const comandoSugerido = comandoDaVarredura(raizDoCheckout, scriptDoOrk);
169
+ const linhaNova = (comando) => `${exports.BATIDA_DO_TEMPLATE} ${comando}`;
170
+ const ausente = (detalhe) => ({ nome, nivel: 'warn', detalhe,
171
+ correcao: `adicione ao crontab (crontab -e), como no template monitor/pulse.cron: ${linhaNova(comandoSugerido)}` });
172
+ const leitura = ler();
173
+ if (!leitura.ok)
174
+ return ausente(`pulse-host.json presente, mas sem crontab para ler (${leitura.motivo}): a varredura nao bate`);
175
+ const linhas = linhasDaVarredura(leitura.texto);
176
+ if (linhas.length === 0)
177
+ return ausente(`pulse-host.json presente, e nenhuma linha do crontab roda ${exports.SCRIPT_DA_VARREDURA}: o resumo nao sai`);
178
+ const lentas = [];
179
+ for (const linha of linhas) {
180
+ const partes = dividirLinha(linha);
181
+ const intervalo = partes ? intervaloEmMinutos(partes.quando) : null;
182
+ if (intervalo === null || intervalo > 15)
183
+ lentas.push({ linha, comando: partes?.comando ?? comandoSugerido, intervalo });
184
+ }
185
+ if (lentas.length < linhas.length) {
186
+ return { nome, nivel: 'ok', detalhe: `varredura no cron a cada 15 minutos ou menos (${linhas.length} linha(s))` };
187
+ }
188
+ const l = lentas[0];
189
+ const ritmo = l.intervalo === null ? 'com horario que o doctor nao le' : l.intervalo === 60 ? 'de hora em hora'
190
+ : `a cada ${l.intervalo} minutos`;
191
+ return { nome, nivel: 'warn',
192
+ detalhe: `a varredura do pulse bate ${ritmo} (\`${l.linha}\`): a cadencia por tag (RM-039) pede 15 minutos`,
193
+ correcao: `troque a linha no crontab (crontab -e) por: ${linhaNova(l.comando)}` };
194
+ }