@pathbit/agentdevkit 0.0.16 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.pbai4devs/INSTRUCTIONS.md +1 -1
  2. package/.pbai4devs/README.md +1 -1
  3. package/.pbai4devs/commands/sincronizar/COMMAND.md +6 -4
  4. package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +3 -2
  5. package/.pbai4devs/scripts/inspecionar-projeto/tests/TEST.mjs +18 -4
  6. package/.pbai4devs/scripts/lib/README.md +3 -0
  7. package/.pbai4devs/scripts/lib/gitignore.mjs +46 -9
  8. package/.pbai4devs/scripts/lib/state-path.mjs +20 -0
  9. package/.pbai4devs/scripts/sincronizar-agentes/README.md +8 -1
  10. package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +75 -34
  11. package/.pbai4devs/scripts/sincronizar-agentes/tests/COEXISTENCE.mjs +5 -5
  12. package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +1 -1
  13. package/.pbai4devs/scripts/sincronizar-agentes/tests/MANAGED_CONTENT.mjs +9 -9
  14. package/.pbai4devs/scripts/sincronizar-agentes/tests/PRESERVATION.mjs +306 -0
  15. package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +41 -8
  16. package/.pbai4devs/scripts/testar-maturidade/SCRIPT.mjs +6 -4
  17. package/.pbai4devs/scripts/testar-maturidade/tests/TEST.mjs +4 -3
  18. package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +5 -3
  19. package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +15 -7
  20. package/.pbai4devs/skills/produto/criar-design-doc/SKILL.md +1 -1
  21. package/.pbai4devs/skills/produto/criar-prd/SKILL.md +1 -1
  22. package/.pbai4devs/skills/produto/criar-ux-ui/SKILL.md +1 -1
  23. package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +1 -1
  24. package/.pbai4devs/skills/projeto/proximos-passos/SKILL.md +1 -1
  25. package/.pbai4devs/skills/projeto/sincronizar-agentes/SKILL.md +1 -1
  26. package/.pbai4devs/skills/specs/criar-spec/SKILL.md +1 -1
  27. package/.pbai4devs/skills/specs/implementar-spec/SKILL.md +1 -1
  28. package/.pbai4devs/templates/entrypoints/AGENTS.md +1 -1
  29. package/.pbai4devs/templates/gitignore/local.ignore +2 -0
  30. package/.pbai4devs/templates/gitignore/shared.ignore +2 -0
  31. package/CHANGELOG.md +11 -0
  32. package/docs/guides/cli.md +46 -15
  33. package/docs/guides/installation.md +20 -8
  34. package/docs/guides/troubleshooting.md +9 -1
  35. package/docs/guides/workflow.md +1 -1
  36. package/docs/specs/README.md +2 -0
  37. package/docs/specs/safe-synchronization/SPEC.md +21 -0
  38. package/docs/specs/state-directory/SPEC.md +22 -0
  39. package/package.json +1 -1
  40. package/src/cli.mjs +2 -2
  41. package/src/config.mjs +2 -1
  42. package/src/i18n.mjs +25 -0
  43. package/src/install.mjs +373 -27
  44. /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.
@@ -4,14 +4,16 @@ description: Atualiza ou confere os apontamentos compartilhados dos agentes.
4
4
 
5
5
  # Sincronizar
6
6
 
7
- Entrada: sem argumentos usa `--force` para reparar; `--check` só confere.
7
+ Entrada: este comando agêntico usa o script local com `--force` para reparar;
8
+ `--check` só confere. O `sync` da CLI usa as fontes locais e não implica
9
+ `--force`; `update` usa as fontes da versão do pacote executado.
8
10
  Pré-requisitos: Node.js 24+ e manifesto `.pbai4devs/espelhos.conf` válido.
9
11
 
10
12
  Siga [sincronizar-agentes](../../skills/projeto/sincronizar-agentes/SKILL.md)
11
13
  e execute o script correspondente pelo seletor. A reconstrução repara
