@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.
- package/adapters/README.md +11 -0
- package/adapters/claude-code/.claude-plugin/plugin.json +3 -1
- package/adapters/claude-code/agents/ork-check.md +1 -1
- package/adapters/claude-code/agents/ork-go.md +2 -2
- 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/master.md +11 -8
- package/adapters/claude-code/commands/ork.md +8 -0
- package/adapters/claude-code/commands/ship.md +2 -2
- package/adapters/codex/skills/ork/SKILL.md +12 -1
- package/adapters/hermes/README.md +3 -0
- package/adapters/hermes/bin/ork-abrir-thread.sh +17 -2
- package/adapters/hermes/bin/ork-brain.sh +3 -0
- package/adapters/hermes/bin/ork-maestro.sh +4 -1
- package/adapters/hermes/bin/ork-network-roadmap.sh +11 -0
- package/adapters/hermes/bin/ork-roadmap-status.sh +9 -0
- package/adapters/hermes/hermes.plugin.json +2 -0
- package/adapters/hermes/hitl-ingress/__init__.py +36 -3
- package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +19 -20
- package/adapters/openclaw/README.md +34 -2
- package/adapters/openclaw/dist/hitl-ingress.js +37 -3
- package/adapters/openclaw/dist/index.js +92 -20
- package/adapters/openclaw/openclaw.plugin.json +10 -1
- package/adapters/openclaw/package.json +2 -2
- package/adapters/openclaw/src/hitl-ingress.ts +32 -3
- package/adapters/openclaw/src/index.ts +108 -20
- package/assets/docs/padroes/documentacao-de-produto.md +2 -2
- package/assets/docs/padroes/roadmap-de-produto.md +1 -1
- package/assets/docs/produto/_modelo-feature.md +2 -0
- package/assets/orkmind_bridge.py +214 -1
- package/dist/adapters/claude-bg.js +6 -3
- package/dist/board.js +74 -0
- package/dist/branch-de-estado.js +5 -4
- package/dist/busca-semantica.js +161 -0
- package/dist/ci.js +141 -10
- package/dist/company-brain-cli.js +27 -2
- package/dist/company-brain-context.js +132 -0
- package/dist/company-brain-dossie.js +376 -0
- package/dist/company-brain-mcp.js +4 -1
- package/dist/conducao.js +71 -18
- package/dist/decisao-autonoma.js +42 -0
- package/dist/docs.js +64 -22
- package/dist/doctor.js +33 -0
- package/dist/entrega-pr.js +109 -5
- package/dist/experiencia-instalacao.js +256 -0
- package/dist/experiencia.js +72 -0
- package/dist/fabrica-estado.js +43 -10
- package/dist/fechamento.js +105 -0
- package/dist/forja.js +435 -0
- package/dist/gates.js +9 -1
- package/dist/hitl-classificacao.js +11 -1
- package/dist/hitl-contract.js +50 -7
- package/dist/hitl-curto.js +158 -0
- package/dist/hitl-gates.js +67 -1
- package/dist/hitl-lote.js +30 -15
- package/dist/hitl-presentation.js +10 -7
- package/dist/hitl-resumo.js +33 -12
- package/dist/hitl-texto-livre.js +150 -0
- package/dist/hosts.js +86 -1
- package/dist/index.js +602 -38
- package/dist/indice-vetorial.js +317 -0
- package/dist/init.js +13 -0
- package/dist/integracoes-locais.js +1 -1
- package/dist/intelligence-benchmark-contract.js +570 -0
- package/dist/intelligence-graph-cli.js +430 -0
- package/dist/intelligence-graph-contract.js +577 -0
- package/dist/intelligence-graph-extract-md.js +469 -0
- package/dist/intelligence-graph-extract-ts.js +810 -0
- package/dist/intelligence-graph-extract.js +369 -0
- package/dist/intelligence-graph-index.js +512 -0
- package/dist/intelligence-graph-parsers.js +327 -0
- package/dist/intelligence-graph-query.js +344 -0
- package/dist/intelligence-graph-repo.js +187 -0
- package/dist/leases.js +84 -0
- package/dist/ledger.js +14 -0
- package/dist/maestro-cli.js +10 -1
- package/dist/maestro-contract.js +5 -1
- package/dist/maestro-discovery.js +16 -2
- package/dist/maestro-snapshot.js +6 -2
- package/dist/manifest.js +142 -5
- package/dist/master-batch.js +3 -2
- package/dist/master-digest.js +2 -2
- package/dist/master-nota.js +203 -0
- package/dist/master.js +4 -0
- package/dist/mcp-experiencia.js +164 -0
- package/dist/mcp-git.js +4 -0
- package/dist/mcp-install.js +3 -1
- package/dist/mcp-maestro.js +3 -2
- package/dist/mcp-server.js +84 -5
- package/dist/memoria.js +147 -1
- package/dist/network-roadmap.js +907 -0
- package/dist/ocupacao.js +38 -2
- package/dist/onboarding.js +113 -7
- package/dist/orkmind.js +165 -9
- package/dist/phase.js +166 -13
- package/dist/policies.js +2 -0
- package/dist/projeto-alvo.js +551 -0
- package/dist/prompts.js +1 -1
- package/dist/pulse-consentimento.js +4 -2
- package/dist/pulse-delivery.js +5 -0
- package/dist/pulse-resposta.js +674 -49
- package/dist/pulse.js +21 -0
- package/dist/retry.js +51 -8
- package/dist/roadmap-reservas.js +262 -20
- package/dist/roadmap-status.js +213 -0
- package/dist/thread-close.js +3 -0
- package/dist/verify.js +4 -3
- package/eval/casos/orchestration-experience-pt-br.json +48 -0
- package/eval/casos/orchestration-experience.json +48 -0
- package/package.json +1 -1
- package/schemas/code-artifact-graph.v1.schema.json +581 -0
- package/schemas/graph-benchmark.v1.schema.json +2240 -0
- package/schemas/maestro-snapshot.schema.json +23 -0
- package/skills/README.md +7 -5
- package/skills/core/onboarding/SKILL.md +7 -0
- package/skills/core/orchestration-experience/SKILL.md +113 -0
- package/skills/core/orchestration-experience-pt-br/SKILL.md +112 -0
- package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
- 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(
|
|
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
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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.
|
package/dist/entrega-pr.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|