@pathbit/agentdevkit 0.0.21 → 0.0.23

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 (39) hide show
  1. package/.pbai4devs/COMPATIBILIDADE.md +11 -1
  2. package/.pbai4devs/rules/ambiente-local/RULE.md +5 -0
  3. package/.pbai4devs/rules/git/RULE.md +5 -1
  4. package/.pbai4devs/scripts/README.md +1 -0
  5. package/.pbai4devs/scripts/diagnosticar-ambiente/tests/TEST.mjs +60 -1
  6. package/.pbai4devs/scripts/inspecionar-projeto/README.md +6 -0
  7. package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +218 -18
  8. package/.pbai4devs/scripts/lib/adapters.mjs +5 -2
  9. package/.pbai4devs/scripts/lib/runtime.mjs +38 -2
  10. package/.pbai4devs/scripts/sincronizar-agentes/README.md +5 -1
  11. package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +136 -9
  12. package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +30 -0
  13. package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +22 -0
  14. package/.pbai4devs/scripts/testar-maturidade/README.md +12 -10
  15. package/.pbai4devs/scripts/testar-maturidade/SCRIPT.mjs +86 -44
  16. package/.pbai4devs/scripts/testar-maturidade/tests/TEST.mjs +157 -33
  17. package/.pbai4devs/scripts/verificar-configuracao/README.md +22 -0
  18. package/.pbai4devs/scripts/verificar-configuracao/SCRIPT.mjs +63 -0
  19. package/.pbai4devs/scripts/verificar-configuracao/tests/TEST.mjs +145 -0
  20. package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +12 -1
  21. package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +22 -0
  22. package/.pbai4devs/skills/desenvolvimento/implementar-backend/SKILL.md +15 -3
  23. package/.pbai4devs/skills/desenvolvimento/implementar-frontend/SKILL.md +15 -3
  24. package/.pbai4devs/skills/git/git-diff/SKILL.md +3 -1
  25. package/.pbai4devs/skills/git/git-status/SKILL.md +2 -1
  26. package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +13 -8
  27. package/.pbai4devs/skills/projeto/proximos-passos/SKILL.md +4 -2
  28. package/.pbai4devs/skills/specs/implementar-spec/SKILL.md +14 -2
  29. package/.pbai4devs/templates/clients/ADAPTERS.json +3 -2
  30. package/CHANGELOG.md +20 -0
  31. package/docs/guides/cli.md +9 -0
  32. package/docs/guides/troubleshooting.md +4 -0
  33. package/docs/specs/README.md +5 -0
  34. package/docs/specs/discovery-preservation/SPEC.md +26 -0
  35. package/docs/specs/preimplementation-order/SPEC.md +23 -0
  36. package/docs/specs/target-paths/SPEC.md +19 -0
  37. package/package.json +1 -1
  38. package/src/install.mjs +155 -17
  39. package/src/next.mjs +26 -9
@@ -42,20 +42,25 @@ aprovações já dadas.
42
42
  2. Prepare as ferramentas definidas em `.pbai4devs/tools/<nome>/TOOL.json` com
43
43
  `preparar-ferramentas`; confira versões e compatibilidade com SO/arquitetura.
44
44
  Reutilize instalações e lockfiles; não repita init nem troque a stack.
45
- 3. Aplique a [estrutura e o ambiente obrigatórios][estrutura],
46
- preenchendo os modelos de Makefile, Compose e dockerignore. Configure os
47
- serviços reais, healthchecks, portas, volumes e comandos da tecnologia.
45
+ 3. Aplique a [estrutura e o ambiente obrigatórios][estrutura], preenchendo os
46
+ modelos de Makefile, Compose e dockerignore. Configure os serviços de
47
+ infraestrutura necessários, healthchecks, portas, volumes e comandos.
48
+ Antes de concluir a configuração, não implemente código da aplicação;
49
+ `src/` e `tests/` podem existir vazios ou conter apenas configuração.
48
50
  4. Configure VS Code e recomende EditorConfig, sem regras conflitantes.
