@pathbit/agentdevkit 0.0.11 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pbai4devs/rules/documentacao/RULE.md +2 -0
- package/.pbai4devs/scripts/lib/README.md +4 -0
- package/.pbai4devs/scripts/lib/gitignore.mjs +41 -4
- package/.pbai4devs/scripts/lib/managed-content.mjs +86 -0
- package/.pbai4devs/scripts/lib/managed-toml.mjs +192 -0
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +9 -7
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +133 -38
- package/.pbai4devs/scripts/sincronizar-agentes/tests/COEXISTENCE.mjs +25 -15
- package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +7 -2
- package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +3 -3
- package/.pbai4devs/scripts/sincronizar-agentes/tests/MANAGED_CONTENT.mjs +318 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +47 -26
- package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +43 -7
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +3 -2
- package/CHANGELOG.md +8 -0
- package/README.en.md +2 -2
- package/README.es.md +2 -2
- package/README.md +7 -3
- package/docs/guides/cli.md +2 -1
- package/docs/guides/installation.md +23 -6
- package/docs/guides/troubleshooting.md +8 -5
- package/docs/maintainers/maintenance.md +5 -0
- package/docs/maintainers/packages.md +2 -2
- package/docs/product/design.md +10 -3
- package/docs/product/prd.md +2 -1
- package/docs/product/roadmap.md +2 -0
- package/docs/specs/README.md +3 -0
- package/docs/specs/additive-integration/README.md +9 -0
- package/docs/specs/additive-integration/SPEC.md +53 -0
- package/package.json +1 -1
- 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,7 @@ 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.
|
|
@@ -125,6 +125,34 @@ 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 stem = rule.replace(/^\*\*\//, '').replace(/\[([^\]]+)\]/g, (_match, letters) => letters[0]).replace(/\*/g, 'agentdevkit-private-probe').replace(/\?/g, 'x');
|
|
140
|
+
probes.push(`${root}/${stem}${stem.endsWith('/') ? '.agentdevkit-ignore-probe' : ''}`);
|
|
141
|
+
if (rule.startsWith('**/')) probes.push(`${root}/.agentdevkit-ignore-probes/${stem}${stem.endsWith('/') ? '.agentdevkit-ignore-probe' : ''}`);
|
|
142
|
+
}
|
|
143
|
+
return { rules, probes };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function safeIgnoreProbe(root, path) {
|
|
147
|
+
let parent = root;
|
|
148
|
+
for (const part of path.split('/').slice(0, -1)) {
|
|
149
|
+
parent = join(parent, part);
|
|
150
|
+
const item = localStat(parent);
|
|
151
|
+
if (item && !item.isDirectory()) return false;
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
128
156
|
function artifactProbes(root, patterns) {
|
|
129
157
|
const probes = new Set();
|
|
130
158
|
for (const pattern of patterns) {
|
|
@@ -233,10 +261,14 @@ export function planLocalIgnore(root, { outputs = [], templateRoot } = {}) {
|
|
|
233
261
|
const artifacts = artifactPolicy(templateFile);
|
|
234
262
|
if (template.split('{{OUTPUTS}}').length !== 2) throw new Error('local.ignore exige um unico {{OUTPUTS}}.');
|
|
235
263
|
if (template.split('{{ROOTS}}').length > 2) throw new Error('local.ignore permite um unico {{ROOTS}}.');
|
|
264
|
+
// A client directory can also contain human instructions and settings. Only
|
|
265
|
+
// whole files owned by the kit belong in the local output exclusion list.
|
|
266
|
+
const roots = [];
|
|
236
267
|
const localPolicy = join(templateRoot || root, '.pbai4devs/templates/clients/ADAPTERS.json');
|
|
237
268
|
const policy = JSON.parse(readFileSync(checkedLocalPath(localStat(localPolicy) ? localPolicy : fileURLToPath(new URL('../../templates/clients/ADAPTERS.json', import.meta.url))), 'utf8'));
|
|
238
|
-
const
|
|
239
|
-
if (
|
|
269
|
+
const clientRoots = [...new Set(policy.adapters.flatMap(adapter => [adapter.root, ...(adapter.extraRoots || [])]))];
|
|
270
|
+
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.');
|
|
271
|
+
const privateClients = privateClientPolicy(templateFile, clientRoots);
|
|
240
272
|
const paths = [...new Set([...outputs.map(checkedOutput), ...template.split(/\r?\n/).filter(line => line.startsWith('/')).map(line => checkedOutput(line.slice(1)))])].sort();
|
|
241
273
|
const info = gitCommand(resolved.parent, ['rev-parse', '--show-toplevel', '--git-path', 'info/exclude']);
|
|
242
274
|
let location = null;
|
|
@@ -256,13 +288,18 @@ export function planLocalIgnore(root, { outputs = [], templateRoot } = {}) {
|
|
|
256
288
|
const id = createHash('sha256').update(root).digest('hex').slice(0, 16);
|
|
257
289
|
const first = `# BEGIN AGENTDEVKIT LOCAL ${id}`, last = `# END AGENTDEVKIT LOCAL ${id}`;
|
|
258
290
|
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'))
|
|
291
|
+
+ '\n# Estado privado dos clientes\n' + privateClients.rules.join('\n') + '\n'
|
|
259
292
|
+ '\n# Resultados e arquivos temporarios\n' + artifacts.map(pattern => `/${pattern}`).join('\n') + '\n';
|
|
260
|
-
const body = fixed.split(/\r?\n/).map(line =>
|
|
293
|
+
const body = fixed.split(/\r?\n/).map(line => {
|
|
294
|
+
if (!location?.prefix) return line;
|
|
295
|
+
if (line.startsWith('!/')) return `!/${escapePattern(location.prefix)}${line.slice(1)}`;
|
|
296
|
+
return line.startsWith('/') ? `/${escapePattern(location.prefix)}${line}` : line;
|
|
297
|
+
}).join('\n');
|
|
261
298
|
const target = location?.exclude ?? checkedLocalPath(join(root, '.gitignore'));
|
|
262
299
|
const before = localRead(target), after = localBlock(before, first, last, body);
|
|
263
300
|
const edits = before === after ? [] : [{ path: target, before, after, beforeMode: localMode(target) }];
|
|
264
301
|
const probes = [...paths, ...rootProbes(root, roots)];
|
|
265
|
-
const artifactPaths = [...new Set([...artifactProbes(root, artifacts), ...(location ? visibleArtifactProbes(root, location, artifacts) : [])])];
|
|
302
|
+
const artifactPaths = [...new Set([...artifactProbes(root, artifacts), ...privateClients.probes.filter(path => safeIgnoreProbe(root, path)), ...(location ? visibleArtifactProbes(root, location, artifacts) : [])])];
|
|
266
303
|
const wanted = new Set(probes.map(path => location?.prefix ? `${location.prefix}/${path}` : path));
|
|
267
304
|
if (location && !before?.includes(first) && !localRead(join(root, '.gitignore'))?.includes(first)) {
|
|
268
305
|
const ignored = ignoreMatches(location, [...probes, ...artifactPaths]);
|
|
@@ -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
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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.
|