@pathbit/agentdevkit 0.0.8 → 0.0.10

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.
Files changed (58) hide show
  1. package/.pbai4devs/COMPATIBILIDADE.md +4 -2
  2. package/.pbai4devs/INSTRUCTIONS.md +4 -0
  3. package/.pbai4devs/commands/sincronizar/COMMAND.md +4 -3
  4. package/.pbai4devs/rules/documentacao/RULE.md +4 -0
  5. package/.pbai4devs/rules/qualidade/RULE.md +4 -2
  6. package/.pbai4devs/scripts/README.md +3 -3
  7. package/.pbai4devs/scripts/inspecionar-projeto/README.md +31 -0
  8. package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +142 -1
  9. package/.pbai4devs/scripts/inspecionar-projeto/tests/TEST.mjs +168 -1
  10. package/.pbai4devs/scripts/lib/native-paths.mjs +35 -0
  11. package/.pbai4devs/scripts/sincronizar-agentes/README.md +3 -0
  12. package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +73 -29
  13. package/.pbai4devs/scripts/sincronizar-agentes/tests/COEXISTENCE.mjs +190 -0
  14. package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +8 -2
  15. package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +28 -28
  16. package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +56 -8
  17. package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +92 -1
  18. package/.pbai4devs/skills/produto/criar-design-doc/SKILL.md +2 -0
  19. package/.pbai4devs/skills/produto/criar-prd/SKILL.md +2 -0
  20. package/.pbai4devs/skills/produto/criar-ux-ui/SKILL.md +2 -0
  21. package/.pbai4devs/skills/projeto/atualizar-documentacao/SKILL.md +18 -4
  22. package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +2 -0
  23. package/.pbai4devs/skills/projeto/criar-recurso/SKILL.md +1 -0
  24. package/.pbai4devs/skills/projeto/sincronizar-agentes/SKILL.md +3 -2
  25. package/.pbai4devs/skills/specs/criar-spec/SKILL.md +1 -0
  26. package/.pbai4devs/skills/specs/gerenciar-roadmap/SKILL.md +2 -1
  27. package/.pbai4devs/templates/README.md +4 -1
  28. package/.pbai4devs/templates/configuracao/TEMPLATE.md +6 -0
  29. package/CHANGELOG.md +18 -0
  30. package/README.en.md +18 -5
  31. package/README.es.md +19 -6
  32. package/README.md +16 -3
  33. package/docs/README.md +4 -2
  34. package/docs/guides/README.md +3 -1
  35. package/docs/guides/cli.md +5 -0
  36. package/docs/guides/documentation.md +63 -0
  37. package/docs/guides/installation.md +71 -14
  38. package/docs/guides/prerequisites.md +123 -0
  39. package/docs/guides/resources.md +6 -5
  40. package/docs/guides/tools-and-testing.md +2 -1
  41. package/docs/guides/troubleshooting.md +29 -13
  42. package/docs/guides/workflow.md +2 -0
  43. package/docs/maintainers/README.md +1 -1
  44. package/docs/maintainers/maintenance.md +8 -4
  45. package/docs/maintainers/packages.md +45 -13
  46. package/docs/product/design.md +12 -0
  47. package/docs/product/prd.md +4 -1
  48. package/docs/product/roadmap.md +4 -0
  49. package/docs/specs/README.md +3 -0
  50. package/docs/specs/dual-registry/SPEC.md +43 -0
  51. package/docs/specs/v0.0.10/README.md +9 -0
  52. package/docs/specs/v0.0.10/SPEC.md +42 -0
  53. package/docs/specs/v0.0.9/README.md +9 -0
  54. package/docs/specs/v0.0.9/SPEC.md +68 -0
  55. package/package.json +3 -2
  56. package/src/cli.mjs +3 -0
  57. package/src/i18n.mjs +3 -0
  58. 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(absolute(target));
139
- if (info && !info.isDirectory()) fail(`Pasta de agente nao e diretorio: ${target}`);
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 (force && value.split('/')[1] === 'external-skills') planRemove(value, true);
246
- else if (onlyReferences(value)) planRemove(value);
247
- else if (force) planRemove(value, true);
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 conflicts.add(file);
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 = stat(safe(entryRegistry)) ? JSON.parse(read(safe(entryRegistry))) : { schemaVersion: 1, entries: {} };
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
- if (check) return { ...summary, ownedOutputs, exitCode: dirty ? 1 : 0 };
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
- assert.throws(() => run(root), /--force/);
48
- assert.deepEqual(snapshot(root), before);
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; --force salva e limpa cópias indevidas', t => {
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
- for (const client of ['.codex', '.claude']) {
354
- assert.equal(existsSync(join(root, client, 'external-skills')), false);
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(result.backupRoot, '.codex/external-skills/.local/upstream-0/SKILL.md'), '# Cópia indevida\n');
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 salva conteúdo próprio, repara ancestrais e preserva arquivos fora do escopo', t => {
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/scripts/local.sh', '.claude/tools/program.exe', '.claude/docs/plain.md', '.claude/skills/local.bin', '.claude/skills/projeto/demo/SKILL.md', '.kimi-code/agents/revisor.md', '.codex/skills/outro', '.github/prompts/edited.prompt.md']) assert.ok(existsSync(join(result.backupRoot, path)), `Backup ausente: ${path}`);
380
- assert.deepEqual(readFileSync(join(result.backupRoot, '.claude/tools/program.exe')), Buffer.from([1, 2, 3]));
381
- assert.equal(existsSync(join(root, '.claude/scripts')), false);
382
- assert.equal(existsSync(join(root, '.claude/skills/local.bin')), false);
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 ['.claude/tools', '.pbai4devs/skills/projeto/demo/external.md', '.pbai4devs/skills.json', '.pbai4devs/espelhos.conf']) {
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 customizado exige force e recebe backup', t => {
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
- run(root);
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
- rejected(root, {}, /force/);
851
- const result = run(root, { force: true });
852
- assert.equal(readFileSync(join(result.backupRoot, '.agents/skills.json'), 'utf8'), custom);
853
- assertCheckReadonly(root, {}, 0);
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 => {