@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.
- package/.pbai4devs/COMPATIBILIDADE.md +11 -1
- package/.pbai4devs/scripts/diagnosticar-ambiente/tests/TEST.mjs +60 -1
- package/.pbai4devs/scripts/lib/adapters.mjs +5 -2
- package/.pbai4devs/scripts/lib/runtime.mjs +38 -2
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +5 -1
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +136 -9
- package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +30 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +22 -0
- package/.pbai4devs/scripts/testar-maturidade/README.md +12 -10
- package/.pbai4devs/scripts/testar-maturidade/SCRIPT.mjs +86 -44
- package/.pbai4devs/scripts/testar-maturidade/tests/TEST.mjs +157 -33
- package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +12 -1
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +22 -0
- package/.pbai4devs/templates/clients/ADAPTERS.json +3 -2
- package/CHANGELOG.md +15 -0
- package/docs/maintainers/maintenance.md +16 -5
- package/docs/specs/README.md +3 -0
- package/docs/specs/automatic-release-tags/SPEC.md +16 -0
- package/docs/specs/discovery-preservation/SPEC.md +26 -0
- package/package.json +1 -1
- package/src/install.mjs +134 -12
|
@@ -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
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 })) {
|