@pathbit/agentdevkit 0.0.21 → 0.0.23
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/.pbai4devs/COMPATIBILIDADE.md +11 -1
- package/.pbai4devs/rules/ambiente-local/RULE.md +5 -0
- package/.pbai4devs/rules/git/RULE.md +5 -1
- package/.pbai4devs/scripts/README.md +1 -0
- package/.pbai4devs/scripts/diagnosticar-ambiente/tests/TEST.mjs +60 -1
- package/.pbai4devs/scripts/inspecionar-projeto/README.md +6 -0
- package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +218 -18
- package/.pbai4devs/scripts/lib/adapters.mjs +5 -2
- package/.pbai4devs/scripts/lib/runtime.mjs +38 -2
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +5 -1
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +136 -9
- package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +30 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +22 -0
- package/.pbai4devs/scripts/testar-maturidade/README.md +12 -10
- package/.pbai4devs/scripts/testar-maturidade/SCRIPT.mjs +86 -44
- package/.pbai4devs/scripts/testar-maturidade/tests/TEST.mjs +157 -33
- package/.pbai4devs/scripts/verificar-configuracao/README.md +22 -0
- package/.pbai4devs/scripts/verificar-configuracao/SCRIPT.mjs +63 -0
- package/.pbai4devs/scripts/verificar-configuracao/tests/TEST.mjs +145 -0
- package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +12 -1
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +22 -0
- package/.pbai4devs/skills/desenvolvimento/implementar-backend/SKILL.md +15 -3
- package/.pbai4devs/skills/desenvolvimento/implementar-frontend/SKILL.md +15 -3
- package/.pbai4devs/skills/git/git-diff/SKILL.md +3 -1
- package/.pbai4devs/skills/git/git-status/SKILL.md +2 -1
- package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +13 -8
- package/.pbai4devs/skills/projeto/proximos-passos/SKILL.md +4 -2
- package/.pbai4devs/skills/specs/implementar-spec/SKILL.md +14 -2
- package/.pbai4devs/templates/clients/ADAPTERS.json +3 -2
- package/CHANGELOG.md +20 -0
- package/docs/guides/cli.md +9 -0
- package/docs/guides/troubleshooting.md +4 -0
- package/docs/specs/README.md +5 -0
- package/docs/specs/discovery-preservation/SPEC.md +26 -0
- package/docs/specs/preimplementation-order/SPEC.md +23 -0
- package/docs/specs/target-paths/SPEC.md +19 -0
- package/package.json +1 -1
- package/src/install.mjs +155 -17
- package/src/next.mjs +26 -9
|
@@ -45,7 +45,8 @@ representa categorias opcionais; `nome` é o identificador único do recurso.
|
|
|
45
45
|
Os caminhos são relativos à pasta do cliente, exceto onde indicado.
|
|
46
46
|
|
|
47
47
|
- Codex: `.agents/skills/grupo/nome/SKILL.md` e referências agrupadas em
|
|
48
|
-
`.codex`.
|
|
48
|
+
`.codex`. Se a rota principal de uma skill estiver obstruída, ela usa
|
|
49
|
+
`.codex/skills` como alternativa. `AGENTS.md` apresenta o hub.
|
|
49
50
|
- Claude: `skills/nome/SKILL.md`, `commands/grupo/nome.md`,
|
|
50
51
|
`rules/grupo/nome/RULE.md` e `agents/nome.md`.
|
|
51
52
|
- Cursor: `skills/grupo/nome/SKILL.md`, `rules/grupo/nome.mdc`,
|
|
@@ -91,12 +92,21 @@ Prompts `.prompt.md` pertencem ao harness Local do VS Code, anunciado para
|
|
|
91
92
|
remoção em fevereiro de 2027. No Copilot, use skills para rotinas invocáveis.
|
|
92
93
|
`GEMINI.md` importa `AGENTS.md`; `CLAUDE.md` aponta para esse mesmo guia.
|
|
93
94
|
|
|
95
|
+
Com Claude e Copilot selecionados, o Copilot CLI pode receber referências
|
|
96
|
+
equivalentes de `.claude/rules` e `.github/instructions`. As duas rotas são
|
|
97
|
+
mantidas para seus respectivos clientes; o suporte a instruções varia entre
|
|
98
|
+
[ambientes do Copilot](https://docs.github.com/en/copilot/reference/custom-instructions-support).
|
|
99
|
+
|
|
94
100
|
O instalador gera a seleção e a política em `espelhos.conf`; `sincronizar`
|
|
95
101
|
reconstrói os apontamentos somente com Node e os arquivos do hub instalado.
|
|
96
102
|
`--check` não altera arquivos. `--force` faz backup ignorado ao reparar
|
|
97
103
|
conflitos gerenciados. Recursos e links humanos dos clientes são preservados;
|
|
98
104
|
uma rota preservada não recebe apontamentos novos. No Zod, `.codex/skills`
|
|
99
105
|
continua apontando para suas skills e o Codex recebe o kit em `.agents/skills`.
|
|
106
|
+
Quando a rota principal volta a ficar disponível, apontamentos alternativos
|
|
107
|
+
obsoletos só são removidos com propriedade comprovada. Conteúdo humano e o
|
|
108
|
+
índice Git são preservados; se ambas as rotas estiverem obstruídas, a operação
|
|
109
|
+
informa o conflito sem substituí-las.
|
|
100
110
|
|
|
101
111
|
As fontes confirmam formatos, não execução de cada cliente. Valide descoberta,
|
|
102
112
|
invocação e resultado no cliente escolhido antes de considerar uma integração
|
|
@@ -27,6 +27,11 @@ O Makefile deve oferecer `help`, `doctor`, `setup`, `up`, `down`, `logs`,
|
|
|
27
27
|
`test`, `lint` e `build`, com comandos reais da stack. Etapas ainda indefinidas
|
|
28
28
|
devem falhar com mensagem útil; não simule sucesso. Documente os comandos,
|
|
29
29
|
dependências e URL/porta no README; valide Compose e saúde antes de concluir.
|
|
30
|
+
Antes de existir aplicação, valide o runtime, o Compose e os serviços de
|
|
31
|
+
infraestrutura necessários. Build, saúde e fluxo da aplicação inexistente
|
|
32
|
+
permanecem pendentes; não crie código de negócio para simular essas provas. Se
|
|
33
|
+
uma dependência obrigatória estiver indisponível, mantenha a configuração
|
|
34
|
+
pendente e registre o impedimento. Após a implementação, prove a aplicação.
|
|
30
35
|
|
|
31
36
|
Use o [modelo de ambiente](../../templates/ambiente-local/TEMPLATE.md) para
|
|
32
37
|
perguntas e arquivos-base. Versões da aplicação seguem as decisões do projeto;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
trigger: model_decision
|
|
3
|
-
description: Aplique ao preparar, integrar ou publicar alterações Git.
|
|
3
|
+
description: Aplique ao consultar, preparar, integrar ou publicar alterações Git.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Regras Git
|
|
7
7
|
|
|
8
|
+
Em consultas sem alterações, defina `GIT_OPTIONAL_LOCKS=0` no ambiente do
|
|
9
|
+
subprocesso para evitar atualizações opcionais dos bytes do índice. Isso não
|
|
10
|
+
substitui as travas necessárias para operações que alteram o Git.
|
|
11
|
+
|
|
8
12
|
Use Conventional Commits (`tipo(escopo): descrição`, escopo opcional), inclusive
|
|
9
13
|
em checkpoints e merges; destaque mudanças incompatíveis com `!` ou
|
|
10
14
|
`BREAKING CHANGE:`. Autor e committer devem ser o usuário, nunca um agente;
|
|
@@ -26,6 +26,7 @@ node .pbai4devs/scripts/executar-script/SCRIPT.mjs diagnosticar-ambiente
|
|
|
26
26
|
| --- | --- |
|
|
27
27
|
| `diagnosticar-ambiente` | Runtimes, pacotes e navegadores disponíveis |
|
|
28
28
|
| `inspecionar-projeto` | Detecta manifests, comandos e estrutura |
|
|
29
|
+
| `verificar-configuracao` | Confere o estado antes de implementar código |
|
|
29
30
|
| `preparar-ferramentas` | Instala/atualiza ferramentas locais |
|
|
30
31
|
| `gerenciar-skills-externas` | Instala/check/read de skills curadas locais |
|
|
31
32
|
| `auditar-seguranca` | Scanners offline, bases locais e relatórios redigidos |
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { chmodSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
5
7
|
import { diagnose } from '../SCRIPT.mjs';
|
|
6
8
|
import { findExecutable, projectRoot, requireNode } from '../../lib/runtime.mjs';
|
|
7
9
|
|
|
@@ -28,3 +30,60 @@ test('requisito comum recusa Node antigo e procura executaveis sem shell', t =>
|
|
|
28
30
|
assert.equal(findExecutable('fixture', { PATH: dirname(file) }), file);
|
|
29
31
|
assert.equal(findExecutable('missing', { PATH: dirname(file) }), null);
|
|
30
32
|
});
|
|
33
|
+
|
|
34
|
+
test('isMain reconhece caminho principal por alias sem executar imports como entrada', t => {
|
|
35
|
+
const output = join(projectRoot, '.agent-tests'); mkdirSync(output, { recursive: true });
|
|
36
|
+
const root = mkdtempSync(join(output, 'runtime alias espaço-'));
|
|
37
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
38
|
+
const real = join(root, 'real'), alias = join(root, 'alias'); mkdirSync(real);
|
|
39
|
+
try { symlinkSync(real, alias, process.platform === 'win32' ? 'junction' : 'dir'); }
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (!['EPERM', 'EACCES'].includes(error.code)) throw error;
|
|
42
|
+
t.skip('Sistema sem permissão para criar alias de diretório.'); return;
|
|
43
|
+
}
|
|
44
|
+
const script = join(real, 'entry.mjs');
|
|
45
|
+
const runtime = pathToFileURL(join(projectRoot, '.pbai4devs/scripts/lib/runtime.mjs')).href;
|
|
46
|
+
writeFileSync(script, `import { isMain } from ${JSON.stringify(runtime)};\nconst main = isMain(import.meta.url);\nconsole.log(main ? 'entrada' : 'importado');\nif (main && process.env.ADK_RUNTIME_IMPORT) { const target = process.env.ADK_RUNTIME_IMPORT; delete process.env.ADK_RUNTIME_IMPORT; await import(target); }\n`);
|
|
47
|
+
const run = (args, extra = {}) => {
|
|
48
|
+
const env = { ...process.env, ...extra }; if (!Object.hasOwn(extra, 'NODE_OPTIONS')) delete env.NODE_OPTIONS;
|
|
49
|
+
if (!Object.hasOwn(extra, 'NODE_PRESERVE_SYMLINKS_MAIN')) delete env.NODE_PRESERVE_SYMLINKS_MAIN;
|
|
50
|
+
if (!Object.hasOwn(extra, 'NODE_PRESERVE_SYMLINKS')) delete env.NODE_PRESERVE_SYMLINKS;
|
|
51
|
+
const result = spawnSync(process.execPath, args, { encoding: 'utf8', env, timeout: 30000, windowsHide: true });
|
|
52
|
+
assert.equal(result.status, 0, result.stderr); return result.stdout.trim().split(/\r?\n/);
|
|
53
|
+
};
|
|
54
|
+
assert.deepEqual(run([script]), ['entrada']);
|
|
55
|
+
assert.deepEqual(run([join(alias, 'entry.mjs')]), ['entrada']);
|
|
56
|
+
const fileAlias = join(root, 'entry-link.mjs');
|
|
57
|
+
try {
|
|
58
|
+
symlinkSync(script, fileAlias, 'file');
|
|
59
|
+
assert.deepEqual(run([fileAlias]), ['entrada']);
|
|
60
|
+
} catch (error) { if (!['EPERM', 'EACCES'].includes(error.code)) throw error; }
|
|
61
|
+
const importer = join(root, 'importer.mjs');
|
|
62
|
+
writeFileSync(importer, `await import(${JSON.stringify(pathToFileURL(join(alias, 'entry.mjs')).href)});\n`);
|
|
63
|
+
assert.deepEqual(run([importer]), ['importado']);
|
|
64
|
+
writeFileSync(importer, `await import(${JSON.stringify(pathToFileURL(script).href + '?importado')});\n`);
|
|
65
|
+
assert.deepEqual(run([importer]), ['importado']);
|
|
66
|
+
assert.deepEqual(run(['--input-type=module', '-e', `await import(${JSON.stringify(pathToFileURL(script).href)});`, script]), ['importado']);
|
|
67
|
+
assert.deepEqual(run(['--input-type=module', `--eval=await import(${JSON.stringify(pathToFileURL(script).href)});`, script]), ['importado']);
|
|
68
|
+
const physical = pathToFileURL(script).href, logical = pathToFileURL(join(alias, 'entry.mjs')).href;
|
|
69
|
+
assert.deepEqual(run(['--preserve-symlinks-main', join(alias, 'entry.mjs')], { ADK_RUNTIME_IMPORT: physical }), ['entrada', 'importado']);
|
|
70
|
+
assert.deepEqual(run(['--preserve-symlinks-main=false', join(alias, 'entry.mjs')], { ADK_RUNTIME_IMPORT: physical }), ['entrada', 'importado']);
|
|
71
|
+
assert.deepEqual(run(['--preserve-symlinks-main=ignored', join(alias, 'entry.mjs')], { ADK_RUNTIME_IMPORT: physical }), ['entrada', 'importado']);
|
|
72
|
+
assert.deepEqual(run(['--preserve-symlinks', script], { ADK_RUNTIME_IMPORT: logical }), ['entrada', 'importado']);
|
|
73
|
+
assert.deepEqual(run(['--preserve-symlinks', join(alias, 'entry.mjs')], { ADK_RUNTIME_IMPORT: logical }), ['entrada', 'importado']);
|
|
74
|
+
assert.deepEqual(run([join(alias, 'entry.mjs')], { NODE_OPTIONS: '--preserve-symlinks', ADK_RUNTIME_IMPORT: logical }), ['entrada', 'importado']);
|
|
75
|
+
assert.deepEqual(run([join(alias, 'entry.mjs')], { NODE_OPTIONS: '--preserve-symlinks-main', ADK_RUNTIME_IMPORT: physical }), ['entrada', 'importado']);
|
|
76
|
+
assert.deepEqual(run(['--no-preserve-symlinks-main', join(alias, 'entry.mjs')], { NODE_OPTIONS: '--preserve-symlinks-main' }), ['entrada']);
|
|
77
|
+
assert.deepEqual(run([join(alias, 'entry.mjs')], { NODE_PRESERVE_SYMLINKS_MAIN: '1', ADK_RUNTIME_IMPORT: physical }), ['entrada', 'importado']);
|
|
78
|
+
assert.deepEqual(run(['--no-preserve-symlinks-main', join(alias, 'entry.mjs')], { NODE_PRESERVE_SYMLINKS_MAIN: '1' }), ['entrada']);
|
|
79
|
+
const positiveText = join(root, 'preload --preserve-symlinks-main');
|
|
80
|
+
const negativeText = join(root, 'preload --no-preserve-symlinks-main');
|
|
81
|
+
for (const file of [positiveText, negativeText]) writeFileSync(file, '// preload sem efeitos\n');
|
|
82
|
+
assert.deepEqual(run(['--require', positiveText, join(alias, 'entry.mjs')]), ['entrada']);
|
|
83
|
+
assert.deepEqual(run([join(alias, 'entry.mjs')], { NODE_OPTIONS: `--require ${JSON.stringify(positiveText)}` }), ['entrada']);
|
|
84
|
+
assert.deepEqual(run(['--preserve-symlinks-main', '--require', negativeText, join(alias, 'entry.mjs')], { ADK_RUNTIME_IMPORT: physical }), ['entrada', 'importado']);
|
|
85
|
+
assert.deepEqual(run([join(alias, 'entry.mjs')], { NODE_OPTIONS: `--preserve-symlinks-main --require ${JSON.stringify(negativeText)}`, ADK_RUNTIME_IMPORT: physical }), ['entrada', 'importado']);
|
|
86
|
+
const toolAlias = join(root, 'tool');
|
|
87
|
+
symlinkSync(join(projectRoot, '.pbai4devs/scripts/gerenciar-skills-externas'), toolAlias, process.platform === 'win32' ? 'junction' : 'dir');
|
|
88
|
+
assert.match(run([join(toolAlias, 'SCRIPT.mjs'), '--help']).join('\n'), /Uso:/);
|
|
89
|
+
});
|
|
@@ -12,6 +12,10 @@ O JSON identifica tecnologia, comandos, estrutura e documentos existentes.
|
|
|
12
12
|
`documentation.files` mantém caminhos e caixa, papel provável, tamanho e hash
|
|
13
13
|
quando a leitura couber nos limites. Referências válidas do estado aparecem em
|
|
14
14
|
`configured`; nomes sugerem papéis, mas não comprovam conteúdo ou aprovação.
|
|
15
|
+
Na maturidade, o roadmap e as specs são consultados nos caminhos configurados;
|
|
16
|
+
status são declarações e um vínculo local não comprova aprovação ou revisão.
|
|
17
|
+
Uma lista de specs ambígua ou incompleta não autoriza escolher um incremento;
|
|
18
|
+
o estado do vínculo no roadmap também é apenas uma indicação para consulta.
|
|
15
19
|
|
|
16
20
|
Links simbólicos são listados como opacos, sem ler seus destinos. Conteúdo dos
|
|
17
21
|
documentos e valores de configuração não aparecem no relatório. `scan` e
|
|
@@ -20,6 +24,8 @@ condições. Uma pasta simbólica como `docs` também aparece, sem presumir que
|
|
|
20
24
|
esteja vazia. Links não classificados recebem `role: unclassified`; enquanto
|
|
21
25
|
existir uma fronteira não inspecionada, `scan.complete` será falso. Não use
|
|
22
26
|
inventário incompleto como prova de ausência de documentos.
|
|
27
|
+
O inspetor revalida a identidade dos caminhos durante a leitura; isso detecta
|
|
28
|
+
mudanças, mas não é uma garantia atômica contra toda troca simultânea.
|
|
23
29
|
|
|
24
30
|
`exclusions` descreve pastas de clientes, artefatos e caches fora do inventário.
|
|
25
31
|
Documentos humanos em `.github`, exemplos, wiki e RFCs são incluídos; os
|
|
@@ -41,12 +41,69 @@ const present = (root, name, directory) => {
|
|
|
41
41
|
const inside = candidate => { const relative = path.relative(repository, candidate); return relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative); };
|
|
42
42
|
const fixturePath = candidate => /^(?:\.agent-tests\/|tmp\/examples\/|examples\/generated\/)/.test(normalize(path.relative(repository, candidate)));
|
|
43
43
|
|
|
44
|
+
// Rechecking is a conservative race detection, not an atomic openat traversal.
|
|
45
|
+
function pathIdentity(root, file = '') {
|
|
46
|
+
try {
|
|
47
|
+
let current = root; const result = [], parts = file ? file.split('/') : [];
|
|
48
|
+
for (const [index, part] of ['', ...parts].entries()) {
|
|
49
|
+
if (part) current = path.join(current, part);
|
|
50
|
+
const item = fs.lstatSync(current);
|
|
51
|
+
if (item.isSymbolicLink() || (index < parts.length && !item.isDirectory())) return null;
|
|
52
|
+
result.push({ dev: item.dev, ino: item.ino, mode: item.mode, size: item.size, directory: item.isDirectory() });
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
} catch { return null; }
|
|
56
|
+
}
|
|
57
|
+
const sameIdentity = (left, right) => left && right && left.length === right.length
|
|
58
|
+
&& left.every((item, index) => item.dev === right[index].dev && item.ino === right[index].ino && item.mode === right[index].mode);
|
|
59
|
+
|
|
60
|
+
function boundedEntries(folder, limit, verify = () => true) {
|
|
61
|
+
const handle = fs.opendirSync(folder), entries = [];
|
|
62
|
+
try {
|
|
63
|
+
if (!verify()) return { entries: [], truncated: true, changed: true };
|
|
64
|
+
while (true) {
|
|
65
|
+
const entry = handle.readSync();
|
|
66
|
+
if (!entry) return verify() ? { entries, truncated: false } : { entries: [], truncated: true, changed: true };
|
|
67
|
+
if (entries.length >= limit) return verify() ? { entries, truncated: true } : { entries: [], truncated: true, changed: true };
|
|
68
|
+
entries.push(entry);
|
|
69
|
+
}
|
|
70
|
+
} finally { handle.closeSync(); }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function stableDirectory(root, directory, limit) {
|
|
74
|
+
const identity = pathIdentity(root, directory);
|
|
75
|
+
if (!identity?.at(-1).directory) return { entries: [], truncated: true, changed: true, verify: () => false };
|
|
76
|
+
const verify = () => sameIdentity(identity, pathIdentity(root, directory));
|
|
77
|
+
return { ...boundedEntries(path.join(root, directory), limit, verify), verify };
|
|
78
|
+
}
|
|
79
|
+
|
|
44
80
|
function metadata(root, file) {
|
|
45
|
-
|
|
46
|
-
const
|
|
81
|
+
const empty = { fields: {}, documents: {}, text: null };
|
|
82
|
+
const identity = () => pathIdentity(root, file);
|
|
83
|
+
let descriptor, text;
|
|
84
|
+
try {
|
|
85
|
+
const name = path.join(root, file), ancestors = identity();
|
|
86
|
+
if (!ancestors || !sameIdentity(ancestors, identity())) return empty;
|
|
87
|
+
const before = ancestors.at(-1);
|
|
88
|
+
if (before.size > 1024 * 1024) return empty;
|
|
89
|
+
descriptor = fs.openSync(name, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
90
|
+
const opened = fs.fstatSync(descriptor);
|
|
91
|
+
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino || opened.size !== before.size || !sameIdentity(ancestors, identity())) return empty;
|
|
92
|
+
const buffer = Buffer.alloc(opened.size + 1); let length = 0;
|
|
93
|
+
while (length < buffer.length) {
|
|
94
|
+
const count = fs.readSync(descriptor, buffer, length, buffer.length - length, length);
|
|
95
|
+
if (!count) break;
|
|
96
|
+
length += count;
|
|
97
|
+
}
|
|
98
|
+
const after = fs.fstatSync(descriptor);
|
|
99
|
+
if (length !== opened.size || after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs || !sameIdentity(ancestors, identity())) return empty;
|
|
100
|
+
text = buffer.subarray(0, length).toString('utf8').replaceAll('\r', '');
|
|
101
|
+
} catch { return empty; }
|
|
102
|
+
finally { if (descriptor !== undefined) fs.closeSync(descriptor); }
|
|
47
103
|
const header = text.match(/^---\n([\s\S]*?)\n---(?:\n|$)/)?.[1];
|
|
48
|
-
const result = { fields: {}, documents: {} }; if (!header) return result;
|
|
49
|
-
let documents = false;
|
|
104
|
+
const result = { fields: {}, documents: {}, text }; if (!header) return result;
|
|
105
|
+
let documents = false, invalidDocuments = false;
|
|
106
|
+
const seen = new Set(), duplicates = new Set(), seenDocuments = new Set(), duplicateDocuments = new Set();
|
|
50
107
|
const scalar = raw => {
|
|
51
108
|
const value = raw.trim();
|
|
52
109
|
if (!value || ['null', '~'].includes(value)) return null;
|
|
@@ -54,16 +111,131 @@ function metadata(root, file) {
|
|
|
54
111
|
return value.startsWith("'") && value.endsWith("'") ? value.slice(1, -1).replaceAll("''", "'") : value;
|
|
55
112
|
};
|
|
56
113
|
for (const line of header.split('\n')) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
114
|
+
if (!line.trim() || /^\s*#/.test(line)) continue;
|
|
115
|
+
const top = line.match(/^(?:([\p{L}_][\p{L}\p{N}_.-]*)|"([^"\n]+)"|'([^'\n]+)'):\s*(.*)$/u);
|
|
116
|
+
if (top) {
|
|
117
|
+
const key = top[1] || top[2] || top[3];
|
|
118
|
+
if (key.includes('\\')) return { ...empty, text };
|
|
119
|
+
if (seen.has(key)) duplicates.add(key);
|
|
120
|
+
seen.add(key); result.fields[key] = duplicates.has(key) ? null : scalar(top[4]);
|
|
121
|
+
if (key === 'documentos' && duplicates.has(key)) { invalidDocuments = true; result.documents = {}; }
|
|
122
|
+
documents = key === 'documentos' && !invalidDocuments && (!top[4].trim() || ['null', '~'].includes(top[4].trim()));
|
|
123
|
+
} else if (!/^\s/.test(line)) return { ...empty, text };
|
|
124
|
+
else if (documents) {
|
|
125
|
+
const item = line.match(/^ (?:([\p{L}_][\p{L}\p{N}_.-]*)|"([^"\n]+)"|'([^'\n]+)'):\s*(.*)$/u);
|
|
126
|
+
if (item) {
|
|
127
|
+
const key = item[1] || item[2] || item[3];
|
|
128
|
+
if (key.includes('\\')) { invalidDocuments = true; result.documents = {}; documents = false; continue; }
|
|
129
|
+
if (seenDocuments.has(key)) duplicateDocuments.add(key);
|
|
130
|
+
seenDocuments.add(key); result.documents[key] = duplicateDocuments.has(key) ? null : scalar(item[4]);
|
|
131
|
+
} else if (/^ \S/.test(line)) { invalidDocuments = true; result.documents = {}; documents = false; }
|
|
132
|
+
}
|
|
60
133
|
}
|
|
61
134
|
return result;
|
|
62
135
|
}
|
|
63
136
|
|
|
137
|
+
// The configured specs directory contains <slug>/SPEC.md. Refuse ambiguous
|
|
138
|
+
// layouts instead of guessing which increment or declared status to use.
|
|
139
|
+
function configuredSpecs(root, directory) {
|
|
140
|
+
const result = { path: directory, present: false, complete: false, items: [] };
|
|
141
|
+
if (!directory) return result;
|
|
142
|
+
let current = root;
|
|
143
|
+
for (const component of directory.split('/')) {
|
|
144
|
+
current = path.join(current, component);
|
|
145
|
+
try {
|
|
146
|
+
const info = fs.lstatSync(current);
|
|
147
|
+
if (info.isSymbolicLink() || !info.isDirectory()) return result;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error.code === 'ENOENT') result.complete = true;
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
result.present = true;
|
|
154
|
+
const incomplete = () => { result.items = []; return result; };
|
|
155
|
+
let scanned = 0;
|
|
156
|
+
function entriesAt(folder) {
|
|
157
|
+
const relative = normalize(path.relative(root, folder));
|
|
158
|
+
const result = stableDirectory(root, relative, Math.min(200, 500 - scanned));
|
|
159
|
+
scanned += result.entries.length;
|
|
160
|
+
return result.truncated ? null : result.entries;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const entries = entriesAt(current);
|
|
164
|
+
if (!entries) return incomplete();
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
if (/^readme(?:\.md)?$/i.test(entry.name) && !entry.isSymbolicLink()) continue;
|
|
167
|
+
if (['.git', '.pbai4devs', '.agent-tests', 'node_modules'].includes(entry.name.toLowerCase())) return incomplete();
|
|
168
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) return incomplete();
|
|
169
|
+
const folder = `${directory}/${entry.name}`;
|
|
170
|
+
if (!present(root, folder, true)) return incomplete();
|
|
171
|
+
const children = entriesAt(path.join(root, folder));
|
|
172
|
+
if (!children) return incomplete();
|
|
173
|
+
const names = children.filter(item => /^spec\.md$/i.test(item.name));
|
|
174
|
+
if (names.length !== 1 || !names[0].isFile() || names[0].isSymbolicLink()) return incomplete();
|
|
175
|
+
const file = `${folder}/${names[0].name}`;
|
|
176
|
+
if (!present(root, file, false)) return incomplete();
|
|
177
|
+
const document = metadata(root, file);
|
|
178
|
+
if (document.text === null) return incomplete();
|
|
179
|
+
result.items.push({ path: file, status: ['rascunho', 'pendente', 'aprovado'].includes(document.fields.status) ? document.fields.status : null });
|
|
180
|
+
if (result.items.length > 1) return incomplete();
|
|
181
|
+
}
|
|
182
|
+
if (!present(root, directory, true)) return incomplete();
|
|
183
|
+
result.complete = true;
|
|
184
|
+
} catch { return incomplete(); }
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function referenceState(roadmap, roadmapFile, specFile) {
|
|
189
|
+
if (!roadmap || !roadmapFile) return { linked: false, roadmapState: 'unknown' };
|
|
190
|
+
// Examples and orphan reference definitions do not select an increment.
|
|
191
|
+
const lines = []; let fence = null;
|
|
192
|
+
const body = roadmap.replace(/^---\n[^]*?\n---(?:\n|$)/, '').replace(/<!--[^]*?-->/g, '');
|
|
193
|
+
for (const line of body.split('\n')) {
|
|
194
|
+
const marker = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
195
|
+
if (fence) {
|
|
196
|
+
if (marker && marker[1][0] === fence[0] && marker[1].length >= fence.length && !line.slice(marker[0].length).trim()) fence = null;
|
|
197
|
+
} else if (marker) fence = marker[1];
|
|
198
|
+
else lines.push(line.replace(/(`+)[^`\n]*?\1/g, ''));
|
|
199
|
+
}
|
|
200
|
+
const normalizeLabel = label => label.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
201
|
+
const definitions = new Map();
|
|
202
|
+
for (const line of lines) {
|
|
203
|
+
const match = line.match(/^ {0,3}\[([^\]]+)\]:[ \t]*(?:<([^>\n]+)>|(\S+))(?:[ \t]+(?:"[^"\n]*"|'[^'\n]*'))?[ \t]*$/);
|
|
204
|
+
if (match) { const label = normalizeLabel(match[1]); definitions.set(label, [...(definitions.get(label) || []), match[2] || match[3]]); }
|
|
205
|
+
}
|
|
206
|
+
const references = [], ancestry = [];
|
|
207
|
+
for (const line of lines) {
|
|
208
|
+
const list = line.match(/^([ \t]*)(?:[-+*]|\d+[.)])\s+(.*)$/);
|
|
209
|
+
const marker = list?.[2].match(/^\[([ xX-])\](?:\s|$)/);
|
|
210
|
+
const closed = /(?:^|[^\p{L}])(?:cancelad[ao]s?|conclu[ií]d[ao]s?|publicad[ao]s?|finalizad[ao]s?|descartad[ao]s?|cancelled|completed|published)(?=$|[^\p{L}])/iu.test(line);
|
|
211
|
+
let state = 'unknown';
|
|
212
|
+
if (list) {
|
|
213
|
+
const indent = list[1].length;
|
|
214
|
+
while (ancestry.length && ancestry.at(-1).indent >= indent) ancestry.pop();
|
|
215
|
+
const own = closed || marker && /x/i.test(marker[1]) ? 'closed' : marker?.[1] === ' ' ? 'open' : marker?.[1] === '-' ? 'in-progress' : 'unknown';
|
|
216
|
+
state = ancestry.some(item => item.state === 'closed') ? 'closed' : /\t/.test(list[1]) || ancestry.some(item => item.state === 'unknown') ? 'unknown' : own;
|
|
217
|
+
ancestry.push({ indent, state });
|
|
218
|
+
} else if (line.trim() && !/^\s/.test(line)) ancestry.length = 0;
|
|
219
|
+
for (const match of line.matchAll(/(?<![!\\])\[[^\]\n]+\]\(\s*(?:<([^>\n]+)>|([^\s)]+))(?:\s+(?:"[^"\n]*"|'[^'\n]*'))?\s*\)/g)) references.push({ destination: match[1] || match[2], state });
|
|
220
|
+
for (const match of line.matchAll(/(?<![!\\])\[([^\]\n]+)\]\[([^\]\n]*)\]/g)) {
|
|
221
|
+
const label = normalizeLabel(match[2] || match[1]);
|
|
222
|
+
if (definitions.get(label)?.length === 1) references.push({ destination: definitions.get(label)[0], state });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const matches = references.filter(({ destination }) => {
|
|
226
|
+
try {
|
|
227
|
+
const value = decodeURIComponent(destination).split(/[?#]/, 1)[0];
|
|
228
|
+
if (!value || /^(?:[a-z][a-z0-9+.-]*:|\/|\\)/i.test(value) || value.includes('\\')) return false;
|
|
229
|
+
return path.posix.resolve('/', path.posix.dirname(roadmapFile), value) === '/' + specFile;
|
|
230
|
+
} catch { return false; }
|
|
231
|
+
});
|
|
232
|
+
if (matches.length !== 1) return { linked: matches.length > 0, roadmapState: matches.length ? 'ambiguous' : 'unknown' };
|
|
233
|
+
return { linked: true, roadmapState: matches[0].state };
|
|
234
|
+
}
|
|
235
|
+
|
|
64
236
|
function maturity(root, report) {
|
|
65
237
|
const stateFile = `${stateDirectory(root)}/configuracao/ESTADO.md`; const state = metadata(root, stateFile);
|
|
66
|
-
const safeDocument = value => typeof value === 'string' && !path.isAbsolute(value) &&
|
|
238
|
+
const safeDocument = value => typeof value === 'string' && !path.isAbsolute(value) && !/[\\\x00-\x1f\x7f:]/.test(value) && !value.split('/').some(part => !part || ['.', '..', '.git'].includes(part.toLowerCase()));
|
|
67
239
|
const planning = {};
|
|
68
240
|
for (const name of ['prd', 'ux_ui', 'design']) {
|
|
69
241
|
const file = state.documents[name];
|
|
@@ -72,10 +244,22 @@ function maturity(root, report) {
|
|
|
72
244
|
planning[name] = { path: safeDocument(file) ? file : null, present: Boolean(exists), status: ['rascunho', 'aprovado', 'nao-aplicavel', 'pendente'].includes(status) ? status : null };
|
|
73
245
|
}
|
|
74
246
|
planning.ready = planning.prd.status === 'aprovado' && planning.design.status === 'aprovado' && ['aprovado', 'nao-aplicavel'].includes(planning.ux_ui.status);
|
|
75
|
-
|
|
247
|
+
const roadmapPath = safeDocument(state.documents.roadmap) && /\.md$/i.test(state.documents.roadmap) ? state.documents.roadmap : null;
|
|
248
|
+
const roadmapPresent = Boolean(roadmapPath && present(root, roadmapPath, false));
|
|
249
|
+
const roadmapDocument = roadmapPresent ? metadata(root, roadmapPath) : { fields: {}, text: null };
|
|
250
|
+
planning.roadmap = { path: roadmapPath, present: roadmapPresent, status: ['pendente', 'rascunho', 'planejado', 'aprovado'].includes(roadmapDocument.fields.status) ? roadmapDocument.fields.status : null };
|
|
251
|
+
const specsPath = safeDocument(state.documents.specs) ? state.documents.specs : null;
|
|
252
|
+
planning.specs = configuredSpecs(root, specsPath);
|
|
253
|
+
for (const spec of planning.specs.items) Object.assign(spec, referenceState(roadmapDocument.text, roadmapPath, spec.path));
|
|
254
|
+
let released = false, releasesComplete = true; const releases = state.documents.releases;
|
|
76
255
|
if (safeDocument(releases) && present(root, releases, true)) {
|
|
77
256
|
const visit = (directory, depth = 0) => {
|
|
78
|
-
|
|
257
|
+
let listing;
|
|
258
|
+
try { listing = stableDirectory(root, directory, 100); }
|
|
259
|
+
catch { releasesComplete = false; return; }
|
|
260
|
+
if (listing.truncated) releasesComplete = false;
|
|
261
|
+
for (const entry of listing.entries) {
|
|
262
|
+
if (!listing.verify()) { releasesComplete = false; return; }
|
|
79
263
|
const file = normalize(path.join(directory, entry.name));
|
|
80
264
|
if (entry.isSymbolicLink()) continue;
|
|
81
265
|
if (entry.isDirectory() && depth < 2) visit(file, depth + 1);
|
|
@@ -91,7 +275,7 @@ function maturity(root, report) {
|
|
|
91
275
|
if (planning.ready) level = 'planejado';
|
|
92
276
|
if (report.mode === 'aplicacao-detectada') level = 'implementacao';
|
|
93
277
|
if (report.mode === 'aplicacao-detectada' && planning.ready && report.contract.complete && configuration.configuracao_tecnica === 'concluido') level = released ? 'manutencao' : 'configurado';
|
|
94
|
-
return { level, planning, configuration, released, signals: { application: report.mode === 'aplicacao-detectada', structure: report.contract.complete }, limits: ['Classificacao descritiva; status em arquivos nao substitui aprovacao, testes ou provas de funcionamento.'] };
|
|
278
|
+
return { level, planning, configuration, released, releasesComplete, signals: { application: report.mode === 'aplicacao-detectada', structure: report.contract.complete }, limits: ['Classificacao descritiva; status em arquivos nao substitui aprovacao, testes ou provas de funcionamento.'] };
|
|
95
279
|
}
|
|
96
280
|
|
|
97
281
|
const documentRoles = new Set(['readme', 'contribuicao', 'changelog', 'seguranca', 'adrs', 'prd', 'ux_ui', 'design', 'roadmap', 'releases', 'specs', 'tarefas']);
|
|
@@ -152,10 +336,12 @@ export function inspectDocumentation(root, limits = {}) {
|
|
|
152
336
|
}
|
|
153
337
|
let descriptor;
|
|
154
338
|
try {
|
|
339
|
+
const identity = pathIdentity(root, file);
|
|
340
|
+
if (!identity || !sameIdentity(identity, pathIdentity(root, file))) { warn(file, 'changed-during-read'); return; }
|
|
155
341
|
descriptor = fs.openSync(path.join(root, file), fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
156
342
|
const before = fs.fstatSync(descriptor);
|
|
157
343
|
if (!before.isFile() || before.dev !== expected.dev || before.ino !== expected.ino || before.size !== expected.size
|
|
158
|
-
|| before.mtimeMs !== expected.mtimeMs || before.ctimeMs !== expected.ctimeMs) { warn(file, 'changed-during-read'); return; }
|
|
344
|
+
|| before.mtimeMs !== expected.mtimeMs || before.ctimeMs !== expected.ctimeMs || !sameIdentity(identity, pathIdentity(root, file))) { warn(file, 'changed-during-read'); return; }
|
|
159
345
|
const buffer = Buffer.alloc(Math.min(before.size + 1, bounds.maxTotalBytes - scan.bytesRead));
|
|
160
346
|
let length = 0;
|
|
161
347
|
while (length < buffer.length) {
|
|
@@ -165,7 +351,7 @@ export function inspectDocumentation(root, limits = {}) {
|
|
|
165
351
|
scan.bytesRead += read;
|
|
166
352
|
}
|
|
167
353
|
const after = fs.fstatSync(descriptor);
|
|
168
|
-
if (length !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs) {
|
|
354
|
+
if (length !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs || !sameIdentity(identity, pathIdentity(root, file))) {
|
|
169
355
|
warn(file, 'changed-during-read'); return;
|
|
170
356
|
}
|
|
171
357
|
scan.bytesHashed += length;
|
|
@@ -194,8 +380,13 @@ export function inspectDocumentation(root, limits = {}) {
|
|
|
194
380
|
const found = locate(directory);
|
|
195
381
|
if (found.kind !== 'directory') { if (found.kind === 'symbolic-link') add(found.boundary, found, role); return; }
|
|
196
382
|
}
|
|
197
|
-
let children;
|
|
198
|
-
try {
|
|
383
|
+
let children, listing;
|
|
384
|
+
try {
|
|
385
|
+
listing = stableDirectory(root, directory, Math.max(0, bounds.maxEntries - scan.entries));
|
|
386
|
+
if (listing.changed) { scan.truncated = true; warn(directory || '.', 'changed-during-read'); return; }
|
|
387
|
+
if (listing.truncated) scan.truncated = true;
|
|
388
|
+
children = listing.entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
389
|
+
}
|
|
199
390
|
catch { warn(directory || '.', 'unreadable-directory'); return; }
|
|
200
391
|
// Discard a listing if the directory became a link while being inspected.
|
|
201
392
|
if (directory && locate(directory).kind !== 'directory') { warn(directory, 'changed-during-read'); return; }
|
|
@@ -204,6 +395,7 @@ export function inspectDocumentation(root, limits = {}) {
|
|
|
204
395
|
scan.entries++;
|
|
205
396
|
const file = directory ? `${directory}/${child.name}` : child.name;
|
|
206
397
|
if (excludedDocument(file)) continue;
|
|
398
|
+
if (!listing.verify()) { scan.truncated = true; warn(directory || '.', 'changed-during-read'); return; }
|
|
207
399
|
const found = locate(file);
|
|
208
400
|
if (found.kind === 'directory') {
|
|
209
401
|
if (depth < bounds.maxDepth) walk(file, depth + 1, role);
|
|
@@ -244,8 +436,11 @@ export function inspectProject(root = repository, { check = false, documentation
|
|
|
244
436
|
const warning = (file, reason) => report.warnings.push({ file, reason });
|
|
245
437
|
function commands(file, name) {
|
|
246
438
|
if (!['package.json', 'composer.json', 'deno.json', 'deno.jsonc', 'Makefile', 'makefile', 'GNUmakefile', 'pyproject.toml'].includes(name)) return;
|
|
247
|
-
|
|
248
|
-
|
|
439
|
+
const identity = pathIdentity(root, file);
|
|
440
|
+
if (!identity) { warning(file, 'manifest-nao-inspecionado'); return; }
|
|
441
|
+
if (identity.at(-1).size > 1024 * 1024) { warning(file, 'manifest-grande-demais'); return; }
|
|
442
|
+
const text = metadata(root, file).text;
|
|
443
|
+
if (text === null) { warning(file, 'manifest-nao-inspecionado'); return; }
|
|
249
444
|
let names = [];
|
|
250
445
|
try {
|
|
251
446
|
if (['package.json', 'composer.json', 'deno.json', 'deno.jsonc'].includes(name)) {
|
|
@@ -268,7 +463,10 @@ export function inspectProject(root = repository, { check = false, documentation
|
|
|
268
463
|
} catch { warning(file, 'manifest-nao-interpretado'); }
|
|
269
464
|
}
|
|
270
465
|
function walk(directory = '', depth = 0) {
|
|
271
|
-
const
|
|
466
|
+
const listing = stableDirectory(root, directory, Math.max(0, 25000 - entries));
|
|
467
|
+
if (listing.changed) { report.scan.truncated = true; warning(directory || '.', 'changed-during-read'); return; }
|
|
468
|
+
if (listing.truncated) report.scan.truncated = true;
|
|
469
|
+
const children = listing.entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
272
470
|
for (const item of children) {
|
|
273
471
|
if (++entries > 25000) { report.scan.truncated = true; return; }
|
|
274
472
|
const file = normalize(path.join(directory, item.name));
|
|
@@ -276,10 +474,12 @@ export function inspectProject(root = repository, { check = false, documentation
|
|
|
276
474
|
if (item.isSymbolicLink()) continue;
|
|
277
475
|
if (item.isDirectory()) {
|
|
278
476
|
if (excluded.has(item.name) || excluded.has(item.name.toLowerCase()) || /^\.?playwright/i.test(item.name)) continue;
|
|
477
|
+
if (!listing.verify()) { report.scan.truncated = true; warning(directory || '.', 'changed-during-read'); return; }
|
|
279
478
|
if (depth < report.scan.maxDepth) walk(file, depth + 1); else report.scan.truncated = true;
|
|
280
479
|
continue;
|
|
281
480
|
}
|
|
282
481
|
if (!item.isFile()) continue;
|
|
482
|
+
if (!listing.verify()) { report.scan.truncated = true; warning(directory || '.', 'changed-during-read'); return; }
|
|
283
483
|
let language = manifests.get(item.name);
|
|
284
484
|
if (/\.(?:csproj|fsproj|vbproj|sln)$/.test(item.name)) language = 'dotnet';
|
|
285
485
|
if (/^requirements(?:[.-][A-Za-z0-9_-]+)?\.txt$/.test(item.name)) language = 'python';
|
|
@@ -6,15 +6,16 @@ import { isGeneratedOutput } from './gitignore.mjs';
|
|
|
6
6
|
|
|
7
7
|
const policy = JSON.parse(readFileSync(new URL('../../templates/clients/ADAPTERS.json', import.meta.url), 'utf8'));
|
|
8
8
|
const kinds = new Set(['skills', 'agents', 'commands', 'rules']);
|
|
9
|
-
const layouts = new Set(['agrupado', 'plano', 'arquivo', 'mdc', 'agente', 'prompt', 'instrucoes']);
|
|
9
|
+
const layouts = new Set(['agrupado', 'plano', 'arquivo', 'mdc', 'agente', 'prompt', 'instrucoes', 'nenhum']);
|
|
10
10
|
const entryNames = new Set(['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']);
|
|
11
11
|
const safeRoot = value => typeof value === 'string' && /^\.[a-z0-9][a-z0-9-]*$/.test(value) && !['.git', '.pbai4devs', '.agent-tests'].includes(value);
|
|
12
12
|
const stringList = value => Array.isArray(value) && value.every(item => typeof item === 'string');
|
|
13
13
|
if (policy.schemaVersion !== 1 || !Array.isArray(policy.adapters) || !policy.adapters.length || !stringList(policy.entrypoints) || policy.entrypoints.some(name => !entryNames.has(name))) throw new Error('Politica de clientes invalida.');
|
|
14
14
|
const ids = new Set(), roots = new Set(), aliases = {};
|
|
15
15
|
for (const adapter of policy.adapters) {
|
|
16
|
-
if (!adapter || typeof adapter.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(adapter.id) || adapter.id === 'all' || ids.has(adapter.id) || typeof adapter.name !== 'string' || !safeRoot(adapter.root) || roots.has(adapter.root) || !adapter.formats || Object.entries(adapter.formats).some(([kind, layout]) => !kinds.has(kind) || !layouts.has(layout))) throw new Error('Adaptador invalido.');
|
|
16
|
+
if (!adapter || typeof adapter.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(adapter.id) || adapter.id === 'all' || ids.has(adapter.id) || typeof adapter.name !== 'string' || !safeRoot(adapter.root) || roots.has(adapter.root) || !adapter.formats || Object.entries(adapter.formats).some(([kind, layout]) => !kinds.has(kind) || !layouts.has(layout) || layout === 'nenhum' && kind !== 'skills')) throw new Error('Adaptador invalido.');
|
|
17
17
|
if (/[\r\n]/.test(adapter.name) || typeof adapter.reason !== 'string' || /[\r\n]/.test(adapter.reason) || (adapter.discovery !== undefined && adapter.discovery !== 'antigravity')) throw new Error('Descricao ou descoberta de adaptador invalida.');
|
|
18
|
+
if (adapter.skillFallback !== undefined && (!safeRoot(adapter.skillFallback) || adapter.skillFallback === adapter.root || adapter.formats.skills !== 'nenhum')) throw new Error('Fallback de skills invalido.');
|
|
18
19
|
for (const field of ['aliases', 'requires', 'extraRoots', 'entrypoints', 'sources']) if (!stringList(adapter[field])) throw new Error('Lista invalida no adaptador: ' + field);
|
|
19
20
|
if (adapter.extraRoots.some(root => !safeRoot(root)) || adapter.entrypoints.some(name => !entryNames.has(name)) || adapter.sources.some(url => !url.startsWith('https://'))) throw new Error('Caminho ou fonte invalida no adaptador.');
|
|
20
21
|
for (const field of ['agentMetadata', 'ruleMetadata']) if (adapter[field] !== undefined && !/^[a-z0-9-]+\.ya?ml$/.test(adapter[field])) throw new Error('Metadados invalidos no adaptador.');
|
|
@@ -22,6 +23,7 @@ for (const adapter of policy.adapters) {
|
|
|
22
23
|
}
|
|
23
24
|
for (const adapter of policy.adapters) {
|
|
24
25
|
if (adapter.requires.some(id => !ids.has(id))) throw new Error('Dependencia de adaptador desconhecida.');
|
|
26
|
+
if (adapter.skillFallback && !policy.adapters.some(other => other.root === adapter.skillFallback && adapter.requires.includes(other.id))) throw new Error('Fallback exige cliente de origem.');
|
|
25
27
|
for (const alias of adapter.aliases) {
|
|
26
28
|
if (!/^[a-z][a-z0-9-]*$/.test(alias) || ids.has(alias) || alias === 'all' || Object.hasOwn(aliases, alias)) throw new Error('Alias de adaptador invalido.');
|
|
27
29
|
aliases[alias] = adapter.id;
|
|
@@ -102,6 +104,7 @@ export function adapterManifest(root, { clients } = {}) {
|
|
|
102
104
|
for (const adapter of selection.adapters) {
|
|
103
105
|
rows.push(`# ${adapter.id}: ${adapter.reason}`, `agente|${adapter.root}|agrupado`);
|
|
104
106
|
for (const [kind, mode] of Object.entries(adapter.formats)) rows.push(`formato|${adapter.root}|${kind}|${mode}`);
|
|
107
|
+
if (adapter.skillFallback) rows.push(`fallback|${adapter.root}|${adapter.skillFallback}|skills`);
|
|
105
108
|
if (adapter.discovery) rows.push(`descoberta|${adapter.root}|${adapter.discovery}`);
|
|
106
109
|
for (const [kind, key] of [['agents', 'agentMetadata'], ['rules', 'ruleMetadata']]) {
|
|
107
110
|
if (!adapter[key]) continue;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { delimiter, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
2
2
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
3
|
-
import { accessSync, constants, statSync } from 'node:fs';
|
|
3
|
+
import { accessSync, constants, realpathSync, statSync } from 'node:fs';
|
|
4
4
|
|
|
5
5
|
export const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
6
6
|
|
|
@@ -10,8 +10,44 @@ export function requireNode(minimum = 24, version = process.versions.node) {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// NODE_OPTIONS uses Node's double-quoted token syntax, not shell quoting.
|
|
14
|
+
// Keep option values as a single token so text in a preload path is not a flag.
|
|
15
|
+
function nodeOptionTokens(value) {
|
|
16
|
+
const result = []; let token = '', quoted = false, escaped = false, active = false;
|
|
17
|
+
for (const character of value) {
|
|
18
|
+
if (escaped) { token += character; escaped = false; active = true; }
|
|
19
|
+
else if (character === '\\' && quoted) escaped = true;
|
|
20
|
+
else if (character === '"') { quoted = !quoted; active = true; }
|
|
21
|
+
else if (character === ' ' && !quoted) { if (active) result.push(token); token = ''; active = false; }
|
|
22
|
+
else { token += character; active = true; }
|
|
23
|
+
}
|
|
24
|
+
if (escaped) token += '\\';
|
|
25
|
+
if (active) result.push(token);
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function preserveMainSymlinks() {
|
|
30
|
+
let preserved = process.env.NODE_PRESERVE_SYMLINKS_MAIN === '1';
|
|
31
|
+
for (const tokens of [nodeOptionTokens(process.env.NODE_OPTIONS || ''), process.execArgv]) {
|
|
32
|
+
for (const token of tokens) {
|
|
33
|
+
if (/^--preserve-symlinks-main(?:=.*)?$/.test(token)) preserved = true;
|
|
34
|
+
else if (/^--no-preserve-symlinks-main(?:=.*)?$/.test(token)) preserved = false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return preserved;
|
|
38
|
+
}
|
|
39
|
+
|
|
13
40
|
export function isMain(metaUrl) {
|
|
14
|
-
|
|
41
|
+
if (!process.argv[1] || typeof metaUrl !== 'string') return false;
|
|
42
|
+
// In eval/print mode argv[1] is a user argument, not the entry module.
|
|
43
|
+
if (process.execArgv.some(arg => /^-(?:e|p|ep|pe)$/.test(arg) || /^--(?:eval|print)(?:=|$)/.test(arg))) return false;
|
|
44
|
+
const entry = resolve(process.argv[1]);
|
|
45
|
+
// Only --preserve-symlinks-main keeps the main URL logical. The separate
|
|
46
|
+
// --preserve-symlinks option can preserve an imported alias while the main
|
|
47
|
+
// URL is physical, so comparing both paths would execute that import twice.
|
|
48
|
+
if (preserveMainSymlinks()) return metaUrl === pathToFileURL(entry).href;
|
|
49
|
+
try { return metaUrl === pathToFileURL(realpathSync(entry)).href; }
|
|
50
|
+
catch { return false; }
|
|
15
51
|
}
|
|
16
52
|
|
|
17
53
|
export function findExecutable(name, environment = process.env, platform = process.platform) {
|
|
@@ -29,7 +29,9 @@ catálogo; resultados, caches e pastas temporárias não entram nos índices.
|
|
|
29
29
|
`formato|pasta|tipo|formato` e registrar `descoberta|.agents|antigravity`.
|
|
30
30
|
`agrupado` preserva a árvore; `plano` retira categorias; `arquivo` preserva
|
|
31
31
|
categorias e usa `nome.md`. `mdc`, `instrucoes`, `prompt` e `agente` produzem as
|
|
32
|
-
extensões nativas
|
|
32
|
+
extensões nativas; `nenhum` desativa a derivação de skills naquela rota.
|
|
33
|
+
`fallback|destino|origem|skills` usa o destino para cada skill cuja rota de
|
|
34
|
+
origem estiver obstruída. A descoberta gera os três manifestos JSON do Antigravity.
|
|
33
35
|
Veja os motivos e limites em [Compatibilidade](../../COMPATIBILIDADE.md).
|
|
34
36
|
|
|
35
37
|
Sem manifesto, o sincronizador gera a política de
|
|
@@ -49,3 +51,5 @@ atualizar arquivos gerados e blocos separadamente, preservando alterações huma
|
|
|
49
51
|
Apontamentos obsoletos podem ser removidos quando a propriedade é comprovada;
|
|
50
52
|
arquivos e pastas próprios, inclusive os adicionais dentro do mesmo recurso,
|
|
51
53
|
devem permanecer.
|
|
54
|
+
O estado derivado registra remoções comprovadas para reconciliá-las no próximo
|
|
55
|
+
`update`; campos adicionais existentes no estado são preservados.
|