49
51
  Termine `.gitignore` com o modelo CUSTOM IGNORE e os padrões da tecnologia;
50
52
  confirme que templates, scripts, testes e fixtures continuam versionáveis.
51
53
  5. Documente versões, estrutura, setup, comandos, origem do template/init e
52
54
  decisões no README/estado. Use `atualizar-documentacao` para contribuição,
53
55
  configuração e ADRs aplicáveis. Preserve arquivos e convenções existentes.
54
- 6. Use `verificar-trabalho`: valide Compose, build, saúde dos serviços e testes
55
- acordados; confira a estrutura com `inspecionar-projeto --check`.
56
- Com aplicação, prove o fluxo real no navegador/desktop conforme
57
- seu tipo; sem aplicação, registre a prova funcional pendente. Marque
58
- `configuracao_tecnica: concluido` somente com evidência da configuração.
56
+ 6. Use `verificar-trabalho`: valide runtime, configuração Compose e saúde dos
57
+ serviços de infraestrutura necessários; confira a estrutura com
58
+ `inspecionar-projeto --check`. Se Docker ou outra dependência obrigatória
59
+ estiver inacessível, mantenha a configuração pendente e informe a prova
60
+ faltante. Sem aplicação, registre build, saúde e fluxo dela como pendentes;
61
+ não os reporte como aprovados. Com aplicação existente, prove o fluxo real
62
+ conforme seu tipo. Marque `configuracao_tecnica: concluido` somente com
63
+ evidência da configuração prévia e dos serviços aplicáveis.
59
64
 
60
65
  Após alterações técnicas, conclua `revisar-entrega`. Sincronize apontamentos
61
66
  somente quando recursos dos agentes forem alterados.
@@ -17,7 +17,9 @@ Faça apenas as perguntas que impedem avançar.
17
17
  Continue o trabalho já autorizado; não reinicie fases concluídas nem apresente
18
18
  suposições como aprovações.
19
19
 
20
- Projeto novo segue preparação → PRD → UX/design → configuração → specs →
21
- implementação → revisão. Projeto existente parte de suas evidências atuais;
20
+ Projeto novo segue preparação → PRD → UX → design (arquitetura e tecnologia) →
21
+ roadmap → spec aprovada → plano aprovado → configuração técnica → implementação
22
+ → revisão. UX pode ser não aplicável com justificativa aprovada.
23
+ Projeto existente parte de suas evidências atuais;
22
24
  instalar o kit não autoriza recriar documentos ou substituir decisões.
23
25
  Se não houver pendências no escopo, diga isso claramente.
@@ -18,8 +18,20 @@ antes de implementar.
18
18
  seus IDs/revisões. Não invente uma solução já atendida por elas.
19
19
  3. Depois da aprovação, crie ou retome `feature/<slug-da-spec>` de `develop`.
20
20
  Se faltar `develop`, use `main` quando seguro; sem base/remoto, combine como
21
- proceder. Preserve alterações. Registre a aprovação no roadmap e confira a
22
- configuração real; complete-a com `configurar-projeto` antes de codificar.
21
+ proceder. Preserve alterações. Registre a aprovação no roadmap e complete a
22
+ configuração real com `configurar-projeto` antes de codificar.
23
+ Antes de qualquer patch de código da spec, execute:
24
+
25
+ ```sh
26
+ node .pbai4devs/scripts/executar-script/SCRIPT.mjs verificar-configuracao
27
+ ```
28
+
29
+ Releia o estado e confira as evidências da infraestrutura obrigatória. Se o
30
+ estado estiver ausente, inválido ou pendente, ou uma prova falhar, pare sem
31
+ escrever código; retome quando o impedimento for resolvido. Código zero do
32
+ script só confirma o estado registrado, não comprova as evidências nem impede
33
+ escrita por um agente que ignore o resultado. Não peça novamente aprovações
34
+ já concedidas.
23
35
  4. Use o modelo de tarefas em `<documentos.tarefas>/<slug-da-spec>/TASKS.md`.
