@pathbit/agentdevkit 0.0.22 → 0.0.24

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 (31) hide show
  1. package/.pbai4devs/rules/ambiente-local/RULE.md +5 -0
  2. package/.pbai4devs/rules/git/RULE.md +5 -1
  3. package/.pbai4devs/scripts/README.md +1 -0
  4. package/.pbai4devs/scripts/inspecionar-projeto/README.md +6 -0
  5. package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +218 -18
  6. package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +59 -11
  7. package/.pbai4devs/scripts/sincronizar-agentes/tests/CATALOG_OWNERSHIP.mjs +251 -0
  8. package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +33 -13
  9. package/.pbai4devs/scripts/testar-maturidade/README.md +12 -3
  10. package/.pbai4devs/scripts/testar-maturidade/SCRIPT.mjs +65 -25
  11. package/.pbai4devs/scripts/testar-maturidade/tests/TEST.mjs +150 -5
  12. package/.pbai4devs/scripts/verificar-configuracao/README.md +22 -0
  13. package/.pbai4devs/scripts/verificar-configuracao/SCRIPT.mjs +63 -0
  14. package/.pbai4devs/scripts/verificar-configuracao/tests/TEST.mjs +145 -0
  15. package/.pbai4devs/skills/desenvolvimento/implementar-backend/SKILL.md +15 -3
  16. package/.pbai4devs/skills/desenvolvimento/implementar-frontend/SKILL.md +15 -3
  17. package/.pbai4devs/skills/git/git-diff/SKILL.md +3 -1
  18. package/.pbai4devs/skills/git/git-status/SKILL.md +2 -1
  19. package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +13 -8
  20. package/.pbai4devs/skills/projeto/proximos-passos/SKILL.md +4 -2
  21. package/.pbai4devs/skills/specs/implementar-spec/SKILL.md +14 -2
  22. package/CHANGELOG.md +17 -0
  23. package/docs/guides/cli.md +9 -0
  24. package/docs/guides/troubleshooting.md +12 -6
  25. package/docs/specs/README.md +3 -0
  26. package/docs/specs/preimplementation-order/SPEC.md +23 -0
  27. package/docs/specs/target-paths/SPEC.md +19 -0
  28. package/package.json +1 -1
  29. package/src/i18n.mjs +3 -3
  30. package/src/install.mjs +121 -23
  31. package/src/next.mjs +26 -9
@@ -39,6 +39,10 @@ agente; arquivos próprios também podem manter uma pasta após a desmarcação.
39
39
  | `E_MANAGED_BLOCK` | Revise marcadores ou formato; preserve suas instruções. |
40
40
  | `E_CHANGED` | Um arquivo mudou durante a operação; gere outro plano. |
41
41
 
42
+ No macOS, `/tmp` e `/var/folders` são aceitos quando forem os aliases de
43
+ sistema esperados. Destinos que atravessam links humanos continuam recusados,
44
+ inclusive quando apontados por `TMPDIR`, `TEMP` ou `TMP`.
45
+
42
46
  ## Conflitos e backups
43
47
 
44
48
  Simule antes de substituir uma alteração própria:
@@ -52,9 +56,10 @@ Compare cada `conflicts` com o que deseja manter. Edite a origem em
52
56
  recurso. Use `sync` para atualizar suas derivações. `update` reaplica os
53
57
  recursos do pacote invocado e pode conflitar com essa personalização.
54
58
 
55
- Quando a substituição for intencional, repita com `--force`. Arquivos
56
- existentes afetados recebem backup antes da escrita, inclusive em operações
57
- normais. O campo `backup` informa uma pasta relativa ao projeto:
59
+ `--force` não substitui arquivos próprios nem blocos alterados. Resolva o
60
+ conflito preservando o conteúdo que deseja manter e confira novamente o plano.
61
+ Arquivos gerenciados afetados recebem backup antes da escrita, inclusive em
62
+ operações normais. O campo `backup` informa uma pasta relativa ao projeto:
58
63
 
