release-skill 0.5.1 → 0.6.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 (94) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codebuddy-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +13 -0
  7. package/INSTALL.md +2 -2
  8. package/INSTALL.zh-CN.md +2 -2
  9. package/README.md +14 -14
  10. package/README.zh-CN.md +15 -15
  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 +3226 -565
  14. package/adapters/claude/schemas/.render-manifest.json +6 -6
  15. package/adapters/claude/schemas/release-plan.schema.json +401 -2
  16. package/adapters/claude/schemas/release-project.schema.json +291 -0
  17. package/adapters/claude/schemas/release-run.schema.json +59 -6
  18. package/adapters/claude/skills/release-config/SKILL.md +140 -0
  19. package/adapters/claude/skills/release-docs/SKILL.md +106 -0
  20. package/adapters/claude/skills/release-help/SKILL.md +39 -0
  21. package/adapters/claude/skills/release-marketplace/SKILL.md +147 -0
  22. package/adapters/claude/skills/release-publish/SKILL.md +25 -6
  23. package/adapters/claude/skills/release-verify/SKILL.md +10 -0
  24. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  25. package/adapters/codex/bin/release-skill.bundle.mjs +3226 -565
  26. package/adapters/codex/schemas/.render-manifest.json +6 -6
  27. package/adapters/codex/schemas/release-plan.schema.json +401 -2
  28. package/adapters/codex/schemas/release-project.schema.json +291 -0
  29. package/adapters/codex/schemas/release-run.schema.json +59 -6
  30. package/adapters/codex/skills/release-config/SKILL.md +147 -0
  31. package/adapters/codex/skills/release-docs/SKILL.md +113 -0
  32. package/adapters/codex/skills/release-help/SKILL.md +39 -0
  33. package/adapters/codex/skills/release-marketplace/SKILL.md +154 -0
  34. package/adapters/codex/skills/release-publish/SKILL.md +25 -6
  35. package/adapters/codex/skills/release-verify/SKILL.md +10 -0
  36. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  37. package/adapters/kimi/bin/release-skill.bundle.mjs +3226 -565
  38. package/adapters/kimi/schemas/.render-manifest.json +6 -6
  39. package/adapters/kimi/schemas/release-plan.schema.json +401 -2
  40. package/adapters/kimi/schemas/release-project.schema.json +291 -0
  41. package/adapters/kimi/schemas/release-run.schema.json +59 -6
  42. package/adapters/kimi/skills/release-config/SKILL.md +147 -0
  43. package/adapters/kimi/skills/release-docs/SKILL.md +113 -0
  44. package/adapters/kimi/skills/release-help/SKILL.md +39 -0
  45. package/adapters/kimi/skills/release-marketplace/SKILL.md +154 -0
  46. package/adapters/kimi/skills/release-publish/SKILL.md +25 -6
  47. package/adapters/kimi/skills/release-verify/SKILL.md +10 -0
  48. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  49. package/adapters/workbuddy/bin/release-skill.bundle.mjs +3226 -565
  50. package/adapters/workbuddy/schemas/.render-manifest.json +6 -6
  51. package/adapters/workbuddy/schemas/release-plan.schema.json +401 -2
  52. package/adapters/workbuddy/schemas/release-project.schema.json +291 -0
  53. package/adapters/workbuddy/schemas/release-run.schema.json +59 -6
  54. package/adapters/workbuddy/skills/release-config/SKILL.md +140 -0
  55. package/adapters/workbuddy/skills/release-docs/SKILL.md +106 -0
  56. package/adapters/workbuddy/skills/release-help/SKILL.md +39 -0
  57. package/adapters/workbuddy/skills/release-marketplace/SKILL.md +147 -0
  58. package/adapters/workbuddy/skills/release-publish/SKILL.md +25 -6
  59. package/adapters/workbuddy/skills/release-verify/SKILL.md +10 -0
  60. package/bin/release-skill-cli.mjs +235 -7
  61. package/bin/release-skill.bundle.mjs +3226 -565
  62. package/package.json +3 -2
  63. package/platform-manifest.json +359 -0
  64. package/references/.render-manifest.json +9 -9
  65. package/references/02-project-config.md +35 -0
  66. package/references/05-evidence-and-errors.md +44 -0
  67. package/references/06-adapter-contract.md +13 -0
  68. package/schemas/.render-manifest.json +6 -6
  69. package/schemas/release-plan.schema.json +401 -2
  70. package/schemas/release-project.schema.json +291 -0
  71. package/schemas/release-run.schema.json +59 -6
  72. package/skills/release-config/SKILL.md +140 -0
  73. package/skills/release-docs/SKILL.md +106 -0
  74. package/skills/release-help/SKILL.md +39 -0
  75. package/skills/release-marketplace/SKILL.md +147 -0
  76. package/skills/release-publish/SKILL.md +25 -6
  77. package/skills/release-verify/SKILL.md +10 -0
  78. package/skills-src/release-config/SKILL.md +140 -0
  79. package/skills-src/release-docs/SKILL.md +106 -0
  80. package/skills-src/release-help/SKILL.md +39 -0
  81. package/skills-src/release-marketplace/SKILL.md +147 -0
  82. package/skills-src/release-publish/SKILL.md +25 -6
  83. package/skills-src/release-verify/SKILL.md +10 -0
  84. package/src/adapters/contract.mjs +5 -0
  85. package/src/adapters/distribute-git.mjs +625 -0
  86. package/src/commands/distribute.mjs +1078 -0
  87. package/src/commands/lineage.mjs +616 -0
  88. package/src/commands/prepare.mjs +335 -76
  89. package/src/commands/route.mjs +686 -0
  90. package/src/commands/ship.mjs +45 -1
  91. package/src/commands/verify.mjs +176 -0
  92. package/src/core/baseline-advance.mjs +185 -0
  93. package/src/core/checkpoints.mjs +25 -0
  94. package/src/core/postpublish.mjs +315 -0
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Post-publish distribution declaration validation (R1/R2/R3 semantics source).
3
+ *
4
+ * `validatePostPublishDeclaration` is the runtime authority for the per-unit
5
+ * `postPublish` block, shared by prepare (freeze time) and distribute
6
+ * (execution time). The JSON schema is the first gate; this module re-checks
7
+ * every security-critical rule so plans frozen by older schema versions
8
+ * cannot smuggle unsafe declarations through:
9
+ *
10
+ * - hooks are executable + argument arrays only (never shell strings);
11
+ * - envAllowlist entries matching the secret-ish denylist
12
+ * /TOKEN|SECRET|PASSWORD|PASSPHRASE|API_KEY|CREDENTIAL/i fail closed;
13
+ * - dependsOn must reference an existing payload-mirror target and the
14
+ * dependency graph must be acyclic;
15
+ * - remoteUrl/branch/commitIdentity are re-checked for git-argument safety;
16
+ * - commitIdentity is mandatory whenever targets exist;
17
+ * - marketplace is mandatory for marketplace-index targets and forbidden on
18
+ * payload-mirror targets.
19
+ *
20
+ * Every violation throws ReleaseError(GATE_FAILED): the declaration layer is
21
+ * fail-closed and never repairs declarations in place.
22
+ *
23
+ * @module core/postpublish
24
+ */
25
+
26
+ import { ReleaseError, GATE_FAILED } from './errors.mjs';
27
+
28
+ /** Secret-ish environment variable denylist (R3 credential hygiene). */
29
+ export const ENV_ALLOWLIST_DENYLIST = /TOKEN|SECRET|PASSWORD|PASSPHRASE|API_KEY|CREDENTIAL/i;
30
+
31
+ /** Shape every envAllowlist key must satisfy (before the denylist check). */
32
+ const ENV_KEY_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
33
+
34
+ /** Safe target/step id and step name pattern. */
35
+ const SAFE_ID_RE = /^[a-z0-9][a-z0-9._-]*$/;
36
+
37
+ /** Remote URL pattern: http(s) or file (test transport) and .git-suffixed. */
38
+ const REMOTE_URL_RE = /^(?:https?|file):\/\/.+\.git$/;
39
+
40
+ /** Branch pattern (leading alphanumeric blocks option-like names). */
41
+ const BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
42
+
43
+ /** Frozen payload source binding: payload only ever comes from the tag worktree. */
44
+ export const PAYLOAD_SOURCE_TAG_WORKTREE = 'tag-worktree';
45
+
46
+ function fail(message, details = {}) {
47
+ throw new ReleaseError(GATE_FAILED, `postPublish declaration invalid: ${message}`, details);
48
+ }
49
+
50
+ function assertNoControlChars(label, value) {
51
+ if (/[\x00-\x1f\x7f]/.test(value)) {
52
+ fail(`${label} contains control characters`, { label });
53
+ }
54
+ }
55
+
56
+ function validateHookCommand(where, hook) {
57
+ if (!hook || typeof hook !== 'object' || Array.isArray(hook)) {
58
+ fail(`${where} must be a non-null object`);
59
+ }
60
+ if (!Array.isArray(hook.command) || hook.command.length === 0) {
61
+ fail(`${where}.command must be a non-empty array (shell strings are never accepted)`);
62
+ }
63
+ for (const element of hook.command) {
64
+ if (typeof element !== 'string' || element.length === 0) {
65
+ fail(`${where}.command must contain only non-empty strings`);
66
+ }
67
+ assertNoControlChars(`${where}.command`, element);
68
+ if (element.startsWith('-') && element === hook.command[0]) {
69
+ fail(`${where}.command executable must not start with "-"`, { executable: element });
70
+ }
71
+ }
72
+ if (hook.cwd !== undefined) {
73
+ if (typeof hook.cwd !== 'string' || hook.cwd.length === 0) {
74
+ fail(`${where}.cwd must be a non-empty string when provided`);
75
+ }
76
+ if (hook.cwd.startsWith('/') || hook.cwd.startsWith('./') || hook.cwd.includes('..')) {
77
+ fail(`${where}.cwd must be a relative path inside the execution root`, { cwd: hook.cwd });
78
+ }
79
+ }
80
+ if (hook.timeoutMs !== undefined) {
81
+ if (!Number.isInteger(hook.timeoutMs) || hook.timeoutMs < 1000 || hook.timeoutMs > 7200000) {
82
+ fail(`${where}.timeoutMs must be an integer in [1000, 7200000]`, { timeoutMs: hook.timeoutMs });
83
+ }
84
+ }
85
+ if (hook.envAllowlist !== undefined) {
86
+ if (!Array.isArray(hook.envAllowlist)) {
87
+ fail(`${where}.envAllowlist must be an array`);
88
+ }
89
+ const seen = new Set();
90
+ for (const key of hook.envAllowlist) {
91
+ if (typeof key !== 'string' || !ENV_KEY_PATTERN.test(key)) {
92
+ fail(`${where}.envAllowlist key ${JSON.stringify(key)} must be an uppercase [A-Z_][A-Z0-9_]* identifier`);
93
+ }
94
+ if (ENV_ALLOWLIST_DENYLIST.test(key)) {
95
+ fail(
96
+ `${where}.envAllowlist key "${key}" matches the secret-ish denylist (TOKEN/SECRET/PASSWORD/PASSPHRASE/API_KEY/CREDENTIAL); distribute never reads or forwards credentials`,
97
+ { key },
98
+ );
99
+ }
100
+ if (seen.has(key)) {
101
+ fail(`${where}.envAllowlist contains duplicate key "${key}"`);
102
+ }
103
+ seen.add(key);
104
+ }
105
+ }
106
+ }
107
+
108
+ function validateTarget(target, index) {
109
+ const where = `targets[${index}]`;
110
+ if (!target || typeof target !== 'object' || Array.isArray(target)) {
111
+ fail(`${where} must be a non-null object`);
112
+ }
113
+ if (typeof target.id !== 'string' || !SAFE_ID_RE.test(target.id)) {
114
+ fail(`${where}.id must match /^[a-z0-9][a-z0-9._-]*$/`, { id: target.id });
115
+ }
116
+ if (target.kind !== 'payload-mirror' && target.kind !== 'marketplace-index') {
117
+ fail(`${where}.kind must be "payload-mirror" or "marketplace-index"`, { kind: target.kind });
118
+ }
119
+ if (typeof target.remoteUrl !== 'string' || !REMOTE_URL_RE.test(target.remoteUrl)) {
120
+ fail(`${where}.remoteUrl must be an http(s)/file URL ending in .git`, { remoteUrl: target.remoteUrl });
121
+ }
122
+ assertNoControlChars(`${where}.remoteUrl`, target.remoteUrl);
123
+ if (target.visibility !== 'internal' && target.visibility !== 'public') {
124
+ fail(`${where}.visibility must be "internal" or "public"`, { visibility: target.visibility });
125
+ }
126
+ if (typeof target.branch !== 'string' || !BRANCH_RE.test(target.branch)) {
127
+ fail(`${where}.branch is not a safe Git branch name`, { branch: target.branch });
128
+ }
129
+ if (target.branch.includes('..') || target.branch.endsWith('.') || target.branch.endsWith('.lock')) {
130
+ fail(`${where}.branch is not a safe Git branch name`, { branch: target.branch });
131
+ }
132
+ if (target.dependsOn !== undefined) {
133
+ if (typeof target.dependsOn !== 'string' || !SAFE_ID_RE.test(target.dependsOn)) {
134
+ fail(`${where}.dependsOn must match /^[a-z0-9][a-z0-9._-]*$/`, { dependsOn: target.dependsOn });
135
+ }
136
+ if (target.dependsOn === target.id) {
137
+ fail(`${where}.dependsOn references itself; dependency cycles are rejected`);
138
+ }
139
+ }
140
+ if (target.kind === 'marketplace-index') {
141
+ if (!target.marketplace || typeof target.marketplace !== 'object') {
142
+ fail(`${where} is marketplace-index and must carry a marketplace block`);
143
+ }
144
+ const { form, name, owner } = target.marketplace;
145
+ if (form !== 'github' && form !== 'url') {
146
+ fail(`${where}.marketplace.form must be "github" or "url"`, { form });
147
+ }
148
+ if (typeof name !== 'string' || name.length === 0) {
149
+ fail(`${where}.marketplace.name must be a non-empty string`);
150
+ }
151
+ if (typeof owner !== 'string' || owner.length === 0) {
152
+ fail(`${where}.marketplace.owner must be a non-empty string`);
153
+ }
154
+ if (target.marketplace.sourceRepo !== undefined
155
+ && (typeof target.marketplace.sourceRepo !== 'string'
156
+ || !/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(target.marketplace.sourceRepo))) {
157
+ fail(`${where}.marketplace.sourceRepo must be owner/repo when provided`);
158
+ }
159
+ } else if (target.marketplace !== undefined) {
160
+ fail(`${where} is payload-mirror and must not carry a marketplace block`);
161
+ }
162
+ if (target.staticFiles !== undefined) {
163
+ if (!Array.isArray(target.staticFiles)) {
164
+ fail(`${where}.staticFiles must be an array`);
165
+ }
166
+ for (const [fileIndex, file] of target.staticFiles.entries()) {
167
+ for (const field of ['from', 'to']) {
168
+ const value = file?.[field];
169
+ if (typeof value !== 'string' || value.length === 0) {
170
+ fail(`${where}.staticFiles[${fileIndex}].${field} must be a non-empty string`);
171
+ }
172
+ if (value.startsWith('/') || value.startsWith('./') || value === '.'
173
+ || value.includes('..') || value.includes('\\') || value.includes(':')) {
174
+ fail(`${where}.staticFiles[${fileIndex}].${field} is not a safe relative path`, { value });
175
+ }
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Validate a per-unit postPublish declaration.
183
+ *
184
+ * @param {object} postPublish - The declaration (already schema-checked by
185
+ * the caller's config layer; this function is the runtime re-check).
186
+ * @param {object} [options]
187
+ * @param {string} [options.unitId] - Unit id for error context.
188
+ * @returns {object} The validated declaration (unmodified).
189
+ * @throws {ReleaseError} GATE_FAILED on any violation (fail-closed).
190
+ */
191
+ export function validatePostPublishDeclaration(postPublish, options = {}) {
192
+ const unitLabel = options.unitId ? `unit "${options.unitId}" ` : '';
193
+ if (!postPublish || typeof postPublish !== 'object' || Array.isArray(postPublish)) {
194
+ fail(`${unitLabel}postPublish must be a non-null object`);
195
+ }
196
+
197
+ validateHookCommand(`${unitLabel}materialize`, postPublish.materialize);
198
+ if (typeof postPublish.materialize.outputMarker !== 'string'
199
+ || postPublish.materialize.outputMarker.length === 0) {
200
+ fail(`${unitLabel}materialize.outputMarker must be a non-empty string`);
201
+ }
202
+ assertNoControlChars(`${unitLabel}materialize.outputMarker`, postPublish.materialize.outputMarker);
203
+ if (postPublish.materialize.requireReport !== undefined) {
204
+ const { parse, equals } = postPublish.materialize.requireReport ?? {};
205
+ if (parse !== 'stdout-first-json') {
206
+ fail(`${unitLabel}materialize.requireReport.parse must be "stdout-first-json"`);
207
+ }
208
+ if (equals !== undefined && (typeof equals !== 'object' || Array.isArray(equals) || equals === null)) {
209
+ fail(`${unitLabel}materialize.requireReport.equals must be a plain object`);
210
+ }
211
+ }
212
+
213
+ if (!Array.isArray(postPublish.targets) || postPublish.targets.length === 0) {
214
+ fail(`${unitLabel}targets must be a non-empty array`);
215
+ }
216
+ const ids = new Set();
217
+ postPublish.targets.forEach((target, index) => {
218
+ validateTarget(target, index);
219
+ if (ids.has(target.id)) {
220
+ fail(`${unitLabel}duplicate target id "${target.id}"`);
221
+ }
222
+ ids.add(target.id);
223
+ });
224
+
225
+ // dependsOn references must exist and point at payload-mirror targets.
226
+ const byId = new Map(postPublish.targets.map((target) => [target.id, target]));
227
+ for (const target of postPublish.targets) {
228
+ if (target.dependsOn === undefined) continue;
229
+ const dependency = byId.get(target.dependsOn);
230
+ if (!dependency) {
231
+ fail(`${unitLabel}target "${target.id}" dependsOn unknown target "${target.dependsOn}"`);
232
+ }
233
+ if (dependency.kind !== 'payload-mirror') {
234
+ fail(`${unitLabel}target "${target.id}" dependsOn "${target.dependsOn}" which is not a payload-mirror target`);
235
+ }
236
+ }
237
+
238
+ // commitIdentity is mandatory whenever targets exist (always, here).
239
+ const identity = postPublish.commitIdentity;
240
+ if (!identity || typeof identity !== 'object' || Array.isArray(identity)) {
241
+ fail(`${unitLabel}commitIdentity is required when targets are declared`);
242
+ }
243
+ for (const field of ['name', 'email']) {
244
+ if (typeof identity[field] !== 'string' || identity[field].length === 0) {
245
+ fail(`${unitLabel}commitIdentity.${field} must be a non-empty string`);
246
+ }
247
+ assertNoControlChars(`${unitLabel}commitIdentity.${field}`, identity[field]);
248
+ if (identity[field].startsWith('-')) {
249
+ fail(`${unitLabel}commitIdentity.${field} must not start with "-"`, { value: identity[field] });
250
+ }
251
+ }
252
+
253
+ if (postPublish.steps !== undefined) {
254
+ if (!Array.isArray(postPublish.steps)) {
255
+ fail(`${unitLabel}steps must be an array`);
256
+ }
257
+ const stepNames = new Set();
258
+ postPublish.steps.forEach((step, index) => {
259
+ const where = `${unitLabel}steps[${index}]`;
260
+ validateHookCommand(where, step);
261
+ if (typeof step.name !== 'string' || !SAFE_ID_RE.test(step.name)) {
262
+ fail(`${where}.name must match /^[a-z0-9][a-z0-9._-]*$/`, { name: step?.name });
263
+ }
264
+ if (stepNames.has(step.name)) {
265
+ fail(`${unitLabel}duplicate step name "${step.name}"`);
266
+ }
267
+ stepNames.add(step.name);
268
+ });
269
+ }
270
+
271
+ if (postPublish.assertMainVersionAhead !== undefined
272
+ && typeof postPublish.assertMainVersionAhead !== 'boolean') {
273
+ fail(`${unitLabel}assertMainVersionAhead must be a boolean`);
274
+ }
275
+
276
+ return postPublish;
277
+ }
278
+
279
+ /**
280
+ * Order targets so every target comes after its dependsOn target.
281
+ *
282
+ * Deterministic: among ready targets the declaration order is preserved.
283
+ * Dependency cycles and dangling references fail closed (references are also
284
+ * validated by validatePostPublishDeclaration; this guard makes ordering
285
+ * independently safe).
286
+ *
287
+ * @param {object[]} targets - Validated postPublish targets.
288
+ * @returns {object[]} Targets in execution order.
289
+ * @throws {ReleaseError} GATE_FAILED on a cycle or unknown reference.
290
+ */
291
+ export function orderTargetsByDependency(targets) {
292
+ const byId = new Map(targets.map((target) => [target.id, target]));
293
+ const ordered = [];
294
+ const placed = new Set();
295
+ while (ordered.length < targets.length) {
296
+ let progress = false;
297
+ for (const target of targets) {
298
+ if (placed.has(target.id)) continue;
299
+ if (target.dependsOn !== undefined) {
300
+ if (!byId.has(target.dependsOn)) {
301
+ fail(`target "${target.id}" dependsOn unknown target "${target.dependsOn}"`);
302
+ }
303
+ if (!placed.has(target.dependsOn)) continue;
304
+ }
305
+ ordered.push(target);
306
+ placed.add(target.id);
307
+ progress = true;
308
+ }
309
+ if (!progress) {
310
+ const pending = targets.filter((t) => !placed.has(t.id)).map((t) => t.id);
311
+ fail(`postPublish target dependency cycle detected among: ${pending.join(', ')}`);
312
+ }
313
+ }
314
+ return ordered;
315
+ }