@pathbit/agentdevkit 0.0.0-stage → 0.0.5

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 (240) hide show
  1. package/.pbai4devs/COMPATIBILIDADE.md +109 -0
  2. package/.pbai4devs/INSTRUCTIONS.md +87 -0
  3. package/.pbai4devs/README.md +50 -0
  4. package/.pbai4devs/agents/README.md +18 -0
  5. package/.pbai4devs/agents/revisor/AGENT.md +16 -0
  6. package/.pbai4devs/agents/revisor/antigravity.yaml +8 -0
  7. package/.pbai4devs/agents/revisor/claude.yaml +5 -0
  8. package/.pbai4devs/agents/revisor/cursor.yaml +5 -0
  9. package/.pbai4devs/agents/revisor/gemini.yaml +6 -0
  10. package/.pbai4devs/agents/revisor/github.yaml +5 -0
  11. package/.pbai4devs/agents/revisor/kimi-code.yaml +5 -0
  12. package/.pbai4devs/agents/revisor/mimocode.yaml +10 -0
  13. package/.pbai4devs/agents/revisor/opencode.yaml +7 -0
  14. package/.pbai4devs/agents/revisor/zcode.yaml +5 -0
  15. package/.pbai4devs/commands/README.md +30 -0
  16. package/.pbai4devs/commands/atualizar-ferramentas/COMMAND.md +17 -0
  17. package/.pbai4devs/commands/diagnosticar/COMMAND.md +18 -0
  18. package/.pbai4devs/commands/instalar-ferramentas/COMMAND.md +21 -0
  19. package/.pbai4devs/commands/preparar-dev/COMMAND.md +14 -0
  20. package/.pbai4devs/commands/proximos-passos/COMMAND.md +9 -0
  21. package/.pbai4devs/commands/revisar-entrega/COMMAND.md +29 -0
  22. package/.pbai4devs/commands/sincronizar/COMMAND.md +20 -0
  23. package/.pbai4devs/commands/status-roadmap/COMMAND.md +12 -0
  24. package/.pbai4devs/commands/testar-navegador/COMMAND.md +16 -0
  25. package/.pbai4devs/commands/verificar-agentes/COMMAND.md +19 -0
  26. package/.pbai4devs/estado/README.md +12 -0
  27. package/.pbai4devs/external-skills/CATALOG.json +121 -0
  28. package/.pbai4devs/external-skills/README.md +48 -0
  29. package/.pbai4devs/rules/README.md +18 -0
  30. package/.pbai4devs/rules/ambiente-local/RULE.md +33 -0
  31. package/.pbai4devs/rules/ambiente-local/cursor.yaml +4 -0
  32. package/.pbai4devs/rules/documentacao/RULE.md +11 -0
  33. package/.pbai4devs/rules/documentacao/cursor.yaml +4 -0
  34. package/.pbai4devs/rules/estrutura/RULE.md +33 -0
  35. package/.pbai4devs/rules/estrutura/cursor.yaml +4 -0
  36. package/.pbai4devs/rules/git/RULE.md +18 -0
  37. package/.pbai4devs/rules/git/cursor.yaml +4 -0
  38. package/.pbai4devs/rules/proximos-passos/RULE.md +14 -0
  39. package/.pbai4devs/rules/qualidade/RULE.md +32 -0
  40. package/.pbai4devs/rules/qualidade/cursor.yaml +4 -0
  41. package/.pbai4devs/scripts/README.md +69 -0
  42. package/.pbai4devs/scripts/diagnosticar-ambiente/SCRIPT.mjs +41 -0
  43. package/.pbai4devs/scripts/diagnosticar-ambiente/tests/TEST.mjs +30 -0
  44. package/.pbai4devs/scripts/executar-script/README.md +28 -0
  45. package/.pbai4devs/scripts/executar-script/SCRIPT.mjs +78 -0
  46. package/.pbai4devs/scripts/executar-script/tests/TEST.mjs +122 -0
  47. package/.pbai4devs/scripts/gerenciar-skills-externas/README.md +31 -0
  48. package/.pbai4devs/scripts/gerenciar-skills-externas/SCRIPT.mjs +182 -0
  49. package/.pbai4devs/scripts/gerenciar-skills-externas/tests/TEST.mjs +134 -0
  50. package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +190 -0
  51. package/.pbai4devs/scripts/inspecionar-projeto/tests/TEST.mjs +161 -0
  52. package/.pbai4devs/scripts/lib/README.md +9 -0
  53. package/.pbai4devs/scripts/lib/adapters.mjs +135 -0
  54. package/.pbai4devs/scripts/lib/catalog.mjs +170 -0
  55. package/.pbai4devs/scripts/lib/gitignore.mjs +348 -0
  56. package/.pbai4devs/scripts/lib/npm.mjs +17 -0
  57. package/.pbai4devs/scripts/lib/resource-path.mjs +20 -0
  58. package/.pbai4devs/scripts/lib/runtime.mjs +28 -0
  59. package/.pbai4devs/scripts/lib/script-config.mjs +8 -0
  60. package/.pbai4devs/scripts/preparar-ferramentas/BOOTSTRAP.md +34 -0
  61. package/.pbai4devs/scripts/preparar-ferramentas/BOOTSTRAP.mjs +189 -0
  62. package/.pbai4devs/scripts/preparar-ferramentas/README.md +71 -0
  63. package/.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs +455 -0
  64. package/.pbai4devs/scripts/preparar-ferramentas/tests/FIXTURE.html +20 -0
  65. package/.pbai4devs/scripts/preparar-ferramentas/tests/SMOKE_BROWSER.mjs +55 -0
  66. package/.pbai4devs/scripts/preparar-ferramentas/tests/SMOKE_DESKTOP.mjs +61 -0
  67. package/.pbai4devs/scripts/preparar-ferramentas/tests/SMOKE_VERCEL.mjs +192 -0
  68. package/.pbai4devs/scripts/preparar-ferramentas/tests/TEST.mjs +552 -0
  69. package/.pbai4devs/scripts/preparar-ferramentas/tests/bootstrap.test.mjs +94 -0
  70. package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/just-bash/input.txt +2 -0
  71. package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/just-bash-overlay/input.txt +1 -0
  72. package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/skills/agentdevkit-smoke/SKILL.md +8 -0
  73. package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/webreel/index.html +6 -0
  74. package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/webreel/webreel.config.json +10 -0
  75. package/.pbai4devs/scripts/revisar-entrega/README.md +36 -0
  76. package/.pbai4devs/scripts/revisar-entrega/SCRIPT.mjs +249 -0
  77. package/.pbai4devs/scripts/revisar-entrega/tests/TEST.mjs +249 -0
  78. package/.pbai4devs/scripts/sincronizar-agentes/README.md +41 -0
  79. package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +473 -0
  80. package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +290 -0
  81. package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +869 -0
  82. package/.pbai4devs/scripts/testar-infraestrutura/SCRIPT.mjs +39 -0
  83. package/.pbai4devs/scripts/testar-infraestrutura/tests/TEST.mjs +29 -0
  84. package/.pbai4devs/scripts/testar-maturidade/README.md +54 -0
  85. package/.pbai4devs/scripts/testar-maturidade/SCRIPT.mjs +477 -0
  86. package/.pbai4devs/scripts/testar-maturidade/assets/calculator.mjs +1 -0
  87. package/.pbai4devs/scripts/testar-maturidade/assets/calculator.test.mjs.template +8 -0
  88. package/.pbai4devs/scripts/testar-maturidade/assets/http-api.test.mjs.template +28 -0
  89. package/.pbai4devs/scripts/testar-maturidade/assets/server.mjs +34 -0
  90. package/.pbai4devs/scripts/testar-maturidade/tests/TEST.mjs +305 -0
  91. package/.pbai4devs/scripts/testar-navegador/REFERENCE.md +49 -0
  92. package/.pbai4devs/scripts/testar-navegador/SCRIPT.conf +1 -0
  93. package/.pbai4devs/scripts/testar-navegador/SCRIPT.js +64 -0
  94. package/.pbai4devs/scripts/testar-navegador/tests/FAIL.js +8 -0
  95. package/.pbai4devs/scripts/testar-navegador/tests/SUCCESS.js +13 -0
  96. package/.pbai4devs/scripts/verificar-recursos/README.md +25 -0
  97. package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +209 -0
  98. package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +310 -0
  99. package/.pbai4devs/scripts/verificar-recursos/tests/ambiente-local.mjs +80 -0
  100. package/.pbai4devs/skills/README.md +24 -0
  101. package/.pbai4devs/skills/git/git-branch/SKILL.md +11 -0
  102. package/.pbai4devs/skills/git/git-commit/SKILL.md +16 -0
  103. package/.pbai4devs/skills/git/git-diff/SKILL.md +9 -0
  104. package/.pbai4devs/skills/git/git-fetch/SKILL.md +10 -0
  105. package/.pbai4devs/skills/git/git-log/SKILL.md +9 -0
  106. package/.pbai4devs/skills/git/git-merge/SKILL.md +13 -0
  107. package/.pbai4devs/skills/git/git-pull/SKILL.md +12 -0
  108. package/.pbai4devs/skills/git/git-push/SKILL.md +12 -0
  109. package/.pbai4devs/skills/git/git-status/SKILL.md +9 -0
  110. package/.pbai4devs/skills/git/git-worktree/SKILL.md +10 -0
  111. package/.pbai4devs/skills/produto/criar-design-doc/SKILL.md +37 -0
  112. package/.pbai4devs/skills/produto/criar-prd/SKILL.md +35 -0
  113. package/.pbai4devs/skills/produto/criar-ux-ui/SKILL.md +34 -0
  114. package/.pbai4devs/skills/projeto/atualizar-documentacao/SKILL.md +26 -0
  115. package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +62 -0
  116. package/.pbai4devs/skills/projeto/criar-agente/SKILL.md +20 -0
  117. package/.pbai4devs/skills/projeto/criar-comando/SKILL.md +25 -0
  118. package/.pbai4devs/skills/projeto/criar-recurso/SKILL.md +41 -0
  119. package/.pbai4devs/skills/projeto/criar-regra/SKILL.md +19 -0
  120. package/.pbai4devs/skills/projeto/criar-skill/SKILL.md +22 -0
  121. package/.pbai4devs/skills/projeto/executar-script/SKILL.md +29 -0
  122. package/.pbai4devs/skills/projeto/preparar-dev/SKILL.md +45 -0
  123. package/.pbai4devs/skills/projeto/preparar-ferramentas/SKILL.md +56 -0
  124. package/.pbai4devs/skills/projeto/proximos-passos/SKILL.md +23 -0
  125. package/.pbai4devs/skills/projeto/sincronizar-agentes/SKILL.md +42 -0
  126. package/.pbai4devs/skills/projeto/usar-skills-externas/SKILL.md +28 -0
  127. package/.pbai4devs/skills/projeto/verificar-trabalho/SKILL.md +45 -0
  128. package/.pbai4devs/skills/specs/criar-spec/SKILL.md +33 -0
  129. package/.pbai4devs/skills/specs/gerenciar-roadmap/SKILL.md +26 -0
  130. package/.pbai4devs/skills/specs/implementar-spec/SKILL.md +48 -0
  131. package/.pbai4devs/templates/README.md +65 -0
  132. package/.pbai4devs/templates/adr/TEMPLATE.md +21 -0
  133. package/.pbai4devs/templates/ambiente-local/Makefile.template +48 -0
  134. package/.pbai4devs/templates/ambiente-local/TEMPLATE.md +48 -0
  135. package/.pbai4devs/templates/ambiente-local/compose.local.yml.template +16 -0
  136. package/.pbai4devs/templates/ambiente-local/dockerignore.template +28 -0
  137. package/.pbai4devs/templates/changelog/TEMPLATE.md +17 -0
  138. package/.pbai4devs/templates/claude/TEMPLATE.md +8 -0
  139. package/.pbai4devs/templates/clients/ADAPTERS.json +306 -0
  140. package/.pbai4devs/templates/clients/TEMPLATE.md +12 -0
  141. package/.pbai4devs/templates/comando/TEMPLATE.md +15 -0
  142. package/.pbai4devs/templates/configuracao/TEMPLATE.md +46 -0
  143. package/.pbai4devs/templates/configuracao-raiz/TEMPLATE.md +22 -0
  144. package/.pbai4devs/templates/configuracao-raiz/editorconfig.template +21 -0
  145. package/.pbai4devs/templates/configuracao-raiz/env.example.template +3 -0
  146. package/.pbai4devs/templates/configuracao-raiz/gitattributes.template +6 -0
  147. package/.pbai4devs/templates/contribuicao/TEMPLATE.md +23 -0
  148. package/.pbai4devs/templates/design/TEMPLATE.md +30 -0
  149. package/.pbai4devs/templates/entrypoints/AGENTS.md +19 -0
  150. package/.pbai4devs/templates/entrypoints/CLAUDE.md +7 -0
  151. package/.pbai4devs/templates/entrypoints/GEMINI.md +7 -0
  152. package/.pbai4devs/templates/entrypoints/TEMPLATE.md +12 -0
  153. package/.pbai4devs/templates/gitignore/TEMPLATE.md +42 -0
  154. package/.pbai4devs/templates/gitignore/agents.ignore +45 -0
  155. package/.pbai4devs/templates/gitignore/clients.json +20 -0
  156. package/.pbai4devs/templates/gitignore/local.ignore +10 -0
  157. package/.pbai4devs/templates/gitignore/shared.ignore +60 -0
  158. package/.pbai4devs/templates/instrucoes-agentes/TEMPLATE.md +25 -0
  159. package/.pbai4devs/templates/plano-implementacao/TEMPLATE.md +27 -0
  160. package/.pbai4devs/templates/prd/TEMPLATE.md +30 -0
  161. package/.pbai4devs/templates/readme-pasta/TEMPLATE.md +16 -0
  162. package/.pbai4devs/templates/readme-projeto/TEMPLATE.md +34 -0
  163. package/.pbai4devs/templates/release/TEMPLATE.md +23 -0
  164. package/.pbai4devs/templates/revisao-entrega/TEMPLATE.md +41 -0
  165. package/.pbai4devs/templates/roadmap/TEMPLATE.md +18 -0
  166. package/.pbai4devs/templates/seguranca/TEMPLATE.md +22 -0
  167. package/.pbai4devs/templates/spec/TEMPLATE.md +61 -0
  168. package/.pbai4devs/templates/tasks/TEMPLATE.md +20 -0
  169. package/.pbai4devs/templates/ux-ui/TEMPLATE.md +26 -0
  170. package/.pbai4devs/tools/README.md +87 -0
  171. package/.pbai4devs/tools/agent-browser/README.md +26 -0
  172. package/.pbai4devs/tools/agent-browser/SCRIPT.mjs +5 -0
  173. package/.pbai4devs/tools/agent-browser/TOOL.json +33 -0
  174. package/.pbai4devs/tools/agent-desktop/README.md +26 -0
  175. package/.pbai4devs/tools/agent-desktop/SCRIPT.mjs +5 -0
  176. package/.pbai4devs/tools/agent-desktop/TOOL.json +30 -0
  177. package/.pbai4devs/tools/just-bash/README.md +23 -0
  178. package/.pbai4devs/tools/just-bash/SCRIPT.mjs +5 -0
  179. package/.pbai4devs/tools/just-bash/TOOL.json +27 -0
  180. package/.pbai4devs/tools/markdownlint-cli2/README.md +18 -0
  181. package/.pbai4devs/tools/markdownlint-cli2/SCRIPT.mjs +5 -0
  182. package/.pbai4devs/tools/markdownlint-cli2/TOOL.json +28 -0
  183. package/.pbai4devs/tools/opensrc/README.md +25 -0
  184. package/.pbai4devs/tools/opensrc/SCRIPT.mjs +5 -0
  185. package/.pbai4devs/tools/opensrc/TOOL.json +29 -0
  186. package/.pbai4devs/tools/portless/README.md +21 -0
  187. package/.pbai4devs/tools/portless/SCRIPT.mjs +5 -0
  188. package/.pbai4devs/tools/portless/TOOL.json +30 -0
  189. package/.pbai4devs/tools/skills/README.md +24 -0
  190. package/.pbai4devs/tools/skills/SCRIPT.mjs +5 -0
  191. package/.pbai4devs/tools/skills/TOOL.json +33 -0
  192. package/.pbai4devs/tools/webreel/README.md +23 -0
  193. package/.pbai4devs/tools/webreel/SCRIPT.mjs +5 -0
  194. package/.pbai4devs/tools/webreel/TOOL.json +25 -0
  195. package/CHANGELOG.md +58 -0
  196. package/CONTRIBUTING.md +73 -0
  197. package/LICENSE +21 -0
  198. package/README.en.md +78 -0
  199. package/README.es.md +80 -0
  200. package/README.md +88 -2
  201. package/bin/README.md +8 -0
  202. package/bin/agentdevkit.mjs +35 -0
  203. package/docs/README.md +81 -0
  204. package/docs/guides/README.md +14 -0
  205. package/docs/guides/cli.md +81 -0
  206. package/docs/guides/installation.md +78 -0
  207. package/docs/guides/resources.md +121 -0
  208. package/docs/guides/tools-and-testing.md +85 -0
  209. package/docs/guides/troubleshooting.md +151 -0
  210. package/docs/guides/workflow.md +130 -0
  211. package/docs/maintainers/README.md +12 -0
  212. package/docs/maintainers/community.md +49 -0
  213. package/docs/maintainers/development.md +54 -0
  214. package/docs/maintainers/maintenance.md +104 -0
  215. package/docs/maintainers/packages.md +76 -0
  216. package/docs/product/README.md +12 -0
  217. package/docs/product/design.md +94 -0
  218. package/docs/product/prd.md +64 -0
  219. package/docs/product/roadmap.md +36 -0
  220. package/docs/product/ux-ui.md +42 -0
  221. package/docs/specs/README.md +20 -0
  222. package/docs/specs/github-packages/SPEC.md +45 -0
  223. package/docs/specs/v0.0.1/README.md +10 -0
  224. package/docs/specs/v0.0.1/SPEC.md +57 -0
  225. package/docs/specs/v0.0.2/README.md +10 -0
  226. package/docs/specs/v0.0.2/SPEC.md +67 -0
  227. package/docs/specs/v0.0.3/README.md +10 -0
  228. package/docs/specs/v0.0.3/SPEC.md +64 -0
  229. package/docs/specs/v0.0.4/README.md +10 -0
  230. package/docs/specs/v0.0.4/SPEC.md +53 -0
  231. package/docs/specs/v0.0.5/README.md +9 -0
  232. package/docs/specs/v0.0.5/SPEC.md +42 -0
  233. package/package.json +52 -4
  234. package/src/README.md +16 -0
  235. package/src/adapters/index.mjs +1 -0
  236. package/src/cli.mjs +162 -0
  237. package/src/config.mjs +14 -0
  238. package/src/i18n.mjs +42 -0
  239. package/src/install.mjs +367 -0
  240. package/src/next.mjs +79 -0