59
64
  ```text
60
65
  .agent-tests/agentdevkit/backups/<identificador>/
@@ -101,9 +106,10 @@ ou caminhos já gerenciados em links.
101
106
  Destinos dentro de `.git`, `node_modules` ou `.pbai4devs` são recusados,
102
107
  inclusive com variações de maiúsculas/minúsculas.
103
108
 
104
- Na CLI e no script `sincronizar-agentes`, `--force` repara conflitos gerenciados
105
- com backup. Recursos nativos humanos e links preservados continuam intactos.
106
- Para manter a instalação e sua seleção de clientes, prefira a CLI.
109
+ No script `sincronizar-agentes`, `--force` repara conflitos gerenciados com
110
+ backup. A CLI bloqueia conflitos de propriedade mesmo com `--force`. Recursos
111
+ nativos humanos e links preservados continuam intactos. Para manter a instalação
112
+ e sua seleção de clientes, prefira a CLI.
107
113
 
108
114
  ## Download, plataforma ou ferramenta falhando
109
115
 
@@ -28,6 +28,9 @@ description: Incrementos verificáveis do AgentDevKit.
28
28
  - [Ignore de clientes](client-ignore/SPEC.md): padrão local e exceções do projeto.
29
29
  - [Ferramenta e versões](tool-release/SPEC.md): atualização segura e tags de release.
30
30
  - [Tags automáticas](automatic-release-tags/SPEC.md): publicação após CI aprovada.
31
+ - [Destinos temporários](target-paths/SPEC.md): caminhos corretos e links seguros.
32
+ - [Ordem antes da implementação](preimplementation-order/SPEC.md): produto,
33
+ spec e configuração antes do código.
31
34
  - [Descoberta e preservação](discovery-preservation/SPEC.md): rotas de skills,
32
35
  sincronização segura e validação em projetos reais.
33
36
 
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: Orientação e verificação da ordem anterior à implementação.
3
+ ---
4
+
5
+ # Ordem antes da implementação
6
+
7
+ O projeto deve partir de decisões de produto e design, passar por roadmap e
8
+ spec e concluir a configuração técnica antes do código da aplicação.
9
+
10
+ ## Critérios de aceite
11
+
12
+ - AC-001: `next` orienta roadmap e spec antes da configuração técnica e não
13
+ presume aprovação ou escolhe uma spec ambígua.
14
+ - AC-002: A verificação prévia informa bloqueio se o estado de preparação ou
15
+ configuração estiver ausente, inválido ou pendente; ela não modifica Git.
16
+ - AC-003: As skills exigem provas da infraestrutura antes de implementar e
17
+ registram como pendentes as provas de uma aplicação que ainda não existe.
18
+ - AC-004: Projetos existentes, arquivos humanos, links e índice são
19
+ preservados; testes e revisão cobrem decisões, lacunas e retomada.
20
+
21
+ O status registrado não comprova a evidência por si só. Instruções e scripts
22
+ não impedem escrita arbitrária por um agente que os ignore; essa proteção
23
+ depende das permissões ou hooks do cliente usado.
@@ -0,0 +1,19 @@
1
+ ---
2
+ description: Resolução segura dos destinos de instalação.
3
+ ---
4
+
5
+ # Destinos de instalação
6
+
7
+ O instalador deve manter o caminho solicitado e preservar projetos existentes,
8
+ inclusive quando trabalha com diretórios temporários no macOS.
9
+
10
+ ## Critérios de aceite
11
+
12
+ - AC-001: Destinos inexistentes preservam todos os nomes e sua ordem,
13
+ inclusive quando o primeiro componente está diretamente sob a raiz.
14
+ - AC-002: Os aliases temporários de sistema esperados no macOS funcionam para
15
+ instalação e consulta sem modificar o índice ou o `.gitignore` do projeto.
16
+ - AC-003: Destinos que atravessam links humanos são recusados, inclusive os
17
+ apontados por variáveis de ambiente; caminhos reservados continuam protegidos.
18
+ - AC-004: Testes de regressão, revisão independente e CI cobrem os cenários
19
+ pertinentes em Linux, Windows e macOS.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pathbit/agentdevkit",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "description": "Shared skills, specifications and workflows for AI coding agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/i18n.mjs CHANGED
@@ -12,7 +12,7 @@ const messages = {
12
12
  preserved: 'Caminhos existentes preservados', preservedDetails: 'Use --json para consultar os caminhos e motivos.',
13
13
  help: 'AgentDevKit: init [diretório], update, doctor, sync [--check], lang [pt|en|es], agents [lista], next, tools <ação>.\nOpções: --target <diretório>, --lang <idioma>, --agents <lista>, --yes, --dry-run, --force, --json.\nAliases: --clients e clients.',
14
14
  clientsCurrent: 'Agentes selecionados', clientsAvailable: 'Agentes disponíveis', clientsSelect: 'Agentes (IDs separados por vírgula ou all) [all]: ', agents: 'Seleção de agentes atualizada.',
15
- next: 'Próximo passo', init: 'Estrutura instalada.', update: 'Estrutura atualizada.', sync: 'Apontamentos sincronizados.', lang: 'Idioma configurado.', unchanged: 'Estrutura já atualizada.', plan: 'Plano calculado; nenhum arquivo alterado.', conflict: 'Há arquivos próprios ou alterados em conflito. Revise o plano; --force salva backup antes de substituir.',
15
+ next: 'Próximo passo', init: 'Estrutura instalada.', update: 'Estrutura atualizada.', sync: 'Apontamentos sincronizados.', lang: 'Idioma configurado.', unchanged: 'Estrutura já atualizada.', plan: 'Plano calculado; nenhum arquivo alterado.', conflict: 'Há arquivos próprios ou alterados em conflito. Revise o plano e resolva os conflitos; --force não substitui conteúdo próprio.',
16
16
  healthy: 'Estrutura verificada.', unhealthy: 'A estrutura precisa de ajustes.', missing: 'AgentDevKit ainda não foi inicializado neste diretório.', error: 'Não foi possível concluir a operação.', invalid: 'Argumentos inválidos.', language: 'Idioma atual', version: 'Versão', select: 'Idioma / Language / Idioma [pt/en/es] (pt): ',
17
17
  project: 'Projeto', readme: 'AgentDevKit instalado. Leia AGENTS.md e siga as etapas de produto, design e implementação.', state: 'Estado local do AgentDevKit e configuração compartilhada do projeto.',
18
18
  agentLanguage: 'Responda em português, salvo quando o usuário solicitar outro idioma.', inspect: 'Verifique a instalação', workflow: 'Consulte a próxima etapa', install: 'Inicialize este projeto', repair: 'Confira a sincronização', review: 'Revise os arquivos em conflito', tools: 'Resultado da ferramenta',
@@ -23,7 +23,7 @@ const messages = {
23
23
  preserved: 'Existing paths preserved', preservedDetails: 'Use --json to inspect paths and reasons.',
24
24
  help: 'AgentDevKit: init [directory], update, doctor, sync [--check], lang [pt|en|es], agents [list], next, tools <action>.\nOptions: --target <directory>, --lang <language>, --agents <list>, --yes, --dry-run, --force, --json.\nAliases: --clients and clients.',
25
25
  clientsCurrent: 'Selected agents', clientsAvailable: 'Available agents', clientsSelect: 'Agents (comma-separated IDs or all) [all]: ', agents: 'Agent selection updated.',
26
- next: 'Next step', init: 'Structure installed.', update: 'Structure updated.', sync: 'Pointers synchronized.', lang: 'Language configured.', unchanged: 'Structure is already current.', plan: 'Plan calculated; no files changed.', conflict: 'User files or edits conflict with this plan. Review it; --force backs them up before replacement.',
26
+ next: 'Next step', init: 'Structure installed.', update: 'Structure updated.', sync: 'Pointers synchronized.', lang: 'Language configured.', unchanged: 'Structure is already current.', plan: 'Plan calculated; no files changed.', conflict: 'User files or edits conflict with this plan. Review and resolve the conflicts; --force does not replace user content.',
27
27
  healthy: 'Structure verified.', unhealthy: 'The structure needs attention.', missing: 'AgentDevKit has not been initialized in this directory.', error: 'The operation could not be completed.', invalid: 'Invalid arguments.', language: 'Current language', version: 'Version', select: 'Idioma / Language / Idioma [pt/en/es] (pt): ',
28
28
  project: 'Project', readme: 'AgentDevKit installed. Read AGENTS.md and follow the product, design and implementation stages.', state: 'AgentDevKit state and shared project configuration.',
29
29
  agentLanguage: 'Respond in English unless the user requests another language.', inspect: 'Check the installation', workflow: 'Review the next stage', install: 'Initialize this project', repair: 'Check synchronization', review: 'Review conflicting files', tools: 'Tool result',
@@ -34,7 +34,7 @@ const messages = {
34
34
  preserved: 'Rutas existentes preservadas', preservedDetails: 'Use --json para consultar las rutas y los motivos.',
35
35
  help: 'AgentDevKit: init [directorio], update, doctor, sync [--check], lang [pt|en|es], agents [lista], next, tools <acción>.\nOpciones: --target <directorio>, --lang <idioma>, --agents <lista>, --yes, --dry-run, --force, --json.\nAlias: --clients y clients.',
36
36
  clientsCurrent: 'Agentes seleccionados', clientsAvailable: 'Agentes disponibles', clientsSelect: 'Agentes (IDs separados por coma o all) [all]: ', agents: 'Selección de agentes actualizada.',
37
- next: 'Próximo paso', init: 'Estructura instalada.', update: 'Estructura actualizada.', sync: 'Referencias sincronizadas.', lang: 'Idioma configurado.', unchanged: 'La estructura ya está actualizada.', plan: 'Plan calculado; ningún archivo modificado.', conflict: 'Hay archivos propios o cambios en conflicto. Revise el plan; --force guarda una copia antes de reemplazarlos.',
37
+ next: 'Próximo paso', init: 'Estructura instalada.', update: 'Estructura actualizada.', sync: 'Referencias sincronizadas.', lang: 'Idioma configurado.', unchanged: 'La estructura ya está actualizada.', plan: 'Plan calculado; ningún archivo modificado.', conflict: 'Hay archivos propios o cambios en conflicto. Revise y resuelva los conflictos; --force no reemplaza contenido propio.',
38
38
  healthy: 'Estructura verificada.', unhealthy: 'La estructura necesita ajustes.', missing: 'AgentDevKit no se ha inicializado en este directorio.', error: 'No se pudo completar la operación.', invalid: 'Argumentos inválidos.', language: 'Idioma actual', version: 'Versión', select: 'Idioma / Language / Idioma [pt/en/es] (pt): ',
39
39
  project: 'Proyecto', readme: 'AgentDevKit instalado. Lea AGENTS.md y siga las etapas de producto, diseño e implementación.', state: 'Estado de AgentDevKit y configuración compartida del proyecto.',
40
40
  agentLanguage: 'Responde en español salvo que el usuario solicite otro idioma.', inspect: 'Compruebe la instalación', workflow: 'Consulte la siguiente etapa', install: 'Inicialice este proyecto', repair: 'Compruebe la sincronización', review: 'Revise los archivos en conflicto', tools: 'Resultado de la herramienta',
package/src/install.mjs CHANGED
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import { chmodSync, closeSync, existsSync, fstatSync, lstatSync, openSync, readFileSync, readSync, writeFileSync, readdirSync, mkdirSync, mkdtempSync, realpathSync, renameSync, rmSync, rmdirSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
- import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
5
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
6
6
  import { synchronize, isReference } from '../.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs';
7
7
  import { isGeneratedOutput, planLocalIgnore, checkLocalIgnore, assertLocalIgnoreUnchanged, applyLocalIgnore, indexContentMatches } from '../.pbai4devs/scripts/lib/gitignore.mjs';
8
8
  import { nativePathBoundary } from '../.pbai4devs/scripts/lib/native-paths.mjs';
@@ -138,12 +138,28 @@ const checkTarget = path => {
138
138
  if (path.split(sep).some(part => reservedTargets.has(part.toLowerCase()))) fail('E_TARGET', 'Unsafe target directory.');
139
139
  };
140
140
 
141
+ // Only normalize macOS aliases owned by the system. Environment-controlled
142
+ // TMPDIR is not evidence that a symbolic link is safe to follow.
143
+ function normalizeSystemTemporaryAlias(target) {
144
+ if (process.platform !== 'darwin') return target;
145
+ for (const { alias, allowed, physical } of [
146
+ { alias: '/tmp', allowed: '/tmp', physical: '/private/tmp' },
147
+ { alias: '/var', allowed: '/var/folders', physical: '/private/var' },
148
+ ]) {
149
+ if (target !== allowed && !target.startsWith(`${allowed}${sep}`)) continue;
150
+ const link = stat(alias), directory = stat(physical);
151
+ if (!link?.isSymbolicLink() || link.uid !== 0 || !directory?.isDirectory() || directory.uid !== 0) return target;
152
+ try { if (realpathSync(alias) !== physical) return target; }
153
+ catch (error) { if (error.code === 'ENOENT' || error.code === 'ELOOP') return target; throw error; }
154
+ return join(physical, relative(alias, target));
155
+ }
156
+ return target;
157
+ }
158
+
141
159
  export function resolveTarget(input, cwd = process.cwd()) {
142
160
  let target = resolve(cwd, input || '.');
143
161
  checkTarget(target);
144
- // The OS may expose its own temporary directory through a system alias.
145
- const temporary = resolve(tmpdir()), insideTemporary = relative(temporary, target);
146
- if (insideTemporary === '' || (!insideTemporary.startsWith(`..${sep}`) && insideTemporary !== '..' && !isAbsolute(insideTemporary))) target = join(realpathSync(temporary), insideTemporary);
162
+ target = normalizeSystemTemporaryAlias(target);
147
163
  let ancestor = target;
148
164
  while (true) {
149
165
  const info = stat(ancestor);
@@ -151,7 +167,7 @@ export function resolveTarget(input, cwd = process.cwd()) {
151
167
  const parent = dirname(ancestor); if (parent === ancestor) break; ancestor = parent;
152
168
  }
153
169
  const missing = []; let parent = target;
154
- while (!stat(parent)) { missing.unshift(parent.slice(dirname(parent).length + 1)); parent = dirname(parent); }
170
+ while (!stat(parent)) { missing.unshift(basename(parent)); parent = dirname(parent); }
155
171
  const info = stat(parent);
156
172
  if (info.isSymbolicLink() || !info.isDirectory()) fail('E_TARGET', 'Target must be a real directory, without a symbolic link.');
157
173
  const canonical = join(realpathSync(parent), ...missing);
@@ -441,7 +457,14 @@ function replaceJsonMember(bytes, member, value) {
441
457
  fail('E_STATE', 'Invalid JSON member separator.');
442
458
  }
443
459
  const matches = properties.filter(item => item.key === member);
444
- if (matches.length !== 1) fail('E_STATE', 'Ambiguous derived state member.');
460
+ if (matches.length > 1) fail('E_STATE', 'Ambiguous state member.');
461
+ if (!matches.length) {
462
+ if (value === undefined) return bytes;
463
+ const insertion = `${properties.length ? ',' : ''}\n ${JSON.stringify(member)}: ${JSON.stringify(value, null, 2)}\n`;
464
+ const next = text.slice(0, i) + insertion + text.slice(i);
465
+ try { JSON.parse(next); } catch { fail('E_STATE', 'Unable to preserve state safely.'); }
466
+ return Buffer.from(next);
467
+ }
445
468
  const current = matches[0], index = properties.indexOf(current);
446
469
  let next;
447
470
  if (value !== undefined) next = text.slice(0, current.valueStart) + JSON.stringify(value, null, 2) + text.slice(current.end);
@@ -452,29 +475,44 @@ function replaceJsonMember(bytes, member, value) {
452
475
  return Buffer.from(next);
453
476
  }
454
477
 
478
+ function reconcileStateBytes(original, previous, next) {
479
+ if (!original) return Buffer.from(`${JSON.stringify(next, null, 2)}\n`);
480
+ const canonical = value => value && typeof value === 'object' && !Array.isArray(value)
481
+ ? Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])]))
482
+ : Array.isArray(value) ? value.map(canonical) : value;
483
+ let result = original;
484
+ for (const key of ['schemaVersion', 'version', 'language', 'clients', 'files', 'blocks']) {
485
+ if (JSON.stringify(canonical(previous?.[key])) !== JSON.stringify(canonical(next[key]))) result = replaceJsonMember(result, key, next[key]);
486
+ }
487
+ return result;
488
+ }
489
+
455
490
  function readDerivedState(target) {
456
- let entries = {}, outputs = {}, blocks = {}, files = {}, removedBlocks = [], removedFiles = {};
491
+ let entries = {}, outputs = {}, blocks = {}, files = {}, catalogs = {}, config = null, removedBlocks = [], removedFiles = {};
457
492
  const derived = read(target, actualStatePath(target).replace(/agentdevkit\.json$/, 'derived.json'));
458
493
  if (derived) {
459
494
  try {
460
495
  const value = JSON.parse(derived);
461
496
  if (value.schemaVersion !== 1 || !value.entries || typeof value.entries !== 'object' || Array.isArray(value.entries)
462
- || ['outputs', 'blocks', 'files', 'removedFiles'].some(key => value[key] !== undefined && (!value[key] || typeof value[key] !== 'object' || Array.isArray(value[key])))) fail('E_STATE', 'Invalid derived entrypoint state.');
463
- entries = value.entries; outputs = value.outputs ?? {}; blocks = value.blocks ?? {}; files = value.files ?? {}; removedFiles = value.removedFiles ?? {};
497
+ || ['outputs', 'blocks', 'files', 'catalogs', 'removedFiles'].some(key => value[key] !== undefined && (!value[key] || typeof value[key] !== 'object' || Array.isArray(value[key])))) fail('E_STATE', 'Invalid derived entrypoint state.');
498
+ entries = value.entries; outputs = value.outputs ?? {}; blocks = value.blocks ?? {}; files = value.files ?? {}; catalogs = value.catalogs ?? {}; removedFiles = value.removedFiles ?? {};
499
+ if (value.config !== undefined && (typeof value.config !== 'string' || !/^[a-f0-9]{64}$/.test(value.config))) fail('E_STATE', 'Invalid derived configuration state.');
500
+ config = value.config ?? null;
464
501
  removedBlocks = value.removedBlocks ?? [];
465
502
  if (!Array.isArray(removedBlocks) || removedBlocks.some(path => typeof path !== 'string' || !nativeManagedText(path))) fail('E_STATE', 'Invalid removed block state.');
466
503
  for (const [path, digest] of Object.entries(entries)) if (!canonicalEntrypoint(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived entrypoint state.');
467
504
  for (const [path, digest] of Object.entries(outputs)) if (!allowedManaged(path) || !/^\.[^/]+\/(?:skills|agents|rules)\.json$/.test(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived output state.');
468
- for (const [path, digest] of Object.entries(blocks)) if (!(nativeManagedText(path) || adapterRoots.some(root => path === `${root}/.gitignore`)) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived block state.');
505
+ for (const [path, digest] of Object.entries(blocks)) if (!(nativeManagedText(path) || path === '.pbai4devs/.gitignore' || adapterRoots.some(root => path === `${root}/.gitignore`)) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived block state.');
469
506
  for (const [path, digest] of Object.entries(files)) if (!allowedManaged(path) || !/\.(?:md|mdc|toml)$/.test(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived file state.');
507
+ for (const [path, digest] of Object.entries(catalogs)) if (!/^\.pbai4devs\/(?:catalog|skills|agents|rules)\.json$/.test(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived catalogue state.');
470
508
  for (const [path, digest] of Object.entries(removedFiles)) if (canonicalEntrypoint(path) || !(nativeMarkdown(path) || nativeToml(path)) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid removed file state.');
471
509
  const installed = loadState(target);
472
510
  if (Object.entries(removedFiles).some(([path, digest]) => installed?.files?.[path] && installed.files[path] !== digest)) fail('E_STATE', 'Removed file state differs from installation.');
473
511
  }
474
512
  catch { fail('E_STATE', 'Invalid derived entrypoint state.'); }
475
- for (const path of [...Object.keys(entries), ...Object.keys(outputs), ...Object.keys(blocks), ...Object.keys(files), ...Object.keys(removedFiles), ...removedBlocks]) safePath(target, path);
513
+ for (const path of [...Object.keys(entries), ...Object.keys(outputs), ...Object.keys(blocks), ...Object.keys(files), ...Object.keys(catalogs), ...Object.keys(removedFiles), ...removedBlocks]) safePath(target, path);
476
514
  }
477
- return { entries, outputs, blocks, files, removedBlocks, removedFiles };
515
+ return { entries, outputs, blocks, files, catalogs, config, removedBlocks, removedFiles };
478
516
  }
479
517
 
480
518
  export function planInstallation({ target, language = 'pt', mode = 'init', force = false, clients }) {
@@ -495,11 +533,12 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
495
533
  if (original === null ? content !== null : !content?.equals(original)) fail('E_CHANGED', `File changed while planning: ${path}`);
496
534
  } else nativeInputs.set(path, content);
497
535
  };
498
- const { entries: derivedEntries, outputs: derivedOutputs, blocks: derivedBlocks, files: derivedFiles, removedBlocks, removedFiles } = readDerivedState(target);
536
+ const { entries: derivedEntries, outputs: derivedOutputs, blocks: derivedBlocks, files: derivedFiles, catalogs: derivedCatalogs, config: derivedConfig, removedBlocks, removedFiles } = readDerivedState(target);
537
+ const derivedConfigFiles = derivedConfig ? { '.pbai4devs/espelhos.conf': derivedConfig } : {};
499
538
  if (previous) previous = { ...previous, files: Object.fromEntries(Object.entries(previous.files).filter(([path, digest]) => removedFiles[path] !== digest)) };
500
539
  const previousBlocks = Object.fromEntries(Object.entries(previous?.blocks || {}).filter(([path]) => !removedBlocks.includes(path)));
501
540
  const knownBlocks = { ...previousBlocks, ...derivedBlocks };
502
- const ownedPaths = [...Object.keys(previous?.files || {}), ...Object.keys(previousBlocks), ...Object.keys(derivedEntries), ...Object.keys(derivedOutputs), ...Object.keys(derivedBlocks), ...Object.keys(derivedFiles)];
541
+ const ownedPaths = [...Object.keys(previous?.files || {}), ...Object.keys(previousBlocks), ...Object.keys(derivedEntries), ...Object.keys(derivedOutputs), ...Object.keys(derivedBlocks), ...Object.keys(derivedFiles), ...Object.keys(derivedCatalogs), ...Object.keys(derivedConfigFiles)];
503
542
  const entryPaths = new Map(entryNames.map(name => [name, resolveEntrypoint(target, name)]));
504
543
  const blockHash = path => knownBlocks[path]
505
544
  ?? (canonicalEntrypoint(path) ? Object.entries(knownBlocks).find(([candidate]) => canonicalEntrypoint(candidate) === canonicalEntrypoint(path))?.[1] : undefined);
@@ -581,7 +620,17 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
581
620
  const ownIgnore = read(target, '.pbai4devs/.gitignore');
582
621
  if (ownIgnore) write(stage, '.pbai4devs/.gitignore', ownIgnore);
583
622
  writeAdapterManifest(stage, { clients: selected });
584
- const synchronized = synchronize({ root: stage, force: true, localIgnores: false, generateEntrypoints: false, recordDerivedState: false, outputBlocked, output() {} });
623
+ // Supply the already-validated ownership records to the private stage so
624
+ // the synchronizer can distinguish an owned ignore block from a user's.
625
+ // These temporary records never replace or bypass checks on the target.
626
+ const stagingState = '.pbai4devs/state/agentdevkit.json', stagingDerived = '.pbai4devs/state/derived.json';
627
+ if (previous) write(stage, stagingState, JSON.stringify(previous));
628
+ if (Object.keys(derivedEntries).length || Object.keys(derivedOutputs).length || Object.keys(derivedBlocks).length || Object.keys(derivedFiles).length || Object.keys(derivedCatalogs).length || derivedConfig || removedBlocks.length || Object.keys(removedFiles).length) {
629
+ write(stage, stagingDerived, JSON.stringify({ schemaVersion: 1, entries: derivedEntries, outputs: derivedOutputs, blocks: derivedBlocks, files: derivedFiles, catalogs: derivedCatalogs, ...(derivedConfig ? { config: derivedConfig } : {}), removedBlocks, removedFiles }));
630
+ }
631
+ let synchronized;
632
+ try { synchronized = synchronize({ root: stage, force: true, localIgnores: false, generateEntrypoints: false, recordDerivedState: false, outputBlocked, output() {} }); }
633
+ finally { rmSync(join(stage, stagingState), { force: true }); rmSync(join(stage, stagingDerived), { force: true }); }
585
634
  for (const canonical of active.entrypoints) {
586
635
  const name = entryPaths.get(canonical);
587
636
  if (preserveNative(name)) continue;
@@ -657,14 +706,14 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
657
706
  // A standalone sync records its proven outputs separately. During a later
658
707
  // check/sync, compare that overlay before declaring the installation state
659
708
  // stale; update still consolidates it into the main state transactionally.
660
- let stateContent = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
709
+ let stateContent = reconcileStateBytes(installedState ? read(target, actualStatePath(target)) : null, installedState, manifest);
661
710
  if (mode === 'sync' && installedState && !changingClients) {
662
711
  const files = { ...installedState.files }, blocks = { ...installedState.blocks };
663
712
  for (const [path, digest] of Object.entries(removedFiles)) {
664
713
  if (files[path] === digest && !stat(safePath(target, path))) delete files[path];
665
714
  }
666
715
  for (const path of removedBlocks) delete blocks[path];
667
- for (const [path, digest] of Object.entries({ ...derivedFiles, ...derivedOutputs, ...derivedEntries })) { files[path] = digest; delete blocks[path]; }
716
+ for (const [path, digest] of Object.entries({ ...derivedFiles, ...derivedOutputs, ...derivedEntries, ...derivedCatalogs, ...derivedConfigFiles })) { files[path] = digest; delete blocks[path]; }
668
717
  for (const [path, digest] of Object.entries(derivedBlocks)) { blocks[path] = digest; delete files[path]; }
669
718
  const same = (a, b) => {
670
719
  const keys = Object.keys(a).sort();
@@ -676,23 +725,70 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
676
725
  desired.set(statePath, stateContent);
677
726
  const derivedPath = actualStatePath(target).replace(/agentdevkit\.json$/, 'derived.json');
678
727
  const derivedBytes = !migration ? read(target, derivedPath) : null;
679
- if (derivedBytes && Object.keys(removedFiles).length) {
728
+ let nextDerivedBytes = derivedBytes;
729
+ if (nextDerivedBytes && Object.keys(removedFiles).length) {
680
730
  const finalState = JSON.parse(stateContent);
681
731
  const remaining = Object.fromEntries(Object.entries(removedFiles).filter(([path, digest]) =>
682
732
  finalState.files[path] === digest && !stat(safePath(target, path))));
683
733
  if (Object.keys(remaining).length !== Object.keys(removedFiles).length) {
684
734
  observeNative(derivedPath, derivedBytes);
685
- desired.set(derivedPath, replaceJsonMember(derivedBytes, 'removedFiles', Object.keys(remaining).length ? remaining : undefined));
735
+ nextDerivedBytes = replaceJsonMember(nextDerivedBytes, 'removedFiles', Object.keys(remaining).length ? remaining : undefined);
736
+ }
737
+ }
738
+ if (nextDerivedBytes && Object.keys(derivedCatalogs).length) {
739
+ const finalState = JSON.parse(stateContent);
740
+ const remaining = Object.fromEntries(Object.entries(derivedCatalogs).filter(([path]) => {
741
+ const content = desired.get(path);
742
+ return !content || finalState.files[path] !== hash(content);
743
+ }));
744
+ if (Object.keys(remaining).length !== Object.keys(derivedCatalogs).length) {
745
+ observeNative(derivedPath, derivedBytes);
746
+ nextDerivedBytes = replaceJsonMember(nextDerivedBytes, 'catalogs', Object.keys(remaining).length ? remaining : undefined);
747
+ }
748
+ }
749
+ if (nextDerivedBytes && Object.keys(derivedBlocks).length) {
750
+ const finalState = JSON.parse(stateContent);
751
+ const remaining = Object.fromEntries(Object.entries(derivedBlocks).filter(([path, digest]) => {
752
+ const content = desired.get(path);
753
+ // A deselected generated block is removed later in this transaction.
754
+ // Drop its overlay only if its current bytes are still proven; edited
755
+ // blocks remain conflicts and no metadata is committed on failure.
756
+ if (!content && !canonicalEntrypoint(path) && !finalState.blocks[path] && !finalState.files[path]) {
757
+ const current = read(target, path);
758
+ return Boolean(current && blockDigest(path, current) !== digest);
759
+ }
760
+ return !content || finalState.blocks[path] !== blockDigest(path, content);
761
+ }));
762
+ if (Object.keys(remaining).length !== Object.keys(derivedBlocks).length) {
763
+ observeNative(derivedPath, derivedBytes);
764
+ nextDerivedBytes = replaceJsonMember(nextDerivedBytes, 'blocks', Object.keys(remaining).length ? remaining : undefined);
686
765
  }
687
766
  }
767
+ if (nextDerivedBytes && derivedConfig) {
768
+ const finalState = JSON.parse(stateContent), path = '.pbai4devs/espelhos.conf', content = desired.get(path);
769
+ if (content && finalState.files[path] === hash(content)) {
770
+ observeNative(derivedPath, derivedBytes);
771
+ nextDerivedBytes = replaceJsonMember(nextDerivedBytes, 'config', undefined);
772
+ }
773
+ }
774
+ if (nextDerivedBytes && !nextDerivedBytes.equals(derivedBytes)) desired.set(derivedPath, nextDerivedBytes);
688
775
  const changes = [], conflicts = [];
689
776
  const aliasedEntrypoint = path => canonicalEntrypoint(path) && entryPaths.get(canonicalEntrypoint(path)) !== path;
777
+ const generatedInternal = new Set(['.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/agents.json', '.pbai4devs/rules.json', '.pbai4devs/espelhos.conf']);
690
778
  for (const [path, content] of desired) {
691
779
  const current = read(target, path);
692
780
  if (nativeInputs.has(path) && (nativeInputs.get(path) === null ? current !== null : !current?.equals(nativeInputs.get(path)))) fail('E_CHANGED', `File changed while planning: ${path}`);
693
- if (current?.equals(content)) continue;
781
+ const wholeFileOwned = current && [previous?.files[path], derivedOutputs[path], derivedEntries[path], derivedFiles[path], derivedCatalogs[path], derivedConfigFiles[path]].some(digest => digest && digest === hash(current));
782
+ const existingBlock = current && (nativeManagedText(path) || canonicalEntrypoint(path) || path.endsWith('/.gitignore')) ? blockDigest(path, current) : null;
783
+ if (existingBlock && !wholeFileOwned && knownBlocks[path] !== existingBlock) conflicts.push(path);
784
+ if (current?.equals(content)) {
785
+ // Identical bytes do not prove ownership of a pre-existing catalogue.
786
+ // Refuse adoption: a later sync could otherwise overwrite user data.
787
+ if (!wholeFileOwned && (generatedInternal.has(path) || native(path) && !existingBlock && !mixedPaths.has(path) && !preserved.has(path))) conflicts.push(path);
788
+ continue;
789
+ }
694
790
  const managed = path === derivedPath && derivedBytes && current?.equals(derivedBytes)
695
- || [previous?.files[path], derivedOutputs[path], derivedEntries[path], derivedFiles[path]].some(digest => digest && hash(current || '') === digest)
791
+ || [previous?.files[path], derivedOutputs[path], derivedEntries[path], derivedFiles[path], derivedCatalogs[path], derivedConfigFiles[path]].some(digest => digest && hash(current || '') === digest)
696
792
  || knownBlocks[path] && blockDigest(path, current || Buffer.alloc(0)) === knownBlocks[path];
697
793
  const appendBlock = mixedPaths.has(path) || canonicalEntrypoint(path) || path === '.gitignore' || path.endsWith('/.gitignore');
698
794
  const conflict = current && path !== statePath && !managed && !appendBlock;
@@ -812,7 +908,9 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
812
908
  const localIgnore = planLocalIgnore(target, { outputs: ownedOutputs, templateRoot: stage });
813
909
  const localIgnoreCheck = checkLocalIgnore(localIgnore, { replacedBlocks: replacedIgnoreBlocks(target, changes, previous) });
814
910
  conflicts.push(...(untrack?.conflicts || []), ...localIgnoreCheck.missing);
815
- 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, fallbackChecks: [...fallbackChecks], fallbackOwnedPaths: ownedPaths, preserved: [...preserved.values()].sort((a, b) => a.path.localeCompare(b.path)) };
911
+ // A conflict means ownership or the managed bytes could not be proven.
912
+ // Backups do not authorize replacing user content, even with --force.
913
+ return { target, language, mode, changes, conflicts: [...new Set(conflicts)].sort(), canApply: !conflicts.length && !untrack?.conflicts?.length && !localIgnoreCheck.missing.length, manifest, retiredRoots, ownedOutputs, untrack, localIgnore, localIgnoreCheck, migration, fallbackChecks: [...fallbackChecks], fallbackOwnedPaths: ownedPaths, preserved: [...preserved.values()].sort((a, b) => a.path.localeCompare(b.path)) };
816
914
  } finally { rmSync(stage, { recursive: true, force: true }); }
817
915
  }
818
916
 
@@ -1003,7 +1101,7 @@ export function inspect(target) {
1003
1101
  if (content) issues.push({ code: 'modified', path });
1004
1102
  continue;
1005
1103
  }
1006
- if (!content || hash(content) !== digest && hash(content) !== derived.outputs[path] && hash(content) !== derived.files[path] && hash(content) !== derived.entries[path]) issues.push({ code: content ? 'modified' : 'missing', path });
1104
+ if (!content || hash(content) !== digest && hash(content) !== derived.outputs[path] && hash(content) !== derived.files[path] && hash(content) !== derived.entries[path] && hash(content) !== derived.catalogs[path] && !(path === '.pbai4devs/espelhos.conf' && hash(content) === derived.config)) issues.push({ code: content ? 'modified' : 'missing', path });
1007
1105
  }
1008
1106
  for (const [path, digest] of Object.entries({ ...state.blocks, ...derived.blocks })) {
1009
1107
  if (derived.removedBlocks.includes(path) && !Object.hasOwn(derived.blocks, path)) continue;
package/src/next.mjs CHANGED
@@ -8,9 +8,11 @@ const messages = {
8
8
  install: 'Instale o AgentDevKit neste projeto.', verify: 'Confira a instalação.',
9
9
  configure: 'Conclua a preparação inicial.', prepare: 'Prepare o ambiente existente.',
10
10
  prd: 'Defina ou conclua o PRD.', ux: 'Defina UX/UI ou justifique sua não aplicação.',
11
- design: 'Conclua o design técnico.', technical: 'Conclua a configuração técnica.',
12
- spec: 'Defina a próxima spec.', roadmap: 'Revise o roadmap e escolha o próximo incremento.',
11
+ design: 'Conclua o design técnico.',
12
+ spec: 'Defina a próxima spec.', implement: 'Planeje a implementação da spec.', roadmap: 'Revise o roadmap e escolha o próximo incremento.',
13
13
  inspect: 'Confira o estado do projeto antes de avançar.',
14
+ incomplete: 'O inventário do projeto está incompleto.',
15
+ incompleteInstruction: 'Inspecione manualmente a estrutura e os documentos antes de escolher uma etapa; não presuma que o projeto está vazio.',
14
16
  ask: 'Use a skill indicada no agente; confirme as decisões ainda pendentes.',
15
17
  existing: 'Preserve código e decisões existentes; não reinicie a definição do produto.',
16
18
  },
@@ -18,9 +20,11 @@ const messages = {
18
20
  install: 'Install AgentDevKit in this project.', verify: 'Check the installation.',
19
21
  configure: 'Complete the initial preparation.', prepare: 'Prepare the existing environment.',
20
22
  prd: 'Define or complete the PRD.', ux: 'Define UX/UI or document why it does not apply.',
21
- design: 'Complete the technical design.', technical: 'Complete the technical configuration.',
22
- spec: 'Define the next spec.', roadmap: 'Review the roadmap and select the next increment.',
23
+ design: 'Complete the technical design.',
24
+ spec: 'Define the next spec.', implement: 'Plan the spec implementation.', roadmap: 'Review the roadmap and select the next increment.',
23
25
  inspect: 'Inspect the project before proceeding.',
26
+ incomplete: 'The project inventory is incomplete.',
27
+ incompleteInstruction: 'Inspect the structure and documents manually before choosing a step; do not assume the project is empty.',
24
28
  ask: 'Use the indicated skill in your agent; confirm outstanding decisions.',
25
29
  existing: 'Preserve existing code and decisions; do not restart product discovery.',
26
30
  },
@@ -28,9 +32,11 @@ const messages = {
28
32
  install: 'Instale AgentDevKit en este proyecto.', verify: 'Compruebe la instalación.',
29
33
  configure: 'Complete la preparación inicial.', prepare: 'Prepare el entorno existente.',
30
34
  prd: 'Defina o complete el PRD.', ux: 'Defina UX/UI o justifique que no se aplica.',
31
- design: 'Complete el diseño técnico.', technical: 'Complete la configuración técnica.',
32
- spec: 'Defina la próxima spec.', roadmap: 'Revise el roadmap y elija el próximo incremento.',
35
+ design: 'Complete el diseño técnico.',
36
+ spec: 'Defina la próxima spec.', implement: 'Planifique la implementación de la spec.', roadmap: 'Revise el roadmap y elija el próximo incremento.',
33
37
  inspect: 'Inspeccione el proyecto antes de continuar.',
38
+ incomplete: 'El inventario del proyecto está incompleto.',
39
+ incompleteInstruction: 'Inspeccione manualmente la estructura y los documentos antes de elegir una etapa; no suponga que el proyecto está vacío.',
34
40
  ask: 'Use la skill indicada en su agente y confirme las decisiones pendientes.',
35
41
  existing: 'Preserve el código y las decisiones existentes; no reinicie el producto.',
36
42
  },
@@ -48,14 +54,21 @@ function commandLine(args, platform) {
48
54
  function workflow(report) {
49
55
  const state = report.maturity;
50
56
  const app = state.signals.application;
57
+ if (report.scan.truncated) return ['incomplete', null];
51
58
  if (app && (!state.configuration.present || !state.planning.ready)) return ['prepare', 'preparar-dev'];
52
59
  if (state.configuration.preparacao !== 'concluido') return app ? ['prepare', 'preparar-dev'] : ['configure', 'configurar-projeto'];
53
60
  if (state.planning.prd.status !== 'aprovado') return ['prd', 'criar-prd'];
54
61
  if (!['aprovado', 'nao-aplicavel'].includes(state.planning.ux_ui.status)) return ['ux', 'criar-ux-ui'];
55
62
  if (state.planning.design.status !== 'aprovado') return ['design', 'criar-design-doc'];
56
- if (state.configuration.configuracao_tecnica !== 'concluido') return ['technical', 'configurar-projeto'];
57
- if (app && !report.contract.complete) return ['prepare', 'preparar-dev'];
58
- return state.level === 'manutencao' ? ['roadmap', 'gerenciar-roadmap'] : ['spec', 'criar-spec'];
63
+ if (state.level === 'manutencao' || !state.releasesComplete) return ['roadmap', 'gerenciar-roadmap'];
64
+ if (app && state.configuration.configuracao_tecnica === 'concluido' && !report.contract.complete) return ['prepare', 'preparar-dev'];
65
+ const { roadmap, specs } = state.planning;
66
+ if (!roadmap?.present || !['planejado', 'aprovado'].includes(roadmap.status) || !specs?.path || !specs.complete || specs.items.length > 1) return ['roadmap', 'gerenciar-roadmap'];
67
+ if (!specs.items.length) return ['spec', 'criar-spec'];
68
+ const [spec] = specs.items;
69
+ if (!spec.linked || !['open', 'in-progress'].includes(spec.roadmapState) || !spec.status) return ['roadmap', 'gerenciar-roadmap'];
70
+ if (spec.status !== 'aprovado') return ['spec', 'criar-spec'];
71
+ return ['implement', 'implementar-spec'];
59
72
  }
60
73
 
61
74
  export function getNextSteps({ command, target, language = 'pt', installed, platform = process.platform }) {
@@ -68,6 +81,10 @@ export function getNextSteps({ command, target, language = 'pt', installed, plat
68
81
  try { report = inspectProject(target); }
69
82
  catch { return [verification]; }
70
83
  const [key, skill] = workflow(report);
84
+ if (key === 'incomplete') {
85
+ const step = { label: text.incomplete, instruction: text.incompleteInstruction };
86
+ return ['init', 'update', 'sync', 'lang', 'agents', 'tools'].includes(command) ? [verification, step] : [step];
87
+ }
71
88
  const inspector = join(target, '.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs');
72
89
  const step = {
73
90
  label: text[key],