@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.
@@ -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', '---\nmode: subagent\npermission:\n edit: deny\n---\n\n# Revisor\n\nLeia e siga [origem](../../../.pbai4devs/agents/revisor/AGENT.md).\n');
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.7 release][release] retains artifacts and checksums for verification.
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.7
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.7][release] conserva artefactos y checksums para verificarlos.
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.7
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.7][release],
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.7
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
@@ -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.7][spec]
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.7/SPEC.md
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.7 -F package_only=true
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.
@@ -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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pathbit/agentdevkit",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "Shared skills, specifications and workflows for AI coding agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
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 (['--target', '--lang', '--agents', '--clients'].includes(arg)) {
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
- let options = { json: argv.includes('--json') }, language = normalizeLanguage(argv[argv.indexOf('--lang') + 1]) || 'pt', target = resolve(context.cwd || process.cwd()), clients = ['all'];
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
- target = resolveTarget(options.target || (options.command === 'init' ? options.positionals[0] : '.'), context.cwd || process.cwd());
93
- const previous = ['help', 'version'].includes(options.command) ? null : loadState(target);
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)) {