@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
package/dist/ocupacao.js CHANGED
@@ -25,6 +25,7 @@ exports.EVENTOS_QUE_DESTRAVAM = exports.PAUSA_PREVISTA = void 0;
25
25
  exports.ehAprovacaoHumana = ehAprovacaoHumana;
26
26
  exports.ehPausaPrevista = ehPausaPrevista;
27
27
  exports.faseTemPausaPrevista = faseTemPausaPrevista;
28
+ exports.sessaoLivreDaVaga = sessaoLivreDaVaga;
28
29
  exports.avaliarOcupacao = avaliarOcupacao;
29
30
  const ledger_1 = require("./ledger");
30
31
  const sessoes_adopt_1 = require("./sessoes-adopt");
@@ -70,15 +71,50 @@ function ultimaSessao(thread, eventos) {
70
71
  /**
71
72
  * Carimbo do ultimo sinal de vida: o evento mais novo do ledger que seja TRABALHO da
72
73
  * thread. `slot_released` fica de fora: e contabilidade do escalonador, e se contasse
73
- * como atividade a thread stale rejuvenesceria no exato momento em que e expulsa.
74
+ * como atividade a thread stale rejuvenesceria no exato momento em que e expulsa. O
75
+ * `slot_refused` (RM-037) tambem: despacho recusado por vaga nao e trabalho.
74
76
  */
75
77
  function ultimaAtividadeEm(eventos) {
76
78
  for (let i = eventos.length - 1; i >= 0; i--) {
77
- if (eventos[i].tipo !== ledger_1.TIPOS_DE_EVENTO.vagaLiberada)
79
+ const t = eventos[i].tipo;
80
+ if (t !== ledger_1.TIPOS_DE_EVENTO.vagaLiberada && t !== ledger_1.TIPOS_DE_EVENTO.vagaRecusada)
78
81
  return eventos[i].ts;
79
82
  }
80
83
  return null;
81
84
  }
85
+ /**
86
+ * RM-037 (rm037defeito, defeito 3; achado N1 da rodada 2 do CHECK): a sessao com conducao VIVA que mesmo
87
+ * assim nao ocupa vaga do despacho. Sao tres: a parada (sem trabalho no ledger ha mais que `staleMin`,
88
+ * o mesmo teto do escalonador) e a que escalou para o humano depois de comecar (`gate_blocked`
89
+ * `human.pending` sem destravamento, ou sessao bloqueada no runtime), e a que o pulse declarou em silencio
90
+ * (`runtime.silencio`). A pausa PREVISTA ao fim do bloco nao entra: ela e gravada no proprio
91
+ * despacho e a sessao roda ate o fim do bloco. O impedimento de verify tambem nao: a sessao corrige e
92
+ * verifica de novo. Somente leitura.
93
+ */
94
+ function sessaoLivreDaVaga(eventos, desde, agora, staleMin,
95
+ /** A-3 do CHECK 4: o bloqueio no runtime e por sessao; o de outra sessao (a sucedida) nao libera esta. */
96
+ sessionId) {
97
+ const ultima = ultimaAtividadeEm(eventos);
98
+ if (ultima !== null && minutos(ultima, agora) >= staleMin)
99
+ return 'stale';
100
+ const inicio = Date.parse(desde);
101
+ const depois = eventos.filter((e) => Date.parse(e.ts) >= inicio);
102
+ // A-2 e S-7 do CHECK 3: o silencio que o pulse declarou (`runtime.silencio`) e a sessao bloqueada no runtime
103
+ // esperando o dono tambem nao executam; os dois saem da conta ate o evento que os resolve.
104
+ for (let i = depois.length - 1; i >= 0; i--) {
105
+ const e = depois[i];
106
+ if (e.tipo !== ledger_1.TIPOS_DE_EVENTO.gateBloqueado || (e.motivo !== 'human.pending' && e.motivo !== 'runtime.silencio'))
107
+ continue;
108
+ const resolvida = depois.slice(i + 1).some((p) => exports.EVENTOS_QUE_DESTRAVAM.includes(p.tipo) || ehAprovacaoHumana(p));
109
+ if (!resolvida)
110
+ return e.motivo;
111
+ }
112
+ const bloqueio = depois.filter((e) => (e.tipo === 'sessao_bloqueada' || e.tipo === 'sessao_destravada') &&
113
+ (!sessionId || e.sessionId === sessionId)).at(-1);
114
+ if (bloqueio?.tipo === 'sessao_bloqueada')
115
+ return 'human.pending';
116
+ return null;
117
+ }
82
118
  /** A thread tem pausa humana ABERTA? Mesmas tres fontes do monitor, resumidas. */
83
119
  function pausaHumanaAberta(thread, eventos) {
84
120
  if (thread.status === 'pausada' && faseTemPausaPrevista(thread)) {
@@ -51,9 +51,11 @@ const project_state_1 = require("./project-state");
51
51
  const ledger_1 = require("./ledger");
52
52
  const policies_1 = require("./policies");
53
53
  const horario_1 = require("./horario");
54
+ const experiencia_1 = require("./experiencia");
55
+ const yaml_1 = require("./yaml");
54
56
  exports.CONTRATO_ONBOARDING = 'ork.onboarding/v1';
55
57
  exports.PAUTA_ONBOARDING = [
56
- { etapa: 'maestro', pergunta: `Quem conduz o projeto, quais são seus objetivos e preferências, e em qual fuso horário o Orkastery deve mostrar horários? Informe o fuso como nome IANA, ex.: {"fuso":"${horario_1.FUSO_DE_BRASILIA}"}; ele orienta ${horario_1.CHAVE_DO_FUSO} no manifesto, sem editá-lo.` },
58
+ { etapa: 'maestro', pergunta: `Quem conduz o projeto, quais são seus objetivos e preferências, e em qual fuso horário o Orkastery deve mostrar horários? Informe o fuso como nome IANA, ex.: {"fuso":"${horario_1.FUSO_DE_BRASILIA}"}; ele orienta ${horario_1.CHAVE_DO_FUSO} no manifesto, sem editá-lo. Preferências em {"owner":{...}} gravam owner no manifesto.` },
57
59
  { etapa: 'credenciais', pergunta: 'Quais provedores públicos e nomes de variáveis em env serão usados? Segredos somente em ~/.hermes/.env; nunca informe valores.' },
58
60
  { etapa: 'bancos', pergunta: 'Quais bancos públicos e nomes de variáveis em env configuram as conexões? Nunca informe DSN ou senha; valores somente em ~/.hermes/.env.' },
59
61
  { etapa: 'memoria', pergunta: 'Deseja OrkMind? Informe {"modo":"orkmind"} ou {"modo":"files"}; a escolha orienta memory.mode no manifesto, sem editá-lo.' },
@@ -118,6 +120,16 @@ function validarConteudo(etapa, conteudo) {
118
120
  // I-35: o fuso do dono, quando informado, precisa ser um nome IANA que o Intl aceita.
119
121
  if (etapa === 'maestro' && objeto(conteudo) && Object.hasOwn(conteudo, 'fuso') && (0, horario_1.normalizarFuso)(conteudo.fuso) === undefined)
120
122
  invalido();
123
+ if (etapa === 'maestro' && objeto(conteudo) && Object.hasOwn(conteudo, 'owner')) {
124
+ if (!objeto(conteudo.owner) || Object.keys(conteudo.owner).some(k => !['language', 'timezone', 'depth', 'experience'].includes(k)))
125
+ invalido();
126
+ try {
127
+ (0, experiencia_1.validarPreferencias)(conteudo.owner);
128
+ }
129
+ catch {
130
+ invalido();
131
+ }
132
+ }
121
133
  // Etapas sensíveis usam um formato fechado: não há espaço para texto de credenciais.
122
134
  if (etapa === 'credenciais' || etapa === 'bancos') {
123
135
  if (!objeto(conteudo))
@@ -263,18 +275,109 @@ function comTrava(raiz, alterar) {
263
275
  fs.unlinkSync(trava);
264
276
  }
265
277
  }
278
+ /** Comentário depois de valor simples ou entre aspas completas; `#` dentro das aspas é valor. */
279
+ const COMENTARIO_NA_LINHA = /^\s*[^:#]+:\s*(?:"(?:[^"\\]|\\.)*"|'(?:[^']|'')*'|[^"'#\s][^#]*?)?(\s+#.*)$/;
280
+ /** Altera somente as chaves públicas solicitadas, preservando comentários e outras seções. */
281
+ function prepararPreferencias(raiz, conteudo) {
282
+ if (!objeto(conteudo) || !objeto(conteudo.owner) || !Object.keys(conteudo.owner).length)
283
+ return null;
284
+ const c = (0, manifest_1.exigirManifesto)((0, project_state_1.projectState)(raiz).root), arquivo = c.caminho;
285
+ const stat = fs.lstatSync(arquivo);
286
+ if (!stat.isFile() || stat.nlink !== 1)
287
+ throw Error('experiencia.manifesto.unsafe');
288
+ const anterior = fs.readFileSync(arquivo, 'utf8'), eol = anterior.includes('\r\n') ? '\r\n' : '\n';
289
+ const linhas = anterior.split(eol), ocorrencias = linhas.map((l, i) => /^(?:owner|"owner"|'owner')\s*:/.test(l) ? i : -1).filter(i => i >= 0);
290
+ if (ocorrencias.length > 1)
291
+ throw Error('experiencia.manifesto.conflict: owner duplicado');
292
+ let inicio = ocorrencias[0];
293
+ // Seção nova antes da quebra final, sem linha em branco sobrando nem perda do newline.
294
+ if (inicio === undefined) {
295
+ inicio = linhas.at(-1) === '' ? linhas.length - 1 : linhas.length;
296
+ linhas.splice(inicio, 0, 'owner:');
297
+ }
298
+ if (!/^(?:owner|"owner"|'owner')\s*:\s*(?:\{\}\s*)?(?:#.*)?$/.test(linhas[inicio]))
299
+ throw Error('experiencia.manifesto.conflict: owner não é mapa em bloco');
300
+ linhas[inicio] = linhas[inicio].replace('{}', '');
301
+ let fim = inicio + 1;
302
+ while (fim < linhas.length && !/^[^\s#][^:]*:/.test(linhas[fim]))
303
+ fim++;
304
+ for (const [k, v] of Object.entries((0, experiencia_1.validarPreferencias)(conteudo.owner))) {
305
+ const re = new RegExp('^ (?:' + k + '|"' + k + '"|\x27' + k + '\x27)\\s*:');
306
+ const indices = linhas.slice(inicio + 1, fim).map((l, i) => re.test(l) ? i + inicio + 1 : -1).filter(i => i >= 0);
307
+ if (indices.length > 1)
308
+ throw Error('experiencia.manifesto.conflict: preferência duplicada');
309
+ const linha = ` ${k}: ${JSON.stringify(v)}`;
310
+ // Comentário na mesma linha fica; a conferência semântica abaixo pega leitura errada.
311
+ if (indices.length)
312
+ linhas[indices[0]] = linha + (COMENTARIO_NA_LINHA.exec(linhas[indices[0]])?.[1] ?? '');
313
+ else {
314
+ linhas.splice(inicio + 1, 0, linha);
315
+ fim++;
316
+ }
317
+ }
318
+ const proximo = linhas.join(eol);
319
+ if (Buffer.byteLength(proximo) > manifest_1.LIMITE_MANIFESTO_BYTES)
320
+ throw Error('experiencia.manifesto.large');
321
+ // Sintaxes/indentações não representadas pelo editor não podem descartar dados do YAML.
322
+ const dados = (0, yaml_1.lerYaml)(anterior), novos = (0, yaml_1.lerYaml)(proximo);
323
+ if (!objeto(dados) || !objeto(novos))
324
+ throw Error('experiencia.manifesto.conflict');
325
+ const esperado = { ...dados, owner: { ...(objeto(dados.owner) ? dados.owner : {}), ...(0, experiencia_1.validarPreferencias)(conteudo.owner) } };
326
+ if (jsonCanonico(novos) !== jsonCanonico(esperado))
327
+ throw Error('experiencia.manifesto.conflict: edição perderia dados');
328
+ return { arquivo, anterior, proximo, modo: stat.mode };
329
+ }
266
330
  function gravarEtapa(raiz, nome, conteudo, por = 'owner') {
267
331
  const etapa = exigirEtapa(nome);
268
332
  exigirAutor(por);
269
333
  validarConteudo(etapa, conteudo);
270
334
  return comTrava(raiz, () => {
335
+ const solicitadas = conteudo;
271
336
  const { estado } = lerParaMutacao(raiz), anterior = estado.etapas[etapa];
272
- if (anterior && jsonCanonico(anterior.conteudo) === jsonCanonico(conteudo))
337
+ // Configurar preferências não apaga objetivos ou outras respostas da etapa maestro.
338
+ if (etapa === 'maestro' && objeto(conteudo) && objeto(conteudo.owner) && anterior && objeto(anterior.conteudo)) {
339
+ conteudo = { ...anterior.conteudo, ...conteudo, owner: {
340
+ ...(objeto(anterior.conteudo.owner) ? anterior.conteudo.owner : {}), ...conteudo.owner,
341
+ } };
342
+ validarConteudo(etapa, conteudo);
343
+ }
344
+ validarConteudo(etapa, conteudo);
345
+ // Persistir só chaves deste pedido: uma resposta antiga não vence edição explícita do manifesto.
346
+ validarConteudo(etapa, solicitadas);
347
+ const preferencias = etapa === 'maestro' ? prepararPreferencias(raiz, solicitadas) : null;
348
+ const manifestoMuda = !!preferencias && preferencias.proximo !== preferencias.anterior;
349
+ const mesmaResposta = !!anterior && jsonCanonico(anterior.conteudo) === jsonCanonico(conteudo);
350
+ // Resposta igual com o manifesto editado à mão depois: o pedido explícito vale, e a entrevista
351
+ // guarda autoria e horário da resposta original.
352
+ if (mesmaResposta && !manifestoMuda)
273
353
  return estado;
274
- const timestamp = new Date().toISOString();
275
- estado.etapas[etapa] = { respondidaEm: timestamp, por, conteudo };
276
- estado.atualizadoEm = dataMaisRecente([estado.atualizadoEm, timestamp]);
277
- persistir(raiz, estado);
354
+ if (!mesmaResposta) {
355
+ const timestamp = new Date().toISOString();
356
+ estado.etapas[etapa] = { respondidaEm: timestamp, por, conteudo };
357
+ estado.atualizadoEm = dataMaisRecente([estado.atualizadoEm, timestamp]);
358
+ }
359
+ const salvarEstado = () => { if (!mesmaResposta)
360
+ persistir(raiz, estado); };
361
+ if (preferencias && manifestoMuda) {
362
+ const temporario = preferencias.arquivo + '.' + (0, node_crypto_1.randomUUID)() + '.tmp';
363
+ try {
364
+ fs.writeFileSync(temporario, preferencias.proximo, { flag: 'wx', mode: preferencias.modo });
365
+ fs.renameSync(temporario, preferencias.arquivo);
366
+ try {
367
+ salvarEstado();
368
+ }
369
+ catch (e) {
370
+ fs.writeFileSync(preferencias.arquivo, preferencias.anterior);
371
+ throw e;
372
+ }
373
+ }
374
+ finally {
375
+ if (fs.existsSync(temporario))
376
+ fs.unlinkSync(temporario);
377
+ }
378
+ }
379
+ else
380
+ salvarEstado();
278
381
  (0, ledger_1.registrar)((0, manifest_1.dirEstado)((0, project_state_1.projectState)(raiz).root), 'projeto', ledger_1.TIPOS_DE_EVENTO.onboardingRegistrado, { acao: 'set', etapa, por, sha256: sha(conteudo) });
279
382
  return estado;
280
383
  });
@@ -296,8 +399,11 @@ function resetarOnboarding(raiz, nome, por = 'owner') {
296
399
  return estado;
297
400
  });
298
401
  }
299
- function textoDaPauta(estado = onboardingPadrao()) {
402
+ function textoDaPauta(estado = onboardingPadrao(), owner = {}) {
403
+ const p = (0, experiencia_1.resolverExperiencia)(owner);
300
404
  return ['Onboarding do projeto (' + exports.CONTRATO_ONBOARDING + ')',
405
+ `Experiência recomendada: ${p.language}, ${p.timezone}, resposta ${p.depth}. Ativar com esses valores, configurar ou desativar?`,
406
+ 'Registre a escolha em maestro com {"owner":{"language":"' + p.language + '","timezone":"' + p.timezone + '","depth":"' + p.depth + '","experience":true}}; use experience:false para desativar. Consulta não registra resposta.',
301
407
  ...exports.PAUTA_ONBOARDING.map(({ etapa, pergunta }) => `${estado.etapas[etapa] ? '[respondida]' : '[pendente]'} ${etapa}: ${pergunta}`),
302
408
  'Responda com ork onboarding set <etapa> --conteudo <JSON> --por <quem>.'].join('\n');
303
409
  }
package/dist/orkmind.js CHANGED
@@ -54,17 +54,22 @@ var __importStar = (this && this.__importStar) || (function () {
54
54
  };
55
55
  })();
56
56
  Object.defineProperty(exports, "__esModule", { value: true });
57
- exports.DriverEmMemoria = exports.DriverCliOrkMind = exports.LIMITE_CONSULTA_MAXIMO = exports.LIMITE_CONSULTA_PADRAO = exports.ESQUEMA_ORKMIND = exports.DIMENSOES = exports.COLECOES_DE_GOVERNANCA = exports.COLECOES_DO_ORK = void 0;
57
+ exports.DriverEmMemoria = exports.DriverCliOrkMind = exports.LIMITE_CONSULTA_MAXIMO = exports.LIMITE_CONSULTA_PADRAO = exports.CODIGOS_DE_EMBEDDING = exports.MODELO_DE_EMBEDDING = exports.EMBED_MAX_CARACTERES = exports.EMBED_MAX_TEXTOS = exports.ESQUEMA_ORKMIND = exports.DIMENSOES = exports.COLECOES_DE_GOVERNANCA = exports.COLECOES_DO_ORK = void 0;
58
58
  exports.enderecoDeMemoria = enderecoDeMemoria;
59
59
  exports.lerEndereco = lerEndereco;
60
60
  exports.violacoesDeGovernanca = violacoesDeGovernanca;
61
61
  exports.casaTags = casaTags;
62
+ exports.textoDeBuscaValido = textoDeBuscaValido;
63
+ exports.chaveDeEmbeddingAceita = chaveDeEmbeddingAceita;
64
+ exports.validarPedidoDeEmbedding = validarPedidoDeEmbedding;
65
+ exports.conferirRespostaDeEmbedding = conferirRespostaDeEmbedding;
62
66
  exports.criarEscopoDeLeitura = criarEscopoDeLeitura;
63
67
  exports.validarConsultaDelimitada = validarConsultaDelimitada;
64
68
  exports.tagsDoEscopo = tagsDoEscopo;
65
69
  exports.pertenceAoEscopo = pertenceAoEscopo;
66
70
  exports.concluirConsultaDelimitada = concluirConsultaDelimitada;
67
71
  exports.entradaDoJson = entradaDoJson;
72
+ exports.vetorDeDuble = vetorDeDuble;
68
73
  exports.camposNativos = camposNativos;
69
74
  exports.agenteProspectivoValido = agenteProspectivoValido;
70
75
  exports.prospectivaCompativel = prospectivaCompativel;
@@ -180,6 +185,66 @@ function casaTags(entrada, consulta) {
180
185
  }
181
186
  return todasCasaram || (entrada.mandatory && algumaCasou);
182
187
  }
188
+ /** Texto de busca: nao vazio, sem caractere de controle, ate 2.000 caracteres. */
189
+ function textoDeBuscaValido(texto) {
190
+ return typeof texto === 'string' && texto.trim() !== '' && texto.length <= 2000 && !/[\x00-\x1f\x7f]/.test(texto);
191
+ }
192
+ /** Limites da operacao `embed` (D10): lote e tamanho de texto, sem truncar em silencio. */
193
+ exports.EMBED_MAX_TEXTOS = 32;
194
+ exports.EMBED_MAX_CARACTERES = 24_000;
195
+ /** Domicilio unico do formato `org/nome` de modelo de embedding (primario e fallback). */
196
+ exports.MODELO_DE_EMBEDDING = /^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*$/;
197
+ /**
198
+ * O valor lido da variavel declarada so vale como chave se nao parecer outra coisa: URL, DSN,
199
+ * texto com espaco ou a propria DSN da memoria nunca vao ao provider (nome trocado por engano).
200
+ */
201
+ function chaveDeEmbeddingAceita(valor, dsn) {
202
+ if (valor === '' || /\s/.test(valor) || valor.includes('://') || valor === dsn.trim())
203
+ return false;
204
+ // A senha da DSN sozinha tambem nunca vai ao provider como Bearer.
205
+ try {
206
+ const senha = new URL(dsn).password;
207
+ if (senha && (valor === senha || valor === decodeURIComponent(senha)))
208
+ return false;
209
+ }
210
+ catch { /* DSN opaca (key=value): a comparacao inteira acima ja vale */ }
211
+ return true;
212
+ }
213
+ /** Codigos tipados que a ponte pode devolver na operacao `embed`. */
214
+ exports.CODIGOS_DE_EMBEDDING = [
215
+ 'memory.embed.invalid', 'embeddings.chave-ausente', 'embeddings.provider-indisponivel', 'embeddings.timeout',
216
+ 'embeddings.local-ausente', 'embeddings.dependencia-ausente', 'embeddings.dimensao-divergente',
217
+ 'embeddings.conteudo-recusado',
218
+ ];
219
+ /** Copia validada: o que vai a ponte e exatamente o que foi conferido aqui. */
220
+ function validarPedidoDeEmbedding(bruto) {
221
+ const p = bruto;
222
+ if (!objetoDeConsulta(p) || Object.keys(p).some(k => !['papel', 'alvo', 'modelo', 'dim', 'textos'].includes(k)) ||
223
+ !['consulta', 'documento'].includes(p.papel) || !['primario', 'fallback'].includes(p.alvo) ||
224
+ typeof p.modelo !== 'string' || !exports.MODELO_DE_EMBEDDING.test(p.modelo) ||
225
+ !Number.isInteger(p.dim) || Number(p.dim) < 32 || Number(p.dim) > 4096 ||
226
+ !Array.isArray(p.textos) || p.textos.length < 1 || p.textos.length > exports.EMBED_MAX_TEXTOS ||
227
+ !p.textos.every(t => typeof t === 'string' && t.trim() !== '' && t.length <= exports.EMBED_MAX_CARACTERES)) {
228
+ throw new Error('memory.embed.invalid');
229
+ }
230
+ return { papel: p.papel, alvo: p.alvo,
231
+ modelo: p.modelo, dim: Number(p.dim), textos: [...p.textos] };
232
+ }
233
+ /** Resposta da ponte conferida: um vetor finito por texto, todos na dimensao pedida. */
234
+ function conferirRespostaDeEmbedding(r, pedido) {
235
+ const o = r;
236
+ if (!objetoDeConsulta(o) || o.alvo !== pedido.alvo || o.modelo !== pedido.modelo || o.dim !== pedido.dim ||
237
+ !Array.isArray(o.vetores))
238
+ throw new Error('memory.transport.embed');
239
+ if (o.vetores.length !== pedido.textos.length || !o.vetores.every(v => Array.isArray(v) && v.length === pedido.dim &&
240
+ v.every(x => typeof x === 'number' && Number.isFinite(x))))
241
+ throw new Error('embeddings.dimensao-divergente');
242
+ const truncados = o.truncados === undefined ? [] : o.truncados;
243
+ if (!Array.isArray(truncados) || !truncados.every(i => Number.isInteger(i) && i >= 0 && i < pedido.textos.length)) {
244
+ throw new Error('memory.transport.embed');
245
+ }
246
+ return { alvo: pedido.alvo, modelo: pedido.modelo, dim: pedido.dim, vetores: o.vetores, truncados: truncados };
247
+ }
183
248
  exports.LIMITE_CONSULTA_PADRAO = 100;
184
249
  exports.LIMITE_CONSULTA_MAXIMO = 1000;
185
250
  function objetoDeConsulta(x) {
@@ -314,9 +379,19 @@ class DriverCliOrkMind {
314
379
  throw new Error('memory.transport.interpreter: shebang nao suportado');
315
380
  return match[2] ? [match[1], match[2]] : [match[1]];
316
381
  }
382
+ /** Valor da chave de embedding no ambiente do `ork`; nunca guardado no driver. */
383
+ chaveDeEmbedding() {
384
+ const nome = this.config.variavelDaChaveDeEmbedding ?? '';
385
+ const valor = nome ? (process.env[nome] ?? '').trim() : '';
386
+ return chaveDeEmbeddingAceita(valor, this.config.dsn) ? valor : '';
387
+ }
317
388
  contemSegredo(valor) {
318
389
  const segredo = this.config.dsn;
319
390
  const proibidos = [segredo];
391
+ const nome = this.config.variavelDaChaveDeEmbedding ?? '';
392
+ const chave = nome ? (process.env[nome] ?? '').trim() : '';
393
+ if (chave.length >= 12)
394
+ proibidos.push(chave);
320
395
  try {
321
396
  const senha = new URL(segredo).password;
322
397
  if (senha.length >= 12)
@@ -328,7 +403,7 @@ class DriverCliOrkMind {
328
403
  : !!x && typeof x === 'object' && Object.entries(x).some(([k, v]) => examinar(k) || examinar(v));
329
404
  return examinar(valor);
330
405
  }
331
- rodar(pedido) {
406
+ rodar(pedido, timeoutMs = this.config.timeoutMs) {
332
407
  if (!this.config.dsn.trim())
333
408
  throw new Error('dsn.env-ausente');
334
409
  if (this.contemSegredo(pedido))
@@ -338,13 +413,19 @@ class DriverCliOrkMind {
338
413
  path.join(__dirname, '../../assets/orkmind_bridge.py')].find(p => fs.existsSync(p));
339
414
  if (!ponte)
340
415
  throw new Error('memory.transport.bridge: ponte ausente no pacote');
341
- const env = { ORKMIND_DATABASE_URL: this.config.dsn, PYTHONDONTWRITEBYTECODE: '1' };
342
- for (const nome of ['PATH', 'HOME', 'LANG', 'LC_ALL', 'SYSTEMROOT']) {
416
+ // I-38 (D1): `embed` nao toca a base, entao nao recebe a DSN; a chave vai SO ao `embed` primario.
417
+ const embed = pedido.op === 'embed';
418
+ const env = embed ? { PYTHONDONTWRITEBYTECODE: '1' }
419
+ : { ORKMIND_DATABASE_URL: this.config.dsn, PYTHONDONTWRITEBYTECODE: '1' };
420
+ for (const nome of ['PATH', 'HOME', 'LANG', 'LC_ALL', 'SYSTEMROOT', 'HF_HOME', 'HF_HUB_CACHE']) {
343
421
  if (process.env[nome])
344
422
  env[nome] = process.env[nome];
345
423
  }
424
+ const chave = embed && pedido.alvo === 'primario' ? this.chaveDeEmbedding() : '';
425
+ if (chave)
426
+ env.ORKMIND_EMBEDDING_API_KEY = chave;
346
427
  const r = (0, node_child_process_1.spawnSync)(python, [...prefixo, ponte], {
347
- encoding: 'utf8', input: JSON.stringify(pedido), timeout: this.config.timeoutMs,
428
+ encoding: 'utf8', input: JSON.stringify(pedido), timeout: timeoutMs,
348
429
  maxBuffer: 32 * 1024 * 1024, env,
349
430
  });
350
431
  // Nunca reutilizar stdout, stderr, error.message ou causa de uma falha do filho.
@@ -359,7 +440,7 @@ class DriverCliOrkMind {
359
440
  code = JSON.parse(r.stdout)?.error;
360
441
  }
361
442
  catch { /* nenhum detalhe do filho */ }
362
- throw new Error(['memory.schema.absent', 'memory.legacy.provenance-collision', 'memory.prospective.marker-invalid', 'memory.native.schema-mismatch', 'memory.query.invalid', 'memory.query.window-saturated', 'memory.query.scope-violation'].includes(code) ? code : 'memory.transport.failed');
443
+ throw new Error(['memory.schema.absent', 'memory.legacy.provenance-collision', 'memory.prospective.marker-invalid', 'memory.native.schema-mismatch', 'memory.query.invalid', 'memory.query.window-saturated', 'memory.query.scope-violation', 'memory.health.invalid', 'memory.fts.invalid', ...exports.CODIGOS_DE_EMBEDDING].includes(code) ? code : 'memory.transport.failed');
363
444
  }
364
445
  let json;
365
446
  try {
@@ -372,18 +453,33 @@ class DriverCliOrkMind {
372
453
  throw new Error('memory.transport.secret: resposta recusada');
373
454
  return json;
374
455
  }
456
+ /** O detalhe sai do que a operacao `health` observou, nunca de frase fixa (I-38 T4). */
375
457
  disponivel() {
376
458
  if (this.saude)
377
459
  return this.saude;
378
460
  try {
379
- this.contagens();
380
- this.saude = { ok: true, detalhe: 'ponte OrkMind respondeu; embeddings desativados' };
461
+ const saude = this.sondarSaude();
462
+ const total = Object.values(saude.contagens).reduce((a, b) => a + b, 0);
463
+ this.saude = { ok: true, saude, detalhe: `ponte OrkMind respondeu a sonda health (orkmind ${saude.orkmind ?? 'sem versao declarada'}, ` +
464
+ `${total} entrada(s) em ${Object.keys(saude.contagens).length} colecao(oes))` };
381
465
  }
382
466
  catch (e) {
383
467
  this.saude = { ok: false, detalhe: e.message };
384
468
  }
385
469
  return this.saude;
386
470
  }
471
+ sondarSaude() {
472
+ const r = this.rodar({ op: 'health' });
473
+ const contagens = r?.contagens;
474
+ const fallback = r?.fallback;
475
+ if (!objetoDeConsulta(r) || !objetoDeConsulta(contagens) || !objetoDeConsulta(fallback) ||
476
+ !Object.values(contagens).every(v => Number.isInteger(v) && Number(v) >= 0) ||
477
+ typeof fallback.dependencias !== 'boolean' || !(r.orkmind === null || typeof r.orkmind === 'string')) {
478
+ throw new Error('memory.transport.health');
479
+ }
480
+ return { contagens: contagens, orkmind: r.orkmind,
481
+ fallback: { dependencias: fallback.dependencias } };
482
+ }
387
483
  contagens() {
388
484
  const r = this.rodar({ op: 'stats' });
389
485
  if (!r || Array.isArray(r) || typeof r !== 'object' ||
@@ -444,6 +540,21 @@ class DriverCliOrkMind {
444
540
  throw new Error('memory.transport.readback');
445
541
  return e;
446
542
  }
543
+ embeddar(pedido, opcoes = {}) {
544
+ const p = validarPedidoDeEmbedding(pedido);
545
+ if (p.alvo === 'primario' && !this.chaveDeEmbedding())
546
+ throw new Error('embeddings.chave-ausente');
547
+ return conferirRespostaDeEmbedding(this.rodar({ op: 'embed', ...p }, opcoes.timeoutMs), p);
548
+ }
549
+ buscarTexto(tenant, texto) {
550
+ if (!textoDeConsulta(tenant, 128) || !textoDeBuscaValido(texto))
551
+ throw new Error('memory.fts.invalid');
552
+ const r = this.rodar({ op: 'fts', tenant, texto });
553
+ if (!objetoDeConsulta(r) || !Array.isArray(r.ids) || !r.ids.every(id => typeof id === 'string' && id)) {
554
+ throw new Error('memory.transport.fts');
555
+ }
556
+ return r.ids;
557
+ }
447
558
  submeterHandoff(pedido) {
448
559
  try {
449
560
  const r = this.rodar({ op: 'handoff', pedido });
@@ -458,6 +569,15 @@ class DriverCliOrkMind {
458
569
  }
459
570
  }
460
571
  exports.DriverCliOrkMind = DriverCliOrkMind;
572
+ /** Vetor do dublê: cada palavra soma 1 numa posicao por hash; normalizado. Deterministico. */
573
+ function vetorDeDuble(texto, dim) {
574
+ const v = new Array(dim).fill(0);
575
+ const palavras = texto.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().match(/[a-z0-9]+/g) ?? [];
576
+ for (const palavra of palavras)
577
+ v[(0, node_crypto_1.createHash)('sha256').update(palavra).digest().readUInt32BE(0) % dim] += 1;
578
+ const norma = Math.hypot(...v);
579
+ return norma === 0 ? v.map((_, i) => (i === 0 ? 1 : 0)) : v.map(x => x / norma);
580
+ }
461
581
  function tagsFixasDaDecisao(tags) {
462
582
  const fases = new Set(['GOAL', 'PLAN', 'GO', 'CHECK', 'SHIP', 'MASTER']);
463
583
  return Object.fromEntries(Object.entries(tags).filter(([k]) => k !== 'agent').map(([k, values]) => [k, [...new Set(values.filter(v => !(k === 'skill' && fases.has(v)) &&
@@ -547,6 +667,10 @@ class DriverEmMemoria {
547
667
  contador = 0;
548
668
  /** Quando false, o driver se declara indisponivel: e o teste de degradacao. */
549
669
  ligado = true;
670
+ /** I-38: quais alvos de embedding o dublê atende; o resto responde com o motivo tipado. */
671
+ embedding = { primario: true, fallback: true };
672
+ /** Pedidos de embedding recebidos, para as assercoes de custo (chamadas ao provider). */
673
+ pedidosDeEmbedding = [];
550
674
  constructor(entradas = []) {
551
675
  for (const e of entradas)
552
676
  this.entradas.push(e);
@@ -554,7 +678,8 @@ class DriverEmMemoria {
554
678
  }
555
679
  disponivel() {
556
680
  return this.ligado
557
- ? { ok: true, detalhe: `driver em memoria com ${this.entradas.length} entrada(s)` }
681
+ ? { ok: true, detalhe: `driver em memoria com ${this.entradas.length} entrada(s)`,
682
+ saude: { contagens: this.contagens(), orkmind: null, fallback: { dependencias: this.embedding.fallback } } }
558
683
  : { ok: false, detalhe: 'driver em memoria desligado' };
559
684
  }
560
685
  adicionar(entrada) {
@@ -664,6 +789,35 @@ class DriverEmMemoria {
664
789
  return { ok: true, id: handoff.id, duplicada: false, collection: 'handoff', detalhe: 'duplo de transporte G3',
665
790
  session_entry_id: sessao.id, package_entry_id: pacote.id, package_id: packageId, readback: true };
666
791
  }
792
+ /**
793
+ * Dublê declarado de embedding: saco de palavras com hash, sem semantica nenhuma. Serve para
794
+ * provar contrato (dimensao, tenant, idempotencia, custo) sem rede e sem modelo.
795
+ */
796
+ embeddar(pedido) {
797
+ const p = validarPedidoDeEmbedding(pedido);
798
+ if (!this.ligado)
799
+ throw new Error('memory.transport.unavailable');
800
+ if (!this.embedding[p.alvo])
801
+ throw new Error(p.alvo === 'primario' ? 'embeddings.chave-ausente' : 'embeddings.local-ausente');
802
+ this.pedidosDeEmbedding.push(p);
803
+ return { alvo: p.alvo, modelo: p.modelo, dim: p.dim, vetores: p.textos.map(t => vetorDeDuble(t, p.dim)) };
804
+ }
805
+ /** Dublê de FTS: todas as palavras da consulta no conteudo, sem stemming, com tenant obrigatorio. */
806
+ buscarTexto(tenant, texto) {
807
+ if (!textoDeConsulta(tenant, 128) || !textoDeBuscaValido(texto))
808
+ throw new Error('memory.fts.invalid');
809
+ if (!this.ligado)
810
+ throw new Error('memory.transport.unavailable');
811
+ const palavras = (t) => t.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().match(/[a-z0-9]+/g) ?? [];
812
+ const consulta = palavras(texto);
813
+ return this.entradas
814
+ .filter(e => exports.COLECOES_DO_ORK.includes(e.collection) && (e.tags.project ?? []).includes(tenant))
815
+ .map(e => ({ id: e.id, conteudo: palavras(e.content) }))
816
+ .filter(e => consulta.every(p => e.conteudo.includes(p)))
817
+ .map(e => ({ id: e.id, peso: e.conteudo.filter(p => consulta.includes(p)).length }))
818
+ .sort((a, b) => b.peso - a.peso || a.id.localeCompare(b.id))
819
+ .map(e => e.id);
820
+ }
667
821
  /** So para os testes: injeta uma entrada que o `ork` nunca gravaria (humano, mandatory). */
668
822
  semear(entrada) {
669
823
  this.entradas.push(entrada);
@@ -685,6 +839,8 @@ function configDoManifesto(manifesto) {
685
839
  dsn: variavel ? (process.env[variavel] ?? '') : '',
686
840
  timeoutMs: manifesto.memory.timeout_ms || 15000,
687
841
  tenant: manifesto.memory.tenant || manifesto.project.name,
842
+ // Lido direto do bloco: este modulo carrega sem o parser do manifesto (memory-native-schema).
843
+ variavelDaChaveDeEmbedding: manifesto.memory.embedding?.provider === 'openrouter' ? manifesto.memory.embedding.api_key_env : '',
688
844
  };
689
845
  }
690
846
  /** Texto acionavel para cada motivo de degradacao. */