@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.
Files changed (90) hide show
  1. package/.pbai4devs/INSTRUCTIONS.md +16 -1
  2. package/.pbai4devs/agents/README.md +5 -0
  3. package/.pbai4devs/agents/analista-performance/AGENT.md +12 -0
  4. package/.pbai4devs/agents/analista-performance/antigravity.yaml +8 -0
  5. package/.pbai4devs/agents/analista-performance/claude.yaml +5 -0
  6. package/.pbai4devs/agents/analista-performance/cursor.yaml +5 -0
  7. package/.pbai4devs/agents/analista-performance/gemini.yaml +6 -0
  8. package/.pbai4devs/agents/analista-performance/github.yaml +5 -0
  9. package/.pbai4devs/agents/analista-performance/kimi-code.yaml +5 -0
  10. package/.pbai4devs/agents/analista-performance/mimocode.yaml +8 -0
  11. package/.pbai4devs/agents/analista-performance/opencode.yaml +11 -0
  12. package/.pbai4devs/agents/arquiteto/AGENT.md +14 -0
  13. package/.pbai4devs/agents/arquiteto/antigravity.yaml +8 -0
  14. package/.pbai4devs/agents/arquiteto/claude.yaml +5 -0
  15. package/.pbai4devs/agents/arquiteto/cursor.yaml +5 -0
  16. package/.pbai4devs/agents/arquiteto/gemini.yaml +6 -0
  17. package/.pbai4devs/agents/arquiteto/github.yaml +5 -0
  18. package/.pbai4devs/agents/arquiteto/kimi-code.yaml +5 -0
  19. package/.pbai4devs/agents/arquiteto/mimocode.yaml +8 -0
  20. package/.pbai4devs/agents/arquiteto/opencode.yaml +11 -0
  21. package/.pbai4devs/agents/engenheiro-qa/AGENT.md +16 -0
  22. package/.pbai4devs/agents/engenheiro-qa/antigravity.yaml +8 -0
  23. package/.pbai4devs/agents/engenheiro-qa/claude.yaml +5 -0
  24. package/.pbai4devs/agents/engenheiro-qa/cursor.yaml +5 -0
  25. package/.pbai4devs/agents/engenheiro-qa/gemini.yaml +6 -0
  26. package/.pbai4devs/agents/engenheiro-qa/github.yaml +5 -0
  27. package/.pbai4devs/agents/engenheiro-qa/kimi-code.yaml +5 -0
  28. package/.pbai4devs/agents/engenheiro-qa/mimocode.yaml +8 -0
  29. package/.pbai4devs/agents/engenheiro-qa/opencode.yaml +11 -0
  30. package/.pbai4devs/agents/especialista-backend/AGENT.md +14 -0
  31. package/.pbai4devs/agents/especialista-backend/antigravity.yaml +8 -0
  32. package/.pbai4devs/agents/especialista-backend/claude.yaml +5 -0
  33. package/.pbai4devs/agents/especialista-backend/cursor.yaml +5 -0
  34. package/.pbai4devs/agents/especialista-backend/gemini.yaml +6 -0
  35. package/.pbai4devs/agents/especialista-backend/github.yaml +5 -0
  36. package/.pbai4devs/agents/especialista-backend/kimi-code.yaml +5 -0
  37. package/.pbai4devs/agents/especialista-backend/mimocode.yaml +8 -0
  38. package/.pbai4devs/agents/especialista-backend/opencode.yaml +11 -0
  39. package/.pbai4devs/agents/especialista-frontend/AGENT.md +14 -0
  40. package/.pbai4devs/agents/especialista-frontend/antigravity.yaml +8 -0
  41. package/.pbai4devs/agents/especialista-frontend/claude.yaml +5 -0
  42. package/.pbai4devs/agents/especialista-frontend/cursor.yaml +5 -0
  43. package/.pbai4devs/agents/especialista-frontend/gemini.yaml +6 -0
  44. package/.pbai4devs/agents/especialista-frontend/github.yaml +5 -0
  45. package/.pbai4devs/agents/especialista-frontend/kimi-code.yaml +5 -0
  46. package/.pbai4devs/agents/especialista-frontend/mimocode.yaml +8 -0
  47. package/.pbai4devs/agents/especialista-frontend/opencode.yaml +11 -0
  48. package/.pbai4devs/agents/revisor/AGENT.md +3 -2
  49. package/.pbai4devs/agents/revisor/opencode.yaml +10 -3
  50. package/.pbai4devs/agents/validador/AGENT.md +4 -1
  51. package/.pbai4devs/commands/implementar-entrega/COMMAND.md +7 -0
  52. package/.pbai4devs/commands/planejar-entrega/COMMAND.md +8 -2
  53. package/.pbai4devs/commands/revisar-entrega/COMMAND.md +6 -3
  54. package/.pbai4devs/commands/validar-entrega/COMMAND.md +10 -1
  55. package/.pbai4devs/rules/qualidade/RULE.md +5 -6
  56. package/.pbai4devs/scripts/lib/README.md +2 -3
  57. package/.pbai4devs/scripts/lib/gitignore.mjs +129 -43
  58. package/.pbai4devs/scripts/sincronizar-agentes/README.md +1 -1
  59. package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +2 -1
  60. package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +94 -53
  61. package/.pbai4devs/skills/README.md +7 -0
  62. package/.pbai4devs/skills/arquitetura/avaliar-arquitetura/SKILL.md +21 -0
  63. package/.pbai4devs/skills/desenvolvimento/implementar-backend/SKILL.md +20 -0
  64. package/.pbai4devs/skills/desenvolvimento/implementar-frontend/SKILL.md +20 -0
  65. package/.pbai4devs/skills/projeto/verificar-trabalho/SKILL.md +3 -0
  66. package/.pbai4devs/skills/qualidade/avaliar-performance/SKILL.md +20 -0
  67. package/.pbai4devs/skills/qualidade/planejar-qa/SKILL.md +21 -0
  68. package/.pbai4devs/skills/qualidade/revisar-codigo/SKILL.md +19 -0
  69. package/.pbai4devs/skills/qualidade/validar-adversarial/SKILL.md +22 -0
  70. package/.pbai4devs/skills/seguranca/revisar-seguranca/SKILL.md +2 -0
  71. package/.pbai4devs/skills/specs/implementar-spec/SKILL.md +5 -0
  72. package/.pbai4devs/templates/gitignore/TEMPLATE.md +5 -5
  73. package/CHANGELOG.md +16 -0
  74. package/README.en.md +2 -2
  75. package/README.es.md +2 -2
  76. package/README.md +3 -2
  77. package/docs/README.md +2 -0
  78. package/docs/guides/README.md +1 -0
  79. package/docs/guides/installation.md +9 -5
  80. package/docs/guides/recursos-na-pratica.md +103 -0
  81. package/docs/guides/resources.md +29 -0
  82. package/docs/maintainers/maintenance.md +1 -1
  83. package/docs/maintainers/packages.md +2 -1
  84. package/docs/specs/README.md +3 -0
  85. package/docs/specs/development-ecosystem/SPEC.md +32 -0
  86. package/docs/specs/project-ignore/SPEC.md +26 -0
  87. package/package.json +1 -1
  88. package/src/cli.mjs +9 -4
  89. package/src/i18n.mjs +6 -0
  90. 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
