@pathbit/agentdevkit 0.0.20 → 0.0.22

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.
@@ -0,0 +1,16 @@
1
+ ---
2
+ description: Criação e publicação seguras de tags de versões verificadas.
3
+ ---
4
+
5
+ # Tags automáticas de release
6
+
7
+ ## Critérios de aceite
8
+
9
+ - AC-001: A CI cria `v<versão>` no commit da branch principal somente após
10
+ toda a matriz passar e inicia a publicação do mesmo commit.
11
+ - AC-002: Alterações sem nova versão não duplicam publicações; tags existentes
12
+ nunca são movidas e colisões ou regressões de versão impedem a publicação.
13
+ - AC-003: A release e os dois pacotes usam o mesmo artefato e a mesma versão
14
+ da tag; a publicação exige o CI completo aprovado.
15
+ - AC-004: Testes verificam eventos indevidos, falhas, colisões, retomada e
16
+ corridas sem criar tags ou publicar pacotes reais.
@@ -0,0 +1,26 @@
1
+ ---
2
+ description: Descoberta de skills e preservação em projetos existentes.
3
+ ---
4
+
5
+ # Descoberta e preservação
6
+
7
+ As integrações devem disponibilizar os recursos sem duplicar skills geradas
8
+ para o Codex e sem substituir configurações ou alterações próprias do projeto.
9
+
10
+ ## Critérios de aceite
11
+
12
+ - AC-001: O Codex descobre cada skill na rota principal; rotas alternativas
13
+ são usadas por recurso quando necessário e conflitos são informados.
14
+ - AC-002: `init`, `update` e `sync` preservam conteúdo humano, índice Git e
15
+ `.gitignore` da raiz; limpeza exige propriedade comprovada.
16
+ - AC-003: A sincronização independente e a atualização reconciliam o estado,
17
+ preservando campos adicionais e sinalizando inconsistências.
18
+ - AC-004: Scripts identificam sua execução principal por caminhos reais ou
19
+ aliases, sem executar novamente quando importados.
20
+ - AC-005: Os exemplos de maturidade funcionam também a partir do pacote
21
+ distribuído, preservando arquivos da origem e exemplos anteriores.
22
+ - AC-006: Testes cobrem combinações de clientes e projetos existentes; resultados,
23
+ clones e projetos gerados ficam ignorados ou fora do repositório.
24
+
25
+ Descoberta e validação estrutural não demonstram execução completa de todas as
26
+ skills e perfis em cada cliente. Registre os limites das provas realizadas.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pathbit/agentdevkit",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "description": "Shared skills, specifications and workflows for AI coding agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/install.mjs CHANGED
@@ -169,6 +169,19 @@ export function safePath(root, path) {
169
169
  return current;
170
170
  }
171
171
 
