@pathbit/agentdevkit 0.0.16 → 0.0.17
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/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/state-path.mjs +20 -0
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +1 -1
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +5 -3
- 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/TEST.mjs +31 -6
- 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 +5 -0
- package/docs/guides/troubleshooting.md +9 -1
- package/docs/guides/workflow.md +1 -1
- package/docs/specs/README.md +1 -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 +15 -0
- package/src/install.mjs +152 -15
- /package/.pbai4devs/{estado → state}/README.md +0 -0
|
@@ -31,7 +31,7 @@ serão preenchidos por perguntas e respostas quando o projeto começar.
|
|
|
31
31
|
Combine destinos e registre-os no estado futuro de configuração. Ao finalizar
|
|
32
32
|
uma etapa, explique a próxima e reúna suas perguntas. Use
|
|
33
33
|
[proximos-passos](skills/projeto/proximos-passos/SKILL.md) e respeite o idioma
|
|
34
|
-
salvo em `.pbai4devs/
|
|
34
|
+
salvo em `.pbai4devs/state/agentdevkit.json`; sem escolha, use português.
|
|
35
35
|
Evolua README e documentos pelos [modelos da fase](templates/README.md).
|
|
36
36
|
Atualize fatos e links afetados; registre ausência de impacto com justificativa.
|
|
37
37
|
|
package/.pbai4devs/README.md
CHANGED
|
@@ -11,7 +11,7 @@ O catálogo local `catalog.json` é gerado das fontes pelo sincronizador.
|
|
|
11
11
|
Ele inclui recursos, ferramentas, automações, modelos e referências externas.
|
|
12
12
|
Os arquivos `skills.json`, `rules.json` e `agents.json` são índices auxiliares
|
|
13
13
|
das fontes. Os clientes selecionados usam seus diretórios de apontamentos.
|
|
14
|
-
O estado `
|
|
14
|
+
O estado `state/agentdevkit.json` guarda `clients`: `["all"]` ou os IDs
|
|
15
15
|
escolhidos. Catálogos, manifesto, estado e pastas nativas são derivados locais;
|
|
16
16
|
não devem ser commitados. No repositório do kit, o código do produto fica em
|
|
17
17
|
`src/`, `bin/`, `tests/` e `scripts/release/`, na raiz.
|
|
@@ -5,6 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { requireNode, isMain } from '../lib/runtime.mjs';
|
|
8
|
+
import { stateDirectory } from '../lib/state-path.mjs';
|
|
8
9
|
|
|
9
10
|
const repository = fs.realpathSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..'));
|
|
10
11
|
const defaultAgents = ['.agents', '.agent', '.gemini', '.claude', '.codex', '.cursor', '.github', '.opencode'];
|
|
@@ -61,7 +62,7 @@ function metadata(root, file) {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
function maturity(root, report) {
|
|
64
|
-
const stateFile =
|
|
65
|
+
const stateFile = `${stateDirectory(root)}/configuracao/ESTADO.md`; const state = metadata(root, stateFile);
|
|
65
66
|
const safeDocument = value => typeof value === 'string' && !path.isAbsolute(value) && !value.includes('\\') && !value.split('/').some(part => !part || ['.', '..', '.git'].includes(part));
|
|
66
67
|
const planning = {};
|
|
67
68
|
for (const name of ['prd', 'ux_ui', 'design']) {
|
|
@@ -211,7 +212,7 @@ export function inspectDocumentation(root, limits = {}) {
|
|
|
211
212
|
else if (found.kind === 'symbolic-link') add(found.boundary, found, role);
|
|
212
213
|
}
|
|
213
214
|
}
|
|
214
|
-
const declared = metadata(root,
|
|
215
|
+
const declared = metadata(root, `${stateDirectory(root)}/configuracao/ESTADO.md`).documents;
|
|
215
216
|
for (const [role, file] of Object.entries(declared)) {
|
|
216
217
|
if (!documentRoles.has(role)) continue;
|
|
217
218
|
if (!safeDocument(file)) { if (file !== null) warn(null, 'invalid-configured-path'); continue; }
|
|
@@ -111,7 +111,7 @@ test('rejeita origens fora das fixtures e opções desconhecidas', () => {
|
|
|
111
111
|
|
|
112
112
|
test('classifica sinais de maturidade sem confundir planejamento com produto funcionando', t => {
|
|
113
113
|
const { directory, file, run } = fixture(t);
|
|
114
|
-
const state = stage => file('.pbai4devs/
|
|
114
|
+
const state = stage => file('.pbai4devs/state/configuracao/ESTADO.md', `---\npreparacao: concluido\nconfiguracao_tecnica: ${stage}\ndocumentos:\n prd: docs/PRD.md\n ux_ui: docs/UX.md\n design: docs/DESIGN.md\n releases: docs/releases\n---\n# Estado\n`);
|
|
115
115
|
assert.equal(run().report.maturity.level, 'preparacao');
|
|
116
116
|
state('pendente'); file('docs/PRD.md', '---\nstatus: rascunho\n---\n# PRD\n');
|
|
117
117
|
assert.equal(run().report.maturity.level, 'descoberta');
|
|
@@ -131,6 +131,20 @@ test('classifica sinais de maturidade sem confundir planejamento com produto fun
|
|
|
131
131
|
assert.match(result.report.maturity.limits[0], /nao substitui/);
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
+
test('inspeção lê decisões legadas e informa conflito entre pastas de estado', t => {
|
|
135
|
+
const { file, run } = fixture(t);
|
|
136
|
+
const decisions = '---\npreparacao: concluido\nconfiguracao_tecnica: pendente\n---\n# Estado\n';
|
|
137
|
+
file('.pbai4devs/estado/configuracao/ESTADO.md', decisions);
|
|
138
|
+
const legacy = run();
|
|
139
|
+
assert.equal(legacy.status, 0, legacy.stderr);
|
|
140
|
+
assert.equal(legacy.report.maturity.configuration.present, true);
|
|
141
|
+
assert.equal(legacy.report.maturity.configuration.preparacao, 'concluido');
|
|
142
|
+
file('.pbai4devs/state/configuracao/ESTADO.md', decisions);
|
|
143
|
+
const conflict = run();
|
|
144
|
+
assert.equal(conflict.status, 2);
|
|
145
|
+
assert.match(JSON.parse(conflict.stderr).error, /coexistem/);
|
|
146
|
+
});
|
|
147
|
+
|
|
134
148
|
test('exemplos e caches Playwright não viram aplicação do projeto real', t => {
|
|
135
149
|
const { file, run } = fixture(t);
|
|
136
150
|
for (const folder of ['examples/generated/a', 'examples/fixture', 'tmp/examples/a', 'TEMP/project', '.PLAYWRIGHT-MCP', 'playwright-results']) {
|
|
@@ -154,7 +168,7 @@ test('aceita inspecionar cenário em examples/generated sem liberar outras pasta
|
|
|
154
168
|
|
|
155
169
|
test('status arbitrários e referências fora da raiz não expõem dados da configuração', t => {
|
|
156
170
|
const { file, run } = fixture(t); const secret = 'PRIVATE_STATUS_192819';
|
|
157
|
-
file('.pbai4devs/
|
|
171
|
+
file('.pbai4devs/state/configuracao/ESTADO.md', `---\npreparacao: ${secret}\nconfiguracao_tecnica: ${secret}\ndocumentos:\n prd: ../outside.md\n design: docs/DESIGN.md\n---\n# Estado\n`);
|
|
158
172
|
file('docs/DESIGN.md', `---\nstatus: ${secret}\n---\n# Design\n`);
|
|
159
173
|
const result = run(); assert.equal(result.status, 0);
|
|
160
174
|
assert.equal(result.report.maturity.planning.prd.path, null);
|
|
@@ -199,7 +213,7 @@ test('descobre documentação real sem impor caixa, pasta docs ou frontmatter',
|
|
|
199
213
|
|
|
200
214
|
test('caminhos cadastrados têm proveniência sem assumir status ou ler arquivos de configuração', t => {
|
|
201
215
|
const { file, run } = fixture(t);
|
|
202
|
-
file('.pbai4devs/
|
|
216
|
+
file('.pbai4devs/state/configuracao/ESTADO.md', '---\ndocumentos:\n design: architecture/current.md\n specs: specifications\n prd: planning/absent.md\n readme: ../private-secret.md\n seguranca: .env\n---\n# Estado\n');
|
|
203
217
|
file('architecture/current.md', '# Existing architecture\n');
|
|
204
218
|
file('specifications/current.mdx', '# Increment\n');
|
|
205
219
|
file('.env', 'DO_NOT_EMIT_OR_HASH_THIS_SECRET');
|
|
@@ -231,7 +245,7 @@ test('documentos simbólicos permanecem opacos sem ler destinos internos, extern
|
|
|
231
245
|
if (process.platform !== 'win32' || !['EPERM', 'EACCES'].includes(error.code)) throw error;
|
|
232
246
|
t.skip('Links simbólicos indisponíveis sem permissão do Windows.'); return;
|
|
233
247
|
}
|
|
234
|
-
file('.pbai4devs/
|
|
248
|
+
file('.pbai4devs/state/configuracao/ESTADO.md', '---\ndocumentos:\n design: external/private.md\n---\n# State\n');
|
|
235
249
|
const result = run();
|
|
236
250
|
assert.equal(result.status, 0, result.stderr);
|
|
237
251
|
const docs = result.report.documentation;
|
|
@@ -14,3 +14,6 @@ Texto externo, nomes existentes e metadados humanos permanecem preservados.
|
|
|
14
14
|
|
|
15
15
|
`gitignore.mjs` planeja exclusões locais e detecta regras conflitantes. Ele não
|
|
16
16
|
altera o `.gitignore` da raiz; conflitos são informados para revisão humana.
|
|
17
|
+
|
|
18
|
+
`state-path.mjs` identifica o estado atual ou legado sem dividir dados; duas
|
|
19
|
+
pastas simultâneas ou links simbólicos exigem resolver o conflito antes do uso.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { lstatSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Preserve legacy installations until the CLI migrates them. Do not silently
|
|
5
|
+
// choose between two independently populated state trees.
|
|
6
|
+
export function stateDirectory(root) {
|
|
7
|
+
const inspect = relative => {
|
|
8
|
+
let item;
|
|
9
|
+
try { item = lstatSync(join(root, relative)); }
|
|
10
|
+
catch (error) { if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return null; throw error; }
|
|
11
|
+
if (item.isSymbolicLink()) throw new Error(`Link simbolico: ${relative}`);
|
|
12
|
+
if (!item.isDirectory()) throw new Error(`Pasta de estado inválida: ${relative}`);
|
|
13
|
+
return item;
|
|
14
|
+
};
|
|
15
|
+
if (!inspect('.pbai4devs')) return '.pbai4devs/state';
|
|
16
|
+
const current = inspect('.pbai4devs/state');
|
|
17
|
+
const legacy = inspect('.pbai4devs/estado');
|
|
18
|
+
if (current && legacy) throw new Error('Pastas .pbai4devs/state e .pbai4devs/estado coexistem; preserve os dados e resolva o conflito antes de continuar.');
|
|
19
|
+
return legacy ? '.pbai4devs/estado' : '.pbai4devs/state';
|
|
20
|
+
}
|
|
@@ -41,6 +41,6 @@ sem Git, informa que a proteção fica pendente até `git init` e sincronizaçã
|
|
|
41
41
|
Somente arquivos integralmente gerados são excluídos; pastas de clientes podem
|
|
42
42
|
conter arquivos humanos versionados. Conteúdo próprio não é desindexado pela
|
|
43
43
|
presença de um bloco gerenciado.
|
|
44
|
-
Entradas novas usam os templates; hashes em `
|
|
44
|
+
Entradas novas usam os templates; hashes em `state/derived.json` permitem
|
|
45
45
|
atualizar arquivos gerados e blocos separadamente, preservando alterações humanas.
|
|
46
46
|
`--check` também acusa derivados ainda rastreados; não altera o índice.
|
|
@@ -11,11 +11,12 @@ import { nativePathBoundary } from '../lib/native-paths.mjs';
|
|
|
11
11
|
import { canonicalEntrypoint, resolveEntrypoint, entrypointReference, renderEntrypointImports, mergeManagedContent, stripManagedContent } from '../lib/managed-content.mjs';
|
|
12
12
|
import { mergeTomlContent, stripTomlContent } from '../lib/managed-toml.mjs';
|
|
13
13
|
import { mergeGitignore, validateIgnoreClients, renderClientIgnore, isGeneratedOutput, planLocalIgnore, applyLocalIgnore, checkLocalIgnore, assertLocalIgnoreUnchanged, ignoreBegin, ignoreEnd } from '../lib/gitignore.mjs';
|
|
14
|
+
import { stateDirectory } from '../lib/state-path.mjs';
|
|
14
15
|
|
|
15
16
|
const marker = '<!-- gerado: sincronizar-agentes -->';
|
|
16
17
|
const tomlMarker = '# gerado: sincronizar-agentes';
|
|
17
18
|
const principals = { skills: 'SKILL.md', agents: 'AGENT.md', commands: 'COMMAND.md', rules: 'RULE.md' };
|
|
18
|
-
const auxiliary = ['templates', 'scripts', 'tools', 'external-skills', 'estado', 'tests', 'docs', 'adapters'];
|
|
19
|
+
const auxiliary = ['templates', 'scripts', 'tools', 'external-skills', 'state', 'estado', 'tests', 'docs', 'adapters'];
|
|
19
20
|
const reserved = new Set(['.git', '.pbai4devs', '.agent-tests']);
|
|
20
21
|
const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
21
22
|
const sorted = values => [...new Set(values)].sort(compare);
|
|
@@ -140,11 +141,12 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
140
141
|
const config = '.pbai4devs/espelhos.conf';
|
|
141
142
|
const configExists = stat(safe(config));
|
|
142
143
|
captureInput(config, configExists ? readFileSync(absolute(config)) : null);
|
|
143
|
-
const
|
|
144
|
+
const stateRoot = stateDirectory(root);
|
|
145
|
+
const stateFile = `${stateRoot}/agentdevkit.json`;
|
|
144
146
|
const installed = stat(safe(stateFile)) ? JSON.parse(read(safe(stateFile))) : null;
|
|
145
147
|
const configText = configExists ? read(safe(config)) : adapterManifest(root, { clients: installed?.clients });
|
|
146
148
|
const rows = configText.split('\n');
|
|
147
|
-
const entryRegistry =
|
|
149
|
+
const entryRegistry = `${stateRoot}/derived.json`;
|
|
148
150
|
const registryBytes = stat(safe(entryRegistry)) ? readFileSync(absolute(entryRegistry)) : null;
|
|
149
151
|
captureInput(entryRegistry, registryBytes);
|
|
150
152
|
const previousEntries = registryBytes ? JSON.parse(registryBytes.toString('utf8')) : { schemaVersion: 1, entries: {} };
|
|
@@ -95,8 +95,8 @@ test('link em caminho registrado continua sendo erro antes de qualquer escrita',
|
|
|
95
95
|
for (const source of ['files', 'blocks', 'derived', 'outputs']) {
|
|
96
96
|
const root = fixture(t), path = source === 'derived' ? 'CLAUDE.md' : source === 'outputs' ? '.agents/skills.json' : '.claude/skills/demo/SKILL.md';
|
|
97
97
|
if (!link(t, root, 'missing', path)) return;
|
|
98
|
-
if (source === 'derived' || source === 'outputs') put(root, '.pbai4devs/
|
|
99
|
-
else put(root, '.pbai4devs/
|
|
98
|
+
if (source === 'derived' || source === 'outputs') put(root, '.pbai4devs/state/derived.json', JSON.stringify({ schemaVersion: 1, entries: source === 'derived' ? { [path]: 'a'.repeat(64) } : {}, ...(source === 'outputs' ? { outputs: { [path]: 'a'.repeat(64) } } : {}) }));
|
|
99
|
+
else put(root, '.pbai4devs/state/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: source === 'files' ? { [path]: 'a'.repeat(64) } : {}, blocks: source === 'blocks' ? { [path]: 'a'.repeat(64) } : {} }));
|
|
100
100
|
const before = snapshot(root);
|
|
101
101
|
assert.throws(() => run(root, { force: true }), /Link simbolico em caminho gerenciado/);
|
|
102
102
|
assert.deepEqual(snapshot(root), before);
|
|
@@ -106,7 +106,7 @@ test('link em caminho registrado continua sendo erro antes de qualquer escrita',
|
|
|
106
106
|
test('manifesto standalone editado pelo usuário passa a ser preservado', t => {
|
|
107
107
|
const root = fixture(t); run(root);
|
|
108
108
|
const path = '.agents/skills.json', original = fs.readFileSync(join(root, path), 'utf8');
|
|
109
|
-
const derived = JSON.parse(fs.readFileSync(join(root, '.pbai4devs/
|
|
109
|
+
const derived = JSON.parse(fs.readFileSync(join(root, '.pbai4devs/state/derived.json')));
|
|
110
110
|
assert.equal(derived.outputs[path], hash(original));
|
|
111
111
|
put(root, path, '{"entries":[{"path":".agents/skills/custom"}]}\n');
|
|
112
112
|
const result = run(root, { force: true });
|
|
@@ -151,7 +151,7 @@ test('tipo alterado em caminho nativo registrado continua recusado antes de grav
|
|
|
151
151
|
]) {
|
|
152
152
|
const root = fixture(t);
|
|
153
153
|
if (path === '.codex') put(root, path, 'Human root\n'); else put(root, `${path}/keep.txt`, 'Human directory\n');
|
|
154
|
-
put(root, '.pbai4devs/
|
|
154
|
+
put(root, '.pbai4devs/state/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: { [owned]: 'a'.repeat(64) }, blocks: {} }));
|
|
155
155
|
const before = snapshot(root);
|
|
156
156
|
assert.throws(() => run(root, { force: true }), /caminho gerenciado/);
|
|
157
157
|
assert.deepEqual(snapshot(root), before);
|
|
@@ -191,7 +191,7 @@ test('variante única de entrada registrada conserva conteúdo como arquivo mist
|
|
|
191
191
|
for (const owned of ['AGENTS.md', 'agents.md']) {
|
|
192
192
|
const root = fixture(t); put(root, 'agents.md', '# Renamed human entry\n');
|
|
193
193
|
assert.throws(() => nativePathBoundary(root, 'AGENTS.md', [owned], { preserveRootAliases: true }), /maiusculas\/minusculas/);
|
|
194
|
-
put(root, '.pbai4devs/
|
|
194
|
+
put(root, '.pbai4devs/state/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: { [owned]: 'a'.repeat(64) }, blocks: {} }));
|
|
195
195
|
run(root, { force: true });
|
|
196
196
|
assert.ok(fs.readFileSync(join(root, 'agents.md'), 'utf8').startsWith('# Renamed human entry\n'));
|
|
197
197
|
assert.ok(!fs.readdirSync(root).includes('AGENTS.md'));
|
|
@@ -64,7 +64,7 @@ test('texto humano com formato de referência nunca comprova propriedade', t =>
|
|
|
64
64
|
test('hash registrado comprova referência legada sem marcador e diretório vazio humano permanece', t => {
|
|
65
65
|
const root = fixture(t), stale = '.claude/skills/legacy.md';
|
|
66
66
|
write(root, stale, human);
|
|
67
|
-
write(root, '.pbai4devs/
|
|
67
|
+
write(root, '.pbai4devs/state/agentdevkit.json', JSON.stringify({ schemaVersion: 1, clients: ['claude'], files: { [stale]: hash(human) }, blocks: {} }));
|
|
68
68
|
fs.mkdirSync(join(root, '.claude/skills/user-empty'), { recursive: true });
|
|
69
69
|
run(root);
|
|
70
70
|
assert.equal(fs.existsSync(join(root, stale)), false);
|
|
@@ -38,7 +38,7 @@ test('sync complementa Markdown humano, preserva CRLF/metadados e acompanha só
|
|
|
38
38
|
assert.equal(fs.statSync(join(root, path)).mode & 0o777, mode);
|
|
39
39
|
assert.equal(result.managedBlocks[path], managedBlockDigest(actual));
|
|
40
40
|
assert.ok(!result.ownedOutputs.includes(path));
|
|
41
|
-
const state = JSON.parse(read(root, '.pbai4devs/
|
|
41
|
+
const state = JSON.parse(read(root, '.pbai4devs/state/derived.json'));
|
|
42
42
|
assert.equal(state.blocks[path], managedBlockDigest(actual));
|
|
43
43
|
assert.equal(state.files[path], undefined);
|
|
44
44
|
assert.equal(run(root, { check: true }).exitCode, 0);
|
|
@@ -84,7 +84,7 @@ test('remover recurso ou cliente retira somente bloco próprio de Markdown misto
|
|
|
84
84
|
run(root);
|
|
85
85
|
assert.equal(read(root, path), outside);
|
|
86
86
|
assert.ok(read(root, path).startsWith(human));
|
|
87
|
-
assert.equal(JSON.parse(read(root, '.pbai4devs/
|
|
87
|
+
assert.equal(JSON.parse(read(root, '.pbai4devs/state/derived.json')).blocks?.[path], undefined);
|
|
88
88
|
assert.equal(run(root, { check: true }).exitCode, 0);
|
|
89
89
|
}
|
|
90
90
|
});
|
|
@@ -130,10 +130,10 @@ test('retirada standalone registra tombstone para bloco do estado instalado', t
|
|
|
130
130
|
const root = fixture(t), path = '.claude/skills/demo/SKILL.md';
|
|
131
131
|
put(root, path, '# Human\n'); run(root);
|
|
132
132
|
const block = managedBlockDigest(read(root, path));
|
|
133
|
-
put(root, '.pbai4devs/
|
|
133
|
+
put(root, '.pbai4devs/state/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: {}, blocks: { [path]: block } }));
|
|
134
134
|
fs.rmSync(join(root, '.pbai4devs/skills/old/demo'), { recursive: true });
|
|
135
135
|
run(root);
|
|
136
|
-
const derived = JSON.parse(read(root, '.pbai4devs/
|
|
136
|
+
const derived = JSON.parse(read(root, '.pbai4devs/state/derived.json'));
|
|
137
137
|
assert.ok(derived.removedBlocks.includes(path));
|
|
138
138
|
assert.equal(extractManagedBlock(read(root, path)), null);
|
|
139
139
|
assert.equal(run(root, { check: true }).exitCode, 0);
|
|
@@ -143,7 +143,7 @@ test('stage pode gerar referências sem gravar estado derivado', t => {
|
|
|
143
143
|
const root = fixture(t);
|
|
144
144
|
run(root, { generateEntrypoints: false, recordDerivedState: false });
|
|
145
145
|
assert.ok(fs.existsSync(join(root, '.claude/skills/demo/SKILL.md')));
|
|
146
|
-
assert.ok(!fs.existsSync(join(root, '.pbai4devs/
|
|
146
|
+
assert.ok(!fs.existsSync(join(root, '.pbai4devs/state/derived.json')));
|
|
147
147
|
});
|
|
148
148
|
|
|
149
149
|
test('regra Cursor mantém globs e modo humanos ao receber referência', t => {
|
|
@@ -292,10 +292,10 @@ test('arquivo misto removido é recriado sem conservar propriedade do bloco ause
|
|
|
292
292
|
const root = fixture(t);
|
|
293
293
|
put(root, path, '# Humano\n'); run(root);
|
|
294
294
|
const hash = managedBlockDigest(read(root, path));
|
|
295
|
-
put(root, '.pbai4devs/
|
|
295
|
+
put(root, '.pbai4devs/state/agentdevkit.json', JSON.stringify({ schemaVersion: 1, clients: ['claude'], files: {}, blocks: { [path]: hash } }));
|
|
296
296
|
fs.rmSync(join(root, path));
|
|
297
297
|
run(root);
|
|
298
|
-
const derived = JSON.parse(read(root, '.pbai4devs/
|
|
298
|
+
const derived = JSON.parse(read(root, '.pbai4devs/state/derived.json'));
|
|
299
299
|
assert.ok(derived.removedBlocks.includes(path));
|
|
300
300
|
assert.ok(derived.files?.[path] || derived.entries?.[path]);
|
|
301
301
|
assert.equal(extractManagedBlock(read(root, path)), null);
|
|
@@ -307,10 +307,10 @@ test('renomear caixa de entrada mista transfere só a propriedade do bloco', t =
|
|
|
307
307
|
const root = fixture(t), before = '# Humano\n';
|
|
308
308
|
put(root, 'AGENTS.md', before); run(root);
|
|
309
309
|
const hash = managedBlockDigest(read(root, 'AGENTS.md'));
|
|
310
|
-
put(root, '.pbai4devs/
|
|
310
|
+
put(root, '.pbai4devs/state/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: {}, blocks: { 'AGENTS.md': hash } }));
|
|
311
311
|
fs.renameSync(join(root, 'AGENTS.md'), join(root, 'agents.md'));
|
|
312
312
|
const result = run(root);
|
|
313
|
-
const derived = JSON.parse(read(root, '.pbai4devs/
|
|
313
|
+
const derived = JSON.parse(read(root, '.pbai4devs/state/derived.json'));
|
|
314
314
|
assert.ok(read(root, 'agents.md').startsWith(before));
|
|
315
315
|
assert.ok(derived.removedBlocks.includes('AGENTS.md'));
|
|
316
316
|
assert.equal(derived.blocks['agents.md'], result.managedBlocks['agents.md']);
|
|
@@ -14,6 +14,7 @@ import './HUMAN_OWNERSHIP.mjs';
|
|
|
14
14
|
import './COEXISTENCE.mjs';
|
|
15
15
|
import './MANAGED_CONTENT.mjs';
|
|
16
16
|
import { validateCatalog as validateExternalCatalog } from '../../gerenciar-skills-externas/SCRIPT.mjs';
|
|
17
|
+
import { stateDirectory } from '../../lib/state-path.mjs';
|
|
17
18
|
|
|
18
19
|
const project = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
19
20
|
const cli = resolve(dirname(fileURLToPath(import.meta.url)), '../SCRIPT.mjs');
|
|
@@ -24,7 +25,7 @@ const read = (root, path) => readFileSync(join(root, path), 'utf8');
|
|
|
24
25
|
const write = (root, path, content) => { mkdirSync(dirname(join(root, path)), { recursive: true }); writeFileSync(join(root, path), content); };
|
|
25
26
|
const pointer = (origin = 'origem.md', title = 'Referencia') => `---\ndescription: Referencia.\n---\n\n# ${title}\n\n${marker}\nLeia e siga [origem](${origin}).\n`;
|
|
26
27
|
const recordOwned = (root, paths) => {
|
|
27
|
-
const statePath = '.pbai4devs/
|
|
28
|
+
const statePath = '.pbai4devs/state/agentdevkit.json';
|
|
28
29
|
const previous = existsSync(join(root, statePath)) ? JSON.parse(read(root, statePath)) : {};
|
|
29
30
|
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
31
|
};
|
|
@@ -75,7 +76,7 @@ function fixture(t, minimal = false) {
|
|
|
75
76
|
write(root, '.github/prompts/custom.prompt.md', '# Prompt proprio\n');
|
|
76
77
|
write(root, '.claude/notas.md', '# Nota propria\n');
|
|
77
78
|
write(root, 'README.md', 'README raiz preservado\n');
|
|
78
|
-
for (const kind of ['templates', 'scripts', 'tools', 'external-skills', '
|
|
79
|
+
for (const kind of ['templates', 'scripts', 'tools', 'external-skills', 'state', 'tests', 'docs', 'adapters', 'extra']) {
|
|
79
80
|
write(root, `.pbai4devs/${kind}/demo/REFERENCE.md`, '---\ndescription: Exclusivo.\n---\n\n# Exclusivo\n');
|
|
80
81
|
if (kind !== 'extra') {
|
|
81
82
|
const target = `.claude/${kind}/demo/REFERENCE.md`;
|
|
@@ -92,6 +93,30 @@ function fixture(t, minimal = false) {
|
|
|
92
93
|
recordOwned(root, ownedLegacy);
|
|
93
94
|
return root;
|
|
94
95
|
}
|
|
96
|
+
|
|
97
|
+
test('sincronização direta conserva estado legado e recusa duas pastas de estado', t => {
|
|
98
|
+
const root = fixture(t, true);
|
|
99
|
+
write(root, source, skill);
|
|
100
|
+
write(root, '.pbai4devs/estado/agentdevkit.json', JSON.stringify({
|
|
101
|
+
schemaVersion: 1, clients: ['codex'], files: {}, blocks: {},
|
|
102
|
+
}));
|
|
103
|
+
assert.equal(run(root).exitCode, 0);
|
|
104
|
+
assert.equal(existsSync(join(root, '.pbai4devs/estado/derived.json')), true);
|
|
105
|
+
assert.equal(existsSync(join(root, '.pbai4devs/state')), false);
|
|
106
|
+
write(root, '.pbai4devs/state/configuracao/ESTADO.md', '# Decisões próprias\n');
|
|
107
|
+
const before = snapshot(root);
|
|
108
|
+
assert.throws(() => run(root), /coexistem/);
|
|
109
|
+
assert.deepEqual(snapshot(root), before);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('detecção de estado recusa link simbólico sem abrir o destino', { skip: process.platform === 'win32' }, t => {
|
|
113
|
+
const root = fixture(t, true);
|
|
114
|
+
const external = join(root, 'externo');
|
|
115
|
+
mkdirSync(external);
|
|
116
|
+
symlinkSync(external, join(root, '.pbai4devs/estado'), 'dir');
|
|
117
|
+
assert.throws(() => stateDirectory(root), /Link simbolico/);
|
|
118
|
+
assert.equal(existsSync(join(root, '.pbai4devs/state')), false);
|
|
119
|
+
});
|
|
95
120
|
function rejected(root, options = {}, pattern) {
|
|
96
121
|
const before = snapshot(root);
|
|
97
122
|
assert.throws(() => run(root, options), pattern);
|
|
@@ -115,7 +140,7 @@ test('sincroniza somente principais, preserva metadados e usa referências UTF-8
|
|
|
115
140
|
assert.match(read(root, '.kimi-code/agents/revisor.md'), /tools: \[Read, Grep, Glob\]/);
|
|
116
141
|
assert.ok(existsSync(join(root, '.claude/skills/projeto/demo/SKILL.md')));
|
|
117
142
|
for (const client of ['.claude', '.opencode', '.github', '.codex', '.cursor', '.kimi-code', '.vscode', '.agents', '.agent', '.gemini']) {
|
|
118
|
-
|
|
143
|
+
for (const kind of ['templates', 'scripts', 'tools', 'external-skills', 'state', 'estado', 'tests', 'docs', 'adapters', 'extra', 'README.md', 'INSTRUCTIONS.md']) assert.equal(existsSync(join(root, client, kind)), false, `${client}/${kind}`);
|
|
119
144
|
}
|
|
120
145
|
assert.deepEqual(JSON.parse(read(root, '.pbai4devs/agents.json')).entries, [{ path: '.pbai4devs/agents/revisor/AGENT.md' }]);
|
|
121
146
|
assert.ok(JSON.parse(read(root, '.pbai4devs/skills.json')).entries.some(entry => entry.path === '.pbai4devs/skills/grupo com espaço/outro'));
|
|
@@ -589,12 +614,12 @@ test('entradas da CLI mantêm ownership integral e recursos têm proveniência d
|
|
|
589
614
|
write(root, source, skill);
|
|
590
615
|
write(root, '.pbai4devs/templates/entrypoints/AGENTS.md', template);
|
|
591
616
|
write(root, 'AGENTS.md', entry);
|
|
592
|
-
write(root, '.pbai4devs/
|
|
617
|
+
write(root, '.pbai4devs/state/agentdevkit.json', JSON.stringify({ clients: ['codex'], files: { 'AGENTS.md': createHash('sha256').update(entry).digest('hex') } }));
|
|
593
618
|
const result = run(root, { generateEntrypoints: true });
|
|
594
619
|
assert.ok(result.ownedOutputs.includes('AGENTS.md'));
|
|
595
620
|
assert.equal(read(root, 'AGENTS.md'), entry);
|
|
596
|
-
assert.equal(JSON.parse(read(root, '.pbai4devs/
|
|
597
|
-
assert.ok(JSON.parse(read(root, '.pbai4devs/
|
|
621
|
+
assert.equal(JSON.parse(read(root, '.pbai4devs/state/derived.json')).entries['AGENTS.md'], undefined);
|
|
622
|
+
assert.ok(JSON.parse(read(root, '.pbai4devs/state/derived.json')).files['.codex/skills/demo/SKILL.md']);
|
|
598
623
|
assertCheckReadonly(root, { generateEntrypoints: true }, 0);
|
|
599
624
|
});
|
|
600
625
|
|
|
@@ -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
|
|
|
@@ -28,7 +28,7 @@ Não apague raízes inteiras: workflows, configurações nativas e a origem fica
|
|
|
28
28
|
fora da limpeza. `--check` só confere, sem corrigir nem criar backups.
|
|
29
29
|
|
|
30
30
|
O manifesto `.pbai4devs/espelhos.conf` registra clientes e formatos nativos.
|
|
31
|
-
Respeite a seleção salva em `
|
|
31
|
+
Respeite a seleção salva em `state/agentdevkit.json`; não reative clientes para
|
|
32
32
|
reparar apontamentos. Para alterá-la, use `agents` na CLI. Pastas compartilhadas
|
|
33
33
|
continuam quando necessárias; arquivos próprios dos clientes são preservados
|
|
34
34
|
pela CLI, inclusive ao reduzir a seleção.
|
|
@@ -6,7 +6,7 @@ description: Cria uma spec de incremento, esclarecendo requisitos e dúvidas com
|
|
|
6
6
|
# Criar spec
|
|
7
7
|
|
|
8
8
|
Antes de escrever, aplique o inventário e plano de `atualizar-documentacao`.
|
|
9
|
-
Leia os caminhos em `.pbai4devs/
|
|
9
|
+
Leia os caminhos em `.pbai4devs/state/configuracao/ESTADO.md`. Exija PRD/design
|
|
10
10
|
aprovados e UX/UI aprovado ou não aplicável com justificativa aprovada.
|
|
11
11
|
Confira evidências, atualidade e decisões essenciais ao incremento; se faltar
|
|
12
12
|
algo, encaminhe à etapa correspondente antes de escrever a spec.
|
|
@@ -5,7 +5,7 @@ description: Planeja e implementa uma spec aprovada, acompanhando tarefas e crit
|
|
|
5
5
|
|
|
6
6
|
# Implementar spec
|
|
7
7
|
|
|
8
|
-
Leia `.pbai4devs/
|
|
8
|
+
Leia `.pbai4devs/state/configuracao/ESTADO.md`. Exija spec, PRD e design aprovados;
|
|
9
9
|
UX/UI aprovado ou não aplicável com justificativa aprovada. Confira evidências,
|
|
10
10
|
atualidade, decisões essenciais e vínculos com o roadmap; resolva pendências
|
|
11
11
|
antes de implementar.
|
|
@@ -15,5 +15,5 @@ cliente em `.pbai4devs/COMPATIBILIDADE.md`.
|
|
|
15
15
|
|
|
16
16
|
Use o `README.md` do projeto para seu contexto. Os clientes escolhidos têm
|
|
17
17
|
apontamentos; scripts, ferramentas e modelos permanecem na origem compartilhada.
|
|
18
|
-
Respeite a seleção em `.pbai4devs/
|
|
18
|
+
Respeite a seleção em `.pbai4devs/state/agentdevkit.json`. Para alterá-la, use
|
|
19
19
|
`agents` na CLI; não deduza o agente ativo pelo aplicativo ou pelas pastas.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,11 @@ description: Alterações verificáveis por versão do AgentDevKit.
|
|
|
4
4
|
|
|
5
5
|
# Changelog
|
|
6
6
|
|
|
7
|
+
## 0.0.17 — 2026-11-13
|
|
8
|
+
|
|
9
|
+
- A pasta compartilhada de estado passa a se chamar `.pbai4devs/state`.
|
|
10
|
+
- Instalações existentes migram os dados sem alterar o índice ou o ignore.
|
|
11
|
+
|
|
7
12
|
## 0.0.16 — 2026-11-13
|
|
8
13
|
|
|
9
14
|
- Testes portáteis distribuídos entre jobs para Windows e macOS.
|
|
@@ -73,11 +73,19 @@ Os backups são locais e ignorados; não substituem o controle de versão.
|
|
|
73
73
|
|
|
74
74
|
## Estado ou apontamentos inconsistentes
|
|
75
75
|
|
|
76
|
+
Se a instalação ainda usa `.pbai4devs/estado`, execute `agentdevkit update`
|
|
77
|
+
para migrar os dados e atualizar os scripts juntos. Se `estado` e `state`
|
|
78
|
+
coexistirem, compare e preserve as duas pastas antes de resolver o conflito;
|
|
79
|
+
o kit não escolhe nem sobrescreve uma delas automaticamente.
|
|
80
|
+
Se a configuração apontar para caminhos dentro da pasta antiga, atualize
|
|
81
|
+
esses caminhos para `.pbai4devs/state` antes de tentar novamente. O kit não
|
|
82
|
+
reescreve as decisões do projeto.
|
|
83
|
+
|
|
76
84
|
`missing`, `modified` e `outdated` em `issues` indicam arquivo ausente,
|
|
77
85
|
conteúdo alterado ou derivação pendente. Use `sync --check` para inspecionar;
|
|
78
86
|
se as alterações da origem forem intencionais, execute `sync` e confira de novo.
|
|
79
87
|
|
|
80
|
-
Se `.pbai4devs/
|
|
88
|
+
Se `.pbai4devs/state/agentdevkit.json` estiver inválido, recupere um backup
|
|
81
89
|
local coerente com os arquivos correspondentes. O estado pessoal não é
|
|
82
90
|
versionado. Não invente hashes nem apague o estado para ocultar conflitos;
|
|
83
91
|
peça ao agente para comparar as fontes e planejar o reparo com `init`/`sync`.
|
package/docs/guides/workflow.md
CHANGED
|
@@ -56,7 +56,7 @@ Neste momento quero apenas preparar os agentes; não crie produto ou aplicação
|
|
|
56
56
|
|
|
57
57
|
Quando o produto começar, combine os caminhos dos documentos no estado de
|
|
58
58
|
configuração. Não presuma que todo projeto usa as mesmas pastas de documentos.
|
|
59
|
-
O registro é `.pbai4devs/
|
|
59
|
+
O registro é `.pbai4devs/state/configuracao/ESTADO.md`.
|
|
60
60
|
|
|
61
61
|
## 3. Definir produto e experiência
|
|
62
62
|
|
package/docs/specs/README.md
CHANGED
|
@@ -20,6 +20,7 @@ description: Incrementos verificáveis do AgentDevKit.
|
|
|
20
20
|
- [Ecossistema de desenvolvimento](development-ecosystem/SPEC.md):
|
|
21
21
|
especialistas, colaboração e provas.
|
|
22
22
|
- [Confiabilidade da pipeline](ci-resilience/SPEC.md): testes e releases.
|
|
23
|
+
- [Pasta de estado](state-directory/SPEC.md): migração segura para `state`.
|
|
23
24
|
- [Integração sem substituição][additive]: acréscimos em arquivos existentes.
|
|
24
25
|
- [Compatibilidade com projetos reais][oss]: projetos públicos e exemplos reproduzíveis.
|
|
25
26
|
- [Ignore do projeto](project-ignore/SPEC.md): preservar o arquivo da raiz.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Nome em inglês para a pasta de estado compartilhado.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Pasta de estado
|
|
6
|
+
|
|
7
|
+
A pasta de primeiro nível passa de `.pbai4devs/estado` para
|
|
8
|
+
`.pbai4devs/state`. Ela contém estado de instalação e configuração, não
|
|
9
|
+
memória de conversas. Os nomes internos permanecem iguais.
|
|
10
|
+
|
|
11
|
+
## Critérios de aceite
|
|
12
|
+
|
|
13
|
+
- AC-001: Instalações novas usam `state` e recursos, documentação, exclusões
|
|
14
|
+
e pacote apontam para ela, sem expor estado local no pacote ou no Git.
|
|
15
|
+
- AC-002: A atualização de instalações antigas preserva seleção, idioma,
|
|
16
|
+
decisões, conteúdo humano e permissões; conflitos e links inseguros não
|
|
17
|
+
causam perda ou sobrescrita silenciosa.
|
|
18
|
+
- AC-003: Simulação, repetição, falhas e recuperação preservam arquivos,
|
|
19
|
+
índice Git e `.gitignore` do projeto. Scripts independentes identificam
|
|
20
|
+
instalações antigas e novas de forma coerente.
|
|
21
|
+
- AC-004: Testes locais, CI em Linux, Windows e macOS e revisão independente
|
|
22
|
+
verificam o comportamento e os limites da migração.
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { resolve, join } from 'node:path';
|
|
3
3
|
import { createInterface } from 'node:readline/promises';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import { packageInfo } from './config.mjs';
|
|
5
|
+
import { packageInfo, statePath, legacyStatePath } from './config.mjs';
|
|
6
6
|
import { text, normalizeLanguage, explainError } from './i18n.mjs';
|
|
7
7
|
import { resolveTarget, loadState, planInstallation, applyPlan, inspect } from './install.mjs';
|
|
8
8
|
import { getNextSteps } from './next.mjs';
|
|
@@ -169,7 +169,7 @@ export async function main(argv = process.argv.slice(2), context = {}) {
|
|
|
169
169
|
}
|
|
170
170
|
print(result, options, output); return exitCode;
|
|
171
171
|
} catch (error) {
|
|
172
|
-
const result = { ok: false, command: options.command || 'help', version: packageInfo.version, language, clients, target, message: explainError(language, error.code), error: { code: error.code || 'E_OPERATION', detail: error.message }, next: getNextSteps({ command: 'error', language, target, installed: existsSync(join(target,
|
|
172
|
+
const result = { ok: false, command: options.command || 'help', version: packageInfo.version, language, clients, target, message: explainError(language, error.code), error: { code: error.code || 'E_OPERATION', detail: error.message }, next: getNextSteps({ command: 'error', language, target, installed: existsSync(join(target, statePath)) || existsSync(join(target, legacyStatePath)) }) };
|
|
173
173
|
if (!options.json) result.message += ` (${error.code || 'E_OPERATION'})`;
|
|
174
174
|
print(result, options, output); return error.exitCode || 1;
|
|
175
175
|
}
|
package/src/config.mjs
CHANGED
|
@@ -5,7 +5,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
export const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
6
6
|
export const frameworkRoot = resolve(packageRoot, '.pbai4devs');
|
|
7
7
|
export const packageInfo = JSON.parse(readFileSync(resolve(packageRoot, 'package.json'), 'utf8'));
|
|
8
|
-
export const statePath = '.pbai4devs/
|
|
8
|
+
export const statePath = '.pbai4devs/state/agentdevkit.json';
|
|
9
|
+
export const legacyStatePath = '.pbai4devs/estado/agentdevkit.json';
|
|
9
10
|
export const entryNames = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'];
|
|
10
11
|
export const begin = '<!-- BEGIN AGENTDEVKIT -->';
|
|
11
12
|
export const end = '<!-- END AGENTDEVKIT -->';
|
package/src/i18n.mjs
CHANGED
|
@@ -42,6 +42,21 @@ const messages = {
|
|
|
42
42
|
};
|
|
43
43
|
export const text = (language, key) => messages[language || 'pt'][key] || key;
|
|
44
44
|
export function explainError(language, code) {
|
|
45
|
+
if (code === 'E_CONFIG_PARSE') return {
|
|
46
|
+
pt: 'Não foi possível analisar a configuração legada com segurança; revise o frontmatter e as aspas antes de executar update.',
|
|
47
|
+
en: 'The legacy configuration could not be safely parsed; review its frontmatter and quoted values before running update.',
|
|
48
|
+
es: 'No se pudo analizar la configuración anterior de forma segura; revise el frontmatter y las comillas antes de ejecutar update.',
|
|
49
|
+
}[language || 'pt'];
|
|
50
|
+
if (code === 'E_LEGACY_PATH') return {
|
|
51
|
+
pt: 'Ajuste os caminhos .pbai4devs/estado para .pbai4devs/state na configuração antes de executar update.',
|
|
52
|
+
en: 'Change .pbai4devs/estado paths to .pbai4devs/state in the configuration before running update.',
|
|
53
|
+
es: 'Cambie las rutas .pbai4devs/estado por .pbai4devs/state en la configuración antes de ejecutar update.',
|
|
54
|
+
}[language || 'pt'];
|
|
55
|
+
if (code === 'E_UPGRADE') return {
|
|
56
|
+
pt: 'Execute agentdevkit update antes de sincronizar ou alterar a configuração legada.',
|
|
57
|
+
en: 'Run agentdevkit update before syncing or changing the legacy configuration.',
|
|
58
|
+
es: 'Ejecute agentdevkit update antes de sincronizar o cambiar la configuración anterior.',
|
|
59
|
+
}[language || 'pt'];
|
|
45
60
|
const value = {
|
|
46
61
|
pt: { E_ARGS: 'Argumentos inválidos; consulte --help.', E_STATE: 'O estado do AgentDevKit está inválido.', E_SYMLINK: 'Um link simbólico impede a operação segura.', E_TARGET: 'Escolha uma pasta real e segura como destino.', E_PATH: 'Um caminho gerenciado é inválido.', E_BLOCK: 'Os marcadores do bloco gerenciado estão inválidos.', E_CHANGED: 'Um arquivo mudou depois do planejamento; execute novamente.', E_TEMPLATE: 'Um template necessário está inválido.' },
|
|
47
62
|
en: { E_ARGS: 'Invalid arguments; see --help.', E_STATE: 'AgentDevKit state is invalid.', E_SYMLINK: 'A symbolic link prevents this operation.', E_TARGET: 'Choose a real, safe target directory.', E_PATH: 'A managed path is invalid.', E_BLOCK: 'Managed block markers are invalid.', E_CHANGED: 'A file changed after planning; try again.', E_TEMPLATE: 'A required template is invalid.' },
|
package/src/install.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { chmodSync, existsSync, lstatSync, readFileSync, writeFileSync, readdirSync, mkdirSync, mkdtempSync, realpathSync, renameSync, rmSync, rmdirSync } from 'node:fs';
|
|
2
|
+
import { chmodSync, closeSync, existsSync, fstatSync, lstatSync, openSync, readFileSync, readSync, writeFileSync, readdirSync, mkdirSync, mkdtempSync, realpathSync, renameSync, rmSync, rmdirSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
5
|
import { synchronize, isReference } from '../.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs';
|
|
@@ -8,7 +8,7 @@ import { nativePathBoundary } from '../.pbai4devs/scripts/lib/native-paths.mjs';
|
|
|
8
8
|
import { canonicalEntrypoint, resolveEntrypoint, entrypointReference, renderEntrypointImports, extractManagedBlock, managedBlockDigest, mergeManagedContent, stripManagedContent } from '../.pbai4devs/scripts/lib/managed-content.mjs';
|
|
9
9
|
import { extractTomlManagedBlock, tomlManagedBlockDigest, mergeTomlContent, stripTomlContent } from '../.pbai4devs/scripts/lib/managed-toml.mjs';
|
|
10
10
|
import { writeAdapterManifest, adapterRoots, normalizeClients, resolveAdapters } from './adapters/index.mjs';
|
|
11
|
-
import { packageRoot, packageInfo, statePath, entryNames, begin, end } from './config.mjs';
|
|
11
|
+
import { packageRoot, packageInfo, statePath, legacyStatePath, entryNames, begin, end } from './config.mjs';
|
|
12
12
|
import { normalizeLanguage, text } from './i18n.mjs';
|
|
13
13
|
|
|
14
14
|
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
@@ -54,11 +54,11 @@ export function safePath(root, path) {
|
|
|
54
54
|
|
|
55
55
|
function resourceAllowed(path, directory = false) {
|
|
56
56
|
const parts = path.split('/');
|
|
57
|
-
const folders = new Set(['agents', 'commands', 'external-skills', 'rules', 'scripts', 'skills', 'templates', 'tools', '
|
|
57
|
+
const folders = new Set(['agents', 'commands', 'external-skills', 'rules', 'scripts', 'skills', 'templates', 'tools', 'state']);
|
|
58
58
|
const files = new Set(['README.md', 'INSTRUCTIONS.md', 'COMPATIBILIDADE.md', 'LICENSE', '.gitignore']);
|
|
59
59
|
if (!folders.has(parts[0]) && !(parts.length === 1 && files.has(parts[0]))) return false;
|
|
60
|
-
if (parts[0] === '
|
|
61
|
-
if (isGeneratedOutput(path, directory) || parts.some(part => ignored.has(part)) || parts[0] === 'adapters' || path === '
|
|
60
|
+
if (parts[0] === 'state') return directory && parts.length === 1 || path === 'state/README.md';
|
|
61
|
+
if (isGeneratedOutput(path, directory) || parts.some(part => ignored.has(part)) || parts[0] === 'adapters' || path === 'state/agentdevkit.json' || path === '.npmignore') return false;
|
|
62
62
|
if (parts[0] === 'tools') return parts.length === 1 || (directory && parts.length === 2 && !['bin', 'npm-cache'].includes(parts[1])) || (parts.length === 2 && parts[1] === 'README.md') || (parts.length === 3 && ['TOOL.json', 'SCRIPT.mjs', 'README.md'].includes(parts[2]));
|
|
63
63
|
if (parts[0] === 'external-skills') return parts.length === 1 || (parts.length === 2 && ['README.md', 'CATALOG.json'].includes(parts[1]));
|
|
64
64
|
return true;
|
|
@@ -109,6 +109,7 @@ function read(root, path) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
function allowedManaged(path) {
|
|
112
|
+
if (path === '.pbai4devs/estado/README.md') return true;
|
|
112
113
|
if (canonicalEntrypoint(path) || path === '.gitignore') return true;
|
|
113
114
|
if (nativeMarkdown(path) || nativeToml(path)) return true;
|
|
114
115
|
if (['catalog.json', 'skills.json', 'agents.json', 'rules.json', 'espelhos.conf'].some(name => path === `.pbai4devs/${name}`)) return true;
|
|
@@ -122,7 +123,7 @@ function nativeMarkdown(path) {
|
|
|
122
123
|
&& adapterRoots.some(root => {
|
|
123
124
|
if (!path.startsWith(`${root}/`)) return false;
|
|
124
125
|
const relative = path.slice(root.length + 1).split('/');
|
|
125
|
-
return !['tools', 'scripts', 'templates', 'external-skills', 'estado', 'tests', 'docs', 'adapters', 'workflows'].includes(relative[0].toLowerCase())
|
|
126
|
+
return !['tools', 'scripts', 'templates', 'external-skills', 'estado', 'state', 'tests', 'docs', 'adapters', 'workflows'].includes(relative[0].toLowerCase())
|
|
126
127
|
&& !['readme.md', 'instructions.md'].includes(relative.at(-1).toLowerCase());
|
|
127
128
|
});
|
|
128
129
|
}
|
|
@@ -130,7 +131,7 @@ const nativeToml = path => path.startsWith('.gemini/commands/') && path.endsWith
|
|
|
130
131
|
const nativeManagedText = path => nativeMarkdown(path) || nativeToml(path);
|
|
131
132
|
|
|
132
133
|
export function loadState(root) {
|
|
133
|
-
const content = read(root,
|
|
134
|
+
const content = read(root, actualStatePath(root)); if (content === null) return null;
|
|
134
135
|
let state; try { state = JSON.parse(content); } catch { fail('E_STATE', 'Invalid AgentDevKit state JSON.'); }
|
|
135
136
|
if (state?.schemaVersion !== 1 || !normalizeLanguage(state.language) || typeof state.version !== 'string' || !state.files || typeof state.files !== 'object' || Array.isArray(state.files) || !state.blocks || typeof state.blocks !== 'object' || Array.isArray(state.blocks)) fail('E_STATE', 'Invalid AgentDevKit state.');
|
|
136
137
|
if (state.clients !== undefined) {
|
|
@@ -144,6 +145,81 @@ export function loadState(root) {
|
|
|
144
145
|
return state;
|
|
145
146
|
}
|
|
146
147
|
|
|
148
|
+
function actualStatePath(root) {
|
|
149
|
+
const legacy = stat(safePath(root, '.pbai4devs/estado'));
|
|
150
|
+
const current = stat(safePath(root, '.pbai4devs/state'));
|
|
151
|
+
if (legacy && !legacy.isDirectory() || current && !current.isDirectory()) fail('E_STATE', 'AgentDevKit state must be a directory.');
|
|
152
|
+
if (legacy && current) fail('E_STATE', 'Both .pbai4devs/estado and .pbai4devs/state exist; preserve both and resolve the conflict.');
|
|
153
|
+
return legacy ? legacyStatePath : statePath;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function readLegacyConfiguration(root, path) {
|
|
157
|
+
const limit = 4 * 1024 * 1024, fd = openSync(safePath(root, path), 'r');
|
|
158
|
+
try {
|
|
159
|
+
if (!fstatSync(fd).isFile() || fstatSync(fd).size > limit) fail('E_STATE', 'Legacy configuration is too large or not a regular file.');
|
|
160
|
+
const buffer = Buffer.allocUnsafe(limit + 1);
|
|
161
|
+
let total = 0, count;
|
|
162
|
+
while (total < buffer.length && (count = readSync(fd, buffer, total, buffer.length - total, null)) > 0) total += count;
|
|
163
|
+
if (total > limit) fail('E_STATE', 'Legacy configuration is too large to inspect safely.');
|
|
164
|
+
return buffer.subarray(0, total);
|
|
165
|
+
} finally { closeSync(fd); }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function assertNoLegacyReferences(value) {
|
|
169
|
+
const legacy = /\.pbai4devs\/estado(?=$|\/|[\s"'\],}:#])/i;
|
|
170
|
+
const reject = () => fail('E_LEGACY_PATH', 'Replace .pbai4devs/estado references with .pbai4devs/state in the legacy configuration before running update.');
|
|
171
|
+
if (legacy.test(value)) reject();
|
|
172
|
+
const normalized = value.replaceAll('\r', '');
|
|
173
|
+
const header = normalized.match(/^---\n([\s\S]*?)\n---(?:\n|$)/)?.[1];
|
|
174
|
+
if (header === undefined) {
|
|
175
|
+
if (normalized.startsWith('---')) fail('E_CONFIG_PARSE', 'Legacy configuration has an incomplete frontmatter block.');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
let documents = false;
|
|
179
|
+
for (const line of header.split('\n')) {
|
|
180
|
+
const top = line.match(/^([a-z_]+):\s*(.*)$/);
|
|
181
|
+
if (top) documents = top[1] === 'documentos';
|
|
182
|
+
const nested = !top && documents ? line.match(/^ ([a-z_]+):\s*(.*)$/) : null;
|
|
183
|
+
const raw = (top?.[2] ?? nested?.[2])?.trim();
|
|
184
|
+
if (!raw?.startsWith('"')) continue;
|
|
185
|
+
let decoded;
|
|
186
|
+
try { decoded = JSON.parse(raw); }
|
|
187
|
+
catch { fail('E_CONFIG_PARSE', 'Legacy configuration contains an ambiguous quoted scalar.'); }
|
|
188
|
+
if (typeof decoded !== 'string') fail('E_CONFIG_PARSE', 'Legacy configuration contains an invalid quoted scalar.');
|
|
189
|
+
if (legacy.test(decoded)) reject();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function legacyStateMigration(root) {
|
|
194
|
+
if (actualStatePath(root) !== legacyStatePath) return null;
|
|
195
|
+
const base = '.pbai4devs/estado', files = [], directories = [];
|
|
196
|
+
const visit = folder => {
|
|
197
|
+
const info = stat(safePath(root, folder));
|
|
198
|
+
if (!info?.isDirectory()) fail('E_STATE', `Invalid state directory: ${folder}`);
|
|
199
|
+
const names = readdirSync(safePath(root, folder)).sort();
|
|
200
|
+
directories.push({ path: folder, mode: info.mode & 0o777, names });
|
|
201
|
+
for (const name of names) {
|
|
202
|
+
const path = `${folder}/${name}`, item = stat(safePath(root, path));
|
|
203
|
+
if (item?.isDirectory()) visit(path);
|
|
204
|
+
else if (item?.isFile()) {
|
|
205
|
+
const configuration = path === `${base}/configuracao/ESTADO.md`;
|
|
206
|
+
const content = configuration ? readLegacyConfiguration(root, path) : read(root, path);
|
|
207
|
+
if (configuration) {
|
|
208
|
+
if (content.includes(0)) fail('E_STATE', 'Legacy configuration contains binary data.');
|
|
209
|
+
let value;
|
|
210
|
+
try { value = new TextDecoder('utf-8', { fatal: true }).decode(content); }
|
|
211
|
+
catch { fail('E_STATE', 'Legacy configuration is not valid UTF-8.'); }
|
|
212
|
+
assertNoLegacyReferences(value);
|
|
213
|
+
}
|
|
214
|
+
files.push({ path, target: path.replace(`${base}/`, '.pbai4devs/state/'), mode: item.mode & 0o777, content });
|
|
215
|
+
}
|
|
216
|
+
else fail('E_STATE', `Unsupported state entry: ${path}`);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
visit(base);
|
|
220
|
+
return { files, directories };
|
|
221
|
+
}
|
|
222
|
+
|
|
147
223
|
function extractBlock(content, first = begin, last = end) {
|
|
148
224
|
const start = content.indexOf(first), finish = content.indexOf(last);
|
|
149
225
|
if (start < 0 && finish < 0) return null;
|
|
@@ -194,7 +270,7 @@ function copyResources(source, destination) {
|
|
|
194
270
|
|
|
195
271
|
function readDerivedState(target) {
|
|
196
272
|
let entries = {}, outputs = {}, blocks = {}, files = {}, removedBlocks = [];
|
|
197
|
-
const derived = read(target,
|
|
273
|
+
const derived = read(target, actualStatePath(target).replace(/agentdevkit\.json$/, 'derived.json'));
|
|
198
274
|
if (derived) {
|
|
199
275
|
try {
|
|
200
276
|
const value = JSON.parse(derived);
|
|
@@ -216,6 +292,10 @@ function readDerivedState(target) {
|
|
|
216
292
|
|
|
217
293
|
export function planInstallation({ target, language = 'pt', mode = 'init', force = false, clients }) {
|
|
218
294
|
const previous = loadState(target);
|
|
295
|
+
const migration = legacyStateMigration(target);
|
|
296
|
+
// A migration must update the installed scripts and pointers in the same
|
|
297
|
+
// transaction; a plain sync/agents/lang would leave old hard-coded paths.
|
|
298
|
+
if (migration && !['init', 'update'].includes(mode)) fail('E_UPGRADE', 'Run AgentDevKit update before migrating the legacy state directory.');
|
|
219
299
|
if (mode !== 'init' && !previous) fail('E_MISSING', text(language, 'missing'));
|
|
220
300
|
const selected = normalizeClients(clients ?? previous?.clients ?? ['all']);
|
|
221
301
|
const active = resolveAdapters(selected);
|
|
@@ -272,7 +352,7 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
272
352
|
bundled.push('LICENSE');
|
|
273
353
|
}
|
|
274
354
|
const supplied = new Set(bundled);
|
|
275
|
-
const internal = new Set(['catalog.json', 'skills.json', 'agents.json', 'rules.json', 'espelhos.conf', '.gitignore', 'estado/README.md']);
|
|
355
|
+
const internal = new Set(['catalog.json', 'skills.json', 'agents.json', 'rules.json', 'espelhos.conf', '.gitignore', 'state/README.md', 'estado/README.md']);
|
|
276
356
|
if (bundled.length) for (const path of Object.keys(previous?.files || {})) {
|
|
277
357
|
if (path.startsWith('.pbai4devs/') && !supplied.has(path.slice(11)) && !internal.has(path.slice(11))) rmSync(safePath(stage, path), { force: true });
|
|
278
358
|
}
|
|
@@ -302,10 +382,32 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
302
382
|
}
|
|
303
383
|
}
|
|
304
384
|
if (!preserveNative('README.md') && !read(target, 'README.md')) write(stage, 'README.md', `---\ndescription: ${JSON.stringify(text(language, 'readme'))}\nlanguage: ${language}\n---\n\n# ${text(language, 'project')}\n\n${text(language, 'readme')}\n`);
|
|
305
|
-
if (!existsSync(join(stage, '.pbai4devs/
|
|
385
|
+
if (!existsSync(join(stage, '.pbai4devs/state/README.md'))) write(stage, '.pbai4devs/state/README.md', `---\ndescription: AgentDevKit state.\n---\n\n# AgentDevKit\n\n${text(language, 'state')}\n`);
|
|
306
386
|
const unknown = new Set(existing.filter(path => !supplied.has(path) && !previous?.files[`.pbai4devs/${path}`] && !internal.has(path)));
|
|
307
387
|
const paths = walk(stage, '', path => !path.split('/').includes('.agent-tests')).filter(path => !path.startsWith('.pbai4devs/') || !unknown.has(path.slice(11)));
|
|
308
388
|
const desired = new Map(paths.filter(path => !preserveNative(path)).map(path => [path, readFileSync(join(stage, path))]));
|
|
389
|
+
const migratedHuman = new Set();
|
|
390
|
+
for (const item of migration?.files || []) {
|
|
391
|
+
if (item.path === legacyStatePath) continue;
|
|
392
|
+
if (item.path === '.pbai4devs/estado/README.md') {
|
|
393
|
+
const managed = previous?.files?.[item.path] === hash(item.content);
|
|
394
|
+
const matchesTemplate = desired.get(item.target)?.equals(item.content);
|
|
395
|
+
desired.set(item.target, item.content);
|
|
396
|
+
if (!managed || !matchesTemplate) { recordPreserved(item.target, 'existing-file'); migratedHuman.add(item.target); }
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (desired.has(item.target)) fail('E_STATE', `State migration collides with a managed file: ${item.target}`);
|
|
400
|
+
desired.set(item.target, item.content);
|
|
401
|
+
migratedHuman.add(item.target);
|
|
402
|
+
}
|
|
403
|
+
// Preserve human README content; verified managed files may be updated.
|
|
404
|
+
const stateReadme = '.pbai4devs/state/README.md';
|
|
405
|
+
const currentStateReadme = read(target, stateReadme);
|
|
406
|
+
if (currentStateReadme && previous?.files?.[stateReadme] !== hash(currentStateReadme)) {
|
|
407
|
+
desired.set(stateReadme, currentStateReadme);
|
|
408
|
+
migratedHuman.add(stateReadme);
|
|
409
|
+
recordPreserved(stateReadme, 'existing-file');
|
|
410
|
+
}
|
|
309
411
|
for (const [path, content] of desired) {
|
|
310
412
|
if (!nativeManagedText(path) || canonicalEntrypoint(path)) continue;
|
|
311
413
|
const current = read(target, path);
|
|
@@ -330,7 +432,7 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
330
432
|
const manifest = { schemaVersion: 1, version: bundled.length ? packageInfo.version : previous.version, language, files: {}, blocks: {} };
|
|
331
433
|
if (mode !== 'sync' || clients !== undefined || previous?.clients !== undefined) manifest.clients = selected;
|
|
332
434
|
for (const [path, content] of desired) {
|
|
333
|
-
if (path === 'README.md') continue;
|
|
435
|
+
if (path === 'README.md' || migratedHuman.has(path)) continue;
|
|
334
436
|
const block = blockDigest(path, content);
|
|
335
437
|
if (block) manifest.blocks[path] = block;
|
|
336
438
|
else manifest.files[path] = hash(content);
|
|
@@ -348,10 +450,12 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
348
450
|
const conflict = current && path !== statePath && !managed && !appendBlock;
|
|
349
451
|
const changedBlock = current && knownBlocks[path] && !managed;
|
|
350
452
|
if (conflict || changedBlock) conflicts.push(path);
|
|
351
|
-
|
|
453
|
+
const migrated = migration?.files.find(item => item.target === path);
|
|
454
|
+
changes.push({ path, action: current ? 'update' : 'create', content, beforeHash: current ? hash(current) : null, ...(migrated ? { mode: migrated.mode } : {}) });
|
|
352
455
|
}
|
|
353
456
|
const wholeRecords = [previous?.files || {}, derivedEntries, derivedFiles];
|
|
354
457
|
for (const path of new Set(wholeRecords.flatMap(record => Object.keys(record)))) if (!desired.has(path)) {
|
|
458
|
+
if (migration && path.startsWith('.pbai4devs/estado/')) continue;
|
|
355
459
|
if (path === '.gitignore') { recordPreserved(path, 'project-owned'); continue; }
|
|
356
460
|
if (aliasedEntrypoint(path)) continue;
|
|
357
461
|
const current = read(target, path); if (!current) continue;
|
|
@@ -361,6 +465,7 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
361
465
|
changes.push({ path, action: 'remove', content: null, beforeHash: hash(current) });
|
|
362
466
|
}
|
|
363
467
|
for (const [recordedPath, digest] of Object.entries(knownBlocks)) {
|
|
468
|
+
if (migration && recordedPath.startsWith('.pbai4devs/estado/')) continue;
|
|
364
469
|
const path = canonicalEntrypoint(recordedPath) ? entryPaths.get(canonicalEntrypoint(recordedPath)) : recordedPath;
|
|
365
470
|
if (desired.has(path)) continue;
|
|
366
471
|
if (changes.some(change => change.path === path)) continue;
|
|
@@ -410,6 +515,7 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
410
515
|
if (!known) conflicts.push(legacy);
|
|
411
516
|
changes.push({ path: legacy, action: 'remove', content: null, beforeHash: hash(legacyContent) });
|
|
412
517
|
}
|
|
518
|
+
for (const item of migration?.files || []) changes.push({ path: item.path, action: 'remove', content: null, beforeHash: hash(item.content) });
|
|
413
519
|
const retiredRoots = adapterRoots.filter(root => !active.roots.includes(root) && changes.some(change => change.action === 'remove' && change.path.startsWith(`${root}/`) && (previous?.files[change.path] || previous?.blocks[change.path])));
|
|
414
520
|
const ownedOutputs = [...new Set([...synchronized.ownedOutputs, ...ownedEntrypoints, statePath])].filter(path => !mixedPaths.has(path) && !overlapsPreserved(path));
|
|
415
521
|
const hashes = {};
|
|
@@ -419,12 +525,13 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
419
525
|
const current = read(target, path);
|
|
420
526
|
if (current) { const source = current.toString('utf8'), block = extractBlock(source, '# BEGIN AGENT IGNORE', '# END AGENT IGNORE'); if (block && source.trim() === block.trim() && hash(block) === previous.blocks[path]) hashes[path] = hash(current); }
|
|
421
527
|
}
|
|
422
|
-
if (previous) hashes[statePath] = hash(read(target, statePath));
|
|
528
|
+
if (previous && !migration) hashes[statePath] = hash(read(target, statePath));
|
|
423
529
|
const untrack = existsSync(target) ? planUntrack(target, hashes) : null;
|
|
530
|
+
if (migration && untrack?.paths.length) fail('E_STATE', 'State migration would change the Git index; preserve and resolve tracked outputs first.');
|
|
424
531
|
const localIgnore = planLocalIgnore(target, { outputs: ownedOutputs, templateRoot: stage });
|
|
425
532
|
const localIgnoreCheck = checkLocalIgnore(localIgnore, { replacedBlocks: replacedIgnoreBlocks(target, changes, previous) });
|
|
426
533
|
conflicts.push(...(untrack?.conflicts || []), ...localIgnoreCheck.missing);
|
|
427
|
-
return { target, language, mode, changes, conflicts: [...new Set(conflicts)].sort(), canApply: (!conflicts.length || force) && !untrack?.conflicts?.length && !localIgnoreCheck.missing.length, manifest, retiredRoots, ownedOutputs, untrack, localIgnore, localIgnoreCheck, preserved: [...preserved.values()].sort((a, b) => a.path.localeCompare(b.path)) };
|
|
534
|
+
return { target, language, mode, changes, conflicts: [...new Set(conflicts)].sort(), canApply: (!conflicts.length || force) && !untrack?.conflicts?.length && !localIgnoreCheck.missing.length, manifest, retiredRoots, ownedOutputs, untrack, localIgnore, localIgnoreCheck, migration, preserved: [...preserved.values()].sort((a, b) => a.path.localeCompare(b.path)) };
|
|
428
535
|
} finally { rmSync(stage, { recursive: true, force: true }); }
|
|
429
536
|
}
|
|
430
537
|
|
|
@@ -432,6 +539,17 @@ export function applyPlan(plan) {
|
|
|
432
539
|
if (!plan.canApply) fail('E_CONFLICT', text(plan.language, 'conflict'));
|
|
433
540
|
assertLocalIgnoreUnchanged(plan.localIgnore);
|
|
434
541
|
const { target } = plan;
|
|
542
|
+
if (plan.migration) {
|
|
543
|
+
if (stat(safePath(target, '.pbai4devs/state'))) fail('E_CHANGED', 'State destination appeared after planning.');
|
|
544
|
+
for (const directory of plan.migration.directories) {
|
|
545
|
+
const info = stat(safePath(target, directory.path));
|
|
546
|
+
if (!info?.isDirectory() || (info.mode & 0o777) !== directory.mode || JSON.stringify(readdirSync(safePath(target, directory.path)).sort()) !== JSON.stringify(directory.names)) fail('E_CHANGED', `State directory changed after planning: ${directory.path}`);
|
|
547
|
+
}
|
|
548
|
+
for (const item of plan.migration.files) {
|
|
549
|
+
const info = stat(safePath(target, item.path));
|
|
550
|
+
if (!info?.isFile() || (info.mode & 0o777) !== item.mode || hash(readFileSync(safePath(target, item.path))) !== hash(item.content)) fail('E_CHANGED', `State file changed after planning: ${item.path}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
435
553
|
const backup = `.agent-tests/agentdevkit/backups/${Date.now()}-${randomUUID()}`;
|
|
436
554
|
safePath(target, backup);
|
|
437
555
|
for (const change of plan.changes) safePath(target, change.path);
|
|
@@ -457,10 +575,24 @@ export function applyPlan(plan) {
|
|
|
457
575
|
const content = change.content;
|
|
458
576
|
const signature = { path: change.path, hash: content === null ? null : hash(content) };
|
|
459
577
|
if (content === null) { rmSync(file, { force: true }); applied.push(signature); }
|
|
460
|
-
else atomicWrite(target, change.path, content, beforeModes.get(change.path), mode => {
|
|
578
|
+
else atomicWrite(target, change.path, content, beforeModes.get(change.path) ?? change.mode, mode => {
|
|
461
579
|
signature.mode = mode; applied.push(signature);
|
|
462
580
|
}, () => assertOriginal(change.path));
|
|
463
581
|
}
|
|
582
|
+
if (plan.migration) {
|
|
583
|
+
for (const directory of plan.migration.directories) {
|
|
584
|
+
const destination = directory.path.replace(/^\.pbai4devs\/estado/, '.pbai4devs/state');
|
|
585
|
+
const file = safePath(target, destination), existing = stat(file);
|
|
586
|
+
if (existing && !existing.isDirectory()) fail('E_CHANGED', `Invalid state destination: ${destination}`);
|
|
587
|
+
if (!existing) mkdirSync(file, { recursive: true, mode: directory.mode });
|
|
588
|
+
chmodSync(file, directory.mode);
|
|
589
|
+
}
|
|
590
|
+
for (const directory of [...plan.migration.directories].sort((a, b) => b.path.length - a.path.length)) {
|
|
591
|
+
const file = safePath(target, directory.path), info = stat(file);
|
|
592
|
+
if (!info?.isDirectory() || readdirSync(file).length) fail('E_CHANGED', `State directory changed during migration: ${directory.path}`);
|
|
593
|
+
rmdirSync(file); removedDirectories.push({ path: directory.path, mode: directory.mode });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
464
596
|
pruneRemovedDirectories(plan, directory => removedDirectories.push(directory));
|
|
465
597
|
const changesFallback = plan.changes.some(change => change.path === '.gitignore');
|
|
466
598
|
// Metadata is committed last; its writer rolls back partial writes itself.
|
|
@@ -498,6 +630,11 @@ export function applyPlan(plan) {
|
|
|
498
630
|
else rollbackErrors.push(failure);
|
|
499
631
|
}
|
|
500
632
|
}
|
|
633
|
+
if (plan.migration) for (const directory of [...plan.migration.directories].sort((a, b) => b.path.length - a.path.length)) {
|
|
634
|
+
const destination = directory.path.replace(/^\.pbai4devs\/estado/, '.pbai4devs/state');
|
|
635
|
+
try { const file = safePath(target, destination); if (stat(file)?.isDirectory() && !readdirSync(file).length) rmdirSync(file); }
|
|
636
|
+
catch (failure) { if (!['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(failure.code)) rollbackErrors.push(failure); }
|
|
637
|
+
}
|
|
501
638
|
if (preserved.length) {
|
|
502
639
|
try { error.rollbackPreservedPaths = preserved; } catch { /* Preserve a exceção original. */ }
|
|
503
640
|
}
|
|
File without changes
|