@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.
Files changed (31) hide show
  1. package/.pbai4devs/rules/documentacao/RULE.md +2 -0
  2. package/.pbai4devs/scripts/lib/README.md +4 -0
  3. package/.pbai4devs/scripts/lib/gitignore.mjs +41 -4
  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 +3 -3
  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 +8 -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 +23 -6
  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 +3 -0
  28. package/docs/specs/additive-integration/README.md +9 -0
  29. package/docs/specs/additive-integration/SPEC.md +53 -0
  30. package/package.json +1 -1
  31. package/src/install.mjs +139 -63
package/src/install.mjs CHANGED
@@ -5,6 +5,8 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
5
5
  import { synchronize, isReference } from '../.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs';
6
6
  import { isGeneratedOutput, planLocalIgnore, checkLocalIgnore, assertLocalIgnoreUnchanged, applyLocalIgnore, planUntrack, applyUntrack, restoreUntrack } from '../.pbai4devs/scripts/lib/gitignore.mjs';
7
7
  import { nativePathBoundary } from '../.pbai4devs/scripts/lib/native-paths.mjs';
8
+ import { canonicalEntrypoint, resolveEntrypoint, entrypointReference, renderEntrypointImports, extractManagedBlock, managedBlockDigest, mergeManagedContent, stripManagedContent } from '../.pbai4devs/scripts/lib/managed-content.mjs';
9
+ import { extractTomlManagedBlock, tomlManagedBlockDigest, mergeTomlContent, stripTomlContent } from '../.pbai4devs/scripts/lib/managed-toml.mjs';
8
10
  import { writeAdapterManifest, adapterRoots, normalizeClients, resolveAdapters } from './adapters/index.mjs';
9
11
  import { packageRoot, packageInfo, statePath, entryNames, begin, end } from './config.mjs';
10
12
  import { normalizeLanguage, text } from './i18n.mjs';
@@ -84,7 +86,7 @@ function write(root, path, content, mode) {
84
86
  if (mode !== undefined) chmodSync(target, mode);
85
87
  }
86
88
 