12
- apontamentos e remove saídas obsoletas cuja propriedade do kit foi comprovada,
13
- com backup local. Recursos, configurações e links humanos são preservados,
14
- inclusive quando seus nomes coincidem com os destinos do kit.
14
+ apontamentos e remove saídas obsoletas cuja propriedade do kit foi comprovada.
15
+ Recursos adicionais, configurações, pastas e links humanos são preservados,
16
+ inclusive em colisões de nomes. O backup local não substitui o histórico Git.
15
17
  Após reparar, execute também `--check`.
16
18
 
17
19
  Respeite os clientes registrados no manifesto. Para mudar a seleção salva,
@@ -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.
@@ -98,10 +98,33 @@ function localBlock(current, first, last, body) {
98
98
  return text.slice(0, start) + block + text.slice(finish);
99
99
  }
100
100
 
101
- function gitCommand(root, args, input) {
101
+ function gitCommand(root, args, input, encoding = 'utf8') {
102
102
  const env = { ...process.env };
103
103
  for (const key of ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE']) delete env[key];
104
- return spawnSync('git', [...(['ls-files', 'rm'].includes(args[0]) ? ['--literal-pathspecs'] : []), '-C', root, ...args], { input, encoding: 'utf8', env, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
104
+ return spawnSync('git', [...(['ls-files', 'rm'].includes(args[0]) ? ['--literal-pathspecs'] : []), '-C', root, ...args], { input, encoding, env, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
105
+ }
106
+
107
+ // Compare exact bytes first. Only accept Git's plain text CRLF conversion when
108
+ // effective attributes prove it applies and no custom transformation can run.
109
+ // This deliberately does not invoke hash-object or repository-defined filters.
110
+ export function indexContentMatches(root, path, working, indexed) {
111
+ if (working.equals(indexed)) return true;
112
+ const attributes = gitCommand(root, ['check-attr', '-z', 'text', 'eol', 'filter', 'working-tree-encoding', 'ident', '--', path]);
113
+ if (attributes.status !== 0) return false;
114
+ const parts = attributes.stdout.split('\0');
115
+ const values = {};
116
+ for (let i = 0; i + 2 < parts.length; i += 3) values[parts[i + 1]] = parts[i + 2];
117
+ const absent = value => value === 'unspecified' || value === 'unset';
118
+ if (!absent(values.filter) || !absent(values['working-tree-encoding']) || !absent(values.ident) || values.text === 'unset') return false;
119
+ const config = gitCommand(root, ['config', '--bool', 'core.autocrlf']);
120
+ if (config.status !== 0 && config.status !== 1) return false;
121
+ const textEnabled = values.text === 'set' || values.text === 'auto' || ['lf', 'crlf'].includes(values.eol) || config.status === 0 && config.stdout.trim() === 'true';
122
+ if (!textEnabled || !working.includes(13) || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(working.toString('latin1'))) return false;
123
+ try {
124
+ const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(working);
125
+ if (text.replaceAll('\r\n', '').includes('\r')) return false;
126
+ return Buffer.from(text.replaceAll('\r\n', '\n')).equals(indexed);
127
+ } catch { return false; }
105
128
  }
106
129
 
107
130
  function checkedOutput(path) {
@@ -502,25 +525,39 @@ export function planUntrack(root, expectedHashes = {}) {
502
525
  if (!candidates.length || !localStat(root)) return { root, paths: [], hashes: {}, index: '', conflicts: [] };
503
526
  const result = gitCommand(root, ['ls-files', '--stage', '-z']);
504
527
  if (result.status !== 0) {
505
- if (result.error?.code === 'ENOENT' || /not a git repository/i.test(result.stderr || '')) return { root, paths: [], hashes: {}, index: '', conflicts: [] };
528
+ if (result.error?.code === 'ENOENT' || /not a git repository/i.test(result.stderr || '')) {
529
+ // Fail closed if a repository exists even when Git cannot be launched.
530
+ let directory = root;
531
+ while (true) {
532
+ if (localStat(join(directory, '.git'))) throw new Error('Nao foi possivel consultar o Git deste projeto; indice e arquivos preservados.');
533
+ const parent = dirname(directory);
534
+ if (parent === directory) break;
535
+ directory = parent;
536
+ }
537
+ return { root, paths: [], hashes: {}, index: '', conflicts: [] };
538
+ }
506
539
  throw new Error(result.error?.message || result.stderr || 'Falha ao ler indice Git.');
507
540
  }
508
541
  const conflicts = [];
509
542
  const records = result.stdout.split('\0').filter(Boolean);
510
- const wanted = new Set(candidates);
543
+ const folded = new Map();
544
+ for (const path of candidates) folded.set(path.toLowerCase(), [...(folded.get(path.toLowerCase()) || []), path]);
511
545
  const matches = new Map();
512
546
  for (const record of records) {
513
547
  const position = record.indexOf('\t'), path = record.slice(position + 1);
514
- if (position < 0 || !wanted.has(path)) continue;
515
- matches.set(path, [...(matches.get(path) || []), record]);
548
+ if (position < 0) continue;
549
+ for (const candidate of folded.get(path.toLowerCase()) || []) {
550
+ matches.set(candidate, [...(matches.get(candidate) || []), record]);
551
+ }
516
552
  }
517
553
  const paths = [...matches.keys()].filter(path => {
518
554
  const file = checkedLocalPath(join(root, path));
519
555
  const entries = matches.get(path);
520
556
  const match = entries.length === 1 && entries[0].match(/^\d+ ([a-f0-9]+) 0\t/);
521
- const blob = match ? gitCommand(root, ['cat-file', 'blob', match[1]]) : null;
522
- const owned = localStat(file) && createHash('sha256').update(readFileSync(file)).digest('hex') === expectedHashes[path];
523
- const indexOwned = blob?.status === 0 && createHash('sha256').update(blob.stdout).digest('hex') === expectedHashes[path];
557
+ const blob = match ? gitCommand(root, ['cat-file', 'blob', match[1]], undefined, null) : null;
558
+ const data = localStat(file) ? readFileSync(file) : null;
559
+ const owned = data && createHash('sha256').update(data).digest('hex') === expectedHashes[path];
560
+ const indexOwned = entries[0]?.slice(entries[0].indexOf('\t') + 1) === path && blob?.status === 0 && data && indexContentMatches(root, path, data, blob.stdout);
524
561
  if (!owned || !indexOwned) conflicts.push(path);
525
562
  return owned && indexOwned;
526
563
  });
@@ -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
+ }
@@ -12,6 +12,9 @@ node --test .pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs
12
12
 
13
13
  `--check` só confere; a execução normal atualiza os recursos. `--force` salva
14
14
  conflitos em `.agent-tests/` antes de reparar os caminhos gerenciados.
15
+ O [comando agêntico](../../commands/sincronizar/COMMAND.md) usa `--force` por
16
+ padrão; executar este script diretamente não ativa essa opção. A CLI oferece
17
+ `sync --dry-run --json` para consultar o plano antes de aplicá-lo.
15
18
  Recursos nativos humanos recebem um bloco curto com a referência ao hub.
16
19
  Texto, frontmatter e nomes existentes são preservados. O estado guarda só o
17
20
  hash desse bloco; edições externas continuam permitidas. Um bloco alterado
@@ -41,6 +44,10 @@ sem Git, informa que a proteção fica pendente até `git init` e sincronizaçã
41
44
  Somente arquivos integralmente gerados são excluídos; pastas de clientes podem
42
45
  conter arquivos humanos versionados. Conteúdo próprio não é desindexado pela
43
46
  presença de um bloco gerenciado.
44
- Entradas novas usam os templates; hashes em `estado/derived.json` permitem
47
+ Entradas novas usam os templates; hashes em `state/derived.json` permitem
45
48
  atualizar arquivos gerados e blocos separadamente, preservando alterações humanas.
46
49
  `--check` também acusa derivados ainda rastreados; não altera o índice.
50
+ Apontamentos obsoletos podem ser removidos quando a propriedade é comprovada;
51
+ arquivos e pastas próprios, inclusive os adicionais dentro do mesmo recurso,
52
+ devem permanecer. Na CLI, confira `untrackedPaths` antes de aplicar: saídas
53
+ geradas comprovadas podem deixar de ser rastreadas, sem excluir seus bytes.
@@ -3,6 +3,7 @@
3
3
  import { chmodSync, cpSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync } from 'node:fs';
4
4
  import { dirname, join, posix, resolve } from 'node:path';
5
5
  import { randomUUID, createHash } from 'node:crypto';
6
+ import { spawnSync } from 'node:child_process';
6
7
  import { requireNode, projectRoot, isMain } from '../lib/runtime.mjs';
7
8
  import { buildCatalog } from '../lib/catalog.mjs';
8
9
  import { resourcePath } from '../lib/resource-path.mjs';
@@ -10,12 +11,13 @@ import { adapterManifest, resolveAdapters } from '../lib/adapters.mjs';
10
11
  import { nativePathBoundary } from '../lib/native-paths.mjs';
11
12
  import { canonicalEntrypoint, resolveEntrypoint, entrypointReference, renderEntrypointImports, mergeManagedContent, stripManagedContent } from '../lib/managed-content.mjs';
12
13
  import { mergeTomlContent, stripTomlContent } from '../lib/managed-toml.mjs';
13
- import { mergeGitignore, validateIgnoreClients, renderClientIgnore, isGeneratedOutput, planLocalIgnore, applyLocalIgnore, checkLocalIgnore, assertLocalIgnoreUnchanged, ignoreBegin, ignoreEnd } from '../lib/gitignore.mjs';
14
+ import { mergeGitignore, validateIgnoreClients, renderClientIgnore, isGeneratedOutput, planLocalIgnore, applyLocalIgnore, checkLocalIgnore, assertLocalIgnoreUnchanged, planUntrack, ignoreBegin, ignoreEnd } from '../lib/gitignore.mjs';
15
+ import { stateDirectory } from '../lib/state-path.mjs';
14
16
 
15
17
  const marker = '<!-- gerado: sincronizar-agentes -->';
16
18
  const tomlMarker = '# gerado: sincronizar-agentes';
17
19
  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'];
20
+ const auxiliary = ['templates', 'scripts', 'tools', 'external-skills', 'state', 'estado', 'tests', 'docs', 'adapters'];
19
21
  const reserved = new Set(['.git', '.pbai4devs', '.agent-tests']);
20
22
  const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
21
23
  const sorted = values => [...new Set(values)].sort(compare);
@@ -78,6 +80,28 @@ export function synchronize({ root = projectRoot, check = false, force = false,
78
80
  requireNode();
79
81
  root = realpathSync(resolve(root));
80
82
  const absolute = value => join(root, ...value.split('/'));
83
+ function assertGitReadableForChanges() {
84
+ // A project may be nested in a worktree; .git can be a file or directory.
85
+ // If that marker exists we must not treat an unavailable Git executable,
86
+ // an invalid repository or an unreadable index as a Git-free project.
87
+ let folder = root, found = false;
88
+ while (true) {
89
+ if (stat(join(folder, '.git'))) { found = true; break; }
90
+ const parent = dirname(folder);
91
+ if (parent === folder) break;
92
+ folder = parent;
93
+ }
94
+ if (!found) return false;
95
+ const env = { ...process.env };
96
+ for (const key of ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE']) delete env[key];
97
+ const probe = spawnSync('git', ['-C', root, 'rev-parse', '--is-inside-work-tree'], {
98
+ encoding: 'utf8', env, windowsHide: true, timeout: 20000,
99
+ });
100
+ if (probe.error || probe.status !== 0 || probe.stdout.trim() !== 'true') {
101
+ fail('Nao foi possivel consultar o Git deste projeto; sincronizacao bloqueada para preservar arquivos e indice.');
102
+ }
103
+ return true;
104
+ }
81
105
  function safe(value) {
82
106
  if (typeof value !== 'string' || !value || /^[A-Za-z]:/.test(value) || /[|\\\x00-\x1f\x7f]/.test(value) || value.split('/').some(part => !part || part === '.' || part === '..')) fail(`Caminho invalido: ${value}`);
83
107
  let current = root;
@@ -129,7 +153,6 @@ export function synchronize({ root = projectRoot, check = false, force = false,
129
153
  const recorded = recordedHash === digest;
130
154
  return isReference(text) && recorded;
131
155
  };
132
- const onlyReferences = value => { const files = scan(value); return files.length > 0 && ![...preserved.keys()].some(path => path === value || path.startsWith(`${value}/`)) && files.every(file => /\.(md|mdc|toml)$/i.test(file) && referenceFile(file)); };
133
156
  const protectedPath = value => /^(workflows(?:\/|$)|settings\.json$|extensions\.json$)/.test(value.slice(value.indexOf('/') + 1).toLowerCase());
134
157
  const agents = new Map();
135
158
  const formats = new Map();
@@ -140,11 +163,12 @@ export function synchronize({ root = projectRoot, check = false, force = false,
140
163
  const config = '.pbai4devs/espelhos.conf';
141
164
  const configExists = stat(safe(config));
142
165
  captureInput(config, configExists ? readFileSync(absolute(config)) : null);
143
- const stateFile = '.pbai4devs/estado/agentdevkit.json';
166
+ const stateRoot = stateDirectory(root);
167
+ const stateFile = `${stateRoot}/agentdevkit.json`;
144
168
  const installed = stat(safe(stateFile)) ? JSON.parse(read(safe(stateFile))) : null;
145
169
  const configText = configExists ? read(safe(config)) : adapterManifest(root, { clients: installed?.clients });
146
170
  const rows = configText.split('\n');
147
- const entryRegistry = '.pbai4devs/estado/derived.json';
171
+ const entryRegistry = `${stateRoot}/derived.json`;
148
172
  const registryBytes = stat(safe(entryRegistry)) ? readFileSync(absolute(entryRegistry)) : null;
149
173
  captureInput(entryRegistry, registryBytes);
150
174
  const previousEntries = registryBytes ? JSON.parse(registryBytes.toString('utf8')) : { schemaVersion: 1, entries: {} };
@@ -293,6 +317,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
293
317
  const remove = new Set();
294
318
  const backup = new Set();
295
319
  const conflicts = new Set();
320
+ const protectedConflicts = new Set();
296
321
  const expected = new Map();
297
322
  const managedBlocks = {};
298
323
  const generatedFiles = {};
@@ -305,17 +330,31 @@ export function synchronize({ root = projectRoot, check = false, force = false,
305
330
  function planRemove(value, custom = false) {
306
331
  safe(value);
307
332
  if (!managed(value) || protectedPath(value)) fail(`Limpeza fora dos espelhos ou caminho protegido: ${value}`);
333
+ // Directories, unowned files and edited references can contain project
334
+ // data even when --force is used. Only a recorded, unchanged reference
335
+ // may be removed; empty directories are never removed recursively.
336
+ if (!stat(safe(value))?.isFile() || !referenceFile(value)) {
337
+ remember(value, 'existing-file');
338
+ if (custom) { conflicts.add(value); protectedConflicts.add(value); }
339
+ return;
340
+ }
308
341
  remove.add(value);
309
342
  if (custom) backup.add(value);
310
343
  }
311
344
  function cleanScope(value) {
312
345
  if (preserveBoundary(value)) return;
313
- if (!stat(safe(value))) return;
314
- if (stat(safe(value)).isDirectory() && !scan(value).length) return;
315
- if (Object.keys(priorBlocks).some(path => path === value || path.startsWith(value + '/'))) return;
316
- if (onlyReferences(value)) planRemove(value);
317
- else if (claimed(value)) remember(value, 'edited-generated-file');
318
- else remember(value);
346
+ const info = stat(safe(value));
347
+ if (!info) return;
348
+ const files = info.isDirectory() ? scan(value) : [value];
349
+ if (info.isDirectory() && !files.length) { remember(value, 'existing-directory'); return; }
350
+ let hasHumanContent = false;
351
+ for (const file of files) {
352
+ if (expected.has(file) || priorBlocks[file] || protectedPath(file)) continue;
353
+ if (referenceFile(file)) planRemove(file);
354
+ else if (claimed(file)) { remember(file, 'edited-generated-file'); hasHumanContent = true; }
355
+ else { remember(file); hasHumanContent = true; }
356
+ }
357
+ if (hasHumanContent && info.isDirectory()) remember(value);
319
358
  }
320
359
  const registries = new Map();
321
360
  const discoveryOutputs = {};
@@ -549,6 +588,22 @@ export function synchronize({ root = projectRoot, check = false, force = false,
549
588
  const omittedRows = new Set([...legacy, ...missing.map(native => native.index)]);
550
589
  const finalConfig = rows.filter((row, index) => !omittedRows.has(index)).join('\n').replace(/\n*$/, '\n');
551
590
  const configChanged = !configExists || configText !== finalConfig;
591
+ // Every planned write/removal may cover a different staged version. Compare
592
+ // the worktree bytes with the index before touching any path, even in force
593
+ // mode, and repeat immediately before applying the transaction.
594
+ const affected = sorted([...removals, ...changed, ...registries.keys(), ...ignoreFiles.keys(), ...rootEntryChanges.map(([name]) => name), ...(configChanged ? [config] : [])]);
595
+ const emptyHash = createHash('sha256').update(Buffer.alloc(0)).digest('hex');
596
+ const affectedHashes = Object.fromEntries(affected.flatMap(value => {
597
+ const info = stat(safe(value));
598
+ if (info && !info.isFile()) return []; // A type conflict already blocks application.
599
+ return [[value, info ? createHash('sha256').update(readFileSync(absolute(value))).digest('hex') : emptyHash]];
600
+ }));
601
+ const hadGit = Object.keys(affectedHashes).length ? assertGitReadableForChanges() : false;
602
+ const affectedIndex = planUntrack(root, affectedHashes);
603
+ for (const path of affectedIndex.conflicts) {
604
+ conflicts.add(path);
605
+ remember(path, 'staged-content-preserved');
606
+ }
552
607
  function capture(value) {
553
608
  const file = safe(value), info = stat(file);
554
609
  if (!info) return null;
@@ -574,15 +629,15 @@ export function synchronize({ root = projectRoot, check = false, force = false,
574
629
  let parent = posix.dirname(value);
575
630
  while (parent !== '.') { if (stat(safe(parent))?.isDirectory()) originalDirectories.add(parent); parent = posix.dirname(parent); }
576
631
  }
577
- const touched = new Set(), createdDirectories = new Set(), restoredDirectories = new Set();
578
- const prunedDirectories = new Map();
579
- let restoring = false;
632
+ const touched = new Set(), createdDirectories = new Set();
580
633
  const summary = { changed: changed.length + rootEntryChanges.length, stale: removals.length, conflicts: conflicts.size, missing: missing.length, legacy: legacy.length, registries: registries.size, gitignores: ignoreFiles.size, manifests: configExists ? 0 : 1, localIgnores: localStatus.changed + localStatus.missing.length + localStatus.trackedOutputs.length + (localStatus.status === 'no-git' ? 1 : 0) };
581
634
  const dirty = Object.values(summary).some(Boolean);
582
635
  output(`${summary.changed} desatualizadas; ${summary.stale} obsoletas; ${summary.conflicts} conflitos; ${summary.missing} origens nativas ausentes; ${summary.legacy} documentos antigos; ${summary.registries} indices derivados; ${summary.gitignores} gitignores; ${summary.localIgnores} exclusoes locais.`);
583
636
  if (localStatus.status === 'no-git') output('Git ausente: apos git init, execute a sincronizacao para proteger os arquivos locais.');
584
637
  const preservedPaths = sorted(preserved.keys()).map(path => preserved.get(path));
585
638
  if (check) return { ...summary, ownedOutputs, managedBlocks, preserved: preservedPaths, exitCode: dirty ? 1 : 0 };
639
+ if (affectedIndex.conflicts.length) fail(`Indice Git contem alteracoes proprias; arquivos preservados: ${sorted(affectedIndex.conflicts).join(', ')}`);
640
+ if (protectedConflicts.size) fail(`Conteudo proprio preservado; resolva a colisao antes de sincronizar: ${sorted(protectedConflicts).join(', ')}`);
586
641
  if (conflicts.size) fail(`Use --force para reconstruir somente os caminhos gerenciados: ${sorted(conflicts).join(', ')}`);
587
642
  if (localStatus.missing.length) fail(`Regras humanas reabrem derivados locais: ${localStatus.missing.join(', ')}`);
588
643
  if (localPlan) assertLocalIgnoreUnchanged(localPlan);
@@ -592,7 +647,6 @@ export function synchronize({ root = projectRoot, check = false, force = false,
592
647
  while (parent !== '.' && !stat(safe(parent))) { missing.push(parent); parent = posix.dirname(parent); }
593
648
  for (const directory of missing.reverse()) {
594
649
  mkdirSync(safe(directory)); createdDirectories.add(directory);
595
- if (restoring) restoredDirectories.add(directory);
596
650
  }
597
651
  }
598
652
  function mutate(value, action, text) {
@@ -647,40 +701,27 @@ export function synchronize({ root = projectRoot, check = false, force = false,
647
701
  }
648
702
  try {
649
703
  if (localPlan) assertLocalIgnoreUnchanged(localPlan);
704
+ if (Object.keys(affectedHashes).length) {
705
+ if (!assertGitReadableForChanges() && hadGit) fail('Git deixou de estar disponivel antes da sincronizacao; arquivos preservados.');
706
+ const current = planUntrack(root, affectedHashes);
707
+ if (current.conflicts.length || current.index !== affectedIndex.index) fail('Indice Git mudou antes da sincronizacao; conteudo preservado.');
708
+ }
650
709
  for (const value of removals) mutate(value, () => rmSync(safe(value), { recursive: true, force: true }), null);
651
710
  for (const value of changed) mutate(value, () => atomic(value, expected.get(value)), expected.get(value));
652
711
  for (const [value, text] of registries) mutate(value, () => atomic(value, text), text);
653
712
  for (const [value, text] of ignoreFiles) mutate(value, () => atomic(value, text), text);
654
713
  for (const [value, text] of rootEntryChanges) mutate(value, () => atomic(value, text), text);
655
714
  if (configChanged) mutate(config, () => atomic(config, finalConfig), finalConfig);
656
- // Só os ancestrais de remoções conhecidas são candidatos à limpeza.
657
- for (const value of removals) {
658
- let parent = posix.dirname(value);
659
- const boundary = value.split('/').slice(0, 2).join('/');
660
- while (parent.startsWith(boundary + '/') || parent === boundary) {
661
- const info = stat(safe(parent));
662
- if (!info?.isDirectory() || readdirSync(absolute(parent)).length) break;
663
- rmdirSync(safe(parent)); prunedDirectories.set(parent, info.mode & 0o777); parent = posix.dirname(parent);
664
- }
665
- }
715
+ // Directory ownership is not recorded: keep even empty parent directories.
666
716
  if (localPlan) applyLocalIgnore(localPlan);
667
717
  } catch (error) {
668
718
  const failures = [];
669
- restoring = true;
670
719
  for (const value of [...touched].reverse()) {
671
720
  try {
672
721
  if (fingerprint(capture(value)) !== currentHashes.get(value)) fail(`Mudanca concorrente preservada: ${value}`);
673
722
  restore(value, originals.get(value));
674
723
  } catch (failure) { failures.push(failure); }
675
724
  }
676
- for (const [directory, mode] of [...prunedDirectories].sort(([left], [right]) => right.length - left.length)) {
677
- try {
678
- if (!stat(safe(directory))) ensureDirectory(`${directory}/.restore-probe`);
679
- const info = stat(safe(directory));
680
- if (!info?.isDirectory() || !restoredDirectories.has(directory) && (info.mode & 0o777) !== mode) fail(`Diretorio concorrente preservado: ${directory}`);
681
- chmodSync(safe(directory), mode);
682
- } catch (failure) { failures.push(failure); }
683
- }
684
725
  for (const directory of sorted(createdDirectories).sort((a, b) => b.length - a.length)) {
685
726
  try { if (!originalDirectories.has(directory) && stat(safe(directory))?.isDirectory() && !readdirSync(absolute(directory)).length) rmdirSync(safe(directory)); }
686
727
  catch (failure) { failures.push(failure); }
@@ -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']);