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/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
- for (const skill of adapted.skills) {
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';