@pathbit/agentdevkit 0.0.10 → 0.0.12

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 (134) hide show
  1. package/.pbai4devs/COMPATIBILIDADE.md +6 -0
  2. package/.pbai4devs/INSTRUCTIONS.md +3 -0
  3. package/.pbai4devs/agents/README.md +19 -0
  4. package/.pbai4devs/agents/auditor-seguranca/AGENT.md +20 -0
  5. package/.pbai4devs/agents/auditor-seguranca/antigravity.yaml +8 -0
  6. package/.pbai4devs/agents/auditor-seguranca/claude.yaml +5 -0
  7. package/.pbai4devs/agents/auditor-seguranca/cursor.yaml +5 -0
  8. package/.pbai4devs/agents/auditor-seguranca/gemini.yaml +6 -0
  9. package/.pbai4devs/agents/auditor-seguranca/github.yaml +5 -0
  10. package/.pbai4devs/agents/auditor-seguranca/kimi-code.yaml +5 -0
  11. package/.pbai4devs/agents/auditor-seguranca/mimocode.yaml +8 -0
  12. package/.pbai4devs/agents/auditor-seguranca/opencode.yaml +11 -0
  13. package/.pbai4devs/agents/documentador/AGENT.md +19 -0
  14. package/.pbai4devs/agents/documentador/antigravity.yaml +8 -0
  15. package/.pbai4devs/agents/documentador/claude.yaml +5 -0
  16. package/.pbai4devs/agents/documentador/cursor.yaml +5 -0
  17. package/.pbai4devs/agents/documentador/gemini.yaml +6 -0
  18. package/.pbai4devs/agents/documentador/github.yaml +5 -0
  19. package/.pbai4devs/agents/documentador/kimi-code.yaml +5 -0
  20. package/.pbai4devs/agents/documentador/mimocode.yaml +8 -0
  21. package/.pbai4devs/agents/documentador/opencode.yaml +11 -0
  22. package/.pbai4devs/agents/implementador/AGENT.md +20 -0
  23. package/.pbai4devs/agents/implementador/antigravity.yaml +8 -0
  24. package/.pbai4devs/agents/implementador/claude.yaml +5 -0
  25. package/.pbai4devs/agents/implementador/cursor.yaml +5 -0
  26. package/.pbai4devs/agents/implementador/gemini.yaml +6 -0
  27. package/.pbai4devs/agents/implementador/github.yaml +5 -0
  28. package/.pbai4devs/agents/implementador/kimi-code.yaml +5 -0
  29. package/.pbai4devs/agents/implementador/mimocode.yaml +8 -0
  30. package/.pbai4devs/agents/implementador/opencode.yaml +11 -0
  31. package/.pbai4devs/agents/planejador/AGENT.md +19 -0
  32. package/.pbai4devs/agents/planejador/antigravity.yaml +8 -0
  33. package/.pbai4devs/agents/planejador/claude.yaml +5 -0
  34. package/.pbai4devs/agents/planejador/cursor.yaml +5 -0
  35. package/.pbai4devs/agents/planejador/gemini.yaml +6 -0
  36. package/.pbai4devs/agents/planejador/github.yaml +5 -0
  37. package/.pbai4devs/agents/planejador/kimi-code.yaml +5 -0
  38. package/.pbai4devs/agents/planejador/mimocode.yaml +8 -0
  39. package/.pbai4devs/agents/planejador/opencode.yaml +11 -0
  40. package/.pbai4devs/agents/validador/AGENT.md +19 -0
  41. package/.pbai4devs/agents/validador/antigravity.yaml +8 -0
  42. package/.pbai4devs/agents/validador/claude.yaml +5 -0
  43. package/.pbai4devs/agents/validador/cursor.yaml +5 -0
  44. package/.pbai4devs/agents/validador/gemini.yaml +6 -0
  45. package/.pbai4devs/agents/validador/github.yaml +5 -0
  46. package/.pbai4devs/agents/validador/kimi-code.yaml +5 -0
  47. package/.pbai4devs/agents/validador/mimocode.yaml +8 -0
  48. package/.pbai4devs/agents/validador/opencode.yaml +11 -0
  49. package/.pbai4devs/commands/README.md +13 -0
  50. package/.pbai4devs/commands/atualizar-documentacao/COMMAND.md +17 -0
  51. package/.pbai4devs/commands/auditar-seguranca/COMMAND.md +25 -0
  52. package/.pbai4devs/commands/implementar-entrega/COMMAND.md +20 -0
  53. package/.pbai4devs/commands/planejar-entrega/COMMAND.md +17 -0
  54. package/.pbai4devs/commands/preparar-seguranca/COMMAND.md +22 -0
  55. package/.pbai4devs/commands/validar-entrega/COMMAND.md +18 -0
  56. package/.pbai4devs/commands/verificar-dependencias/COMMAND.md +18 -0
  57. package/.pbai4devs/commands/verificar-segredos/COMMAND.md +19 -0
  58. package/.pbai4devs/external-skills/CATALOG.json +39 -1
  59. package/.pbai4devs/external-skills/README.md +13 -0
  60. package/.pbai4devs/rules/README.md +4 -0
  61. package/.pbai4devs/rules/documentacao/RULE.md +2 -0
  62. package/.pbai4devs/rules/execucao-local/RULE.md +20 -0
  63. package/.pbai4devs/rules/execucao-local/cursor.yaml +4 -0
  64. package/.pbai4devs/rules/seguranca/RULE.md +19 -0
  65. package/.pbai4devs/rules/seguranca/cursor.yaml +4 -0
  66. package/.pbai4devs/scripts/README.md +1 -0
  67. package/.pbai4devs/scripts/auditar-seguranca/README.md +57 -0
  68. package/.pbai4devs/scripts/auditar-seguranca/SCRIPT.mjs +411 -0
  69. package/.pbai4devs/scripts/auditar-seguranca/database-zip.mjs +143 -0
  70. package/.pbai4devs/scripts/auditar-seguranca/rules/javascript.yml +50 -0
  71. package/.pbai4devs/scripts/auditar-seguranca/tests/SMOKE.mjs +74 -0
  72. package/.pbai4devs/scripts/auditar-seguranca/tests/TEST.mjs +247 -0
  73. package/.pbai4devs/scripts/auditar-seguranca/tests/zip-fixture.mjs +45 -0
  74. package/.pbai4devs/scripts/lib/README.md +4 -0
  75. package/.pbai4devs/scripts/lib/gitignore.mjs +41 -4
  76. package/.pbai4devs/scripts/lib/managed-content.mjs +86 -0
  77. package/.pbai4devs/scripts/lib/managed-toml.mjs +192 -0
  78. package/.pbai4devs/scripts/lib/native-release.mjs +193 -0
  79. package/.pbai4devs/scripts/preparar-ferramentas/README.md +8 -2
  80. package/.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs +46 -16
  81. package/.pbai4devs/scripts/preparar-ferramentas/tests/TEST.mjs +2 -0
  82. package/.pbai4devs/scripts/preparar-ferramentas/tests/native-release.test.mjs +103 -0
  83. package/.pbai4devs/scripts/sincronizar-agentes/README.md +9 -7
  84. package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +133 -38
  85. package/.pbai4devs/scripts/sincronizar-agentes/tests/COEXISTENCE.mjs +25 -15
  86. package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +7 -2
  87. package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +3 -3
  88. package/.pbai4devs/scripts/sincronizar-agentes/tests/MANAGED_CONTENT.mjs +318 -0
  89. package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +47 -26
  90. package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +43 -7
  91. package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +3 -2
  92. package/.pbai4devs/skills/README.md +10 -2
  93. package/.pbai4devs/skills/projeto/preparar-ferramentas/SKILL.md +5 -1
  94. package/.pbai4devs/skills/seguranca/REFERENCE.md +104 -0
  95. package/.pbai4devs/skills/seguranca/corrigir-vulnerabilidade/SKILL.md +27 -0
  96. package/.pbai4devs/skills/seguranca/modelar-ameacas/SKILL.md +25 -0
  97. package/.pbai4devs/skills/seguranca/revisar-autorizacao/SKILL.md +29 -0
  98. package/.pbai4devs/skills/seguranca/revisar-seguranca/SKILL.md +27 -0
  99. package/.pbai4devs/templates/README.md +2 -0
  100. package/.pbai4devs/templates/revisao-seguranca/TEMPLATE.md +33 -0
  101. package/.pbai4devs/tools/README.md +11 -3
  102. package/.pbai4devs/tools/gitleaks/README.md +22 -0
  103. package/.pbai4devs/tools/gitleaks/SCRIPT.mjs +5 -0
  104. package/.pbai4devs/tools/gitleaks/TOOL.json +67 -0
  105. package/.pbai4devs/tools/opengrep/README.md +22 -0
  106. package/.pbai4devs/tools/opengrep/SCRIPT.mjs +5 -0
  107. package/.pbai4devs/tools/opengrep/TOOL.json +71 -0
  108. package/.pbai4devs/tools/osv-scanner/README.md +23 -0
  109. package/.pbai4devs/tools/osv-scanner/SCRIPT.mjs +5 -0
  110. package/.pbai4devs/tools/osv-scanner/TOOL.json +70 -0
  111. package/CHANGELOG.md +17 -0
  112. package/README.en.md +3 -2
  113. package/README.es.md +3 -2
  114. package/README.md +8 -3
  115. package/docs/README.md +6 -3
  116. package/docs/guides/README.md +1 -0
  117. package/docs/guides/cli.md +2 -1
  118. package/docs/guides/installation.md +25 -8
  119. package/docs/guides/resources.md +48 -1
  120. package/docs/guides/security-references.md +97 -0
  121. package/docs/guides/security.md +32 -0
  122. package/docs/guides/troubleshooting.md +8 -5
  123. package/docs/maintainers/maintenance.md +12 -0
  124. package/docs/maintainers/packages.md +2 -2
  125. package/docs/product/design.md +10 -3
  126. package/docs/product/prd.md +5 -1
  127. package/docs/product/roadmap.md +4 -0
  128. package/docs/specs/README.md +4 -0
  129. package/docs/specs/additive-integration/README.md +9 -0
  130. package/docs/specs/additive-integration/SPEC.md +53 -0
  131. package/docs/specs/security-ecosystem/README.md +9 -0
  132. package/docs/specs/security-ecosystem/SPEC.md +53 -0
  133. package/package.json +1 -1
  134. package/src/install.mjs +139 -63