87
- function atomicWrite(root, path, content, mode, onPublished) {
89
+ function atomicWrite(root, path, content, mode, onPublished, beforePublish) {
88
90
  const file = safePath(root, path);
89
91
  mkdirSync(dirname(file), { recursive: true });
90
92
  const temporary = `${file}.agentdevkit-${randomUUID()}`;
@@ -92,6 +94,7 @@ function atomicWrite(root, path, content, mode, onPublished) {
92
94
  writeFileSync(temporary, content, { flag: 'wx', ...(mode === undefined ? {} : { mode }) });
93
95
  if (mode !== undefined) chmodSync(temporary, mode);
94
96
  const writtenMode = stat(temporary).mode & 0o777;
97
+ beforePublish?.();
95
98
  renameSync(temporary, file);
96
99
  onPublished?.(writtenMode);
97
100
  return writtenMode;
@@ -106,13 +109,26 @@ function read(root, path) {
106
109
  }
107
110
 
108
111
  function allowedManaged(path) {
109
- if (entryNames.includes(path) || path === '.gitignore') return true;
112
+ if (canonicalEntrypoint(path) || path === '.gitignore') return true;
113
+ if (nativeMarkdown(path) || nativeToml(path)) return true;
110
114
  if (['catalog.json', 'skills.json', 'agents.json', 'rules.json', 'espelhos.conf'].some(name => path === `.pbai4devs/${name}`)) return true;
111
115
  if (['.agents/skills.json', '.agents/agents.json', '.agents/rules.json'].includes(path)) return true;
112
116
  if (path.startsWith('.pbai4devs/')) return resourceAllowed(path.slice('.pbai4devs/'.length));
113
117
  return adapterRoots.some(root => path === `${root}/.gitignore` || ['skills', 'agents', 'commands', 'rules', 'instructions', 'prompts'].some(kind => path.startsWith(`${root}/${kind}/`)));
114
118
  }
115
119
 
120
+ function nativeMarkdown(path) {
121
+ return Boolean(canonicalEntrypoint(path)) || /\.(?:md|mdc)$/i.test(path)
122
+ && adapterRoots.some(root => {
123
+ if (!path.startsWith(`${root}/`)) return false;
124
+ const relative = path.slice(root.length + 1).split('/');
125
+ return !['tools', 'scripts', 'templates', 'external-skills', 'estado', 'tests', 'docs', 'adapters', 'workflows'].includes(relative[0].toLowerCase())
126
+ && !['readme.md', 'instructions.md'].includes(relative.at(-1).toLowerCase());
127
+ });
128
+ }
129
+ const nativeToml = path => path.startsWith('.gemini/commands/') && path.endsWith('.toml');
130
+ const nativeManagedText = path => nativeMarkdown(path) || nativeToml(path);
131
+
116
132
  export function loadState(root) {
117
133
  const content = read(root, statePath); if (content === null) return null;
118
134
  let state; try { state = JSON.parse(content); } catch { fail('E_STATE', 'Invalid AgentDevKit state JSON.'); }
@@ -137,7 +153,9 @@ function extractBlock(content, first = begin, last = end) {
137
153
 
138
154
  function blockDigest(path, content) {
139
155
  const value = content.toString('utf8');
140
- const block = path === '.gitignore' ? extractBlock(value, '# BEGIN AGENTDEVKIT', '# END AGENTDEVKIT') : entryNames.includes(path) ? extractBlock(value) : path.endsWith('/.gitignore') ? extractBlock(value, '# BEGIN AGENT IGNORE', '# END AGENT IGNORE') : null;
156
+ if (nativeToml(path)) return tomlManagedBlockDigest(content);
157
+ if (nativeMarkdown(path)) return managedBlockDigest(content);
158
+ const block = path === '.gitignore' ? extractBlock(value, '# BEGIN AGENTDEVKIT', '# END AGENTDEVKIT') : path.endsWith('/.gitignore') ? extractBlock(value, '# BEGIN AGENT IGNORE', '# END AGENT IGNORE') : null;
141
159
  return block === null ? null : hash(block);
142
160
  }
143
161
 
@@ -175,21 +193,25 @@ function copyResources(source, destination) {
175
193
  }
176
194
 
177
195
  function readDerivedState(target) {
178
- let entries = {}, outputs = {};
196
+ let entries = {}, outputs = {}, blocks = {}, files = {}, removedBlocks = [];
179
197
  const derived = read(target, '.pbai4devs/estado/derived.json');
180
198
  if (derived) {
181
199
  try {
182
200
  const value = JSON.parse(derived);
183
201
  if (value.schemaVersion !== 1 || !value.entries || typeof value.entries !== 'object' || Array.isArray(value.entries)
184
- || value.outputs !== undefined && (!value.outputs || typeof value.outputs !== 'object' || Array.isArray(value.outputs))) fail('E_STATE', 'Invalid derived entrypoint state.');
185
- entries = value.entries; outputs = value.outputs ?? {};
186
- for (const [path, digest] of Object.entries(entries)) if (!entryNames.includes(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived entrypoint state.');
202
+ || ['outputs', 'blocks', 'files'].some(key => value[key] !== undefined && (!value[key] || typeof value[key] !== 'object' || Array.isArray(value[key])))) fail('E_STATE', 'Invalid derived entrypoint state.');
203
+ entries = value.entries; outputs = value.outputs ?? {}; blocks = value.blocks ?? {}; files = value.files ?? {};
204
+ removedBlocks = value.removedBlocks ?? [];
205
+ if (!Array.isArray(removedBlocks) || removedBlocks.some(path => typeof path !== 'string' || !nativeManagedText(path))) fail('E_STATE', 'Invalid removed block state.');
206
+ for (const [path, digest] of Object.entries(entries)) if (!canonicalEntrypoint(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived entrypoint state.');
187
207
  for (const [path, digest] of Object.entries(outputs)) if (!allowedManaged(path) || !/^\.[^/]+\/(?:skills|agents|rules)\.json$/.test(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived output state.');
208
+ for (const [path, digest] of Object.entries(blocks)) if (!nativeManagedText(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived block state.');
209
+ for (const [path, digest] of Object.entries(files)) if (!allowedManaged(path) || !/\.(?:md|mdc|toml)$/.test(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived file state.');
188
210
  }
189
211
  catch { fail('E_STATE', 'Invalid derived entrypoint state.'); }
190
- for (const path of [...Object.keys(entries), ...Object.keys(outputs)]) safePath(target, path);
212
+ for (const path of [...Object.keys(entries), ...Object.keys(outputs), ...Object.keys(blocks), ...Object.keys(files), ...removedBlocks]) safePath(target, path);
191
213
  }
192
- return { entries, outputs };
214
+ return { entries, outputs, blocks, files, removedBlocks };
193
215
  }
194
216
 
195
217
  export function planInstallation({ target, language = 'pt', mode = 'init', force = false, clients }) {
@@ -198,15 +220,26 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
198
220
  const selected = normalizeClients(clients ?? previous?.clients ?? ['all']);
199
221
  const active = resolveAdapters(selected);
200
222
  const changingClients = clients !== undefined || mode === 'agents';
201
- const protectedEntries = new Set(), entryConflicts = [], ownedEntrypoints = new Set();
202
- const { entries: derivedEntries, outputs: derivedOutputs } = readDerivedState(target);
203
- const ownedPaths = [...Object.keys(previous?.files || {}), ...Object.keys(previous?.blocks || {}), ...Object.keys(derivedEntries), ...Object.keys(derivedOutputs)];
223
+ const ownedEntrypoints = new Set(), mixedPaths = new Set(), nativeInputs = new Map();
224
+ const observeNative = (path, content) => {
225
+ if (nativeInputs.has(path)) {
226
+ const original = nativeInputs.get(path);
227
+ if (original === null ? content !== null : !content?.equals(original)) fail('E_CHANGED', `File changed while planning: ${path}`);
228
+ } else nativeInputs.set(path, content);
229
+ };
230
+ const { entries: derivedEntries, outputs: derivedOutputs, blocks: derivedBlocks, files: derivedFiles, removedBlocks } = readDerivedState(target);
231
+ const previousBlocks = Object.fromEntries(Object.entries(previous?.blocks || {}).filter(([path]) => !removedBlocks.includes(path)));
232
+ const knownBlocks = { ...previousBlocks, ...derivedBlocks };
233
+ const ownedPaths = [...Object.keys(previous?.files || {}), ...Object.keys(previousBlocks), ...Object.keys(derivedEntries), ...Object.keys(derivedOutputs), ...Object.keys(derivedBlocks), ...Object.keys(derivedFiles)];
234
+ const entryPaths = new Map(entryNames.map(name => [name, resolveEntrypoint(target, name)]));
235
+ const blockHash = path => knownBlocks[path]
236
+ ?? (canonicalEntrypoint(path) ? Object.entries(knownBlocks).find(([candidate]) => canonicalEntrypoint(candidate) === canonicalEntrypoint(path))?.[1] : undefined);
204
237
  const preserved = new Map();
205
238
  const recordPreserved = (path, reason) => { preserved.set(path, { path, reason }); return true; };
206
- const native = path => entryNames.includes(path) || path === 'README.md' || adapterRoots.some(root => path === root || path.startsWith(`${root}/`));
239
+ const native = path => canonicalEntrypoint(path) || path === 'README.md' || adapterRoots.some(root => path === root || path.startsWith(`${root}/`));
207
240
  const boundary = path => {
208
241
  if (!native(path)) return false;
209
- const found = nativePathBoundary(target, path, ownedPaths, { preserveRootAliases: path === 'README.md' || entryNames.includes(path) });
242
+ const found = nativePathBoundary(target, path, ownedPaths, { preserveRootAliases: path === 'README.md' });
210
243
  return found ? recordPreserved(found.path, found.reason) : false;
211
244
  };
212
245
  const owned = path => ownedPaths.includes(path);
@@ -216,20 +249,18 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
216
249
  if (!native(path)) return false;
217
250
  if (boundary(path)) return true;
218
251
  const item = stat(safePath(target, path));
219
- if (!item || owned(path)) return false;
252
+ if (!item) { observeNative(path, null); return false; }
253
+ const current = item.isFile() ? read(target, path) : null;
254
+ if (item.isFile()) observeNative(path, current);
255
+ if (owned(path)) return false;
220
256
  if (!item.isFile() && ownedPaths.some(value => value.startsWith(`${path}/`))) fail('E_PATH', `Managed subtree occupies a file destination: ${path}`);
221
257
  if (item.isFile()) {
222
- const current = read(target, path);
258
+ if (nativeManagedText(path)) return false;
223
259
  if (generatedReference(current)) return false;
224
260
  if (path.endsWith('/.gitignore') && current.toString('utf8').split(/\r?\n/).some(line => ['# BEGIN AGENT IGNORE', '# END AGENT IGNORE'].includes(line))) return false;
225
261
  }
226
262
  return recordPreserved(path, item.isFile() ? 'existing-file' : 'existing-directory');
227
263
  };
228
- for (const name of entryNames) {
229
- if (preserveNative(name)) continue;
230
- const current = read(target, name);
231
- if (current && previous?.files[name] && previous.files[name] !== hash(current)) { protectedEntries.add(name); entryConflicts.push(name); }
232
- }
233
264
  const stage = realpathSync(mkdtempSync(join(tmpdir(), 'agentdevkit-')));
234
265
  try {
235
266
  const existing = copyResources(join(target, '.pbai4devs'), join(stage, '.pbai4devs'));
@@ -253,23 +284,44 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
253
284
  const ownIgnore = read(target, '.pbai4devs/.gitignore');
254
285
  if (ownIgnore) write(stage, '.pbai4devs/.gitignore', ownIgnore);
255
286
  writeAdapterManifest(stage, { clients: selected });
256
- const synchronized = synchronize({ root: stage, force: true, localIgnores: false, generateEntrypoints: false, output() {} });
257
- for (const name of active.entrypoints) {
258
- if (protectedEntries.has(name) || preserveNative(name)) continue;
259
- const current = read(target, name), template = readFileSync(join(packageRoot, '.pbai4devs/templates/entrypoints', name), 'utf8');
260
- const legacy = entryContent(null, template, language, name);
261
- const content = legacy.replace(`${begin}\n`, '').replace(`\n${end}`, '');
287
+ const synchronized = synchronize({ root: stage, force: true, localIgnores: false, generateEntrypoints: false, recordDerivedState: false, output() {} });
288
+ for (const canonical of active.entrypoints) {
289
+ const name = entryPaths.get(canonical);
290
+ if (preserveNative(name)) continue;
291
+ const current = read(target, name), template = readFileSync(join(packageRoot, '.pbai4devs/templates/entrypoints', canonical), 'utf8');
292
+ observeNative(name, current);
293
+ const legacy = entryContent(null, template, language, canonical);
294
+ const content = renderEntrypointImports(target, legacy.replace(`${begin}\n`, '').replace(`\n${end}`, ''));
262
295
  const knownFile = current && (previous?.files[name] === hash(current) || derivedEntries[name] === hash(current));
263
- const knownLegacy = current && previous?.blocks[name] && current.toString('utf8') === entryContent(null, template, previous.language, name);
296
+ const knownLegacy = current && previous?.blocks[name] && current.toString('utf8') === entryContent(null, template, previous.language, canonical);
264
297
  if (!current || knownFile || knownLegacy) { write(stage, name, content); ownedEntrypoints.add(name); }
298
+ else {
299
+ const body = entrypointReference(target, canonical);
300
+ const merged = mergeManagedContent(current, body, { expectedHash: blockHash(name) });
301
+ write(stage, name, merged.text); mixedPaths.add(name); recordPreserved(name, 'existing-file-extended');
302
+ }
265
303
  }
266
304
  if (!preserveNative('README.md') && !read(target, 'README.md')) write(stage, 'README.md', `---\ndescription: ${JSON.stringify(text(language, 'readme'))}\nlanguage: ${language}\n---\n\n# ${text(language, 'project')}\n\n${text(language, 'readme')}\n`);
267
305
  if (!existsSync(join(stage, '.pbai4devs/estado/README.md'))) write(stage, '.pbai4devs/estado/README.md', `---\ndescription: AgentDevKit state.\n---\n\n# AgentDevKit\n\n${text(language, 'state')}\n`);
268
306
  const unknown = new Set(existing.filter(path => !supplied.has(path) && !previous?.files[`.pbai4devs/${path}`] && !internal.has(path)));
269
307
  const paths = walk(stage, '', path => !path.split('/').includes('.agent-tests')).filter(path => !path.startsWith('.pbai4devs/') || !unknown.has(path.slice(11)));
270
308
  const desired = new Map(paths.filter(path => !preserveNative(path)).map(path => [path, readFileSync(join(stage, path))]));
309
+ for (const [path, content] of desired) {
310
+ if (!nativeManagedText(path) || canonicalEntrypoint(path)) continue;
311
+ const current = read(target, path);
312
+ observeNative(path, current);
313
+ if (!current) continue;
314
+ const known = [previous?.files[path], derivedFiles[path]].some(digest => digest && digest === hash(current));
315
+ if (known) continue;
316
+ const source = content.toString('utf8'), marker = '<!-- gerado: sincronizar-agentes -->';
317
+ const index = source.indexOf(marker);
318
+ if (!nativeToml(path) && index < 0) fail('E_TEMPLATE', `Missing reference marker: ${path}`);
319
+ const body = nativeToml(path) ? JSON.parse(source.match(/^prompt = (.+)$/m)[1]) : source.slice(index + marker.length).trim();
320
+ const merged = (nativeToml(path) ? mergeTomlContent : mergeManagedContent)(current, body, { expectedHash: blockHash(path) });
321
+ desired.set(path, Buffer.from(merged.text)); mixedPaths.add(path); recordPreserved(path, 'existing-file-extended');
322
+ }
271
323
  // Native discovery manifests must not advertise resources omitted at a human boundary.
272
- const overlapsPreserved = path => [...preserved.keys()].some(kept => path === kept || path.startsWith(`${kept}/`) || kept.startsWith(`${path}/`));
324
+ const overlapsPreserved = path => [...preserved.keys()].some(kept => !mixedPaths.has(kept) && (path === kept || path.startsWith(`${kept}/`) || kept.startsWith(`${path}/`)));
273
325
  for (const [path, content] of desired) if (/^\.agents\/(skills|rules|agents)\.json$/.test(path)) {
274
326
  const discovery = JSON.parse(content);
275
327
  discovery.entries = discovery.entries.filter(entry => !overlapsPreserved(entry.path));
@@ -284,40 +336,46 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
284
336
  else manifest.files[path] = hash(content);
285
337
  }
286
338
  desired.set(statePath, Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`));
287
- const changes = [], conflicts = [...entryConflicts];
339
+ const changes = [], conflicts = [];
340
+ const aliasedEntrypoint = path => canonicalEntrypoint(path) && entryPaths.get(canonicalEntrypoint(path)) !== path;
288
341
  for (const [path, content] of desired) {
289
342
  const current = read(target, path);
343
+ if (nativeInputs.has(path) && (nativeInputs.get(path) === null ? current !== null : !current?.equals(nativeInputs.get(path)))) fail('E_CHANGED', `File changed while planning: ${path}`);
290
344
  if (current?.equals(content)) continue;
291
- const managed = previous?.blocks[path] ? blockDigest(path, current || Buffer.alloc(0)) === previous.blocks[path]
292
- : [previous?.files[path], derivedOutputs[path], derivedEntries[path]].some(digest => digest && hash(current || '') === digest);
293
- const appendBlock = entryNames.includes(path) || path === '.gitignore' || path.endsWith('/.gitignore');
345
+ const managed = [previous?.files[path], derivedOutputs[path], derivedEntries[path], derivedFiles[path]].some(digest => digest && hash(current || '') === digest)
346
+ || knownBlocks[path] && blockDigest(path, current || Buffer.alloc(0)) === knownBlocks[path];
347
+ const appendBlock = mixedPaths.has(path) || canonicalEntrypoint(path) || path === '.gitignore' || path.endsWith('/.gitignore');
294
348
  const conflict = current && path !== statePath && !managed && !appendBlock;
295
- const changedBlock = current && previous?.blocks[path] && !managed;
349
+ const changedBlock = current && knownBlocks[path] && !managed;
296
350
  if (conflict || changedBlock) conflicts.push(path);
297
351
  changes.push({ path, action: current ? 'update' : 'create', content, beforeHash: current ? hash(current) : null });
298
352
  }
299
- for (const path of protectedEntries) {
300
- const current = read(target, path);
301
- changes.push({ path, action: 'update', content: current, beforeHash: hash(current) });
302
- }
303
- for (const [path, digest] of Object.entries(previous?.files || {})) if (!desired.has(path) && !protectedEntries.has(path)) {
353
+ const wholeRecords = [previous?.files || {}, derivedEntries, derivedFiles];
354
+ for (const path of new Set(wholeRecords.flatMap(record => Object.keys(record)))) if (!desired.has(path)) {
355
+ if (aliasedEntrypoint(path)) continue;
304
356
  const current = read(target, path); if (!current) continue;
305
- if (hash(current) !== digest) conflicts.push(path);
357
+ const known = wholeRecords.some(record => record[path] === hash(current));
358
+ if (nativeManagedText(path) && !known) { recordPreserved(path, 'existing-file'); continue; }
359
+ if (!known) conflicts.push(path);
306
360
  changes.push({ path, action: 'remove', content: null, beforeHash: hash(current) });
307
361
  }
308
- for (const [path, digest] of Object.entries(previous?.blocks || {})) if (!desired.has(path)) {
362
+ for (const [recordedPath, digest] of Object.entries(knownBlocks)) {
363
+ const path = canonicalEntrypoint(recordedPath) ? entryPaths.get(canonicalEntrypoint(recordedPath)) : recordedPath;
364
+ if (desired.has(path)) continue;
365
+ if (changes.some(change => change.path === path)) continue;
309
366
  if (path === '.gitignore') continue;
310
367
  const current = read(target, path); if (!current) continue;
311
368
  const source = current.toString('utf8');
312
- const block = path === '.gitignore' ? extractBlock(source, '# BEGIN AGENTDEVKIT', '# END AGENTDEVKIT') : path.endsWith('/.gitignore') ? extractBlock(source, '# BEGIN AGENT IGNORE', '# END AGENT IGNORE') : extractBlock(source);
369
+ const block = path.endsWith('/.gitignore') ? extractBlock(source, '# BEGIN AGENT IGNORE', '# END AGENT IGNORE') : nativeToml(path) ? extractTomlManagedBlock(current) : extractManagedBlock(current);
313
370
  if (!block || hash(block) !== digest) conflicts.push(path);
314
371
  if (!block) continue;
315
- let content = source.replace(block, '');
316
- if (entryNames.includes(path)) {
317
- const template = readFileSync(join(packageRoot, '.pbai4devs/templates/entrypoints', path), 'utf8');
318
- if (source === entryContent(null, template, previous.language, path)) content = '';
372
+ let content = path.endsWith('/.gitignore') ? source.replace(block, '') : (nativeToml(path) ? stripTomlContent : stripManagedContent)(current, digest);
373
+ let removable = path.endsWith('/.gitignore') && !content.trim();
374
+ if (canonicalEntrypoint(path)) {
375
+ const template = readFileSync(join(packageRoot, '.pbai4devs/templates/entrypoints', canonicalEntrypoint(path)), 'utf8');
376
+ if (source === entryContent(null, template, previous?.language || language, canonicalEntrypoint(path))) { content = ''; removable = true; }
319
377
  }
320
- changes.push({ path, action: content.trim() ? 'update' : 'remove', content: content.trim() ? Buffer.from(content) : null, beforeHash: hash(current) });
378
+ changes.push({ path, action: removable ? 'remove' : 'update', content: removable ? null : Buffer.from(content), beforeHash: hash(current) });
321
379
  }
322
380
  function nativeFiles(root) {
323
381
  if (boundary(root)) return [];
@@ -340,9 +398,9 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
340
398
  const path = full.slice(root.length + 1);
341
399
  if (!['skills', 'agents', 'commands', 'rules', 'instructions', 'prompts'].includes(path.split('/')[0])) continue;
342
400
  if (desired.has(full) || changes.some(change => change.path === full)) continue;
343
- const current = read(target, full), source = current.toString('utf8');
344
- if (!(source.includes('<!-- gerado: sincronizar-agentes -->') || source.startsWith('# gerado: sincronizar-agentes\n'))) { recordPreserved(full, 'existing-file'); continue; }
345
- if (!isReference(source)) conflicts.push(full);
401
+ const current = read(target, full);
402
+ const known = [previous?.files[full], derivedFiles[full]].some(digest => digest && digest === hash(current));
403
+ if (preserved.has(full) || !known || !generatedReference(current)) { recordPreserved(full, 'existing-file'); continue; }
346
404
  changes.push({ path: full, action: 'remove', content: null, beforeHash: hash(current) });
347
405
  }
348
406
  const legacy = '.pbai4devs/adapters/README.md', legacyContent = read(target, legacy);
@@ -352,9 +410,9 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
352
410
  changes.push({ path: legacy, action: 'remove', content: null, beforeHash: hash(legacyContent) });
353
411
  }
354
412
  const retiredRoots = adapterRoots.filter(root => !active.roots.includes(root) && changes.some(change => change.action === 'remove' && change.path.startsWith(`${root}/`) && (previous?.files[change.path] || previous?.blocks[change.path])));
355
- const ownedOutputs = [...new Set([...synchronized.ownedOutputs, ...ownedEntrypoints, statePath])].filter(path => !overlapsPreserved(path));
413
+ const ownedOutputs = [...new Set([...synchronized.ownedOutputs, ...ownedEntrypoints, statePath])].filter(path => !mixedPaths.has(path) && !overlapsPreserved(path));
356
414
  const hashes = {};
357
- for (const [path, digest] of Object.entries(previous?.files || {})) if ((ownedOutputs.includes(path) || !path.startsWith('.pbai4devs/')) && !protectedEntries.has(path)) hashes[path] = digest;
415
+ for (const [path, digest] of Object.entries(previous?.files || {})) if ((ownedOutputs.includes(path) || !path.startsWith('.pbai4devs/')) && !mixedPaths.has(path) && !overlapsPreserved(path) && !aliasedEntrypoint(path)) hashes[path] = digest;
358
416
  for (const path of ownedEntrypoints) { const current = read(target, path); if (current && (previous?.blocks[path] || derivedEntries[path] === hash(current))) hashes[path] = hash(current); }
359
417
  for (const path of Object.keys(previous?.blocks || {})) if (path.endsWith('/.gitignore') && adapterRoots.some(root => path === `${root}/.gitignore`)) {
360
418
  const current = read(target, path);
@@ -385,16 +443,22 @@ export function applyPlan(plan) {
385
443
  for (const [path, content] of before) if (content) write(target, `${backup}/${path}`, content, beforeModes.get(path));
386
444
  const applied = [], removedDirectories = [];
387
445
  let untracked = false;
446
+ const assertOriginal = path => {
447
+ const file = safePath(target, path), info = stat(file), bytes = before.get(path);
448
+ if (bytes === null ? info !== null : !info?.isFile() || !readFileSync(file).equals(bytes) || (info.mode & 0o777) !== beforeModes.get(path)) fail('E_CHANGED', `File changed before writing: ${path}`);
449
+ };
388
450
  try {
451
+ for (const change of plan.changes) assertOriginal(change.path);
389
452
  if (plan.untrack?.paths.length) { applyUntrack(plan.untrack); untracked = true; }
390
453
  for (const change of plan.changes) {
454
+ assertOriginal(change.path);
391
455
  const file = safePath(target, change.path);
392
456
  const content = change.content;
393
457
  const signature = { path: change.path, hash: content === null ? null : hash(content) };
394
458
  if (content === null) { rmSync(file, { force: true }); applied.push(signature); }
395
459
  else atomicWrite(target, change.path, content, beforeModes.get(change.path), mode => {
396
460
  signature.mode = mode; applied.push(signature);
397
- });
461
+ }, () => assertOriginal(change.path));
398
462
  }
399
463
  pruneRemovedDirectories(plan, directory => removedDirectories.push(directory));
400
464
  const changesFallback = plan.changes.some(change => change.path === '.gitignore');
@@ -413,16 +477,25 @@ export function applyPlan(plan) {
413
477
  }
414
478
  for (const appliedFile of applied.reverse()) {
415
479
  const { path } = appliedFile;
416
- let unchanged = false;
417
- try {
418
- const file = safePath(target, path), info = stat(file);
419
- unchanged = appliedFile.hash === null ? !info : Boolean(info?.isFile()
420
- && (info.mode & 0o777) === appliedFile.mode && hash(readFileSync(file)) === appliedFile.hash);
421
- } catch { /* Não restaurar quando não é possível comprovar a própria gravação. */ }
480
+ const stillApplied = () => {
481
+ try {
482
+ const file = safePath(target, path), info = stat(file);
483
+ return appliedFile.hash === null ? !info : Boolean(info?.isFile()
484
+ && (info.mode & 0o777) === appliedFile.mode && hash(readFileSync(file)) === appliedFile.hash);
485
+ } catch { return false; }
486
+ };
487
+ const unchanged = stillApplied();
422
488
  if (!unchanged) { preserved.push(path); continue; }
423
489
  const content = before.get(path);
424
- try { if (content) atomicWrite(target, path, content, beforeModes.get(path)); else rmSync(safePath(target, path), { force: true }); }
425
- catch (failure) { rollbackErrors.push(failure); }
490
+ try {
491
+ if (content) atomicWrite(target, path, content, beforeModes.get(path), undefined, () => {
492
+ if (!stillApplied()) fail('E_CHANGED', `File changed during rollback: ${path}`);
493
+ });
494
+ else rmSync(safePath(target, path), { force: true });
495
+ } catch (failure) {
496
+ if (failure.code === 'E_CHANGED') preserved.push(path);
497
+ else rollbackErrors.push(failure);
498
+ }
426
499
  }
427
500
  if (preserved.length) {
428
501
  try { error.rollbackPreservedPaths = preserved; } catch { /* Preserve a exceção original. */ }
@@ -469,11 +542,14 @@ export function inspect(target) {
469
542
  const derived = readDerivedState(target);
470
543
  const issues = [];
471
544
  for (const [path, digest] of Object.entries(state.files)) {
545
+ if (derived.blocks[path] || derived.removedBlocks.includes(path)) continue;
472
546
  const content = read(target, path);
473
- if (!content || hash(content) !== digest && hash(content) !== derived.outputs[path]) issues.push({ code: content ? 'modified' : 'missing', path });
547
+ if (!content || hash(content) !== digest && hash(content) !== derived.outputs[path] && hash(content) !== derived.files[path] && hash(content) !== derived.entries[path]) issues.push({ code: content ? 'modified' : 'missing', path });
474
548
  }
475
- for (const [path, digest] of Object.entries(state.blocks)) {
549
+ for (const [path, digest] of Object.entries({ ...state.blocks, ...derived.blocks })) {
550
+ if (derived.removedBlocks.includes(path) && !Object.hasOwn(derived.blocks, path)) continue;
476
551
  const content = read(target, path);
552
+ if (content && [derived.entries[path], derived.files[path]].some(value => value && hash(content) === value)) continue;
477
553
  if (!content || blockDigest(path, content) !== digest) issues.push({ code: content ? 'modified' : 'missing', path });
478
554
  }
479
555
  return { installed: true, version: state.version, language: state.language, issues };