@pathbit/agentdevkit 0.0.16 → 0.0.18
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/INSTRUCTIONS.md +1 -1
- package/.pbai4devs/README.md +1 -1
- package/.pbai4devs/commands/sincronizar/COMMAND.md +6 -4
- package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +3 -2
- package/.pbai4devs/scripts/inspecionar-projeto/tests/TEST.mjs +18 -4
- package/.pbai4devs/scripts/lib/README.md +3 -0
- package/.pbai4devs/scripts/lib/gitignore.mjs +46 -9
- package/.pbai4devs/scripts/lib/state-path.mjs +20 -0
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +8 -1
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +75 -34
- package/.pbai4devs/scripts/sincronizar-agentes/tests/COEXISTENCE.mjs +5 -5
- package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +1 -1
- package/.pbai4devs/scripts/sincronizar-agentes/tests/MANAGED_CONTENT.mjs +9 -9
- package/.pbai4devs/scripts/sincronizar-agentes/tests/PRESERVATION.mjs +306 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +41 -8
- package/.pbai4devs/scripts/testar-maturidade/SCRIPT.mjs +6 -4
- package/.pbai4devs/scripts/testar-maturidade/tests/TEST.mjs +4 -3
- package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +5 -3
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +15 -7
- package/.pbai4devs/skills/produto/criar-design-doc/SKILL.md +1 -1
- package/.pbai4devs/skills/produto/criar-prd/SKILL.md +1 -1
- package/.pbai4devs/skills/produto/criar-ux-ui/SKILL.md +1 -1
- package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +1 -1
- package/.pbai4devs/skills/projeto/proximos-passos/SKILL.md +1 -1
- package/.pbai4devs/skills/projeto/sincronizar-agentes/SKILL.md +1 -1
- package/.pbai4devs/skills/specs/criar-spec/SKILL.md +1 -1
- package/.pbai4devs/skills/specs/implementar-spec/SKILL.md +1 -1
- package/.pbai4devs/templates/entrypoints/AGENTS.md +1 -1
- package/.pbai4devs/templates/gitignore/local.ignore +2 -0
- package/.pbai4devs/templates/gitignore/shared.ignore +2 -0
- package/CHANGELOG.md +11 -0
- package/docs/guides/cli.md +46 -15
- package/docs/guides/installation.md +20 -8
- package/docs/guides/troubleshooting.md +9 -1
- package/docs/guides/workflow.md +1 -1
- package/docs/specs/README.md +2 -0
- package/docs/specs/safe-synchronization/SPEC.md +21 -0
- package/docs/specs/state-directory/SPEC.md +22 -0
- package/package.json +1 -1
- package/src/cli.mjs +2 -2
- package/src/config.mjs +2 -1
- package/src/i18n.mjs +25 -0
- package/src/install.mjs +373 -27
- /package/.pbai4devs/{estado → state}/README.md +0 -0
|
@@ -31,7 +31,7 @@ serão preenchidos por perguntas e respostas quando o projeto começar.
|
|
|
31
31
|
Combine destinos e registre-os no estado futuro de configuração. Ao finalizar
|
|
32
32
|
uma etapa, explique a próxima e reúna suas perguntas. Use
|
|
33
33
|
[proximos-passos](skills/projeto/proximos-passos/SKILL.md) e respeite o idioma
|
|
34
|
-
salvo em `.pbai4devs/
|
|
34
|
+
salvo em `.pbai4devs/state/agentdevkit.json`; sem escolha, use português.
|
|
35
35
|
Evolua README e documentos pelos [modelos da fase](templates/README.md).
|
|
36
36
|
Atualize fatos e links afetados; registre ausência de impacto com justificativa.
|
|
37
37
|
|
package/.pbai4devs/README.md
CHANGED
|
@@ -11,7 +11,7 @@ O catálogo local `catalog.json` é gerado das fontes pelo sincronizador.
|
|
|
11
11
|
Ele inclui recursos, ferramentas, automações, modelos e referências externas.
|
|
12
12
|
Os arquivos `skills.json`, `rules.json` e `agents.json` são índices auxiliares
|
|
13
13
|
das fontes. Os clientes selecionados usam seus diretórios de apontamentos.
|
|
14
|
-
O estado `
|
|
14
|
+
O estado `state/agentdevkit.json` guarda `clients`: `["all"]` ou os IDs
|
|
15
15
|
escolhidos. Catálogos, manifesto, estado e pastas nativas são derivados locais;
|
|
16
16
|
não devem ser commitados. No repositório do kit, o código do produto fica em
|
|
17
17
|
`src/`, `bin/`, `tests/` e `scripts/release/`, na raiz.
|
|
@@ -4,14 +4,16 @@ description: Atualiza ou confere os apontamentos compartilhados dos agentes.
|
|
|
4
4
|
|
|
5
5
|
# Sincronizar
|
|
6
6
|
|
|
7
|
-
Entrada:
|
|
7
|
+
Entrada: este comando agêntico usa o script local com `--force` para reparar;
|
|
8
|
+
`--check` só confere. O `sync` da CLI usa as fontes locais e não implica
|
|
9
|
+
`--force`; `update` usa as fontes da versão do pacote executado.
|
|
8
10
|
Pré-requisitos: Node.js 24+ e manifesto `.pbai4devs/espelhos.conf` válido.
|
|
9
11
|
|
|
10
12
|
Siga [sincronizar-agentes](../../skills/projeto/sincronizar-agentes/SKILL.md)
|
|
11
13
|
e execute o script correspondente pelo seletor. A reconstrução repara
|
|
12
|
-
apontamentos e remove saídas obsoletas cuja propriedade do kit foi comprovada
|
|
13
|
-
|
|
14
|
-
inclusive
|
|
14
|
+
apontamentos e remove saídas obsoletas cuja propriedade do kit foi comprovada.
|
|
15
|
+
Recursos adicionais, configurações, pastas e links humanos são preservados,
|
|
16
|
+
inclusive em colisões de nomes. O backup local não substitui o histórico Git.
|
|
15
17
|
Após reparar, execute também `--check`.
|
|
16
18
|
|
|
17
19
|
Respeite os clientes registrados no manifesto. Para mudar a seleção salva,
|
|
@@ -5,6 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { requireNode, isMain } from '../lib/runtime.mjs';
|
|
8
|
+
import { stateDirectory } from '../lib/state-path.mjs';
|
|
8
9
|
|
|
9
10
|
const repository = fs.realpathSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..'));
|
|
10
11
|
const defaultAgents = ['.agents', '.agent', '.gemini', '.claude', '.codex', '.cursor', '.github', '.opencode'];
|
|
@@ -61,7 +62,7 @@ function metadata(root, file) {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
function maturity(root, report) {
|
|
64
|
-
const stateFile =
|
|
65
|
+
const stateFile = `${stateDirectory(root)}/configuracao/ESTADO.md`; const state = metadata(root, stateFile);
|
|
65
66
|
const safeDocument = value => typeof value === 'string' && !path.isAbsolute(value) && !value.includes('\\') && !value.split('/').some(part => !part || ['.', '..', '.git'].includes(part));
|
|
66
67
|
const planning = {};
|
|
67
68
|
for (const name of ['prd', 'ux_ui', 'design']) {
|
|
@@ -211,7 +212,7 @@ export function inspectDocumentation(root, limits = {}) {
|
|
|
211
212
|
else if (found.kind === 'symbolic-link') add(found.boundary, found, role);
|
|
212
213
|
}
|
|
213
214
|
}
|
|
214
|
-
const declared = metadata(root,
|
|
215
|
+
const declared = metadata(root, `${stateDirectory(root)}/configuracao/ESTADO.md`).documents;
|
|
215
216
|
for (const [role, file] of Object.entries(declared)) {
|
|
216
217
|
if (!documentRoles.has(role)) continue;
|
|
217
218
|
if (!safeDocument(file)) { if (file !== null) warn(null, 'invalid-configured-path'); continue; }
|
|
@@ -111,7 +111,7 @@ test('rejeita origens fora das fixtures e opções desconhecidas', () => {
|
|
|
111
111
|
|
|
112
112
|
test('classifica sinais de maturidade sem confundir planejamento com produto funcionando', t => {
|
|
113
113
|
const { directory, file, run } = fixture(t);
|
|
114
|
-
const state = stage => file('.pbai4devs/
|
|
114
|
+
const state = stage => file('.pbai4devs/state/configuracao/ESTADO.md', `---\npreparacao: concluido\nconfiguracao_tecnica: ${stage}\ndocumentos:\n prd: docs/PRD.md\n ux_ui: docs/UX.md\n design: docs/DESIGN.md\n releases: docs/releases\n---\n# Estado\n`);
|
|
115
115
|
assert.equal(run().report.maturity.level, 'preparacao');
|
|
116
116
|
state('pendente'); file('docs/PRD.md', '---\nstatus: rascunho\n---\n# PRD\n');
|
|
117
117
|
assert.equal(run().report.maturity.level, 'descoberta');
|
|
@@ -131,6 +131,20 @@ test('classifica sinais de maturidade sem confundir planejamento com produto fun
|
|
|
131
131
|
assert.match(result.report.maturity.limits[0], /nao substitui/);
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
+
test('inspeção lê decisões legadas e informa conflito entre pastas de estado', t => {
|
|
135
|
+
const { file, run } = fixture(t);
|
|
136
|
+
const decisions = '---\npreparacao: concluido\nconfiguracao_tecnica: pendente\n---\n# Estado\n';
|
|
137
|
+
file('.pbai4devs/estado/configuracao/ESTADO.md', decisions);
|
|
138
|
+
const legacy = run();
|
|
139
|
+
assert.equal(legacy.status, 0, legacy.stderr);
|
|
140
|
+
assert.equal(legacy.report.maturity.configuration.present, true);
|
|
141
|
+
assert.equal(legacy.report.maturity.configuration.preparacao, 'concluido');
|
|
142
|
+
file('.pbai4devs/state/configuracao/ESTADO.md', decisions);
|
|
143
|
+
const conflict = run();
|
|
144
|
+
assert.equal(conflict.status, 2);
|
|
145
|
+
assert.match(JSON.parse(conflict.stderr).error, /coexistem/);
|
|
146
|
+
});
|
|
147
|
+
|
|
134
148
|
test('exemplos e caches Playwright não viram aplicação do projeto real', t => {
|
|
135
149
|
const { file, run } = fixture(t);
|
|
136
150
|
for (const folder of ['examples/generated/a', 'examples/fixture', 'tmp/examples/a', 'TEMP/project', '.PLAYWRIGHT-MCP', 'playwright-results']) {
|
|
@@ -154,7 +168,7 @@ test('aceita inspecionar cenário em examples/generated sem liberar outras pasta
|
|
|
154
168
|
|
|
155
169
|
test('status arbitrários e referências fora da raiz não expõem dados da configuração', t => {
|
|
156
170
|
const { file, run } = fixture(t); const secret = 'PRIVATE_STATUS_192819';
|
|
157
|
-
file('.pbai4devs/
|
|
171
|
+
file('.pbai4devs/state/configuracao/ESTADO.md', `---\npreparacao: ${secret}\nconfiguracao_tecnica: ${secret}\ndocumentos:\n prd: ../outside.md\n design: docs/DESIGN.md\n---\n# Estado\n`);
|
|
158
172
|
file('docs/DESIGN.md', `---\nstatus: ${secret}\n---\n# Design\n`);
|
|
159
173
|
const result = run(); assert.equal(result.status, 0);
|
|
160
174
|
assert.equal(result.report.maturity.planning.prd.path, null);
|
|
@@ -199,7 +213,7 @@ test('descobre documentação real sem impor caixa, pasta docs ou frontmatter',
|
|
|
199
213
|
|
|
200
214
|
test('caminhos cadastrados têm proveniência sem assumir status ou ler arquivos de configuração', t => {
|
|
201
215
|
const { file, run } = fixture(t);
|
|
202
|
-
file('.pbai4devs/
|
|
216
|
+
file('.pbai4devs/state/configuracao/ESTADO.md', '---\ndocumentos:\n design: architecture/current.md\n specs: specifications\n prd: planning/absent.md\n readme: ../private-secret.md\n seguranca: .env\n---\n# Estado\n');
|
|
203
217
|
file('architecture/current.md', '# Existing architecture\n');
|
|
204
218
|
file('specifications/current.mdx', '# Increment\n');
|
|
205
219
|
file('.env', 'DO_NOT_EMIT_OR_HASH_THIS_SECRET');
|
|
@@ -231,7 +245,7 @@ test('documentos simbólicos permanecem opacos sem ler destinos internos, extern
|
|
|
231
245
|
if (process.platform !== 'win32' || !['EPERM', 'EACCES'].includes(error.code)) throw error;
|
|
232
246
|
t.skip('Links simbólicos indisponíveis sem permissão do Windows.'); return;
|
|
233
247
|
}
|
|
234
|
-
file('.pbai4devs/
|
|
248
|
+
file('.pbai4devs/state/configuracao/ESTADO.md', '---\ndocumentos:\n design: external/private.md\n---\n# State\n');
|
|
235
249
|
const result = run();
|
|
236
250
|
assert.equal(result.status, 0, result.stderr);
|
|
237
251
|
const docs = result.report.documentation;
|
|
@@ -14,3 +14,6 @@ Texto externo, nomes existentes e metadados humanos permanecem preservados.
|
|
|
14
14
|
|
|
15
15
|
`gitignore.mjs` planeja exclusões locais e detecta regras conflitantes. Ele não
|
|
16
16
|
altera o `.gitignore` da raiz; conflitos são informados para revisão humana.
|
|
17
|
+
|
|
18
|
+
`state-path.mjs` identifica o estado atual ou legado sem dividir dados; duas
|
|
19
|
+
pastas simultâneas ou links simbólicos exigem resolver o conflito antes do uso.
|
|
@@ -98,10 +98,33 @@ function localBlock(current, first, last, body) {
|
|
|
98
98
|
return text.slice(0, start) + block + text.slice(finish);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
function gitCommand(root, args, input) {
|
|
101
|
+
function gitCommand(root, args, input, encoding = 'utf8') {
|
|
102
102
|
const env = { ...process.env };
|
|
103
103
|
for (const key of ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE']) delete env[key];
|
|
104
|
-
return spawnSync('git', [...(['ls-files', 'rm'].includes(args[0]) ? ['--literal-pathspecs'] : []), '-C', root, ...args], { input, encoding
|
|
104
|
+
return spawnSync('git', [...(['ls-files', 'rm'].includes(args[0]) ? ['--literal-pathspecs'] : []), '-C', root, ...args], { input, encoding, env, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Compare exact bytes first. Only accept Git's plain text CRLF conversion when
|
|
108
|
+
// effective attributes prove it applies and no custom transformation can run.
|
|
109
|
+
// This deliberately does not invoke hash-object or repository-defined filters.
|
|
110
|
+
export function indexContentMatches(root, path, working, indexed) {
|
|
111
|
+
if (working.equals(indexed)) return true;
|
|
112
|
+
const attributes = gitCommand(root, ['check-attr', '-z', 'text', 'eol', 'filter', 'working-tree-encoding', 'ident', '--', path]);
|
|
113
|
+
if (attributes.status !== 0) return false;
|
|
114
|
+
const parts = attributes.stdout.split('\0');
|
|
115
|
+
const values = {};
|
|
116
|
+
for (let i = 0; i + 2 < parts.length; i += 3) values[parts[i + 1]] = parts[i + 2];
|
|
117
|
+
const absent = value => value === 'unspecified' || value === 'unset';
|
|
118
|
+
if (!absent(values.filter) || !absent(values['working-tree-encoding']) || !absent(values.ident) || values.text === 'unset') return false;
|
|
119
|
+
const config = gitCommand(root, ['config', '--bool', 'core.autocrlf']);
|
|
120
|
+
if (config.status !== 0 && config.status !== 1) return false;
|
|
121
|
+
const textEnabled = values.text === 'set' || values.text === 'auto' || ['lf', 'crlf'].includes(values.eol) || config.status === 0 && config.stdout.trim() === 'true';
|
|
122
|
+
if (!textEnabled || !working.includes(13) || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(working.toString('latin1'))) return false;
|
|
123
|
+
try {
|
|
124
|
+
const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(working);
|
|
125
|
+
if (text.replaceAll('\r\n', '').includes('\r')) return false;
|
|
126
|
+
return Buffer.from(text.replaceAll('\r\n', '\n')).equals(indexed);
|
|
127
|
+
} catch { return false; }
|
|
105
128
|
}
|
|
106
129
|
|
|
107
130
|
function checkedOutput(path) {
|
|
@@ -502,25 +525,39 @@ export function planUntrack(root, expectedHashes = {}) {
|
|
|
502
525
|
if (!candidates.length || !localStat(root)) return { root, paths: [], hashes: {}, index: '', conflicts: [] };
|
|
503
526
|
const result = gitCommand(root, ['ls-files', '--stage', '-z']);
|
|
504
527
|
if (result.status !== 0) {
|
|
505
|
-
if (result.error?.code === 'ENOENT' || /not a git repository/i.test(result.stderr || ''))
|
|
528
|
+
if (result.error?.code === 'ENOENT' || /not a git repository/i.test(result.stderr || '')) {
|
|
529
|
+
// Fail closed if a repository exists even when Git cannot be launched.
|
|
530
|
+
let directory = root;
|
|
531
|
+
while (true) {
|
|
532
|
+
if (localStat(join(directory, '.git'))) throw new Error('Nao foi possivel consultar o Git deste projeto; indice e arquivos preservados.');
|
|
533
|
+
const parent = dirname(directory);
|
|
534
|
+
if (parent === directory) break;
|
|
535
|
+
directory = parent;
|
|
536
|
+
}
|
|
537
|
+
return { root, paths: [], hashes: {}, index: '', conflicts: [] };
|
|
538
|
+
}
|
|
506
539
|
throw new Error(result.error?.message || result.stderr || 'Falha ao ler indice Git.');
|
|
507
540
|
}
|
|
508
541
|
const conflicts = [];
|
|
509
542
|
const records = result.stdout.split('\0').filter(Boolean);
|
|
510
|
-
const
|
|
543
|
+
const folded = new Map();
|
|
544
|
+
for (const path of candidates) folded.set(path.toLowerCase(), [...(folded.get(path.toLowerCase()) || []), path]);
|
|
511
545
|
const matches = new Map();
|
|
512
546
|
for (const record of records) {
|
|
513
547
|
const position = record.indexOf('\t'), path = record.slice(position + 1);
|
|
514
|
-
if (position < 0
|
|
515
|
-
|
|
548
|
+
if (position < 0) continue;
|
|
549
|
+
for (const candidate of folded.get(path.toLowerCase()) || []) {
|
|
550
|
+
matches.set(candidate, [...(matches.get(candidate) || []), record]);
|
|
551
|
+
}
|
|
516
552
|
}
|
|
517
553
|
const paths = [...matches.keys()].filter(path => {
|
|
518
554
|
const file = checkedLocalPath(join(root, path));
|
|
519
555
|
const entries = matches.get(path);
|
|
520
556
|
const match = entries.length === 1 && entries[0].match(/^\d+ ([a-f0-9]+) 0\t/);
|
|
521
|
-
const blob = match ? gitCommand(root, ['cat-file', 'blob', match[1]]) : null;
|
|
522
|
-
const
|
|
523
|
-
const
|
|
557
|
+
const blob = match ? gitCommand(root, ['cat-file', 'blob', match[1]], undefined, null) : null;
|
|
558
|
+
const data = localStat(file) ? readFileSync(file) : null;
|
|
559
|
+
const owned = data && createHash('sha256').update(data).digest('hex') === expectedHashes[path];
|
|
560
|
+
const indexOwned = entries[0]?.slice(entries[0].indexOf('\t') + 1) === path && blob?.status === 0 && data && indexContentMatches(root, path, data, blob.stdout);
|
|
524
561
|
if (!owned || !indexOwned) conflicts.push(path);
|
|
525
562
|
return owned && indexOwned;
|
|
526
563
|
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { lstatSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Preserve legacy installations until the CLI migrates them. Do not silently
|
|
5
|
+
// choose between two independently populated state trees.
|
|
6
|
+
export function stateDirectory(root) {
|
|
7
|
+
const inspect = relative => {
|
|
8
|
+
let item;
|
|
9
|
+
try { item = lstatSync(join(root, relative)); }
|
|
10
|
+
catch (error) { if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return null; throw error; }
|
|
11
|
+
if (item.isSymbolicLink()) throw new Error(`Link simbolico: ${relative}`);
|
|
12
|
+
if (!item.isDirectory()) throw new Error(`Pasta de estado inválida: ${relative}`);
|
|
13
|
+
return item;
|
|
14
|
+
};
|
|
15
|
+
if (!inspect('.pbai4devs')) return '.pbai4devs/state';
|
|
16
|
+
const current = inspect('.pbai4devs/state');
|
|
17
|
+
const legacy = inspect('.pbai4devs/estado');
|
|
18
|
+
if (current && legacy) throw new Error('Pastas .pbai4devs/state e .pbai4devs/estado coexistem; preserve os dados e resolva o conflito antes de continuar.');
|
|
19
|
+
return legacy ? '.pbai4devs/estado' : '.pbai4devs/state';
|
|
20
|
+
}
|
|
@@ -12,6 +12,9 @@ node --test .pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs
|
|
|
12
12
|
|
|
13
13
|
`--check` só confere; a execução normal atualiza os recursos. `--force` salva
|
|
14
14
|
conflitos em `.agent-tests/` antes de reparar os caminhos gerenciados.
|
|
15
|
+
O [comando agêntico](../../commands/sincronizar/COMMAND.md) usa `--force` por
|
|
16
|
+
padrão; executar este script diretamente não ativa essa opção. A CLI oferece
|
|
17
|
+
`sync --dry-run --json` para consultar o plano antes de aplicá-lo.
|
|
15
18
|
Recursos nativos humanos recebem um bloco curto com a referência ao hub.
|
|
16
19
|
Texto, frontmatter e nomes existentes são preservados. O estado guarda só o
|
|
17
20
|
hash desse bloco; edições externas continuam permitidas. Um bloco alterado
|
|
@@ -41,6 +44,10 @@ sem Git, informa que a proteção fica pendente até `git init` e sincronizaçã
|
|
|
41
44
|
Somente arquivos integralmente gerados são excluídos; pastas de clientes podem
|
|
42
45
|
conter arquivos humanos versionados. Conteúdo próprio não é desindexado pela
|
|
43
46
|
presença de um bloco gerenciado.
|
|
44
|
-
Entradas novas usam os templates; hashes em `
|
|
47
|
+
Entradas novas usam os templates; hashes em `state/derived.json` permitem
|
|
45
48
|
atualizar arquivos gerados e blocos separadamente, preservando alterações humanas.
|
|
46
49
|
`--check` também acusa derivados ainda rastreados; não altera o índice.
|
|
50
|
+
Apontamentos obsoletos podem ser removidos quando a propriedade é comprovada;
|
|
51
|
+
arquivos e pastas próprios, inclusive os adicionais dentro do mesmo recurso,
|
|
52
|
+
devem permanecer. Na CLI, confira `untrackedPaths` antes de aplicar: saídas
|
|
53
|
+
geradas comprovadas podem deixar de ser rastreadas, sem excluir seus bytes.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { chmodSync, cpSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, join, posix, resolve } from 'node:path';
|
|
5
5
|
import { randomUUID, createHash } from 'node:crypto';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
6
7
|
import { requireNode, projectRoot, isMain } from '../lib/runtime.mjs';
|
|
7
8
|
import { buildCatalog } from '../lib/catalog.mjs';
|
|
8
9
|
import { resourcePath } from '../lib/resource-path.mjs';
|
|
@@ -10,12 +11,13 @@ import { adapterManifest, resolveAdapters } from '../lib/adapters.mjs';
|
|
|
10
11
|
import { nativePathBoundary } from '../lib/native-paths.mjs';
|
|
11
12
|
import { canonicalEntrypoint, resolveEntrypoint, entrypointReference, renderEntrypointImports, mergeManagedContent, stripManagedContent } from '../lib/managed-content.mjs';
|
|
12
13
|
import { mergeTomlContent, stripTomlContent } from '../lib/managed-toml.mjs';
|
|
13
|
-
import { mergeGitignore, validateIgnoreClients, renderClientIgnore, isGeneratedOutput, planLocalIgnore, applyLocalIgnore, checkLocalIgnore, assertLocalIgnoreUnchanged, ignoreBegin, ignoreEnd } from '../lib/gitignore.mjs';
|
|
14
|
+
import { mergeGitignore, validateIgnoreClients, renderClientIgnore, isGeneratedOutput, planLocalIgnore, applyLocalIgnore, checkLocalIgnore, assertLocalIgnoreUnchanged, planUntrack, ignoreBegin, ignoreEnd } from '../lib/gitignore.mjs';
|
|
15
|
+
import { stateDirectory } from '../lib/state-path.mjs';
|
|
14
16
|
|
|
15
17
|
const marker = '<!-- gerado: sincronizar-agentes -->';
|
|
16
18
|
const tomlMarker = '# gerado: sincronizar-agentes';
|
|
17
19
|
const principals = { skills: 'SKILL.md', agents: 'AGENT.md', commands: 'COMMAND.md', rules: 'RULE.md' };
|
|
18
|
-
const auxiliary = ['templates', 'scripts', 'tools', 'external-skills', 'estado', 'tests', 'docs', 'adapters'];
|
|
20
|
+
const auxiliary = ['templates', 'scripts', 'tools', 'external-skills', 'state', 'estado', 'tests', 'docs', 'adapters'];
|
|
19
21
|
const reserved = new Set(['.git', '.pbai4devs', '.agent-tests']);
|
|
20
22
|
const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
21
23
|
const sorted = values => [...new Set(values)].sort(compare);
|
|
@@ -78,6 +80,28 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
78
80
|
requireNode();
|
|
79
81
|
root = realpathSync(resolve(root));
|
|
80
82
|
const absolute = value => join(root, ...value.split('/'));
|
|
83
|
+
function assertGitReadableForChanges() {
|
|
84
|
+
// A project may be nested in a worktree; .git can be a file or directory.
|
|
85
|
+
// If that marker exists we must not treat an unavailable Git executable,
|
|
86
|
+
// an invalid repository or an unreadable index as a Git-free project.
|
|
87
|
+
let folder = root, found = false;
|
|
88
|
+
while (true) {
|
|
89
|
+
if (stat(join(folder, '.git'))) { found = true; break; }
|
|
90
|
+
const parent = dirname(folder);
|
|
91
|
+
if (parent === folder) break;
|
|
92
|
+
folder = parent;
|
|
93
|
+
}
|
|
94
|
+
if (!found) return false;
|
|
95
|
+
const env = { ...process.env };
|
|
96
|
+
for (const key of ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE']) delete env[key];
|
|
97
|
+
const probe = spawnSync('git', ['-C', root, 'rev-parse', '--is-inside-work-tree'], {
|
|
98
|
+
encoding: 'utf8', env, windowsHide: true, timeout: 20000,
|
|
99
|
+
});
|
|
100
|
+
if (probe.error || probe.status !== 0 || probe.stdout.trim() !== 'true') {
|
|
101
|
+
fail('Nao foi possivel consultar o Git deste projeto; sincronizacao bloqueada para preservar arquivos e indice.');
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
81
105
|
function safe(value) {
|
|
82
106
|
if (typeof value !== 'string' || !value || /^[A-Za-z]:/.test(value) || /[|\\\x00-\x1f\x7f]/.test(value) || value.split('/').some(part => !part || part === '.' || part === '..')) fail(`Caminho invalido: ${value}`);
|
|
83
107
|
let current = root;
|
|
@@ -129,7 +153,6 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
129
153
|
const recorded = recordedHash === digest;
|
|
130
154
|
return isReference(text) && recorded;
|
|
131
155
|
};
|
|
132
|
-
const onlyReferences = value => { const files = scan(value); return files.length > 0 && ![...preserved.keys()].some(path => path === value || path.startsWith(`${value}/`)) && files.every(file => /\.(md|mdc|toml)$/i.test(file) && referenceFile(file)); };
|
|
133
156
|
const protectedPath = value => /^(workflows(?:\/|$)|settings\.json$|extensions\.json$)/.test(value.slice(value.indexOf('/') + 1).toLowerCase());
|
|
134
157
|
const agents = new Map();
|
|
135
158
|
const formats = new Map();
|
|
@@ -140,11 +163,12 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
140
163
|
const config = '.pbai4devs/espelhos.conf';
|
|
141
164
|
const configExists = stat(safe(config));
|
|
142
165
|
captureInput(config, configExists ? readFileSync(absolute(config)) : null);
|
|
143
|
-
const
|
|
166
|
+
const stateRoot = stateDirectory(root);
|
|
167
|
+
const stateFile = `${stateRoot}/agentdevkit.json`;
|
|
144
168
|
const installed = stat(safe(stateFile)) ? JSON.parse(read(safe(stateFile))) : null;
|
|
145
169
|
const configText = configExists ? read(safe(config)) : adapterManifest(root, { clients: installed?.clients });
|
|
146
170
|
const rows = configText.split('\n');
|
|
147
|
-
const entryRegistry =
|
|
171
|
+
const entryRegistry = `${stateRoot}/derived.json`;
|
|
148
172
|
const registryBytes = stat(safe(entryRegistry)) ? readFileSync(absolute(entryRegistry)) : null;
|
|
149
173
|
captureInput(entryRegistry, registryBytes);
|
|
150
174
|
const previousEntries = registryBytes ? JSON.parse(registryBytes.toString('utf8')) : { schemaVersion: 1, entries: {} };
|
|
@@ -293,6 +317,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
293
317
|
const remove = new Set();
|
|
294
318
|
const backup = new Set();
|
|
295
319
|
const conflicts = new Set();
|
|
320
|
+
const protectedConflicts = new Set();
|
|
296
321
|
const expected = new Map();
|
|
297
322
|
const managedBlocks = {};
|
|
298
323
|
const generatedFiles = {};
|
|
@@ -305,17 +330,31 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
305
330
|
function planRemove(value, custom = false) {
|
|
306
331
|
safe(value);
|
|
307
332
|
if (!managed(value) || protectedPath(value)) fail(`Limpeza fora dos espelhos ou caminho protegido: ${value}`);
|
|
333
|
+
// Directories, unowned files and edited references can contain project
|
|
334
|
+
// data even when --force is used. Only a recorded, unchanged reference
|
|
335
|
+
// may be removed; empty directories are never removed recursively.
|
|
336
|
+
if (!stat(safe(value))?.isFile() || !referenceFile(value)) {
|
|
337
|
+
remember(value, 'existing-file');
|
|
338
|
+
if (custom) { conflicts.add(value); protectedConflicts.add(value); }
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
308
341
|
remove.add(value);
|
|
309
342
|
if (custom) backup.add(value);
|
|
310
343
|
}
|
|
311
344
|
function cleanScope(value) {
|
|
312
345
|
if (preserveBoundary(value)) return;
|
|
313
|
-
|
|
314
|
-
if (
|
|
315
|
-
|
|
316
|
-
if (
|
|
317
|
-
|
|
318
|
-
|
|
346
|
+
const info = stat(safe(value));
|
|
347
|
+
if (!info) return;
|
|
348
|
+
const files = info.isDirectory() ? scan(value) : [value];
|
|
349
|
+
if (info.isDirectory() && !files.length) { remember(value, 'existing-directory'); return; }
|
|
350
|
+
let hasHumanContent = false;
|
|
351
|
+
for (const file of files) {
|
|
352
|
+
if (expected.has(file) || priorBlocks[file] || protectedPath(file)) continue;
|
|
353
|
+
if (referenceFile(file)) planRemove(file);
|
|
354
|
+
else if (claimed(file)) { remember(file, 'edited-generated-file'); hasHumanContent = true; }
|
|
355
|
+
else { remember(file); hasHumanContent = true; }
|
|
356
|
+
}
|
|
357
|
+
if (hasHumanContent && info.isDirectory()) remember(value);
|
|
319
358
|
}
|
|
320
359
|
const registries = new Map();
|
|
321
360
|
const discoveryOutputs = {};
|
|
@@ -549,6 +588,22 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
549
588
|
const omittedRows = new Set([...legacy, ...missing.map(native => native.index)]);
|
|
550
589
|
const finalConfig = rows.filter((row, index) => !omittedRows.has(index)).join('\n').replace(/\n*$/, '\n');
|
|
551
590
|
const configChanged = !configExists || configText !== finalConfig;
|
|
591
|
+
// Every planned write/removal may cover a different staged version. Compare
|
|
592
|
+
// the worktree bytes with the index before touching any path, even in force
|
|
593
|
+
// mode, and repeat immediately before applying the transaction.
|
|
594
|
+
const affected = sorted([...removals, ...changed, ...registries.keys(), ...ignoreFiles.keys(), ...rootEntryChanges.map(([name]) => name), ...(configChanged ? [config] : [])]);
|
|
595
|
+
const emptyHash = createHash('sha256').update(Buffer.alloc(0)).digest('hex');
|
|
596
|
+
const affectedHashes = Object.fromEntries(affected.flatMap(value => {
|
|
597
|
+
const info = stat(safe(value));
|
|
598
|
+
if (info && !info.isFile()) return []; // A type conflict already blocks application.
|
|
599
|
+
return [[value, info ? createHash('sha256').update(readFileSync(absolute(value))).digest('hex') : emptyHash]];
|
|
600
|
+
}));
|
|
601
|
+
const hadGit = Object.keys(affectedHashes).length ? assertGitReadableForChanges() : false;
|
|
602
|
+
const affectedIndex = planUntrack(root, affectedHashes);
|
|
603
|
+
for (const path of affectedIndex.conflicts) {
|
|
604
|
+
conflicts.add(path);
|
|
605
|
+
remember(path, 'staged-content-preserved');
|
|
606
|
+
}
|
|
552
607
|
function capture(value) {
|
|
553
608
|
const file = safe(value), info = stat(file);
|
|
554
609
|
if (!info) return null;
|
|
@@ -574,15 +629,15 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
574
629
|
let parent = posix.dirname(value);
|
|
575
630
|
while (parent !== '.') { if (stat(safe(parent))?.isDirectory()) originalDirectories.add(parent); parent = posix.dirname(parent); }
|
|
576
631
|
}
|
|
577
|
-
const touched = new Set(), createdDirectories = new Set()
|
|
578
|
-
const prunedDirectories = new Map();
|
|
579
|
-
let restoring = false;
|
|
632
|
+
const touched = new Set(), createdDirectories = new Set();
|
|
580
633
|
const summary = { changed: changed.length + rootEntryChanges.length, stale: removals.length, conflicts: conflicts.size, missing: missing.length, legacy: legacy.length, registries: registries.size, gitignores: ignoreFiles.size, manifests: configExists ? 0 : 1, localIgnores: localStatus.changed + localStatus.missing.length + localStatus.trackedOutputs.length + (localStatus.status === 'no-git' ? 1 : 0) };
|
|
581
634
|
const dirty = Object.values(summary).some(Boolean);
|
|
582
635
|
output(`${summary.changed} desatualizadas; ${summary.stale} obsoletas; ${summary.conflicts} conflitos; ${summary.missing} origens nativas ausentes; ${summary.legacy} documentos antigos; ${summary.registries} indices derivados; ${summary.gitignores} gitignores; ${summary.localIgnores} exclusoes locais.`);
|
|
583
636
|
if (localStatus.status === 'no-git') output('Git ausente: apos git init, execute a sincronizacao para proteger os arquivos locais.');
|
|
584
637
|
const preservedPaths = sorted(preserved.keys()).map(path => preserved.get(path));
|
|
585
638
|
if (check) return { ...summary, ownedOutputs, managedBlocks, preserved: preservedPaths, exitCode: dirty ? 1 : 0 };
|
|
639
|
+
if (affectedIndex.conflicts.length) fail(`Indice Git contem alteracoes proprias; arquivos preservados: ${sorted(affectedIndex.conflicts).join(', ')}`);
|
|
640
|
+
if (protectedConflicts.size) fail(`Conteudo proprio preservado; resolva a colisao antes de sincronizar: ${sorted(protectedConflicts).join(', ')}`);
|
|
586
641
|
if (conflicts.size) fail(`Use --force para reconstruir somente os caminhos gerenciados: ${sorted(conflicts).join(', ')}`);
|
|
587
642
|
if (localStatus.missing.length) fail(`Regras humanas reabrem derivados locais: ${localStatus.missing.join(', ')}`);
|
|
588
643
|
if (localPlan) assertLocalIgnoreUnchanged(localPlan);
|
|
@@ -592,7 +647,6 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
592
647
|
while (parent !== '.' && !stat(safe(parent))) { missing.push(parent); parent = posix.dirname(parent); }
|
|
593
648
|
for (const directory of missing.reverse()) {
|
|
594
649
|
mkdirSync(safe(directory)); createdDirectories.add(directory);
|
|
595
|
-
if (restoring) restoredDirectories.add(directory);
|
|
596
650
|
}
|
|
597
651
|
}
|
|
598
652
|
function mutate(value, action, text) {
|
|
@@ -647,40 +701,27 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
647
701
|
}
|
|
648
702
|
try {
|
|
649
703
|
if (localPlan) assertLocalIgnoreUnchanged(localPlan);
|
|
704
|
+
if (Object.keys(affectedHashes).length) {
|
|
705
|
+
if (!assertGitReadableForChanges() && hadGit) fail('Git deixou de estar disponivel antes da sincronizacao; arquivos preservados.');
|
|
706
|
+
const current = planUntrack(root, affectedHashes);
|
|
707
|
+
if (current.conflicts.length || current.index !== affectedIndex.index) fail('Indice Git mudou antes da sincronizacao; conteudo preservado.');
|
|
708
|
+
}
|
|
650
709
|
for (const value of removals) mutate(value, () => rmSync(safe(value), { recursive: true, force: true }), null);
|
|
651
710
|
for (const value of changed) mutate(value, () => atomic(value, expected.get(value)), expected.get(value));
|
|
652
711
|
for (const [value, text] of registries) mutate(value, () => atomic(value, text), text);
|
|
653
712
|
for (const [value, text] of ignoreFiles) mutate(value, () => atomic(value, text), text);
|
|
654
713
|
for (const [value, text] of rootEntryChanges) mutate(value, () => atomic(value, text), text);
|
|
655
714
|
if (configChanged) mutate(config, () => atomic(config, finalConfig), finalConfig);
|
|
656
|
-
//
|
|
657
|
-
for (const value of removals) {
|
|
658
|
-
let parent = posix.dirname(value);
|
|
659
|
-
const boundary = value.split('/').slice(0, 2).join('/');
|
|
660
|
-
while (parent.startsWith(boundary + '/') || parent === boundary) {
|
|
661
|
-
const info = stat(safe(parent));
|
|
662
|
-
if (!info?.isDirectory() || readdirSync(absolute(parent)).length) break;
|
|
663
|
-
rmdirSync(safe(parent)); prunedDirectories.set(parent, info.mode & 0o777); parent = posix.dirname(parent);
|
|
664
|
-
}
|
|
665
|
-
}
|
|
715
|
+
// Directory ownership is not recorded: keep even empty parent directories.
|
|
666
716
|
if (localPlan) applyLocalIgnore(localPlan);
|
|
667
717
|
} catch (error) {
|
|
668
718
|
const failures = [];
|
|
669
|
-
restoring = true;
|
|
670
719
|
for (const value of [...touched].reverse()) {
|
|
671
720
|
try {
|
|
672
721
|
if (fingerprint(capture(value)) !== currentHashes.get(value)) fail(`Mudanca concorrente preservada: ${value}`);
|
|
673
722
|
restore(value, originals.get(value));
|
|
674
723
|
} catch (failure) { failures.push(failure); }
|
|
675
724
|
}
|
|
676
|
-
for (const [directory, mode] of [...prunedDirectories].sort(([left], [right]) => right.length - left.length)) {
|
|
677
|
-
try {
|
|
678
|
-
if (!stat(safe(directory))) ensureDirectory(`${directory}/.restore-probe`);
|
|
679
|
-
const info = stat(safe(directory));
|
|
680
|
-
if (!info?.isDirectory() || !restoredDirectories.has(directory) && (info.mode & 0o777) !== mode) fail(`Diretorio concorrente preservado: ${directory}`);
|
|
681
|
-
chmodSync(safe(directory), mode);
|
|
682
|
-
} catch (failure) { failures.push(failure); }
|
|
683
|
-
}
|
|
684
725
|
for (const directory of sorted(createdDirectories).sort((a, b) => b.length - a.length)) {
|
|
685
726
|
try { if (!originalDirectories.has(directory) && stat(safe(directory))?.isDirectory() && !readdirSync(absolute(directory)).length) rmdirSync(safe(directory)); }
|
|
686
727
|
catch (failure) { failures.push(failure); }
|
|
@@ -95,8 +95,8 @@ test('link em caminho registrado continua sendo erro antes de qualquer escrita',
|
|
|
95
95
|
for (const source of ['files', 'blocks', 'derived', 'outputs']) {
|
|
96
96
|
const root = fixture(t), path = source === 'derived' ? 'CLAUDE.md' : source === 'outputs' ? '.agents/skills.json' : '.claude/skills/demo/SKILL.md';
|
|
97
97
|
if (!link(t, root, 'missing', path)) return;
|
|
98
|
-
if (source === 'derived' || source === 'outputs') put(root, '.pbai4devs/
|
|
99
|
-
else put(root, '.pbai4devs/
|
|
98
|
+
if (source === 'derived' || source === 'outputs') put(root, '.pbai4devs/state/derived.json', JSON.stringify({ schemaVersion: 1, entries: source === 'derived' ? { [path]: 'a'.repeat(64) } : {}, ...(source === 'outputs' ? { outputs: { [path]: 'a'.repeat(64) } } : {}) }));
|
|
99
|
+
else put(root, '.pbai4devs/state/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: source === 'files' ? { [path]: 'a'.repeat(64) } : {}, blocks: source === 'blocks' ? { [path]: 'a'.repeat(64) } : {} }));
|
|
100
100
|
const before = snapshot(root);
|
|
101
101
|
assert.throws(() => run(root, { force: true }), /Link simbolico em caminho gerenciado/);
|
|
102
102
|
assert.deepEqual(snapshot(root), before);
|
|
@@ -106,7 +106,7 @@ test('link em caminho registrado continua sendo erro antes de qualquer escrita',
|
|
|
106
106
|
test('manifesto standalone editado pelo usuário passa a ser preservado', t => {
|
|
107
107
|
const root = fixture(t); run(root);
|
|
108
108
|
const path = '.agents/skills.json', original = fs.readFileSync(join(root, path), 'utf8');
|
|
109
|
-
const derived = JSON.parse(fs.readFileSync(join(root, '.pbai4devs/
|
|
109
|
+
const derived = JSON.parse(fs.readFileSync(join(root, '.pbai4devs/state/derived.json')));
|
|
110
110
|
assert.equal(derived.outputs[path], hash(original));
|
|
111
111
|
put(root, path, '{"entries":[{"path":".agents/skills/custom"}]}\n');
|
|
112
112
|
const result = run(root, { force: true });
|
|
@@ -151,7 +151,7 @@ test('tipo alterado em caminho nativo registrado continua recusado antes de grav
|
|
|
151
151
|
]) {
|
|
152
152
|
const root = fixture(t);
|
|
153
153
|
if (path === '.codex') put(root, path, 'Human root\n'); else put(root, `${path}/keep.txt`, 'Human directory\n');
|
|
154
|
-
put(root, '.pbai4devs/
|
|
154
|
+
put(root, '.pbai4devs/state/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: { [owned]: 'a'.repeat(64) }, blocks: {} }));
|
|
155
155
|
const before = snapshot(root);
|
|
156
156
|
assert.throws(() => run(root, { force: true }), /caminho gerenciado/);
|
|
157
157
|
assert.deepEqual(snapshot(root), before);
|
|
@@ -191,7 +191,7 @@ test('variante única de entrada registrada conserva conteúdo como arquivo mist
|
|
|
191
191
|
for (const owned of ['AGENTS.md', 'agents.md']) {
|
|
192
192
|
const root = fixture(t); put(root, 'agents.md', '# Renamed human entry\n');
|
|
193
193
|
assert.throws(() => nativePathBoundary(root, 'AGENTS.md', [owned], { preserveRootAliases: true }), /maiusculas\/minusculas/);
|
|
194
|
-
put(root, '.pbai4devs/
|
|
194
|
+
put(root, '.pbai4devs/state/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: { [owned]: 'a'.repeat(64) }, blocks: {} }));
|
|
195
195
|
run(root, { force: true });
|
|
196
196
|
assert.ok(fs.readFileSync(join(root, 'agents.md'), 'utf8').startsWith('# Renamed human entry\n'));
|
|
197
197
|
assert.ok(!fs.readdirSync(root).includes('AGENTS.md'));
|
|
@@ -64,7 +64,7 @@ test('texto humano com formato de referência nunca comprova propriedade', t =>
|
|
|
64
64
|
test('hash registrado comprova referência legada sem marcador e diretório vazio humano permanece', t => {
|
|
65
65
|
const root = fixture(t), stale = '.claude/skills/legacy.md';
|
|
66
66
|
write(root, stale, human);
|
|
67
|
-
write(root, '.pbai4devs/
|
|
67
|
+
write(root, '.pbai4devs/state/agentdevkit.json', JSON.stringify({ schemaVersion: 1, clients: ['claude'], files: { [stale]: hash(human) }, blocks: {} }));
|
|
68
68
|
fs.mkdirSync(join(root, '.claude/skills/user-empty'), { recursive: true });
|
|
69
69
|
run(root);
|
|
70
70
|
assert.equal(fs.existsSync(join(root, stale)), false);
|
|
@@ -38,7 +38,7 @@ test('sync complementa Markdown humano, preserva CRLF/metadados e acompanha só
|
|
|
38
38
|
assert.equal(fs.statSync(join(root, path)).mode & 0o777, mode);
|
|
39
39
|
assert.equal(result.managedBlocks[path], managedBlockDigest(actual));
|
|
40
40
|
assert.ok(!result.ownedOutputs.includes(path));
|
|
41
|
-
const state = JSON.parse(read(root, '.pbai4devs/
|
|
41
|
+
const state = JSON.parse(read(root, '.pbai4devs/state/derived.json'));
|
|
42
42
|
assert.equal(state.blocks[path], managedBlockDigest(actual));
|
|
43
43
|
assert.equal(state.files[path], undefined);
|
|
44
44
|
assert.equal(run(root, { check: true }).exitCode, 0);
|
|
@@ -84,7 +84,7 @@ test('remover recurso ou cliente retira somente bloco próprio de Markdown misto
|
|
|
84
84
|
run(root);
|
|
85
85
|
assert.equal(read(root, path), outside);
|
|
86
86
|
assert.ok(read(root, path).startsWith(human));
|
|
87
|
-
assert.equal(JSON.parse(read(root, '.pbai4devs/
|
|
87
|
+
assert.equal(JSON.parse(read(root, '.pbai4devs/state/derived.json')).blocks?.[path], undefined);
|
|
88
88
|
assert.equal(run(root, { check: true }).exitCode, 0);
|
|
89
89
|
}
|
|
90
90
|
});
|
|
@@ -130,10 +130,10 @@ test('retirada standalone registra tombstone para bloco do estado instalado', t
|
|
|
130
130
|
const root = fixture(t), path = '.claude/skills/demo/SKILL.md';
|
|
131
131
|
put(root, path, '# Human\n'); run(root);
|
|
132
132
|
const block = managedBlockDigest(read(root, path));
|
|
133
|
-
put(root, '.pbai4devs/
|
|
133
|
+
put(root, '.pbai4devs/state/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: {}, blocks: { [path]: block } }));
|
|
134
134
|
fs.rmSync(join(root, '.pbai4devs/skills/old/demo'), { recursive: true });
|
|
135
135
|
run(root);
|
|
136
|
-
const derived = JSON.parse(read(root, '.pbai4devs/
|
|
136
|
+
const derived = JSON.parse(read(root, '.pbai4devs/state/derived.json'));
|
|
137
137
|
assert.ok(derived.removedBlocks.includes(path));
|
|
138
138
|
assert.equal(extractManagedBlock(read(root, path)), null);
|
|
139
139
|
assert.equal(run(root, { check: true }).exitCode, 0);
|
|
@@ -143,7 +143,7 @@ test('stage pode gerar referências sem gravar estado derivado', t => {
|
|
|
143
143
|
const root = fixture(t);
|
|
144
144
|
run(root, { generateEntrypoints: false, recordDerivedState: false });
|
|
145
145
|
assert.ok(fs.existsSync(join(root, '.claude/skills/demo/SKILL.md')));
|
|
146
|
-
assert.ok(!fs.existsSync(join(root, '.pbai4devs/
|
|
146
|
+
assert.ok(!fs.existsSync(join(root, '.pbai4devs/state/derived.json')));
|
|
147
147
|
});
|
|
148
148
|
|
|
149
149
|
test('regra Cursor mantém globs e modo humanos ao receber referência', t => {
|
|
@@ -292,10 +292,10 @@ test('arquivo misto removido é recriado sem conservar propriedade do bloco ause
|
|
|
292
292
|
const root = fixture(t);
|
|
293
293
|
put(root, path, '# Humano\n'); run(root);
|
|
294
294
|
const hash = managedBlockDigest(read(root, path));
|
|
295
|
-
put(root, '.pbai4devs/
|
|
295
|
+
put(root, '.pbai4devs/state/agentdevkit.json', JSON.stringify({ schemaVersion: 1, clients: ['claude'], files: {}, blocks: { [path]: hash } }));
|
|
296
296
|
fs.rmSync(join(root, path));
|
|
297
297
|
run(root);
|
|
298
|
-
const derived = JSON.parse(read(root, '.pbai4devs/
|
|
298
|
+
const derived = JSON.parse(read(root, '.pbai4devs/state/derived.json'));
|
|
299
299
|
assert.ok(derived.removedBlocks.includes(path));
|
|
300
300
|
assert.ok(derived.files?.[path] || derived.entries?.[path]);
|
|
301
301
|
assert.equal(extractManagedBlock(read(root, path)), null);
|
|
@@ -307,10 +307,10 @@ test('renomear caixa de entrada mista transfere só a propriedade do bloco', t =
|
|
|
307
307
|
const root = fixture(t), before = '# Humano\n';
|
|
308
308
|
put(root, 'AGENTS.md', before); run(root);
|
|
309
309
|
const hash = managedBlockDigest(read(root, 'AGENTS.md'));
|
|
310
|
-
put(root, '.pbai4devs/
|
|
310
|
+
put(root, '.pbai4devs/state/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: {}, blocks: { 'AGENTS.md': hash } }));
|
|
311
311
|
fs.renameSync(join(root, 'AGENTS.md'), join(root, 'agents.md'));
|
|
312
312
|
const result = run(root);
|
|
313
|
-
const derived = JSON.parse(read(root, '.pbai4devs/
|
|
313
|
+
const derived = JSON.parse(read(root, '.pbai4devs/state/derived.json'));
|
|
314
314
|
assert.ok(read(root, 'agents.md').startsWith(before));
|
|
315
315
|
assert.ok(derived.removedBlocks.includes('AGENTS.md'));
|
|
316
316
|
assert.equal(derived.blocks['agents.md'], result.managedBlocks['agents.md']);
|