@pathbit/agentdevkit 0.0.7 → 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.
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/gitignore.mjs +84 -15
  11. package/.pbai4devs/scripts/lib/native-paths.mjs +35 -0
  12. package/.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs +41 -20
  13. package/.pbai4devs/scripts/preparar-ferramentas/tests/TEST.mjs +81 -0
  14. package/.pbai4devs/scripts/sincronizar-agentes/README.md +3 -0
  15. package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +193 -49
  16. package/.pbai4devs/scripts/sincronizar-agentes/tests/COEXISTENCE.mjs +190 -0
  17. package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +151 -0
  18. package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +145 -1
  19. package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +30 -29
  20. package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +56 -8
  21. package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +92 -1
  22. package/.pbai4devs/skills/produto/criar-design-doc/SKILL.md +2 -0
  23. package/.pbai4devs/skills/produto/criar-prd/SKILL.md +2 -0
  24. package/.pbai4devs/skills/produto/criar-ux-ui/SKILL.md +2 -0
  25. package/.pbai4devs/skills/projeto/atualizar-documentacao/SKILL.md +18 -4
  26. package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +2 -0
  27. package/.pbai4devs/skills/projeto/criar-recurso/SKILL.md +1 -0
  28. package/.pbai4devs/skills/projeto/sincronizar-agentes/SKILL.md +3 -2
  29. package/.pbai4devs/skills/specs/criar-spec/SKILL.md +1 -0
  30. package/.pbai4devs/skills/specs/gerenciar-roadmap/SKILL.md +2 -1
  31. package/.pbai4devs/templates/README.md +4 -1
  32. package/.pbai4devs/templates/configuracao/TEMPLATE.md +6 -0
  33. package/CHANGELOG.md +19 -0
  34. package/README.en.md +3 -2
  35. package/README.es.md +3 -2
  36. package/README.md +3 -2
  37. package/docs/README.md +1 -0
  38. package/docs/guides/README.md +1 -0
  39. package/docs/guides/cli.md +11 -0
  40. package/docs/guides/documentation.md +63 -0
  41. package/docs/guides/installation.md +18 -5
  42. package/docs/guides/resources.md +6 -5
  43. package/docs/guides/tools-and-testing.md +2 -0
  44. package/docs/guides/troubleshooting.md +6 -6
  45. package/docs/guides/workflow.md +2 -0
  46. package/docs/maintainers/packages.md +1 -1
  47. package/docs/product/design.md +12 -0
  48. package/docs/product/prd.md +4 -1
  49. package/docs/product/roadmap.md +4 -0
  50. package/docs/specs/README.md +2 -0
  51. package/docs/specs/v0.0.8/README.md +10 -0
  52. package/docs/specs/v0.0.8/SPEC.md +62 -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 +13 -4
  57. package/src/i18n.mjs +3 -0
  58. package/src/install.mjs +145 -38
