@pathbit/agentdevkit 0.0.21 → 0.0.23

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 (39) hide show
  1. package/.pbai4devs/COMPATIBILIDADE.md +11 -1
  2. package/.pbai4devs/rules/ambiente-local/RULE.md +5 -0
  3. package/.pbai4devs/rules/git/RULE.md +5 -1
  4. package/.pbai4devs/scripts/README.md +1 -0
  5. package/.pbai4devs/scripts/diagnosticar-ambiente/tests/TEST.mjs +60 -1
  6. package/.pbai4devs/scripts/inspecionar-projeto/README.md +6 -0
  7. package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +218 -18
  8. package/.pbai4devs/scripts/lib/adapters.mjs +5 -2
  9. package/.pbai4devs/scripts/lib/runtime.mjs +38 -2
  10. package/.pbai4devs/scripts/sincronizar-agentes/README.md +5 -1
  11. package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +136 -9
  12. package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +30 -0
  13. package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +22 -0
  14. package/.pbai4devs/scripts/testar-maturidade/README.md +12 -10
  15. package/.pbai4devs/scripts/testar-maturidade/SCRIPT.mjs +86 -44
  16. package/.pbai4devs/scripts/testar-maturidade/tests/TEST.mjs +157 -33
  17. package/.pbai4devs/scripts/verificar-configuracao/README.md +22 -0
  18. package/.pbai4devs/scripts/verificar-configuracao/SCRIPT.mjs +63 -0
  19. package/.pbai4devs/scripts/verificar-configuracao/tests/TEST.mjs +145 -0
  20. package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +12 -1
  21. package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +22 -0
  22. package/.pbai4devs/skills/desenvolvimento/implementar-backend/SKILL.md +15 -3
  23. package/.pbai4devs/skills/desenvolvimento/implementar-frontend/SKILL.md +15 -3
  24. package/.pbai4devs/skills/git/git-diff/SKILL.md +3 -1
  25. package/.pbai4devs/skills/git/git-status/SKILL.md +2 -1
  26. package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +13 -8
  27. package/.pbai4devs/skills/projeto/proximos-passos/SKILL.md +4 -2
  28. package/.pbai4devs/skills/specs/implementar-spec/SKILL.md +14 -2
  29. package/.pbai4devs/templates/clients/ADAPTERS.json +3 -2
  30. package/CHANGELOG.md +20 -0
  31. package/docs/guides/cli.md +9 -0
  32. package/docs/guides/troubleshooting.md +4 -0
  33. package/docs/specs/README.md +5 -0
  34. package/docs/specs/discovery-preservation/SPEC.md +26 -0
  35. package/docs/specs/preimplementation-order/SPEC.md +23 -0
  36. package/docs/specs/target-paths/SPEC.md +19 -0
  37. package/package.json +1 -1
  38. package/src/install.mjs +155 -17
  39. package/src/next.mjs +26 -9
@@ -21,7 +21,7 @@ const auxiliary = ['templates', 'scripts', 'tools', 'external-skills', 'state',
21
21
  const reserved = new Set(['.git', '.pbai4devs', '.agent-tests']);
22
22
  const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
23
23
  const sorted = values => [...new Set(values)].sort(compare);
24
- const layouts = new Set(['agrupado', 'plano', 'arquivo', 'mdc', 'instrucoes', 'prompt', 'agente']);
24
+ const layouts = new Set(['agrupado', 'plano', 'arquivo', 'mdc', 'instrucoes', 'prompt', 'agente', 'nenhum']);
25
25
  const fail = message => { throw new Error(message); };
26
26
  const normalize = text => text.replace(/\r\n?/g, '\n');
27
27
  const read = file => normalize(readFileSync(file, 'utf8'));
@@ -30,6 +30,77 @@ const stat = file => {
30
30
  catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code)) return null; throw error; }
31
31
  };
32
32
 
