@pathbit/agentdevkit 0.0.11 → 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.
- package/.pbai4devs/rules/documentacao/RULE.md +2 -0
- package/.pbai4devs/scripts/lib/README.md +4 -0
- package/.pbai4devs/scripts/lib/gitignore.mjs +41 -4
- package/.pbai4devs/scripts/lib/managed-content.mjs +86 -0
- package/.pbai4devs/scripts/lib/managed-toml.mjs +192 -0
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +9 -7
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +133 -38
- package/.pbai4devs/scripts/sincronizar-agentes/tests/COEXISTENCE.mjs +25 -15
- package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +7 -2
- package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +3 -3
- package/.pbai4devs/scripts/sincronizar-agentes/tests/MANAGED_CONTENT.mjs +318 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +47 -26
- package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +43 -7
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +3 -2
- package/CHANGELOG.md +8 -0
- package/README.en.md +2 -2
- package/README.es.md +2 -2
- package/README.md +7 -3
- package/docs/guides/cli.md +2 -1
- package/docs/guides/installation.md +23 -6
- package/docs/guides/troubleshooting.md +8 -5
- package/docs/maintainers/maintenance.md +5 -0
- package/docs/maintainers/packages.md +2 -2
- package/docs/product/design.md +10 -3
- package/docs/product/prd.md +2 -1
- package/docs/product/roadmap.md +2 -0
- package/docs/specs/README.md +3 -0
- package/docs/specs/additive-integration/README.md +9 -0
- package/docs/specs/additive-integration/SPEC.md +53 -0
- package/package.json +1 -1
- package/src/install.mjs +139 -63
|
@@ -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) =>
|
|
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')
|
|
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
|
|
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'), /
|
|
132
|
-
assert.
|
|
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);
|
|
197
|
+
assertCheckReadonly(root);
|
|
186
198
|
const repaired = run(root, { force: true });
|
|
187
|
-
assert.
|
|
188
|
-
assert.
|
|
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);
|
|
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(
|
|
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
|
|
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
|
-
|
|
377
|
-
|
|
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.
|
|
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
|
|
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')),
|
|
418
|
-
assert.equal(existsSync(join(root, '.github/prompts/missing.prompt.md')),
|
|
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
|
-
|
|
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.
|
|
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
|
|
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(
|
|
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
|
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
5
6
|
import { isMain, projectRoot, requireNode } from '../lib/runtime.mjs';
|
|
6
7
|
import { buildCatalog } from '../lib/catalog.mjs';
|
|
7
8
|
import { isGeneratedOutput } from '../lib/gitignore.mjs';
|
|
9
|
+
import { canonicalEntrypoint, extractManagedBlock, managedBlockDigest } from '../lib/managed-content.mjs';
|
|
10
|
+
import { extractTomlManagedBlock, tomlManagedBlockDigest } from '../lib/managed-toml.mjs';
|
|
8
11
|
|
|
9
12
|
const fail = message => { throw new Error(message); };
|
|
10
13
|
const normalize = value => value.split(sep).join('/');
|
|
@@ -29,28 +32,38 @@ function checkedPath(root, path) {
|
|
|
29
32
|
|
|
30
33
|
function ownership(root) {
|
|
31
34
|
const file = checkedPath(root, '.pbai4devs/estado/agentdevkit.json');
|
|
32
|
-
|
|
35
|
+
const derivedFile = checkedPath(root, '.pbai4devs/estado/derived.json');
|
|
36
|
+
if (!checkedStat(root, file) && !checkedStat(root, derivedFile)) return null;
|
|
33
37
|
const readState = location => {
|
|
34
38
|
try { return JSON.parse(readFileSync(location, 'utf8')); }
|
|
35
39
|
catch { fail(`Estado invalido: ${normalize(relative(root, location))}`); }
|
|
36
40
|
};
|
|
37
|
-
const state = readState(file);
|
|
41
|
+
const state = checkedStat(root, file) ? readState(file) : { schemaVersion: 1, files: {}, blocks: {} };
|
|
38
42
|
const validMap = value => value && typeof value === 'object' && !Array.isArray(value)
|
|
39
43
|
&& Object.values(value).every(hash => typeof hash === 'string' && /^[a-f0-9]{64}$/.test(hash));
|
|
40
44
|
if (state?.schemaVersion !== 1 || !validMap(state.files) || !validMap(state.blocks)) fail('Estado de propriedade invalido.');
|
|
41
45
|
const files = { ...state.files };
|
|
42
|
-
const
|
|
46
|
+
const blocks = { ...state.blocks };
|
|
43
47
|
if (checkedStat(root, derivedFile)) {
|
|
44
48
|
const derived = readState(derivedFile);
|
|
45
|
-
if (derived?.schemaVersion !== 1 || !validMap(derived.entries) || Object.keys(derived.entries).some(path => !
|
|
49
|
+
if (derived?.schemaVersion !== 1 || !validMap(derived.entries) || Object.keys(derived.entries).some(path => !canonicalEntrypoint(path))
|
|
50
|
+
|| !validMap(derived.files ?? {}) || !validMap(derived.blocks ?? {})
|
|
51
|
+
|| !Array.isArray(derived.removedBlocks ?? []) || (derived.removedBlocks ?? []).some(path => typeof path !== 'string')
|
|
46
52
|
|| !validMap(derived.outputs ?? {}) || Object.keys(derived.outputs ?? {}).some(path => !/^\.[a-z0-9][a-z0-9-]*\/(?:skills|rules|agents)\.json$/.test(path))) fail('Estado de entradas derivadas invalido.');
|
|
47
|
-
Object.assign(files, derived.entries, derived.outputs);
|
|
53
|
+
Object.assign(files, derived.entries, derived.outputs, derived.files);
|
|
54
|
+
for (const [path, hash] of Object.entries({ ...derived.entries, ...derived.files })) {
|
|
55
|
+
const location = checkedPath(root, path);
|
|
56
|
+
if (checkedStat(root, location)?.isFile() && createHash('sha256').update(readFileSync(location)).digest('hex') === hash) delete blocks[path];
|
|
57
|
+
}
|
|
58
|
+
for (const path of derived.removedBlocks ?? []) { checkedPath(root, path); delete blocks[path]; }
|
|
59
|
+
Object.assign(blocks, derived.blocks);
|
|
48
60
|
}
|
|
49
|
-
for (const path of
|
|
61
|
+
for (const path of Object.keys(blocks)) delete files[path];
|
|
62
|
+
for (const path of [...Object.keys(files), ...Object.keys(blocks)]) {
|
|
50
63
|
const location = checkedPath(root, path);
|
|
51
64
|
if (!checkedStat(root, location)?.isFile()) fail(`Arquivo gerenciado ausente: ${path}`);
|
|
52
65
|
}
|
|
53
|
-
return { files, blocks
|
|
66
|
+
return { files, blocks };
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
export function registryRoots(root) {
|
|
@@ -151,6 +164,28 @@ export function localLinkTargets(text) {
|
|
|
151
164
|
}).filter(Boolean);
|
|
152
165
|
}
|
|
153
166
|
|
|
167
|
+
export function checkManagedBlocks(root, blocks) {
|
|
168
|
+
for (const [path, expected] of Object.entries(blocks)) {
|
|
169
|
+
const toml = extname(path).toLowerCase() === '.toml';
|
|
170
|
+
if (!toml && !['.md', '.mdc'].includes(extname(path).toLowerCase())) continue;
|
|
171
|
+
const file = checkedPath(root, path), bytes = readFileSync(file);
|
|
172
|
+
if ((toml ? tomlManagedBlockDigest(bytes) : managedBlockDigest(bytes)) !== expected) fail(`Bloco AgentDevKit alterado ou ausente: ${path}`);
|
|
173
|
+
const block = toml ? extractTomlManagedBlock(bytes) : extractManagedBlock(bytes);
|
|
174
|
+
const imports = [...block.matchAll(/^@(?:\{([^}\r\n]+)\}|([^\r\n]+))$/gm)]
|
|
175
|
+
.filter(match => !match[2]?.startsWith('['))
|
|
176
|
+
.map(match => ({ target: match[1] || match[2], base: match[1] ? root : dirname(file) }));
|
|
177
|
+
for (const { target, base } of [...localLinkTargets(block).map(target => ({ target, base: dirname(file) })), ...imports]) {
|
|
178
|
+
const destination = resolve(base, target), local = normalize(relative(root, destination));
|
|
179
|
+
if (!local || isAbsolute(local) || local === '..' || local.startsWith('../')) fail(`Referencia fora do projeto: ${path}`);
|
|
180
|
+
if (canonicalEntrypoint(local)) {
|
|
181
|
+
// The user's root entry may be an existing link; do not follow it.
|
|
182
|
+
const item = lstatSync(destination);
|
|
183
|
+
if (!item.isFile() && !item.isSymbolicLink()) fail(`Referencia indisponivel: ${path}`);
|
|
184
|
+
} else if (!checkedStat(root, checkedPath(root, local))?.isFile()) fail(`Referencia indisponivel: ${path}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
154
189
|
export function checkMarkdown(root, files, { virtualPaths = new Set() } = {}) {
|
|
155
190
|
for (const file of files) {
|
|
156
191
|
const label = normalize(relative(root, file));
|
|
@@ -237,6 +272,7 @@ export function verifyResources({ root = projectRoot, sourceOnly = false, synchr
|
|
|
237
272
|
const files = collectMarkdown(root, registry, { installed });
|
|
238
273
|
const virtualPaths = new Set(sourceOnly ? [...built.outputs.keys(), '.pbai4devs/espelhos.conf'] : []);
|
|
239
274
|
checkMarkdown(root, files, { virtualPaths });
|
|
275
|
+
if (installed) checkManagedBlocks(root, installed.blocks);
|
|
240
276
|
checkResourceReferences(root, files);
|
|
241
277
|
checkAgentExtras(root, registry.agents, { installed });
|
|
242
278
|
return { files, messages: [...lint(root, files), 'recursos=passed'] };
|
|
@@ -162,10 +162,11 @@ test('instalação confere entradas derivadas sem aplicar lint a blocos humanos'
|
|
|
162
162
|
const contents = document('# Entrada gerada\n');
|
|
163
163
|
file('GEMINI.md', contents);
|
|
164
164
|
file('.pbai4devs/estado/derived.json', JSON.stringify({ schemaVersion: 1, entries: { 'GEMINI.md': createHash('sha256').update(contents).digest('hex') } }));
|
|
165
|
-
|
|
165
|
+
const block = '<!-- BEGIN AGENTDEVKIT -->\nLeia as fontes.\n<!-- END AGENTDEVKIT -->';
|
|
166
|
+
file('AGENTS.md', `# Instruções humanas sem frontmatter\n\n${block}\n`);
|
|
166
167
|
const statePath = join(root, '.pbai4devs/estado/agentdevkit.json');
|
|
167
168
|
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
168
|
-
state.blocks['AGENTS.md'] = createHash('sha256').update(
|
|
169
|
+
state.blocks['AGENTS.md'] = createHash('sha256').update(block).digest('hex');
|
|
169
170
|
state.files['.pbai4devs/skills/exemplo/SKILL.md'] = createHash('sha256').update('fonte anterior').digest('hex');
|
|
170
171
|
writeFileSync(statePath, JSON.stringify(state));
|
|
171
172
|
const result = verify(root);
|