@@ -0,0 +1,35 @@
1
+ import { lstatSync, readdirSync } from 'node:fs';
2
+ import { isAbsolute, join } from 'node:path';
3
+
4
+ // Inspect native paths without opening a symlink target. Callers restrict this
5
+ // exception to user-owned adapter paths; canonical kit files stay strict.
6
+ export function nativePathBoundary(root, path, ownedPaths = [], { preserveRootAliases = false } = {}) {
7
+ if (typeof path !== 'string' || !path || isAbsolute(path) || /[|\\\x00-\x1f\x7f:]/.test(path) || path.split('/').some(part => !part || part === '.' || part === '..')) {
8
+ throw Object.assign(new Error(`Caminho invalido: ${path}`), { code: 'E_PATH' });
9
+ }
10
+ const parts = path.split('/');
11
+ let current = root;
12
+ for (const [index, part] of parts.entries()) {
13
+ try {
14
+ const aliases = lstatSync(current).isDirectory() ? readdirSync(current).filter(name => name.toLowerCase() === part.toLowerCase() && name !== part).sort() : [];
15
+ if (aliases.length) {
16
+ const rootEntry = parts.length === 1 && ['README.md', 'AGENTS.md', 'CLAUDE.md', 'GEMINI.md'].includes(path);
17
+ const owned = [...ownedPaths].some(value => value.toLowerCase() === path.toLowerCase() || value.toLowerCase().startsWith(`${path.toLowerCase()}/`));
18
+ if (preserveRootAliases && rootEntry && !owned) return { path: aliases[0], reason: 'case-variant' };
19
+ throw Object.assign(new Error(`Caminhos diferem somente por maiusculas/minusculas: ${path}`), { code: 'E_PATH' });
20
+ }
21
+ } catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code)) return null; throw error; }
22
+ current = join(current, part);
23
+ let info;
24
+ try { info = lstatSync(current); }
25
+ catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code)) return null; throw error; }
26
+ const reason = info.isSymbolicLink() ? 'symbolic-link' : index < parts.length - 1 && !info.isDirectory() ? 'non-directory' : null;
27
+ if (!reason) continue;
28
+ const boundary = parts.slice(0, index + 1).join('/');
29
+ if ([...ownedPaths].some(value => value === boundary || value.startsWith(`${boundary}/`))) {
30
+ throw Object.assign(new Error(`${reason === 'symbolic-link' ? 'Link simbolico' : 'Ancestral nao e diretorio'} em caminho gerenciado: ${boundary}`), { code: reason === 'symbolic-link' ? 'E_SYMLINK' : 'E_PATH' });
31
+ }
32
+ return { path: boundary, reason };
33
+ }
34
+ return null;
35
+ }
@@ -101,6 +101,14 @@ function sourcePath(root, name) {
101
101
  return file;
102
102
  }
103
103
 
