@pathbit/agentdevkit 0.0.16 → 0.0.18
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/INSTRUCTIONS.md +1 -1
- package/.pbai4devs/README.md +1 -1
- package/.pbai4devs/commands/sincronizar/COMMAND.md +6 -4
- package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +3 -2
- package/.pbai4devs/scripts/inspecionar-projeto/tests/TEST.mjs +18 -4
- package/.pbai4devs/scripts/lib/README.md +3 -0
- package/.pbai4devs/scripts/lib/gitignore.mjs +46 -9
- package/.pbai4devs/scripts/lib/state-path.mjs +20 -0
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +8 -1
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +75 -34
- package/.pbai4devs/scripts/sincronizar-agentes/tests/COEXISTENCE.mjs +5 -5
- package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +1 -1
- package/.pbai4devs/scripts/sincronizar-agentes/tests/MANAGED_CONTENT.mjs +9 -9
- package/.pbai4devs/scripts/sincronizar-agentes/tests/PRESERVATION.mjs +306 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +41 -8
- package/.pbai4devs/scripts/testar-maturidade/SCRIPT.mjs +6 -4
- package/.pbai4devs/scripts/testar-maturidade/tests/TEST.mjs +4 -3
- package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +5 -3
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +15 -7
- package/.pbai4devs/skills/produto/criar-design-doc/SKILL.md +1 -1
- package/.pbai4devs/skills/produto/criar-prd/SKILL.md +1 -1
- package/.pbai4devs/skills/produto/criar-ux-ui/SKILL.md +1 -1
- package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +1 -1
- package/.pbai4devs/skills/projeto/proximos-passos/SKILL.md +1 -1
- package/.pbai4devs/skills/projeto/sincronizar-agentes/SKILL.md +1 -1
- package/.pbai4devs/skills/specs/criar-spec/SKILL.md +1 -1
- package/.pbai4devs/skills/specs/implementar-spec/SKILL.md +1 -1
- package/.pbai4devs/templates/entrypoints/AGENTS.md +1 -1
- package/.pbai4devs/templates/gitignore/local.ignore +2 -0
- package/.pbai4devs/templates/gitignore/shared.ignore +2 -0
- package/CHANGELOG.md +11 -0
- package/docs/guides/cli.md +46 -15
- package/docs/guides/installation.md +20 -8
- package/docs/guides/troubleshooting.md +9 -1
- package/docs/guides/workflow.md +1 -1
- package/docs/specs/README.md +2 -0
- package/docs/specs/safe-synchronization/SPEC.md +21 -0
- package/docs/specs/state-directory/SPEC.md +22 -0
- package/package.json +1 -1
- package/src/cli.mjs +2 -2
- package/src/config.mjs +2 -1
- package/src/i18n.mjs +25 -0
- package/src/install.mjs +373 -27
- /package/.pbai4devs/{estado → state}/README.md +0 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { synchronize } from '../SCRIPT.mjs';
|
|
9
|
+
import { indexContentMatches } from '../../lib/gitignore.mjs';
|
|
10
|
+
|
|
11
|
+
const repository = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
12
|
+
const original = '.pbai4devs/skills/projeto/criar-skill/SKILL.md';
|
|
13
|
+
const pointer = '.claude/skills/criar-skill/SKILL.md';
|
|
14
|
+
const file = (root, relative) => join(root, relative);
|
|
15
|
+
const put = (root, relative, content) => { mkdirSync(dirname(file(root, relative)), { recursive: true }); writeFileSync(file(root, relative), content); };
|
|
16
|
+
function git(root, ...args) {
|
|
17
|
+
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
|
|
18
|
+
assert.equal(result.status, 0, `${args.join(' ')}: ${result.stderr}`);
|
|
19
|
+
return result.stdout;
|
|
20
|
+
}
|
|
21
|
+
function fixture(t, withGit = true) {
|
|
22
|
+
mkdirSync(join(repository, '.agent-tests'), { recursive: true });
|
|
23
|
+
const root = mkdtempSync(join(withGit ? join(repository, '.agent-tests') : tmpdir(), 'sync-preservation-'));
|
|
24
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
25
|
+
if (withGit) git(root, 'init', '--quiet');
|
|
26
|
+
populate(root);
|
|
27
|
+
return root;
|
|
28
|
+
}
|
|
29
|
+
function populate(root) {
|
|
30
|
+
put(root, '.gitignore', '# Human ignore\nnode_modules/\n');
|
|
31
|
+
put(root, '.pbai4devs/templates/gitignore/agents.ignore', '# Cache\n/*\n!/.gitignore\n{{ALLOWLIST}}\n/cache/\n*.log\n');
|
|
32
|
+
put(root, '.pbai4devs/templates/gitignore/clients.json', '{}\n');
|
|
33
|
+
put(root, '.pbai4devs/templates/gitignore/shared.ignore', '# Shared\n/tools/*/.local/\n');
|
|
34
|
+
put(root, '.pbai4devs/espelhos.conf', 'agente|.claude|plano\n');
|
|
35
|
+
put(root, original, '---\nname: criar-skill\ndescription: Criar recurso.\n---\n\n# Criar skill\n');
|
|
36
|
+
assert.equal(run(root).exitCode, 0);
|
|
37
|
+
assert.ok(existsSync(file(root, pointer)));
|
|
38
|
+
}
|
|
39
|
+
function run(root, options = {}) {
|
|
40
|
+
return synchronize({ root, localIgnores: false, generateEntrypoints: false, output() {}, ...options });
|
|
41
|
+
}
|
|
42
|
+
function withoutGit(t, callback) {
|
|
43
|
+
const emptyPath = mkdtempSync(join(tmpdir(), 'sync-empty-path-'));
|
|
44
|
+
t.after(() => rmSync(emptyPath, { recursive: true, force: true }));
|
|
45
|
+
const keys = Object.keys(process.env).filter(key => key.toLowerCase() === 'path');
|
|
46
|
+
const previous = new Map(keys.map(key => [key, process.env[key]]));
|
|
47
|
+
if (!keys.length) keys.push('PATH');
|
|
48
|
+
try {
|
|
49
|
+
for (const key of keys) process.env[key] = emptyPath;
|
|
50
|
+
return callback();
|
|
51
|
+
} finally {
|
|
52
|
+
for (const key of keys) {
|
|
53
|
+
if (previous.has(key)) process.env[key] = previous.get(key);
|
|
54
|
+
else delete process.env[key];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const [withGit, force] of [[true, false], [true, true], [false, false], [false, true]]) {
|
|
60
|
+
test(`remoção obsoleta preserva pasta vazia humana (git=${withGit}, force=${force})`, t => {
|
|
61
|
+
const root = fixture(t, withGit), human = '.claude/skills/criar-skill/Equipe-Vazia';
|
|
62
|
+
mkdirSync(file(root, human));
|
|
63
|
+
const state = readFileSync(file(root, '.pbai4devs/state/derived.json'));
|
|
64
|
+
const index = withGit ? git(root, 'ls-files', '--stage', '-z') : null;
|
|
65
|
+
const ignore = readFileSync(file(root, '.gitignore'));
|
|
66
|
+
rmSync(dirname(file(root, original)), { recursive: true });
|
|
67
|
+
const check = run(root, { check: true, force });
|
|
68
|
+
assert.notEqual(check.exitCode, 0);
|
|
69
|
+
assert.ok(existsSync(file(root, pointer)));
|
|
70
|
+
assert.ok(existsSync(file(root, human)));
|
|
71
|
+
assert.deepEqual(readFileSync(file(root, '.pbai4devs/state/derived.json')), state);
|
|
72
|
+
run(root, { force });
|
|
73
|
+
assert.equal(existsSync(file(root, pointer)), false, 'apontamento obsoleto comprovado é removido');
|
|
74
|
+
assert.ok(existsSync(file(root, human)), 'pasta vazia própria permanece');
|
|
75
|
+
assert.deepEqual(readFileSync(file(root, '.gitignore')), ignore);
|
|
76
|
+
if (withGit) assert.equal(git(root, 'ls-files', '--stage', '-z'), index);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const force of [false, true]) {
|
|
81
|
+
test(`índice com conteúdo humano divergente bloqueia remoção (force=${force})`, t => {
|
|
82
|
+
const root = fixture(t), generated = readFileSync(file(root, pointer));
|
|
83
|
+
put(root, pointer, Buffer.concat([generated, Buffer.from('\nConteúdo humano staged.\n')]));
|
|
84
|
+
git(root, 'add', '-f', '--', pointer);
|
|
85
|
+
const index = git(root, 'ls-files', '--stage', '-z');
|
|
86
|
+
put(root, pointer, generated);
|
|
87
|
+
rmSync(dirname(file(root, original)), { recursive: true });
|
|
88
|
+
const state = readFileSync(file(root, '.pbai4devs/state/derived.json'));
|
|
89
|
+
const check = run(root, { check: true, force });
|
|
90
|
+
assert.notEqual(check.exitCode, 0);
|
|
91
|
+
assert.deepEqual(readFileSync(file(root, pointer)), generated);
|
|
92
|
+
assert.deepEqual(readFileSync(file(root, '.pbai4devs/state/derived.json')), state);
|
|
93
|
+
assert.throws(() => run(root, { force }), /conflitos|indice|índice/i);
|
|
94
|
+
assert.deepEqual(readFileSync(file(root, pointer)), generated);
|
|
95
|
+
assert.equal(git(root, 'ls-files', '--stage', '-z'), index);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const force of [false, true]) {
|
|
100
|
+
test(`índice divergente impede atualização de apontamento ativo (force=${force})`, t => {
|
|
101
|
+
const root = fixture(t), generated = readFileSync(file(root, pointer));
|
|
102
|
+
put(root, pointer, Buffer.concat([generated, Buffer.from('\nAlteração humana staged.\n')]));
|
|
103
|
+
git(root, 'add', '-f', '--', pointer);
|
|
104
|
+
const index = git(root, 'ls-files', '--stage', '-z');
|
|
105
|
+
put(root, pointer, generated);
|
|
106
|
+
put(root, original, '---\nname: criar-skill\ndescription: Mudança real.\n---\n\n# Criar skill\n');
|
|
107
|
+
const state = readFileSync(file(root, '.pbai4devs/state/derived.json'));
|
|
108
|
+
assert.notEqual(run(root, { check: true, force }).exitCode, 0);
|
|
109
|
+
assert.throws(() => run(root, { force }), /indice/i);
|
|
110
|
+
assert.deepEqual(readFileSync(file(root, pointer)), generated);
|
|
111
|
+
assert.deepEqual(readFileSync(file(root, '.pbai4devs/state/derived.json')), state);
|
|
112
|
+
assert.equal(git(root, 'ls-files', '--stage', '-z'), index);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const flag of ['--assume-unchanged', '--skip-worktree']) {
|
|
117
|
+
test(`índice divergente continua protegido com ${flag}`, t => {
|
|
118
|
+
const root = fixture(t), generated = readFileSync(file(root, pointer));
|
|
119
|
+
put(root, pointer, Buffer.concat([generated, Buffer.from('\nAlteração humana staged.\n')]));
|
|
120
|
+
git(root, 'add', '-f', '--', pointer);
|
|
121
|
+
put(root, pointer, generated);
|
|
122
|
+
git(root, 'update-index', flag, '--', pointer);
|
|
123
|
+
const index = git(root, 'ls-files', '--stage', '-z');
|
|
124
|
+
const flags = git(root, 'ls-files', '-v', '--', pointer);
|
|
125
|
+
put(root, original, '---\nname: criar-skill\ndescription: Mudança real.\n---\n\n# Criar skill\n');
|
|
126
|
+
assert.notEqual(run(root, { check: true, force: true }).exitCode, 0);
|
|
127
|
+
assert.throws(() => run(root, { force: true }), /indice/i);
|
|
128
|
+
assert.deepEqual(readFileSync(file(root, pointer)), generated);
|
|
129
|
+
assert.equal(git(root, 'ls-files', '--stage', '-z'), index);
|
|
130
|
+
assert.equal(git(root, 'ls-files', '-v', '--', pointer), flags);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
test('índice com alias de caixa bloqueia alteração conservadoramente em qualquer sistema', t => {
|
|
135
|
+
const root = fixture(t), generated = readFileSync(file(root, pointer));
|
|
136
|
+
const alias = pointer.replace('/skills/', '/SKILLS/');
|
|
137
|
+
put(root, alias, Buffer.concat([generated, Buffer.from('\nAlteração humana staged.\n')]));
|
|
138
|
+
git(root, 'add', '-f', '--', alias);
|
|
139
|
+
const index = git(root, 'ls-files', '--stage', '-z');
|
|
140
|
+
put(root, pointer, generated);
|
|
141
|
+
put(root, original, '---\nname: criar-skill\ndescription: Mudança real.\n---\n\n# Criar skill\n');
|
|
142
|
+
const aliasBytes = readFileSync(file(root, alias));
|
|
143
|
+
const state = readFileSync(file(root, '.pbai4devs/state/derived.json'));
|
|
144
|
+
const ignore = readFileSync(file(root, '.gitignore'));
|
|
145
|
+
const preserved = () => {
|
|
146
|
+
assert.deepEqual(readFileSync(file(root, pointer)), generated);
|
|
147
|
+
assert.deepEqual(readFileSync(file(root, alias)), aliasBytes);
|
|
148
|
+
assert.deepEqual(readFileSync(file(root, '.pbai4devs/state/derived.json')), state);
|
|
149
|
+
assert.deepEqual(readFileSync(file(root, '.gitignore')), ignore);
|
|
150
|
+
assert.equal(git(root, 'ls-files', '--stage', '-z'), index);
|
|
151
|
+
};
|
|
152
|
+
let check, error;
|
|
153
|
+
try { check = run(root, { check: true, force: true }); } catch (failure) { error = failure; }
|
|
154
|
+
if (error) assert.match(error.message, /maiusculas\/minusculas/i);
|
|
155
|
+
else assert.notEqual(check.exitCode, 0);
|
|
156
|
+
preserved();
|
|
157
|
+
assert.throws(() => run(root, { force: true }), /indice|maiusculas\/minusculas/i);
|
|
158
|
+
preserved();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('comparação de índice entende CRLF autorizado e recusa filtros, binários e divergência', t => {
|
|
162
|
+
const root = fixture(t), path = 'arquivo.txt';
|
|
163
|
+
const crlf = Buffer.from('texto\r\noutra linha\r\n'), lf = Buffer.from('texto\noutra linha\n');
|
|
164
|
+
git(root, 'config', 'core.autocrlf', 'false');
|
|
165
|
+
git(root, 'config', 'core.attributesfile', file(root, 'atributos-ausentes'));
|
|
166
|
+
assert.equal(indexContentMatches(root, path, crlf, lf), false);
|
|
167
|
+
git(root, 'config', 'core.autocrlf', 'true');
|
|
168
|
+
assert.equal(indexContentMatches(root, path, crlf, lf), true);
|
|
169
|
+
assert.equal(indexContentMatches(root, path, crlf, Buffer.from('texto humano\noutra linha\n')), false);
|
|
170
|
+
assert.equal(indexContentMatches(root, path, Buffer.from([0, 13, 10]), Buffer.from([0, 10])), false);
|
|
171
|
+
assert.equal(indexContentMatches(root, path, Buffer.from('um\r\ndois\r'), Buffer.from('um\ndois\r')), false);
|
|
172
|
+
assert.equal(indexContentMatches(root, path, Buffer.from('\ufefftexto\r\n'), Buffer.from('texto\n')), false);
|
|
173
|
+
assert.equal(indexContentMatches(root, path, Buffer.from('\ufefftexto\r\n'), Buffer.from('\ufefftexto\n')), true);
|
|
174
|
+
put(root, '.gitattributes', '*.txt -text\n');
|
|
175
|
+
assert.equal(indexContentMatches(root, path, crlf, lf), false);
|
|
176
|
+
put(root, '.gitattributes', '*.txt text filter=nao-executar\n');
|
|
177
|
+
git(root, 'config', 'filter.nao-executar.clean', 'exit 1');
|
|
178
|
+
assert.equal(indexContentMatches(root, path, crlf, lf), false);
|
|
179
|
+
put(root, '.gitattributes', '*.txt text working-tree-encoding=UTF-16\n');
|
|
180
|
+
assert.equal(indexContentMatches(root, path, crlf, lf), false);
|
|
181
|
+
put(root, '.gitattributes', '*.txt text ident\n');
|
|
182
|
+
assert.equal(indexContentMatches(root, path, crlf, lf), false);
|
|
183
|
+
put(root, '.gitattributes', '*.txt text eol=crlf\n');
|
|
184
|
+
git(root, 'config', 'core.autocrlf', 'false');
|
|
185
|
+
assert.equal(indexContentMatches(root, path, crlf, lf), true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('sync atualiza apontamento staged equivalente em repositório com autocrlf', t => {
|
|
189
|
+
const root = fixture(t), generated = readFileSync(file(root, pointer), 'utf8').replaceAll('\n', '\r\n');
|
|
190
|
+
git(root, 'config', 'core.autocrlf', 'true');
|
|
191
|
+
put(root, pointer, generated);
|
|
192
|
+
git(root, 'add', '-f', '--', pointer);
|
|
193
|
+
const index = git(root, 'ls-files', '--stage', '-z');
|
|
194
|
+
put(root, original, '---\nname: criar-skill\ndescription: Mudança real.\n---\n\n# Criar skill\n');
|
|
195
|
+
assert.equal(run(root, { force: true }).exitCode, 0);
|
|
196
|
+
assert.equal(git(root, 'ls-files', '--stage', '-z'), index);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
for (const target of ['.pbai4devs/state/derived.json', '.claude/.gitignore']) {
|
|
200
|
+
test(`índice divergente impede atualização de ${target}`, t => {
|
|
201
|
+
const root = fixture(t), generated = readFileSync(file(root, target));
|
|
202
|
+
put(root, target, Buffer.concat([generated, Buffer.from('\nAlteração humana staged.\n')]));
|
|
203
|
+
git(root, 'add', '-f', '--', target);
|
|
204
|
+
const index = git(root, 'ls-files', '--stage', '-z');
|
|
205
|
+
put(root, target, generated);
|
|
206
|
+
if (target.endsWith('derived.json')) {
|
|
207
|
+
put(root, original, '---\nname: criar-skill\ndescription: Mudança real.\n---\n\n# Criar skill\n');
|
|
208
|
+
} else {
|
|
209
|
+
put(root, '.pbai4devs/templates/gitignore/agents.ignore', '# Cache atualizado\n/*\n!/.gitignore\n{{ALLOWLIST}}\n/cache/\n*.log\n');
|
|
210
|
+
}
|
|
211
|
+
assert.notEqual(run(root, { check: true, force: true }).exitCode, 0);
|
|
212
|
+
assert.throws(() => run(root, { force: true }), /indice/i);
|
|
213
|
+
assert.deepEqual(readFileSync(file(root, target)), generated);
|
|
214
|
+
assert.equal(git(root, 'ls-files', '--stage', '-z'), index);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (const options of [{ check: true }, { force: true }]) {
|
|
219
|
+
test(`Git indisponível bloqueia limpeza em repositório (check=${Boolean(options.check)}, force=${Boolean(options.force)})`, t => {
|
|
220
|
+
const root = fixture(t), generated = readFileSync(file(root, pointer));
|
|
221
|
+
put(root, pointer, Buffer.concat([generated, Buffer.from('\nConteúdo humano staged.\n')]));
|
|
222
|
+
git(root, 'add', '-f', '--', pointer);
|
|
223
|
+
const index = git(root, 'ls-files', '--stage', '-z');
|
|
224
|
+
put(root, pointer, generated);
|
|
225
|
+
rmSync(dirname(file(root, original)), { recursive: true });
|
|
226
|
+
const state = readFileSync(file(root, '.pbai4devs/state/derived.json'));
|
|
227
|
+
withoutGit(t, () => assert.throws(() => run(root, options), /consultar o Git.*preservar/i));
|
|
228
|
+
assert.deepEqual(readFileSync(file(root, pointer)), generated);
|
|
229
|
+
assert.deepEqual(readFileSync(file(root, '.pbai4devs/state/derived.json')), state);
|
|
230
|
+
assert.equal(git(root, 'ls-files', '--stage', '-z'), index);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
test('projeto realmente sem Git pode limpar apontamento próprio sem executável Git', t => {
|
|
235
|
+
const root = fixture(t, false), human = '.claude/skills/criar-skill/equipe-vazia';
|
|
236
|
+
mkdirSync(file(root, human));
|
|
237
|
+
rmSync(dirname(file(root, original)), { recursive: true });
|
|
238
|
+
withoutGit(t, () => assert.equal(run(root, { force: true }).exitCode, 0));
|
|
239
|
+
assert.equal(existsSync(file(root, pointer)), false);
|
|
240
|
+
assert.ok(existsSync(file(root, human)));
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('preserva diretório preexistente sem prova de propriedade após remover único apontamento', t => {
|
|
244
|
+
const root = fixture(t), directory = dirname(file(root, pointer));
|
|
245
|
+
rmSync(dirname(file(root, original)), { recursive: true });
|
|
246
|
+
assert.equal(run(root, { force: true }).exitCode, 0);
|
|
247
|
+
assert.equal(existsSync(file(root, pointer)), false);
|
|
248
|
+
assert.ok(existsSync(directory));
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('alteração concorrente no índice bloqueia limpeza antes de qualquer exclusão', t => {
|
|
252
|
+
const root = fixture(t), generated = readFileSync(file(root, pointer));
|
|
253
|
+
rmSync(dirname(file(root, original)), { recursive: true });
|
|
254
|
+
let staged;
|
|
255
|
+
assert.throws(() => synchronize({ root, localIgnores: false, generateEntrypoints: false, force: true,
|
|
256
|
+
output() {
|
|
257
|
+
if (staged !== undefined) return;
|
|
258
|
+
put(root, pointer, Buffer.concat([generated, Buffer.from('\nStaged pela equipe.\n')]));
|
|
259
|
+
git(root, 'add', '-f', '--', pointer);
|
|
260
|
+
staged = git(root, 'ls-files', '--stage', '-z');
|
|
261
|
+
put(root, pointer, generated);
|
|
262
|
+
},
|
|
263
|
+
}), /indice git mudou/i);
|
|
264
|
+
assert.notEqual(staged, undefined);
|
|
265
|
+
assert.deepEqual(readFileSync(file(root, pointer)), generated);
|
|
266
|
+
assert.equal(git(root, 'ls-files', '--stage', '-z'), staged);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('índice divergente é protegido quando o projeto fica em subpasta do repositório Git', t => {
|
|
270
|
+
const outer = mkdtempSync(join(tmpdir(), 'sync-parent-git-'));
|
|
271
|
+
t.after(() => rmSync(outer, { recursive: true, force: true }));
|
|
272
|
+
git(outer, 'init', '--quiet');
|
|
273
|
+
const root = join(outer, 'projeto'); mkdirSync(root);
|
|
274
|
+
populate(root);
|
|
275
|
+
const generated = readFileSync(file(root, pointer));
|
|
276
|
+
put(root, pointer, Buffer.concat([generated, Buffer.from('\nStaged pela equipe.\n')]));
|
|
277
|
+
git(root, 'add', '-f', '--', pointer);
|
|
278
|
+
const index = git(outer, 'ls-files', '--stage', '-z');
|
|
279
|
+
put(root, pointer, generated);
|
|
280
|
+
rmSync(dirname(file(root, original)), { recursive: true });
|
|
281
|
+
assert.throws(() => run(root, { force: true }), /indice/i);
|
|
282
|
+
assert.deepEqual(readFileSync(file(root, pointer)), generated);
|
|
283
|
+
assert.equal(git(outer, 'ls-files', '--stage', '-z'), index);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('conteúdo humano na pasta e apontamento gerenciado editado sobrevivem à limpeza forçada', t => {
|
|
287
|
+
const root = fixture(t), human = '.claude/skills/criar-skill/notas-da-equipe.md';
|
|
288
|
+
const notes = '# Referência própria\n', edited = Buffer.concat([readFileSync(file(root, pointer)), Buffer.from('\nNota própria.\n')]);
|
|
289
|
+
put(root, human, notes);
|
|
290
|
+
put(root, pointer, edited);
|
|
291
|
+
rmSync(dirname(file(root, original)), { recursive: true });
|
|
292
|
+
run(root, { force: true });
|
|
293
|
+
assert.equal(readFileSync(file(root, human), 'utf8'), notes);
|
|
294
|
+
assert.deepEqual(readFileSync(file(root, pointer)), edited);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('colisão com arquivo humano ancestral não é removida por --force', t => {
|
|
298
|
+
const root = fixture(t), ancestor = '.claude/skills/equipe-custom';
|
|
299
|
+
const human = 'Arquivo da equipe; não substituir.\n';
|
|
300
|
+
put(root, ancestor, human);
|
|
301
|
+
put(root, '.pbai4devs/skills/projeto/equipe-custom/SKILL.md', '---\nname: equipe-custom\ndescription: Exemplo.\n---\n\n# Equipe\n');
|
|
302
|
+
const index = git(root, 'ls-files', '--stage', '-z');
|
|
303
|
+
try { run(root, { force: true }); } catch (error) { assert.match(error.message, /conflito|gerenciado|arquivo|diretor/i); }
|
|
304
|
+
assert.equal(readFileSync(file(root, ancestor), 'utf8'), human);
|
|
305
|
+
assert.equal(git(root, 'ls-files', '--stage', '-z'), index);
|
|
306
|
+
});
|
|
@@ -13,7 +13,9 @@ import './LOCAL_IGNORE.mjs';
|
|
|
13
13
|
import './HUMAN_OWNERSHIP.mjs';
|
|
14
14
|
import './COEXISTENCE.mjs';
|
|
15
15
|
import './MANAGED_CONTENT.mjs';
|
|
16
|
+
import './PRESERVATION.mjs';
|
|
16
17
|
import { validateCatalog as validateExternalCatalog } from '../../gerenciar-skills-externas/SCRIPT.mjs';
|
|
18
|
+
import { stateDirectory } from '../../lib/state-path.mjs';
|
|
17
19
|
|
|
18
20
|
const project = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
19
21
|
const cli = resolve(dirname(fileURLToPath(import.meta.url)), '../SCRIPT.mjs');
|
|
@@ -24,7 +26,7 @@ const read = (root, path) => readFileSync(join(root, path), 'utf8');
|
|
|
24
26
|
const write = (root, path, content) => { mkdirSync(dirname(join(root, path)), { recursive: true }); writeFileSync(join(root, path), content); };
|
|
25
27
|
const pointer = (origin = 'origem.md', title = 'Referencia') => `---\ndescription: Referencia.\n---\n\n# ${title}\n\n${marker}\nLeia e siga [origem](${origin}).\n`;
|
|
26
28
|
const recordOwned = (root, paths) => {
|
|
27
|
-
const statePath = '.pbai4devs/
|
|
29
|
+
const statePath = '.pbai4devs/state/agentdevkit.json';
|
|
28
30
|
const previous = existsSync(join(root, statePath)) ? JSON.parse(read(root, statePath)) : {};
|
|
29
31
|
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
32
|
};
|
|
@@ -75,7 +77,7 @@ function fixture(t, minimal = false) {
|
|
|
75
77
|
write(root, '.github/prompts/custom.prompt.md', '# Prompt proprio\n');
|
|
76
78
|
write(root, '.claude/notas.md', '# Nota propria\n');
|
|
77
79
|
write(root, 'README.md', 'README raiz preservado\n');
|
|
78
|
-
for (const kind of ['templates', 'scripts', 'tools', 'external-skills', '
|
|
80
|
+
for (const kind of ['templates', 'scripts', 'tools', 'external-skills', 'state', 'tests', 'docs', 'adapters', 'extra']) {
|
|
79
81
|
write(root, `.pbai4devs/${kind}/demo/REFERENCE.md`, '---\ndescription: Exclusivo.\n---\n\n# Exclusivo\n');
|
|
80
82
|
if (kind !== 'extra') {
|
|
81
83
|
const target = `.claude/${kind}/demo/REFERENCE.md`;
|
|
@@ -92,6 +94,30 @@ function fixture(t, minimal = false) {
|
|
|
92
94
|
recordOwned(root, ownedLegacy);
|
|
93
95
|
return root;
|
|
94
96
|
}
|
|
97
|
+
|
|
98
|
+
test('sincronização direta conserva estado legado e recusa duas pastas de estado', t => {
|
|
99
|
+
const root = fixture(t, true);
|
|
100
|
+
write(root, source, skill);
|
|
101
|
+
write(root, '.pbai4devs/estado/agentdevkit.json', JSON.stringify({
|
|
102
|
+
schemaVersion: 1, clients: ['codex'], files: {}, blocks: {},
|
|
103
|
+
}));
|
|
104
|
+
assert.equal(run(root).exitCode, 0);
|
|
105
|
+
assert.equal(existsSync(join(root, '.pbai4devs/estado/derived.json')), true);
|
|
106
|
+
assert.equal(existsSync(join(root, '.pbai4devs/state')), false);
|
|
107
|
+
write(root, '.pbai4devs/state/configuracao/ESTADO.md', '# Decisões próprias\n');
|
|
108
|
+
const before = snapshot(root);
|
|
109
|
+
assert.throws(() => run(root), /coexistem/);
|
|
110
|
+
assert.deepEqual(snapshot(root), before);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('detecção de estado recusa link simbólico sem abrir o destino', { skip: process.platform === 'win32' }, t => {
|
|
114
|
+
const root = fixture(t, true);
|
|
115
|
+
const external = join(root, 'externo');
|
|
116
|
+
mkdirSync(external);
|
|
117
|
+
symlinkSync(external, join(root, '.pbai4devs/estado'), 'dir');
|
|
118
|
+
assert.throws(() => stateDirectory(root), /Link simbolico/);
|
|
119
|
+
assert.equal(existsSync(join(root, '.pbai4devs/state')), false);
|
|
120
|
+
});
|
|
95
121
|
function rejected(root, options = {}, pattern) {
|
|
96
122
|
const before = snapshot(root);
|
|
97
123
|
assert.throws(() => run(root, options), pattern);
|
|
@@ -115,7 +141,12 @@ test('sincroniza somente principais, preserva metadados e usa referências UTF-8
|
|
|
115
141
|
assert.match(read(root, '.kimi-code/agents/revisor.md'), /tools: \[Read, Grep, Glob\]/);
|
|
116
142
|
assert.ok(existsSync(join(root, '.claude/skills/projeto/demo/SKILL.md')));
|
|
117
143
|
for (const client of ['.claude', '.opencode', '.github', '.codex', '.cursor', '.kimi-code', '.vscode', '.agents', '.agent', '.gemini']) {
|
|
118
|
-
for (const kind of ['templates', 'scripts', 'tools', 'external-skills', 'estado', 'tests', 'docs', 'adapters', 'extra', 'README.md', 'INSTRUCTIONS.md'])
|
|
144
|
+
for (const kind of ['templates', 'scripts', 'tools', 'external-skills', 'state', 'estado', 'tests', 'docs', 'adapters', 'extra', 'README.md', 'INSTRUCTIONS.md']) {
|
|
145
|
+
if (client === '.claude' && ['templates', 'scripts', 'tools', 'external-skills', 'state', 'tests', 'docs', 'adapters'].includes(kind)) {
|
|
146
|
+
assert.equal(existsSync(join(root, client, kind, 'demo/REFERENCE.md')), false);
|
|
147
|
+
assert.ok(existsSync(join(root, client, kind)), 'diretório sem prova de propriedade permanece');
|
|
148
|
+
} else assert.equal(existsSync(join(root, client, kind)), false, `${client}/${kind}`);
|
|
149
|
+
}
|
|
119
150
|
}
|
|
120
151
|
assert.deepEqual(JSON.parse(read(root, '.pbai4devs/agents.json')).entries, [{ path: '.pbai4devs/agents/revisor/AGENT.md' }]);
|
|
121
152
|
assert.ok(JSON.parse(read(root, '.pbai4devs/skills.json')).entries.some(entry => entry.path === '.pbai4devs/skills/grupo com espaço/outro'));
|
|
@@ -416,7 +447,8 @@ test('atualiza fontes, adiciona clientes, remove recursos e migra agrupado para
|
|
|
416
447
|
rmSync(join(root, '.pbai4devs/rules/example/RULE.md'));
|
|
417
448
|
write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf').replace('agente|.claude\n', 'agente|.claude|plano\n'));
|
|
418
449
|
run(root);
|
|
419
|
-
assert.equal(existsSync(join(root, '.claude/skills/projeto')), false);
|
|
450
|
+
assert.equal(existsSync(join(root, '.claude/skills/projeto/demo/SKILL.md')), false);
|
|
451
|
+
assert.ok(existsSync(join(root, '.claude/skills/projeto')));
|
|
420
452
|
assert.ok(existsSync(join(root, '.claude/skills/demo/SKILL.md')));
|
|
421
453
|
assert.equal(existsSync(join(root, '.codex/rules/example.md')), false);
|
|
422
454
|
assert.deepEqual(JSON.parse(read(root, '.pbai4devs/rules.json')).entries, []);
|
|
@@ -589,12 +621,12 @@ test('entradas da CLI mantêm ownership integral e recursos têm proveniência d
|
|
|
589
621
|
write(root, source, skill);
|
|
590
622
|
write(root, '.pbai4devs/templates/entrypoints/AGENTS.md', template);
|
|
591
623
|
write(root, 'AGENTS.md', entry);
|
|
592
|
-
write(root, '.pbai4devs/
|
|
624
|
+
write(root, '.pbai4devs/state/agentdevkit.json', JSON.stringify({ clients: ['codex'], files: { 'AGENTS.md': createHash('sha256').update(entry).digest('hex') } }));
|
|
593
625
|
const result = run(root, { generateEntrypoints: true });
|
|
594
626
|
assert.ok(result.ownedOutputs.includes('AGENTS.md'));
|
|
595
627
|
assert.equal(read(root, 'AGENTS.md'), entry);
|
|
596
|
-
assert.equal(JSON.parse(read(root, '.pbai4devs/
|
|
597
|
-
assert.ok(JSON.parse(read(root, '.pbai4devs/
|
|
628
|
+
assert.equal(JSON.parse(read(root, '.pbai4devs/state/derived.json')).entries['AGENTS.md'], undefined);
|
|
629
|
+
assert.ok(JSON.parse(read(root, '.pbai4devs/state/derived.json')).files['.codex/skills/demo/SKILL.md']);
|
|
598
630
|
assertCheckReadonly(root, { generateEntrypoints: true }, 0);
|
|
599
631
|
});
|
|
600
632
|
|
|
@@ -857,7 +889,8 @@ test('manifestos Antigravity acompanham skills, regras e agentes agrupados', t =
|
|
|
857
889
|
rmSync(join(root, '.pbai4devs/skills/projeto/demo'), { recursive: true });
|
|
858
890
|
run(root, { generateEntrypoints: true });
|
|
859
891
|
assert.deepEqual(JSON.parse(read(root, '.agents/skills.json')).entries, [{ path: '.agents/skills/projeto/new-skill' }]);
|
|
860
|
-
assert.equal(existsSync(join(root, '.agents/skills/projeto/demo')), false);
|
|
892
|
+
assert.equal(existsSync(join(root, '.agents/skills/projeto/demo/SKILL.md')), false);
|
|
893
|
+
assert.ok(existsSync(join(root, '.agents/skills/projeto/demo')));
|
|
861
894
|
assertCheckReadonly(root, { generateEntrypoints: true }, 0);
|
|
862
895
|
});
|
|
863
896
|
|
|
@@ -8,6 +8,7 @@ import { isMain, projectRoot, requireNode } from '../lib/runtime.mjs';
|
|
|
8
8
|
import { isGeneratedOutput } from '../lib/gitignore.mjs';
|
|
9
9
|
import { resourcePath, synchronize } from '../sincronizar-agentes/SCRIPT.mjs';
|
|
10
10
|
import { adapterManifest } from '../lib/adapters.mjs';
|
|
11
|
+
import { stateDirectory } from '../lib/state-path.mjs';
|
|
11
12
|
|
|
12
13
|
const assets = join(dirname(fileURLToPath(import.meta.url)), 'assets');
|
|
13
14
|
const owner = 'testar-maturidade';
|
|
@@ -47,9 +48,9 @@ export const SCENARIOS = [
|
|
|
47
48
|
];
|
|
48
49
|
|
|
49
50
|
function manifest(root) {
|
|
51
|
+
const state = join(root, stateDirectory(root), 'agentdevkit.json');
|
|
50
52
|
const file = join(root, '.pbai4devs/espelhos.conf');
|
|
51
53
|
if (existsSync(file)) return readFileSync(file, 'utf8');
|
|
52
|
-
const state = join(root, '.pbai4devs/estado/agentdevkit.json');
|
|
53
54
|
return adapterManifest(root, { clients: existsSync(state) ? json(state).clients : undefined });
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -94,7 +95,7 @@ function copyAllowed(relativePath, directory = false) {
|
|
|
94
95
|
|
|
95
96
|
export function copyAgentStructure(sourceRoot, destination) {
|
|
96
97
|
const roots = ['.pbai4devs', ...registeredRoots(sourceRoot)];
|
|
97
|
-
const stateFile = join(sourceRoot, '
|
|
98
|
+
const stateFile = join(sourceRoot, stateDirectory(sourceRoot), 'agentdevkit.json');
|
|
98
99
|
const stateInfo = stat(stateFile);
|
|
99
100
|
if (stateInfo && (!stateInfo.isFile() || stateInfo.isSymbolicLink())) fail('Estado da instalação deve ser arquivo regular.');
|
|
100
101
|
const installation = stateInfo ? json(stateFile) : null;
|
|
@@ -162,10 +163,11 @@ function ensureOutput(output) {
|
|
|
162
163
|
}
|
|
163
164
|
|
|
164
165
|
function planning(root, scenario) {
|
|
165
|
-
const
|
|
166
|
+
const stateRoot = stateDirectory(root);
|
|
167
|
+
const state = `${stateRoot}/configuracao/ESTADO.md`;
|
|
166
168
|
rmSync(join(root, state), { force: true });
|
|
167
169
|
if (scenario.planning === 'none' && !scenario.app) return;
|
|
168
|
-
write(root,
|
|
170
|
+
write(root, `${stateRoot}/README.md`, document('Estado dos exemplos', 'Dados sintéticos deste cenário, sem aprovação real de produto.'));
|
|
169
171
|
let paths = { prd: null, ux_ui: null, design: null, releases: null, specs: null };
|
|
170
172
|
if (scenario.planning !== 'none') {
|
|
171
173
|
for (const folder of ['docs', 'docs/produto', 'docs/design']) write(root, `${folder}/README.md`, document('Documentos do cenário', 'Entradas sintéticas para testar a classificação de maturidade.'));
|
|
@@ -27,6 +27,7 @@ function fixture(t) {
|
|
|
27
27
|
put('.gitignore', readFileSync(join(projectRoot, '.gitignore')));
|
|
28
28
|
put('.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs', readFileSync(join(projectRoot, '.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs')));
|
|
29
29
|
put('.pbai4devs/scripts/lib/runtime.mjs', readFileSync(join(projectRoot, '.pbai4devs/scripts/lib/runtime.mjs')));
|
|
30
|
+
put('.pbai4devs/scripts/lib/state-path.mjs', readFileSync(join(projectRoot, '.pbai4devs/scripts/lib/state-path.mjs')));
|
|
30
31
|
return { root, put, outputRoot: join(root, 'examples/generated') };
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -112,7 +113,7 @@ test('preparação interrompida não conserva resultado anterior nem declara suc
|
|
|
112
113
|
const index = join(outputRoot, 'README.md');
|
|
113
114
|
writeFileSync(report, JSON.stringify({ owner: 'testar-maturidade', status: 'passed', results: [{}] }));
|
|
114
115
|
writeFileSync(index, readFileSync(index, 'utf8').replaceAll('prepared', 'passed'));
|
|
115
|
-
put('.pbai4devs/
|
|
116
|
+
put('.pbai4devs/state/agentdevkit.json', JSON.stringify({ blocks: null }));
|
|
116
117
|
assert.throws(() => prepareExamples(options), /Estado da instalação/);
|
|
117
118
|
assert.equal(existsSync(report), false);
|
|
118
119
|
assert.match(readFileSync(index, 'utf8'), /status: preparing/);
|
|
@@ -181,7 +182,7 @@ test('instalação parcial preserva entradas próprias e blocos editados', t =>
|
|
|
181
182
|
const { root, put } = fixture(t);
|
|
182
183
|
put('.pbai4devs/espelhos.conf', 'agente|.codex|agrupado\nagente|.agents|agrupado\n');
|
|
183
184
|
const block = '<!-- BEGIN AGENTDEVKIT -->\nEntrada gerenciada.\n<!-- END AGENTDEVKIT -->';
|
|
184
|
-
put('.pbai4devs/
|
|
185
|
+
put('.pbai4devs/state/agentdevkit.json', JSON.stringify({ schemaVersion: 1, version: '1.0.0', language: 'pt', clients: ['codex'], files: {}, blocks: { 'AGENTS.md': createHash('sha256').update(block).digest('hex') } }));
|
|
185
186
|
put('AGENTS.md', `# Agentes\n\n${block}\n`);
|
|
186
187
|
const human = '# Claude pessoal\r\n\r\nPreservar byte a byte: ação.\r\n';
|
|
187
188
|
put('CLAUDE.md', human);
|
|
@@ -222,7 +223,7 @@ test('checkout de fontes sem manifesto deriva clientes em memória', t => {
|
|
|
222
223
|
assert.ok(registeredRoots(root).includes('.claude'));
|
|
223
224
|
const source = '.pbai4devs/skills/projeto/demo/SKILL.md';
|
|
224
225
|
assert.ok(resourceTargets(root, source).includes('.claude/skills/demo/SKILL.md'));
|
|
225
|
-
put('.pbai4devs/
|
|
226
|
+
put('.pbai4devs/state/agentdevkit.json', JSON.stringify({ clients: ['claude'] }));
|
|
226
227
|
assert.deepEqual(registeredRoots(root), ['.claude']);
|
|
227
228
|
assert.deepEqual(resourceTargets(root, source), ['.claude/skills/demo/SKILL.md']);
|
|
228
229
|
assert.equal(existsSync(manifest), false);
|
|
@@ -8,12 +8,13 @@ import { buildCatalog } from '../lib/catalog.mjs';
|
|
|
8
8
|
import { isGeneratedOutput } from '../lib/gitignore.mjs';
|
|
9
9
|
import { canonicalEntrypoint, extractManagedBlock, managedBlockDigest } from '../lib/managed-content.mjs';
|
|
10
10
|
import { extractTomlManagedBlock, tomlManagedBlockDigest } from '../lib/managed-toml.mjs';
|
|
11
|
+
import { stateDirectory } from '../lib/state-path.mjs';
|
|
11
12
|
|
|
12
13
|
const fail = message => { throw new Error(message); };
|
|
13
14
|
const normalize = value => value.split(sep).join('/');
|
|
14
15
|
const excluded = new Set(['node_modules', '.git', 'opencode-loop', '.agent-tests']);
|
|
15
16
|
const toolState = new Set(['bin', '.cache', 'npm-cache']);
|
|
16
|
-
const auxiliary = ['templates', 'scripts', 'tools', 'external-skills', 'adapters', 'estado', 'tests', 'docs'];
|
|
17
|
+
const auxiliary = ['templates', 'scripts', 'tools', 'external-skills', 'adapters', 'state', 'estado', 'tests', 'docs'];
|
|
17
18
|
|
|
18
19
|
function checkedStat(root, location) {
|
|
19
20
|
let item;
|
|
@@ -31,8 +32,9 @@ function checkedPath(root, path) {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
function ownership(root) {
|
|
34
|
-
const
|
|
35
|
-
const
|
|
35
|
+
const stateRoot = stateDirectory(root);
|
|
36
|
+
const file = checkedPath(root, `${stateRoot}/agentdevkit.json`);
|
|
37
|
+
const derivedFile = checkedPath(root, `${stateRoot}/derived.json`);
|
|
36
38
|
if (!checkedStat(root, file) && !checkedStat(root, derivedFile)) return null;
|
|
37
39
|
const readState = location => {
|
|
38
40
|
try { return JSON.parse(readFileSync(location, 'utf8')); }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, readdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, readdirSync, renameSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { join, relative } from 'node:path';
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
@@ -43,14 +43,22 @@ const verify = root => verifyResources({ root, synchronize() {}, catalogCheck()
|
|
|
43
43
|
|
|
44
44
|
function installedFixture(t) {
|
|
45
45
|
const context = resourceFixture(t);
|
|
46
|
-
context.file('.pbai4devs/
|
|
46
|
+
context.file('.pbai4devs/state/README.md');
|
|
47
47
|
const generated = '.agents/skills/managed/SKILL.md';
|
|
48
48
|
context.file(generated, document('# Gerado\n'));
|
|
49
49
|
const files = { [generated]: createHash('sha256').update(readFileSync(join(context.root, generated))).digest('hex') };
|
|
50
|
-
context.file('.pbai4devs/
|
|
50
|
+
context.file('.pbai4devs/state/agentdevkit.json', JSON.stringify({ schemaVersion: 1, version: '0.0.9', language: 'pt', files, blocks: {} }));
|
|
51
51
|
return { ...context, generated };
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
test('verificação consulta propriedade legada e recusa estado ambíguo', t => {
|
|
55
|
+
const { root } = installedFixture(t);
|
|
56
|
+
renameSync(join(root, '.pbai4devs/state'), join(root, '.pbai4devs/estado'));
|
|
57
|
+
assert.doesNotThrow(() => collectMarkdown(root));
|
|
58
|
+
mkdirSync(join(root, '.pbai4devs/state'));
|
|
59
|
+
assert.throws(() => collectMarkdown(root), /coexistem/);
|
|
60
|
+
});
|
|
61
|
+
|
|
54
62
|
function writeCatalog(root) {
|
|
55
63
|
for (const [path, content] of buildCatalog(root).outputs) { mkdirSync(join(root, path, '..'), { recursive: true }); writeFileSync(join(root, path), content); }
|
|
56
64
|
}
|
|
@@ -161,10 +169,10 @@ test('instalação confere entradas derivadas sem aplicar lint a blocos humanos'
|
|
|
161
169
|
const { root, file } = installedFixture(t);
|
|
162
170
|
const contents = document('# Entrada gerada\n');
|
|
163
171
|
file('GEMINI.md', contents);
|
|
164
|
-
file('.pbai4devs/
|
|
172
|
+
file('.pbai4devs/state/derived.json', JSON.stringify({ schemaVersion: 1, entries: { 'GEMINI.md': createHash('sha256').update(contents).digest('hex') } }));
|
|
165
173
|
const block = '<!-- BEGIN AGENTDEVKIT -->\nLeia as fontes.\n<!-- END AGENTDEVKIT -->';
|
|
166
174
|
file('AGENTS.md', `# Instruções humanas sem frontmatter\n\n${block}\n`);
|
|
167
|
-
const statePath = join(root, '.pbai4devs/
|
|
175
|
+
const statePath = join(root, '.pbai4devs/state/agentdevkit.json');
|
|
168
176
|
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
169
177
|
state.blocks['AGENTS.md'] = createHash('sha256').update(block).digest('hex');
|
|
170
178
|
state.files['.pbai4devs/skills/exemplo/SKILL.md'] = createHash('sha256').update('fonte anterior').digest('hex');
|
|
@@ -182,7 +190,7 @@ test('instalação também protege manifestos registrados em derived.outputs', t
|
|
|
182
190
|
const manifest = '.agents/skills.json';
|
|
183
191
|
const contents = '{"entries":[]}\n';
|
|
184
192
|
file(manifest, contents);
|
|
185
|
-
file('.pbai4devs/
|
|
193
|
+
file('.pbai4devs/state/derived.json', JSON.stringify({ schemaVersion: 1, entries: {}, outputs: { [manifest]: createHash('sha256').update(contents).digest('hex') } }));
|
|
186
194
|
assert.ok(!verify(root).files.includes(join(root, manifest)));
|
|
187
195
|
rmSync(join(root, manifest));
|
|
188
196
|
assert.throws(() => verify(root), /Arquivo gerenciado ausente: .agents\/skills.json/);
|
|
@@ -372,7 +380,7 @@ test('fontes novas permanecem elegíveis e derivados locais ficam fora do índic
|
|
|
372
380
|
const outputs = ['.github/skills/generated/SKILL.md', '.vscode/.gitignore', '.github/.gitignore'];
|
|
373
381
|
applyLocalIgnore(planLocalIgnore(root, { outputs }));
|
|
374
382
|
const sources = ['src/new.mjs', 'bin/new.mjs', 'tests/new.test.mjs', 'scripts/release/new.mjs', '.github/workflows/own.yml', '.github/ISSUE_TEMPLATE/bug.yml', '.github/skills/human/SKILL.md', '.vscode/settings.json'];
|
|
375
|
-
const generated = [...outputs, '.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/espelhos.conf', '.pbai4devs/
|
|
383
|
+
const generated = [...outputs, '.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/espelhos.conf', '.pbai4devs/state/agentdevkit.json'];
|
|
376
384
|
for (const path of [...sources, ...generated]) file(path, 'fixture\n');
|
|
377
385
|
assert.deepEqual(ignoredPaths(root, sources), []);
|
|
378
386
|
assert.deepEqual(new Set(ignoredPaths(root, generated)), new Set(generated));
|
|
@@ -9,7 +9,7 @@ Siga `.pbai4devs/rules/qualidade/RULE.md` ao criar arquivos Markdown.
|
|
|
9
9
|
Antes de escrever, use `atualizar-documentacao` para ler e reaproveitar documentos
|
|
10
10
|
existentes; aplique o template somente às lacunas.
|
|
11
11
|
|
|
12
|
-
Consulte `.pbai4devs/
|
|
12
|
+
Consulte `.pbai4devs/state/configuracao/ESTADO.md` e confira o PRD aprovado
|
|
13
13
|
e UX/UI aprovado ou não aplicável com justificativa,
|
|
14
14
|
incluindo a evidência das decisões. Se faltar, estiver pendente ou houver
|
|
15
15
|
conflito, informe o bloqueio e encaminhe à skill correspondente.
|
|
@@ -9,7 +9,7 @@ Siga `.pbai4devs/rules/qualidade/RULE.md` ao criar arquivos Markdown.
|
|
|
9
9
|
Antes de escrever, use `atualizar-documentacao` para ler e reaproveitar documentos
|
|
10
10
|
existentes; aplique o template somente às lacunas.
|
|
11
11
|
|
|
12
|
-
Confira a preparação em `.pbai4devs/
|
|
12
|
+
Confira a preparação em `.pbai4devs/state/configuracao/ESTADO.md`. Se não estiver
|
|
13
13
|
concluída e comprovada, encaminhe a `configurar-projeto` para a fase
|
|
14
14
|
inicial antes de criar o PRD.
|
|
15
15
|
|
|
@@ -9,7 +9,7 @@ Siga `.pbai4devs/rules/qualidade/RULE.md` ao criar arquivos Markdown.
|
|
|
9
9
|
Antes de escrever, use `atualizar-documentacao` para ler e reaproveitar documentos
|
|
10
10
|
existentes; aplique o template somente às lacunas.
|
|
11
11
|
|
|
12
|
-
Consulte `.pbai4devs/
|
|
12
|
+
Consulte `.pbai4devs/state/configuracao/ESTADO.md` e confira o PRD
|
|
13
13
|
com `status: aprovado` e a
|
|
14
14
|
aprovação registrada. Se faltar, estiver pendente ou for insuficiente, informe
|
|
15
15
|
o bloqueio e encaminhe a `criar-prd`.
|
|
@@ -20,7 +20,7 @@ as ferramentas e rode o diagnóstico. Isso não escolhe a stack da aplicação.
|
|
|
20
20
|
Antes de criar estado ou documentos, faça o inventário de `atualizar-documentacao`.
|
|
21
21
|
Mapeie os documentos já existentes aos seus papéis, sem reiniciar o produto.
|
|
22
22
|
Reúna dúvidas no início e aproveite decisões existentes. Quando o projeto
|
|
23
|
-
começar, crie ou retome `.pbai4devs/
|
|
23
|
+
começar, crie ou retome `.pbai4devs/state/configuracao/ESTADO.md` pelo modelo.
|
|
24
24
|
Combine os arquivos `prd`, `ux_ui`, `design`, `roadmap` e diretórios `releases`,
|
|
25
25
|
`specs`, `tarefas` e documentos de apoio aplicáveis. Crie/evolua o README
|
|
26
26
|
da raiz pelos [modelos da fase](../../../templates/README.md), preservando os
|
|
@@ -6,7 +6,7 @@ description: Identifica o estado atual e orienta a próxima ação útil do proj
|
|
|
6
6
|
# Próximos passos
|
|
7
7
|
|
|
8
8
|
Leia o pedido, o estado salvo e os documentos existentes. Use o idioma de
|
|
9
|
-
`.pbai4devs/
|
|
9
|
+
`.pbai4devs/state/agentdevkit.json` ou português quando ainda não houver escolha.
|
|
10
10
|
Se a CLI estiver disponível, consulte `agentdevkit next --target . --json`;
|
|
11
11
|
caso contrário, use os arquivos e a inspeção local do projeto.
|
|
12
12
|
|