@pathbit/agentdevkit 0.0.23 → 0.0.25
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/commands/revisar-entrega/COMMAND.md +6 -0
- package/.pbai4devs/scripts/revisar-entrega/SCRIPT.mjs +73 -4
- package/.pbai4devs/scripts/revisar-entrega/tests/TEST.mjs +112 -2
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +59 -11
- package/.pbai4devs/scripts/sincronizar-agentes/tests/CATALOG_OWNERSHIP.mjs +251 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +33 -13
- package/.pbai4devs/scripts/testar-maturidade/README.md +12 -3
- package/.pbai4devs/scripts/testar-maturidade/SCRIPT.mjs +65 -25
- package/.pbai4devs/scripts/testar-maturidade/tests/TEST.mjs +150 -5
- package/.pbai4devs/skills/git/git-merge/SKILL.md +24 -1
- package/CHANGELOG.md +15 -0
- package/docs/guides/installation.md +5 -3
- package/docs/guides/resources.md +4 -1
- package/docs/guides/troubleshooting.md +8 -6
- package/package.json +1 -1
- package/src/i18n.mjs +3 -3
- package/src/install.mjs +100 -18
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
4
|
import { dirname, join } from 'node:path';
|
|
4
5
|
import childProcess, { spawnSync } from 'node:child_process';
|
|
5
6
|
import { syncBuiltinESMExports } from 'node:module';
|
|
6
7
|
import { createHash } from 'node:crypto';
|
|
7
8
|
import test from 'node:test';
|
|
8
9
|
import { projectRoot, requireNode } from '../../lib/runtime.mjs';
|
|
10
|
+
import { adapterManifest } from '../../lib/adapters.mjs';
|
|
9
11
|
import { copyAgentStructure, prepareExamples as prepare, registeredRoots, resourceTargets, SCENARIOS } from '../SCRIPT.mjs';
|
|
10
12
|
|
|
11
13
|
// As fixtures pequenas isolam a cópia; o caso source-only usa o renderizador real.
|
|
@@ -32,6 +34,23 @@ function fixture(t) {
|
|
|
32
34
|
return { root, put, outputRoot: join(root, 'examples/generated') };
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
function controlledSource(root) {
|
|
38
|
+
const source = join(root, 'controlled-source');
|
|
39
|
+
// Only four explicit resources; exclude local state and additional skills.
|
|
40
|
+
for (const path of ['.pbai4devs/skills/projeto/criar-skill/SKILL.md', '.pbai4devs/templates/gitignore/clients.json', '.pbai4devs/templates/gitignore/agents.ignore', '.pbai4devs/templates/gitignore/shared.ignore']) {
|
|
41
|
+
let current = projectRoot;
|
|
42
|
+
for (const part of path.split('/')) {
|
|
43
|
+
current = join(current, part);
|
|
44
|
+
assert.equal(lstatSync(current).isSymbolicLink(), false, `Fonte não pode ser link: ${path}`);
|
|
45
|
+
}
|
|
46
|
+
const destination = join(source, path);
|
|
47
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
48
|
+
copyFileSync(join(projectRoot, path), destination);
|
|
49
|
+
}
|
|
50
|
+
writeFileSync(join(source, '.gitignore'), 'node_modules/\n');
|
|
51
|
+
return source;
|
|
52
|
+
}
|
|
53
|
+
|
|
35
54
|
test('copia recursos e configurações, excluindo payloads, caches e segredos', t => {
|
|
36
55
|
const { root, put } = fixture(t);
|
|
37
56
|
put('.pbai4devs/tools/demo/TOOL.json', '{}'); put('.pbai4devs/tools/demo/SCRIPT.mjs', 'export {};'); put('.pbai4devs/tools/demo/README.md', '# Tool');
|
|
@@ -110,6 +129,34 @@ test('troca de ancestral por symlink antes da criação não escreve no destino
|
|
|
110
129
|
}
|
|
111
130
|
});
|
|
112
131
|
|
|
132
|
+
test('troca de saída durante escrita é detectada, sem alegar atomicidade', t => {
|
|
133
|
+
const { root, outputRoot } = fixture(t);
|
|
134
|
+
const target = join(outputRoot, 'race-output'), outside = mkdtempSync(join(root, 'race-outside-'));
|
|
135
|
+
const sentinel = join(outside, 'sentinel'); writeFileSync(sentinel, 'Não alterar.\n');
|
|
136
|
+
const linkType = process.platform === 'win32' ? 'junction' : 'dir';
|
|
137
|
+
const probe = join(root, 'race-link-probe');
|
|
138
|
+
try { symlinkSync(outside, probe, linkType); }
|
|
139
|
+
catch (error) { if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error.code)) { t.skip('Links simbólicos indisponíveis.'); return; } throw error; }
|
|
140
|
+
unlinkSync(probe);
|
|
141
|
+
const original = fs.writeFileSync; let injected = false;
|
|
142
|
+
try {
|
|
143
|
+
fs.writeFileSync = (file, ...args) => {
|
|
144
|
+
if (!injected && String(file) === join(target, '.maturity-suite.json')) {
|
|
145
|
+
renameSync(target, `${target}-moved`); symlinkSync(outside, target, linkType); injected = true;
|
|
146
|
+
}
|
|
147
|
+
return original.call(fs, file, ...args);
|
|
148
|
+
};
|
|
149
|
+
syncBuiltinESMExports();
|
|
150
|
+
assert.throws(() => prepareExamples({ sourceRoot: root, outputRoot: target, scenarios: [SCENARIOS[0]], initializeGit: false }), /link|alterado/i);
|
|
151
|
+
} finally { fs.writeFileSync = original; syncBuiltinESMExports(); }
|
|
152
|
+
assert.equal(injected, true);
|
|
153
|
+
assert.equal(readFileSync(sentinel, 'utf8'), 'Não alterar.\n');
|
|
154
|
+
// O write já em curso pode ter criado o marcador no destino externo: a
|
|
155
|
+
// verificação detecta a troca, mas não é uma operação atômica de diretório.
|
|
156
|
+
assert.equal(existsSync(join(outside, '.maturity-suite.json')), true);
|
|
157
|
+
assert.equal(existsSync(join(outside, 'REPORT.json')), false);
|
|
158
|
+
});
|
|
159
|
+
|
|
113
160
|
test('cada destino padrão é exclusivo, preserva provas anteriores e permanece ignorado', t => {
|
|
114
161
|
const { root, outputRoot } = fixture(t);
|
|
115
162
|
const git = (...args) => {
|
|
@@ -172,16 +219,16 @@ test('preparação interrompida não altera resultado anterior nem declara suces
|
|
|
172
219
|
assert.doesNotMatch(readFileSync(index, 'utf8'), /status: passed|\| passed \|/);
|
|
173
220
|
});
|
|
174
221
|
|
|
175
|
-
test('
|
|
222
|
+
test('sem estado comprovado conserva entradas humanas mesmo com template', t => {
|
|
176
223
|
const { root, put } = fixture(t);
|
|
177
224
|
const template = '---\ndescription: Entrada genérica.\n---\n\n# Agentes do projeto\n';
|
|
178
225
|
put('AGENTS.md', '# Produto do distribuidor\n\n[PRD](docs/prd.md)\n');
|
|
179
226
|
put('.pbai4devs/templates/entrypoints/AGENTS.md', template);
|
|
180
227
|
const destination = join(root, 'copy');
|
|
181
228
|
const copied = copyAgentStructure(root, destination);
|
|
182
|
-
assert.equal(readFileSync(join(destination, 'AGENTS.md'), 'utf8'),
|
|
229
|
+
assert.equal(readFileSync(join(destination, 'AGENTS.md'), 'utf8'), '# Produto do distribuidor\n\n[PRD](docs/prd.md)\n');
|
|
183
230
|
assert.equal(readFileSync(join(destination, 'CLAUDE.md'), 'utf8'), 'fixture\n');
|
|
184
|
-
assert.equal(copied.entrypoints['AGENTS.md'], '
|
|
231
|
+
assert.equal(copied.entrypoints['AGENTS.md'], 'AGENTS.md');
|
|
185
232
|
assert.equal(copied.entrypoints['CLAUDE.md'], 'CLAUDE.md');
|
|
186
233
|
assert.equal(readFileSync(join(root, 'AGENTS.md'), 'utf8'), '# Produto do distribuidor\n\n[PRD](docs/prd.md)\n');
|
|
187
234
|
});
|
|
@@ -315,7 +362,9 @@ test('checkout de fontes sem manifesto deriva clientes em memória', t => {
|
|
|
315
362
|
test('prepare gera apontamentos reais mesmo sem pastas de clientes na origem', t => {
|
|
316
363
|
const { root } = fixture(t);
|
|
317
364
|
const sourceRoot = join(root, 'source-only');
|
|
318
|
-
copyAgentStructure(
|
|
365
|
+
copyAgentStructure(controlledSource(root), sourceRoot);
|
|
366
|
+
// Model a clean source checkout regardless of ignored local developer data.
|
|
367
|
+
for (const path of ['.pbai4devs/espelhos.conf', '.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/agents.json', '.pbai4devs/rules.json', '.pbai4devs/state/agentdevkit.json', '.pbai4devs/state/derived.json', '.pbai4devs/estado/agentdevkit.json', '.pbai4devs/estado/derived.json']) rmSync(join(sourceRoot, path), { force: true });
|
|
319
368
|
const roots = registeredRoots(sourceRoot);
|
|
320
369
|
for (const client of roots) rmSync(join(sourceRoot, client), { recursive: true, force: true });
|
|
321
370
|
const [example] = prepare({ sourceRoot, scenarios: [SCENARIOS[0]], initializeGit: false });
|
|
@@ -325,6 +374,102 @@ test('prepare gera apontamentos reais mesmo sem pastas de clientes na origem', t
|
|
|
325
374
|
for (const file of ['src/cli.mjs', 'bin/agentdevkit.mjs', 'scripts/release/pipeline.mjs', 'tests/cli.test.mjs']) assert.equal(existsSync(join(example.root, file)), false, file);
|
|
326
375
|
});
|
|
327
376
|
|
|
377
|
+
for (const withState of [false, true]) test(`prepare preserva catálogos humanos na cópia e na origem (estado=${withState})`, t => {
|
|
378
|
+
const { root } = fixture(t), sourceRoot = join(root, `source-catalog-${withState}`);
|
|
379
|
+
copyAgentStructure(controlledSource(root), sourceRoot);
|
|
380
|
+
// Source checkouts do not include generated manifests; provide this fixture
|
|
381
|
+
// explicitly instead of depending on ignored files in the developer's tree.
|
|
382
|
+
writeFileSync(join(sourceRoot, '.pbai4devs/espelhos.conf'), adapterManifest(sourceRoot, { clients: ['codex'] }));
|
|
383
|
+
const state = join(sourceRoot, '.pbai4devs/state/agentdevkit.json');
|
|
384
|
+
const derived = join(sourceRoot, '.pbai4devs/state/derived.json');
|
|
385
|
+
rmSync(state, { force: true }); rmSync(derived, { force: true }); // Só cópias criadas nesta fixture.
|
|
386
|
+
if (withState) {
|
|
387
|
+
mkdirSync(dirname(state), { recursive: true });
|
|
388
|
+
writeFileSync(state, JSON.stringify({ schemaVersion: 1, version: '0.0.24', language: 'pt', clients: ['codex'], files: {}, blocks: {} }) + '\n');
|
|
389
|
+
}
|
|
390
|
+
const originals = {};
|
|
391
|
+
for (const name of ['catalog.json', 'skills.json', 'agents.json', 'rules.json']) {
|
|
392
|
+
const file = join(sourceRoot, '.pbai4devs', name), content = `{ "arquivo humano": ${JSON.stringify(name)} }\n`;
|
|
393
|
+
writeFileSync(file, content); originals[name] = content;
|
|
394
|
+
}
|
|
395
|
+
for (const path of ['.pbai4devs/.gitignore', '.codex/.gitignore']) {
|
|
396
|
+
const file = join(sourceRoot, path);
|
|
397
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
398
|
+
writeFileSync(file, '# BEGIN AGENT IGNORE\n# Alteração humana dentro do bloco\n!/nao-apagar/\n# END AGENT IGNORE\n');
|
|
399
|
+
}
|
|
400
|
+
const skill = '.pbai4devs/skills/projeto/equipe-local/SKILL.md';
|
|
401
|
+
const skillFile = join(sourceRoot, skill);
|
|
402
|
+
mkdirSync(dirname(skillFile), { recursive: true });
|
|
403
|
+
writeFileSync(skillFile, '---\nname: equipe-local\ndescription: Conteúdo próprio.\n---\n\n# Equipe local\n');
|
|
404
|
+
const sourceIgnore = readFileSync(join(sourceRoot, '.gitignore'));
|
|
405
|
+
const sourceConfig = readFileSync(join(sourceRoot, '.pbai4devs/espelhos.conf'));
|
|
406
|
+
const [example] = prepare({ sourceRoot, scenarios: [SCENARIOS[0]], initializeGit: false });
|
|
407
|
+
assert.equal(example.copied.catalogSnapshots.length, 4);
|
|
408
|
+
for (const [name, content] of Object.entries(originals)) {
|
|
409
|
+
assert.equal(readFileSync(join(sourceRoot, '.pbai4devs', name), 'utf8'), content);
|
|
410
|
+
assert.equal(readFileSync(join(example.root, '.agent-tests/testar-maturidade/original-catalogs', name), 'utf8'), content);
|
|
411
|
+
assert.doesNotMatch(readFileSync(join(example.root, '.pbai4devs', name), 'utf8'), /arquivo humano/);
|
|
412
|
+
}
|
|
413
|
+
for (const path of ['.pbai4devs/.gitignore', '.codex/.gitignore']) {
|
|
414
|
+
const original = readFileSync(join(sourceRoot, path));
|
|
415
|
+
const saved = example.copied.overriddenSnapshots.find(item => item.source === path);
|
|
416
|
+
assert.ok(saved, path);
|
|
417
|
+
assert.deepEqual(readFileSync(join(example.root, saved.snapshot)), original);
|
|
418
|
+
assert.match(original.toString('utf8'), /Alteração humana/);
|
|
419
|
+
assert.doesNotMatch(readFileSync(join(example.root, path), 'utf8'), /Alteração humana/);
|
|
420
|
+
}
|
|
421
|
+
assert.equal(readFileSync(join(example.root, skill), 'utf8'), readFileSync(skillFile, 'utf8'));
|
|
422
|
+
assert.deepEqual(readFileSync(join(sourceRoot, '.gitignore')), sourceIgnore);
|
|
423
|
+
assert.deepEqual(readFileSync(join(sourceRoot, '.pbai4devs/espelhos.conf')), sourceConfig);
|
|
424
|
+
const report = JSON.parse(readFileSync(join(dirname(example.root), 'REPORT.json'), 'utf8'));
|
|
425
|
+
assert.deepEqual(report.copies[0].catalogSnapshots, example.copied.catalogSnapshots);
|
|
426
|
+
assert.match(readFileSync(join(example.root, 'README.md'), 'utf8'), /não um teste no projeto original/);
|
|
427
|
+
assert.equal(existsSync(state), withState);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test('arquiva arquivos substituídos mesmo quando o cliente não está selecionado', t => {
|
|
431
|
+
const { root, put } = fixture(t);
|
|
432
|
+
put('.pbai4devs/espelhos.conf', 'agente|.codex|plano\n');
|
|
433
|
+
const files = ['.vscode/settings.json', '.github/workflows/legacy.yml'];
|
|
434
|
+
for (const path of files) put(path, `Conteúdo de equipe: ${path}\r\n`);
|
|
435
|
+
const [example] = prepareExamples({ sourceRoot: root, scenarios: [SCENARIOS.find(item => item.id === 'legado')], initializeGit: false });
|
|
436
|
+
for (const path of files) {
|
|
437
|
+
const saved = example.copied.overriddenSnapshots.filter(item => item.source === path);
|
|
438
|
+
assert.equal(saved.length, 1, path);
|
|
439
|
+
assert.deepEqual(readFileSync(join(example.root, saved[0].snapshot)), readFileSync(join(root, path)));
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test('arquivo de cliente não selecionado com ancestral simbólico é recusado', t => {
|
|
444
|
+
const { root, put } = fixture(t);
|
|
445
|
+
put('.pbai4devs/espelhos.conf', 'agente|.codex|plano\n');
|
|
446
|
+
const outside = mkdtempSync(join(root, 'outside-'));
|
|
447
|
+
writeFileSync(join(outside, 'settings.json'), 'Conteúdo externo.\n');
|
|
448
|
+
rmSync(join(root, '.vscode'), { recursive: true });
|
|
449
|
+
try { symlinkSync(outside, join(root, '.vscode'), 'dir'); }
|
|
450
|
+
catch (error) { if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error.code)) { t.skip('Links simbólicos indisponíveis.'); return; } throw error; }
|
|
451
|
+
assert.throws(() => prepareExamples({ sourceRoot: root, scenarios: [SCENARIOS.find(item => item.id === 'legado')], initializeGit: false }), /link/i);
|
|
452
|
+
assert.equal(readFileSync(join(outside, 'settings.json'), 'utf8'), 'Conteúdo externo.\n');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test('arquivos da fonte substituídos por fixtures têm cópias byte a byte', t => {
|
|
456
|
+
const { root, put } = fixture(t);
|
|
457
|
+
const files = ['README.md', 'Makefile', '.dockerignore', '.env.example', '.pbai4devs/state/README.md', '.pbai4devs/state/configuracao/ESTADO.md', '.vscode/settings.json', '.github/workflows/legacy.yml'];
|
|
458
|
+
const contents = Object.fromEntries(files.map(file => [file, `Conteúdo humano sintético: ${file}\r\n`]));
|
|
459
|
+
for (const [file, content] of Object.entries(contents)) put(file, content);
|
|
460
|
+
const cases = [SCENARIOS.find(item => item.id === 'app-configurada'), SCENARIOS.find(item => item.id === 'legado')];
|
|
461
|
+
const examples = prepareExamples({ sourceRoot: root, scenarios: cases, initializeGit: false });
|
|
462
|
+
for (const example of examples) {
|
|
463
|
+
for (const { source, snapshot } of example.copied.overriddenSnapshots) {
|
|
464
|
+
assert.equal(readFileSync(join(example.root, snapshot), 'utf8'), contents[source], source);
|
|
465
|
+
assert.equal(readFileSync(join(root, source), 'utf8'), contents[source], source);
|
|
466
|
+
}
|
|
467
|
+
for (const path of ['README.md', 'Makefile', '.pbai4devs/state/README.md', '.pbai4devs/state/configuracao/ESTADO.md']) assert.ok(example.copied.overriddenSnapshots.some(item => item.source === path), path);
|
|
468
|
+
const expected = example.id === 'legado' ? ['.vscode/settings.json', '.github/workflows/legacy.yml'] : ['.dockerignore', '.env.example'];
|
|
469
|
+
for (const path of expected) assert.ok(example.copied.overriddenSnapshots.some(item => item.source === path), path);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
328
473
|
test('saída não pode sobrepor recursos de origem nem escapar das áreas de teste', t => {
|
|
329
474
|
const { root } = fixture(t);
|
|
330
475
|
const original = readFileSync(join(root, '.pbai4devs/espelhos.conf'), 'utf8');
|
|
@@ -8,6 +8,29 @@ description: Integra branches com trabalho concluído e verificado.
|
|
|
8
8
|
Confira origem, destino, histórico, identidades, mensagens, alterações locais e
|
|
9
9
|
verificações; integre somente tarefas completas conforme AGENTS.md. Se os
|
|
10
10
|
commits a integrar identificarem agentes como autores ou coautores, informe e
|
|
11
|
-
não os integre.
|
|
11
|
+
não os integre.
|
|
12
|
+
|
|
13
|
+
Quando houver implementação ou gate exigido, na origem execute isoladamente:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
node .pbai4devs/scripts/revisar-entrega/SCRIPT.mjs check-merge \
|
|
17
|
+
--destino refs/heads/<branch> <revisao.json> [outros...]
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
O check exige fast-forward, cobertura de todo o diff e snapshots correspondentes
|
|
21
|
+
ao commit de origem; qualquer falha bloqueia a integração. Registre os hashes
|
|
22
|
+
de origem e destino retornados e confirme ambos imediatamente antes do merge,
|
|
23
|
+
com comandos que interrompam a execução em qualquer falha; não mascare o status.
|
|
24
|
+
Confirme também que `git symbolic-ref HEAD` é a ref de destino e que o HEAD
|
|
25
|
+
corresponde ao hash de destino validado. Se alguma conferência falhar,
|
|
26
|
+
interrompa; se os hashes mudarem, refaça a revisão e o check. Integre com
|
|
27
|
+
`git merge --ff-only <hash-da-origem-validada>`. Essas verificações não são
|
|
28
|
+
atômicas. O check recusa links, submódulos, arquivos executáveis e saídas Git
|
|
29
|
+
maiores que 64 MiB; informe bloqueios sem contorná-los. Preserve trabalho fora
|
|
30
|
+
do escopo.
|
|
31
|
+
Se o resultado integrado divergir do conteúdo revisado, faça nova revisão e
|
|
32
|
+
verificações antes de concluir. Sem implementação, siga os critérios aplicáveis.
|
|
33
|
+
|
|
34
|
+
Se gerar commit de merge, confira as identidades efetivas e siga
|
|
12
35
|
`.pbai4devs/rules/git/RULE.md` para identidade, mensagem e assinatura. Preserve
|
|
13
36
|
alterações, resolva conflitos no escopo autorizado e respeite proteções da branch.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ description: Alterações verificáveis por versão do AgentDevKit.
|
|
|
4
4
|
|
|
5
5
|
# Changelog
|
|
6
6
|
|
|
7
|
+
## 0.0.25 — 2026-11-14
|
|
8
|
+
|
|
9
|
+
- Verifica a cobertura e o conteúdo revisado antes de um merge fast-forward;
|
|
10
|
+
bloqueia situações que o gate não consegue comprovar.
|
|
11
|
+
- Esclarece a validação na origem e a interrupção do merge quando o gate falha.
|
|
12
|
+
- Amplia as provas de preservação em projetos existentes e explica os limites
|
|
13
|
+
do campo `preserved`. Os projetos de exemplo gerados continuam locais.
|
|
14
|
+
|
|
15
|
+
## 0.0.24 — 2026-11-14
|
|
16
|
+
|
|
17
|
+
- Preserva catálogos, configuração e blocos existentes sem comprovação de
|
|
18
|
+
propriedade, inclusive ao usar `--force` ou sincronização direta.
|
|
19
|
+
- Mantém o estado coerente quando a sincronização atualiza saídas gerenciadas.
|
|
20
|
+
- Acrescenta testes de colisão, arquivos idênticos e preservação do índice Git.
|
|
21
|
+
|
|
7
22
|
## 0.0.23 — 2026-11-13
|
|
8
23
|
|
|
9
24
|
- Corrige a resolução de destinos inexistentes diretamente sob a raiz.
|
|
@@ -183,9 +183,11 @@ Arquivos mistos não passam a pertencer ao kit. Sua visibilidade para o Git
|
|
|
183
183
|
também depende da regra aplicada à pasta do cliente.
|
|
184
184
|
|
|
185
185
|
Links simbólicos preexistentes são preservados sem atravessá-los. O campo
|
|
186
|
-
`preserved`
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
`preserved` lista caminhos próprios encontrados no planejamento ou em colisões,
|
|
187
|
+
sem inventariar rotas não examinadas. A ausência de um caminho não prova que ele
|
|
188
|
+
foi verificado ou preservado. Um formato não suportado ou um bloco alterado é
|
|
189
|
+
informado; `--force` não permite apagar instruções humanas para reconstruir um
|
|
190
|
+
apontamento.
|
|
189
191
|
|
|
190
192
|
Essa preservação não autoriza trocar arquivos gerenciados por links. Links
|
|
191
193
|
em caminhos já pertencentes ao kit, no hub, no estado ou em ferramentas
|
package/docs/guides/resources.md
CHANGED
|
@@ -158,7 +158,10 @@ Não apague pastas inteiras para contornar um conflito.
|
|
|
158
158
|
|
|
159
159
|
Recursos nativos humanos são preservados, inclusive com `--force` e nomes
|
|
160
160
|
iguais aos do kit. Links humanos são tratados como caminhos opacos; o gerador
|
|
161
|
-
não os atravessa.
|
|
161
|
+
não os atravessa. No JSON, `preserved` lista caminhos humanos encontrados no
|
|
162
|
+
planejamento ou em colisões; não inventaria rotas não examinadas, como um
|
|
163
|
+
fallback não utilizado. A ausência de um caminho não prova que ele foi
|
|
164
|
+
verificado ou preservado.
|
|
162
165
|
|
|
163
166
|
Não edite ou versione `catalog.json`, `skills.json`, `rules.json` ou
|
|
164
167
|
`agents.json`: são derivados locais. Configurações nativas e documentos
|
|
@@ -56,9 +56,10 @@ Compare cada `conflicts` com o que deseja manter. Edite a origem em
|
|
|
56
56
|
recurso. Use `sync` para atualizar suas derivações. `update` reaplica os
|
|
57
57
|
recursos do pacote invocado e pode conflitar com essa personalização.
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
`--force` não substitui arquivos próprios nem blocos alterados. Resolva o
|
|
60
|
+
conflito preservando o conteúdo que deseja manter e confira novamente o plano.
|
|
61
|
+
Arquivos gerenciados afetados recebem backup antes da escrita, inclusive em
|
|
62
|
+
operações normais. O campo `backup` informa uma pasta relativa ao projeto:
|
|
62
63
|
|
|
63
64
|
```text
|
|
64
65
|
.agent-tests/agentdevkit/backups/<identificador>/
|
|
@@ -105,9 +106,10 @@ ou caminhos já gerenciados em links.
|
|
|
105
106
|
Destinos dentro de `.git`, `node_modules` ou `.pbai4devs` são recusados,
|
|
106
107
|
inclusive com variações de maiúsculas/minúsculas.
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
No script `sincronizar-agentes`, `--force` repara conflitos gerenciados com
|
|
110
|
+
backup. A CLI bloqueia conflitos de propriedade mesmo com `--force`. Recursos
|
|
111
|
+
nativos humanos e links preservados continuam intactos. Para manter a instalação
|
|
112
|
+
e sua seleção de clientes, prefira a CLI.
|
|
111
113
|
|
|
112
114
|
## Download, plataforma ou ferramenta falhando
|
|
113
115
|
|
package/package.json
CHANGED
package/src/i18n.mjs
CHANGED
|
@@ -12,7 +12,7 @@ const messages = {
|
|
|
12
12
|
preserved: 'Caminhos existentes preservados', preservedDetails: 'Use --json para consultar os caminhos e motivos.',
|
|
13
13
|
help: 'AgentDevKit: init [diretório], update, doctor, sync [--check], lang [pt|en|es], agents [lista], next, tools <ação>.\nOpções: --target <diretório>, --lang <idioma>, --agents <lista>, --yes, --dry-run, --force, --json.\nAliases: --clients e clients.',
|
|
14
14
|
clientsCurrent: 'Agentes selecionados', clientsAvailable: 'Agentes disponíveis', clientsSelect: 'Agentes (IDs separados por vírgula ou all) [all]: ', agents: 'Seleção de agentes atualizada.',
|
|
15
|
-
next: 'Próximo passo', init: 'Estrutura instalada.', update: 'Estrutura atualizada.', sync: 'Apontamentos sincronizados.', lang: 'Idioma configurado.', unchanged: 'Estrutura já atualizada.', plan: 'Plano calculado; nenhum arquivo alterado.', conflict: 'Há arquivos próprios ou alterados em conflito. Revise o plano; --force
|
|
15
|
+
next: 'Próximo passo', init: 'Estrutura instalada.', update: 'Estrutura atualizada.', sync: 'Apontamentos sincronizados.', lang: 'Idioma configurado.', unchanged: 'Estrutura já atualizada.', plan: 'Plano calculado; nenhum arquivo alterado.', conflict: 'Há arquivos próprios ou alterados em conflito. Revise o plano e resolva os conflitos; --force não substitui conteúdo próprio.',
|
|
16
16
|
healthy: 'Estrutura verificada.', unhealthy: 'A estrutura precisa de ajustes.', missing: 'AgentDevKit ainda não foi inicializado neste diretório.', error: 'Não foi possível concluir a operação.', invalid: 'Argumentos inválidos.', language: 'Idioma atual', version: 'Versão', select: 'Idioma / Language / Idioma [pt/en/es] (pt): ',
|
|
17
17
|
project: 'Projeto', readme: 'AgentDevKit instalado. Leia AGENTS.md e siga as etapas de produto, design e implementação.', state: 'Estado local do AgentDevKit e configuração compartilhada do projeto.',
|
|
18
18
|
agentLanguage: 'Responda em português, salvo quando o usuário solicitar outro idioma.', inspect: 'Verifique a instalação', workflow: 'Consulte a próxima etapa', install: 'Inicialize este projeto', repair: 'Confira a sincronização', review: 'Revise os arquivos em conflito', tools: 'Resultado da ferramenta',
|
|
@@ -23,7 +23,7 @@ const messages = {
|
|
|
23
23
|
preserved: 'Existing paths preserved', preservedDetails: 'Use --json to inspect paths and reasons.',
|
|
24
24
|
help: 'AgentDevKit: init [directory], update, doctor, sync [--check], lang [pt|en|es], agents [list], next, tools <action>.\nOptions: --target <directory>, --lang <language>, --agents <list>, --yes, --dry-run, --force, --json.\nAliases: --clients and clients.',
|
|
25
25
|
clientsCurrent: 'Selected agents', clientsAvailable: 'Available agents', clientsSelect: 'Agents (comma-separated IDs or all) [all]: ', agents: 'Agent selection updated.',
|
|
26
|
-
next: 'Next step', init: 'Structure installed.', update: 'Structure updated.', sync: 'Pointers synchronized.', lang: 'Language configured.', unchanged: 'Structure is already current.', plan: 'Plan calculated; no files changed.', conflict: 'User files or edits conflict with this plan. Review
|
|
26
|
+
next: 'Next step', init: 'Structure installed.', update: 'Structure updated.', sync: 'Pointers synchronized.', lang: 'Language configured.', unchanged: 'Structure is already current.', plan: 'Plan calculated; no files changed.', conflict: 'User files or edits conflict with this plan. Review and resolve the conflicts; --force does not replace user content.',
|
|
27
27
|
healthy: 'Structure verified.', unhealthy: 'The structure needs attention.', missing: 'AgentDevKit has not been initialized in this directory.', error: 'The operation could not be completed.', invalid: 'Invalid arguments.', language: 'Current language', version: 'Version', select: 'Idioma / Language / Idioma [pt/en/es] (pt): ',
|
|
28
28
|
project: 'Project', readme: 'AgentDevKit installed. Read AGENTS.md and follow the product, design and implementation stages.', state: 'AgentDevKit state and shared project configuration.',
|
|
29
29
|
agentLanguage: 'Respond in English unless the user requests another language.', inspect: 'Check the installation', workflow: 'Review the next stage', install: 'Initialize this project', repair: 'Check synchronization', review: 'Review conflicting files', tools: 'Tool result',
|
|
@@ -34,7 +34,7 @@ const messages = {
|
|
|
34
34
|
preserved: 'Rutas existentes preservadas', preservedDetails: 'Use --json para consultar las rutas y los motivos.',
|
|
35
35
|
help: 'AgentDevKit: init [directorio], update, doctor, sync [--check], lang [pt|en|es], agents [lista], next, tools <acción>.\nOpciones: --target <directorio>, --lang <idioma>, --agents <lista>, --yes, --dry-run, --force, --json.\nAlias: --clients y clients.',
|
|
36
36
|
clientsCurrent: 'Agentes seleccionados', clientsAvailable: 'Agentes disponibles', clientsSelect: 'Agentes (IDs separados por coma o all) [all]: ', agents: 'Selección de agentes actualizada.',
|
|
37
|
-
next: 'Próximo paso', init: 'Estructura instalada.', update: 'Estructura actualizada.', sync: 'Referencias sincronizadas.', lang: 'Idioma configurado.', unchanged: 'La estructura ya está actualizada.', plan: 'Plan calculado; ningún archivo modificado.', conflict: 'Hay archivos propios o cambios en conflicto. Revise
|
|
37
|
+
next: 'Próximo paso', init: 'Estructura instalada.', update: 'Estructura actualizada.', sync: 'Referencias sincronizadas.', lang: 'Idioma configurado.', unchanged: 'La estructura ya está actualizada.', plan: 'Plan calculado; ningún archivo modificado.', conflict: 'Hay archivos propios o cambios en conflicto. Revise y resuelva los conflictos; --force no reemplaza contenido propio.',
|
|
38
38
|
healthy: 'Estructura verificada.', unhealthy: 'La estructura necesita ajustes.', missing: 'AgentDevKit no se ha inicializado en este directorio.', error: 'No se pudo completar la operación.', invalid: 'Argumentos inválidos.', language: 'Idioma actual', version: 'Versión', select: 'Idioma / Language / Idioma [pt/en/es] (pt): ',
|
|
39
39
|
project: 'Proyecto', readme: 'AgentDevKit instalado. Lea AGENTS.md y siga las etapas de producto, diseño e implementación.', state: 'Estado de AgentDevKit y configuración compartida del proyecto.',
|
|
40
40
|
agentLanguage: 'Responde en español salvo que el usuario solicite otro idioma.', inspect: 'Compruebe la instalación', workflow: 'Consulte la siguiente etapa', install: 'Inicialice este proyecto', repair: 'Compruebe la sincronización', review: 'Revise los archivos en conflicto', tools: 'Resultado de la herramienta',
|
package/src/install.mjs
CHANGED
|
@@ -457,7 +457,14 @@ function replaceJsonMember(bytes, member, value) {
|
|
|
457
457
|
fail('E_STATE', 'Invalid JSON member separator.');
|
|
458
458
|
}
|
|
459
459
|
const matches = properties.filter(item => item.key === member);
|
|
460
|
-
if (matches.length
|
|
460
|
+
if (matches.length > 1) fail('E_STATE', 'Ambiguous state member.');
|
|
461
|
+
if (!matches.length) {
|
|
462
|
+
if (value === undefined) return bytes;
|
|
463
|
+
const insertion = `${properties.length ? ',' : ''}\n ${JSON.stringify(member)}: ${JSON.stringify(value, null, 2)}\n`;
|
|
464
|
+
const next = text.slice(0, i) + insertion + text.slice(i);
|
|
465
|
+
try { JSON.parse(next); } catch { fail('E_STATE', 'Unable to preserve state safely.'); }
|
|
466
|
+
return Buffer.from(next);
|
|
467
|
+
}
|
|
461
468
|
const current = matches[0], index = properties.indexOf(current);
|
|
462
469
|
let next;
|
|
463
470
|
if (value !== undefined) next = text.slice(0, current.valueStart) + JSON.stringify(value, null, 2) + text.slice(current.end);
|
|
@@ -468,29 +475,44 @@ function replaceJsonMember(bytes, member, value) {
|
|
|
468
475
|
return Buffer.from(next);
|
|
469
476
|
}
|
|
470
477
|
|
|
478
|
+
function reconcileStateBytes(original, previous, next) {
|
|
479
|
+
if (!original) return Buffer.from(`${JSON.stringify(next, null, 2)}\n`);
|
|
480
|
+
const canonical = value => value && typeof value === 'object' && !Array.isArray(value)
|
|
481
|
+
? Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])]))
|
|
482
|
+
: Array.isArray(value) ? value.map(canonical) : value;
|
|
483
|
+
let result = original;
|
|
484
|
+
for (const key of ['schemaVersion', 'version', 'language', 'clients', 'files', 'blocks']) {
|
|
485
|
+
if (JSON.stringify(canonical(previous?.[key])) !== JSON.stringify(canonical(next[key]))) result = replaceJsonMember(result, key, next[key]);
|
|
486
|
+
}
|
|
487
|
+
return result;
|
|
488
|
+
}
|
|
489
|
+
|
|
471
490
|
function readDerivedState(target) {
|
|
472
|
-
let entries = {}, outputs = {}, blocks = {}, files = {}, removedBlocks = [], removedFiles = {};
|
|
491
|
+
let entries = {}, outputs = {}, blocks = {}, files = {}, catalogs = {}, config = null, removedBlocks = [], removedFiles = {};
|
|
473
492
|
const derived = read(target, actualStatePath(target).replace(/agentdevkit\.json$/, 'derived.json'));
|
|
474
493
|
if (derived) {
|
|
475
494
|
try {
|
|
476
495
|
const value = JSON.parse(derived);
|
|
477
496
|
if (value.schemaVersion !== 1 || !value.entries || typeof value.entries !== 'object' || Array.isArray(value.entries)
|
|
478
|
-
|| ['outputs', 'blocks', 'files', 'removedFiles'].some(key => value[key] !== undefined && (!value[key] || typeof value[key] !== 'object' || Array.isArray(value[key])))) fail('E_STATE', 'Invalid derived entrypoint state.');
|
|
479
|
-
entries = value.entries; outputs = value.outputs ?? {}; blocks = value.blocks ?? {}; files = value.files ?? {}; removedFiles = value.removedFiles ?? {};
|
|
497
|
+
|| ['outputs', 'blocks', 'files', 'catalogs', 'removedFiles'].some(key => value[key] !== undefined && (!value[key] || typeof value[key] !== 'object' || Array.isArray(value[key])))) fail('E_STATE', 'Invalid derived entrypoint state.');
|
|
498
|
+
entries = value.entries; outputs = value.outputs ?? {}; blocks = value.blocks ?? {}; files = value.files ?? {}; catalogs = value.catalogs ?? {}; removedFiles = value.removedFiles ?? {};
|
|
499
|
+
if (value.config !== undefined && (typeof value.config !== 'string' || !/^[a-f0-9]{64}$/.test(value.config))) fail('E_STATE', 'Invalid derived configuration state.');
|
|
500
|
+
config = value.config ?? null;
|
|
480
501
|
removedBlocks = value.removedBlocks ?? [];
|
|
481
502
|
if (!Array.isArray(removedBlocks) || removedBlocks.some(path => typeof path !== 'string' || !nativeManagedText(path))) fail('E_STATE', 'Invalid removed block state.');
|
|
482
503
|
for (const [path, digest] of Object.entries(entries)) if (!canonicalEntrypoint(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived entrypoint state.');
|
|
483
504
|
for (const [path, digest] of Object.entries(outputs)) if (!allowedManaged(path) || !/^\.[^/]+\/(?:skills|agents|rules)\.json$/.test(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived output state.');
|
|
484
|
-
for (const [path, digest] of Object.entries(blocks)) if (!(nativeManagedText(path) || adapterRoots.some(root => path === `${root}/.gitignore`)) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived block state.');
|
|
505
|
+
for (const [path, digest] of Object.entries(blocks)) if (!(nativeManagedText(path) || path === '.pbai4devs/.gitignore' || adapterRoots.some(root => path === `${root}/.gitignore`)) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived block state.');
|
|
485
506
|
for (const [path, digest] of Object.entries(files)) if (!allowedManaged(path) || !/\.(?:md|mdc|toml)$/.test(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived file state.');
|
|
507
|
+
for (const [path, digest] of Object.entries(catalogs)) if (!/^\.pbai4devs\/(?:catalog|skills|agents|rules)\.json$/.test(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived catalogue state.');
|
|
486
508
|
for (const [path, digest] of Object.entries(removedFiles)) if (canonicalEntrypoint(path) || !(nativeMarkdown(path) || nativeToml(path)) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid removed file state.');
|
|
487
509
|
const installed = loadState(target);
|
|
488
510
|
if (Object.entries(removedFiles).some(([path, digest]) => installed?.files?.[path] && installed.files[path] !== digest)) fail('E_STATE', 'Removed file state differs from installation.');
|
|
489
511
|
}
|
|
490
512
|
catch { fail('E_STATE', 'Invalid derived entrypoint state.'); }
|
|
491
|
-
for (const path of [...Object.keys(entries), ...Object.keys(outputs), ...Object.keys(blocks), ...Object.keys(files), ...Object.keys(removedFiles), ...removedBlocks]) safePath(target, path);
|
|
513
|
+
for (const path of [...Object.keys(entries), ...Object.keys(outputs), ...Object.keys(blocks), ...Object.keys(files), ...Object.keys(catalogs), ...Object.keys(removedFiles), ...removedBlocks]) safePath(target, path);
|
|
492
514
|
}
|
|
493
|
-
return { entries, outputs, blocks, files, removedBlocks, removedFiles };
|
|
515
|
+
return { entries, outputs, blocks, files, catalogs, config, removedBlocks, removedFiles };
|
|
494
516
|
}
|
|
495
517
|
|
|
496
518
|
export function planInstallation({ target, language = 'pt', mode = 'init', force = false, clients }) {
|
|
@@ -511,11 +533,12 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
511
533
|
if (original === null ? content !== null : !content?.equals(original)) fail('E_CHANGED', `File changed while planning: ${path}`);
|
|
512
534
|
} else nativeInputs.set(path, content);
|
|
513
535
|
};
|
|
514
|
-
const { entries: derivedEntries, outputs: derivedOutputs, blocks: derivedBlocks, files: derivedFiles, removedBlocks, removedFiles } = readDerivedState(target);
|
|
536
|
+
const { entries: derivedEntries, outputs: derivedOutputs, blocks: derivedBlocks, files: derivedFiles, catalogs: derivedCatalogs, config: derivedConfig, removedBlocks, removedFiles } = readDerivedState(target);
|
|
537
|
+
const derivedConfigFiles = derivedConfig ? { '.pbai4devs/espelhos.conf': derivedConfig } : {};
|
|
515
538
|
if (previous) previous = { ...previous, files: Object.fromEntries(Object.entries(previous.files).filter(([path, digest]) => removedFiles[path] !== digest)) };
|
|
516
539
|
const previousBlocks = Object.fromEntries(Object.entries(previous?.blocks || {}).filter(([path]) => !removedBlocks.includes(path)));
|
|
517
540
|
const knownBlocks = { ...previousBlocks, ...derivedBlocks };
|
|
518
|
-
const ownedPaths = [...Object.keys(previous?.files || {}), ...Object.keys(previousBlocks), ...Object.keys(derivedEntries), ...Object.keys(derivedOutputs), ...Object.keys(derivedBlocks), ...Object.keys(derivedFiles)];
|
|
541
|
+
const ownedPaths = [...Object.keys(previous?.files || {}), ...Object.keys(previousBlocks), ...Object.keys(derivedEntries), ...Object.keys(derivedOutputs), ...Object.keys(derivedBlocks), ...Object.keys(derivedFiles), ...Object.keys(derivedCatalogs), ...Object.keys(derivedConfigFiles)];
|
|
519
542
|
const entryPaths = new Map(entryNames.map(name => [name, resolveEntrypoint(target, name)]));
|
|
520
543
|
const blockHash = path => knownBlocks[path]
|
|
521
544
|
?? (canonicalEntrypoint(path) ? Object.entries(knownBlocks).find(([candidate]) => canonicalEntrypoint(candidate) === canonicalEntrypoint(path))?.[1] : undefined);
|
|
@@ -597,7 +620,17 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
597
620
|
const ownIgnore = read(target, '.pbai4devs/.gitignore');
|
|
598
621
|
if (ownIgnore) write(stage, '.pbai4devs/.gitignore', ownIgnore);
|
|
599
622
|
writeAdapterManifest(stage, { clients: selected });
|
|
600
|
-
|
|
623
|
+
// Supply the already-validated ownership records to the private stage so
|
|
624
|
+
// the synchronizer can distinguish an owned ignore block from a user's.
|
|
625
|
+
// These temporary records never replace or bypass checks on the target.
|
|
626
|
+
const stagingState = '.pbai4devs/state/agentdevkit.json', stagingDerived = '.pbai4devs/state/derived.json';
|
|
627
|
+
if (previous) write(stage, stagingState, JSON.stringify(previous));
|
|
628
|
+
if (Object.keys(derivedEntries).length || Object.keys(derivedOutputs).length || Object.keys(derivedBlocks).length || Object.keys(derivedFiles).length || Object.keys(derivedCatalogs).length || derivedConfig || removedBlocks.length || Object.keys(removedFiles).length) {
|
|
629
|
+
write(stage, stagingDerived, JSON.stringify({ schemaVersion: 1, entries: derivedEntries, outputs: derivedOutputs, blocks: derivedBlocks, files: derivedFiles, catalogs: derivedCatalogs, ...(derivedConfig ? { config: derivedConfig } : {}), removedBlocks, removedFiles }));
|
|
630
|
+
}
|
|
631
|
+
let synchronized;
|
|
632
|
+
try { synchronized = synchronize({ root: stage, force: true, localIgnores: false, generateEntrypoints: false, recordDerivedState: false, outputBlocked, output() {} }); }
|
|
633
|
+
finally { rmSync(join(stage, stagingState), { force: true }); rmSync(join(stage, stagingDerived), { force: true }); }
|
|
601
634
|
for (const canonical of active.entrypoints) {
|
|
602
635
|
const name = entryPaths.get(canonical);
|
|
603
636
|
if (preserveNative(name)) continue;
|
|
@@ -673,14 +706,14 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
673
706
|
// A standalone sync records its proven outputs separately. During a later
|
|
674
707
|
// check/sync, compare that overlay before declaring the installation state
|
|
675
708
|
// stale; update still consolidates it into the main state transactionally.
|
|
676
|
-
let stateContent =
|
|
709
|
+
let stateContent = reconcileStateBytes(installedState ? read(target, actualStatePath(target)) : null, installedState, manifest);
|
|
677
710
|
if (mode === 'sync' && installedState && !changingClients) {
|
|
678
711
|
const files = { ...installedState.files }, blocks = { ...installedState.blocks };
|
|
679
712
|
for (const [path, digest] of Object.entries(removedFiles)) {
|
|
680
713
|
if (files[path] === digest && !stat(safePath(target, path))) delete files[path];
|
|
681
714
|
}
|
|
682
715
|
for (const path of removedBlocks) delete blocks[path];
|
|
683
|
-
for (const [path, digest] of Object.entries({ ...derivedFiles, ...derivedOutputs, ...derivedEntries })) { files[path] = digest; delete blocks[path]; }
|
|
716
|
+
for (const [path, digest] of Object.entries({ ...derivedFiles, ...derivedOutputs, ...derivedEntries, ...derivedCatalogs, ...derivedConfigFiles })) { files[path] = digest; delete blocks[path]; }
|
|
684
717
|
for (const [path, digest] of Object.entries(derivedBlocks)) { blocks[path] = digest; delete files[path]; }
|
|
685
718
|
const same = (a, b) => {
|
|
686
719
|
const keys = Object.keys(a).sort();
|
|
@@ -692,23 +725,70 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
692
725
|
desired.set(statePath, stateContent);
|
|
693
726
|
const derivedPath = actualStatePath(target).replace(/agentdevkit\.json$/, 'derived.json');
|
|
694
727
|
const derivedBytes = !migration ? read(target, derivedPath) : null;
|
|
695
|
-
|
|
728
|
+
let nextDerivedBytes = derivedBytes;
|
|
729
|
+
if (nextDerivedBytes && Object.keys(removedFiles).length) {
|
|
696
730
|
const finalState = JSON.parse(stateContent);
|
|
697
731
|
const remaining = Object.fromEntries(Object.entries(removedFiles).filter(([path, digest]) =>
|
|
698
732
|
finalState.files[path] === digest && !stat(safePath(target, path))));
|
|
699
733
|
if (Object.keys(remaining).length !== Object.keys(removedFiles).length) {
|
|
700
734
|
observeNative(derivedPath, derivedBytes);
|
|
701
|
-
|
|
735
|
+
nextDerivedBytes = replaceJsonMember(nextDerivedBytes, 'removedFiles', Object.keys(remaining).length ? remaining : undefined);
|
|
702
736
|
}
|
|
703
737
|
}
|
|
738
|
+
if (nextDerivedBytes && Object.keys(derivedCatalogs).length) {
|
|
739
|
+
const finalState = JSON.parse(stateContent);
|
|
740
|
+
const remaining = Object.fromEntries(Object.entries(derivedCatalogs).filter(([path]) => {
|
|
741
|
+
const content = desired.get(path);
|
|
742
|
+
return !content || finalState.files[path] !== hash(content);
|
|
743
|
+
}));
|
|
744
|
+
if (Object.keys(remaining).length !== Object.keys(derivedCatalogs).length) {
|
|
745
|
+
observeNative(derivedPath, derivedBytes);
|
|
746
|
+
nextDerivedBytes = replaceJsonMember(nextDerivedBytes, 'catalogs', Object.keys(remaining).length ? remaining : undefined);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (nextDerivedBytes && Object.keys(derivedBlocks).length) {
|
|
750
|
+
const finalState = JSON.parse(stateContent);
|
|
751
|
+
const remaining = Object.fromEntries(Object.entries(derivedBlocks).filter(([path, digest]) => {
|
|
752
|
+
const content = desired.get(path);
|
|
753
|
+
// A deselected generated block is removed later in this transaction.
|
|
754
|
+
// Drop its overlay only if its current bytes are still proven; edited
|
|
755
|
+
// blocks remain conflicts and no metadata is committed on failure.
|
|
756
|
+
if (!content && !canonicalEntrypoint(path) && !finalState.blocks[path] && !finalState.files[path]) {
|
|
757
|
+
const current = read(target, path);
|
|
758
|
+
return Boolean(current && blockDigest(path, current) !== digest);
|
|
759
|
+
}
|
|
760
|
+
return !content || finalState.blocks[path] !== blockDigest(path, content);
|
|
761
|
+
}));
|
|
762
|
+
if (Object.keys(remaining).length !== Object.keys(derivedBlocks).length) {
|
|
763
|
+
observeNative(derivedPath, derivedBytes);
|
|
764
|
+
nextDerivedBytes = replaceJsonMember(nextDerivedBytes, 'blocks', Object.keys(remaining).length ? remaining : undefined);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (nextDerivedBytes && derivedConfig) {
|
|
768
|
+
const finalState = JSON.parse(stateContent), path = '.pbai4devs/espelhos.conf', content = desired.get(path);
|
|
769
|
+
if (content && finalState.files[path] === hash(content)) {
|
|
770
|
+
observeNative(derivedPath, derivedBytes);
|
|
771
|
+
nextDerivedBytes = replaceJsonMember(nextDerivedBytes, 'config', undefined);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (nextDerivedBytes && !nextDerivedBytes.equals(derivedBytes)) desired.set(derivedPath, nextDerivedBytes);
|
|
704
775
|
const changes = [], conflicts = [];
|
|
705
776
|
const aliasedEntrypoint = path => canonicalEntrypoint(path) && entryPaths.get(canonicalEntrypoint(path)) !== path;
|
|
777
|
+
const generatedInternal = new Set(['.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/agents.json', '.pbai4devs/rules.json', '.pbai4devs/espelhos.conf']);
|
|
706
778
|
for (const [path, content] of desired) {
|
|
707
779
|
const current = read(target, path);
|
|
708
780
|
if (nativeInputs.has(path) && (nativeInputs.get(path) === null ? current !== null : !current?.equals(nativeInputs.get(path)))) fail('E_CHANGED', `File changed while planning: ${path}`);
|
|
709
|
-
|
|
781
|
+
const wholeFileOwned = current && [previous?.files[path], derivedOutputs[path], derivedEntries[path], derivedFiles[path], derivedCatalogs[path], derivedConfigFiles[path]].some(digest => digest && digest === hash(current));
|
|
782
|
+
const existingBlock = current && (nativeManagedText(path) || canonicalEntrypoint(path) || path.endsWith('/.gitignore')) ? blockDigest(path, current) : null;
|
|
783
|
+
if (existingBlock && !wholeFileOwned && knownBlocks[path] !== existingBlock) conflicts.push(path);
|
|
784
|
+
if (current?.equals(content)) {
|
|
785
|
+
// Identical bytes do not prove ownership of a pre-existing catalogue.
|
|
786
|
+
// Refuse adoption: a later sync could otherwise overwrite user data.
|
|
787
|
+
if (!wholeFileOwned && (generatedInternal.has(path) || native(path) && !existingBlock && !mixedPaths.has(path) && !preserved.has(path))) conflicts.push(path);
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
710
790
|
const managed = path === derivedPath && derivedBytes && current?.equals(derivedBytes)
|
|
711
|
-
|| [previous?.files[path], derivedOutputs[path], derivedEntries[path], derivedFiles[path]].some(digest => digest && hash(current || '') === digest)
|
|
791
|
+
|| [previous?.files[path], derivedOutputs[path], derivedEntries[path], derivedFiles[path], derivedCatalogs[path], derivedConfigFiles[path]].some(digest => digest && hash(current || '') === digest)
|
|
712
792
|
|| knownBlocks[path] && blockDigest(path, current || Buffer.alloc(0)) === knownBlocks[path];
|
|
713
793
|
const appendBlock = mixedPaths.has(path) || canonicalEntrypoint(path) || path === '.gitignore' || path.endsWith('/.gitignore');
|
|
714
794
|
const conflict = current && path !== statePath && !managed && !appendBlock;
|
|
@@ -828,7 +908,9 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
828
908
|
const localIgnore = planLocalIgnore(target, { outputs: ownedOutputs, templateRoot: stage });
|
|
829
909
|
const localIgnoreCheck = checkLocalIgnore(localIgnore, { replacedBlocks: replacedIgnoreBlocks(target, changes, previous) });
|
|
830
910
|
conflicts.push(...(untrack?.conflicts || []), ...localIgnoreCheck.missing);
|
|
831
|
-
|
|
911
|
+
// A conflict means ownership or the managed bytes could not be proven.
|
|
912
|
+
// Backups do not authorize replacing user content, even with --force.
|
|
913
|
+
return { target, language, mode, changes, conflicts: [...new Set(conflicts)].sort(), canApply: !conflicts.length && !untrack?.conflicts?.length && !localIgnoreCheck.missing.length, manifest, retiredRoots, ownedOutputs, untrack, localIgnore, localIgnoreCheck, migration, fallbackChecks: [...fallbackChecks], fallbackOwnedPaths: ownedPaths, preserved: [...preserved.values()].sort((a, b) => a.path.localeCompare(b.path)) };
|
|
832
914
|
} finally { rmSync(stage, { recursive: true, force: true }); }
|
|
833
915
|
}
|
|
834
916
|
|
|
@@ -1019,7 +1101,7 @@ export function inspect(target) {
|
|
|
1019
1101
|
if (content) issues.push({ code: 'modified', path });
|
|
1020
1102
|
continue;
|
|
1021
1103
|
}
|
|
1022
|
-
if (!content || hash(content) !== digest && hash(content) !== derived.outputs[path] && hash(content) !== derived.files[path] && hash(content) !== derived.entries[path]) issues.push({ code: content ? 'modified' : 'missing', path });
|
|
1104
|
+
if (!content || hash(content) !== digest && hash(content) !== derived.outputs[path] && hash(content) !== derived.files[path] && hash(content) !== derived.entries[path] && hash(content) !== derived.catalogs[path] && !(path === '.pbai4devs/espelhos.conf' && hash(content) === derived.config)) issues.push({ code: content ? 'modified' : 'missing', path });
|
|
1023
1105
|
}
|
|
1024
1106
|
for (const [path, digest] of Object.entries({ ...state.blocks, ...derived.blocks })) {
|
|
1025
1107
|
if (derived.removedBlocks.includes(path) && !Object.hasOwn(derived.blocks, path)) continue;
|