@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.
Files changed (38) hide show
  1. package/.pbai4devs/INSTRUCTIONS.md +1 -1
  2. package/.pbai4devs/README.md +1 -1
  3. package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +3 -2
  4. package/.pbai4devs/scripts/inspecionar-projeto/tests/TEST.mjs +18 -4
  5. package/.pbai4devs/scripts/lib/README.md +3 -0
  6. package/.pbai4devs/scripts/lib/state-path.mjs +20 -0
  7. package/.pbai4devs/scripts/sincronizar-agentes/README.md +1 -1
  8. package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +5 -3
  9. package/.pbai4devs/scripts/sincronizar-agentes/tests/COEXISTENCE.mjs +5 -5
  10. package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +1 -1
  11. package/.pbai4devs/scripts/sincronizar-agentes/tests/MANAGED_CONTENT.mjs +9 -9
  12. package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +31 -6
  13. package/.pbai4devs/scripts/testar-maturidade/SCRIPT.mjs +6 -4
  14. package/.pbai4devs/scripts/testar-maturidade/tests/TEST.mjs +4 -3
  15. package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +5 -3
  16. package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +15 -7
  17. package/.pbai4devs/skills/produto/criar-design-doc/SKILL.md +1 -1
  18. package/.pbai4devs/skills/produto/criar-prd/SKILL.md +1 -1
  19. package/.pbai4devs/skills/produto/criar-ux-ui/SKILL.md +1 -1
  20. package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +1 -1
  21. package/.pbai4devs/skills/projeto/proximos-passos/SKILL.md +1 -1
  22. package/.pbai4devs/skills/projeto/sincronizar-agentes/SKILL.md +1 -1
  23. package/.pbai4devs/skills/specs/criar-spec/SKILL.md +1 -1
  24. package/.pbai4devs/skills/specs/implementar-spec/SKILL.md +1 -1
  25. package/.pbai4devs/templates/entrypoints/AGENTS.md +1 -1
  26. package/.pbai4devs/templates/gitignore/local.ignore +2 -0
  27. package/.pbai4devs/templates/gitignore/shared.ignore +2 -0
  28. package/CHANGELOG.md +5 -0
  29. package/docs/guides/troubleshooting.md +9 -1
  30. package/docs/guides/workflow.md +1 -1
  31. package/docs/specs/README.md +1 -0
  32. package/docs/specs/state-directory/SPEC.md +22 -0
  33. package/package.json +1 -1
  34. package/src/cli.mjs +2 -2
  35. package/src/config.mjs +2 -1
  36. package/src/i18n.mjs +15 -0
  37. package/src/install.mjs +152 -15
  38. /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/estado/agentdevkit.json`; sem escolha, use português.
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
 
@@ -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 `estado/agentdevkit.json` guarda `clients`: `["all"]` ou os IDs
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 = '.pbai4devs/estado/configuracao/ESTADO.md'; const state = metadata(root, 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, '.pbai4devs/estado/configuracao/ESTADO.md').documents;
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/estado/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`);
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/estado/configuracao/ESTADO.md', `---\npreparacao: ${secret}\nconfiguracao_tecnica: ${secret}\ndocumentos:\n prd: ../outside.md\n design: docs/DESIGN.md\n---\n# Estado\n`);
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/estado/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');
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/estado/configuracao/ESTADO.md', '---\ndocumentos:\n design: external/private.md\n---\n# State\n');
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 `estado/derived.json` permitem
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 stateFile = '.pbai4devs/estado/agentdevkit.json';
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 = '.pbai4devs/estado/derived.json';
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/estado/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/estado/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: source === 'files' ? { [path]: 'a'.repeat(64) } : {}, blocks: source === 'blocks' ? { [path]: 'a'.repeat(64) } : {} }));
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/estado/derived.json')));
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/estado/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: { [owned]: 'a'.repeat(64) }, blocks: {} }));
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/estado/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: { [owned]: 'a'.repeat(64) }, blocks: {} }));
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/estado/agentdevkit.json', JSON.stringify({ schemaVersion: 1, clients: ['claude'], files: { [stale]: hash(human) }, blocks: {} }));
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/estado/derived.json'));
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/estado/derived.json')).blocks?.[path], undefined);
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/estado/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: {}, blocks: { [path]: block } }));
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/estado/derived.json'));
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/estado/derived.json')));
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/estado/agentdevkit.json', JSON.stringify({ schemaVersion: 1, clients: ['claude'], files: {}, blocks: { [path]: hash } }));
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/estado/derived.json'));
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/estado/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: {}, blocks: { 'AGENTS.md': hash } }));
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/estado/derived.json'));
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/estado/agentdevkit.json';
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', 'estado', 'tests', 'docs', 'adapters', 'extra']) {
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
- for (const kind of ['templates', 'scripts', 'tools', 'external-skills', 'estado', 'tests', 'docs', 'adapters', 'extra', 'README.md', 'INSTRUCTIONS.md']) assert.equal(existsSync(join(root, client, kind)), false, `${client}/${kind}`);
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/estado/agentdevkit.json', JSON.stringify({ clients: ['codex'], files: { 'AGENTS.md': createHash('sha256').update(entry).digest('hex') } }));
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/estado/derived.json')).entries['AGENTS.md'], undefined);
597
- assert.ok(JSON.parse(read(root, '.pbai4devs/estado/derived.json')).files['.codex/skills/demo/SKILL.md']);
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, '.pbai4devs/estado/agentdevkit.json');
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 state = '.pbai4devs/estado/configuracao/ESTADO.md';
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, '.pbai4devs/estado/README.md', document('Estado dos exemplos', 'Dados sintéticos deste cenário, sem aprovação real de produto.'));
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/estado/agentdevkit.json', JSON.stringify({ blocks: null }));
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/estado/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
+ 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/estado/agentdevkit.json', JSON.stringify({ clients: ['claude'] }));
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 file = checkedPath(root, '.pbai4devs/estado/agentdevkit.json');
35
- const derivedFile = checkedPath(root, '.pbai4devs/estado/derived.json');
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/estado/README.md');
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/estado/agentdevkit.json', JSON.stringify({ schemaVersion: 1, version: '0.0.9', language: 'pt', files, blocks: {} }));
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/estado/derived.json', JSON.stringify({ schemaVersion: 1, entries: { 'GEMINI.md': createHash('sha256').update(contents).digest('hex') } }));
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/estado/agentdevkit.json');
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/estado/derived.json', JSON.stringify({ schemaVersion: 1, entries: {}, outputs: { [manifest]: createHash('sha256').update(contents).digest('hex') } }));
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/estado/agentdevkit.json'];
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/estado/configuracao/ESTADO.md` e confira o PRD aprovado
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/estado/configuracao/ESTADO.md`. Se não estiver
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/estado/configuracao/ESTADO.md` e confira o PRD
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/estado/configuracao/ESTADO.md` pelo modelo.
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/estado/agentdevkit.json` ou português quando ainda não houver escolha.
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 `estado/agentdevkit.json`; não reative clientes para
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/estado/configuracao/ESTADO.md`. Exija PRD/design
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/estado/configuracao/ESTADO.md`. Exija spec, PRD e design aprovados;
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/estado/agentdevkit.json`. Para alterá-la, use
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.
@@ -6,5 +6,7 @@
6
6
  /.pbai4devs/espelhos.conf