@@ -119,15 +119,15 @@ test('desindexação não descarta edição humana staged com workingtree restau
119
119
  assert.equal(readFileSync(join(root, 'generated.md'), 'utf8'), 'gerado\n');
120
120
  });
121
121
 
122
- test('raízes dedicadas são locais, enquanto configurações de GitHub e VS Code são fontes', t => {
122
+ test('configurações humanas dos clientes continuam versionáveis', t => {
123
123
  const root = fixture(t); git(root, ['init', '-q']);
124
124
  applyLocalIgnore(planLocalIgnore(root));
125
125
  put(root, '.codex/config.toml', 'local'); put(root, '.claude/settings.json', 'local');
126
126
  put(root, '.github/settings.json', 'fonte'); put(root, '.vscode/settings.json', 'fonte');
127
127
  git(root, ['add', '--all']);
128
128
  const tracked = git(root, ['ls-files', '-z']).split('\0');
129
- assert.equal(tracked.includes('.codex/config.toml'), false);
130
- assert.equal(tracked.includes('.claude/settings.json'), false);
129
+ assert.equal(tracked.includes('.codex/config.toml'), true);
130
+ assert.equal(tracked.includes('.claude/settings.json'), true);
131
131
  assert.ok(tracked.includes('.github/settings.json')); assert.ok(tracked.includes('.vscode/settings.json'));
132
132
  });
133
133
 
@@ -0,0 +1,318 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import { join, dirname } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ import { createHash } from 'node:crypto';
7
+ import { syncBuiltinESMExports } from 'node:module';
8
+ import { projectRoot } from '../../lib/runtime.mjs';
9
+ import { extractManagedBlock, managedBlockDigest } from '../../lib/managed-content.mjs';
10
+ import { extractTomlManagedBlock } from '../../lib/managed-toml.mjs';
11
+ import { isReference, synchronize } from '../SCRIPT.mjs';
12
+
13
+ const hash = bytes => createHash('sha256').update(bytes).digest('hex');
14
+ const read = (root, path) => fs.readFileSync(join(root, path), 'utf8');
15
+ const put = (root, path, text) => { fs.mkdirSync(dirname(join(root, path)), { recursive: true }); fs.writeFileSync(join(root, path), text); };
16
+ const run = (root, options = {}) => synchronize({ root, localIgnores: false, output() {}, ...options });
17
+ function fixture(t) {
18
+ const root = fs.realpathSync(fs.mkdtempSync(join(tmpdir(), 'agentdevkit-mixed-')));
19
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
20
+ put(root, '.pbai4devs/espelhos.conf', 'agente|.claude|plano\nagente|.cursor|plano\n');
21
+ put(root, '.pbai4devs/skills/old/demo/SKILL.md', '---\nname: demo\ndescription: Demo.\n---\n\n# Demo\n');
22
+ for (const name of ['agents.ignore', 'shared.ignore', 'local.ignore', 'clients.json']) put(root, `.pbai4devs/templates/gitignore/${name}`, fs.readFileSync(join(projectRoot, '.pbai4devs/templates/gitignore', name)));
23
+ for (const name of ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']) put(root, `.pbai4devs/templates/entrypoints/${name}`, `---\ndescription: Entry.\n---\n\n# ${name}\n\nLeia AGENTS.md.\n`);
24
+ return root;
25
+ }
26
+
27
+ test('sync complementa Markdown humano, preserva CRLF/metadados e acompanha só o bloco', t => {
28
+ const root = fixture(t), path = '.claude/skills/demo/SKILL.md';
29
+ const human = '\ufeff---\r\nname: meu-demo\r\ncustom: manter\r\n---\r\n\r\n# Minhas instrucoes\r\n\r\nTexto humano sem newline';
30
+ put(root, path, human);
31
+ if (process.platform !== 'win32') fs.chmodSync(join(root, path), 0o640);
32
+ const mode = fs.statSync(join(root, path)).mode & 0o777;
33
+ const result = run(root);
34
+ const actual = read(root, path), block = extractManagedBlock(actual);
35
+ assert.ok(actual.startsWith(human));
36
+ assert.match(block, /old\/demo\/SKILL\.md/);
37
+ assert.ok(block.includes('\r\n'));
38
+ assert.equal(fs.statSync(join(root, path)).mode & 0o777, mode);
39
+ assert.equal(result.managedBlocks[path], managedBlockDigest(actual));
40
+ assert.ok(!result.ownedOutputs.includes(path));
41
+ const state = JSON.parse(read(root, '.pbai4devs/estado/derived.json'));
42
+ assert.equal(state.blocks[path], managedBlockDigest(actual));
43
+ assert.equal(state.files[path], undefined);
44
+ assert.equal(run(root, { check: true }).exitCode, 0);
45
+ run(root, { force: true });
46
+ assert.equal(read(root, path), actual);
47
+ put(root, path, actual + '\r\nOutro texto humano.\r\n');
48
+ assert.equal(run(root, { check: true }).exitCode, 0);
49
+ fs.mkdirSync(join(root, '.pbai4devs/skills/new'), { recursive: true });
50
+ fs.renameSync(join(root, '.pbai4devs/skills/old/demo'), join(root, '.pbai4devs/skills/new/demo'));
51
+ run(root);
52
+ const updated = read(root, path);
53
+ assert.ok(updated.startsWith(human));
54
+ assert.ok(updated.endsWith('\r\nOutro texto humano.\r\n'));
55
+ assert.match(extractManagedBlock(updated), /new\/demo\/SKILL\.md/);
56
+ assert.equal(updated.split('<!-- BEGIN AGENTDEVKIT -->').length, 2);
57
+ });
58
+
59
+ test('sync promove apontamento editado por humano e nunca força um bloco alterado', t => {
60
+ const root = fixture(t), path = '.claude/skills/demo/SKILL.md';
61
+ run(root);
62
+ const generated = read(root, path);
63
+ const edited = generated.replace('description: Demo.', 'description: Escolha humana.') + '\nMinhas regras extras.\n';
64
+ put(root, path, edited);
65
+ run(root, { force: true });
66
+ const mixed = read(root, path);
67
+ assert.ok(mixed.startsWith(edited));
68
+ assert.ok(!run(root, { check: true }).ownedOutputs.includes(path));
69
+ const changed = mixed.replace(extractManagedBlock(mixed), extractManagedBlock(mixed).replace('Leia e siga [origem]', 'Leia [origem humana]'));
70
+ put(root, path, changed);
71
+ for (const options of [{}, { force: true }, { check: true }]) {
72
+ assert.throws(() => run(root, options), { code: 'E_MANAGED_BLOCK' });
73
+ assert.equal(read(root, path), changed);
74
+ }
75
+ });
76
+
77
+ test('remover recurso ou cliente retira somente bloco próprio de Markdown misto', t => {
78
+ for (const change of ['resource', 'client']) {
79
+ const root = fixture(t), path = '.claude/skills/demo/SKILL.md', human = '# Humano\n\nMinha skill.\n';
80
+ put(root, path, human); run(root);
81
+ const mixed = read(root, path), outside = mixed.replace(extractManagedBlock(mixed), '');
82
+ if (change === 'resource') fs.rmSync(join(root, '.pbai4devs/skills/old/demo'), { recursive: true });
83
+ else put(root, '.pbai4devs/espelhos.conf', 'agente|.cursor|plano\n');
84
+ run(root);
85
+ assert.equal(read(root, path), outside);
86
+ assert.ok(read(root, path).startsWith(human));
87
+ assert.equal(JSON.parse(read(root, '.pbai4devs/estado/derived.json')).blocks?.[path], undefined);
88
+ assert.equal(run(root, { check: true }).exitCode, 0);
89
+ }
90
+ });
91
+
92
+ test('entradas raiz com caixa existente recebem só a referência e mantêm alterações humanas', t => {
93
+ const root = fixture(t), paths = ['agents.md', 'claude.md', 'Gemini.md'];
94
+ for (const path of paths) put(root, path, `# Minhas instrucoes de ${path}\n\nNao apague.\n`);
95
+ const before = Object.fromEntries(paths.map(path => [path, read(root, path)]));
96
+ const result = run(root);
97
+ for (const path of paths) {
98
+ assert.ok(read(root, path).startsWith(before[path]));
99
+ assert.ok(result.managedBlocks[path]);
100
+ assert.ok(!result.ownedOutputs.includes(path));
101
+ }
102
+ assert.match(extractManagedBlock(read(root, 'claude.md')), /@agents\.md/);
103
+ assert.match(extractManagedBlock(read(root, 'Gemini.md')), /@\.\/agents\.md/);
104
+ for (const path of ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']) assert.ok(!fs.readdirSync(root).includes(path));
105
+ put(root, 'agents.md', read(root, 'agents.md') + '\nTexto depois.\n');
106
+ assert.equal(run(root, { check: true }).exitCode, 0);
107
+ const beforeRepeat = hash(read(root, 'agents.md')); run(root, { force: true });
108
+ assert.equal(hash(read(root, 'agents.md')), beforeRepeat);
109
+ });
110
+
111
+ test('comando TOML existente recebe referência dentro do prompt e mantém metadados', t => {
112
+ const root = fixture(t), path = '.gemini/commands/check.toml';
113
+ put(root, '.pbai4devs/espelhos.conf', 'agente|.gemini|plano\n');
114
+ put(root, '.pbai4devs/commands/check/COMMAND.md', '---\ndescription: Check.\n---\n\n# Check\n');
115
+ const before = 'description = "Escolha humana"\r\nprompt = \'Meu comando {{args}}\'\r\n# Comentario humano\r\n';
116
+ put(root, path, before);
117
+ const result = run(root);
118
+ const after = read(root, path);
119
+ assert.ok(after.startsWith('description = "Escolha humana"\r\n'));
120
+ assert.ok(after.endsWith('\r\n# Comentario humano\r\n'));
121
+ assert.match(after, /Meu comando \{\{args\}\}/);
122
+ assert.match(extractTomlManagedBlock(after), /@\{\.pbai4devs\/commands\/check\/COMMAND\.md\}/);
123
+ assert.ok(result.managedBlocks[path]);
124
+ assert.ok(!result.ownedOutputs.includes(path));
125
+ assert.equal(run(root, { check: true }).exitCode, 0);
126
+ run(root, { force: true }); assert.equal(read(root, path), after);
127
+ });
128
+
129
+ test('retirada standalone registra tombstone para bloco do estado instalado', t => {
130
+ const root = fixture(t), path = '.claude/skills/demo/SKILL.md';
131
+ put(root, path, '# Human\n'); run(root);
132
+ const block = managedBlockDigest(read(root, path));
133
+ put(root, '.pbai4devs/estado/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: {}, blocks: { [path]: block } }));
134
+ fs.rmSync(join(root, '.pbai4devs/skills/old/demo'), { recursive: true });
135
+ run(root);
136
+ const derived = JSON.parse(read(root, '.pbai4devs/estado/derived.json'));
137
+ assert.ok(derived.removedBlocks.includes(path));
138
+ assert.equal(extractManagedBlock(read(root, path)), null);
139
+ assert.equal(run(root, { check: true }).exitCode, 0);
140
+ });
141
+
142
+ test('stage pode gerar referências sem gravar estado derivado', t => {
143
+ const root = fixture(t);
144
+ run(root, { generateEntrypoints: false, recordDerivedState: false });
145
+ assert.ok(fs.existsSync(join(root, '.claude/skills/demo/SKILL.md')));
146
+ assert.ok(!fs.existsSync(join(root, '.pbai4devs/estado/derived.json')));
147
+ });
148
+
149
+ test('regra Cursor mantém globs e modo humanos ao receber referência', t => {
150
+ const root = fixture(t), path = '.cursor/rules/code.mdc';
151
+ put(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + 'formato|.cursor|rules|mdc\n');
152
+ put(root, '.pbai4devs/rules/code/RULE.md', '---\ntrigger: always_on\ndescription: Canonical.\n---\n\n# Rule\n');
153
+ const human = '---\ndescription: Minha regra\nglobs: src/custom/**\nalwaysApply: false\n---\n\n# Regra própria\n\nConserve isto.\n';
154
+ put(root, path, human);
155
+ const result = run(root);
156
+ assert.ok(read(root, path).startsWith(human));
157
+ assert.equal(result.managedBlocks[path], managedBlockDigest(read(root, path)));
158
+ assert.ok(!result.ownedOutputs.includes(path));
159
+ assert.equal(run(root, { check: true }).exitCode, 0);
160
+ });
161
+
162
+ test('edição humana durante planejamento não é sobrescrita pelo merge calculado antes', t => {
163
+ const root = fixture(t), path = '.claude/skills/demo/SKILL.md', before = '# Humano antes\n', concurrent = '# Humano durante planejamento\n';
164
+ put(root, path, before);
165
+ const originalRead = fs.readFileSync;
166
+ let injected = false;
167
+ t.mock.method(fs, 'readFileSync', (file, ...options) => {
168
+ const result = originalRead(file, ...options);
169
+ if (!injected && String(file) === join(root, '.pbai4devs/templates/entrypoints/AGENTS.md')) {
170
+ injected = true;
171
+ fs.writeFileSync(join(root, path), concurrent);
172
+ }
173
+ return result;
174
+ });
175
+ syncBuiltinESMExports();
176
+ t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
177
+ assert.throws(() => run(root), /Arquivo mudou apos planejar/);
178
+ assert.equal(injected, true);
179
+ assert.equal(read(root, path), concurrent);
180
+ assert.ok(!fs.existsSync(join(root, '.pbai4devs/catalog.json')));
181
+ });
182
+
183
+ test('apontamento gerado que recebe edição concorrente antes do snapshot é preservado', t => {
184
+ const root = fixture(t), path = '.claude/skills/demo/SKILL.md', concurrent = '# Novo texto humano\n';
185
+ run(root);
186
+ const previousCatalog = read(root, '.pbai4devs/catalog.json');
187
+ put(root, '.pbai4devs/skills/old/demo/SKILL.md', '---\nname: demo\ndescription: Updated.\n---\n\n# Demo\n');
188
+ const originalRead = fs.readFileSync;
189
+ let injected = false;
190
+ t.mock.method(fs, 'readFileSync', (file, ...options) => {
191
+ const result = originalRead(file, ...options);
192
+ if (!injected && String(file) === join(root, '.pbai4devs/templates/entrypoints/AGENTS.md')) {
193
+ injected = true;
194
+ fs.writeFileSync(join(root, path), concurrent);
195
+ }
196
+ return result;
197
+ });
198
+ syncBuiltinESMExports();
199
+ t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
200
+ assert.throws(() => run(root), /Arquivo mudou apos planejar/);
201
+ assert.equal(injected, true);
202
+ assert.equal(read(root, path), concurrent);
203
+ assert.equal(read(root, '.pbai4devs/catalog.json'), previousCatalog);
204
+ });
205
+
206
+ test('destino nativo customizado mantém compatibilidade e complementa o arquivo humano', t => {
207
+ const root = fixture(t), path = '.claude/custom.md', human = '# Meu documento\n';
208
+ put(root, path, human);
209
+ put(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + `nativo|${path}|.pbai4devs/skills/old/demo/SKILL.md\n`);
210
+ const result = run(root, { force: true });
211
+ assert.ok(read(root, path).startsWith(human));
212
+ assert.match(read(root, path), /<!-- BEGIN AGENTDEVKIT -->/);
213
+ assert.ok(result.managedBlocks[path]);
214
+ assert.ok(!result.ownedOutputs.includes(path));
215
+ assert.equal(run(root, { check: true }).exitCode, 0);
216
+ });
217
+
218
+ test('alteração apenas no YAML não pode ser apagada como wrapper integralmente gerado', t => {
219
+ for (const stale of [false, true]) {
220
+ const root = fixture(t), path = '.claude/skills/demo/SKILL.md';
221
+ run(root);
222
+ const human = read(root, path).replace('description: Demo.', 'description: Metadado humano.');
223
+ put(root, path, human);
224
+ if (stale) fs.rmSync(join(root, '.pbai4devs/skills/old/demo'), { recursive: true });
225
+ const result = run(root, { force: true });
226
+ assert.ok(read(root, path).startsWith(human));
227
+ assert.ok(!result.ownedOutputs.includes(path));
228
+ if (stale) assert.equal(read(root, path), human);
229
+ else assert.ok(result.managedBlocks[path]);
230
+ assert.equal(run(root, { check: true }).exitCode, 0);
231
+ run(root, { force: true });
232
+ assert.ok(read(root, path).startsWith(human));
233
+ }
234
+ });
235
+
236
+ test('destino Markdown com extensão em maiúsculas também preserva conteúdo humano', t => {
237
+ const root = fixture(t), path = '.claude/custom.MD', human = '# Humano\n';
238
+ put(root, path, human);
239
+ put(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + `nativo|${path}|.pbai4devs/skills/old/demo/SKILL.md\n`);
240
+ const result = run(root);
241
+ assert.ok(result.managedBlocks[path]);
242
+ assert.ok(read(root, path).startsWith(human));
243
+ assert.equal(run(root, { check: true }).exitCode, 0);
244
+ });
245
+
246
+ test('marcador legado sem hash não autoriza substituir ou remover texto humano', t => {
247
+ for (const stale of [false, true]) {
248
+ const root = fixture(t), path = '.claude/skills/demo/SKILL.md';
249
+ const human = '---\ndescription: Metadado humano\n---\n\n# Escolha humana\n\n<!-- gerado: sincronizar-agentes -->\nLeia e siga [origem](../../origem.md).\n';
250
+ assert.equal(isReference(human), true);
251
+ put(root, path, human);
252
+ if (stale) fs.rmSync(join(root, '.pbai4devs/skills/old/demo'), { recursive: true });
253
+ const result = run(root, { force: true });
254
+ assert.ok(read(root, path).startsWith(human));
255
+ assert.ok(!result.ownedOutputs.includes(path));
256
+ if (stale) assert.equal(read(root, path), human);
257
+ else assert.ok(result.managedBlocks[path]);
258
+ assert.equal(run(root, { check: true }).exitCode, 0);
259
+ run(root, { force: true });
260
+ assert.ok(read(root, path).startsWith(human));
261
+ }
262
+ });
263
+
264
+ test('arquivo humano criado durante planejamento não é substituído por saída prevista', t => {
265
+ for (const path of ['.claude/skills/demo/SKILL.md', '.agents/skills.json', '.claude/.gitignore']) {
266
+ const root = fixture(t), human = path.endsWith('.json') ? '{"entries":[],"human":true}\n' : '# Criado durante planejamento\n';
267
+ if (path.startsWith('.agents/')) put(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + 'agente|.agents|plano\ndescoberta|.agents|antigravity\n');
268
+ const originalRead = fs.readFileSync;
269
+ let injected = false;
270
+ t.mock.method(fs, 'readFileSync', (file, ...options) => {
271
+ const result = originalRead(file, ...options);
272
+ if (!injected && String(file) === join(root, '.pbai4devs/templates/entrypoints/AGENTS.md')) {
273
+ injected = true;
274
+ put(root, path, human);
275
+ }
276
+ return result;
277
+ });
278
+ syncBuiltinESMExports();
279
+ try {
280
+ assert.throws(() => run(root, { force: true }), /Arquivo mudou apos planejar/);
281
+ assert.equal(injected, true, path);
282
+ assert.equal(read(root, path), human, path);
283
+ assert.ok(!fs.existsSync(join(root, '.pbai4devs/catalog.json')), path);
284
+ } finally {
285
+ t.mock.restoreAll(); syncBuiltinESMExports();
286
+ }
287
+ }
288
+ });
289
+
290
+ test('arquivo misto removido é recriado sem conservar propriedade do bloco ausente', t => {
291
+ for (const path of ['.claude/skills/demo/SKILL.md', 'AGENTS.md']) {
292
+ const root = fixture(t);
293
+ put(root, path, '# Humano\n'); run(root);
294
+ const hash = managedBlockDigest(read(root, path));
295
+ put(root, '.pbai4devs/estado/agentdevkit.json', JSON.stringify({ schemaVersion: 1, clients: ['claude'], files: {}, blocks: { [path]: hash } }));
296
+ fs.rmSync(join(root, path));
297
+ run(root);
298
+ const derived = JSON.parse(read(root, '.pbai4devs/estado/derived.json'));
299
+ assert.ok(derived.removedBlocks.includes(path));
300
+ assert.ok(derived.files?.[path] || derived.entries?.[path]);
301
+ assert.equal(extractManagedBlock(read(root, path)), null);
302
+ assert.equal(run(root, { check: true }).exitCode, 0);
303
+ }
304
+ });
305
+
306
+ test('renomear caixa de entrada mista transfere só a propriedade do bloco', t => {
307
+ const root = fixture(t), before = '# Humano\n';
308
+ put(root, 'AGENTS.md', before); run(root);
309
+ const hash = managedBlockDigest(read(root, 'AGENTS.md'));
310
+ put(root, '.pbai4devs/estado/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: {}, blocks: { 'AGENTS.md': hash } }));
311
+ fs.renameSync(join(root, 'AGENTS.md'), join(root, 'agents.md'));
312
+ const result = run(root);
313
+ const derived = JSON.parse(read(root, '.pbai4devs/estado/derived.json'));
314
+ assert.ok(read(root, 'agents.md').startsWith(before));
315
+ assert.ok(derived.removedBlocks.includes('AGENTS.md'));
316
+ assert.equal(derived.blocks['agents.md'], result.managedBlocks['agents.md']);
317
+ assert.equal(run(root, { check: true }).exitCode, 0);
318
+ });
@@ -12,6 +12,7 @@ import { mergeGitignore, ignoreBegin, ignoreEnd, validateIgnoreClients } from '.
12
12
  import './LOCAL_IGNORE.mjs';
13
13
  import './HUMAN_OWNERSHIP.mjs';
14
14
  import './COEXISTENCE.mjs';
15
+ import './MANAGED_CONTENT.mjs';
15
16
  import { validateCatalog as validateExternalCatalog } from '../../gerenciar-skills-externas/SCRIPT.mjs';
16
17
 
17
18
  const project = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
@@ -22,7 +23,11 @@ const source = '.pbai4devs/skills/projeto/demo/SKILL.md';
22
23
  const read = (root, path) => readFileSync(join(root, path), 'utf8');
23
24
  const write = (root, path, content) => { mkdirSync(dirname(join(root, path)), { recursive: true }); writeFileSync(join(root, path), content); };
24
25
  const pointer = (origin = 'origem.md', title = 'Referencia') => `---\ndescription: Referencia.\n---\n\n# ${title}\n\n${marker}\nLeia e siga [origem](${origin}).\n`;
25
- const recordOwned = (root, paths) => write(root, '.pbai4devs/estado/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: Object.fromEntries(paths.map(path => [path, createHash('sha256').update(readFileSync(join(root, path))).digest('hex')])), blocks: {} }));
26
+ const recordOwned = (root, paths) => {
27
+ const statePath = '.pbai4devs/estado/agentdevkit.json';
28
+ const previous = existsSync(join(root, statePath)) ? JSON.parse(read(root, statePath)) : {};
29
+ write(root, statePath, JSON.stringify({ schemaVersion: 1, files: { ...previous.files, ...Object.fromEntries(paths.map(path => [path, createHash('sha256').update(readFileSync(join(root, path))).digest('hex')])) }, blocks: previous.blocks ?? {} }));
30
+ };
26
31
  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'] });
27
32
  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.' }] });
28
33
  const run = (root, options = {}) => synchronize({ root, localIgnores: false, generateEntrypoints: false, output() {}, ...options });
@@ -53,6 +58,7 @@ function fixture(t, minimal = false) {
53
58
  'nativo|.opencode/agents/revisor/AGENT.md|.pbai4devs/agents/revisor/AGENT.md|.pbai4devs/agents/revisor/opencode.yaml',
54
59
  ].join('\n') + '\n');
55
60
  if (minimal) return root;
61
+ const ownedLegacy = ['.opencode/agents/revisor/AGENT.md'];
56
62
  write(root, source, skill);
57
63
  write(root, '.pbai4devs/skills/grupo com espaço/outro/SKILL.md', '---\nname: outro\ndescription: Outro.\n---\n\n# Outro\n');
58
64
  write(root, '.pbai4devs/agents/revisor/AGENT.md', '---\nname: revisor\ndescription: Perfil.\n---\n\n# Revisor\n');
@@ -71,14 +77,19 @@ function fixture(t, minimal = false) {
71
77
  write(root, 'README.md', 'README raiz preservado\n');
72
78
  for (const kind of ['templates', 'scripts', 'tools', 'external-skills', 'estado', 'tests', 'docs', 'adapters', 'extra']) {
73
79
  write(root, `.pbai4devs/${kind}/demo/REFERENCE.md`, '---\ndescription: Exclusivo.\n---\n\n# Exclusivo\n');
74
- if (kind !== 'extra') write(root, `.claude/${kind}/demo/REFERENCE.md`, pointer(`../../../.pbai4devs/${kind}/demo/REFERENCE.md`));
80
+ if (kind !== 'extra') {
81
+ const target = `.claude/${kind}/demo/REFERENCE.md`;
82
+ write(root, target, pointer(`../../../.pbai4devs/${kind}/demo/REFERENCE.md`)); ownedLegacy.push(target);
83
+ }
75
84
  }
76
85
  write(root, '.pbai4devs/tools/demo/TOOL.json', JSON.stringify(toolConfig()));
77
86
  write(root, '.pbai4devs/tools/demo/SCRIPT.mjs', toolWrapper('demo'));
78
87
  for (const file of ['README.md', 'INSTRUCTIONS.md']) {
79
88
  write(root, `.pbai4devs/${file}`, '---\ndescription: Guia.\n---\n\n# Guia\n');
80
89
  write(root, `.claude/${file}`, pointer(`../.pbai4devs/${file}`, 'Guia'));
90
+ ownedLegacy.push(`.claude/${file}`);
81
91
  }
92
+ recordOwned(root, ownedLegacy);
82
93
  return root;
83
94
  }
84
95
  function rejected(root, options = {}, pattern) {
@@ -120,7 +131,7 @@ test('sincroniza somente principais, preserva metadados e usa referências UTF-8
120
131
  assert.deepEqual(snapshot(root), before, 'A segunda sincronização deve ser idempotente.');
121
132
  });
122
133
 
123
- test('metadados genericos seguem a origem e overrides nativos ficam no YAML central', t => {
134
+ test('metadados gerados seguem a origem e metadados editados pelo humano permanecem', t => {
124
135
  const root = fixture(t); run(root);
125
136
  write(root, '.pbai4devs/commands/check/COMMAND.md', '---\nname: check-novo\ndescription: Comando atualizado.\n---\n\n# Comando\n');
126
137
  write(root, '.pbai4devs/rules/example/RULE.md', '---\ntrigger: glob\nglobs: "**/*.ts"\ndescription: Regra atualizada.\n---\n\n# Regra\n');
@@ -128,8 +139,8 @@ test('metadados genericos seguem a origem e overrides nativos ficam no YAML cent
128
139
  write(root, '.codex/commands/check.md', read(root, '.codex/commands/check.md').replace('description: Comando.', 'description: Obsoleto.\nlocal-only: true'));
129
140
  assertCheckReadonly(root);
130
141
  run(root);
131
- assert.match(read(root, '.codex/commands/check.md'), /name: check-novo\ndescription: Comando atualizado\./);
132
- assert.doesNotMatch(read(root, '.codex/commands/check.md'), /Obsoleto|local-only/);
142
+ assert.match(read(root, '.codex/commands/check.md'), /description: Obsoleto\.\nlocal-only: true/);
143
+ assert.match(read(root, '.codex/commands/check.md'), /<!-- BEGIN AGENTDEVKIT -->/);
133
144
  assert.match(read(root, '.codex/rules/example.md'), /trigger: glob\nglobs: "\*\*\/\*\.ts"/);
134
145
  assert.match(read(root, '.codex/agents/revisor.md'), /Perfil atualizado\./);
135
146
  assert.match(read(root, '.opencode/agents/revisor/AGENT.md'), /edit: deny/);
@@ -174,6 +185,7 @@ test('Gemini usa TOML com inclusao relativa ao workspace e argumentos, sem Markd
174
185
  const root = fixture(t);
175
186
  write(root, '.pbai4devs/commands/check/COMMAND.md', '---\ndescription: "Verifica \\"aspas\\" e regras."\n---\n\n# Check\n');
176
187
  write(root, '.gemini/commands/check.md', pointer('../../.pbai4devs/commands/check/COMMAND.md'));
188
+ recordOwned(root, ['.gemini/commands/check.md']);
177
189
  run(root);
178
190
  const target = '.gemini/commands/check.toml';
179
191
  const text = read(root, target); const lines = text.trimEnd().split('\n');
@@ -182,12 +194,18 @@ test('Gemini usa TOML com inclusao relativa ao workspace e argumentos, sem Markd
182
194
  assert.equal(JSON.parse(lines[2].slice('prompt = '.length)), '@{.pbai4devs/commands/check/COMMAND.md}\n\n{{args}}');
183
195
  assert.ok(isReference(text)); assert.equal(existsSync(join(root, '.gemini/commands/check.md')), false);
184
196
  write(root, target, text + '# instrucao propria\n');
185
- assertCheckReadonly(root); rejected(root, {}, /--force/);
197
+ assertCheckReadonly(root);
186
198
  const repaired = run(root, { force: true });
187
- assert.equal(read(repaired.backupRoot, target), text + '# instrucao propria\n');
188
- assert.equal(read(root, target), text);
199
+ assert.ok(repaired.managedBlocks[target]);
200
+ assert.match(read(root, target), /# instrucao propria/);
201
+ assert.match(read(root, target), /BEGIN AGENTDEVKIT/);
189
202
  rmSync(join(root, '.pbai4devs/commands/check'), { recursive: true });
190
- run(root); assert.equal(existsSync(join(root, target)), false);
203
+ run(root);
204
+ const stripped = read(root, target).split('\n');
205
+ assert.equal(stripped[0], '# gerado: sincronizar-agentes');
206
+ assert.equal(stripped[1], lines[1]);
207
+ assert.equal(JSON.parse(stripped[2].slice('prompt = '.length)).trimEnd(), '@{.pbai4devs/commands/check/COMMAND.md}\n\n{{args}}');
208
+ assert.equal(stripped[3], '# instrucao propria');
191
209
  assert.match(read(root, '.github/workflows/ci.yml'), /workflow-preservado/);
192
210
  });
193
211
 
@@ -349,16 +367,16 @@ test('skills externas baixadas não entram nos índices; cópias humanas permane
349
367
  const sourceBefore = read(root, '.pbai4devs/external-skills/.local/upstream-0/SKILL.md');
350
368
  write(root, '.codex/external-skills/.local/upstream-0/SKILL.md', '# Cópia indevida\n');
351
369
  write(root, '.claude/external-skills/README.md', pointer('../../.pbai4devs/external-skills/README.md'));
352
- assertCheckReadonly(root);
370
+ assertCheckReadonly(root, {}, 0);
353
371
  const result = run(root, { force: true });
354
- assert.equal(existsSync(join(root, '.claude/external-skills')), false);
372
+ assert.equal(read(root, '.claude/external-skills/README.md'), pointer('../../.pbai4devs/external-skills/README.md'));
355
373
  assert.ok(result.preserved.some(item => item.path === '.codex/external-skills'));
356
374
  assert.equal(read(root, '.pbai4devs/external-skills/.local/upstream-0/SKILL.md'), sourceBefore);
357
375
  assert.equal(read(root, '.codex/external-skills/.local/upstream-0/SKILL.md'), '# Cópia indevida\n');
358
376
  assertCheckReadonly(root, {}, 0);
359
377
  });
360
378
 
361
- test('--force repara conteúdo registrado e preserva recursos humanos sem propriedade', t => {
379
+ test('--force complementa conteúdo editado e preserva recursos humanos', t => {
362
380
  const root = fixture(t);
363
381
  run(root);
364
382
  recordOwned(root, ['.claude/skills/projeto/demo/SKILL.md', '.kimi-code/agents/revisor.md']);
@@ -371,14 +389,16 @@ test('--force repara conteúdo registrado e preserva recursos humanos sem propri
371
389
  write(root, '.github/prompts/edited.prompt.md', pointer() + '\nRevisar [manual](bar).\n');
372
390
  assertCheckReadonly(root);
373
391
  assertCheckReadonly(root, { force: true });
374
- rejected(root, {}, /--force/);
375
392
  const result = run(root, { force: true });
376
- assert.ok(result.backupRoot.startsWith(join(root, '.agent-tests')));
377
- for (const path of ['.claude/skills/projeto/demo/SKILL.md', '.kimi-code/agents/revisor.md', '.github/prompts/edited.prompt.md']) assert.ok(existsSync(join(result.backupRoot, path)), `Backup ausente: ${path}`);
393
+ for (const path of ['.claude/skills/projeto/demo/SKILL.md', '.kimi-code/agents/revisor.md']) {
394
+ assert.ok(read(root, path).startsWith('corrompido\n'));
395
+ assert.ok(result.managedBlocks[path]);
396
+ assert.ok(!result.ownedOutputs.includes(path));
397
+ }
378
398
  assert.deepEqual(readFileSync(join(root, '.claude/tools/program.exe')), Buffer.from([1, 2, 3]));
379
399
  assert.equal(read(root, '.claude/scripts/local.sh'), 'script humano\n');
380
400
  assert.equal(read(root, '.claude/skills/local.bin'), 'custom\n');
381
- assert.equal(existsSync(join(root, '.github/prompts/edited.prompt.md')), false);
401
+ assert.match(read(root, '.github/prompts/edited.prompt.md'), /Revisar \[manual\]/);
382
402
  assert.match(read(root, '.codex/skills/outro/SKILL.md'), /name: outro/);
383
403
  assert.match(read(root, '.github/workflows/ci.yml'), /workflow-preservado/);
384
404
  assert.match(read(root, '.codex/config.toml'), /config-native-preservada/);
@@ -403,7 +423,7 @@ test('atualiza fontes, adiciona clientes, remove recursos e migra agrupado para
403
423
  assertCheckReadonly(root, {}, 0);
404
424
  });
405
425
 
406
- test('remove linhas legadas e origens nativas ausentes sem replicar documentos', t => {
426
+ test('remove linhas legadas e preserva arquivos sem prova de propriedade', t => {
407
427
  const root = fixture(t);
408
428
  run(root);
409
429
  write(root, 'docs/produto/prd/demo/PRD.md', '# Produto real\n');
@@ -414,8 +434,8 @@ test('remove linhas legadas e origens nativas ausentes sem replicar documentos',
414
434
  assertCheckReadonly(root);
415
435
  run(root);
416
436
  assert.doesNotMatch(read(root, '.pbai4devs/espelhos.conf'), /documento\||missing/);
417
- assert.equal(existsSync(join(root, '.claude/manual/DOC.md')), false);
418
- assert.equal(existsSync(join(root, '.github/prompts/missing.prompt.md')), false);
437
+ assert.equal(existsSync(join(root, '.claude/manual/DOC.md')), true);
438
+ assert.equal(existsSync(join(root, '.github/prompts/missing.prompt.md')), true);
419
439
  assert.ok(existsSync(join(root, 'docs/produto/prd/demo/PRD.md')));
420
440
  assert.ok(existsSync(join(root, 'manual/guia com espaço/DOC.md')));
421
441
  assertCheckReadonly(root, {}, 0);
@@ -441,8 +461,8 @@ test('colisões de destino, ancestral, metadados e slugs não são resolvidas po
441
461
  let extra;
442
462
  if (variant === 'slug') write(root, '.pbai4devs/skills/duplicado/demo/SKILL.md', skill);
443
463
  if (variant === 'source') extra = 'nativo|.codex/skills/demo/SKILL.md|.pbai4devs/commands/check/COMMAND.md';
444
- if (variant === 'ancestor') extra = [`nativo|.github/custom.md|${source}`, `nativo|.github/custom.md/child.md|${source}`].join('\n');
445
- if (variant === 'target-case') extra = [`nativo|.github/custom.md|${source}`, `nativo|.github/CUSTOM.md|${source}`].join('\n');
464
+ if (variant === 'ancestor') extra = [`nativo|.github/prompts/custom.md|${source}`, `nativo|.github/prompts/custom.md/child.md|${source}`].join('\n');
465
+ if (variant === 'target-case') extra = [`nativo|.github/prompts/custom.md|${source}`, `nativo|.github/prompts/CUSTOM.md|${source}`].join('\n');
446
466
  if (variant === 'metadata') {
447
467
  write(root, '.pbai4devs/agents/revisor/other.yaml', '---\nname: other\n---\n');
448
468
  extra = 'nativo|.kimi-code/agents/revisor.md|.pbai4devs/agents/revisor/AGENT.md|.pbai4devs/agents/revisor/other.yaml';
@@ -498,8 +518,7 @@ test('registros não regulares são rejeitados sem efeitos', t => {
498
518
  test('backup recusa raiz simbólica antes de remover conteúdo próprio', t => {
499
519
  const root = fixture(t);
500
520
  run(root);
501
- recordOwned(root, ['.claude/skills/projeto/demo/SKILL.md']);
502
- write(root, '.claude/skills/projeto/demo/SKILL.md', 'registrado alterado\n');
521
+ write(root, '.pbai4devs/templates/gitignore/shared.ignore', read(root, '.pbai4devs/templates/gitignore/shared.ignore') + '*.extra-local\n');
503
522
  write(root, '.claude/scripts/own.sh', 'preservar\n');
504
523
  const outside = join(root, 'outside');
505
524
  mkdirSync(outside);
@@ -550,7 +569,8 @@ test('gera manifesto e entradas sem estado e atualiza apenas entradas integralme
550
569
  const first = run(root, { generateEntrypoints: true });
551
570
  assert.ok(first.ownedOutputs.includes('AGENTS.md'));
552
571
  assert.equal(first.ownedOutputs.includes('CLAUDE.md'), false);
553
- assert.equal(read(root, 'CLAUDE.md'), '# Instruções humanas\n');
572
+ assert.ok(read(root, 'CLAUDE.md').startsWith('# Instruções humanas\n'));
573
+ assert.match(read(root, 'CLAUDE.md'), /<!-- BEGIN AGENTDEVKIT -->/);
554
574
  assert.ok(existsSync(join(root, '.agents/skills/projeto/demo/SKILL.md')));
555
575
  assertCheckReadonly(root, { generateEntrypoints: true }, 0);
556
576
  write(root, '.pbai4devs/templates/entrypoints/AGENTS.md', '---\ndescription: Entrada.\n---\n\n# Atualizada\n');
@@ -562,7 +582,7 @@ test('gera manifesto e entradas sem estado e atualiza apenas entradas integralme
562
582
  assert.match(read(root, 'AGENTS.md'), /Texto humano/);
563
583
  });
564
584
 
565
- test('entradas da CLI usam ownership integral sem criar registro derivado redundante', t => {
585
+ test('entradas da CLI mantêm ownership integral e recursos têm proveniência derivada', t => {
566
586
  const root = fixture(t, true);
567
587
  const template = '---\ndescription: Entrada.\n---\n\n# Agentes\n';
568
588
  const entry = template + '\nResponda em português.\n';
@@ -573,7 +593,8 @@ test('entradas da CLI usam ownership integral sem criar registro derivado redund
573
593
  const result = run(root, { generateEntrypoints: true });
574
594
  assert.ok(result.ownedOutputs.includes('AGENTS.md'));
575
595
  assert.equal(read(root, 'AGENTS.md'), entry);
576
- assert.equal(existsSync(join(root, '.pbai4devs/estado/derived.json')), false);
596
+ assert.equal(JSON.parse(read(root, '.pbai4devs/estado/derived.json')).entries['AGENTS.md'], undefined);
597
+ assert.ok(JSON.parse(read(root, '.pbai4devs/estado/derived.json')).files['.codex/skills/demo/SKILL.md']);
577
598
  assertCheckReadonly(root, { generateEntrypoints: true }, 0);
578
599
  });
579
600