104
+ function storagePath(root, name, kind) {
105
+ const file = sourcePath(root, name);
106
+ let item;
107
+ try { item = lstatSync(file); } catch (error) { if (error.code !== 'ENOENT') throw error; }
108
+ if (item && !(kind === 'directory' ? item.isDirectory() : item.isFile())) fail(`Caminho local exige ${kind}: ${name}`);
109
+ return file;
110
+ }
111
+
104
112
  export function loadCatalog(root = projectRoot) {
105
113
  root = realpathSync(root);
106
114
  const home = sourcePath(root, '.pbai4devs/tools'); const catalog = Object.create(null);
@@ -272,15 +280,21 @@ export class ToolManager {
272
280
  return { mode: 'setup', selection: selected, platform: this.target, node: process.version, nodePath: process.execPath, npm, results: entries, summary, status: summary.failed ? 'failed' : summary.passed ? 'passed' : 'not-applicable', exitCode: summary.failed ? 1 : 0 };
273
281
  }
274
282
  local(name) { this.entry(name); return join(this.home, name, '.local'); }
283
+ storage(name) {
284
+ const local = this.local(name), tool = join(this.home, name);
285
+ for (const path of [this.home, tool, local, join(local, 'versions'), join(tool, 'versions'), join(this.home, '.cache'), join(this.home, '.cache/npm'), join(this.home, 'npm-cache')]) storagePath(this.root, path, 'directory');
286
+ for (const path of [join(local, 'CURRENT.json'), join(tool, 'CURRENT.json')]) storagePath(this.root, path, 'file');
287
+ return { local, tool };
288
+ }
275
289
  current(name) {
276
- this.entry(name);
277
- const local = this.local(name); const legacy = join(this.home, name);
290
+ const { local, tool: legacy } = this.storage(name);
278
291
  const base = existsSync(join(local, 'CURRENT.json')) ? local : legacy;
279
292
  const file = join(base, 'CURRENT.json');
280
293
  if (!existsSync(file)) fail(`Ferramenta ausente: ${name}; execute install ${name}.`);
281
294
  const info = json(file);
282
295
  if (info.name !== name || info.platform !== this.target || !versionPattern.test(info.version || '')) fail(`Instalacao incompativel: ${name}`);
283
- return { ...info, layout: base === local ? 'local' : 'legacy', prefix: join(base, 'versions', `${info.version}-${this.target}`) };
296
+ const prefix = storagePath(this.root, join(base, 'versions', `${info.version}-${this.target}`), 'directory');
297
+ return { ...info, layout: base === local ? 'local' : 'legacy', prefix };
284
298
  }
285
299
  check(name) {
286
300
  const entry = this.entry(name); const runtime = this.runtime(name); const info = this.current(name);
@@ -306,7 +320,7 @@ export class ToolManager {
306
320
  for (const legacy of [name, `${name}.cmd`, `${name}.ps1`, `${name}.sh`]) rmSync(join(bin, legacy), { force: true });
307
321
  }
308
322
  migrate(name) {
309
- const local = this.local(name); const tool = join(this.home, name);
323
+ const { local, tool } = this.storage(name);
310
324
  const legacyState = join(tool, 'CURRENT.json'); const legacyVersions = join(tool, 'versions');
311
325
  if (existsSync(legacyState)) {
312
326
  if (existsSync(join(local, 'CURRENT.json')) || (existsSync(join(local, 'versions')) && readdirSync(join(local, 'versions')).length)) fail(`Layouts local e antigo coexistem: ${name}; revise antes de migrar.`);
@@ -318,15 +332,15 @@ export class ToolManager {
318
332
  }
319
333
  const oldCache = join(this.home, 'npm-cache');
320
334
  if (existsSync(oldCache)) {
321
- const cache = join(this.home, '.cache'); mkdirSync(cache, { recursive: true });
322
- try { renameSync(oldCache, join(cache, existsSync(join(cache, 'npm')) ? `npm-legacy-${process.pid}-${Date.now()}` : 'npm')); }
335
+ const cache = storagePath(this.root, join(this.home, '.cache'), 'directory'); mkdirSync(cache, { recursive: true });
336
+ const destination = storagePath(this.root, join(cache, existsSync(join(cache, 'npm')) ? `npm-legacy-${randomUUID()}` : 'npm'), 'directory');
337
+ try { renameSync(oldCache, destination); }
323
338
  catch (error) { if (error.code !== 'ENOENT' || existsSync(oldCache)) throw error; }
324
339
  }
325
340
  }
326
341
  async withLock(name, action) {
327
- const local = this.local(name);
328
- for (const directory of [this.home, join(this.home, name), local]) if (existsSync(directory) && lstatSync(directory).isSymbolicLink()) fail(`Diretorio local nao pode ser um link: ${directory}`);
329
- mkdirSync(local, { recursive: true }); const lock = join(local, '.update-lock');
342
+ const { local } = this.storage(name);
343
+ mkdirSync(local, { recursive: true }); const lock = storagePath(this.root, join(local, '.update-lock'), 'directory');
330
344
  try { mkdirSync(lock); } catch (error) {
331
345
  if (error.code === 'EEXIST') {
332
346
  const status = this.lockStatus(name);
@@ -334,8 +348,8 @@ export class ToolManager {
334
348
  }
335
349
  throw error;
336
350
  }
337
- try { writeFileSync(join(lock, 'owner.json'), JSON.stringify({ pid: process.pid, hostname: hostname(), startedAt: new Date().toISOString() })); return await action(); }
338
- finally { rmSync(lock, { recursive: true, force: true }); }
351
+ try { writeFileSync(storagePath(this.root, join(lock, 'owner.json'), 'file'), JSON.stringify({ pid: process.pid, hostname: hostname(), startedAt: new Date().toISOString() }), { flag: 'wx' }); return await action(); }
352
+ finally { rmSync(storagePath(this.root, lock, 'directory'), { recursive: true, force: true }); }
339
353
  }
340
354
  lockStatus(name) {
341
355
  const lock = sourcePath(this.root, join(this.local(name), '.update-lock'));
@@ -380,29 +394,36 @@ export class ToolManager {
380
394
  return this.updateLocked(name);
381
395
  }); }
382
396
  async update(name) { this.preflight([name]); return this.withLock(name, async () => { this.migrate(name); return this.updateLocked(name); }); }
383
- commitState(name, temporary) { renameSync(temporary, join(this.local(name), 'CURRENT.json')); }
397
+ commitState(name, temporary) {
398
+ const { local } = this.storage(name);
399
+ renameSync(storagePath(this.root, temporary, 'file'), storagePath(this.root, join(local, 'CURRENT.json'), 'file'));
400
+ }
384
401
  async updateLocked(name) {
385
402
  const entry = this.entry(name); const runtime = this.runtime(name); const release = validateRelease(name, entry, await this.fetchRelease(name, entry));
403
+ const { local } = this.storage(name);
404
+ const target = storagePath(this.root, join(local, 'versions', `${release.version}-${this.target}`), 'directory');
386
405
  let installed;
387
406
  try { installed = this.check(name); } catch { /* Ausente ou quebrado: reparar em staging. */ }
388
407
  if (installed?.version === release.version) { this.launchers(name); return { ...installed, changed: false }; }
389
- const local = this.local(name); mkdirSync(join(local, 'versions'), { recursive: true });
390
- const stage = mkdtempSync(join(local, '.stage-')); const target = join(local, 'versions', `${release.version}-${this.target}`);
391
- const temporary = join(local, `CURRENT.${process.pid}.${Date.now()}.json`); let previous; let moved = false;
408
+ mkdirSync(storagePath(this.root, join(local, 'versions'), 'directory'), { recursive: true });
409
+ const stage = mkdtempSync(join(local, '.stage-'));
410
+ const temporary = storagePath(this.root, join(local, `CURRENT.${randomUUID()}.json`), 'file'); let previous; let moved = false;
392
411
  try {
393
412
  this.installPackage(stage, entry.download.package, release.version, entry, runtime);
413
+ this.storage(name); storagePath(this.root, stage, 'directory');
394
414
  const pkg = json(join(stage, 'node_modules', entry.download.package, 'package.json')); const lock = json(join(stage, 'package-lock.json'));
395
415
  if (pkg.name !== entry.download.package || pkg.version !== release.version || lock.packages?.[`node_modules/${entry.download.package}`]?.integrity !== release.dist.integrity) fail('Pacote instalado nao corresponde ao registro.');
396
416
  prepareExecutables(stage, entry, this.target);
397
417
  execute(stage, entry, entry.probe, true, entry.probeInput, toolEnvironment(entry, local, runtime), runtime);
418
+ this.storage(name); storagePath(this.root, target, 'directory');
398
419
  const info = { name, package: entry.download.package, version: release.version, platform: this.target, node: runtime.version, nodePath: runtime.file, source: entry.source, tarball: release.dist.tarball, integrity: release.dist.integrity, checkedAt: new Date().toISOString() };
399
- writeFileSync(temporary, `${JSON.stringify(info, null, 2)}\n`);
420
+ writeFileSync(storagePath(this.root, temporary, 'file'), `${JSON.stringify(info, null, 2)}\n`, { flag: 'wx' });
400
421
  this.launchers(name);
401
- if (existsSync(target)) { previous = `${target}.previous-${process.pid}-${Date.now()}`; renameSync(target, previous); }
402
- try { renameSync(stage, target); moved = true; this.commitState(name, temporary); }
403
- catch (error) { if (moved) rmSync(target, { recursive: true, force: true }); if (previous) renameSync(previous, target); throw error; }
422
+ if (existsSync(target)) { previous = storagePath(this.root, `${target}.previous-${randomUUID()}`, 'directory'); renameSync(storagePath(this.root, target, 'directory'), previous); }
423
+ try { renameSync(storagePath(this.root, stage, 'directory'), storagePath(this.root, target, 'directory')); moved = true; this.commitState(name, temporary); }
424
+ catch (error) { if (moved) rmSync(storagePath(this.root, target, 'directory'), { recursive: true, force: true }); if (previous) renameSync(storagePath(this.root, previous, 'directory'), storagePath(this.root, target, 'directory')); throw error; }
404
425
  return { ...info, layout: 'local', prefix: target, changed: true };
405
- } finally { rmSync(stage, { recursive: true, force: true }); rmSync(temporary, { force: true }); }
426
+ } finally { rmSync(storagePath(this.root, stage, 'directory'), { recursive: true, force: true }); rmSync(storagePath(this.root, temporary, 'file'), { force: true }); }
406
427
  }
407
428
  run(name, args) { const entry = this.entry(name); const runtime = this.runtime(name); return execute(this.current(name).prefix, entry, args, false, undefined, toolEnvironment(entry, this.local(name), runtime), runtime); }
408
429
  }
@@ -232,6 +232,87 @@ test('falha ao gravar CURRENT restaura o target anterior e o pacote em uso', asy
232
232
  assert.equal(manager.check('test-tool').version, '1.0.0'); assert.ok(!existsSync(join(local, '.update-lock')));
233
233
  });