- probes.add(checkedOutput(directory ? `${candidate}/.agentdevkit-artifact-probe` : candidate));
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 ?? checkedLocalPath(join(root, '.gitignore'));
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 artifactPaths = [...new Set([...artifactProbes(root, artifacts), ...privateClients.probes.filter(path => safeIgnoreProbe(root, path)), ...(location ? visibleArtifactProbes(root, location, artifacts) : [])])];
305
- const privateProbes = privateClients.probes.filter(path => safeIgnoreProbe(root, path));
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) && !localRead(join(root, '.gitignore'))?.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
- if (location) {
322
- const fallback = checkedLocalPath(join(root, '.gitignore'));
323
- const old = localRead(fallback);
324
- let next = localBlock(old, first, last, null);
325
- if (rootOverrideProbes.size || old?.includes(overrideFirst) || old?.includes(overrideLast)) {
326
- const overrideBody = privateClients.rules.join('\n');
327
- if ((old?.includes(overrideFirst) || old?.includes(overrideLast)) && localBlock(old, overrideFirst, overrideLast, overrideBody) !== old) throw new Error('Bloco privado AgentDevKit alterado; conteudo preservado.');
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
- const probes = [...new Set([...plan.paths, ...rootProbes(plan.root, plan.roots || []), ...(plan.artifactProbes || []),
353
- ...(plan.artifactPatterns ? visibleArtifactProbes(plan.root, plan.location, plan.artifactPatterns) : [])])];
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
- const pendingRootOverride = humanOverride && plan.rootOverrideProbes?.includes(probes[index]) && plan.edits.some(edit => edit.path === join(plan.root, '.gitignore'));
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, usa um bloco na raiz que migra ao executar depois de `git init`.
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('fallback protege outputs antes de git init e migra para exclude preservando regras', t => {
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, 'fallback'); assert.equal(checkLocalIgnore(plan).ok, false);
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, true);
33
- assert.match(readFileSync(join(root, '.gitignore'), 'utf8'), /^# Regras humanas\n\*\.own-cache\n/);
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('negação genérica de cache preserva regras humanas e protege caches de clientes', t => {
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!*.[Cc]ache/\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.ok(updated.startsWith(original));
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 privado modificado é preservado e recusa atualização', t => {
99
+ test('bloco legado na raiz pertence ao projeto e permanece intacto', t => {
100
100
  const root = fixture(t); git(root, ['init', '-q']);
101
- put(root, '.gitignore', '!*.[Cc]ache/\n');
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
- const path = join(root, '.gitignore');
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(checkLocalIgnore(planLocalIgnore(root)).ok, true);
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
- for (const path of ['.cursor/.LoCaL/private.json', '.cursor/.env.production']) assert.ok(git(root, ['check-ignore', '--no-index', path]).includes(path));
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.ok(readFileSync(join(root, '.gitignore'), 'utf8').startsWith(original));
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 acrescentada após o bloco recebe proteção na próxima sincronização', t => {
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', '!*.cache/\n'); applyLocalIgnore(planLocalIgnore(root));
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.edits.some(edit => edit.path === path));
151
- applyLocalIgnore(plan);
152
- assert.match(readFileSync(path, 'utf8'), /!\*\.cache\/\n!\/\.cursor\/\.env\.production\n/);
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 é somente leitura e aplicação detecta mudança concorrente', t => {
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
- assert.throws(() => applyLocalIgnore(plan), /alterada apos planejar/);
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, 'fallback'); assert.equal(existsSync(target), false);
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 não entram na lista de outputs', t => {
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(!plan.trackedOutputs.includes('.agent-tests/human.txt'));
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, true);
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/**\n');
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('fallback e rollback de exclusões mantêm o modo original', { skip: process.platform === 'win32' }, t => {
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'), before = readFileSync(exclude);
520
- chmodSync(exclude, 0o600);
521
- const plan = planLocalIgnore(root); assert.equal(plan.edits.length, 2);
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('rollback restaura exclusão somente leitura com escrita atômica', { skip: process.platform === 'win32' }, t => {
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'), before = readFileSync(exclude);
575
+ const exclude = join(root, '.git/info/exclude');
534
576
  chmodSync(exclude, 0o444);
535
577
  const plan = planLocalIgnore(root);
536
- Object.defineProperty(plan.edits[1], 'after', { get() { throw new Error('readonly rollback probe'); } });
537
- assert.throws(() => applyLocalIgnore(plan), /readonly rollback probe/);
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.