@pathbit/agentdevkit 0.0.11 → 0.0.13

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 (32) hide show
  1. package/.pbai4devs/rules/documentacao/RULE.md +2 -0
  2. package/.pbai4devs/scripts/lib/README.md +8 -0
  3. package/.pbai4devs/scripts/lib/gitignore.mjs +66 -7
  4. package/.pbai4devs/scripts/lib/managed-content.mjs +86 -0
  5. package/.pbai4devs/scripts/lib/managed-toml.mjs +192 -0
  6. package/.pbai4devs/scripts/sincronizar-agentes/README.md +9 -7
  7. package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +133 -38
  8. package/.pbai4devs/scripts/sincronizar-agentes/tests/COEXISTENCE.mjs +25 -15
  9. package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +7 -2
  10. package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +90 -4
  11. package/.pbai4devs/scripts/sincronizar-agentes/tests/MANAGED_CONTENT.mjs +318 -0
  12. package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +47 -26
  13. package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +43 -7
  14. package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +3 -2
  15. package/CHANGELOG.md +14 -0
  16. package/README.en.md +2 -2
  17. package/README.es.md +2 -2
  18. package/README.md +7 -3
  19. package/docs/guides/cli.md +2 -1
  20. package/docs/guides/installation.md +27 -8
  21. package/docs/guides/troubleshooting.md +8 -5
  22. package/docs/maintainers/maintenance.md +5 -0
  23. package/docs/maintainers/packages.md +2 -2
  24. package/docs/product/design.md +10 -3
  25. package/docs/product/prd.md +2 -1
  26. package/docs/product/roadmap.md +2 -0
  27. package/docs/specs/README.md +5 -0
  28. package/docs/specs/additive-integration/README.md +9 -0
  29. package/docs/specs/additive-integration/SPEC.md +53 -0
  30. package/docs/specs/real-projects/SPEC.md +26 -0
  31. package/package.json +1 -1
  32. package/src/install.mjs +139 -63
@@ -8,6 +8,8 @@ description: Documentação e revisão fazem parte de toda entrega implementada.
8
8
  Antes de criar, organizar ou alterar documentação, use `atualizar-documentacao`:
9
9
  inventarie e leia o que já existe. Reutilize caminhos e formatos;
10
10
  complemente lacunas sem apagar, mover ou substituir arquivos por templates.
11
+ Em instruções existentes dos agentes, acrescente somente a referência do kit;
12
+ preserve texto, nomes e metadados humanos, inclusive em colisões de nomes.
11
13
 
12
14
  Após implementar, siga `atualizar-documentacao` e o comando `revisar-entrega`
13
15
  antes de commit ou merge. Use os modelos da fase atual; registre decisões sem
@@ -7,3 +7,11 @@ description: Utilitários mínimos compartilhados das automações Node.js.
7
7
  `runtime.mjs` centraliza o requisito Node.js 24+, a raiz do repositório e a
8
8
  identificação do arquivo executado. Não instala runtimes nem configura a máquina.
9
9
  Também procura executáveis opcionais no PATH sem iniciar um shell.
10
+
11
+ `managed-content.mjs` limita a propriedade do kit aos seus blocos Markdown;
12
+ `managed-toml.mjs` aplica o mesmo contrato ao `prompt` de comandos Gemini.
13
+ Texto externo, nomes existentes e metadados humanos permanecem preservados.
14
+
15
+ `gitignore.mjs` planeja exclusões locais e detecta regras conflitantes. Com
16
+ exceções na raiz, pode acrescentar um bloco limitado ao estado privado dos
17
+ clientes; alterações no bloco gerenciado impedem sua substituição.
@@ -125,6 +125,36 @@ function artifactPolicy(templateFile) {
125
125
  return patterns;
126
126
  }
127
127
 
