@pathbit/agentdevkit 0.0.12 → 0.0.14
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/INSTRUCTIONS.md +3 -1
- package/.pbai4devs/rules/qualidade/RULE.md +5 -6
- package/.pbai4devs/scripts/lib/README.md +3 -0
- package/.pbai4devs/scripts/lib/gitignore.mjs +133 -25
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +1 -1
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +2 -1
- package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +151 -24
- package/.pbai4devs/templates/gitignore/TEMPLATE.md +5 -5
- package/CHANGELOG.md +13 -0
- package/README.en.md +2 -2
- package/README.es.md +2 -2
- package/README.md +3 -2
- package/docs/README.md +1 -0
- package/docs/guides/README.md +1 -0
- package/docs/guides/installation.md +10 -4
- package/docs/guides/recursos-na-pratica.md +91 -0
- package/docs/guides/resources.md +3 -0
- package/docs/maintainers/maintenance.md +1 -1
- package/docs/maintainers/packages.md +2 -1
- package/docs/specs/README.md +3 -0
- package/docs/specs/project-ignore/SPEC.md +26 -0
- package/docs/specs/real-projects/SPEC.md +26 -0
- package/package.json +1 -1
- package/src/cli.mjs +9 -4
- package/src/i18n.mjs +6 -0
- package/src/install.mjs +2 -1
|
@@ -79,7 +79,9 @@ permanecem exclusivamente em `.pbai4devs`.
|
|
|
79
79
|
O sincronizador mantém a proteção Git das saídas locais e de `.pbai4devs`.
|
|
80
80
|
Use os [modelos de ignore](templates/gitignore/TEMPLATE.md) para alterar a lista
|
|
81
81
|
de fontes e derivados; em Git, os apontamentos usam exclusões locais.
|
|
82
|
-
|
|
82
|
+
O `.gitignore` da raiz pertence ao projeto e não é alterado pelo kit.
|
|
83
|
+
Sem Git, a proteção local fica pendente; após `git init`, sincronize antes
|
|
84
|
+
de adicionar arquivos ao índice.
|
|
83
85
|
Documentos de produto também ficam fora dos espelhos. O comando `sincronizar`
|
|
84
86
|
reconstrói apontamentos com `--force`; `--check` verifica sem alterações.
|
|
85
87
|
|
|
@@ -20,12 +20,11 @@ H1; preserve os metadados exigidos pela ferramenta. Em documentos humanos
|
|
|
20
20
|
existentes, mantenha o formato e o conteúdo: não os reformate apenas para
|
|
21
21
|
impor esse padrão.
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
[modelo de gitignore](../../templates/gitignore/TEMPLATE.md)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
na raiz evita versioná-los ao iniciar o repositório.
|
|
23
|
+
O `.gitignore` da raiz pertence ao projeto e não é alterado pelo kit. O
|
|
24
|
+
[modelo de gitignore](../../templates/gitignore/TEMPLATE.md) é opcional para
|
|
25
|
+
regras do projeto. `.pbai4devs/.gitignore` controla o hub; o gerador protege
|
|
26
|
+
os derivados externos no Git local. Sem Git, essa proteção fica pendente até
|
|
27
|
+
`git init` e sincronização, antes de adicionar arquivos ao índice.
|
|
29
28
|
Confira com Git tanto os arquivos permitidos quanto os artefatos ignorados.
|
|
30
29
|
Scripts compartilhados devem ser versionados; resultados locais, ignorados.
|
|
31
30
|
Ignore o conteúdo baixado de tools, preservando `TOOL.json`, scripts próprios
|
|
@@ -11,3 +11,6 @@ Também procura executáveis opcionais no PATH sem iniciar um shell.
|
|
|
11
11
|
`managed-content.mjs` limita a propriedade do kit aos seus blocos Markdown;
|
|
12
12
|
`managed-toml.mjs` aplica o mesmo contrato ao `prompt` de comandos Gemini.
|
|
13
13
|
Texto externo, nomes existentes e metadados humanos permanecem preservados.
|
|
14
|
+
|
|
15
|
+
`gitignore.mjs` planeja exclusões locais e detecta regras conflitantes. Ele não
|
|
16
|
+
altera o `.gitignore` da raiz; conflitos são informados para revisão humana.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { chmodSync, closeSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { chmodSync, closeSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -112,7 +112,7 @@ function checkedOutput(path) {
|
|
|
112
112
|
function escapePattern(path) { return path.replace(/[\\!#*?\[\] ]/g, '\\$&'); }
|
|
113
113
|
|
|
114
114
|
function rootProbes(root, roots) {
|
|
115
|
-
return roots.filter(path => { const item = localStat(join(root, path)); return !item || item.isDirectory(); }).map(path => `${path}/.agentdevkit-ignore-probe`);
|
|
115
|
+
return roots.filter(path => { const item = localStat(join(root, path)); return !item || item.isDirectory() && !item.isSymbolicLink(); }).map(path => `${path}/.agentdevkit-ignore-probe`);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
function artifactPolicy(templateFile) {
|
|
@@ -136,9 +136,11 @@ function privateClientPolicy(templateFile, roots) {
|
|
|
136
136
|
const negative = pattern.startsWith('!'), rule = pattern.replace(/^!/, '').replace(/^\//, '');
|
|
137
137
|
rules.push(`${negative ? '!' : ''}/${escapePattern(root)}/${rule}`);
|
|
138
138
|
if (negative) continue;
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
const witness = lower => rule.replace(/^\*\*\//, '').replace(/\[([^\]]+)\]/g, (_match, letters) => lower ? letters[0].toLowerCase() : letters[0]).replace(/\*/g, 'agentdevkit-private-probe').replace(/\?/g, 'x');
|
|
140
|
+
for (const variant of new Set([witness(false), witness(true)])) {
|
|
141
|
+
probes.push(`${root}/${variant}${variant.endsWith('/') ? '.agentdevkit-ignore-probe' : ''}`);
|
|
142
|
+
if (rule.startsWith('**/')) probes.push(`${root}/.agentdevkit-ignore-probes/${variant}${variant.endsWith('/') ? '.agentdevkit-ignore-probe' : ''}`);
|
|
143
|
+
}
|
|
142
144
|
}
|
|
143
145
|
return { rules, probes };
|
|
144
146
|
}
|
|
@@ -148,7 +150,7 @@ function safeIgnoreProbe(root, path) {
|
|
|
148
150
|
for (const part of path.split('/').slice(0, -1)) {
|
|
149
151
|
parent = join(parent, part);
|
|
150
152
|
const item = localStat(parent);
|
|
151
|
-
if (item && !item.isDirectory()) return false;
|
|
153
|
+
if (item && (!item.isDirectory() || item.isSymbolicLink())) return false;
|
|
152
154
|
}
|
|
153
155
|
return true;
|
|
154
156
|
}
|
|
@@ -164,7 +166,7 @@ function artifactProbes(root, patterns) {
|
|
|
164
166
|
let parent = root, safe = true;
|
|
165
167
|
for (const part of path.split('/').slice(0, -1)) {
|
|
166
168
|
parent = join(parent, part); const item = localStat(parent);
|
|
167
|
-
if (item && !item.isDirectory()) { safe = false; break; }
|
|
169
|
+
if (item && (!item.isDirectory() || item.isSymbolicLink())) { safe = false; break; }
|
|
168
170
|
}
|
|
169
171
|
if (safe) probes.add(path);
|
|
170
172
|
}
|
|
@@ -202,8 +204,8 @@ function visibleArtifactProbes(root, location, patterns) {
|
|
|
202
204
|
const listed = gitCommand(location.top, ['--literal-pathspecs', '-c', 'core.excludesFile=', 'ls-files', '--cached', '--others', '--exclude-standard', '-z', '--', location.prefix ? `${location.prefix}/` : '.']);
|
|
203
205
|
if (listed.error || listed.status !== 0) throw new Error(listed.error?.message || listed.stderr || 'Falha ao conferir artefatos existentes.');
|
|
204
206
|
const paths = listed.stdout.split('\0').filter(Boolean), prefix = location.prefix ? `${location.prefix}/` : '';
|
|
205
|
-
const probes = new Set(paths.filter(path => path.startsWith(prefix)).map(path => path.slice(prefix.length)).filter(path => artifactPath(path, patterns)));
|
|
206
|
-
const ignores = new Set(paths.filter(path => path.startsWith(prefix) && (path === '.gitignore' || path.endsWith('/.gitignore'))));
|
|
207
|
+
const probes = new Set(paths.filter(path => path.startsWith(prefix)).map(path => path.slice(prefix.length)).filter(path => artifactPath(path, patterns) && safeIgnoreProbe(root, path)));
|
|
208
|
+
const ignores = new Set(paths.filter(path => path.startsWith(prefix) && (path === '.gitignore' || path.endsWith('/.gitignore')) && safeIgnoreProbe(root, path.slice(prefix.length))));
|
|
207
209
|
let ancestor = root;
|
|
208
210
|
while (true) {
|
|
209
211
|
ignores.add(portable(relative(location.top, join(ancestor, '.gitignore'))));
|
|
@@ -222,12 +224,111 @@ function visibleArtifactProbes(root, location, patterns) {
|
|
|
222
224
|
const ruleBase = !rule.startsWith('/') && !literal.includes('/') && relative(dirname(file), root).split(sep)[0] !== '..' ? root : dirname(file);
|
|
223
225
|
const candidate = portable(relative(root, resolve(ruleBase, literal.replace(/\\(.)/g, '$1'))));
|
|
224
226
|
if (!candidate || candidate === '..' || candidate.startsWith('../') || !artifactPath(candidate, patterns, directory)) continue;
|
|
225
|
-
|
|
227
|
+
const probe = checkedOutput(directory ? `${candidate}/.agentdevkit-artifact-probe` : candidate);
|
|
228
|
+
if (safeIgnoreProbe(root, probe)) probes.add(probe);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return [...probes];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Existing unignored private files and literal/mixed-case root exceptions must
|
|
235
|
+
// not slip through merely because a synthetic wildcard used another spelling.
|
|
236
|
+
function visiblePrivateProbes(root, location, clientRoots) {
|
|
237
|
+
const listed = gitCommand(location.top, ['--literal-pathspecs', '-c', 'core.excludesFile=', 'ls-files', '--others', '--exclude-standard', '-z', '--', location.prefix ? `${location.prefix}/` : '.']);
|
|
238
|
+
if (listed.error || listed.status !== 0) throw new Error(listed.error?.message || listed.stderr || 'Falha ao conferir arquivos privados.');
|
|
239
|
+
const prefix = location.prefix ? `${location.prefix}/` : '', roots = new Set(clientRoots);
|
|
240
|
+
const probes = new Set(listed.stdout.split('\0').filter(path => path.startsWith(prefix)).map(path => path.slice(prefix.length))
|
|
241
|
+
.filter(path => roots.has(path.split('/')[0]) && isGeneratedOutput(path) && safeIgnoreProbe(root, path)));
|
|
242
|
+
const ignore = join(root, '.gitignore'), item = localStat(ignore);
|
|
243
|
+
if (item?.isFile() && !item.isSymbolicLink()) for (const line of readFileSync(ignore, 'utf8').replace(/^\uFEFF/, '').split(/\r?\n/)) {
|
|
244
|
+
if (!line.startsWith('!') || line.length === 1) continue;
|
|
245
|
+
const rule = line.slice(1), directory = rule.endsWith('/');
|
|
246
|
+
const witness = rule.replace(/^\//, '').replace(/\*\*\//g, '').replace(/\[([^\]]+)\]/g, (_m, letters) => letters[0]).replace(/\*/g, '').replace(/\?/g, 'x').replace(/\/$/, '');
|
|
247
|
+
if (!witness || /[\\\x00-\x1f]/.test(witness)) continue;
|
|
248
|
+
const candidates = roots.has(witness.split('/')[0]) ? [witness] : [...roots].map(client => `${client}/${witness}`);
|
|
249
|
+
for (const path of candidates) {
|
|
250
|
+
const probe = checkedOutput(directory ? `${path}/.agentdevkit-ignore-probe` : path);
|
|
251
|
+
if (isGeneratedOutput(path, directory) && safeIgnoreProbe(root, probe)) probes.add(probe);
|
|
226
252
|
}
|
|
227
253
|
}
|
|
228
254
|
return [...probes];
|
|
229
255
|
}
|
|
230
256
|
|
|
257
|
+
// Wildcard negations may expose files that do not exist yet. Inspect client
|
|
258
|
+
// ignores directly, even when an old rule currently hides the entire client.
|
|
259
|
+
function ambiguousExceptions(root, location, clientRoots, artifacts) {
|
|
260
|
+
const files = [];
|
|
261
|
+
const visit = directory => {
|
|
262
|
+
const item = localStat(directory);
|
|
263
|
+
if (!item?.isDirectory() || item.isSymbolicLink()) return;
|
|
264
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
265
|
+
const path = join(directory, entry.name);
|
|
266
|
+
if (entry.isSymbolicLink()) continue;
|
|
267
|
+
if (entry.isFile() && entry.name === '.gitignore') files.push(path);
|
|
268
|
+
else if (entry.isDirectory()) {
|
|
269
|
+
const local = portable(relative(root, path)), client = clientRoots.includes(local.split('/')[0]);
|
|
270
|
+
const downloaded = entry.name === 'node_modules' || /^\.pbai4devs\/(?:tools\/(?:npm-cache|bin)|external-skills\/[^/]+)$/.test(local);
|
|
271
|
+
const gitPath = location.prefix ? `${location.prefix}/${local}/` : `${local}/`;
|
|
272
|
+
const ignoredDownload = downloaded && gitCommand(location.top, ['-c', 'core.excludesFile=', 'check-ignore', '--no-index', '-q', '--', gitPath]).status === 0;
|
|
273
|
+
const protectedDir = entry.name === '.git' || artifactPath(local, artifacts, true)
|
|
274
|
+
|| ignoredDownload || (client || local.startsWith('.pbai4devs/')) && isGeneratedOutput(local, true);
|
|
275
|
+
if (!protectedDir) visit(path);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
visit(root);
|
|
280
|
+
let ancestor = dirname(root);
|
|
281
|
+
while (root !== location.top && (ancestor === location.top || !relative(location.top, ancestor).startsWith(`..${sep}`))) {
|
|
282
|
+
files.push(join(ancestor, '.gitignore'));
|
|
283
|
+
if (ancestor === location.top) break;
|
|
284
|
+
ancestor = dirname(ancestor);
|
|
285
|
+
}
|
|
286
|
+
const rootIgnore = join(root, '.gitignore');
|
|
287
|
+
const conflicts = [];
|
|
288
|
+
for (const file of files) {
|
|
289
|
+
const item = localStat(file);
|
|
290
|
+
if (!item?.isFile() || item.isSymbolicLink()) continue;
|
|
291
|
+
const lines = readFileSync(file, 'utf8').replace(/^\uFEFF/, '').split(/\r?\n/);
|
|
292
|
+
for (const line of lines) {
|
|
293
|
+
if (!line.startsWith('!')) continue;
|
|
294
|
+
const rule = line.slice(1).replace(/^\/?(?:\*\*\/)?/, '');
|
|
295
|
+
if (/^\.env\.(?:example|sample|template|\*\.example)$/.test(rule)) continue;
|
|
296
|
+
const wildcard = /(^|[^\\])[*?\[]/.test(line);
|
|
297
|
+
const scope = portable(relative(dirname(file), root));
|
|
298
|
+
if (scope && !scope.startsWith('..') && !/^!\/?\*\*\//.test(line) && rule.includes('/')) {
|
|
299
|
+
const parts = rule.split('/'), project = scope.split('/');
|
|
300
|
+
if (project.some((part, index) => parts[index] && !/[*?\[]/.test(parts[index]) && parts[index] !== part && parts.slice(0, index).every(value => value && !/[*?\[]/.test(value)))) continue;
|
|
301
|
+
}
|
|
302
|
+
if (!wildcard) {
|
|
303
|
+
if (file !== rootIgnore && isGeneratedOutput(portable(relative(root, join(dirname(file), rule))), rule.endsWith('/'))) conflicts.push(portable(relative(root, file)) + ': ' + line);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
conflicts.push(portable(relative(root, file)) + ': ' + line);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return conflicts;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function foreignHiddenClients(plan) {
|
|
313
|
+
if (!plan.location) return [];
|
|
314
|
+
const content = localRead(plan.location.exclude) || '', ranges = [];
|
|
315
|
+
let start = null;
|
|
316
|
+
content.split(/\r?\n/).forEach((line, index) => {
|
|
317
|
+
const begin = line.match(/^# BEGIN AGENTDEVKIT LOCAL ([a-f0-9]{16})$/);
|
|
318
|
+
const end = line.match(/^# END AGENTDEVKIT LOCAL ([a-f0-9]{16})$/);
|
|
319
|
+
if (begin) start = { id: begin[1], line: index + 1 };
|
|
320
|
+
else if (end && start?.id === end[1]) { ranges.push({ ...start, end: index + 1 }); start = null; }
|
|
321
|
+
});
|
|
322
|
+
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`);
|
|
323
|
+
if (!probes.length) return [];
|
|
324
|
+
const { paths, matches } = ignoreMatches(plan.location, probes);
|
|
325
|
+
return paths.flatMap((path, index) => {
|
|
326
|
+
const match = matches.get(path);
|
|
327
|
+
if (!match || match.pattern.startsWith('!') || resolve(plan.location.top, match.source) !== plan.location.exclude) return [];
|
|
328
|
+
return ranges.some(range => `# BEGIN AGENTDEVKIT LOCAL ${range.id}` !== plan.first && match.line >= range.line && match.line <= range.end) ? [probes[index]] : [];
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
231
332
|
function localIgnoreSource(location, source) {
|
|
232
333
|
const path = resolve(location.top, source);
|
|
233
334
|
if (path === location.exclude) return true;
|
|
@@ -236,6 +337,8 @@ function localIgnoreSource(location, source) {
|
|
|
236
337
|
}
|
|
237
338
|
|
|
238
339
|
function ignoreMatches(location, probes) {
|
|
340
|
+
const root = join(location.top, location.prefix);
|
|
341
|
+
if (probes.some(path => !safeIgnoreProbe(root, path))) throw new Error('Probe atravessa caminho simbolico.');
|
|
239
342
|
const paths = probes.map(path => location.prefix ? `${location.prefix}/${path}` : path);
|
|
240
343
|
const result = gitCommand(location.top, ['-c', 'core.excludesFile=', 'check-ignore', '--verbose', '--no-index', '--stdin', '-z'], paths.join('\0') + '\0');
|
|
241
344
|
if (result.error || ![0, 1].includes(result.status)) throw new Error(result.error?.message || result.stderr || 'Falha ao conferir exclusoes Git.');
|
|
@@ -295,27 +398,29 @@ export function planLocalIgnore(root, { outputs = [], templateRoot } = {}) {
|
|
|
295
398
|
if (line.startsWith('!/')) return `!/${escapePattern(location.prefix)}${line.slice(1)}`;
|
|
296
399
|
return line.startsWith('/') ? `/${escapePattern(location.prefix)}${line}` : line;
|
|
297
400
|
}).join('\n');
|
|
298
|
-
const target = location?.exclude
|
|
299
|
-
const before = localRead(target), after = localBlock(before, first, last, body);
|
|
401
|
+
const target = location?.exclude;
|
|
402
|
+
const before = target ? localRead(target) : null, after = target ? localBlock(before, first, last, body) : null;
|
|
300
403
|
const edits = before === after ? [] : [{ path: target, before, after, beforeMode: localMode(target) }];
|
|
301
404
|
const probes = [...paths, ...rootProbes(root, roots)];
|
|
302
|
-
const
|
|
405
|
+
const existingPrivate = privateClients.probes.filter(path => localStat(join(root, path.split('/')[0]))?.isDirectory() && safeIgnoreProbe(root, path));
|
|
406
|
+
const artifactPaths = [...new Set([...artifactProbes(root, artifacts), ...existingPrivate, ...(location ? [...visibleArtifactProbes(root, location, artifacts), ...visiblePrivateProbes(root, location, clientRoots)] : [])])];
|
|
303
407
|
const wanted = new Set(probes.map(path => location?.prefix ? `${location.prefix}/${path}` : path));
|
|
304
|
-
if (location && !before?.includes(first)
|
|
305
|
-
const ignored = ignoreMatches(location, [...probes, ...artifactPaths]);
|
|
408
|
+
if (location && !before?.includes(first)) {
|
|
409
|
+
const ignored = ignoreMatches(location, [...probes, ...artifactPaths].filter(path => safeIgnoreProbe(root, path)));
|
|
306
410
|
if (ignored.paths.every(path => { const match = ignored.matches.get(path); return match && !match.pattern.startsWith('!') && localIgnoreSource(location, match.source); })) edits.length = 0;
|
|
307
411
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
return { root, status: location ? 'git' : '
|
|
412
|
+
const prefix = location?.prefix ? `${location.prefix}/` : '';
|
|
413
|
+
const tracked = location ? gitCommand(location.top, ['ls-files', '-z']).stdout.split('\0').filter(path => {
|
|
414
|
+
if (!path.startsWith(prefix)) return false;
|
|
415
|
+
const local = path.slice(prefix.length);
|
|
416
|
+
return wanted.has(path) || artifactPath(local, artifacts) || clientRoots.includes(local.split('/')[0]) && isGeneratedOutput(local);
|
|
417
|
+
}).map(path => path.slice(prefix.length)) : [];
|
|
418
|
+
return { root, status: location ? 'git' : 'no-git', location, paths, roots, clientRoots, artifactPatterns: artifacts, artifactProbes: artifactPaths, privateProbes: privateClients.probes, edits, trackedOutputs: tracked, first, last };
|
|
315
419
|
}
|
|
316
420
|
|
|
317
421
|
export function checkLocalIgnore(plan, { replacedBlocks = [] } = {}) {
|
|
318
422
|
const missing = [];
|
|
423
|
+
if (plan.location) missing.push(...ambiguousExceptions(plan.root, plan.location, plan.clientRoots || [], plan.artifactPatterns || []), ...foreignHiddenClients(plan));
|
|
319
424
|
const replacements = replacedBlocks.map(item => {
|
|
320
425
|
const path = canonicalExisting(checkedLocalPath(resolve(item.path))), inside = relative(localRoot(plan.root).root, path);
|
|
321
426
|
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.');
|
|
@@ -328,8 +433,11 @@ export function checkLocalIgnore(plan, { replacedBlocks = [] } = {}) {
|
|
|
328
433
|
return { ...item, path };
|
|
329
434
|
});
|
|
330
435
|
if (plan.location && (plan.paths.length || plan.artifactProbes?.length)) {
|
|
331
|
-
|
|
332
|
-
|
|
436
|
+
missing.push(...plan.paths.filter(path => !safeIgnoreProbe(plan.root, path)));
|
|
437
|
+
const privateProbes = (plan.privateProbes || []).filter(path => localStat(join(plan.root, path.split('/')[0]))?.isDirectory() && safeIgnoreProbe(plan.root, path));
|
|
438
|
+
const probes = [...new Set([...plan.paths, ...rootProbes(plan.root, plan.roots || []), ...(plan.artifactProbes || []), ...privateProbes,
|
|
439
|
+
...(plan.artifactPatterns ? visibleArtifactProbes(plan.root, plan.location, plan.artifactPatterns) : []),
|
|
440
|
+
...visiblePrivateProbes(plan.root, plan.location, plan.clientRoots || [])])].filter(path => safeIgnoreProbe(plan.root, path));
|
|
333
441
|
const { paths: relativePaths, matches } = ignoreMatches(plan.location, probes);
|
|
334
442
|
for (let index = 0; index < relativePaths.length; index++) {
|
|
335
443
|
const match = matches.get(relativePaths[index]);
|
|
@@ -340,7 +448,7 @@ export function checkLocalIgnore(plan, { replacedBlocks = [] } = {}) {
|
|
|
340
448
|
if (!replaced && ((!plan.edits.length && (!match || negative || !local)) || humanOverride)) missing.push(probes[index]);
|
|
341
449
|
}
|
|
342
450
|
}
|
|
343
|
-
return { ok: plan.edits.length === 0 && missing.length === 0 && plan.trackedOutputs.length === 0, status: plan.status, missing, trackedOutputs: plan.trackedOutputs, changed: plan.edits.length };
|
|
451
|
+
return { ok: plan.status === 'git' && plan.edits.length === 0 && missing.length === 0 && plan.trackedOutputs.length === 0, status: plan.status, missing, trackedOutputs: plan.trackedOutputs, changed: plan.edits.length };
|
|
344
452
|
}
|
|
345
453
|
|
|
346
454
|
export function assertLocalIgnoreUnchanged(plan) {
|
|
@@ -37,7 +37,7 @@ a seleção, use `agentdevkit agents <lista>`; não edite o manifesto gerado.
|
|
|
37
37
|
|
|
38
38
|
Os derivados ficam locais: catálogos, manifesto, estado e apontamentos são
|
|
39
39
|
excluídos pelo Git. O bloco em `.git/info/exclude` preserva regras próprias;
|
|
40
|
-
sem Git,
|
|
40
|
+
sem Git, informa que a proteção fica pendente até `git init` e sincronização.
|
|
41
41
|
Somente arquivos integralmente gerados são excluídos; pastas de clientes podem
|
|
42
42
|
conter arquivos humanos versionados. Conteúdo próprio não é desindexado pela
|
|
43
43
|
presença de um bloco gerenciado.
|
|
@@ -577,9 +577,10 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
577
577
|
const touched = new Set(), createdDirectories = new Set(), restoredDirectories = new Set();
|
|
578
578
|
const prunedDirectories = new Map();
|
|
579
579
|
let restoring = false;
|
|
580
|
-
const summary = { changed: changed.length + rootEntryChanges.length, stale: removals.length, conflicts: conflicts.size, missing: missing.length, legacy: legacy.length, registries: registries.size, gitignores: ignoreFiles.size, manifests: configExists ? 0 : 1, localIgnores: localStatus.changed + localStatus.missing.length + localStatus.trackedOutputs.length };
|
|
580
|
+
const summary = { changed: changed.length + rootEntryChanges.length, stale: removals.length, conflicts: conflicts.size, missing: missing.length, legacy: legacy.length, registries: registries.size, gitignores: ignoreFiles.size, manifests: configExists ? 0 : 1, localIgnores: localStatus.changed + localStatus.missing.length + localStatus.trackedOutputs.length + (localStatus.status === 'no-git' ? 1 : 0) };
|
|
581
581
|
const dirty = Object.values(summary).some(Boolean);
|
|
582
582
|
output(`${summary.changed} desatualizadas; ${summary.stale} obsoletas; ${summary.conflicts} conflitos; ${summary.missing} origens nativas ausentes; ${summary.legacy} documentos antigos; ${summary.registries} indices derivados; ${summary.gitignores} gitignores; ${summary.localIgnores} exclusoes locais.`);
|
|
583
|
+
if (localStatus.status === 'no-git') output('Git ausente: apos git init, execute a sincronizacao para proteger os arquivos locais.');
|
|
583
584
|
const preservedPaths = sorted(preserved.keys()).map(path => preserved.get(path));
|
|
584
585
|
if (check) return { ...summary, ownedOutputs, managedBlocks, preserved: preservedPaths, exitCode: dirty ? 1 : 0 };
|
|
585
586
|
if (conflicts.size) fail(`Use --force para reconstruir somente os caminhos gerenciados: ${sorted(conflicts).join(', ')}`);
|
|
@@ -23,14 +23,14 @@ function git(root, args, input) {
|
|
|
23
23
|
assert.ifError(result.error); assert.equal(result.status, 0, result.stderr); return result.stdout;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
test('
|
|
26
|
+
test('sem Git preserva ignore e informa pendência; após git init protege outputs', t => {
|
|
27
27
|
const root = fixture(t), outputs = ['.github/skills/generated/SKILL.md', '.vscode/.gitignore', 'AGENTS.md'];
|
|
28
28
|
put(root, '.gitignore', '# Regras humanas\n*.own-cache\n');
|
|
29
29
|
const plan = planLocalIgnore(root, { outputs });
|
|
30
|
-
assert.equal(plan.status, '
|
|
30
|
+
assert.equal(plan.status, 'no-git'); assert.equal(checkLocalIgnore(plan).ok, false);
|
|
31
31
|
applyLocalIgnore(plan);
|
|
32
|
-
assert.equal(checkLocalIgnore(planLocalIgnore(root, { outputs })).ok,
|
|
33
|
-
assert.
|
|
32
|
+
assert.equal(checkLocalIgnore(planLocalIgnore(root, { outputs })).ok, false);
|
|
33
|
+
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), '# Regras humanas\n*.own-cache\n');
|
|
34
34
|
git(root, ['init', '-q']);
|
|
35
35
|
put(root, '.git/info/exclude', '# Exclusoes humanas\n*.local-backup\n');
|
|
36
36
|
applyLocalIgnore(planLocalIgnore(root, { outputs }));
|
|
@@ -68,12 +68,86 @@ test('check identifica negação humana e outputs rastreados sem alterar o índi
|
|
|
68
68
|
assert.equal(git(root, ['ls-files', 'generated.md']).trim(), 'generated.md');
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
test('
|
|
71
|
+
test('exceções de templates preservam o ignore raiz e protegem dados privados', t => {
|
|
72
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
73
|
+
git(root, ['config', 'core.ignorecase', 'false']);
|
|
74
|
+
const original = '# Regras do projeto\n.env.*\n!.env.example\n!.env.*.example\n';
|
|
75
|
+
put(root, '.gitignore', original); git(root, ['add', '.gitignore']);
|
|
76
|
+
const index = git(root, ['ls-files', '--stage', '-z']);
|
|
77
|
+
const plan = planLocalIgnore(root);
|
|
78
|
+
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), original);
|
|
79
|
+
assert.equal(existsSync(join(root, '.cursor')), false);
|
|
80
|
+
applyLocalIgnore(plan);
|
|
81
|
+
const updated = readFileSync(join(root, '.gitignore'), 'utf8');
|
|
82
|
+
assert.equal(updated, original);
|
|
83
|
+
assert.equal(git(root, ['ls-files', '--stage', '-z']), index);
|
|
84
|
+
assert.equal(existsSync(join(root, '.cursor')), false);
|
|
85
|
+
assert.equal(checkLocalIgnore(planLocalIgnore(root)).ok, true);
|
|
86
|
+
put(root, '.cursor/.CACHE/private.json', 'local');
|
|
87
|
+
put(root, '.cursor/.cache/private.json', 'local');
|
|
88
|
+
put(root, '.cursor/human.md', 'human');
|
|
89
|
+
assert.ok(git(root, ['check-ignore', '--no-index', '.cursor/.CACHE/private.json']).includes('.cursor/.CACHE/private.json'));
|
|
90
|
+
assert.ok(git(root, ['check-ignore', '--no-index', '.cursor/.cache/private.json']).includes('.cursor/.cache/private.json'));
|
|
91
|
+
const visible = spawnSync('git', ['-C', root, 'check-ignore', '--no-index', '-q', '.cursor/human.md']);
|
|
92
|
+
assert.equal(visible.status, 1);
|
|
93
|
+
applyLocalIgnore(planLocalIgnore(root));
|
|
94
|
+
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), updated);
|
|
95
|
+
put(root, '.gitignore', updated + '# edição humana posterior\n');
|
|
96
|
+
assert.equal(checkLocalIgnore(planLocalIgnore(root)).ok, true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('bloco legado na raiz pertence ao projeto e permanece intacto', t => {
|
|
100
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
101
|
+
const original = '# BEGIN AGENTDEVKIT PRIVATE legado\n# edição humana\n# END AGENTDEVKIT PRIVATE legado\n';
|
|
102
|
+
put(root, '.gitignore', original);
|
|
103
|
+
applyLocalIgnore(planLocalIgnore(root));
|
|
104
|
+
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), original);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('negação com caixa mista não expõe cache em Git sensível a maiúsculas', t => {
|
|
108
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
109
|
+
git(root, ['config', 'core.ignorecase', 'false']);
|
|
110
|
+
put(root, '.gitignore', '!*.CaChE/\n');
|
|
111
|
+
put(root, '.cursor/.CaChE/private.json', 'local');
|
|
112
|
+
assert.throws(() => applyLocalIgnore(planLocalIgnore(root)), /Regras humanas/);
|
|
113
|
+
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), '!*.CaChE/\n');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('negações literais e com caixa mista não expõem estado privado', t => {
|
|
117
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
118
|
+
git(root, ['config', 'core.ignorecase', 'false']);
|
|
119
|
+
put(root, '.gitignore', '!*.LoCaL/\n!/.cursor/.env.production\n');
|
|
120
|
+
for (const path of ['.cursor/.LoCaL/private.json', '.cursor/.env.production']) put(root, path, 'privado');
|
|
121
|
+
assert.throws(() => applyLocalIgnore(planLocalIgnore(root)), /Regras humanas/);
|
|
122
|
+
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), '!*.LoCaL/\n!/.cursor/.env.production\n');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('negação na primeira linha com BOM preserva bytes e protege segredos', t => {
|
|
126
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
127
|
+
const original = '\uFEFF!/.cursor/.env.production\r\n';
|
|
128
|
+
put(root, '.gitignore', original); put(root, '.cursor/.env.production', 'privado');
|
|
129
|
+
assert.throws(() => applyLocalIgnore(planLocalIgnore(root)), /Regras humanas/);
|
|
130
|
+
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), original);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('negação humana posterior é informada sem reescrever a raiz', t => {
|
|
134
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
135
|
+
put(root, '.gitignore', '# humano\n'); applyLocalIgnore(planLocalIgnore(root));
|
|
136
|
+
const path = join(root, '.gitignore');
|
|
137
|
+
writeFileSync(path, readFileSync(path, 'utf8') + '!/.cursor/.env.production\n');
|
|
138
|
+
put(root, '.cursor/.env.production', 'privado');
|
|
139
|
+
const plan = planLocalIgnore(root);
|
|
140
|
+
assert.ok(checkLocalIgnore(plan).missing.includes('.cursor/.env.production'));
|
|
141
|
+
assert.throws(() => applyLocalIgnore(plan), /Regras humanas/);
|
|
142
|
+
assert.equal(readFileSync(path, 'utf8'), '# humano\n!/.cursor/.env.production\n');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('planejamento sem Git não modifica ignore nem edição humana posterior', t => {
|
|
72
146
|
const root = fixture(t); put(root, '.gitignore', 'antes\r\n');
|
|
73
147
|
const plan = planLocalIgnore(root, { outputs: ['generated.md'] });
|
|
74
148
|
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), 'antes\r\n');
|
|
75
149
|
put(root, '.gitignore', 'depois\r\n');
|
|
76
|
-
|
|
150
|
+
applyLocalIgnore(plan);
|
|
77
151
|
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), 'depois\r\n');
|
|
78
152
|
for (const path of ['../outside', '/absolute', 'line\nname', 'a\\b']) assert.throws(() => planLocalIgnore(root, { outputs: [path] }), /invalida/);
|
|
79
153
|
});
|
|
@@ -99,7 +173,7 @@ test('plano de instalação em alvo inexistente não cria arquivos', t => {
|
|
|
99
173
|
const root = fixture(t);
|
|
100
174
|
const target = join(root, 'new project', 'nested');
|
|
101
175
|
const fallback = planLocalIgnore(target, { outputs: ['AGENTS.md'] });
|
|
102
|
-
assert.equal(fallback.status, '
|
|
176
|
+
assert.equal(fallback.status, 'no-git'); assert.equal(existsSync(target), false);
|
|
103
177
|
git(root, ['init', '-q']);
|
|
104
178
|
const plan = planLocalIgnore(target, { outputs: ['AGENTS.md'] });
|
|
105
179
|
assert.equal(plan.status, 'git'); assert.equal(existsSync(target), false);
|
|
@@ -131,21 +205,21 @@ test('configurações humanas dos clientes continuam versionáveis', t => {
|
|
|
131
205
|
assert.ok(tracked.includes('.github/settings.json')); assert.ok(tracked.includes('.vscode/settings.json'));
|
|
132
206
|
});
|
|
133
207
|
|
|
134
|
-
test('artefatos vêm da política compartilhada e
|
|
208
|
+
test('artefatos vêm da política compartilhada e rastreados são informados', t => {
|
|
135
209
|
const root = fixture(t); git(root, ['init', '-q']);
|
|
136
210
|
put(root, '.agent-tests/human.txt', 'evidência humana'); git(root, ['add', '-f', '.agent-tests/human.txt']);
|
|
137
211
|
const plan = planLocalIgnore(root, { outputs: ['AGENTS.md'] });
|
|
138
212
|
assert.ok(plan.artifactProbes.some(path => path.startsWith('.agent-tests/')));
|
|
139
213
|
assert.ok(plan.artifactProbes.some(path => path.startsWith('.agentdevkit-ignore-probes/')));
|
|
140
214
|
assert.ok(plan.paths.every(path => !plan.artifactProbes.includes(path)));
|
|
141
|
-
assert.ok(
|
|
215
|
+
assert.ok(plan.trackedOutputs.includes('.agent-tests/human.txt'));
|
|
142
216
|
applyLocalIgnore(plan);
|
|
143
217
|
for (const path of ['.agent-tests/x', 'TMP/x', 'nested/TeMp/x', '.PlAyWrIgHt-McP/x', 'nested/playwright-report/x', 'TEST-RESULTS/x', 'nested/TestResults/x', 'BLOB-REPORT/x']) {
|
|
144
218
|
assert.equal(git(root, ['check-ignore', '--no-index', path]).trim(), path);
|
|
145
219
|
}
|
|
146
220
|
assert.equal(git(root, ['ls-files', '.agent-tests/human.txt']).trim(), '.agent-tests/human.txt');
|
|
147
221
|
assert.equal(readFileSync(join(root, '.agent-tests/human.txt'), 'utf8'), 'evidência humana');
|
|
148
|
-
assert.equal(checkLocalIgnore(planLocalIgnore(root, { outputs: ['AGENTS.md'] })).ok,
|
|
222
|
+
assert.equal(checkLocalIgnore(planLocalIgnore(root, { outputs: ['AGENTS.md'] })).ok, false);
|
|
149
223
|
});
|
|
150
224
|
|
|
151
225
|
test('regras de artefatos são limitadas ao projeto dentro do monorepo', t => {
|
|
@@ -250,7 +324,7 @@ test('negação humana é detectada antes de aplicar novas exclusões e permanec
|
|
|
250
324
|
put(root, '.gitignore', '!/AGENTS.md\n');
|
|
251
325
|
const before = readFileSync(join(root, '.git/info/exclude'), 'utf8');
|
|
252
326
|
const plan = planLocalIgnore(root, { outputs: ['AGENTS.md'] });
|
|
253
|
-
assert.
|
|
327
|
+
assert.ok(plan.edits.some(edit => edit.path === join(root, '.git/info/exclude')));
|
|
254
328
|
assert.deepEqual(checkLocalIgnore(plan).missing, ['AGENTS.md']);
|
|
255
329
|
assert.throws(() => applyLocalIgnore(plan), /Regras humanas/);
|
|
256
330
|
assert.equal(readFileSync(join(root, '.git/info/exclude'), 'utf8'), before);
|
|
@@ -381,6 +455,63 @@ test('arquivo real reaberto por glob é detectado também se surgir após o plan
|
|
|
381
455
|
assert.equal(readFileSync(join(root, path), 'utf8'), 'artefato local\n');
|
|
382
456
|
});
|
|
383
457
|
|
|
458
|
+
test('negações globais e aninhadas não expõem futuros arquivos privados', t => {
|
|
459
|
+
for (const [file, rule] of [['.gitignore', '!/.claude/s*.json'], ['.claude/.gitignore', '!s*.json'], ['.claude/.gitignore', '!/settings.local.json'], ['.pbai4devs/.gitignore', '!.playwright-*/'], ['.pbai4devs/module/.gitignore', '!.playwright-*/'], ['logs/.gitignore', '!.playwright-*/'], ['.gitignore', '!**/foo/s*.json'], ['.gitignore', '!**/foo/.playwright-*.png'], ['.gitignore', '!/docs/.playwright-*.png'], ['docs/.gitignore', '!.playwright-*/'], ['.gitignore', '!*.CaChE/'], ['.gitignore', '!/.playwright-*.png']]) {
|
|
460
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
461
|
+
put(root, file, `\uFEFF${rule}\n`);
|
|
462
|
+
const before = readFileSync(join(root, '.git/info/exclude'));
|
|
463
|
+
const index = git(root, ['ls-files', '--stage', '-z']);
|
|
464
|
+
const plan = planLocalIgnore(root);
|
|
465
|
+
assert.ok(checkLocalIgnore(plan).missing.some(value => value.includes(rule)), rule);
|
|
466
|
+
assert.throws(() => applyLocalIgnore(plan), /Regras humanas/);
|
|
467
|
+
assert.deepEqual(readFileSync(join(root, '.git/info/exclude')), before);
|
|
468
|
+
assert.equal(git(root, ['ls-files', '--stage', '-z']), index);
|
|
469
|
+
assert.equal(readFileSync(join(root, file), 'utf8'), `\uFEFF${rule}\n`);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test('glob ancestral do subprojeto é detectado antes de alterar o exclude', t => {
|
|
474
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
475
|
+
const child = join(root, 'packages/app'); mkdirSync(child, { recursive: true });
|
|
476
|
+
put(root, '.gitignore', '!/packages/app/.claude/s*.json\n');
|
|
477
|
+
const before = readFileSync(join(root, '.git/info/exclude'));
|
|
478
|
+
const plan = planLocalIgnore(child);
|
|
479
|
+
assert.ok(checkLocalIgnore(plan).missing.some(value => value.includes('s*.json')));
|
|
480
|
+
assert.throws(() => applyLocalIgnore(plan), /Regras humanas/);
|
|
481
|
+
assert.deepEqual(readFileSync(join(root, '.git/info/exclude')), before);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test('dependências já ignoradas não causam conflito, mas ignores visíveis são verificados', t => {
|
|
485
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
486
|
+
put(root, '.gitignore', 'node_modules/\n');
|
|
487
|
+
put(root, 'node_modules/pacote/.gitignore', '!.playwright-*/\n');
|
|
488
|
+
assert.equal(checkLocalIgnore(planLocalIgnore(root)).missing.length, 0);
|
|
489
|
+
put(root, '.gitignore', '# dependências visíveis\n');
|
|
490
|
+
assert.ok(checkLocalIgnore(planLocalIgnore(root)).missing.some(value => value.includes('!.playwright-*/')));
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test('bloco antigo de outro caminho não oculta arquivos humanos silenciosamente', t => {
|
|
494
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
495
|
+
const old = '# BEGIN AGENTDEVKIT LOCAL 0123456789abcdef\n/.claude/\n# END AGENTDEVKIT LOCAL 0123456789abcdef\n';
|
|
496
|
+
put(root, '.git/info/exclude', old);
|
|
497
|
+
const plan = planLocalIgnore(root);
|
|
498
|
+
assert.ok(checkLocalIgnore(plan).missing.includes('.claude/.agentdevkit-human-probe.md'));
|
|
499
|
+
assert.throws(() => applyLocalIgnore(plan), /Regras humanas/);
|
|
500
|
+
assert.equal(readFileSync(join(root, '.git/info/exclude'), 'utf8'), old);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test('artefatos já versionados são informados sem alterar o índice', t => {
|
|
504
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
505
|
+
const artifacts = ['.playwright-mcp/trace.zip', 'playwright-report/index.html', 'test-results/result.txt', '.agent-tests/log.txt', '.claude/settings.local.json', '.codex/.env'];
|
|
506
|
+
for (const path of artifacts) put(root, path, 'trace');
|
|
507
|
+
git(root, ['add', '-f', ...artifacts]);
|
|
508
|
+
const before = git(root, ['ls-files', '--stage', '-z']);
|
|
509
|
+
const plan = planLocalIgnore(root);
|
|
510
|
+
assert.deepEqual([...plan.trackedOutputs].sort(), artifacts.sort());
|
|
511
|
+
assert.equal(checkLocalIgnore(plan).ok, false);
|
|
512
|
+
assert.equal(git(root, ['ls-files', '--stage', '-z']), before);
|
|
513
|
+
});
|
|
514
|
+
|
|
384
515
|
test('regras customizadas aninhadas continuam válidas e incluem pais ocultos', t => {
|
|
385
516
|
const root = fixture(t); git(root, ['init', '-q']);
|
|
386
517
|
const policy = readFileSync(join(projectRoot, '.pbai4devs/templates/gitignore/agents.ignore'), 'utf8');
|
|
@@ -399,7 +530,7 @@ test('chaves literais em padrões Git não viram expansão de glob', t => {
|
|
|
399
530
|
const policy = readFileSync(join(projectRoot, '.pbai4devs/templates/gitignore/agents.ignore'), 'utf8');
|
|
400
531
|
put(root, '.pbai4devs/templates/gitignore/agents.ignore', `${policy}\n**/cache{keep,archive}/\n`);
|
|
401
532
|
put(root, 'cachekeep/source.txt', 'código humano\n');
|
|
402
|
-
put(root, '.gitignore', '!cachekeep
|
|
533
|
+
put(root, '.gitignore', '!cachekeep/source.txt\n');
|
|
403
534
|
assert.doesNotThrow(() => applyLocalIgnore(planLocalIgnore(root)));
|
|
404
535
|
assert.equal(readFileSync(join(root, 'cachekeep/source.txt'), 'utf8'), 'código humano\n');
|
|
405
536
|
});
|
|
@@ -424,32 +555,28 @@ test('escrita de exclusões preserva permissões e detecta alteração concorren
|
|
|
424
555
|
assert.equal(statSync(exclude).mode & 0o777, 0o640);
|
|
425
556
|
});
|
|
426
557
|
|
|
427
|
-
test('
|
|
558
|
+
test('ignore raiz mantém modo e bytes antes e depois de git init', { skip: process.platform === 'win32' }, t => {
|
|
428
559
|
const root = fixture(t), ignore = join(root, '.gitignore');
|
|
429
560
|
put(root, '.gitignore', '# humano\n'); chmodSync(ignore, 0o600);
|
|
430
561
|
applyLocalIgnore(planLocalIgnore(root));
|
|
431
562
|
assert.equal(statSync(ignore).mode & 0o777, 0o600);
|
|
432
563
|
const original = readFileSync(ignore); git(root, ['init', '-q']);
|
|
433
|
-
const exclude = join(root, '.git/info/exclude')
|
|
434
|
-
|
|
435
|
-
|
|
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);
|
|
564
|
+
const exclude = join(root, '.git/info/exclude'); chmodSync(exclude, 0o600);
|
|
565
|
+
applyLocalIgnore(planLocalIgnore(root));
|
|
566
|
+
assert.equal(statSync(exclude).mode & 0o777, 0o600);
|
|
439
567
|
assert.deepEqual(readFileSync(ignore), original); assert.equal(statSync(ignore).mode & 0o777, 0o600);
|
|
440
568
|
});
|
|
441
569
|
|
|
442
|
-
test('
|
|
570
|
+
test('ignore raiz somente leitura não impede exclusão local no Git', { skip: process.platform === 'win32' }, t => {
|
|
443
571
|
const root = fixture(t), ignore = join(root, '.gitignore');
|
|
444
572
|
put(root, '.gitignore', '# humano\n'); chmodSync(ignore, 0o444);
|
|
445
573
|
applyLocalIgnore(planLocalIgnore(root));
|
|
446
574
|
const original = readFileSync(ignore); git(root, ['init', '-q']);
|
|
447
|
-
const exclude = join(root, '.git/info/exclude')
|
|
575
|
+
const exclude = join(root, '.git/info/exclude');
|
|
448
576
|
chmodSync(exclude, 0o444);
|
|
449
577
|
const plan = planLocalIgnore(root);
|
|
450
|
-
|
|
451
|
-
assert.
|
|
452
|
-
assert.deepEqual(readFileSync(exclude), before); assert.equal(statSync(exclude).mode & 0o777, 0o444);
|
|
578
|
+
applyLocalIgnore(plan);
|
|
579
|
+
assert.equal(statSync(exclude).mode & 0o777, 0o444);
|
|
453
580
|
assert.deepEqual(readFileSync(ignore), original); assert.equal(statSync(ignore).mode & 0o777, 0o444);
|
|
454
581
|
});
|
|
455
582
|
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: Exemplo opcional de regras de ignore para o projeto.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# Custom ignore
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
O `.gitignore` da raiz pertence ao projeto; o kit não o cria nem o altera.
|
|
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
10
|
[agents.ignore](agents.ignore). As saídas calculadas usam
|
|
11
|
-
[local.ignore](local.ignore) em `.git/info/exclude`; sem Git,
|
|
12
|
-
|
|
11
|
+
[local.ignore](local.ignore) em `.git/info/exclude`; sem Git, essa proteção
|
|
12
|
+
fica pendente até `git init` e sincronização, antes de adicionar arquivos.
|
|
13
13
|
O sincronizador mantém o bloco `BEGIN AGENT IGNORE` / `END AGENT IGNORE`;
|
|
14
14
|
regras próprias ficam fora dele, com exceções depois do bloco. Para permitir
|
|
15
15
|
novos arquivos compartilhados, ajuste as fontes e sincronize. Raízes dedicadas
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ description: Alterações verificáveis por versão do AgentDevKit.
|
|
|
4
4
|
|
|
5
5
|
# Changelog
|
|
6
6
|
|
|
7
|
+
## 0.0.14 — 2026-11-12
|
|
8
|
+
|
|
9
|
+
- O `.gitignore` da raiz pertence ao projeto e não é alterado pelo kit.
|
|
10
|
+
- Exclusões locais ficam no Git e conflitos são informados antes da instalação.
|
|
11
|
+
- Projetos sem Git recebem orientação para ativar a proteção após `git init`.
|
|
12
|
+
- Negação ambígua, blocos antigos e artefatos rastreados recebem diagnóstico.
|
|
13
|
+
|
|
14
|
+
## 0.0.13 — 2026-11-12
|
|
15
|
+
|
|
16
|
+
- Proteção de caches de agentes mesmo com negações genéricas no `.gitignore`.
|
|
17
|
+
- Matriz de compatibilidade ampliada para os sete projetos públicos solicitados.
|
|
18
|
+
- Dois exemplos reproduzíveis com produto, design e três specs testadas.
|
|
19
|
+
|
|
7
20
|
## 0.0.12 — 2026-11-12
|
|
8
21
|
|
|
9
22
|
- Entradas e recursos existentes recebem referências em blocos próprios do kit.
|
package/README.en.md
CHANGED
|
@@ -43,7 +43,7 @@ Then open the project in your coding agent and ask:
|
|
|
43
43
|
|
|
44
44
|
See [installation](docs/guides/installation.md) for runtime preparation, previews
|
|
45
45
|
and updates. Both [package registries][packages] provide the same version;
|
|
46
|
-
the [
|
|
46
|
+
the [latest release][release] retains artifacts and checksums for verification.
|
|
47
47
|
No global installation is required.
|
|
48
48
|
The CLI supports `pt`, `en` and `es`; it saves your selection per project.
|
|
49
49
|
|
|
@@ -90,7 +90,7 @@ forms. See [how to participate](docs/maintainers/community.md).
|
|
|
90
90
|
agent to read `AGENTS.md`.
|
|
91
91
|
|
|
92
92
|
[clients]: .pbai4devs/COMPATIBILIDADE.md
|
|
93
|
-
[release]: https://github.com/pathbit/AgentDevKit/releases/
|
|
93
|
+
[release]: https://github.com/pathbit/AgentDevKit/releases/latest
|
|
94
94
|
[npm-package]: https://www.npmjs.com/package/@pathbit/agentdevkit
|
|
95
95
|
[npm-badge]: https://img.shields.io/npm/v/%40pathbit%2Fagentdevkit
|
|
96
96
|
[license-badge]: https://img.shields.io/badge/license-MIT-blue
|
package/README.es.md
CHANGED
|
@@ -44,7 +44,7 @@ Después, abre el proyecto en tu agente y pide:
|
|
|
44
44
|
La [guía de instalación](docs/guides/installation.md) incluye preparación del
|
|
45
45
|
runtime, simulación y actualización. Los dos [registros][packages] ofrecen
|
|
46
46
|
la misma versión;
|
|
47
|
-
la [release
|
|
47
|
+
la [última release][release] conserva artefactos y checksums para verificarlos.
|
|
48
48
|
No requiere instalación global.
|
|
49
49
|
La CLI guarda el idioma `pt`, `en` o `es` por proyecto.
|
|
50
50
|
|
|
@@ -92,7 +92,7 @@ formularios. Consulta [cómo participar](docs/maintainers/community.md).
|
|
|
92
92
|
al agente que lea `AGENTS.md`.
|
|
93
93
|
|
|
94
94
|
[clients]: .pbai4devs/COMPATIBILIDADE.md
|
|
95
|
-
[release]: https://github.com/pathbit/AgentDevKit/releases/
|
|
95
|
+
[release]: https://github.com/pathbit/AgentDevKit/releases/latest
|
|
96
96
|
[npm-package]: https://www.npmjs.com/package/@pathbit/agentdevkit
|
|
97
97
|
[npm-badge]: https://img.shields.io/npm/v/%40pathbit%2Fagentdevkit
|
|
98
98
|
[license-badge]: https://img.shields.io/badge/license-MIT-blue
|
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ Depois, abra o projeto no seu agente e peça:
|
|
|
46
46
|
|
|
47
47
|
Windows, instalação sem Node, simulação e atualização estão no
|
|
48
48
|
[guia de instalação](https://github.com/pathbit/AgentDevKit/blob/master/docs/guides/installation.md). A distribuição usa a
|
|
49
|
-
versão publicada no [npm e GitHub Packages][packages] e na [release
|
|
49
|
+
versão publicada no [npm e GitHub Packages][packages] e na [release][release],
|
|
50
50
|
sem instalação global. Instalação e atualização usam o pacote do registro;
|
|
51
51
|
os arquivos da release servem para conferir os artefatos publicados.
|
|
52
52
|
O idioma da CLI pode ser `pt`, `en` ou `es`.
|
|
@@ -60,6 +60,7 @@ O idioma da CLI pode ser `pt`, `en` ou `es`.
|
|
|
60
60
|
| Planejar ou retomar um projeto | [Fluxo de trabalho](https://github.com/pathbit/AgentDevKit/blob/master/docs/guides/workflow.md) |
|
|
61
61
|
| Evoluir documentos existentes | [Inventário e complementos](https://github.com/pathbit/AgentDevKit/blob/master/docs/guides/documentation.md) |
|
|
62
62
|
| Criar uma skill ou comando | [Recursos](https://github.com/pathbit/AgentDevKit/blob/master/docs/guides/resources.md) |
|
|
63
|
+
| Entender agentes, skills e comandos | [Recursos na prática](https://github.com/pathbit/AgentDevKit/blob/master/docs/guides/recursos-na-pratica.md) |
|
|
63
64
|
| Instalar ferramentas e comprovar uma entrega | [Ferramentas e testes][tests] |
|
|
64
65
|
| Revisar segurança sem serviços externos | [Segurança local](https://github.com/pathbit/AgentDevKit/blob/master/docs/guides/security.md) |
|
|
65
66
|
| Consultar opções ou resolver um erro | [CLI][cli] · [Soluções][help] |
|
|
@@ -106,7 +107,7 @@ ao seu agente para ler `AGENTS.md`.
|
|
|
106
107
|
[help]: https://github.com/pathbit/AgentDevKit/blob/master/docs/guides/troubleshooting.md
|
|
107
108
|
[issues]: https://github.com/pathbit/AgentDevKit/issues/new/choose
|
|
108
109
|
[packages]: https://github.com/pathbit/AgentDevKit/blob/master/docs/maintainers/packages.md
|
|
109
|
-
[release]: https://github.com/pathbit/AgentDevKit/releases/
|
|
110
|
+
[release]: https://github.com/pathbit/AgentDevKit/releases/latest
|
|
110
111
|
[npm-package]: https://www.npmjs.com/package/@pathbit/agentdevkit
|
|
111
112
|
[npm-badge]: https://img.shields.io/npm/v/%40pathbit%2Fagentdevkit
|
|
112
113
|
[license-badge]: https://img.shields.io/badge/license-MIT-blue
|
package/docs/README.md
CHANGED
|
@@ -27,6 +27,7 @@ pode trocar de agente sem manter várias cópias das mesmas instruções.
|
|
|
27
27
|
| Projeto em andamento | [Instalar com simulação][install] → [retomar][flow] |
|
|
28
28
|
| Evoluir documentação existente | [Inventariar e complementar](guides/documentation.md) |
|
|
29
29
|
| Personalizar agentes | [Recursos][resources] → [compatibilidade][clients] |
|
|
30
|
+
| Entender cada peça com um exemplo | [Recursos na prática](guides/recursos-na-pratica.md) |
|
|
30
31
|
| Revisar segurança | [Scanners locais e controles](guides/security.md) |
|
|
31
32
|
| Executar ou verificar algo | [CLI](guides/cli.md) → [ferramentas e testes][tests] |
|
|
32
33
|
| Corrigir uma dificuldade | [Soluções e recuperação](guides/troubleshooting.md) |
|
package/docs/guides/README.md
CHANGED
|
@@ -10,6 +10,7 @@ description: Guias de instalação, uso e verificação do AgentDevKit.
|
|
|
10
10
|
- [Fluxo de trabalho](workflow.md)
|
|
11
11
|
- [Evoluir documentação existente](documentation.md)
|
|
12
12
|
- [Recursos dos agentes](resources.md)
|
|
13
|
+
- [Recursos na prática: do pedido à entrega](recursos-na-pratica.md)
|
|
13
14
|
- [Segurança local](security.md)
|
|
14
15
|
- [Ferramentas e testes](tools-and-testing.md)
|
|
15
16
|
- [Solução de problemas](troubleshooting.md)
|
|
@@ -105,18 +105,24 @@ identifica a licença dos recursos do AgentDevKit, sem redefinir a do projeto.
|
|
|
105
105
|
Um `.gitignore` dentro de `.pbai4devs` só controla essa árvore. Para os
|
|
106
106
|
apontamentos externos, o kit gera exclusões locais do Git, sem ignorar
|
|
107
107
|
workflows, formulários de issues ou configurações humanas em `.github`.
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
O kit não cria nem altera o `.gitignore` da raiz. Sem Git, a exclusão local
|
|
109
|
+
fica pendente; execute `git init` e `npx @pathbit/agentdevkit sync` antes de
|
|
110
|
+
usar `git add`. No GitHub Packages, use o prefixo de registry da instalação.
|
|
110
111
|
|
|
111
112
|
O bloco local também protege `.agent-tests`, `tmp`, `temp`, `.playwright*`
|
|
112
113
|
e relatórios de testes, inclusive com letras maiúsculas e em subpastas.
|
|
113
|
-
O `.gitignore` existente e
|
|
114
|
-
|
|
114
|
+
O conteúdo do `.gitignore` existente e os arquivos rastreados são preservados.
|
|
115
|
+
Uma exceção que reabra arquivo privado ou artefato é informada como conflito;
|
|
116
|
+
o projeto decide como ajustar suas próprias regras.
|
|
117
|
+
Negativas com curingas também podem ser recusadas preventivamente, mesmo antes
|
|
118
|
+
de existir um arquivo correspondente. Examine a regra indicada no diagnóstico.
|
|
115
119
|
Nomes próprios de artefatos e arquivos existentes também são conferidos;
|
|
116
120
|
a validação fica restrita ao projeto e aos ignores de seus ancestrais.
|
|
117
121
|
|
|
118
122
|
Arquivos já rastreados exigem migração do índice: apenas saídas identificadas
|
|
119
123
|
do kit podem deixar de ser rastreadas, preservando seus bytes no disco.
|
|
124
|
+
Artefatos já rastreados são informados por `doctor` e `sync --check` para
|
|
125
|
+
revisão; não são removidos automaticamente do índice.
|
|
120
126
|
Revise o plano e os conflitos em um projeto existente; use `--dry-run` antes
|
|
121
127
|
de aplicar. Conteúdo humano não deve ser classificado como saída do gerador.
|
|
122
128
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Como usar os recursos do AgentDevKit no desenvolvimento de software.
|
|
3
|
+
language: pt
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Recursos na prática
|
|
7
|
+
|
|
8
|
+
O AgentDevKit organiza o trabalho de desenvolvimento de software com agentes
|
|
9
|
+
que você escolhe. O kit reúne instruções e ferramentas; decisões de produto,
|
|
10
|
+
revisão humana e testes continuam necessários no projeto atendido.
|
|
11
|
+
|
|
12
|
+
## Qual recurso usar?
|
|
13
|
+
|
|
14
|
+
| Recurso | Para que serve | Exemplo concreto |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| [Skill][skills] | Orienta decisões e passos de uma tarefa recorrente. | [Criar uma spec][spec] com dúvidas e critérios. |
|
|
17
|
+
| [Comando][commands] | Dá entrada a um caso de uso e encaminha o trabalho. | [Validar entrega][validar] com provas. |
|
|
18
|
+
| [Agente][agents] | Define responsabilidades e limites de um papel. | [Revisor][reviewer] examina a mudança em leitura. |
|
|
19
|
+
| [Regra][rules] | Registra uma convenção aplicável a um contexto. | [Qualidade][quality] orienta os cuidados da entrega. |
|
|
20
|
+
| [Template][templates] | Oferece uma estrutura para registrar decisões. | Preencher a [spec][spec-template] de um incremento. |
|
|
21
|
+
| [Script][scripts] | Executa uma automação reutilizável. | [Sincronizar][sync] os apontamentos. |
|
|
22
|
+
| [Ferramenta][tools] | Fornece uma capacidade técnica verificável. | [agent-browser][browser] exercita uma interface web. |
|
|
23
|
+
| [Testes e evidências][tests] | Comprovam comportamento e registram resultados. | Versionar o teste e manter relatório local. |
|
|
24
|
+
|
|
25
|
+
Os links levam às fontes e aos catálogos. Uma instrução em Markdown não inicia
|
|
26
|
+
um processo sozinha; o cliente de IA decide como descobrir e executar recursos
|
|
27
|
+
conforme suas capacidades e permissões. Consulte a
|
|
28
|
+
[compatibilidade por cliente][compat] antes de depender de um atalho.
|
|
29
|
+
|
|
30
|
+
## Exemplo: entregar uma funcionalidade
|
|
31
|
+
|
|
32
|
+
1. Peça ao agente para ler `AGENTS.md` e usar
|
|
33
|
+
[proximos-passos][next]. Ele identifica a fase e as pendências do projeto.
|
|
34
|
+
2. Defina produto, experiência quando aplicável e design técnico com o usuário.
|
|
35
|
+
Os [templates][templates] são pontos de partida, não decisões prontas.
|
|
36
|
+
3. Use [criar-spec][spec] para esclarecer dúvidas, limitar o incremento e
|
|
37
|
+
registrar critérios verificáveis. Planeje e obtenha a aprovação necessária.
|
|
38
|
+
4. Use [implementar-entrega][implement] com a spec e o plano. O perfil
|
|
39
|
+
[implementador][developer] segue a skill e respeita o escopo aprovado.
|
|
40
|
+
5. Execute as verificações adequadas: testes automatizados, navegador para um
|
|
41
|
+
fluxo web ou desktop para uma aplicação desktop, quando aplicáveis. Registre
|
|
42
|
+
o resultado de cada critério, incluindo o que não pôde ser executado.
|
|
43
|
+
6. Atualize a documentação afetada e encaminhe a mudança à
|
|
44
|
+
[revisão independente][review]. A equipe decide quando integrar a entrega.
|
|
45
|
+
|
|
46
|
+
Para um projeto existente, comece pelo inventário e preserve suas decisões;
|
|
47
|
+
consulte o [fluxo de trabalho][workflow] e a
|
|
48
|
+
[instalação em projetos existentes][install].
|
|
49
|
+
|
|
50
|
+
## Onde ficam os arquivos?
|
|
51
|
+
|
|
52
|
+
`.pbai4devs/` contém as fontes compartilhadas. Pastas como `.claude/` ou
|
|
53
|
+
`.codex/` recebem apenas os recursos de integração dos clientes selecionados;
|
|
54
|
+
os arquivos humanos existentes continuam pertencendo ao projeto. A seleção pode
|
|
55
|
+
ser consultada ou alterada pela [CLI][cli].
|
|
56
|
+
|
|
57
|
+
O código dos testes e as fixtures necessárias são versionáveis. Relatórios,
|
|
58
|
+
capturas, downloads e resultados locais ficam fora do versionamento. O
|
|
59
|
+
`.gitignore` da raiz pertence ao projeto; com Git inicializado, o kit mantém
|
|
60
|
+
suas exclusões locais em `.git/info/exclude` e informa conflitos. Sem Git,
|
|
61
|
+
execute a sincronização após `git init`, antes de adicionar arquivos ao índice.
|
|
62
|
+
Veja [instalação][install] e [ferramentas e testes][tests].
|
|
63
|
+
|
|
64
|
+
Para criar ou mudar um recurso, use o [guia de recursos][resources] e depois
|
|
65
|
+
sincronize e verifique os apontamentos. Para dúvidas de operação, consulte
|
|
66
|
+
[solução de problemas](troubleshooting.md).
|
|
67
|
+
|
|
68
|
+
[skills]: ../../.pbai4devs/skills/README.md
|
|
69
|
+
[commands]: ../../.pbai4devs/commands/README.md
|
|
70
|
+
[agents]: ../../.pbai4devs/agents/README.md
|
|
71
|
+
[rules]: ../../.pbai4devs/rules/README.md
|
|
72
|
+
[templates]: ../../.pbai4devs/templates/README.md
|
|
73
|
+
[scripts]: ../../.pbai4devs/scripts/README.md
|
|
74
|
+
[tools]: ../../.pbai4devs/tools/README.md
|
|
75
|
+
[spec]: ../../.pbai4devs/skills/specs/criar-spec/SKILL.md
|
|
76
|
+
[spec-template]: ../../.pbai4devs/templates/spec/TEMPLATE.md
|
|
77
|
+
[validar]: ../../.pbai4devs/commands/validar-entrega/COMMAND.md
|
|
78
|
+
[reviewer]: ../../.pbai4devs/agents/revisor/AGENT.md
|
|
79
|
+
[developer]: ../../.pbai4devs/agents/implementador/AGENT.md
|
|
80
|
+
[quality]: ../../.pbai4devs/rules/qualidade/RULE.md
|
|
81
|
+
[sync]: ../../.pbai4devs/scripts/sincronizar-agentes/README.md
|
|
82
|
+
[browser]: ../../.pbai4devs/tools/agent-browser/README.md
|
|
83
|
+
[tests]: tools-and-testing.md
|
|
84
|
+
[compat]: ../../.pbai4devs/COMPATIBILIDADE.md
|
|
85
|
+
[next]: ../../.pbai4devs/skills/projeto/proximos-passos/SKILL.md
|
|
86
|
+
[implement]: ../../.pbai4devs/commands/implementar-entrega/COMMAND.md
|
|
87
|
+
[review]: ../../.pbai4devs/commands/revisar-entrega/COMMAND.md
|
|
88
|
+
[workflow]: workflow.md
|
|
89
|
+
[install]: installation.md
|
|
90
|
+
[cli]: cli.md
|
|
91
|
+
[resources]: resources.md
|
package/docs/guides/resources.md
CHANGED
|
@@ -14,6 +14,9 @@ O hub permanece completo mesmo com poucos clientes selecionados. Apenas os
|
|
|
14
14
|
apontamentos necessários são gerados; consulte ou altere a seleção com `agents`
|
|
15
15
|
na [CLI](cli.md). Pastas compartilhadas podem atender mais de um cliente.
|
|
16
16
|
|
|
17
|
+
Para um exemplo de cada tipo aplicado ao desenvolvimento de software, veja
|
|
18
|
+
[recursos na prática](recursos-na-pratica.md).
|
|
19
|
+
|
|
17
20
|
## Escolher o tipo certo
|
|
18
21
|
|
|
19
22
|
Os caminhos abaixo são relativos a `.pbai4devs/`.
|
|
@@ -91,7 +91,7 @@ existente, mesmo em rascunho. Remova somente o rascunho incompleto dessa
|
|
|
91
91
|
tentativa após conferir a origem; nunca apague uma release publicada para
|
|
92
92
|
repetir o fluxo.
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
Se a release ainda não existe, use **Re-run failed jobs** ou
|
|
95
95
|
`gh workflow run release.yml --ref vX.Y.Z` com a tag real. A publicação prioriza
|
|
96
96
|
o secret `NPMJS_TOKEN` do repositório; sem ele, usa OIDC, conforme
|
|
97
97
|
[publicação](packages.md).
|
|
@@ -103,7 +103,8 @@ publicados. Assim, a verificação comprova o acesso de um usuário comum.
|
|
|
103
103
|
Confirme também que `latest` aponta para a versão estável e que o tarball é
|
|
104
104
|
baixável; metadados visíveis sozinhos não comprovam uma instalação funcional.
|
|
105
105
|
Se o registro ainda não disponibilizar o pacote, aguarde a verificação sem
|
|
106
|
-
republicar automaticamente a mesma versão.
|
|
106
|
+
republicar automaticamente a mesma versão. Durante a propagação, `E404` e
|
|
107
|
+
`ETARGET` do download do tarball são repetidos dentro da mesma janela limitada.
|
|
107
108
|
|
|
108
109
|
O GitHub Packages verifica os mesmos comandos com autenticação. Metadados
|
|
109
110
|
devem indicar visibilidade pública e associação ao repositório correto.
|
package/docs/specs/README.md
CHANGED
|
@@ -18,6 +18,8 @@ description: Incrementos verificáveis do AgentDevKit.
|
|
|
18
18
|
- [v0.0.10](v0.0.10/SPEC.md): release com instalação pelos dois registries e pré-requisitos.
|
|
19
19
|
- [Ecossistema de segurança](security-ecosystem/SPEC.md): fluxos e scanners locais.
|
|
20
20
|
- [Integração sem substituição][additive]: acréscimos em arquivos existentes.
|
|
21
|
+
- [Compatibilidade com projetos reais][oss]: projetos públicos e exemplos reproduzíveis.
|
|
22
|
+
- [Ignore do projeto](project-ignore/SPEC.md): preservar o arquivo da raiz.
|
|
21
23
|
|
|
22
24
|
Cada incremento vincula requisitos, critérios, testes e evidências locais.
|
|
23
25
|
|
|
@@ -28,3 +30,4 @@ projeto, siga o [fluxo](../guides/workflow.md) e use os
|
|
|
28
30
|
[Wiki](../README.md) · [Roadmap](../product/roadmap.md)
|
|
29
31
|
|
|
30
32
|
[additive]: additive-integration/SPEC.md
|
|
33
|
+
[oss]: real-projects/SPEC.md
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Preservar o ignore do projeto ao instalar o AgentDevKit.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Ignore do projeto
|
|
6
|
+
|
|
7
|
+
O arquivo `.gitignore` da raiz pertence ao projeto. Instalação, atualização e
|
|
8
|
+
sincronização preservam seus bytes, permissões e estado no índice Git, inclusive
|
|
9
|
+
quando um estado antigo do kit registra esse arquivo.
|
|
10
|
+
|
|
11
|
+
## Critérios de aceite
|
|
12
|
+
|
|
13
|
+
- AC-001: O kit não cria, altera, remove ou desindexa o `.gitignore` da raiz.
|
|
14
|
+
- AC-002: Derivados e artefatos locais são protegidos via `.git/info/exclude`.
|
|
15
|
+
- AC-003: Conflitos com regras humanas são informados antes das alterações; exceções
|
|
16
|
+
ambíguas não são consideradas seguras apenas por ausência do arquivo.
|
|
17
|
+
- AC-004: Sem Git, a instalação informa a pendência e orienta a executar a
|
|
18
|
+
sincronização depois de `git init` e antes de `git add`.
|
|
19
|
+
- AC-005: Arquivos humanos e configurações existentes são preservados.
|
|
20
|
+
- AC-006: Instalação e atualização são verificadas em projetos pelo pacote.
|
|
21
|
+
|
|
22
|
+
## Verificação
|
|
23
|
+
|
|
24
|
+
Os testes de exclusão local cobrem negações, migração, arquivos rastreados,
|
|
25
|
+
permissões e worktrees. Os testes de instalação e compatibilidade conferem a
|
|
26
|
+
preservação do projeto e o comportamento com e sem Git.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Compatibilidade com repositórios públicos e exemplos incrementais.
|
|
3
|
+
status: aprovado
|
|
4
|
+
version: 0.0.13
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Projetos reais e exemplos incrementais
|
|
8
|
+
|
|
9
|
+
Validar a instalação e manutenção do pacote em commits fixos de projetos reais,
|
|
10
|
+
preservando suas configurações e seu índice Git. Demonstrar o fluxo de produto,
|
|
11
|
+
design e três specs em dois projetos novos, com resultados locais ignorados.
|
|
12
|
+
|
|
13
|
+
## Critérios de aceite
|
|
14
|
+
|
|
15
|
+
| ID | Resultado observável |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| AC-001 | Testar os sete repositórios, com commits registrados. |
|
|
18
|
+
| AC-002 | Testar init repetido, update, sync e preservação nos clones. |
|
|
19
|
+
| AC-003 | Proteger caches ante exceções Git, preservando texto e índice. |
|
|
20
|
+
| AC-004 | Produto e design antes das três specs de cada projeto novo. |
|
|
21
|
+
| AC-005 | Documentar reprodução, limites e saídas ignoradas. |
|
|
22
|
+
| AC-006 | Testes de regressão, pacote e revisão independente passam. |
|
|
23
|
+
|
|
24
|
+
Os testes de compatibilidade não executam nem compilam as aplicações externas.
|
|
25
|
+
Aprovações de produto e revisão nos exemplos são simuladas. Código de teste é
|
|
26
|
+
versionado; clones, projetos gerados e evidências permanecem fora do Git.
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -59,6 +59,7 @@ export function parseArgs(argv) {
|
|
|
59
59
|
function print(result, options, output) {
|
|
60
60
|
if (options.json) { output.write(`${JSON.stringify(result)}\n`); return; }
|
|
61
61
|
output.write(`${result.message}\n`);
|
|
62
|
+
if (result.warnings?.length) for (const warning of result.warnings) output.write(`${warning}\n`);
|
|
62
63
|
if (result.clients) output.write(`${text(result.language, 'clientsCurrent')}: ${result.clients.join(', ')}\n`);
|
|
63
64
|
if (result.availableClients) output.write(`${text(result.language, 'clientsAvailable')}: ${result.availableClients.map(client => client.id).join(', ')}\n`);
|
|
64
65
|
if (result.toolOutput) output.write(`${result.toolOutput.trimEnd()}\n`);
|
|
@@ -125,9 +126,11 @@ export async function main(argv = process.argv.slice(2), context = {}) {
|
|
|
125
126
|
if (found.installed && !found.issues.length) {
|
|
126
127
|
const plan = planInstallation({ target, language, mode: 'sync' });
|
|
127
128
|
result.preserved = plan.preserved;
|
|
129
|
+
if (plan.localIgnore.status === 'no-git') { found.issues.push({ code: 'git-pending', path: '.git/info/exclude' }); result.warnings = [text(language, 'gitPending')]; }
|
|
128
130
|
found.issues.push(...plan.changes.map(change => ({ code: 'outdated', path: change.path })));
|
|
129
131
|
found.issues.push(...plan.localIgnoreCheck.missing.map(path => ({ code: 'unignored', path })));
|
|
130
|
-
|
|
132
|
+
found.issues.push(...plan.localIgnoreCheck.trackedOutputs.map(path => ({ code: 'tracked-local-output', path })));
|
|
133
|
+
if (plan.localIgnore.edits.length || plan.untrack?.paths.length) found.issues.push({ code: 'local-ignore', path: 'Git info/exclude' });
|
|
131
134
|
}
|
|
132
135
|
result.issues = found.issues; result.ok = found.installed && found.issues.length === 0;
|
|
133
136
|
result.message = text(language, result.ok ? 'healthy' : found.installed ? 'unhealthy' : 'missing'); exitCode = result.ok ? 0 : 1;
|
|
@@ -149,13 +152,15 @@ export async function main(argv = process.argv.slice(2), context = {}) {
|
|
|
149
152
|
result.preserved = plan.preserved;
|
|
150
153
|
result.plan = plan.changes.map(({ path, action }) => ({ path, action, conflict: plan.conflicts.includes(path) }));
|
|
151
154
|
result.untrackedPaths = [...(plan.untrack?.paths || [])];
|
|
152
|
-
result.localIgnore = { status: plan.localIgnore.status, edits: plan.localIgnore.edits.map(({ path, before }) => ({ path, action: before === null ? 'create' : 'update' })), missing: plan.localIgnoreCheck.missing };
|
|
153
|
-
if (plan.localIgnore.
|
|
155
|
+
result.localIgnore = { status: plan.localIgnore.status, edits: plan.localIgnore.edits.map(({ path, before }) => ({ path, action: before === null ? 'create' : 'update' })), missing: plan.localIgnoreCheck.missing, trackedOutputs: plan.localIgnoreCheck.trackedOutputs };
|
|
156
|
+
if (plan.localIgnore.status === 'no-git') result.warnings = [text(language, 'gitPending')];
|
|
157
|
+
if (plan.localIgnoreCheck.trackedOutputs.length) (result.warnings ||= []).push(`${text(language, 'trackedLocal')} ${plan.localIgnoreCheck.trackedOutputs.join(', ')}`);
|
|
158
|
+
if (plan.localIgnore.edits.length) result.plan.push({ path: 'Git info/exclude', action: 'update', conflict: false });
|
|
154
159
|
if (plan.untrack?.paths.length) result.plan.push({ path: 'Git index (generated files)', action: 'update', conflict: false });
|
|
155
160
|
result.conflicts = plan.conflicts;
|
|
156
161
|
const preview = options.dryRun || (options.command === 'sync' && options.check);
|
|
157
162
|
if (!plan.canApply || preview) {
|
|
158
|
-
result.ok = plan.canApply && (!options.check || result.plan.length === 0);
|
|
163
|
+
result.ok = plan.canApply && (!options.check || result.plan.length === 0 && plan.localIgnore.status !== 'no-git' && !plan.localIgnoreCheck.trackedOutputs.length);
|
|
159
164
|
result.message = text(language, plan.canApply ? 'plan' : 'conflict'); exitCode = result.ok ? 0 : 1;
|
|
160
165
|
} else {
|
|
161
166
|
Object.assign(result, applyPlan(plan)); result.message = text(language, result.changed ? options.command : 'unchanged');
|
package/src/i18n.mjs
CHANGED
|
@@ -7,6 +7,8 @@ export function normalizeLanguage(value) {
|
|
|
7
7
|
|
|
8
8
|
const messages = {
|
|
9
9
|
pt: {
|
|
10
|
+
gitPending: 'Git não foi inicializado; após git init, execute sync para proteger os arquivos locais.',
|
|
11
|
+
trackedLocal: 'Artefatos locais já estão versionados; revise o índice sem excluir conteúdo automaticamente.',
|
|
10
12
|
preserved: 'Caminhos existentes preservados', preservedDetails: 'Use --json para consultar os caminhos e motivos.',
|
|
11
13
|
help: 'AgentDevKit: init [diretório], update, doctor, sync [--check], lang [pt|en|es], agents [lista], next, tools <ação>.\nOpções: --target <diretório>, --lang <idioma>, --agents <lista>, --yes, --dry-run, --force, --json.\nAliases: --clients e clients.',
|
|
12
14
|
clientsCurrent: 'Agentes selecionados', clientsAvailable: 'Agentes disponíveis', clientsSelect: 'Agentes (IDs separados por vírgula ou all) [all]: ', agents: 'Seleção de agentes atualizada.',
|
|
@@ -16,6 +18,8 @@ const messages = {
|
|
|
16
18
|
agentLanguage: 'Responda em português, salvo quando o usuário solicitar outro idioma.', inspect: 'Verifique a instalação', workflow: 'Consulte a próxima etapa', install: 'Inicialize este projeto', repair: 'Confira a sincronização', review: 'Revise os arquivos em conflito', tools: 'Resultado da ferramenta',
|
|
17
19
|
},
|
|
18
20
|
en: {
|
|
21
|
+
gitPending: 'Git is not initialized; after git init, run sync to protect local files.',
|
|
22
|
+
trackedLocal: 'Local artifacts are already tracked; review the index without automatically deleting content.',
|
|
19
23
|
preserved: 'Existing paths preserved', preservedDetails: 'Use --json to inspect paths and reasons.',
|
|
20
24
|
help: 'AgentDevKit: init [directory], update, doctor, sync [--check], lang [pt|en|es], agents [list], next, tools <action>.\nOptions: --target <directory>, --lang <language>, --agents <list>, --yes, --dry-run, --force, --json.\nAliases: --clients and clients.',
|
|
21
25
|
clientsCurrent: 'Selected agents', clientsAvailable: 'Available agents', clientsSelect: 'Agents (comma-separated IDs or all) [all]: ', agents: 'Agent selection updated.',
|
|
@@ -25,6 +29,8 @@ const messages = {
|
|
|
25
29
|
agentLanguage: 'Respond in English unless the user requests another language.', inspect: 'Check the installation', workflow: 'Review the next stage', install: 'Initialize this project', repair: 'Check synchronization', review: 'Review conflicting files', tools: 'Tool result',
|
|
26
30
|
},
|
|
27
31
|
es: {
|
|
32
|
+
gitPending: 'Git no está inicializado; tras git init, ejecute sync para proteger los archivos locales.',
|
|
33
|
+
trackedLocal: 'Hay artefactos locales versionados; revise el índice sin eliminar contenido automáticamente.',
|
|
28
34
|
preserved: 'Rutas existentes preservadas', preservedDetails: 'Use --json para consultar las rutas y los motivos.',
|
|
29
35
|
help: 'AgentDevKit: init [directorio], update, doctor, sync [--check], lang [pt|en|es], agents [lista], next, tools <acción>.\nOpciones: --target <directorio>, --lang <idioma>, --agents <lista>, --yes, --dry-run, --force, --json.\nAlias: --clients y clients.',
|
|
30
36
|
clientsCurrent: 'Agentes seleccionados', clientsAvailable: 'Agentes disponibles', clientsSelect: 'Agentes (IDs separados por coma o all) [all]: ', agents: 'Selección de agentes actualizada.',
|
package/src/install.mjs
CHANGED
|
@@ -352,6 +352,7 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
352
352
|
}
|
|
353
353
|
const wholeRecords = [previous?.files || {}, derivedEntries, derivedFiles];
|
|
354
354
|
for (const path of new Set(wholeRecords.flatMap(record => Object.keys(record)))) if (!desired.has(path)) {
|
|
355
|
+
if (path === '.gitignore') { recordPreserved(path, 'project-owned'); continue; }
|
|
355
356
|
if (aliasedEntrypoint(path)) continue;
|
|
356
357
|
const current = read(target, path); if (!current) continue;
|
|
357
358
|
const known = wholeRecords.some(record => record[path] === hash(current));
|
|
@@ -412,7 +413,7 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
412
413
|
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])));
|
|
413
414
|
const ownedOutputs = [...new Set([...synchronized.ownedOutputs, ...ownedEntrypoints, statePath])].filter(path => !mixedPaths.has(path) && !overlapsPreserved(path));
|
|
414
415
|
const hashes = {};
|
|
415
|
-
for (const [path, digest] of Object.entries(previous?.files || {})) if ((ownedOutputs.includes(path) || !path.startsWith('.pbai4devs/')) && !mixedPaths.has(path) && !overlapsPreserved(path) && !aliasedEntrypoint(path)) hashes[path] = digest;
|
|
416
|
+
for (const [path, digest] of Object.entries(previous?.files || {})) if (path !== '.gitignore' && (ownedOutputs.includes(path) || !path.startsWith('.pbai4devs/')) && !mixedPaths.has(path) && !overlapsPreserved(path) && !aliasedEntrypoint(path)) hashes[path] = digest;
|
|
416
417
|
for (const path of ownedEntrypoints) { const current = read(target, path); if (current && (previous?.blocks[path] || derivedEntries[path] === hash(current))) hashes[path] = hash(current); }
|
|
417
418
|
for (const path of Object.keys(previous?.blocks || {})) if (path.endsWith('/.gitignore') && adapterRoots.some(root => path === `${root}/.gitignore`)) {
|
|
418
419
|
const current = read(target, path);
|