@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/ci.js CHANGED
@@ -33,14 +33,21 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.BUNDLE_LEGADO = exports.DIR_DO_BUNDLE = void 0;
36
37
  exports.motivoDiferimentoCi = motivoDiferimentoCi;
37
38
  exports.repositorioGitHub = repositorioGitHub;
38
39
  exports.consultarCi = consultarCi;
40
+ exports.consultarCiDoRepositorio = consultarCiDoRepositorio;
39
41
  exports.executarCi = executarCi;
42
+ exports.arquivoDoBundle = arquivoDoBundle;
40
43
  exports.lintDoBundle = lintDoBundle;
44
+ exports.destinoDoBundle = destinoDoBundle;
41
45
  exports.prepararBundleCi = prepararBundleCi;
46
+ exports.bundleDaBranch = bundleDaBranch;
47
+ exports.executarCiDaBranch = executarCiDaBranch;
42
48
  exports.executarBundleCi = executarBundleCi;
43
49
  const util_1 = require("./util");
50
+ const worktree_1 = require("./worktree");
44
51
  const verify_1 = require("./verify");
45
52
  const verify_2 = require("./verify");
46
53
  const claims_1 = require("./claims");
@@ -64,6 +71,21 @@ const CASOS_DE_VERIFICADOR_LOCAIS = [
64
71
  ];