128
+ function privateClientPolicy(templateFile, roots) {
129
+ const adjacent = join(dirname(templateFile), 'agents.ignore');
130
+ const source = checkedLocalPath(localStat(adjacent) ? adjacent : fileURLToPath(new URL('../../templates/gitignore/agents.ignore', import.meta.url)));
131
+ const section = readFileSync(source, 'utf8').split(/^## Estado local, caches e credenciais conhecidas\r?$/m)[1]?.split(/^## /m)[0];
132
+ const patterns = section?.split(/\r?\n/).map(line => line.trim()).filter(line => line && !line.startsWith('#'));
133
+ if (!patterns?.length || patterns.some(pattern => !/^(?:!?\*\*\/|\/)/.test(pattern) || /[\\\x00-\x1f]/.test(pattern) || pattern.split('/').includes('..'))) throw new Error('Secao de estado local invalida em agents.ignore.');
134
+ const rules = [], probes = [];
135
+ for (const root of roots) for (const pattern of patterns) {
136
+ const negative = pattern.startsWith('!'), rule = pattern.replace(/^!/, '').replace(/^\//, '');
137
+ rules.push(`${negative ? '!' : ''}/${escapePattern(root)}/${rule}`);
138
+ if (negative) continue;
139
+ const witness = lower => rule.replace(/^\*\*\//, '').replace(/\[([^\]]+)\]/g, (_match, letters) => lower ? letters[0].toLowerCase() : letters[0]).replace(/\*/g, 'agentdevkit-private-probe').replace(/\?/g, 'x');
140
+ for (const variant of new Set([witness(false), witness(true)])) {
141
+ probes.push(`${root}/${variant}${variant.endsWith('/') ? '.agentdevkit-ignore-probe' : ''}`);
142
+ if (rule.startsWith('**/')) probes.push(`${root}/.agentdevkit-ignore-probes/${variant}${variant.endsWith('/') ? '.agentdevkit-ignore-probe' : ''}`);
143
+ }
144
+ }
145
+ return { rules, probes };
146
+ }
147
+
148
+ function safeIgnoreProbe(root, path) {
149
+ let parent = root;
150
+ for (const part of path.split('/').slice(0, -1)) {
151
+ parent = join(parent, part);
152
+ const item = localStat(parent);
153
+ if (item && !item.isDirectory()) return false;
154
+ }
155
+ return true;
156
+ }
157
+
128
158
  function artifactProbes(root, patterns) {
129
159
  const probes = new Set();
130
160
  for (const pattern of patterns) {
@@ -233,10 +263,14 @@ export function planLocalIgnore(root, { outputs = [], templateRoot } = {}) {
233
263
  const artifacts = artifactPolicy(templateFile);
234
264
  if (template.split('{{OUTPUTS}}').length !== 2) throw new Error('local.ignore exige um unico {{OUTPUTS}}.');
235
265
  if (template.split('{{ROOTS}}').length > 2) throw new Error('local.ignore permite um unico {{ROOTS}}.');
266
+ // A client directory can also contain human instructions and settings. Only
267
+ // whole files owned by the kit belong in the local output exclusion list.
268
+ const roots = [];
236
269
  const localPolicy = join(templateRoot || root, '.pbai4devs/templates/clients/ADAPTERS.json');
237
270
  const policy = JSON.parse(readFileSync(checkedLocalPath(localStat(localPolicy) ? localPolicy : fileURLToPath(new URL('../../templates/clients/ADAPTERS.json', import.meta.url))), 'utf8'));
238
- const roots = [...new Set(policy.adapters.flatMap(adapter => [adapter.root, ...(adapter.extraRoots || [])]))].filter(path => !['.github', '.vscode'].includes(path));
239
- if (roots.some(path => typeof path !== 'string' || !/^\.[a-z0-9][a-z0-9-]*$/.test(path) || ['.git', '.pbai4devs', '.agent-tests'].includes(path))) throw new Error('Raiz de cliente invalida para exclusao.');
271
+ const clientRoots = [...new Set(policy.adapters.flatMap(adapter => [adapter.root, ...(adapter.extraRoots || [])]))];
272
+ if (clientRoots.some(path => typeof path !== 'string' || !/^\.[a-z0-9][a-z0-9-]*$/.test(path) || ['.git', '.pbai4devs', '.agent-tests'].includes(path))) throw new Error('Raiz de cliente invalida para exclusao.');
273
+ const privateClients = privateClientPolicy(templateFile, clientRoots);
240
274
  const paths = [...new Set([...outputs.map(checkedOutput), ...template.split(/\r?\n/).filter(line => line.startsWith('/')).map(line => checkedOutput(line.slice(1)))])].sort();
241
275
  const info = gitCommand(resolved.parent, ['rev-parse', '--show-toplevel', '--git-path', 'info/exclude']);
242
276
  let location = null;
@@ -256,13 +290,29 @@ export function planLocalIgnore(root, { outputs = [], templateRoot } = {}) {
256
290
  const id = createHash('sha256').update(root).digest('hex').slice(0, 16);
257
291
  const first = `# BEGIN AGENTDEVKIT LOCAL ${id}`, last = `# END AGENTDEVKIT LOCAL ${id}`;
258
292
  const fixed = template.replace('{{ROOTS}}', roots.map(path => `/${path}/`).join('\n')).replace('{{OUTPUTS}}', paths.filter(path => !roots.some(root => path === root || path.startsWith(root + '/'))).map(path => `/${escapePattern(path)}`).join('\n'))
293
+ + '\n# Estado privado dos clientes\n' + privateClients.rules.join('\n') + '\n'
259
294
  + '\n# Resultados e arquivos temporarios\n' + artifacts.map(pattern => `/${pattern}`).join('\n') + '\n';
260
- const body = fixed.split(/\r?\n/).map(line => location?.prefix && line.startsWith('/') ? `/${escapePattern(location.prefix)}${line}` : line).join('\n');
295
+ const body = fixed.split(/\r?\n/).map(line => {
296
+ if (!location?.prefix) return line;
297
+ if (line.startsWith('!/')) return `!/${escapePattern(location.prefix)}${line.slice(1)}`;
298
+ return line.startsWith('/') ? `/${escapePattern(location.prefix)}${line}` : line;
299
+ }).join('\n');
261
300
  const target = location?.exclude ?? checkedLocalPath(join(root, '.gitignore'));
262
301
  const before = localRead(target), after = localBlock(before, first, last, body);
263
302
  const edits = before === after ? [] : [{ path: target, before, after, beforeMode: localMode(target) }];
264
303
  const probes = [...paths, ...rootProbes(root, roots)];
265
- const artifactPaths = [...new Set([...artifactProbes(root, artifacts), ...(location ? visibleArtifactProbes(root, location, artifacts) : [])])];
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
+ }
266
316
  const wanted = new Set(probes.map(path => location?.prefix ? `${location.prefix}/${path}` : path));
267
317
  if (location && !before?.includes(first) && !localRead(join(root, '.gitignore'))?.includes(first)) {
268
318
  const ignored = ignoreMatches(location, [...probes, ...artifactPaths]);
@@ -270,11 +320,19 @@ export function planLocalIgnore(root, { outputs = [], templateRoot } = {}) {
270
320
  }
271
321
  if (location) {
272
322
  const fallback = checkedLocalPath(join(root, '.gitignore'));
273
- const old = localRead(fallback), next = localBlock(old, first, last, null);
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
+ }
274
332
  if (old !== null && old !== next) edits.push({ path: fallback, before: old, after: next, beforeMode: localMode(fallback) });
275
333
  }
276
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) : [];
277
- return { root, status: location ? 'git' : 'fallback', location, paths, roots, artifactPatterns: artifacts, artifactProbes: artifactPaths, edits, trackedOutputs: tracked, first, last };
335
+ return { root, status: location ? 'git' : 'fallback', location, paths, roots, artifactPatterns: artifacts, artifactProbes: artifactPaths, edits, trackedOutputs: tracked, first, last, rootOverrideProbes: [...rootOverrideProbes] };
278
336
  }
279
337
 
280
338
  export function checkLocalIgnore(plan, { replacedBlocks = [] } = {}) {
@@ -300,7 +358,8 @@ export function checkLocalIgnore(plan, { replacedBlocks = [] } = {}) {
300
358
  const local = match && localIgnoreSource(plan.location, match.source);
301
359
  const humanOverride = negative && local && /(?:^|[\\/])\.gitignore$/.test(match.source);
302
360
  const replaced = negative && replacements.some(item => item.path === canonicalExisting(resolve(plan.location.top, match.source)) && match.line >= item.firstLine && match.line <= item.lastLine);
303
- if (!replaced && ((!plan.edits.length && (!match || negative || !local)) || humanOverride)) missing.push(probes[index]);
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]);
304
363
  }
305
364
  }
306
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 };
@@ -0,0 +1,86 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readdirSync } from 'node:fs';
3
+
4
+ export const managedBegin = '<!-- BEGIN AGENTDEVKIT -->';
5
+ export const managedEnd = '<!-- END AGENTDEVKIT -->';
6
+ const digest = value => createHash('sha256').update(value).digest('hex');
7
+ const fail = message => { throw Object.assign(new Error(message), { code: 'E_MANAGED_BLOCK' }); };
8
+
9
+ export function canonicalEntrypoint(path) {
10
+ return ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'].find(name => name.toLowerCase() === path.toLowerCase()) || null;
11
+ }
12
+
13
+ export function resolveEntrypoint(root, canonical) {
14
+ if (!canonicalEntrypoint(canonical)) fail(`Entrada desconhecida: ${canonical}`);
15
+ let names;
16
+ try { names = readdirSync(root); }
17
+ catch (error) { if (error.code === 'ENOENT') return canonical; throw error; }
18
+ const matches = names.filter(name => name.toLowerCase() === canonical.toLowerCase());
19
+ if (matches.length > 1) fail(`Entradas ambiguas: ${matches.sort().join(', ')}`);
20
+ return matches[0] || canonical;
21
+ }
22
+
23
+ export function entrypointReference(root, path) {
24
+ const canonical = canonicalEntrypoint(path);
25
+ if (canonical === 'AGENTS.md') return 'Leia e siga [.pbai4devs/INSTRUCTIONS.md](.pbai4devs/INSTRUCTIONS.md).';
26
+ if (canonical === 'CLAUDE.md') return `@${resolveEntrypoint(root, 'AGENTS.md')}`;
27
+ if (canonical === 'GEMINI.md') return `@./${resolveEntrypoint(root, 'AGENTS.md')}`;
28
+ fail(`Entrada desconhecida: ${path}`);
29
+ }
30
+
31
+ export function renderEntrypointImports(root, text) {
32
+ const name = resolveEntrypoint(root, 'AGENTS.md');
33
+ return text.replace(/^(@(?:\.\/)?)AGENTS\.md(?=\r?$)/gim, (_match, prefix) => `${prefix}${name}`);
34
+ }
35
+
36
+ export function managedText(value) {
37
+ const text = Buffer.isBuffer(value) ? new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(value) : String(value ?? '');
38
+ if (text.includes('\0')) fail('Conteudo binario nao pode receber instrucoes Markdown.');
39
+ return text;
40
+ }
41
+
42
+ function locate(value) {
43
+ const text = managedText(value);
44
+ let fence = null, start = null, finish = null, offset = 0;
45
+ for (const match of text.matchAll(/([^\r\n]*)(\r\n|\r|\n|$)/g)) {
46
+ if (!match[0]) continue;
47
+ const line = match[1];
48
+ const marker = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
49
+ if (fence) {
50
+ if (marker && marker[1][0] === fence[0] && marker[1].length >= fence.length && !marker[2].trim()) fence = null;
51
+ } else if (marker) fence = marker[1];
52
+ else if (line === managedBegin) {
53
+ if (start !== null || finish !== null) fail('Marcadores AgentDevKit duplicados.');
54
+ start = offset;
55
+ } else if (line === managedEnd) {
56
+ if (start === null || finish !== null) fail('Marcadores AgentDevKit incompletos.');
57
+ finish = offset + line.length;
58
+ }
59
+ offset += match[0].length;
60
+ }
61
+ if ((start === null) !== (finish === null)) fail('Marcadores AgentDevKit incompletos.');
62
+ return { text, start, finish, fence, block: start === null ? null : text.slice(start, finish) };
63
+ }
64
+
65
+ export function extractManagedBlock(value) { return locate(value).block; }
66
+ export function managedBlockDigest(value) { const block = extractManagedBlock(value); return block === null ? null : digest(block); }
67
+
68
+ export function mergeManagedContent(value, body, { expectedHash } = {}) {
69
+ const current = locate(value), eol = current.text.match(/\r\n|\n|\r/)?.[0] || '\n';
70
+ const content = managedText(body).trim().replace(/\r\n|\r|\n/g, eol);
71
+ if (!content || content.includes(managedBegin) || content.includes(managedEnd)) fail('Referencia AgentDevKit invalida.');
72
+ const block = `${managedBegin}${eol}${content}${eol}${managedEnd}`;
73
+ if (current.block !== null && (expectedHash ? digest(current.block) !== expectedHash : current.block !== block)) fail('Bloco AgentDevKit alterado; conteudo preservado.');
74
+ if (current.fence && current.block === null) fail('Bloco de codigo Markdown nao fechado; conteudo preservado.');
75
+ const separator = !current.text || current.text.endsWith(eol + eol) ? '' : /[\r\n]$/.test(current.text) ? eol : eol + eol;
76
+ const text = current.block === null ? `${current.text}${separator}${block}${eol}`
77
+ : current.text.slice(0, current.start) + block + current.text.slice(current.finish);
78
+ return { text, block, sha256: digest(block), changed: text !== current.text };
79
+ }
80
+
81
+ export function stripManagedContent(value, expectedHash) {
82
+ const current = locate(value);
83
+ if (current.block === null) return current.text;
84
+ if (!expectedHash || digest(current.block) !== expectedHash) fail('Bloco AgentDevKit alterado; conteudo preservado.');
85
+ return current.text.slice(0, current.start) + current.text.slice(current.finish);
86
+ }
@@ -0,0 +1,192 @@
1
+ import { extractManagedBlock, managedBlockDigest, managedText, mergeManagedContent, stripManagedContent } from './managed-content.mjs';
2
+
3
+ // Localized editing of Gemini's root prompt; not a general TOML serializer.
4
+ const fail = message => { throw Object.assign(new Error(message), { code: 'E_MANAGED_BLOCK' }); };
5
+ const horizontal = character => character === ' ' || character === '\t';
6
+ const whitespace = character => horizontal(character) || character === '\n' || character === '\r';
7
+ const escapes = { b: '\b', t: '\t', n: '\n', f: '\f', r: '\r', '"': '"', '\\': '\\' };
8
+
9
+ function scalar(code) {
10
+ if (code > 0x10ffff || code >= 0xd800 && code <= 0xdfff) fail('Escape Unicode TOML invalido.');
11
+ return String.fromCodePoint(code);
12
+ }
13
+
14
+ function stringAt(text, start, key = false) {
15
+ const quote = text[start];
16
+ if (!['"', "'"].includes(quote)) fail('O prompt TOML deve ser uma string.');
17
+ const multiline = text.slice(start, start + 3) === quote.repeat(3);
18
+ if (key && multiline) fail('Chave TOML multiline nao suportada.');
19
+ const basic = quote === '"';
20
+ let position = start + (multiline ? 3 : 1), value = '';
21
+ if (multiline) {
22
+ if (text.slice(position, position + 2) === '\r\n') position += 2;
23
+ else if (text[position] === '\n') position++;
24
+ }
25
+ while (position < text.length) {
26
+ const character = text[position];
27
+ if (character === quote) {
28
+ if (!multiline) return { start, end: position + 1, value };
29
+ let count = 1;
30
+ while (text[position + count] === quote) count++;
31
+ if (count >= 3) {
32
+ if (count > 5) fail('Delimitador TOML ambiguo.');
33
+ return { start, end: position + count, value: value + quote.repeat(count - 3) };
34
+ }
35
+ value += quote.repeat(count); position += count; continue;
36
+ }
37
+ if (basic && character === '\\') {
38
+ const escaped = text[++position];
39
+ if (multiline && whitespace(escaped)) {
40
+ let cursor = position, lineBreak = false;
41
+ while (whitespace(text[cursor])) {
42
+ if (text[cursor] === '\r') {
43
+ if (text[cursor + 1] !== '\n') fail('TOML exige LF ou CRLF.');
44
+ lineBreak = true; cursor += 2;
45
+ } else { if (text[cursor] === '\n') lineBreak = true; cursor++; }
46
+ }
47
+ if (!lineBreak) fail('Continuacao de string TOML invalida.');
48
+ position = cursor; continue;
49
+ }
50
+ if (Object.hasOwn(escapes, escaped)) { value += escapes[escaped]; position++; continue; }
51
+ if (escaped === 'u' || escaped === 'U') {
52
+ const width = escaped === 'u' ? 4 : 8, digits = text.slice(position + 1, position + 1 + width);
53
+ if (!new RegExp(`^[0-9a-fA-F]{${width}}$`).test(digits)) fail('Escape Unicode TOML invalido.');
54
+ value += scalar(Number.parseInt(digits, 16)); position += width + 1; continue;
55
+ }
56
+ fail('Escape TOML nao suportado.');
57
+ }
58
+ const code = text.charCodeAt(position);
59
+ if (code === 0x7f || code < 0x20 && character !== '\t' && !(multiline && ['\r', '\n'].includes(character))) fail('Controle invalido em string TOML.');
60
+ if (!multiline && ['\r', '\n'].includes(character)) fail('String TOML de uma linha interrompida.');
61
+ if (character === '\r' && text[position + 1] !== '\n') fail('TOML exige LF ou CRLF.');
62
+ value += character; position++;
63
+ }
64
+ fail('String TOML sem fechamento.');
65
+ }
66
+
67
+ function keyAt(text, start) {
68
+ let position = start;
69
+ const path = [];
70
+ while (true) {
71
+ while (horizontal(text[position])) position++;
72
+ if (['"', "'"].includes(text[position])) {
73
+ const part = stringAt(text, position, true); path.push(part.value); position = part.end;
74
+ } else {
75
+ const part = /^[A-Za-z0-9_-]+/.exec(text.slice(position));
76
+ if (!part) fail('Chave TOML nao suportada.');
77
+ path.push(part[0]); position += part[0].length;
78
+ }
79
+ while (horizontal(text[position])) position++;
80
+ if (text[position] !== '.') break;
81
+ position++;
82
+ }
83
+ return { path, end: position };
84
+ }
85
+
86
+ function valueEnd(text, start) {
87
+ if (['"', "'"].includes(text[start])) return stringAt(text, start).end;
88
+ const stack = [];
89
+ let position = start;
90
+ while (position < text.length) {
91
+ const character = text[position];
92
+ if (['"', "'"].includes(character)) { position = stringAt(text, position).end; continue; }
93
+ if (character === '#') {
94
+ if (!stack.length) break;
95
+ while (position < text.length && !['\r', '\n'].includes(text[position])) position++;
96
+ continue;
97
+ }
98
+ if (['\n', '\r'].includes(character) && !stack.length) break;
99
+ if (character === '[' || character === '{') stack.push(character);
100
+ if (character === ']' || character === '}') {
101
+ if (stack.pop() !== (character === ']' ? '[' : '{')) fail('Valor TOML desbalanceado.');
102
+ }
103
+ position++;
104
+ }
105
+ if (stack.length || !text.slice(start, position).trim()) fail('Valor TOML incompleto.');
106
+ return position;
107
+ }
108
+
109
+ function statementEnd(text, start) {
110
+ let position = start;
111
+ while (horizontal(text[position])) position++;
112
+ if (text[position] === '#') while (position < text.length && !['\r', '\n'].includes(text[position])) position++;
113
+ if (position < text.length && !['\r', '\n'].includes(text[position])) fail('Texto inesperado apos valor TOML.');
114
+ if (text[position] === '\r') {
115
+ if (text[position + 1] !== '\n') fail('TOML exige LF ou CRLF.');
116
+ return position + 2;
117
+ }
118
+ return text[position] === '\n' ? position + 1 : position;
119
+ }
120
+
121
+ function locatePrompt(input) {
122
+ let text;
123
+ try { text = managedText(input); }
124
+ catch (error) {
125
+ if (error.code === 'E_MANAGED_BLOCK') throw error;
126
+ fail('Texto TOML deve ser UTF-8 valido.');
127
+ }
128
+ for (const character of text) {
129
+ const code = character.codePointAt(0);
130
+ if (code >= 0xd800 && code <= 0xdfff) fail('Unicode TOML invalido.');
131
+ }
132
+ let position = text.startsWith('\ufeff') ? 1 : 0, root = true, insertion = text.length, prompt = null, reserved = false;
133
+ while (position < text.length) {
134
+ const lineStart = position;
135
+ while (horizontal(text[position])) position++;
136
+ if (position === text.length) break;
137
+ if (text[position] === '#' || ['\r', '\n'].includes(text[position])) { position = statementEnd(text, position); continue; }
138
+ if (text[position] === '[') {
139
+ const arrayTable = text[position + 1] === '[';
140
+ const header = keyAt(text, position + (arrayTable ? 2 : 1));
141
+ if (text.slice(header.end, header.end + (arrayTable ? 2 : 1)) !== (arrayTable ? ']]' : ']')) fail('Tabela TOML invalida.');
142
+ if (root) insertion = lineStart;
143
+ root = false;
144
+ if (header.path[0] === 'prompt') reserved = true;
145
+ position = statementEnd(text, header.end + (arrayTable ? 2 : 1)); continue;
146
+ }
147
+ const key = keyAt(text, position);
148
+ if (text[key.end] !== '=') fail('Atribuicao TOML invalida.');
149
+ position = key.end + 1;
150
+ while (horizontal(text[position])) position++;
151
+ if (root && key.path[0] === 'prompt') {
152
+ if (key.path.length !== 1 || prompt || reserved) fail('Prompt TOML duplicado ou ambiguo.');
153
+ prompt = stringAt(text, position);
154
+ position = prompt.end;
155
+ } else position = valueEnd(text, position);
156
+ position = statementEnd(text, position);
157
+ }
158
+ if (reserved) fail('Prompt TOML ocupa uma tabela; conteudo preservado.');
159
+ return { text, prompt, insertion };
160
+ }
161
+
162
+ function encode(value) {
163
+ return JSON.stringify(value).replace(/\x7f/g, '\\u007F');
164
+ }
165
+
166
+ export function extractTomlManagedBlock(input) {
167
+ const { prompt } = locatePrompt(input);
168
+ return prompt ? extractManagedBlock(prompt.value) : null;
169
+ }
170
+
171
+ export function tomlManagedBlockDigest(input) {
172
+ const { prompt } = locatePrompt(input);
173
+ return prompt ? managedBlockDigest(prompt.value) : null;
174
+ }
175
+
176
+ export function mergeTomlContent(input, body, options = {}) {
177
+ const { text, prompt, insertion } = locatePrompt(input);
178
+ const result = mergeManagedContent(prompt?.value ?? '', body, options);
179
+ if (prompt && !result.changed) return { ...result, text, changed: false };
180
+ const eol = text.match(/\r\n|\n/)?.[0] || '\n';
181
+ const separator = insertion && !/[\r\n]$/.test(text.slice(0, insertion)) ? eol : '';
182
+ const content = prompt ? text.slice(0, prompt.start) + encode(result.text) + text.slice(prompt.end)
183
+ : text.slice(0, insertion) + separator + `prompt = ${encode(result.text)}${eol}` + text.slice(insertion);
184
+ return { ...result, text: content, changed: content !== text };
185
+ }
186
+
187
+ export function stripTomlContent(input, expectedHash) {
188
+ const { text, prompt } = locatePrompt(input);
189
+ if (!prompt) return text;
190
+ const stripped = stripManagedContent(prompt.value, expectedHash);
191
+ return stripped === prompt.value ? text : text.slice(0, prompt.start) + encode(stripped) + text.slice(prompt.end);
192
+ }
@@ -12,9 +12,11 @@ node --test .pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs
12
12
 
