@pathbit/agentdevkit 0.0.23 → 0.0.24
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/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/CHANGELOG.md +7 -0
- package/docs/guides/troubleshooting.md +8 -6
- package/package.json +1 -1
- package/src/i18n.mjs +3 -3
- package/src/install.mjs +100 -18
|
@@ -9,8 +9,8 @@ import { buildCatalog } from '../lib/catalog.mjs';
|
|
|
9
9
|
import { resourcePath } from '../lib/resource-path.mjs';
|
|
10
10
|
import { adapterManifest, resolveAdapters } from '../lib/adapters.mjs';
|
|
11
11
|
import { nativePathBoundary } from '../lib/native-paths.mjs';
|
|
12
|
-
import { canonicalEntrypoint, resolveEntrypoint, entrypointReference, renderEntrypointImports, mergeManagedContent, stripManagedContent } from '../lib/managed-content.mjs';
|
|
13
|
-
import { mergeTomlContent, stripTomlContent } from '../lib/managed-toml.mjs';
|
|
12
|
+
import { canonicalEntrypoint, resolveEntrypoint, entrypointReference, renderEntrypointImports, extractManagedBlock, mergeManagedContent, stripManagedContent } from '../lib/managed-content.mjs';
|
|
13
|
+
import { extractTomlManagedBlock, mergeTomlContent, stripTomlContent } from '../lib/managed-toml.mjs';
|
|
14
14
|
import { mergeGitignore, validateIgnoreClients, renderClientIgnore, isGeneratedOutput, planLocalIgnore, applyLocalIgnore, checkLocalIgnore, assertLocalIgnoreUnchanged, planUntrack, ignoreBegin, ignoreEnd } from '../lib/gitignore.mjs';
|
|
15
15
|
import { stateDirectory } from '../lib/state-path.mjs';
|
|
16
16
|
|
|
@@ -90,7 +90,7 @@ function replaceJsonMember(bytes, member, value) {
|
|
|
90
90
|
// Compare owned fields structurally, preserving all other JSON bytes.
|
|
91
91
|
function derivedContent(original, previous, next) {
|
|
92
92
|
if (!original) return JSON.stringify(next, null, 2) + '\n';
|
|
93
|
-
const keys = ['schemaVersion', 'entries', 'outputs', 'files', 'blocks', 'removedBlocks', 'removedFiles'];
|
|
93
|
+
const keys = ['schemaVersion', 'entries', 'outputs', 'files', 'blocks', 'removedBlocks', 'removedFiles', 'catalogs', 'config'];
|
|
94
94
|
const canonical = value => value && typeof value === 'object' && !Array.isArray(value)
|
|
95
95
|
? Object.fromEntries(Object.keys(value).sort(compare).map(key => [key, canonical(value[key])]))
|
|
96
96
|
: Array.isArray(value) ? value.map(canonical) : value;
|
|
@@ -247,6 +247,11 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
247
247
|
if (previousEntries?.schemaVersion !== 1 || !previousEntries.entries || typeof previousEntries.entries !== 'object' || Array.isArray(previousEntries.entries) || Object.entries(previousEntries.entries).some(([name, hash]) => !canonicalEntrypoint(name) || !/^[a-f0-9]{64}$/.test(hash))) fail('Registro de entradas derivadas invalido.');
|
|
248
248
|
const previousOutputs = previousEntries.outputs ?? {};
|
|
249
249
|
if (!previousOutputs || typeof previousOutputs !== 'object' || Array.isArray(previousOutputs) || Object.entries(previousOutputs).some(([path, hash]) => !/^\.[A-Za-z0-9][A-Za-z0-9._-]*\/(skills|rules|agents)\.json$/.test(path) || reserved.has(path.split('/')[0].toLowerCase()) || !/^[a-f0-9]{64}$/.test(hash))) fail('Registro de saidas derivadas invalido.');
|
|
250
|
+
const previousCatalogs = previousEntries.catalogs ?? {};
|
|
251
|
+
const catalogPaths = new Set(['.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/agents.json', '.pbai4devs/rules.json']);
|
|
252
|
+
if (!previousCatalogs || typeof previousCatalogs !== 'object' || Array.isArray(previousCatalogs) || Object.entries(previousCatalogs).some(([path, hash]) => !catalogPaths.has(path) || !/^[a-f0-9]{64}$/.test(hash))) fail('Registro de catalogos derivados invalido.');
|
|
253
|
+
for (const path of Object.keys(previousCatalogs)) safe(path);
|
|
254
|
+
if (previousEntries.config !== undefined && (typeof previousEntries.config !== 'string' || !/^[a-f0-9]{64}$/.test(previousEntries.config))) fail('Registro de configuracao derivada invalido.');
|
|
250
255
|
const validResourcePath = path => {
|
|
251
256
|
if (typeof path !== 'string' || !/^\.[A-Za-z0-9][A-Za-z0-9._-]*\/.+/.test(path)) return false;
|
|
252
257
|
const [root, scope] = path.split('/');
|
|
@@ -254,7 +259,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
254
259
|
&& !['readme.md', 'instructions.md'].includes(posix.basename(path).toLowerCase())
|
|
255
260
|
&& (/\.(md|mdc)$/i.test(path) || path.startsWith('.gemini/commands/') && path.endsWith('.toml'));
|
|
256
261
|
};
|
|
257
|
-
const validIgnoreBlock = path => typeof path === 'string' && /^\.[A-Za-z0-9][A-Za-z0-9._-]*\/\.gitignore$/.test(path) && !reserved.has(path.split('/')[0].toLowerCase());
|
|
262
|
+
const validIgnoreBlock = path => path === '.pbai4devs/.gitignore' || typeof path === 'string' && /^\.[A-Za-z0-9][A-Za-z0-9._-]*\/\.gitignore$/.test(path) && !reserved.has(path.split('/')[0].toLowerCase());
|
|
258
263
|
for (const [name, values] of [['files', previousEntries.files ?? {}], ['blocks', previousEntries.blocks ?? {}]]) {
|
|
259
264
|
if (!values || typeof values !== 'object' || Array.isArray(values) || Object.entries(values).some(([path, hash]) => !(validResourcePath(path) || name === 'blocks' && (canonicalEntrypoint(path) || validIgnoreBlock(path))) || !/^[a-f0-9]{64}$/.test(hash))) fail(`Registro de ${name} derivados invalido.`);
|
|
260
265
|
for (const path of Object.keys(values)) safe(canonicalEntrypoint(path) ? resolveEntrypoint(root, canonicalEntrypoint(path)) : path);
|
|
@@ -475,11 +480,23 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
475
480
|
}
|
|
476
481
|
const registries = new Map();
|
|
477
482
|
const discoveryOutputs = {};
|
|
483
|
+
const catalogOutputs = {};
|
|
478
484
|
for (const [target, content] of built.outputs) {
|
|
479
485
|
const info = stat(safe(target));
|
|
480
486
|
if (info && !info.isFile()) fail(`Registro nao e arquivo: ${target}`);
|
|
481
487
|
const current = info ? readFileSync(absolute(target)) : null;
|
|
482
488
|
captureInput(target, current);
|
|
489
|
+
const currentHash = current && createHash('sha256').update(current).digest('hex');
|
|
490
|
+
const known = current && [installed?.files?.[target], previousCatalogs[target]].some(digest => digest && digest === currentHash);
|
|
491
|
+
if (current && !known) {
|
|
492
|
+
// Identical bytes do not establish ownership. An unowned catalogue can
|
|
493
|
+
// remain if already current, but it cannot be adopted or regenerated.
|
|
494
|
+
remember(target, 'existing-catalog');
|
|
495
|
+
if (current.toString('utf8') !== content) { conflicts.add(target); protectedConflicts.add(target); }
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
const nextHash = createHash('sha256').update(content).digest('hex');
|
|
499
|
+
if (installed?.files?.[target] !== nextHash) catalogOutputs[target] = nextHash;
|
|
483
500
|
if (!current || current.toString('utf8') !== content) {
|
|
484
501
|
registries.set(target, content);
|
|
485
502
|
if (force && info) backup.add(target);
|
|
@@ -533,10 +550,22 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
533
550
|
merged = mergeGitignore(current, patterns);
|
|
534
551
|
}
|
|
535
552
|
catch (error) { fail(`${target}: ${error.message}`); }
|
|
536
|
-
if (
|
|
537
|
-
const start =
|
|
538
|
-
|
|
553
|
+
if (merged.hasBlock) {
|
|
554
|
+
const start = current.indexOf(ignoreBegin), finish = current.indexOf(ignoreEnd);
|
|
555
|
+
const blockHash = start >= 0 && finish > start ? createHash('sha256').update(current.slice(start, finish + ignoreEnd.length)).digest('hex') : null;
|
|
556
|
+
const fileHash = createHash('sha256').update(current).digest('hex');
|
|
557
|
+
const known = [installed?.blocks?.[target], previousEntries.blocks?.[target]].some(hash => hash && hash === blockHash)
|
|
558
|
+
|| priorFiles[target] && priorFiles[target] === fileHash;
|
|
559
|
+
if (!known) {
|
|
560
|
+
// A marker or identical contents is not proof that this block belongs
|
|
561
|
+
// to the kit. Do not adopt, replace or remove it even with --force.
|
|
562
|
+
remember(target, 'existing-ignore-block');
|
|
563
|
+
if (merged.text !== current) { conflicts.add(target); protectedConflicts.add(target); }
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
539
566
|
}
|
|
567
|
+
const start = merged.text.indexOf(ignoreBegin), finish = merged.text.indexOf(ignoreEnd);
|
|
568
|
+
if (start >= 0 && finish > start) managedBlocks[target] = createHash('sha256').update(merged.text.slice(start, finish + ignoreEnd.length)).digest('hex');
|
|
540
569
|
if (merged.text !== current) {
|
|
541
570
|
ignoreFiles.set(target, merged.text);
|
|
542
571
|
if (force && info && merged.hasBlock) backup.add(target);
|
|
@@ -568,6 +597,10 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
568
597
|
if (!command) fail(`Comando Gemini exige COMMAND.md: ${source}`);
|
|
569
598
|
const current = mixed ? targetBytes : null;
|
|
570
599
|
const merged = mixed ? mergeTomlContent(current, `@{${source}}`, { expectedHash: priorBlocks[target] }) : null;
|
|
600
|
+
if (current && !priorBlocks[target] && priorFiles[target] !== createHash('sha256').update(current).digest('hex') && extractTomlManagedBlock(current)) {
|
|
601
|
+
// An identical block can remain, but its bytes are not ownership proof.
|
|
602
|
+
remember(target, 'existing-managed-block'); expected.set(target, current); continue;
|
|
603
|
+
}
|
|
571
604
|
const content = merged ? merged.text : `${tomlMarker}\ndescription = ${JSON.stringify(command.description)}\nprompt = ${JSON.stringify(`@{${source}}\n\n{{args}}`)}\n`;
|
|
572
605
|
if (mixed) { captureInput(target, current); managedBlocks[target] = merged.sha256; remember(target, 'existing-file-extended'); }
|
|
573
606
|
else rememberGenerated(target, content);
|
|
@@ -588,6 +621,9 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
588
621
|
const inclusion = ruleInclusion ? `@[origem](${link(target, source)})` : `Leia e siga [origem](${link(target, source)}).`;
|
|
589
622
|
const current = mixed ? targetBytes : null;
|
|
590
623
|
const merged = mixed ? mergeManagedContent(current, inclusion, { expectedHash: priorBlocks[target] }) : null;
|
|
624
|
+
if (current && !priorBlocks[target] && priorFiles[target] !== createHash('sha256').update(current).digest('hex') && extractManagedBlock(current)) {
|
|
625
|
+
remember(target, 'existing-managed-block'); expected.set(target, current); continue;
|
|
626
|
+
}
|
|
591
627
|
const content = merged ? merged.text : `${frontmatter}\n${title}\n\n${marker}\n${inclusion}\n`;
|
|
592
628
|
if (mixed) { captureInput(target, current); managedBlocks[target] = merged.sha256; remember(target, 'existing-file-extended'); }
|
|
593
629
|
else rememberGenerated(target, content);
|
|
@@ -644,6 +680,9 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
644
680
|
const previousBlockHash = priorBlocks[name] ?? Object.entries(priorBlocks).find(([path]) => canonicalEntrypoint(path) === canonical)?.[1];
|
|
645
681
|
if (current && (previousBlockHash || createHash('sha256').update(current).digest('hex') !== previousHash)) {
|
|
646
682
|
const merged = mergeManagedContent(current, entrypointReference(root, canonical), { expectedHash: previousBlockHash });
|
|
683
|
+
if (!previousBlockHash && previousHash !== createHash('sha256').update(current).digest('hex') && extractManagedBlock(current)) {
|
|
684
|
+
remember(name, 'existing-managed-block'); continue;
|
|
685
|
+
}
|
|
647
686
|
captureInput(name, current);
|
|
648
687
|
managedBlocks[name] = merged.sha256;
|
|
649
688
|
rootEntries.set(name, merged.text);
|
|
@@ -682,8 +721,20 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
682
721
|
removedBlocks.add(target);
|
|
683
722
|
remember(target, 'existing-file-extended');
|
|
684
723
|
}
|
|
724
|
+
const omittedRows = new Set([...legacy, ...missing.map(native => native.index)]);
|
|
725
|
+
const finalConfig = rows.filter((row, index) => !omittedRows.has(index)).join('\n').replace(/\n*$/, '\n');
|
|
726
|
+
const configChanged = !configExists || configText !== finalConfig;
|
|
727
|
+
const configHash = configExists ? createHash('sha256').update(readFileSync(absolute(config))).digest('hex') : null;
|
|
728
|
+
const configKnown = configExists && [installed?.files?.[config], previousEntries.config].some(hash => hash && hash === configHash);
|
|
729
|
+
if (configExists && !configKnown) {
|
|
730
|
+
remember(config, 'existing-configuration');
|
|
731
|
+
if (configChanged) { conflicts.add(config); protectedConflicts.add(config); }
|
|
732
|
+
}
|
|
685
733
|
const next = { schemaVersion: 1, entries: nextEntries };
|
|
686
734
|
if (Object.keys(discoveryOutputs).length) next.outputs = discoveryOutputs;
|
|
735
|
+
if (Object.keys(catalogOutputs).length) next.catalogs = catalogOutputs;
|
|
736
|
+
const nextConfigHash = createHash('sha256').update(finalConfig).digest('hex');
|
|
737
|
+
if ((!configExists || configKnown) && installed?.files?.[config] !== nextConfigHash) next.config = nextConfigHash;
|
|
687
738
|
if (Object.keys(generatedFiles).length) next.files = generatedFiles;
|
|
688
739
|
const derivedBlocks = Object.fromEntries(Object.entries(managedBlocks).filter(([path, hash]) => installed?.blocks?.[path] !== hash || Object.hasOwn(previousEntries.blocks ?? {}, path)));
|
|
689
740
|
if (Object.keys(derivedBlocks).length) next.blocks = derivedBlocks;
|
|
@@ -693,7 +744,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
693
744
|
for (const path of remove) if (validResourcePath(path) && installed?.files?.[path] && !expected.has(path)) effectiveRemovedFiles[path] = installed.files[path];
|
|
694
745
|
if (Object.keys(effectiveRemovedFiles).length) next.removedFiles = Object.fromEntries(Object.entries(effectiveRemovedFiles).sort(([a], [b]) => compare(a, b)));
|
|
695
746
|
const registryContent = derivedContent(registryBytes, previousEntries, next);
|
|
696
|
-
if (recordDerivedState && (Object.keys(nextEntries).length || Object.keys(discoveryOutputs).length || Object.keys(generatedFiles).length || Object.keys(derivedBlocks).length || removedBlocks.size || Object.keys(effectiveRemovedFiles).length || stat(safe(entryRegistry))) && (!stat(safe(entryRegistry)) || readFileSync(absolute(entryRegistry), 'utf8') !== registryContent)) registries.set(entryRegistry, registryContent);
|
|
747
|
+
if (recordDerivedState && (Object.keys(nextEntries).length || Object.keys(discoveryOutputs).length || Object.keys(catalogOutputs).length || next.config || Object.keys(generatedFiles).length || Object.keys(derivedBlocks).length || removedBlocks.size || Object.keys(effectiveRemovedFiles).length || stat(safe(entryRegistry))) && (!stat(safe(entryRegistry)) || readFileSync(absolute(entryRegistry), 'utf8') !== registryContent)) registries.set(entryRegistry, registryContent);
|
|
697
748
|
const ownedOutputs = sorted([...targets, ...built.outputs.keys(), '.pbai4devs/espelhos.conf', ...rootEntries.keys(), ...(recordDerivedState && (stat(safe(entryRegistry)) || registries.has(entryRegistry)) ? [entryRegistry] : []),
|
|
698
749
|
...[...discoveries].flatMap(agent => ['skills', 'rules', 'agents'].map(kind => `${agent}/${kind}.json`)),
|
|
699
750
|
...[...ignoreTemplates.keys()].filter(target => {
|
|
@@ -709,9 +760,6 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
709
760
|
const removals = topPaths(remove);
|
|
710
761
|
const backups = topPaths(backup);
|
|
711
762
|
const rootEntryChanges = [...rootEntries].filter(([name, content]) => !stat(absolute(name)) || readFileSync(absolute(name), 'utf8') !== content);
|
|
712
|
-
const omittedRows = new Set([...legacy, ...missing.map(native => native.index)]);
|
|
713
|
-
const finalConfig = rows.filter((row, index) => !omittedRows.has(index)).join('\n').replace(/\n*$/, '\n');
|
|
714
|
-
const configChanged = !configExists || configText !== finalConfig;
|
|
715
763
|
// Every planned write/removal may cover a different staged version. Compare
|
|
716
764
|
// the worktree bytes with the index before touching any path, even in force
|
|
717
765
|
// mode, and repeat immediately before applying the transaction.
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { dirname, join, resolve } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { synchronize } from '../SCRIPT.mjs';
|
|
10
|
+
import { entrypointReference, mergeManagedContent } from '../../lib/managed-content.mjs';
|
|
11
|
+
import { mergeTomlContent } from '../../lib/managed-toml.mjs';
|
|
12
|
+
|
|
13
|
+
const catalogs = ['catalog.json', 'skills.json', 'agents.json', 'rules.json'].map(name => `.pbai4devs/${name}`);
|
|
14
|
+
const state = '.pbai4devs/state/derived.json';
|
|
15
|
+
const original = '.pbai4devs/skills/demo/SKILL.md';
|
|
16
|
+
const digest = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
17
|
+
const get = (root, path) => readFileSync(join(root, path));
|
|
18
|
+
const put = (root, path, bytes) => { mkdirSync(dirname(join(root, path)), { recursive: true }); writeFileSync(join(root, path), bytes); };
|
|
19
|
+
const gitEnvironment = root => ({ ...Object.fromEntries(Object.entries(process.env).filter(([key]) => !/^GIT_/i.test(key))), GIT_CONFIG_GLOBAL: join(root, '.git-config-ausente'), GIT_CONFIG_NOSYSTEM: '1', GIT_OPTIONAL_LOCKS: '0' });
|
|
20
|
+
const sync = (root, options = {}) => {
|
|
21
|
+
// synchronize is synchronous and launches Git internally. Isolate only for
|
|
22
|
+
// this call so an inherited GIT_INDEX_FILE/GIT_DIR cannot touch another repo.
|
|
23
|
+
const saved = Object.fromEntries(Object.entries(process.env).filter(([key]) => /^GIT_/i.test(key)));
|
|
24
|
+
for (const key of Object.keys(saved)) delete process.env[key];
|
|
25
|
+
Object.assign(process.env, { GIT_CONFIG_GLOBAL: join(root, '.git-config-ausente'), GIT_CONFIG_NOSYSTEM: '1', GIT_OPTIONAL_LOCKS: '0' });
|
|
26
|
+
try { return synchronize({ root, localIgnores: false, generateEntrypoints: false, output() {}, ...options }); }
|
|
27
|
+
finally {
|
|
28
|
+
for (const key of Object.keys(process.env)) if (/^GIT_/i.test(key)) delete process.env[key];
|
|
29
|
+
Object.assign(process.env, saved);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
function git(root, ...args) {
|
|
33
|
+
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8', env: gitEnvironment(root) });
|
|
34
|
+
assert.equal(result.status, 0, result.stderr);
|
|
35
|
+
}
|
|
36
|
+
function fixture(t) {
|
|
37
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), 'adk-catalog-ownership-')));
|
|
38
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
39
|
+
git(root, 'init', '-q');
|
|
40
|
+
put(root, '.gitignore', 'node_modules/\n');
|
|
41
|
+
put(root, '.pbai4devs/espelhos.conf', 'agente|.claude|plano\n');
|
|
42
|
+
put(root, '.pbai4devs/templates/gitignore/agents.ignore', '# Cache\n/*\n!/.gitignore\n{{ALLOWLIST}}\n');
|
|
43
|
+
put(root, '.pbai4devs/templates/gitignore/clients.json', '{}\n');
|
|
44
|
+
put(root, '.pbai4devs/templates/gitignore/shared.ignore', '# Shared\n/tools/*/.local/\n');
|
|
45
|
+
put(root, original, '---\nname: demo\ndescription: Demo.\n---\n\n# Demo\n');
|
|
46
|
+
git(root, 'add', '-f', '.gitignore');
|
|
47
|
+
return root;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const path of catalogs) for (const force of [false, true]) {
|
|
51
|
+
test(`catálogo editado permanece intacto na sincronização direta: ${path}, force=${force}`, t => {
|
|
52
|
+
const root = fixture(t);
|
|
53
|
+
assert.equal(sync(root).exitCode, 0);
|
|
54
|
+
put(root, path, '{"human":"preservar"}\n');
|
|
55
|
+
git(root, 'add', '-f', '--', path);
|
|
56
|
+
const before = Object.fromEntries([path, state, '.gitignore', '.git/index'].map(name => [name, get(root, name)]));
|
|
57
|
+
assert.notEqual(sync(root, { check: true, force }).exitCode, 0);
|
|
58
|
+
assert.throws(() => sync(root, { force }), /Conteudo proprio preservado/);
|
|
59
|
+
for (const [name, content] of Object.entries(before)) assert.deepEqual(get(root, name), content, name);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
test('catálogo anterior idêntico não é assumido como próprio sem estado', t => {
|
|
64
|
+
const root = fixture(t);
|
|
65
|
+
assert.equal(sync(root).exitCode, 0);
|
|
66
|
+
const recorded = JSON.parse(get(root, state));
|
|
67
|
+
delete recorded.catalogs;
|
|
68
|
+
put(root, state, `${JSON.stringify(recorded, null, 2)}\n`);
|
|
69
|
+
const before = Object.fromEntries([...catalogs, '.gitignore', '.git/index'].map(name => [name, get(root, name)]));
|
|
70
|
+
const result = sync(root, { force: true });
|
|
71
|
+
for (const path of catalogs) assert.ok(result.preserved.some(entry => entry.path === path && entry.reason === 'existing-catalog'), path);
|
|
72
|
+
assert.equal(JSON.parse(get(root, state)).catalogs, undefined);
|
|
73
|
+
for (const [name, content] of Object.entries(before)) assert.deepEqual(get(root, name), content, name);
|
|
74
|
+
put(root, original, '---\nname: demo\ndescription: Mudança humana.\n---\n\n# Demo\n');
|
|
75
|
+
assert.throws(() => sync(root, { force: true }), /Conteudo proprio preservado/);
|
|
76
|
+
for (const [name, content] of Object.entries(before)) assert.deepEqual(get(root, name), content, name);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('sincronizações diretas sucessivas registram hashes dos catálogos criados', t => {
|
|
80
|
+
const root = fixture(t);
|
|
81
|
+
assert.equal(sync(root).exitCode, 0);
|
|
82
|
+
for (const path of catalogs) assert.equal(JSON.parse(get(root, state)).catalogs[path], digest(get(root, path)), path);
|
|
83
|
+
put(root, original, '---\nname: demo\ndescription: Nova descrição.\n---\n\n# Demo\n');
|
|
84
|
+
assert.equal(sync(root).exitCode, 0);
|
|
85
|
+
for (const path of catalogs) assert.equal(JSON.parse(get(root, state)).catalogs[path], digest(get(root, path)), path);
|
|
86
|
+
assert.equal(sync(root, { check: true }).exitCode, 0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('registro de catálogo desconhecido ou hash inválido bloqueia antes de alterar arquivos', t => {
|
|
90
|
+
const root = fixture(t);
|
|
91
|
+
assert.equal(sync(root).exitCode, 0);
|
|
92
|
+
const registry = JSON.parse(get(root, state));
|
|
93
|
+
registry.catalogs['.pbai4devs/other.json'] = 'a'.repeat(64);
|
|
94
|
+
put(root, state, JSON.stringify(registry));
|
|
95
|
+
const before = Object.fromEntries([...catalogs, state, '.gitignore', '.git/index'].map(name => [name, get(root, name)]));
|
|
96
|
+
assert.throws(() => sync(root, { force: true }), /Registro de catalogos derivados invalido/);
|
|
97
|
+
for (const [name, content] of Object.entries(before)) assert.deepEqual(get(root, name), content, name);
|
|
98
|
+
assert.ok(existsSync(join(root, original)));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
for (const path of ['.pbai4devs/.gitignore', '.claude/.gitignore']) for (const force of [false, true]) {
|
|
102
|
+
test(`bloco ignore editado permanece intacto: ${path}, force=${force}`, t => {
|
|
103
|
+
const root = fixture(t);
|
|
104
|
+
assert.equal(sync(root).exitCode, 0);
|
|
105
|
+
const content = get(root, path).toString('utf8').replace('# BEGIN AGENT IGNORE', '# BEGIN AGENT IGNORE\n# Regra humana\n!/conteudo-humano/');
|
|
106
|
+
put(root, path, content);
|
|
107
|
+
git(root, 'add', '-f', '--', path);
|
|
108
|
+
const before = Object.fromEntries([path, state, '.gitignore', '.git/index'].map(name => [name, get(root, name)]));
|
|
109
|
+
assert.notEqual(sync(root, { check: true, force }).exitCode, 0);
|
|
110
|
+
assert.throws(() => sync(root, { force }), /Conteudo proprio preservado/);
|
|
111
|
+
for (const [name, bytes] of Object.entries(before)) assert.deepEqual(get(root, name), bytes, name);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const path of ['.pbai4devs/.gitignore', '.claude/.gitignore']) {
|
|
116
|
+
test(`bloco ignore igual sem comprovante não é adotado: ${path}`, t => {
|
|
117
|
+
const root = fixture(t);
|
|
118
|
+
assert.equal(sync(root).exitCode, 0);
|
|
119
|
+
const registry = JSON.parse(get(root, state));
|
|
120
|
+
delete registry.blocks[path];
|
|
121
|
+
put(root, state, JSON.stringify(registry, null, 2) + '\n');
|
|
122
|
+
const originalIgnore = get(root, path);
|
|
123
|
+
const result = sync(root, { force: true });
|
|
124
|
+
assert.ok(result.preserved.some(entry => entry.path === path && entry.reason === 'existing-ignore-block'));
|
|
125
|
+
assert.equal(JSON.parse(get(root, state)).blocks?.[path], undefined);
|
|
126
|
+
assert.deepEqual(get(root, path), originalIgnore);
|
|
127
|
+
const template = path.startsWith('.pbai4devs/') ? 'shared.ignore' : 'agents.ignore';
|
|
128
|
+
const templatePath = `.pbai4devs/templates/gitignore/${template}`;
|
|
129
|
+
put(root, templatePath, Buffer.concat([get(root, templatePath), Buffer.from('/novo-item/\n')]));
|
|
130
|
+
assert.throws(() => sync(root, { force: true }), /Conteudo proprio preservado/);
|
|
131
|
+
assert.deepEqual(get(root, path), originalIgnore);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
test('bloco idêntico em AGENTS.md sem registro não é adotado', t => {
|
|
136
|
+
const root = fixture(t), path = 'AGENTS.md';
|
|
137
|
+
put(root, '.pbai4devs/templates/entrypoints/AGENTS.md', '# Instruções\n');
|
|
138
|
+
const content = mergeManagedContent('# Instruções humanas\n', entrypointReference(root, path)).text;
|
|
139
|
+
put(root, path, content);
|
|
140
|
+
const result = sync(root, { force: true, generateEntrypoints: true });
|
|
141
|
+
assert.ok(result.preserved.some(entry => entry.path === path && entry.reason === 'existing-managed-block'));
|
|
142
|
+
assert.equal(get(root, path).toString('utf8'), content);
|
|
143
|
+
const registry = JSON.parse(get(root, state));
|
|
144
|
+
assert.equal(registry.blocks?.[path], undefined);
|
|
145
|
+
assert.equal(registry.entries?.[path], undefined);
|
|
146
|
+
assert.equal(sync(root, { force: true, generateEntrypoints: true }).exitCode, 0);
|
|
147
|
+
assert.equal(get(root, path).toString('utf8'), content);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
for (const kind of ['markdown', 'toml']) test(`bloco nativo ${kind} idêntico sem registro não é adotado`, t => {
|
|
151
|
+
const root = fixture(t);
|
|
152
|
+
const path = kind === 'markdown' ? '.claude/skills/demo/SKILL.md' : '.gemini/commands/demo.toml';
|
|
153
|
+
let content;
|
|
154
|
+
if (kind === 'markdown') {
|
|
155
|
+
content = mergeManagedContent('# Instruções humanas\n', 'Leia e siga [origem](../../../.pbai4devs/skills/demo/SKILL.md).').text;
|
|
156
|
+
} else {
|
|
157
|
+
put(root, '.pbai4devs/espelhos.conf', 'agente|.claude|plano\nagente|.gemini|plano\n');
|
|
158
|
+
put(root, '.pbai4devs/commands/demo/COMMAND.md', '---\ndescription: Demo.\n---\n\n# Demo\n');
|
|
159
|
+
content = mergeTomlContent('description = "Humano"\nprompt = "Meu prompt"\n', '@{.pbai4devs/commands/demo/COMMAND.md}').text;
|
|
160
|
+
}
|
|
161
|
+
put(root, path, content);
|
|
162
|
+
const result = sync(root, { force: true });
|
|
163
|
+
assert.ok(result.preserved.some(entry => entry.path === path && entry.reason === 'existing-managed-block'));
|
|
164
|
+
assert.equal(get(root, path).toString('utf8'), content);
|
|
165
|
+
const registry = JSON.parse(get(root, state));
|
|
166
|
+
assert.equal(registry.blocks?.[path], undefined);
|
|
167
|
+
assert.equal(registry.files?.[path], undefined);
|
|
168
|
+
assert.equal(sync(root, { force: true }).exitCode, 0);
|
|
169
|
+
assert.equal(get(root, path).toString('utf8'), content);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
for (const force of [false, true]) test(`configuração humana não é alterada nem adotada: force=${force}`, t => {
|
|
173
|
+
const root = fixture(t), path = '.pbai4devs/espelhos.conf';
|
|
174
|
+
const identical = get(root, path);
|
|
175
|
+
const result = sync(root, { force });
|
|
176
|
+
assert.ok(result.preserved.some(entry => entry.path === path && entry.reason === 'existing-configuration'));
|
|
177
|
+
assert.equal(JSON.parse(get(root, state)).config, undefined);
|
|
178
|
+
assert.deepEqual(get(root, path), identical);
|
|
179
|
+
put(root, path, Buffer.concat([identical, Buffer.from('documento|manual-humano\n')]));
|
|
180
|
+
git(root, 'add', '-f', '--', path);
|
|
181
|
+
const before = Object.fromEntries([path, state, '.gitignore', '.git/index'].map(name => [name, get(root, name)]));
|
|
182
|
+
assert.notEqual(sync(root, { check: true, force }).exitCode, 0);
|
|
183
|
+
assert.throws(() => sync(root, { force }), /Conteudo proprio preservado/);
|
|
184
|
+
for (const [name, content] of Object.entries(before)) assert.deepEqual(get(root, name), content, name);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('overlay de configuração inválido não autoriza escrita', t => {
|
|
188
|
+
const root = fixture(t);
|
|
189
|
+
assert.equal(sync(root).exitCode, 0);
|
|
190
|
+
const registry = JSON.parse(get(root, state));
|
|
191
|
+
registry.config = 'nao-e-sha256';
|
|
192
|
+
put(root, state, JSON.stringify(registry));
|
|
193
|
+
const before = get(root, state);
|
|
194
|
+
assert.throws(() => sync(root, { force: true }), /Registro de configuracao derivada invalido/);
|
|
195
|
+
assert.deepEqual(get(root, state), before);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('CLI reconhece catálogos da sincronização direta e preserva atualização de ignore', t => {
|
|
199
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), 'adk-cli-sync-ownership-')));
|
|
200
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
201
|
+
git(root, 'init', '-q');
|
|
202
|
+
put(root, '.gitignore', 'node_modules/\n');
|
|
203
|
+
put(root, 'AGENTS.md', '# Instruções existentes\n');
|
|
204
|
+
put(root, 'staged.txt', 'Conteúdo staged humano.\n');
|
|
205
|
+
git(root, 'add', '-f', '.gitignore', 'AGENTS.md', 'staged.txt');
|
|
206
|
+
const initial = Object.fromEntries(['.gitignore', '.git/index', 'staged.txt'].map(path => [path, get(root, path)]));
|
|
207
|
+
const cliPath = resolve(dirname(fileURLToPath(import.meta.url)), '../../../../bin/agentdevkit.mjs');
|
|
208
|
+
function runCli(...args) {
|
|
209
|
+
const result = spawnSync(process.execPath, [cliPath, ...args, '--target', root, '--json'], {
|
|
210
|
+
cwd: root, env: gitEnvironment(root), encoding: 'utf8', timeout: 90000, maxBuffer: 16 * 1024 * 1024,
|
|
211
|
+
});
|
|
212
|
+
let output; try { output = JSON.parse(result.stdout); } catch { assert.fail(`Resposta CLI inválida: ${result.stderr} ${result.stdout}`); }
|
|
213
|
+
assert.equal(result.status, 0, `${args.join(' ')}: ${result.stderr} ${result.stdout}`);
|
|
214
|
+
return output;
|
|
215
|
+
}
|
|
216
|
+
function direct() {
|
|
217
|
+
const result = spawnSync(process.execPath, [join(root, '.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs')], {
|
|
218
|
+
cwd: root, env: gitEnvironment(root), encoding: 'utf8', timeout: 90000,
|
|
219
|
+
});
|
|
220
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
221
|
+
}
|
|
222
|
+
runCli('init', '--agents', 'codex');
|
|
223
|
+
assert.ok(get(root, 'AGENTS.md').toString('utf8').startsWith('# Instruções existentes\n'));
|
|
224
|
+
put(root, '.pbai4devs/skills/projeto/human-extra/SKILL.md', '---\nname: human-extra\ndescription: Minha skill.\n---\n\n# Minha skill\n');
|
|
225
|
+
direct(); direct();
|
|
226
|
+
assert.equal(runCli('doctor').ok, true);
|
|
227
|
+
assert.equal(runCli('sync', '--check').ok, true);
|
|
228
|
+
assert.equal(runCli('update').ok, true);
|
|
229
|
+
assert.equal(runCli('sync', '--check').ok, true);
|
|
230
|
+
// Simule uma versão legada que registrou legitimamente este manifesto.
|
|
231
|
+
const config = '.pbai4devs/espelhos.conf', installed = '.pbai4devs/state/agentdevkit.json';
|
|
232
|
+
put(root, config, Buffer.concat([get(root, config), Buffer.from('documento|legado-comprovado\n')]));
|
|
233
|
+
const previous = JSON.parse(get(root, installed));
|
|
234
|
+
previous.files[config] = digest(get(root, config));
|
|
235
|
+
put(root, installed, JSON.stringify(previous, null, 2) + '\n');
|
|
236
|
+
direct();
|
|
237
|
+
assert.doesNotMatch(get(root, config).toString('utf8'), /documento\|legado-comprovado/);
|
|
238
|
+
assert.equal(JSON.parse(get(root, state)).config, digest(get(root, config)));
|
|
239
|
+
assert.equal(runCli('doctor').ok, true);
|
|
240
|
+
assert.equal(runCli('sync', '--check').ok, true);
|
|
241
|
+
assert.equal(runCli('update').ok, true);
|
|
242
|
+
const template = '.pbai4devs/templates/gitignore/shared.ignore';
|
|
243
|
+
const humanTemplate = Buffer.concat([get(root, template), Buffer.from('\n/artefato-humano/\n')]);
|
|
244
|
+
put(root, template, humanTemplate);
|
|
245
|
+
direct();
|
|
246
|
+
assert.match(get(root, '.pbai4devs/.gitignore').toString('utf8'), /\/artefato-humano\//);
|
|
247
|
+
assert.equal(runCli('update').ok, true);
|
|
248
|
+
assert.deepEqual(get(root, template), humanTemplate);
|
|
249
|
+
assert.equal(runCli('sync', '--check').ok, true);
|
|
250
|
+
for (const [path, content] of Object.entries(initial)) assert.deepEqual(get(root, path), content, path);
|
|
251
|
+
});
|
|
@@ -14,6 +14,7 @@ import './HUMAN_OWNERSHIP.mjs';
|
|
|
14
14
|
import './COEXISTENCE.mjs';
|
|
15
15
|
import './MANAGED_CONTENT.mjs';
|
|
16
16
|
import './PRESERVATION.mjs';
|
|
17
|
+
import './CATALOG_OWNERSHIP.mjs';
|
|
17
18
|
import { validateCatalog as validateExternalCatalog } from '../../gerenciar-skills-externas/SCRIPT.mjs';
|
|
18
19
|
import { stateDirectory } from '../../lib/state-path.mjs';
|
|
19
20
|
|
|
@@ -276,21 +277,22 @@ test('gera gitignore canonico e de clientes; registro gitignore nao espelha recu
|
|
|
276
277
|
assertCheckReadonly(root, {}, 0);
|
|
277
278
|
});
|
|
278
279
|
|
|
279
|
-
test('
|
|
280
|
+
test('preserva bloco gitignore editado e atualiza bloco conhecido sem perder regras externas', t => {
|
|
280
281
|
const root = fixture(t, true); run(root);
|
|
281
282
|
const prefix = '# Regras do usuario\r\n/private-cache/\r\n';
|
|
282
283
|
const suffix = '# Excecao local\r\n!keep.log\r\n';
|
|
284
|
+
const original = read(root, '.codex/.gitignore');
|
|
283
285
|
const old = `${prefix}${ignoreBegin}\r\n/custom-inside/\r\n${ignoreEnd}\r\n${suffix}`;
|
|
284
286
|
write(root, '.codex/.gitignore', old);
|
|
285
287
|
assertCheckReadonly(root); assertCheckReadonly(root, { force: true });
|
|
286
|
-
|
|
287
|
-
assert.equal(
|
|
288
|
-
|
|
289
|
-
assert.ok(repaired.startsWith(prefix)); assert.ok(repaired.endsWith(suffix));
|
|
290
|
-
assert.ok(!repaired.includes('/custom-inside/')); assert.ok(repaired.includes('/cache/'));
|
|
288
|
+
assert.throws(() => run(root, { force: true }), /Conteudo proprio preservado/);
|
|
289
|
+
assert.equal(read(root, '.codex/.gitignore'), old);
|
|
290
|
+
write(root, '.codex/.gitignore', `${prefix}${original}${suffix}`);
|
|
291
291
|
write(root, '.pbai4devs/templates/gitignore/agents.ignore', '# Novo padrao\n{{ALLOWLIST}}\n/cache/\n/session/\n');
|
|
292
292
|
assertCheckReadonly(root);
|
|
293
|
-
run(root);
|
|
293
|
+
const result = run(root, { force: true });
|
|
294
|
+
assert.equal(readFileSync(join(result.backupRoot, '.codex/.gitignore'), 'utf8'), `${prefix}${original}${suffix}`);
|
|
295
|
+
assert.ok(read(root, '.codex/.gitignore').startsWith(prefix));
|
|
294
296
|
assert.ok(read(root, '.codex/.gitignore').includes('/session/'));
|
|
295
297
|
assert.ok(readFileSync(join(root, '.codex/.gitignore'), 'utf8').endsWith(suffix));
|
|
296
298
|
assertCheckReadonly(root, {}, 0);
|
|
@@ -374,6 +376,9 @@ test('registros são derivados, determinísticos e --check nunca os corrige', t
|
|
|
374
376
|
run(root);
|
|
375
377
|
write(root, '.pbai4devs/skills.json', '{"incorreto":true}\n');
|
|
376
378
|
assertCheckReadonly(root);
|
|
379
|
+
assert.throws(() => run(root, { force: true }), /Conteudo proprio preservado/);
|
|
380
|
+
assert.equal(read(root, '.pbai4devs/skills.json'), '{"incorreto":true}\n');
|
|
381
|
+
rmSync(join(root, '.pbai4devs/skills.json'));
|
|
377
382
|
run(root);
|
|
378
383
|
assert.ok(Array.isArray(JSON.parse(read(root, '.pbai4devs/skills.json')).entries));
|
|
379
384
|
rmSync(join(root, '.pbai4devs/adapters/antigravity/agents/revisor.md'));
|
|
@@ -455,7 +460,7 @@ test('atualiza fontes, adiciona clientes, remove recursos e migra agrupado para
|
|
|
455
460
|
assertCheckReadonly(root, {}, 0);
|
|
456
461
|
});
|
|
457
462
|
|
|
458
|
-
test('
|
|
463
|
+
test('preserva linhas legadas humanas sem prova de propriedade', t => {
|
|
459
464
|
const root = fixture(t);
|
|
460
465
|
run(root);
|
|
461
466
|
write(root, 'docs/produto/prd/demo/PRD.md', '# Produto real\n');
|
|
@@ -463,13 +468,26 @@ test('remove linhas legadas e preserva arquivos sem prova de propriedade', t =>
|
|
|
463
468
|
write(root, '.claude/manual/DOC.md', pointer('../../manual/guia%20com%20espa%C3%A7o/DOC.md'));
|
|
464
469
|
write(root, '.github/prompts/missing.prompt.md', pointer());
|
|
465
470
|
write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + 'documento|manual\n' + 'nativo|.github/prompts/missing.prompt.md|.pbai4devs/commands/missing/COMMAND.md\n');
|
|
471
|
+
const config = read(root, '.pbai4devs/espelhos.conf');
|
|
466
472
|
assertCheckReadonly(root);
|
|
467
|
-
run(root);
|
|
468
|
-
assert.
|
|
473
|
+
assert.throws(() => run(root, { force: true }), /Conteudo proprio preservado/);
|
|
474
|
+
assert.equal(read(root, '.pbai4devs/espelhos.conf'), config);
|
|
469
475
|
assert.equal(existsSync(join(root, '.claude/manual/DOC.md')), true);
|
|
470
476
|
assert.equal(existsSync(join(root, '.github/prompts/missing.prompt.md')), true);
|
|
471
477
|
assert.ok(existsSync(join(root, 'docs/produto/prd/demo/PRD.md')));
|
|
472
478
|
assert.ok(existsSync(join(root, 'manual/guia com espaço/DOC.md')));
|
|
479
|
+
assertCheckReadonly(root);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test('limpa configuração legada comprovadamente gerenciada e registra novo hash', t => {
|
|
483
|
+
const root = fixture(t);
|
|
484
|
+
const path = '.pbai4devs/espelhos.conf';
|
|
485
|
+
write(root, path, read(root, path) + 'documento|manual\n');
|
|
486
|
+
recordOwned(root, [path]);
|
|
487
|
+
run(root);
|
|
488
|
+
assert.doesNotMatch(read(root, path), /documento\|/);
|
|
489
|
+
const derived = JSON.parse(read(root, '.pbai4devs/state/derived.json'));
|
|
490
|
+
assert.equal(derived.config, createHash('sha256').update(readFileSync(join(root, path))).digest('hex'));
|
|
473
491
|
assertCheckReadonly(root, {}, 0);
|
|
474
492
|
});
|
|
475
493
|
|
|
@@ -745,13 +763,15 @@ test('criar editar e remover recursos atualiza todos os tipos e os índices nati
|
|
|
745
763
|
assertCheckReadonly(root, {}, 0);
|
|
746
764
|
});
|
|
747
765
|
|
|
748
|
-
test('JSON derivado
|
|
766
|
+
test('JSON derivado alterado é preservado até remoção deliberada pelo usuário', t => {
|
|
749
767
|
const root = completeCatalogFixture(t);
|
|
750
768
|
run(root);
|
|
751
769
|
for (const path of ['.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/agents.json', '.pbai4devs/rules.json']) write(root, path, '{corrompido');
|
|
752
770
|
assertCheckReadonly(root, { force: true });
|
|
753
|
-
|
|
754
|
-
for (const path of ['.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/agents.json', '.pbai4devs/rules.json']) assert.equal(read(
|
|
771
|
+
assert.throws(() => run(root, { force: true }), /Conteudo proprio preservado/);
|
|
772
|
+
for (const path of ['.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/agents.json', '.pbai4devs/rules.json']) assert.equal(read(root, path), '{corrompido');
|
|
773
|
+
for (const path of ['.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/agents.json', '.pbai4devs/rules.json']) rmSync(join(root, path));
|
|
774
|
+
run(root);
|
|
755
775
|
assert.equal(JSON.parse(read(root, '.pbai4devs/catalog.json')).schemaVersion, 1);
|
|
756
776
|
assertCheckReadonly(root, {}, 0);
|
|
757
777
|
});
|
|
@@ -29,21 +29,30 @@ execuções e projetos anteriores não são substituídos nem removidos. Cada
|
|
|
29
29
|
exemplo recebe Git local, sem commits nem identidade alterada. O diretório
|
|
30
30
|
gerado contém um ignore próprio, inclusive em projetos consumidores. Se a
|
|
31
31
|
preparação falhar, o índice da nova execução permanece em `preparing`.
|
|
32
|
+
O gerador verifica links e identidade da saída antes e depois das etapas, mas
|
|
33
|
+
isso não é uma proteção atômica. Um processo concorrente com permissão para
|
|
34
|
+
trocar diretórios ainda pode redirecionar uma escrita entre as verificações;
|
|
35
|
+
execute-o em uma área sem esse acesso concorrente.
|
|
32
36
|
O hub `.pbai4devs` e os clientes registrados são copiados, incluindo comandos
|
|
33
37
|
nativos em TOML. Sem manifesto local, os templates definem os clientes em
|
|
34
38
|
memória, respeitando a seleção salva. `.vscode` exige registro. Apenas as entradas
|
|
35
39
|
`AGENTS.md`, `CLAUDE.md` e `GEMINI.md` presentes na origem são incluídas na raiz.
|
|
36
40
|
Os templates substituem somente entradas com blocos gerenciados válidos;
|
|
37
41
|
entradas próprias ou editadas são preservadas. Sem estado de instalação,
|
|
38
|
-
|
|
42
|
+
as entradas existentes são copiadas sem presumir propriedade do kit.
|
|
39
43
|
Os testes resolvem cada apontamento pelo formato declarado do cliente e recurso.
|
|
40
44
|
|
|
41
|
-
As cópias
|
|
45
|
+
As cópias guardam os catálogos originais em `.agent-tests/testar-maturidade/original-catalogs/`
|
|
46
|
+
e geram catálogos operacionais novos, sem alterar os arquivos da origem.
|
|
47
|
+
Arquivos da origem substituídos pela fixture ficam em `original-files/` nessa
|
|
48
|
+
mesma pasta. O relatório identifica cada cópia: o cenário não é um teste do
|
|
49
|
+
projeto original no local.
|
|
42
50
|
Não copiam o código do produto em `bin/`, `src/`, `scripts/` ou `tests/`,
|
|
43
51
|
nem essas pastas na localização antiga dentro do hub.
|
|
44
52
|
Executam sincronização/reparo/idempotência, catálogo, verificação de
|
|
45
53
|
recursos, diagnóstico, inspeção e gates negativos/positivo. Em dois casos,
|
|
46
|
-
criação
|
|
54
|
+
criação, edição, renomeação, remoção e preservação de índices alterados
|
|
55
|
+
exercitam o ciclo todo.
|
|
47
56
|
Aplicações executam testes HTTP reais em
|
|
48
57
|
portas efêmeras, encerradas ao fim. Legado e monorepo preservam seus arquivos.
|
|
49
58
|
Ferramentas copiadas ficam sem downloads: o check negativo é esperado.
|