234
234
 
235
+ test('versions simbolico existente ou quebrado e recusado antes de baixar ou escrever', async t => {
236
+ for (const broken of [false, true]) await t.test(broken ? 'broken' : 'existing', async subtest => {
237
+ const { root, manager, state, local } = fixture(subtest);
238
+ const outside = join(root, 'outside'); mkdirSync(outside);
239
+ writeFileSync(join(outside, 'keep.txt'), 'preservado'); mkdirSync(local);
240
+ if (!createSymlink(subtest, broken ? join(outside, 'missing') : outside, join(local, 'versions'), process.platform === 'win32' ? 'junction' : 'dir')) return;
241
+ for (const action of ['install', 'update']) await assert.rejects(manager[action]('test-tool'), /nao pode ser um link/);
242
+ assert.equal(state.fetches, 0); assert.equal(state.installs, 0);
243
+ assert.deepEqual(readdirSync(outside), ['keep.txt']);
244
+ assert.equal(readFileSync(join(outside, 'keep.txt'), 'utf8'), 'preservado');
245
+ assert.equal(existsSync(join(local, 'CURRENT.json')), false);
246
+ assert.equal(existsSync(join(local, '.update-lock')), false);
247
+ });
248
+ });
249
+
250
+ test('recibo CURRENT e destino de versao simbolicos nao sao seguidos nem substituidos', async t => {
251
+ for (const kind of ['current', 'target', 'broken-target']) await t.test(kind, async subtest => {
252
+ const { root, manager, state, local } = fixture(subtest); await manager.install('test-tool');
253
+ const receipt = readFileSync(join(local, 'CURRENT.json'));
254
+ const outside = join(root, 'outside'); mkdirSync(outside); writeFileSync(join(outside, 'keep.txt'), 'preservado');
255
+ if (kind === 'current') {
256
+ writeFileSync(join(outside, 'CURRENT.json'), receipt); rmSync(join(local, 'CURRENT.json'));
257
+ if (!createSymlink(subtest, join(outside, 'CURRENT.json'), join(local, 'CURRENT.json'))) return;
258
+ assert.throws(() => manager.check('test-tool'), /nao pode ser um link/);
259
+ } else {
260
+ state.version = '2.0.0';
261
+ if (!createSymlink(subtest, kind === 'target' ? outside : join(outside, 'missing'), join(local, 'versions', `2.0.0-${platformKey()}`), process.platform === 'win32' ? 'junction' : 'dir')) return;
262
+ }
263
+ const before = readdirSync(outside), installs = state.installs;
264
+ await assert.rejects(manager.update('test-tool'), /nao pode ser um link/);
265
+ assert.equal(state.installs, installs); assert.deepEqual(readdirSync(outside), before);
266
+ assert.equal(readFileSync(join(outside, 'keep.txt'), 'utf8'), 'preservado');
267
+ assert.deepEqual(readFileSync(kind === 'current' ? join(outside, 'CURRENT.json') : join(local, 'CURRENT.json')), receipt);
268
+ });
269
+ });
270
+
271
+ test('troca de versions durante fetch ou install nao escapa da arvore local', async t => {
272
+ for (const phase of ['fetch', 'install']) await t.test(phase, async subtest => {
273
+ const { root, manager, state, local } = fixture(subtest); await manager.install('test-tool');
274
+ const receipt = readFileSync(join(local, 'CURRENT.json')); state.version = '2.0.0';
275
+ const outside = join(root, 'outside'); mkdirSync(outside);
276
+ const probe = join(local, 'symlink-probe');
277
+ if (!createSymlink(subtest, outside, probe, process.platform === 'win32' ? 'junction' : 'dir')) return;
278
+ unlinkSync(probe);
279
+ const versions = join(local, 'versions'), kept = join(local, 'versions-kept'); let linked = false;
280
+ const replace = () => {
281
+ renameSync(versions, kept);
282
+ symlinkSync(outside, versions, process.platform === 'win32' ? 'junction' : 'dir'); linked = true;
283
+ };
284
+ if (phase === 'fetch') manager.fetchRelease = async () => { replace(); return release('2.0.0'); };
285
+ else manager.installPackage = (prefix, name, version) => { packageFiles(prefix, name, version); replace(); };
286
+ try {
287
+ await assert.rejects(manager.update('test-tool'), /nao pode ser um link/);
288
+ assert.deepEqual(readdirSync(outside), []); assert.deepEqual(readFileSync(join(local, 'CURRENT.json')), receipt);
289
+ } finally { if (linked) { unlinkSync(versions); renameSync(kept, versions); } }
290
+ assert.equal(manager.check('test-tool').version, '1.0.0');
291
+ assert.equal(existsSync(join(local, '.update-lock')), false);
292
+ });
293
+ });
294
+
295
+ test('migracao recusa links de versions antigo e cache compartilhado sem mover dados', async t => {
296
+ for (const kind of ['legacy-versions', 'cache']) await t.test(kind, async subtest => {
297
+ const { root, manager, state } = fixture(subtest);
298
+ const home = join(root, '.pbai4devs/tools'), legacy = join(home, 'test-tool');
299
+ const outside = join(root, 'outside'); mkdirSync(outside); writeFileSync(join(outside, 'keep.txt'), 'preservado');
300
+ if (kind === 'legacy-versions') {
301
+ writeFileSync(join(legacy, 'CURRENT.json'), JSON.stringify({ name: 'test-tool', version: '1.0.0', platform: platformKey() }));
302
+ if (!createSymlink(subtest, outside, join(legacy, 'versions'), process.platform === 'win32' ? 'junction' : 'dir')) return;
303
+ } else {
304
+ mkdirSync(join(home, 'npm-cache')); writeFileSync(join(home, 'npm-cache/keep.txt'), 'cache antigo');
305
+ if (!createSymlink(subtest, outside, join(home, '.cache'), process.platform === 'win32' ? 'junction' : 'dir')) return;
306
+ }
307
+ await assert.rejects(manager.install('test-tool'), /nao pode ser um link/);
308
+ assert.equal(state.fetches, 0); assert.equal(state.installs, 0);
309
+ assert.deepEqual(readdirSync(outside), ['keep.txt']);
310
+ assert.equal(readFileSync(join(outside, 'keep.txt'), 'utf8'), 'preservado');
311
+ if (kind === 'legacy-versions') assert.ok(existsSync(join(legacy, 'CURRENT.json')));
312
+ else assert.equal(readFileSync(join(home, 'npm-cache/keep.txt'), 'utf8'), 'cache antigo');
313
+ });
314
+ });
315
+
235
316
  test('duas atualizacoes nao disputam a mesma ferramenta nem apagam lock alheio', async t => {
236
317
  const { manager, state, local } = fixture(t); let enter; let proceed;
237
318
  const entered = new Promise(resolve => { enter = resolve; });
@@ -12,6 +12,9 @@ node --test .pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs
12
12
 
13
13
  `--check` só confere; a execução normal atualiza os recursos. `--force` salva
14
14
  conflitos em `.agent-tests/` antes de reparar os caminhos gerenciados.
15
+ Recursos nativos humanos e seus links permanecem intactos, inclusive com
16
+ `--force`. O retorno inclui `preserved`; esses caminhos não contam como
17
+ derivados pendentes nem passam a pertencer ao kit.
15
18
 
16
19
  Metadados vêm da fonte ou do YAML nativo declarado no manifesto. Regras exigem
17
20
  `trigger` válido; `glob` exige `globs`. Origens nativas precisam pertencer ao