13
13
  `--check` só confere; a execução normal atualiza os recursos. `--force` salva
14
14
  conflitos em `.agent-tests/` antes de reparar os caminhos gerenciados.
15
- Recursos nativos humanos e seus links permanecem intactos, inclusive com
16
- `--force`. O retorno inclui `preserved`; esses caminhos não contam como
17
- derivados pendentes nem passam a pertencer ao kit.
15
+ Recursos nativos humanos recebem um bloco curto com a referência ao hub.
16
+ Texto, frontmatter e nomes existentes são preservados. O estado guarda só o
17
+ hash desse bloco; edições externas continuam permitidas. Um bloco alterado
18
+ não é sobrescrito por `--force`. Links humanos continuam opacos.
19
+ Comandos Gemini recebem o complemento dentro do `prompt` TOML.
18
20
 
19
21
  Metadados vêm da fonte ou do YAML nativo declarado no manifesto. Regras exigem
20
22
  `trigger` válido; `glob` exige `globs`. Origens nativas precisam pertencer ao
@@ -36,9 +38,9 @@ a seleção, use `agentdevkit agents <lista>`; não edite o manifesto gerado.
36
38
  Os derivados ficam locais: catálogos, manifesto, estado e apontamentos são
37
39
  excluídos pelo Git. O bloco em `.git/info/exclude` preserva regras próprias;
38
40
  sem Git, usa um bloco na raiz que migra ao executar depois de `git init`.
39
- Raízes dedicadas são locais por completo; `.github` e `.vscode` excluem somente
40
- os arquivos gerados. Conteúdo próprio permanece no disco e não é desindexado
41
- sem comprovação de propriedade do kit.
41
+ Somente arquivos integralmente gerados são excluídos; pastas de clientes podem
42
+ conter arquivos humanos versionados. Conteúdo próprio não é desindexado pela
43
+ presença de um bloco gerenciado.
42
44
  Entradas novas usam os templates; hashes em `estado/derived.json` permitem
43
- atualizar apenas arquivos integralmente gerados, preservando alterações humanas.
45
+ atualizar arquivos gerados e blocos separadamente, preservando alterações humanas.
44
46
  `--check` também acusa derivados ainda rastreados; não altera o índice.