release-skill 0.1.10 → 0.2.1

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 (82) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codebuddy-plugin/plugin.json +10 -0
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +33 -0
  7. package/INSTALL.md +24 -4
  8. package/INSTALL.zh-CN.md +22 -4
  9. package/README.md +19 -32
  10. package/README.zh-CN.md +14 -25
  11. package/adapters/claude/.claude-plugin/marketplace.json +1 -1
  12. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  13. package/adapters/claude/bin/release-skill.bundle.mjs +2721 -1843
  14. package/adapters/claude/schemas/.render-manifest.json +8 -8
  15. package/adapters/claude/schemas/approval-record.schema.json +1 -1
  16. package/adapters/claude/schemas/release-plan.schema.json +6 -2
  17. package/adapters/claude/schemas/release-project.schema.json +14 -0
  18. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  19. package/adapters/codex/bin/release-skill.bundle.mjs +2721 -1843
  20. package/adapters/codex/schemas/.render-manifest.json +8 -8
  21. package/adapters/codex/schemas/approval-record.schema.json +1 -1
  22. package/adapters/codex/schemas/release-plan.schema.json +6 -2
  23. package/adapters/codex/schemas/release-project.schema.json +14 -0
  24. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  25. package/adapters/kimi/bin/release-skill.bundle.mjs +2721 -1843
  26. package/adapters/kimi/schemas/.render-manifest.json +8 -8
  27. package/adapters/kimi/schemas/approval-record.schema.json +1 -1
  28. package/adapters/kimi/schemas/release-plan.schema.json +6 -2
  29. package/adapters/kimi/schemas/release-project.schema.json +14 -0
  30. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +10 -0
  31. package/adapters/workbuddy/bin/release-skill.bundle.mjs +85363 -0
  32. package/adapters/workbuddy/bin/release-skill.mjs +54 -0
  33. package/adapters/workbuddy/native/safe-write/binding.gyp +41 -0
  34. package/adapters/workbuddy/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
  35. package/adapters/workbuddy/native/safe-write/prebuilds.json +24 -0
  36. package/adapters/workbuddy/native/safe-write/src/safe_write.cc +2032 -0
  37. package/adapters/workbuddy/schemas/.render-manifest.json +37 -0
  38. package/adapters/workbuddy/schemas/approval-record.schema.json +115 -0
  39. package/adapters/workbuddy/schemas/artifact-lock.schema.json +111 -0
  40. package/adapters/workbuddy/schemas/artifact-plan.schema.json +52 -0
  41. package/adapters/workbuddy/schemas/artifact-policy.schema.json +76 -0
  42. package/adapters/workbuddy/schemas/evidence-event.schema.json +89 -0
  43. package/adapters/workbuddy/schemas/release-plan.schema.json +882 -0
  44. package/adapters/workbuddy/schemas/release-project.schema.json +909 -0
  45. package/adapters/workbuddy/schemas/release-run.schema.json +343 -0
  46. package/adapters/workbuddy/skills/release-assess/SKILL.md +51 -0
  47. package/adapters/workbuddy/skills/release-help/SKILL.md +77 -0
  48. package/adapters/workbuddy/skills/release-prepare/SKILL.md +92 -0
  49. package/adapters/workbuddy/skills/release-publish/SKILL.md +57 -0
  50. package/adapters/workbuddy/skills/release-reconcile/SKILL.md +73 -0
  51. package/adapters/workbuddy/skills/release-setup/SKILL.md +95 -0
  52. package/adapters/workbuddy/skills/release-verify/SKILL.md +70 -0
  53. package/bin/release-skill-cli.mjs +3 -0
  54. package/bin/release-skill.bundle.mjs +2721 -1843
  55. package/package.json +9 -2
  56. package/references/.render-manifest.json +8 -8
  57. package/references/01-state-machine.md +5 -5
  58. package/references/02-project-config.md +1 -1
  59. package/references/05-evidence-and-errors.md +1 -1
  60. package/references/06-adapter-contract.md +41 -1
  61. package/schemas/.render-manifest.json +8 -8
  62. package/schemas/approval-record.schema.json +1 -1
  63. package/schemas/release-plan.schema.json +6 -2
  64. package/schemas/release-project.schema.json +14 -0
  65. package/scripts/sync-public-files.mjs +481 -0
  66. package/src/adapters/contract.mjs +60 -0
  67. package/src/adapters/plugin-marketplace.mjs +289 -736
  68. package/src/commands/prepare.mjs +195 -182
  69. package/src/commands/publish.mjs +438 -122
  70. package/src/commands/reconcile.mjs +369 -191
  71. package/src/commands/verify.mjs +13 -2
  72. package/src/core/approval.mjs +72 -45
  73. package/src/core/baseline.mjs +5 -0
  74. package/src/core/checkpoints.mjs +143 -0
  75. package/src/core/evidence.mjs +30 -3
  76. package/src/core/hook-cache.mjs +254 -0
  77. package/src/core/hooks.mjs +37 -1
  78. package/src/core/observe-retry.mjs +223 -0
  79. package/src/core/plan.mjs +162 -253
  80. package/src/platforms/kimi.mjs +514 -0
  81. package/src/platforms/registry.mjs +393 -0
  82. package/src/producers/build-adapters.mjs +49 -23
