@pathbit/agentdevkit 0.0.18 → 0.0.19
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 +54 -34
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +7 -9
- package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +155 -17
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +23 -6
- package/.pbai4devs/templates/gitignore/TEMPLATE.md +14 -7
- package/.pbai4devs/templates/gitignore/shared.ignore +1 -0
- package/CHANGELOG.md +5 -0
- package/README.en.md +1 -1
- package/README.es.md +2 -2
- package/docs/guides/cli.md +6 -4
- package/docs/guides/installation.md +35 -21
- package/docs/guides/troubleshooting.md +3 -2
- package/docs/product/design.md +6 -6
- package/docs/specs/README.md +1 -0
- package/docs/specs/client-ignore/SPEC.md +21 -0
- package/package.json +1 -1
- package/src/install.mjs +5 -15
|
@@ -332,26 +332,6 @@ function ambiguousExceptions(root, location, clientRoots, artifacts) {
|
|
|
332
332
|
return conflicts;
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
-
function foreignHiddenClients(plan) {
|
|
336
|
-
if (!plan.location) return [];
|
|
337
|
-
const content = localRead(plan.location.exclude) || '', ranges = [];
|
|
338
|
-
let start = null;
|
|
339
|
-
content.split(/\r?\n/).forEach((line, index) => {
|
|
340
|
-
const begin = line.match(/^# BEGIN AGENTDEVKIT LOCAL ([a-f0-9]{16})$/);
|
|
341
|
-
const end = line.match(/^# END AGENTDEVKIT LOCAL ([a-f0-9]{16})$/);
|
|
342
|
-
if (begin) start = { id: begin[1], line: index + 1 };
|
|
343
|
-
else if (end && start?.id === end[1]) { ranges.push({ ...start, end: index + 1 }); start = null; }
|
|
344
|
-
});
|
|
345
|
-
const probes = plan.clientRoots.filter(path => { const item = localStat(join(plan.root, path)); return !item || item.isDirectory() && !item.isSymbolicLink(); }).map(path => `${path}/.agentdevkit-human-probe.md`);
|
|
346
|
-
if (!probes.length) return [];
|
|
347
|
-
const { paths, matches } = ignoreMatches(plan.location, probes);
|
|
348
|
-
return paths.flatMap((path, index) => {
|
|
349
|
-
const match = matches.get(path);
|
|
350
|
-
if (!match || match.pattern.startsWith('!') || resolve(plan.location.top, match.source) !== plan.location.exclude) return [];
|
|
351
|
-
return ranges.some(range => `# BEGIN AGENTDEVKIT LOCAL ${range.id}` !== plan.first && match.line >= range.line && match.line <= range.end) ? [probes[index]] : [];
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
|
|
355
335
|
function localIgnoreSource(location, source) {
|
|
356
336
|
const path = resolve(location.top, source);
|
|
357
337
|
if (path === location.exclude) return true;
|
|
@@ -370,6 +350,21 @@ function ignoreMatches(location, probes) {
|
|
|
370
350
|
return { paths, matches };
|
|
371
351
|
}
|
|
372
352
|
|
|
353
|
+
// A nested .gitignore cannot reopen a project hidden by an ignored ancestor.
|
|
354
|
+
// Detect this before claiming that its client exceptions are effective.
|
|
355
|
+
function ignoredProjectAncestors(location) {
|
|
356
|
+
if (!location.prefix) return [];
|
|
357
|
+
const segments = location.prefix.split('/');
|
|
358
|
+
const paths = segments.map((_part, index) => segments.slice(0, index + 1).join('/'));
|
|
359
|
+
const result = gitCommand(location.top, ['check-ignore', '--verbose', '--no-index', '--stdin', '-z'], `${paths.join('\0')}\0`);
|
|
360
|
+
if (result.error || ![0, 1].includes(result.status)) throw new Error(result.error?.message || result.stderr || 'Falha ao conferir ancestrais Git.');
|
|
361
|
+
const values = result.stdout.split('\0'), ignored = [];
|
|
362
|
+
for (let index = 0; index + 3 < values.length; index += 4) {
|
|
363
|
+
if (!values[index + 2].startsWith('!')) ignored.push(`Ancestral Git ignorado: ${values[index + 3]}/; reabra-o no .gitignore do repositorio pai`);
|
|
364
|
+
}
|
|
365
|
+
return ignored;
|
|
366
|
+
}
|
|
367
|
+
|
|
373
368
|
function localRoot(value) {
|
|
374
369
|
const missing = []; let parent = resolve(value);
|
|
375
370
|
while (!localStat(parent)) { missing.unshift(parent.slice(dirname(parent).length + 1)); parent = dirname(parent); }
|
|
@@ -387,15 +382,17 @@ export function planLocalIgnore(root, { outputs = [], templateRoot } = {}) {
|
|
|
387
382
|
const artifacts = artifactPolicy(templateFile);
|
|
388
383
|
if (template.split('{{OUTPUTS}}').length !== 2) throw new Error('local.ignore exige um unico {{OUTPUTS}}.');
|
|
389
384
|
if (template.split('{{ROOTS}}').length > 2) throw new Error('local.ignore permite um unico {{ROOTS}}.');
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
const roots = [];
|
|
385
|
+
// Client roots are ignored locally. Projects can explicitly opt in to a
|
|
386
|
+
// complete client with a literal negation in their own root .gitignore.
|
|
393
387
|
const localPolicy = join(templateRoot || root, '.pbai4devs/templates/clients/ADAPTERS.json');
|
|
394
388
|
const policy = JSON.parse(readFileSync(checkedLocalPath(localStat(localPolicy) ? localPolicy : fileURLToPath(new URL('../../templates/clients/ADAPTERS.json', import.meta.url))), 'utf8'));
|
|
395
389
|
const clientRoots = [...new Set(policy.adapters.flatMap(adapter => [adapter.root, ...(adapter.extraRoots || [])]))];
|
|
396
390
|
if (clientRoots.some(path => typeof path !== 'string' || !/^\.[a-z0-9][a-z0-9-]*$/.test(path) || ['.git', '.pbai4devs', '.agent-tests'].includes(path))) throw new Error('Raiz de cliente invalida para exclusao.');
|
|
391
|
+
const roots = [...clientRoots];
|
|
397
392
|
const privateClients = privateClientPolicy(templateFile, clientRoots);
|
|
398
|
-
const
|
|
393
|
+
const outputPaths = [...new Set(outputs.map(checkedOutput))];
|
|
394
|
+
const paths = [...new Set([...outputPaths, ...template.split(/\r?\n/).filter(line => line.startsWith('/')).map(line => checkedOutput(line.slice(1)))])]
|
|
395
|
+
.filter(path => !roots.some(client => path === client || path.startsWith(`${client}/`))).sort();
|
|
399
396
|
const info = gitCommand(resolved.parent, ['rev-parse', '--show-toplevel', '--git-path', 'info/exclude']);
|
|
400
397
|
let location = null;
|
|
401
398
|
if (info.status === 0) {
|
|
@@ -412,6 +409,19 @@ export function planLocalIgnore(root, { outputs = [], templateRoot } = {}) {
|
|
|
412
409
|
throw new Error(`Nao foi possivel localizar Git: ${info.error?.message || info.stderr}`);
|
|
413
410
|
}
|
|
414
411
|
const id = createHash('sha256').update(root).digest('hex').slice(0, 16);
|
|
412
|
+
const projectIgnore = join(root, '.gitignore'), projectIgnoreStat = localStat(projectIgnore);
|
|
413
|
+
const literalExceptions = projectIgnoreStat?.isFile() && !projectIgnoreStat.isSymbolicLink()
|
|
414
|
+
? new Set(readFileSync(projectIgnore, 'utf8').replace(/^\uFEFF/, '').split(/\r?\n/)) : new Set();
|
|
415
|
+
const requestedRoots = location ? roots.filter(client => literalExceptions.has(`!/${client}/`) || literalExceptions.has(`!${client}/`)) : [];
|
|
416
|
+
const optCandidates = requestedRoots.filter(client => safeIgnoreProbe(root, `${client}/.agentdevkit-opt-in-probe`));
|
|
417
|
+
// A later rule or an ancestor can override a literal negation. Consult Git
|
|
418
|
+
// before accepting it, including when the client folder does not yet exist.
|
|
419
|
+
const optProbes = optCandidates.map(client => `${client}/.agentdevkit-opt-in-probe`);
|
|
420
|
+
const optMatches = location && optProbes.length ? ignoreMatches(location, optProbes) : { paths: [], matches: new Map() };
|
|
421
|
+
const optedRoots = optCandidates.filter((_client, index) => {
|
|
422
|
+
const match = optMatches.matches.get(optMatches.paths[index]);
|
|
423
|
+
return !match || match.pattern.startsWith('!');
|
|
424
|
+
});
|
|
415
425
|
const first = `# BEGIN AGENTDEVKIT LOCAL ${id}`, last = `# END AGENTDEVKIT LOCAL ${id}`;
|
|
416
426
|
const fixed = template.replace('{{ROOTS}}', roots.map(path => `/${path}/`).join('\n')).replace('{{OUTPUTS}}', paths.filter(path => !roots.some(root => path === root || path.startsWith(root + '/'))).map(path => `/${escapePattern(path)}`).join('\n'))
|
|
417
427
|
+ '\n# Estado privado dos clientes\n' + privateClients.rules.join('\n') + '\n'
|
|
@@ -424,10 +434,9 @@ export function planLocalIgnore(root, { outputs = [], templateRoot } = {}) {
|
|
|
424
434
|
const target = location?.exclude;
|
|
425
435
|
const before = target ? localRead(target) : null, after = target ? localBlock(before, first, last, body) : null;
|
|
426
436
|
const edits = before === after ? [] : [{ path: target, before, after, beforeMode: localMode(target) }];
|
|
427
|
-
const probes = [...paths, ...rootProbes(root, roots)];
|
|
437
|
+
const probes = [...paths, ...rootProbes(root, roots.filter(client => !optedRoots.includes(client)))];
|
|
428
438
|
const existingPrivate = privateClients.probes.filter(path => localStat(join(root, path.split('/')[0]))?.isDirectory() && safeIgnoreProbe(root, path));
|
|
429
439
|
const artifactPaths = [...new Set([...artifactProbes(root, artifacts), ...existingPrivate, ...(location ? [...visibleArtifactProbes(root, location, artifacts), ...visiblePrivateProbes(root, location, clientRoots)] : [])])];
|
|
430
|
-
const wanted = new Set(probes.map(path => location?.prefix ? `${location.prefix}/${path}` : path));
|
|
431
440
|
if (location && !before?.includes(first)) {
|
|
432
441
|
const ignored = ignoreMatches(location, [...probes, ...artifactPaths].filter(path => safeIgnoreProbe(root, path)));
|
|
433
442
|
if (ignored.paths.every(path => { const match = ignored.matches.get(path); return match && !match.pattern.startsWith('!') && localIgnoreSource(location, match.source); })) edits.length = 0;
|
|
@@ -436,14 +445,16 @@ export function planLocalIgnore(root, { outputs = [], templateRoot } = {}) {
|
|
|
436
445
|
const tracked = location ? gitCommand(location.top, ['ls-files', '-z']).stdout.split('\0').filter(path => {
|
|
437
446
|
if (!path.startsWith(prefix)) return false;
|
|
438
447
|
const local = path.slice(prefix.length);
|
|
439
|
-
|
|
448
|
+
// Git ignore never untracks existing files. Report private data and test
|
|
449
|
+
// artifacts, but don't flag legitimate already-tracked project content.
|
|
450
|
+
return artifactPath(local, artifacts) || clientRoots.includes(local.split('/')[0]) && isGeneratedOutput(local);
|
|
440
451
|
}).map(path => path.slice(prefix.length)) : [];
|
|
441
|
-
return { root, status: location ? 'git' : 'no-git', location, paths, roots, clientRoots, artifactPatterns: artifacts, artifactProbes: artifactPaths, privateProbes: privateClients.probes, edits, trackedOutputs: tracked, first, last };
|
|
452
|
+
return { root, status: location ? 'git' : 'no-git', location, paths, outputPaths, roots, requestedRoots, optedRoots, clientRoots, artifactPatterns: artifacts, artifactProbes: artifactPaths, privateProbes: privateClients.probes, edits, trackedOutputs: tracked, first, last };
|
|
442
453
|
}
|
|
443
454
|
|
|
444
455
|
export function checkLocalIgnore(plan, { replacedBlocks = [] } = {}) {
|
|
445
456
|
const missing = [];
|
|
446
|
-
if (plan.location) missing.push(...ambiguousExceptions(plan.root, plan.location, plan.clientRoots || [], plan.artifactPatterns || [])
|
|
457
|
+
if (plan.location) missing.push(...((plan.requestedRoots || []).length ? ignoredProjectAncestors(plan.location) : []), ...ambiguousExceptions(plan.root, plan.location, plan.clientRoots || [], plan.artifactPatterns || []));
|
|
447
458
|
const replacements = replacedBlocks.map(item => {
|
|
448
459
|
const path = canonicalExisting(checkedLocalPath(resolve(item.path))), inside = relative(localRoot(plan.root).root, path);
|
|
449
460
|
if (isAbsolute(inside) || inside === '..' || inside.startsWith(`..${sep}`) || !Number.isInteger(item.firstLine) || !Number.isInteger(item.lastLine) || item.firstLine < 1 || item.lastLine < item.firstLine || !/^[a-f0-9]{64}$/.test(item.sha256)) throw new Error('Faixa de ignore gerenciado invalida.');
|
|
@@ -456,9 +467,9 @@ export function checkLocalIgnore(plan, { replacedBlocks = [] } = {}) {
|
|
|
456
467
|
return { ...item, path };
|
|
457
468
|
});
|
|
458
469
|
if (plan.location && (plan.paths.length || plan.artifactProbes?.length)) {
|
|
459
|
-
missing.push(...plan.paths.filter(path => !safeIgnoreProbe(plan.root, path)));
|
|
470
|
+
missing.push(...new Set([...plan.paths, ...(plan.outputPaths || [])].filter(path => !safeIgnoreProbe(plan.root, path))));
|
|
460
471
|
const privateProbes = (plan.privateProbes || []).filter(path => localStat(join(plan.root, path.split('/')[0]))?.isDirectory() && safeIgnoreProbe(plan.root, path));
|
|
461
|
-
const probes = [...new Set([...plan.paths, ...rootProbes(plan.root, plan.roots || []), ...(plan.artifactProbes || []), ...privateProbes,
|
|
472
|
+
const probes = [...new Set([...plan.paths, ...rootProbes(plan.root, (plan.roots || []).filter(client => !(plan.optedRoots || []).includes(client))), ...(plan.artifactProbes || []), ...privateProbes,
|
|
462
473
|
...(plan.artifactPatterns ? visibleArtifactProbes(plan.root, plan.location, plan.artifactPatterns) : []),
|
|
463
474
|
...visiblePrivateProbes(plan.root, plan.location, plan.clientRoots || [])])].filter(path => safeIgnoreProbe(plan.root, path));
|
|
464
475
|
const { paths: relativePaths, matches } = ignoreMatches(plan.location, probes);
|
|
@@ -481,12 +492,13 @@ export function assertLocalIgnoreUnchanged(plan) {
|
|
|
481
492
|
}
|
|
482
493
|
}
|
|
483
494
|
|
|
484
|
-
function atomicLocalWrite(path, content, mode, published) {
|
|
495
|
+
function atomicLocalWrite(path, content, mode, published, beforePublish) {
|
|
485
496
|
const temporary = `${path}.agentdevkit-${randomUUID()}`;
|
|
486
497
|
try {
|
|
487
498
|
writeFileSync(temporary, content, { flag: 'wx' });
|
|
488
499
|
if (mode !== null && mode !== undefined) chmodSync(temporary, mode);
|
|
489
500
|
const writtenMode = localMode(temporary);
|
|
501
|
+
beforePublish?.();
|
|
490
502
|
renameSync(temporary, path);
|
|
491
503
|
published?.(writtenMode);
|
|
492
504
|
} finally { rmSync(temporary, { force: true }); }
|
|
@@ -500,7 +512,10 @@ export function applyLocalIgnore(plan) {
|
|
|
500
512
|
try {
|
|
501
513
|
for (const edit of plan.edits) {
|
|
502
514
|
mkdirSync(dirname(edit.path), { recursive: true });
|
|
503
|
-
atomicLocalWrite(edit.path, edit.after, edit.beforeMode, mode => written.push({ edit, mode }))
|
|
515
|
+
atomicLocalWrite(edit.path, edit.after, edit.beforeMode, mode => written.push({ edit, mode }), () => {
|
|
516
|
+
checkedLocalPath(edit.path);
|
|
517
|
+
if (localRead(edit.path) !== edit.before || localMode(edit.path) !== edit.beforeMode) throw new Error(`Exclusao alterada antes de publicar; conteudo preservado: ${edit.path}`);
|
|
518
|
+
});
|
|
504
519
|
}
|
|
505
520
|
const verified = checkLocalIgnore({ ...plan, edits: [], trackedOutputs: [] });
|
|
506
521
|
if (verified.missing.length) throw new Error(`Regras humanas reabrem derivados locais: ${verified.missing.join(', ')}`);
|
|
@@ -509,7 +524,12 @@ export function applyLocalIgnore(plan) {
|
|
|
509
524
|
for (const { edit, mode } of written.reverse()) {
|
|
510
525
|
try {
|
|
511
526
|
if (localRead(edit.path) !== edit.after || localMode(edit.path) !== mode) continue;
|
|
512
|
-
|
|
527
|
+
const checkPublished = () => {
|
|
528
|
+
checkedLocalPath(edit.path);
|
|
529
|
+
if (localRead(edit.path) !== edit.after || localMode(edit.path) !== mode) throw new Error(`Exclusao alterada durante rollback; conteudo preservado: ${edit.path}`);
|
|
530
|
+
};
|
|
531
|
+
if (edit.before === null) { checkPublished(); rmSync(edit.path, { force: true }); }
|
|
532
|
+
else atomicLocalWrite(edit.path, edit.before, edit.beforeMode, undefined, checkPublished);
|
|
513
533
|
} catch (failure) { rollbackErrors.push(failure); }
|
|
514
534
|
}
|
|
515
535
|
if (rollbackErrors.length) throw new AggregateError([error, ...rollbackErrors], `${error.message}; rollback incompleto: ${rollbackErrors.map(failure => failure.message).join('; ')}`, { cause: error });
|
|
@@ -38,16 +38,14 @@ Após a instalação, depende somente de Node e do hub local, sem o pacote npm.
|
|
|
38
38
|
Ele respeita os clientes escolhidos, sem recriar os desmarcados. Para trocar
|
|
39
39
|
a seleção, use `agentdevkit agents <lista>`; não edite o manifesto gerado.
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
presença de um bloco gerenciado.
|
|
41
|
+
O bloco local em `.git/info/exclude` ignora as pastas de clientes por padrão;
|
|
42
|
+
sem Git, a proteção fica pendente até `git init` e sincronização. Para incluir
|
|
43
|
+
uma pasta, use uma exceção literal no `.gitignore` da raiz, como `!/.claude/`.
|
|
44
|
+
Isso também vale para `.github` e `.vscode`, que podem conter arquivos humanos.
|
|
45
|
+
Arquivos já rastreados e o índice não são alterados automaticamente.
|
|
47
46
|
Entradas novas usam os templates; hashes em `state/derived.json` permitem
|
|
48
47
|
atualizar arquivos gerados e blocos separadamente, preservando alterações humanas.
|
|
49
|
-
`--check`
|
|
48
|
+
`--check` somente verifica; não altera o índice.
|
|
50
49
|
Apontamentos obsoletos podem ser removidos quando a propriedade é comprovada;
|
|
51
50
|
arquivos e pastas próprios, inclusive os adicionais dentro do mesmo recurso,
|
|
52
|
-
devem permanecer.
|
|
53
|
-
geradas comprovadas podem deixar de ser rastreadas, sem excluir seus bytes.
|
|
51
|
+
devem permanecer.
|
|
@@ -42,7 +42,7 @@ test('sem Git preserva ignore e informa pendência; após git init protege outpu
|
|
|
42
42
|
git(root, ['add', '--all']);
|
|
43
43
|
const tracked = git(root, ['ls-files', '-z']).split('\0').filter(Boolean);
|
|
44
44
|
for (const path of outputs) assert.equal(tracked.includes(path), false, path);
|
|
45
|
-
for (const path of ['.github/workflows/ci.yml', '.github/ISSUE_TEMPLATE/bug.yml', '.vscode/settings.json', '.github/skills/human/SKILL.md']) assert.
|
|
45
|
+
for (const path of ['.github/workflows/ci.yml', '.github/ISSUE_TEMPLATE/bug.yml', '.vscode/settings.json', '.github/skills/human/SKILL.md']) assert.equal(tracked.includes(path), false, path);
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
test('prefixos e projetos distintos preservam blocos locais independentes', t => {
|
|
@@ -58,13 +58,49 @@ test('prefixos e projetos distintos preservam blocos locais independentes', t =>
|
|
|
58
58
|
assert.match(content, /first\\ space/); assert.match(content, /second/);
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
test('
|
|
61
|
+
test('projeto aninhado sob ancestral ignorado falha antes de alterar índices ou excludes', t => {
|
|
62
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
63
|
+
put(root, '.gitignore', '/.github/\n');
|
|
64
|
+
const child = join(root, '.github/project');
|
|
65
|
+
put(child, '.gitignore', '!/.claude/\n');
|
|
66
|
+
put(child, '.claude/custom.md', 'instruções próprias\n');
|
|
67
|
+
const exclude = readFileSync(join(root, '.git/info/exclude'));
|
|
68
|
+
const index = git(root, ['ls-files', '--stage', '-z']);
|
|
69
|
+
const plan = planLocalIgnore(child, { outputs: ['.claude/custom.md'] });
|
|
70
|
+
assert.ok(checkLocalIgnore(plan).missing.some(value => value.includes('Ancestral Git ignorado: .github/')));
|
|
71
|
+
assert.throws(() => applyLocalIgnore(plan), /Ancestral Git ignorado/);
|
|
72
|
+
assert.deepEqual(readFileSync(join(root, '.git/info/exclude')), exclude);
|
|
73
|
+
assert.equal(git(root, ['ls-files', '--stage', '-z']), index);
|
|
74
|
+
assert.equal(readFileSync(join(child, '.claude/custom.md'), 'utf8'), 'instruções próprias\n');
|
|
75
|
+
put(root, '.gitignore', '/.github/\n!/.github/\n');
|
|
76
|
+
const opened = planLocalIgnore(child, { outputs: ['.claude/custom.md'] });
|
|
77
|
+
applyLocalIgnore(opened);
|
|
78
|
+
assert.equal(checkLocalIgnore(planLocalIgnore(child, { outputs: ['.claude/custom.md'] })).ok, true);
|
|
79
|
+
assert.equal(spawnSync('git', ['-C', root, 'check-ignore', '--no-index', '-q', '.github/project/.claude/custom.md']).status, 1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('opt-in aninhado respeita também excludesFile configurado pelo usuário', t => {
|
|
83
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
84
|
+
put(root, 'global-ignore', '/.private/\n');
|
|
85
|
+
git(root, ['config', '--local', 'core.excludesFile', join(root, 'global-ignore')]);
|
|
86
|
+
const child = join(root, '.private/project');
|
|
87
|
+
put(child, '.gitignore', '!/.claude/\n');
|
|
88
|
+
put(child, '.claude/custom.md', 'instruções próprias\n');
|
|
89
|
+
const before = readFileSync(join(root, '.git/info/exclude'));
|
|
90
|
+
const plan = planLocalIgnore(child);
|
|
91
|
+
assert.ok(checkLocalIgnore(plan).missing.some(value => value.includes('Ancestral Git ignorado: .private/')));
|
|
92
|
+
assert.throws(() => applyLocalIgnore(plan), /Ancestral Git ignorado/);
|
|
93
|
+
assert.deepEqual(readFileSync(join(root, '.git/info/exclude')), before);
|
|
94
|
+
assert.equal(readFileSync(join(child, '.claude/custom.md'), 'utf8'), 'instruções próprias\n');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('check identifica negação humana sem alterar nem diagnosticar conteúdo rastreado legítimo', t => {
|
|
62
98
|
const root = fixture(t), outputs = ['generated.md']; git(root, ['init', '-q']);
|
|
63
99
|
put(root, 'generated.md', 'gerado'); git(root, ['add', 'generated.md']);
|
|
64
100
|
applyLocalIgnore(planLocalIgnore(root, { outputs }));
|
|
65
101
|
put(root, '.gitignore', '!/generated.md\n');
|
|
66
102
|
const result = checkLocalIgnore(planLocalIgnore(root, { outputs }));
|
|
67
|
-
assert.equal(result.ok, false); assert.deepEqual(result.missing, outputs); assert.deepEqual(result.trackedOutputs,
|
|
103
|
+
assert.equal(result.ok, false); assert.deepEqual(result.missing, outputs); assert.deepEqual(result.trackedOutputs, []);
|
|
68
104
|
assert.equal(git(root, ['ls-files', 'generated.md']).trim(), 'generated.md');
|
|
69
105
|
});
|
|
70
106
|
|
|
@@ -89,7 +125,7 @@ test('exceções de templates preservam o ignore raiz e protegem dados privados'
|
|
|
89
125
|
assert.ok(git(root, ['check-ignore', '--no-index', '.cursor/.CACHE/private.json']).includes('.cursor/.CACHE/private.json'));
|
|
90
126
|
assert.ok(git(root, ['check-ignore', '--no-index', '.cursor/.cache/private.json']).includes('.cursor/.cache/private.json'));
|
|
91
127
|
const visible = spawnSync('git', ['-C', root, 'check-ignore', '--no-index', '-q', '.cursor/human.md']);
|
|
92
|
-
assert.equal(visible.status,
|
|
128
|
+
assert.equal(visible.status, 0);
|
|
93
129
|
applyLocalIgnore(planLocalIgnore(root));
|
|
94
130
|
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), updated);
|
|
95
131
|
put(root, '.gitignore', updated + '# edição humana posterior\n');
|
|
@@ -134,12 +170,12 @@ test('negação humana posterior é informada sem reescrever a raiz', t => {
|
|
|
134
170
|
const root = fixture(t); git(root, ['init', '-q']);
|
|
135
171
|
put(root, '.gitignore', '# humano\n'); applyLocalIgnore(planLocalIgnore(root));
|
|
136
172
|
const path = join(root, '.gitignore');
|
|
137
|
-
writeFileSync(path, readFileSync(path, 'utf8') + '!/.cursor/.env.production\n');
|
|
173
|
+
writeFileSync(path, readFileSync(path, 'utf8') + '!/.cursor/\n!/.cursor/.env.production\n');
|
|
138
174
|
put(root, '.cursor/.env.production', 'privado');
|
|
139
175
|
const plan = planLocalIgnore(root);
|
|
140
176
|
assert.ok(checkLocalIgnore(plan).missing.includes('.cursor/.env.production'));
|
|
141
177
|
assert.throws(() => applyLocalIgnore(plan), /Regras humanas/);
|
|
142
|
-
assert.equal(readFileSync(path, 'utf8'), '# humano\n!/.cursor/.env.production\n');
|
|
178
|
+
assert.equal(readFileSync(path, 'utf8'), '# humano\n!/.cursor/\n!/.cursor/.env.production\n');
|
|
143
179
|
});
|
|
144
180
|
|
|
145
181
|
test('planejamento sem Git não modifica ignore nem edição humana posterior', t => {
|
|
@@ -193,11 +229,13 @@ test('desindexação não descarta edição humana staged com workingtree restau
|
|
|
193
229
|
assert.equal(readFileSync(join(root, 'generated.md'), 'utf8'), 'gerado\n');
|
|
194
230
|
});
|
|
195
231
|
|
|
196
|
-
test('configurações humanas
|
|
232
|
+
test('configurações humanas podem ser versionadas após exceção explícita', t => {
|
|
197
233
|
const root = fixture(t); git(root, ['init', '-q']);
|
|
198
234
|
applyLocalIgnore(planLocalIgnore(root));
|
|
199
235
|
put(root, '.codex/config.toml', 'local'); put(root, '.claude/settings.json', 'local');
|
|
200
236
|
put(root, '.github/settings.json', 'fonte'); put(root, '.vscode/settings.json', 'fonte');
|
|
237
|
+
put(root, '.gitignore', '!/.codex/\n!/.claude/\n!/.github/\n!/.vscode/\n');
|
|
238
|
+
assert.equal(checkLocalIgnore(planLocalIgnore(root)).ok, true);
|
|
201
239
|
git(root, ['add', '--all']);
|
|
202
240
|
const tracked = git(root, ['ls-files', '-z']).split('\0');
|
|
203
241
|
assert.equal(tracked.includes('.codex/config.toml'), true);
|
|
@@ -205,6 +243,53 @@ test('configurações humanas dos clientes continuam versionáveis', t => {
|
|
|
205
243
|
assert.ok(tracked.includes('.github/settings.json')); assert.ok(tracked.includes('.vscode/settings.json'));
|
|
206
244
|
});
|
|
207
245
|
|
|
246
|
+
test('exceções de raiz usam precedência real e mantêm credenciais ignoradas', t => {
|
|
247
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
248
|
+
put(root, '.gitignore', '!/.claude/\n!/.github/\n');
|
|
249
|
+
put(root, '.claude/skills/team/SKILL.md', 'skill humana\n');
|
|
250
|
+
put(root, '.claude/auth.json', 'segredo');
|
|
251
|
+
put(root, '.github/workflows/ci.yml', 'workflow humano\n');
|
|
252
|
+
put(root, '.vscode/settings.json', 'configuração humana\n');
|
|
253
|
+
const original = readFileSync(join(root, '.gitignore'));
|
|
254
|
+
const plan = planLocalIgnore(root, { outputs: ['.claude/skills/team/SKILL.md', '.github/workflows/ci.yml'] });
|
|
255
|
+
assert.ok(plan.optedRoots.includes('.claude'));
|
|
256
|
+
assert.ok(plan.optedRoots.includes('.github'));
|
|
257
|
+
applyLocalIgnore(plan);
|
|
258
|
+
assert.equal(checkLocalIgnore(planLocalIgnore(root)).ok, true);
|
|
259
|
+
for (const path of ['.claude/skills/team/SKILL.md', '.github/workflows/ci.yml']) {
|
|
260
|
+
assert.equal(spawnSync('git', ['-C', root, 'check-ignore', '--no-index', '-q', path]).status, 1, path);
|
|
261
|
+
}
|
|
262
|
+
for (const path of ['.claude/auth.json', '.vscode/settings.json']) {
|
|
263
|
+
assert.equal(spawnSync('git', ['-C', root, 'check-ignore', '--no-index', '-q', path]).status, 0, path);
|
|
264
|
+
}
|
|
265
|
+
assert.deepEqual(readFileSync(join(root, '.gitignore')), original);
|
|
266
|
+
put(root, '.gitignore', '!/.claude/\n/.claude/\n');
|
|
267
|
+
const overridden = planLocalIgnore(root);
|
|
268
|
+
assert.equal(overridden.optedRoots.includes('.claude'), false);
|
|
269
|
+
assert.equal(spawnSync('git', ['-C', root, 'check-ignore', '--no-index', '-q', '.claude/skills/team/SKILL.md']).status, 0);
|
|
270
|
+
put(root, '.gitignore', '!/.claude/\n!/.claude/**\n');
|
|
271
|
+
assert.throws(() => applyLocalIgnore(planLocalIgnore(root)), /Regras humanas/);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('derivados e entradas legítimas já rastreados permanecem no índice com ou sem opt-in', t => {
|
|
275
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
276
|
+
put(root, '.claude/skills/team/SKILL.md', 'skill rastreada\n');
|
|
277
|
+
put(root, '.github/workflows/ci.yml', 'workflow rastreado\n');
|
|
278
|
+
put(root, 'AGENTS.md', 'instruções rastreadas\n');
|
|
279
|
+
git(root, ['add', '--all']);
|
|
280
|
+
const before = git(root, ['ls-files', '--stage', '-z']);
|
|
281
|
+
const outputs = ['.claude/skills/team/SKILL.md', '.github/workflows/ci.yml', 'AGENTS.md'];
|
|
282
|
+
applyLocalIgnore(planLocalIgnore(root, { outputs }));
|
|
283
|
+
assert.equal(checkLocalIgnore(planLocalIgnore(root, { outputs })).ok, true);
|
|
284
|
+
assert.equal(git(root, ['ls-files', '--stage', '-z']), before);
|
|
285
|
+
put(root, '.gitignore', '!/.claude/\n');
|
|
286
|
+
const opted = planLocalIgnore(root, { outputs });
|
|
287
|
+
assert.ok(opted.optedRoots.includes('.claude'));
|
|
288
|
+
applyLocalIgnore(opted);
|
|
289
|
+
assert.equal(checkLocalIgnore(planLocalIgnore(root, { outputs })).ok, true);
|
|
290
|
+
assert.equal(git(root, ['ls-files', '--stage', '-z']), before);
|
|
291
|
+
});
|
|
292
|
+
|
|
208
293
|
test('artefatos vêm da política compartilhada e rastreados são informados', t => {
|
|
209
294
|
const root = fixture(t); git(root, ['init', '-q']);
|
|
210
295
|
put(root, '.agent-tests/human.txt', 'evidência humana'); git(root, ['add', '-f', '.agent-tests/human.txt']);
|
|
@@ -271,12 +356,13 @@ test('probes reconhecem exceções humanas para nomes reais de Playwright', asyn
|
|
|
271
356
|
});
|
|
272
357
|
});
|
|
273
358
|
|
|
274
|
-
test('milhares de caminhos usam stdin
|
|
359
|
+
test('milhares de caminhos usam stdin sem ampliar metadata de área já ignorada', t => {
|
|
275
360
|
const root = fixture(t); git(root, ['init', '-q']); put(root, '.gitignore', '/ignored/\n');
|
|
276
361
|
const before = readFileSync(join(root, '.git/info/exclude'), 'utf8');
|
|
277
362
|
const target = join(root, 'ignored'); mkdirSync(target);
|
|
278
|
-
const outputs = Array.from({ length: 1500 }, (_, index) =>
|
|
363
|
+
const outputs = Array.from({ length: 1500 }, (_, index) => `generated-${index}-${'x'.repeat(45)}.md`);
|
|
279
364
|
const plan = planLocalIgnore(target, { outputs });
|
|
365
|
+
assert.ok(plan.paths.length >= 1500);
|
|
280
366
|
assert.deepEqual(plan.edits, []); assert.equal(checkLocalIgnore(plan).ok, true);
|
|
281
367
|
applyLocalIgnore(plan); assert.equal(readFileSync(join(root, '.git/info/exclude'), 'utf8'), before);
|
|
282
368
|
assert.deepEqual(planUntrack(target, Object.fromEntries(outputs.map(path => [path, 'a'.repeat(64)]))).paths, []);
|
|
@@ -341,16 +427,17 @@ test('cliente não selecionado com symlink próprio não é atravessado pelo pro
|
|
|
341
427
|
assert.deepEqual(readdirSync(outside), before);
|
|
342
428
|
});
|
|
343
429
|
|
|
344
|
-
test('preflight
|
|
430
|
+
test('preflight valida hash do bloco substituído sem impor exclusão sobre cliente optado', t => {
|
|
345
431
|
const root = fixture(t); git(root, ['init', '-q']);
|
|
346
432
|
const block = '# BEGIN AGENT IGNORE\r\n!/generated.md\r\n# END AGENT IGNORE';
|
|
433
|
+
put(root, '.gitignore', '!/.github/\n');
|
|
347
434
|
put(root, '.github/.gitignore', `# Humano\r\n${block}\r\n!/human.md\r\n`);
|
|
348
435
|
const plan = planLocalIgnore(root, { outputs: ['.github/generated.md', '.github/human.md'] });
|
|
349
436
|
const replacement = { path: join(root, '.github/.gitignore'), firstLine: 2, lastLine: 4, sha256: createHash('sha256').update(block).digest('hex') };
|
|
350
|
-
assert.deepEqual(checkLocalIgnore(plan).missing, [
|
|
351
|
-
assert.deepEqual(checkLocalIgnore(plan, { replacedBlocks: [replacement] }).missing, [
|
|
437
|
+
assert.deepEqual(checkLocalIgnore(plan).missing, []);
|
|
438
|
+
assert.deepEqual(checkLocalIgnore(plan, { replacedBlocks: [replacement] }).missing, []);
|
|
352
439
|
assert.throws(() => checkLocalIgnore(plan, { replacedBlocks: [{ ...replacement, sha256: '0'.repeat(64) }] }), /Bloco de ignore mudou/);
|
|
353
|
-
assert.
|
|
440
|
+
assert.doesNotThrow(() => applyLocalIgnore(plan));
|
|
354
441
|
assert.equal(readFileSync(join(root, '.github/.gitignore'), 'utf8'), `# Humano\r\n${block}\r\n!/human.md\r\n`);
|
|
355
442
|
});
|
|
356
443
|
|
|
@@ -490,14 +577,14 @@ test('dependências já ignoradas não causam conflito, mas ignores visíveis s
|
|
|
490
577
|
assert.ok(checkLocalIgnore(planLocalIgnore(root)).missing.some(value => value.includes('!.playwright-*/')));
|
|
491
578
|
});
|
|
492
579
|
|
|
493
|
-
test('bloco
|
|
580
|
+
test('bloco de outra worktree não bloqueia a política local de clientes', t => {
|
|
494
581
|
const root = fixture(t); git(root, ['init', '-q']);
|
|
495
582
|
const old = '# BEGIN AGENTDEVKIT LOCAL 0123456789abcdef\n/.claude/\n# END AGENTDEVKIT LOCAL 0123456789abcdef\n';
|
|
496
583
|
put(root, '.git/info/exclude', old);
|
|
497
584
|
const plan = planLocalIgnore(root);
|
|
498
|
-
assert.
|
|
499
|
-
assert.
|
|
500
|
-
assert.
|
|
585
|
+
assert.equal(checkLocalIgnore(plan).missing.length, 0);
|
|
586
|
+
assert.doesNotThrow(() => applyLocalIgnore(plan));
|
|
587
|
+
assert.ok(readFileSync(join(root, '.git/info/exclude'), 'utf8').startsWith(old));
|
|
501
588
|
});
|
|
502
589
|
|
|
503
590
|
test('artefatos já versionados são informados sem alterar o índice', t => {
|
|
@@ -555,6 +642,57 @@ test('escrita de exclusões preserva permissões e detecta alteração concorren
|
|
|
555
642
|
assert.equal(statSync(exclude).mode & 0o777, 0o640);
|
|
556
643
|
});
|
|
557
644
|
|
|
645
|
+
test('alteração no exclude durante escrita temporária é preservada antes da promoção', t => {
|
|
646
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
647
|
+
const file = join(root, '.git/info/exclude'), before = readFileSync(file, 'utf8');
|
|
648
|
+
const plan = planLocalIgnore(root), index = git(root, ['ls-files', '--stage', '-z']);
|
|
649
|
+
const write = fs.writeFileSync; let injected = false;
|
|
650
|
+
try {
|
|
651
|
+
fs.writeFileSync = (path, content, options) => {
|
|
652
|
+
const value = write(path, content, options);
|
|
653
|
+
if (!injected && String(path).startsWith(`${file}.agentdevkit-`)) {
|
|
654
|
+
injected = true;
|
|
655
|
+
write(file, `${before}\n# regra humana concorrente\n`);
|
|
656
|
+
}
|
|
657
|
+
return value;
|
|
658
|
+
};
|
|
659
|
+
syncBuiltinESMExports();
|
|
660
|
+
assert.throws(() => applyLocalIgnore(plan), /Exclusao alterada antes de publicar/);
|
|
661
|
+
} finally { fs.writeFileSync = write; syncBuiltinESMExports(); }
|
|
662
|
+
assert.equal(injected, true);
|
|
663
|
+
assert.equal(readFileSync(file, 'utf8'), `${before}\n# regra humana concorrente\n`);
|
|
664
|
+
assert.equal(git(root, ['ls-files', '--stage', '-z']), index);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
test('alteração no exclude durante rollback não é substituída', t => {
|
|
668
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
669
|
+
const file = join(root, '.git/info/exclude'), plan = planLocalIgnore(root);
|
|
670
|
+
const index = git(root, ['ls-files', '--stage', '-z']);
|
|
671
|
+
const write = fs.writeFileSync, remove = fs.rmSync;
|
|
672
|
+
let failed = false, injected = false;
|
|
673
|
+
try {
|
|
674
|
+
fs.rmSync = (path, options) => {
|
|
675
|
+
if (!failed && String(path).startsWith(`${file}.agentdevkit-`) && !existsSync(path)) {
|
|
676
|
+
failed = true; throw new Error('falha após promoção');
|
|
677
|
+
}
|
|
678
|
+
return remove(path, options);
|
|
679
|
+
};
|
|
680
|
+
fs.writeFileSync = (path, content, options) => {
|
|
681
|
+
const value = write(path, content, options);
|
|
682
|
+
if (failed && !injected && String(path).startsWith(`${file}.agentdevkit-`)) {
|
|
683
|
+
injected = true;
|
|
684
|
+
write(file, `${readFileSync(file, 'utf8')}\n# regra humana no rollback\n`);
|
|
685
|
+
}
|
|
686
|
+
return value;
|
|
687
|
+
};
|
|
688
|
+
syncBuiltinESMExports();
|
|
689
|
+
assert.throws(() => applyLocalIgnore(plan), /rollback incompleto.*conteudo preservado/s);
|
|
690
|
+
} finally { fs.rmSync = remove; fs.writeFileSync = write; syncBuiltinESMExports(); }
|
|
691
|
+
assert.equal(injected, true);
|
|
692
|
+
assert.match(readFileSync(file, 'utf8'), /# regra humana no rollback/);
|
|
693
|
+
assert.equal(git(root, ['ls-files', '--stage', '-z']), index);
|
|
694
|
+
});
|
|
695
|
+
|
|
558
696
|
test('ignore raiz mantém modo e bytes antes e depois de git init', { skip: process.platform === 'win32' }, t => {
|
|
559
697
|
const root = fixture(t), ignore = join(root, '.gitignore');
|
|
560
698
|
put(root, '.gitignore', '# humano\n'); chmodSync(ignore, 0o600);
|
|
@@ -375,19 +375,36 @@ function ignoreFixture(t) {
|
|
|
375
375
|
return context;
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
-
test('fontes
|
|
378
|
+
test('fontes permanecem elegíveis e pastas de clientes aceitam opt-in explícito', t => {
|
|
379
379
|
const { root, file } = ignoreFixture(t);
|
|
380
380
|
const outputs = ['.github/skills/generated/SKILL.md', '.vscode/.gitignore', '.github/.gitignore'];
|
|
381
|
-
|
|
382
|
-
const
|
|
381
|
+
const sources = ['src/new.mjs', 'bin/new.mjs', 'tests/new.test.mjs', 'scripts/release/new.mjs'];
|
|
382
|
+
const clients = ['.github/workflows/own.yml', '.github/ISSUE_TEMPLATE/bug.yml', '.github/skills/human/SKILL.md', '.vscode/settings.json'];
|
|
383
383
|
const generated = [...outputs, '.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/espelhos.conf', '.pbai4devs/state/agentdevkit.json'];
|
|
384
|
-
for (const path of [...sources, ...generated]) file(path, 'fixture\n');
|
|
384
|
+
for (const path of [...sources, ...clients, ...generated.filter(path => !path.endsWith('/.gitignore'))]) file(path, 'fixture\n');
|
|
385
|
+
file('.github/auth.json', 'credencial local\n');
|
|
386
|
+
file('.vscode/logs/session.log', 'artefato local\n');
|
|
387
|
+
assert.equal(git(root, ['add', '--', clients[0], sources[0]]).status, 0);
|
|
388
|
+
const index = git(root, ['ls-files', '--stage', '-z']).stdout;
|
|
389
|
+
applyLocalIgnore(planLocalIgnore(root, { outputs }));
|
|
385
390
|
assert.deepEqual(ignoredPaths(root, sources), []);
|
|
391
|
+
assert.deepEqual(new Set(ignoredPaths(root, clients)), new Set(clients));
|
|
386
392
|
assert.deepEqual(new Set(ignoredPaths(root, generated)), new Set(generated));
|
|
393
|
+
assert.equal(git(root, ['ls-files', '--stage', '-z']).stdout, index);
|
|
394
|
+
for (const path of clients) assert.equal(readFileSync(join(root, path), 'utf8'), 'fixture\n', path);
|
|
395
|
+
|
|
396
|
+
const exceptions = '!/.github/\n!/.vscode/\n';
|
|
397
|
+
file('.gitignore', exceptions);
|
|
398
|
+
applyLocalIgnore(planLocalIgnore(root, { outputs }));
|
|
399
|
+
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), exceptions);
|
|
400
|
+
assert.equal(git(root, ['ls-files', '--stage', '-z']).stdout, index);
|
|
401
|
+
assert.deepEqual(ignoredPaths(root, clients), []);
|
|
402
|
+
const privateFiles = ['.github/auth.json', '.vscode/logs/session.log'];
|
|
403
|
+
assert.deepEqual(new Set(ignoredPaths(root, privateFiles)), new Set(privateFiles));
|
|
387
404
|
assert.equal(git(root, ['add', '--all']).status, 0);
|
|
388
405
|
const tracked = git(root, ['ls-files', '-z']).stdout.split('\0');
|
|
389
|
-
for (const path of sources) assert.ok(tracked.includes(path), path);
|
|
390
|
-
for (const path of generated) assert.equal(tracked.includes(path), false, path);
|
|
406
|
+
for (const path of [...sources, ...clients, ...outputs]) assert.ok(tracked.includes(path), path);
|
|
407
|
+
for (const path of [...generated.filter(path => !outputs.includes(path)), ...privateFiles]) assert.equal(tracked.includes(path), false, path);
|
|
391
408
|
});
|
|
392
409
|
|
|
393
410
|
test('ignores locais cobrem resultados em qualquer caixa sem bloquear fixtures', t => {
|
|
@@ -7,14 +7,21 @@ description: Exemplo opcional de regras de ignore para o projeto.
|
|
|
7
7
|
O `.gitignore` da raiz pertence ao projeto; o kit não o cria nem o altera.
|
|
8
8
|
O projeto pode adotar este exemplo e seus próprios padrões se desejar.
|
|
9
9
|
Regras de artefatos locais vêm de [shared.ignore](shared.ignore) e
|
|
10
|
-
[agents.ignore](agents.ignore). As
|
|
11
|
-
[local.ignore](local.ignore) em `.git/info/exclude`;
|
|
12
|
-
|
|
10
|
+
[agents.ignore](agents.ignore). As pastas dos clientes usam
|
|
11
|
+
[local.ignore](local.ignore) em `.git/info/exclude`; o `.gitignore` deste hub
|
|
12
|
+
não alcança pastas irmãs com `../`. As regras locais não são copiadas pelo Git:
|
|
13
|
+
execute `init` ou `sync` em cada clone com Git inicializado, antes de adicionar
|
|
14
|
+
arquivos. Worktrees do mesmo repositório podem compartilhar as exclusões;
|
|
15
|
+
sem Git, essa proteção fica pendente.
|
|
13
16
|
O sincronizador mantém o bloco `BEGIN AGENT IGNORE` / `END AGENT IGNORE`;
|
|
14
|
-
regras próprias ficam fora dele, com exceções depois do bloco.
|
|
15
|
-
|
|
16
|
-
dos clientes
|
|
17
|
-
|
|
17
|
+
regras próprias ficam fora dele, com exceções depois do bloco. Arquivos já
|
|
18
|
+
rastreados permanecem no índice; o kit não os desindexa automaticamente.
|
|
19
|
+
Pastas dos clientes também podem conter arquivos humanos ainda não rastreados.
|
|
20
|
+
Em especial, `.github` pode conter workflows e formulários do projeto.
|
|
21
|
+
Para permitir o versionamento, acrescente uma exceção literal ao `.gitignore`
|
|
22
|
+
da raiz, como `!/.claude/` ou `!/.github/`. Regras mais profundas continuam
|
|
23
|
+
válidas. Evite exceções amplas como `!/.claude/**`, que podem expor arquivos
|
|
24
|
+
privados e artefatos; `doctor` informa regras conflitantes.
|
|
18
25
|
Ignore binários baixados por local, não por extensão: `.bat`, `.exe` e outros
|
|
19
26
|
arquivos podem ser scripts ou fixtures necessários. Não use regras genéricas
|
|
20
27
|
como `Release/`, `Scripts/` ou `Lib/` que escondam fontes e modelos.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,11 @@ description: Alterações verificáveis por versão do AgentDevKit.
|
|
|
4
4
|
|
|
5
5
|
# Changelog
|
|
6
6
|
|
|
7
|
+
## 0.0.19 — 2026-11-13
|
|
8
|
+
|
|
9
|
+
- Pastas dos clientes são ignoradas localmente por padrão, com exceções do projeto.
|
|
10
|
+
- O índice Git e os arquivos já rastreados permanecem intactos.
|
|
11
|
+
|
|
7
12
|
## 0.0.18 — 2026-11-13
|
|
8
13
|
|
|
9
14
|
- Sincronização preserva recursos próprios, colisões e alterações humanas.
|
package/README.en.md
CHANGED
|
@@ -13,7 +13,7 @@ Organize work with coding agents, from planning to verification. Keep skills,
|
|
|
13
13
|
rules, commands and agent profiles in `.pbai4devs`; the kit generates the
|
|
14
14
|
references each client can discover. Code lives in `src/`, product tests in
|
|
15
15
|
`tests/`, and shared agent resources in `.pbai4devs/`. Generated client
|
|
16
|
-
folders
|
|
16
|
+
folders are ignored locally by default; projects can opt in to tracking them.
|
|
17
17
|
See the [supported clients][clients].
|
|
18
18
|
|
|
19
19
|
## Start in your project
|
package/README.es.md
CHANGED
|
@@ -13,8 +13,8 @@ Organiza el trabajo con agentes de código, desde la planificación hasta la
|
|
|
13
13
|
verificación. Mantén skills, reglas, comandos y perfiles en `.pbai4devs`;
|
|
14
14
|
el kit genera las referencias de cada [cliente compatible][clients].
|
|
15
15
|
El código vive en `src/`, las pruebas del producto en `tests/` y los recursos
|
|
16
|
-
compartidos en `.pbai4devs/`. Las carpetas
|
|
17
|
-
|
|
16
|
+
compartidos en `.pbai4devs/`. Las carpetas de clientes se ignoran localmente
|
|
17
|
+
por defecto; cada proyecto puede elegir versionarlas.
|
|
18
18
|
|
|
19
19
|
## Empezar en tu proyecto
|
|
20
20
|
|
package/docs/guides/cli.md
CHANGED
|
@@ -85,10 +85,12 @@ comando agêntico [sincronizar](../../.pbai4devs/commands/sincronizar/COMMAND.md
|
|
|
85
85
|
orienta executar o script com `--force`; ele não é o mesmo que `sync` da CLI.
|
|
86
86
|
|
|
87
87
|
No Git, o kit preserva o `.gitignore` da raiz e pode atualizar
|
|
88
|
-
`.git/info/exclude
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
`.git/info/exclude` para ocultar por padrão as pastas dos clientes. O projeto
|
|
89
|
+
pode reabri-las no `.gitignore` da raiz, como em `!/.claude/`; consulte as
|
|
90
|
+
[exceções e seus efeitos](installation.md#git-e-migração). Os comandos não
|
|
91
|
+
retiram arquivos já rastreados do índice, e `untrackedPaths` permanece vazio
|
|
92
|
+
por compatibilidade. Backups locais ficam em `.agent-tests/` e não substituem
|
|
93
|
+
o histórico Git.
|
|
92
94
|
|
|
93
95
|
## Ferramentas
|
|
94
96
|
|
|
@@ -87,7 +87,7 @@ O próximo passo é [preparar as ferramentas](prerequisites.md#depois-de-instala
|
|
|
87
87
|
| Conteúdo | Tratamento |
|
|
88
88
|
| --- | --- |
|
|
89
89
|
| `.pbai4devs` com recursos e templates | Fonte compartilhada versionável. |
|
|
90
|
-
|
|
|
90
|
+
| Recursos nos clientes | Apontamentos locais; pastas ignoradas por padrão. |
|
|
91
91
|
| Entradas da raiz criadas pelo kit | Arquivos locais gerados. |
|
|
92
92
|
| Índices, manifesto e seleção pessoal | Derivados locais ignorados. |
|
|
93
93
|
| Código, documentos e configurações humanas | Preservados. |
|
|
@@ -102,12 +102,31 @@ identifica a licença dos recursos do AgentDevKit, sem redefinir a do projeto.
|
|
|
102
102
|
|
|
103
103
|
## Git e migração
|
|
104
104
|
|
|
105
|
-
Um `.gitignore` dentro de `.pbai4devs` só controla essa árvore
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
105
|
+
Um `.gitignore` dentro de `.pbai4devs` só controla essa árvore: `../` não
|
|
106
|
+
alcança pastas irmãs. O kit gera regras em `.git/info/exclude` para ignorar
|
|
107
|
+
por padrão as pastas dos clientes, sem alterar o `.gitignore` da raiz.
|
|
108
|
+
Essas regras são locais, não acompanham o clone; execute `init` ou `sync`
|
|
109
|
+
em cada clone com Git inicializado, antes de usar `git add`. Sem Git, a
|
|
110
|
+
exclusão fica pendente até `git init` e sincronização.
|
|
111
|
+
Worktrees do mesmo repositório podem compartilhar esse arquivo de exclusões.
|
|
112
|
+
|
|
113
|
+
Para versionar uma pasta de cliente, o projeto pode acrescentar uma exceção
|
|
114
|
+
literal ao seu próprio `.gitignore` da raiz, por exemplo:
|
|
115
|
+
|
|
116
|
+
```gitignore
|
|
117
|
+
!/.claude/
|
|
118
|
+
!/.github/
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
A exceção da raiz tem precedência sobre `.git/info/exclude`; regras de um
|
|
122
|
+
`.gitignore` mais profundo continuam valendo. Não use `!/.claude/**`: um
|
|
123
|
+
curinga amplo pode reabrir credenciais e artefatos que devem ficar locais.
|
|
124
|
+
O diagnóstico informa exceções que exponham esses arquivos.
|
|
125
|
+
|
|
126
|
+
O padrão também oculta arquivos humanos ainda não rastreados nas pastas dos
|
|
127
|
+
clientes. Em especial, `.github` pode conter workflows, formulários de issues
|
|
128
|
+
e outras configurações do projeto; use `!/.github/` se precisar versioná-los.
|
|
129
|
+
O mesmo cuidado vale para `.vscode` e configurações próprias de outros agentes.
|
|
111
130
|
|
|
112
131
|
O bloco local também protege `.agent-tests`, `tmp`, `temp`, `.playwright*`
|
|
113
132
|
e relatórios de testes, inclusive com letras maiúsculas e em subpastas.
|
|
@@ -119,15 +138,10 @@ de existir um arquivo correspondente. Examine a regra indicada no diagnóstico.
|
|
|
119
138
|
Nomes próprios de artefatos e arquivos existentes também são conferidos;
|
|
120
139
|
a validação fica restrita ao projeto e aos ignores de seus ancestrais.
|
|
121
140
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
e
|
|
126
|
-
alteram o índice.
|
|
127
|
-
Artefatos já rastreados são informados por `doctor` e `sync --check` para
|
|
128
|
-
revisão; não são removidos automaticamente do índice.
|
|
129
|
-
Revise o plano e os conflitos em um projeto existente; use `--dry-run` antes
|
|
130
|
-
de aplicar. Conteúdo humano não deve ser classificado como saída do gerador.
|
|
141
|
+
Ignorar uma pasta não retira arquivos já rastreados do índice. O kit preserva
|
|
142
|
+
o índice, inclusive alterações preparadas; para mudar o rastreamento, o projeto
|
|
143
|
+
decide a ação Git. `--dry-run` e `sync --check` também não alteram o índice.
|
|
144
|
+
Revise o plano e os conflitos antes de aplicar em um projeto existente.
|
|
131
145
|
|
|
132
146
|
## Atualizar e escolher agentes
|
|
133
147
|
|
|
@@ -137,9 +151,9 @@ continua válida; `agents` consulta e `agents codex,claude` altera a seleção.
|
|
|
137
151
|
necessárias para outro cliente, como `.agents` para Codex e Antigravity.
|
|
138
152
|
|
|
139
153
|
Confira `update --dry-run --json` antes de atualizar um projeto existente.
|
|
140
|
-
Revise arquivos criados, atualizados e removidos, conflitos
|
|
141
|
-
|
|
142
|
-
|
|
154
|
+
Revise arquivos criados, atualizados e removidos, conflitos e `preserved`.
|
|
155
|
+
`sync` usa as fontes já instaladas e não baixa uma nova versão. Consulte a
|
|
156
|
+
[referência da CLI](cli.md) para todos os comandos.
|
|
143
157
|
|
|
144
158
|
Skills e outros recursos próprios podem coexistir com os do kit. Um nome
|
|
145
159
|
igual não comprova propriedade: conteúdo humano e arquivos adicionais não
|
|
@@ -165,8 +179,8 @@ dele: atualizações alteram somente a contribuição do kit. Nomes de entradas
|
|
|
165
179
|
com caixa diferente são reaproveitados quando há uma única opção.
|
|
166
180
|
Comandos Gemini mantêm a referência dentro do valor `prompt` em TOML válido.
|
|
167
181
|
Configurações que não são instruções não recebem uma mesclagem arbitrária.
|
|
168
|
-
Arquivos mistos
|
|
169
|
-
|
|
182
|
+
Arquivos mistos não passam a pertencer ao kit. Sua visibilidade para o Git
|
|
183
|
+
também depende da regra aplicada à pasta do cliente.
|
|
170
184
|
|
|
171
185
|
Links simbólicos preexistentes são preservados sem atravessá-los. O campo
|
|
172
186
|
`preserved` informa rotas que continuam sob controle do projeto. Um formato
|
|
@@ -164,8 +164,9 @@ não devem ser versionados. Verifique com `git status` e `git check-ignore -v
|
|
|
164
164
|
retira automaticamente do Git; revise a remoção do índice sem apagar dados.
|
|
165
165
|
|
|
166
166
|
O `.gitignore` do hub não controla pastas irmãs. Confira a proteção local
|
|
167
|
-
gerada pelo kit
|
|
168
|
-
|
|
167
|
+
gerada pelo kit em `info/exclude`. Para versionar configurações humanas novas
|
|
168
|
+
em `.github` ou `.vscode`, reabra a pasta com `!/.github/` ou `!/.vscode/`
|
|
169
|
+
no `.gitignore` da raiz. Confira as regras com `git check-ignore -v`.
|
|
169
170
|
|
|
170
171
|
Para pedir ajuda, inclua SO/arquitetura, versões de Node/npm/AgentDevKit,
|
|
171
172
|
comando executado, código de saída e trecho relevante do JSON, sem segredos.
|
package/docs/product/design.md
CHANGED
|
@@ -60,13 +60,13 @@ bloco exigem revisão e não autorizam sobrescrever o arquivo com `--force`.
|
|
|
60
60
|
Arquivos novos e integralmente gerados continuam com propriedade integral.
|
|
61
61
|
Comandos TOML recebem a referência no valor de `prompt`, preservando as demais
|
|
62
62
|
chaves. Configurações nativas fora desse escopo permanecem sob controle humano.
|
|
63
|
-
Nunca é possível ignorar somente um trecho de arquivo no Git
|
|
64
|
-
|
|
63
|
+
Nunca é possível ignorar somente um trecho de arquivo no Git. Veja a
|
|
64
|
+
[spec de integração][additive].
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
da raiz
|
|
69
|
-
|
|
66
|
+
O Git usa `info/exclude` para ignorar localmente pastas de clientes, inclusive
|
|
67
|
+
seus arquivos humanos ainda não rastreados. Exceções literais no `.gitignore`
|
|
68
|
+
da raiz permitem versioná-las. Arquivos já rastreados e o índice permanecem;
|
|
69
|
+
sem Git, a proteção fica pendente. A política vem dos templates.
|
|
70
70
|
|
|
71
71
|
O estado registra versão, escolha e propriedade local. Índices e manifesto
|
|
72
72
|
podem ser reconstruídos. Downloads, credenciais e relatórios ficam fora do
|
package/docs/specs/README.md
CHANGED
|
@@ -25,6 +25,7 @@ description: Incrementos verificáveis do AgentDevKit.
|
|
|
25
25
|
- [Integração sem substituição][additive]: acréscimos em arquivos existentes.
|
|
26
26
|
- [Compatibilidade com projetos reais][oss]: projetos públicos e exemplos reproduzíveis.
|
|
27
27
|
- [Ignore do projeto](project-ignore/SPEC.md): preservar o arquivo da raiz.
|
|
28
|
+
- [Ignore de clientes](client-ignore/SPEC.md): padrão local e exceções do projeto.
|
|
28
29
|
|
|
29
30
|
Cada incremento vincula requisitos, critérios, testes e evidências locais.
|
|
30
31
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Exclusão local de clientes com escolha explícita do projeto.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Ignore de clientes
|
|
6
|
+
|
|
7
|
+
O Git não aplica `../` no `.gitignore` do hub às pastas irmãs. O kit usa
|
|
8
|
+
exclusões locais do Git e o projeto escolhe exceções no seu `.gitignore`.
|
|
9
|
+
|
|
10
|
+
## Critérios de aceite
|
|
11
|
+
|
|
12
|
+
- AC-001: Pastas de clientes são ignoradas por padrão em repositórios Git,
|
|
13
|
+
sem modificar o `.gitignore` da raiz.
|
|
14
|
+
- AC-002: Uma exceção literal da raiz, como `!/.claude/`, permite versionar
|
|
15
|
+
o cliente e continua válida após instalação, atualização e sincronização.
|
|
16
|
+
- AC-003: Credenciais e artefatos continuam protegidos; exceções inseguras
|
|
17
|
+
são informadas antes de qualquer mudança.
|
|
18
|
+
- AC-004: Arquivos humanos e o índice Git, incluindo alterações preparadas,
|
|
19
|
+
são preservados. Arquivos já rastreados continuam rastreados.
|
|
20
|
+
- AC-005: Documentação e testes explicam e verificam precedência, limites,
|
|
21
|
+
clones, clientes compartilhados, cenários sem Git e sistemas suportados.
|
package/package.json
CHANGED
package/src/install.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { chmodSync, closeSync, existsSync, fstatSync, lstatSync, openSync, readF
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { synchronize, isReference } from '../.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs';
|
|
7
|
-
import { isGeneratedOutput, planLocalIgnore, checkLocalIgnore, assertLocalIgnoreUnchanged, applyLocalIgnore,
|
|
7
|
+
import { isGeneratedOutput, planLocalIgnore, checkLocalIgnore, assertLocalIgnoreUnchanged, applyLocalIgnore, indexContentMatches } from '../.pbai4devs/scripts/lib/gitignore.mjs';
|
|
8
8
|
import { nativePathBoundary } from '../.pbai4devs/scripts/lib/native-paths.mjs';
|
|
9
9
|
import { canonicalEntrypoint, resolveEntrypoint, entrypointReference, renderEntrypointImports, extractManagedBlock, managedBlockDigest, mergeManagedContent, stripManagedContent } from '../.pbai4devs/scripts/lib/managed-content.mjs';
|
|
10
10
|
import { extractTomlManagedBlock, tomlManagedBlockDigest, mergeTomlContent, stripTomlContent } from '../.pbai4devs/scripts/lib/managed-toml.mjs';
|
|
@@ -698,16 +698,9 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
698
698
|
if (stagedChanges.length) fail('E_STAGED', `Staged content differs from the working tree for planned changes: ${stagedChanges.map(change => `${change.action} ${change.path}`).join(', ')}. Preserve and review these paths before updating.`);
|
|
699
699
|
const retiredRoots = adapterRoots.filter(root => !active.roots.includes(root) && changes.some(change => change.action === 'remove' && change.path.startsWith(`${root}/`) && (previous?.files[change.path] || previous?.blocks[change.path])));
|
|
700
700
|
const ownedOutputs = [...new Set([...synchronized.ownedOutputs, ...ownedEntrypoints, statePath])].filter(path => !mixedPaths.has(path) && !overlapsPreserved(path));
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
for (const path of Object.keys(previous?.blocks || {})) if (path.endsWith('/.gitignore') && adapterRoots.some(root => path === `${root}/.gitignore`)) {
|
|
705
|
-
const current = read(target, path);
|
|
706
|
-
if (current) { const source = current.toString('utf8'), block = extractBlock(source, '# BEGIN AGENT IGNORE', '# END AGENT IGNORE'); if (block && source.trim() === block.trim() && hash(block) === previous.blocks[path]) hashes[path] = hash(current); }
|
|
707
|
-
}
|
|
708
|
-
if (previous && !migration) hashes[statePath] = hash(read(target, statePath));
|
|
709
|
-
const untrack = existsSync(target) ? planUntrack(target, hashes) : null;
|
|
710
|
-
if (migration && untrack?.paths.length) fail('E_STATE', 'State migration would change the Git index; preserve and resolve tracked outputs first.');
|
|
701
|
+
// Ignore rules affect only untracked files. Never remove existing index
|
|
702
|
+
// entries on behalf of the project; staged bytes are checked separately.
|
|
703
|
+
const untrack = { root: target, paths: [], hashes: {}, index: '', conflicts: [] };
|
|
711
704
|
const localIgnore = planLocalIgnore(target, { outputs: ownedOutputs, templateRoot: stage });
|
|
712
705
|
const localIgnoreCheck = checkLocalIgnore(localIgnore, { replacedBlocks: replacedIgnoreBlocks(target, changes, previous) });
|
|
713
706
|
conflicts.push(...(untrack?.conflicts || []), ...localIgnoreCheck.missing);
|
|
@@ -759,14 +752,12 @@ export function applyPlan(plan) {
|
|
|
759
752
|
for (const change of plan.changes) if ((before.get(change.path) ? hash(before.get(change.path)) : null) !== change.beforeHash) fail('E_CHANGED', `File changed after planning: ${change.path}`);
|
|
760
753
|
for (const [path, content] of before) if (content) write(target, `${backup}/${path}`, content, beforeModes.get(path));
|
|
761
754
|
const applied = [], removedDirectories = [];
|
|
762
|
-
let untracked = false;
|
|
763
755
|
const assertOriginal = path => {
|
|
764
756
|
const file = safePath(target, path), info = stat(file), bytes = before.get(path);
|
|
765
757
|
if (bytes === null ? info !== null : !info?.isFile() || !readFileSync(file).equals(bytes) || (info.mode & 0o777) !== beforeModes.get(path)) fail('E_CHANGED', `File changed before writing: ${path}`);
|
|
766
758
|
};
|
|
767
759
|
try {
|
|
768
760
|
for (const change of plan.changes) assertOriginal(change.path);
|
|
769
|
-
if (plan.untrack?.paths.length) { applyUntrack(plan.untrack); untracked = true; }
|
|
770
761
|
for (const change of plan.changes) {
|
|
771
762
|
assertOriginal(change.path);
|
|
772
763
|
const file = safePath(target, change.path);
|
|
@@ -806,7 +797,7 @@ export function applyPlan(plan) {
|
|
|
806
797
|
const changesFallback = plan.changes.some(change => change.path === '.gitignore');
|
|
807
798
|
// Metadata is committed last; its writer rolls back partial writes itself.
|
|
808
799
|
const ignored = applyLocalIgnore(changesFallback ? planLocalIgnore(target, { outputs: plan.ownedOutputs }) : plan.localIgnore);
|
|
809
|
-
return { changed: Boolean(plan.changes.length || ignored.changed
|
|
800
|
+
return { changed: Boolean(plan.changes.length || ignored.changed), backup: [...before.values()].some(Boolean) ? backup : null };
|
|
810
801
|
} catch (error) {
|
|
811
802
|
const rollbackErrors = [], preserved = [];
|
|
812
803
|
for (const removed of [...removedDirectories].sort((a, b) => a.path.length - b.path.length)) {
|
|
@@ -847,7 +838,6 @@ export function applyPlan(plan) {
|
|
|
847
838
|
if (preserved.length) {
|
|
848
839
|
try { error.rollbackPreservedPaths = preserved; } catch { /* Preserve a exceção original. */ }
|
|
849
840
|
}
|
|
850
|
-
if (untracked) try { restoreUntrack(plan.untrack); } catch (failure) { rollbackErrors.push(failure); }
|
|
851
841
|
if (rollbackErrors.length) throw Object.assign(new AggregateError([error, ...rollbackErrors], `${error.message}; rollback incompleto: ${rollbackErrors.map(failure => failure.message).join('; ')}`, { cause: error }), { code: error.code || 'E_OPERATION' });
|
|
852
842
|
throw error;
|
|
853
843
|
}
|