@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.
- package/.pbai4devs/rules/documentacao/RULE.md +2 -0
- package/.pbai4devs/scripts/lib/README.md +8 -0
- package/.pbai4devs/scripts/lib/gitignore.mjs +66 -7
- 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 +90 -4
- 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 +14 -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 +27 -8
- 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 +5 -0
- package/docs/specs/additive-integration/README.md +9 -0
- package/docs/specs/additive-integration/SPEC.md +53 -0
- package/docs/specs/real-projects/SPEC.md +26 -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,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
|
|
239
|
-
if (
|
|
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 =>
|
|
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)
|
|
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
|
-
|
|
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
|
|
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.
|