172
+ // Inspect the route without following user-owned links. A final directory is
173
+ // also unavailable where the generated skill requires a regular file.
174
+ function nativeOutputBoundary(root, path, ownedPaths) {
175
+ let boundary = nativePathBoundary(root, path, ownedPaths, { preserveRootAliases: true });
176
+ if (!boundary) {
177
+ const info = stat(safePath(root, path));
178
+ if (info && !info.isFile()) boundary = { path, reason: 'non-file' };
179
+ }
180
+ if (!boundary) return { blocked: false };
181
+ const info = lstatSync(join(root, ...boundary.path.split('/')));
182
+ return { blocked: true, ...boundary, identity: [info.dev, info.ino, info.mode, info.size, info.mtimeMs, info.ctimeMs] };
183
+ }
184
+
172
185
  function resourceAllowed(path, directory = false) {
173
186
  const parts = path.split('/');
174
187
  const folders = new Set(['agents', 'commands', 'external-skills', 'rules', 'scripts', 'skills', 'templates', 'tools', 'state']);
@@ -388,30 +401,85 @@ function copyResources(source, destination) {
388
401
  return files;
389
402
  }
390
403
 
404
+ // Change only one top-level JSON member. Other fields, escaping and whitespace
405
+ // remain byte-for-byte intact, including user-provided fields in local state.
406
+ function replaceJsonMember(bytes, member, value) {
407
+ const text = bytes.toString('utf8');
408
+ if (!Buffer.from(text, 'utf8').equals(bytes)) fail('E_STATE', 'Invalid UTF-8 in derived state.');
409
+ let i = 0;
410
+ const space = () => { while (/\s/.test(text[i] || '') && i < text.length) i++; };
411
+ const stringEnd = () => {
412
+ if (text[i++] !== '"') fail('E_STATE', 'Invalid JSON member.');
413
+ while (i < text.length) {
414
+ const char = text[i++];
415
+ if (char === '\\') { i++; continue; }
416
+ if (char === '"') return;
417
+ }
418
+ fail('E_STATE', 'Unterminated JSON string.');
419
+ };
420
+ space(); if (text[i++] !== '{') fail('E_STATE', 'Invalid derived state.');
421
+ const properties = [];
422
+ while (true) {
423
+ space(); if (text[i] === '}') break;
424
+ const start = i; stringEnd(); const key = JSON.parse(text.slice(start, i));
425
+ space(); if (text[i++] !== ':') fail('E_STATE', 'Invalid JSON member.');
426
+ space(); const valueStart = i;
427
+ if (text[i] === '"') stringEnd();
428
+ else if (text[i] === '{' || text[i] === '[') {
429
+ const stack = [];
430
+ do {
431
+ const char = text[i];
432
+ if (char === '"') { stringEnd(); continue; }
433
+ i++;
434
+ if (char === '{' || char === '[') stack.push(char);
435
+ else if (char === '}' || char === ']') stack.pop();
436
+ } while (stack.length && i < text.length);
437
+ if (stack.length) fail('E_STATE', 'Invalid derived state.');
438
+ } else { while (i < text.length && text[i] !== ',' && text[i] !== '}') i++; while (i > valueStart && /\s/.test(text[i - 1])) i--; }
439
+ const end = i; properties.push({ key, start, valueStart, end });
440
+ space(); if (text[i] === ',') { i++; continue; } if (text[i] === '}') break;
441
+ fail('E_STATE', 'Invalid JSON member separator.');
442
+ }
443
+ const matches = properties.filter(item => item.key === member);
444
+ if (matches.length !== 1) fail('E_STATE', 'Ambiguous derived state member.');
445
+ const current = matches[0], index = properties.indexOf(current);
446
+ let next;
447
+ if (value !== undefined) next = text.slice(0, current.valueStart) + JSON.stringify(value, null, 2) + text.slice(current.end);
448
+ else if (index > 0) next = text.slice(0, properties[index - 1].end) + text.slice(current.end);
449
+ else if (properties.length > 1) next = text.slice(0, current.start) + text.slice(properties[1].start);
450
+ else next = text.slice(0, current.start) + text.slice(current.end);
451
+ try { JSON.parse(next); } catch { fail('E_STATE', 'Unable to preserve derived state safely.'); }
452
+ return Buffer.from(next);
453
+ }
454
+
391
455
  function readDerivedState(target) {
392
- let entries = {}, outputs = {}, blocks = {}, files = {}, removedBlocks = [];
456
+ let entries = {}, outputs = {}, blocks = {}, files = {}, removedBlocks = [], removedFiles = {};
393
457
  const derived = read(target, actualStatePath(target).replace(/agentdevkit\.json$/, 'derived.json'));
394
458
  if (derived) {
395
459
  try {
396
460
  const value = JSON.parse(derived);
397
461
  if (value.schemaVersion !== 1 || !value.entries || typeof value.entries !== 'object' || Array.isArray(value.entries)
398
- || ['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.');
399
- entries = value.entries; outputs = value.outputs ?? {}; blocks = value.blocks ?? {}; files = value.files ?? {};
462
+ || ['outputs', 'blocks', 'files', 'removedFiles'].some(key => value[key] !== undefined && (!value[key] || typeof value[key] !== 'object' || Array.isArray(value[key])))) fail('E_STATE', 'Invalid derived entrypoint state.');
463
+ entries = value.entries; outputs = value.outputs ?? {}; blocks = value.blocks ?? {}; files = value.files ?? {}; removedFiles = value.removedFiles ?? {};
400
464
  removedBlocks = value.removedBlocks ?? [];
401
465
  if (!Array.isArray(removedBlocks) || removedBlocks.some(path => typeof path !== 'string' || !nativeManagedText(path))) fail('E_STATE', 'Invalid removed block state.');
402
466
  for (const [path, digest] of Object.entries(entries)) if (!canonicalEntrypoint(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived entrypoint state.');
403
467
  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.');
404
- for (const [path, digest] of Object.entries(blocks)) if (!nativeManagedText(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived block state.');
468
+ for (const [path, digest] of Object.entries(blocks)) if (!(nativeManagedText(path) || adapterRoots.some(root => path === `${root}/.gitignore`)) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived block state.');
405
469
  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.');
470
+ for (const [path, digest] of Object.entries(removedFiles)) if (canonicalEntrypoint(path) || !(nativeMarkdown(path) || nativeToml(path)) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid removed file state.');
471
+ const installed = loadState(target);
472
+ if (Object.entries(removedFiles).some(([path, digest]) => installed?.files?.[path] && installed.files[path] !== digest)) fail('E_STATE', 'Removed file state differs from installation.');
406
473
  }
407
474
  catch { fail('E_STATE', 'Invalid derived entrypoint state.'); }
408
- for (const path of [...Object.keys(entries), ...Object.keys(outputs), ...Object.keys(blocks), ...Object.keys(files), ...removedBlocks]) safePath(target, path);
475
+ for (const path of [...Object.keys(entries), ...Object.keys(outputs), ...Object.keys(blocks), ...Object.keys(files), ...Object.keys(removedFiles), ...removedBlocks]) safePath(target, path);
409
476
  }
410
- return { entries, outputs, blocks, files, removedBlocks };
477
+ return { entries, outputs, blocks, files, removedBlocks, removedFiles };
411
478
  }
412
479
 
413
480
  export function planInstallation({ target, language = 'pt', mode = 'init', force = false, clients }) {
414
- const previous = loadState(target);
481
+ let previous = loadState(target);
482
+ const installedState = previous;
415
483
  const migration = legacyStateMigration(target);
416
484
  // A migration must update the installed scripts and pointers in the same
417
485
  // transaction; a plain sync/agents/lang would leave old hard-coded paths.
@@ -427,7 +495,8 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
427
495
  if (original === null ? content !== null : !content?.equals(original)) fail('E_CHANGED', `File changed while planning: ${path}`);
428
496
  } else nativeInputs.set(path, content);
429
497
  };
430
- const { entries: derivedEntries, outputs: derivedOutputs, blocks: derivedBlocks, files: derivedFiles, removedBlocks } = readDerivedState(target);
498
+ const { entries: derivedEntries, outputs: derivedOutputs, blocks: derivedBlocks, files: derivedFiles, removedBlocks, removedFiles } = readDerivedState(target);
499
+ if (previous) previous = { ...previous, files: Object.fromEntries(Object.entries(previous.files).filter(([path, digest]) => removedFiles[path] !== digest)) };
431
500
  const previousBlocks = Object.fromEntries(Object.entries(previous?.blocks || {}).filter(([path]) => !removedBlocks.includes(path)));
432
501
  const knownBlocks = { ...previousBlocks, ...derivedBlocks };
433
502
  const ownedPaths = [...Object.keys(previous?.files || {}), ...Object.keys(previousBlocks), ...Object.keys(derivedEntries), ...Object.keys(derivedOutputs), ...Object.keys(derivedBlocks), ...Object.keys(derivedFiles)];
@@ -436,6 +505,14 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
436
505
  ?? (canonicalEntrypoint(path) ? Object.entries(knownBlocks).find(([candidate]) => canonicalEntrypoint(candidate) === canonicalEntrypoint(path))?.[1] : undefined);
437
506
  const preserved = new Map();
438
507
  const recordPreserved = (path, reason) => { preserved.set(path, { path, reason }); return true; };
508
+ const fallbackChecks = new Map();
509
+ const outputBlocked = path => {
510
+ const result = nativeOutputBoundary(target, path, ownedPaths);
511
+ if (fallbackChecks.has(path) && JSON.stringify(fallbackChecks.get(path)) !== JSON.stringify(result)) fail('E_CHANGED', `Skill route changed while planning: ${path}`);
512
+ fallbackChecks.set(path, result);
513
+ if (result.blocked) recordPreserved(result.path, result.reason);
514
+ return result;
515
+ };
439
516
  const stagedHuman = stagedDivergences(target);
440
517
  const native = path => canonicalEntrypoint(path) || path === 'README.md' || adapterRoots.some(root => path === root || path.startsWith(`${root}/`));
441
518
  const boundary = path => {
@@ -504,7 +581,7 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
504
581
  const ownIgnore = read(target, '.pbai4devs/.gitignore');
505
582
  if (ownIgnore) write(stage, '.pbai4devs/.gitignore', ownIgnore);
506
583
  writeAdapterManifest(stage, { clients: selected });
507
- const synchronized = synchronize({ root: stage, force: true, localIgnores: false, generateEntrypoints: false, recordDerivedState: false, output() {} });
584
+ const synchronized = synchronize({ root: stage, force: true, localIgnores: false, generateEntrypoints: false, recordDerivedState: false, outputBlocked, output() {} });
508
585
  for (const canonical of active.entrypoints) {
509
586
  const name = entryPaths.get(canonical);
510
587
  if (preserveNative(name)) continue;
@@ -577,14 +654,45 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
577
654
  if (block) manifest.blocks[path] = block;
578
655
  else manifest.files[path] = hash(content);
579
656
  }
580
- desired.set(statePath, Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`));
657
+ // A standalone sync records its proven outputs separately. During a later
658
+ // check/sync, compare that overlay before declaring the installation state
659
+ // stale; update still consolidates it into the main state transactionally.
660
+ let stateContent = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
661
+ if (mode === 'sync' && installedState && !changingClients) {
662
+ const files = { ...installedState.files }, blocks = { ...installedState.blocks };
663
+ for (const [path, digest] of Object.entries(removedFiles)) {
664
+ if (files[path] === digest && !stat(safePath(target, path))) delete files[path];
665
+ }
666
+ for (const path of removedBlocks) delete blocks[path];
667
+ for (const [path, digest] of Object.entries({ ...derivedFiles, ...derivedOutputs, ...derivedEntries })) { files[path] = digest; delete blocks[path]; }
668
+ for (const [path, digest] of Object.entries(derivedBlocks)) { blocks[path] = digest; delete files[path]; }
669
+ const same = (a, b) => {
670
+ const keys = Object.keys(a).sort();
671
+ return keys.length === Object.keys(b).length && keys.every(key => a[key] === b[key]);
672
+ };
673
+ if (installedState.schemaVersion === manifest.schemaVersion && installedState.version === manifest.version && installedState.language === manifest.language
674
+ && JSON.stringify(installedState.clients) === JSON.stringify(manifest.clients) && same(files, manifest.files) && same(blocks, manifest.blocks)) stateContent = read(target, statePath);
675
+ }
676
+ desired.set(statePath, stateContent);
677
+ const derivedPath = actualStatePath(target).replace(/agentdevkit\.json$/, 'derived.json');
678
+ const derivedBytes = !migration ? read(target, derivedPath) : null;
679
+ if (derivedBytes && Object.keys(removedFiles).length) {
680
+ const finalState = JSON.parse(stateContent);
681
+ const remaining = Object.fromEntries(Object.entries(removedFiles).filter(([path, digest]) =>
682
+ finalState.files[path] === digest && !stat(safePath(target, path))));
683
+ if (Object.keys(remaining).length !== Object.keys(removedFiles).length) {
684
+ observeNative(derivedPath, derivedBytes);
685
+ desired.set(derivedPath, replaceJsonMember(derivedBytes, 'removedFiles', Object.keys(remaining).length ? remaining : undefined));
686
+ }
687
+ }
581
688
  const changes = [], conflicts = [];
582
689
  const aliasedEntrypoint = path => canonicalEntrypoint(path) && entryPaths.get(canonicalEntrypoint(path)) !== path;
583
690
  for (const [path, content] of desired) {
584
691
  const current = read(target, path);
585
692
  if (nativeInputs.has(path) && (nativeInputs.get(path) === null ? current !== null : !current?.equals(nativeInputs.get(path)))) fail('E_CHANGED', `File changed while planning: ${path}`);
586
693
  if (current?.equals(content)) continue;
587
- const managed = [previous?.files[path], derivedOutputs[path], derivedEntries[path], derivedFiles[path]].some(digest => digest && hash(current || '') === digest)
694
+ const managed = path === derivedPath && derivedBytes && current?.equals(derivedBytes)
695
+ || [previous?.files[path], derivedOutputs[path], derivedEntries[path], derivedFiles[path]].some(digest => digest && hash(current || '') === digest)
588
696
  || knownBlocks[path] && blockDigest(path, current || Buffer.alloc(0)) === knownBlocks[path];
589
697
  const appendBlock = mixedPaths.has(path) || canonicalEntrypoint(path) || path === '.gitignore' || path.endsWith('/.gitignore');
590
698
  const conflict = current && path !== statePath && !managed && !appendBlock;
@@ -704,7 +812,7 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
704
812
  const localIgnore = planLocalIgnore(target, { outputs: ownedOutputs, templateRoot: stage });
705
813
  const localIgnoreCheck = checkLocalIgnore(localIgnore, { replacedBlocks: replacedIgnoreBlocks(target, changes, previous) });
706
814
  conflicts.push(...(untrack?.conflicts || []), ...localIgnoreCheck.missing);
707
- return { target, language, mode, changes, conflicts: [...new Set(conflicts)].sort(), canApply: (!conflicts.length || force) && !untrack?.conflicts?.length && !localIgnoreCheck.missing.length, manifest, retiredRoots, ownedOutputs, untrack, localIgnore, localIgnoreCheck, migration, preserved: [...preserved.values()].sort((a, b) => a.path.localeCompare(b.path)) };
815
+ return { target, language, mode, changes, conflicts: [...new Set(conflicts)].sort(), canApply: (!conflicts.length || force) && !untrack?.conflicts?.length && !localIgnoreCheck.missing.length, manifest, retiredRoots, ownedOutputs, untrack, localIgnore, localIgnoreCheck, migration, fallbackChecks: [...fallbackChecks], fallbackOwnedPaths: ownedPaths, preserved: [...preserved.values()].sort((a, b) => a.path.localeCompare(b.path)) };
708
816
  } finally { rmSync(stage, { recursive: true, force: true }); }
709
817
  }
710
818
 
@@ -712,6 +820,15 @@ export function applyPlan(plan) {
712
820
  if (!plan.canApply) fail('E_CONFLICT', text(plan.language, 'conflict'));
713
821
  assertLocalIgnoreUnchanged(plan.localIgnore);
714
822
  const { target } = plan;
823
+ const assertFallbackChecks = () => {
824
+ for (const [path, expected] of plan.fallbackChecks || []) {
825
+ let current;
826
+ try { current = nativeOutputBoundary(target, path, plan.fallbackOwnedPaths || []); }
827
+ catch { fail('E_CHANGED', `Skill route changed after planning: ${path}`); }
828
+ if (JSON.stringify(current) !== JSON.stringify(expected)) fail('E_CHANGED', `Skill route changed after planning: ${path}`);
829
+ }
830
+ };
831
+ assertFallbackChecks();
715
832
  const stagedHuman = stagedDivergences(target);
716
833
  const currentState = loadState(target);
717
834
  const stagedChanges = plan.changes.filter(change => {
@@ -793,6 +910,7 @@ export function applyPlan(plan) {
793
910
  rmdirSync(file); removedDirectories.push({ path: directory.path, mode: directory.mode });
794
911
  }
795
912
  }
913
+ assertFallbackChecks();
796
914
  pruneRemovedDirectories(plan, directory => removedDirectories.push(directory));
797
915
  const changesFallback = plan.changes.some(change => change.path === '.gitignore');
798
916
  // Metadata is committed last; its writer rolls back partial writes itself.
@@ -881,6 +999,10 @@ export function inspect(target) {
881
999
  for (const [path, digest] of Object.entries(state.files)) {
882
1000
  if (derived.blocks[path] || derived.removedBlocks.includes(path)) continue;
883
1001
  const content = read(target, path);
1002
+ if (derived.removedFiles[path] === digest && !derived.files[path] && !derived.entries[path]) {
1003
+ if (content) issues.push({ code: 'modified', path });
1004
+ continue;
1005
+ }
884
1006
  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 });
885
1007
  }
886
1008
  for (const [path, digest] of Object.entries({ ...state.blocks, ...derived.blocks })) {