@orkastery/cli 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapters/claude-code/README.md +1 -1
- package/adapters/claude-code/agents/ork-check.md +1 -1
- package/adapters/claude-code/agents/ork-go.md +1 -1
- package/adapters/claude-code/agents/ork-goal.md +1 -1
- package/adapters/claude-code/agents/ork-master.md +1 -1
- package/adapters/claude-code/agents/ork-plan.md +1 -1
- package/adapters/claude-code/agents/ork-ship.md +1 -1
- package/adapters/claude-code/commands/ork.md +1 -1
- package/adapters/claude-code/mods/ork-torre/hooks/frases.ts +2 -0
- package/adapters/codex/skills/ork/SKILL.md +1 -1
- package/adapters/hermes/README.md +1 -1
- package/adapters/openclaw/openclaw.plugin.json +1 -1
- package/adapters/openclaw/package.json +1 -1
- package/assets/adapters-en/claude-code/README.md +1 -1
- package/assets/adapters-en/claude-code/agents/ork-check.md +1 -1
- package/assets/adapters-en/claude-code/agents/ork-go.md +1 -1
- package/assets/adapters-en/claude-code/agents/ork-goal.md +1 -1
- package/assets/adapters-en/claude-code/agents/ork-master.md +1 -1
- package/assets/adapters-en/claude-code/agents/ork-plan.md +1 -1
- package/assets/adapters-en/claude-code/agents/ork-ship.md +1 -1
- package/assets/adapters-en/claude-code/commands/ork.md +1 -1
- package/assets/adapters-en/codex/skills/ork/SKILL.md +1 -1
- package/assets/adapters-en/hermes/README.md +1 -1
- package/assets/docs/padroes/roadmap-de-produto.md +1 -1
- package/assets/docs-en/padroes/roadmap-de-produto.md +1 -1
- package/assets/orkmind_fixture.py +4 -1
- package/assets/references-en/testing-patterns.md +1 -1
- package/assets/skills-en/core/orkastery-bootstrap/SKILL.md +1 -1
- package/assets/skills-en/phases/check-quality/SKILL.md +1 -1
- package/assets/skills-en/phases/go-implementation/SKILL.md +1 -1
- package/dist/adapters/claude-bg.js +3 -1
- package/dist/adapters/grok-mcp-servidor.js +22 -12
- package/dist/adapters/grok.js +1 -1
- package/dist/agendador-cpu.js +10 -2
- package/dist/catalogo.js +3 -3
- package/dist/{canarios-hitl.js → cenarios-hitl.js} +7 -7
- package/dist/{canarios-i43.js → cenarios-i43.js} +16 -16
- package/dist/{canarios-pulse.js → cenarios-pulse.js} +3 -3
- package/dist/{canarios-rm057.js → cenarios-rm057.js} +3 -3
- package/dist/{canarios-sensores.js → cenarios-sensores.js} +5 -5
- package/dist/{canarios.js → cenarios.js} +22 -22
- package/dist/changelog-check.js +217 -0
- package/dist/check-agil.js +242 -5
- package/dist/check-paralelo.js +17 -4
- package/dist/ci.js +3 -2
- package/dist/claim-lint.js +29 -0
- package/dist/claims.js +5 -1
- package/dist/docs.js +8 -1
- package/dist/evalrunner.js +37 -37
- package/dist/fila-pesada.js +121 -11
- package/dist/hitl-canais.js +1 -1
- package/dist/hitl-classificacao.js +1 -0
- package/dist/hitl-presentation.js +1 -1
- package/dist/hitl-tempo-parado.js +1 -1
- package/dist/index.js +47 -29
- package/dist/init.js +1 -1
- package/dist/intelligence-graph-cli.js +27 -9
- package/dist/locale-ajuda.js +6 -6
- package/dist/locale-claims.js +16 -2
- package/dist/locale-grafo.js +56 -1
- package/dist/locale-maestro.js +38 -0
- package/dist/locale-network.js +8 -6
- package/dist/locale-onboarding.js +14 -0
- package/dist/locale-operacao-restante.js +18 -18
- package/dist/locale-preflight.js +80 -0
- package/dist/locale-projetos.js +6 -0
- package/dist/locale-prompts.js +8 -8
- package/dist/locale-pulse.js +4 -0
- package/dist/locale-retry.js +16 -0
- package/dist/locale-security-scan.js +35 -1
- package/dist/locale-verify.js +15 -0
- package/dist/locale.js +24 -7
- package/dist/maestro-cli.js +12 -17
- package/dist/mcp-install.js +2 -1
- package/dist/mcp-server.js +20 -17
- package/dist/mcp-verify-async.js +353 -0
- package/dist/mcp-verify-worker.js +17 -0
- package/dist/mcp-verify.js +10 -1
- package/dist/pedido-conducao.js +51 -0
- package/dist/preflight.js +25 -26
- package/dist/prompts.js +4 -2
- package/dist/pulse.js +4 -2
- package/dist/rede.js +24 -12
- package/dist/retry.js +12 -0
- package/dist/sandbox.js +11 -11
- package/dist/security-scan/cli.js +2 -2
- package/dist/security-scan/report.js +1 -1
- package/dist/session-watcher.js +33 -7
- package/dist/verify.js +166 -34
- package/eval/README.md +8 -8
- package/eval/fixtures/fx-codex-dry/caso.json +1 -1
- package/eval/fixtures/fx-happy/caso.json +1 -1
- package/eval/fixtures/fx-indice-reversao/caso.json +1 -1
- package/eval/fixtures/fx-listagem-abertas/caso.json +1 -1
- package/eval/fixtures/fx-omnicanal/caso.json +1 -1
- package/eval/fixtures/fx-pedido-colado/caso.json +1 -1
- package/eval/fixtures/fx-schema-drift/caso.json +1 -1
- package/package.json +1 -1
- package/references/testing-patterns.md +1 -1
- package/skills/core/orkastery-bootstrap/SKILL.md +1 -1
- package/skills/phases/check-quality/SKILL.md +1 -1
- package/skills/phases/go-implementation/SKILL.md +1 -1
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Cenarios da I-43: cada remocao so e dada como pronta junto do cenario que prova que
|
|
4
4
|
* a falha evitada pelo mecanismo removido continua evitada por outra coisa.
|
|
5
5
|
*
|
|
6
|
-
* Remocao sem
|
|
6
|
+
* Remocao sem cenario nao e simplificacao, e aposta. E a razao de este arquivo existir
|
|
7
7
|
* separado: o CHECK confere as cinco remocoes num lugar so, e o que cada uma perdeu
|
|
8
8
|
* esta escrito ao lado do que a substitui.
|
|
9
9
|
*
|
|
10
|
-
* Os
|
|
10
|
+
* Os cenarios rodam em sandbox git propria (`sandboxGit`), nunca contra o board vivo:
|
|
11
11
|
* `raizDoEstado` manda o estado para a arvore principal mesmo de dentro de uma worktree
|
|
12
12
|
* vinculada, entao qualquer conferencia feita "no projeto" leria as 137 threads reais
|
|
13
13
|
* e viraria contagem de artefato vivo, que reprova sozinha quando a branch cresce.
|
|
@@ -46,7 +46,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
46
46
|
};
|
|
47
47
|
})();
|
|
48
48
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
-
exports.
|
|
49
|
+
exports.CENARIOS_I43 = void 0;
|
|
50
50
|
const sandbox_1 = require("./sandbox");
|
|
51
51
|
const fs = __importStar(require("node:fs"));
|
|
52
52
|
const hosts_1 = require("./hosts");
|
|
@@ -82,7 +82,7 @@ function pedidoLegado(modo) {
|
|
|
82
82
|
* `modo: look, profundidade: profunda` deixaria de validar EM SILENCIO: o produto
|
|
83
83
|
* passaria a negar o que ele mesmo emitiu e assinou.
|
|
84
84
|
*
|
|
85
|
-
* O
|
|
85
|
+
* O cenario grava uma thread em modo aposentado direto no disco da sandbox, como uma
|
|
86
86
|
* thread de setembro esta gravada, e cobra que ela abra inteira.
|
|
87
87
|
*/
|
|
88
88
|
const fxModoAposentadoLeitor = {
|
|
@@ -139,7 +139,7 @@ const fxModoAposentadoLeitor = {
|
|
|
139
139
|
* resultado desta aposentadoria: o builder escreve `#Look` no pedido e recebe uma
|
|
140
140
|
* thread em `#Auto` sem saber que trocou de regime de supervisao.
|
|
141
141
|
*
|
|
142
|
-
* O
|
|
142
|
+
* O cenario cobra as tres coisas juntas: a recusa acontece, ela e TIPADA, e ela nomeia
|
|
143
143
|
* um substituto que EXISTE. Toda #TAG do texto e um modo da matriz, e o substituto e
|
|
144
144
|
* sempre de ciclo completo: com a I-42, o `#Fast` existe, mas nao substitui modo de
|
|
145
145
|
* vigilancia maxima.
|
|
@@ -207,12 +207,12 @@ const fxModoAposentadoEscritor = {
|
|
|
207
207
|
* `Sessao adotada claude-bg <uuid>`, lixo do mecanismo de adocao de 07 a 14/09. Quem
|
|
208
208
|
* pagava era quem nao sabia que existia flag, porque flag nenhuma existia.
|
|
209
209
|
*
|
|
210
|
-
* O
|
|
210
|
+
* O cenario roda em SANDBOX e nunca contra o board vivo, e isso nao e detalhe: M8 do
|
|
211
211
|
* PLAN mediu que `ork thread list` rodado DENTRO desta worktree lista as 137 threads
|
|
212
212
|
* da arvore principal, porque `raizDoEstado` resolve o estado pelo `git-common-dir`.
|
|
213
213
|
* Conferir "no projeto" seria comparar 137 com 10 e afirmar contagem de artefato vivo,
|
|
214
214
|
* que reprova sozinha quando a branch cresce. Aqui as contagens sao 1 e 2, fixadas
|
|
215
|
-
* pelo proprio
|
|
215
|
+
* pelo proprio cenario.
|
|
216
216
|
*
|
|
217
217
|
* A thread PAUSADA e a razao de o default ser "nao fechadas" e nao "so abertas", que
|
|
218
218
|
* era o texto do GOAL: esconder a pausada seria esconder a que mais pede atencao.
|
|
@@ -246,7 +246,7 @@ const fxListagemAbertas = {
|
|
|
246
246
|
comTodas: todas.length,
|
|
247
247
|
todasTemAFechada: todas.some((l) => l.id === fechada.id),
|
|
248
248
|
// A soma bate: default + fechadas = tudo. Sem afirmar numero de artefato vivo,
|
|
249
|
-
// porque as tres threads sao criadas por este
|
|
249
|
+
// porque as tres threads sao criadas por este cenario.
|
|
250
250
|
somaBate: padrao.length + todas.filter((l) => l.status === 'fechada').length === todas.length,
|
|
251
251
|
// E o texto avisa que escondeu algo, em vez de esconder em silencio.
|
|
252
252
|
avisaQueEscondeu: texto.includes('fechada(s) omitida(s)'),
|
|
@@ -261,7 +261,7 @@ const fxListagemAbertas = {
|
|
|
261
261
|
* fx-indice-reversao: a entrega revertida se rebaixa SOZINHA, pelo caminho que o
|
|
262
262
|
* produto percorre de verdade.
|
|
263
263
|
*
|
|
264
|
-
* E o
|
|
264
|
+
* E o cenario que libera a saida da fila de ratificacao de score, e ele prova a
|
|
265
265
|
* propriedade exata que a fila nao entregava: a fila tinha 13 entradas esperando nota
|
|
266
266
|
* humana justamente porque lembrete nao converte em nota, e nenhuma delas dizia se a
|
|
267
267
|
* entrega tinha sido desfeita depois.
|
|
@@ -269,7 +269,7 @@ const fxListagemAbertas = {
|
|
|
269
269
|
* ELE NAO CHAMA A DETECCAO A MAO, e essa e a mudanca do GO-FIX 1. Na primeira versao
|
|
270
270
|
* ele chamava `registrarReversaoDaEntrega` direto, que era justamente o passo que no
|
|
271
271
|
* produto ninguem executava: o unico chamador era o COMECO de um `ork ship` seguinte
|
|
272
|
-
* da mesma thread, e depois do MASTER nao ha ship seguinte. O
|
|
272
|
+
* da mesma thread, e depois do MASTER nao ha ship seguinte. O cenario ficava verde com
|
|
273
273
|
* a propriedade quebrada, e o CHECK provou com `git revert` real que o produto gravava
|
|
274
274
|
* score 5 e a justificativa "sem reversao" numa entrega desfeita.
|
|
275
275
|
*
|
|
@@ -360,7 +360,7 @@ const fxIndiceReversao = {
|
|
|
360
360
|
* de CHECK, e em 11 delas o CHECK rodou apenas em runtime que o GO tambem usou. As
|
|
361
361
|
* quatro threads mais recentes sao todas `claude-bg` conferindo `claude-bg`.
|
|
362
362
|
*
|
|
363
|
-
* O
|
|
363
|
+
* O cenario prova os TRES lados, porque so o positivo nao provaria nada:
|
|
364
364
|
* - a thread que EXIGE recusa o CHECK no runtime do GO, com motivo tipado;
|
|
365
365
|
* - a mesma thread ACEITA o CHECK em outro runtime;
|
|
366
366
|
* - a thread que NAO exige despacha normalmente, entao a regra nao vazou para quem
|
|
@@ -421,7 +421,7 @@ const fxCheckRuntimeCruzado = {
|
|
|
421
421
|
* objective da thread nao esta approved, com teste que cobre a recusa". Bonito, e
|
|
422
422
|
* impossivel de reprovar.
|
|
423
423
|
*
|
|
424
|
-
* O
|
|
424
|
+
* O cenario prova as duas metades, porque criterio que so passa nao prova nada:
|
|
425
425
|
* - o criterio que o comando SUSTENTA passa e a thread verifica;
|
|
426
426
|
* - o criterio que o comando NAO sustenta reprova, e o veredito diz QUAL criterio
|
|
427
427
|
* falhou, pela alegacao que a claim carrega.
|
|
@@ -478,7 +478,7 @@ const fxDoneWhenExecutavel = {
|
|
|
478
478
|
* a copia. Trocar uma recusa barulhenta por nada seria trocar investigacao de
|
|
479
479
|
* madrugada por defeito que ninguem ve.
|
|
480
480
|
*
|
|
481
|
-
* O que muda e o que BARRA, nao o que DETECTA. O
|
|
481
|
+
* O que muda e o que BARRA, nao o que DETECTA. O cenario cobra os dois lados:
|
|
482
482
|
* - a copia editada e detectada, NOMEADA, e classificada como `copia-editada`;
|
|
483
483
|
* - a divergencia segura (`catalogo-andou`) resolve sozinha e nao vira decisao;
|
|
484
484
|
* - a saida traz o `diff` do arquivo e um caminho de UM comando, com o `--dir` em
|
|
@@ -492,7 +492,7 @@ const fxAdapterEditadoDetectado = {
|
|
|
492
492
|
rodar: (ctx) => {
|
|
493
493
|
const destino = (0, sandbox_1.dirTemporario)('adapter-editado');
|
|
494
494
|
try {
|
|
495
|
-
const comum = { projeto: destino, dir: '.', catalogo: ctx.catalogo, versao: '0.0.0-
|
|
495
|
+
const comum = { projeto: destino, dir: '.', catalogo: ctx.catalogo, versao: '0.0.0-cenario' };
|
|
496
496
|
// 1. Instalacao limpa: nasce o recibo com o sha de cada arquivo.
|
|
497
497
|
const primeira = (0, hosts_1.instalarAdaptador)('hermes', comum);
|
|
498
498
|
const recibo = (0, hosts_1.lerRecibo)(destino);
|
|
@@ -532,7 +532,7 @@ const fxAdapterEditadoDetectado = {
|
|
|
532
532
|
}
|
|
533
533
|
},
|
|
534
534
|
};
|
|
535
|
-
exports.
|
|
535
|
+
exports.CENARIOS_I43 = [
|
|
536
536
|
fxModoAposentadoLeitor,
|
|
537
537
|
fxModoAposentadoEscritor,
|
|
538
538
|
fxListagemAbertas,
|
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.
|
|
36
|
+
exports.CENARIOS_PULSE = void 0;
|
|
37
37
|
const fs = __importStar(require("node:fs"));
|
|
38
38
|
const path = __importStar(require("node:path"));
|
|
39
39
|
const node_child_process_1 = require("node:child_process");
|
|
@@ -52,7 +52,7 @@ const pulse_1 = require("./pulse");
|
|
|
52
52
|
const orquestracao_1 = require("./orquestracao");
|
|
53
53
|
const pulse_delivery_1 = require("./pulse-delivery");
|
|
54
54
|
const catalogo_1 = require("./catalogo");
|
|
55
|
-
exports.
|
|
55
|
+
exports.CENARIOS_PULSE = [
|
|
56
56
|
{ id: 'fx-estado-dividido', sobre: '90 eventos na worktree não podem parecer 14 na main', precisaDeGit: true, rodar: () => {
|
|
57
57
|
const s = (0, sandbox_1.sandboxGit)('estado-dividido');
|
|
58
58
|
try {
|
|
@@ -116,7 +116,7 @@ exports.CANARIOS_PULSE = [
|
|
|
116
116
|
const t = (0, thread_1.novaThread)(s.carregado, { nome: 'resposta simulada', modo: 'classic' }).thread;
|
|
117
117
|
(0, ledger_1.registrar)((0, thread_1.dirThread)(s.dir, t.id), t.id, 'phase_result', { fase: 'GOAL', evidencia: 'SIMULADO' });
|
|
118
118
|
const pedido = (0, hitl_gates_1.abrirPedidoGate)(s.dir, t.id);
|
|
119
|
-
const chave = '
|
|
119
|
+
const chave = 'cenario-SIMULADO-sem-credencial-real-000';
|
|
120
120
|
const resposta = { origem: 'telegram', por: 'telegram:42', mensagem: 'telegram:-7:99',
|
|
121
121
|
resposta: '1', recebidoEm: new Date().toISOString() };
|
|
122
122
|
const envelope = { ...resposta, prova: (0, hitl_gates_1.assinaturaDaResposta)(t.id, pedido.id, resposta, chave) };
|
|
@@ -35,11 +35,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.fxPedidoColado = exports.PROMPTS_DE_CONDUCAO = exports.FRASE_DO_PEDIDO_COLADO = exports.PEDIDO_COLADO_DO_INCIDENTE = void 0;
|
|
37
37
|
/**
|
|
38
|
-
* RM-057 (fatia 2): o
|
|
38
|
+
* RM-057 (fatia 2): o cenario do incidente de 01/10/2026.
|
|
39
39
|
*
|
|
40
40
|
* Na noite de 01 para 02/10 o condutor recebeu, colado, o pedido do dono com push e merge
|
|
41
41
|
* autorizados ate um horario, parou para pedir um "confirmo" em texto livre e a fabrica ficou mais
|
|
42
|
-
* de 10 h sem produzir. O
|
|
42
|
+
* de 10 h sem produzir. O cenario refaz a conducao inteira em sandbox SIMULADO e exige que o mesmo
|
|
43
43
|
* pedido colado siga sem parar:
|
|
44
44
|
*
|
|
45
45
|
* 1. o modo sai da #TAG do texto colado (`#Auto`), pelo nucleo;
|
|
@@ -97,7 +97,7 @@ exports.fxPedidoColado = {
|
|
|
97
97
|
const decisao = (0, decisao_autonoma_1.registrarDecisao)(s.dir, t.id, {
|
|
98
98
|
decidido: 'Seguir com push e merge dentro da autorização colada', porque: 'o dono autorizou até 02/10 às 12h',
|
|
99
99
|
comoMudar: 'reverter o merge', custoDeReverter: { agora: 'um revert', depois: 'um revert' },
|
|
100
|
-
criterio: { tipo: 'ledger', referencia: `${t.id}#evento:1` }, quemDecidiu: '
|
|
100
|
+
criterio: { tipo: 'ledger', referencia: `${t.id}#evento:1` }, quemDecidiu: 'cenario-SIMULADO', evidencia: 'pedido colado',
|
|
101
101
|
});
|
|
102
102
|
const confirmo = {
|
|
103
103
|
contrato: hitl_contract_1.CONTRATO_HITL_V2, id: 'confirmo-SIMULADO', thread: t.id, fase: t.faseAtual, modo: t.modo,
|
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.
|
|
36
|
+
exports.CENARIO_SENSORES = void 0;
|
|
37
37
|
/** T8: subprocessos, CLI e estado canônico reais; protocolo Codex sintético. */
|
|
38
38
|
const fs = __importStar(require("node:fs"));
|
|
39
39
|
const path = __importStar(require("node:path"));
|
|
@@ -50,11 +50,11 @@ function esperar(teste) {
|
|
|
50
50
|
const limite = Date.now() + 5000;
|
|
51
51
|
while (!teste()) {
|
|
52
52
|
if (Date.now() >= limite)
|
|
53
|
-
throw new Error('subprocesso do
|
|
53
|
+
throw new Error('subprocesso do cenário não terminou em 5000 ms');
|
|
54
54
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
exports.
|
|
57
|
+
exports.CENARIO_SENSORES = {
|
|
58
58
|
id: 'fx-sensores-runtime', sobre: 'hooks, supervisor e watcher integrados em sandbox Git', precisaDeGit: true,
|
|
59
59
|
rodar: ({ catalogo }) => {
|
|
60
60
|
const s = (0, sandbox_1.sandboxGit)('sensores-runtime');
|
|
@@ -75,7 +75,7 @@ exports.CANARIO_SENSORES = {
|
|
|
75
75
|
input: JSON.stringify({ cwd: wt, session_id: 'fixture-claude', hook_event_name: 'PermissionRequest', tool_use_id: 'fixture-tool' }),
|
|
76
76
|
env: { ...process.env, ORK_SENSOR_CLI: cli } });
|
|
77
77
|
if (h.status !== 0 || h.stderr || h.stdout)
|
|
78
|
-
throw new Error('hook/CLI do
|
|
78
|
+
throw new Error('hook/CLI do cenário indisponível');
|
|
79
79
|
const hookLatenciaMs = Date.now() - start;
|
|
80
80
|
const bloqueio = (0, ledger_1.lerLedger)(dir).find(e => e.tipo === 'sessao_bloqueada');
|
|
81
81
|
const carimboLatenciaMs = bloqueio ? Date.parse(String(bloqueio.recebidoEm)) - Date.parse(bloqueio.ts) : null;
|
|
@@ -100,7 +100,7 @@ exports.CANARIO_SENSORES = {
|
|
|
100
100
|
const runner = require.resolve('./adapters/codex-runner');
|
|
101
101
|
const morto = (0, node_child_process_1.spawnSync)(process.execPath, ['-e', `console.log(JSON.stringify(require(process.argv[1]).identidadeProcesso(process.pid)))`, runner], { cwd: wt, encoding: 'utf8', timeout: 5000 });
|
|
102
102
|
if (morto.status !== 0)
|
|
103
|
-
throw new Error('processo de morte do
|
|
103
|
+
throw new Error('processo de morte do cenário indisponível');
|
|
104
104
|
const filho = JSON.parse(morto.stdout);
|
|
105
105
|
if ((0, codex_runner_1.estadoProcesso)(filho) !== 'ausente')
|
|
106
106
|
throw new Error('processo ainda não está ausente');
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Os 7
|
|
3
|
+
* Os 7 cenarios de `ork eval` (6 do bloco B4 + fx-codex-dry da homologacao do codex).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Cenario nao e teste unitario: e um comportamento inteiro do produto, exercitado ponta a
|
|
6
6
|
* ponta contra git de verdade, que precisa continuar acontecendo depois de qualquer mudanca.
|
|
7
7
|
* Cada um nasce de um jeito conhecido de o trabalho de um agente parecer certo e nao ser:
|
|
8
8
|
*
|
|
@@ -51,8 +51,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
51
51
|
};
|
|
52
52
|
})();
|
|
53
53
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
-
exports.gitDisponivel = exports.
|
|
55
|
-
exports.
|
|
54
|
+
exports.gitDisponivel = exports.CENARIOS = void 0;
|
|
55
|
+
exports.cenarioPorId = cenarioPorId;
|
|
56
56
|
const node_child_process_1 = require("node:child_process");
|
|
57
57
|
const fs = __importStar(require("node:fs"));
|
|
58
58
|
const path = __importStar(require("node:path"));
|
|
@@ -67,11 +67,11 @@ Object.defineProperty(exports, "gitDisponivel", { enumerable: true, get: functio
|
|
|
67
67
|
const thread_1 = require("./thread");
|
|
68
68
|
const verify_1 = require("./verify");
|
|
69
69
|
const worktree_1 = require("./worktree");
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const
|
|
70
|
+
const cenarios_pulse_1 = require("./cenarios-pulse");
|
|
71
|
+
const cenarios_sensores_1 = require("./cenarios-sensores");
|
|
72
|
+
const cenarios_hitl_1 = require("./cenarios-hitl");
|
|
73
|
+
const cenarios_i43_1 = require("./cenarios-i43");
|
|
74
|
+
const cenarios_rm057_1 = require("./cenarios-rm057");
|
|
75
75
|
const objective_1 = require("./objective");
|
|
76
76
|
const maestro_discovery_1 = require("./maestro-discovery");
|
|
77
77
|
const maestro_cli_1 = require("./maestro-cli");
|
|
@@ -196,7 +196,7 @@ const fxWikiDestroy = {
|
|
|
196
196
|
};
|
|
197
197
|
}
|
|
198
198
|
};
|
|
199
|
-
// A outra metade do
|
|
199
|
+
// A outra metade do cenario: a policy bloqueante do nucleo, que reprova entregar em cima
|
|
200
200
|
// da propria base. Guard do host e policy do nucleo sao defesas independentes de proposito.
|
|
201
201
|
const manifesto = {
|
|
202
202
|
runtime: { provider_policy: 'subscription-only' },
|
|
@@ -273,7 +273,7 @@ const fxConcurrency = {
|
|
|
273
273
|
* Nasce da homologacao do segundo runtime (thread ork-homologarcod): um `setup.json`
|
|
274
274
|
* apontando o bloco para o codex tem de mudar o COMANDO que o despacho monta, e o
|
|
275
275
|
* default sem setup tem de continuar saindo pelo claude-bg byte a byte (paridade sem
|
|
276
|
-
* downgrade). Tudo em dry-run:
|
|
276
|
+
* downgrade). Tudo em dry-run: cenario nao gasta assinatura de ninguem.
|
|
277
277
|
*/
|
|
278
278
|
const fxCodexDry = {
|
|
279
279
|
id: 'fx-codex-dry',
|
|
@@ -335,7 +335,7 @@ const fxMaestroBootstrap = {
|
|
|
335
335
|
const exit = (0, maestro_cli_1.runMaestroCli)(['--json'], s.dir, {}, { out: v => { raw = v; }, err: () => { } });
|
|
336
336
|
const clean = JSON.parse(raw);
|
|
337
337
|
const createsThread = (0, thread_1.listarIds)(s.dir).length !== before;
|
|
338
|
-
const t = (0, thread_1.novaThread)(s.carregado, { nome: '
|
|
338
|
+
const t = (0, thread_1.novaThread)(s.carregado, { nome: 'Cenário Maestro', modo: 'auto' }).thread;
|
|
339
339
|
const context = (0, maestro_discovery_1.discoverMaestro)({ cwd: s.dir });
|
|
340
340
|
const snapshot = () => (0, maestro_cli_1.readMaestro)(context, { host: { tools: ['ork_thread_status', 'ork_phase_run'], child: true } });
|
|
341
341
|
const state = snapshot();
|
|
@@ -359,8 +359,8 @@ const fxMaestroBootstrap = {
|
|
|
359
359
|
};
|
|
360
360
|
}),
|
|
361
361
|
};
|
|
362
|
-
/** Os
|
|
363
|
-
exports.
|
|
362
|
+
/** Os cenarios registrados, na ordem em que o `ork eval` os roda. */
|
|
363
|
+
exports.CENARIOS = [
|
|
364
364
|
fxHappy,
|
|
365
365
|
fxHallucination,
|
|
366
366
|
fxStaleBase,
|
|
@@ -370,13 +370,13 @@ exports.CANARIOS = [
|
|
|
370
370
|
fxCodexDry,
|
|
371
371
|
fxObjectiveOscillation,
|
|
372
372
|
fxMaestroBootstrap,
|
|
373
|
-
...
|
|
374
|
-
|
|
375
|
-
...
|
|
376
|
-
...
|
|
377
|
-
|
|
373
|
+
...cenarios_pulse_1.CENARIOS_PULSE,
|
|
374
|
+
cenarios_sensores_1.CENARIO_SENSORES,
|
|
375
|
+
...cenarios_hitl_1.CENARIOS_HITL,
|
|
376
|
+
...cenarios_i43_1.CENARIOS_I43,
|
|
377
|
+
cenarios_rm057_1.fxPedidoColado,
|
|
378
378
|
];
|
|
379
|
-
/** Um
|
|
380
|
-
function
|
|
381
|
-
return exports.
|
|
379
|
+
/** Um cenario por id, ou null quando a fixture aponta para um cenario que nao existe. */
|
|
380
|
+
function cenarioPorId(id) {
|
|
381
|
+
return exports.CENARIOS.find((c) => c.id === id) ?? null;
|
|
382
382
|
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GRUPOS_NAO_PUBLICADOS = exports.SECAO_NAO_PUBLICADO = exports.CHANGELOG = void 0;
|
|
4
|
+
exports.ehTeste = ehTeste;
|
|
5
|
+
exports.pedeLinha = pedeLinha;
|
|
6
|
+
exports.linhasDaSecao = linhasDaSecao;
|
|
7
|
+
exports.versoes = versoes;
|
|
8
|
+
exports.changelogAplicavel = changelogAplicavel;
|
|
9
|
+
exports.checarChangelog = checarChangelog;
|
|
10
|
+
exports.basePadrao = basePadrao;
|
|
11
|
+
/**
|
|
12
|
+
* RM-037: regra unica do CHANGELOG para o script do CI, `docs verificar --pr` e `ork verify`.
|
|
13
|
+
* O diff decide quais linhas a thread acrescentou; o texto do HEAD decide em que secao e grupo
|
|
14
|
+
* cada linha caiu; uma tag local `v<versao>` prova que a secao ja foi publicada.
|
|
15
|
+
*/
|
|
16
|
+
const node_child_process_1 = require("node:child_process");
|
|
17
|
+
exports.CHANGELOG = 'CHANGELOG.md';
|
|
18
|
+
exports.SECAO_NAO_PUBLICADO = 'Não publicado';
|
|
19
|
+
exports.GRUPOS_NAO_PUBLICADOS = ['Adicionado', 'Alterado', 'Corrigido', 'Segurança'];
|
|
20
|
+
const PASTAS_DE_PRODUTO = ['core/', 'adapters/', 'marketplaces/'];
|
|
21
|
+
const DE_CI = [/^\.github\//, /^\.ork-ci\//, /^core\/scripts\/test-ci\.js$/, /^core\/scripts\/checar-[a-z0-9-]+\.cjs$/];
|
|
22
|
+
const CORRECAO = 'acrescente em "## Não publicado" do CHANGELOG.md uma linha que diga o que muda para quem usa, ' +
|
|
23
|
+
'sob "### Adicionado", "### Alterado", "### Corrigido" ou "### Segurança" ' +
|
|
24
|
+
'(PR só de testes ou só de CI fica de fora: docs/guias/contribuir/pull-request.md)';
|
|
25
|
+
function git(raiz, args) {
|
|
26
|
+
const r = (0, node_child_process_1.spawnSync)('git', args, { cwd: raiz, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
27
|
+
return { ok: r.status === 0, saida: r.stdout ?? '' };
|
|
28
|
+
}
|
|
29
|
+
/** Arquivo de teste: pasta `test`, `tests` ou `__tests__`, ou nome `*.test.*`/`*.spec.*`. */
|
|
30
|
+
function ehTeste(arquivo) {
|
|
31
|
+
const partes = arquivo.split('/');
|
|
32
|
+
return partes.slice(0, -1).some((p) => p === 'test' || p === 'tests' || p === '__tests__')
|
|
33
|
+
|| /\.(test|spec)\.[A-Za-z0-9]+$/.test(partes[partes.length - 1]);
|
|
34
|
+
}
|
|
35
|
+
/** Arquivo de produto que exige uma entrada no CHANGELOG. */
|
|
36
|
+
function pedeLinha(arquivo) {
|
|
37
|
+
return PASTAS_DE_PRODUTO.some((p) => arquivo.startsWith(p)) && !ehTeste(arquivo) && !DE_CI.some((re) => re.test(arquivo));
|
|
38
|
+
}
|
|
39
|
+
/** Linhas de texto de `## Não publicado`, mantida para consumidores e testes legados. */
|
|
40
|
+
function linhasDaSecao(texto) {
|
|
41
|
+
const linhas = texto.replace(/\r\n/g, '\n').split('\n');
|
|
42
|
+
const inicio = linhas.findIndex((linha) => linha.trim() === `## ${exports.SECAO_NAO_PUBLICADO}`);
|
|
43
|
+
if (inicio < 0)
|
|
44
|
+
return null;
|
|
45
|
+
const corpo = [];
|
|
46
|
+
for (const linha of linhas.slice(inicio + 1)) {
|
|
47
|
+
if (/^## /.test(linha))
|
|
48
|
+
break;
|
|
49
|
+
if (linha.trim() && !/^#/.test(linha.trim()))
|
|
50
|
+
corpo.push(linha.trimEnd());
|
|
51
|
+
}
|
|
52
|
+
return corpo;
|
|
53
|
+
}
|
|
54
|
+
/** Versoes nos dois formatos historicos do arquivo. */
|
|
55
|
+
function versoes(texto) {
|
|
56
|
+
return new Set([...texto.replace(/\r\n/g, '\n').matchAll(/^## \[?(\d+\.\d+\.\d+[0-9A-Za-z.+-]*)\]?(?=\s|$)/gm)]
|
|
57
|
+
.map((m) => m[1]));
|
|
58
|
+
}
|
|
59
|
+
function changelogEm(raiz, ref) {
|
|
60
|
+
const r = git(raiz, ['show', `${ref}:${exports.CHANGELOG}`]);
|
|
61
|
+
return r.ok ? r.saida : '';
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A regra protege um histórico publicado. Repositórios ainda sem versão `v*` ou sem
|
|
65
|
+
* CHANGELOG no HEAD não têm esse contrato para comparar e ficam fora do verificador.
|
|
66
|
+
*/
|
|
67
|
+
function changelogAplicavel(raiz, head = 'HEAD') {
|
|
68
|
+
const arquivo = git(raiz, ['cat-file', '-e', `${head}:${exports.CHANGELOG}`]);
|
|
69
|
+
if (!arquivo.ok)
|
|
70
|
+
return false;
|
|
71
|
+
const tags = git(raiz, ['tag', '--merged', head, '--list', 'v*']);
|
|
72
|
+
return tags.ok && tags.saida.split('\n').some((tag) => tag.trim().length > 1);
|
|
73
|
+
}
|
|
74
|
+
/** Linhas acrescentadas no arquivo, com o numero que ocupam no HEAD. */
|
|
75
|
+
function linhasAcrescentadas(raiz, base, head) {
|
|
76
|
+
const d = git(raiz, ['diff', '--no-color', '--unified=0', '--no-renames', base, head, '--', exports.CHANGELOG]);
|
|
77
|
+
if (!d.ok)
|
|
78
|
+
return [];
|
|
79
|
+
const novas = [];
|
|
80
|
+
let numero = 0, emTrecho = false;
|
|
81
|
+
for (const linha of d.saida.replace(/\r\n/g, '\n').split('\n')) {
|
|
82
|
+
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(linha);
|
|
83
|
+
if (hunk) {
|
|
84
|
+
numero = Number(hunk[1]);
|
|
85
|
+
emTrecho = true;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!emTrecho || linha.startsWith('\\ No newline'))
|
|
89
|
+
continue;
|
|
90
|
+
if (linha.startsWith('+')) {
|
|
91
|
+
novas.push({ numero, texto: linha.slice(1) });
|
|
92
|
+
numero++;
|
|
93
|
+
}
|
|
94
|
+
else if (!linha.startsWith('-'))
|
|
95
|
+
numero++;
|
|
96
|
+
}
|
|
97
|
+
return novas;
|
|
98
|
+
}
|
|
99
|
+
function contextos(texto) {
|
|
100
|
+
const porLinha = [{ secao: null, grupo: null, versao: null }];
|
|
101
|
+
let secao = null, grupo = null, versao = null;
|
|
102
|
+
for (const linha of texto.replace(/\r\n/g, '\n').split('\n')) {
|
|
103
|
+
const h2 = /^## (.+?)\s*$/.exec(linha);
|
|
104
|
+
if (h2) {
|
|
105
|
+
secao = h2[1];
|
|
106
|
+
grupo = null;
|
|
107
|
+
versao = /^\[?(\d+\.\d+\.\d+[0-9A-Za-z.+-]*)\]?(?:\s|$)/.exec(secao)?.[1] ?? null;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
const h3 = /^### (.+?)\s*$/.exec(linha);
|
|
111
|
+
if (h3)
|
|
112
|
+
grupo = h3[1];
|
|
113
|
+
}
|
|
114
|
+
porLinha.push({ secao, grupo, versao });
|
|
115
|
+
}
|
|
116
|
+
return porLinha;
|
|
117
|
+
}
|
|
118
|
+
function tagPublicada(raiz, versao) {
|
|
119
|
+
return git(raiz, ['rev-parse', '--verify', '--quiet', `refs/tags/v${versao}^{commit}`]).ok;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Confere o diff da thread ate `head`.
|
|
123
|
+
*
|
|
124
|
+
* A referencia local da branch alvo pode estar atrasada em relacao ao commit que abriu a
|
|
125
|
+
* worktree. Quando a base carimbada tambem e ancestral do HEAD e vem depois da merge-base,
|
|
126
|
+
* ela delimita o que a thread de fato acrescentou. Se a branch alvo avancou para alem dela,
|
|
127
|
+
* a merge-base mais nova continua vencendo.
|
|
128
|
+
*/
|
|
129
|
+
function checarChangelog(raiz, opcoes = {}) {
|
|
130
|
+
const head = opcoes.head ?? 'HEAD', base = opcoes.base;
|
|
131
|
+
const indisponivel = (detalhe) => ({ ok: false, motivo: 'changelog.base-indisponivel', detalhe,
|
|
132
|
+
correcao: 'rode com o histórico completo do git (fetch-depth: 0 no CI) ou passe --base <ref>', arquivos: [], linhas: [] });
|
|
133
|
+
if (!git(raiz, ['rev-parse', '--verify', '--quiet', `${head}^{commit}`]).ok)
|
|
134
|
+
return indisponivel(`o head ${head} não existe neste repositório`);
|
|
135
|
+
if (!changelogAplicavel(raiz, head)) {
|
|
136
|
+
const temArquivo = git(raiz, ['cat-file', '-e', `${head}:${exports.CHANGELOG}`]).ok;
|
|
137
|
+
return { ok: true, motivo: null, arquivos: [], linhas: [], detalhe: temArquivo
|
|
138
|
+
? 'regra do CHANGELOG não aplicável: o repositório ainda não tem versão publicada em tag v*'
|
|
139
|
+
: `regra do CHANGELOG não aplicável: ${exports.CHANGELOG} não existe em ${head}` };
|
|
140
|
+
}
|
|
141
|
+
if (!base)
|
|
142
|
+
return indisponivel('nenhuma base para comparar');
|
|
143
|
+
if (!git(raiz, ['rev-parse', '--verify', '--quiet', `${base}^{commit}`]).ok)
|
|
144
|
+
return indisponivel(`a base ${base} não existe neste repositório`);
|
|
145
|
+
const mb = git(raiz, ['merge-base', base, head]);
|
|
146
|
+
let ancestral = mb.saida.trim();
|
|
147
|
+
if (!mb.ok || !ancestral)
|
|
148
|
+
return indisponivel(`${base} e ${head} não têm ancestral comum`);
|
|
149
|
+
const carimbada = opcoes.baseCarimbada?.trim();
|
|
150
|
+
if (carimbada
|
|
151
|
+
&& git(raiz, ['rev-parse', '--verify', '--quiet', `${carimbada}^{commit}`]).ok
|
|
152
|
+
&& git(raiz, ['merge-base', '--is-ancestor', carimbada, head]).ok
|
|
153
|
+
&& git(raiz, ['merge-base', '--is-ancestor', ancestral, carimbada]).ok) {
|
|
154
|
+
ancestral = carimbada;
|
|
155
|
+
}
|
|
156
|
+
const mudados = git(raiz, ['diff', '--name-only', '--no-renames', '-z', ancestral, head]);
|
|
157
|
+
const arquivos = (mudados.ok ? mudados.saida.split('\0') : []).filter(Boolean);
|
|
158
|
+
const pedem = arquivos.filter(pedeLinha);
|
|
159
|
+
const antes = changelogEm(raiz, ancestral), depois = changelogEm(raiz, head);
|
|
160
|
+
const novas = linhasAcrescentadas(raiz, ancestral, head);
|
|
161
|
+
const porLinha = contextos(depois);
|
|
162
|
+
// D4: uma entrada correta em Nao publicado nunca esconde uma edicao retrospectiva.
|
|
163
|
+
for (const linha of novas) {
|
|
164
|
+
if (!linha.texto.trim())
|
|
165
|
+
continue;
|
|
166
|
+
const contexto = porLinha[linha.numero] ?? { secao: null, grupo: null, versao: null };
|
|
167
|
+
if (contexto.versao && tagPublicada(raiz, contexto.versao)) {
|
|
168
|
+
return { ok: false, motivo: 'changelog.secao-publicada', arquivos: pedem, linhas: [linha.texto], secao: contexto.secao ?? undefined,
|
|
169
|
+
detalhe: `a linha nova ${linha.numero} caiu em "## ${contexto.secao}", versão já publicada pela tag v${contexto.versao}`,
|
|
170
|
+
correcao: `mova a linha para "## ${exports.SECAO_NAO_PUBLICADO}", sob um dos grupos permitidos` };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const entradasNaoPublicadas = novas.filter((linha) => {
|
|
174
|
+
const t = linha.texto.trim(), contexto = porLinha[linha.numero];
|
|
175
|
+
return !!t && !/^#{2,3} /.test(t) && contexto?.secao === exports.SECAO_NAO_PUBLICADO;
|
|
176
|
+
});
|
|
177
|
+
for (const linha of entradasNaoPublicadas) {
|
|
178
|
+
const grupo = porLinha[linha.numero]?.grupo ?? null;
|
|
179
|
+
if (!grupo)
|
|
180
|
+
return { ok: false, motivo: 'changelog.grupo-ausente', arquivos: pedem, linhas: [linha.texto], grupo,
|
|
181
|
+
detalhe: `a linha nova ${linha.numero} de "## ${exports.SECAO_NAO_PUBLICADO}" não está sob um título ###`,
|
|
182
|
+
correcao: `ponha a linha sob ${exports.GRUPOS_NAO_PUBLICADOS.map((g) => `"### ${g}"`).join(', ')}` };
|
|
183
|
+
if (!exports.GRUPOS_NAO_PUBLICADOS.includes(grupo)) {
|
|
184
|
+
return { ok: false, motivo: 'changelog.grupo-invalido', arquivos: pedem, linhas: [linha.texto], grupo,
|
|
185
|
+
detalhe: `a linha nova ${linha.numero} está sob "### ${grupo}", grupo que não vale em "## ${exports.SECAO_NAO_PUBLICADO}"`,
|
|
186
|
+
correcao: `use ${exports.GRUPOS_NAO_PUBLICADOS.map((g) => `"### ${g}"`).join(', ')}` };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (pedem.length === 0)
|
|
190
|
+
return { ok: true, motivo: null, arquivos: [], linhas: [],
|
|
191
|
+
detalhe: arquivos.length === 0 ? `nada a conferir: ${head} não muda nada desde ${base}`
|
|
192
|
+
: 'nada pede linha: o PR não muda core/, adapters/ nem marketplaces/ fora de teste e de CI' };
|
|
193
|
+
const versaoNova = [...versoes(depois)].filter((v) => !versoes(antes).has(v));
|
|
194
|
+
if (versaoNova.length > 0)
|
|
195
|
+
return { ok: true, motivo: null, arquivos: pedem, linhas: [],
|
|
196
|
+
detalhe: `versão nova no ${exports.CHANGELOG}: ${versaoNova.join(', ')} (o PR de versão leva as linhas para ela)` };
|
|
197
|
+
if (linhasDaSecao(depois) === null)
|
|
198
|
+
return { ok: false, motivo: 'changelog.secao-ausente', arquivos: pedem, linhas: [],
|
|
199
|
+
detalhe: `o ${exports.CHANGELOG} de ${head} não tem a seção "## ${exports.SECAO_NAO_PUBLICADO}"`,
|
|
200
|
+
correcao: `abra a seção "## ${exports.SECAO_NAO_PUBLICADO}" no alto do ${exports.CHANGELOG} e acrescente a linha da mudança` };
|
|
201
|
+
if (entradasNaoPublicadas.length > 0)
|
|
202
|
+
return { ok: true, motivo: null, arquivos: pedem,
|
|
203
|
+
linhas: entradasNaoPublicadas.map((linha) => linha.texto),
|
|
204
|
+
detalhe: `${entradasNaoPublicadas.length} linha(s) nova(s) em "## ${exports.SECAO_NAO_PUBLICADO}" sob grupo permitido` };
|
|
205
|
+
return { ok: false, motivo: 'changelog.linha-ausente', arquivos: pedem, linhas: [],
|
|
206
|
+
detalhe: `o PR muda ${pedem.length} arquivo(s) de core/, adapters/ ou marketplaces/ (${pedem.slice(0, 3).join(', ')}${pedem.length > 3 ? ', ...' : ''}) e não acrescenta linha válida em "## ${exports.SECAO_NAO_PUBLICADO}" do ${exports.CHANGELOG}`,
|
|
207
|
+
correcao: CORRECAO };
|
|
208
|
+
}
|
|
209
|
+
/** Base padrao do CI: alvo do PR, main remota, main local. */
|
|
210
|
+
function basePadrao(raiz, env = process.env) {
|
|
211
|
+
if (env.GITHUB_BASE_REF)
|
|
212
|
+
return `origin/${env.GITHUB_BASE_REF}`;
|
|
213
|
+
for (const ref of ['origin/main', 'main'])
|
|
214
|
+
if (git(raiz, ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]).ok)
|
|
215
|
+
return ref;
|
|
216
|
+
return null;
|
|
217
|
+
}
|