@ikon85/agent-workflow-kit 0.34.6 → 0.36.0
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/.agents/skills/ask-matt/SKILL.md +8 -0
- package/.agents/skills/audit-skills/SKILL.md +8 -0
- package/.agents/skills/board-to-waves/SKILL.md +8 -0
- package/.agents/skills/census-update/SKILL.md +8 -0
- package/.agents/skills/code-review/SKILL.md +8 -0
- package/.agents/skills/codebase-design/SKILL.md +8 -0
- package/.agents/skills/codex-adapter-sync/SKILL.md +8 -0
- package/.agents/skills/decision-gate/SKILL.md +8 -0
- package/.agents/skills/diagnose/SKILL.md +8 -0
- package/.agents/skills/domain-modeling/SKILL.md +8 -0
- package/.agents/skills/git-worktree-recover/SKILL.md +8 -0
- package/.agents/skills/grill-me/SKILL.md +8 -0
- package/.agents/skills/grill-with-docs/SKILL.md +8 -0
- package/.agents/skills/implement/SKILL.md +8 -0
- package/.agents/skills/improve-codebase-architecture/SKILL.md +8 -0
- package/.agents/skills/kit-release/SKILL.md +8 -0
- package/.agents/skills/kit-update/SKILL.md +32 -11
- package/.agents/skills/local-ci/SKILL.md +8 -0
- package/.agents/skills/memory-lifecycle/SKILL.md +8 -0
- package/.agents/skills/orchestrate-wave/SKILL.md +2 -2
- package/.agents/skills/project-release/SKILL.md +8 -0
- package/.agents/skills/prototype/SKILL.md +8 -0
- package/.agents/skills/research/SKILL.md +8 -0
- package/.agents/skills/resolving-merge-conflicts/SKILL.md +8 -0
- package/.agents/skills/retro/SKILL.md +19 -3
- package/.agents/skills/scale-check/SKILL.md +8 -0
- package/.agents/skills/security-audit/SKILL.md +8 -0
- package/.agents/skills/setup-workflow/SKILL.md +52 -1
- package/.agents/skills/setup-workflow/contribution-routing.md +52 -0
- package/.agents/skills/setup-workflow/orchestrate-wave-seed.md +1 -0
- package/.agents/skills/setup-workflow/spec-self-critique-seed.md +1 -0
- package/.agents/skills/spec-self-critique/SKILL.md +8 -0
- package/.agents/skills/tdd/SKILL.md +8 -0
- package/.agents/skills/to-issues/SKILL.md +8 -0
- package/.agents/skills/to-prd/SKILL.md +8 -0
- package/.agents/skills/to-waves/SKILL.md +8 -0
- package/.agents/skills/triage/SKILL.md +8 -0
- package/.agents/skills/verify-spike/SKILL.md +8 -0
- package/.agents/skills/wayfinder/SKILL.md +8 -0
- package/.agents/skills/wrapup/SKILL.md +8 -0
- package/.claude/hooks/kit-origin-edit-hint.py +18 -13
- package/.claude/skills/ask-matt/SKILL.md +8 -0
- package/.claude/skills/audit-skills/SKILL.md +8 -0
- package/.claude/skills/board-to-waves/SKILL.md +8 -0
- package/.claude/skills/census-update/SKILL.md +8 -0
- package/.claude/skills/code-review/SKILL.md +8 -0
- package/.claude/skills/codebase-design/SKILL.md +8 -0
- package/.claude/skills/codex-build/SKILL.md +8 -0
- package/.claude/skills/codex-review/SKILL.md +8 -0
- package/.claude/skills/decision-gate/SKILL.md +8 -0
- package/.claude/skills/diagnose/SKILL.md +8 -0
- package/.claude/skills/domain-modeling/SKILL.md +8 -0
- package/.claude/skills/git-guardrails-claude-code/SKILL.md +8 -0
- package/.claude/skills/git-worktree-recover/SKILL.md +8 -0
- package/.claude/skills/grill-me/SKILL.md +8 -0
- package/.claude/skills/grill-me-codex/SKILL.md +8 -0
- package/.claude/skills/grill-with-docs/SKILL.md +8 -0
- package/.claude/skills/grill-with-docs-codex/SKILL.md +8 -0
- package/.claude/skills/implement/SKILL.md +8 -0
- package/.claude/skills/improve-codebase-architecture/SKILL.md +8 -0
- package/.claude/skills/kit-release/SKILL.md +8 -0
- package/.claude/skills/kit-update/SKILL.md +32 -11
- package/.claude/skills/local-ci/SKILL.md +8 -0
- package/.claude/skills/memory-lifecycle/SKILL.md +8 -0
- package/.claude/skills/orchestrate-wave/SKILL.md +2 -2
- package/.claude/skills/project-release/SKILL.md +8 -0
- package/.claude/skills/prototype/SKILL.md +8 -0
- package/.claude/skills/research/SKILL.md +8 -0
- package/.claude/skills/resolving-merge-conflicts/SKILL.md +8 -0
- package/.claude/skills/retro/SKILL.md +19 -3
- package/.claude/skills/scale-check/SKILL.md +8 -0
- package/.claude/skills/security-audit/SKILL.md +8 -0
- package/.claude/skills/setup-pre-commit/SKILL.md +8 -0
- package/.claude/skills/setup-workflow/SKILL.md +52 -1
- package/.claude/skills/setup-workflow/contribution-routing.md +52 -0
- package/.claude/skills/setup-workflow/orchestrate-wave-seed.md +1 -0
- package/.claude/skills/setup-workflow/spec-self-critique-seed.md +1 -0
- package/.claude/skills/spec-self-critique/SKILL.md +8 -0
- package/.claude/skills/tdd/SKILL.md +8 -0
- package/.claude/skills/to-issues/SKILL.md +8 -0
- package/.claude/skills/to-prd/SKILL.md +8 -0
- package/.claude/skills/to-waves/SKILL.md +8 -0
- package/.claude/skills/triage/SKILL.md +8 -0
- package/.claude/skills/verify-spike/SKILL.md +8 -0
- package/.claude/skills/wayfinder/SKILL.md +8 -0
- package/.claude/skills/wrapup/SKILL.md +8 -0
- package/.claude/skills/write-a-skill/SKILL.md +8 -0
- package/README.md +172 -5
- package/agent-workflow-kit.package.json +149 -89
- package/docs/agents/skills/local-ci.md +1 -0
- package/docs/agents/skills/orchestrate-wave.md +1 -1
- package/docs/agents/skills/spec-self-critique.md +1 -1
- package/package.json +1 -1
- package/scripts/find-by-marker.py +2 -0
- package/scripts/project-skill-extension.mjs +37 -0
- package/scripts/readiness.mjs +2 -1
- package/scripts/test_marker_lib.py +8 -0
- package/scripts/test_retro_wrapup_contract.py +6 -4
- package/scripts/test_skill_publish_audit.py +9 -1
- package/src/cli.mjs +86 -12
- package/src/commands/init.mjs +24 -0
- package/src/commands/own.mjs +7 -3
- package/src/commands/update.mjs +87 -21
- package/src/lib/bundle.mjs +5 -0
- package/src/lib/contributionBridge.mjs +176 -0
- package/src/lib/contributionRouting.mjs +152 -0
- package/src/lib/manifest.mjs +17 -2
- package/src/lib/ownershipClassifier.mjs +65 -0
- package/src/lib/projectSkillExtension.mjs +60 -0
- package/src/lib/skillRegistry.mjs +167 -0
- package/src/lib/updateCandidate.mjs +105 -4
- package/src/lib/updateDecisions.mjs +5 -0
- package/src/lib/updateReconcile.mjs +102 -9
- package/src/lib/verifyUpdateCandidate.mjs +28 -1
- package/src/lib/verifyUpdateCandidateProtocol.mjs +2 -0
- package/src/lib/verifyUpdateCandidateTransaction.mjs +19 -3
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
CONSUMER_MANIFEST_NAME, CONSUMER_ORIGIN, READINESS_MANIFEST_PATH,
|
|
16
16
|
filesForInstallRole, indexByPath, readManifest, writeManifest,
|
|
17
17
|
} from './manifest.mjs';
|
|
18
|
+
import {
|
|
19
|
+
PROJECT_SKILL_REGISTRY_PATH, emptyProjectSkillRegistry, migrateLegacySkillRegistry,
|
|
20
|
+
readComposedSkillRegistry,
|
|
21
|
+
} from './skillRegistry.mjs';
|
|
18
22
|
import { checkSkill, evaluateCapability, inspectProdSections } from '../../scripts/readiness.mjs';
|
|
19
23
|
|
|
20
24
|
const exists = (path) => access(path).then(() => true, () => false);
|
|
@@ -56,6 +60,7 @@ export async function materializeUpdateCandidate({
|
|
|
56
60
|
export function candidateInputPaths({ pkg, manifests }) {
|
|
57
61
|
const candidates = [
|
|
58
62
|
CONSUMER_MANIFEST_NAME,
|
|
63
|
+
PROJECT_SKILL_REGISTRY_PATH,
|
|
59
64
|
...INTEGRATION_INPUTS,
|
|
60
65
|
...filesForInstallRole(pkg).map(({ path }) => path),
|
|
61
66
|
];
|
|
@@ -349,12 +354,18 @@ async function assertConsumerStillMatchesPreview(consumerRoot, preview) {
|
|
|
349
354
|
}
|
|
350
355
|
|
|
351
356
|
/** Seed only newly declared, decision-free project-layer stubs in a staged candidate. */
|
|
352
|
-
export async function adoptReadinessCandidate({
|
|
357
|
+
export async function adoptReadinessCandidate({
|
|
358
|
+
candidateRoot, consumerRoot, kitRoot, priorManifest, nextManifest,
|
|
359
|
+
}) {
|
|
353
360
|
const priorPaths = readinessStubPaths(priorManifest);
|
|
354
361
|
const manifestPath = join(candidateRoot, CONSUMER_MANIFEST_NAME);
|
|
355
|
-
|
|
362
|
+
let manifest = await readManifest(manifestPath);
|
|
363
|
+
const registry = await adoptProjectSkillRegistry({
|
|
364
|
+
candidateRoot, consumerRoot, kitRoot, manifest, priorManifest, nextManifest,
|
|
365
|
+
});
|
|
366
|
+
manifest = await readManifest(manifestPath);
|
|
356
367
|
const candidateInstalled = indexByPath(manifest, 'installed');
|
|
357
|
-
const generated = [];
|
|
368
|
+
const generated = [...registry.generated];
|
|
358
369
|
for (const path of readinessStubPaths(nextManifest)) {
|
|
359
370
|
if (priorPaths.has(path)) continue;
|
|
360
371
|
if (await exists(join(candidateRoot, path))) {
|
|
@@ -377,9 +388,11 @@ export async function adoptReadinessCandidate({ candidateRoot, consumerRoot, pri
|
|
|
377
388
|
}
|
|
378
389
|
await writeManifest(manifestPath, { ...manifest, installed });
|
|
379
390
|
}
|
|
380
|
-
const
|
|
391
|
+
const prod = await migrateProdSections({
|
|
381
392
|
candidateRoot, consumerRoot, nextManifest,
|
|
382
393
|
});
|
|
394
|
+
const migrations = [...registry.migrations, ...prod.migrations];
|
|
395
|
+
const migrationConflicts = [...registry.migrationConflicts, ...prod.migrationConflicts];
|
|
383
396
|
const before = await readinessSnapshot(consumerRoot, priorManifest);
|
|
384
397
|
const after = await readinessSnapshot(candidateRoot, nextManifest);
|
|
385
398
|
const incompatible = Object.entries(after.skills)
|
|
@@ -397,6 +410,94 @@ export async function adoptReadinessCandidate({ candidateRoot, consumerRoot, pri
|
|
|
397
410
|
};
|
|
398
411
|
}
|
|
399
412
|
|
|
413
|
+
async function adoptProjectSkillRegistry({
|
|
414
|
+
candidateRoot, consumerRoot, kitRoot, manifest, priorManifest, nextManifest,
|
|
415
|
+
}) {
|
|
416
|
+
if (!nextManifest) {
|
|
417
|
+
return { generated: [], migrations: [], migrationConflicts: [] };
|
|
418
|
+
}
|
|
419
|
+
const tracked = manifest.installed.find(({ path }) => path === READINESS_MANIFEST_PATH);
|
|
420
|
+
if (tracked?.origin !== CONSUMER_ORIGIN) {
|
|
421
|
+
const registryPath = join(candidateRoot, PROJECT_SKILL_REGISTRY_PATH);
|
|
422
|
+
if (!await exists(registryPath)) {
|
|
423
|
+
await writeAtomic(
|
|
424
|
+
registryPath,
|
|
425
|
+
`${JSON.stringify(emptyProjectSkillRegistry(nextManifest), null, 2)}\n`,
|
|
426
|
+
);
|
|
427
|
+
const installedSha256 = await sha256File(registryPath);
|
|
428
|
+
await writeManifest(join(candidateRoot, CONSUMER_MANIFEST_NAME), {
|
|
429
|
+
...manifest,
|
|
430
|
+
installed: [...manifest.installed, {
|
|
431
|
+
path: PROJECT_SKILL_REGISTRY_PATH,
|
|
432
|
+
kind: 'doc',
|
|
433
|
+
installedSha256,
|
|
434
|
+
origin: CONSUMER_ORIGIN,
|
|
435
|
+
installRole: 'consumer',
|
|
436
|
+
}],
|
|
437
|
+
});
|
|
438
|
+
await readComposedSkillRegistry(candidateRoot, nextManifest);
|
|
439
|
+
return {
|
|
440
|
+
generated: [PROJECT_SKILL_REGISTRY_PATH],
|
|
441
|
+
migrations: [],
|
|
442
|
+
migrationConflicts: [],
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
await readComposedSkillRegistry(candidateRoot, nextManifest);
|
|
446
|
+
return { generated: [], migrations: [], migrationConflicts: [] };
|
|
447
|
+
}
|
|
448
|
+
const registryPath = join(candidateRoot, PROJECT_SKILL_REGISTRY_PATH);
|
|
449
|
+
if (await exists(registryPath)) {
|
|
450
|
+
return {
|
|
451
|
+
generated: [],
|
|
452
|
+
migrations: [],
|
|
453
|
+
migrationConflicts: [
|
|
454
|
+
`${PROJECT_SKILL_REGISTRY_PATH}: legacy mixed Core already has a Project registry`,
|
|
455
|
+
],
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
let registry;
|
|
459
|
+
try {
|
|
460
|
+
registry = migrateLegacySkillRegistry({ legacyCore: priorManifest, nextCore: nextManifest });
|
|
461
|
+
} catch (error) {
|
|
462
|
+
return {
|
|
463
|
+
generated: [], migrations: [],
|
|
464
|
+
migrationConflicts: [`${READINESS_MANIFEST_PATH}: ${error.message}`],
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
const corePath = join(candidateRoot, READINESS_MANIFEST_PATH);
|
|
468
|
+
await writeAtomic(corePath, await readFile(join(kitRoot, READINESS_MANIFEST_PATH)));
|
|
469
|
+
await writeAtomic(registryPath, `${JSON.stringify(registry, null, 2)}\n`);
|
|
470
|
+
const coreHash = await sha256File(corePath);
|
|
471
|
+
const registryHash = await sha256File(registryPath);
|
|
472
|
+
const installed = manifest.installed.map((entry) => {
|
|
473
|
+
if (entry.path !== READINESS_MANIFEST_PATH) return entry;
|
|
474
|
+
const { ownershipState: _legacyLifecycle, ...coreEntry } = entry;
|
|
475
|
+
return { ...coreEntry, installedSha256: coreHash, origin: 'kit' };
|
|
476
|
+
});
|
|
477
|
+
if (!installed.some(({ path }) => path === PROJECT_SKILL_REGISTRY_PATH)) {
|
|
478
|
+
installed.push({
|
|
479
|
+
path: PROJECT_SKILL_REGISTRY_PATH,
|
|
480
|
+
kind: 'doc',
|
|
481
|
+
installedSha256: registryHash,
|
|
482
|
+
origin: CONSUMER_ORIGIN,
|
|
483
|
+
installRole: 'consumer',
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
await writeManifest(join(candidateRoot, CONSUMER_MANIFEST_NAME), {
|
|
487
|
+
...manifest, installed,
|
|
488
|
+
});
|
|
489
|
+
return {
|
|
490
|
+
generated: [PROJECT_SKILL_REGISTRY_PATH],
|
|
491
|
+
migrations: [{
|
|
492
|
+
path: READINESS_MANIFEST_PATH,
|
|
493
|
+
beforeSha256: await sha256File(join(consumerRoot, READINESS_MANIFEST_PATH)),
|
|
494
|
+
afterSha256: coreHash,
|
|
495
|
+
ownership: 'kit-core',
|
|
496
|
+
}],
|
|
497
|
+
migrationConflicts: [],
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
400
501
|
async function migrateProdSections({ candidateRoot, consumerRoot, nextManifest }) {
|
|
401
502
|
const paths = [...new Set(Object.values(nextManifest?.readiness?.capabilities ?? {})
|
|
402
503
|
.flatMap(({ evidence }) => evidence?.type === 'prod-section' ? evidence.paths ?? [] : []))];
|
|
@@ -4,6 +4,11 @@ import { sha256File } from './hash.mjs';
|
|
|
4
4
|
import { lineDiff, writeAtomic } from './atomicWrite.mjs';
|
|
5
5
|
import { hookReferenced } from './settings.mjs';
|
|
6
6
|
import { validateConsumerFile } from './consumerPath.mjs';
|
|
7
|
+
import { validateContributionBridge } from './contributionBridge.mjs';
|
|
8
|
+
import { inspectProjectSkillExtension } from './projectSkillExtension.mjs';
|
|
9
|
+
import {
|
|
10
|
+
OwnershipState, classifyOwnershipEvidence,
|
|
11
|
+
} from './ownershipClassifier.mjs';
|
|
7
12
|
import {
|
|
8
13
|
CONSUMER_MANIFEST_NAME, PACKAGE_MANIFEST_NAME, emptyConsumerManifest,
|
|
9
14
|
CONSUMER_INSTALL_ROLE, CONSUMER_ORIGIN, KIT_ORIGIN, filesForInstallRole,
|
|
@@ -32,6 +37,40 @@ export async function reconcile({ kitRoot, consumerRoot, decide = () => false, d
|
|
|
32
37
|
const dest = join(consumerRoot, file.path);
|
|
33
38
|
const prior = installedIdx.get(file.path);
|
|
34
39
|
if (prior?.origin === CONSUMER_ORIGIN) {
|
|
40
|
+
const bridge = prior.ownershipState === OwnershipState.CONTRIBUTION_BRIDGE
|
|
41
|
+
? contributionBridgeEvidence(prior) : null;
|
|
42
|
+
const classification = classifyOwnershipEvidence({
|
|
43
|
+
path: file.path,
|
|
44
|
+
packageEntry: file,
|
|
45
|
+
installedEntry: prior,
|
|
46
|
+
destinationPresent: await exists(dest),
|
|
47
|
+
projectExtension: prior.ownershipState === OwnershipState.PROJECT_EXTENSION
|
|
48
|
+
? await projectExtensionEvidence(consumerRoot, file.path) : null,
|
|
49
|
+
contributionBridge: bridge,
|
|
50
|
+
});
|
|
51
|
+
result.ownershipStates.push(classification);
|
|
52
|
+
if (classification.state === OwnershipState.AMBIGUOUS_COLLISION) {
|
|
53
|
+
result.conflicts.push({
|
|
54
|
+
path: file.path,
|
|
55
|
+
kind: 'ownership-lifecycle',
|
|
56
|
+
diff: 'Consumer lifecycle metadata does not match its declared path/schema.',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (classification.state === OwnershipState.CONTRIBUTION_BRIDGE) {
|
|
60
|
+
const present = await exists(dest);
|
|
61
|
+
if (present) {
|
|
62
|
+
await validateConsumerFile(consumerRoot, file.path);
|
|
63
|
+
const current = await sha256File(dest);
|
|
64
|
+
if (current === file.sha256 && current === bridge.localSha256) {
|
|
65
|
+
if (!dryRun) {
|
|
66
|
+
await writeAtomic(dest, await readFile(join(kitRoot, file.path)), file.mode);
|
|
67
|
+
}
|
|
68
|
+
nextInstalled.push(entry(file, file.sha256));
|
|
69
|
+
result.bridgeRetired.push(file.path);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
35
74
|
nextInstalled.push(withInstallRole(prior));
|
|
36
75
|
result.consumerOwned.push(file.path);
|
|
37
76
|
continue;
|
|
@@ -39,6 +78,13 @@ export async function reconcile({ kitRoot, consumerRoot, decide = () => false, d
|
|
|
39
78
|
const present = await exists(dest);
|
|
40
79
|
if (!prior && present) {
|
|
41
80
|
await validateConsumerFile(consumerRoot, file.path);
|
|
81
|
+
const classification = classifyOwnershipEvidence({
|
|
82
|
+
path: file.path,
|
|
83
|
+
packageEntry: file,
|
|
84
|
+
destinationPresent: true,
|
|
85
|
+
projectExtension: await projectExtensionEvidence(consumerRoot, file.path),
|
|
86
|
+
});
|
|
87
|
+
result.ownershipStates.push(classification);
|
|
42
88
|
const decision = await decide('collision', file.path);
|
|
43
89
|
if (decision === false || decision === null || decision === undefined) {
|
|
44
90
|
if (dryRun) {
|
|
@@ -47,15 +93,38 @@ export async function reconcile({ kitRoot, consumerRoot, decide = () => false, d
|
|
|
47
93
|
}
|
|
48
94
|
throw new Error(`collision decision required for ${file.path}`);
|
|
49
95
|
}
|
|
50
|
-
|
|
51
|
-
|
|
96
|
+
const allowed = [
|
|
97
|
+
'keep-as-owned', 'project-extension', 'contribution-bridge', 'explicit-fork', 'replace',
|
|
98
|
+
];
|
|
99
|
+
if (!allowed.includes(decision)
|
|
100
|
+
|| (decision === 'project-extension'
|
|
101
|
+
&& classification.state !== OwnershipState.PROJECT_EXTENSION)) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`collision decision for ${file.path} must select a valid explicit ownership route`,
|
|
104
|
+
);
|
|
52
105
|
}
|
|
53
106
|
const destinationSha256 = await sha256File(dest);
|
|
54
|
-
|
|
107
|
+
const resolvedState = decision === 'replace' ? OwnershipState.CLEAN_CORE
|
|
108
|
+
: (decision === 'keep-as-owned' ? OwnershipState.EXPLICIT_FORK : decision);
|
|
109
|
+
const resolution = {
|
|
55
110
|
path: file.path, outcome: decision, destinationSha256,
|
|
56
|
-
}
|
|
57
|
-
if (
|
|
58
|
-
|
|
111
|
+
};
|
|
112
|
+
if (!['replace', 'keep-as-owned'].includes(decision)) {
|
|
113
|
+
resolution.ownershipState = resolvedState;
|
|
114
|
+
}
|
|
115
|
+
result.collisionResolutions.push(resolution);
|
|
116
|
+
if (decision !== 'replace') {
|
|
117
|
+
const lifecycle = resolvedState === OwnershipState.CONTRIBUTION_BRIDGE ? {
|
|
118
|
+
contributionBridge: {
|
|
119
|
+
schemaVersion: 1,
|
|
120
|
+
baseKitVersion: pkg.kitVersion,
|
|
121
|
+
baseSha256: file.sha256,
|
|
122
|
+
localSha256: destinationSha256,
|
|
123
|
+
},
|
|
124
|
+
} : {};
|
|
125
|
+
nextInstalled.push(entry(
|
|
126
|
+
file, destinationSha256, CONSUMER_ORIGIN, resolvedState, lifecycle,
|
|
127
|
+
));
|
|
59
128
|
result.consumerOwned.push(file.path);
|
|
60
129
|
} else {
|
|
61
130
|
if (!dryRun) await writeAtomic(dest, await readFile(join(kitRoot, file.path)), file.mode);
|
|
@@ -115,6 +184,7 @@ export async function reconcile({ kitRoot, consumerRoot, decide = () => false, d
|
|
|
115
184
|
}
|
|
116
185
|
|
|
117
186
|
result.manifestChanged = consumer.installRole !== CONSUMER_INSTALL_ROLE ||
|
|
187
|
+
result.bridgeRetired.length > 0 ||
|
|
118
188
|
nextInstalled.some((next) => installedIdx.get(next.path)?.installRole !== next.installRole);
|
|
119
189
|
|
|
120
190
|
if (!dryRun) {
|
|
@@ -128,17 +198,40 @@ export async function reconcile({ kitRoot, consumerRoot, decide = () => false, d
|
|
|
128
198
|
function emptyResult() {
|
|
129
199
|
return {
|
|
130
200
|
unchanged: [], updated: [], conflicts: [], collisions: [], collisionResolutions: [], userModified: [],
|
|
201
|
+
ownershipStates: [],
|
|
202
|
+
bridgeRetired: [],
|
|
131
203
|
added: [], deleted: [], keptDeleted: [], consumerOwned: [], manifestChanged: false,
|
|
132
204
|
};
|
|
133
205
|
}
|
|
134
206
|
|
|
135
|
-
function
|
|
136
|
-
|
|
207
|
+
function contributionBridgeEvidence(installed) {
|
|
208
|
+
try {
|
|
209
|
+
return validateContributionBridge(installed);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return { invalid: error.message };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function entry(file, installedSha256, origin = KIT_ORIGIN, ownershipState, lifecycle = {}) {
|
|
216
|
+
const result = {
|
|
137
217
|
path: file.path, kind: file.kind, ownerSkill: file.ownerSkill, surface: file.surface,
|
|
138
|
-
installedSha256, origin, installRole: CONSUMER_INSTALL_ROLE,
|
|
218
|
+
installedSha256, origin, installRole: CONSUMER_INSTALL_ROLE, ...lifecycle,
|
|
139
219
|
};
|
|
220
|
+
if (ownershipState) result.ownershipState = ownershipState;
|
|
221
|
+
return result;
|
|
140
222
|
}
|
|
141
223
|
|
|
142
224
|
function withInstallRole(installed, installRole = CONSUMER_INSTALL_ROLE) {
|
|
143
225
|
return { ...installed, installRole };
|
|
144
226
|
}
|
|
227
|
+
|
|
228
|
+
async function projectExtensionEvidence(consumerRoot, path) {
|
|
229
|
+
const match = /^docs\/agents\/skills\/([a-z0-9-]+)\.md$/.exec(path);
|
|
230
|
+
if (!match) return null;
|
|
231
|
+
try {
|
|
232
|
+
const result = await inspectProjectSkillExtension({ root: consumerRoot, skill: match[1] });
|
|
233
|
+
return result.state === 'active' ? result : null;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
return { invalid: error.message };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -2,6 +2,7 @@ import { lstat, readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { isDeepStrictEqual } from 'node:util';
|
|
4
4
|
import { sha256File } from './hash.mjs';
|
|
5
|
+
import { validateContributionBridge } from './contributionBridge.mjs';
|
|
5
6
|
import {
|
|
6
7
|
CONSUMER_INSTALL_ROLE, CONSUMER_MANIFEST_NAME, CONSUMER_ORIGIN, KIT_ORIGIN,
|
|
7
8
|
emptyConsumerManifest, filesForInstallRole,
|
|
@@ -189,6 +190,27 @@ function validateInstalledEntry(tracked, desired, origin) {
|
|
|
189
190
|
if (tracked.origin === KIT_ORIGIN && tracked.installedSha256 !== desired.sha256) {
|
|
190
191
|
throw new Error(`candidate invariant ownership: Kit hash identity mismatch ${tracked.path}`);
|
|
191
192
|
}
|
|
193
|
+
if (tracked.origin === KIT_ORIGIN && tracked.ownershipState !== undefined) {
|
|
194
|
+
throw new Error(`candidate invariant ownership: Kit path has Consumer lifecycle ${tracked.path}`);
|
|
195
|
+
}
|
|
196
|
+
if (tracked.origin === KIT_ORIGIN && tracked.contributionBridge !== undefined) {
|
|
197
|
+
throw new Error(`candidate invariant ownership: Kit path has contribution bridge ${tracked.path}`);
|
|
198
|
+
}
|
|
199
|
+
if (tracked.origin === CONSUMER_ORIGIN && ![
|
|
200
|
+
undefined, 'project-extension', 'contribution-bridge', 'explicit-fork',
|
|
201
|
+
].includes(tracked.ownershipState)) {
|
|
202
|
+
throw new Error(`candidate invariant ownership: invalid lifecycle ${tracked.path}`);
|
|
203
|
+
}
|
|
204
|
+
if (tracked.origin === CONSUMER_ORIGIN
|
|
205
|
+
&& tracked.ownershipState === 'contribution-bridge') {
|
|
206
|
+
try {
|
|
207
|
+
validateContributionBridge(tracked);
|
|
208
|
+
} catch {
|
|
209
|
+
throw new Error(`candidate invariant ownership: invalid contribution bridge ${tracked.path}`);
|
|
210
|
+
}
|
|
211
|
+
} else if (tracked.contributionBridge !== undefined) {
|
|
212
|
+
throw new Error(`candidate invariant ownership: unexpected contribution bridge ${tracked.path}`);
|
|
213
|
+
}
|
|
192
214
|
}
|
|
193
215
|
|
|
194
216
|
function verifyLedgerMetadata(ledger, context) {
|
|
@@ -207,8 +229,13 @@ function verifyLedgerMetadata(ledger, context) {
|
|
|
207
229
|
}
|
|
208
230
|
|
|
209
231
|
function expectedOrigin(path, priorInstalled, preview) {
|
|
232
|
+
if ((preview.bridgeRetired ?? []).includes(path)) return KIT_ORIGIN;
|
|
233
|
+
const transferredToCore = (preview.migrations ?? []).some(
|
|
234
|
+
(migration) => migration.path === path && migration.ownership === 'kit-core',
|
|
235
|
+
);
|
|
236
|
+
if (transferredToCore) return KIT_ORIGIN;
|
|
210
237
|
const keptOwned = (preview.collisionResolutions ?? []).some(
|
|
211
|
-
(resolution) => resolution.path === path && resolution.outcome
|
|
238
|
+
(resolution) => resolution.path === path && resolution.outcome !== 'replace',
|
|
212
239
|
);
|
|
213
240
|
return keptOwned || (priorInstalled.get(path)?.origin ?? KIT_ORIGIN) === CONSUMER_ORIGIN
|
|
214
241
|
? CONSUMER_ORIGIN
|
|
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { CONSUMER_INSTALL_ROLE, CONSUMER_ORIGIN, filesForInstallRole } from './manifest.mjs';
|
|
4
4
|
import { validateCandidateManifestPath } from './updateCandidate.mjs';
|
|
5
|
+
import { readComposedSkillRegistry } from './skillRegistry.mjs';
|
|
5
6
|
|
|
6
7
|
const READINESS_MANIFEST_PATH = '.claude/skills/skill-manifest.json';
|
|
7
8
|
const SURFACE_ROOT = {
|
|
@@ -18,6 +19,7 @@ export async function verifyCandidateProtocol(candidateRoot, pkg, installed) {
|
|
|
18
19
|
throw new Error('candidate invariant schema: readiness manifest is invalid JSON');
|
|
19
20
|
}
|
|
20
21
|
validateReadinessManifest(manifest);
|
|
22
|
+
await readComposedSkillRegistry(candidateRoot, manifest);
|
|
21
23
|
validateSkillArtifactReferences(manifest, pkg);
|
|
22
24
|
for (const [name, declaration] of Object.entries(manifest.skills)) {
|
|
23
25
|
if (!declaration.publish
|
|
@@ -60,6 +60,16 @@ export function verifyTransactionPreview(preview, installable, installed) {
|
|
|
60
60
|
}
|
|
61
61
|
actionSets.set(key, local);
|
|
62
62
|
}
|
|
63
|
+
if (!Array.isArray(preview.bridgeRetired ?? [])) {
|
|
64
|
+
throw new Error('candidate invariant transaction: bridgeRetired must be an array');
|
|
65
|
+
}
|
|
66
|
+
const retired = new Set();
|
|
67
|
+
for (const path of preview.bridgeRetired ?? []) {
|
|
68
|
+
claimTransactionPath(path, 'bridgeRetired', retired, owners);
|
|
69
|
+
if (!installablePaths.has(path) || installed.get(path)?.origin !== KIT_ORIGIN) {
|
|
70
|
+
throw new Error(`candidate invariant transaction: invalid bridge retirement ${path}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
63
73
|
if (!Array.isArray(preview.migrations ?? [])) {
|
|
64
74
|
throw new Error('candidate invariant transaction: migrations must be an array');
|
|
65
75
|
}
|
|
@@ -109,15 +119,21 @@ function verifyCollisionRecords(preview, installablePaths, installed, actionSets
|
|
|
109
119
|
for (const resolution of preview.collisionResolutions ?? []) {
|
|
110
120
|
const path = resolution?.path;
|
|
111
121
|
claimTransactionPath(path, 'collision resolution', resolutions);
|
|
122
|
+
const nonReplace = resolution?.outcome !== 'replace';
|
|
123
|
+
const expectedOwnershipState = resolution?.outcome === 'keep-as-owned'
|
|
124
|
+
? 'explicit-fork' : resolution?.ownershipState;
|
|
112
125
|
if (!installablePaths.has(path)
|
|
113
|
-
|| ![
|
|
126
|
+
|| ![
|
|
127
|
+
'keep-as-owned', 'project-extension', 'contribution-bridge', 'explicit-fork', 'replace',
|
|
128
|
+
].includes(resolution?.outcome)
|
|
114
129
|
|| !HASH.test(resolution?.destinationSha256 ?? '')) {
|
|
115
130
|
throw new Error(`candidate invariant transaction: invalid collision resolution ${path}`);
|
|
116
131
|
}
|
|
117
|
-
const expectedOrigin =
|
|
132
|
+
const expectedOrigin = nonReplace ? CONSUMER_ORIGIN : KIT_ORIGIN;
|
|
118
133
|
if (installed.get(path)?.origin !== expectedOrigin
|
|
134
|
+
|| (nonReplace && installed.get(path)?.ownershipState !== expectedOwnershipState)
|
|
119
135
|
|| (resolution.outcome === 'replace' && !actionSets.get('added').has(path))
|
|
120
|
-
|| (
|
|
136
|
+
|| (nonReplace && owners.has(path))) {
|
|
121
137
|
throw new Error(`candidate invariant transaction: incoherent collision resolution ${path}`);
|
|
122
138
|
}
|
|
123
139
|
}
|