@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
@@ -1,7 +1,8 @@
1
1
  import assert from 'node:assert/strict';
2
- import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
2
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
- import { spawnSync } from 'node:child_process';
4
+ import childProcess, { spawnSync } from 'node:child_process';
5
+ import { syncBuiltinESMExports } from 'node:module';
5
6
  import { createHash } from 'node:crypto';
6
7
  import test from 'node:test';
7
8
  import { projectRoot, requireNode } from '../../lib/runtime.mjs';
@@ -55,21 +56,61 @@ test('copia recursos e configurações, excluindo payloads, caches e segredos',
55
56
  assert.deepEqual(result.roots, ['.pbai4devs', '.codex', '.github', '.agents', '.agent', '.gemini', '.vscode']);
56
57
  });
57
58
 
58
- test('regeneração preserva pastas alheias e recusa sobrescrevê-las', t => {
59
+ test('destino explícito existente é recusado sem alterar projetos humanos ou gerados', t => {
59
60
  const { root, put, outputRoot } = fixture(t);
60
61
  put('examples/generated/anotacoes/keep.txt', 'preservado');
61
62
  put('examples/README.md', 'Guia humano.');
62
63
  const options = { sourceRoot: root, outputRoot, scenarios: [SCENARIOS[0]], initializeGit: false };
63
- prepareExamples(options); prepareExamples(options);
64
+ assert.throws(() => prepareExamples(options), /saída já existe/);
64
65
  assert.equal(readFileSync(join(outputRoot, 'anotacoes/keep.txt'), 'utf8'), 'preservado');
65
66
  assert.equal(readFileSync(join(root, 'examples/README.md'), 'utf8'), 'Guia humano.');
66
- put('examples/generated/descoberta/keep.txt', 'humano');
67
- assert.throws(() => prepareExamples({ ...options, scenarios: [SCENARIOS[1]] }), /não é gerenciado/);
68
- assert.equal(readFileSync(join(outputRoot, 'descoberta/keep.txt'), 'utf8'), 'humano');
69
- assert.throws(() => prepareExamples({ ...options, scenarios: [{ ...SCENARIOS[0], id: '../outside' }] }), /IDs únicos e seguros/);
67
+ const output = join(outputRoot, 'nova-execucao');
68
+ const [example] = prepareExamples({ ...options, outputRoot: output });
69
+ writeFileSync(join(example.root, 'anotacao-humana.txt'), 'não apagar');
70
+ const report = readFileSync(join(output, 'REPORT.json'));
71
+ assert.throws(() => prepareExamples({ ...options, outputRoot: output, scenarios: [SCENARIOS[1]] }), /saída já existe/);
72
+ assert.throws(() => prepareExamples({ ...options, outputRoot: join(example.root, 'nova'), scenarios: [SCENARIOS[1]] }), /execução anterior/);
73
+ assert.deepEqual(readFileSync(join(output, 'REPORT.json')), report);
74
+ assert.equal(readFileSync(join(example.root, 'anotacao-humana.txt'), 'utf8'), 'não apagar');
75
+ assert.throws(() => prepareExamples({ ...options, outputRoot: join(outputRoot, 'invalid'), scenarios: [{ ...SCENARIOS[0], id: '../outside' }] }), /IDs únicos e seguros/);
76
+ assert.equal(existsSync(join(outputRoot, 'invalid')), false);
70
77
  });
71
78
 
72
- test('destino padrão redefine apenas relatório próprio e mantém resultados fora do Git', t => {
79
+ test('troca de ancestral por symlink antes da criação não escreve no destino externo', t => {
80
+ const { root, outputRoot } = fixture(t);
81
+ const outside = mkdtempSync(join(dirname(root), 'maturity-outside-'));
82
+ t.after(() => rmSync(outside, { recursive: true, force: true }));
83
+ mkdirSync(outputRoot, { recursive: true });
84
+ const externalSentinel = join(outside, 'sentinel.txt');
85
+ writeFileSync(externalSentinel, 'externo preservado\n');
86
+ const linkProbe = join(root, 'probe-link');
87
+ try { symlinkSync(outside, linkProbe, process.platform === 'win32' ? 'junction' : 'dir'); }
88
+ catch (error) { if (['EPERM', 'EACCES'].includes(error.code)) { t.skip('Sem permissão para links simbólicos.'); return; } throw error; }
89
+ unlinkSync(linkProbe);
90
+ const canonical = realpathSync(outputRoot);
91
+ const original = childProcess.spawnSync;
92
+ let injected = false;
93
+ childProcess.spawnSync = function (file, args, options) {
94
+ if (!injected && file === process.execPath && args?.[0] === '-e' && options?.cwd === canonical) {
95
+ injected = true;
96
+ renameSync(outputRoot, `${outputRoot}-original`);
97
+ symlinkSync(outside, outputRoot, process.platform === 'win32' ? 'junction' : 'dir');
98
+ }
99
+ return original.call(this, file, args, options);
100
+ };
101
+ syncBuiltinESMExports();
102
+ try {
103
+ assert.throws(() => prepareExamples({ sourceRoot: root, outputRoot: join(outputRoot, 'nova'), scenarios: [SCENARIOS[0]], initializeGit: false }), /diretório exclusivo|ancestral|link simbólico/i);
104
+ assert.equal(injected, true);
105
+ assert.deepEqual(readdirSync(outside), ['sentinel.txt']);
106
+ assert.equal(readFileSync(externalSentinel, 'utf8'), 'externo preservado\n');
107
+ } finally {
108
+ childProcess.spawnSync = original;
109
+ syncBuiltinESMExports();
110
+ }
111
+ });
112
+
113
+ test('cada destino padrão é exclusivo, preserva provas anteriores e permanece ignorado', t => {
73
114
  const { root, outputRoot } = fixture(t);
74
115
  const git = (...args) => {
75
116
  const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8', shell: false });
@@ -77,48 +118,58 @@ test('destino padrão redefine apenas relatório próprio e mantém resultados f
77
118
  };
78
119
  git('init', '--template=', '--quiet');
79
120
  const options = { sourceRoot: root, scenarios: [SCENARIOS[0]], initializeGit: false };
80
- const examples = prepareExamples(options);
81
- assert.equal(examples[0].root, join(outputRoot, SCENARIOS[0].id));
82
- const reportFile = join(outputRoot, 'REPORT.json');
83
- writeFileSync(reportFile, JSON.stringify({ owner: 'testar-maturidade', status: 'passed', results: [{ stale: true }] }));
84
- prepareExamples(options);
85
- assert.deepEqual(JSON.parse(readFileSync(reportFile)), { owner: 'testar-maturidade', version: 1,
86
- status: 'prepared', scenarios: [SCENARIOS[0].id], results: [] });
87
- assert.match(git('check-ignore', '--verbose', 'examples/generated/REPORT.json'), /examples\/generated\/\.gitignore:2:\*/);
88
- assert.match(git('check-ignore', '--verbose', 'examples/generated/somente-agentes/README.md'), /examples\/generated\/\.gitignore:2:\*/);
89
- const human = JSON.stringify({ owner: 'human', notes: 'preservar' });
90
- writeFileSync(reportFile, human);
91
- assert.throws(() => prepareExamples(options), /Relatório.*não pertence/);
92
- assert.equal(readFileSync(reportFile, 'utf8'), human);
121
+ const [first] = prepareExamples(options);
122
+ const firstOutput = dirname(first.root);
123
+ assert.equal(dirname(firstOutput), outputRoot);
124
+ const human = 'Prova humana que não pode ser apagada.\n';
125
+ writeFileSync(join(first.root, 'human.txt'), human);
126
+ const reportFile = join(firstOutput, 'REPORT.json');
127
+ const oldReport = readFileSync(reportFile);
128
+ const [second] = prepareExamples(options);
129
+ assert.notEqual(dirname(second.root), firstOutput);
130
+ assert.equal(readFileSync(join(first.root, 'human.txt'), 'utf8'), human);
131
+ assert.deepEqual(readFileSync(reportFile), oldReport);
132
+ for (const file of [join(firstOutput, 'REPORT.json'), join(first.root, 'README.md'), join(second.root, 'README.md')]) {
133
+ git('check-ignore', '--quiet', '--', file);
134
+ }
93
135
  });
94
136
 
95
- test('preserva ignore humano e continua aceitando destinos legados', t => {
137
+ test('preserva ignore humano em pastas ancestrais e aceita destinos novos permitidos', t => {
96
138
  const { root, put } = fixture(t);
97
139
  const options = { sourceRoot: root, scenarios: [SCENARIOS[0]], initializeGit: false };
98
140
  put('examples/generated/.gitignore', '!README.md\n');
99
- assert.throws(() => prepareExamples(options), /Ignore.*não pertence/);
141
+ put('examples/generated/.maturity-suite.json', '{"owner":"testar-maturidade"}\n');
142
+ put('examples/generated/somente-agentes/.maturity-case.json', '{"owner":"testar-maturidade","id":"somente-agentes"}\n');
143
+ put('examples/generated/somente-agentes/anotacao.txt', 'Conteúdo humano anterior.\n');
144
+ put('examples/generated/REPORT.json', '{"resultado":"anterior"}\n');
145
+ const [generated] = prepareExamples(options);
146
+ assert.equal(dirname(dirname(generated.root)), join(root, 'examples/generated'));
100
147
  assert.equal(readFileSync(join(root, 'examples/generated/.gitignore'), 'utf8'), '!README.md\n');
101
- for (const target of ['tmp/examples', '.agent-tests/cases']) {
148
+ assert.equal(readFileSync(join(root, 'examples/generated/somente-agentes/anotacao.txt'), 'utf8'), 'Conteúdo humano anterior.\n');
149
+ assert.equal(readFileSync(join(root, 'examples/generated/REPORT.json'), 'utf8'), '{"resultado":"anterior"}\n');
150
+ for (const target of ['tmp/examples/caso-novo', '.agent-tests/cases']) {
102
151
  const outputRoot = join(root, target);
103
152
  const [example] = prepareExamples({ ...options, outputRoot });
104
153
  assert.equal(example.root, join(outputRoot, SCENARIOS[0].id));
105
154
  }
106
155
  });
107
156
 
108
- test('preparação interrompida não conserva resultado anterior nem declara sucesso', t => {
157
+ test('preparação interrompida não altera resultado anterior nem declara sucesso', t => {
109
158
  const { root, put, outputRoot } = fixture(t);
110
159
  const options = { sourceRoot: root, scenarios: [SCENARIOS[0]], initializeGit: false };
111
- prepareExamples(options);
112
- const report = join(outputRoot, 'REPORT.json');
113
- const index = join(outputRoot, 'README.md');
114
- writeFileSync(report, JSON.stringify({ owner: 'testar-maturidade', status: 'passed', results: [{}] }));
115
- writeFileSync(index, readFileSync(index, 'utf8').replaceAll('prepared', 'passed'));
160
+ const [first] = prepareExamples(options);
161
+ const firstOutput = dirname(first.root);
162
+ const report = join(firstOutput, 'REPORT.json');
163
+ const previous = readFileSync(report);
116
164
  put('.pbai4devs/state/agentdevkit.json', JSON.stringify({ blocks: null }));
117
165
  assert.throws(() => prepareExamples(options), /Estado da instalação/);
118
- assert.equal(existsSync(report), false);
166
+ assert.deepEqual(readFileSync(report), previous);
167
+ const failed = readdirSync(outputRoot).filter(name => join(outputRoot, name) !== firstOutput);
168
+ assert.equal(failed.length, 1);
169
+ const index = join(outputRoot, failed[0], 'README.md');
119
170
  assert.match(readFileSync(index, 'utf8'), /status: preparing/);
171
+ assert.equal(existsSync(join(outputRoot, failed[0], 'REPORT.json')), false);
120
172
  assert.doesNotMatch(readFileSync(index, 'utf8'), /status: passed|\| passed \|/);
121
- assert.doesNotMatch(readFileSync(index, 'utf8'), /\n{3,}/);
122
173
  });
123
174
 
124
175
  test('usa entradas distribuídas do hub e mantém fallback legado', t => {
@@ -216,6 +267,38 @@ test('localiza apontamentos pelo formato de cada tipo e por overrides nativos',
216
267
  ]);
217
268
  });
218
269
 
270
+ test('respeita tipo sem espelho e o formato TOML nativo dos comandos Gemini', t => {
271
+ const { root, put } = fixture(t);
272
+ put('.pbai4devs/espelhos.conf', [
273
+ 'agente|.agents|agrupado',
274
+ 'agente|.codex|agrupado', 'formato|.codex|skills|nenhum',
275
+ 'agente|.gemini|agrupado', 'formato|.gemini|commands|arquivo',
276
+ ].join('\n') + '\n');
277
+ const skill = resourceTargets(root, '.pbai4devs/skills/projeto/demo/SKILL.md');
278
+ assert.deepEqual(skill, ['.agents/skills/projeto/demo/SKILL.md', '.gemini/skills/projeto/demo/SKILL.md']);
279
+ assert.ok(!skill.some(target => target.startsWith('.codex/')));
280
+ const command = resourceTargets(root, '.pbai4devs/commands/projeto/demo/COMMAND.md', 'commands');
281
+ assert.ok(command.includes('.codex/commands/projeto/demo/COMMAND.md'));
282
+ assert.ok(command.includes('.gemini/commands/projeto/demo.toml'));
283
+ assert.ok(!command.includes('.gemini/commands/projeto/demo.md'));
284
+ });
285
+
286
+ test('identifica o fallback Codex apenas para a skill cuja rota primária está obstruída', t => {
287
+ const { root, put } = fixture(t);
288
+ put('.pbai4devs/espelhos.conf', [
289
+ 'agente|.agents|agrupado', 'agente|.codex|agrupado',
290
+ 'formato|.codex|skills|nenhum', 'fallback|.codex|.agents|skills',
291
+ ].join('\n') + '\n');
292
+ const blocked = '.pbai4devs/skills/projeto/demo/SKILL.md';
293
+ const available = '.pbai4devs/skills/projeto/outra/SKILL.md';
294
+ const target = join(root, '.agents/skills/projeto/demo/SKILL.md');
295
+ mkdirSync(target, { recursive: true });
296
+ assert.deepEqual(resourceTargets(root, blocked), ['.codex/skills/projeto/demo/SKILL.md']);
297
+ assert.deepEqual(resourceTargets(root, available), ['.agents/skills/projeto/outra/SKILL.md']);
298
+ rmSync(target, { recursive: true });
299
+ assert.deepEqual(resourceTargets(root, blocked), ['.agents/skills/projeto/demo/SKILL.md']);
300
+ });
301
+
219
302
  test('checkout de fontes sem manifesto deriva clientes em memória', t => {
220
303
  const { root, put } = fixture(t);
221
304
  const manifest = join(root, '.pbai4devs/espelhos.conf');
@@ -277,6 +360,47 @@ test('oito cópias exercitam os seis níveis e preservam layouts legado/monorepo
277
360
  assert.equal(existsSync(join(monorepo.root, 'src')), false);
278
361
  });
279
362
 
363
+ test('cenários configurados completam fixtures ausentes sem alterar a origem ou arquivos copiados', t => {
364
+ const { root, put, outputRoot } = fixture(t);
365
+ const files = ['.gitignore', '.gitattributes', '.editorconfig'];
366
+ const source = Object.fromEntries(files.map(file => [file, `# configuração própria de ${file}\n`]));
367
+ // As duas fontes ausentes reproduzem uma instalação distribuída; o arquivo
368
+ // existente prova que a preparação não substitui configurações copiadas.
369
+ rmSync(join(root, '.gitattributes'));
370
+ rmSync(join(root, '.editorconfig'));
371
+ put('.gitignore', source['.gitignore']);
372
+ const scenarios = SCENARIOS.filter(example => ['app-configurada', 'manutencao', 'app-inicial'].includes(example.id));
373
+ const examples = prepareExamples({ sourceRoot: root, outputRoot: join(outputRoot, 'parcial'), scenarios, initializeGit: false });
374
+ assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), source['.gitignore']);
375
+ for (const file of ['.gitattributes', '.editorconfig']) assert.equal(existsSync(join(root, file)), false, file);
376
+ for (const example of examples) {
377
+ assert.equal(readFileSync(join(example.root, '.gitignore'), 'utf8'), source['.gitignore']);
378
+ if (!example.configured) {
379
+ for (const file of ['.gitattributes', '.editorconfig']) assert.equal(existsSync(join(example.root, file)), false, file);
380
+ continue;
381
+ }
382
+ for (const file of ['.gitattributes', '.editorconfig']) assert.ok(readFileSync(join(example.root, file), 'utf8').trim(), file);
383
+ const inspected = spawnSync(process.execPath, ['.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs', '--check'], { cwd: example.root, encoding: 'utf8' });
384
+ assert.ifError(inspected.error);
385
+ assert.equal(inspected.status, 0, inspected.stderr || inspected.stdout);
386
+ assert.equal(JSON.parse(inspected.stdout).contract.complete, true, example.id);
387
+ }
388
+ // Retest with every file absent, as in the released npm tarball.
389
+ rmSync(join(root, '.gitignore'));
390
+ const configured = scenarios.find(example => example.id === 'app-configurada');
391
+ const missing = prepareExamples({ sourceRoot: root, outputRoot: join(outputRoot, 'ausentes'), scenarios: [configured], initializeGit: false })[0];
392
+ for (const file of files) {
393
+ assert.equal(existsSync(join(root, file)), false, file);
394
+ assert.ok(readFileSync(join(missing.root, file), 'utf8').trim(), file);
395
+ }
396
+ for (const file of files) put(file, source[file]);
397
+ const preserved = prepareExamples({ sourceRoot: root, outputRoot: join(outputRoot, 'presentes'), scenarios: [configured], initializeGit: false })[0];
398
+ for (const file of files) {
399
+ assert.equal(readFileSync(join(root, file), 'utf8'), source[file]);
400
+ assert.equal(readFileSync(join(preserved.root, file), 'utf8'), source[file]);
401
+ }
402
+ });
403
+
280
404
  test('fixtures padrão, legado e monorepo executam HTTP real e encerram', t => {
281
405
  const { root, outputRoot } = fixture(t);
282
406
  const scenarios = SCENARIOS.filter(example => ['app-inicial', 'legado', 'monorepo'].includes(example.id));
@@ -0,0 +1,22 @@
1
+ ---
2
+ description: Confere os indicadores registrados antes da implementação.
3
+ ---
4
+
5
+ # Verificar configuração
6
+
7
+ ```sh
8
+ node .pbai4devs/scripts/executar-script/SCRIPT.mjs verificar-configuracao
9
+ ```
10
+
11
+ Somente leitura: retorna `1` se o estado estiver ausente, inacessível,
12
+ ambíguo ou com preparação ou configuração pendente. Retorna `0` e
13
+ `verificar-evidencias` quando ambos os indicadores registram `concluido`.
14
+ Para evitar ambiguidades, recusa chaves complexas e tags, âncoras, aliases ou
15
+ mesclagens no topo do cabeçalho. Links simbólicos detectados são recusados;
16
+ a leitura não é uma trava
17
+ atômica contra mudanças simultâneas de caminhos.
18
+ Não valida o YAML inteiro, a infraestrutura ou as provas; confira-os antes de
19
+ escrever código. O comando não bloqueia alterações de quem ignore seu resultado.
20
+
21
+ O estado legado é reconhecido sem migração; estados em conflito são recusados.
22
+ Erros de uso retornam `2`.
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ import { lstatSync, readFileSync, realpathSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { isMain, projectRoot, requireNode } from '../lib/runtime.mjs';
5
+ import { stateDirectory } from '../lib/state-path.mjs';
6
+
7
+ const limit = 'O estado não comprova evidências nem impede um agente de escrever.';
8
+ const blocked = motivo => ({ status: 'bloqueado', motivo, limite: limit });
9
+
10
+ // Read only the two indicators; never print arbitrary state content.
11
+ export function verifyConfiguration(root = projectRoot) {
12
+ requireNode();
13
+ try {
14
+ root = realpathSync(resolve(root));
15
+ const parts = `${stateDirectory(root)}/configuracao/ESTADO.md`.split('/');
16
+ let current = root; let info;
17
+ for (let index = 0; index < parts.length; index++) {
18
+ current = join(current, parts[index]);
19
+ info = lstatSync(current);
20
+ if (info.isSymbolicLink() || (index < parts.length - 1 && !info.isDirectory())) return blocked('estado-inseguro');
21
+ }
22
+ if (!info.isFile() || info.size > 1024 * 1024) return blocked('estado-invalido');
23
+ const text = readFileSync(current, 'utf8').replaceAll('\r\n', '\n');
24
+ const header = text.match(/^---\n([\s\S]*?)\n---(?:\n|$)/)?.[1];
25
+ if (header === undefined) return blocked('estado-invalido');
26
+ const values = {};
27
+ for (const line of header.split('\n')) {
28
+ if (!line.trim() || line.trimStart().startsWith('#') || /^[ \t]/.test(line)) continue;
29
+ // Explicit mapping keys and merge keys may hide duplicate indicators.
30
+ if (/^(?:\?|<<\s*:)/.test(line)) return blocked('estado-invalido');
31
+ const match = line.match(/^("(?:[^"\\]|\\.)*"|'(?:[^']|'')*'|[a-zA-Z_][a-zA-Z0-9_.-]*)[ \t]*:(.*)$/);
32
+ if (!match) return blocked('estado-invalido');
33
+ if (/^[!&*]/.test(match[2].trim())) return blocked('estado-invalido');
34
+ let key = match[1];
35
+ if (key.startsWith('"')) {
36
+ try { key = JSON.parse(key); } catch { return blocked('estado-invalido'); }
37
+ } else if (key.startsWith("'")) key = key.slice(1, -1).replaceAll("''", "'");
38
+ if (!['preparacao', 'configuracao_tecnica'].includes(key)) continue;
39
+ if (Object.hasOwn(values, key)) return blocked('estado-invalido');
40
+ const value = match[2].trim().match(/^(?:concluido|pendente|'(?:concluido|pendente)'|"(?:concluido|pendente)")(?:[ \t]+#.*)?$/)?.[0];
41
+ if (!value) return blocked('estado-invalido');
42
+ values[key] = value.replace(/[ \t]+#.*$/, '').replace(/^['"]|['"]$/g, '');
43
+ }
44
+ if (!Object.hasOwn(values, 'preparacao') || !Object.hasOwn(values, 'configuracao_tecnica')) return blocked('estado-invalido');
45
+ if (values.preparacao !== 'concluido' || values.configuracao_tecnica !== 'concluido') return blocked('configuracao-pendente');
46
+ return { status: 'verificar-evidencias', estado: values, limite: limit };
47
+ } catch (error) {
48
+ if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return blocked('estado-ausente');
49
+ return blocked('estado-inacessivel-ou-inseguro');
50
+ }
51
+ }
52
+
53
+ export function main(args = process.argv.slice(2)) {
54
+ if (args.length) throw new Error('Uso: verificar-configuracao sem argumentos.');
55
+ const report = verifyConfiguration();
56
+ console.log(JSON.stringify(report));
57
+ return report.status === 'bloqueado' ? 1 : 0;
58
+ }
59
+
60
+ if (isMain(import.meta.url)) {
61
+ try { process.exitCode = main(); }
62
+ catch (error) { console.error(error.message); process.exitCode = 2; }
63
+ }
@@ -0,0 +1,145 @@
1
+ import assert from 'node:assert/strict';
2
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmdirSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import test from 'node:test';
6
+ import { projectRoot } from '../../lib/runtime.mjs';
7
+ import { verifyConfiguration } from '../SCRIPT.mjs';
8
+
9
+ function fixture(t) {
10
+ const base = join(projectRoot, '.agent-tests'); mkdirSync(base, { recursive: true });
11
+ const root = mkdtempSync(join(base, 'config-gate-'));
12
+ t.after(() => rmSync(root, { recursive: true, force: true }));
13
+ const write = (file, content) => {
14
+ const destination = join(root, file); mkdirSync(dirname(destination), { recursive: true });
15
+ writeFileSync(destination, content);
16
+ };
17
+ for (const file of ['lib/runtime.mjs', 'lib/state-path.mjs', 'lib/script-config.mjs', 'executar-script/SCRIPT.mjs', 'verificar-configuracao/SCRIPT.mjs']) {
18
+ const destination = join(root, '.pbai4devs/scripts', file);
19
+ mkdirSync(dirname(destination), { recursive: true });
20
+ copyFileSync(join(projectRoot, '.pbai4devs/scripts', file), destination);
21
+ }
22
+ const state = (body, folder = 'state') => write(`.pbai4devs/${folder}/configuracao/ESTADO.md`, body);
23
+ const run = (...args) => {
24
+ const file = join(root, '.pbai4devs/scripts/verificar-configuracao/SCRIPT.mjs');
25
+ const result = spawnSync(process.execPath, [file, ...args], { encoding: 'utf8' });
26
+ assert.ifError(result.error);
27
+ return { ...result, report: result.stdout ? JSON.parse(result.stdout) : null };
28
+ };
29
+ return { root, state, write, run };
30
+ }
31
+
32
+ const document = (preparation, configuration) => `---\npreparacao: ${preparation}\nconfiguracao_tecnica: ${configuration}\ndocumentos:\n prd: docs/PRD.md\n---\n\n# Estado\n`;
33
+
34
+ test('bloqueia antes do código quando o estado está ausente ou pendente', t => {
35
+ const { root, state, run } = fixture(t);
36
+ assert.deepEqual(run().report.motivo, 'estado-ausente');
37
+ assert.equal(run().status, 1);
38
+ for (const [preparation, configuration] of [['pendente', 'pendente'], ['concluido', 'pendente'], ['pendente', 'concluido']]) {
39
+ state(document(preparation, configuration));
40
+ const result = run(); assert.equal(result.status, 1);
41
+ assert.equal(result.report.motivo, 'configuracao-pendente');
42
+ assert.equal(existsSync(join(root, 'src')), false);
43
+ }
44
+ });
45
+
46
+ test('aceita o modelo real e mantém provas da aplicação como verificação separada', t => {
47
+ const { root, state, run } = fixture(t);
48
+ const template = readFileSync(join(projectRoot, '.pbai4devs/templates/configuracao/TEMPLATE.md'), 'utf8');
49
+ state(template); assert.equal(run().report.motivo, 'configuracao-pendente');
50
+ state(template.replace('preparacao: pendente', 'preparacao: concluido')
51
+ .replace('configuracao_tecnica: pendente', 'configuracao_tecnica: concluido'));
52
+ assert.equal(run().report.status, 'verificar-evidencias');
53
+ assert.equal(existsSync(join(root, 'src')), false);
54
+ });
55
+
56
+ test('indicadores concluídos exigem revisão adicional de evidências e não alteram Git', t => {
57
+ const { root, state, write, run } = fixture(t);
58
+ state(document('"concluido" # aprovado', "'concluido'"));
59
+ write('.gitignore', 'node_modules/\n'); write('AGENTS.md', '# Instruções humanas\n');
60
+ assert.equal(spawnSync('git', ['init', '--quiet', root]).status, 0);
61
+ assert.equal(spawnSync('git', ['-C', root, 'add', '.gitignore', 'AGENTS.md']).status, 0);
62
+ const index = readFileSync(join(root, '.git/index'));
63
+ const statePath = join(root, '.pbai4devs/state/configuracao/ESTADO.md');
64
+ const original = readFileSync(statePath), ignore = readFileSync(join(root, '.gitignore'));
65
+ const result = run(); assert.equal(result.status, 0);
66
+ assert.equal(result.report.status, 'verificar-evidencias');
67
+ assert.match(result.report.limite, /não comprova evidências/);
68
+ assert.deepEqual(readFileSync(statePath), original);
69
+ assert.deepEqual(readFileSync(join(root, '.git/index')), index);
70
+ assert.deepEqual(readFileSync(join(root, '.gitignore')), ignore);
71
+ assert.equal(existsSync(join(root, 'src')), false);
72
+ });
73
+
74
+ test('recusa indicadores ausentes, desconhecidos, duplicados e não expõe valores', t => {
75
+ const { state, run } = fixture(t);
76
+ const secret = 'SENSITIVE_VALUE_47849';
77
+ for (const value of [
78
+ 'preparacao: concluido',
79
+ 'preparacao: concluido\nconfiguracao_tecnica: ausente',
80
+ 'preparacao: concluido\npreparacao: pendente\nconfiguracao_tecnica: concluido',
81
+ 'preparacao: concluido\nconfiguracao_tecnica: concluido\n"configuracao_tecnica": pendente',
82
+ "'preparacao': concluido\npreparacao: pendente\nconfiguracao_tecnica: concluido",
83
+ 'preparacao: concluido\n"prepara\\u0063ao": pendente\nconfiguracao_tecnica: concluido',
84
+ 'preparacao: concluido\nconfiguracao_tecnica: concluido\n!!str configuracao_tecnica: pendente',
85
+ 'preparacao: concluido\nconfiguracao_tecnica: concluido\npadrao: &base { configuracao_tecnica: pendente }',
86
+ 'preparacao: concluido\nconfiguracao_tecnica: concluido\npadrao: *base',
87
+ 'preparacao: concluido\nconfiguracao_tecnica: concluido\npadrao: !!str valor',
88
+ 'preparacao: concluido\nconfiguracao_tecnica: concluido\n<<: *base',
89
+ 'preparacao: concluido\nconfiguracao_tecnica: concluido\n? configuracao_tecnica\n: pendente',
90
+ `preparacao: concluido\nconfiguracao_tecnica: ${secret}`,
91
+ 'preparacao: concluido\nconfiguracao_tecnica: true',
92
+ ]) {
93
+ state(`---\n${value}\n---\n# Estado\n`);
94
+ const result = run(); assert.equal(result.status, 1);
95
+ assert.equal(result.report.motivo, 'estado-invalido');
96
+ assert.ok(!result.stdout.includes(secret)); assert.ok(!result.stderr.includes(secret));
97
+ }
98
+ state('preparacao: concluido\nconfiguracao_tecnica: concluido\n');
99
+ assert.equal(run().report.motivo, 'estado-invalido');
100
+ state('---\n"preparacao": concluido\n\'configuracao_tecnica\': concluido\n---\n# Estado\n');
101
+ assert.equal(run().report.status, 'verificar-evidencias');
102
+ });
103
+
104
+ test('aceita estado legado e recusa coexistência de árvores de estado', t => {
105
+ const { state, run } = fixture(t);
106
+ state(document('concluido', 'concluido'), 'estado');
107
+ assert.equal(run().report.status, 'verificar-evidencias');
108
+ state(document('concluido', 'concluido'));
109
+ const result = run(); assert.equal(result.status, 1);
110
+ assert.equal(result.report.motivo, 'estado-inacessivel-ou-inseguro');
111
+ });
112
+
113
+ test('não segue link humano em arquivo ou pasta de estado', t => {
114
+ const { root, state, run } = fixture(t);
115
+ const outside = join(root, 'fora'); mkdirSync(outside);
116
+ const target = join(outside, 'ESTADO.md'); writeFileSync(target, document('concluido', 'concluido'));
117
+ const directory = join(root, '.pbai4devs/state/configuracao'); mkdirSync(directory, { recursive: true });
118
+ try { symlinkSync(target, join(directory, 'ESTADO.md')); }
119
+ catch (error) { if (['EPERM', 'EACCES', 'ENOSYS'].includes(error.code)) { t.skip('Sem permissão para links.'); return; } throw error; }
120
+ assert.equal(run().report.motivo, 'estado-inseguro');
121
+ assert.equal(readFileSync(target, 'utf8'), document('concluido', 'concluido'));
122
+ unlinkSync(join(directory, 'ESTADO.md')); rmdirSync(directory);
123
+ symlinkSync(outside, directory, process.platform === 'win32' ? 'junction' : 'dir');
124
+ assert.equal(run().report.motivo, 'estado-inseguro');
125
+ assert.equal(readFileSync(target, 'utf8'), document('concluido', 'concluido'));
126
+ });
127
+
128
+ test('exige Node e rejeita argumentos extras sem ler outro projeto', t => {
129
+ const { root, run } = fixture(t);
130
+ const result = run('--root', root); assert.equal(result.status, 2);
131
+ assert.match(result.stderr, /sem argumentos/);
132
+ assert.equal(verifyConfiguration(root).status, 'bloqueado');
133
+ });
134
+
135
+ test('seletor de scripts propaga bloqueio e exige revisão de evidências', t => {
136
+ const { root, state } = fixture(t);
137
+ const selector = join(root, '.pbai4devs/scripts/executar-script/SCRIPT.mjs');
138
+ const execute = () => spawnSync(process.execPath, [selector, 'verificar-configuracao'], { cwd: root, encoding: 'utf8' });
139
+ state(document('concluido', 'pendente'));
140
+ const pending = execute(); assert.ifError(pending.error); assert.equal(pending.status, 1);
141
+ assert.match(pending.stdout, /"motivo":"configuracao-pendente"/);
142
+ state(document('concluido', 'concluido'));
143
+ const recorded = execute(); assert.ifError(recorded.error); assert.equal(recorded.status, 0);
144
+ assert.match(recorded.stdout, /"status":"verificar-evidencias"/);
145
+ });
@@ -49,9 +49,20 @@ function ownership(root) {
49
49
  if (checkedStat(root, derivedFile)) {
50
50
  const derived = readState(derivedFile);
51
51
  if (derived?.schemaVersion !== 1 || !validMap(derived.entries) || Object.keys(derived.entries).some(path => !canonicalEntrypoint(path))
52
- || !validMap(derived.files ?? {}) || !validMap(derived.blocks ?? {})
52
+ || !validMap(derived.files ?? {}) || !validMap(derived.blocks ?? {}) || !validMap(derived.removedFiles ?? {})
53
53
  || !Array.isArray(derived.removedBlocks ?? []) || (derived.removedBlocks ?? []).some(path => typeof path !== 'string')
54
54
  || !validMap(derived.outputs ?? {}) || Object.keys(derived.outputs ?? {}).some(path => !/^\.[a-z0-9][a-z0-9-]*\/(?:skills|rules|agents)\.json$/.test(path))) fail('Estado de entradas derivadas invalido.');
55
+ for (const [path, hash] of Object.entries(derived.removedFiles ?? {})) {
56
+ const [nativeRoot, scope] = path.split('/');
57
+ if (!/^\.[A-Za-z0-9][A-Za-z0-9._-]*$/.test(nativeRoot) || ['.git', '.pbai4devs', '.agent-tests'].includes(nativeRoot.toLowerCase())
58
+ || !scope || [...auxiliary, 'workflows'].includes(scope.toLowerCase()) || ['readme.md', 'instructions.md'].includes(basename(path).toLowerCase())
59
+ || !(/\.(?:md|mdc)$/i.test(path) || path.startsWith('.gemini/commands/') && path.endsWith('.toml'))) fail('Registro de arquivos removidos invalido.');
60
+ const location = checkedPath(root, path);
61
+ if (state.files[path] && state.files[path] !== hash) fail(`Registro de arquivos removidos divergente: ${path}`);
62
+ if (state.files[path] !== hash || derived.files?.[path] || derived.blocks?.[path] || derived.entries?.[path]) continue;
63
+ if (checkedStat(root, location)) fail(`Arquivo removido foi recriado; revise o conteúdo: ${path}`);
64
+ delete files[path];
65
+ }
55
66
  Object.assign(files, derived.entries, derived.outputs, derived.files);
56
67
  for (const [path, hash] of Object.entries({ ...derived.entries, ...derived.files })) {
57
68
  const location = checkedPath(root, path);
@@ -196,6 +196,28 @@ test('instalação também protege manifestos registrados em derived.outputs', t
196
196
  assert.throws(() => verify(root), /Arquivo gerenciado ausente: .agents\/skills.json/);
197
197
  });
198
198
 
199
+ test('remoções derivadas exigem hash compatível e não ocultam arquivo humano recriado', t => {
200
+ const { root, file, generated } = installedFixture(t);
201
+ const statePath = join(root, '.pbai4devs/state/agentdevkit.json');
202
+ const state = JSON.parse(readFileSync(statePath, 'utf8'));
203
+ const hash = state.files[generated], derivedPath = '.pbai4devs/state/derived.json';
204
+ rmSync(join(root, generated));
205
+ const record = removedFiles => file(derivedPath, JSON.stringify({ schemaVersion: 1, entries: {}, removedFiles }));
206
+ record({ [generated]: hash });
207
+ assert.doesNotThrow(() => verify(root));
208
+ file(generated, document('# Conteúdo recriado pelo usuário\n'));
209
+ const recreated = readFileSync(join(root, generated));
210
+ assert.throws(() => verify(root), /Arquivo removido foi recriado/);
211
+ assert.deepEqual(readFileSync(join(root, generated)), recreated);
212
+ rmSync(join(root, generated));
213
+ record({ [generated]: 'f'.repeat(64) });
214
+ assert.throws(() => verify(root), /Registro de arquivos removidos divergente/);
215
+ record({ [generated]: 'invalido' });
216
+ assert.throws(() => verify(root), /Estado de entradas derivadas invalido/);
217
+ record({ '.pbai4devs/skills/exemplo/SKILL.md': hash });
218
+ assert.throws(() => verify(root), /Registro de arquivos removidos invalido/);
219
+ });
220
+
199
221
  test('catalogo derivado cobre recursos e detecta JSON editado sem reescrever', t => {
200
222
  const { root, file } = resourceFixture(t);
201
223
  file('.pbai4devs/commands/inspecionar/COMMAND.md', document('# Inspecionar\n\n[Script](../../scripts/inspecionar/SCRIPT.mjs).\n'));
@@ -5,9 +5,21 @@ description: Implementa mudanças de backend com contratos, dados e provas de co
5
5
 
6
6
  # Implementar backend
7
7
 
8
- Siga a spec e o plano aprovados, os contratos e a stack existentes. Esclareça
9
- dúvidas sobre entradas, saídas, persistência,
10
- compatibilidade e falhas antes de decidir. Altere somente o escopo atribuído.
8
+ Siga o escopo aprovado, os contratos e a stack existentes. Ao implementar uma
9
+ spec, confirme spec e plano aprovados e, antes de alterar código, execute
10
+ `node .pbai4devs/scripts/executar-script/SCRIPT.mjs verificar-configuracao`.
11
+ Se o estado estiver ausente, inválido ou pendente, ou faltar prova obrigatória,
12
+ pare e informe o responsável. Saída zero ainda exige conferir as evidências.
13
+ Siga a ordem e as pré-condições já acordadas em
14
+ [implementar-spec](../../specs/implementar-spec/SKILL.md), sem reiniciar o fluxo
15
+ nem pedir novamente aprovações já dadas.
16
+
17
+ Em correção avulsa autorizada de projeto existente, preserve suas decisões e
18
+ use `preparar-dev` quando necessário, sem inventar spec ou estado. Uma tarefa
19
+ que cite uma spec ou esteja coberta por seu escopo é tarefa de spec, mesmo
20
+ sendo correção pontual; não a trate como avulsa. Esclareça dúvidas sobre
21
+ entradas, saídas, persistência, compatibilidade e falhas antes de decidir.
22
+ Altere somente o escopo atribuído.
11
23
 
12
24
  Verifique validação de entrada, autorização por recurso e isolamento de dados.
13
25
  Considere limites, concorrência, repetição, timeout, migração e recuperação
@@ -5,9 +5,21 @@ description: Implementa interfaces web conforme a spec, o design e as provas de
5
5
 
6
6
  # Implementar frontend
7
7
 
8
- Siga a spec e o plano aprovados, o design e os componentes existentes.
9
- Esclareça comportamento, conteúdo e acessibilidade
10
- quando faltarem decisões; não escolha framework nem crie telas fora do escopo.
8
+ Siga o escopo aprovado, o design e os componentes existentes. Ao implementar
9
+ uma spec, confirme spec e plano aprovados e, antes de alterar código, execute
10
+ `node .pbai4devs/scripts/executar-script/SCRIPT.mjs verificar-configuracao`.
11
+ Se o estado estiver ausente, inválido ou pendente, ou faltar prova obrigatória,
12
+ pare e informe o responsável. Saída zero ainda exige conferir as evidências.
13
+ Siga a ordem e as pré-condições já acordadas em
14
+ [implementar-spec](../../specs/implementar-spec/SKILL.md), sem reiniciar o fluxo
15
+ nem pedir novamente aprovações já dadas.
16
+
17
+ Em correção avulsa autorizada de projeto existente, preserve suas decisões e
18
+ use `preparar-dev` quando necessário, sem inventar spec ou estado. Uma tarefa
19
+ que cite uma spec ou esteja coberta por seu escopo é tarefa de spec, mesmo
20
+ sendo correção pontual; não a trate como avulsa. Esclareça comportamento,
21
+ conteúdo e acessibilidade quando faltarem decisões; não escolha framework nem
22
+ crie telas fora do escopo.
11
23
 
12
24
  Considere estados de carregamento, vazio, erro e sucesso; navegação por teclado,
13
25
  semântica acessível e diferentes tamanhos de tela. Preserve contratos com o
@@ -6,4 +6,6 @@ description: Examina e resume alterações Git.
6
6
  # git-diff
7
7
 
8
8
  Examine os diffs preparados e não preparados conforme o pedido, incluindo
9
- arquivos não rastreados quando relevantes. Não altere arquivos.
9
+ arquivos não rastreados quando relevantes. Execute consultas com
10
+ `GIT_OPTIONAL_LOCKS=0` no ambiente do subprocesso para evitar atualizar o índice;
11
+ não altere arquivos.
@@ -6,4 +6,5 @@ description: Inspeciona o estado do repositório e das branches locais e remotas
6
6
  # git-status
7
7
 
8
8
  Mostre branch, alterações preparadas e não preparadas, arquivos não rastreados e
9
- divergência do upstream. Não altere arquivos.
9
+ divergência do upstream. Execute a consulta com `GIT_OPTIONAL_LOCKS=0` no
10
+ ambiente do subprocesso para evitar atualizar o índice; não altere arquivos.