24
36
  Vincule spec/release e registre tarefas, dependências, critérios e evidências.
25
37
  Atualize `[ ]`, `[-]` e `[x]` ao avançar; conclusão exige prova. Ao retomar,
@@ -79,9 +79,10 @@
79
79
  "name": "Codex",
80
80
  "root": ".codex",
81
81
  "formats": {
82
- "skills": "agrupado"
82
+ "skills": "nenhum"
83
83
  },
84
- "reason": "Skills agrupadas; a entrada nativa de projeto é .agents/skills. Outros recursos em .codex são referências, não registros de agentes.",
84
+ "skillFallback": ".agents",
85
+ "reason": "Skills usam apenas .agents/skills para evitar descoberta duplicada; os demais recursos em .codex são referências.",
85
86
  "sources": [
86
87
  "https://learn.chatgpt.com/docs/build-skills",
87
88
  "https://github.com/openai/codex/blob/392ced7d3a9160fd35d40c606359869215f50522/codex-rs/ext/skills/src/loader/host.rs"
package/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@ description: Alterações verificáveis por versão do AgentDevKit.
4
4
 
5
5
  # Changelog
6
6
 
7
+ ## 0.0.23 — 2026-11-13
8
+
9
+ - Corrige a resolução de destinos inexistentes diretamente sob a raiz.
10
+ - Aceita aliases temporários confiáveis do macOS e recusa destinos que
11
+ atravessam links humanos, inclusive os indicados por variáveis de ambiente.
12
+ - Orienta roadmap e spec antes da configuração técnica e acrescenta uma
13
+ verificação do estado antes da implementação.
14
+ - Aplica a verificação também aos especialistas e orienta consultas Git que
15
+ preservam os bytes do índice.
16
+
17
+ ## 0.0.22 — 2026-11-13
18
+
19
+ - Descoberta de skills do Codex sem duplicação, com alternativa segura quando a
20
+ rota principal está obstruída.
21
+ - Sincronização e atualização reconciliam o estado sem substituir recursos ou
22
+ metadados humanos.
23
+ - Scripts reconhecem caminhos por aliases e exemplos de maturidade usam saídas isoladas.
24
+ - Testes ampliados para combinações de clientes, projetos existentes e
25
+ preservação do Git.
26
+
7
27
  ## 0.0.21 — 2026-11-13
8
28
 
9
29
  - Criação automática da tag da versão após aprovação da matriz de CI.
@@ -125,5 +125,14 @@ do kit. Conteúdo humano não é substituído por `--force`.
125
125
 
126
126
  `doctor` verifica o kit, não o produto inteiro. `next` pode terminar com
127
127
  sucesso enquanto orienta uma etapa ainda pendente.
128
+ Após produto e design, ele usa os caminhos de roadmap e specs registrados no
129
+ estado. Para orientar uma spec existente, exige uma única referência aberta
130
+ `[ ]` ou em andamento `[-]` no roadmap. Com mais de uma spec, entrega encerrada,
131
+ vínculo ausente ou leitura ambígua, orienta a revisão do roadmap. Um status
132
+ declarado não comprova aprovação: a skill confere decisões e evidências. Com uma
133
+ spec aberta e declarada aprovada, `implementar-spec` verifica o plano antes de
134
+ encaminhar a configuração técnica; `next` não presume aprovação do plano.
135
+ Se o inventário for truncado, `next` orienta inspeção manual, sem escolher uma
136
+ fase ou presumir que um projeto existente está vazio.
128
137
 
129
138
  [Wiki](../README.md) · [Solução de problemas](troubleshooting.md)
@@ -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:
@@ -28,6 +28,11 @@ 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.
34
+ - [Descoberta e preservação](discovery-preservation/SPEC.md): rotas de skills,
35
+ sincronização segura e validação em projetos reais.
31
36
 
32
37
  Cada incremento vincula requisitos, critérios, testes e evidências locais.
33
38
 
@@ -0,0 +1,26 @@
1
+ ---
2
+ description: Descoberta de skills e preservação em projetos existentes.
3
+ ---
4
+
5
+ # Descoberta e preservação
6
+
7
+ As integrações devem disponibilizar os recursos sem duplicar skills geradas
8
+ para o Codex e sem substituir configurações ou alterações próprias do projeto.
9
+
10
+ ## Critérios de aceite
11
+
12
+ - AC-001: O Codex descobre cada skill na rota principal; rotas alternativas
13
+ são usadas por recurso quando necessário e conflitos são informados.
14
+ - AC-002: `init`, `update` e `sync` preservam conteúdo humano, índice Git e
15
+ `.gitignore` da raiz; limpeza exige propriedade comprovada.
16
+ - AC-003: A sincronização independente e a atualização reconciliam o estado,
17
+ preservando campos adicionais e sinalizando inconsistências.
18
+ - AC-004: Scripts identificam sua execução principal por caminhos reais ou
19
+ aliases, sem executar novamente quando importados.
20
+ - AC-005: Os exemplos de maturidade funcionam também a partir do pacote
21
+ distribuído, preservando arquivos da origem e exemplos anteriores.
22
+ - AC-006: Testes cobrem combinações de clientes e projetos existentes; resultados,
23
+ clones e projetos gerados ficam ignorados ou fora do repositório.
24
+
25
+ Descoberta e validação estrutural não demonstram execução completa de todas as
26
+ skills e perfis em cada cliente. Registre os limites das provas realizadas.
@@ -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.21",
3
+ "version": "0.0.23",
4
4
  "description": "Shared skills, specifications and workflows for AI coding agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
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);
@@ -169,6 +185,19 @@ export function safePath(root, path) {
169
185
  return current;
170
186
  }