@@ -0,0 +1,393 @@
1
+ /**
2
+ * Platform registry — the single source of truth for consumer-platform
3
+ * knowledge (claude / codex / kimi).
4
+ *
5
+ * T2.2 converges platform facts that were scattered across plugin-marketplace.mjs,
6
+ * build-adapters.mjs, prepare.mjs, plan.mjs and project.yaml into this module.
7
+ * Data that is purely declarative (paths, argv templates, env construction,
8
+ * manifest layout, host-artifact exemptions, schema required fields) lives in
9
+ * each platform's descriptor; protocol LOGIC differences (claude `list` returns
10
+ * an array vs codex returns `{installed:[]}`) live in per-platform strategy
11
+ * functions referenced from the descriptor. Data in tables, logic in functions
12
+ * — neither scattered.
13
+ *
14
+ * Adding a platform is meant to converge on ~3 hand edits: a descriptor +
15
+ * strategy here, a schema enum bump (the schema-contract test turns red until
16
+ * it is done), and a regeneration of the manifest/adapter producers.
17
+ *
18
+ * `assertRegistry()` runs at module load: a malformed registry throws at import
19
+ * time, never at runtime.
20
+ *
21
+ * NOTE (T2.2 step 2): the claude/codex strategy functions (pure list-parse /
22
+ * install-path / identity extraction / list cross-validation) live in this
23
+ * file; kimi's side-effecting manual-requirement and manifest-reading
24
+ * strategies live in ./kimi.mjs (§4.2's per-platform-module option — the only
25
+ * non-trivial strategies) and are referenced from the kimi descriptor below.
26
+ * plugin-marketplace.mjs consumes every platform difference through this
27
+ * registry; see the t2-2 deviation record for the step-2 wiring details.
28
+ *
29
+ * @module platforms/registry
30
+ */
31
+
32
+ import { join } from 'node:path';
33
+
34
+ import {
35
+ executeKimiManualRequirement,
36
+ readKimiManifest,
37
+ KIMI_MANIFEST_CANDIDATES,
38
+ } from './kimi.mjs';
39
+
40
+ // --- claude strategy -------------------------------------------------------
41
+
42
+ function claudeParseListOutput(listOutput, pluginId) {
43
+ // Claude `plugin list --json` returns a top-level array; installPath is read
44
+ // from the matching entry.
45
+ if (!Array.isArray(listOutput)) {
46
+ return { ok: false, error: 'Claude plugin list did not return an array' };
47
+ }
48
+ const found = listOutput.find((p) => p.id === pluginId);
49
+ if (!found) {
50
+ return { ok: false, error: `plugin "${pluginId}" not found in Claude plugin list` };
51
+ }
52
+ if (!found.installPath) {
53
+ return { ok: false, error: `plugin "${pluginId}" found but missing installPath` };
54
+ }
55
+ return { ok: true, found, installPath: found.installPath };
56
+ }
57
+
58
+ function claudeExtractInstallPath({ listParsed }) {
59
+ // Claude reports no installedPath at install time; the install path comes from
60
+ // the parsed list entry. Callers only invoke this after parseListOutput
61
+ // returned ok, which guarantees a non-empty installPath (fail-closed there).
62
+ return { ok: true, installPath: listParsed?.installPath };
63
+ }
64
+
65
+ function claudeExtractListIdentity(found) {
66
+ // Claude list may not have name; extract plugin/marketplace from id.
67
+ // Key order (plugin, marketplace, version) is observation-serialization
68
+ // relevant and mirrors the legacy observe backfill byte-for-byte.
69
+ const idParts = found.id.split('@');
70
+ const identity = { plugin: idParts[0], marketplace: idParts.slice(1).join('@') };
71
+ if (found.version) identity.version = found.version;
72
+ return identity;
73
+ }
74
+
75
+ // --- codex strategy --------------------------------------------------------
76
+
77
+ function codexParseListOutput(listOutput, pluginId) {
78
+ // Codex `plugin list --json` returns `{installed: [...]}`; the list does NOT
79
+ // carry installedPath (that comes from the install evidence).
80
+ const installed = listOutput?.installed;
81
+ if (!Array.isArray(installed)) {
82
+ return { ok: false, error: 'Codex plugin list did not return {installed: [...]}' };
83
+ }
84
+ const found = installed.find((p) => p.pluginId === pluginId);
85
+ if (!found) {
86
+ return { ok: false, error: `plugin "${pluginId}" not found in Codex installed list` };
87
+ }
88
+ return { ok: true, found };
89
+ }
90
+
91
+ function codexExtractInstallPath({ execEvidence }) {
92
+ const installPath = execEvidence?.installOutput?.installedPath;
93
+ if (!installPath) {
94
+ return { ok: false, error: 'evidence install JSON missing installedPath' };
95
+ }
96
+ return { ok: true, installPath };
97
+ }
98
+
99
+ function codexExtractListIdentity(found) {
100
+ // Every field is conditional in the codex list output. Key order (plugin,
101
+ // marketplace, version) mirrors the legacy observe backfill byte-for-byte.
102
+ const identity = {};
103
+ if (found.name) identity.plugin = found.name;
104
+ if (found.marketplaceName) identity.marketplace = found.marketplaceName;
105
+ if (found.version) identity.version = found.version;
106
+ return identity;
107
+ }
108
+
109
+ function codexCrossValidateListEntry(found, action) {
110
+ // Cross-validate: list fields must match evidence/action.
111
+ if (found.name !== action.plugin) {
112
+ return { ok: false, error: `list name "${found.name}" does not match action plugin "${action.plugin}"` };
113
+ }
114
+ if (found.marketplaceName !== action.marketplace) {
115
+ return { ok: false, error: `list marketplaceName "${found.marketplaceName}" does not match action marketplace "${action.marketplace}"` };
116
+ }
117
+ if (found.version !== action.version) {
118
+ return { ok: false, error: `list version "${found.version}" does not match action version "${action.version}"` };
119
+ }
120
+ return { ok: true };
121
+ }
122
+
123
+ // --- platform descriptors --------------------------------------------------
124
+
125
+ const CLAUDE = Object.freeze({
126
+ id: 'claude',
127
+ distributionType: 'claude-plugin',
128
+ actionType: 'claude-marketplace-install',
129
+ // Runtime adapter implementing this platform's marketplace-install action.
130
+ // prepare.mjs stamps it onto generated actions; plan.mjs derives its
131
+ // actionType -> adapter map from it (T2.2 step 3).
132
+ adapter: 'plugin-marketplace',
133
+ automatable: true,
134
+ cli: Object.freeze({
135
+ binary: 'claude',
136
+ marketplaceAdd: (repo, ref) => ['plugin', 'marketplace', 'add', `${repo}@${ref}`],
137
+ install: (plugin, marketplace) => ['plugin', 'install', `${plugin}@${marketplace}`],
138
+ list: () => ['plugin', 'list', '--json'],
139
+ }),
140
+ jsonProtocol: Object.freeze({
141
+ listOutput: 'array',
142
+ installPathSource: 'list',
143
+ // claude marketplace add / plugin install emit no validated JSON output.
144
+ marketplaceAddOutput: null,
145
+ pluginInstallOutput: null,
146
+ }),
147
+ isolationEnv: (home) => ({ HOME: home, CLAUDE_CONFIG_DIR: join(home, '.claude') }),
148
+ // execute pre-creates the claude config dir under the isolated HOME.
149
+ isolationSubdirs: Object.freeze(['.claude']),
150
+ manifestPaths: Object.freeze({
151
+ plugin: '.claude-plugin/plugin.json',
152
+ marketplace: '.claude-plugin/marketplace.json',
153
+ }),
154
+ marketplaceSourceForm: 'string',
155
+ // Claude carries the authoritative version in the marketplace entry: the
156
+ // preflight binds entry.version to the action version.
157
+ marketplaceEntryCarriesVersion: true,
158
+ knownHostArtifacts: Object.freeze(['.in_use']),
159
+ schemaRequiredFields: Object.freeze(['plugin', 'marketplace', 'entrySkill']),
160
+ skillRendering: Object.freeze({ mode: 'verbatim', preamble: null, placeholder: '${CLAUDE_PLUGIN_ROOT}' }),
161
+ buildAdapter: Object.freeze({
162
+ pluginDirName: '.claude-plugin',
163
+ templateFileName: 'plugin.json',
164
+ marketplaceFileName: 'marketplace.json',
165
+ hasMarketplace: true,
166
+ }),
167
+ strategy: Object.freeze({
168
+ parseListOutput: claudeParseListOutput,
169
+ extractInstallPath: claudeExtractInstallPath,
170
+ extractListIdentity: claudeExtractListIdentity,
171
+ crossValidateListEntry: null,
172
+ buildManualRequirement: null,
173
+ readManifest: null,
174
+ }),
175
+ });
176
+
177
+ const CODEX = Object.freeze({
178
+ id: 'codex',
179
+ distributionType: 'codex-plugin',
180
+ actionType: 'codex-marketplace-install',
181
+ adapter: 'plugin-marketplace',
182
+ automatable: true,
183
+ cli: Object.freeze({
184
+ binary: 'codex',
185
+ marketplaceAdd: (repo, ref) => ['plugin', 'marketplace', 'add', repo, '--ref', ref, '--json'],
186
+ install: (plugin, marketplace) => ['plugin', 'add', `${plugin}@${marketplace}`, '--json'],
187
+ list: () => ['plugin', 'list', '--json'],
188
+ }),
189
+ jsonProtocol: Object.freeze({
190
+ listOutput: 'installed-object',
191
+ installPathSource: 'install-output',
192
+ // codex emits validated JSON for both marketplace add and plugin add.
193
+ marketplaceAddOutput: 'json',
194
+ pluginInstallOutput: 'json',
195
+ }),
196
+ isolationEnv: (home) => ({ HOME: home, CODEX_HOME: home }),
197
+ // execute pre-creates the codex state dir under the isolated HOME.
198
+ isolationSubdirs: Object.freeze(['.codex']),
199
+ manifestPaths: Object.freeze({
200
+ plugin: '.codex-plugin/plugin.json',
201
+ marketplace: '.agents/plugins/marketplace.json',
202
+ }),
203
+ marketplaceSourceForm: 'local-path-object',
204
+ // Codex keeps the authoritative version in .codex-plugin/plugin.json; the
205
+ // marketplace entry version is not bound to the action version.
206
+ marketplaceEntryCarriesVersion: false,
207
+ knownHostArtifacts: Object.freeze(['.git', '.codex-plugin/migrated-command-skills']),
208
+ schemaRequiredFields: Object.freeze(['plugin', 'marketplace', 'entrySkill']),
209
+ skillRendering: Object.freeze({ mode: 'substitute', preamble: 'codex', placeholder: '${CLAUDE_PLUGIN_ROOT}' }),
210
+ buildAdapter: Object.freeze({
211
+ pluginDirName: '.codex-plugin',
212
+ templateFileName: 'plugin.json',
213
+ marketplaceFileName: null,
214
+ hasMarketplace: false,
215
+ }),
216
+ strategy: Object.freeze({
217
+ parseListOutput: codexParseListOutput,
218
+ extractInstallPath: codexExtractInstallPath,
219
+ extractListIdentity: codexExtractListIdentity,
220
+ crossValidateListEntry: codexCrossValidateListEntry,
221
+ buildManualRequirement: null,
222
+ readManifest: null,
223
+ }),
224
+ });
225
+
226
+ const KIMI = Object.freeze({
227
+ id: 'kimi',
228
+ distributionType: 'kimi-plugin',
229
+ actionType: 'kimi-marketplace-install',
230
+ adapter: 'plugin-marketplace',
231
+ automatable: false,
232
+ cli: null,
233
+ jsonProtocol: Object.freeze({
234
+ listOutput: null,
235
+ installPathSource: null,
236
+ marketplaceAddOutput: null,
237
+ pluginInstallOutput: null,
238
+ }),
239
+ isolationEnv: (home) => ({ HOME: home, KIMI_CODE_HOME: home }),
240
+ // kimi never execs a CLI; its stable home is created by the
241
+ // manual-requirement strategy under the attestation authority.
242
+ isolationSubdirs: Object.freeze([]),
243
+ manifestPaths: Object.freeze({
244
+ // kimi.plugin.json takes precedence over .kimi-plugin/plugin.json
245
+ // (strategy.readManifest / readKimiManifest in ./kimi.mjs).
246
+ pluginCandidates: KIMI_MANIFEST_CANDIDATES,
247
+ marketplace: null,
248
+ }),
249
+ marketplaceSourceForm: null,
250
+ // kimi has no marketplace at all, so the entry-version binding does not
251
+ // apply (null = N/A, never consulted: kimi preflight reads its manifest via
252
+ // strategy.readManifest).
253
+ marketplaceEntryCarriesVersion: null,
254
+ knownHostArtifacts: Object.freeze(['.git']),
255
+ schemaRequiredFields: Object.freeze(['plugin', 'entrySkill']),
256
+ skillRendering: Object.freeze({ mode: 'substitute', preamble: 'kimi', placeholder: '${CLAUDE_PLUGIN_ROOT}' }),
257
+ buildAdapter: Object.freeze({
258
+ pluginDirName: '.kimi-plugin',
259
+ templateFileName: 'plugin.json',
260
+ marketplaceFileName: null,
261
+ hasMarketplace: false,
262
+ }),
263
+ strategy: Object.freeze({
264
+ parseListOutput: null,
265
+ extractInstallPath: null,
266
+ extractListIdentity: null,
267
+ crossValidateListEntry: null,
268
+ // kimi's side-effecting manual-requirement builder and manifest reader
269
+ // live in ./kimi.mjs (§4.2 per-platform-module option).
270
+ buildManualRequirement: executeKimiManualRequirement,
271
+ readManifest: readKimiManifest,
272
+ }),
273
+ });
274
+
275
+ /** Ordered platform registry. Order is significant (claude, codex, kimi). */
276
+ export const PLATFORMS = Object.freeze([CLAUDE, CODEX, KIMI]);
277
+
278
+ const VALID_DISTRIBUTION_TYPES = new Set(['claude-plugin', 'codex-plugin', 'kimi-plugin']);
279
+ const VALID_ACTION_TYPES = new Set(['claude-marketplace-install', 'codex-marketplace-install', 'kimi-marketplace-install']);
280
+ const VALID_SOURCE_FORMS = new Set(['string', 'local-path-object', null]);
281
+ const VALID_LIST_OUTPUTS = new Set(['array', 'installed-object', null]);
282
+ const VALID_CLI_OUTPUTS = new Set(['json', null]);
283
+ const VALID_ENTRY_VERSION_BINDING = new Set([true, false, null]);
284
+
285
+ /**
286
+ * Look up a platform descriptor by id.
287
+ *
288
+ * @param {string} id
289
+ * @returns {object}
290
+ */
291
+ export function getPlatform(id) {
292
+ const platform = PLATFORMS.find((p) => p.id === id);
293
+ if (!platform) throw new Error(`unknown platform: ${id}`);
294
+ return platform;
295
+ }
296
+
297
+ /**
298
+ * Validate a registry (defaults to the module registry). Throws on the first
299
+ * malformed description so a registry typo fails at import time.
300
+ *
301
+ * @param {object[]} [registry] - Platform descriptors to validate.
302
+ */
303
+ export function assertRegistry(registry = PLATFORMS) {
304
+ const seen = new Set();
305
+ for (const platform of registry) {
306
+ const label = platform?.id ?? '<missing id>';
307
+ if (typeof platform?.id !== 'string' || platform.id.length === 0) {
308
+ throw new Error('platform registry: every platform needs a string id');
309
+ }
310
+ if (seen.has(platform.id)) {
311
+ throw new Error(`platform registry: ids must be unique, found duplicate "${platform.id}"`);
312
+ }
313
+ seen.add(platform.id);
314
+
315
+ if (!VALID_DISTRIBUTION_TYPES.has(platform.distributionType)) {
316
+ throw new Error(`platform registry: ${label} has illegal distributionType "${platform.distributionType}"`);
317
+ }
318
+ if (!VALID_ACTION_TYPES.has(platform.actionType)) {
319
+ throw new Error(`platform registry: ${label} has illegal actionType "${platform.actionType}"`);
320
+ }
321
+ if (typeof platform.adapter !== 'string' || platform.adapter.length === 0) {
322
+ throw new Error(`platform registry: ${label} needs a non-empty adapter id`);
323
+ }
324
+ if (!VALID_ENTRY_VERSION_BINDING.has(platform.marketplaceEntryCarriesVersion)) {
325
+ throw new Error(`platform registry: ${label} has illegal marketplaceEntryCarriesVersion "${platform.marketplaceEntryCarriesVersion}"`);
326
+ }
327
+ if (typeof platform.automatable !== 'boolean') {
328
+ throw new Error(`platform registry: ${label} automatable must be boolean`);
329
+ }
330
+ if (typeof platform.isolationEnv !== 'function') {
331
+ throw new Error(`platform registry: ${label} isolationEnv must be a function`);
332
+ }
333
+ if (!VALID_SOURCE_FORMS.has(platform.marketplaceSourceForm)) {
334
+ throw new Error(`platform registry: ${label} has illegal marketplaceSourceForm "${platform.marketplaceSourceForm}"`);
335
+ }
336
+ if (!Array.isArray(platform.knownHostArtifacts)) {
337
+ throw new Error(`platform registry: ${label} knownHostArtifacts must be an array`);
338
+ }
339
+ if (!Array.isArray(platform.schemaRequiredFields)) {
340
+ throw new Error(`platform registry: ${label} schemaRequiredFields must be an array`);
341
+ }
342
+ if (!platform.manifestPaths || typeof platform.manifestPaths !== 'object') {
343
+ throw new Error(`platform registry: ${label} manifestPaths must be an object`);
344
+ }
345
+ if (!platform.buildAdapter || typeof platform.buildAdapter !== 'object') {
346
+ throw new Error(`platform registry: ${label} buildAdapter must be an object`);
347
+ }
348
+ if (!VALID_LIST_OUTPUTS.has(platform.jsonProtocol?.listOutput)) {
349
+ throw new Error(`platform registry: ${label} has illegal jsonProtocol.listOutput "${platform.jsonProtocol?.listOutput}"`);
350
+ }
351
+ if (!VALID_CLI_OUTPUTS.has(platform.jsonProtocol?.marketplaceAddOutput)) {
352
+ throw new Error(`platform registry: ${label} has illegal jsonProtocol.marketplaceAddOutput "${platform.jsonProtocol?.marketplaceAddOutput}"`);
353
+ }
354
+ if (!VALID_CLI_OUTPUTS.has(platform.jsonProtocol?.pluginInstallOutput)) {
355
+ throw new Error(`platform registry: ${label} has illegal jsonProtocol.pluginInstallOutput "${platform.jsonProtocol?.pluginInstallOutput}"`);
356
+ }
357
+ if (!Array.isArray(platform.isolationSubdirs)) {
358
+ throw new Error(`platform registry: ${label} isolationSubdirs must be an array`);
359
+ }
360
+
361
+ if (platform.automatable) {
362
+ if (!platform.cli || typeof platform.cli.marketplaceAdd !== 'function'
363
+ || typeof platform.cli.install !== 'function' || typeof platform.cli.list !== 'function') {
364
+ throw new Error(`platform registry: automatable platform ${label} needs cli template functions`);
365
+ }
366
+ if (typeof platform.cli.binary !== 'string' || platform.cli.binary.length === 0) {
367
+ throw new Error(`platform registry: automatable platform ${label} needs a non-empty cli.binary executable name`);
368
+ }
369
+ if (!platform.strategy || typeof platform.strategy.parseListOutput !== 'function') {
370
+ throw new Error(`platform registry: automatable platform ${label} needs strategy.parseListOutput`);
371
+ }
372
+ if (typeof platform.strategy.extractInstallPath !== 'function') {
373
+ throw new Error(`platform registry: automatable platform ${label} needs strategy.extractInstallPath`);
374
+ }
375
+ } else {
376
+ if (platform.cli !== null) {
377
+ throw new Error(`platform registry: non-automatable platform ${label} must have cli === null`);
378
+ }
379
+ // The manual-requirement path IS the execute for a non-automatable
380
+ // platform; without it (and its manifest reader) the checkpoint has no
381
+ // execute behaviour at all.
382
+ if (!platform.strategy || typeof platform.strategy.buildManualRequirement !== 'function') {
383
+ throw new Error(`platform registry: non-automatable platform ${label} needs strategy.buildManualRequirement`);
384
+ }
385
+ if (typeof platform.strategy.readManifest !== 'function') {
386
+ throw new Error(`platform registry: non-automatable platform ${label} needs strategy.readManifest`);
387
+ }
388
+ }
389
+ }
390
+ }
391
+
392
+ // Self-validate at import time: a malformed registry never reaches runtime.
393
+ assertRegistry();
@@ -2,10 +2,12 @@
2
2
  * Pure producer: build adapters from skills and plugin templates.
