release-skill 0.2.8 → 0.3.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 (39) 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 +36 -0
  7. package/INSTALL.md +4 -4
  8. package/INSTALL.zh-CN.md +4 -4
  9. package/README.md +38 -13
  10. package/README.zh-CN.md +36 -13
  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 +3961 -2882
  14. package/adapters/claude/skills/release-help/SKILL.md +9 -1
  15. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  16. package/adapters/codex/bin/release-skill.bundle.mjs +3961 -2882
  17. package/adapters/codex/skills/release-help/SKILL.md +9 -1
  18. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  19. package/adapters/kimi/bin/release-skill.bundle.mjs +3961 -2882
  20. package/adapters/kimi/skills/release-help/SKILL.md +9 -1
  21. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  22. package/adapters/workbuddy/bin/release-skill.bundle.mjs +3961 -2882
  23. package/adapters/workbuddy/skills/release-help/SKILL.md +9 -1
  24. package/bin/release-skill-cli.mjs +180 -4
  25. package/bin/release-skill.bundle.mjs +3961 -2882
  26. package/package.json +1 -1
  27. package/skills/release-help/SKILL.md +9 -1
  28. package/skills-src/release-help/SKILL.md +9 -1
  29. package/src/adapters/plugin-marketplace.mjs +1 -1
  30. package/src/adapters/push-snapshot.mjs +3 -1
  31. package/src/commands/assess.mjs +18 -5
  32. package/src/commands/attest.mjs +195 -0
  33. package/src/commands/hooks.mjs +46 -0
  34. package/src/commands/prepare.mjs +14 -2
  35. package/src/commands/ship.mjs +356 -0
  36. package/src/commands/verify.mjs +147 -4
  37. package/src/core/git-transport.mjs +93 -0
  38. package/src/core/public-surface.mjs +26 -0
  39. package/src/core/release-metadata.mjs +105 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "release-skill",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
4
4
  "description": "Safe preparation and frozen GitHub/npm production publishing with full happy end verification",