33
+ function replaceJsonMember(bytes, member, value) {
34
+ const text = bytes.toString('utf8');
35
+ if (!Buffer.from(text, 'utf8').equals(bytes)) fail('Invalid UTF-8 in derived state.');
36
+ let i = 0;
37
+ const space = () => { while (/\s/.test(text[i] || '') && i < text.length) i++; };
38
+ const stringEnd = () => {
39
+ if (text[i++] !== '"') fail('Invalid JSON member.');
40
+ while (i < text.length) {
41
+ const char = text[i++];
42
+ if (char === '\\') { i++; continue; }
43
+ if (char === '"') return;
44
+ }
45
+ fail('Unterminated JSON string.');
46
+ };
47
+ space(); if (text[i++] !== '{') fail('Invalid derived state.');
48
+ const properties = [];
49
+ while (true) {
50
+ space(); if (text[i] === '}') break;
51
+ const start = i; stringEnd(); const key = JSON.parse(text.slice(start, i));
52
+ space(); if (text[i++] !== ':') fail('Invalid JSON member.');
53
+ space(); const valueStart = i;
54
+ if (text[i] === '"') stringEnd();
55
+ else if (text[i] === '{' || text[i] === '[') {
56
+ const stack = [];
57
+ do {
58
+ const char = text[i];
59
+ if (char === '"') { stringEnd(); continue; }
60
+ i++;
61
+ if (char === '{' || char === '[') stack.push(char);
62
+ else if (char === '}' || char === ']') stack.pop();
63
+ } while (stack.length && i < text.length);
64
+ if (stack.length) fail('Invalid derived state.');
65
+ } else { while (i < text.length && text[i] !== ',' && text[i] !== '}') i++; while (i > valueStart && /\s/.test(text[i - 1])) i--; }
66
+ const end = i; properties.push({ key, start, valueStart, end });
67
+ space(); if (text[i] === ',') { i++; continue; } if (text[i] === '}') break;
68
+ fail('Invalid JSON member separator.');
69
+ }
70
+ const matches = properties.filter(item => item.key === member);
71
+ if (matches.length > 1) fail('Ambiguous derived state member.');
72
+ if (!matches.length) {
73
+ if (value === undefined) return bytes;
74
+ const insertion = `${properties.length ? ',' : ''}\n ${JSON.stringify(member)}: ${JSON.stringify(value, null, 2)}\n`;
75
+ const next = text.slice(0, i) + insertion + text.slice(i);
76
+ try { JSON.parse(next); } catch { fail('Unable to preserve derived state safely.'); }
77
+ return Buffer.from(next);
78
+ }
79
+ const current = matches[0], index = properties.indexOf(current);
80
+ let next;
81
+ if (value !== undefined) next = text.slice(0, current.valueStart) + JSON.stringify(value, null, 2) + text.slice(current.end);
82
+ else if (index > 0) next = text.slice(0, properties[index - 1].end) + text.slice(current.end);
83
+ else if (properties.length > 1) next = text.slice(0, current.start) + text.slice(properties[1].start);
84
+ else next = text.slice(0, current.start) + text.slice(current.end);
85
+ try { JSON.parse(next); } catch { fail('Unable to preserve derived state safely.'); }
86
+ return Buffer.from(next);
87
+ }
88
+
89
+
90
+ // Compare owned fields structurally, preserving all other JSON bytes.
91
+ function derivedContent(original, previous, next) {
92
+ if (!original) return JSON.stringify(next, null, 2) + '\n';
93
+ const keys = ['schemaVersion', 'entries', 'outputs', 'files', 'blocks', 'removedBlocks', 'removedFiles'];
94
+ const canonical = value => value && typeof value === 'object' && !Array.isArray(value)
95
+ ? Object.fromEntries(Object.keys(value).sort(compare).map(key => [key, canonical(value[key])]))
96
+ : Array.isArray(value) ? value.map(canonical) : value;
97
+ let result = original;
98
+ for (const key of keys) {
99
+ if (JSON.stringify(canonical(previous[key])) !== JSON.stringify(canonical(next[key]))) result = replaceJsonMember(result, key, next[key]);
100
+ }
101
+ return result.toString('utf8');
102
+ }
103
+
33
104
  export function header(text) {
34
105
  const lines = normalize(text).split('\n');
35
106
  if (lines[0] !== '---') fail('Frontmatter YAML ausente.');
@@ -76,7 +147,7 @@ function link(target, source) {
76
147
 
77
148
  export { resourcePath } from '../lib/resource-path.mjs';
78
149
 
79
- export function synchronize({ root = projectRoot, check = false, force = false, localIgnores = true, generateEntrypoints = true, recordDerivedState = true, output = console.log } = {}) {
150
+ export function synchronize({ root = projectRoot, check = false, force = false, localIgnores = true, generateEntrypoints = true, recordDerivedState = true, outputBlocked, output = console.log } = {}) {
80
151
  requireNode();
81
152
  root = realpathSync(resolve(root));
82
153
  const absolute = value => join(root, ...value.split('/'));
@@ -156,6 +227,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
156
227
  const protectedPath = value => /^(workflows(?:\/|$)|settings\.json$|extensions\.json$)/.test(value.slice(value.indexOf('/') + 1).toLowerCase());
157
228
  const agents = new Map();
158
229
  const formats = new Map();
230
+ const fallbacks = new Map();
159
231
  const discoveries = new Set();
160
232
  const ignoreRoots = new Set();
161
233
  const natives = [];
@@ -182,14 +254,20 @@ export function synchronize({ root = projectRoot, check = false, force = false,
182
254
  && !['readme.md', 'instructions.md'].includes(posix.basename(path).toLowerCase())
183
255
  && (/\.(md|mdc)$/i.test(path) || path.startsWith('.gemini/commands/') && path.endsWith('.toml'));
184
256
  };
257
+ const validIgnoreBlock = path => typeof path === 'string' && /^\.[A-Za-z0-9][A-Za-z0-9._-]*\/\.gitignore$/.test(path) && !reserved.has(path.split('/')[0].toLowerCase());
185
258
  for (const [name, values] of [['files', previousEntries.files ?? {}], ['blocks', previousEntries.blocks ?? {}]]) {
186
- if (!values || typeof values !== 'object' || Array.isArray(values) || Object.entries(values).some(([path, hash]) => !(validResourcePath(path) || name === 'blocks' && canonicalEntrypoint(path)) || !/^[a-f0-9]{64}$/.test(hash))) fail(`Registro de ${name} derivados invalido.`);
259
+ if (!values || typeof values !== 'object' || Array.isArray(values) || Object.entries(values).some(([path, hash]) => !(validResourcePath(path) || name === 'blocks' && (canonicalEntrypoint(path) || validIgnoreBlock(path))) || !/^[a-f0-9]{64}$/.test(hash))) fail(`Registro de ${name} derivados invalido.`);
187
260
  for (const path of Object.keys(values)) safe(canonicalEntrypoint(path) ? resolveEntrypoint(root, canonicalEntrypoint(path)) : path);
188
261
  }
189
262
  const removedBlocks = new Set(previousEntries.removedBlocks ?? []);
190
263
  if (!Array.isArray(previousEntries.removedBlocks ?? []) || [...removedBlocks].some(path => typeof path !== 'string' || !(validResourcePath(path) || canonicalEntrypoint(path)))) fail('Registro de blocos removidos invalido.');
191
264
  for (const path of removedBlocks) safe(canonicalEntrypoint(path) ? resolveEntrypoint(root, canonicalEntrypoint(path)) : path);
192
- const priorFiles = { ...installed?.files, ...previousEntries.files };
265
+ const removedFiles = previousEntries.removedFiles ?? {};
266
+ if (!removedFiles || typeof removedFiles !== 'object' || Array.isArray(removedFiles) || Object.entries(removedFiles).some(([path, hash]) => !validResourcePath(path) || !/^[a-f0-9]{64}$/.test(hash))) fail('Registro de arquivos removidos invalido.');
267
+ if (Object.entries(removedFiles).some(([path, hash]) => installed?.files?.[path] && installed.files[path] !== hash)) fail('Registro de arquivos removidos diverge do estado instalado.');
268
+ for (const path of Object.keys(removedFiles)) safe(path);
269
+ const effectiveRemovedFiles = Object.fromEntries(Object.entries(removedFiles).filter(([path, hash]) => installed?.files?.[path] === hash));
270
+ const priorFiles = { ...Object.fromEntries(Object.entries(installed?.files ?? {}).filter(([path, hash]) => effectiveRemovedFiles[path] !== hash)), ...previousEntries.files };
193
271
  const priorBlocks = { ...Object.fromEntries(Object.entries(installed?.blocks ?? {}).filter(([path]) => !removedBlocks.has(path))), ...previousEntries.blocks };
194
272
  const ownedPaths = new Set([...Object.keys(priorFiles), ...Object.keys(priorBlocks), ...Object.keys(previousEntries.entries), ...Object.keys(previousOutputs)]);
195
273
  const nativeRoots = new Set(rows.map(row => row.split('|')).filter(([type]) => ['agente', 'gitignore'].includes(type)).map(([, target]) => target));
@@ -200,6 +278,28 @@ export function synchronize({ root = projectRoot, check = false, force = false,
200
278
  const boundary = nativePathBoundary(root, value, ownedPaths, { preserveRootAliases: true });
201
279
  return boundary ? remember(boundary.path, boundary.reason) : false;
202
280
  }
281
+ const fallbackChecks = new Map();
282
+ function blockedOutput(path) {
283
+ if (outputBlocked) return outputBlocked(path);
284
+ let boundary = nativePathBoundary(root, path, ownedPaths, { preserveRootAliases: true });
285
+ if (!boundary) {
286
+ const info = stat(safe(path));
287
+ if (info && !info.isFile()) boundary = { path, reason: 'non-file' };
288
+ }
289
+ if (!boundary) return { blocked: false };
290
+ remember(boundary.path, boundary.reason);
291
+ const info = lstatSync(absolute(boundary.path));
292
+ return { blocked: true, ...boundary, identity: [info.dev, info.ino, info.mode, info.size, info.mtimeMs, info.ctimeMs] };
293
+ }
294
+ function unavailable(path) {
295
+ const status = blockedOutput(path);
296
+ if (fallbackChecks.has(path) && JSON.stringify(fallbackChecks.get(path)) !== JSON.stringify(status)) fail(`Caminho de skill mudou durante o planejamento: ${path}`);
297
+ fallbackChecks.set(path, status);
298
+ return status.blocked;
299
+ }
300
+ function assertFallbackChecks() {
301
+ for (const [path, status] of fallbackChecks) if (JSON.stringify(blockedOutput(path)) !== JSON.stringify(status)) fail(`Caminho de skill mudou apos o planejamento: ${path}`);
302
+ }
203
303
  function claimed(value) {
204
304
  if (ownedPaths.has(value)) return true;
205
305
  const info = stat(safe(value));
@@ -241,7 +341,8 @@ export function synchronize({ root = projectRoot, check = false, force = false,
241
341
  agents.set(target, mode);
242
342
  } else if (type === 'formato') {
243
343
  if (fields.length !== 4 || !Object.hasOwn(principals, source) || !layouts.has(metadata)) fail(`Formato invalido: ${row}`);
244
- if (source === 'skills' && !['agrupado', 'plano'].includes(metadata)) fail(`Skills exigem formato agrupado ou plano: ${row}`);
344
+ if (source === 'skills' && !['agrupado', 'plano', 'nenhum'].includes(metadata)) fail(`Skills exigem formato agrupado, plano ou nenhum: ${row}`);
345
+ if (metadata === 'nenhum' && source !== 'skills') fail(`Formato incompativel com recurso: ${row}`);
245
346
  if ((metadata === 'mdc' || metadata === 'instrucoes') && source !== 'rules' || metadata === 'prompt' && source !== 'commands' || metadata === 'agente' && source !== 'agents') fail(`Formato incompativel com recurso: ${row}`);
246
347
  const key = `${target}/${source}`;
247
348
  if (formats.has(key)) fail(`Formato duplicado: ${key}`);
@@ -249,6 +350,9 @@ export function synchronize({ root = projectRoot, check = false, force = false,
249
350
  } else if (type === 'descoberta') {
250
351
  if (fields.length !== 3 || target !== '.agents' || source !== 'antigravity' || discoveries.has(target)) fail(`Descoberta invalida: ${row}`);
251
352
  discoveries.add(target);
353
+ } else if (type === 'fallback') {
354
+ if (fields.length !== 4 || metadata !== 'skills' || target === source || fallbacks.has(target)) fail(`Fallback invalido: ${row}`);
355
+ fallbacks.set(target, source);
252
356
  } else if (type === 'gitignore') {
253
357
  if (fields.length !== 2) fail('gitignore aceita somente a pasta de destino.');
254
358
  ignoreRoot(target);
@@ -269,6 +373,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
269
373
  }
270
374
  if (!agents.size) fail('Nenhum agente cadastrado.');
271
375
  for (const key of formats.keys()) if (!agents.has(key.split('/')[0])) fail(`Formato fora dos agentes: ${key}`);
376
+ for (const [target, source] of fallbacks) if (!agents.has(target) || !agents.has(source) || formats.get(`${target}/skills`) !== 'nenhum' || formats.get(`${source}/skills`) === 'nenhum') fail(`Fallback fora dos agentes: ${target}`);
272
377
  for (const root of discoveries) if (!agents.has(root)) fail(`Descoberta fora dos agentes: ${root}`);
273
378
  const managed = value => [...agents.keys()].some(agent => value.startsWith(agent + '/'));
274
379
  const built = buildCatalog(root);
@@ -287,7 +392,19 @@ export function synchronize({ root = projectRoot, check = false, force = false,
287
392
  }
288
393
  for (const [source, kind] of sources) {
289
394
  for (const [agent, mode] of agents) {
290
- let path = resourcePath(source, kind, formats.get(`${agent}/${kind}`) || mode);
395
+ const format = formats.get(`${agent}/${kind}`) || mode;
396
+ if (format === 'nenhum') {
397
+ const primary = kind === 'skills' && fallbacks.get(agent);
398
+ if (!primary) continue;
399
+ const primaryFormat = formats.get(`${primary}/${kind}`) || agents.get(primary);
400
+ const primaryTarget = `${primary}/${resourcePath(source, kind, primaryFormat)}`;
401
+ if (!unavailable(primaryTarget)) continue;
402
+ const target = `${agent}/${resourcePath(source, kind, mode)}`;
403
+ if (unavailable(target)) fail(`Skill indisponivel: ${source}; caminhos ${primaryTarget} e ${target} obstruidos.`);
404
+ add(target, source);
405
+ continue;
406
+ }
407
+ let path = resourcePath(source, kind, format);
291
408
  if (agent === '.gemini' && kind === 'commands') path = path.replace(/\.md$/, '.toml');
292
409
  add(`${agent}/${path}`, source);
293
410
  }
@@ -385,7 +502,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
385
502
  const unchanged = current === null || priorHashes.includes(createHash('sha256').update(current).digest('hex'));
386
503
  if (!unchanged) { remember(manifest, 'edited-generated-file'); continue; }
387
504
  const nextHash = createHash('sha256').update(content).digest('hex');
388
- if (installed?.files?.[manifest] !== nextHash) discoveryOutputs[manifest] = nextHash;
505
+ if (installed?.files?.[manifest] !== nextHash || Object.hasOwn(previousOutputs, manifest)) discoveryOutputs[manifest] = nextHash;
389
506
  if (!info || readFileSync(absolute(manifest), 'utf8') !== content) {
390
507
  if (info && force) backup.add(manifest);
391
508
  registries.set(manifest, content);
@@ -416,6 +533,10 @@ export function synchronize({ root = projectRoot, check = false, force = false,
416
533
  merged = mergeGitignore(current, patterns);
417
534
  }
418
535
  catch (error) { fail(`${target}: ${error.message}`); }
536
+ if (target !== '.pbai4devs/.gitignore') {
537
+ const start = merged.text.indexOf(ignoreBegin), finish = merged.text.indexOf(ignoreEnd);
538
+ if (start >= 0 && finish > start) managedBlocks[target] = createHash('sha256').update(merged.text.slice(start, finish + ignoreEnd.length)).digest('hex');
539
+ }
419
540
  if (merged.text !== current) {
420
541
  ignoreFiles.set(target, merged.text);
421
542
  if (force && info && merged.hasBlock) backup.add(target);
@@ -568,8 +689,11 @@ export function synchronize({ root = projectRoot, check = false, force = false,
568
689
  if (Object.keys(derivedBlocks).length) next.blocks = derivedBlocks;
569
690
  for (const path of Object.keys(managedBlocks)) removedBlocks.delete(path);
570
691
  if (removedBlocks.size) next.removedBlocks = sorted(removedBlocks);
571
- const registryContent = JSON.stringify(next, null, 2) + '\n';
572
- if (recordDerivedState && (Object.keys(nextEntries).length || Object.keys(discoveryOutputs).length || Object.keys(generatedFiles).length || Object.keys(derivedBlocks).length || removedBlocks.size || stat(safe(entryRegistry))) && (!stat(safe(entryRegistry)) || readFileSync(absolute(entryRegistry), 'utf8') !== registryContent)) registries.set(entryRegistry, registryContent);
692
+ for (const path of expected.keys()) delete effectiveRemovedFiles[path];
693
+ for (const path of remove) if (validResourcePath(path) && installed?.files?.[path] && !expected.has(path)) effectiveRemovedFiles[path] = installed.files[path];
694
+ if (Object.keys(effectiveRemovedFiles).length) next.removedFiles = Object.fromEntries(Object.entries(effectiveRemovedFiles).sort(([a], [b]) => compare(a, b)));
695
+ const registryContent = derivedContent(registryBytes, previousEntries, next);
696
+ if (recordDerivedState && (Object.keys(nextEntries).length || Object.keys(discoveryOutputs).length || Object.keys(generatedFiles).length || Object.keys(derivedBlocks).length || removedBlocks.size || Object.keys(effectiveRemovedFiles).length || stat(safe(entryRegistry))) && (!stat(safe(entryRegistry)) || readFileSync(absolute(entryRegistry), 'utf8') !== registryContent)) registries.set(entryRegistry, registryContent);
573
697
  const ownedOutputs = sorted([...targets, ...built.outputs.keys(), '.pbai4devs/espelhos.conf', ...rootEntries.keys(), ...(recordDerivedState && (stat(safe(entryRegistry)) || registries.has(entryRegistry)) ? [entryRegistry] : []),
574
698
  ...[...discoveries].flatMap(agent => ['skills', 'rules', 'agents'].map(kind => `${agent}/${kind}.json`)),
575
699
  ...[...ignoreTemplates.keys()].filter(target => {
@@ -633,6 +757,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
633
757
  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) };
634
758
  const dirty = Object.values(summary).some(Boolean);
635
759
  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.`);
760
+ assertFallbackChecks();
636
761
  if (localStatus.status === 'no-git') output('Git ausente: apos git init, execute a sincronizacao para proteger os arquivos locais.');
637
762
  const preservedPaths = sorted(preserved.keys()).map(path => preserved.get(path));
638
763
  if (check) return { ...summary, ownedOutputs, managedBlocks, preserved: preservedPaths, exitCode: dirty ? 1 : 0 };
@@ -700,6 +825,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
700
825
  chmodSync(safe(value), content.mode);
701
826
  }
702
827
  try {
828
+ assertFallbackChecks();
703
829
  if (localPlan) assertLocalIgnoreUnchanged(localPlan);
704
830
  if (Object.keys(affectedHashes).length) {
705
831
  if (!assertGitReadableForChanges() && hadGit) fail('Git deixou de estar disponivel antes da sincronizacao; arquivos preservados.');
@@ -712,6 +838,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
712
838
  for (const [value, text] of ignoreFiles) mutate(value, () => atomic(value, text), text);
713
839
  for (const [value, text] of rootEntryChanges) mutate(value, () => atomic(value, text), text);
714
840
  if (configChanged) mutate(config, () => atomic(config, finalConfig), finalConfig);
841
+ assertFallbackChecks();
715
842
  // Directory ownership is not recorded: keep even empty parent directories.
716
843
  if (localPlan) applyLocalIgnore(localPlan);
717
844
  } catch (error) {
@@ -122,6 +122,36 @@ test('conteúdo humano concorrente não é apagado durante rollback', t => {
122
122
  assert.equal(fs.readFileSync(join(root, generated), 'utf8'), human);
123
123
  });
124
124
 
125
+ test('falha ao registrar remoção derivada restaura fallback e preserva índice', t => {
126
+ const root = fixture(t), fallback = '.codex/skills/demo/SKILL.md';
127
+ write(root, '.pbai4devs/espelhos.conf', [
128
+ 'agente|.agents|agrupado', 'agente|.codex|plano',
129
+ 'formato|.codex|skills|nenhum', 'fallback|.codex|.agents|skills',
130
+ ].join('\n') + '\n');
131
+ write(root, '.agents/skills', 'obstrução humana');
132
+ write(root, '.gitignore', '# ignore humano\n');
133
+ assert.equal(spawnSync('git', ['-C', root, 'add', '.gitignore']).status, 0);
134
+ run(root);
135
+ const initial = fs.readFileSync(join(root, fallback));
136
+ write(root, '.pbai4devs/state/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: { [fallback]: hash(initial) }, blocks: {} }));
137
+ fs.rmSync(join(root, '.pbai4devs/state/derived.json'), { force: true });
138
+ fs.rmSync(join(root, '.agents/skills')); // A obstrução pertence à fixture.
139
+ const before = snapshot(root), index = fs.readFileSync(join(root, '.git/index'));
140
+ const originalRename = fs.renameSync;
141
+ let injected = false;
142
+ t.mock.method(fs, 'renameSync', (from, to) => {
143
+ if (!injected && to === join(root, '.pbai4devs/state/derived.json')) { injected = true; throw new Error('falha injetada no registro derivado'); }
144
+ return originalRename(from, to);
145
+ });
146
+ syncBuiltinESMExports();
147
+ t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
148
+ assert.throws(() => run(root), /falha injetada no registro derivado/);
149
+ assert.equal(injected, true);
150
+ assert.deepEqual(snapshot(root), before);
151
+ assert.deepEqual(fs.readFileSync(join(root, '.git/index')), index);
152
+ assert.deepEqual(fs.readFileSync(join(root, fallback)), initial);
153
+ });
154
+
125
155
  test('conteúdo humano é preservado quando a escrita lança erro após promover o arquivo', t => {
126
156
  const root = fixture(t), rename = fs.renameSync;
127
157
  const failure = new Error('falha depois de promover');
@@ -913,6 +913,9 @@ test('formatos desconhecidos ou fora do cliente falham antes de mutar', t => {
913
913
  for (const row of [
914
914
  'formato|.missing|skills|agrupado', 'formato|.codex|unknown|agrupado',
915
915
  'formato|.codex|skills|mdc', 'formato|.codex|rules|prompt',
916
+ 'formato|.codex|agents|nenhum', 'formato|.codex|commands|nenhum',
917
+ 'fallback|.codex|.missing|skills', 'fallback|.codex|.codex|skills',
918
+ 'fallback|.codex|.agents|rules',
916
919
  'formato|.codex|skills|plano\nformato|.codex|skills|agrupado',
917
920
  'descoberta|.codex|antigravity', 'descoberta|.agents|antigravity',
918
921
  ]) {
@@ -922,3 +925,22 @@ test('formatos desconhecidos ou fora do cliente falham antes de mutar', t => {
922
925
  assert.equal(resourcePath('.pbai4devs/commands/team/check/COMMAND.md', 'commands', 'arquivo'), 'commands/team/check.md');
923
926
  assert.throws(() => resourcePath(source, 'skills', 'arquivo'), /Skills/);
924
927
  });
928
+
929
+ test('sincronização direta recusa troca concorrente da obstrução de fallback', t => {
930
+ const root = fixture(t, true);
931
+ write(root, source, skill);
932
+ write(root, '.pbai4devs/espelhos.conf', [
933
+ 'agente|.agents|agrupado', 'agente|.codex|agrupado',
934
+ 'formato|.codex|skills|nenhum', 'fallback|.codex|.agents|skills',
935
+ ].join('\n') + '\n');
936
+ write(root, '.agents/skills/projeto', 'Obstrução humana.');
937
+ let called = false;
938
+ assert.throws(() => synchronize({ root, localIgnores: false, generateEntrypoints: false, output() {
939
+ if (called) return;
940
+ called = true;
941
+ rmSync(join(root, '.agents/skills/projeto')); // Simula mudança humana após o planejamento.
942
+ } }), /mudou apos o planejamento/);
943
+ assert.equal(called, true);
944
+ assert.equal(existsSync(join(root, '.codex')), false);
945
+ assert.equal(existsSync(join(root, '.agents/skills/projeto/demo/SKILL.md')), false);
946
+ });
@@ -14,19 +14,21 @@ node .pbai4devs/scripts/testar-maturidade/SCRIPT.mjs run --case legado
14
14
  node --test .pbai4devs/scripts/testar-maturidade/tests/TEST.mjs
15
15
  ```
16
16
 
17
- `prepare` só gera as cópias; `run` gera e executa. Os oito cenários ficam em
18
- `examples/generated/`: agentes, descoberta, planejamento, aplicação inicial,
19
- configurada, manutenção, legado e monorepo com caminhos em português.
17
+ `prepare` só gera as cópias; `run` gera e executa. Cada execução cria uma nova
18
+ pasta `examples/generated/maturity-*` e informa seu caminho no console. Por
19
+ padrão são oito cenários: agentes, descoberta, planejamento, aplicação inicial,
20
+ configurada, manutenção, legado e monorepo com caminhos em português. Use
21
+ `--case` para executar apenas um.
20
22
 
21
23
  O harness copia recursos e configurações reais; exclui pacotes, caches, logs,
22
24
  resultados de testes, Playwright e pastas temporárias em qualquer caixa,
23
25
  `.git` e nomes conhecidos de credenciais/overrides locais. Não faz varredura
24
26
  semântica de segredos. A API também aceita `tmp/examples/` para compatibilidade
25
- e descendentes de `.agent-tests/`. Regenera apenas pastas identificadas como
26
- suas. Cada exemplo recebe Git local, sem commits nem identidade alterada.
27
- O diretório gerado contém um ignore próprio, inclusive em projetos consumidores.
28
- `prepare` redefine o relatório como `prepared`, sem conservar provas antigas.
29
- Se a preparação falhar, o índice fica em `preparing` e o relatório antigo sai.
27
+ e descendentes de `.agent-tests/`. Um destino explícito deve ser inexistente;
28
+ execuções e projetos anteriores não são substituídos nem removidos. Cada
29
+ exemplo recebe Git local, sem commits nem identidade alterada. O diretório
30
+ gerado contém um ignore próprio, inclusive em projetos consumidores. Se a
31
+ preparação falhar, o índice da nova execução permanece em `preparing`.
30
32
  O hub `.pbai4devs` e os clientes registrados são copiados, incluindo comandos
31
33
  nativos em TOML. Sem manifesto local, os templates definem os clientes em
32
34
  memória, respeitando a seleção salva. `.vscode` exige registro. Apenas as entradas
@@ -50,5 +52,5 @@ O gate positivo simula o parecer para testar seu contrato; não representa
50
52
  revisão humana. Estados de aprovação são dados sintéticos. Nenhum teste
51
53
  certifica produção, Docker, interface visual ou outro sistema operacional.
52
54
 
53
- Índice e relatório: `examples/generated/README.md` e `REPORT.json`. Logs e provas
54
- individuais ficam em `.agent-tests/` dentro de cada exemplo, todos ignorados.
55
+ Índice e relatório ficam em `README.md` e `REPORT.json` na pasta informada.
56
+ Logs e provas ficam em `.agent-tests/` dentro de cada exemplo, todos ignorados.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { createHash } from 'node:crypto';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
3
  import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
- import { dirname, join, relative, resolve, sep } from 'node:path';
4
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { spawnSync } from 'node:child_process';
7
7
  import { isMain, projectRoot, requireNode } from '../lib/runtime.mjs';
@@ -9,6 +9,7 @@ import { isGeneratedOutput } from '../lib/gitignore.mjs';
9
9
  import { resourcePath, synchronize } from '../sincronizar-agentes/SCRIPT.mjs';
10
10
  import { adapterManifest } from '../lib/adapters.mjs';
11
11
  import { stateDirectory } from '../lib/state-path.mjs';
12
+ import { nativePathBoundary } from '../lib/native-paths.mjs';
12
13
 
13
14
  const assets = join(dirname(fileURLToPath(import.meta.url)), 'assets');
14
15
  const owner = 'testar-maturidade';
@@ -36,6 +37,39 @@ function noLinks(root, target) {
36
37
  }
37
38
  }
38
39
 
40
+ // The child binds its cwd to the verified parent directory. A swapped parent
41
+ // changes the inode or real path before the child can create the new directory.
42
+ const createDirectoryScript = `const fs=require('node:fs');
43
+ const [name,expected,dev,ino]=process.argv.slice(1);
44
+ const info=fs.statSync('.');
45
+ if(fs.realpathSync('.')!==expected || String(info.dev)!==dev || String(info.ino)!==ino) process.exit(73);
46
+ try { fs.mkdirSync(name); } catch(error) { if(error.code==='EEXIST') process.exit(17); throw error; }
47
+ `;
48
+ function createChild(root, parent, name, { allowExisting = false } = {}) {
49
+ if (!name || name === '.' || name === '..' || basename(name) !== name) fail('Nome de diretório inválido.');
50
+ noLinks(root, parent);
51
+ const info = stat(parent);
52
+ if (!info?.isDirectory() || info.isSymbolicLink()) fail('Diretório ancestral inválido.');
53
+ const canonical = realpathSync(parent), inside = relative(root, canonical);
54
+ if (isAbsolute(inside) || inside === '..' || inside.startsWith(`..${sep}`)) fail('Diretório ancestral fora do projeto.');
55
+ const env = { ...process.env }; delete env.NODE_TEST_CONTEXT;
56
+ const result = spawnSync(process.execPath, ['-e', createDirectoryScript, name, canonical, String(info.dev), String(info.ino)], { cwd: canonical, env, encoding: 'utf8', timeout: 30000, windowsHide: true, shell: false });
57
+ if (result.error || result.status !== 0 && !(allowExisting && result.status === 17)) fail(`Não foi possível criar diretório exclusivo: ${result.error?.message || result.stderr?.trim() || result.status}`);
58
+ noLinks(root, join(parent, name));
59
+ const output = stat(join(parent, name));
60
+ if (!output?.isDirectory() || output.isSymbolicLink()) fail('Diretório de saída inválido.');
61
+ }
62
+ function ensureDirectories(root, target) {
63
+ let current = root;
64
+ for (const part of relative(root, target).split(sep).filter(Boolean)) {
65
+ const next = join(current, part), info = stat(next);
66
+ if (!info) createChild(root, current, part, { allowExisting: true });
67
+ else if (!info.isDirectory() || info.isSymbolicLink()) fail('Diretório ancestral inválido ou link simbólico.');
68
+ noLinks(root, next);
69
+ current = next;
70
+ }
71
+ }
72
+
39
73
  export const SCENARIOS = [
40
74
  { id: 'somente-agentes', title: 'Somente agentes', expected: 'preparacao', complete: false, app: false, planning: 'none', note: 'Recursos compartilhados, sem produto ou aplicação.' },
41
75
  { id: 'descoberta', title: 'Descoberta de produto', expected: 'descoberta', complete: false, app: false, planning: 'draft', note: 'PRD em rascunho; nenhuma stack é iniciada.' },
@@ -63,11 +97,28 @@ export function registeredRoots(root) {
63
97
 
64
98
  export function resourceTargets(root, source, kind = 'skills') {
65
99
  const rows = manifest(root).split(/\r?\n/).filter(line => line && !line.startsWith('#')).map(line => line.split('|'));
66
- const targets = [];
100
+ const targets = [], suppressed = new Set();
101
+ for (const [type, fallback, primary, resource] of rows) {
102
+ if (type !== 'fallback' || resource !== kind) continue;
103
+ const primaryRow = rows.find(([entry, target]) => entry === 'agente' && target === primary);
104
+ const fallbackRow = rows.find(([entry, target]) => entry === 'agente' && target === fallback);
105
+ if (!primaryRow || !fallbackRow) fail(`Fallback sem cliente: ${fallback}`);
106
+ const primaryFormat = rows.find(([entry, target, scope]) => entry === 'formato' && target === primary && scope === kind)?.[3] || primaryRow[2] || 'agrupado';
107
+ const primaryTarget = `${primary}/${resourcePath(source, kind, primaryFormat)}`;
108
+ const boundary = nativePathBoundary(root, primaryTarget, [], { preserveRootAliases: true });
109
+ const info = boundary ? null : stat(join(root, primaryTarget));
110
+ if (boundary || (info && !info.isFile())) {
111
+ suppressed.add(primaryTarget);
112
+ targets.push(`${fallback}/${resourcePath(source, kind, fallbackRow[2] || 'agrupado')}`);
113
+ }
114
+ }
67
115
  for (const [type, agent, layout = 'agrupado'] of rows) {
68
116
  if (type !== 'agente') continue;
69
117
  const override = rows.find(([entry, target, resource]) => entry === 'formato' && target === agent && resource === kind)?.[3];
70
- targets.push(`${agent}/${resourcePath(source, kind, override || layout)}`);
118
+ if (override === 'nenhum') continue;
119
+ let target = `${agent}/${resourcePath(source, kind, override || layout)}`;
120
+ if (agent === '.gemini' && kind === 'commands') target = target.replace(/\.md$/, '.toml');
121
+ if (!suppressed.has(target)) targets.push(target);
71
122
  }
72
123
  for (const [type, target, origin] of rows) if (type === 'nativo' && origin === source) targets.push(target);
73
124
  return [...new Set(targets)];
@@ -139,29 +190,6 @@ export function copyAgentStructure(sourceRoot, destination) {
139
190
  return { roots, copiedFiles: files.length, skipped, files, sourceDigest, entrypoints, toolDefinitions: files.filter(file => /^\.pbai4devs\/tools\/[^/]+\/TOOL\.json$/.test(file)).length };
140
191
  }
141
192
 
142
- function ownership(root, file, expectedId) {
143
- if (stat(root)?.isSymbolicLink()) return false;
144
- const path = join(root, file);
145
- if (!existsSync(path)) return false;
146
- const item = lstatSync(path);
147
- if (!item.isFile() || item.isSymbolicLink()) return false;
148
- try { const data = json(path); return data.owner === owner && (!expectedId || data.id === expectedId); }
149
- catch { return false; }
150
- }
151
-
152
- function ensureOutput(output) {
153
- if (existsSync(output) && lstatSync(output).isSymbolicLink()) fail('A pasta de exemplos não pode ser um link.');
154
- for (const name of [suiteMarker, 'README.md', 'REPORT.json', '.gitignore']) {
155
- const item = stat(join(output, name));
156
- if (item && (!item.isFile() || item.isSymbolicLink())) fail(`Arquivo de saída deve ser regular: ${name}`);
157
- }
158
- if (existsSync(output) && [suiteMarker, 'README.md', 'REPORT.json'].some(name => existsSync(join(output, name)))
159
- && !ownership(output, suiteMarker)) fail('Índice de exemplos existente não pertence a este gerador.');
160
- if (existsSync(join(output, 'REPORT.json')) && !ownership(output, 'REPORT.json')) fail('Relatório de exemplos existente não pertence a este gerador.');
161
- if (existsSync(join(output, '.gitignore')) && readFileSync(join(output, '.gitignore'), 'utf8') !== outputIgnore) fail('Ignore de exemplos existente não pertence a este gerador.');
162
- mkdirSync(output, { recursive: true });
163
- }
164
-
165
193
  function planning(root, scenario) {
166
194
  const stateRoot = stateDirectory(root);
167
195
  const state = `${stateRoot}/configuracao/ESTADO.md`;
@@ -228,6 +256,13 @@ function application(root, scenario) {
228
256
  preserved.push(server, test, 'config/legacy.json', '.github/workflows/legacy.yml', '.vscode/settings.json', 'Makefile', 'package.json');
229
257
  }
230
258
  if (scenario.configured) {
259
+ // Published packages and existing projects need not contain these files;
260
+ // create only the missing synthetic fixtures, never replace copied content.
261
+ for (const [file, content] of Object.entries({
262
+ '.gitignore': '# Arquivos temporários do cenário sintético.\nnode_modules/\n.agent-tests/\n',
263
+ '.gitattributes': '* text=auto\n',
264
+ '.editorconfig': 'root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\n',
265
+ })) if (!stat(join(root, file))) write(root, file, content);
231
266
  write(root, '.dockerignore', '.git\n.agent-tests\nnode_modules\n');
232
267
  write(root, '.env.example', 'PORT=3000\n');
233
268
  write(root, 'Dockerfile', 'FROM node:24-alpine\nWORKDIR /app\nCOPY package.json ./\nCOPY src ./src\nEXPOSE 3000\nCMD ["node", "src/server.mjs"]\n');
@@ -242,31 +277,37 @@ function gitInit(root) {
242
277
  if (result.error || result.status !== 0) fail(`Git init: ${result.error?.message || result.stderr}`);
243
278
  }
244
279
 
245
- export function prepareExamples({ sourceRoot = projectRoot, outputRoot = defaultOutput(sourceRoot), scenarios = SCENARIOS, initializeGit = true, renderResources = prepareResources } = {}) {
246
- requireNode(); sourceRoot = realpathSync(resolve(sourceRoot)); outputRoot = resolve(outputRoot);
247
- const outputPath = portable(relative(sourceRoot, outputRoot));
280
+ export function prepareExamples({ sourceRoot = projectRoot, outputRoot, scenarios = SCENARIOS, initializeGit = true, renderResources = prepareResources } = {}) {
281
+ requireNode(); sourceRoot = realpathSync(resolve(sourceRoot));
282
+ const base = resolve(outputRoot ?? defaultOutput(sourceRoot));
283
+ const outputPath = portable(relative(sourceRoot, base));
248
284
  if (!(outputPath === 'examples/generated' || outputPath.startsWith('examples/generated/')
249
285
  || outputPath === 'tmp/examples' || outputPath.startsWith('tmp/examples/') || outputPath.startsWith('.agent-tests/'))) fail('Saída permitida somente em examples/generated, tmp/examples ou descendente de .agent-tests.');
250
- noLinks(sourceRoot, outputRoot);
286
+ noLinks(sourceRoot, base);
251
287
  const ids = new Set();
288
+ if (!Array.isArray(scenarios) || !scenarios.length) fail('Selecione pelo menos um cenário.');
252
289
  for (const scenario of scenarios) {
253
290
  if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(scenario.id) || ids.has(scenario.id)) fail('Cenários devem ter IDs únicos e seguros.');
254
291
  ids.add(scenario.id);
255
292
  }
256
- ensureOutput(outputRoot);
257
- for (const scenario of scenarios) {
258
- const destination = join(outputRoot, scenario.id);
259
- noLinks(sourceRoot, destination);
260
- if (existsSync(destination) && !ownership(destination, marker, scenario.id)) fail(`Diretório existente não é gerenciado: ${destination}`);
293
+ if (outputRoot !== undefined && stat(base)) fail(`Pasta de saída já existe; escolha um destino novo: ${base}`);
294
+ if (outputRoot !== undefined) {
295
+ for (let parent = dirname(base); parent !== sourceRoot && parent.startsWith(sourceRoot + sep); parent = dirname(parent)) {
296
+ if (stat(join(parent, suiteMarker)) || stat(join(parent, marker))) fail('Saída não pode ficar dentro de uma execução anterior.');
297
+ }
261
298
  }
299
+ const parent = outputRoot === undefined ? base : dirname(base);
300
+ ensureDirectories(sourceRoot, parent);
301
+ const name = outputRoot === undefined ? `maturity-${randomUUID()}` : basename(base);
302
+ createChild(sourceRoot, parent, name);
303
+ outputRoot = join(parent, name);
262
304
  write(outputRoot, suiteMarker, JSON.stringify({ owner, version: 1 }, null, 2) + '\n');
263
305
  write(outputRoot, '.gitignore', outputIgnore);
264
- rmSync(join(outputRoot, 'REPORT.json'), { force: true });
265
306
  writeIndex(outputRoot, [], 'preparing');
266
307
  const examples = [];
267
308
  for (const scenario of scenarios) {
268
309
  const destination = join(outputRoot, scenario.id);
269
- rmSync(destination, { recursive: true, force: true }); mkdirSync(destination, { recursive: true });
310
+ mkdirSync(destination);
270
311
  write(destination, marker, JSON.stringify({ owner, version: 1, id: scenario.id }, null, 2) + '\n');
271
312
  const copied = copyAgentStructure(sourceRoot, destination);
272
313
  planning(destination, scenario);
@@ -283,8 +324,8 @@ export function prepareExamples({ sourceRoot = projectRoot, outputRoot = default
283
324
 
284
325
  function writeIndex(outputRoot, examples, status) {
285
326
  const rows = examples.map(item => `| [${item.id}](${item.id}/README.md) | ${item.expected} | ${item.status || 'prepared'} |`).join('\n');
286
- const progress = status === 'preparing' ? '\nPreparação em andamento; os resultados anteriores foram invalidados.\n' : '';
287
- write(outputRoot, 'README.md', document('Exemplos de maturidade', `Projetos sintéticos com cópias locais da estrutura de agentes.\nNenhum resultado certifica produção, revisão humana ou outros sistemas operacionais.\n${progress}\n| Cenário | Nível esperado | Resultado |\n| --- | --- | --- |${rows ? '\n' + rows : ''}\n\nRelatório: \`REPORT.json\`. Logs e evidências por projeto ficam em \`.agent-tests/\`.\nPastas sem marcador deste gerador são preservadas.\n`, `status: ${status}\n`));
327
+ const progress = status === 'preparing' ? '\nPreparação em andamento nesta execução.\n' : '';
328
+ write(outputRoot, 'README.md', document('Exemplos de maturidade', `Projetos sintéticos com cópias locais da estrutura de agentes.\nNenhum resultado certifica produção, revisão humana ou outros sistemas operacionais.\n${progress}\n| Cenário | Nível esperado | Resultado |\n| --- | --- | --- |${rows ? '\n' + rows : ''}\n\nRelatório: \`REPORT.json\`. Logs e evidências por projeto ficam em \`.agent-tests/\`.\nCada execução usa uma pasta nova e preserva as anteriores.\n`, `status: ${status}\n`));
288
329
  }
289
330
 
290
331
  function hashes(root, files) { return Object.fromEntries(files.map(file => [file, digest(readFileSync(join(root, file)))])); }
@@ -453,7 +494,7 @@ export function testExample(example) {
453
494
  export function runExamples(options = {}) {
454
495
  const examples = prepareExamples(options);
455
496
  const results = examples.map(example => { const result = testExample(example); console.log(`${example.id}=${result.status}${result.error ? ': ' + result.error : ''}`); return result; });
456
- const outputRoot = resolve(options.outputRoot || defaultOutput(options.sourceRoot || projectRoot));
497
+ const outputRoot = dirname(examples[0].root);
457
498
  const report = { owner, version: 1, status: results.every(item => item.status === 'passed') ? 'passed' : 'failed', platform: process.platform, arch: process.arch, node: process.version, results, limits: ['Provas reais somente das operações descritas e APIs locais.', 'Pareceres do gate são simulações explícitas, sem revisão humana.', 'Não certifica produção nem sistemas operacionais não executados.'] };
458
499
  write(outputRoot, 'REPORT.json', JSON.stringify(report, null, 2) + '\n');
459
500
  writeIndex(outputRoot, examples.map(example => ({ ...example, status: results.find(result => result.id === example.id).status })), report.status);
@@ -462,15 +503,16 @@ export function runExamples(options = {}) {
462
503
 
463
504
  export function main(args = process.argv.slice(2)) {
464
505
  requireNode(); const [mode = 'help', ...rest] = args;
465
- if (['help', '--help', '-h'].includes(mode)) { console.log('Uso: testar-maturidade prepare|run [--case <id>]. Exemplos gerados em examples/generated; apenas pastas marcadas são substituídas.'); return 0; }
506
+ if (['help', '--help', '-h'].includes(mode)) { console.log('Uso: testar-maturidade prepare|run [--case <id>]. Cada execução cria uma nova pasta em examples/generated.'); return 0; }
466
507
  if (!['prepare', 'run'].includes(mode)) fail('Use prepare ou run.');
467
508
  let scenarios = SCENARIOS;
468
509
  if (rest.length) {
469
510
  if (rest.length !== 2 || rest[0] !== '--case') fail('Use --case <id>.');
470
511
  scenarios = SCENARIOS.filter(item => item.id === rest[1]); if (!scenarios.length) fail('Cenário desconhecido.');
471
512
  }
472
- if (mode === 'prepare') { const examples = prepareExamples({ scenarios }); console.log(`prepared=${examples.length}`); return 0; }
473
- return runExamples({ scenarios }).report.status === 'passed' ? 0 : 1;
513
+ if (mode === 'prepare') { const examples = prepareExamples({ scenarios }); console.log(`prepared=${examples.length}\noutput=${dirname(examples[0].root)}`); return 0; }
514
+ const result = runExamples({ scenarios }); console.log(`output=${result.outputRoot}`);
515
+ return result.report.status === 'passed' ? 0 : 1;
474
516
  }
475
517
 
476
518
  if (isMain(import.meta.url)) {