@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/docs.js CHANGED
@@ -35,6 +35,8 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.LIMITE_DO_PARAGRAFO = exports.LIMITE_DA_FRASE = exports.CATEGORIAS = exports.DIMENSOES = exports.ESTADOS_DA_ESPECIFICACAO = exports.DIR_PADROES = exports.DIR_ROADMAP = exports.DIR_PRODUTO = void 0;
37
37
  exports.separarFrontmatter = separarFrontmatter;
38
+ exports.ehPaginaDeDocs = ehPaginaDeDocs;
39
+ exports.documentoDeTexto = documentoDeTexto;
38
40
  exports.carregarDocs = carregarDocs;
39
41
  exports.resolverBase = resolverBase;
40
42
  exports.comandosDaAjuda = comandosDaAjuda;
@@ -44,6 +46,7 @@ exports.renderizarEstado = renderizarEstado;
44
46
  exports.textoDaVerificacao = textoDaVerificacao;
45
47
  exports.escreverYaml = escreverYaml;
46
48
  exports.mergeDaThread = mergeDaThread;
49
+ exports.escopoPadraoDoSync = escopoPadraoDoSync;
47
50
  exports.sincronizarDocs = sincronizarDocs;
48
51
  exports.tabelaDoRoadmap = tabelaDoRoadmap;
49
52
  exports.tabelaDoProduto = tabelaDoProduto;
@@ -136,38 +139,49 @@ function separarFrontmatter(texto) {
136
139
  return { bruto: null, corpo: t };
137
140
  return { bruto: t.slice(4, fim + 1), corpo: t.slice(fim + 5) };
138
141
  }
142
+ /** `_modelo-*.md` e `README.md` sao modelo e indice, nao paginas de entidade. */
143
+ function ehPaginaDeDocs(nome) {
144
+ return nome.endsWith('.md') && !nome.startsWith('_') && nome !== 'README.md';
145
+ }
139
146
  function paginas(raiz, dir) {
140
147
  const abs = path.join(raiz, dir);
141
148
  if (!fs.existsSync(abs))
142
149
  return [];
143
- // `_modelo-*.md` e `README.md` sao modelo e indice, nao paginas de entidade.
144
150
  return fs.readdirSync(abs)
145
- .filter((n) => n.endsWith('.md') && !n.startsWith('_') && n !== 'README.md')
151
+ .filter(ehPaginaDeDocs)
146
152
  .sort()
147
153
  .map((n) => `${dir}/${n}`);
148
154
  }
155
+ /**
156
+ * RM-054: uma pagina a partir do texto, venha ele do disco, de um blob do git ou da forja. O
157
+ * achado e o mesmo que a leitura do disco daria.
158
+ */
159
+ function documentoDeTexto(arquivo, texto) {
160
+ const { bruto, corpo } = separarFrontmatter(texto);
161
+ if (bruto === null) {
162
+ return { achado: erro(arquivo, undefined, 'docs.frontmatter.ausente', 'página sem frontmatter YAML', 'comece o arquivo com as chaves do modelo entre duas linhas ---') };
163
+ }
164
+ let dados;
165
+ try {
166
+ dados = (0, yaml_1.lerYaml)(bruto);
167
+ }
168
+ catch (e) {
169
+ return { achado: erro(arquivo, undefined, 'docs.frontmatter.invalido', `frontmatter inválido: ${e.message}`) };
170
+ }
171
+ if (!ehMapa(dados)) {
172
+ return { achado: erro(arquivo, undefined, 'docs.frontmatter.invalido', 'o frontmatter precisa ser um mapa de chaves') };
173
+ }
174
+ return { doc: { arquivo, tipo: dados.tipo, id: String(dados.id ?? ''), dados, corpo } };
175
+ }
149
176
  function carregarDocs(raiz) {
150
177
  const docs = [];
151
178
  const achados = [];
152
179
  for (const arquivo of [...paginas(raiz, exports.DIR_PRODUTO), ...paginas(raiz, exports.DIR_ROADMAP)]) {
153
- const { bruto, corpo } = separarFrontmatter(fs.readFileSync(path.join(raiz, arquivo), 'utf8'));
154
- if (bruto === null) {
155
- achados.push(erro(arquivo, undefined, 'docs.frontmatter.ausente', 'página sem frontmatter YAML', 'comece o arquivo com as chaves do modelo entre duas linhas ---'));
156
- continue;
157
- }
158
- let dados;
159
- try {
160
- dados = (0, yaml_1.lerYaml)(bruto);
161
- }
162
- catch (e) {
163
- achados.push(erro(arquivo, undefined, 'docs.frontmatter.invalido', `frontmatter inválido: ${e.message}`));
164
- continue;
165
- }
166
- if (!ehMapa(dados)) {
167
- achados.push(erro(arquivo, undefined, 'docs.frontmatter.invalido', 'o frontmatter precisa ser um mapa de chaves'));
168
- continue;
169
- }
170
- docs.push({ arquivo, tipo: dados.tipo, id: String(dados.id ?? ''), dados, corpo });
180
+ const lido = documentoDeTexto(arquivo, fs.readFileSync(path.join(raiz, arquivo), 'utf8'));
181
+ if ('doc' in lido)
182
+ docs.push(lido.doc);
183
+ else
184
+ achados.push(lido.achado);
171
185
  }
172
186
  return { docs, achados };
173
187
  }
