@pathbit/agentdevkit 0.0.20 → 0.0.22
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/scripts/diagnosticar-ambiente/tests/TEST.mjs +60 -1
- 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-recursos/SCRIPT.mjs +12 -1
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +22 -0
- package/.pbai4devs/templates/clients/ADAPTERS.json +3 -2
- package/CHANGELOG.md +15 -0
- package/docs/maintainers/maintenance.md +16 -5
- package/docs/specs/README.md +3 -0
- package/docs/specs/automatic-release-tags/SPEC.md +16 -0
- package/docs/specs/discovery-preservation/SPEC.md +26 -0
- package/package.json +1 -1
- package/src/install.mjs +134 -12
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
7
|
import { isMain, projectRoot, requireNode } from '../lib/runtime.mjs';
|
|
@@ -9,6 +9,7 @@ import { isGeneratedOutput } from '../lib/gitignore.mjs';
|
|
|
9
9
|
import { resourcePath, synchronize } from '../sincronizar-agentes/SCRIPT.mjs';
|
|
10
10
|
import { adapterManifest } from '../lib/adapters.mjs';
|
|
11
11
|
import { stateDirectory } from '../lib/state-path.mjs';
|
|
12
|
+
import { nativePathBoundary } from '../lib/native-paths.mjs';
|
|
12
13
|
|
|
13
14
|
const assets = join(dirname(fileURLToPath(import.meta.url)), 'assets');
|
|
14
15
|
const owner = 'testar-maturidade';
|
|
@@ -36,6 +37,39 @@ function noLinks(root, target) {
|
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
// The child binds its cwd to the verified parent directory. A swapped parent
|
|
41
|
+
// changes the inode or real path before the child can create the new directory.
|
|
42
|
+
const createDirectoryScript = `const fs=require('node:fs');
|
|
43
|
+
const [name,expected,dev,ino]=process.argv.slice(1);
|
|
44
|
+
const info=fs.statSync('.');
|
|
45
|
+
if(fs.realpathSync('.')!==expected || String(info.dev)!==dev || String(info.ino)!==ino) process.exit(73);
|
|
46
|
+
try { fs.mkdirSync(name); } catch(error) { if(error.code==='EEXIST') process.exit(17); throw error; }
|
|
47
|
+
`;
|
|
48
|
+
function createChild(root, parent, name, { allowExisting = false } = {}) {
|
|
49
|
+
if (!name || name === '.' || name === '..' || basename(name) !== name) fail('Nome de diretório inválido.');
|
|
50
|
+
noLinks(root, parent);
|
|
51
|
+
const info = stat(parent);
|
|
52
|
+
if (!info?.isDirectory() || info.isSymbolicLink()) fail('Diretório ancestral inválido.');
|
|
53
|
+
const canonical = realpathSync(parent), inside = relative(root, canonical);
|
|
54
|
+
if (isAbsolute(inside) || inside === '..' || inside.startsWith(`..${sep}`)) fail('Diretório ancestral fora do projeto.');
|
|
55
|
+
const env = { ...process.env }; delete env.NODE_TEST_CONTEXT;
|
|
56
|
+
const result = spawnSync(process.execPath, ['-e', createDirectoryScript, name, canonical, String(info.dev), String(info.ino)], { cwd: canonical, env, encoding: 'utf8', timeout: 30000, windowsHide: true, shell: false });
|
|
57
|
+
if (result.error || result.status !== 0 && !(allowExisting && result.status === 17)) fail(`Não foi possível criar diretório exclusivo: ${result.error?.message || result.stderr?.trim() || result.status}`);
|
|
58
|
+
noLinks(root, join(parent, name));
|
|
59
|
+
const output = stat(join(parent, name));
|
|
60
|
+
if (!output?.isDirectory() || output.isSymbolicLink()) fail('Diretório de saída inválido.');
|
|
61
|
+
}
|
|
62
|
+
function ensureDirectories(root, target) {
|
|
63
|
+
let current = root;
|
|
64
|
+
for (const part of relative(root, target).split(sep).filter(Boolean)) {
|
|
65
|
+
const next = join(current, part), info = stat(next);
|
|
66
|
+
if (!info) createChild(root, current, part, { allowExisting: true });
|
|
67
|
+
else if (!info.isDirectory() || info.isSymbolicLink()) fail('Diretório ancestral inválido ou link simbólico.');
|
|
68
|
+
noLinks(root, next);
|
|
69
|
+
current = next;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
39
73
|
export const SCENARIOS = [
|
|
40
74
|
{ id: 'somente-agentes', title: 'Somente agentes', expected: 'preparacao', complete: false, app: false, planning: 'none', note: 'Recursos compartilhados, sem produto ou aplicação.' },
|
|
41
75
|
{ id: 'descoberta', title: 'Descoberta de produto', expected: 'descoberta', complete: false, app: false, planning: 'draft', note: 'PRD em rascunho; nenhuma stack é iniciada.' },
|
|
@@ -63,11 +97,28 @@ export function registeredRoots(root) {
|
|
|
63
97
|
|
|
64
98
|
export function resourceTargets(root, source, kind = 'skills') {
|
|
65
99
|
const rows = manifest(root).split(/\r?\n/).filter(line => line && !line.startsWith('#')).map(line => line.split('|'));
|
|
66
|
-
const targets = [];
|
|
100
|
+
const targets = [], suppressed = new Set();
|
|
101
|
+
for (const [type, fallback, primary, resource] of rows) {
|
|
102
|
+
if (type !== 'fallback' || resource !== kind) continue;
|
|
103
|
+
const primaryRow = rows.find(([entry, target]) => entry === 'agente' && target === primary);
|
|
104
|
+
const fallbackRow = rows.find(([entry, target]) => entry === 'agente' && target === fallback);
|
|
105
|
+
if (!primaryRow || !fallbackRow) fail(`Fallback sem cliente: ${fallback}`);
|
|
106
|
+
const primaryFormat = rows.find(([entry, target, scope]) => entry === 'formato' && target === primary && scope === kind)?.[3] || primaryRow[2] || 'agrupado';
|
|
107
|
+
const primaryTarget = `${primary}/${resourcePath(source, kind, primaryFormat)}`;
|
|
108
|
+
const boundary = nativePathBoundary(root, primaryTarget, [], { preserveRootAliases: true });
|
|
109
|
+
const info = boundary ? null : stat(join(root, primaryTarget));
|
|
110
|
+
if (boundary || (info && !info.isFile())) {
|
|
111
|
+
suppressed.add(primaryTarget);
|
|
112
|
+
targets.push(`${fallback}/${resourcePath(source, kind, fallbackRow[2] || 'agrupado')}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
67
115
|
for (const [type, agent, layout = 'agrupado'] of rows) {
|
|
68
116
|
if (type !== 'agente') continue;
|
|
69
117
|
const override = rows.find(([entry, target, resource]) => entry === 'formato' && target === agent && resource === kind)?.[3];
|
|
70
|
-
|
|
118
|
+
if (override === 'nenhum') continue;
|
|
119
|
+
let target = `${agent}/${resourcePath(source, kind, override || layout)}`;
|
|
120
|
+
if (agent === '.gemini' && kind === 'commands') target = target.replace(/\.md$/, '.toml');
|
|
121
|
+
if (!suppressed.has(target)) targets.push(target);
|
|
71
122
|
}
|
|
72
123
|
for (const [type, target, origin] of rows) if (type === 'nativo' && origin === source) targets.push(target);
|
|
73
124
|
return [...new Set(targets)];
|
|
@@ -139,29 +190,6 @@ export function copyAgentStructure(sourceRoot, destination) {
|
|
|
139
190
|
return { roots, copiedFiles: files.length, skipped, files, sourceDigest, entrypoints, toolDefinitions: files.filter(file => /^\.pbai4devs\/tools\/[^/]+\/TOOL\.json$/.test(file)).length };
|
|
140
191
|
}
|
|
141
192
|
|
|
142
|
-
function ownership(root, file, expectedId) {
|
|
143
|
-
if (stat(root)?.isSymbolicLink()) return false;
|
|
144
|
-
const path = join(root, file);
|
|
145
|
-
if (!existsSync(path)) return false;
|
|
146
|
-
const item = lstatSync(path);
|
|
147
|
-
if (!item.isFile() || item.isSymbolicLink()) return false;
|
|
148
|
-
try { const data = json(path); return data.owner === owner && (!expectedId || data.id === expectedId); }
|
|
149
|
-
catch { return false; }
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function ensureOutput(output) {
|
|
153
|
-
if (existsSync(output) && lstatSync(output).isSymbolicLink()) fail('A pasta de exemplos não pode ser um link.');
|
|
154
|
-
for (const name of [suiteMarker, 'README.md', 'REPORT.json', '.gitignore']) {
|
|
155
|
-
const item = stat(join(output, name));
|
|
156
|
-
if (item && (!item.isFile() || item.isSymbolicLink())) fail(`Arquivo de saída deve ser regular: ${name}`);
|
|
157
|
-
}
|
|
158
|
-
if (existsSync(output) && [suiteMarker, 'README.md', 'REPORT.json'].some(name => existsSync(join(output, name)))
|
|
159
|
-
&& !ownership(output, suiteMarker)) fail('Índice de exemplos existente não pertence a este gerador.');
|
|
160
|
-
if (existsSync(join(output, 'REPORT.json')) && !ownership(output, 'REPORT.json')) fail('Relatório de exemplos existente não pertence a este gerador.');
|
|
161
|
-
if (existsSync(join(output, '.gitignore')) && readFileSync(join(output, '.gitignore'), 'utf8') !== outputIgnore) fail('Ignore de exemplos existente não pertence a este gerador.');
|
|
162
|
-
mkdirSync(output, { recursive: true });
|
|
163
|
-
}
|
|
164
|
-
|
|
165
193
|
function planning(root, scenario) {
|
|
166
194
|
const stateRoot = stateDirectory(root);
|
|
167
195
|
const state = `${stateRoot}/configuracao/ESTADO.md`;
|
|
@@ -228,6 +256,13 @@ function application(root, scenario) {
|
|
|
228
256
|
preserved.push(server, test, 'config/legacy.json', '.github/workflows/legacy.yml', '.vscode/settings.json', 'Makefile', 'package.json');
|
|
229
257
|
}
|
|
230
258
|
if (scenario.configured) {
|
|
259
|
+
// Published packages and existing projects need not contain these files;
|
|
260
|
+
// create only the missing synthetic fixtures, never replace copied content.
|
|
261
|
+
for (const [file, content] of Object.entries({
|
|
262
|
+
'.gitignore': '# Arquivos temporários do cenário sintético.\nnode_modules/\n.agent-tests/\n',
|
|
263
|
+
'.gitattributes': '* text=auto\n',
|
|
264
|
+
'.editorconfig': 'root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\n',
|
|
265
|
+
})) if (!stat(join(root, file))) write(root, file, content);
|
|
231
266
|
write(root, '.dockerignore', '.git\n.agent-tests\nnode_modules\n');
|
|
232
267
|
write(root, '.env.example', 'PORT=3000\n');
|
|
233
268
|
write(root, 'Dockerfile', 'FROM node:24-alpine\nWORKDIR /app\nCOPY package.json ./\nCOPY src ./src\nEXPOSE 3000\nCMD ["node", "src/server.mjs"]\n');
|
|
@@ -242,31 +277,37 @@ function gitInit(root) {
|
|
|
242
277
|
if (result.error || result.status !== 0) fail(`Git init: ${result.error?.message || result.stderr}`);
|
|
243
278
|
}
|
|
244
279
|
|
|
245
|
-
export function prepareExamples({ sourceRoot = projectRoot, outputRoot
|
|
246
|
-
requireNode(); sourceRoot = realpathSync(resolve(sourceRoot));
|
|
247
|
-
const
|
|
280
|
+
export function prepareExamples({ sourceRoot = projectRoot, outputRoot, scenarios = SCENARIOS, initializeGit = true, renderResources = prepareResources } = {}) {
|
|
281
|
+
requireNode(); sourceRoot = realpathSync(resolve(sourceRoot));
|
|
282
|
+
const base = resolve(outputRoot ?? defaultOutput(sourceRoot));
|
|
283
|
+
const outputPath = portable(relative(sourceRoot, base));
|
|
248
284
|
if (!(outputPath === 'examples/generated' || outputPath.startsWith('examples/generated/')
|
|
249
285
|
|| outputPath === 'tmp/examples' || outputPath.startsWith('tmp/examples/') || outputPath.startsWith('.agent-tests/'))) fail('Saída permitida somente em examples/generated, tmp/examples ou descendente de .agent-tests.');
|
|
250
|
-
noLinks(sourceRoot,
|
|
286
|
+
noLinks(sourceRoot, base);
|
|
251
287
|
const ids = new Set();
|
|
288
|
+
if (!Array.isArray(scenarios) || !scenarios.length) fail('Selecione pelo menos um cenário.');
|
|
252
289
|
for (const scenario of scenarios) {
|
|
253
290
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(scenario.id) || ids.has(scenario.id)) fail('Cenários devem ter IDs únicos e seguros.');
|
|
254
291
|
ids.add(scenario.id);
|
|
255
292
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
293
|
+
if (outputRoot !== undefined && stat(base)) fail(`Pasta de saída já existe; escolha um destino novo: ${base}`);
|
|
294
|
+
if (outputRoot !== undefined) {
|
|
295
|
+
for (let parent = dirname(base); parent !== sourceRoot && parent.startsWith(sourceRoot + sep); parent = dirname(parent)) {
|
|
296
|
+
if (stat(join(parent, suiteMarker)) || stat(join(parent, marker))) fail('Saída não pode ficar dentro de uma execução anterior.');
|
|
297
|
+
}
|
|
261
298
|
}
|
|
299
|
+
const parent = outputRoot === undefined ? base : dirname(base);
|
|
300
|
+
ensureDirectories(sourceRoot, parent);
|
|
301
|
+
const name = outputRoot === undefined ? `maturity-${randomUUID()}` : basename(base);
|
|
302
|
+
createChild(sourceRoot, parent, name);
|
|
303
|
+
outputRoot = join(parent, name);
|
|
262
304
|
write(outputRoot, suiteMarker, JSON.stringify({ owner, version: 1 }, null, 2) + '\n');
|
|
263
305
|
write(outputRoot, '.gitignore', outputIgnore);
|
|
264
|
-
rmSync(join(outputRoot, 'REPORT.json'), { force: true });
|
|
265
306
|
writeIndex(outputRoot, [], 'preparing');
|
|
266
307
|
const examples = [];
|
|
267
308
|
for (const scenario of scenarios) {
|
|
268
309
|
const destination = join(outputRoot, scenario.id);
|
|
269
|
-
|
|
310
|
+
mkdirSync(destination);
|
|
270
311
|
write(destination, marker, JSON.stringify({ owner, version: 1, id: scenario.id }, null, 2) + '\n');
|
|
271
312
|
const copied = copyAgentStructure(sourceRoot, destination);
|
|
272
313
|
planning(destination, scenario);
|
|
@@ -283,8 +324,8 @@ export function prepareExamples({ sourceRoot = projectRoot, outputRoot = default
|
|
|
283
324
|
|
|
284
325
|
function writeIndex(outputRoot, examples, status) {
|
|
285
326
|
const rows = examples.map(item => `| [${item.id}](${item.id}/README.md) | ${item.expected} | ${item.status || 'prepared'} |`).join('\n');
|
|
286
|
-
const progress = status === 'preparing' ? '\nPreparação em andamento
|
|
287
|
-
write(outputRoot, 'README.md', document('Exemplos de maturidade', `Projetos sintéticos com cópias locais da estrutura de agentes.\nNenhum resultado certifica produção, revisão humana ou outros sistemas operacionais.\n${progress}\n| Cenário | Nível esperado | Resultado |\n| --- | --- | --- |${rows ? '\n' + rows : ''}\n\nRelatório: \`REPORT.json\`. Logs e evidências por projeto ficam em \`.agent-tests/\`.\
|
|
327
|
+
const progress = status === 'preparing' ? '\nPreparação em andamento nesta execução.\n' : '';
|
|
328
|
+
write(outputRoot, 'README.md', document('Exemplos de maturidade', `Projetos sintéticos com cópias locais da estrutura de agentes.\nNenhum resultado certifica produção, revisão humana ou outros sistemas operacionais.\n${progress}\n| Cenário | Nível esperado | Resultado |\n| --- | --- | --- |${rows ? '\n' + rows : ''}\n\nRelatório: \`REPORT.json\`. Logs e evidências por projeto ficam em \`.agent-tests/\`.\nCada execução usa uma pasta nova e preserva as anteriores.\n`, `status: ${status}\n`));
|
|
288
329
|
}
|
|
289
330
|
|
|
290
331
|
function hashes(root, files) { return Object.fromEntries(files.map(file => [file, digest(readFileSync(join(root, file)))])); }
|
|
@@ -453,7 +494,7 @@ export function testExample(example) {
|
|
|
453
494
|
export function runExamples(options = {}) {
|
|
454
495
|
const examples = prepareExamples(options);
|
|
455
496
|
const results = examples.map(example => { const result = testExample(example); console.log(`${example.id}=${result.status}${result.error ? ': ' + result.error : ''}`); return result; });
|
|
456
|
-
const outputRoot =
|
|
497
|
+
const outputRoot = dirname(examples[0].root);
|
|
457
498
|
const report = { owner, version: 1, status: results.every(item => item.status === 'passed') ? 'passed' : 'failed', platform: process.platform, arch: process.arch, node: process.version, results, limits: ['Provas reais somente das operações descritas e APIs locais.', 'Pareceres do gate são simulações explícitas, sem revisão humana.', 'Não certifica produção nem sistemas operacionais não executados.'] };
|
|
458
499
|
write(outputRoot, 'REPORT.json', JSON.stringify(report, null, 2) + '\n');
|
|
459
500
|
writeIndex(outputRoot, examples.map(example => ({ ...example, status: results.find(result => result.id === example.id).status })), report.status);
|
|
@@ -462,15 +503,16 @@ export function runExamples(options = {}) {
|
|
|
462
503
|
|
|
463
504
|
export function main(args = process.argv.slice(2)) {
|
|
464
505
|
requireNode(); const [mode = 'help', ...rest] = args;
|
|
465
|
-
if (['help', '--help', '-h'].includes(mode)) { console.log('Uso: testar-maturidade prepare|run [--case <id>].
|
|
506
|
+
if (['help', '--help', '-h'].includes(mode)) { console.log('Uso: testar-maturidade prepare|run [--case <id>]. Cada execução cria uma nova pasta em examples/generated.'); return 0; }
|
|
466
507
|
if (!['prepare', 'run'].includes(mode)) fail('Use prepare ou run.');
|
|
467
508
|
let scenarios = SCENARIOS;
|
|
468
509
|
if (rest.length) {
|
|
469
510
|
if (rest.length !== 2 || rest[0] !== '--case') fail('Use --case <id>.');
|
|
470
511
|
scenarios = SCENARIOS.filter(item => item.id === rest[1]); if (!scenarios.length) fail('Cenário desconhecido.');
|
|
471
512
|
}
|
|
472
|
-
if (mode === 'prepare') { const examples = prepareExamples({ scenarios }); console.log(`prepared=${examples.length}`); return 0; }
|
|
473
|
-
|
|
513
|
+
if (mode === 'prepare') { const examples = prepareExamples({ scenarios }); console.log(`prepared=${examples.length}\noutput=${dirname(examples[0].root)}`); return 0; }
|
|
514
|
+
const result = runExamples({ scenarios }); console.log(`output=${result.outputRoot}`);
|
|
515
|
+
return result.report.status === 'passed' ? 0 : 1;
|
|
474
516
|
}
|
|
475
517
|
|
|
476
518
|
if (isMain(import.meta.url)) {
|
|
@@ -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));
|
|
@@ -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'));
|
|
@@ -79,9 +79,10 @@
|
|
|
79
79
|
"name": "Codex",
|
|
80
80
|
"root": ".codex",
|
|
81
81
|
"formats": {
|
|
82
|
-
"skills": "
|
|
82
|
+
"skills": "nenhum"
|
|
83
83
|
},
|
|
84
|
-
"
|
|
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,21 @@ description: Alterações verificáveis por versão do AgentDevKit.
|
|
|
4
4
|
|
|
5
5
|
# Changelog
|
|
6
6
|
|
|
7
|
+
## 0.0.22 — 2026-11-13
|
|
8
|
+
|
|
9
|
+
- Descoberta de skills do Codex sem duplicação, com alternativa segura quando a
|
|
10
|
+
rota principal está obstruída.
|
|
11
|
+
- Sincronização e atualização reconciliam o estado sem substituir recursos ou
|
|
12
|
+
metadados humanos.
|
|
13
|
+
- Scripts reconhecem caminhos por aliases e exemplos de maturidade usam saídas isoladas.
|
|
14
|
+
- Testes ampliados para combinações de clientes, projetos existentes e
|
|
15
|
+
preservação do Git.
|
|
16
|
+
|
|
17
|
+
## 0.0.21 — 2026-11-13
|
|
18
|
+
|
|
19
|
+
- Criação automática da tag da versão após aprovação da matriz de CI.
|
|
20
|
+
- Publicação acionada pela automação com proteção contra colisões de tags.
|
|
21
|
+
|
|
7
22
|
## 0.0.20 — 2026-11-13
|
|
8
23
|
|
|
9
24
|
- Atualização da ferramenta agent-browser compatível com sua distribuição atual.
|
|
@@ -75,8 +75,15 @@ nada. Nunca versione o tarball ou seus relatórios.
|
|
|
75
75
|
|
|
76
76
|
1. Atualize versão e lockfile, changelog, exemplos e escopo da entrega.
|
|
77
77
|
2. Conclua testes, documentação e revisão independente.
|
|
78
|
-
3. Envie o commit à branch principal e
|
|
79
|
-
|
|
78
|
+
3. Envie o commit à branch principal e acompanhe o CI e a release.
|
|
79
|
+
|
|
80
|
+
Após toda a matriz de CI passar, a automação cria a tag `v<versão>` no
|
|
81
|
+
commit verificado e inicia o workflow de release. Um push sem mudança de versão
|
|
82
|
+
publicada não gera outra release. Uma tag existente não é movida: se a versão
|
|
83
|
+
mudou e a tag aponta para outro commit, a automação falha para revisão.
|
|
84
|
+
Versões inferiores às já registradas também não são publicadas automaticamente.
|
|
85
|
+
Se um push cancelar o CI anterior e a versão ainda não tiver tag, o próximo
|
|
86
|
+
CI aprovado da branch principal poderá publicar a versão no seu próprio commit.
|
|
80
87
|
|
|
81
88
|
O [workflow de release][release] localiza o CI aprovado de um push na branch
|
|
82
89
|
principal, confere o commit e baixa seu artifact por ID. Verifica procedência,
|
|
@@ -85,9 +92,10 @@ Também envia o mesmo pacote ao [npm público e GitHub Packages](packages.md),
|
|
|
85
92
|
confere a integridade nos dois destinos e testa versão, instalação, diagnóstico
|
|
86
93
|
e sincronização. O consumo pelo npm é anônimo; pelo GitHub, autenticado.
|
|
87
94
|
|
|
88
|
-
A tag é a origem da publicação: `v0.0.
|
|
89
|
-
`0.0.
|
|
90
|
-
não crie outra tag para uma versão nem mova uma tag já publicada.
|
|
95
|
+
A tag é a origem da publicação: `v0.0.21`, a release `v0.0.21` e os pacotes
|
|
96
|
+
`0.0.21` apontam para a mesma entrega. A execução manual também exige a tag;
|
|
97
|
+
não crie outra tag para uma versão nem mova uma tag já publicada. Publicações
|
|
98
|
+
feitas fora desse fluxo não criam tags automaticamente.
|
|
91
99
|
|
|
92
100
|
Para consultar o código de uma versão sem trocar os arquivos atuais:
|
|
93
101
|
|
|
@@ -124,6 +132,9 @@ Se a release ainda não existe, use **Re-run failed jobs** ou
|
|
|
124
132
|
`gh workflow run release.yml --ref vX.Y.Z` com a tag real. A publicação prioriza
|
|
125
133
|
o secret `NPMJS_TOKEN` do repositório; sem ele, usa OIDC, conforme
|
|
126
134
|
[publicação](packages.md).
|
|
135
|
+
Se a criação da tag ou o acionamento falhar, consulte a execução **Automatic
|
|
136
|
+
release tag**. Uma tag no commit correto pode ser retomada sem movê-la; se ela
|
|
137
|
+
apontar para outro commit, investigue antes de qualquer publicação.
|
|
127
138
|
|
|
128
139
|
Se a release já existe e falta somente o pacote em um registro, use o modo
|
|
129
140
|
`package_only` descrito em [pacote npm](packages.md#publicar-pelo-actions).
|
package/docs/specs/README.md
CHANGED
|
@@ -27,6 +27,9 @@ description: Incrementos verificáveis do AgentDevKit.
|
|
|
27
27
|
- [Ignore do projeto](project-ignore/SPEC.md): preservar o arquivo da raiz.
|
|
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
|
+
- [Tags automáticas](automatic-release-tags/SPEC.md): publicação após CI aprovada.
|
|
31
|
+
- [Descoberta e preservação](discovery-preservation/SPEC.md): rotas de skills,
|
|
32
|
+
sincronização segura e validação em projetos reais.
|
|
30
33
|
|
|
31
34
|
Cada incremento vincula requisitos, critérios, testes e evidências locais.
|
|
32
35
|
|