171
187
 
188
+ // Inspect the route without following user-owned links. A final directory is
189
+ // also unavailable where the generated skill requires a regular file.
190
+ function nativeOutputBoundary(root, path, ownedPaths) {
191
+ let boundary = nativePathBoundary(root, path, ownedPaths, { preserveRootAliases: true });
192
+ if (!boundary) {
193
+ const info = stat(safePath(root, path));
194
+ if (info && !info.isFile()) boundary = { path, reason: 'non-file' };
195
+ }
196
+ if (!boundary) return { blocked: false };
197
+ const info = lstatSync(join(root, ...boundary.path.split('/')));
198
+ return { blocked: true, ...boundary, identity: [info.dev, info.ino, info.mode, info.size, info.mtimeMs, info.ctimeMs] };
199
+ }
200
+
172
201
  function resourceAllowed(path, directory = false) {
173
202
  const parts = path.split('/');
174
203
  const folders = new Set(['agents', 'commands', 'external-skills', 'rules', 'scripts', 'skills', 'templates', 'tools', 'state']);
@@ -388,30 +417,85 @@ function copyResources(source, destination) {
388
417
  return files;
389
418
  }
390
419
 
420
+ // Change only one top-level JSON member. Other fields, escaping and whitespace
421
+ // remain byte-for-byte intact, including user-provided fields in local state.
422
+ function replaceJsonMember(bytes, member, value) {
423
+ const text = bytes.toString('utf8');
424
+ if (!Buffer.from(text, 'utf8').equals(bytes)) fail('E_STATE', 'Invalid UTF-8 in derived state.');
425
+ let i = 0;
426
+ const space = () => { while (/\s/.test(text[i] || '') && i < text.length) i++; };
427
+ const stringEnd = () => {
428
+ if (text[i++] !== '"') fail('E_STATE', 'Invalid JSON member.');
429
+ while (i < text.length) {
430
+ const char = text[i++];
431
+ if (char === '\\') { i++; continue; }
432
+ if (char === '"') return;
433
+ }
434
+ fail('E_STATE', 'Unterminated JSON string.');
435
+ };
436
+ space(); if (text[i++] !== '{') fail('E_STATE', 'Invalid derived state.');
437
+ const properties = [];
438
+ while (true) {
439
+ space(); if (text[i] === '}') break;
440
+ const start = i; stringEnd(); const key = JSON.parse(text.slice(start, i));
441
+ space(); if (text[i++] !== ':') fail('E_STATE', 'Invalid JSON member.');
442
+ space(); const valueStart = i;
443
+ if (text[i] === '"') stringEnd();
444
+ else if (text[i] === '{' || text[i] === '[') {
445
+ const stack = [];
446
+ do {
447
+ const char = text[i];
448
+ if (char === '"') { stringEnd(); continue; }
449
+ i++;
450
+ if (char === '{' || char === '[') stack.push(char);
451
+ else if (char === '}' || char === ']') stack.pop();
452
+ } while (stack.length && i < text.length);
453
+ if (stack.length) fail('E_STATE', 'Invalid derived state.');
454
+ } else { while (i < text.length && text[i] !== ',' && text[i] !== '}') i++; while (i > valueStart && /\s/.test(text[i - 1])) i--; }
455
+ const end = i; properties.push({ key, start, valueStart, end });
456
+ space(); if (text[i] === ',') { i++; continue; } if (text[i] === '}') break;
457
+ fail('E_STATE', 'Invalid JSON member separator.');
458
+ }
459
+ const matches = properties.filter(item => item.key === member);
460
+ if (matches.length !== 1) fail('E_STATE', 'Ambiguous derived state member.');
461
+ const current = matches[0], index = properties.indexOf(current);
462
+ let next;
463
+ if (value !== undefined) next = text.slice(0, current.valueStart) + JSON.stringify(value, null, 2) + text.slice(current.end);
464
+ else if (index > 0) next = text.slice(0, properties[index - 1].end) + text.slice(current.end);
465
+ else if (properties.length > 1) next = text.slice(0, current.start) + text.slice(properties[1].start);
466
+ else next = text.slice(0, current.start) + text.slice(current.end);
467
+ try { JSON.parse(next); } catch { fail('E_STATE', 'Unable to preserve derived state safely.'); }
468
+ return Buffer.from(next);
469
+ }
470
+
391
471
  function readDerivedState(target) {
392
- let entries = {}, outputs = {}, blocks = {}, files = {}, removedBlocks = [];
472
+ let entries = {}, outputs = {}, blocks = {}, files = {}, removedBlocks = [], removedFiles = {};
393
473
  const derived = read(target, actualStatePath(target).replace(/agentdevkit\.json$/, 'derived.json'));
394
474
  if (derived) {
395
475
  try {
396
476
  const value = JSON.parse(derived);
397
477
  if (value.schemaVersion !== 1 || !value.entries || typeof value.entries !== 'object' || Array.isArray(value.entries)
398
- || ['outputs', 'blocks', 'files'].some(key => value[key] !== undefined && (!value[key] || typeof value[key] !== 'object' || Array.isArray(value[key])))) fail('E_STATE', 'Invalid derived entrypoint state.');
399
- entries = value.entries; outputs = value.outputs ?? {}; blocks = value.blocks ?? {}; files = value.files ?? {};
478
+ || ['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.');
479
+ entries = value.entries; outputs = value.outputs ?? {}; blocks = value.blocks ?? {}; files = value.files ?? {}; removedFiles = value.removedFiles ?? {};
400
480
  removedBlocks = value.removedBlocks ?? [];
401
481
  if (!Array.isArray(removedBlocks) || removedBlocks.some(path => typeof path !== 'string' || !nativeManagedText(path))) fail('E_STATE', 'Invalid removed block state.');
402
482
  for (const [path, digest] of Object.entries(entries)) if (!canonicalEntrypoint(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived entrypoint state.');
403
483
  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.');
404
- for (const [path, digest] of Object.entries(blocks)) if (!nativeManagedText(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived block state.');
484
+ 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.');
405
485
  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.');
486
+ 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.');
487
+ const installed = loadState(target);
488
+ if (Object.entries(removedFiles).some(([path, digest]) => installed?.files?.[path] && installed.files[path] !== digest)) fail('E_STATE', 'Removed file state differs from installation.');
406
489
  }
407
490
  catch { fail('E_STATE', 'Invalid derived entrypoint state.'); }
408
- for (const path of [...Object.keys(entries), ...Object.keys(outputs), ...Object.keys(blocks), ...Object.keys(files), ...removedBlocks]) safePath(target, path);
491
+ for (const path of [...Object.keys(entries), ...Object.keys(outputs), ...Object.keys(blocks), ...Object.keys(files), ...Object.keys(removedFiles), ...removedBlocks]) safePath(target, path);
409
492
  }
410
- return { entries, outputs, blocks, files, removedBlocks };
493
+ return { entries, outputs, blocks, files, removedBlocks, removedFiles };
411
494
  }
412
495
 
413
496
  export function planInstallation({ target, language = 'pt', mode = 'init', force = false, clients }) {
414
- const previous = loadState(target);
497
+ let previous = loadState(target);
498
+ const installedState = previous;
415
499
  const migration = legacyStateMigration(target);
416
500
  // A migration must update the installed scripts and pointers in the same
417
501
  // transaction; a plain sync/agents/lang would leave old hard-coded paths.
@@ -427,7 +511,8 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
427
511
  if (original === null ? content !== null : !content?.equals(original)) fail('E_CHANGED', `File changed while planning: ${path}`);
428
512
  } else nativeInputs.set(path, content);
429
513
  };
430
- const { entries: derivedEntries, outputs: derivedOutputs, blocks: derivedBlocks, files: derivedFiles, removedBlocks } = readDerivedState(target);
514
+ const { entries: derivedEntries, outputs: derivedOutputs, blocks: derivedBlocks, files: derivedFiles, removedBlocks, removedFiles } = readDerivedState(target);
515
+ if (previous) previous = { ...previous, files: Object.fromEntries(Object.entries(previous.files).filter(([path, digest]) => removedFiles[path] !== digest)) };
431
516
  const previousBlocks = Object.fromEntries(Object.entries(previous?.blocks || {}).filter(([path]) => !removedBlocks.includes(path)));
432
517
  const knownBlocks = { ...previousBlocks, ...derivedBlocks };
433
518
  const ownedPaths = [...Object.keys(previous?.files || {}), ...Object.keys(previousBlocks), ...Object.keys(derivedEntries), ...Object.keys(derivedOutputs), ...Object.keys(derivedBlocks), ...Object.keys(derivedFiles)];
@@ -436,6 +521,14 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
436
521
  ?? (canonicalEntrypoint(path) ? Object.entries(knownBlocks).find(([candidate]) => canonicalEntrypoint(candidate) === canonicalEntrypoint(path))?.[1] : undefined);
437
522
  const preserved = new Map();
438
523
  const recordPreserved = (path, reason) => { preserved.set(path, { path, reason }); return true; };
524
+ const fallbackChecks = new Map();
525
+ const outputBlocked = path => {
526
+ const result = nativeOutputBoundary(target, path, ownedPaths);
527
+ if (fallbackChecks.has(path) && JSON.stringify(fallbackChecks.get(path)) !== JSON.stringify(result)) fail('E_CHANGED', `Skill route changed while planning: ${path}`);
528
+ fallbackChecks.set(path, result);
529
+ if (result.blocked) recordPreserved(result.path, result.reason);
530
+ return result;
531
+ };
439
532
  const stagedHuman = stagedDivergences(target);
440
533
  const native = path => canonicalEntrypoint(path) || path === 'README.md' || adapterRoots.some(root => path === root || path.startsWith(`${root}/`));
441
534
  const boundary = path => {
@@ -504,7 +597,7 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
504
597
  const ownIgnore = read(target, '.pbai4devs/.gitignore');
505
598
  if (ownIgnore) write(stage, '.pbai4devs/.gitignore', ownIgnore);
506
599
  writeAdapterManifest(stage, { clients: selected });
507
- const synchronized = synchronize({ root: stage, force: true, localIgnores: false, generateEntrypoints: false, recordDerivedState: false, output() {} });
600
+ const synchronized = synchronize({ root: stage, force: true, localIgnores: false, generateEntrypoints: false, recordDerivedState: false, outputBlocked, output() {} });
508
601
  for (const canonical of active.entrypoints) {
509
602
  const name = entryPaths.get(canonical);
510
603
  if (preserveNative(name)) continue;
@@ -577,14 +670,45 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
577
670
  if (block) manifest.blocks[path] = block;
578
671
  else manifest.files[path] = hash(content);
579
672
  }
580
- desired.set(statePath, Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`));
673
+ // A standalone sync records its proven outputs separately. During a later
674
+ // check/sync, compare that overlay before declaring the installation state
675
+ // stale; update still consolidates it into the main state transactionally.
676
+ let stateContent = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
677
+ if (mode === 'sync' && installedState && !changingClients) {
678
+ const files = { ...installedState.files }, blocks = { ...installedState.blocks };
679
+ for (const [path, digest] of Object.entries(removedFiles)) {
680
+ if (files[path] === digest && !stat(safePath(target, path))) delete files[path];
681
+ }
682
+ for (const path of removedBlocks) delete blocks[path];
683
+ for (const [path, digest] of Object.entries({ ...derivedFiles, ...derivedOutputs, ...derivedEntries })) { files[path] = digest; delete blocks[path]; }
684
+ for (const [path, digest] of Object.entries(derivedBlocks)) { blocks[path] = digest; delete files[path]; }
685
+ const same = (a, b) => {
686
+ const keys = Object.keys(a).sort();
687
+ return keys.length === Object.keys(b).length && keys.every(key => a[key] === b[key]);
688
+ };
689
+ if (installedState.schemaVersion === manifest.schemaVersion && installedState.version === manifest.version && installedState.language === manifest.language
690
+ && JSON.stringify(installedState.clients) === JSON.stringify(manifest.clients) && same(files, manifest.files) && same(blocks, manifest.blocks)) stateContent = read(target, statePath);
691
+ }
692
+ desired.set(statePath, stateContent);
693
+ const derivedPath = actualStatePath(target).replace(/agentdevkit\.json$/, 'derived.json');
694
+ const derivedBytes = !migration ? read(target, derivedPath) : null;
695
+ if (derivedBytes && Object.keys(removedFiles).length) {
696
+ const finalState = JSON.parse(stateContent);
697
+ const remaining = Object.fromEntries(Object.entries(removedFiles).filter(([path, digest]) =>
698
+ finalState.files[path] === digest && !stat(safePath(target, path))));
699
+ if (Object.keys(remaining).length !== Object.keys(removedFiles).length) {
700
+ observeNative(derivedPath, derivedBytes);
701
+ desired.set(derivedPath, replaceJsonMember(derivedBytes, 'removedFiles', Object.keys(remaining).length ? remaining : undefined));
702
+ }
703
+ }
581
704
  const changes = [], conflicts = [];
582
705
  const aliasedEntrypoint = path => canonicalEntrypoint(path) && entryPaths.get(canonicalEntrypoint(path)) !== path;
583
706
  for (const [path, content] of desired) {
584
707
  const current = read(target, path);
585
708
  if (nativeInputs.has(path) && (nativeInputs.get(path) === null ? current !== null : !current?.equals(nativeInputs.get(path)))) fail('E_CHANGED', `File changed while planning: ${path}`);
586
709
  if (current?.equals(content)) continue;
587
- const managed = [previous?.files[path], derivedOutputs[path], derivedEntries[path], derivedFiles[path]].some(digest => digest && hash(current || '') === digest)
710
+ const managed = path === derivedPath && derivedBytes && current?.equals(derivedBytes)
711
+ || [previous?.files[path], derivedOutputs[path], derivedEntries[path], derivedFiles[path]].some(digest => digest && hash(current || '') === digest)
588
712
  || knownBlocks[path] && blockDigest(path, current || Buffer.alloc(0)) === knownBlocks[path];
589
713
  const appendBlock = mixedPaths.has(path) || canonicalEntrypoint(path) || path === '.gitignore' || path.endsWith('/.gitignore');
590
714
  const conflict = current && path !== statePath && !managed && !appendBlock;
@@ -704,7 +828,7 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
704
828
  const localIgnore = planLocalIgnore(target, { outputs: ownedOutputs, templateRoot: stage });
705
829
  const localIgnoreCheck = checkLocalIgnore(localIgnore, { replacedBlocks: replacedIgnoreBlocks(target, changes, previous) });
706
830
  conflicts.push(...(untrack?.conflicts || []), ...localIgnoreCheck.missing);
707
- 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)) };
831
+ 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)) };
708
832
  } finally { rmSync(stage, { recursive: true, force: true }); }
709
833
  }
710
834
 
@@ -712,6 +836,15 @@ export function applyPlan(plan) {
712
836
  if (!plan.canApply) fail('E_CONFLICT', text(plan.language, 'conflict'));
713
837
  assertLocalIgnoreUnchanged(plan.localIgnore);
714
838
  const { target } = plan;
839
+ const assertFallbackChecks = () => {
840
+ for (const [path, expected] of plan.fallbackChecks || []) {
841
+ let current;
842
+ try { current = nativeOutputBoundary(target, path, plan.fallbackOwnedPaths || []); }
843
+ catch { fail('E_CHANGED', `Skill route changed after planning: ${path}`); }
844
+ if (JSON.stringify(current) !== JSON.stringify(expected)) fail('E_CHANGED', `Skill route changed after planning: ${path}`);
845
+ }
846
+ };
847
+ assertFallbackChecks();
715
848
  const stagedHuman = stagedDivergences(target);
716
849
  const currentState = loadState(target);
717
850
  const stagedChanges = plan.changes.filter(change => {
@@ -793,6 +926,7 @@ export function applyPlan(plan) {
793
926
  rmdirSync(file); removedDirectories.push({ path: directory.path, mode: directory.mode });
794
927
  }
795
928
  }
929
+ assertFallbackChecks();
796
930
  pruneRemovedDirectories(plan, directory => removedDirectories.push(directory));
797
931
  const changesFallback = plan.changes.some(change => change.path === '.gitignore');
798
932
  // Metadata is committed last; its writer rolls back partial writes itself.
@@ -881,6 +1015,10 @@ export function inspect(target) {
881
1015
  for (const [path, digest] of Object.entries(state.files)) {
882
1016
  if (derived.blocks[path] || derived.removedBlocks.includes(path)) continue;
883
1017
  const content = read(target, path);
1018
+ if (derived.removedFiles[path] === digest && !derived.files[path] && !derived.entries[path]) {
1019
+ if (content) issues.push({ code: 'modified', path });
1020
+ continue;
1021
+ }
884
1022
  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 });
885
1023
  }
886
1024
  for (const [path, digest] of Object.entries({ ...state.blocks, ...derived.blocks })) {
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],