@@ -664,12 +678,40 @@ function temBranch(raiz, thread) {
664
678
  return r.ok && r.saida.length > 0;
665
679
  }
666
680
  const ORDEM_DO_CODIGO = exports.DIMENSOES.codigo;
681
+ /**
682
+ * RM-037 (rm037noite, defeito 5): o escopo padrao do `ork docs sincronizar`. Na worktree de uma thread
683
+ * com item do roadmap, so o item dela: da worktree da ork-rm037noite, o sincronizar gravava RM-025,
684
+ * 026, 031, 038, 048, 049, 050 e 054, e cada PR conflitava com as outras. Fora de worktree de thread
685
+ * (a raiz, onde o condutor sincroniza depois do merge), todo item, como antes.
686
+ */
687
+ function escopoPadraoDoSync(raiz) {
688
+ const real = (p) => { try {
689
+ return fs.realpathSync(p);
690
+ }
691
+ catch {
692
+ return path.resolve(p);
693
+ } };
694
+ const aqui = real(raiz);
695
+ for (const id of (0, thread_1.listarIds)(raiz)) {
696
+ let t;
697
+ try {
698
+ t = (0, thread_1.lerThread)(raiz, id);
699
+ }
700
+ catch {
701
+ continue;
702
+ }
703
+ if (t.worktree && real(t.worktree) === aqui)
704
+ return { itens: t.roadmap ? [t.roadmap] : null, thread: t.id };
705
+ }
706
+ return { itens: null, thread: null };
707
+ }
667
708
  function sincronizarDocs(raiz, opcoes = {}) {
668
709
  const base = resolverBase(raiz, opcoes.baseBranch) ?? (opcoes.baseBranch ?? 'main');
669
710
  const agora = opcoes.agora ?? (() => isoComFusoLocal(new Date()));
670
711
  const { docs } = carregarDocs(raiz);
671
712
  const mudancas = [];
672
- for (const d of docs.filter((x) => x.tipo === 'roadmap')) {
713
+ const noEscopo = (d) => !opcoes.itens || opcoes.itens.includes(d.id);
714
+ for (const d of docs.filter((x) => x.tipo === 'roadmap' && noEscopo(x))) {
673
715
  const sdlc = ehMapa(d.dados.sdlc) ? d.dados.sdlc : null;
674
716
  const thread = sdlc && typeof sdlc.thread === 'string' ? sdlc.thread : null;
675
717
  if (!thread)
@@ -715,7 +757,7 @@ function sincronizarDocs(raiz, opcoes = {}) {
715
757
  }
716
758
  // As tabelas de estado do corpo sao vistas do frontmatter: regeradas em todo item de roadmap,
717
759
  // inclusive quando a pessoa mudou o frontmatter a mao (ciclo, deploy, exposicao...).
718
- for (const d of docs.filter((x) => x.tipo === 'roadmap')) {
760
+ for (const d of docs.filter((x) => x.tipo === 'roadmap' && noEscopo(x))) {
719
761
  const novo = substituirBloco(substituirBloco(d.corpo, RELANCE_INI, RELANCE_FIM, renderizarRelance(d)), ESTADO_INI, ESTADO_FIM, renderizarEstado(d));
720
762
  if (novo !== d.corpo) {
721
763
  if (!mudancas.some((m) => m.id === d.id)) {
package/dist/doctor.js CHANGED
@@ -40,6 +40,7 @@ var __importStar = (this && this.__importStar) || (function () {
40
40
  })();
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.checarContas = checarContas;
43
+ exports.checarChaveDeEmbedding = checarChaveDeEmbedding;
43
44
  exports.checarOnboarding = checarOnboarding;
44
45
  exports.checar = checar;
45
46
  exports.relatorio = relatorio;
@@ -116,6 +117,37 @@ const ENVS_QUE_REDIRECIONAM = [
116
117
  function versaoNode() {
117
118
  return Number(process.versions.node.split('.')[0]);
118
119
  }
120
+ /**
121
+ * I-38 (T7): a chave de embedding aparece pelo NOME, nunca pelo valor. Ausente e aviso (a busca
122
+ * cai para o fallback local ou para FTS e o regime segue); nome de provider pago e falha, porque a
123
+ * entrada do `ork` apagaria a variavel sob `subscription-only` e o preflight reprovaria o codex.
124
+ */
125
+ function checarChaveDeEmbedding(carregado, env = process.env) {
126
+ const nome = 'chave de embedding';
127
+ const config = (0, manifest_1.configDeEmbedding)(carregado.manifesto);
128
+ if (config.provider === 'none')
129
+ return { nome, nivel: 'ok', detalhe: 'memory.embedding com provider none: busca por significado desligada' };
130
+ const variavel = config.api_key_env;
131
+ if (runtime_ambiente_1.ENVS_DE_PROVIDER_PAGO.includes(variavel)) {
132
+ return { nome, nivel: 'fail', detalhe: `memory.embedding.api_key_env declara ${variavel}, nome da lista de provider pago`,
133
+ correcao: 'use uma chave dedicada ao Orkastery com nome proprio, por exemplo ORKASTERY_EMBEDDING_API_KEY' };
134
+ }
135
+ if (!variavel) {
136
+ return { nome, nivel: 'fail', detalhe: `memory.embedding com provider ${config.provider} sem api_key_env`,
137
+ correcao: 'declare em memory.embedding.api_key_env o NOME da variavel com a chave dedicada' };
138
+ }
139
+ const valor = (env[variavel] ?? '').trim();
140
+ const dsn = carregado.manifesto.memory.database_url_env ? (env[carregado.manifesto.memory.database_url_env] ?? '') : '';
141
+ if (valor !== '' && !(0, orkmind_1.chaveDeEmbeddingAceita)(valor, dsn)) {
142
+ return { nome, nivel: 'fail', detalhe: `${variavel} tem valor recusado: parece URL, DSN ou texto com espaco (valor nunca impresso)`,
143
+ correcao: `confira se ${variavel} guarda a chave dedicada, e nao outra credencial` };
144
+ }
145
+ if (valor !== '') {
146
+ return { nome, nivel: 'ok', detalhe: `${variavel} presente no ambiente (valor nunca impresso); ${config.provider} ${config.model}` };
147
+ }
148
+ return { nome, nivel: 'warn', detalhe: `${variavel} ausente do ambiente: a busca por significado usa o fallback local ou cai para FTS; o regime nao muda`,
149
+ correcao: `exporte ${variavel} com a chave dedicada ao Orkastery (com limite de credito no painel do provider)` };
150
+ }
119
151
  /** Checks locais; ler a entrevista não abre driver, banco, runtime ou rede. */
120
152
  function checarOnboarding(carregado) {
121
153
  const estado = (0, onboarding_1.lerOnboarding)(carregado.raiz);
@@ -315,6 +347,7 @@ function checar(dirInicial = process.cwd(), nomesHerdados = (0, runtime_ambiente
315
347
  : 'files (fallback honesto: handoff por arquivos, ponteiro path#ancora)',
316
348
  correcao: estado.pedido === 'orkmind' && estado.efetivo === 'files' ? estado.correcao : undefined,
317
349
  });
350
+ checks.push(checarChaveDeEmbedding(context.loaded));
318
351
  // Bloco B3: a fila de rate limit e estado do mundo, nao detalhe interno. Uma fase
319
352
  // esperando janela e uma fase que NAO esta rodando, e quem olha o doctor precisa
320
353
  // saber disso antes de concluir que a thread travou.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.mergeDaEntrega = mergeDaEntrega;
4
4
  exports.registrarEntregaPorPr = registrarEntregaPorPr;
5
+ exports.registrarEntregaExternaPorPr = registrarEntregaExternaPorPr;
5
6
  exports.registrarEntregasPorPr = registrarEntregasPorPr;
6
7
  /**
7
8
  * I-57 (RM-008, fatia 2): a entrega feita por PR tambem prova entrega para o MASTER.
@@ -35,6 +36,18 @@ function mergeDaEntrega(raiz, thread, remoto, base) {
35
36
  }
36
37
  return null;
37
38
  }
39
+ /**
40
+ * A thread chega ao SHIP quando o ciclo dela tem SHIP, e so AVANCA: o segundo PR da mesma entrega (os
41
+ * dois sites da ork-siteshomesco, por exemplo) com a thread ja no MASTER nao a devolve ao SHIP (achado S9).
42
+ */
43
+ function avancarParaShip(raiz, threadId) {
44
+ const atualizada = (0, thread_1.lerThread)(raiz, threadId);
45
+ const fases = atualizada.fases ?? [];
46
+ if (!fases.includes('SHIP') || fases.indexOf(atualizada.faseAtual) >= fases.indexOf('SHIP'))
47
+ return;
48
+ atualizada.faseAtual = 'SHIP';
49
+ (0, thread_1.gravarThread)(raiz, atualizada);
50
+ }
38
51
  function resultado(thread, acao, motivo, extra = {}) {
39
52
  return { thread, acao, motivo, mergeSha: null, headSha: null, ci: null, ...extra };
40
53
  }
@@ -85,15 +98,106 @@ function registrarEntregaPorPr(carregado, threadId, opcoes = {}) {
85
98
  evidencia: { viaPr: true, ci: { state: ci.state, context: ci.context, url: ci.url, sha: ci.sha } },
86
99
  });
87
100
  // Como no `ork ship`: a thread chega ao SHIP quando o ciclo dela tem SHIP.
88
- const atualizada = (0, thread_1.lerThread)(raiz, threadId);
89
- if (atualizada.fases?.includes('SHIP')) {
90
- atualizada.faseAtual = 'SHIP';
91
- (0, thread_1.gravarThread)(raiz, atualizada);
92
- }
101
+ avancarParaShip(raiz, threadId);
93
102
  if (opcoes.publicar !== false)
94
103
  (0, fabrica_publicar_1.publicarEmSegundoPlano)(raiz);
95
104
  return resultado(threadId, 'registrou', `ship_done pelo merge ${merge.mergeSha.slice(0, 7)} do PR`, { ...merge, ci });
96
105
  }
106
+ const ghApi = (caminho) => (0, util_1.exec)('gh', ['api', caminho, '-H', 'Accept: application/vnd.github+json'], process.cwd(), 120000);
107
+ function lerJson(r) {
108
+ if (!r.ok)
109
+ return null;
110
+ try {
111
+ return JSON.parse(r.stdout);
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ }
117
+ /**
118
+ * `ork ship registrar-pr <thread> --repo <dono/nome> --pr <n>`: a thread que entrega por PR em outro
119
+ * repositorio (os sites, por exemplo) nao tinha como virar `ship_done`, e a entrega virava decisao. A
120
+ * exigencia e a mesma do caminho local: o merge dentro da ponta da base (pela API do GitHub, porque o
121
+ * repositorio nao e remoto deste checkout) e o check declarado verde no head do PR. O repositorio precisa
122
+ * estar declarado no manifesto; e o dono que diz onde, e com que check, a entrega vale. Idempotente.
123
+ */
124
+ function registrarEntregaExternaPorPr(carregado, threadId, opcoes) {
125
+ const raiz = carregado.raiz, repo = opcoes.repositorio, n = opcoes.pr;
126
+ const gh = opcoes.executorGitHub ?? ghApi;
127
+ const declarados = carregado.manifesto.ci.external_repositories;
128
+ if (!Object.hasOwn(declarados, repo)) {
129
+ return resultado(threadId, 'recusada', `o repositorio ${repo} nao esta declarado em ci.external_repositories do orkastery.yaml`);
130
+ }
131
+ if (!Number.isSafeInteger(n) || n < 1)
132
+ return resultado(threadId, 'recusada', `--pr precisa ser o numero do PR, recebido ${String(n)}`);
133
+ const thread = (0, thread_1.lerThread)(raiz, threadId);
134
+ if (thread.status === 'fechada')
135
+ return resultado(threadId, 'ja-registrada', 'thread ja fechada');
136
+ const dir = (0, thread_1.dirThread)(raiz, threadId);
137
+ if ((0, ledger_1.lerLedger)(dir).some((e) => e.tipo === ledger_1.TIPOS_DE_EVENTO.shipConcluido && e.repositorio === repo && e.pr === n)) {
138
+ return resultado(threadId, 'ja-registrada', `o PR #${n} de ${repo} ja tem ship_done`);
139
+ }
140
+ const pr = lerJson(gh(`repos/${repo}/pulls/${n}`));
141
+ if (!pr)
142
+ return resultado(threadId, 'recusada', `a consulta do PR #${n} de ${repo} ao GitHub falhou`);
143
+ const mergeSha = typeof pr.merge_commit_sha === 'string' && /^[0-9a-f]{40}$/.test(pr.merge_commit_sha) ? pr.merge_commit_sha : null;
144
+ const headSha = typeof pr.head?.sha === 'string' && /^[0-9a-f]{40}$/.test(pr.head.sha) ? pr.head.sha : null;
145
+ const base = typeof pr.base?.ref === 'string' && /^[A-Za-z0-9._/-]{1,200}$/.test(pr.base.ref) ? pr.base.ref : null;
146
+ if (pr.merged !== true || !mergeSha || !headSha || !base) {
147
+ return resultado(threadId, 'sem-merge', `o PR #${n} de ${repo} nao esta mesclado`);
148
+ }
149
+ // Achado A4 do CHECK: o PR precisa ser DESTA thread e ter entrado na branch padrao do repositorio. Sem o
150
+ // vinculo, qualquer PR mesclado do repositorio viraria entrega de qualquer thread; sem a base, um PR
151
+ // empilhado numa branch de feature contaria como entrega.
152
+ const vinculo = [pr.title, pr.body, pr.head?.ref].filter((x) => typeof x === 'string')
153
+ // N4 do CHECK 2: a branch do proprio ork e `ork/<thread>-<variante>`, entao o hifen depois do id vale;
154
+ // letra ou digito colado nao (outro id). Sem diferenciar maiusculas.
155
+ .some((x) => new RegExp(`(^|[^a-z0-9-])${threadId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![a-z0-9])`, 'i').test(x));
156
+ if (!vinculo) {
157
+ return resultado(threadId, 'recusada', `o PR #${n} de ${repo} nao cita a thread ${threadId} no titulo, no corpo nem na branch`, { mergeSha, headSha });
158
+ }
159
+ const repositorio = lerJson(gh(`repos/${repo}`));
160
+ if (!repositorio)
161
+ return resultado(threadId, 'recusada', `a consulta de ${repo} ao GitHub falhou`, { mergeSha, headSha });
162
+ if (repositorio.default_branch !== base) {
163
+ return resultado(threadId, 'recusada', `o PR #${n} entrou em ${base}, e a branch padrao de ${repo} e ${repositorio.default_branch ?? 'desconhecida'}`, { mergeSha, headSha });
164
+ }
165
+ // A ponta da base AGORA, e o merge dentro dela: o push provado, pela mesma pergunta do ls-remote.
166
+ const ramo = lerJson(gh(`repos/${repo}/branches/${encodeURIComponent(base)}`));
167
+ const ponta = typeof ramo?.commit?.sha === 'string' && /^[0-9a-f]{40}$/.test(ramo.commit.sha) ? ramo.commit.sha : '';
168
+ const comparacao = ponta ? lerJson(gh(`repos/${repo}/compare/${mergeSha}...${ponta}`)) : null;
169
+ if (!ponta || !comparacao || !['ahead', 'identical'].includes(String(comparacao.status))) {
170
+ return resultado(threadId, 'recusada', `o merge ${mergeSha.slice(0, 7)} nao esta na ponta de ${repo}:${base}`, { mergeSha, headSha });
171
+ }
172
+ const check = declarados[repo];
173
+ const ci = check ? (0, ci_1.consultarCiDoRepositorio)(repo, headSha, check, opcoes.executorCi) : null;
174
+ if (ci && !ci.ok) {
175
+ return resultado(threadId, 'recusada', `sem CI verde no head do PR: ${ci.detail}`, { mergeSha, headSha, ci });
176
+ }
177
+ (0, ledger_1.registrar)(dir, threadId, ledger_1.TIPOS_DE_EVENTO.shipConcluido, {
178
+ de: thread.base?.branch ?? null,
179
+ para: `${repo}:${base}`,
180
+ repositorio: repo,
181
+ pr: n,
182
+ url: pr.html_url ?? `https://github.com/${repo}/pull/${n}`,
183
+ shaDe: headSha,
184
+ mergeSha,
185
+ jaIncorporado: true,
186
+ remoto: `github:${repo}`,
187
+ shaRemoto: ponta,
188
+ pushVerificado: true,
189
+ fonteDaProva: `gh api repos/${repo}/compare/${mergeSha}...${ponta} (status ${String(comparacao.status)}; ponta de ${base} por gh api repos/${repo}/branches/${base})`,
190
+ autorizadoPor: ci ? 'merge de PR com o CI independente verde no SHA exato' : 'merge de PR em repositorio externo declarado sem CI no manifesto',
191
+ tipoDeAutorizacao: 'pr-externo',
192
+ modo: thread.modo,
193
+ tag: (0, modos_1.tagDoModo)(thread.modo),
194
+ evidencia: { viaPr: true, externo: true, ciExigido: !!ci, ci: ci ? { state: ci.state, context: ci.context, url: ci.url, sha: ci.sha } : null },
195
+ });
196
+ avancarParaShip(raiz, threadId);
197
+ if (opcoes.publicar !== false)
198
+ (0, fabrica_publicar_1.publicarEmSegundoPlano)(raiz);
199
+ return resultado(threadId, 'registrou', `ship_done pelo merge ${mergeSha.slice(0, 7)} do PR #${n} de ${repo}`, { mergeSha, headSha, ci });
200
+ }
97
201
  /** `ork ship registrar-pr --todas`: toda thread aberta que ja tem merge `ship(<thread>)` na base. */
98
202
  function registrarEntregasPorPr(carregado, opcoes = {}) {
99
203
  const remoto = opcoes.remoto ?? 'origin';
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.FIM_EXPERIENCIA = exports.INICIO_EXPERIENCIA = void 0;
37
+ exports.caminhoSeguro = caminhoSeguro;
38
+ exports.planejarExperiencia = planejarExperiencia;
39
+ exports.aplicarExperiencia = aplicarExperiencia;
40
+ /** Bloco próprio nos arquivos lidos pelo host. Nunca altera o bloco de ork init. */
41
+ const fs = __importStar(require("node:fs"));
42
+ const path = __importStar(require("node:path"));
43
+ const node_crypto_1 = require("node:crypto");
44
+ exports.INICIO_EXPERIENCIA = '<!-- orkastery:experiencia:begin -->';
45
+ exports.FIM_EXPERIENCIA = '<!-- orkastery:experiencia:end -->';
46
+ const CONTRATO = 'ork.experiencia-install/v1';
47
+ const LINHA_DO_DIRETORIO = /^Se ativo, leia a skill selecionada pelo campo skill no diretório (".*")\.$/;
48
+ const conflito = () => { throw Error('experiencia.bloco.conflict: bloco ou recibo alterado/duplicado; preserve os arquivos e revise a instalação'); };
49
+ /** Recusa symlinks e hardlinks em qualquer componente antes da leitura/escrita. */
50
+ function caminhoSeguro(projeto, arquivo) {
51
+ if (fs.realpathSync(projeto) !== projeto || !arquivo.startsWith(projeto + path.sep))
52
+ throw Error('experiencia.path.unsafe');
53
+ let atual = projeto;
54
+ const partes = path.relative(projeto, arquivo).split(path.sep);
55
+ for (let i = 0; i < partes.length; i++) {
56
+ atual = path.join(atual, partes[i]);
57
+ const s = fs.lstatSync(atual, { throwIfNoEntry: false });
58
+ if (!s)
59
+ continue;
60
+ if (s.isSymbolicLink() || (i < partes.length - 1 ? !s.isDirectory() : !s.isFile() || s.nlink !== 1))
61
+ throw Error('experiencia.path.unsafe');
62
+ }
63
+ }
64
+ function ler(projeto, arquivo) {
65
+ caminhoSeguro(projeto, arquivo);
66
+ return fs.existsSync(arquivo) ? fs.readFileSync(arquivo) : null;
67
+ }
68
+ function iguais(a, b) { return a === null ? b === null : b !== null && a.equals(b); }
69
+ function contar(b, texto) {
70
+ let n = 0, i = 0;
71
+ while ((i = b.indexOf(texto, i)) >= 0) {
72
+ n++;
73
+ i += Buffer.byteLength(texto);
74
+ }
75
+ return n;
76
+ }
77
+ function decodificar(s) {
78
+ if (typeof s !== 'string')
79
+ return conflito();
80
+ const b = Buffer.from(s, 'base64');
81
+ if (b.toString('base64') !== s)
82
+ return conflito();
83
+ return b;
84
+ }
85
+ const eolDe = (b) => b.includes('\r\n') ? '\r\n' : '\n';
86
+ /** O caminho vai relativo ao projeto: o arquivo de instruções costuma ser versionado. */
87
+ function nucleoDoBloco(skill, eol) {
88
+ return Buffer.from([exports.INICIO_EXPERIENCIA,
89
+ '## Experiência de orquestração',
90
+ 'Consulte `ork experiencia show --json` neste projeto. Se experience for false, não ative o pacote.',
91
+ `Se ativo, leia a skill selecionada pelo campo skill no diretório ${JSON.stringify(skill)}.`,
92
+ 'As preferências são dados do núcleo; preserve gates, proveniência humana e permissões do host.',
93
+ exports.FIM_EXPERIENCIA, ''].join(eol));
94
+ }
95
+ /**
96
+ * Bloco sem recibo (clone novo, `.orkastery/` apagado) só é adotado quando é exatamente o texto
97
+ * que esta versão ou a anterior geram, com qualquer diretório; qualquer outra edição é conflito.
98
+ */
99
+ function reconhecerBloco(antes) {
100
+ if (contar(antes, exports.INICIO_EXPERIENCIA) !== 1 || contar(antes, exports.FIM_EXPERIENCIA) !== 1)
101
+ return null;
102
+ const inicio = antes.indexOf(exports.INICIO_EXPERIENCIA), marcaFim = antes.indexOf(exports.FIM_EXPERIENCIA);
103
+ if (marcaFim < inicio || (inicio > 0 && antes[inicio - 1] !== 10))
104
+ return null;
105
+ let fim = marcaFim + Buffer.byteLength(exports.FIM_EXPERIENCIA);
106
+ const eol = eolDe(antes.subarray(inicio, fim));
107
+ if (antes.subarray(fim, fim + eol.length).toString() === eol)
108
+ fim += eol.length;
109
+ const texto = antes.subarray(inicio, fim);
110
+ const linha = texto.toString('utf8').split(eol)[3] ?? '';
111
+ const m = LINHA_DO_DIRETORIO.exec(linha);
112
+ let skill;
113
+ try {
114
+ skill = m ? JSON.parse(m[1]) : undefined;
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ if (typeof skill !== 'string' || /[\r\n`]/.test(skill))
120
+ return null;
121
+ const esperado = nucleoDoBloco(skill, eol);
122
+ if (!texto.equals(esperado) && !texto.equals(esperado.subarray(0, esperado.length - eol.length)))
123
+ return null;
124
+ // Uma linha em branco antes do bloco é o separador que a instalação põe depois de uma linha.
125
+ const sep = Buffer.from(eol);
126
+ const separador = inicio >= 2 * sep.length && antes.subarray(inicio - sep.length, inicio).equals(sep) &&
127
+ antes[inicio - sep.length - 1] === 10 ? sep : Buffer.alloc(0);
128
+ return { inicio, fim, separador, eol, doRecibo: false };
129
+ }
130
+ /**
131
+ * Remove o bloco e, se ainda intacto, o separador. O separador de uma quebra só sai quando a linha
132
+ * anterior continua terminada; o de duas, quando a última linha do conteúdo original continua sem
133
+ * quebra. Conteúdo externo depois do bloco não se funde à última linha do prefixo.
134
+ */
135
+ function removerBloco(antes, bloco) {
136
+ let inicio = bloco.inicio;
137
+ const sep = bloco.separador, antesDoSep = bloco.inicio - sep.length;
138
+ if (sep.length && antesDoSep > 0 && antes.subarray(antesDoSep, bloco.inicio).equals(sep)) {
139
+ const terminaEmLinha = antes[antesDoSep - 1] === 10;
140
+ if ((contar(sep, '\n') === 1) === terminaEmLinha)
141
+ inicio = antesDoSep;
142
+ }
143
+ const prefixo = antes.subarray(0, inicio), sufixo = antes.subarray(bloco.fim);
144
+ const juncao = prefixo.length && prefixo.at(-1) !== 10 && sufixo.length ? Buffer.from(bloco.eol) : Buffer.alloc(0);
145
+ return Buffer.concat([prefixo, juncao, sufixo]);
146
+ }
147
+ /** Planejamento puro em relação ao disco: dry-run e preflight usam o mesmo contrato. */
148
+ function planejarExperiencia(projeto, host, skill) {
149
+ projeto = fs.realpathSync(path.resolve(projeto));
150
+ if (!['claude-code', 'codex'].includes(host))
151
+ throw Error('experiencia.host.unsupported');
152
+ if (skill !== null && /[\r\n`]/.test(skill))
153
+ throw Error('experiencia.skill.invalid');
154
+ const arquivo = path.join(projeto, host === 'codex' ? 'AGENTS.md' : 'CLAUDE.md');
155
+ const reciboPath = path.join(projeto, '.orkastery', 'experiencia', host + '.json');
156
+ const antes = ler(projeto, arquivo), bytesRecibo = ler(projeto, reciboPath);
157
+ let recibo = null;
158
+ if (bytesRecibo) {
159
+ try {
160
+ recibo = JSON.parse(bytesRecibo.toString('utf8'));
161
+ }
162
+ catch {
163
+ conflito();
164
+ }
165
+ if (!recibo || recibo.contrato !== CONTRATO || recibo.host !== host)
166
+ return conflito();
167
+ if (recibo.anterior !== null)
168
+ decodificar(recibo.anterior);
169
+ }
170
+ const marcas = antes ? contar(antes, exports.INICIO_EXPERIENCIA) + contar(antes, exports.FIM_EXPERIENCIA) : 0;
171
+ // Sem marcas, um recibo que sobrou (arquivo apagado ou bloco tirado à mão) não descreve nada.
172
+ let bloco = null;
173
+ if (antes && marcas) {
174
+ if (recibo) {
175
+ const adicao = decodificar(recibo.adicao), corte = adicao.indexOf(exports.INICIO_EXPERIENCIA);
176
+ if (contar(adicao, exports.INICIO_EXPERIENCIA) !== 1 || contar(adicao, exports.FIM_EXPERIENCIA) !== 1 || corte < 0)
177
+ return conflito();
178
+ const nucleo = adicao.subarray(corte), inicio = antes.indexOf(nucleo);
179
+ if (marcas === 2 && inicio >= 0) {
180
+ bloco = { inicio, fim: inicio + nucleo.length, separador: adicao.subarray(0, corte), eol: eolDe(nucleo), doRecibo: true };
181
+ }
182
+ }
183
+ bloco ??= reconhecerBloco(antes);
184
+ if (!bloco)
185
+ return conflito();
186
+ }
187
+ let depois = antes, proximoRecibo = null;
188
+ if (skill !== null) {
189
+ let separador, nucleo, anterior;
190
+ if (antes && bloco) {
191
+ // Atualização no lugar: o que está antes e depois do bloco, inclusive edição externa, fica.
192
+ nucleo = nucleoDoBloco(skill, bloco.eol);
193
+ separador = bloco.separador;
194
+ depois = Buffer.concat([antes.subarray(0, bloco.inicio), nucleo, antes.subarray(bloco.fim)]);
195
+ // Adotado sem recibo: se só havia o bloco, o arquivo conta como criado pelo pacote.
196
+ const semBloco = removerBloco(antes, bloco);
197
+ anterior = bloco.doRecibo ? recibo.anterior : semBloco.length ? semBloco.toString('base64') : null;
198
+ }
199
+ else {
200
+ const base = antes ?? Buffer.alloc(0), eol = eolDe(base);
201
+ nucleo = nucleoDoBloco(skill, eol);
202
+ separador = Buffer.from(base.length ? (base.at(-1) === 10 ? eol : eol + eol) : '');
203
+ depois = Buffer.concat([base, separador, nucleo]);
204
+ anterior = antes?.toString('base64') ?? null;
205
+ }
206
+ proximoRecibo = Buffer.from(JSON.stringify({ contrato: CONTRATO, host, anterior,
207
+ adicao: Buffer.concat([separador, nucleo]).toString('base64'),
208
+ }, null, 2) + '\n');
209
+ }
210
+ else if (antes && bloco) {
211
+ depois = removerBloco(antes, bloco);
212
+ // Vazio só some quando o arquivo não existia (recibo) ou quando, sem recibo, só havia o bloco.
213
+ if (depois.length === 0 && (recibo ? recibo.anterior === null : true))
214
+ depois = null;
215
+ }
216
+ const mudancas = [{ arquivo, antes, depois }, { arquivo: reciboPath, antes: bytesRecibo, depois: proximoRecibo }]
217
+ .filter(m => !iguais(m.antes, m.depois));
218
+ return { projeto, host, acao: skill === null ? 'remover' : 'instalar', mudancas };
219
+ }
220
+ /** Revalida o plano inteiro; falha de gravação restaura as mudanças já aplicadas. */
221
+ function aplicarExperiencia(plano) {
222
+ for (const m of plano.mudancas)
223
+ if (!iguais(ler(plano.projeto, m.arquivo), m.antes))
224
+ throw Error('experiencia.plan.stale');
225
+ const aplicadas = [];
226
+ const gravar = (arquivo, conteudo) => {
227
+ caminhoSeguro(plano.projeto, arquivo);
228
+ if (conteudo === null) {
229
+ fs.unlinkSync(arquivo);
230
+ return;
231
+ }
232
+ fs.mkdirSync(path.dirname(arquivo), { recursive: true });
233
+ const tmp = arquivo + '.' + (0, node_crypto_1.randomUUID)() + '.tmp';
234
+ try {
235
+ // Arquivo novo segue o umask, como um CLAUDE.md criado à mão.
236
+ const modo = fs.statSync(arquivo, { throwIfNoEntry: false })?.mode ?? 0o666;
237
+ fs.writeFileSync(tmp, conteudo, { flag: 'wx', mode: modo });
238
+ fs.renameSync(tmp, arquivo);
239
+ }
240
+ finally {
241
+ if (fs.existsSync(tmp))
242
+ fs.unlinkSync(tmp);
243
+ }
244
+ };
245
+ try {
246
+ for (const m of plano.mudancas) {
247
+ gravar(m.arquivo, m.depois);
248
+ aplicadas.push(m);
249
+ }
250
+ }
251
+ catch (e) {
252
+ for (const m of aplicadas.reverse())
253
+ gravar(m.arquivo, m.antes);
254
+ throw e;
255
+ }
256
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizarIdioma = normalizarIdioma;
4
+ exports.validarPreferencias = validarPreferencias;
5
+ exports.idiomaDoSistema = idiomaDoSistema;
6
+ exports.resolverExperiencia = resolverExperiencia;
7
+ /** Preferências públicas da experiência. Consulta não grava respostas nem autoria. */
8
+ const horario_1 = require("./horario");
9
+ function normalizarIdioma(valor) {
10
+ if (typeof valor !== 'string' || !valor.trim())
11
+ return undefined;
12
+ try {
13
+ return Intl.getCanonicalLocales(valor.trim())[0];
14
+ }
15
+ catch {
16
+ return undefined;
17
+ }
18
+ }
19
+ function validarPreferencias(valor) {
20
+ if (!valor || typeof valor !== 'object' || Array.isArray(valor))
21
+ throw Error('experiencia.config.invalid: owner deve ser um objeto');
22
+ const v = valor, resultado = {};
23
+ if (v.language !== undefined) {
24
+ const idioma = normalizarIdioma(v.language);
25
+ if (!idioma)
26
+ throw Error('experiencia.config.invalid: owner.language exige locale BCP-47');
27
+ resultado.language = idioma;
28
+ }
29
+ if (v.timezone !== undefined) {
30
+ const fuso = (0, horario_1.normalizarFuso)(v.timezone);
31
+ if (!fuso)
32
+ throw Error('experiencia.config.invalid: owner.timezone exige fuso IANA');
33
+ resultado.timezone = fuso;
34
+ }
35
+ if (v.depth !== undefined) {
36
+ if (v.depth !== 'curta' && v.depth !== 'detalhada')
37
+ throw Error('experiencia.config.invalid: owner.depth exige curta ou detalhada');
38
+ resultado.depth = v.depth;
39
+ }
40
+ if (v.experience !== undefined) {
41
+ if (typeof v.experience !== 'boolean')
42
+ throw Error('experiencia.config.invalid: owner.experience exige booleano');
43
+ resultado.experience = v.experience;
44
+ }
45
+ return resultado;
46
+ }
47
+ /** C, POSIX e und (raiz do ICU) dizem que o sistema não escolheu idioma: vale o padrão. */
48
+ const SEM_IDIOMA = new Set(['c', 'posix', 'und']);
49
+ function idiomaEscolhido(valor) {
50
+ const idioma = normalizarIdioma(valor);
51
+ return idioma && !SEM_IDIOMA.has(idioma.toLowerCase().split('-')[0]) ? idioma : undefined;
52
+ }
53
+ function idiomaDoSistema(env = process.env, doIntl = () => Intl.DateTimeFormat().resolvedOptions().locale) {
54
+ const locale = env.LC_ALL || env.LC_MESSAGES || env.LANG;
55
+ return idiomaEscolhido(locale?.split(/[.@]/)[0].replace(/_/g, '-')) ?? idiomaEscolhido(doIntl()) ?? 'en-US';
56
+ }
57
+ function resolverExperiencia(owner = {}, sistema = { language: idiomaDoSistema(), timezone: (0, horario_1.fusoDoSistema)() }) {
58
+ const p = validarPreferencias(owner);
59
+ const language = p.language ?? normalizarIdioma(sistema.language) ?? 'en-US';
60
+ return {
61
+ language, timezone: p.timezone ?? (0, horario_1.normalizarFuso)(sistema.timezone) ?? 'UTC',
62
+ depth: p.depth ?? 'curta', experience: p.experience ?? true,
63
+ skill: language.toLowerCase().startsWith('pt-') || language === 'pt'
64
+ ? 'orchestration-experience-pt-br' : 'orchestration-experience',
65
+ origem: {
66
+ language: p.language === undefined ? 'sistema' : 'manifesto',
67
+ timezone: p.timezone === undefined ? 'sistema' : 'manifesto',
68
+ depth: p.depth === undefined ? 'padrao' : 'manifesto',
69
+ experience: p.experience === undefined ? 'padrao' : 'manifesto',
70
+ },
71
+ };
72
+ }