@pathbit/agentdevkit 0.0.23 → 0.0.24
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/scripts/sincronizar-agentes/SCRIPT.mjs +59 -11
- package/.pbai4devs/scripts/sincronizar-agentes/tests/CATALOG_OWNERSHIP.mjs +251 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +33 -13
- package/.pbai4devs/scripts/testar-maturidade/README.md +12 -3
- package/.pbai4devs/scripts/testar-maturidade/SCRIPT.mjs +65 -25
- package/.pbai4devs/scripts/testar-maturidade/tests/TEST.mjs +150 -5
- package/CHANGELOG.md +7 -0
- package/docs/guides/troubleshooting.md +8 -6
- package/package.json +1 -1
- package/src/i18n.mjs +3 -3
- package/src/install.mjs +100 -18
package/src/install.mjs
CHANGED
|
@@ -457,7 +457,14 @@ function replaceJsonMember(bytes, member, value) {
|
|
|
457
457
|
fail('E_STATE', 'Invalid JSON member separator.');
|
|
458
458
|
}
|
|
459
459
|
const matches = properties.filter(item => item.key === member);
|
|
460
|
-
if (matches.length
|
|
460
|
+
if (matches.length > 1) fail('E_STATE', 'Ambiguous state member.');
|
|
461
|
+
if (!matches.length) {
|
|
462
|
+
if (value === undefined) return bytes;
|
|
463
|
+
const insertion = `${properties.length ? ',' : ''}\n ${JSON.stringify(member)}: ${JSON.stringify(value, null, 2)}\n`;
|
|
464
|
+
const next = text.slice(0, i) + insertion + text.slice(i);
|
|
465
|
+
try { JSON.parse(next); } catch { fail('E_STATE', 'Unable to preserve state safely.'); }
|
|
466
|
+
return Buffer.from(next);
|
|
467
|
+
}
|
|
461
468
|
const current = matches[0], index = properties.indexOf(current);
|
|
462
469
|
let next;
|
|
463
470
|
if (value !== undefined) next = text.slice(0, current.valueStart) + JSON.stringify(value, null, 2) + text.slice(current.end);
|
|
@@ -468,29 +475,44 @@ function replaceJsonMember(bytes, member, value) {
|
|
|
468
475
|
return Buffer.from(next);
|
|
469
476
|
}
|
|
470
477
|
|
|
478
|
+
function reconcileStateBytes(original, previous, next) {
|
|
479
|
+
if (!original) return Buffer.from(`${JSON.stringify(next, null, 2)}\n`);
|
|
480
|
+
const canonical = value => value && typeof value === 'object' && !Array.isArray(value)
|
|
481
|
+
? Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])]))
|
|
482
|
+
: Array.isArray(value) ? value.map(canonical) : value;
|
|
483
|
+
let result = original;
|
|
484
|
+
for (const key of ['schemaVersion', 'version', 'language', 'clients', 'files', 'blocks']) {
|
|
485
|
+
if (JSON.stringify(canonical(previous?.[key])) !== JSON.stringify(canonical(next[key]))) result = replaceJsonMember(result, key, next[key]);
|
|
486
|
+
}
|
|
487
|
+
return result;
|
|
488
|
+
}
|
|
489
|
+
|
|
471
490
|
function readDerivedState(target) {
|
|
472
|
-
let entries = {}, outputs = {}, blocks = {}, files = {}, removedBlocks = [], removedFiles = {};
|
|
491
|
+
let entries = {}, outputs = {}, blocks = {}, files = {}, catalogs = {}, config = null, removedBlocks = [], removedFiles = {};
|
|
473
492
|
const derived = read(target, actualStatePath(target).replace(/agentdevkit\.json$/, 'derived.json'));
|
|
474
493
|
if (derived) {
|
|
475
494
|
try {
|
|
476
495
|
const value = JSON.parse(derived);
|
|
477
496
|
if (value.schemaVersion !== 1 || !value.entries || typeof value.entries !== 'object' || Array.isArray(value.entries)
|
|
478
|
-
|| ['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.');
|
|
479
|
-
entries = value.entries; outputs = value.outputs ?? {}; blocks = value.blocks ?? {}; files = value.files ?? {}; removedFiles = value.removedFiles ?? {};
|
|
497
|
+
|| ['outputs', 'blocks', 'files', 'catalogs', 'removedFiles'].some(key => value[key] !== undefined && (!value[key] || typeof value[key] !== 'object' || Array.isArray(value[key])))) fail('E_STATE', 'Invalid derived entrypoint state.');
|
|
498
|
+
entries = value.entries; outputs = value.outputs ?? {}; blocks = value.blocks ?? {}; files = value.files ?? {}; catalogs = value.catalogs ?? {}; removedFiles = value.removedFiles ?? {};
|
|
499
|
+
if (value.config !== undefined && (typeof value.config !== 'string' || !/^[a-f0-9]{64}$/.test(value.config))) fail('E_STATE', 'Invalid derived configuration state.');
|
|
500
|
+
config = value.config ?? null;
|
|
480
501
|
removedBlocks = value.removedBlocks ?? [];
|
|
481
502
|
if (!Array.isArray(removedBlocks) || removedBlocks.some(path => typeof path !== 'string' || !nativeManagedText(path))) fail('E_STATE', 'Invalid removed block state.');
|
|
482
503
|
for (const [path, digest] of Object.entries(entries)) if (!canonicalEntrypoint(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived entrypoint state.');
|
|
483
504
|
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.');
|
|
484
|
-
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.');
|
|
505
|
+
for (const [path, digest] of Object.entries(blocks)) if (!(nativeManagedText(path) || path === '.pbai4devs/.gitignore' || adapterRoots.some(root => path === `${root}/.gitignore`)) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived block state.');
|
|
485
506
|
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.');
|
|
507
|
+
for (const [path, digest] of Object.entries(catalogs)) if (!/^\.pbai4devs\/(?:catalog|skills|agents|rules)\.json$/.test(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived catalogue state.');
|
|
486
508
|
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.');
|
|
487
509
|
const installed = loadState(target);
|
|
488
510
|
if (Object.entries(removedFiles).some(([path, digest]) => installed?.files?.[path] && installed.files[path] !== digest)) fail('E_STATE', 'Removed file state differs from installation.');
|
|
489
511
|
}
|
|
490
512
|
catch { fail('E_STATE', 'Invalid derived entrypoint state.'); }
|
|
491
|
-
for (const path of [...Object.keys(entries), ...Object.keys(outputs), ...Object.keys(blocks), ...Object.keys(files), ...Object.keys(removedFiles), ...removedBlocks]) safePath(target, path);
|
|
513
|
+
for (const path of [...Object.keys(entries), ...Object.keys(outputs), ...Object.keys(blocks), ...Object.keys(files), ...Object.keys(catalogs), ...Object.keys(removedFiles), ...removedBlocks]) safePath(target, path);
|
|
492
514
|
}
|
|
493
|
-
return { entries, outputs, blocks, files, removedBlocks, removedFiles };
|
|
515
|
+
return { entries, outputs, blocks, files, catalogs, config, removedBlocks, removedFiles };
|
|
494
516
|
}
|
|
495
517
|
|
|
496
518
|
export function planInstallation({ target, language = 'pt', mode = 'init', force = false, clients }) {
|
|
@@ -511,11 +533,12 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
511
533
|
if (original === null ? content !== null : !content?.equals(original)) fail('E_CHANGED', `File changed while planning: ${path}`);
|
|
512
534
|
} else nativeInputs.set(path, content);
|
|
513
535
|
};
|
|
514
|
-
const { entries: derivedEntries, outputs: derivedOutputs, blocks: derivedBlocks, files: derivedFiles, removedBlocks, removedFiles } = readDerivedState(target);
|
|
536
|
+
const { entries: derivedEntries, outputs: derivedOutputs, blocks: derivedBlocks, files: derivedFiles, catalogs: derivedCatalogs, config: derivedConfig, removedBlocks, removedFiles } = readDerivedState(target);
|
|
537
|
+
const derivedConfigFiles = derivedConfig ? { '.pbai4devs/espelhos.conf': derivedConfig } : {};
|
|
515
538
|
if (previous) previous = { ...previous, files: Object.fromEntries(Object.entries(previous.files).filter(([path, digest]) => removedFiles[path] !== digest)) };
|
|
516
539
|
const previousBlocks = Object.fromEntries(Object.entries(previous?.blocks || {}).filter(([path]) => !removedBlocks.includes(path)));
|
|
517
540
|
const knownBlocks = { ...previousBlocks, ...derivedBlocks };
|
|
518
|
-
const ownedPaths = [...Object.keys(previous?.files || {}), ...Object.keys(previousBlocks), ...Object.keys(derivedEntries), ...Object.keys(derivedOutputs), ...Object.keys(derivedBlocks), ...Object.keys(derivedFiles)];
|
|
541
|
+
const ownedPaths = [...Object.keys(previous?.files || {}), ...Object.keys(previousBlocks), ...Object.keys(derivedEntries), ...Object.keys(derivedOutputs), ...Object.keys(derivedBlocks), ...Object.keys(derivedFiles), ...Object.keys(derivedCatalogs), ...Object.keys(derivedConfigFiles)];
|
|
519
542
|
const entryPaths = new Map(entryNames.map(name => [name, resolveEntrypoint(target, name)]));
|
|
520
543
|
const blockHash = path => knownBlocks[path]
|
|
521
544
|
?? (canonicalEntrypoint(path) ? Object.entries(knownBlocks).find(([candidate]) => canonicalEntrypoint(candidate) === canonicalEntrypoint(path))?.[1] : undefined);
|
|
@@ -597,7 +620,17 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
597
620
|
const ownIgnore = read(target, '.pbai4devs/.gitignore');
|
|
598
621
|
if (ownIgnore) write(stage, '.pbai4devs/.gitignore', ownIgnore);
|
|
599
622
|
writeAdapterManifest(stage, { clients: selected });
|
|
600
|
-
|
|
623
|
+
// Supply the already-validated ownership records to the private stage so
|
|
624
|
+
// the synchronizer can distinguish an owned ignore block from a user's.
|
|
625
|
+
// These temporary records never replace or bypass checks on the target.
|
|
626
|
+
const stagingState = '.pbai4devs/state/agentdevkit.json', stagingDerived = '.pbai4devs/state/derived.json';
|
|
627
|
+
if (previous) write(stage, stagingState, JSON.stringify(previous));
|
|
628
|
+
if (Object.keys(derivedEntries).length || Object.keys(derivedOutputs).length || Object.keys(derivedBlocks).length || Object.keys(derivedFiles).length || Object.keys(derivedCatalogs).length || derivedConfig || removedBlocks.length || Object.keys(removedFiles).length) {
|
|
629
|
+
write(stage, stagingDerived, JSON.stringify({ schemaVersion: 1, entries: derivedEntries, outputs: derivedOutputs, blocks: derivedBlocks, files: derivedFiles, catalogs: derivedCatalogs, ...(derivedConfig ? { config: derivedConfig } : {}), removedBlocks, removedFiles }));
|
|
630
|
+
}
|
|
631
|
+
let synchronized;
|
|
632
|
+
try { synchronized = synchronize({ root: stage, force: true, localIgnores: false, generateEntrypoints: false, recordDerivedState: false, outputBlocked, output() {} }); }
|
|
633
|
+
finally { rmSync(join(stage, stagingState), { force: true }); rmSync(join(stage, stagingDerived), { force: true }); }
|
|
601
634
|
for (const canonical of active.entrypoints) {
|
|
602
635
|
const name = entryPaths.get(canonical);
|
|
603
636
|
if (preserveNative(name)) continue;
|
|
@@ -673,14 +706,14 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
673
706
|
// A standalone sync records its proven outputs separately. During a later
|
|
674
707
|
// check/sync, compare that overlay before declaring the installation state
|
|
675
708
|
// stale; update still consolidates it into the main state transactionally.
|
|
676
|
-
let stateContent =
|
|
709
|
+
let stateContent = reconcileStateBytes(installedState ? read(target, actualStatePath(target)) : null, installedState, manifest);
|
|
677
710
|
if (mode === 'sync' && installedState && !changingClients) {
|
|
678
711
|
const files = { ...installedState.files }, blocks = { ...installedState.blocks };
|
|
679
712
|
for (const [path, digest] of Object.entries(removedFiles)) {
|
|
680
713
|
if (files[path] === digest && !stat(safePath(target, path))) delete files[path];
|
|
681
714
|
}
|
|
682
715
|
for (const path of removedBlocks) delete blocks[path];
|
|
683
|
-
for (const [path, digest] of Object.entries({ ...derivedFiles, ...derivedOutputs, ...derivedEntries })) { files[path] = digest; delete blocks[path]; }
|
|
716
|
+
for (const [path, digest] of Object.entries({ ...derivedFiles, ...derivedOutputs, ...derivedEntries, ...derivedCatalogs, ...derivedConfigFiles })) { files[path] = digest; delete blocks[path]; }
|
|
684
717
|
for (const [path, digest] of Object.entries(derivedBlocks)) { blocks[path] = digest; delete files[path]; }
|
|
685
718
|
const same = (a, b) => {
|
|
686
719
|
const keys = Object.keys(a).sort();
|
|
@@ -692,23 +725,70 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
692
725
|
desired.set(statePath, stateContent);
|
|
693
726
|
const derivedPath = actualStatePath(target).replace(/agentdevkit\.json$/, 'derived.json');
|
|
694
727
|
const derivedBytes = !migration ? read(target, derivedPath) : null;
|
|
695
|
-
|
|
728
|
+
let nextDerivedBytes = derivedBytes;
|
|
729
|
+
if (nextDerivedBytes && Object.keys(removedFiles).length) {
|
|
696
730
|
const finalState = JSON.parse(stateContent);
|
|
697
731
|
const remaining = Object.fromEntries(Object.entries(removedFiles).filter(([path, digest]) =>
|
|
698
732
|
finalState.files[path] === digest && !stat(safePath(target, path))));
|
|
699
733
|
if (Object.keys(remaining).length !== Object.keys(removedFiles).length) {
|
|
700
734
|
observeNative(derivedPath, derivedBytes);
|
|
701
|
-
|
|
735
|
+
nextDerivedBytes = replaceJsonMember(nextDerivedBytes, 'removedFiles', Object.keys(remaining).length ? remaining : undefined);
|
|
702
736
|
}
|
|
703
737
|
}
|
|
738
|
+
if (nextDerivedBytes && Object.keys(derivedCatalogs).length) {
|
|
739
|
+
const finalState = JSON.parse(stateContent);
|
|
740
|
+
const remaining = Object.fromEntries(Object.entries(derivedCatalogs).filter(([path]) => {
|
|
741
|
+
const content = desired.get(path);
|
|
742
|
+
return !content || finalState.files[path] !== hash(content);
|
|
743
|
+
}));
|
|
744
|
+
if (Object.keys(remaining).length !== Object.keys(derivedCatalogs).length) {
|
|
745
|
+
observeNative(derivedPath, derivedBytes);
|
|
746
|
+
nextDerivedBytes = replaceJsonMember(nextDerivedBytes, 'catalogs', Object.keys(remaining).length ? remaining : undefined);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (nextDerivedBytes && Object.keys(derivedBlocks).length) {
|
|
750
|
+
const finalState = JSON.parse(stateContent);
|
|
751
|
+
const remaining = Object.fromEntries(Object.entries(derivedBlocks).filter(([path, digest]) => {
|
|
752
|
+
const content = desired.get(path);
|
|
753
|
+
// A deselected generated block is removed later in this transaction.
|
|
754
|
+
// Drop its overlay only if its current bytes are still proven; edited
|
|
755
|
+
// blocks remain conflicts and no metadata is committed on failure.
|
|
756
|
+
if (!content && !canonicalEntrypoint(path) && !finalState.blocks[path] && !finalState.files[path]) {
|
|
757
|
+
const current = read(target, path);
|
|
758
|
+
return Boolean(current && blockDigest(path, current) !== digest);
|
|
759
|
+
}
|
|
760
|
+
return !content || finalState.blocks[path] !== blockDigest(path, content);
|
|
761
|
+
}));
|
|
762
|
+
if (Object.keys(remaining).length !== Object.keys(derivedBlocks).length) {
|
|
763
|
+
observeNative(derivedPath, derivedBytes);
|
|
764
|
+
nextDerivedBytes = replaceJsonMember(nextDerivedBytes, 'blocks', Object.keys(remaining).length ? remaining : undefined);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (nextDerivedBytes && derivedConfig) {
|
|
768
|
+
const finalState = JSON.parse(stateContent), path = '.pbai4devs/espelhos.conf', content = desired.get(path);
|
|
769
|
+
if (content && finalState.files[path] === hash(content)) {
|
|
770
|
+
observeNative(derivedPath, derivedBytes);
|
|
771
|
+
nextDerivedBytes = replaceJsonMember(nextDerivedBytes, 'config', undefined);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (nextDerivedBytes && !nextDerivedBytes.equals(derivedBytes)) desired.set(derivedPath, nextDerivedBytes);
|
|
704
775
|
const changes = [], conflicts = [];
|
|
705
776
|
const aliasedEntrypoint = path => canonicalEntrypoint(path) && entryPaths.get(canonicalEntrypoint(path)) !== path;
|
|
777
|
+
const generatedInternal = new Set(['.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/agents.json', '.pbai4devs/rules.json', '.pbai4devs/espelhos.conf']);
|
|
706
778
|
for (const [path, content] of desired) {
|
|
707
779
|
const current = read(target, path);
|
|
708
780
|
if (nativeInputs.has(path) && (nativeInputs.get(path) === null ? current !== null : !current?.equals(nativeInputs.get(path)))) fail('E_CHANGED', `File changed while planning: ${path}`);
|
|
709
|
-
|
|
781
|
+
const wholeFileOwned = current && [previous?.files[path], derivedOutputs[path], derivedEntries[path], derivedFiles[path], derivedCatalogs[path], derivedConfigFiles[path]].some(digest => digest && digest === hash(current));
|
|
782
|
+
const existingBlock = current && (nativeManagedText(path) || canonicalEntrypoint(path) || path.endsWith('/.gitignore')) ? blockDigest(path, current) : null;
|
|
783
|
+
if (existingBlock && !wholeFileOwned && knownBlocks[path] !== existingBlock) conflicts.push(path);
|
|
784
|
+
if (current?.equals(content)) {
|
|
785
|
+
// Identical bytes do not prove ownership of a pre-existing catalogue.
|
|
786
|
+
// Refuse adoption: a later sync could otherwise overwrite user data.
|
|
787
|
+
if (!wholeFileOwned && (generatedInternal.has(path) || native(path) && !existingBlock && !mixedPaths.has(path) && !preserved.has(path))) conflicts.push(path);
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
710
790
|
const managed = path === derivedPath && derivedBytes && current?.equals(derivedBytes)
|
|
711
|
-
|| [previous?.files[path], derivedOutputs[path], derivedEntries[path], derivedFiles[path]].some(digest => digest && hash(current || '') === digest)
|
|
791
|
+
|| [previous?.files[path], derivedOutputs[path], derivedEntries[path], derivedFiles[path], derivedCatalogs[path], derivedConfigFiles[path]].some(digest => digest && hash(current || '') === digest)
|
|
712
792
|
|| knownBlocks[path] && blockDigest(path, current || Buffer.alloc(0)) === knownBlocks[path];
|
|
713
793
|
const appendBlock = mixedPaths.has(path) || canonicalEntrypoint(path) || path === '.gitignore' || path.endsWith('/.gitignore');
|
|
714
794
|
const conflict = current && path !== statePath && !managed && !appendBlock;
|
|
@@ -828,7 +908,9 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
828
908
|
const localIgnore = planLocalIgnore(target, { outputs: ownedOutputs, templateRoot: stage });
|
|
829
909
|
const localIgnoreCheck = checkLocalIgnore(localIgnore, { replacedBlocks: replacedIgnoreBlocks(target, changes, previous) });
|
|
830
910
|
conflicts.push(...(untrack?.conflicts || []), ...localIgnoreCheck.missing);
|
|
831
|
-
|
|
911
|
+
// A conflict means ownership or the managed bytes could not be proven.
|
|
912
|
+
// Backups do not authorize replacing user content, even with --force.
|
|
913
|
+
return { target, language, mode, changes, conflicts: [...new Set(conflicts)].sort(), canApply: !conflicts.length && !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)) };
|
|
832
914
|
} finally { rmSync(stage, { recursive: true, force: true }); }
|
|
833
915
|
}
|
|
834
916
|
|
|
@@ -1019,7 +1101,7 @@ export function inspect(target) {
|
|
|
1019
1101
|
if (content) issues.push({ code: 'modified', path });
|
|
1020
1102
|
continue;
|
|
1021
1103
|
}
|
|
1022
|
-
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 });
|
|
1104
|
+
if (!content || hash(content) !== digest && hash(content) !== derived.outputs[path] && hash(content) !== derived.files[path] && hash(content) !== derived.entries[path] && hash(content) !== derived.catalogs[path] && !(path === '.pbai4devs/espelhos.conf' && hash(content) === derived.config)) issues.push({ code: content ? 'modified' : 'missing', path });
|
|
1023
1105
|
}
|
|
1024
1106
|
for (const [path, digest] of Object.entries({ ...state.blocks, ...derived.blocks })) {
|
|
1025
1107
|
if (derived.removedBlocks.includes(path) && !Object.hasOwn(derived.blocks, path)) continue;
|