5
5
  "author": {
6
6
  "name": "广州市风荷科技有限公司"
@@ -12,7 +12,7 @@ description: "Discoverable entry point for release-skill: dependency and environ
12
12
  ## 职责
13
13
 
14
14
  - 依赖和环境检查:Node.js >= 22、Git 决定本地准备就绪度;npm/gh 另行决定生产依赖就绪度
15
- - 能力说明:缺少配置时走 `help → setup → assess`;已有配置的安全默认路径是 `help → assess → prepare --offline`;已有公开版本的生产闭环是显式的 `prepare --online --production → approve → publish → verify`
15
+ - 能力说明:缺少配置时走 `help → setup → assess`;已有配置的安全默认路径是 `help → assess → prepare --offline`;日常生产发布优先使用可恢复的 `ship`,兼容的分阶段闭环仍是 `prepare --online --production → approve → publish → verify`
16
16
  - 最小示例:展示从 release-help 到 release-assess 的最短路径
17
17
  - 只读诊断:运行 dry-run 检查,不修改任何文件
18
18
  - 故障引导:根据错误码指向对应的修复 Skill
@@ -35,6 +35,14 @@ description: "Discoverable entry point for release-skill: dependency and environ
35
35
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" help --json
36
36
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" setup --root <path> --json
37
37
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" assess --root <path> --offline --json
38
+ # 日常发布快速路径:最多两个授权摘要,状态文件可恢复
39
+ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" ship --root <path> --target-version <version> --json
40
+ # 开发阶段执行声明 hooks 并生成 prepare 可复用的内容绑定收据
41
+ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" hooks validate --root <path> \
42
+ --acknowledge-hook-side-effects --json
43
+ # Kimi/CodeBuddy 安装完成后记录人工事实,不手改 JSON
44
+ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" attest --root <path> \
45
+ --platform <kimi|codebuddy> --plugin <id> --result <passed|failed> --actor <person> --json
38
46
  # 发布文档刷新:默认只读演练
39
47
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" docs refresh --unit <id> --json
40
48
  # 摘要确认后的本地写入(三项绑定缺一不可)
@@ -12,7 +12,7 @@ description: "Discoverable entry point for release-skill: dependency and environ
12
12
  ## 职责
13
13
 
14
14
  - 依赖和环境检查:Node.js >= 22、Git 决定本地准备就绪度;npm/gh 另行决定生产依赖就绪度
15
- - 能力说明:缺少配置时走 `help → setup → assess`;已有配置的安全默认路径是 `help → assess → prepare --offline`;已有公开版本的生产闭环是显式的 `prepare --online --production → approve → publish → verify`
15
+ - 能力说明:缺少配置时走 `help → setup → assess`;已有配置的安全默认路径是 `help → assess → prepare --offline`;日常生产发布优先使用可恢复的 `ship`,兼容的分阶段闭环仍是 `prepare --online --production → approve → publish → verify`
16
16
  - 最小示例:展示从 release-help 到 release-assess 的最短路径
17
17
  - 只读诊断:运行 dry-run 检查,不修改任何文件
18
18
  - 故障引导:根据错误码指向对应的修复 Skill
@@ -35,6 +35,14 @@ description: "Discoverable entry point for release-skill: dependency and environ
35
35
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" help --json
36
36
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" setup --root <path> --json
37
37
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" assess --root <path> --offline --json
38
+ # 日常发布快速路径:最多两个授权摘要,状态文件可恢复
39
+ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" ship --root <path> --target-version <version> --json
40
+ # 开发阶段执行声明 hooks 并生成 prepare 可复用的内容绑定收据
41
+ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" hooks validate --root <path> \
42
+ --acknowledge-hook-side-effects --json
43
+ # Kimi/CodeBuddy 安装完成后记录人工事实,不手改 JSON
44
+ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" attest --root <path> \
45
+ --platform <kimi|codebuddy> --plugin <id> --result <passed|failed> --actor <person> --json
38
46
  # 发布文档刷新:默认只读演练
39
47
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" docs refresh --unit <id> --json
40
48
  # 摘要确认后的本地写入(三项绑定缺一不可)
@@ -476,7 +476,7 @@ async function resolveInstalledPayloadSubpath(snapshotDir, sourceEntries, action
476
476
  return extractDeclaredPluginSource(consumer, matches[0]);
477
477
  }
478
478
 
479
- async function verifyInstalledMarketplacePayload(action, context, installPath, consumer) {
479
+ export async function verifyInstalledMarketplacePayload(action, context, installPath, consumer) {
480
480
  const sourcePath = await resolveFrozenPath(
481
481
  context.root,
482
482
  action.snapshotPath,
@@ -32,7 +32,9 @@ export function githubRepositoryUrl(repo, host = 'github.com') {
32
32
  ) {
33
33
  throw new Error('githubHost must be a valid hostname');
34
34
  }
35
- return `https://${host}/${repo}.git`;
35
+ return process.env.RELEASE_SKILL_GIT_TRANSPORT === 'ssh'
36
+ ? `git@${host}:${repo}.git`
37
+ : `https://${host}/${repo}.git`;
36
38
  }
37
39
 
38
40
  function validateOid(value, label) {
@@ -23,6 +23,7 @@ import { promisify } from 'node:util';
23
23
 
24
24
  import { loadProjectConfig } from '../core/config.mjs';
25
25
  import { ReleaseError, CONFIG_INVALID, GATE_FAILED } from '../core/errors.mjs';
26
+ import { collectExpectedPublicSurfaceAdoptionWarnings } from '../core/public-surface.mjs';
26
27
 
27
28
  const execFile = promisify(execFileCb);
28
29
 
@@ -880,23 +881,35 @@ export async function assessProject(options) {
880
881
  // --- 2. Topology identification ---
881
882
  const topology = identifyTopology(config);
882
883
 
883
- // --- 3. Common docs check ---
884
+ // --- 3. Expected public-surface adoption ---
885
+ for (const warning of collectExpectedPublicSurfaceAdoptionWarnings(config)) {
886
+ allGaps.push(createGap({
887
+ scope: GapScope.PROJECT,
888
+ category: GapCategory.POLICY,
889
+ severity: Severity.WARNING,
890
+ code: warning.code,
891
+ message: warning.message,
892
+ file: '.release-skill/project.yaml',
893
+ }));
894
+ }
895
+
896
+ // --- 4. Common docs check ---
884
897
  const docGaps = await checkCommonDocs(root, config);
885
898
  allGaps.push(...docGaps);
886
899
 
887
- // --- 4. Plugin manifest check ---
900
+ // --- 5. Plugin manifest check ---
888
901
  const manifestGaps = await checkPluginManifests(root, config);
889
902
  allGaps.push(...manifestGaps);
890
903
 
891
- // --- 5. Package metadata check ---
904
+ // --- 6. Package metadata check ---
892
905
  const metadataGaps = await checkPackageMetadata(root, config);
893
906
  allGaps.push(...metadataGaps);
894
907
 
895
- // --- 6. Remote prerequisites ---
908
+ // --- 7. Remote prerequisites ---
896
909
  const remoteGaps = await checkRemotePrerequisites(root, config, offline);
897
910
  allGaps.push(...remoteGaps);
898
911
 
899
- // --- 7. README structure check ---
912
+ // --- 8. README structure check ---
900
913
  const readmeGaps = await checkReadmeStructure(root, config);
901
914
  allGaps.push(...readmeGaps);
902
915
 
@@ -0,0 +1,195 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve, join } from 'node:path';
3
+
4
+ import { SAFE_ID_RE, writeEvidenceAtomic } from '../adapters/contract.mjs';
5
+ import { ReleaseError, GATE_FAILED, MISSING_PARAMETERS } from '../core/errors.mjs';
6
+
7
+ const PLATFORM = Object.freeze({
8
+ kimi: {
9
+ directory: 'kimi-attestations',
10
+ requirementFile: 'release-skill-kimi-manual-install.json',
11
+ },
12
+ codebuddy: {
13
+ directory: 'codebuddy-attestations',
14
+ requirementFile: 'release-skill-codebuddy-manual-install.json',
15
+ },
16
+ });
17
+
18
+ async function validateInstalledConsumerClosure({
19
+ root,
20
+ platform,
21
+ requirement,
22
+ installPath,
23
+ }) {
24
+ const planPath = resolve(root, '.release-skill', 'plans', `${requirement.planDigest}.json`);
25
+ const [
26
+ { validatePlan, computePlanDigest, assertImmutablePlanAuthority },
27
+ { createPluginMarketplaceAdapter, verifyInstalledMarketplacePayload },
28
+ ] = await Promise.all([
29
+ import('../core/plan.mjs'),
30
+ import('../adapters/plugin-marketplace.mjs'),
31
+ ]);
32
+ let plan;
33
+ try {
34
+ plan = JSON.parse(await readFile(planPath, 'utf8'));
35
+ } catch (error) {
36
+ throw new ReleaseError(
37
+ GATE_FAILED,
38
+ `cannot read the frozen plan required to verify the installed ${platform} payload: ${error.message}`,
39
+ );
40
+ }
41
+ validatePlan(plan);
42
+ assertImmutablePlanAuthority(planPath, plan);
43
+ if (computePlanDigest(plan) !== requirement.planDigest) {
44
+ throw new ReleaseError(GATE_FAILED, 'manual requirement planDigest does not match the frozen plan');
45
+ }
46
+ const actionType = `${platform}-marketplace-install`;
47
+ const action = (plan.externalActions ?? []).find((candidate) => (
48
+ candidate.type === actionType
49
+ && candidate.parameters?.plugin === requirement.plugin
50
+ && candidate.parameters?.version === requirement.version
51
+ ));
52
+ if (!action) {
53
+ throw new ReleaseError(GATE_FAILED, `frozen plan has no matching ${platform} install action`);
54
+ }
55
+ const adapter = createPluginMarketplaceAdapter();
56
+ const context = {
57
+ externalWritesAuthorized: false,
58
+ isolatedConsumerWritesAuthorized: false,
59
+ plan,
60
+ baseline: plan.baseline,
61
+ root,
62
+ };
63
+ const actionInput = { actionType, ...action.parameters };
64
+ const preflight = await adapter.preflight(actionInput, context);
65
+ if (preflight.status !== 'PREFLIGHT_PASSED') {
66
+ throw new ReleaseError(
67
+ GATE_FAILED,
68
+ `${platform} installed-payload preflight failed: ${preflight.error}`,
69
+ );
70
+ }
71
+ const binding = await verifyInstalledMarketplacePayload(
72
+ actionInput,
73
+ context,
74
+ installPath,
75
+ platform,
76
+ );
77
+ return {
78
+ payloadDigest: action.parameters.manifestDigest,
79
+ manifestDigest: binding.manifestDigest,
80
+ extraInstalledPaths: binding.extraInstalledPaths ?? [],
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Record the human fact needed by an interactive-only consumer. Identity
86
+ * fields come exclusively from the generated requirement; the operator only
87
+ * supplies the result, actor and optional observed install facts.
88
+ */
89
+ export async function recordManualAttestation(options = {}, injected = {}) {
90
+ const {
91
+ root = process.cwd(),
92
+ platform,
93
+ plugin,
94
+ actor,
95
+ result,
96
+ installPath,
97
+ installChannel,
98
+ note,
99
+ clock = () => new Date().toISOString(),
100
+ } = options;
101
+ const descriptor = PLATFORM[platform];
102
+ if (!descriptor || !SAFE_ID_RE.test(plugin ?? '')) {
103
+ throw new ReleaseError(
104
+ MISSING_PARAMETERS,
105
+ 'attest requires --platform <kimi|codebuddy> and a safe --plugin <id>',
106
+ );
107
+ }
108
+ if (typeof actor !== 'string' || actor.trim() === '') {
109
+ throw new ReleaseError(MISSING_PARAMETERS, 'attest requires --actor <person>');
110
+ }
111
+ if (result !== 'passed' && result !== 'failed') {
112
+ throw new ReleaseError(MISSING_PARAMETERS, 'attest requires --result <passed|failed>');
113
+ }
114
+
115
+ const authorityDir = resolve(root, '.release-skill', descriptor.directory, plugin);
116
+ const requirementPath = join(authorityDir, descriptor.requirementFile);
117
+ let requirement;
118
+ try {
119
+ requirement = JSON.parse(await readFile(requirementPath, 'utf8'));
120
+ } catch (error) {
121
+ throw new ReleaseError(
122
+ GATE_FAILED,
123
+ `cannot read generated ${platform} requirement: ${error.message}`,
124
+ { platform, plugin, requirementPath },
125
+ );
126
+ }
127
+ if (
128
+ requirement.platform !== platform
129
+ || requirement.plugin !== plugin
130
+ || typeof requirement.version !== 'string'
131
+ || !/^[a-f0-9]{64}$/.test(requirement.planDigest ?? '')
132
+ || typeof requirement.attestationFile !== 'string'
133
+ ) {
134
+ throw new ReleaseError(
135
+ GATE_FAILED,
136
+ `generated ${platform} requirement has invalid identity fields`,
137
+ { platform, plugin, requirementPath },
138
+ );
139
+ }
140
+ if (result === 'passed' && (typeof installPath !== 'string' || installPath === '')) {
141
+ throw new ReleaseError(
142
+ MISSING_PARAMETERS,
143
+ `${platform} passed attestation requires --install-path for installed payload verification`,
144
+ );
145
+ }
146
+ const needsInstallChannel = Object.hasOwn(requirement.attestationTemplate ?? {}, 'installChannel');
147
+ if (
148
+ result === 'passed'
149
+ && needsInstallChannel
150
+ && installChannel !== 'desktop'
151
+ && installChannel !== 'cli'
152
+ ) {
153
+ throw new ReleaseError(
154
+ MISSING_PARAMETERS,
155
+ `${platform} passed attestation requires --install-channel <desktop|cli>`,
156
+ );
157
+ }
158
+
159
+ const installBinding = result === 'passed'
160
+ ? await (injected.validateInstalledConsumerClosure ?? validateInstalledConsumerClosure)({
161
+ root,
162
+ platform,
163
+ requirement,
164
+ installPath,
165
+ })
166
+ : null;
167
+ const receipt = {
168
+ platform,
169
+ version: requirement.version,
170
+ planDigest: requirement.planDigest,
171
+ result,
172
+ actor: actor.trim(),
173
+ confirmedAt: clock(),
174
+ ...(installPath ? { installPath } : {}),
175
+ ...(installChannel ? { installChannel } : {}),
176
+ ...(note ? { note } : {}),
177
+ ...(installBinding ? {
178
+ payloadDigest: installBinding.payloadDigest,
179
+ installedClosureVerified: true,
180
+ extraInstalledPaths: installBinding.extraInstalledPaths,
181
+ } : {}),
182
+ };
183
+ const attestationPath = join(authorityDir, requirement.attestationFile);
184
+ await writeEvidenceAtomic(attestationPath, receipt);
185
+ return {
186
+ command: 'attest',
187
+ status: 'RECORDED',
188
+ platform,
189
+ plugin,
190
+ version: requirement.version,
191
+ planDigest: requirement.planDigest,
192
+ requirementPath,
193
+ attestationPath,
194
+ };
195
+ }
@@ -0,0 +1,46 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+
4
+ import { loadProjectConfig } from '../core/config.mjs';
5
+ import { createEvidenceWriter } from '../core/evidence.mjs';
6
+ import { runDeclaredHooks } from './prepare.mjs';
7
+ import { ReleaseError, GATE_FAILED } from '../core/errors.mjs';
8
+
9
+ /**
10
+ * Run the declared development gates and populate the exact same content-bound
11
+ * hook receipts consumed by prepare. A later ship/prepare reuses only
12
+ * cacheable hooks whose declared input closure is byte-identical.
13
+ */
14
+ export async function validateDeclaredHooks(options = {}) {
15
+ const {
16
+ root = process.cwd(),
17
+ hooksAuthorized,
18
+ hookCache = true,
19
+ runDir = resolve(root, '.release-skill', 'runs', `hooks-${Date.now()}`),
20
+ } = options;
21
+ if (hooksAuthorized !== true) {
22
+ throw new ReleaseError(
23
+ GATE_FAILED,
24
+ 'hooks validate executes declared project commands; pass --acknowledge-hook-side-effects',
25
+ );
26
+ }
27
+ const { config, configDigest } = await loadProjectConfig({ root });
28
+ await mkdir(runDir, { recursive: true });
29
+ const evidence = createEvidenceWriter({
30
+ runDir,
31
+ command: 'hooks-validate',
32
+ clock: () => new Date().toISOString(),
33
+ });
34
+ const startedAt = new Date().toISOString();
35
+ await runDeclaredHooks(config, root, evidence, undefined, { hookCache });
36
+ return {
37
+ command: 'hooks validate',
38
+ status: 'PASSED',
39
+ configDigest,
40
+ hooks: Object.keys(config.hooks ?? {}).sort(),
41
+ cacheEnabled: hookCache,
42
+ evidenceDir: runDir,
43
+ startedAt,
44
+ finishedAt: new Date().toISOString(),
45
+ };
46
+ }
@@ -29,7 +29,10 @@ import { loadProjectConfig } from '../core/config.mjs';
29
29
  import { captureBaseline } from '../core/baseline.mjs';
30
30
  import { runHook } from '../core/hooks.mjs';
31
31
  import { computeHookCacheKey, readHookCache, writeHookCache } from '../core/hook-cache.mjs';
32
- import { assertExpectedPublicSurface } from '../core/public-surface.mjs';
32
+ import {
33
+ assertExpectedPublicSurface,
34
+ collectExpectedPublicSurfaceAdoptionWarnings,
35
+ } from '../core/public-surface.mjs';
33
36
  import { runSnapshotVerificationGates } from '../core/verification-gates.mjs';
34
37
  import { createEvidenceWriter } from '../core/evidence.mjs';
35
38
  import { computePlanDigest, writePlanAtomic, writePlanImmutable } from '../core/plan.mjs';
@@ -1670,7 +1673,7 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1670
1673
  * every declared hook runs in full and the incremental hook cache is neither
1671
1674
  * read nor written.
1672
1675
  *
1673
- * @returns {Promise<{ planPath: string, planDigest: string, evidenceDir: string }>}
1676
+ * @returns {Promise<{ planPath: string, planDigest: string, evidenceDir: string, warnings: ReadonlyArray<object> }>}
1674
1677
  *
1675
1678
  * @throws {ReleaseError} on any gate failure. No PREPARED plan is written.
1676
1679
  */
@@ -1750,6 +1753,7 @@ export async function prepareRelease(options) {
1750
1753
  await evidence.append({ phase: 'config', status: 'started' });
1751
1754
 
1752
1755
  const { config, configPath, configDigest } = await loadProjectConfig({ root: realRoot });
1756
+ const adoptionWarnings = collectExpectedPublicSurfaceAdoptionWarnings(config);
1753
1757
 
1754
1758
  await evidence.append({
1755
1759
  phase: 'config',
@@ -1757,6 +1761,13 @@ export async function prepareRelease(options) {
1757
1761
  configPath: relative(realRoot, configPath),
1758
1762
  configDigest,
1759
1763
  });
1764
+ for (const warning of adoptionWarnings) {
1765
+ await evidence.append({
1766
+ phase: 'public-surface-adoption',
1767
+ status: 'warning',
1768
+ ...warning,
1769
+ });
1770
+ }
1760
1771
 
1761
1772
  // --- Step 1b: Resolve authoritative versions and gate release-document
1762
1773
  // freshness BEFORE hook authorization ---
@@ -2913,6 +2924,7 @@ export async function prepareRelease(options) {
2913
2924
  planPath: writtenPath,
2914
2925
  planDigest,
2915
2926
  evidenceDir,
2927
+ warnings: adoptionWarnings,
2916
2928
  };
2917
2929
  } catch (err) {
2918
2930
  // Record failure evidence