@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.
- package/.pbai4devs/COMPATIBILIDADE.md +11 -1
- package/.pbai4devs/rules/ambiente-local/RULE.md +5 -0
- package/.pbai4devs/rules/git/RULE.md +5 -1
- package/.pbai4devs/scripts/README.md +1 -0
- package/.pbai4devs/scripts/diagnosticar-ambiente/tests/TEST.mjs +60 -1
- package/.pbai4devs/scripts/inspecionar-projeto/README.md +6 -0
- package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +218 -18
- package/.pbai4devs/scripts/lib/adapters.mjs +5 -2
- package/.pbai4devs/scripts/lib/runtime.mjs +38 -2
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +5 -1
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +136 -9
- package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +30 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +22 -0
- package/.pbai4devs/scripts/testar-maturidade/README.md +12 -10
- package/.pbai4devs/scripts/testar-maturidade/SCRIPT.mjs +86 -44
- package/.pbai4devs/scripts/testar-maturidade/tests/TEST.mjs +157 -33
- package/.pbai4devs/scripts/verificar-configuracao/README.md +22 -0
- package/.pbai4devs/scripts/verificar-configuracao/SCRIPT.mjs +63 -0
- package/.pbai4devs/scripts/verificar-configuracao/tests/TEST.mjs +145 -0
- package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +12 -1
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +22 -0
- package/.pbai4devs/skills/desenvolvimento/implementar-backend/SKILL.md +15 -3
- package/.pbai4devs/skills/desenvolvimento/implementar-frontend/SKILL.md +15 -3
- package/.pbai4devs/skills/git/git-diff/SKILL.md +3 -1
- package/.pbai4devs/skills/git/git-status/SKILL.md +2 -1
- package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +13 -8
- package/.pbai4devs/skills/projeto/proximos-passos/SKILL.md +4 -2
- package/.pbai4devs/skills/specs/implementar-spec/SKILL.md +14 -2
- package/.pbai4devs/templates/clients/ADAPTERS.json +3 -2
- package/CHANGELOG.md +20 -0
- package/docs/guides/cli.md +9 -0
- package/docs/guides/troubleshooting.md +4 -0
- package/docs/specs/README.md +5 -0
- package/docs/specs/discovery-preservation/SPEC.md +26 -0
- package/docs/specs/preimplementation-order/SPEC.md +23 -0
- package/docs/specs/target-paths/SPEC.md +19 -0
- package/package.json +1 -1
- package/src/install.mjs +155 -17
- 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('
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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('
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
assert.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
113
|
-
const
|
|
114
|
-
|
|
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.
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
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.
|
|
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.
|
|
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.
|