@pathbit/agentdevkit 0.0.8 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pbai4devs/COMPATIBILIDADE.md +4 -2
- package/.pbai4devs/INSTRUCTIONS.md +4 -0
- package/.pbai4devs/commands/sincronizar/COMMAND.md +4 -3
- package/.pbai4devs/rules/documentacao/RULE.md +4 -0
- package/.pbai4devs/rules/qualidade/RULE.md +4 -2
- package/.pbai4devs/scripts/README.md +3 -3
- package/.pbai4devs/scripts/inspecionar-projeto/README.md +31 -0
- package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +142 -1
- package/.pbai4devs/scripts/inspecionar-projeto/tests/TEST.mjs +168 -1
- package/.pbai4devs/scripts/lib/native-paths.mjs +35 -0
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +3 -0
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +73 -29
- package/.pbai4devs/scripts/sincronizar-agentes/tests/COEXISTENCE.mjs +190 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +8 -2
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +28 -28
- package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +56 -8
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +92 -1
- package/.pbai4devs/skills/produto/criar-design-doc/SKILL.md +2 -0
- package/.pbai4devs/skills/produto/criar-prd/SKILL.md +2 -0
- package/.pbai4devs/skills/produto/criar-ux-ui/SKILL.md +2 -0
- package/.pbai4devs/skills/projeto/atualizar-documentacao/SKILL.md +18 -4
- package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +2 -0
- package/.pbai4devs/skills/projeto/criar-recurso/SKILL.md +1 -0
- package/.pbai4devs/skills/projeto/sincronizar-agentes/SKILL.md +3 -2
- package/.pbai4devs/skills/specs/criar-spec/SKILL.md +1 -0
- package/.pbai4devs/skills/specs/gerenciar-roadmap/SKILL.md +2 -1
- package/.pbai4devs/templates/README.md +4 -1
- package/.pbai4devs/templates/configuracao/TEMPLATE.md +6 -0
- package/CHANGELOG.md +10 -0
- package/README.en.md +3 -2
- package/README.es.md +3 -2
- package/README.md +3 -2
- package/docs/README.md +1 -0
- package/docs/guides/README.md +1 -0
- package/docs/guides/cli.md +2 -0
- package/docs/guides/documentation.md +63 -0
- package/docs/guides/installation.md +12 -5
- package/docs/guides/resources.md +6 -5
- package/docs/guides/troubleshooting.md +6 -6
- package/docs/guides/workflow.md +2 -0
- package/docs/maintainers/packages.md +1 -1
- package/docs/product/design.md +12 -0
- package/docs/product/prd.md +4 -1
- package/docs/product/roadmap.md +2 -0
- package/docs/specs/README.md +1 -0
- package/docs/specs/v0.0.9/README.md +9 -0
- package/docs/specs/v0.0.9/SPEC.md +68 -0
- package/package.json +3 -2
- package/src/cli.mjs +3 -0
- package/src/i18n.mjs +3 -0
- package/src/install.mjs +84 -16
|
@@ -7,6 +7,7 @@ import { requireNode, projectRoot, isMain } from '../lib/runtime.mjs';
|
|
|
7
7
|
import { buildCatalog } from '../lib/catalog.mjs';
|
|
8
8
|
import { resourcePath } from '../lib/resource-path.mjs';
|
|
9
9
|
import { adapterManifest, resolveAdapters } from '../lib/adapters.mjs';
|
|
10
|
+
import { nativePathBoundary } from '../lib/native-paths.mjs';
|
|
10
11
|
import { mergeGitignore, validateIgnoreClients, renderClientIgnore, isGeneratedOutput, planLocalIgnore, applyLocalIgnore, checkLocalIgnore, assertLocalIgnoreUnchanged, ignoreBegin, ignoreEnd } from '../lib/gitignore.mjs';
|
|
11
12
|
|
|
12
13
|
const marker = '<!-- gerado: sincronizar-agentes -->';
|
|
@@ -90,6 +91,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
90
91
|
return current;
|
|
91
92
|
}
|
|
92
93
|
function scan(value, { markdownOnly = false, prune = false } = {}) {
|
|
94
|
+
if (preserveBoundary(value)) return [];
|
|
93
95
|
const path = safe(value);
|
|
94
96
|
const item = stat(path);
|
|
95
97
|
if (!item) return [];
|
|
@@ -99,6 +101,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
99
101
|
function visit(folder) {
|
|
100
102
|
for (const name of readdirSync(safe(folder)).sort(compare)) {
|
|
101
103
|
const child = `${folder}/${name}`;
|
|
104
|
+
if (preserveBoundary(child)) continue;
|
|
102
105
|
const info = stat(safe(child));
|
|
103
106
|
if (prune && isGeneratedOutput(child, info.isDirectory())) continue;
|
|
104
107
|
if (info.isDirectory()) {
|
|
@@ -119,7 +122,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
119
122
|
const marked = text.split('\n').includes(marker) || text.startsWith(tomlMarker + '\n');
|
|
120
123
|
return isReference(text) && (marked || recorded);
|
|
121
124
|
};
|
|
122
|
-
const onlyReferences = value => { const files = scan(value); return files.length > 0 && files.every(file => /\.(md|mdc|toml)$/.test(file) && referenceFile(file)); };
|
|
125
|
+
const onlyReferences = value => { const files = scan(value); return files.length > 0 && ![...preserved.keys()].some(path => path === value || path.startsWith(`${value}/`)) && files.every(file => /\.(md|mdc|toml)$/.test(file) && referenceFile(file)); };
|
|
123
126
|
const protectedPath = value => /^(workflows(?:\/|$)|settings\.json$|extensions\.json$)/.test(value.slice(value.indexOf('/') + 1).toLowerCase());
|
|
124
127
|
const agents = new Map();
|
|
125
128
|
const formats = new Map();
|
|
@@ -133,10 +136,41 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
133
136
|
const installed = stat(safe(stateFile)) ? JSON.parse(read(safe(stateFile))) : null;
|
|
134
137
|
const configText = configExists ? read(safe(config)) : adapterManifest(root, { clients: installed?.clients });
|
|
135
138
|
const rows = configText.split('\n');
|
|
139
|
+
const entryRegistry = '.pbai4devs/estado/derived.json';
|
|
140
|
+
const previousEntries = stat(safe(entryRegistry)) ? JSON.parse(read(safe(entryRegistry))) : { schemaVersion: 1, entries: {} };
|
|
141
|
+
if (previousEntries?.schemaVersion !== 1 || !previousEntries.entries || typeof previousEntries.entries !== 'object' || Array.isArray(previousEntries.entries) || Object.entries(previousEntries.entries).some(([name, hash]) => !['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'].includes(name) || !/^[a-f0-9]{64}$/.test(hash))) fail('Registro de entradas derivadas invalido.');
|
|
142
|
+
const previousOutputs = previousEntries.outputs ?? {};
|
|
143
|
+
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.');
|
|
144
|
+
const ownedPaths = new Set([...Object.keys(installed?.files || {}), ...Object.keys(installed?.blocks || {}), ...Object.keys(previousEntries.entries), ...Object.keys(previousOutputs)]);
|
|
145
|
+
const nativeRoots = new Set(rows.map(row => row.split('|')).filter(([type]) => ['agente', 'gitignore'].includes(type)).map(([, target]) => target));
|
|
146
|
+
const preserved = new Map();
|
|
147
|
+
const remember = (path, reason = 'user-owned') => { preserved.set(path, { path, reason }); return true; };
|
|
148
|
+
function preserveBoundary(value) {
|
|
149
|
+
if (!nativeRoots.has(value.split('/')[0]) && !['AGENTS.md', 'CLAUDE.md', 'GEMINI.md', 'README.md'].includes(value)) return false;
|
|
150
|
+
const boundary = nativePathBoundary(root, value, ownedPaths, { preserveRootAliases: true });
|
|
151
|
+
return boundary ? remember(boundary.path, boundary.reason) : false;
|
|
152
|
+
}
|
|
153
|
+
function claimed(value) {
|
|
154
|
+
if (ownedPaths.has(value)) return true;
|
|
155
|
+
const info = stat(safe(value));
|
|
156
|
+
if (!info?.isFile() || info.size > 1024 * 1024) return false;
|
|
157
|
+
const text = read(safe(value));
|
|
158
|
+
return text.split('\n').includes(marker) || text.startsWith(tomlMarker + '\n');
|
|
159
|
+
}
|
|
160
|
+
function preserveUnknown(value) {
|
|
161
|
+
if (preserveBoundary(value)) return true;
|
|
162
|
+
const info = stat(safe(value));
|
|
163
|
+
return info && !referenceFile(value) && !claimed(value) ? remember(value) : false;
|
|
164
|
+
}
|
|
165
|
+
function preserveNativeType(value, info, type) {
|
|
166
|
+
if (!info || info[type]()) return false;
|
|
167
|
+
if ([...ownedPaths].some(path => path === value || path.startsWith(`${value}/`))) fail(`Tipo alterado em caminho gerenciado: ${value}`);
|
|
168
|
+
return remember(value, info.isDirectory() ? 'existing-directory' : info.isFile() ? 'existing-file' : 'existing-other');
|
|
169
|
+
}
|
|
136
170
|
function ignoreRoot(target) {
|
|
137
171
|
if (!/^\.[^/]+$/.test(target) || reserved.has(target.toLowerCase())) fail(`Pasta de agente invalida: ${target}`);
|
|
138
|
-
const info = stat(
|
|
139
|
-
|
|
172
|
+
const info = preserveBoundary(target) ? null : stat(safe(target));
|
|
173
|
+
preserveNativeType(target, info, 'isDirectory');
|
|
140
174
|
for (const known of ignoreRoots) if (known !== target && known.toLowerCase() === target.toLowerCase()) fail('Raizes de agentes diferem somente por maiusculas/minusculas.');
|
|
141
175
|
ignoreRoots.add(target);
|
|
142
176
|
}
|
|
@@ -145,7 +179,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
145
179
|
const fields = row.split('|');
|
|
146
180
|
if (fields.length < 2 || fields.length > 4) fail('Campos invalidos no manifesto.');
|
|
147
181
|
const [type, target, source = '', metadata = ''] = fields;
|
|
148
|
-
safe(target);
|
|
182
|
+
if (!preserveBoundary(target)) safe(target);
|
|
149
183
|
if (type === 'agente') {
|
|
150
184
|
if (fields.length > 3) fail(`Pasta de agente invalida: ${target}`);
|
|
151
185
|
ignoreRoot(target);
|
|
@@ -191,6 +225,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
191
225
|
const entries = new Map();
|
|
192
226
|
const folded = new Map();
|
|
193
227
|
function add(target, source, metadata = '') {
|
|
228
|
+
if (preserveUnknown(target)) return;
|
|
194
229
|
safe(target);
|
|
195
230
|
const alias = folded.get(target.toLowerCase());
|
|
196
231
|
if (alias && alias !== target) fail(`Destinos diferem somente por maiusculas/minusculas: ${target}`);
|
|
@@ -240,14 +275,15 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
240
275
|
if (custom) backup.add(value);
|
|
241
276
|
}
|
|
242
277
|
function cleanScope(value) {
|
|
278
|
+
if (preserveBoundary(value)) return;
|
|
243
279
|
if (!stat(safe(value))) return;
|
|
244
280
|
if (stat(safe(value)).isDirectory() && !scan(value).length) return;
|
|
245
|
-
if (
|
|
246
|
-
else if (
|
|
247
|
-
else
|
|
248
|
-
else conflicts.add(value);
|
|
281
|
+
if (onlyReferences(value)) planRemove(value);
|
|
282
|
+
else if (claimed(value)) { if (force) planRemove(value, true); else conflicts.add(value); }
|
|
283
|
+
else remember(value);
|
|
249
284
|
}
|
|
250
285
|
const registries = new Map();
|
|
286
|
+
const discoveryOutputs = {};
|
|
251
287
|
for (const [target, content] of built.outputs) {
|
|
252
288
|
const info = stat(safe(target));
|
|
253
289
|
if (info && !info.isFile()) fail(`Registro nao e arquivo: ${target}`);
|
|
@@ -259,22 +295,22 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
259
295
|
for (const agent of discoveries) {
|
|
260
296
|
for (const kind of ['skills', 'rules', 'agents']) {
|
|
261
297
|
const manifest = `${agent}/${kind}.json`;
|
|
298
|
+
if (preserveBoundary(manifest)) continue;
|
|
262
299
|
const paths = [...entries].filter(([target, value]) => target.startsWith(`${agent}/`) && sources.get(value.source) === kind)
|
|
263
300
|
.map(([target]) => kind === 'skills' ? posix.dirname(target) : target).sort(compare);
|
|
264
301
|
const content = JSON.stringify({ entries: paths.map(path => ({ path })) }, null, 2) + '\n';
|
|
265
302
|
const info = stat(safe(manifest));
|
|
303
|
+
if (preserveNativeType(manifest, info, 'isFile')) continue;
|
|
266
304
|
if (info && !info.isFile()) fail(`Registro nao e arquivo: ${manifest}`);
|
|
305
|
+
if (info && !ownedPaths.has(manifest)) { remember(manifest); continue; }
|
|
306
|
+
const current = info ? readFileSync(absolute(manifest)) : null;
|
|
307
|
+
const priorHashes = [previousOutputs[manifest], installed?.files?.[manifest]];
|
|
308
|
+
const unchanged = current === null || priorHashes.includes(createHash('sha256').update(current).digest('hex'));
|
|
309
|
+
if (!unchanged && !force) conflicts.add(manifest);
|
|
310
|
+
if (!unchanged && force) backup.add(manifest);
|
|
311
|
+
const nextHash = createHash('sha256').update(content).digest('hex');
|
|
312
|
+
if (installed?.files?.[manifest] !== nextHash) discoveryOutputs[manifest] = nextHash;
|
|
267
313
|
if (!info || readFileSync(absolute(manifest), 'utf8') !== content) {
|
|
268
|
-
let managedManifest = false;
|
|
269
|
-
if (info) {
|
|
270
|
-
try {
|
|
271
|
-
const previous = JSON.parse(read(absolute(manifest)));
|
|
272
|
-
managedManifest = Object.keys(previous).length === 1 && Array.isArray(previous.entries) && previous.entries.every(entry =>
|
|
273
|
-
entry && Object.keys(entry).length === 1 && typeof entry.path === 'string' && entry.path.startsWith(`${agent}/${kind}/`) && !entry.path.split('/').some(part => !part || part === '..' || part === '.')
|
|
274
|
-
);
|
|
275
|
-
} catch { /* Um manifesto customizado ou corrompido requer --force e backup. */ }
|
|
276
|
-
}
|
|
277
|
-
if (info && !managedManifest && !force) conflicts.add(manifest);
|
|
278
314
|
if (info && force) backup.add(manifest);
|
|
279
315
|
registries.set(manifest, content);
|
|
280
316
|
}
|
|
@@ -289,11 +325,14 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
289
325
|
...sorted(ignoreRoots).map(folder => [`${folder}/.gitignore`, '.pbai4devs/templates/gitignore/agents.ignore']),
|
|
290
326
|
]);
|
|
291
327
|
for (const [target, template] of ignoreTemplates) {
|
|
328
|
+
if (preserveBoundary(target)) continue;
|
|
292
329
|
if (!stat(safe(template))?.isFile()) fail(`Template .gitignore ausente ou invalido: ${template}`);
|
|
293
330
|
const info = stat(safe(target));
|
|
331
|
+
if (target !== '.pbai4devs/.gitignore' && preserveNativeType(target, info, 'isFile')) continue;
|
|
294
332
|
if (info && !info.isFile()) fail(`.gitignore nao e arquivo: ${target}`);
|
|
295
333
|
let merged;
|
|
296
334
|
const current = info ? readFileSync(absolute(target), 'utf8') : '';
|
|
335
|
+
if (target !== '.pbai4devs/.gitignore' && info && !ownedPaths.has(target) && !current.split(/\r?\n/).some(line => [ignoreBegin, ignoreEnd].includes(line))) { remember(target); continue; }
|
|
297
336
|
try {
|
|
298
337
|
let patterns = readFileSync(absolute(template), 'utf8');
|
|
299
338
|
if (target !== '.pbai4devs/.gitignore') patterns = renderClientIgnore(patterns, ignoreClients[posix.dirname(target)]);
|
|
@@ -349,18 +388,20 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
349
388
|
for (const kind of [...auxiliary, 'README.md', 'INSTRUCTIONS.md']) cleanScope(`${agent}/${kind}`);
|
|
350
389
|
for (const kind of Object.keys(principals)) {
|
|
351
390
|
const scope = `${agent}/${kind}`;
|
|
391
|
+
if (preserveBoundary(scope)) continue;
|
|
352
392
|
const info = stat(safe(scope));
|
|
353
393
|
if (!info) continue;
|
|
354
394
|
const files = scan(scope);
|
|
355
|
-
if (!info.isDirectory()) { if (force) planRemove(scope, true); else conflicts.add(scope); continue; }
|
|
395
|
+
if (!info.isDirectory()) { if (claimed(scope)) { if (force) planRemove(scope, true); else conflicts.add(scope); } else remember(scope); continue; }
|
|
356
396
|
for (const file of files) if (!expected.has(file)) {
|
|
357
397
|
if (referenceFile(file)) planRemove(file);
|
|
358
|
-
else if (force) planRemove(file, true);
|
|
359
|
-
else
|
|
398
|
+
else if (claimed(file)) { if (force) planRemove(file, true); else conflicts.add(file); }
|
|
399
|
+
else remember(file);
|
|
360
400
|
}
|
|
361
401
|
function directories(value) {
|
|
362
402
|
for (const name of readdirSync(safe(value))) {
|
|
363
403
|
const child = `${value}/${name}`;
|
|
404
|
+
if (preserveBoundary(child)) continue;
|
|
364
405
|
if (!stat(safe(child))?.isDirectory()) continue;
|
|
365
406
|
if (![...expected.keys()].some(target => target.startsWith(child + '/'))) cleanScope(child);
|
|
366
407
|
directories(child);
|
|
@@ -377,18 +418,19 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
377
418
|
}
|
|
378
419
|
for (const { target } of missing) cleanScope(target);
|
|
379
420
|
const rootEntries = new Map();
|
|
380
|
-
const entryRegistry = '.pbai4devs/estado/derived.json';
|
|
381
421
|
if (generateEntrypoints) {
|
|
382
|
-
const previous =
|
|
383
|
-
if (previous?.schemaVersion !== 1 || !previous.entries || typeof previous.entries !== 'object' || Array.isArray(previous.entries) || Object.entries(previous.entries).some(([name, hash]) => !['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'].includes(name) || !/^[a-f0-9]{64}$/.test(hash))) fail('Registro de entradas derivadas invalido.');
|
|
422
|
+
const previous = previousEntries;
|
|
384
423
|
const next = { schemaVersion: 1, entries: {} };
|
|
424
|
+
if (Object.keys(discoveryOutputs).length) next.outputs = discoveryOutputs;
|
|
385
425
|
for (const name of resolveAdapters(installed?.clients).entrypoints) {
|
|
386
426
|
const template = `.pbai4devs/templates/entrypoints/${name}`;
|
|
387
427
|
if (!stat(safe(template))?.isFile()) continue;
|
|
428
|
+
if (preserveBoundary(name)) continue;
|
|
388
429
|
const info = stat(safe(name));
|
|
430
|
+
if (preserveNativeType(name, info, 'isFile')) continue;
|
|
389
431
|
const current = info?.isFile() ? readFileSync(absolute(name)) : null;
|
|
390
432
|
const previousHash = previous.entries[name] ?? installed?.files?.[name];
|
|
391
|
-
if (info && (!current || createHash('sha256').update(current).digest('hex') !== previousHash)) continue;
|
|
433
|
+
if (info && (!current || createHash('sha256').update(current).digest('hex') !== previousHash)) { remember(name); continue; }
|
|
392
434
|
if (current && createHash('sha256').update(current).digest('hex') === installed?.files?.[name]) {
|
|
393
435
|
rootEntries.set(name, current.toString('utf8'));
|
|
394
436
|
continue;
|
|
@@ -399,17 +441,18 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
399
441
|
rootEntries.set(name, content);
|
|
400
442
|
}
|
|
401
443
|
const content = JSON.stringify(next, null, 2) + '\n';
|
|
402
|
-
if ((Object.keys(next.entries).length || stat(safe(entryRegistry))) && (!stat(safe(entryRegistry)) || readFileSync(absolute(entryRegistry), 'utf8') !== content)) registries.set(entryRegistry, content);
|
|
444
|
+
if ((Object.keys(next.entries).length || Object.keys(discoveryOutputs).length || stat(safe(entryRegistry))) && (!stat(safe(entryRegistry)) || readFileSync(absolute(entryRegistry), 'utf8') !== content)) registries.set(entryRegistry, content);
|
|
403
445
|
}
|
|
404
446
|
const ownedOutputs = sorted([...targets, ...built.outputs.keys(), '.pbai4devs/espelhos.conf', ...rootEntries.keys(), ...(generateEntrypoints && (stat(safe(entryRegistry)) || registries.has(entryRegistry)) ? [entryRegistry] : []),
|
|
405
447
|
...[...discoveries].flatMap(agent => ['skills', 'rules', 'agents'].map(kind => `${agent}/${kind}.json`)),
|
|
406
448
|
...[...ignoreTemplates.keys()].filter(target => {
|
|
407
449
|
if (target === '.pbai4devs/.gitignore') return false;
|
|
450
|
+
if (preserved.has(target) || preserveBoundary(target)) return false;
|
|
408
451
|
const content = ignoreFiles.get(target) ?? readFileSync(absolute(target), 'utf8');
|
|
409
452
|
const start = content.indexOf(ignoreBegin), finish = content.indexOf(ignoreEnd);
|
|
410
453
|
return start >= 0 && finish > start && !(content.slice(0, start) + content.slice(finish + ignoreEnd.length)).trim();
|
|
411
454
|
}),
|
|
412
|
-
]);
|
|
455
|
+
]).filter(path => ![...preserved.keys()].some(value => path === value || path.startsWith(`${value}/`)));
|
|
413
456
|
const localPlan = localIgnores ? planLocalIgnore(root, { outputs: ownedOutputs }) : null;
|
|
414
457
|
const localStatus = localPlan ? checkLocalIgnore(localPlan) : { changed: 0, missing: [], trackedOutputs: [] };
|
|
415
458
|
const removals = topPaths(remove);
|
|
@@ -442,7 +485,8 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
442
485
|
const summary = { changed: changed.length + rootEntryChanges.length, stale: removals.length, conflicts: conflicts.size, missing: missing.length, legacy: legacy.length, registries: registries.size, gitignores: ignoreFiles.size, manifests: configExists ? 0 : 1, localIgnores: localStatus.changed + localStatus.missing.length + localStatus.trackedOutputs.length };
|
|
443
486
|
const dirty = Object.values(summary).some(Boolean);
|
|
444
487
|
output(`${summary.changed} desatualizadas; ${summary.stale} obsoletas; ${summary.conflicts} conflitos; ${summary.missing} origens nativas ausentes; ${summary.legacy} documentos antigos; ${summary.registries} indices derivados; ${summary.gitignores} gitignores; ${summary.localIgnores} exclusoes locais.`);
|
|
445
|
-
|
|
488
|
+
const preservedPaths = sorted(preserved.keys()).map(path => preserved.get(path));
|
|
489
|
+
if (check) return { ...summary, ownedOutputs, preserved: preservedPaths, exitCode: dirty ? 1 : 0 };
|
|
446
490
|
if (conflicts.size) fail(`Use --force para reconstruir somente os caminhos gerenciados: ${sorted(conflicts).join(', ')}`);
|
|
447
491
|
if (localStatus.missing.length) fail(`Regras humanas reabrem derivados locais: ${localStatus.missing.join(', ')}`);
|
|
448
492
|
if (localPlan) assertLocalIgnoreUnchanged(localPlan);
|
|
@@ -548,7 +592,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
548
592
|
if (failures.length) throw new AggregateError([error, ...failures], `${error.message}; rollback incompleto: ${failures.map(failure => failure.message).join('; ')}`);
|
|
549
593
|
throw error;
|
|
550
594
|
}
|
|
551
|
-
return { ...summary, ownedOutputs, exitCode: 0, ...(backupRoot ? { backupRoot } : {}) };
|
|
595
|
+
return { ...summary, ownedOutputs, preserved: preservedPaths, exitCode: 0, ...(backupRoot ? { backupRoot } : {}) };
|
|
552
596
|
}
|
|
553
597
|
|
|
554
598
|
export function main(args = process.argv.slice(2)) {
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { projectRoot } from '../../lib/runtime.mjs';
|
|
9
|
+
import { nativePathBoundary } from '../../lib/native-paths.mjs';
|
|
10
|
+
import { synchronize } from '../SCRIPT.mjs';
|
|
11
|
+
|
|
12
|
+
const hash = content => createHash('sha256').update(content).digest('hex');
|
|
13
|
+
const put = (root, path, text) => { fs.mkdirSync(dirname(join(root, path)), { recursive: true }); fs.writeFileSync(join(root, path), text); };
|
|
14
|
+
const run = (root, options = {}) => synchronize({ root, localIgnores: false, output() {}, ...options });
|
|
15
|
+
function fixture(t) {
|
|
16
|
+
const root = fs.realpathSync(fs.mkdtempSync(join(tmpdir(), 'agentdevkit-coexist-')));
|
|
17
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
18
|
+
put(root, '.pbai4devs/espelhos.conf', 'agente|.claude|plano\nagente|.codex|plano\nagente|.agents|plano\ndescoberta|.agents|antigravity\n');
|
|
19
|
+
put(root, '.pbai4devs/skills/demo/SKILL.md', '---\nname: demo\ndescription: Demo.\n---\n\n# Demo\n');
|
|
20
|
+
put(root, '.pbai4devs/agents/revisor/AGENT.md', '---\nname: revisor\ndescription: Revisor.\n---\n\n# Revisor\n');
|
|
21
|
+
for (const name of ['agents.ignore', 'shared.ignore', 'local.ignore', 'clients.json']) put(root, `.pbai4devs/templates/gitignore/${name}`, fs.readFileSync(join(projectRoot, '.pbai4devs/templates/gitignore', name)));
|
|
22
|
+
for (const name of ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']) put(root, `.pbai4devs/templates/entrypoints/${name}`, `---\ndescription: Entry.\n---\n\n# ${name}\n\nLeia AGENTS.md.\n`);
|
|
23
|
+
return root;
|
|
24
|
+
}
|
|
25
|
+
function link(t, root, target, path, type = 'file') {
|
|
26
|
+
fs.mkdirSync(dirname(join(root, path)), { recursive: true });
|
|
27
|
+
try { fs.symlinkSync(target, join(root, path), type); return true; }
|
|
28
|
+
catch (error) { if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error.code)) { t.skip('Sistema não permite criar este link simbólico.'); return false; } throw error; }
|
|
29
|
+
}
|
|
30
|
+
function snapshot(root) {
|
|
31
|
+
const result = {};
|
|
32
|
+
function visit(prefix = '') {
|
|
33
|
+
for (const name of fs.readdirSync(join(root, prefix)).sort()) {
|
|
34
|
+
const path = prefix ? `${prefix}/${name}` : name, info = fs.lstatSync(join(root, path));
|
|
35
|
+
result[path] = info.isSymbolicLink() ? ['link', fs.readlinkSync(join(root, path))] : info.isDirectory() ? ['directory', info.mode & 0o777] : ['file', info.mode & 0o777, hash(fs.readFileSync(join(root, path)))];
|
|
36
|
+
if (info.isDirectory()) visit(path);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
visit(); return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test('coexiste com aliases nativos, configuração e colisão humana sem alterar os destinos', t => {
|
|
43
|
+
const root = fixture(t);
|
|
44
|
+
put(root, '.claude/skills/existing/SKILL.md', '# Human skill\n');
|
|
45
|
+
put(root, '.claude/agents/revisor.md', '# Human reviewer\n');
|
|
46
|
+
put(root, '.claude/.gitignore', '# Human ignore\n*.private\n');
|
|
47
|
+
put(root, 'AGENTS.md', '# Human instructions\n');
|
|
48
|
+
put(root, 'external/config.json', '{"human":true}\n');
|
|
49
|
+
if (!link(t, root, '../.claude/skills', '.codex/skills', 'dir') || !link(t, root, 'AGENTS.md', 'CLAUDE.md') || !link(t, root, '../external/config.json', '.claude/settings.local.json')) return;
|
|
50
|
+
const original = snapshot(root), result = run(root, { force: true });
|
|
51
|
+
for (const [path, before] of Object.entries(original)) assert.deepEqual(snapshot(root)[path], before, path);
|
|
52
|
+
for (const path of ['.codex/skills', 'CLAUDE.md', '.claude/agents/revisor.md', '.claude/.gitignore', '.claude/settings.local.json']) assert.ok(result.preserved.some(item => item.path === path), path);
|
|
53
|
+
assert.ok(result.ownedOutputs.includes('.agents/skills/demo/SKILL.md'));
|
|
54
|
+
assert.ok(result.ownedOutputs.every(path => !path.startsWith('.codex/skills/') && path !== '.claude/agents/revisor.md' && path !== 'CLAUDE.md'));
|
|
55
|
+
const stable = snapshot(root);
|
|
56
|
+
assert.equal(run(root, { check: true }).exitCode, 0);
|
|
57
|
+
run(root, { force: true });
|
|
58
|
+
assert.deepEqual(snapshot(root), stable);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('preserva raiz simbólica externa, link pendente e loop sem abri-los', t => {
|
|
62
|
+
for (const targetKind of ['external', 'dangling', 'loop']) {
|
|
63
|
+
const root = fixture(t), outside = fixture(t);
|
|
64
|
+
put(outside, 'sentinel.txt', 'outside\n');
|
|
65
|
+
const beforeOutside = snapshot(outside);
|
|
66
|
+
const target = targetKind === 'external' ? outside : targetKind === 'loop' ? '.codex' : join(outside, 'missing');
|
|
67
|
+
if (!link(t, root, target, '.codex', targetKind === 'external' && process.platform === 'win32' ? 'junction' : 'dir')) return;
|
|
68
|
+
const result = run(root, { force: true });
|
|
69
|
+
assert.equal(fs.readlinkSync(join(root, '.codex')), target);
|
|
70
|
+
assert.deepEqual(snapshot(outside), beforeOutside);
|
|
71
|
+
assert.ok(result.preserved.some(item => item.path === '.codex' && item.reason === 'symbolic-link'));
|
|
72
|
+
assert.equal(run(root, { check: true }).exitCode, 0);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('ignore nativo simbólico permanece opaco na política local do Git', t => {
|
|
77
|
+
const root = fixture(t), outside = fixture(t);
|
|
78
|
+
const git = spawnSync('git', ['-C', root, 'init', '-q'], { encoding: 'utf8' });
|
|
79
|
+
assert.equal(git.status, 0, git.stderr);
|
|
80
|
+
put(outside, 'ignore', '# External human ignore\n!*.private\n');
|
|
81
|
+
if (!link(t, root, join(outside, 'ignore'), '.claude/.gitignore')) return;
|
|
82
|
+
const before = snapshot(outside);
|
|
83
|
+
const result = run(root, { localIgnores: true });
|
|
84
|
+
assert.ok(result.preserved.some(item => item.path === '.claude/.gitignore'));
|
|
85
|
+
assert.deepEqual(snapshot(outside), before);
|
|
86
|
+
assert.equal(run(root, { localIgnores: true, check: true }).exitCode, 0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('link em caminho registrado continua sendo erro antes de qualquer escrita', t => {
|
|
90
|
+
for (const source of ['files', 'blocks', 'derived', 'outputs']) {
|
|
91
|
+
const root = fixture(t), path = source === 'derived' ? 'CLAUDE.md' : source === 'outputs' ? '.agents/skills.json' : '.claude/skills/demo/SKILL.md';
|
|
92
|
+
if (!link(t, root, 'missing', path)) return;
|
|
93
|
+
if (source === 'derived' || source === 'outputs') put(root, '.pbai4devs/estado/derived.json', JSON.stringify({ schemaVersion: 1, entries: source === 'derived' ? { [path]: 'a'.repeat(64) } : {}, ...(source === 'outputs' ? { outputs: { [path]: 'a'.repeat(64) } } : {}) }));
|
|
94
|
+
else put(root, '.pbai4devs/estado/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: source === 'files' ? { [path]: 'a'.repeat(64) } : {}, blocks: source === 'blocks' ? { [path]: 'a'.repeat(64) } : {} }));
|
|
95
|
+
const before = snapshot(root);
|
|
96
|
+
assert.throws(() => run(root, { force: true }), /Link simbolico em caminho gerenciado/);
|
|
97
|
+
assert.deepEqual(snapshot(root), before);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('manifesto standalone tem propriedade explícita e alterações humanas exigem reparo autorizado', t => {
|
|
102
|
+
const root = fixture(t); run(root);
|
|
103
|
+
const path = '.agents/skills.json', original = fs.readFileSync(join(root, path), 'utf8');
|
|
104
|
+
const derived = JSON.parse(fs.readFileSync(join(root, '.pbai4devs/estado/derived.json')));
|
|
105
|
+
assert.equal(derived.outputs[path], hash(original));
|
|
106
|
+
put(root, path, '{"entries":[{"path":".agents/skills/custom"}]}\n');
|
|
107
|
+
const before = snapshot(root);
|
|
108
|
+
assert.throws(() => run(root), /--force/);
|
|
109
|
+
assert.deepEqual(snapshot(root), before);
|
|
110
|
+
const result = run(root, { force: true });
|
|
111
|
+
assert.equal(fs.readFileSync(join(root, path), 'utf8'), original);
|
|
112
|
+
assert.equal(fs.readFileSync(join(result.backupRoot, path), 'utf8'), '{"entries":[{"path":".agents/skills/custom"}]}\n');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('manifesto nativo omite colisões preservadas e inclui somente apontamentos ativos', t => {
|
|
116
|
+
const root = fixture(t), collision = '.agents/skills/demo/SKILL.md';
|
|
117
|
+
put(root, collision, '# Human demo\n');
|
|
118
|
+
const result = run(root);
|
|
119
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(join(root, '.agents/skills.json'))).entries, []);
|
|
120
|
+
assert.ok(result.preserved.some(item => item.path === collision));
|
|
121
|
+
assert.ok(!result.ownedOutputs.includes(collision));
|
|
122
|
+
assert.equal(run(root, { check: true }).exitCode, 0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('preserva diretórios humanos no lugar de índices, ignores e entradas, e arquivo no lugar de raiz', t => {
|
|
126
|
+
const root = fixture(t);
|
|
127
|
+
for (const path of ['.agents/skills.json', '.claude/.gitignore', 'CLAUDE.md']) put(root, `${path}/keep.txt`, 'Human data\n');
|
|
128
|
+
put(root, '.codex', 'Human native configuration\n');
|
|
129
|
+
const before = snapshot(root), result = run(root, { force: true });
|
|
130
|
+
for (const [path, value] of Object.entries(before)) assert.deepEqual(snapshot(root)[path], value, path);
|
|
131
|
+
for (const path of ['.agents/skills.json', '.claude/.gitignore', 'CLAUDE.md', '.codex']) {
|
|
132
|
+
assert.ok(result.preserved.some(item => item.path === path), path);
|
|
133
|
+
assert.ok(result.ownedOutputs.every(output => output !== path && !output.startsWith(`${path}/`)), path);
|
|
134
|
+
}
|
|
135
|
+
const stable = snapshot(root);
|
|
136
|
+
assert.equal(run(root, { check: true }).exitCode, 0);
|
|
137
|
+
run(root, { force: true });
|
|
138
|
+
assert.deepEqual(snapshot(root), stable);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('tipo alterado em caminho nativo registrado continua recusado antes de gravar', t => {
|
|
142
|
+
for (const [path, owned] of [
|
|
143
|
+
['.agents/skills.json', '.agents/skills.json'],
|
|
144
|
+
['.claude/.gitignore', '.claude/.gitignore'],
|
|
145
|
+
['CLAUDE.md', 'CLAUDE.md'],
|
|
146
|
+
['.codex', '.codex/skills/demo/SKILL.md'],
|
|
147
|
+
]) {
|
|
148
|
+
const root = fixture(t);
|
|
149
|
+
if (path === '.codex') put(root, path, 'Human root\n'); else put(root, `${path}/keep.txt`, 'Human directory\n');
|
|
150
|
+
put(root, '.pbai4devs/estado/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: { [owned]: 'a'.repeat(64) }, blocks: {} }));
|
|
151
|
+
const before = snapshot(root);
|
|
152
|
+
assert.throws(() => run(root, { force: true }), /caminho gerenciado/);
|
|
153
|
+
assert.deepEqual(snapshot(root), before);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('classificação preservável não encobre aliases por caixa nem caminhos fora do escopo', t => {
|
|
158
|
+
const root = fixture(t);
|
|
159
|
+
put(root, '.Claude/notes.txt', 'human\n');
|
|
160
|
+
assert.throws(() => nativePathBoundary(root, '.claude/skills'), /maiusculas\/minusculas/);
|
|
161
|
+
assert.throws(() => nativePathBoundary(root, '.claude/skills', [], { preserveRootAliases: true }), /maiusculas\/minusculas/);
|
|
162
|
+
for (const path of ['../outside', '/outside', '.claude//skills', '.claude/./skills', '.claude/skills\n']) assert.throws(() => nativePathBoundary(root, path), /Caminho invalido/);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('variantes humanas de entradas da raiz são preservadas sem criar duplicatas', t => {
|
|
166
|
+
const root = fixture(t);
|
|
167
|
+
for (const path of ['Readme.md', 'agents.md', 'claude.md', 'gemini.md']) put(root, path, `# Human ${path}\n`);
|
|
168
|
+
const before = snapshot(root), result = run(root, { force: true });
|
|
169
|
+
for (const [path, content] of Object.entries(before)) assert.deepEqual(snapshot(root)[path], content, path);
|
|
170
|
+
for (const [canonical, alias] of [['README.md', 'Readme.md'], ['AGENTS.md', 'agents.md'], ['CLAUDE.md', 'claude.md'], ['GEMINI.md', 'gemini.md']]) {
|
|
171
|
+
assert.deepEqual(nativePathBoundary(root, canonical, [], { preserveRootAliases: true }), { path: alias, reason: 'case-variant' });
|
|
172
|
+
if (canonical !== 'README.md') assert.ok(result.preserved.some(item => item.path === alias));
|
|
173
|
+
assert.ok(!result.ownedOutputs.includes(canonical));
|
|
174
|
+
assert.ok(!fs.readdirSync(root).includes(canonical), canonical);
|
|
175
|
+
}
|
|
176
|
+
const stable = snapshot(root);
|
|
177
|
+
assert.equal(run(root, { check: true }).exitCode, 0);
|
|
178
|
+
assert.deepEqual(snapshot(root), stable);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('variante de caixa de entrada registrada não esconde drift de propriedade', t => {
|
|
182
|
+
for (const owned of ['AGENTS.md', 'agents.md']) {
|
|
183
|
+
const root = fixture(t); put(root, 'agents.md', '# Renamed human entry\n');
|
|
184
|
+
assert.throws(() => nativePathBoundary(root, 'AGENTS.md', [owned], { preserveRootAliases: true }), /maiusculas\/minusculas/);
|
|
185
|
+
put(root, '.pbai4devs/estado/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: { [owned]: 'a'.repeat(64) }, blocks: {} }));
|
|
186
|
+
const before = snapshot(root);
|
|
187
|
+
assert.throws(() => run(root, { force: true }), /maiusculas\/minusculas/);
|
|
188
|
+
assert.deepEqual(snapshot(root), before);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
@@ -44,9 +44,15 @@ test('texto humano com formato de referência nunca comprova propriedade', t =>
|
|
|
44
44
|
const before = snapshot(root);
|
|
45
45
|
assert.equal(isReference(human), true, 'O parser de formato continua independente da propriedade.');
|
|
46
46
|
assert.equal(run(root, { check: true }).exitCode, 1);
|
|
47
|
-
|
|
48
|
-
assert.
|
|
47
|
+
const result = run(root);
|
|
48
|
+
assert.ok(result.preserved.some(item => item.path === path));
|
|
49
|
+
assert.ok(!result.ownedOutputs.includes(path));
|
|
49
50
|
assert.equal(fs.readFileSync(join(root, path), 'utf8'), human);
|
|
51
|
+
assert.equal(snapshot(root)[path], before[path]);
|
|
52
|
+
assert.equal(run(root, { check: true }).exitCode, 0);
|
|
53
|
+
const stable = snapshot(root);
|
|
54
|
+
run(root, { force: true });
|
|
55
|
+
assert.deepEqual(snapshot(root), stable);
|
|
50
56
|
}
|
|
51
57
|
});
|
|
52
58
|
|
|
@@ -11,6 +11,7 @@ import { toolWrapper } from '../../preparar-ferramentas/SCRIPT.mjs';
|
|
|
11
11
|
import { mergeGitignore, ignoreBegin, ignoreEnd, validateIgnoreClients } from '../../lib/gitignore.mjs';
|
|
12
12
|
import './LOCAL_IGNORE.mjs';
|
|
13
13
|
import './HUMAN_OWNERSHIP.mjs';
|
|
14
|
+
import './COEXISTENCE.mjs';
|
|
14
15
|
import { validateCatalog as validateExternalCatalog } from '../../gerenciar-skills-externas/SCRIPT.mjs';
|
|
15
16
|
|
|
16
17
|
const project = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
@@ -21,6 +22,7 @@ const source = '.pbai4devs/skills/projeto/demo/SKILL.md';
|
|
|
21
22
|
const read = (root, path) => readFileSync(join(root, path), 'utf8');
|
|
22
23
|
const write = (root, path, content) => { mkdirSync(dirname(join(root, path)), { recursive: true }); writeFileSync(join(root, path), content); };
|
|
23
24
|
const pointer = (origin = 'origem.md', title = 'Referencia') => `---\ndescription: Referencia.\n---\n\n# ${title}\n\n${marker}\nLeia e siga [origem](${origin}).\n`;
|
|
25
|
+
const recordOwned = (root, paths) => write(root, '.pbai4devs/estado/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: Object.fromEntries(paths.map(path => [path, createHash('sha256').update(readFileSync(join(root, path))).digest('hex')])), blocks: {} }));
|
|
24
26
|
const toolConfig = (name = 'demo') => ({ name, description: `Ferramenta ${name}.`, group: 'base', source: `https://github.com/fixture/${name}`, download: { provider: 'npm', package: name, registry: 'https://registry.npmjs.org/', repository: `fixture/${name}`, channel: 'latest' }, platforms: [`${process.platform}-${process.arch}`], scripts: { entry: 'SCRIPT.mjs' }, probe: ['--version'] });
|
|
25
27
|
const externalConfig = () => ({ schemaVersion: 1, allowlist: ['fixture/skills'], skills: [{ id: 'external-demo', repository: 'fixture/skills', commit: 'a'.repeat(40), path: 'skills/demo/SKILL.md', name: 'external-demo', license: 'MIT', use: 'Revisar exemplo externo.', status: 'reviewed', notes: 'Sem execução automática.' }] });
|
|
26
28
|
const run = (root, options = {}) => synchronize({ root, localIgnores: false, generateEntrypoints: false, output() {}, ...options });
|
|
@@ -334,7 +336,7 @@ test('registros são derivados, determinísticos e --check nunca os corrige', t
|
|
|
334
336
|
assertCheckReadonly(empty, {}, 0);
|
|
335
337
|
});
|
|
336
338
|
|
|
337
|
-
test('skills externas baixadas não entram nos índices;
|
|
339
|
+
test('skills externas baixadas não entram nos índices; cópias humanas permanecem intactas', t => {
|
|
338
340
|
const root = fixture(t);
|
|
339
341
|
write(root, '.pbai4devs/external-skills/README.md', '# Catálogo externo\n');
|
|
340
342
|
write(root, '.pbai4devs/external-skills/CATALOG.json', JSON.stringify(externalConfig()));
|
|
@@ -348,38 +350,34 @@ test('skills externas baixadas não entram nos índices; --force salva e limpa c
|
|
|
348
350
|
write(root, '.codex/external-skills/.local/upstream-0/SKILL.md', '# Cópia indevida\n');
|
|
349
351
|
write(root, '.claude/external-skills/README.md', pointer('../../.pbai4devs/external-skills/README.md'));
|
|
350
352
|
assertCheckReadonly(root);
|
|
351
|
-
rejected(root, {}, /--force/);
|
|
352
353
|
const result = run(root, { force: true });
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
assert.ok(existsSync(join(result.backupRoot, client, 'external-skills')));
|
|
356
|
-
}
|
|
354
|
+
assert.equal(existsSync(join(root, '.claude/external-skills')), false);
|
|
355
|
+
assert.ok(result.preserved.some(item => item.path === '.codex/external-skills'));
|
|
357
356
|
assert.equal(read(root, '.pbai4devs/external-skills/.local/upstream-0/SKILL.md'), sourceBefore);
|
|
358
|
-
assert.equal(read(
|
|
357
|
+
assert.equal(read(root, '.codex/external-skills/.local/upstream-0/SKILL.md'), '# Cópia indevida\n');
|
|
359
358
|
assertCheckReadonly(root, {}, 0);
|
|
360
359
|
});
|
|
361
360
|
|
|
362
|
-
test('--force
|
|
361
|
+
test('--force repara conteúdo registrado e preserva recursos humanos sem propriedade', t => {
|
|
363
362
|
const root = fixture(t);
|
|
364
363
|
run(root);
|
|
364
|
+
recordOwned(root, ['.claude/skills/projeto/demo/SKILL.md', '.kimi-code/agents/revisor.md']);
|
|
365
365
|
write(root, '.claude/scripts/local.sh', 'script humano\n');
|
|
366
366
|
write(root, '.claude/tools/program.exe', Buffer.from([1, 2, 3]));
|
|
367
367
|
write(root, '.claude/docs/plain.md', '# Documento auxiliar\n');
|
|
368
368
|
write(root, '.claude/skills/local.bin', 'custom\n');
|
|
369
369
|
write(root, '.claude/skills/projeto/demo/SKILL.md', 'corrompido\n');
|
|
370
370
|
write(root, '.kimi-code/agents/revisor.md', 'corrompido\n');
|
|
371
|
-
rmSync(join(root, '.codex/skills/outro'), { recursive: true });
|
|
372
|
-
write(root, '.codex/skills/outro', 'ancestral arquivo\n');
|
|
373
371
|
write(root, '.github/prompts/edited.prompt.md', pointer() + '\nRevisar [manual](bar).\n');
|
|
374
372
|
assertCheckReadonly(root);
|
|
375
373
|
assertCheckReadonly(root, { force: true });
|
|
376
374
|
rejected(root, {}, /--force/);
|
|
377
375
|
const result = run(root, { force: true });
|
|
378
376
|
assert.ok(result.backupRoot.startsWith(join(root, '.agent-tests')));
|
|
379
|
-
for (const path of ['.claude/
|
|
380
|
-
assert.deepEqual(readFileSync(join(
|
|
381
|
-
assert.equal(
|
|
382
|
-
assert.equal(
|
|
377
|
+
for (const path of ['.claude/skills/projeto/demo/SKILL.md', '.kimi-code/agents/revisor.md', '.github/prompts/edited.prompt.md']) assert.ok(existsSync(join(result.backupRoot, path)), `Backup ausente: ${path}`);
|
|
378
|
+
assert.deepEqual(readFileSync(join(root, '.claude/tools/program.exe')), Buffer.from([1, 2, 3]));
|
|
379
|
+
assert.equal(read(root, '.claude/scripts/local.sh'), 'script humano\n');
|
|
380
|
+
assert.equal(read(root, '.claude/skills/local.bin'), 'custom\n');
|
|
383
381
|
assert.equal(existsSync(join(root, '.github/prompts/edited.prompt.md')), false);
|
|
384
382
|
assert.match(read(root, '.codex/skills/outro/SKILL.md'), /name: outro/);
|
|
385
383
|
assert.match(read(root, '.github/workflows/ci.yml'), /workflow-preservado/);
|
|
@@ -466,8 +464,8 @@ test('metadados incompletos, ausentes e com corpo são rejeitados antes de alter
|
|
|
466
464
|
rejected(root, { force: true }, /Titulo/);
|
|
467
465
|
});
|
|
468
466
|
|
|
469
|
-
test('symlinks são rejeitados sem efeitos quando o sistema permite criá-los', t => {
|
|
470
|
-
for (const location of ['.
|
|
467
|
+
test('symlinks canônicos são rejeitados sem efeitos quando o sistema permite criá-los', t => {
|
|
468
|
+
for (const location of ['.pbai4devs/skills/projeto/demo/external.md', '.pbai4devs/skills.json', '.pbai4devs/espelhos.conf']) {
|
|
471
469
|
const root = fixture(t);
|
|
472
470
|
rmSync(join(root, location), { recursive: true, force: true });
|
|
473
471
|
try { symlinkSync(join(root, 'missing-target'), join(root, location)); }
|
|
@@ -500,6 +498,8 @@ test('registros não regulares são rejeitados sem efeitos', t => {
|
|
|
500
498
|
test('backup recusa raiz simbólica antes de remover conteúdo próprio', t => {
|
|
501
499
|
const root = fixture(t);
|
|
502
500
|
run(root);
|
|
501
|
+
recordOwned(root, ['.claude/skills/projeto/demo/SKILL.md']);
|
|
502
|
+
write(root, '.claude/skills/projeto/demo/SKILL.md', 'registrado alterado\n');
|
|
503
503
|
write(root, '.claude/scripts/own.sh', 'preservar\n');
|
|
504
504
|
const outside = join(root, 'outside');
|
|
505
505
|
mkdirSync(outside);
|
|
@@ -824,33 +824,33 @@ test('manifestos Antigravity acompanham skills, regras e agentes agrupados', t =
|
|
|
824
824
|
write(root, '.pbai4devs/agents/review/security/AGENT.md', '---\nname: security\ndescription: Perfil.\n---\n\n# Perfil\n');
|
|
825
825
|
write(root, '.pbai4devs/espelhos.conf', 'agente|.agents|agrupado\ndescoberta|.agents|antigravity\n');
|
|
826
826
|
const before = snapshot(root);
|
|
827
|
-
assert.equal(run(root, { check: true }).exitCode, 1);
|
|
827
|
+
assert.equal(run(root, { check: true, generateEntrypoints: true }).exitCode, 1);
|
|
828
828
|
assert.deepEqual(snapshot(root), before);
|
|
829
|
-
run(root);
|
|
829
|
+
run(root, { generateEntrypoints: true });
|
|
830
830
|
assert.deepEqual(JSON.parse(read(root, '.agents/skills.json')).entries, [{ path: '.agents/skills/projeto/demo' }]);
|
|
831
831
|
assert.deepEqual(JSON.parse(read(root, '.agents/rules.json')).entries, [{ path: '.agents/rules/projeto/style/RULE.md' }]);
|
|
832
832
|
assert.deepEqual(JSON.parse(read(root, '.agents/agents.json')).entries, [{ path: '.agents/agents/review/security/AGENT.md' }]);
|
|
833
833
|
write(root, '.pbai4devs/skills/projeto/new-skill/SKILL.md', skill.replace('name: demo', 'name: new-skill'));
|
|
834
|
-
run(root);
|
|
834
|
+
run(root, { generateEntrypoints: true });
|
|
835
835
|
assert.equal(JSON.parse(read(root, '.agents/skills.json')).entries.length, 2);
|
|
836
836
|
rmSync(join(root, '.pbai4devs/skills/projeto/demo'), { recursive: true });
|
|
837
|
-
run(root);
|
|
837
|
+
run(root, { generateEntrypoints: true });
|
|
838
838
|
assert.deepEqual(JSON.parse(read(root, '.agents/skills.json')).entries, [{ path: '.agents/skills/projeto/new-skill' }]);
|
|
839
839
|
assert.equal(existsSync(join(root, '.agents/skills/projeto/demo')), false);
|
|
840
|
-
assertCheckReadonly(root, {}, 0);
|
|
840
|
+
assertCheckReadonly(root, { generateEntrypoints: true }, 0);
|
|
841
841
|
});
|
|
842
842
|
|
|
843
|
-
test('manifesto de descoberta
|
|
843
|
+
test('manifesto de descoberta humano permanece intacto mesmo com force', t => {
|
|
844
844
|
const root = fixture(t, true);
|
|
845
845
|
write(root, source, skill);
|
|
846
846
|
write(root, '.pbai4devs/espelhos.conf', 'agente|.agents|agrupado\ndescoberta|.agents|antigravity\n');
|
|
847
|
-
|
|
848
|
-
const custom = '{"entries":[{"path":"custom/skill"}]}\n';
|
|
847
|
+
const custom = '{"entries":[{"path":".agents/skills/custom"}]}\n';
|
|
849
848
|
write(root, '.agents/skills.json', custom);
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
assert.
|
|
853
|
-
|
|
849
|
+
const result = run(root, { force: true, generateEntrypoints: true });
|
|
850
|
+
assert.equal(read(root, '.agents/skills.json'), custom);
|
|
851
|
+
assert.ok(result.preserved.some(item => item.path === '.agents/skills.json'));
|
|
852
|
+
assert.ok(!result.ownedOutputs.includes('.agents/skills.json'));
|
|
853
|
+
assertCheckReadonly(root, { generateEntrypoints: true }, 0);
|
|
854
854
|
});
|
|
855
855
|
|
|
856
856
|
test('formatos desconhecidos ou fora do cliente falham antes de mutar', t => {
|