3
3
  *
4
4
  * Reads skill metadata from skills-src/ and plugin.json templates,
5
- * generates self-contained adapter directories for each platform (claude, codex, kimi).
5
+ * generates self-contained adapter directories for each platform
6
+ * (claude, codex, kimi, and the build-only workbuddy adapter).
6
7
  *
7
8
  * Each adapter root contains:
8
- * - Plugin manifest (.claude-plugin/, .codex-plugin/, or .kimi-plugin/)
9
+ * - Plugin manifest (.claude-plugin/, .codex-plugin/, .kimi-plugin/, or
10
+ * .codebuddy-plugin/)
9
11
  * - Skills with host-specific root resolution
10
12
  * - bin/release-skill.mjs (wrapper) + bin/release-skill.bundle.mjs (self-contained bundle)
11
13
  * - schemas/ (JSON Schema files)
@@ -22,6 +24,7 @@ import { join, dirname, relative, isAbsolute, sep, resolve, posix as pathPosix }
22
24
  import { createHash } from 'node:crypto';
23
25
 
24
26
  import { canonicalJson, sha256Hex } from '../core/digest.mjs';
27
+ import { PLATFORMS as PLATFORM_REGISTRY } from '../platforms/registry.mjs';
25
28
 
26
29
  /**
27
30
  * Assert that a resolved real path is contained within the allowed root.
@@ -190,28 +193,43 @@ export function computeBuildAdaptersDigest(sourceBytes) {
190
193
  return `sha256:${h.digest('hex')}`;
191
194
  }
192
195
 
193
- // Platform definitions
194
- const PLATFORMS = [
195
- {
196
- name: 'claude',
197
- pluginDirName: '.claude-plugin',
198
- templateFileName: 'plugin.json',
199
- marketplaceFileName: 'marketplace.json',
200
- hasMarketplace: true,
201
- },
202
- {
203
- name: 'codex',
204
- pluginDirName: '.codex-plugin',
205
- templateFileName: 'plugin.json',
206
- hasMarketplace: false,
207
- },
208
- {
209
- name: 'kimi',
210
- pluginDirName: '.kimi-plugin',
196
+ // Platform definitions, derived from the platform registry (the single source
197
+ // of platform knowledge). The build shape mirrors the legacy inline table
198
+ // byte-for-byte: marketplaceFileName is present only when the platform
199
+ // co-locates a marketplace manifest in its plugin dir (claude).
200
+ const REGISTRY_PLATFORMS = PLATFORM_REGISTRY.map((platform) => ({
201
+ name: platform.id,
202
+ pluginDirName: platform.buildAdapter.pluginDirName,
203
+ templateFileName: platform.buildAdapter.templateFileName,
204
+ ...(platform.buildAdapter.hasMarketplace
205
+ ? { marketplaceFileName: platform.buildAdapter.marketplaceFileName }
206
+ : {}),
207
+ hasMarketplace: platform.buildAdapter.hasMarketplace,
208
+ }));
209
+
210
+ /**
211
+ * Build-only distribution adapters: generated, self-contained plugin trees
212
+ * that are NOT wired into the automated publish/reconcile/verify pipeline —
213
+ * no registry PLATFORMS membership, no distributionType/actionType in the
214
+ * plan schemas, no automated install checkpoint. A host joins the pipeline
215
+ * only with a full registry descriptor (strategy, CLI protocol, schema enum);
216
+ * until then a build-only entry lets the build emit an installable adapter
217
+ * tree without fabricating an unverified install protocol.
218
+ *
219
+ * workbuddy: CodeBuddy/WorkBuddy plugin (official plugin reference:
220
+ * `.codebuddy-plugin/plugin.json`, components at the plugin root,
221
+ * `${CODEBUDDY_PLUGIN_ROOT}` expanded inline in skill content).
222
+ */
223
+ export const BUILD_ONLY_ADAPTERS = Object.freeze([
224
+ Object.freeze({
225
+ name: 'workbuddy',
226
+ pluginDirName: '.codebuddy-plugin',
211
227
  templateFileName: 'plugin.json',
212
228
  hasMarketplace: false,
213
- },
214
- ];
229
+ }),
230
+ ]);
231
+
232
+ export const PLATFORMS = Object.freeze([...REGISTRY_PLATFORMS, ...BUILD_ONLY_ADAPTERS]);
215
233
 
