@pathbit/agentdevkit 0.0.7 → 0.0.8
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/scripts/lib/gitignore.mjs +84 -15
- package/.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs +41 -20
- package/.pbai4devs/scripts/preparar-ferramentas/tests/TEST.mjs +81 -0
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +120 -20
- package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +145 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +145 -1
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +2 -1
- package/CHANGELOG.md +9 -0
- package/README.en.md +2 -2
- package/README.es.md +2 -2
- package/README.md +2 -2
- package/docs/guides/cli.md +9 -0
- package/docs/guides/installation.md +8 -2
- package/docs/guides/tools-and-testing.md +2 -0
- package/docs/maintainers/packages.md +1 -1
- package/docs/product/roadmap.md +2 -0
- package/docs/specs/README.md +1 -0
- package/docs/specs/v0.0.8/README.md +10 -0
- package/docs/specs/v0.0.8/SPEC.md +62 -0
- package/package.json +1 -1
- package/src/cli.mjs +10 -4
- package/src/install.mjs +61 -22
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { syncBuiltinESMExports } from 'node:module';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { projectRoot } from '../../lib/runtime.mjs';
|
|
10
|
+
import { isReference, synchronize } from '../SCRIPT.mjs';
|
|
11
|
+
|
|
12
|
+
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
13
|
+
const source = '.pbai4devs/skills/demo/SKILL.md';
|
|
14
|
+
const generated = '.claude/skills/demo/SKILL.md';
|
|
15
|
+
const human = '# Título\n\nLeia [manual](https://example.com/manual).\n';
|
|
16
|
+
function write(root, path, data) { fs.mkdirSync(dirname(join(root, path)), { recursive: true }); fs.writeFileSync(join(root, path), data); }
|
|
17
|
+
function fixture(t) {
|
|
18
|
+
const root = fs.realpathSync(fs.mkdtempSync(join(tmpdir(), 'agentdevkit-ownership-')));
|
|
19
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
20
|
+
write(root, '.pbai4devs/espelhos.conf', 'agente|.claude|plano\n');
|
|
21
|
+
write(root, source, '---\nname: demo\ndescription: Exemplo.\n---\n\n# Demo\n');
|
|
22
|
+
for (const name of ['agents.ignore', 'shared.ignore', 'local.ignore', 'clients.json']) write(root, `.pbai4devs/templates/gitignore/${name}`, fs.readFileSync(join(projectRoot, '.pbai4devs/templates/gitignore', name)));
|
|
23
|
+
const initialized = spawnSync('git', ['-C', root, 'init', '-q'], { encoding: 'utf8' });
|
|
24
|
+
assert.equal(initialized.status, 0, initialized.stderr);
|
|
25
|
+
return root;
|
|
26
|
+
}
|
|
27
|
+
const run = (root, options = {}) => synchronize({ root, generateEntrypoints: false, localIgnores: false, output() {}, ...options });
|
|
28
|
+
function snapshot(root) {
|
|
29
|
+
const files = {};
|
|
30
|
+
function visit(path, prefix = '') {
|
|
31
|
+
for (const entry of fs.readdirSync(path, { withFileTypes: true })) {
|
|
32
|
+
if (entry.name === '.git') continue;
|
|
33
|
+
const next = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
34
|
+
if (entry.isDirectory()) { files[next] = 'directory'; visit(join(path, entry.name), next); }
|
|
35
|
+
else files[next] = hash(fs.readFileSync(join(path, entry.name)));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
visit(root); return files;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test('texto humano com formato de referência nunca comprova propriedade', t => {
|
|
42
|
+
for (const path of [generated, '.claude/skills/human.md']) {
|
|
43
|
+
const root = fixture(t); write(root, path, human);
|
|
44
|
+
const before = snapshot(root);
|
|
45
|
+
assert.equal(isReference(human), true, 'O parser de formato continua independente da propriedade.');
|
|
46
|
+
assert.equal(run(root, { check: true }).exitCode, 1);
|
|
47
|
+
assert.throws(() => run(root), /--force/);
|
|
48
|
+
assert.deepEqual(snapshot(root), before);
|
|
49
|
+
assert.equal(fs.readFileSync(join(root, path), 'utf8'), human);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('hash registrado comprova referência legada sem marcador e diretório vazio humano permanece', t => {
|
|
54
|
+
const root = fixture(t), stale = '.claude/skills/legacy.md';
|
|
55
|
+
write(root, stale, human);
|
|
56
|
+
write(root, '.pbai4devs/estado/agentdevkit.json', JSON.stringify({ schemaVersion: 1, clients: ['claude'], files: { [stale]: hash(human) }, blocks: {} }));
|
|
57
|
+
fs.mkdirSync(join(root, '.claude/skills/user-empty'), { recursive: true });
|
|
58
|
+
run(root);
|
|
59
|
+
assert.equal(fs.existsSync(join(root, stale)), false);
|
|
60
|
+
assert.equal(fs.existsSync(join(root, '.claude/skills/user-empty')), true);
|
|
61
|
+
assert.equal(run(root, { check: true }).exitCode, 0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('conflito de ignore é recusado antes de criar recursos ou catálogos', t => {
|
|
65
|
+
const root = fixture(t); write(root, '.gitignore', '!/.agent-tests/\n!/.agent-tests/**\n');
|
|
66
|
+
const before = snapshot(root), exclude = fs.readFileSync(join(root, '.git/info/exclude'));
|
|
67
|
+
assert.throws(() => run(root, { localIgnores: true, force: true }), /Regras humanas/);
|
|
68
|
+
assert.deepEqual(snapshot(root), before);
|
|
69
|
+
assert.deepEqual(fs.readFileSync(join(root, '.git/info/exclude')), exclude);
|
|
70
|
+
assert.equal(fs.existsSync(join(root, generated)), false);
|
|
71
|
+
assert.equal(fs.existsSync(join(root, '.pbai4devs/catalog.json')), false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('atualização de referência própria conserva permissões', t => {
|
|
75
|
+
const root = fixture(t); run(root);
|
|
76
|
+
fs.chmodSync(join(root, generated), 0o600);
|
|
77
|
+
const mode = fs.statSync(join(root, generated)).mode & 0o777;
|
|
78
|
+
write(root, source, '---\nname: demo\ndescription: Atualizado.\n---\n\n# Demo\n');
|
|
79
|
+
run(root);
|
|
80
|
+
assert.equal(fs.statSync(join(root, generated)).mode & 0o777, mode);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('mudança concorrente de ignore após gravação reverte recursos e preserva a regra humana', t => {
|
|
84
|
+
const root = fixture(t); run(root);
|
|
85
|
+
write(root, source, '---\nname: demo\ndescription: Atualizado.\n---\n\n# Demo\n');
|
|
86
|
+
const before = snapshot(root), rename = fs.renameSync;
|
|
87
|
+
let injected = false;
|
|
88
|
+
t.mock.method(fs, 'renameSync', (from, to) => {
|
|
89
|
+
rename(from, to);
|
|
90
|
+
if (!injected && to === join(root, generated)) { injected = true; write(root, '.git/info/exclude', '# concurrent human change\n'); }
|
|
91
|
+
});
|
|
92
|
+
syncBuiltinESMExports();
|
|
93
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
94
|
+
assert.throws(() => run(root, { localIgnores: true }), /Exclusao alterada apos planejar/);
|
|
95
|
+
assert.equal(injected, true);
|
|
96
|
+
assert.deepEqual(snapshot(root), before);
|
|
97
|
+
assert.equal(fs.readFileSync(join(root, '.git/info/exclude'), 'utf8'), '# concurrent human change\n');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('conteúdo humano concorrente não é apagado durante rollback', t => {
|
|
101
|
+
const root = fixture(t), rename = fs.renameSync;
|
|
102
|
+
let injected = false;
|
|
103
|
+
t.mock.method(fs, 'renameSync', (from, to) => {
|
|
104
|
+
rename(from, to);
|
|
105
|
+
if (!injected && to === join(root, generated)) { injected = true; write(root, generated, human); }
|
|
106
|
+
});
|
|
107
|
+
syncBuiltinESMExports();
|
|
108
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
109
|
+
assert.throws(() => run(root), /Mudanca concorrente preservada/);
|
|
110
|
+
assert.equal(injected, true);
|
|
111
|
+
assert.equal(fs.readFileSync(join(root, generated), 'utf8'), human);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('conteúdo humano é preservado quando a escrita lança erro após promover o arquivo', t => {
|
|
115
|
+
const root = fixture(t), rename = fs.renameSync;
|
|
116
|
+
const failure = new Error('falha depois de promover');
|
|
117
|
+
let injected = false;
|
|
118
|
+
t.mock.method(fs, 'renameSync', (from, to) => {
|
|
119
|
+
rename(from, to);
|
|
120
|
+
if (!injected && to === join(root, generated)) { injected = true; write(root, generated, human); throw failure; }
|
|
121
|
+
});
|
|
122
|
+
syncBuiltinESMExports();
|
|
123
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
124
|
+
assert.throws(() => run(root), error => error instanceof AggregateError && error.errors.includes(failure));
|
|
125
|
+
assert.equal(injected, true);
|
|
126
|
+
assert.equal(fs.readFileSync(join(root, generated), 'utf8'), human);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('rollback recria ancestral podado com suas permissões originais', { skip: process.platform === 'win32' }, t => {
|
|
130
|
+
const root = fixture(t); run(root);
|
|
131
|
+
const parent = join(root, '.claude/skills'); fs.chmodSync(parent, 0o700);
|
|
132
|
+
fs.rmSync(join(root, '.pbai4devs/skills/demo'), { recursive: true });
|
|
133
|
+
const before = snapshot(root), rename = fs.renameSync;
|
|
134
|
+
let injected = false;
|
|
135
|
+
t.mock.method(fs, 'renameSync', (from, to) => {
|
|
136
|
+
rename(from, to);
|
|
137
|
+
if (!injected && to === join(root, '.pbai4devs/catalog.json')) { injected = true; write(root, '.git/info/exclude', '# concurrent human change\n'); }
|
|
138
|
+
});
|
|
139
|
+
syncBuiltinESMExports();
|
|
140
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
141
|
+
assert.throws(() => run(root, { localIgnores: true }), /Exclusao alterada apos planejar/);
|
|
142
|
+
assert.equal(injected, true);
|
|
143
|
+
assert.deepEqual(snapshot(root), before);
|
|
144
|
+
assert.equal(fs.statSync(parent).mode & 0o777, 0o700);
|
|
145
|
+
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import test from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import { syncBuiltinESMExports } from 'node:module';
|
|
8
10
|
import { projectRoot } from '../../lib/runtime.mjs';
|
|
9
11
|
import { planLocalIgnore, applyLocalIgnore, checkLocalIgnore, planUntrack, applyUntrack, restoreUntrack, assertLocalIgnoreUnchanged } from '../../lib/gitignore.mjs';
|
|
10
12
|
|
|
@@ -354,3 +356,145 @@ test('worktree usa a raiz nativa, exclude comum e seu próprio índice', t => {
|
|
|
354
356
|
assert.equal(git(linked, ['show', ':generated.md']), 'gerado\n');
|
|
355
357
|
assert.equal(git(root, ['ls-files', 'generated.md']), '');
|
|
356
358
|
});
|
|
359
|
+
|
|
360
|
+
test('exceções com nomes arbitrários de artefatos não passam por amostragem', t => {
|
|
361
|
+
for (const [name, directory] of [['.playwright-custom', true], ['.playwright-report.json', false], ['playwright-e2e', true]]) {
|
|
362
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
363
|
+
const human = `!/${name}${directory ? '/' : ''}\n${directory ? `!/${name}/**\n` : ''}`;
|
|
364
|
+
put(root, '.gitignore', human);
|
|
365
|
+
const before = readFileSync(join(root, '.git/info/exclude'));
|
|
366
|
+
const plan = planLocalIgnore(root);
|
|
367
|
+
assert.ok(checkLocalIgnore(plan).missing.some(path => path.startsWith(name)), name);
|
|
368
|
+
assert.throws(() => applyLocalIgnore(plan), /Regras humanas/);
|
|
369
|
+
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), human);
|
|
370
|
+
assert.deepEqual(readFileSync(join(root, '.git/info/exclude')), before);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test('arquivo real reaberto por glob é detectado também se surgir após o plano', t => {
|
|
375
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
376
|
+
put(root, '.gitignore', '!/.playwright-report-*.json\n');
|
|
377
|
+
const plan = planLocalIgnore(root), path = '.playwright-report-real.json';
|
|
378
|
+
put(root, path, 'artefato local\n');
|
|
379
|
+
assert.ok(checkLocalIgnore(plan).missing.includes(path));
|
|
380
|
+
assert.throws(() => applyLocalIgnore(plan), /Regras humanas/);
|
|
381
|
+
assert.equal(readFileSync(join(root, path), 'utf8'), 'artefato local\n');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('regras customizadas aninhadas continuam válidas e incluem pais ocultos', t => {
|
|
385
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
386
|
+
const policy = readFileSync(join(projectRoot, '.pbai4devs/templates/gitignore/agents.ignore'), 'utf8');
|
|
387
|
+
put(root, '.pbai4devs/templates/gitignore/agents.ignore', `${policy}\n**/reports/cache*/\n`);
|
|
388
|
+
const path = '.hidden/reports/cache-custom/out.json';
|
|
389
|
+
put(root, path, 'local\n');
|
|
390
|
+
put(root, '.gitignore', '!/.hidden/reports/cache-custom/\n!/.hidden/reports/cache-custom/**\n');
|
|
391
|
+
const plan = planLocalIgnore(root);
|
|
392
|
+
assert.ok(checkLocalIgnore(plan).missing.includes(path));
|
|
393
|
+
assert.throws(() => applyLocalIgnore(plan), /Regras humanas/);
|
|
394
|
+
assert.equal(readFileSync(join(root, path), 'utf8'), 'local\n');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test('chaves literais em padrões Git não viram expansão de glob', t => {
|
|
398
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
399
|
+
const policy = readFileSync(join(projectRoot, '.pbai4devs/templates/gitignore/agents.ignore'), 'utf8');
|
|
400
|
+
put(root, '.pbai4devs/templates/gitignore/agents.ignore', `${policy}\n**/cache{keep,archive}/\n`);
|
|
401
|
+
put(root, 'cachekeep/source.txt', 'código humano\n');
|
|
402
|
+
put(root, '.gitignore', '!cachekeep/**\n');
|
|
403
|
+
assert.doesNotThrow(() => applyLocalIgnore(planLocalIgnore(root)));
|
|
404
|
+
assert.equal(readFileSync(join(root, 'cachekeep/source.txt'), 'utf8'), 'código humano\n');
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test('exceção nominal de um ignore ancestral é conferida no subprojeto', t => {
|
|
408
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
409
|
+
put(root, '.gitignore', '!/modules/app/.playwright-personal/\n!/modules/app/.playwright-personal/**\n');
|
|
410
|
+
const child = join(root, 'modules/app'); mkdirSync(child, { recursive: true });
|
|
411
|
+
const plan = planLocalIgnore(child);
|
|
412
|
+
assert.ok(checkLocalIgnore(plan).missing.some(path => path.startsWith('.playwright-personal/')));
|
|
413
|
+
assert.throws(() => applyLocalIgnore(plan), /Regras humanas/);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test('escrita de exclusões preserva permissões e detecta alteração concorrente', { skip: process.platform === 'win32' }, t => {
|
|
417
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
418
|
+
const exclude = join(root, '.git/info/exclude'); chmodSync(exclude, 0o600);
|
|
419
|
+
applyLocalIgnore(planLocalIgnore(root));
|
|
420
|
+
assert.equal(statSync(exclude).mode & 0o777, 0o600);
|
|
421
|
+
const changed = planLocalIgnore(root, { outputs: ['new-generated.md'] });
|
|
422
|
+
chmodSync(exclude, 0o640);
|
|
423
|
+
assert.throws(() => applyLocalIgnore(changed), /alterada apos planejar/);
|
|
424
|
+
assert.equal(statSync(exclude).mode & 0o777, 0o640);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test('fallback e rollback de exclusões mantêm o modo original', { skip: process.platform === 'win32' }, t => {
|
|
428
|
+
const root = fixture(t), ignore = join(root, '.gitignore');
|
|
429
|
+
put(root, '.gitignore', '# humano\n'); chmodSync(ignore, 0o600);
|
|
430
|
+
applyLocalIgnore(planLocalIgnore(root));
|
|
431
|
+
assert.equal(statSync(ignore).mode & 0o777, 0o600);
|
|
432
|
+
const original = readFileSync(ignore); git(root, ['init', '-q']);
|
|
433
|
+
const exclude = join(root, '.git/info/exclude'), before = readFileSync(exclude);
|
|
434
|
+
chmodSync(exclude, 0o600);
|
|
435
|
+
const plan = planLocalIgnore(root); assert.equal(plan.edits.length, 2);
|
|
436
|
+
Object.defineProperty(plan.edits[1], 'after', { get() { throw new Error('injected rollback failure'); } });
|
|
437
|
+
assert.throws(() => applyLocalIgnore(plan), /injected rollback failure/);
|
|
438
|
+
assert.deepEqual(readFileSync(exclude), before); assert.equal(statSync(exclude).mode & 0o777, 0o600);
|
|
439
|
+
assert.deepEqual(readFileSync(ignore), original); assert.equal(statSync(ignore).mode & 0o777, 0o600);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test('rollback restaura exclusão somente leitura com escrita atômica', { skip: process.platform === 'win32' }, t => {
|
|
443
|
+
const root = fixture(t), ignore = join(root, '.gitignore');
|
|
444
|
+
put(root, '.gitignore', '# humano\n'); chmodSync(ignore, 0o444);
|
|
445
|
+
applyLocalIgnore(planLocalIgnore(root));
|
|
446
|
+
const original = readFileSync(ignore); git(root, ['init', '-q']);
|
|
447
|
+
const exclude = join(root, '.git/info/exclude'), before = readFileSync(exclude);
|
|
448
|
+
chmodSync(exclude, 0o444);
|
|
449
|
+
const plan = planLocalIgnore(root);
|
|
450
|
+
Object.defineProperty(plan.edits[1], 'after', { get() { throw new Error('readonly rollback probe'); } });
|
|
451
|
+
assert.throws(() => applyLocalIgnore(plan), /readonly rollback probe/);
|
|
452
|
+
assert.deepEqual(readFileSync(exclude), before); assert.equal(statSync(exclude).mode & 0o777, 0o444);
|
|
453
|
+
assert.deepEqual(readFileSync(ignore), original); assert.equal(statSync(ignore).mode & 0o777, 0o444);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test('projeto aninhado não lê gitignore inacessível do projeto irmão', { skip: process.platform === 'win32' }, t => {
|
|
457
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
458
|
+
const other = join(root, 'other/.gitignore'); put(root, 'other/.gitignore', 'tmp/\n');
|
|
459
|
+
git(root, ['add', 'other/.gitignore']); chmodSync(other, 0o000);
|
|
460
|
+
t.after(() => { try { chmodSync(other, 0o600); } catch {} });
|
|
461
|
+
const child = join(root, 'app'); mkdirSync(child);
|
|
462
|
+
const plan = planLocalIgnore(child);
|
|
463
|
+
applyLocalIgnore(plan);
|
|
464
|
+
assert.equal(checkLocalIgnore(planLocalIgnore(child)).ok, true);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test('falha no cleanup após promoção ainda reverte a exclusão publicada', t => {
|
|
468
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
469
|
+
const file = join(root, '.git/info/exclude'), original = readFileSync(file);
|
|
470
|
+
const plan = planLocalIgnore(root), remove = fs.rmSync;
|
|
471
|
+
const injected = new Error('published cleanup failure'); let failed = false;
|
|
472
|
+
try {
|
|
473
|
+
fs.rmSync = (path, options) => {
|
|
474
|
+
if (!failed && String(path).startsWith(`${file}.agentdevkit-`) && !existsSync(path)) { failed = true; throw injected; }
|
|
475
|
+
return remove(path, options);
|
|
476
|
+
};
|
|
477
|
+
syncBuiltinESMExports();
|
|
478
|
+
assert.throws(() => applyLocalIgnore(plan), error => error === injected);
|
|
479
|
+
} finally { fs.rmSync = remove; syncBuiltinESMExports(); }
|
|
480
|
+
assert.equal(failed, true);
|
|
481
|
+
assert.deepEqual(readFileSync(file), original);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test('falha irrecuperável de rollback informa também o erro original', t => {
|
|
485
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
486
|
+
const file = join(root, '.git/info/exclude'), plan = planLocalIgnore(root);
|
|
487
|
+
const remove = fs.rmSync, rename = fs.renameSync;
|
|
488
|
+
const original = new Error('original cleanup failure'), rollback = new Error('restore unavailable');
|
|
489
|
+
let failed = false;
|
|
490
|
+
try {
|
|
491
|
+
fs.rmSync = (path, options) => {
|
|
492
|
+
if (!failed && String(path).startsWith(`${file}.agentdevkit-`) && !existsSync(path)) { failed = true; throw original; }
|
|
493
|
+
return remove(path, options);
|
|
494
|
+
};
|
|
495
|
+
fs.renameSync = (from, to) => { if (failed && to === file) throw rollback; return rename(from, to); };
|
|
496
|
+
syncBuiltinESMExports();
|
|
497
|
+
assert.throws(() => applyLocalIgnore(plan), error => error instanceof AggregateError
|
|
498
|
+
&& error.cause === original && error.errors.includes(rollback));
|
|
499
|
+
} finally { fs.rmSync = remove; fs.renameSync = rename; syncBuiltinESMExports(); }
|
|
500
|
+
});
|
|
@@ -10,6 +10,7 @@ import { buildCatalog } from '../../lib/catalog.mjs';
|
|
|
10
10
|
import { toolWrapper } from '../../preparar-ferramentas/SCRIPT.mjs';
|
|
11
11
|
import { mergeGitignore, ignoreBegin, ignoreEnd, validateIgnoreClients } from '../../lib/gitignore.mjs';
|
|
12
12
|
import './LOCAL_IGNORE.mjs';
|
|
13
|
+
import './HUMAN_OWNERSHIP.mjs';
|
|
13
14
|
import { validateCatalog as validateExternalCatalog } from '../../gerenciar-skills-externas/SCRIPT.mjs';
|
|
14
15
|
|
|
15
16
|
const project = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
@@ -58,7 +59,7 @@ function fixture(t, minimal = false) {
|
|
|
58
59
|
write(root, '.pbai4devs/agents/revisor/kimi-code.yaml', '---\nname: revisor\ntools: [Read, Grep, Glob]\n---\n');
|
|
59
60
|
write(root, '.pbai4devs/agents/revisor/opencode.yaml', '---\nmode: subagent\npermission:\n edit: deny\n---\n');
|
|
60
61
|
write(root, '.pbai4devs/adapters/antigravity/agents/revisor.md', pointer('../../../agents/revisor/AGENT.md', 'Revisor'));
|
|
61
|
-
write(root, '.opencode/agents/revisor/AGENT.md',
|
|
62
|
+
write(root, '.opencode/agents/revisor/AGENT.md', `---\nmode: subagent\npermission:\n edit: deny\n---\n\n# Revisor\n\n${marker}\nLeia e siga [origem](../../../.pbai4devs/agents/revisor/AGENT.md).\n`);
|
|
62
63
|
write(root, '.github/workflows/ci.yml', 'workflow-preservado\n');
|
|
63
64
|
write(root, '.vscode/settings.json', '{"fixture":"config-preservada"}\n');
|
|
64
65
|
write(root, '.vscode/extensions.json', '{"recommendations":[]}\n');
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ description: Alterações verificáveis por versão do AgentDevKit.
|
|
|
4
4
|
|
|
5
5
|
# Changelog
|
|
6
6
|
|
|
7
|
+
## 0.0.8 — 2026-11-11
|
|
8
|
+
|
|
9
|
+
- Preservação de arquivos humanos que parecem apontamentos gerados.
|
|
10
|
+
- Conflitos de ignore recusados antes das gravações e rollback revisado.
|
|
11
|
+
- Permissões mantidas em atualização, backup e restauração de arquivos.
|
|
12
|
+
- Caminhos simbólicos de ferramentas bloqueados antes de escrita externa.
|
|
13
|
+
- Argumentos de ferramentas preservados após `--`; ajuda sem projeto válido.
|
|
14
|
+
- Verificação de artefatos com nomes próprios, arquivos e regras aninhadas.
|
|
15
|
+
|
|
7
16
|
## 0.0.7 — 2026-11-11
|
|
8
17
|
|
|
9
18
|
- Artefatos de testes e pastas temporárias protegidos em projetos existentes.
|
package/README.en.md
CHANGED
|
@@ -34,7 +34,7 @@ Then open the project in your coding agent and ask:
|
|
|
34
34
|
|
|
35
35
|
See [installation](docs/guides/installation.md) for Windows, missing Node, previews
|
|
36
36
|
and updates. Install and update through the [public npm package][packages]; the
|
|
37
|
-
[v0.0.
|
|
37
|
+
[v0.0.8 release][release] retains artifacts and checksums for verification.
|
|
38
38
|
No global installation is required.
|
|
39
39
|
The CLI supports `pt`, `en` and `es`; it saves your selection per project.
|
|
40
40
|
|
|
@@ -78,7 +78,7 @@ forms. See [how to participate](docs/maintainers/community.md).
|
|
|
78
78
|
agent to read `AGENTS.md`.
|
|
79
79
|
|
|
80
80
|
[clients]: .pbai4devs/COMPATIBILIDADE.md
|
|
81
|
-
[release]: https://github.com/pathbit/AgentDevKit/releases/tag/v0.0.
|
|
81
|
+
[release]: https://github.com/pathbit/AgentDevKit/releases/tag/v0.0.8
|
|
82
82
|
[npm-package]: https://www.npmjs.com/package/@pathbit/agentdevkit
|
|
83
83
|
[npm-badge]: https://img.shields.io/npm/v/%40pathbit%2Fagentdevkit
|
|
84
84
|
[license-badge]: https://img.shields.io/badge/license-MIT-blue
|
package/README.es.md
CHANGED
|
@@ -35,7 +35,7 @@ Después, abre el proyecto en tu agente y pide:
|
|
|
35
35
|
La [guía de instalación](docs/guides/installation.md) incluye Windows, Node ausente,
|
|
36
36
|
simulación y actualización. Instala y actualiza mediante el
|
|
37
37
|
[paquete npm público][packages];
|
|
38
|
-
la [release v0.0.
|
|
38
|
+
la [release v0.0.8][release] conserva artefactos y checksums para verificarlos.
|
|
39
39
|
No requiere instalación global.
|
|
40
40
|
La CLI guarda el idioma `pt`, `en` o `es` por proyecto.
|
|
41
41
|
|
|
@@ -80,7 +80,7 @@ formularios. Consulta [cómo participar](docs/maintainers/community.md).
|
|
|
80
80
|
al agente que lea `AGENTS.md`.
|
|
81
81
|
|
|
82
82
|
[clients]: .pbai4devs/COMPATIBILIDADE.md
|
|
83
|
-
[release]: https://github.com/pathbit/AgentDevKit/releases/tag/v0.0.
|
|
83
|
+
[release]: https://github.com/pathbit/AgentDevKit/releases/tag/v0.0.8
|
|
84
84
|
[npm-package]: https://www.npmjs.com/package/@pathbit/agentdevkit
|
|
85
85
|
[npm-badge]: https://img.shields.io/npm/v/%40pathbit%2Fagentdevkit
|
|
86
86
|
[license-badge]: https://img.shields.io/badge/license-MIT-blue
|
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ Depois, abra o projeto no seu agente e peça:
|
|
|
32
32
|
|
|
33
33
|
Windows, instalação sem Node, simulação e atualização estão no
|
|
34
34
|
[guia de instalação](https://github.com/pathbit/AgentDevKit/blob/master/docs/guides/installation.md). A distribuição usa a
|
|
35
|
-
versão publicada no [npm público][packages] e na [release v0.0.
|
|
35
|
+
versão publicada no [npm público][packages] e na [release v0.0.8][release],
|
|
36
36
|
sem instalação global. Instalação e atualização usam o pacote do registro;
|
|
37
37
|
os arquivos da release servem para conferir os artefatos publicados.
|
|
38
38
|
O idioma da CLI pode ser `pt`, `en` ou `es`.
|
|
@@ -89,7 +89,7 @@ ao seu agente para ler `AGENTS.md`.
|
|
|
89
89
|
[help]: https://github.com/pathbit/AgentDevKit/blob/master/docs/guides/troubleshooting.md
|
|
90
90
|
[issues]: https://github.com/pathbit/AgentDevKit/issues/new/choose
|
|
91
91
|
[packages]: https://github.com/pathbit/AgentDevKit/blob/master/docs/maintainers/packages.md
|
|
92
|
-
[release]: https://github.com/pathbit/AgentDevKit/releases/tag/v0.0.
|
|
92
|
+
[release]: https://github.com/pathbit/AgentDevKit/releases/tag/v0.0.8
|
|
93
93
|
[npm-package]: https://www.npmjs.com/package/@pathbit/agentdevkit
|
|
94
94
|
[npm-badge]: https://img.shields.io/npm/v/%40pathbit%2Fagentdevkit
|
|
95
95
|
[license-badge]: https://img.shields.io/badge/license-MIT-blue
|
package/docs/guides/cli.md
CHANGED
|
@@ -63,6 +63,15 @@ e `unlock`. Consultar ou executar não instala silenciosamente.
|
|
|
63
63
|
`setup` prepara as ferramentas compatíveis; outros comandos podem receber
|
|
64
64
|
um nome ou grupo. Consulte [ferramentas e testes](tools-and-testing.md).
|
|
65
65
|
|
|
66
|
+
Em `tools`, use `--` antes dos argumentos que pertencem à ferramenta:
|
|
67
|
+
|
|
68
|
+
```console
|
|
69
|
+
npx @pathbit/agentdevkit tools run agent-browser -- --json --help
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Nesse exemplo, `--json` pertence ao agent-browser. Opções do kit ficam antes
|
|
73
|
+
do separador. `--help` e `--version` do kit não exigem um projeto instalado.
|
|
74
|
+
|
|
66
75
|
## JSON e retorno
|
|
67
76
|
|
|
68
77
|
As respostas incluem `ok`, `command`, `version`, `language`, `target` e
|
|
@@ -64,6 +64,8 @@ O bloco local também protege `.agent-tests`, `tmp`, `temp`, `.playwright*`
|
|
|
64
64
|
e relatórios de testes, inclusive com letras maiúsculas e em subpastas.
|
|
65
65
|
O `.gitignore` existente e seus arquivos rastreados são preservados;
|
|
66
66
|
uma exceção humana que conflite com essa proteção é informada como conflito.
|
|
67
|
+
Nomes próprios de artefatos e arquivos existentes também são conferidos;
|
|
68
|
+
a validação fica restrita ao projeto e aos ignores de seus ancestrais.
|
|
67
69
|
|
|
68
70
|
Arquivos já rastreados exigem migração do índice: apenas saídas identificadas
|
|
69
71
|
do kit podem deixar de ser rastreadas, preservando seus bytes no disco.
|
|
@@ -86,10 +88,14 @@ Links simbólicos em caminhos usados pela instalação podem bloquear a operaç
|
|
|
86
88
|
O kit informa o caminho e não converte nem remove o link automaticamente.
|
|
87
89
|
Use a simulação para identificar esse caso antes de aplicar.
|
|
88
90
|
|
|
91
|
+
Arquivos humanos não se tornam gerenciados apenas por conterem um link.
|
|
92
|
+
As alterações de arquivos existentes preservam permissões, inclusive em backup
|
|
93
|
+
e restauração após falha.
|
|
94
|
+
|
|
89
95
|
O [fluxo de trabalho](workflow.md) orienta a próxima etapa do projeto.
|
|
90
96
|
Se houver erro, consulte [solução de problemas](troubleshooting.md).
|
|
91
97
|
|
|
92
|
-
[Wiki](../README.md) · [Spec v0.0.
|
|
98
|
+
[Wiki](../README.md) · [Spec v0.0.8][spec]
|
|
93
99
|
|
|
94
100
|
[bootstrap]: ../../.pbai4devs/scripts/preparar-ferramentas/BOOTSTRAP.md
|
|
95
|
-
[spec]: ../specs/v0.0.
|
|
101
|
+
[spec]: ../specs/v0.0.8/SPEC.md
|
|
@@ -32,6 +32,8 @@ node .pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs setup
|
|
|
32
32
|
`setup` reutiliza instalações saudáveis e verifica as compatíveis. Um nome
|
|
33
33
|
ou grupo limita a seleção. `check`, `info` e `run` não trocam uma consulta
|
|
34
34
|
por instalação. Downloads, caches e recibos permanecem locais e ignorados.
|
|
35
|
+
O gerenciador recusa links simbólicos nos caminhos que escreve ou usa como
|
|
36
|
+
estado local; a atualização não segue esses links para fora da instalação.
|
|
35
37
|
|
|
36
38
|
## Escolher a prova
|
|
37
39
|
|
|
@@ -64,7 +64,7 @@ existir; a primeira publicação pode usar o secret `NPMJS_TOKEN`.
|
|
|
64
64
|
Para publicar somente o pacote de uma tag que já tem artefato aprovado:
|
|
65
65
|
|
|
66
66
|
```console
|
|
67
|
-
gh workflow run release.yml --ref master -f tag=v0.0.
|
|
67
|
+
gh workflow run release.yml --ref master -f tag=v0.0.8 -F package_only=true
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
Se o artefato expirou, reexecute o CI original da tag antes da publicação.
|
package/docs/product/roadmap.md
CHANGED
|
@@ -18,6 +18,7 @@ status: vigente
|
|
|
18
18
|
| Instalação pelo registro v0.0.5 | Pacote publicado | [Spec][registry]. |
|
|
19
19
|
| Apresentação e licença v0.0.6 | Distribuição e atualização | [Spec][public]. |
|
|
20
20
|
| Compatibilidade real v0.0.7 | Testes em clones existentes | [Spec][oss]. |
|
|
21
|
+
| Preservação v0.0.8 | Auditoria e regressões | [Spec][robustness]. |
|
|
21
22
|
|
|
22
23
|
A primeira entrega segue a [spec v0.0.1](../specs/v0.0.1/SPEC.md), disponível
|
|
23
24
|
na [release v0.0.1][release]. Os detalhes de mudanças estão no
|
|
@@ -38,3 +39,4 @@ comportamento existente, sem alterar a versão de uma release já publicada.
|
|
|
38
39
|
[registry]: ../specs/v0.0.5/SPEC.md
|
|
39
40
|
[public]: ../specs/v0.0.6/SPEC.md
|
|
40
41
|
[oss]: ../specs/v0.0.7/SPEC.md
|
|
42
|
+
[robustness]: ../specs/v0.0.8/SPEC.md
|
package/docs/specs/README.md
CHANGED
|
@@ -12,6 +12,7 @@ description: Incrementos verificáveis do AgentDevKit.
|
|
|
12
12
|
- [v0.0.5](v0.0.5/SPEC.md): instalação pelo registro em todos os fluxos.
|
|
13
13
|
- [v0.0.6](v0.0.6/SPEC.md): apresentação pública, licença e atualização segura.
|
|
14
14
|
- [v0.0.7](v0.0.7/SPEC.md): projetos reais e proteção local de artefatos.
|
|
15
|
+
- [v0.0.8](v0.0.8/SPEC.md): preservação, transações e validação aprofundada.
|
|
15
16
|
|
|
16
17
|
Cada incremento vincula requisitos, critérios, testes e evidências locais.
|
|
17
18
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Validação aprofundada e preservação de dados do AgentDevKit.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Versão 0.0.8
|
|
6
|
+
|
|
7
|
+
A [spec](SPEC.md) reúne correções da auditoria do instalador, sincronizador,
|
|
8
|
+
gerenciador de ferramentas, CLI e regras de ignore.
|
|
9
|
+
|
|
10
|
+
[Specs](../README.md)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Correções verificadas de preservação, transações e interface CLI.
|
|
3
|
+
status: aprovado
|
|
4
|
+
version: 0.0.8
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Preservação e validação aprofundada
|
|
8
|
+
|
|
9
|
+
## Objetivo e autorização
|
|
10
|
+
|
|
11
|
+
Atender ao pedido de revisão profunda, corrigindo falhas comprovadas e
|
|
12
|
+
publicando uma nova versão estável após validação. Esta etapa continua o
|
|
13
|
+
[PRD](../../product/prd.md), o [design](../../product/design.md) e os
|
|
14
|
+
[testes em projetos reais](../v0.0.7/SPEC.md).
|
|
15
|
+
|
|
16
|
+
## Contrato
|
|
17
|
+
|
|
18
|
+
- Não apagar ou sobrescrever arquivos humanos pela aparência de seu Markdown.
|
|
19
|
+
Exigir propriedade comprovada para limpeza automática de apontamentos.
|
|
20
|
+
- Recusar conflitos conhecidos antes de escrever. Em falhas posteriores,
|
|
21
|
+
restaurar as alterações próprias sem substituir edições humanas concorrentes.
|
|
22
|
+
- Preservar permissões em atualização, backup e rollback, inclusive em
|
|
23
|
+
arquivos somente para leitura quando a operação for permitida pelo sistema.
|
|
24
|
+
- Recusar componentes simbólicos nos caminhos locais de ferramentas, inclusive
|
|
25
|
+
após operações externas, antes de escrever ou confirmar uma instalação.
|
|
26
|
+
- Preservar integralmente argumentos após `tools ... --`; ajuda e versão
|
|
27
|
+
não exigem diretório de projeto válido.
|
|
28
|
+
- Conferir artefatos existentes e exceções literais além dos nomes de amostra,
|
|
29
|
+
sem ler ignores de projetos irmãos ou desindexar arquivos humanos.
|
|
30
|
+
- Manter a recusa de links simbólicos incompatíveis do Zod; não converter
|
|
31
|
+
esse bloqueio documentado em uma instalação bem-sucedida.
|
|
32
|
+
|
|
33
|
+
## Critérios de aceite
|
|
34
|
+
|
|
35
|
+
| ID | Resultado observável |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| AC-001 | Markdown humano é preservado; limpeza usa propriedade comprovada. |
|
|
38
|
+
| AC-002 | Falhas não deixam gravações próprias parciais nem mudam permissões. |
|
|
39
|
+
| AC-003 | Gerenciador não escreve fora da instalação por links simbólicos. |
|
|
40
|
+
| AC-004 | CLI repassa argumentos; ajuda funciona sem projeto. |
|
|
41
|
+
| AC-005 | Ignore protege artefatos sem invadir projetos irmãos. |
|
|
42
|
+
| AC-006 | Regressões, pacote público e atualização do router são verificados. |
|
|
43
|
+
|
|
44
|
+
## Verificação
|
|
45
|
+
|
|
46
|
+
Para AC-001 a AC-005, conservar reproduções anteriores e testes de regressão
|
|
47
|
+
que falham antes da correção, com assertivas de conteúdo, modo, estado e Git.
|
|
48
|
+
Executar os testes relevantes em Node 24/26 e revisar os diffs de forma
|
|
49
|
+
independente. Limitações de plataforma devem ficar explícitas.
|
|
50
|
+
|
|
51
|
+
Para AC-006, executar pipeline completo, cenários de maturidade e matriz OSS
|
|
52
|
+
do pacote gerado. Confirmar no CI Linux, macOS e Windows; publicar o mesmo TGZ,
|
|
53
|
+
comparar checksum e repetir o consumo público. Atualizar o router com preview,
|
|
54
|
+
backup, diagnóstico e comparação da baseline, preservando o índice existente.
|
|
55
|
+
|
|
56
|
+
Testes e fixtures são versionados. Clones e provas ficam em `tmp/` e o registro
|
|
57
|
+
de revisão em `.agent-tests/`. Não prometer ausência universal de bugs.
|
|
58
|
+
|
|
59
|
+
## Documentação afetada
|
|
60
|
+
|
|
61
|
+
Atualizar referência da CLI, instalação, ferramentas, changelog, README,
|
|
62
|
+
roadmap e índices. Não criar documentos de produto nos projetos consumidores.
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -18,7 +18,10 @@ export function parseArgs(argv) {
|
|
|
18
18
|
const options = { command: null, positionals: [], toolArgs: [], target: null, language: null, clients: undefined, yes: false, json: false, dryRun: false, force: false, check: false };
|
|
19
19
|
for (let index = 0; index < argv.length; index++) {
|
|
20
20
|
const arg = argv[index];
|
|
21
|
-
if (
|
|
21
|
+
if (arg === '--' && options.command === 'tools') {
|
|
22
|
+
options.toolArgs.push(...argv.slice(index));
|
|
23
|
+
break;
|
|
24
|
+
} else if (['--target', '--lang', '--agents', '--clients'].includes(arg)) {
|
|
22
25
|
const value = argv[++index]; if (!value || value.startsWith('-')) usage(`Missing value: ${arg}`);
|
|
23
26
|
if (arg === '--target') options.target = value;
|
|
24
27
|
else if (arg === '--lang') { options.language = normalizeLanguage(value); if (!options.language) usage('Use --lang pt, en or es.'); }
|
|
@@ -86,11 +89,14 @@ function print(result, options, output) {
|
|
|
86
89
|
|
|
87
90
|
export async function main(argv = process.argv.slice(2), context = {}) {
|
|
88
91
|
const output = context.stdout || process.stdout, input = context.stdin || process.stdin, errorOutput = context.stderr || process.stderr;
|
|
89
|
-
|
|
92
|
+
const separator = argv.indexOf('--'), globalArgs = separator < 0 ? argv : argv.slice(0, separator);
|
|
93
|
+
let options = { json: globalArgs.includes('--json') }, language = normalizeLanguage(globalArgs[globalArgs.indexOf('--lang') + 1]) || 'pt', target = resolve(context.cwd || process.cwd()), clients = ['all'];
|
|
90
94
|
try {
|
|
91
95
|
options = parseArgs(argv);
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
const informational = ['help', 'version'].includes(options.command);
|
|
97
|
+
target = informational ? resolve(context.cwd || process.cwd(), options.target || '.')
|
|
98
|
+
: resolveTarget(options.target || (options.command === 'init' ? options.positionals[0] : '.'), context.cwd || process.cwd());
|
|
99
|
+
const previous = informational ? null : loadState(target);
|
|
94
100
|
language = options.language || previous?.language || 'pt';
|
|
95
101
|
clients = options.clients ?? previous?.clients ?? ['all'];
|
|
96
102
|
if (options.command === 'init' && !previous && !options.yes && !options.json && !options.dryRun && input.isTTY && output.isTTY && (!options.language || options.clients === undefined)) {
|