@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.
Files changed (116) hide show
  1. package/.agents/skills/ask-matt/SKILL.md +8 -0
  2. package/.agents/skills/audit-skills/SKILL.md +8 -0
  3. package/.agents/skills/board-to-waves/SKILL.md +8 -0
  4. package/.agents/skills/census-update/SKILL.md +8 -0
  5. package/.agents/skills/code-review/SKILL.md +8 -0
  6. package/.agents/skills/codebase-design/SKILL.md +8 -0
  7. package/.agents/skills/codex-adapter-sync/SKILL.md +8 -0
  8. package/.agents/skills/decision-gate/SKILL.md +8 -0
  9. package/.agents/skills/diagnose/SKILL.md +8 -0
  10. package/.agents/skills/domain-modeling/SKILL.md +8 -0
  11. package/.agents/skills/git-worktree-recover/SKILL.md +8 -0
  12. package/.agents/skills/grill-me/SKILL.md +8 -0
  13. package/.agents/skills/grill-with-docs/SKILL.md +8 -0
  14. package/.agents/skills/implement/SKILL.md +8 -0
  15. package/.agents/skills/improve-codebase-architecture/SKILL.md +8 -0
  16. package/.agents/skills/kit-release/SKILL.md +8 -0
  17. package/.agents/skills/kit-update/SKILL.md +32 -11
  18. package/.agents/skills/local-ci/SKILL.md +8 -0
  19. package/.agents/skills/memory-lifecycle/SKILL.md +8 -0
  20. package/.agents/skills/orchestrate-wave/SKILL.md +2 -2
  21. package/.agents/skills/project-release/SKILL.md +8 -0
  22. package/.agents/skills/prototype/SKILL.md +8 -0
  23. package/.agents/skills/research/SKILL.md +8 -0
  24. package/.agents/skills/resolving-merge-conflicts/SKILL.md +8 -0
  25. package/.agents/skills/retro/SKILL.md +19 -3
  26. package/.agents/skills/scale-check/SKILL.md +8 -0
  27. package/.agents/skills/security-audit/SKILL.md +8 -0
  28. package/.agents/skills/setup-workflow/SKILL.md +52 -1
  29. package/.agents/skills/setup-workflow/contribution-routing.md +52 -0
  30. package/.agents/skills/setup-workflow/orchestrate-wave-seed.md +1 -0
  31. package/.agents/skills/setup-workflow/spec-self-critique-seed.md +1 -0
  32. package/.agents/skills/spec-self-critique/SKILL.md +8 -0
  33. package/.agents/skills/tdd/SKILL.md +8 -0
  34. package/.agents/skills/to-issues/SKILL.md +8 -0
  35. package/.agents/skills/to-prd/SKILL.md +8 -0
  36. package/.agents/skills/to-waves/SKILL.md +8 -0
  37. package/.agents/skills/triage/SKILL.md +8 -0
  38. package/.agents/skills/verify-spike/SKILL.md +8 -0
  39. package/.agents/skills/wayfinder/SKILL.md +8 -0
  40. package/.agents/skills/wrapup/SKILL.md +8 -0
  41. package/.claude/hooks/kit-origin-edit-hint.py +18 -13
  42. package/.claude/skills/ask-matt/SKILL.md +8 -0
  43. package/.claude/skills/audit-skills/SKILL.md +8 -0
  44. package/.claude/skills/board-to-waves/SKILL.md +8 -0
  45. package/.claude/skills/census-update/SKILL.md +8 -0
  46. package/.claude/skills/code-review/SKILL.md +8 -0
  47. package/.claude/skills/codebase-design/SKILL.md +8 -0
  48. package/.claude/skills/codex-build/SKILL.md +8 -0
  49. package/.claude/skills/codex-review/SKILL.md +8 -0
  50. package/.claude/skills/decision-gate/SKILL.md +8 -0
  51. package/.claude/skills/diagnose/SKILL.md +8 -0
  52. package/.claude/skills/domain-modeling/SKILL.md +8 -0
  53. package/.claude/skills/git-guardrails-claude-code/SKILL.md +8 -0
  54. package/.claude/skills/git-worktree-recover/SKILL.md +8 -0
  55. package/.claude/skills/grill-me/SKILL.md +8 -0
  56. package/.claude/skills/grill-me-codex/SKILL.md +8 -0
  57. package/.claude/skills/grill-with-docs/SKILL.md +8 -0
  58. package/.claude/skills/grill-with-docs-codex/SKILL.md +8 -0
  59. package/.claude/skills/implement/SKILL.md +8 -0
  60. package/.claude/skills/improve-codebase-architecture/SKILL.md +8 -0
  61. package/.claude/skills/kit-release/SKILL.md +8 -0
  62. package/.claude/skills/kit-update/SKILL.md +32 -11
  63. package/.claude/skills/local-ci/SKILL.md +8 -0
  64. package/.claude/skills/memory-lifecycle/SKILL.md +8 -0
  65. package/.claude/skills/orchestrate-wave/SKILL.md +2 -2
  66. package/.claude/skills/project-release/SKILL.md +8 -0
  67. package/.claude/skills/prototype/SKILL.md +8 -0
  68. package/.claude/skills/research/SKILL.md +8 -0
  69. package/.claude/skills/resolving-merge-conflicts/SKILL.md +8 -0
  70. package/.claude/skills/retro/SKILL.md +19 -3
  71. package/.claude/skills/scale-check/SKILL.md +8 -0
  72. package/.claude/skills/security-audit/SKILL.md +8 -0
  73. package/.claude/skills/setup-pre-commit/SKILL.md +8 -0
  74. package/.claude/skills/setup-workflow/SKILL.md +52 -1
  75. package/.claude/skills/setup-workflow/contribution-routing.md +52 -0
  76. package/.claude/skills/setup-workflow/orchestrate-wave-seed.md +1 -0
  77. package/.claude/skills/setup-workflow/spec-self-critique-seed.md +1 -0
  78. package/.claude/skills/spec-self-critique/SKILL.md +8 -0
  79. package/.claude/skills/tdd/SKILL.md +8 -0
  80. package/.claude/skills/to-issues/SKILL.md +8 -0
  81. package/.claude/skills/to-prd/SKILL.md +8 -0
  82. package/.claude/skills/to-waves/SKILL.md +8 -0
  83. package/.claude/skills/triage/SKILL.md +8 -0
  84. package/.claude/skills/verify-spike/SKILL.md +8 -0
  85. package/.claude/skills/wayfinder/SKILL.md +8 -0
  86. package/.claude/skills/wrapup/SKILL.md +8 -0
  87. package/.claude/skills/write-a-skill/SKILL.md +8 -0
  88. package/README.md +172 -5
  89. package/agent-workflow-kit.package.json +149 -89
  90. package/docs/agents/skills/local-ci.md +1 -0
  91. package/docs/agents/skills/orchestrate-wave.md +1 -1
  92. package/docs/agents/skills/spec-self-critique.md +1 -1
  93. package/package.json +1 -1
  94. package/scripts/find-by-marker.py +2 -0
  95. package/scripts/project-skill-extension.mjs +37 -0
  96. package/scripts/readiness.mjs +2 -1
  97. package/scripts/test_marker_lib.py +8 -0
  98. package/scripts/test_retro_wrapup_contract.py +6 -4
  99. package/scripts/test_skill_publish_audit.py +9 -1
  100. package/src/cli.mjs +86 -12
  101. package/src/commands/init.mjs +24 -0
  102. package/src/commands/own.mjs +7 -3
  103. package/src/commands/update.mjs +87 -21
  104. package/src/lib/bundle.mjs +5 -0
  105. package/src/lib/contributionBridge.mjs +176 -0
  106. package/src/lib/contributionRouting.mjs +152 -0
  107. package/src/lib/manifest.mjs +17 -2
  108. package/src/lib/ownershipClassifier.mjs +65 -0
  109. package/src/lib/projectSkillExtension.mjs +60 -0
  110. package/src/lib/skillRegistry.mjs +167 -0
  111. package/src/lib/updateCandidate.mjs +105 -4
  112. package/src/lib/updateDecisions.mjs +5 -0
  113. package/src/lib/updateReconcile.mjs +102 -9
  114. package/src/lib/verifyUpdateCandidate.mjs +28 -1
  115. package/src/lib/verifyUpdateCandidateProtocol.mjs +2 -0
  116. 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({ candidateRoot, consumerRoot, priorManifest, nextManifest }) {
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
- const manifest = await readManifest(manifestPath);
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 { migrations, migrationConflicts } = await migrateProdSections({
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 ?? [] : []))];
@@ -0,0 +1,5 @@
1
+ export function nonInteractiveUpdateDecision(action) {
2
+ if (action === 'delete') return true;
3
+ if (action === 'collision') return undefined;
4
+ throw new Error(`unknown update decision action: ${action}`);
5
+ }
@@ -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
- if (decision !== 'keep-as-owned' && decision !== 'replace') {
51
- throw new Error(`collision decision for ${file.path} must be keep-as-owned or replace`);
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
- result.collisionResolutions.push({
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 (decision === 'keep-as-owned') {
58
- nextInstalled.push(entry(file, destinationSha256, CONSUMER_ORIGIN));
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 entry(file, installedSha256, origin = KIT_ORIGIN) {
136
- return {
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 === 'keep-as-owned',
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
- || !['keep-as-owned', 'replace'].includes(resolution?.outcome)
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 = resolution.outcome === 'keep-as-owned' ? CONSUMER_ORIGIN : KIT_ORIGIN;
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
- || (resolution.outcome === 'keep-as-owned' && owners.has(path))) {
136
+ || (nonReplace && owners.has(path))) {
121
137
  throw new Error(`candidate invariant transaction: incoherent collision resolution ${path}`);
122
138
  }
123
139
  }