65
72
  const CASOS_DE_VERIFICADOR_COM_INTEGRACAO_LOCAL = [
66
73
  /verify-check-c2-b3\.cjs\s+channel-offer(?:\s|$)/,
74
+ // I-38: a prova da busca por significado le a memoria do tenant pela DSN do manifesto.
75
+ /prova-busca-semantica\.sh(?:\s|$)/,
76
+ // RM-051: o ensaio instala o tarball com npm --offline e resolve as dependencias pelo cache do npm
77
+ // da estacao; o runner hospedado so tem os tarballs do npm ci, sem os metadados do registro.
78
+ /testar-experiencia-e2e\.cjs(?:\s|$)/,
79
+ ];
80
+ /**
81
+ * I-38: o runner hospedado nao oferece OrkMind nem a base do tenant. Claim que chama a memoria
82
+ * pelo CLI (a DSN vem do manifesto), que exige o OrkMind instalado ou que roda a suite inteira
83
+ * (ela inclui `TESTES_DE_INTEGRACAO_LOCAL`; o runner roda `test:ci`) fica para a estacao.
84
+ */
85
+ const USA_A_ESTACAO = [
86
+ /(?:^|\s)memory\s+(?:status|index|search|sync|migrate|inventory)(?=\s|$)/,
87
+ /command -v orkmind(?=[\s)"']|$)/,
88
+ /npm\s+--prefix\s+core\s+test(?![:\w-])/,
67
89
  ];
68
90
  function exigeIntegracaoLocal(command) {
69
91
  return [...command.matchAll(/(?:^|[\s/])([a-z0-9-]+\.test\.js)(?=$|[\s;&|])/g)]
@@ -90,7 +112,8 @@ function motivoDiferimentoCi(claim, raiz) {
90
112
  return 'host-runtime-required';
91
113
  }
92
114
  if (claim.verificar.some((command) => exigeIntegracaoLocal(command)
93
- || CASOS_DE_VERIFICADOR_COM_INTEGRACAO_LOCAL.some((caso) => caso.test(command)))) {
115
+ || CASOS_DE_VERIFICADOR_COM_INTEGRACAO_LOCAL.some((caso) => caso.test(command))
116
+ || USA_A_ESTACAO.some((caso) => caso.test(command)))) {
94
117
  return 'local-integration-required';
95
118
  }
96
119
  return null;
@@ -114,6 +137,13 @@ function consultarCi(carregado, sha, remoto = 'origin', executor = executorPadra
114
137
  const repository = remote.ok ? repositorioGitHub(remote.stdout) : null;
115
138
  if (!repository)
116
139
  return { schema: 'ork.ci-status/v1', required: true, ok: false, provider: 'github', repository: null, sha, context, state: 'unavailable', url: null, detail: `remoto ${remoto} não é um repositório GitHub reconhecível` };
140
+ return consultarCiDoRepositorio(repository, sha, context, executor);
141
+ }
142
+ /**
143
+ * O check `context` no `sha` de um repositorio GitHub ja resolvido: o do remoto do projeto ou, desde a
144
+ * RM-037 (rm037defeito, defeito 5), um repositorio externo declarado em `ci.external_repositories`.
145
+ */
146
+ function consultarCiDoRepositorio(repository, sha, context, executor = executorPadrao) {
117
147
  const result = executor({ repository, sha, context });
118
148
  if (!result.ok)
119
149
  return { schema: 'ork.ci-status/v1', required: true, ok: false, provider: 'github', repository, sha, context, state: 'unavailable', url: null, detail: (result.stderr || result.stdout || 'consulta ao GitHub falhou').trim().slice(0, 400) };
@@ -148,6 +178,28 @@ function executarCi(carregado, threadId) {
148
178
  verify,
149
179
  };
150
180
  }
181
+ /**
182
+ * RM-037 (rm037noite, defeito 1): um bundle por thread. O `.ork-ci/bundle.json` era o mesmo caminho em
183
+ * toda thread, entao toda PR conflitava com todas as outras e cada merge pedia merge da main, `ork ci
184
+ * prepare` e CI de novo na proxima. Cada thread grava agora o proprio `.ork-ci/<thread>.json`, e o CI
185
+ * acha o da thread pelo nome da branch (`ork ci run --branch`). O bundle nao nasce no CI porque as
186
+ * claims ficam no `.orkastery/` da maquina que conduz, fora do git.
187
+ */
188
+ exports.DIR_DO_BUNDLE = '.ork-ci';
189
+ /** O caminho unico de antes: so vale quando o `thread` dele bate com a branch (PR aberta antes da mudanca). */
190
+ exports.BUNDLE_LEGADO = 'bundle.json';
191
+ const THREAD_DO_BUNDLE = /^ork-[a-z0-9-]+$/;
192
+ /** O nome do arquivo do bundle da thread, dentro de `.ork-ci/`. */
193
+ function arquivoDoBundle(threadId) {
194
+ if (!THREAD_DO_BUNDLE.test(threadId))
195
+ throw new Error(`ci.bundle: "${threadId}" nao e id de thread`);
196
+ return `${threadId}.json`;
197
+ }
198
+ function comandosDoBundle(carregado) {
199
+ return carregado.manifesto.ci.command
200
+ ? [{ name: 'ci', command: carregado.manifesto.ci.command }]
201
+ : (0, verify_2.comandosDoManifesto)(carregado.manifesto).map(({ nome, comando }) => ({ name: nome, command: comando }));
202
+ }
151
203
  /**
152
204
  * I-53 (RM-037, P6): o lint no `ci prepare`. Claim nascida sob a regra (com o campo `lint`) que
153
205
  * roda a suite inteira e recusada: o bundle vai para o runner hospedado e reprovaria la, depois de
@@ -166,10 +218,20 @@ function lintDoBundle(claims) {
166
218
  }
167
219
  return { recusas, avisos };
168
220
  }
221
+ /**
222
+ * RM-037 (rm037defeito, defeito 6): onde o bundle nasce. Ele e artefato da branch da thread (o commit
223
+ * `ci(<thread>)` vai no PR dela), entao o destino e a worktree da thread, rodando o `ork` da raiz ou da
224
+ * worktree. Gravado na raiz, ele sujava o checkout compartilhado com o bundle de outra thread, e o
225
+ * `git ls-files` da raiz adiava claim de arquivo que so existe na branch da thread.
226
+ */
227
+ function destinoDoBundle(carregado, threadId) {
228
+ const worktree = (0, thread_1.lerThread)(carregado.raiz, threadId).worktree;
229
+ return worktree && fs.existsSync(worktree) ? worktree : carregado.raiz;
230
+ }
169
231
  function prepararBundleCi(carregado, threadId, opcoes = {}) {
170
- const commands = carregado.manifesto.ci.command
171
- ? [{ name: 'ci', command: carregado.manifesto.ci.command }]
172
- : (0, verify_2.comandosDoManifesto)(carregado.manifesto).map(({ nome, comando }) => ({ name: nome, command: comando }));
232
+ const commands = comandosDoBundle(carregado);
233
+ const nomeDoArquivo = arquivoDoBundle(threadId);
234
+ const branch = (0, worktree_1.branchDaWorktree)((0, thread_1.lerThread)(carregado.raiz, threadId));
173
235
  const activeClaims = (0, claims_1.lerClaims)(carregado.raiz, threadId).filter((claim) => claim.estado !== 'retirada');
174
236
  const lint = lintDoBundle(activeClaims);
175
237
  if (lint.recusas.length || lint.avisos.length) {
@@ -181,13 +243,20 @@ function prepararBundleCi(carregado, threadId, opcoes = {}) {
181
243
  throw new Error(`claims.lint: o bundle nao foi gerado; retire a claim e registre de novo com o comando focado:\n ` +
182
244
  lint.recusas.join('\n '));
183
245
  }
246
+ const destino = destinoDoBundle(carregado, threadId);
247
+ // Achado S8 do CHECK: a worktree registrada que sumiu nao e silencio; o bundle na raiz vem com aviso.
248
+ const registrada = (0, thread_1.lerThread)(carregado.raiz, threadId).worktree;
249
+ if (registrada && destino !== registrada) {
250
+ opcoes.aoAvisar?.(`a worktree ${registrada} da thread nao existe mais; o bundle vai para a raiz do projeto`);
251
+ }
184
252
  const classified = activeClaims.map((claim) => ({
185
253
  claim,
186
- reason: motivoDiferimentoCi(claim, carregado.raiz),
254
+ reason: motivoDiferimentoCi(claim, destino),
187
255
  }));
188
256
  const bundle = {
189
257
  schema: 'ork.ci-bundle/v1',
190
258
  thread: threadId,
259
+ branch,
191
260
  base: carregado.manifesto.worktree.base_branch,
192
261
  claims: classified.filter((item) => item.reason === null).map((item) => item.claim),
193
262
  deferredClaims: classified
@@ -195,20 +264,82 @@ function prepararBundleCi(carregado, threadId, opcoes = {}) {
195
264
  .map((item) => ({ id: item.claim.id, reason: item.reason })),
196
265
  commands,
197
266
  };
198
- const dir = path.join(carregado.raiz, '.ork-ci');
267
+ const dir = path.join(destino, '.ork-ci');
199
268
  fs.mkdirSync(dir, { recursive: true });
200
- const file = path.join(dir, 'bundle.json');
269
+ const file = path.join(dir, nomeDoArquivo);
201
270
  fs.writeFileSync(file, JSON.stringify(bundle, null, 2) + '\n', 'utf8');
202
271
  return file;
203
272
  }
204
- function executarBundleCi(carregado, file = '.ork-ci/bundle.json') {
205
- const absolute = path.resolve(carregado.raiz, file);
273
+ function lerBundle(raiz, file) {
274
+ const absolute = path.resolve(raiz, file);
206
275
  const stat = fs.statSync(absolute);
207
276
  if (!stat.isFile() || stat.size > 1024 * 1024)
208
277
  throw new Error('bundle de CI ausente ou acima de 1 MiB');
209
278
  const bundle = JSON.parse(fs.readFileSync(absolute, 'utf8'));
210
- if (bundle.schema !== 'ork.ci-bundle/v1' || !/^ork-[a-z0-9-]+$/.test(bundle.thread) || !Array.isArray(bundle.claims) || !Array.isArray(bundle.commands))
279
+ if (bundle.schema !== 'ork.ci-bundle/v1' || !THREAD_DO_BUNDLE.test(bundle.thread) || !Array.isArray(bundle.claims) || !Array.isArray(bundle.commands))
211
280
  throw new Error('bundle de CI inválido');
281
+ return bundle;
282
+ }
283
+ const casaComBranch = (thread, branch) => branch === `ork/${thread}` || branch.startsWith(`ork/${thread}-`);
284
+ /**
285
+ * O bundle da branch, entre os `.ork-ci/*.json` do checkout. Vale o de `branch` igual. O bundle sem
286
+ * `branch` (o `bundle.json` legado, de antes da RM-037) vale pela thread, quando a branch e
287
+ * `ork/<thread>` ou `ork/<thread>-*` (a mais longa ganha: `ork-a` nao leva a branch da `ork-ab`).
288
+ * Bundle novo de outra branch nunca vale, nem da mesma thread (achado A4 do CHECK 1), e o arquivo da
289
+ * thread da branch que nao abre reprova em vez de ceder a vez a outro. `null` quando nenhum bate.
290
+ */
291
+ function bundleDaBranch(raiz, branch) {
292
+ const dir = path.join(raiz, exports.DIR_DO_BUNDLE);
293
+ if (!fs.existsSync(dir))
294
+ return null;
295
+ let melhor = null;
296
+ let quebrado = null;
297
+ for (const nome of fs.readdirSync(dir).filter((n) => n.endsWith('.json')).sort()) {
298
+ const arquivo = path.posix.join(exports.DIR_DO_BUNDLE, nome);
299
+ let bundle;
300
+ try {
301
+ bundle = lerBundle(raiz, arquivo);
302
+ }
303
+ catch (e) {
304
+ if (!quebrado && nome !== exports.BUNDLE_LEGADO && casaComBranch(nome.slice(0, -'.json'.length), branch)) {
305
+ quebrado = new Error(`ci.bundle.invalido: ${arquivo} e o bundle da branch ${branch} e nao abre ` +
306
+ `(${e.message}); rode ork ci prepare <thread> de novo e versione o arquivo`);
307
+ }
308
+ continue;
309
+ }
310
+ if (nome !== exports.BUNDLE_LEGADO && nome !== `${bundle.thread}.json`)
311
+ continue;
312
+ const peso = bundle.branch === branch ? Number.MAX_SAFE_INTEGER
313
+ : !bundle.branch && casaComBranch(bundle.thread, branch) ? bundle.thread.length : 0;
314
+ if (peso > 0 && (!melhor || peso > melhor.peso))
315
+ melhor = { arquivo, bundle, peso };
316
+ }
317
+ // Sugestao 4 do CHECK 2: o arquivo quebrado so reprova se nenhum bundle valido for da branch exata.
318
+ if (quebrado && melhor?.peso !== Number.MAX_SAFE_INTEGER)
319
+ throw quebrado;
320
+ return melhor ? { arquivo: melhor.arquivo, bundle: melhor.bundle } : null;
321
+ }
322
+ /**
323
+ * `ork ci run --branch <branch>`: o CHECK do CI pela branch. Branch de thread (`ork/*`) roda o bundle
324
+ * dela e reprova sem ele; outra branch (a `main`, por exemplo) nao tem thread e roda so os comandos
325
+ * do manifesto.
326
+ */
327
+ function executarCiDaBranch(carregado, branch) {
328
+ const achado = bundleDaBranch(carregado.raiz, branch);
329
+ if (achado)
330
+ return { ...rodarBundle(carregado, achado.bundle), branch, bundle: achado.arquivo };
331
+ if (branch.startsWith('ork/')) {
332
+ throw new Error(`ci.bundle.ausente: a branch ${branch} e de thread e nao tem bundle em ${exports.DIR_DO_BUNDLE}/; ` +
333
+ 'rode ork ci prepare <thread> na worktree e versione o arquivo');
334
+ }
335
+ const semThread = { schema: 'ork.ci-bundle/v1', thread: '', base: carregado.manifesto.worktree.base_branch,
336
+ claims: [], commands: comandosDoBundle(carregado) };
337
+ return { ...rodarBundle(carregado, semThread), thread: null, branch, bundle: null };
338
+ }
339
+ function executarBundleCi(carregado, file = path.posix.join(exports.DIR_DO_BUNDLE, exports.BUNDLE_LEGADO)) {
340
+ return rodarBundle(carregado, lerBundle(carregado.raiz, file));
341
+ }
342
+ function rodarBundle(carregado, bundle) {
212
343
  // O CI roda o mesmo preparo do `ork verify` local (I-54), uma vez e antes das claims: a claim
213
344
  // que passa na maquina de quem entrega passa aqui pelo mesmo caminho. Como no verify, o
214
345
  // preparo nao e condicao de claim; ele so deixa a compilacao pronta e sai no resultado.
@@ -48,7 +48,9 @@ const company_brain_source_1 = require("./company-brain-source");
48
48
  const company_brain_migration_1 = require("./company-brain-migration");
49
49
  const company_brain_journal_1 = require("./company-brain-journal");
50
50
  const company_brain_contract_1 = require("./company-brain-contract");
51
- exports.BRAIN_READ = ['status', 'inventory', 'get', 'query', 'receipts', 'reconcile'];
51
+ const company_brain_context_1 = require("./company-brain-context");
52
+ const company_brain_dossie_1 = require("./company-brain-dossie");
53
+ exports.BRAIN_READ = ['status', 'inventory', 'get', 'query', 'receipts', 'reconcile', 'context', 'dossie'];
52
54
  exports.BRAIN_WRITE = ['sync', 'apply', 'rollback', 'bind'];
53
55
  function runBrain(c, sub, options, positionals = [], transport) {
54
56
  const context = (0, project_state_1.memoryState)(c);
@@ -56,7 +58,7 @@ function runBrain(c, sub, options, positionals = [], transport) {
56
58
  const config = c.manifesto.memory;
57
59
  if (![...exports.BRAIN_READ, ...exports.BRAIN_WRITE].includes(sub))
58
60
  throw Error('brain.operation.invalid');
59
- const allowed = new Set(['thread', 'json', 'id', 'ids', 'kinds', 'workspaces', 'limit', 'offset', 'batch', 'dry-run', 'plan', 'expected-sha256', 'project', 'initiatives']);
61
+ const allowed = new Set(['thread', 'json', 'id', 'ids', 'kinds', 'workspaces', 'limit', 'offset', 'batch', 'dry-run', 'plan', 'expected-sha256', 'project', 'initiatives', 'decisao']);
60
62
  if (Object.keys(options).some(k => !allowed.has(k)))
61
63
  throw Error('brain.argument.invalid');
62
64
  const value = (name) => typeof options[name] === 'string' ? options[name] : undefined;
@@ -80,6 +82,29 @@ function runBrain(c, sub, options, positionals = [], transport) {
80
82
  throw Error('brain.migration.dry-run-required');
81
83
  return { state: 'ok', ...(0, company_brain_migration_1.reconcileBrain)(c, scope, value('batch') ?? 'inventory-' + (0, company_brain_contract_1.digest)((0, portfolio_1.readPortfolio)(c.raiz)).slice(0, 20)) };
82
84
  }
85
+ // B4.1: só leitura, por isso fica antes da ativação. Sem --ids, vale o escopo vinculado da thread.
86
+ if (sub === 'context') {
87
+ let ids = (value('ids') ?? '').split(',').map(s => s.trim()).filter(Boolean);
88
+ const bindingFile = path.join((0, thread_1.dirThread)(c.raiz, thread), 'brain-scope.json');
89
+ if (!ids.length && fs.existsSync(bindingFile)) {
90
+ let binding;
91
+ try {
92
+ binding = JSON.parse(fs.readFileSync(bindingFile, 'utf8'));
93
+ }
94
+ catch {
95
+ throw Error('brain.scope.invalid');
96
+ }
97
+ // Mesma conferência do sync: o escopo vinculado vale só se for desta thread e do formato gravado pelo bind.
98
+ if (binding?.schema !== 'ork.brain-cycle-scope/v1' || binding.version !== 1 || binding.thread !== thread || typeof binding.projectId !== 'string' ||
99
+ !Array.isArray(binding.initiativeIds) || binding.initiativeIds.some((i) => typeof i !== 'string'))
100
+ throw Error('brain.scope.invalid');
101
+ ids = [binding.projectId, ...binding.initiativeIds];
102
+ }
103
+ return (0, company_brain_context_1.buildContext)(c, ids, client, thread);
104
+ }
105
+ // K3.1: dossiê de decisão, só leitura como o contexto; decidir continua no registro e no ingresso.
106
+ if (sub === 'dossie')
107
+ return (0, company_brain_dossie_1.buildDossie)(c, thread, options.decisao === undefined ? undefined : value('decisao') ?? '', client);
83
108
  // Existing reviewed activation plus dedicated Brain DB grants: neither grants the other.
84
109
  (0, write_activation_1.exigirAtivacao)(c, thread, 'memory');
85
110
  const dir = (0, thread_1.dirThread)(c.raiz, thread);
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CONTEXT_SCHEMA = void 0;
4
+ exports.buildContext = buildContext;
5
+ const portfolio_1 = require("./portfolio");
6
+ const company_brain_client_1 = require("./company-brain-client");
7
+ const company_brain_contract_1 = require("./company-brain-contract");
8
+ const company_brain_source_1 = require("./company-brain-source");
9
+ exports.CONTEXT_SCHEMA = 'ork.brain-context/v1';
10
+ const ID = /^(prod|proj|init)-[a-z0-9][a-z0-9-]{2,47}$/;
11
+ const ORDEM = { prod: 0, proj: 1, init: 2 };
12
+ const LIMITE = 1000;
13
+ /** Teto de `get` por pacote: só o caso misto (alguns retidos, alguns ausentes) precisa deles. */
14
+ const LIMITE_DE_GET = 100;
15
+ const kindDe = (id) => id.slice(0, 4);
16
+ /** Por code point: o digest é identidade citável e não pode depender do locale do processo. */
17
+ const comparar = (a, b) => a < b ? -1 : a > b ? 1 : 0;
18
+ // Além do que o D7 lista, `conflict` também encerra: resposta de conflito não é conteúdo citável.
19
+ const falha = (state) => ['forbidden', 'unavailable', 'conflict'].includes(state);
20
+ /** A citação só vale inteira: sem um dos campos, o item sai do conteúdo e vira lacuna. */
21
+ function citacaoDe(source) {
22
+ if (!source || typeof source !== 'object')
23
+ return null;
24
+ const { instance, source_ref, source_hash, source_version, location } = source;
25
+ if ([instance, source_ref, location].some(v => typeof v !== 'string' || !v) || typeof source_hash !== 'string' ||
26
+ !/^[a-f0-9]{64}$/.test(source_hash) || !Number.isInteger(source_version))
27
+ return null;
28
+ return { instance, source_ref, source_hash, source_version, location };
29
+ }
30
+ /**
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.
33
+ */
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() });
46
+ 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
+ let gets = 0;
50
+ for (let pendentes = pedido; pendentes.length;) {
51
+ for (let inicio = 0; inicio < pendentes.length; inicio += LIMITE) {
52
+ const lote = pendentes.slice(inicio, inicio + LIMITE), doLote = new Set(lote);
53
+ 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',
54
+ tenant_id: tenant, facets: { ids: lote, kinds: [], workspace_ids: [], source_instances: [] }, mode: 'selection',
55
+ limit: lote.length, offset: 0 }) });
56
+ if (falha(r.state))
57
+ return encerrar(r);
58
+ const itens = Array.isArray(r.items) ? r.items : [];
59
+ for (const item of itens)
60
+ if (item?.state === 'ok' && doLote.has(item.entity?.id))
61
+ brain.set(item.entity.id, item.entity);
62
+ // Retido chega sem id na seleção. Sem retido no lote, o que faltou é desconhecido; com todos os
63
+ // faltantes retidos, não há o que atribuir. Só no caso misto o `get` diz qual id é qual.
64
+ const faltantes = lote.filter(id => !brain.has(id)), retidosNoLote = itens.filter((i) => i?.state === 'withheld').length;
65
+ if (retidosNoLote && retidosNoLote === faltantes.length)
66
+ faltantes.forEach(id => retidos.add(id));
67
+ else if (retidosNoLote)
68
+ for (const id of faltantes) {
69
+ if (++gets > LIMITE_DE_GET)
70
+ return encerrar({ state: 'unavailable', error: 'brain.context.get-limit' });
71
+ const g = transport({ schema: company_brain_client_1.BRAIN_API, operation: 'get', payload: { tenant_id: tenant, id } });
72
+ if (falha(g.state))
73
+ return encerrar(g);
74
+ if (g.state === 'withheld')
75
+ retidos.add(id);
76
+ else if (g.state === 'ok' && g.entity?.id === id)
77
+ brain.set(id, g.entity);
78
+ }
79
+ }
80
+ const novos = [];
81
+ for (const id of pendentes)
82
+ if (!retidos.has(id))
83
+ for (const parent of [pai(id, fonte.get(id)?.parent_id), pai(id, brain.get(id)?.parent_id)])
84
+ if (parent && !escopo.has(parent)) {
85
+ escopo.add(parent);
86
+ novos.push(parent);
87
+ }
88
+ pendentes = novos.sort(comparar);
89
+ }
90
+ const itens = [], lacunas = [];
91
+ const lacuna = (id, codigo) => lacunas.push({ id, codigo });
92
+ for (const id of escopo) {
93
+ if (retidos.has(id)) {
94
+ itens.push({ id, estado: 'retido', frescor: 'retido', origem: 'brain' });
95
+ lacuna(id, 'brain.retido');
96
+ continue;
97
+ }
98
+ const b = brain.get(id), f = fonte.get(id);
99
+ if (!b && !f) {
100
+ lacuna(id, 'entidade.desconhecida');
101
+ continue;
102
+ }
103
+ const entidade = b ?? f, citacao = citacaoDe(entidade.source);
104
+ if (!citacao) {
105
+ lacuna(id, 'citacao.incompleta');
106
+ continue;
107
+ }
108
+ const frescor = !b ? 'ausente-no-brain' : !f ? 'ausente-na-fonte'
109
+ : b.source?.source_hash === f.source.source_hash && b.version === f.version ? 'confere' : 'divergente';
110
+ itens.push({ id, kind: entidade.kind, estado: 'ok', frescor, origem: b ? 'brain' : 'fonte', titulo: entidade.title ?? '',
111
+ status: entidade.status ?? '', parent_id: entidade.parent_id ?? null, depends_on: entidade.depends_on ?? [],
112
+ owner: { raw: entidade.owner?.raw ?? null, state: entidade.owner?.state ?? 'unknown' }, versao: entidade.version, citacao,
113
+ fonte: f ? { source_hash: f.source.source_hash, version: f.version } : null });
114
+ if (frescor === 'ausente-no-brain')
115
+ lacuna(id, 'brain.ausente');
116
+ if (frescor === 'ausente-na-fonte')
117
+ lacuna(id, 'fonte.ausente');
118
+ if (frescor === 'divergente')
119
+ lacuna(id, 'fonte.divergente');
120
+ if ((entidade.owner?.principal ?? null) === null)
121
+ lacuna(id, 'dono.sem-principal');
122
+ if ((entidade.observed_at ?? null) === null)
123
+ lacuna(id, 'observado.desconhecido');
124
+ if ((entidade.recorded_at ?? null) === null)
125
+ lacuna(id, 'registrado.desconhecido');
126
+ }
127
+ itens.sort((a, b) => ORDEM[kindDe(a.id)] - ORDEM[kindDe(b.id)] || comparar(a.id, b.id));
128
+ lacunas.sort((a, b) => comparar(a.id, b.id) || comparar(a.codigo, b.codigo));
129
+ // 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
+ 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() };
132
+ }