@pathbit/agentdevkit 0.0.13 → 0.0.15
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 +16 -1
- package/.pbai4devs/agents/README.md +5 -0
- package/.pbai4devs/agents/analista-performance/AGENT.md +12 -0
- package/.pbai4devs/agents/analista-performance/antigravity.yaml +8 -0
- package/.pbai4devs/agents/analista-performance/claude.yaml +5 -0
- package/.pbai4devs/agents/analista-performance/cursor.yaml +5 -0
- package/.pbai4devs/agents/analista-performance/gemini.yaml +6 -0
- package/.pbai4devs/agents/analista-performance/github.yaml +5 -0
- package/.pbai4devs/agents/analista-performance/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/analista-performance/mimocode.yaml +8 -0
- package/.pbai4devs/agents/analista-performance/opencode.yaml +11 -0
- package/.pbai4devs/agents/arquiteto/AGENT.md +14 -0
- package/.pbai4devs/agents/arquiteto/antigravity.yaml +8 -0
- package/.pbai4devs/agents/arquiteto/claude.yaml +5 -0
- package/.pbai4devs/agents/arquiteto/cursor.yaml +5 -0
- package/.pbai4devs/agents/arquiteto/gemini.yaml +6 -0
- package/.pbai4devs/agents/arquiteto/github.yaml +5 -0
- package/.pbai4devs/agents/arquiteto/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/arquiteto/mimocode.yaml +8 -0
- package/.pbai4devs/agents/arquiteto/opencode.yaml +11 -0
- package/.pbai4devs/agents/engenheiro-qa/AGENT.md +16 -0
- package/.pbai4devs/agents/engenheiro-qa/antigravity.yaml +8 -0
- package/.pbai4devs/agents/engenheiro-qa/claude.yaml +5 -0
- package/.pbai4devs/agents/engenheiro-qa/cursor.yaml +5 -0
- package/.pbai4devs/agents/engenheiro-qa/gemini.yaml +6 -0
- package/.pbai4devs/agents/engenheiro-qa/github.yaml +5 -0
- package/.pbai4devs/agents/engenheiro-qa/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/engenheiro-qa/mimocode.yaml +8 -0
- package/.pbai4devs/agents/engenheiro-qa/opencode.yaml +11 -0
- package/.pbai4devs/agents/especialista-backend/AGENT.md +14 -0
- package/.pbai4devs/agents/especialista-backend/antigravity.yaml +8 -0
- package/.pbai4devs/agents/especialista-backend/claude.yaml +5 -0
- package/.pbai4devs/agents/especialista-backend/cursor.yaml +5 -0
- package/.pbai4devs/agents/especialista-backend/gemini.yaml +6 -0
- package/.pbai4devs/agents/especialista-backend/github.yaml +5 -0
- package/.pbai4devs/agents/especialista-backend/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/especialista-backend/mimocode.yaml +8 -0
- package/.pbai4devs/agents/especialista-backend/opencode.yaml +11 -0
- package/.pbai4devs/agents/especialista-frontend/AGENT.md +14 -0
- package/.pbai4devs/agents/especialista-frontend/antigravity.yaml +8 -0
- package/.pbai4devs/agents/especialista-frontend/claude.yaml +5 -0
- package/.pbai4devs/agents/especialista-frontend/cursor.yaml +5 -0
- package/.pbai4devs/agents/especialista-frontend/gemini.yaml +6 -0
- package/.pbai4devs/agents/especialista-frontend/github.yaml +5 -0
- package/.pbai4devs/agents/especialista-frontend/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/especialista-frontend/mimocode.yaml +8 -0
- package/.pbai4devs/agents/especialista-frontend/opencode.yaml +11 -0
- package/.pbai4devs/agents/revisor/AGENT.md +3 -2
- package/.pbai4devs/agents/revisor/opencode.yaml +10 -3
- package/.pbai4devs/agents/validador/AGENT.md +4 -1
- package/.pbai4devs/commands/implementar-entrega/COMMAND.md +7 -0
- package/.pbai4devs/commands/planejar-entrega/COMMAND.md +8 -2
- package/.pbai4devs/commands/revisar-entrega/COMMAND.md +6 -3
- package/.pbai4devs/commands/validar-entrega/COMMAND.md +10 -1
- package/.pbai4devs/rules/qualidade/RULE.md +5 -6
- package/.pbai4devs/scripts/lib/README.md +2 -3
- package/.pbai4devs/scripts/lib/gitignore.mjs +129 -43
- 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 +94 -53
- package/.pbai4devs/skills/README.md +7 -0
- package/.pbai4devs/skills/arquitetura/avaliar-arquitetura/SKILL.md +21 -0
- package/.pbai4devs/skills/desenvolvimento/implementar-backend/SKILL.md +20 -0
- package/.pbai4devs/skills/desenvolvimento/implementar-frontend/SKILL.md +20 -0
- package/.pbai4devs/skills/projeto/verificar-trabalho/SKILL.md +3 -0
- package/.pbai4devs/skills/qualidade/avaliar-performance/SKILL.md +20 -0
- package/.pbai4devs/skills/qualidade/planejar-qa/SKILL.md +21 -0
- package/.pbai4devs/skills/qualidade/revisar-codigo/SKILL.md +19 -0
- package/.pbai4devs/skills/qualidade/validar-adversarial/SKILL.md +22 -0
- package/.pbai4devs/skills/seguranca/revisar-seguranca/SKILL.md +2 -0
- package/.pbai4devs/skills/specs/implementar-spec/SKILL.md +5 -0
- package/.pbai4devs/templates/gitignore/TEMPLATE.md +5 -5
- package/CHANGELOG.md +16 -0
- package/README.en.md +2 -2
- package/README.es.md +2 -2
- package/README.md +3 -2
- package/docs/README.md +2 -0
- package/docs/guides/README.md +1 -0
- package/docs/guides/installation.md +9 -5
- package/docs/guides/recursos-na-pratica.md +103 -0
- package/docs/guides/resources.md +29 -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/development-ecosystem/SPEC.md +32 -0
- package/docs/specs/project-ignore/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
|
@@ -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) {
|
|
@@ -150,7 +150,7 @@ function safeIgnoreProbe(root, path) {
|
|
|
150
150
|
for (const part of path.split('/').slice(0, -1)) {
|
|
151
151
|
parent = join(parent, part);
|
|
152
152
|
const item = localStat(parent);
|
|
153
|
-
if (item && !item.isDirectory()) return false;
|
|
153
|
+
if (item && (!item.isDirectory() || item.isSymbolicLink())) return false;
|
|
154
154
|
}
|
|
155
155
|
return true;
|
|
156
156
|
}
|
|
@@ -166,7 +166,7 @@ function artifactProbes(root, patterns) {
|
|
|
166
166
|
let parent = root, safe = true;
|
|
167
167
|
for (const part of path.split('/').slice(0, -1)) {
|
|
168
168
|
parent = join(parent, part); const item = localStat(parent);
|
|
169
|
-
if (item && !item.isDirectory()) { safe = false; break; }
|
|
169
|
+
if (item && (!item.isDirectory() || item.isSymbolicLink())) { safe = false; break; }
|
|
170
170
|
}
|
|
171
171
|
if (safe) probes.add(path);
|
|
172
172
|
}
|
|
@@ -204,8 +204,8 @@ function visibleArtifactProbes(root, location, patterns) {
|
|
|
204
204
|
const listed = gitCommand(location.top, ['--literal-pathspecs', '-c', 'core.excludesFile=', 'ls-files', '--cached', '--others', '--exclude-standard', '-z', '--', location.prefix ? `${location.prefix}/` : '.']);
|
|
205
205
|
if (listed.error || listed.status !== 0) throw new Error(listed.error?.message || listed.stderr || 'Falha ao conferir artefatos existentes.');
|
|
206
206
|
const paths = listed.stdout.split('\0').filter(Boolean), prefix = location.prefix ? `${location.prefix}/` : '';
|
|
207
|
-
const probes = new Set(paths.filter(path => path.startsWith(prefix)).map(path => path.slice(prefix.length)).filter(path => artifactPath(path, patterns)));
|
|
208
|
-
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))));
|
|
209
209
|
let ancestor = root;
|
|
210
210
|
while (true) {
|
|
211
211
|
ignores.add(portable(relative(location.top, join(ancestor, '.gitignore'))));
|
|
@@ -224,12 +224,111 @@ function visibleArtifactProbes(root, location, patterns) {
|
|
|
224
224
|
const ruleBase = !rule.startsWith('/') && !literal.includes('/') && relative(dirname(file), root).split(sep)[0] !== '..' ? root : dirname(file);
|
|
225
225
|
const candidate = portable(relative(root, resolve(ruleBase, literal.replace(/\\(.)/g, '$1'))));
|
|
226
226
|
if (!candidate || candidate === '..' || candidate.startsWith('../') || !artifactPath(candidate, patterns, directory)) continue;
|
|
227
|
-
|
|
227
|
+
const probe = checkedOutput(directory ? `${candidate}/.agentdevkit-artifact-probe` : candidate);
|
|
228
|
+
if (safeIgnoreProbe(root, probe)) probes.add(probe);
|
|
228
229
|
}
|
|
229
230
|
}
|
|
230
231
|
return [...probes];
|
|
231
232
|
}
|
|
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);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return [...probes];
|
|
255
|
+
}
|
|
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
|
+
|
|
233
332
|
function localIgnoreSource(location, source) {
|
|
234
333
|
const path = resolve(location.top, source);
|
|
235
334
|
if (path === location.exclude) return true;
|
|
@@ -238,6 +337,8 @@ function localIgnoreSource(location, source) {
|
|
|
238
337
|
}
|
|
239
338
|
|
|
240
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.');
|
|
241
342
|
const paths = probes.map(path => location.prefix ? `${location.prefix}/${path}` : path);
|
|
242
343
|
const result = gitCommand(location.top, ['-c', 'core.excludesFile=', 'check-ignore', '--verbose', '--no-index', '--stdin', '-z'], paths.join('\0') + '\0');
|
|
243
344
|
if (result.error || ![0, 1].includes(result.status)) throw new Error(result.error?.message || result.stderr || 'Falha ao conferir exclusoes Git.');
|
|
@@ -297,46 +398,29 @@ export function planLocalIgnore(root, { outputs = [], templateRoot } = {}) {
|
|
|
297
398
|
if (line.startsWith('!/')) return `!/${escapePattern(location.prefix)}${line.slice(1)}`;
|
|
298
399
|
return line.startsWith('/') ? `/${escapePattern(location.prefix)}${line}` : line;
|
|
299
400
|
}).join('\n');
|
|
300
|
-
const target = location?.exclude
|
|
301
|
-
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;
|
|
302
403
|
const edits = before === after ? [] : [{ path: target, before, after, beforeMode: localMode(target) }];
|
|
303
404
|
const probes = [...paths, ...rootProbes(root, roots)];
|
|
304
|
-
const
|
|
305
|
-
const
|
|
306
|
-
const rootOverrideProbes = new Set();
|
|
307
|
-
const overrideFirst = `# BEGIN AGENTDEVKIT PRIVATE ${id}`, overrideLast = `# END AGENTDEVKIT PRIVATE ${id}`;
|
|
308
|
-
if (location) {
|
|
309
|
-
const humanIgnore = localRead(checkedLocalPath(join(root, '.gitignore')));
|
|
310
|
-
const withoutFallback = localBlock(humanIgnore, first, last, null);
|
|
311
|
-
const withoutOwned = localBlock(withoutFallback, overrideFirst, overrideLast, null);
|
|
312
|
-
// Any root negation can match an arbitrary spelling or wildcard value.
|
|
313
|
-
// Protect private client paths deterministically instead of sampling it.
|
|
314
|
-
if (withoutOwned.replace(/^\uFEFF/, '').split(/\r?\n/).some(line => line.startsWith('!'))) for (const path of privateProbes) rootOverrideProbes.add(path);
|
|
315
|
-
}
|
|
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)] : [])])];
|
|
316
407
|
const wanted = new Set(probes.map(path => location?.prefix ? `${location.prefix}/${path}` : path));
|
|
317
|
-
if (location && !before?.includes(first)
|
|
318
|
-
const ignored = ignoreMatches(location, [...probes, ...artifactPaths]);
|
|
408
|
+
if (location && !before?.includes(first)) {
|
|
409
|
+
const ignored = ignoreMatches(location, [...probes, ...artifactPaths].filter(path => safeIgnoreProbe(root, path)));
|
|
319
410
|
if (ignored.paths.every(path => { const match = ignored.matches.get(path); return match && !match.pattern.startsWith('!') && localIgnoreSource(location, match.source); })) edits.length = 0;
|
|
320
411
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const end = next.indexOf(overrideLast);
|
|
329
|
-
if (end >= 0 && next.slice(end + overrideLast.length).split(/\r?\n/).some(line => line.startsWith('!'))) next = localBlock(next, overrideFirst, overrideLast, null);
|
|
330
|
-
next = localBlock(next, overrideFirst, overrideLast, overrideBody);
|
|
331
|
-
}
|
|
332
|
-
if (old !== null && old !== next) edits.push({ path: fallback, before: old, after: next, beforeMode: localMode(fallback) });
|
|
333
|
-
}
|
|
334
|
-
const tracked = location ? gitCommand(location.top, ['ls-files', '-z']).stdout.split('\0').filter(path => wanted.has(path)).map(path => location.prefix ? path.slice(location.prefix.length + 1) : path) : [];
|
|
335
|
-
return { root, status: location ? 'git' : 'fallback', location, paths, roots, artifactPatterns: artifacts, artifactProbes: artifactPaths, edits, trackedOutputs: tracked, first, last, rootOverrideProbes: [...rootOverrideProbes] };
|
|
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 };
|
|
336
419
|
}
|
|
337
420
|
|
|
338
421
|
export function checkLocalIgnore(plan, { replacedBlocks = [] } = {}) {
|
|
339
422
|
const missing = [];
|
|
423
|
+
if (plan.location) missing.push(...ambiguousExceptions(plan.root, plan.location, plan.clientRoots || [], plan.artifactPatterns || []), ...foreignHiddenClients(plan));
|
|
340
424
|
const replacements = replacedBlocks.map(item => {
|
|
341
425
|
const path = canonicalExisting(checkedLocalPath(resolve(item.path))), inside = relative(localRoot(plan.root).root, path);
|
|
342
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.');
|
|
@@ -349,8 +433,11 @@ export function checkLocalIgnore(plan, { replacedBlocks = [] } = {}) {
|
|
|
349
433
|
return { ...item, path };
|
|
350
434
|
});
|
|
351
435
|
if (plan.location && (plan.paths.length || plan.artifactProbes?.length)) {
|
|
352
|
-
|
|
353
|
-
|
|
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));
|
|
354
441
|
const { paths: relativePaths, matches } = ignoreMatches(plan.location, probes);
|
|
355
442
|
for (let index = 0; index < relativePaths.length; index++) {
|
|
356
443
|
const match = matches.get(relativePaths[index]);
|
|
@@ -358,11 +445,10 @@ export function checkLocalIgnore(plan, { replacedBlocks = [] } = {}) {
|
|
|
358
445
|
const local = match && localIgnoreSource(plan.location, match.source);
|
|
359
446
|
const humanOverride = negative && local && /(?:^|[\\/])\.gitignore$/.test(match.source);
|
|
360
447
|
const replaced = negative && replacements.some(item => item.path === canonicalExisting(resolve(plan.location.top, match.source)) && match.line >= item.firstLine && match.line <= item.lastLine);
|
|
361
|
-
|
|
362
|
-
if (!replaced && !pendingRootOverride && ((!plan.edits.length && (!match || negative || !local)) || humanOverride)) missing.push(probes[index]);
|
|
448
|
+
if (!replaced && ((!plan.edits.length && (!match || negative || !local)) || humanOverride)) missing.push(probes[index]);
|
|
363
449
|
}
|
|
364
450
|
}
|
|
365
|
-
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 };
|
|
366
452
|
}
|
|
367
453
|
|
|
368
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,10 +68,10 @@ 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
72
|
const root = fixture(t); git(root, ['init', '-q']);
|
|
73
73
|
git(root, ['config', 'core.ignorecase', 'false']);
|
|
74
|
-
const original = '# Regras do projeto\n
|
|
74
|
+
const original = '# Regras do projeto\n.env.*\n!.env.example\n!.env.*.example\n';
|
|
75
75
|
put(root, '.gitignore', original); git(root, ['add', '.gitignore']);
|
|
76
76
|
const index = git(root, ['ls-files', '--stage', '-z']);
|
|
77
77
|
const plan = planLocalIgnore(root);
|
|
@@ -79,7 +79,7 @@ test('negação genérica de cache preserva regras humanas e protege caches de c
|
|
|
79
79
|
assert.equal(existsSync(join(root, '.cursor')), false);
|
|
80
80
|
applyLocalIgnore(plan);
|
|
81
81
|
const updated = readFileSync(join(root, '.gitignore'), 'utf8');
|
|
82
|
-
assert.
|
|
82
|
+
assert.equal(updated, original);
|
|
83
83
|
assert.equal(git(root, ['ls-files', '--stage', '-z']), index);
|
|
84
84
|
assert.equal(existsSync(join(root, '.cursor')), false);
|
|
85
85
|
assert.equal(checkLocalIgnore(planLocalIgnore(root)).ok, true);
|
|
@@ -96,18 +96,12 @@ test('negação genérica de cache preserva regras humanas e protege caches de c
|
|
|
96
96
|
assert.equal(checkLocalIgnore(planLocalIgnore(root)).ok, true);
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
-
test('bloco
|
|
99
|
+
test('bloco legado na raiz pertence ao projeto e permanece intacto', t => {
|
|
100
100
|
const root = fixture(t); git(root, ['init', '-q']);
|
|
101
|
-
|
|
101
|
+
const original = '# BEGIN AGENTDEVKIT PRIVATE legado\n# edição humana\n# END AGENTDEVKIT PRIVATE legado\n';
|
|
102
|
+
put(root, '.gitignore', original);
|
|
102
103
|
applyLocalIgnore(planLocalIgnore(root));
|
|
103
|
-
|
|
104
|
-
const changed = readFileSync(path, 'utf8').replace('# Estado local', '# Estado alterado');
|
|
105
|
-
// Alter a concrete managed rule, even if the template has no heading here.
|
|
106
|
-
const edited = changed.replace('/.cursor/', '/.cursor-alterado/');
|
|
107
|
-
assert.notEqual(edited, readFileSync(path, 'utf8'));
|
|
108
|
-
writeFileSync(path, edited);
|
|
109
|
-
assert.throws(() => planLocalIgnore(root), /Bloco privado AgentDevKit alterado/);
|
|
110
|
-
assert.equal(readFileSync(path, 'utf8'), edited);
|
|
104
|
+
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), original);
|
|
111
105
|
});
|
|
112
106
|
|
|
113
107
|
test('negação com caixa mista não expõe cache em Git sensível a maiúsculas', t => {
|
|
@@ -115,9 +109,8 @@ test('negação com caixa mista não expõe cache em Git sensível a maiúsculas
|
|
|
115
109
|
git(root, ['config', 'core.ignorecase', 'false']);
|
|
116
110
|
put(root, '.gitignore', '!*.CaChE/\n');
|
|
117
111
|
put(root, '.cursor/.CaChE/private.json', 'local');
|
|
118
|
-
applyLocalIgnore(planLocalIgnore(root));
|
|
119
|
-
assert.equal(
|
|
120
|
-
assert.ok(git(root, ['check-ignore', '--no-index', '.cursor/.CaChE/private.json']).includes('.cursor/.CaChE/private.json'));
|
|
112
|
+
assert.throws(() => applyLocalIgnore(planLocalIgnore(root)), /Regras humanas/);
|
|
113
|
+
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), '!*.CaChE/\n');
|
|
121
114
|
});
|
|
122
115
|
|
|
123
116
|
test('negações literais e com caixa mista não expõem estado privado', t => {
|
|
@@ -125,41 +118,36 @@ test('negações literais e com caixa mista não expõem estado privado', t => {
|
|
|
125
118
|
git(root, ['config', 'core.ignorecase', 'false']);
|
|
126
119
|
put(root, '.gitignore', '!*.LoCaL/\n!/.cursor/.env.production\n');
|
|
127
120
|
for (const path of ['.cursor/.LoCaL/private.json', '.cursor/.env.production']) put(root, path, 'privado');
|
|
128
|
-
applyLocalIgnore(planLocalIgnore(root));
|
|
129
|
-
|
|
130
|
-
assert.equal(checkLocalIgnore(planLocalIgnore(root)).ok, true);
|
|
121
|
+
assert.throws(() => applyLocalIgnore(planLocalIgnore(root)), /Regras humanas/);
|
|
122
|
+
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), '!*.LoCaL/\n!/.cursor/.env.production\n');
|
|
131
123
|
});
|
|
132
124
|
|
|
133
125
|
test('negação na primeira linha com BOM preserva bytes e protege segredos', t => {
|
|
134
126
|
const root = fixture(t); git(root, ['init', '-q']);
|
|
135
127
|
const original = '\uFEFF!/.cursor/.env.production\r\n';
|
|
136
128
|
put(root, '.gitignore', original); put(root, '.cursor/.env.production', 'privado');
|
|
137
|
-
applyLocalIgnore(planLocalIgnore(root));
|
|
138
|
-
assert.
|
|
139
|
-
assert.ok(git(root, ['check-ignore', '--no-index', '.cursor/.env.production']).includes('.cursor/.env.production'));
|
|
140
|
-
assert.equal(checkLocalIgnore(planLocalIgnore(root)).ok, true);
|
|
129
|
+
assert.throws(() => applyLocalIgnore(planLocalIgnore(root)), /Regras humanas/);
|
|
130
|
+
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), original);
|
|
141
131
|
});
|
|
142
132
|
|
|
143
|
-
test('negação humana
|
|
133
|
+
test('negação humana posterior é informada sem reescrever a raiz', t => {
|
|
144
134
|
const root = fixture(t); git(root, ['init', '-q']);
|
|
145
|
-
put(root, '.gitignore', '
|
|
135
|
+
put(root, '.gitignore', '# humano\n'); applyLocalIgnore(planLocalIgnore(root));
|
|
146
136
|
const path = join(root, '.gitignore');
|
|
147
137
|
writeFileSync(path, readFileSync(path, 'utf8') + '!/.cursor/.env.production\n');
|
|
148
138
|
put(root, '.cursor/.env.production', 'privado');
|
|
149
139
|
const plan = planLocalIgnore(root);
|
|
150
|
-
assert.ok(plan.
|
|
151
|
-
applyLocalIgnore(plan);
|
|
152
|
-
assert.
|
|
153
|
-
assert.ok(git(root, ['check-ignore', '--no-index', '.cursor/.env.production']).includes('.cursor/.env.production'));
|
|
154
|
-
assert.equal(checkLocalIgnore(planLocalIgnore(root)).ok, true);
|
|
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');
|
|
155
143
|
});
|
|
156
144
|
|
|
157
|
-
test('planejamento
|
|
145
|
+
test('planejamento sem Git não modifica ignore nem edição humana posterior', t => {
|
|
158
146
|
const root = fixture(t); put(root, '.gitignore', 'antes\r\n');
|
|
159
147
|
const plan = planLocalIgnore(root, { outputs: ['generated.md'] });
|
|
160
148
|
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), 'antes\r\n');
|
|
161
149
|
put(root, '.gitignore', 'depois\r\n');
|
|
162
|
-
|
|
150
|
+
applyLocalIgnore(plan);
|
|
163
151
|
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), 'depois\r\n');
|
|
164
152
|
for (const path of ['../outside', '/absolute', 'line\nname', 'a\\b']) assert.throws(() => planLocalIgnore(root, { outputs: [path] }), /invalida/);
|
|
165
153
|
});
|
|
@@ -185,7 +173,7 @@ test('plano de instalação em alvo inexistente não cria arquivos', t => {
|
|
|
185
173
|
const root = fixture(t);
|
|
186
174
|
const target = join(root, 'new project', 'nested');
|
|
187
175
|
const fallback = planLocalIgnore(target, { outputs: ['AGENTS.md'] });
|
|
188
|
-
assert.equal(fallback.status, '
|
|
176
|
+
assert.equal(fallback.status, 'no-git'); assert.equal(existsSync(target), false);
|
|
189
177
|
git(root, ['init', '-q']);
|
|
190
178
|
const plan = planLocalIgnore(target, { outputs: ['AGENTS.md'] });
|
|
191
179
|
assert.equal(plan.status, 'git'); assert.equal(existsSync(target), false);
|
|
@@ -217,21 +205,21 @@ test('configurações humanas dos clientes continuam versionáveis', t => {
|
|
|
217
205
|
assert.ok(tracked.includes('.github/settings.json')); assert.ok(tracked.includes('.vscode/settings.json'));
|
|
218
206
|
});
|
|
219
207
|
|
|
220
|
-
test('artefatos vêm da política compartilhada e
|
|
208
|
+
test('artefatos vêm da política compartilhada e rastreados são informados', t => {
|
|
221
209
|
const root = fixture(t); git(root, ['init', '-q']);
|
|
222
210
|
put(root, '.agent-tests/human.txt', 'evidência humana'); git(root, ['add', '-f', '.agent-tests/human.txt']);
|
|
223
211
|
const plan = planLocalIgnore(root, { outputs: ['AGENTS.md'] });
|
|
224
212
|
assert.ok(plan.artifactProbes.some(path => path.startsWith('.agent-tests/')));
|
|
225
213
|
assert.ok(plan.artifactProbes.some(path => path.startsWith('.agentdevkit-ignore-probes/')));
|
|
226
214
|
assert.ok(plan.paths.every(path => !plan.artifactProbes.includes(path)));
|
|
227
|
-
assert.ok(
|
|
215
|
+
assert.ok(plan.trackedOutputs.includes('.agent-tests/human.txt'));
|
|
228
216
|
applyLocalIgnore(plan);
|
|
229
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']) {
|
|
230
218
|
assert.equal(git(root, ['check-ignore', '--no-index', path]).trim(), path);
|
|
231
219
|
}
|
|
232
220
|
assert.equal(git(root, ['ls-files', '.agent-tests/human.txt']).trim(), '.agent-tests/human.txt');
|
|
233
221
|
assert.equal(readFileSync(join(root, '.agent-tests/human.txt'), 'utf8'), 'evidência humana');
|
|
234
|
-
assert.equal(checkLocalIgnore(planLocalIgnore(root, { outputs: ['AGENTS.md'] })).ok,
|
|
222
|
+
assert.equal(checkLocalIgnore(planLocalIgnore(root, { outputs: ['AGENTS.md'] })).ok, false);
|
|
235
223
|
});
|
|
236
224
|
|
|
237
225
|
test('regras de artefatos são limitadas ao projeto dentro do monorepo', t => {
|
|
@@ -467,6 +455,63 @@ test('arquivo real reaberto por glob é detectado também se surgir após o plan
|
|
|
467
455
|
assert.equal(readFileSync(join(root, path), 'utf8'), 'artefato local\n');
|
|
468
456
|
});
|
|
469
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
|
+
|
|
470
515
|
test('regras customizadas aninhadas continuam válidas e incluem pais ocultos', t => {
|
|
471
516
|
const root = fixture(t); git(root, ['init', '-q']);
|
|
472
517
|
const policy = readFileSync(join(projectRoot, '.pbai4devs/templates/gitignore/agents.ignore'), 'utf8');
|
|
@@ -485,7 +530,7 @@ test('chaves literais em padrões Git não viram expansão de glob', t => {
|
|
|
485
530
|
const policy = readFileSync(join(projectRoot, '.pbai4devs/templates/gitignore/agents.ignore'), 'utf8');
|
|
486
531
|
put(root, '.pbai4devs/templates/gitignore/agents.ignore', `${policy}\n**/cache{keep,archive}/\n`);
|
|
487
532
|
put(root, 'cachekeep/source.txt', 'código humano\n');
|
|
488
|
-
put(root, '.gitignore', '!cachekeep
|
|
533
|
+
put(root, '.gitignore', '!cachekeep/source.txt\n');
|
|
489
534
|
assert.doesNotThrow(() => applyLocalIgnore(planLocalIgnore(root)));
|
|
490
535
|
assert.equal(readFileSync(join(root, 'cachekeep/source.txt'), 'utf8'), 'código humano\n');
|
|
491
536
|
});
|
|
@@ -510,32 +555,28 @@ test('escrita de exclusões preserva permissões e detecta alteração concorren
|
|
|
510
555
|
assert.equal(statSync(exclude).mode & 0o777, 0o640);
|
|
511
556
|
});
|
|
512
557
|
|
|
513
|
-
test('
|
|
558
|
+
test('ignore raiz mantém modo e bytes antes e depois de git init', { skip: process.platform === 'win32' }, t => {
|
|
514
559
|
const root = fixture(t), ignore = join(root, '.gitignore');
|
|
515
560
|
put(root, '.gitignore', '# humano\n'); chmodSync(ignore, 0o600);
|
|
516
561
|
applyLocalIgnore(planLocalIgnore(root));
|
|
517
562
|
assert.equal(statSync(ignore).mode & 0o777, 0o600);
|
|
518
563
|
const original = readFileSync(ignore); git(root, ['init', '-q']);
|
|
519
|
-
const exclude = join(root, '.git/info/exclude')
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
Object.defineProperty(plan.edits[1], 'after', { get() { throw new Error('injected rollback failure'); } });
|
|
523
|
-
assert.throws(() => applyLocalIgnore(plan), /injected rollback failure/);
|
|
524
|
-
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);
|
|
525
567
|
assert.deepEqual(readFileSync(ignore), original); assert.equal(statSync(ignore).mode & 0o777, 0o600);
|
|
526
568
|
});
|
|
527
569
|
|
|
528
|
-
test('
|
|
570
|
+
test('ignore raiz somente leitura não impede exclusão local no Git', { skip: process.platform === 'win32' }, t => {
|
|
529
571
|
const root = fixture(t), ignore = join(root, '.gitignore');
|
|
530
572
|
put(root, '.gitignore', '# humano\n'); chmodSync(ignore, 0o444);
|
|
531
573
|
applyLocalIgnore(planLocalIgnore(root));
|
|
532
574
|
const original = readFileSync(ignore); git(root, ['init', '-q']);
|
|
533
|
-
const exclude = join(root, '.git/info/exclude')
|
|
575
|
+
const exclude = join(root, '.git/info/exclude');
|
|
534
576
|
chmodSync(exclude, 0o444);
|
|
535
577
|
const plan = planLocalIgnore(root);
|
|
536
|
-
|
|
537
|
-
assert.
|
|
538
|
-
assert.deepEqual(readFileSync(exclude), before); assert.equal(statSync(exclude).mode & 0o777, 0o444);
|
|
578
|
+
applyLocalIgnore(plan);
|
|
579
|
+
assert.equal(statSync(exclude).mode & 0o777, 0o444);
|
|
539
580
|
assert.deepEqual(readFileSync(ignore), original); assert.equal(statSync(ignore).mode & 0o777, 0o444);
|
|
540
581
|
});
|
|
541
582
|
|
|
@@ -13,6 +13,13 @@ as capacidades do cliente.
|
|
|
13
13
|
- `projeto`: configuração, criação e manutenção dos recursos.
|
|
14
14
|
- `produto`: PRD, experiência e design técnico.
|
|
15
15
|
- `specs`: especificações, roadmap e implementação.
|
|
16
|
+
- `arquitetura`: [avaliar decisões e riscos](arquitetura/avaliar-arquitetura/SKILL.md).
|
|
17
|
+
- `desenvolvimento`: [backend](desenvolvimento/implementar-backend/SKILL.md) e
|
|
18
|
+
[frontend](desenvolvimento/implementar-frontend/SKILL.md).
|
|
19
|
+
- `qualidade`: [QA](qualidade/planejar-qa/SKILL.md),
|
|
20
|
+
[validação adversarial](qualidade/validar-adversarial/SKILL.md),
|
|
21
|
+
[revisão de código](qualidade/revisar-codigo/SKILL.md) e
|
|
22
|
+
[performance](qualidade/avaliar-performance/SKILL.md).
|
|
16
23
|
- `seguranca`: [revisão](seguranca/revisar-seguranca/SKILL.md),
|
|
17
24
|
[autorização](seguranca/revisar-autorizacao/SKILL.md),
|
|
18
25
|
[ameaças](seguranca/modelar-ameacas/SKILL.md) e
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: avaliar-arquitetura
|
|
3
|
+
description: Avalia e orienta decisões de arquitetura conforme requisitos e restrições do software.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Avaliar arquitetura
|
|
7
|
+
|
|
8
|
+
Examine o produto, os fluxos, o design e o sistema existente. Reúna dúvidas
|
|
9
|
+
que mudam decisões; quando faltar definição, encaminhe ao
|
|
10
|
+
[design técnico](../../produto/criar-design-doc/SKILL.md). Não imponha stack.
|
|
11
|
+
|
|
12
|
+
Identifique componentes, responsabilidades, contratos, dados e fronteiras de
|
|
13
|
+
confiança. Avalie operação, evolução, falhas, segurança, desempenho e custos
|
|
14
|
+
conforme o contexto. Compare alternativas e consequências; registre decisões,
|
|
15
|
+
compensações e migração quando necessárias. Diagramas só quando esclarecerem.
|
|
16
|
+
|
|
17
|
+
Questione acoplamento, perda de dados, dependências e falhas simultâneas com
|
|
18
|
+
cenários concretos. Delegue uma análise independente quando disponível e
|
|
19
|
+
permitido. Entregue decisão proposta, evidências, riscos e verificações abertas;
|
|
20
|
+
não apresente hipóteses como fatos. Consulte [C4](https://c4model.com/diagrams)
|
|
21
|
+
quando um diagrama ajudar.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: implementar-backend
|
|
3
|
+
description: Implementa mudanças de backend com contratos, dados e provas de comportamento.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Implementar backend
|
|
7
|
+
|
|
8
|
+
Siga a spec e o plano aprovados, os contratos e a stack existentes. Esclareça
|
|
9
|
+
dúvidas sobre entradas, saídas, persistência,
|
|
10
|
+
compatibilidade e falhas antes de decidir. Altere somente o escopo atribuído.
|
|
11
|
+
|
|
12
|
+
Verifique validação de entrada, autorização por recurso e isolamento de dados.
|
|
13
|
+
Considere limites, concorrência, repetição, timeout, migração e recuperação
|
|
14
|
+
quando se aplicarem; não registre segredos. Reutilize convenções e ferramentas
|
|
15
|
+
do projeto sem presumir banco, framework ou serviço externo.
|
|
16
|
+
|
|
17
|
+
Prove casos normais e negativos por testes automatizados e, quando aplicável,
|
|
18
|
+
por chamadas reais à API com dados isolados. Mocks não comprovam integrações
|
|
19
|
+
que não foram executadas. Versione testes; mantenha resultados locais ignorados.
|
|
20
|
+
Relate critérios, comandos, observações e dependências não verificadas.
|