216
234
  const REQUIRED_SCHEMA_FILES = Object.freeze([
217
235
  'approval-record.schema.json',
@@ -324,14 +342,22 @@ const KIMI_PREAMBLE = `\
324
342
  * - Codex: replaces the Claude-only path with $RELEASE_SKILL_ENTRY,
325
343
  * and prepends the path resolution protocol preamble.
326
344
  * - Kimi: same substitution as Codex, with the Kimi Code preamble.
345
+ * - WorkBuddy: pure variable substitution (${CLAUDE_PLUGIN_ROOT} ->
346
+ * ${CODEBUDDY_PLUGIN_ROOT}); CodeBuddy expands the variable inline in
347
+ * skill content (official plugin reference), the same mechanism Claude
348
+ * uses, so no entry-resolution preamble is needed.
327
349
  *
328
350
  * @param {string} content - Canonical SKILL.md content.
329
- * @param {string} platformName - 'claude', 'codex', or 'kimi'.
351
+ * @param {string} platformName - 'claude', 'codex', 'kimi', or 'workbuddy'.
330
352
  * @returns {string}
331
353
  */
332
354
  function renderSkillForPlatform(content, platformName) {
333
355
  if (platformName === 'claude') return content;
334
356
 
357
+ if (platformName === 'workbuddy') {
358
+ return content.replaceAll('${CLAUDE_PLUGIN_ROOT}', '${CODEBUDDY_PLUGIN_ROOT}');
359
+ }
360
+
335
361
  if (platformName === 'codex' || platformName === 'kimi') {
336
362
  const hostLabel = platformName === 'codex' ? 'Codex' : 'Kimi';
337
363
  const preamble = platformName === 'codex' ? CODEX_PREAMBLE : KIMI_PREAMBLE;