7
7
  /.pbai4devs/estado/agentdevkit.json
8
8
  /.pbai4devs/estado/derived.json
9
+ /.pbai4devs/state/agentdevkit.json
10
+ /.pbai4devs/state/derived.json
9
11
  {{ROOTS}}
10
12
  {{OUTPUTS}}
@@ -6,6 +6,8 @@
6
6
  /espelhos.conf
7
7
  /estado/agentdevkit.json
8
8
  /estado/derived.json
9
+ /state/agentdevkit.json
10
+ /state/derived.json
9
11
 
10
12
  # Downloads e launchers de ferramentas
11
13
  /tools/bin/
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/estado/agentdevkit.json` estiver inválido, recupere um backup
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`.
@@ -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/estado/configuracao/ESTADO.md`.
59
+ O registro é `.pbai4devs/state/configuracao/ESTADO.md`.
60
60
 
61
61
  ## 3. Definir produto e experiência
62
62
 
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pathbit/agentdevkit",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "Shared skills, specifications and workflows for AI coding agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
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, '.pbai4devs/estado/agentdevkit.json')) }) };
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/estado/agentdevkit.json';
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', 'estado']);
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] === 'estado') return directory && parts.length === 1 || path === 'estado/README.md';
61
- if (isGeneratedOutput(path, directory) || parts.some(part => ignored.has(part)) || parts[0] === 'adapters' || path === 'estado/agentdevkit.json' || path === '.npmignore') return false;
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, statePath); if (content === null) return null;
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, '.pbai4devs/estado/derived.json');
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/estado/README.md'))) write(stage, '.pbai4devs/estado/README.md', `---\ndescription: AgentDevKit state.\n---\n\n# AgentDevKit\n\n${text(language, 'state')}\n`);
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
- changes.push({ path, action: current ? 'update' : 'create', content, beforeHash: current ? hash(current) : null });
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