release-skill 0.2.2 → 0.2.3
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +4 -2
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +19 -0
- package/INSTALL.md +7 -7
- package/INSTALL.zh-CN.md +7 -7
- package/README.md +11 -13
- package/README.zh-CN.md +10 -12
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +556 -21
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +556 -21
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +556 -21
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +556 -21
- package/bin/release-skill.bundle.mjs +556 -21
- package/package.json +1 -1
- package/src/adapters/plugin-marketplace.mjs +272 -30
- package/src/commands/prepare.mjs +69 -0
- package/src/commands/verify.mjs +11 -5
- package/src/core/baseline.mjs +13 -0
- package/src/core/plan.mjs +178 -0
- package/src/platforms/codebuddy.mjs +40 -0
- package/src/platforms/registry.mjs +158 -0
- package/src/producers/build-adapters.mjs +9 -2
package/src/core/plan.mjs
CHANGED
|
@@ -882,6 +882,184 @@ export function validatePlanActionCompleteness(plan, options = {}) {
|
|
|
882
882
|
}
|
|
883
883
|
}
|
|
884
884
|
|
|
885
|
+
// sourceCommit: frozen plugin source commit, binding the marketplace
|
|
886
|
+
// install action to the unit's frozen snapshot commit. Both forms
|
|
887
|
+
// require it in production plans; non-production diagnostic plans may
|
|
888
|
+
// omit it (never冒充生产计划).
|
|
889
|
+
if (production) {
|
|
890
|
+
_checkRequired(action, 'parameters.sourceCommit', action.parameters?.sourceCommit, frozen?.commit, unitId, failures);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// --- sourceDescriptor validation (marketplace source identity contract) ---
|
|
894
|
+
// Validates that marketplaceForm and sourceDescriptor.form are consistent,
|
|
895
|
+
// all form-specific fields are present and non-null, pluginRepo !== marketplaceRepo
|
|
896
|
+
// for standalone-index, and no mixed/extra fields exist.
|
|
897
|
+
// Only applies to platforms that require marketplace (claude, codex).
|
|
898
|
+
// Non-automatable platforms (kimi, codebuddy) use human-attestation
|
|
899
|
+
// and carry no marketplace form or source descriptor.
|
|
900
|
+
// Legacy plans (pre-sourceDescriptor) with legacyCompatibility are tolerated.
|
|
901
|
+
if (requiresMarketplace) {
|
|
902
|
+
const sd = action.parameters?.sourceDescriptor;
|
|
903
|
+
const mf = action.parameters?.marketplaceForm;
|
|
904
|
+
if (sd === undefined || sd === null) {
|
|
905
|
+
if (!options.legacyCompatibility) {
|
|
906
|
+
failures.push(
|
|
907
|
+
`unit "${unitId}", action "${action.id}": parameters.sourceDescriptor is missing; every marketplace install action must carry a complete source descriptor`,
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
} else if (typeof sd === 'object') {
|
|
911
|
+
// 1. marketplaceForm must be present when sourceDescriptor is present
|
|
912
|
+
if (mf === undefined || mf === null) {
|
|
913
|
+
failures.push(
|
|
914
|
+
`unit "${unitId}", action "${action.id}": parameters.marketplaceForm is missing but sourceDescriptor is present; form must be explicitly declared`,
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
// 2. sourceDescriptor.form must match marketplaceForm
|
|
918
|
+
if (sd.form !== mf) {
|
|
919
|
+
failures.push(
|
|
920
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.form "${sd.form}" does not match parameters.marketplaceForm "${mf}"`,
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
// 3. Form-specific field completeness and non-null checks
|
|
924
|
+
// payloadDigest is allowed to be null in non-production plans
|
|
925
|
+
// (the frozen manifestDigest is only available after production freeze)
|
|
926
|
+
const BUNDLED_REQUIRED = ['form', 'repo', 'marketplaceEntry', 'pluginSubpath'];
|
|
927
|
+
const STANDALONE_REQUIRED = ['form', 'marketplaceRepo', 'marketplaceEntry', 'pluginRepo', 'sourceType'];
|
|
928
|
+
// Production-only fields that may be null in non-production plans
|
|
929
|
+
const PROD_ONLY_FIELDS = ['marketplaceCommitSha', 'ref', 'payloadDigest'];
|
|
930
|
+
|
|
931
|
+
if (sd.form === 'bundled-family') {
|
|
932
|
+
// Check bundled-family required fields are present and non-null
|
|
933
|
+
for (const field of BUNDLED_REQUIRED) {
|
|
934
|
+
if (sd[field] === undefined || sd[field] === null) {
|
|
935
|
+
failures.push(
|
|
936
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.${field} is missing or null; bundled-family form requires all identity fields to be non-null`,
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
// commit must be non-null in production plans (frozen from asset)
|
|
941
|
+
if (production && (sd.commit === undefined || sd.commit === null)) {
|
|
942
|
+
failures.push(
|
|
943
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.commit is missing or null; production bundled-family plans require a frozen commit`,
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
// commit must match frozenSnapshot.commit (production binding)
|
|
947
|
+
if (production && sd.commit !== undefined && sd.commit !== null &&
|
|
948
|
+
frozen?.commit !== undefined && sd.commit !== frozen.commit) {
|
|
949
|
+
failures.push(
|
|
950
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.commit does not match frozenSnapshot.commit`,
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
// payloadDigest must be non-null in production plans
|
|
954
|
+
if (production && (sd.payloadDigest === undefined || sd.payloadDigest === null)) {
|
|
955
|
+
failures.push(
|
|
956
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.payloadDigest is missing or null; production plans require a frozen payload digest`,
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
// Prohibit standalone-index fields in bundled-family
|
|
960
|
+
const STANDALONE_ONLY = ['marketplaceRepo', 'pluginRepo', 'sourceType', 'marketplaceCommitSha', 'ref'];
|
|
961
|
+
for (const field of STANDALONE_ONLY) {
|
|
962
|
+
if (sd[field] !== undefined) {
|
|
963
|
+
failures.push(
|
|
964
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.${field} is unexpected for bundled-family form; mixed fields are prohibited`,
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
// repo must match the action's repo (publicRepo for bundled-family)
|
|
969
|
+
if (sd.repo !== undefined && sd.repo !== null && sd.repo !== publicRepo) {
|
|
970
|
+
failures.push(
|
|
971
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.repo "${sd.repo}" does not match unit publicRepo "${publicRepo}"`,
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
// marketplaceEntry must match action.plugin
|
|
975
|
+
if (sd.marketplaceEntry !== undefined && sd.marketplaceEntry !== null && sd.marketplaceEntry !== plugin) {
|
|
976
|
+
failures.push(
|
|
977
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.marketplaceEntry "${sd.marketplaceEntry}" does not match action plugin "${plugin}"`,
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
// payloadDigest must match manifestDigest (when both are present)
|
|
981
|
+
if (production && sd.payloadDigest !== undefined && sd.payloadDigest !== null &&
|
|
982
|
+
frozen?.manifestDigest !== undefined && sd.payloadDigest !== frozen.manifestDigest) {
|
|
983
|
+
failures.push(
|
|
984
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.payloadDigest does not match frozen manifestDigest`,
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
} else if (sd.form === 'standalone-index') {
|
|
988
|
+
// Check standalone-index required fields are present and non-null
|
|
989
|
+
for (const field of STANDALONE_REQUIRED) {
|
|
990
|
+
if (sd[field] === undefined || sd[field] === null) {
|
|
991
|
+
failures.push(
|
|
992
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.${field} is missing or null; standalone-index form requires all identity fields to be non-null`,
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
// Production-only fields: must be non-null in production plans
|
|
997
|
+
if (production) {
|
|
998
|
+
for (const field of PROD_ONLY_FIELDS) {
|
|
999
|
+
if (sd[field] === undefined || sd[field] === null) {
|
|
1000
|
+
failures.push(
|
|
1001
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.${field} is missing or null; production standalone-index requires all fields to be non-null`,
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
// Prohibit bundled-family fields in standalone-index
|
|
1007
|
+
const BUNDLED_ONLY = ['repo', 'commit', 'pluginSubpath'];
|
|
1008
|
+
for (const field of BUNDLED_ONLY) {
|
|
1009
|
+
if (sd[field] !== undefined) {
|
|
1010
|
+
failures.push(
|
|
1011
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.${field} is unexpected for standalone-index form; mixed fields are prohibited`,
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
// pluginRepo must NOT equal marketplaceRepo
|
|
1016
|
+
if (sd.pluginRepo !== undefined && sd.pluginRepo !== null &&
|
|
1017
|
+
sd.marketplaceRepo !== undefined && sd.marketplaceRepo !== null &&
|
|
1018
|
+
sd.pluginRepo === sd.marketplaceRepo) {
|
|
1019
|
+
failures.push(
|
|
1020
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.pluginRepo must not equal marketplaceRepo; the plugin repo and marketplace repo are distinct repositories`,
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
// marketplaceRepo must match action.repo (for standalone-index, repo is the external marketplace)
|
|
1024
|
+
if (externalMarketplace && sd.marketplaceRepo !== undefined && sd.marketplaceRepo !== null &&
|
|
1025
|
+
sd.marketplaceRepo !== dist.marketplaceRepo) {
|
|
1026
|
+
failures.push(
|
|
1027
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.marketplaceRepo "${sd.marketplaceRepo}" does not match distribution marketplaceRepo "${dist.marketplaceRepo}"`,
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
// pluginRepo must match the unit's publicRepo
|
|
1031
|
+
if (sd.pluginRepo !== undefined && sd.pluginRepo !== null && sd.pluginRepo !== publicRepo) {
|
|
1032
|
+
failures.push(
|
|
1033
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.pluginRepo "${sd.pluginRepo}" does not match unit publicRepo "${publicRepo}"`,
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
// marketplaceEntry must match action.plugin
|
|
1037
|
+
if (sd.marketplaceEntry !== undefined && sd.marketplaceEntry !== null && sd.marketplaceEntry !== plugin) {
|
|
1038
|
+
failures.push(
|
|
1039
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.marketplaceEntry "${sd.marketplaceEntry}" does not match action plugin "${plugin}"`,
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
// sourceType must be 'marketplace-entry'
|
|
1043
|
+
if (sd.sourceType !== undefined && sd.sourceType !== null && sd.sourceType !== 'marketplace-entry') {
|
|
1044
|
+
failures.push(
|
|
1045
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.sourceType "${sd.sourceType}" is unexpected; standalone-index sourceType must be "marketplace-entry"`,
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
// payloadDigest must match manifestDigest (when both are present)
|
|
1049
|
+
if (production && sd.payloadDigest !== undefined && sd.payloadDigest !== null &&
|
|
1050
|
+
frozen?.manifestDigest !== undefined && sd.payloadDigest !== frozen.manifestDigest) {
|
|
1051
|
+
failures.push(
|
|
1052
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.payloadDigest does not match frozen manifestDigest`,
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
} else {
|
|
1056
|
+
failures.push(
|
|
1057
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.form "${sd.form}" is unknown; expected "bundled-family" or "standalone-index"`,
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
885
1063
|
// Expected checks. Marketplace identity is bound only on platforms
|
|
886
1064
|
// whose schema requires it (MINOR-1): a non-marketplace platform's
|
|
887
1065
|
// observation never binds a marketplace.
|
|
@@ -242,6 +242,19 @@ function codebuddyDesktopTailSegments(marketplace, plugin) {
|
|
|
242
242
|
return [CODEBUDDY_DESKTOP_HOME_DIR, 'plugins', 'marketplaces', marketplace, 'plugins', plugin];
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Segment-level tail an attested cli installPath must end with:
|
|
247
|
+
* `/.codebuddy/plugins/marketplaces/<marketplace>/plugins/<plugin>`
|
|
248
|
+
* (verified CodeBuddy CLI layout).
|
|
249
|
+
*
|
|
250
|
+
* @param {string} marketplace
|
|
251
|
+
* @param {string} plugin
|
|
252
|
+
* @returns {string[]}
|
|
253
|
+
*/
|
|
254
|
+
function codebuddyCliTailSegments(marketplace, plugin) {
|
|
255
|
+
return [CODEBUDDY_CLI_STATE_DIR, 'plugins', 'marketplaces', marketplace, 'plugins', plugin];
|
|
256
|
+
}
|
|
257
|
+
|
|
245
258
|
/**
|
|
246
259
|
* Segment-level check that a desktop installPath ends with the well-known
|
|
247
260
|
* WorkBuddy marketplace layout. Pure string check (no filesystem): the path's
|
|
@@ -261,6 +274,25 @@ function matchesDesktopLayout(installPath, marketplace, plugin) {
|
|
|
261
274
|
return tail.every((segment, index) => segments[offset + index] === segment);
|
|
262
275
|
}
|
|
263
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Segment-level check that a cli installPath ends with the well-known
|
|
279
|
+
* CodeBuddy CLI marketplace layout. Pure string check (no filesystem): the
|
|
280
|
+
* path's segments (split on `/` or `\`, dropping empty/`.` segments) must end
|
|
281
|
+
* with the cli tail. Fails closed otherwise.
|
|
282
|
+
*
|
|
283
|
+
* @param {string} installPath
|
|
284
|
+
* @param {string} marketplace
|
|
285
|
+
* @param {string} plugin
|
|
286
|
+
* @returns {boolean}
|
|
287
|
+
*/
|
|
288
|
+
function matchesCliLayout(installPath, marketplace, plugin) {
|
|
289
|
+
const segments = installPath.split(/[\\/]+/).filter((s) => s !== '' && s !== '.');
|
|
290
|
+
const tail = codebuddyCliTailSegments(marketplace, plugin);
|
|
291
|
+
if (segments.length < tail.length) return false;
|
|
292
|
+
const offset = segments.length - tail.length;
|
|
293
|
+
return tail.every((segment, index) => segments[offset + index] === segment);
|
|
294
|
+
}
|
|
295
|
+
|
|
264
296
|
/**
|
|
265
297
|
* Human-facing, actionable manual-install closed-loop instructions for
|
|
266
298
|
* CodeBuddy / WorkBuddy.
|
|
@@ -411,6 +443,14 @@ export function validateCodeBuddyAttestation(attestation, action, isoNow, boundP
|
|
|
411
443
|
&& !matchesDesktopLayout(attestation.installPath, attestation.marketplace, attestation.plugin)) {
|
|
412
444
|
return { valid: false, error: `codebuddy attestation installPath does not match the WorkBuddy desktop marketplace layout (.workbuddy/plugins/marketplaces/${attestation.marketplace}/plugins/${attestation.plugin})` };
|
|
413
445
|
}
|
|
446
|
+
// CLI installPath must end with the well-known CodeBuddy CLI marketplace
|
|
447
|
+
// layout (segment-level). This is a lexical check; the full realpath
|
|
448
|
+
// containment within the isolated home is enforced in the adapter observe
|
|
449
|
+
// branch (needs the context-derived isolated home + realpath resolution).
|
|
450
|
+
if (attestation.installChannel === 'cli'
|
|
451
|
+
&& !matchesCliLayout(attestation.installPath, attestation.marketplace, attestation.plugin)) {
|
|
452
|
+
return { valid: false, error: `codebuddy attestation installPath does not match the CodeBuddy CLI marketplace layout (.codebuddy/plugins/marketplaces/${attestation.marketplace}/plugins/${attestation.plugin})` };
|
|
453
|
+
}
|
|
414
454
|
return { valid: true, error: null };
|
|
415
455
|
}
|
|
416
456
|
|
|
@@ -137,6 +137,12 @@ const CLAUDE = Object.freeze({
|
|
|
137
137
|
// actionType -> adapter map from it (T2.2 step 3).
|
|
138
138
|
adapter: 'plugin-marketplace',
|
|
139
139
|
automatable: true,
|
|
140
|
+
// --- capability contract (平台能力契约) ------------------------------------
|
|
141
|
+
installMethod: 'structured-cli',
|
|
142
|
+
refStrength: 'name-ref',
|
|
143
|
+
outputProtocol: 'text',
|
|
144
|
+
identityEvidence: 'list-record',
|
|
145
|
+
degradationPolicy: 'block',
|
|
140
146
|
cli: Object.freeze({
|
|
141
147
|
binary: 'claude',
|
|
142
148
|
marketplaceAdd: (repo, ref) => ['plugin', 'marketplace', 'add', `${repo}@${ref}`],
|
|
@@ -190,6 +196,12 @@ const CODEX = Object.freeze({
|
|
|
190
196
|
actionType: 'codex-marketplace-install',
|
|
191
197
|
adapter: 'plugin-marketplace',
|
|
192
198
|
automatable: true,
|
|
199
|
+
// --- capability contract (平台能力契约) ------------------------------------
|
|
200
|
+
installMethod: 'structured-cli',
|
|
201
|
+
refStrength: 'commit-sha',
|
|
202
|
+
outputProtocol: 'structured',
|
|
203
|
+
identityEvidence: 'install-output',
|
|
204
|
+
degradationPolicy: 'block',
|
|
193
205
|
cli: Object.freeze({
|
|
194
206
|
binary: 'codex',
|
|
195
207
|
marketplaceAdd: (repo, ref) => ['plugin', 'marketplace', 'add', repo, '--ref', ref, '--json'],
|
|
@@ -242,6 +254,12 @@ const KIMI = Object.freeze({
|
|
|
242
254
|
actionType: 'kimi-marketplace-install',
|
|
243
255
|
adapter: 'plugin-marketplace',
|
|
244
256
|
automatable: false,
|
|
257
|
+
// --- capability contract (平台能力契约) ------------------------------------
|
|
258
|
+
installMethod: 'interactive-only',
|
|
259
|
+
refStrength: 'unfixable',
|
|
260
|
+
outputProtocol: 'none',
|
|
261
|
+
identityEvidence: 'filesystem-payload',
|
|
262
|
+
degradationPolicy: 'human-attestation',
|
|
245
263
|
cli: null,
|
|
246
264
|
jsonProtocol: Object.freeze({
|
|
247
265
|
listOutput: null,
|
|
@@ -307,6 +325,12 @@ const CODEBUDDY = Object.freeze({
|
|
|
307
325
|
actionType: 'codebuddy-marketplace-install',
|
|
308
326
|
adapter: 'plugin-marketplace',
|
|
309
327
|
automatable: false,
|
|
328
|
+
// --- capability contract (平台能力契约) ------------------------------------
|
|
329
|
+
installMethod: 'human-attestation',
|
|
330
|
+
refStrength: 'unfixable',
|
|
331
|
+
outputProtocol: 'none',
|
|
332
|
+
identityEvidence: 'human-attestation',
|
|
333
|
+
degradationPolicy: 'human-attestation',
|
|
310
334
|
cli: null,
|
|
311
335
|
jsonProtocol: Object.freeze({
|
|
312
336
|
listOutput: null,
|
|
@@ -373,6 +397,11 @@ const VALID_MARKETPLACE_REF_FORMS = new Set(['sha', 'name', null]);
|
|
|
373
397
|
const VALID_LIST_OUTPUTS = new Set(['array', 'installed-object', null]);
|
|
374
398
|
const VALID_CLI_OUTPUTS = new Set(['json', null]);
|
|
375
399
|
const VALID_ENTRY_VERSION_BINDING = new Set([true, false, null]);
|
|
400
|
+
const VALID_INSTALL_METHODS = new Set(['structured-cli', 'interactive-only', 'human-attestation']);
|
|
401
|
+
const VALID_REF_STRENGTHS = new Set(['commit-sha', 'name-ref', 'unfixable']);
|
|
402
|
+
const VALID_OUTPUT_PROTOCOLS = new Set(['structured', 'text', 'none']);
|
|
403
|
+
const VALID_IDENTITY_EVIDENCE = new Set(['list-record', 'install-output', 'filesystem-payload', 'human-attestation']);
|
|
404
|
+
const VALID_DEGRADATION_POLICIES = new Set(['block', 'human-attestation']);
|
|
376
405
|
|
|
377
406
|
/**
|
|
378
407
|
* Look up a platform descriptor by id.
|
|
@@ -453,6 +482,23 @@ export function assertRegistry(registry = PLATFORMS) {
|
|
|
453
482
|
throw new Error(`platform registry: ${label} isolationSubdirs must be an array`);
|
|
454
483
|
}
|
|
455
484
|
|
|
485
|
+
// --- capability contract validation (能力契约验证) -----------------------
|
|
486
|
+
if (!VALID_INSTALL_METHODS.has(platform.installMethod)) {
|
|
487
|
+
throw new Error(`platform registry: ${label} has illegal installMethod "${platform.installMethod}"`);
|
|
488
|
+
}
|
|
489
|
+
if (!VALID_REF_STRENGTHS.has(platform.refStrength)) {
|
|
490
|
+
throw new Error(`platform registry: ${label} has illegal refStrength "${platform.refStrength}"`);
|
|
491
|
+
}
|
|
492
|
+
if (!VALID_OUTPUT_PROTOCOLS.has(platform.outputProtocol)) {
|
|
493
|
+
throw new Error(`platform registry: ${label} has illegal outputProtocol "${platform.outputProtocol}"`);
|
|
494
|
+
}
|
|
495
|
+
if (!VALID_IDENTITY_EVIDENCE.has(platform.identityEvidence)) {
|
|
496
|
+
throw new Error(`platform registry: ${label} has illegal identityEvidence "${platform.identityEvidence}"`);
|
|
497
|
+
}
|
|
498
|
+
if (!VALID_DEGRADATION_POLICIES.has(platform.degradationPolicy)) {
|
|
499
|
+
throw new Error(`platform registry: ${label} has illegal degradationPolicy "${platform.degradationPolicy}"`);
|
|
500
|
+
}
|
|
501
|
+
|
|
456
502
|
if (platform.automatable) {
|
|
457
503
|
if (!platform.cli || typeof platform.cli.marketplaceAdd !== 'function'
|
|
458
504
|
|| typeof platform.cli.install !== 'function' || typeof platform.cli.list !== 'function') {
|
|
@@ -481,8 +527,120 @@ export function assertRegistry(registry = PLATFORMS) {
|
|
|
481
527
|
throw new Error(`platform registry: non-automatable platform ${label} needs strategy.readManifest`);
|
|
482
528
|
}
|
|
483
529
|
}
|
|
530
|
+
|
|
531
|
+
// --- capability contract consistency rules (能力契约一致性规则) -----------
|
|
532
|
+
// automatable === true 必须有 installMethod === 'structured-cli'
|
|
533
|
+
if (platform.automatable === true && platform.installMethod !== 'structured-cli') {
|
|
534
|
+
throw new Error(`platform registry: ${label} is automatable but installMethod is "${platform.installMethod}" (expected "structured-cli")`);
|
|
535
|
+
}
|
|
536
|
+
// automatable === false 必须有 installMethod !== 'structured-cli'
|
|
537
|
+
if (platform.automatable === false && platform.installMethod === 'structured-cli') {
|
|
538
|
+
throw new Error(`platform registry: ${label} is not automatable but installMethod is "structured-cli"`);
|
|
539
|
+
}
|
|
540
|
+
// installMethod === 'structured-cli' 必须有 refStrength !== 'unfixable'
|
|
541
|
+
if (platform.installMethod === 'structured-cli' && platform.refStrength === 'unfixable') {
|
|
542
|
+
throw new Error(`platform registry: ${label} has structured-cli install but unfixable refStrength`);
|
|
543
|
+
}
|
|
544
|
+
// installMethod === 'human-attestation' 必须有 identityEvidence === 'human-attestation'
|
|
545
|
+
if (platform.installMethod === 'human-attestation' && platform.identityEvidence !== 'human-attestation') {
|
|
546
|
+
throw new Error(`platform registry: ${label} has human-attestation install but identityEvidence is "${platform.identityEvidence}"`);
|
|
547
|
+
}
|
|
484
548
|
}
|
|
485
549
|
}
|
|
486
550
|
|
|
487
551
|
// Self-validate at import time: a malformed registry never reaches runtime.
|
|
488
552
|
assertRegistry();
|
|
553
|
+
|
|
554
|
+
// --- capability-driven routing & conflict detection (能力驱动路由与矛盾检测) --
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* 根据平台描述符的 installMethod 决定执行路由。
|
|
558
|
+
*
|
|
559
|
+
* @param {object} platform - 平台描述符
|
|
560
|
+
* @returns {{ route: string, reason: string }}
|
|
561
|
+
* @throws {Error} 无法路由时抛出
|
|
562
|
+
*/
|
|
563
|
+
export function resolvePlatformRoute(platform) {
|
|
564
|
+
const { installMethod } = platform;
|
|
565
|
+
if (installMethod === 'structured-cli') {
|
|
566
|
+
return { route: 'structured-cli', reason: `installMethod is structured-cli` };
|
|
567
|
+
}
|
|
568
|
+
if (installMethod === 'interactive-only' || installMethod === 'human-attestation') {
|
|
569
|
+
return { route: 'human-attestation', reason: `installMethod is ${installMethod}` };
|
|
570
|
+
}
|
|
571
|
+
throw new Error(
|
|
572
|
+
`platform "${platform?.id ?? '<unknown>'}" with installMethod="${installMethod}" cannot be routed`,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* 检测平台能力描述符中的矛盾。
|
|
578
|
+
*
|
|
579
|
+
* @param {object} platform - 平台描述符
|
|
580
|
+
* @returns {{ hasConflict: boolean, conflicts: string[] }}
|
|
581
|
+
* @throws {Error} 发现矛盾时抛出
|
|
582
|
+
*/
|
|
583
|
+
export function resolveCapabilityConflicts(platform) {
|
|
584
|
+
const conflicts = [];
|
|
585
|
+
const { id, automatable, installMethod, refStrength, identityEvidence, cli, strategy } = platform;
|
|
586
|
+
|
|
587
|
+
// automatable <-> installMethod 一致性
|
|
588
|
+
if (automatable === true && installMethod !== 'structured-cli') {
|
|
589
|
+
conflicts.push(`automatable=true but installMethod="${installMethod}"`);
|
|
590
|
+
}
|
|
591
|
+
if (automatable === false && installMethod === 'structured-cli') {
|
|
592
|
+
conflicts.push(`automatable=false but installMethod="structured-cli"`);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// installMethod <-> refStrength 一致性
|
|
596
|
+
if (installMethod === 'structured-cli' && refStrength === 'unfixable') {
|
|
597
|
+
conflicts.push(`structured-cli with unfixable refStrength`);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// installMethod <-> identityEvidence 一致性
|
|
601
|
+
if (installMethod === 'human-attestation' && identityEvidence !== 'human-attestation') {
|
|
602
|
+
conflicts.push(`human-attestation install but identityEvidence="${identityEvidence}"`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// 自动化平台必须有 cli 和策略解析函数
|
|
606
|
+
if (automatable === true) {
|
|
607
|
+
if (!cli || typeof cli !== 'object') {
|
|
608
|
+
conflicts.push(`automatable=true but cli is missing`);
|
|
609
|
+
}
|
|
610
|
+
if (!strategy || typeof strategy.parseListOutput !== 'function') {
|
|
611
|
+
conflicts.push(`automatable=true but strategy.parseListOutput is missing`);
|
|
612
|
+
}
|
|
613
|
+
if (!strategy || typeof strategy.extractInstallPath !== 'function') {
|
|
614
|
+
conflicts.push(`automatable=true but strategy.extractInstallPath is missing`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// 非自动化平台必须 cli === null,且有手动策略函数
|
|
619
|
+
if (automatable === false) {
|
|
620
|
+
if (cli !== null) {
|
|
621
|
+
conflicts.push(`automatable=false but cli is not null`);
|
|
622
|
+
}
|
|
623
|
+
if (!strategy || typeof strategy.buildManualRequirement !== 'function') {
|
|
624
|
+
conflicts.push(`automatable=false but strategy.buildManualRequirement is missing`);
|
|
625
|
+
}
|
|
626
|
+
if (!strategy || typeof strategy.readManifest !== 'function') {
|
|
627
|
+
conflicts.push(`automatable=false but strategy.readManifest is missing`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (conflicts.length > 0) {
|
|
632
|
+
throw new Error(`capability conflict in platform "${id}": ${conflicts.join('; ')}`);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return { hasConflict: false, conflicts: [] };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* 从 installMethod 派生 automatable 标志。
|
|
640
|
+
*
|
|
641
|
+
* @param {object} platform - 平台描述符
|
|
642
|
+
* @returns {boolean}
|
|
643
|
+
*/
|
|
644
|
+
export function deriveAutomatable(platform) {
|
|
645
|
+
return platform.installMethod === 'structured-cli';
|
|
646
|
+
}
|
|
@@ -438,9 +438,16 @@ async function generateAdapterFiles(platform, skills, templateJson, root, market
|
|
|
438
438
|
if (typeof adapted.skills === 'string') {
|
|
439
439
|
adapted.skills = platform.name === 'workbuddy' ? ['./skills/'] : './skills/';
|
|
440
440
|
} else if (Array.isArray(adapted.skills)) {
|
|
441
|
-
|
|
441
|
+
// Handle both string array and object array forms
|
|
442
|
+
adapted.skills = adapted.skills.map((skill) => {
|
|
443
|
+
if (typeof skill === 'string') {
|
|
444
|
+
// String array: transform to relative path for the adapter
|
|
445
|
+
return platform.name === 'workbuddy' ? './skills/' : skill;
|
|
446
|
+
}
|
|
447
|
+
// Object array: preserve existing behavior
|
|
442
448
|
skill.source = `../skills/${skill.name}/SKILL.md`;
|
|
443
|
-
|
|
449
|
+
return skill;
|
|
450
|
+
});
|
|
444
451
|
}
|
|
445
452
|
|
|
446
453
|
const pluginJsonContent = JSON.stringify(adapted, null, 2) + '\n';
|