@@ -0,0 +1,869 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { createHash } from 'node:crypto';
5
+ import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
6
+ import { dirname, join, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { synchronize, header, isReference, resourcePath } from '../SCRIPT.mjs';
9
+ import { buildCatalog } from '../../lib/catalog.mjs';
10
+ import { toolWrapper } from '../../preparar-ferramentas/SCRIPT.mjs';
11
+ import { mergeGitignore, ignoreBegin, ignoreEnd, validateIgnoreClients } from '../../lib/gitignore.mjs';
12
+ import './LOCAL_IGNORE.mjs';
13
+ import { validateCatalog as validateExternalCatalog } from '../../gerenciar-skills-externas/SCRIPT.mjs';
14
+
15
+ const project = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
16
+ const cli = resolve(dirname(fileURLToPath(import.meta.url)), '../SCRIPT.mjs');
17
+ const marker = '<!-- gerado: sincronizar-agentes -->';
18
+ const skill = '---\nname: demo\ndescription: Exemplo.\n---\n\n# Demo\n\nNAO_DUPLICAR_INSTRUCOES\n';
19
+ const source = '.pbai4devs/skills/projeto/demo/SKILL.md';
20
+ const read = (root, path) => readFileSync(join(root, path), 'utf8');
21
+ const write = (root, path, content) => { mkdirSync(dirname(join(root, path)), { recursive: true }); writeFileSync(join(root, path), content); };
22
+ const pointer = (origin = 'origem.md', title = 'Referencia') => `---\ndescription: Referencia.\n---\n\n# ${title}\n\n${marker}\nLeia e siga [origem](${origin}).\n`;
23
+ 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'] });
24
+ 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.' }] });
25
+ const run = (root, options = {}) => synchronize({ root, localIgnores: false, generateEntrypoints: false, output() {}, ...options });
26
+ function snapshot(root) {
27
+ const result = [];
28
+ function visit(folder, parent = '') {
29
+ for (const name of readdirSync(folder).sort()) {
30
+ const file = join(folder, name), path = parent ? `${parent}/${name}` : name, item = lstatSync(file);
31
+ if (item.isSymbolicLink()) result.push([path, 'link', readlinkSync(file)]);
32
+ else if (item.isDirectory()) { result.push([path, 'directory']); visit(file, path); }
33
+ else result.push([path, 'file', createHash('sha256').update(readFileSync(file)).digest('hex')]);
34
+ }
35
+ }
36
+ visit(root);
37
+ return result;
38
+ }
39
+ function fixture(t, minimal = false) {
40
+ mkdirSync(join(project, '.agent-tests'), { recursive: true });
41
+ const root = mkdtempSync(join(project, '.agent-tests/sync-node com espaço-'));
42
+ t.after(() => rmSync(root, { recursive: true, force: true }));
43
+ write(root, '.pbai4devs/templates/gitignore/agents.ignore', '# Cache dos clientes\n/*\n!/.gitignore\n{{ALLOWLIST}}\n/cache/\n*.log\n');
44
+ write(root, '.pbai4devs/templates/gitignore/clients.json', '{}\n');
45
+ write(root, '.pbai4devs/templates/gitignore/shared.ignore', '# Estado compartilhado\n/tools/*/.local/\n/tools/.cache/\n');
46
+ write(root, '.pbai4devs/espelhos.conf', minimal ? 'agente|.codex|plano\n' : [
47
+ '# Clientes da fixture', 'agente|.claude', 'agente|.opencode', 'agente|.github|plano', 'agente|.codex|plano', 'agente|.cursor|plano', 'agente|.kimi-code|plano', 'agente|.vscode|plano', 'agente|.agents|plano', 'agente|.agent|plano', 'agente|.gemini|plano',
48
+ `nativo|.github/instructions/demo.instructions.md|${source}`,
49
+ 'nativo|.kimi-code/agents/revisor.md|.pbai4devs/agents/revisor/AGENT.md|.pbai4devs/agents/revisor/kimi-code.yaml',
50
+ 'nativo|.opencode/agents/revisor/AGENT.md|.pbai4devs/agents/revisor/AGENT.md|.pbai4devs/agents/revisor/opencode.yaml',
51
+ ].join('\n') + '\n');
52
+ if (minimal) return root;
53
+ write(root, source, skill);
54
+ write(root, '.pbai4devs/skills/grupo com espaço/outro/SKILL.md', '---\nname: outro\ndescription: Outro.\n---\n\n# Outro\n');
55
+ write(root, '.pbai4devs/agents/revisor/AGENT.md', '---\nname: revisor\ndescription: Perfil.\n---\n\n# Revisor\n');
56
+ write(root, '.pbai4devs/rules/example/RULE.md', '---\ntrigger: always_on\ndescription: Regra.\n---\n\n# Regra\n');
57
+ write(root, '.pbai4devs/commands/check/COMMAND.md', '---\ndescription: Comando.\n---\n\n# Comando\n');
58
+ write(root, '.pbai4devs/agents/revisor/kimi-code.yaml', '---\nname: revisor\ntools: [Read, Grep, Glob]\n---\n');
59
+ write(root, '.pbai4devs/agents/revisor/opencode.yaml', '---\nmode: subagent\npermission:\n edit: deny\n---\n');
60
+ write(root, '.pbai4devs/adapters/antigravity/agents/revisor.md', pointer('../../../agents/revisor/AGENT.md', 'Revisor'));
61
+ write(root, '.opencode/agents/revisor/AGENT.md', '---\nmode: subagent\npermission:\n edit: deny\n---\n\n# Revisor\n\nLeia e siga [origem](../../../.pbai4devs/agents/revisor/AGENT.md).\n');
62
+ write(root, '.github/workflows/ci.yml', 'workflow-preservado\n');
63
+ write(root, '.vscode/settings.json', '{"fixture":"config-preservada"}\n');
64
+ write(root, '.vscode/extensions.json', '{"recommendations":[]}\n');
65
+ write(root, '.codex/config.toml', 'config-native-preservada\n');
66
+ write(root, '.github/prompts/custom.prompt.md', '# Prompt proprio\n');
67
+ write(root, '.claude/notas.md', '# Nota propria\n');
68
+ write(root, 'README.md', 'README raiz preservado\n');
69
+ for (const kind of ['templates', 'scripts', 'tools', 'external-skills', 'estado', 'tests', 'docs', 'adapters', 'extra']) {
70
+ write(root, `.pbai4devs/${kind}/demo/REFERENCE.md`, '---\ndescription: Exclusivo.\n---\n\n# Exclusivo\n');
71
+ if (kind !== 'extra') write(root, `.claude/${kind}/demo/REFERENCE.md`, pointer(`../../../.pbai4devs/${kind}/demo/REFERENCE.md`));
72
+ }
73
+ write(root, '.pbai4devs/tools/demo/TOOL.json', JSON.stringify(toolConfig()));
74
+ write(root, '.pbai4devs/tools/demo/SCRIPT.mjs', toolWrapper('demo'));
75
+ for (const file of ['README.md', 'INSTRUCTIONS.md']) {
76
+ write(root, `.pbai4devs/${file}`, '---\ndescription: Guia.\n---\n\n# Guia\n');
77
+ write(root, `.claude/${file}`, pointer(`../.pbai4devs/${file}`, 'Guia'));
78
+ }
79
+ return root;
80
+ }
81
+ function rejected(root, options = {}, pattern) {
82
+ const before = snapshot(root);
83
+ assert.throws(() => run(root, options), pattern);
84
+ assert.deepEqual(snapshot(root), before, 'Uma operação rejeitada alterou a fixture.');
85
+ }
86
+ function assertCheckReadonly(root, options = {}, exitCode = 1) {
87
+ const before = snapshot(root);
88
+ assert.equal(run(root, { check: true, ...options }).exitCode, exitCode);
89
+ assert.deepEqual(snapshot(root), before, '--check alterou a fixture.');
90
+ }
91
+
92
+ test('sincroniza somente principais, preserva metadados e usa referências UTF-8 relativas', t => {
93
+ const root = fixture(t);
94
+ assertCheckReadonly(root);
95
+ assert.equal(run(root).exitCode, 0);
96
+ assertCheckReadonly(root, {}, 0);
97
+ assert.match(read(root, '.codex/skills/demo/SKILL.md'), /name: demo/);
98
+ assert.doesNotMatch(read(root, '.codex/skills/demo/SKILL.md'), /NAO_DUPLICAR_INSTRUCOES/);
99
+ assert.match(read(root, '.codex/skills/outro/SKILL.md'), /grupo%20com%20espa%C3%A7o/);
100
+ assert.match(read(root, '.opencode/agents/revisor/AGENT.md'), /edit: deny/);
101
+ assert.match(read(root, '.kimi-code/agents/revisor.md'), /tools: \[Read, Grep, Glob\]/);
102
+ assert.ok(existsSync(join(root, '.claude/skills/projeto/demo/SKILL.md')));
103
+ for (const client of ['.claude', '.opencode', '.github', '.codex', '.cursor', '.kimi-code', '.vscode', '.agents', '.agent', '.gemini']) {
104
+ for (const kind of ['templates', 'scripts', 'tools', 'external-skills', 'estado', 'tests', 'docs', 'adapters', 'extra', 'README.md', 'INSTRUCTIONS.md']) assert.equal(existsSync(join(root, client, kind)), false, `${client}/${kind}`);
105
+ }
106
+ assert.deepEqual(JSON.parse(read(root, '.pbai4devs/agents.json')).entries, [{ path: '.pbai4devs/agents/revisor/AGENT.md' }]);
107
+ assert.ok(JSON.parse(read(root, '.pbai4devs/skills.json')).entries.some(entry => entry.path === '.pbai4devs/skills/grupo com espaço/outro'));
108
+ assert.deepEqual(JSON.parse(read(root, '.pbai4devs/rules.json')).entries, [{ path: '.pbai4devs/rules/example/RULE.md' }]);
109
+ assert.match(read(root, '.github/workflows/ci.yml'), /workflow-preservado/);
110
+ assert.match(read(root, '.vscode/settings.json'), /config-preservada/);
111
+ assert.ok(existsSync(join(root, '.vscode/extensions.json')));
112
+ assert.match(read(root, '.codex/config.toml'), /config-native-preservada/);
113
+ assert.ok(existsSync(join(root, '.github/prompts/custom.prompt.md')));
114
+ assert.ok(existsSync(join(root, '.claude/notas.md')));
115
+ const before = snapshot(root);
116
+ run(root);
117
+ assert.deepEqual(snapshot(root), before, 'A segunda sincronização deve ser idempotente.');
118
+ });
119
+
120
+ test('metadados genericos seguem a origem e overrides nativos ficam no YAML central', t => {
121
+ const root = fixture(t); run(root);
122
+ write(root, '.pbai4devs/commands/check/COMMAND.md', '---\nname: check-novo\ndescription: Comando atualizado.\n---\n\n# Comando\n');
123
+ write(root, '.pbai4devs/rules/example/RULE.md', '---\ntrigger: glob\nglobs: "**/*.ts"\ndescription: Regra atualizada.\n---\n\n# Regra\n');
124
+ write(root, '.pbai4devs/agents/revisor/AGENT.md', '---\nname: revisor\ndescription: Perfil atualizado.\n---\n\n# Revisor\n');
125
+ write(root, '.codex/commands/check.md', read(root, '.codex/commands/check.md').replace('description: Comando.', 'description: Obsoleto.\nlocal-only: true'));
126
+ assertCheckReadonly(root);
127
+ run(root);
128
+ assert.match(read(root, '.codex/commands/check.md'), /name: check-novo\ndescription: Comando atualizado\./);
129
+ assert.doesNotMatch(read(root, '.codex/commands/check.md'), /Obsoleto|local-only/);
130
+ assert.match(read(root, '.codex/rules/example.md'), /trigger: glob\nglobs: "\*\*\/\*\.ts"/);
131
+ assert.match(read(root, '.codex/agents/revisor.md'), /Perfil atualizado\./);
132
+ assert.match(read(root, '.opencode/agents/revisor/AGENT.md'), /edit: deny/);
133
+ assert.match(read(root, '.kimi-code/agents/revisor.md'), /tools: \[Read, Grep, Glob\]/);
134
+ assert.match(read(root, '.github/instructions/demo.instructions.md'), /applyTo: '\*\*'/);
135
+ assertCheckReadonly(root, {}, 0);
136
+ });
137
+
138
+ test('hub unico permite tres novos clientes, regras por inclusao e metadados nativos', t => {
139
+ const root = fixture(t);
140
+ write(root, '.pbai4devs/agents/revisor/antigravity.yaml', '---\nname: revisor\nmainAgent: false\nsubagent: true\n---\n');
141
+ write(root, '.pbai4devs/agents/revisor/gemini.yaml', '---\nname: revisor\nkind: local\n---\n');
142
+ write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + [
143
+ 'nativo|.agents/agents/revisor.md|.pbai4devs/agents/revisor/AGENT.md|.pbai4devs/agents/revisor/antigravity.yaml',
144
+ 'nativo|.agent/agents/revisor.md|.pbai4devs/agents/revisor/AGENT.md|.pbai4devs/agents/revisor/antigravity.yaml',
145
+ 'nativo|.gemini/agents/revisor.md|.pbai4devs/agents/revisor/AGENT.md|.pbai4devs/agents/revisor/gemini.yaml',
146
+ ].join('\n') + '\n');
147
+ run(root);
148
+ for (const client of ['.agents', '.agent', '.gemini']) {
149
+ assert.ok(existsSync(join(root, client, 'skills/demo/SKILL.md')));
150
+ assert.ok(existsSync(join(root, client, '.gitignore')));
151
+ for (const extra of ['tools', 'scripts', 'templates', 'docs', 'external-skills']) assert.equal(existsSync(join(root, client, extra)), false);
152
+ }
153
+ for (const client of ['.agents', '.agent']) {
154
+ assert.match(read(root, `${client}/agents/revisor.md`), /mainAgent: false/);
155
+ assert.match(read(root, `${client}/rules/example.md`), /@\[origem\]\(\.\.\/\.\.\/\.pbai4devs\/rules\/example\/RULE\.md\)/);
156
+ assert.ok(isReference(read(root, `${client}/rules/example.md`)));
157
+ const target = read(root, `${client}/rules/example.md`).match(/@\[origem\]\(([^)]+)\)/)[1];
158
+ assert.ok(existsSync(resolve(root, client, 'rules', decodeURIComponent(target))));
159
+ }
160
+ assert.match(read(root, '.gemini/agents/revisor.md'), /kind: local/);
161
+ const catalog = JSON.parse(read(root, '.pbai4devs/catalog.json'));
162
+ assert.equal(catalog.agents.length, 1);
163
+ assert.deepEqual(JSON.parse(read(root, '.pbai4devs/agents.json')), { entries: [{ path: '.pbai4devs/agents/revisor/AGENT.md' }] });
164
+ assert.ok(catalog.skills.every(entry => entry.path.startsWith('.pbai4devs/')));
165
+ assertCheckReadonly(root, {}, 0);
166
+ assert.doesNotThrow(() => validateIgnoreClients({ '.agents': {}, '.agent': {}, '.gemini': {} }));
167
+ assert.throws(() => validateIgnoreClients({ '.pbai4devs': {} }));
168
+ });
169
+
170
+ test('Gemini usa TOML com inclusao relativa ao workspace e argumentos, sem Markdown duplicado', t => {
171
+ const root = fixture(t);
172
+ write(root, '.pbai4devs/commands/check/COMMAND.md', '---\ndescription: "Verifica \\"aspas\\" e regras."\n---\n\n# Check\n');
173
+ write(root, '.gemini/commands/check.md', pointer('../../.pbai4devs/commands/check/COMMAND.md'));
174
+ run(root);
175
+ const target = '.gemini/commands/check.toml';
176
+ const text = read(root, target); const lines = text.trimEnd().split('\n');
177
+ assert.equal(lines[0], '# gerado: sincronizar-agentes');
178
+ assert.equal(JSON.parse(lines[1].slice('description = '.length)), 'Verifica "aspas" e regras.');
179
+ assert.equal(JSON.parse(lines[2].slice('prompt = '.length)), '@{.pbai4devs/commands/check/COMMAND.md}\n\n{{args}}');
180
+ assert.ok(isReference(text)); assert.equal(existsSync(join(root, '.gemini/commands/check.md')), false);
181
+ write(root, target, text + '# instrucao propria\n');
182
+ assertCheckReadonly(root); rejected(root, {}, /--force/);
183
+ const repaired = run(root, { force: true });
184
+ assert.equal(read(repaired.backupRoot, target), text + '# instrucao propria\n');
185
+ assert.equal(read(root, target), text);
186
+ rmSync(join(root, '.pbai4devs/commands/check'), { recursive: true });
187
+ run(root); assert.equal(existsSync(join(root, target)), false);
188
+ assert.match(read(root, '.github/workflows/ci.yml'), /workflow-preservado/);
189
+ });
190
+
191
+ test('nativos TOML ficam restritos aos comandos Gemini e ponteiros falsos nao sao apagados como gerados', t => {
192
+ for (const row of [
193
+ 'nativo|.claude/commands/check.toml|.pbai4devs/commands/check/COMMAND.md',
194
+ 'nativo|.gemini/agents/revisor.toml|.pbai4devs/agents/revisor/AGENT.md',
195
+ 'nativo|.gemini/commands/check.toml|.pbai4devs/rules/example/RULE.md',
196
+ 'nativo|.gemini/commands/check.md|.pbai4devs/commands/check/COMMAND.md',
197
+ 'nativo|.gemini/commands/check.toml|.pbai4devs/commands/check/COMMAND.md|.pbai4devs/agents/revisor/kimi-code.yaml',
198
+ ]) {
199
+ const root = fixture(t); write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + row + '\n');
200
+ rejected(root, { force: true });
201
+ }
202
+ assert.equal(isReference('# gerado: sincronizar-agentes\ndescription = "teste"\nprompt = "!{rm -rf .}"\n'), false);
203
+ assert.equal(isReference('# gerado: sincronizar-agentes\ndescription = "teste"\nprompt = "@{.pbai4devs/commands/../private/COMMAND.md}\\n\\n{{args}}"\n'), false);
204
+ assert.equal(isReference('---\ndescription: regra\n---\n\n# Regra\n\n@[../../.pbai4devs/rules/demo/RULE.md]\nTexto proprio\n'), false);
205
+ });
206
+
207
+ test('gera gitignore canonico e de clientes; registro gitignore nao espelha recursos', t => {
208
+ const root = fixture(t, true);
209
+ write(root, source, skill);
210
+ write(root, '.pbai4devs/espelhos.conf', 'agente|.codex|plano\ngitignore|.vscode\n');
211
+ write(root, '.pbai4devs/templates/gitignore/clients.json', JSON.stringify({ '.vscode': { resources: false, allow: ['settings.json'] } }));
212
+ write(root, '.vscode/settings.json', '{"preserved":true}\n');
213
+ write(root, '.gitignore', '# Somente regras gerais\n*.tmp\n');
214
+ const result = run(root);
215
+ assert.equal(result.gitignores, 3);
216
+ assert.equal(read(root, '.pbai4devs/.gitignore'), `${ignoreBegin}\n${read(root, '.pbai4devs/templates/gitignore/shared.ignore')}${ignoreEnd}\n`);
217
+ for (const target of ['.codex', '.vscode']) assert.doesNotMatch(read(root, `${target}/.gitignore`), /\{\{ALLOWLIST\}\}/);
218
+ assert.match(read(root, '.codex/.gitignore'), /!\/skills\//);
219
+ assert.doesNotMatch(read(root, '.vscode/.gitignore'), /!\/skills/);
220
+ assert.match(read(root, '.vscode/.gitignore'), /!\/settings\.json/);
221
+ assert.equal(existsSync(join(root, '.vscode/skills')), false);
222
+ assert.equal(read(root, '.vscode/settings.json'), '{"preserved":true}\n');
223
+ assert.equal(read(root, '.gitignore'), '# Somente regras gerais\n*.tmp\n');
224
+ assertCheckReadonly(root, {}, 0);
225
+ });
226
+
227
+ test('repara somente bloco gitignore e force salva o original sem perder regras externas', t => {
228
+ const root = fixture(t, true); run(root);
229
+ const prefix = '# Regras do usuario\r\n/private-cache/\r\n';
230
+ const suffix = '# Excecao local\r\n!keep.log\r\n';
231
+ const old = `${prefix}${ignoreBegin}\r\n/custom-inside/\r\n${ignoreEnd}\r\n${suffix}`;
232
+ write(root, '.codex/.gitignore', old);
233
+ assertCheckReadonly(root); assertCheckReadonly(root, { force: true });
234
+ const result = run(root, { force: true });
235
+ assert.equal(readFileSync(join(result.backupRoot, '.codex/.gitignore'), 'utf8'), old);
236
+ const repaired = readFileSync(join(root, '.codex/.gitignore'), 'utf8');
237
+ assert.ok(repaired.startsWith(prefix)); assert.ok(repaired.endsWith(suffix));
238
+ assert.ok(!repaired.includes('/custom-inside/')); assert.ok(repaired.includes('/cache/'));
239
+ write(root, '.pbai4devs/templates/gitignore/agents.ignore', '# Novo padrao\n{{ALLOWLIST}}\n/cache/\n/session/\n');
240
+ assertCheckReadonly(root);
241
+ run(root);
242
+ assert.ok(read(root, '.codex/.gitignore').includes('/session/'));
243
+ assert.ok(readFileSync(join(root, '.codex/.gitignore'), 'utf8').endsWith(suffix));
244
+ assertCheckReadonly(root, {}, 0);
245
+ });
246
+
247
+ test('adiciona bloco a gitignore proprio, detecta exclusao e mantem idempotencia', t => {
248
+ const root = fixture(t, true);
249
+ const custom = '# Local\n/custom/\n!custom/keep'; write(root, '.codex/.gitignore', custom);
250
+ run(root); assert.ok(read(root, '.codex/.gitignore').startsWith(custom));
251
+ const before = snapshot(root); run(root); assert.deepEqual(snapshot(root), before);
252
+ rmSync(join(root, '.pbai4devs/.gitignore')); assertCheckReadonly(root);
253
+ assert.equal(run(root).gitignores, 1); assertCheckReadonly(root, {}, 0);
254
+ });
255
+
256
+ test('gitignore recusa marcadores invalidos e fontes ausentes antes de qualquer mutacao', t => {
257
+ for (const text of [ignoreBegin, ignoreEnd, `${ignoreEnd}\n${ignoreBegin}\n`, `${ignoreBegin}\na\n${ignoreEnd}\n${ignoreBegin}\nb\n${ignoreEnd}\n`]) {
258
+ const root = fixture(t, true); write(root, '.codex/.gitignore', text);
259
+ rejected(root, { force: true }, /Marcadores/);
260
+ }
261
+ for (const text of ['', `${ignoreBegin}\n/cache/\n${ignoreEnd}\n`]) {
262
+ const root = fixture(t, true); write(root, '.pbai4devs/templates/gitignore/shared.ignore', text);
263
+ rejected(root, { force: true }, /Template/);
264
+ }
265
+ const missing = fixture(t, true); rmSync(join(missing, '.pbai4devs/templates/gitignore/agents.ignore'));
266
+ rejected(missing, { force: true }, /Template .gitignore ausente/);
267
+ for (const row of ['gitignore|.vscode|extra', 'gitignore|.pbai4devs', 'gitignore|../outside', 'gitignore|.git', 'gitignore|.CODEX']) {
268
+ const root = fixture(t, true); write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + row + '\n');
269
+ rejected(root, { force: true });
270
+ }
271
+ });
272
+
273
+ test('gitignore gerado funciona no Git sem esconder definicoes versionadas', t => {
274
+ const root = fixture(t);
275
+ const git = spawnSync('git', ['init', '-q', root], { encoding: 'utf8' });
276
+ if (git.error?.code === 'ENOENT') { t.skip('Git ausente: prova de regras delegada a integracao.'); return; }
277
+ assert.equal(git.status, 0, git.stderr); run(root);
278
+ for (const file of ['.pbai4devs/tools/demo/.local/state.json', '.codex/cache/session.json', '.claude/debug.log']) {
279
+ write(root, file, '{}\n');
280
+ assert.equal(spawnSync('git', ['check-ignore', '--no-index', file], { cwd: root }).status, 0, file);
281
+ }
282
+ for (const file of ['.pbai4devs/tools/demo/TOOL.json', '.pbai4devs/tools/demo/SCRIPT.mjs', '.codex/skills/demo/SKILL.md', '.codex/.gitignore', '.pbai4devs/.gitignore']) {
283
+ assert.equal(spawnSync('git', ['check-ignore', '--no-index', file], { cwd: root }).status, 1, file);
284
+ }
285
+ });
286
+
287
+ test('mergeGitignore preserva bytes exteriores e nao interpreta o arquivo como Markdown', () => {
288
+ const current = `---\r\ncustom: true\r\n---\r\n${ignoreBegin}\r\nold\r\n${ignoreEnd}\r\n**/other\r\n`;
289
+ const merged = mergeGitignore(current, '*.log\n');
290
+ assert.equal(merged.hasBlock, true);
291
+ assert.equal(merged.text, `---\r\ncustom: true\r\n---\r\n${ignoreBegin}\n*.log\n${ignoreEnd}\n**/other\r\n`);
292
+ });
293
+
294
+ test('allowlist abre somente configuracoes e diretorios declarados no Git', t => {
295
+ const root = fixture(t, true);
296
+ write(root, '.pbai4devs/espelhos.conf', 'agente|.codex|plano\ngitignore|.vscode\n');
297
+ write(root, '.pbai4devs/templates/gitignore/clients.json', JSON.stringify({ '.codex': { allow: ['config.toml', 'native/file.json'] }, '.vscode': { resources: false, allow: ['settings.json', 'snippets/'] } }));
298
+ write(root, '.gitignore', '.DS_Store\n*.swp\nThumbs.db\n');
299
+ const git = spawnSync('git', ['init', '-q', root], { encoding: 'utf8' });
300
+ if (git.error?.code === 'ENOENT') { t.skip('Git ausente: prova de allowlist delegada a integracao.'); return; }
301
+ assert.equal(git.status, 0, git.stderr); run(root);
302
+ for (const file of ['.codex/config.toml', '.codex/native/file.json', '.codex/skills/demo/SKILL.md', '.vscode/settings.json', '.vscode/snippets/example.code-snippets']) {
303
+ write(root, file, 'fixture\n'); assert.equal(spawnSync('git', ['check-ignore', '--no-index', file], { cwd: root }).status, 1, file);
304
+ }
305
+ for (const file of ['.codex/native/extra.json', '.codex/auth.json', '.vscode/skills/demo/SKILL.md', '.vscode/tasks.json', '.codex/skills/demo/.DS_Store', '.codex/skills/demo/file.swp', '.vscode/snippets/Thumbs.db']) {
306
+ write(root, file, 'fixture\n'); assert.equal(spawnSync('git', ['check-ignore', '--no-index', file], { cwd: root }).status, 0, file);
307
+ }
308
+ assert.equal(existsSync(join(root, '.codex/clients.json')), false);
309
+ });
310
+
311
+ test('allowlist invalida ou token ausente falha antes de mutacoes', t => {
312
+ for (const config of [[], null, { '.pbai4devs': {} }, { '../outside': {} }, { '.codex': [], '.vscode': {} }, { '.codex': { resources: 'false' } }, { '.codex': { allow: null } }, { '.codex': { extra: true } }, { '.codex': {}, '.CODEX': {} }]) assert.throws(() => validateIgnoreClients(config));
313
+ for (const path of ['/absolute', '../outside', 'nested/../outside', './file', 'C:/file', 'back\\slash', 'double//slash', '*', '*.json', '!file', '#file', 'ends ', '']) assert.throws(() => validateIgnoreClients({ '.codex': { allow: [path] } }), /relativo/);
314
+ for (const body of ['{{ALLOWLIST}}\n{{ALLOWLIST}}\n']) {
315
+ const root = fixture(t, true); write(root, '.pbai4devs/templates/gitignore/agents.ignore', body); rejected(root, { force: true }, /unico/);
316
+ }
317
+ const root = fixture(t, true); write(root, '.pbai4devs/templates/gitignore/clients.json', '{broken'); rejected(root, { force: true });
318
+ });
319
+
320
+ test('registros são derivados, determinísticos e --check nunca os corrige', t => {
321
+ const root = fixture(t);
322
+ run(root);
323
+ write(root, '.pbai4devs/skills.json', '{"incorreto":true}\n');
324
+ assertCheckReadonly(root);
325
+ run(root);
326
+ assert.ok(Array.isArray(JSON.parse(read(root, '.pbai4devs/skills.json')).entries));
327
+ rmSync(join(root, '.pbai4devs/adapters/antigravity/agents/revisor.md'));
328
+ run(root);
329
+ assert.deepEqual(JSON.parse(read(root, '.pbai4devs/agents.json')).entries, [{ path: '.pbai4devs/agents/revisor/AGENT.md' }]);
330
+ const empty = fixture(t, true);
331
+ run(empty);
332
+ assert.deepEqual(JSON.parse(read(empty, '.pbai4devs/skills.json')), { entries: [] });
333
+ assertCheckReadonly(empty, {}, 0);
334
+ });
335
+
336
+ test('skills externas baixadas não entram nos índices; --force salva e limpa cópias indevidas', t => {
337
+ const root = fixture(t);
338
+ write(root, '.pbai4devs/external-skills/README.md', '# Catálogo externo\n');
339
+ write(root, '.pbai4devs/external-skills/CATALOG.json', JSON.stringify(externalConfig()));
340
+ for (let index = 0; index < 80; index++) write(root, `.pbai4devs/external-skills/.local/upstream-${index}/SKILL.md`, '# Skill upstream sem frontmatter\n');
341
+ run(root);
342
+ const index = JSON.parse(read(root, '.pbai4devs/skills.json')).entries;
343
+ assert.equal(index.length, 2);
344
+ assert.ok(index.every(entry => entry.path.startsWith('.pbai4devs/skills/')));
345
+ assert.equal(existsSync(join(root, '.codex/skills/upstream-0')), false);
346
+ const sourceBefore = read(root, '.pbai4devs/external-skills/.local/upstream-0/SKILL.md');
347
+ write(root, '.codex/external-skills/.local/upstream-0/SKILL.md', '# Cópia indevida\n');
348
+ write(root, '.claude/external-skills/README.md', pointer('../../.pbai4devs/external-skills/README.md'));
349
+ assertCheckReadonly(root);
350
+ rejected(root, {}, /--force/);
351
+ const result = run(root, { force: true });
352
+ for (const client of ['.codex', '.claude']) {
353
+ assert.equal(existsSync(join(root, client, 'external-skills')), false);
354
+ assert.ok(existsSync(join(result.backupRoot, client, 'external-skills')));
355
+ }
356
+ assert.equal(read(root, '.pbai4devs/external-skills/.local/upstream-0/SKILL.md'), sourceBefore);
357
+ assert.equal(read(result.backupRoot, '.codex/external-skills/.local/upstream-0/SKILL.md'), '# Cópia indevida\n');
358
+ assertCheckReadonly(root, {}, 0);
359
+ });
360
+
361
+ test('--force salva conteúdo próprio, repara ancestrais e preserva arquivos fora do escopo', t => {
362
+ const root = fixture(t);
363
+ run(root);
364
+ write(root, '.claude/scripts/local.sh', 'script humano\n');
365
+ write(root, '.claude/tools/program.exe', Buffer.from([1, 2, 3]));
366
+ write(root, '.claude/docs/plain.md', '# Documento auxiliar\n');
367
+ write(root, '.claude/skills/local.bin', 'custom\n');
368
+ write(root, '.claude/skills/projeto/demo/SKILL.md', 'corrompido\n');
369
+ write(root, '.kimi-code/agents/revisor.md', 'corrompido\n');
370
+ rmSync(join(root, '.codex/skills/outro'), { recursive: true });
371
+ write(root, '.codex/skills/outro', 'ancestral arquivo\n');
372
+ write(root, '.github/prompts/edited.prompt.md', pointer() + '\nRevisar [manual](bar).\n');
373
+ assertCheckReadonly(root);
374
+ assertCheckReadonly(root, { force: true });
375
+ rejected(root, {}, /--force/);
376
+ const result = run(root, { force: true });
377
+ assert.ok(result.backupRoot.startsWith(join(root, '.agent-tests')));
378
+ 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}`);
379
+ assert.deepEqual(readFileSync(join(result.backupRoot, '.claude/tools/program.exe')), Buffer.from([1, 2, 3]));
380
+ assert.equal(existsSync(join(root, '.claude/scripts')), false);
381
+ assert.equal(existsSync(join(root, '.claude/skills/local.bin')), false);
382
+ assert.equal(existsSync(join(root, '.github/prompts/edited.prompt.md')), false);
383
+ assert.match(read(root, '.codex/skills/outro/SKILL.md'), /name: outro/);
384
+ assert.match(read(root, '.github/workflows/ci.yml'), /workflow-preservado/);
385
+ assert.match(read(root, '.codex/config.toml'), /config-native-preservada/);
386
+ assertCheckReadonly(root, {}, 0);
387
+ });
388
+
389
+ test('atualiza fontes, adiciona clientes, remove recursos e migra agrupado para plano', t => {
390
+ const root = fixture(t);
391
+ run(root);
392
+ write(root, source, skill.replace('Exemplo.', 'Atualizado.'));
393
+ write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + 'agente|.extra|plano\n');
394
+ assertCheckReadonly(root);
395
+ run(root);
396
+ assert.match(read(root, '.extra/skills/demo/SKILL.md'), /Atualizado/);
397
+ rmSync(join(root, '.pbai4devs/rules/example/RULE.md'));
398
+ write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf').replace('agente|.claude\n', 'agente|.claude|plano\n'));
399
+ run(root);
400
+ assert.equal(existsSync(join(root, '.claude/skills/projeto')), false);
401
+ assert.ok(existsSync(join(root, '.claude/skills/demo/SKILL.md')));
402
+ assert.equal(existsSync(join(root, '.codex/rules/example.md')), false);
403
+ assert.deepEqual(JSON.parse(read(root, '.pbai4devs/rules.json')).entries, []);
404
+ assertCheckReadonly(root, {}, 0);
405
+ });
406
+
407
+ test('remove linhas legadas e origens nativas ausentes sem replicar documentos', t => {
408
+ const root = fixture(t);
409
+ run(root);
410
+ write(root, 'docs/produto/prd/demo/PRD.md', '# Produto real\n');
411
+ write(root, 'manual/guia com espaço/DOC.md', '# Documento real\n');
412
+ write(root, '.claude/manual/DOC.md', pointer('../../manual/guia%20com%20espa%C3%A7o/DOC.md'));
413
+ write(root, '.github/prompts/missing.prompt.md', pointer());
414
+ write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + 'documento|manual\n' + 'nativo|.github/prompts/missing.prompt.md|.pbai4devs/commands/missing/COMMAND.md\n');
415
+ assertCheckReadonly(root);
416
+ run(root);
417
+ assert.doesNotMatch(read(root, '.pbai4devs/espelhos.conf'), /documento\||missing/);
418
+ assert.equal(existsSync(join(root, '.claude/manual/DOC.md')), false);
419
+ assert.equal(existsSync(join(root, '.github/prompts/missing.prompt.md')), false);
420
+ assert.ok(existsSync(join(root, 'docs/produto/prd/demo/PRD.md')));
421
+ assert.ok(existsSync(join(root, 'manual/guia com espaço/DOC.md')));
422
+ assertCheckReadonly(root, {}, 0);
423
+ });
424
+
425
+ test('rejeita manifestos fora do escopo, campos extras, caminhos e caixas conflitantes sem mutação', t => {
426
+ const rows = [
427
+ 'agente|.git', 'agente|.AGENTS|plano', 'agente|.GIT|plano', 'agente|.AgEnT-TeStS|plano', 'agente|.CLAUDE|plano', 'agente|.claude|plano', 'agente|.other|invalid', 'agente|.other|plano|extra',
428
+ 'agente|../outside|plano', 'agente|C:/outside|plano', 'agente|.test//dir|plano', 'agente|.test\\dir|plano', 'agente|.test|plano|extra|another',
429
+ `nativo|.github/workflows/unsafe.md|${source}`, `nativo|.github/WorkFlows/unsafe.md|${source}`, `nativo|.claude/templates/unsafe.md|${source}`, `nativo|.claude/external-skills/unsafe.md|${source}`, `nativo|.claude/README.md|${source}`, `nativo|.outside/prompts/unsafe.md|${source}`,
430
+ 'nativo|.claude/prompts/aux.md|.pbai4devs/templates/demo/REFERENCE.md', 'nativo|.github/prompts/aux.md|.pbai4devs/skills/projeto/demo/REFERENCE.md', 'nativo|.claude/prompts/wrong.md|.pbai4devs/commands/check/COMMAND.md|.pbai4devs/agents/revisor/kimi-code.yaml',
431
+ ];
432
+ for (const row of rows) {
433
+ const root = fixture(t);
434
+ write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + row + '\n');
435
+ rejected(root, { force: true });
436
+ }
437
+ });
438
+
439
+ test('colisões de destino, ancestral, metadados e slugs não são resolvidas por --force', t => {
440
+ for (const variant of ['slug', 'source', 'ancestor', 'metadata', 'target-case']) {
441
+ const root = fixture(t);
442
+ let extra;
443
+ if (variant === 'slug') write(root, '.pbai4devs/skills/duplicado/demo/SKILL.md', skill);
444
+ if (variant === 'source') extra = 'nativo|.codex/skills/demo/SKILL.md|.pbai4devs/commands/check/COMMAND.md';
445
+ if (variant === 'ancestor') extra = [`nativo|.github/custom.md|${source}`, `nativo|.github/custom.md/child.md|${source}`].join('\n');
446
+ if (variant === 'target-case') extra = [`nativo|.github/custom.md|${source}`, `nativo|.github/CUSTOM.md|${source}`].join('\n');
447
+ if (variant === 'metadata') {
448
+ write(root, '.pbai4devs/agents/revisor/other.yaml', '---\nname: other\n---\n');
449
+ extra = 'nativo|.kimi-code/agents/revisor.md|.pbai4devs/agents/revisor/AGENT.md|.pbai4devs/agents/revisor/other.yaml';
450
+ }
451
+ if (extra) write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + extra + '\n');
452
+ rejected(root, { force: true }, /conflito|ancestral|maiusculas/i);
453
+ }
454
+ });
455
+
456
+ test('metadados incompletos, ausentes e com corpo são rejeitados antes de alterações', t => {
457
+ for (const text of ['name: revisor\n', '---\nname: revisor\n', '---\nname: revisor\n---\ntexto indevido\n', null]) {
458
+ const root = fixture(t);
459
+ const metadata = '.pbai4devs/agents/revisor/kimi-code.yaml';
460
+ if (text === null) rmSync(join(root, metadata)); else write(root, metadata, text);
461
+ rejected(root, { force: true }, /YAML|Metadados|Frontmatter/);
462
+ }
463
+ const root = fixture(t);
464
+ write(root, source, '---\nname: demo\n---\nSem titulo\n');
465
+ rejected(root, { force: true }, /Titulo/);
466
+ });
467
+
468
+ test('symlinks são rejeitados sem efeitos quando o sistema permite criá-los', t => {
469
+ for (const location of ['.claude/tools', '.pbai4devs/skills/projeto/demo/external.md', '.pbai4devs/skills.json', '.pbai4devs/espelhos.conf']) {
470
+ const root = fixture(t);
471
+ rmSync(join(root, location), { recursive: true, force: true });
472
+ try { symlinkSync(join(root, 'missing-target'), join(root, location)); }
473
+ catch (error) { if (['EPERM', 'EACCES'].includes(error.code)) { t.skip('Sistema não permite criar symlinks de teste.'); return; } throw error; }
474
+ rejected(root, { force: true }, /Link simbolico/);
475
+ }
476
+ });
477
+
478
+ test('nomes de controle são rejeitados sem efeitos em todos os sistemas', t => {
479
+ const root = fixture(t);
480
+ const manifest = read(root, '.pbai4devs/espelhos.conf');
481
+ write(root, '.pbai4devs/espelhos.conf', manifest + `nativo|.claude/skills/invalid\u0001.md|${source}\n`);
482
+ rejected(root, { force: true }, /Caminho invalido/);
483
+ assert.match(read(root, 'README.md'), /README raiz preservado/);
484
+
485
+ if (process.platform !== 'win32') {
486
+ const filesystem = fixture(t);
487
+ write(filesystem, '.claude/skills/invalid\nREADME.md', 'nunca deve fragmentar caminho\n');
488
+ rejected(filesystem, { force: true }, /Caminho invalido/);
489
+ assert.match(read(filesystem, 'README.md'), /README raiz preservado/);
490
+ }
491
+ });
492
+
493
+ test('registros não regulares são rejeitados sem efeitos', t => {
494
+ const other = fixture(t);
495
+ mkdirSync(join(other, '.pbai4devs/skills.json'));
496
+ rejected(other, { force: true }, /Registro nao e arquivo/);
497
+ });
498
+
499
+ test('backup recusa raiz simbólica antes de remover conteúdo próprio', t => {
500
+ const root = fixture(t);
501
+ run(root);
502
+ write(root, '.claude/scripts/own.sh', 'preservar\n');
503
+ const outside = join(root, 'outside');
504
+ mkdirSync(outside);
505
+ try { symlinkSync(outside, join(root, '.agent-tests'), 'dir'); }
506
+ catch (error) { if (['EPERM', 'EACCES'].includes(error.code)) { t.skip('Sistema não permite criar symlinks de teste.'); return; } throw error; }
507
+ rejected(root, { force: true }, /Link simbolico/);
508
+ assert.match(read(root, '.claude/scripts/own.sh'), /preservar/);
509
+ assert.deepEqual(readdirSync(outside), []);
510
+ });
511
+
512
+ test('frontmatter e reconhecimento de apontamentos não confundem instruções próprias', () => {
513
+ assert.equal(header('---\r\nname: test\r\n---\r\nbody\r\n'), '---\nname: test\n---\n');
514
+ assert.ok(isReference(pointer()));
515
+ assert.ok(isReference('# Git\n\nLeia [origem](x) ao realizar operações Git.\n'));
516
+ assert.equal(isReference(pointer() + '\nInstrução própria.\n'), false);
517
+ assert.equal(isReference(pointer() + '\nLeia [outra](x).\n'), false);
518
+ assert.equal(isReference('---\nsem fechamento\n# Titulo\nLeia [origem](x).\n'), false);
519
+ assert.throws(() => header('sem YAML'), /YAML/);
520
+ });
521
+
522
+ test('CLI Node propaga códigos de check e rejeita opções sem invocar shell', t => {
523
+ const root = fixture(t);
524
+ write(root, '.pbai4devs/templates/gitignore/local.ignore', readFileSync(join(project, '.pbai4devs/templates/gitignore/local.ignore'), 'utf8'));
525
+ write(root, '.pbai4devs/templates/gitignore/agents.ignore', readFileSync(join(project, '.pbai4devs/templates/gitignore/agents.ignore'), 'utf8'));
526
+ assert.equal(spawnSync('git', ['init', '-q', root]).status, 0);
527
+ run(root, { localIgnores: true });
528
+ const before = snapshot(root);
529
+ const good = spawnSync(process.execPath, [cli, '--root', root, '--check'], { encoding: 'utf8' });
530
+ assert.equal(good.status, 0, good.stderr);
531
+ assert.match(good.stdout, /0 desatualizadas/);
532
+ for (const option of ['--documento', '--invalid', '--root']) {
533
+ const result = spawnSync(process.execPath, [cli, '--root', root, option], { encoding: 'utf8' });
534
+ assert.equal(result.status, 2, result.stderr);
535
+ }
536
+ assert.deepEqual(snapshot(root), before);
537
+ write(root, source, skill.replace('Exemplo.', 'Novo.'));
538
+ const changed = spawnSync(process.execPath, [cli, '--root', root, '--force', '--check'], { encoding: 'utf8' });
539
+ assert.equal(changed.status, 1, changed.stderr);
540
+ });
541
+
542
+ test('gera manifesto e entradas sem estado e atualiza apenas entradas integralmente próprias', t => {
543
+ const root = fixture(t, true);
544
+ write(root, source, skill);
545
+ rmSync(join(root, '.pbai4devs/espelhos.conf'));
546
+ for (const name of ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']) write(root, `.pbai4devs/templates/entrypoints/${name}`, `---\ndescription: Entrada.\n---\n\n# ${name}\n`);
547
+ write(root, 'CLAUDE.md', '# Instruções humanas\n');
548
+ assertCheckReadonly(root, { generateEntrypoints: true });
549
+ const first = run(root, { generateEntrypoints: true });
550
+ assert.ok(first.ownedOutputs.includes('AGENTS.md'));
551
+ assert.equal(first.ownedOutputs.includes('CLAUDE.md'), false);
552
+ assert.equal(read(root, 'CLAUDE.md'), '# Instruções humanas\n');
553
+ assert.ok(existsSync(join(root, '.agents/skills/projeto/demo/SKILL.md')));
554
+ assertCheckReadonly(root, { generateEntrypoints: true }, 0);
555
+ write(root, '.pbai4devs/templates/entrypoints/AGENTS.md', '---\ndescription: Entrada.\n---\n\n# Atualizada\n');
556
+ run(root, { generateEntrypoints: true });
557
+ assert.match(read(root, 'AGENTS.md'), /# Atualizada/);
558
+ write(root, 'AGENTS.md', read(root, 'AGENTS.md') + '\nTexto humano.\n');
559
+ const second = run(root, { generateEntrypoints: true });
560
+ assert.equal(second.ownedOutputs.includes('AGENTS.md'), false);
561
+ assert.match(read(root, 'AGENTS.md'), /Texto humano/);
562
+ });
563
+
564
+ test('entradas da CLI usam ownership integral sem criar registro derivado redundante', t => {
565
+ const root = fixture(t, true);
566
+ const template = '---\ndescription: Entrada.\n---\n\n# Agentes\n';
567
+ const entry = template + '\nResponda em português.\n';
568
+ write(root, source, skill);
569
+ write(root, '.pbai4devs/templates/entrypoints/AGENTS.md', template);
570
+ write(root, 'AGENTS.md', entry);
571
+ write(root, '.pbai4devs/estado/agentdevkit.json', JSON.stringify({ clients: ['codex'], files: { 'AGENTS.md': createHash('sha256').update(entry).digest('hex') } }));
572
+ const result = run(root, { generateEntrypoints: true });
573
+ assert.ok(result.ownedOutputs.includes('AGENTS.md'));
574
+ assert.equal(read(root, 'AGENTS.md'), entry);
575
+ assert.equal(existsSync(join(root, '.pbai4devs/estado/derived.json')), false);
576
+ assertCheckReadonly(root, { generateEntrypoints: true }, 0);
577
+ });
578
+
579
+ function completeCatalogFixture(t) {
580
+ const root = fixture(t, true);
581
+ for (const [kind, file] of Object.entries({ skills: 'SKILL.md', agents: 'AGENT.md', commands: 'COMMAND.md', rules: 'RULE.md', templates: 'TEMPLATE.md' })) {
582
+ write(root, `.pbai4devs/${kind}/demo/${file}`, `---\nname: demo\n${kind === 'rules' ? 'trigger: always_on\n' : ''}description: ${kind} original.\n---\n\n# Demo ${kind}\n`);
583
+ }
584
+ write(root, '.pbai4devs/templates/demo/config.yaml.template', 'example: true\n');
585
+ write(root, '.pbai4devs/tools/demo/TOOL.json', JSON.stringify(toolConfig()));
586
+ write(root, '.pbai4devs/tools/demo/SCRIPT.mjs', toolWrapper('demo'));
587
+ write(root, '.pbai4devs/scripts/demo/SCRIPT.mjs', '#!/usr/bin/env node\nconsole.log("fixture");\n');
588
+ write(root, '.pbai4devs/scripts/demo/README.md', '---\ndescription: scripts original.\n---\n\n# Demo\n');
589
+ write(root, '.pbai4devs/scripts/demo/tests/TEST.mjs', '// Teste versionado.\n');
590
+ write(root, '.pbai4devs/scripts/demo/tests/SMOKE_DEMO.mjs', '// Smoke versionado.\n');
591
+ write(root, '.pbai4devs/scripts/lib/shared.mjs', 'export const shared = true;\n');
592
+ write(root, '.pbai4devs/external-skills/CATALOG.json', JSON.stringify(externalConfig()));
593
+ return root;
594
+ }
595
+
596
+ test('nativos recusam principais dentro de estado ignorado e fora do catalogo', t => {
597
+ for (const directory of ['.local', '.cache', 'tmp', 'test-results', 'playwright-report']) {
598
+ const root = fixture(t, true);
599
+ const hidden = `.pbai4devs/skills/${directory}/oculto/SKILL.md`;
600
+ write(root, hidden, skill);
601
+ write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + `nativo|.codex/skills/oculto/SKILL.md|${hidden}\n`);
602
+ rejected(root, { force: true }, /fora do catalogo canonico/);
603
+ }
604
+ });
605
+
606
+ test('regras validam trigger e contexto antes de gerar registros ou espelhos', t => {
607
+ const root = fixture(t, true); const path = '.pbai4devs/rules/demo/RULE.md';
608
+ const rule = fields => `---\ndescription: Regra de teste.\n${fields}---\n\n# Regra\n`;
609
+ for (const [fields, expected] of [
610
+ ['trigger: always_on\n', { trigger: 'always_on' }],
611
+ ['trigger: model_decision\n', { trigger: 'model_decision' }],
612
+ ['trigger: manual\n', { trigger: 'manual' }],
613
+ ['trigger: glob\nglobs: "**/*.ts"\n', { trigger: 'glob', globs: '**/*.ts' }],
614
+ ['trigger: glob\nglob: "src/**"\n', { trigger: 'glob', globs: 'src/**' }],
615
+ ]) {
616
+ write(root, path, rule(fields));
617
+ const entry = buildCatalog(root).catalog.rules[0];
618
+ assert.equal(entry.trigger, expected.trigger); assert.equal(entry.globs, expected.globs);
619
+ }
620
+ for (const fields of ['', 'trigger: invalid\n', 'trigger: glob\n', 'trigger: glob\nglobs: []\n', 'trigger: glob\nglob: "a/**"\nglobs: "b/**"\n']) {
621
+ write(root, path, rule(fields)); rejected(root, { force: true }, /Trigger|globs|Metadado|conflitantes/);
622
+ }
623
+ write(root, path, rule('trigger: model_decision\n').replace('description: Regra de teste.\n', ''));
624
+ rejected(root, { force: true }, /Descricao ausente/);
625
+ });
626
+
627
+ test('outputs ignorados nao entram nos assets; fixtures versionaveis continuam', t => {
628
+ const root = completeCatalogFixture(t);
629
+ const hidden = ['scripts/demo/playwright-report/index.html', 'scripts/demo/.Playwright-MCP/session.json', 'scripts/demo/TEST-RESULTS/report.json', 'scripts/demo/TmP/README.md', 'scripts/demo/run.log', 'scripts/demo/Sessions/state.json', 'scripts/demo/auth.json', 'scripts/demo/CREDENTIALS.JSON', 'scripts/demo/history.jsonl', 'scripts/demo/settings.LOCAL.yaml', 'scripts/demo/.env', 'skills/demo/.agent-tests/trace.json', 'templates/demo/temp/data.json'];
630
+ for (const path of hidden) write(root, `.pbai4devs/${path}`, 'output temporario\n');
631
+ const kept = ['scripts/demo/tests/fixtures/cache/input.json', 'scripts/demo/tests/fixtures/playwright-response.json', 'scripts/demo/tests/fixtures/auth.sample.json', 'scripts/demo/.env.example', 'templates/demo/compose.local.yml.template'];
632
+ for (const path of kept) write(root, `.pbai4devs/${path}`, 'fixture versionavel\n');
633
+ const catalog = buildCatalog(root).catalog;
634
+ const assets = new Set(Object.values(catalog).flatMap(entries => Array.isArray(entries) ? entries.flatMap(entry => entry.assets) : []));
635
+ for (const path of hidden) assert.ok(!assets.has(`.pbai4devs/${path}`), path);
636
+ for (const path of kept) assert.ok(assets.has(`.pbai4devs/${path}`), path);
637
+ });
638
+
639
+ test('catálogo completo deriva oito tipos, ações reais e assets sem estado local', t => {
640
+ const root = completeCatalogFixture(t);
641
+ write(root, '.pbai4devs/scripts/browser/SCRIPT.conf', 'executor=playwright-mcp\n');
642
+ write(root, '.pbai4devs/scripts/browser/SCRIPT.js', '(page) => page.title();\n');
643
+ write(root, '.pbai4devs/commands/demo/COMMAND.md', '---\ndescription: Comando com referências.\n---\n\n# Demo\n\nLeia [tool](../../tools/demo/TOOL.json), [script][script], [pasta](../../scripts/demo/) e [template](../../templates/demo/TEMPLATE.md).\n\n[script]: ../../scripts/demo/SCRIPT.mjs\n');
644
+ for (const path of ['.pbai4devs/tools/demo/.local/node_modules/payload/SKILL.md', '.pbai4devs/tools/demo/versions/1.0.0/package.json', '.pbai4devs/external-skills/.local/upstream/SKILL.md', '.pbai4devs/scripts/demo/.cache/large.json']) write(root, path, 'conteúdo local inválido deliberado');
645
+ const before = snapshot(root);
646
+ const first = buildCatalog(root);
647
+ assert.deepEqual(snapshot(root), before, 'Construir o catálogo não pode escrever arquivos.');
648
+ assert.deepEqual(buildCatalog(root), first);
649
+ for (const type of ['skills', 'agents', 'commands', 'rules', 'templates', 'tools', 'scripts', 'externalSkills']) assert.ok(first.catalog[type].length, type);
650
+ assert.deepEqual([...first.outputs.keys()].sort(), ['.pbai4devs/agents.json', '.pbai4devs/catalog.json', '.pbai4devs/rules.json', '.pbai4devs/skills.json']);
651
+ assert.ok(first.catalog.templates[0].assets.includes('.pbai4devs/templates/demo/config.yaml.template'));
652
+ assert.deepEqual(first.catalog.commands[0].references, ['.pbai4devs/scripts/demo', '.pbai4devs/scripts/demo/SCRIPT.mjs', '.pbai4devs/templates/demo/TEMPLATE.md', '.pbai4devs/tools/demo/TOOL.json']);
653
+ const tool = first.catalog.tools[0];
654
+ for (const action of ['install', 'update', 'check', 'info', 'run']) assert.deepEqual(tool.actions[action].argv, ['node', '.pbai4devs/tools/demo/SCRIPT.mjs', action]);
655
+ assert.equal(tool.actions.run.requiresArguments, true);
656
+ const script = first.catalog.scripts.find(entry => entry.id === 'demo');
657
+ assert.deepEqual(script.argv, ['node', '.pbai4devs/scripts/demo/SCRIPT.mjs']);
658
+ assert.deepEqual(script.support.modules, ['.pbai4devs/scripts/lib/shared.mjs']);
659
+ assert.deepEqual(script.support.tests, ['.pbai4devs/scripts/demo/tests/TEST.mjs']);
660
+ assert.deepEqual(script.support.smokes, ['.pbai4devs/scripts/demo/tests/SMOKE_DEMO.mjs']);
661
+ const browser = first.catalog.scripts.find(entry => entry.id === 'browser');
662
+ assert.equal(browser.executor, 'playwright-mcp');
663
+ assert.equal(browser.path, '.pbai4devs/scripts/browser/SCRIPT.js');
664
+ assert.deepEqual(browser.argv, []);
665
+ assert.equal(first.catalog.externalSkills[0].path, '.pbai4devs/external-skills/CATALOG.json');
666
+ assert.equal(first.catalog.externalSkills[0].upstreamPath, 'skills/demo/SKILL.md');
667
+ assert.doesNotMatch(first.outputs.get('.pbai4devs/catalog.json'), /node_modules|\.local|versions|\.cache|installed|conteúdo local|updatedAt/);
668
+ run(root);
669
+ for (const [path, text] of first.outputs) assert.equal(read(root, path), text);
670
+ assertCheckReadonly(root, {}, 0);
671
+ });
672
+
673
+ test('criar editar e remover recursos atualiza todos os tipos e os índices nativos', t => {
674
+ const root = completeCatalogFixture(t);
675
+ run(root);
676
+ for (const [kind, file] of Object.entries({ skills: 'SKILL.md', agents: 'AGENT.md', commands: 'COMMAND.md', rules: 'RULE.md', templates: 'TEMPLATE.md' })) write(root, `.pbai4devs/${kind}/demo/${file}`, `---\nname: demo\n${kind === 'rules' ? 'trigger: always_on\n' : ''}description: ${kind} atualizado.\n---\n\n# Demo\n`);
677
+ write(root, '.pbai4devs/tools/demo/TOOL.json', JSON.stringify({ ...toolConfig(), description: 'tools atualizado.' }));
678
+ write(root, '.pbai4devs/scripts/demo/README.md', '---\ndescription: scripts atualizado.\n---\n\n# Demo\n');
679
+ const external = externalConfig(); external.skills[0].use = 'externalSkills atualizado.';
680
+ write(root, '.pbai4devs/external-skills/CATALOG.json', JSON.stringify(external));
681
+ assertCheckReadonly(root);
682
+ run(root);
683
+ const updated = JSON.parse(read(root, '.pbai4devs/catalog.json'));
684
+ for (const kind of ['skills', 'agents', 'commands', 'rules', 'templates', 'tools', 'scripts', 'externalSkills']) assert.match(updated[kind][0].description, /atualizado/);
685
+ for (const kind of ['skills', 'agents', 'commands', 'rules', 'templates/demo', 'tools', 'scripts', 'external-skills']) rmSync(join(root, '.pbai4devs', kind), { recursive: true });
686
+ assertCheckReadonly(root);
687
+ run(root);
688
+ const removed = JSON.parse(read(root, '.pbai4devs/catalog.json'));
689
+ for (const value of Object.values(removed)) if (Array.isArray(value)) assert.deepEqual(value, []);
690
+ for (const kind of ['skills', 'agents', 'rules']) assert.deepEqual(JSON.parse(read(root, `.pbai4devs/${kind}.json`)), { entries: [] });
691
+ assertCheckReadonly(root, {}, 0);
692
+ });
693
+
694
+ test('JSON derivado corrompido é detectado e reparado com backup sem editar fontes', t => {
695
+ const root = completeCatalogFixture(t);
696
+ run(root);
697
+ for (const path of ['.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/agents.json', '.pbai4devs/rules.json']) write(root, path, '{corrompido');
698
+ assertCheckReadonly(root, { force: true });
699
+ const result = run(root, { force: true });
700
+ for (const path of ['.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/agents.json', '.pbai4devs/rules.json']) assert.equal(read(result.backupRoot, path), '{corrompido');
701
+ assert.equal(JSON.parse(read(root, '.pbai4devs/catalog.json')).schemaVersion, 1);
702
+ assertCheckReadonly(root, {}, 0);
703
+ });
704
+
705
+ test('catalogo e seletor aceitam o mesmo SCRIPT.conf e os tres formatos Node', t => {
706
+ const root = completeCatalogFixture(t);
707
+ write(root, '.pbai4devs/scripts/demo/SCRIPT.conf', '# Node explicito\n executor = node \n');
708
+ write(root, '.pbai4devs/scripts/browser/SCRIPT.conf', '# Conector\n executor = playwright-mcp \n');
709
+ write(root, '.pbai4devs/scripts/browser/SCRIPT.js', '(page) => page.title();\n');
710
+ write(root, '.pbai4devs/scripts/common/SCRIPT.cjs', 'module.exports = {};\n');
711
+ write(root, '.pbai4devs/scripts/plain/SCRIPT.js', 'console.log("Node");\n');
712
+ const scripts = buildCatalog(root).catalog.scripts;
713
+ assert.equal(scripts.find(entry => entry.id === 'demo').executor, 'node');
714
+ assert.equal(scripts.find(entry => entry.id === 'demo').configuration, '.pbai4devs/scripts/demo/SCRIPT.conf');
715
+ assert.equal(scripts.find(entry => entry.id === 'browser').executor, 'playwright-mcp');
716
+ assert.equal(scripts.find(entry => entry.id === 'common').path, '.pbai4devs/scripts/common/SCRIPT.cjs');
717
+ assert.equal(scripts.find(entry => entry.id === 'plain').path, '.pbai4devs/scripts/plain/SCRIPT.js');
718
+ for (const invalid of ['', '# vazio\n', 'executor=bash\n', 'executor=node\nexecutor=playwright-mcp\n']) {
719
+ write(root, '.pbai4devs/scripts/demo/SCRIPT.conf', invalid);
720
+ rejected(root, { force: true }, /SCRIPT.conf exige/);
721
+ }
722
+ rmSync(join(root, '.pbai4devs/scripts/demo/SCRIPT.conf'));
723
+ mkdirSync(join(root, '.pbai4devs/scripts/demo/SCRIPT.conf'));
724
+ rejected(root, { force: true }, /SCRIPT.conf deve ser um arquivo/);
725
+ });
726
+
727
+ test('fontes JSON, referências e entradas de scripts inválidas falham antes de mutações', t => {
728
+ const cases = [
729
+ ['.pbai4devs/tools/demo/TOOL.json', '{invalid', /TOOL|JSON/],
730
+ ['.pbai4devs/external-skills/CATALOG.json', '{invalid', /JSON/],
731
+ ['.pbai4devs/external-skills/CATALOG.json', '{"schemaVersion":1,"skills":[]}', /externo/],
732
+ ['.pbai4devs/commands/demo/COMMAND.md', '---\ndescription: Ref faltando.\n---\n\n# Demo\n\nLeia [ausente](../../scripts/missing/SCRIPT.mjs).\n', /ausente/],
733
+ ['.pbai4devs/scripts/browser/SCRIPT.conf', 'executor=python\n', /SCRIPT.conf exige/],
734
+ ['.pbai4devs/scripts/browser/SCRIPT.conf', 'executor=playwright-mcp\n', /ausente/],
735
+ ['.pbai4devs/templates/demo/TEMPLATE.md', '# Sem metadados\n', /Frontmatter/],
736
+ ];
737
+ for (const [path, body, pattern] of cases) {
738
+ const root = completeCatalogFixture(t);
739
+ write(root, path, body);
740
+ rejected(root, { force: true }, pattern);
741
+ }
742
+ const root = completeCatalogFixture(t);
743
+ rmSync(join(root, '.pbai4devs/tools/demo/SCRIPT.mjs'));
744
+ rejected(root, { force: true }, /SCRIPT|script|ausente/);
745
+ });
746
+
747
+ test('descobre principais aninhados sem transformar auxiliares em recursos', t => {
748
+ const root = completeCatalogFixture(t);
749
+ write(root, '.pbai4devs/skills/demo/nested/SKILL.md', '---\nname: nested\ndescription: Skill aninhada.\n---\n\n# Nested\n');
750
+ write(root, '.pbai4devs/skills/demo/REFERENCE.md', '# Guia auxiliar\n');
751
+ write(root, '.pbai4devs/scripts/demo/tests/fixtures/SKILL.md', '# Fixture, não skill compartilhada\n');
752
+ const { catalog } = buildCatalog(root);
753
+ assert.deepEqual(catalog.skills.map(entry => entry.id), ['demo', 'demo/nested']);
754
+ assert.ok(catalog.skills[0].assets.includes('.pbai4devs/skills/demo/REFERENCE.md'));
755
+ assert.ok(catalog.scripts[0].assets.includes('.pbai4devs/scripts/demo/tests/fixtures/SKILL.md'));
756
+ run(root);
757
+ assert.ok(existsSync(join(root, '.codex/skills/nested/SKILL.md')));
758
+ assert.equal(existsSync(join(root, '.codex/skills/fixtures/SKILL.md')), false);
759
+ assertCheckReadonly(root, {}, 0);
760
+ });
761
+
762
+ test('builder e gerenciador externo compartilham o mesmo contrato de validacao', t => {
763
+ const cases = [
764
+ [true, value => value],
765
+ [true, value => ({ ...value, allowlist: ['Fixture/Skills'] })],
766
+ [false, value => ({ ...value, allowlist: ['fixture/skills', 'Fixture/Skills'] })],
767
+ [false, value => ({ ...value, skills: [{ ...value.skills[0], name: 'Invalid Name' }] })],
768
+ [false, value => ({ ...value, allowlist: ['-fixture/skills'], skills: [{ ...value.skills[0], repository: '-fixture/skills' }] })],
769
+ [false, value => ({ ...value, skills: [{ ...value.skills[0], path: 'skills/demo:other/SKILL.md' }] })],
770
+ [false, value => ({ ...value, skills: [{ ...value.skills[0], licenseSource: 'https://example.com/LICENSE' }] })],
771
+ [false, value => ({ ...value, skills: [{ ...value.skills[0], licenseSource: 'https://github.com/fixture/skills/blob/main/LICENSE' }] })],
772
+ [true, value => ({ ...value, skills: [{ ...value.skills[0], licenseSource: `https://github.com/fixture/skills/blob/${'a'.repeat(40)}/LICENSE` }] })],
773
+ ];
774
+ for (const [valid, update] of cases) {
775
+ const value = update(externalConfig());
776
+ const root = fixture(t, true);
777
+ write(root, '.pbai4devs/external-skills/CATALOG.json', JSON.stringify(value));
778
+ const before = snapshot(root);
779
+ if (valid) {
780
+ assert.doesNotThrow(() => validateExternalCatalog(value));
781
+ const built = buildCatalog(root);
782
+ assert.equal(built.catalog.externalSkills[0].name, value.skills[0].name);
783
+ assert.equal(built.catalog.externalSkills[0].repository, value.skills[0].repository);
784
+ } else {
785
+ assert.throws(() => validateExternalCatalog(value));
786
+ assert.throws(() => buildCatalog(root));
787
+ rejected(root, { force: true });
788
+ }
789
+ assert.deepEqual(snapshot(root), before);
790
+ }
791
+ });
792
+
793
+ test('formatos por tipo preservam grupos e geram apenas o formato nativo escolhido', t => {
794
+ const root = fixture(t, true);
795
+ write(root, source, skill);
796
+ write(root, '.pbai4devs/commands/projeto/check/COMMAND.md', '---\ndescription: Checagem.\n---\n\n# Checagem\n');
797
+ write(root, '.pbai4devs/rules/projeto/style/RULE.md', '---\ntrigger: glob\nglobs: "**/*.js"\ndescription: Estilo.\n---\n\n# Estilo\n');
798
+ write(root, '.pbai4devs/agents/review/security/AGENT.md', '---\nname: security\ndescription: Perfil.\n---\n\n# Perfil\n');
799
+ write(root, '.pbai4devs/espelhos.conf', [
800
+ 'agente|.codex|agrupado', 'agente|.claude|agrupado', 'agente|.cursor|agrupado', 'agente|.github|agrupado', 'agente|.gemini|agrupado',
801
+ 'formato|.claude|skills|plano', 'formato|.claude|commands|arquivo',
802
+ 'formato|.cursor|rules|mdc', 'formato|.github|rules|instrucoes', 'formato|.github|commands|prompt', 'formato|.github|agents|agente',
803
+ 'formato|.gemini|commands|arquivo',
804
+ ].join('\n') + '\n');
805
+ run(root);
806
+ assert.ok(existsSync(join(root, '.codex/skills/projeto/demo/SKILL.md')));
807
+ assert.ok(existsSync(join(root, '.claude/skills/demo/SKILL.md')));
808
+ assert.ok(existsSync(join(root, '.claude/commands/projeto/check.md')));
809
+ assert.ok(existsSync(join(root, '.github/agents/review/security.agent.md')));
810
+ assert.ok(existsSync(join(root, '.github/prompts/projeto/check.prompt.md')));
811
+ assert.ok(existsSync(join(root, '.github/instructions/projeto/style.instructions.md')));
812
+ assert.ok(existsSync(join(root, '.gemini/commands/projeto/check.toml')));
813
+ assert.match(read(root, '.cursor/rules/projeto/style.mdc'), /globs: "\*\*\/\*\.js"/);
814
+ assert.equal(existsSync(join(root, '.github/commands/projeto/check/COMMAND.md')), false);
815
+ assert.equal(existsSync(join(root, '.cursor/rules/projeto/style/RULE.md')), false);
816
+ assertCheckReadonly(root, {}, 0);
817
+ });
818
+
819
+ test('manifestos Antigravity acompanham skills, regras e agentes agrupados', t => {
820
+ const root = fixture(t, true);
821
+ write(root, source, skill);
822
+ write(root, '.pbai4devs/rules/projeto/style/RULE.md', '---\ntrigger: always_on\ndescription: Estilo.\n---\n\n# Estilo\n');
823
+ write(root, '.pbai4devs/agents/review/security/AGENT.md', '---\nname: security\ndescription: Perfil.\n---\n\n# Perfil\n');
824
+ write(root, '.pbai4devs/espelhos.conf', 'agente|.agents|agrupado\ndescoberta|.agents|antigravity\n');
825
+ const before = snapshot(root);
826
+ assert.equal(run(root, { check: true }).exitCode, 1);
827
+ assert.deepEqual(snapshot(root), before);
828
+ run(root);
829
+ assert.deepEqual(JSON.parse(read(root, '.agents/skills.json')).entries, [{ path: '.agents/skills/projeto/demo' }]);
830
+ assert.deepEqual(JSON.parse(read(root, '.agents/rules.json')).entries, [{ path: '.agents/rules/projeto/style/RULE.md' }]);
831
+ assert.deepEqual(JSON.parse(read(root, '.agents/agents.json')).entries, [{ path: '.agents/agents/review/security/AGENT.md' }]);
832
+ write(root, '.pbai4devs/skills/projeto/new-skill/SKILL.md', skill.replace('name: demo', 'name: new-skill'));
833
+ run(root);
834
+ assert.equal(JSON.parse(read(root, '.agents/skills.json')).entries.length, 2);
835
+ rmSync(join(root, '.pbai4devs/skills/projeto/demo'), { recursive: true });
836
+ run(root);
837
+ assert.deepEqual(JSON.parse(read(root, '.agents/skills.json')).entries, [{ path: '.agents/skills/projeto/new-skill' }]);
838
+ assert.equal(existsSync(join(root, '.agents/skills/projeto/demo')), false);
839
+ assertCheckReadonly(root, {}, 0);
840
+ });
841
+
842
+ test('manifesto de descoberta customizado exige force e recebe backup', t => {
843
+ const root = fixture(t, true);
844
+ write(root, source, skill);
845
+ write(root, '.pbai4devs/espelhos.conf', 'agente|.agents|agrupado\ndescoberta|.agents|antigravity\n');
846
+ run(root);
847
+ const custom = '{"entries":[{"path":"custom/skill"}]}\n';
848
+ write(root, '.agents/skills.json', custom);
849
+ rejected(root, {}, /force/);
850
+ const result = run(root, { force: true });
851
+ assert.equal(readFileSync(join(result.backupRoot, '.agents/skills.json'), 'utf8'), custom);
852
+ assertCheckReadonly(root, {}, 0);
853
+ });
854
+
855
+ test('formatos desconhecidos ou fora do cliente falham antes de mutar', t => {
856
+ const root = fixture(t, true);
857
+ write(root, source, skill);
858
+ for (const row of [
859
+ 'formato|.missing|skills|agrupado', 'formato|.codex|unknown|agrupado',
860
+ 'formato|.codex|skills|mdc', 'formato|.codex|rules|prompt',
861
+ 'formato|.codex|skills|plano\nformato|.codex|skills|agrupado',
862
+ 'descoberta|.codex|antigravity', 'descoberta|.agents|antigravity',
863
+ ]) {
864
+ write(root, '.pbai4devs/espelhos.conf', 'agente|.codex|agrupado\n' + row + '\n');
865
+ rejected(root);
866
+ }
867
+ assert.equal(resourcePath('.pbai4devs/commands/team/check/COMMAND.md', 'commands', 'arquivo'), 'commands/team/check.md');
868
+ assert.throws(() => resourcePath(source, 'skills', 'arquivo'), /Skills/);
869
+ });