release-skill 0.9.17 → 0.9.19

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 (266) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codebuddy-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +2 -2
  4. package/.cursor-plugin/plugin.json +10 -0
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/.qoder-plugin/plugin.json +1 -1
  7. package/CHANGELOG.md +70 -2
  8. package/INSTALL.md +66 -9
  9. package/INSTALL.zh-CN.md +49 -8
  10. package/README.md +41 -20
  11. package/README.zh-CN.md +31 -19
  12. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  13. package/adapters/claude/bin/consumer-contract-vectors.json +7 -7
  14. package/adapters/claude/bin/error-codes.json +1 -1
  15. package/adapters/claude/bin/foundation-resource-binding.json +1 -1
  16. package/adapters/claude/bin/prebuild-manifest.json +8 -4
  17. package/adapters/claude/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
  18. package/adapters/claude/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
  19. package/adapters/claude/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
  20. package/adapters/claude/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
  21. package/adapters/claude/bin/registry.json +1 -1
  22. package/adapters/claude/bin/release-skill.bundle.mjs +4425 -1519
  23. package/adapters/claude/bin/rules.json +1 -1
  24. package/adapters/claude/data/hosts/cursor/host-descriptor.json +34 -0
  25. package/adapters/claude/data/hosts/registry.json +1 -0
  26. package/adapters/claude/schemas/release-plan.schema.json +255 -25
  27. package/adapters/claude/schemas/release-project.schema.json +228 -26
  28. package/adapters/claude/skills/release-finish/SKILL.md +46 -7
  29. package/adapters/claude/skills/release-help/SKILL.md +47 -103
  30. package/adapters/claude/skills/release-verify/SKILL.md +2 -2
  31. package/adapters/claude/src/schemas/host-descriptor.schema.json +9 -2
  32. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  33. package/adapters/codex/bin/consumer-contract-vectors.json +7 -7
  34. package/adapters/codex/bin/error-codes.json +1 -1
  35. package/adapters/codex/bin/foundation-resource-binding.json +1 -1
  36. package/adapters/codex/bin/prebuild-manifest.json +8 -4
  37. package/adapters/codex/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
  38. package/adapters/codex/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
  39. package/adapters/codex/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
  40. package/adapters/codex/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
  41. package/adapters/codex/bin/registry.json +1 -1
  42. package/adapters/codex/bin/release-skill.bundle.mjs +4425 -1519
  43. package/adapters/codex/bin/rules.json +1 -1
  44. package/adapters/codex/data/hosts/cursor/host-descriptor.json +34 -0
  45. package/adapters/codex/data/hosts/registry.json +1 -0
  46. package/adapters/codex/schemas/release-plan.schema.json +255 -25
  47. package/adapters/codex/schemas/release-project.schema.json +228 -26
  48. package/adapters/codex/skills/release-finish/SKILL.md +46 -7
  49. package/adapters/codex/skills/release-help/SKILL.md +49 -105
  50. package/adapters/codex/skills/release-verify/SKILL.md +2 -2
  51. package/adapters/codex/src/schemas/host-descriptor.schema.json +9 -2
  52. package/adapters/cursor/.cursor-plugin/plugin.json +10 -0
  53. package/adapters/cursor/bin/consumer-contract-vectors.json +110 -0
  54. package/adapters/cursor/bin/error-codes.json +161 -0
  55. package/adapters/cursor/bin/foundation-legal/skill-family-contracts/LICENSE +202 -0
  56. package/adapters/cursor/bin/foundation-legal/skill-family-contracts/NOTICE +10 -0
  57. package/adapters/cursor/bin/foundation-legal/skill-family-engineering-kit/LICENSE +202 -0
  58. package/adapters/cursor/bin/foundation-legal/skill-family-engineering-kit/NOTICE +10 -0
  59. package/adapters/cursor/bin/foundation-legal/skill-family-engineering-kit/THIRD_PARTY_NOTICES +142 -0
  60. package/adapters/cursor/bin/foundation-legal/skill-family-harness-node/LICENSE +202 -0
  61. package/adapters/cursor/bin/foundation-legal/skill-family-harness-node/NOTICE +10 -0
  62. package/adapters/cursor/bin/foundation-legal/skill-family-harness-node/native-NOTICE +2 -0
  63. package/adapters/cursor/bin/foundation-resource-binding.json +1 -0
  64. package/adapters/cursor/bin/kernel-protocol.json +60 -0
  65. package/adapters/cursor/bin/license-texts/Apache-2.0.txt +201 -0
  66. package/adapters/cursor/bin/license-texts/MIT.txt +21 -0
  67. package/adapters/cursor/bin/prebuild-manifest.json +83 -0
  68. package/adapters/cursor/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
  69. package/adapters/cursor/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
  70. package/adapters/cursor/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
  71. package/adapters/cursor/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
  72. package/adapters/cursor/bin/registry.json +347 -0
  73. package/adapters/cursor/bin/release-skill-local-finish.mjs +4 -0
  74. package/adapters/cursor/bin/release-skill.bundle.mjs +152352 -0
  75. package/adapters/cursor/bin/release-skill.mjs +54 -0
  76. package/adapters/cursor/bin/rules.json +104 -0
  77. package/adapters/cursor/bin/schemas/consumer-contract-vector.schema.json +149 -0
  78. package/adapters/cursor/data/hosts/claude/host-descriptor.json +45 -0
  79. package/adapters/cursor/data/hosts/codebuddy/host-descriptor.json +26 -0
  80. package/adapters/cursor/data/hosts/codex/host-descriptor.json +45 -0
  81. package/adapters/cursor/data/hosts/cursor/host-descriptor.json +34 -0
  82. package/adapters/cursor/data/hosts/deepseek-harness/host-descriptor.json +23 -0
  83. package/adapters/cursor/data/hosts/kimi-code/host-descriptor.json +36 -0
  84. package/adapters/cursor/data/hosts/qoder/host-descriptor.json +26 -0
  85. package/adapters/cursor/data/hosts/registry.json +14 -0
  86. package/adapters/cursor/data/hosts/workbuddy/host-descriptor.json +34 -0
  87. package/adapters/cursor/data/licensing/registry.json +211 -0
  88. package/adapters/cursor/data/licensing/schema.json +207 -0
  89. package/adapters/cursor/native/safe-write/binding.gyp +41 -0
  90. package/adapters/cursor/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
  91. package/adapters/cursor/native/safe-write/prebuilds.json +24 -0
  92. package/adapters/cursor/native/safe-write/src/safe_write.cc +2032 -0
  93. package/adapters/cursor/schemas/.render-manifest.json +41 -0
  94. package/adapters/cursor/schemas/approval-record.schema.json +115 -0
  95. package/adapters/cursor/schemas/artifact-lock.schema.json +111 -0
  96. package/adapters/cursor/schemas/artifact-plan.schema.json +52 -0
  97. package/adapters/cursor/schemas/artifact-policy.schema.json +76 -0
  98. package/adapters/cursor/schemas/evidence-event-v2.schema.json +164 -0
  99. package/adapters/cursor/schemas/evidence-event.schema.json +89 -0
  100. package/adapters/cursor/schemas/postpublish-approval-record.schema.json +47 -0
  101. package/adapters/cursor/schemas/release-plan.schema.json +2226 -0
  102. package/adapters/cursor/schemas/release-project.schema.json +1815 -0
  103. package/adapters/cursor/schemas/release-run.schema.json +777 -0
  104. package/adapters/cursor/skills/release-assess/SKILL.md +61 -0
  105. package/adapters/cursor/skills/release-config/SKILL.md +151 -0
  106. package/adapters/cursor/skills/release-docs/SKILL.md +120 -0
  107. package/adapters/cursor/skills/release-finish/SKILL.md +181 -0
  108. package/adapters/cursor/skills/release-help/SKILL.md +98 -0
  109. package/adapters/cursor/skills/release-marketplace/SKILL.md +157 -0
  110. package/adapters/cursor/skills/release-prepare/SKILL.md +136 -0
  111. package/adapters/cursor/skills/release-publish/SKILL.md +92 -0
  112. package/adapters/cursor/skills/release-reconcile/SKILL.md +81 -0
  113. package/adapters/cursor/skills/release-setup/SKILL.md +132 -0
  114. package/adapters/cursor/skills/release-verify/SKILL.md +110 -0
  115. package/adapters/cursor/src/schemas/adapter-build-manifest.schema.json +41 -0
  116. package/adapters/cursor/src/schemas/adapter-peer-verification-request.schema.json +54 -0
  117. package/adapters/cursor/src/schemas/adapter-peer-verification-result.schema.json +54 -0
  118. package/adapters/cursor/src/schemas/adapter-source.schema.json +47 -0
  119. package/adapters/cursor/src/schemas/audit-baseline-pin.schema.json +163 -0
  120. package/adapters/cursor/src/schemas/declared-read-surface-result.schema.json +61 -0
  121. package/adapters/cursor/src/schemas/engineering-baseline.schema.json +77 -0
  122. package/adapters/cursor/src/schemas/executable-identity-observation.schema.json +162 -0
  123. package/adapters/cursor/src/schemas/filesystem-root-binding.schema.json +14 -0
  124. package/adapters/cursor/src/schemas/filesystem-tree-observation.schema.json +71 -0
  125. package/adapters/cursor/src/schemas/fixed-set-publication-manifest.schema.json +108 -0
  126. package/adapters/cursor/src/schemas/fixed-set-publication-receipt.schema.json +148 -0
  127. package/adapters/cursor/src/schemas/host-capability-fact.schema.json +31 -0
  128. package/adapters/cursor/src/schemas/host-descriptor.schema.json +144 -0
  129. package/adapters/cursor/src/schemas/host-operation-plan.schema.json +57 -0
  130. package/adapters/cursor/src/schemas/host-operation-receipt.schema.json +81 -0
  131. package/adapters/cursor/src/schemas/host-probe-result.schema.json +30 -0
  132. package/adapters/cursor/src/schemas/host-registry.schema.json +18 -0
  133. package/adapters/cursor/src/schemas/host-verification-request.schema.json +67 -0
  134. package/adapters/cursor/src/schemas/host-verification-result.schema.json +149 -0
  135. package/adapters/cursor/src/schemas/managed-file-lock.schema.json +77 -0
  136. package/adapters/cursor/src/schemas/migration-manifest.schema.json +412 -0
  137. package/adapters/cursor/src/schemas/observation-scope.schema.json +91 -0
  138. package/adapters/cursor/src/schemas/operation-request.schema.json +58 -0
  139. package/adapters/cursor/src/schemas/operation-result.schema.json +130 -0
  140. package/adapters/cursor/src/schemas/platform-difference-registry.schema.json +110 -0
  141. package/adapters/cursor/src/schemas/plugin-verification-request.schema.json +499 -0
  142. package/adapters/cursor/src/schemas/plugin-verification-result.schema.json +1217 -0
  143. package/adapters/cursor/src/schemas/profile-adoption-declaration.schema.json +111 -0
  144. package/adapters/cursor/src/schemas/profile-descriptor.schema.json +99 -0
  145. package/adapters/cursor/src/schemas/project-manifest.schema.json +67 -0
  146. package/adapters/cursor/src/schemas/project-profile.schema.json +25 -0
  147. package/adapters/cursor/src/schemas/public-boundary-declaration.schema.json +73 -0
  148. package/adapters/cursor/src/schemas/report-binding.schema.json +42 -0
  149. package/adapters/cursor/src/schemas/report-model.schema.json +308 -0
  150. package/adapters/cursor/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
  151. package/adapters/cursor/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
  152. package/adapters/cursor/src/schemas/source-authority-receipt.schema.json +31 -0
  153. package/adapters/cursor/src/schemas/state-event-envelope.schema.json +38 -0
  154. package/adapters/cursor/src/schemas/state-snapshot-metadata.schema.json +25 -0
  155. package/adapters/cursor/src/schemas/structured-scan-policy.schema.json +97 -0
  156. package/adapters/cursor/src/schemas/surface-scan-policy.schema.json +42 -0
  157. package/adapters/cursor/src/schemas/timeout-policy.schema.json +24 -0
  158. package/adapters/cursor/src/schemas/token-estimate-record.schema.json +75 -0
  159. package/adapters/cursor/src/schemas/token-estimate-result.schema.json +43 -0
  160. package/adapters/cursor/src/schemas/watchdog-termination-envelope.schema.json +156 -0
  161. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  162. package/adapters/kimi/bin/consumer-contract-vectors.json +7 -7
  163. package/adapters/kimi/bin/error-codes.json +1 -1
  164. package/adapters/kimi/bin/foundation-resource-binding.json +1 -1
  165. package/adapters/kimi/bin/prebuild-manifest.json +8 -4
  166. package/adapters/kimi/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
  167. package/adapters/kimi/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
  168. package/adapters/kimi/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
  169. package/adapters/kimi/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
  170. package/adapters/kimi/bin/registry.json +1 -1
  171. package/adapters/kimi/bin/release-skill.bundle.mjs +4425 -1519
  172. package/adapters/kimi/bin/rules.json +1 -1
  173. package/adapters/kimi/data/hosts/cursor/host-descriptor.json +34 -0
  174. package/adapters/kimi/data/hosts/registry.json +1 -0
  175. package/adapters/kimi/schemas/release-plan.schema.json +255 -25
  176. package/adapters/kimi/schemas/release-project.schema.json +228 -26
  177. package/adapters/kimi/skills/release-finish/SKILL.md +46 -7
  178. package/adapters/kimi/skills/release-help/SKILL.md +49 -105
  179. package/adapters/kimi/skills/release-verify/SKILL.md +2 -2
  180. package/adapters/kimi/src/schemas/host-descriptor.schema.json +9 -2
  181. package/adapters/qoder/.qoder-plugin/plugin.json +1 -1
  182. package/adapters/qoder/bin/consumer-contract-vectors.json +7 -7
  183. package/adapters/qoder/bin/error-codes.json +1 -1
  184. package/adapters/qoder/bin/foundation-resource-binding.json +1 -1
  185. package/adapters/qoder/bin/prebuild-manifest.json +8 -4
  186. package/adapters/qoder/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
  187. package/adapters/qoder/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
  188. package/adapters/qoder/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
  189. package/adapters/qoder/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
  190. package/adapters/qoder/bin/registry.json +1 -1
  191. package/adapters/qoder/bin/release-skill.bundle.mjs +4425 -1519
  192. package/adapters/qoder/bin/rules.json +1 -1
  193. package/adapters/qoder/data/hosts/cursor/host-descriptor.json +34 -0
  194. package/adapters/qoder/data/hosts/registry.json +1 -0
  195. package/adapters/qoder/schemas/release-plan.schema.json +255 -25
  196. package/adapters/qoder/schemas/release-project.schema.json +228 -26
  197. package/adapters/qoder/skills/release-finish/SKILL.md +46 -7
  198. package/adapters/qoder/skills/release-help/SKILL.md +49 -105
  199. package/adapters/qoder/skills/release-verify/SKILL.md +2 -2
  200. package/adapters/qoder/src/schemas/host-descriptor.schema.json +9 -2
  201. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  202. package/adapters/workbuddy/bin/consumer-contract-vectors.json +7 -7
  203. package/adapters/workbuddy/bin/error-codes.json +1 -1
  204. package/adapters/workbuddy/bin/foundation-resource-binding.json +1 -1
  205. package/adapters/workbuddy/bin/prebuild-manifest.json +8 -4
  206. package/adapters/workbuddy/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
  207. package/adapters/workbuddy/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
  208. package/adapters/workbuddy/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
  209. package/adapters/workbuddy/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
  210. package/adapters/workbuddy/bin/registry.json +1 -1
  211. package/adapters/workbuddy/bin/release-skill.bundle.mjs +4425 -1519
  212. package/adapters/workbuddy/bin/rules.json +1 -1
  213. package/adapters/workbuddy/data/hosts/cursor/host-descriptor.json +34 -0
  214. package/adapters/workbuddy/data/hosts/registry.json +1 -0
  215. package/adapters/workbuddy/schemas/release-plan.schema.json +255 -25
  216. package/adapters/workbuddy/schemas/release-project.schema.json +228 -26
  217. package/adapters/workbuddy/skills/release-finish/SKILL.md +46 -7
  218. package/adapters/workbuddy/skills/release-help/SKILL.md +47 -103
  219. package/adapters/workbuddy/skills/release-verify/SKILL.md +2 -2
  220. package/adapters/workbuddy/src/schemas/host-descriptor.schema.json +9 -2
  221. package/bin/consumer-contract-vectors.json +7 -7
  222. package/bin/error-codes.json +1 -1
  223. package/bin/foundation-resource-binding.json +1 -1
  224. package/bin/prebuild-manifest.json +8 -4
  225. package/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
  226. package/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
  227. package/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
  228. package/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
  229. package/bin/registry.json +1 -1
  230. package/bin/release-skill-cli.mjs +115 -15
  231. package/bin/release-skill.bundle.mjs +4425 -1519
  232. package/bin/rules.json +1 -1
  233. package/data/hosts/cursor/host-descriptor.json +34 -0
  234. package/data/hosts/registry.json +1 -0
  235. package/package.json +7 -5
  236. package/platform-manifest.json +75 -9
  237. package/references/02-project-config.md +4 -0
  238. package/schemas/release-plan.schema.json +255 -25
  239. package/schemas/release-project.schema.json +228 -26
  240. package/scripts/sync-public-files.mjs +3 -22
  241. package/skills/release-finish/SKILL.md +46 -7
  242. package/skills/release-help/SKILL.md +47 -103
  243. package/skills/release-verify/SKILL.md +2 -2
  244. package/skills-src/release-finish/SKILL.md +46 -7
  245. package/skills-src/release-help/SKILL.md +47 -103
  246. package/skills-src/release-verify/SKILL.md +2 -2
  247. package/src/adapters/contract.mjs +1 -0
  248. package/src/adapters/plugin-marketplace.mjs +201 -9
  249. package/src/commands/post-release-finish.mjs +448 -0
  250. package/src/commands/post-release-local.mjs +312 -36
  251. package/src/commands/prepare.mjs +74 -9
  252. package/src/commands/publish.mjs +4 -1
  253. package/src/commands/ship.mjs +3 -0
  254. package/src/commands/verify.mjs +73 -0
  255. package/src/core/checkpoints.mjs +5 -2
  256. package/src/core/config.mjs +1 -1
  257. package/src/core/foundation-host-verification.mjs +168 -0
  258. package/src/core/plan.mjs +19 -1
  259. package/src/core/postpublish.mjs +9 -1
  260. package/src/core/run-retention.mjs +1 -1
  261. package/src/core/surface-host-bindings.mjs +1 -0
  262. package/src/core/verification-gates.mjs +1 -1
  263. package/src/platforms/registry.mjs +225 -16
  264. package/src/producers/build-adapters.mjs +21 -26
  265. package/src/producers/foundation-resource-projection.mjs +3 -3
  266. package/src/schemas/host-descriptor.schema.json +9 -2
@@ -1,12 +1,19 @@
1
- import { access, readFile } from 'node:fs/promises';
1
+ import { access, readFile, mkdir, mkdtemp, cp, rename, rm, lstat, realpath, chmod } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
- import { join, relative } from 'node:path';
3
+ import { join, relative, isAbsolute, normalize } from 'node:path';
4
4
 
5
5
  import {
6
6
  readFileContained,
7
+ readFileStrict,
7
8
  resolveContained,
8
9
  superviseProcess,
9
10
  withTemporaryWorkspace,
11
+ createTemporaryWorkspace,
12
+ createFilesystemRootBinding,
13
+ observeFilesystemTree,
14
+ createFixedSetPublicationManifest,
15
+ publishFixedSet,
16
+ replaceFixedSetAtomic,
10
17
  } from 'skill-family-harness-node';
11
18
 
12
19
  import { getPlatform } from '../platforms/registry.mjs';
@@ -45,10 +52,11 @@ const QODER_PLUGIN_LIST_ARGS = Object.freeze(['plugins', 'list', '--json']);
45
52
  const QODER_MARKETPLACE_LIST_ARGS = Object.freeze(['plugins', 'marketplace', 'list', '--json']);
46
53
  const QODER_PAYLOAD_CONTRACT = 'external-marketplace-v1';
47
54
 
48
- function attachFoundationFailure(error, { envelope, stdout }) {
55
+ function attachFoundationFailure(error, { envelope, stdout, stderr }) {
49
56
  Object.defineProperties(error, {
50
57
  foundationEnvelope: { value: envelope, enumerable: false },
51
58
  foundationStdout: { value: stdout, enumerable: false },
59
+ foundationStderr: { value: stderr, enumerable: false },
52
60
  });
53
61
  return error;
54
62
  }
@@ -177,6 +185,17 @@ function hubTargets(plan) {
177
185
  workbuddy: 'Handle manually; WorkBuddy cannot pin a Hub ref in this flow (it follows the CodeBuddy manual boundary).',
178
186
  };
179
187
  return local.hosts.map((host) => {
188
+ if (host === 'cursor') {
189
+ return {
190
+ targetKind: 'cursor-local', executionMode: 'executable',
191
+ unitId: declaration.unitId, host, plugin: local.plugin,
192
+ version: unit?.targetVersion,
193
+ snapshotPath: unit?.frozenSnapshot?.path,
194
+ manifestDigest: unit?.frozenSnapshot?.manifestDigest,
195
+ cursor: local.cursor, timeoutMs: 300_000,
196
+ message: `Install or replace the complete frozen ${local.plugin} Cursor Local plugin; quit Cursor first and restart it afterwards.`,
197
+ };
198
+ }
180
199
  if (host === 'qoder') {
181
200
  return {
182
201
  targetKind: 'hub-backed',
@@ -202,6 +221,7 @@ function hubTargets(plan) {
202
221
  unitId: declaration.unitId,
203
222
  host,
204
223
  plugin: local.plugin,
224
+ version: unit?.targetVersion,
205
225
  hub,
206
226
  message: `${manualInstruction[host]} Install or upgrade ${local.plugin} from Hub ${hub.name}; release-skill does not execute or probe this action.`,
207
227
  ...(unit?.publicRepo ? { publicRepo: unit.publicRepo } : {}),
@@ -296,9 +316,23 @@ function buildShipNextStep({ root, statePath, unitIds }) {
296
316
  };
297
317
  }
298
318
 
319
+ function buildFinishCommand({ root, planPath, runPath }) {
320
+ if (![root, planPath, runPath].every((value) => typeof value === 'string' && value.length > 0)) return undefined;
321
+ return {
322
+ argv: [
323
+ 'release-skill', 'post-release',
324
+ '--root', root,
325
+ '--plan', planPath,
326
+ '--run', runPath,
327
+ '--finish',
328
+ ],
329
+ };
330
+ }
331
+
299
332
  export function derivePostReleaseChecklist(plan, {
300
333
  runPath,
301
334
  root,
335
+ planPath,
302
336
  statePath,
303
337
  unitIds,
304
338
  postVerifyComplete = false,
@@ -320,10 +354,12 @@ export function derivePostReleaseChecklist(plan, {
320
354
  const hasPendingPostVerify = postVerifyHooks(plan).length > 0 && !postVerifyComplete && targets.length > 0;
321
355
  const hasStatePath = typeof statePath === 'string' && statePath.length > 0;
322
356
  const selectedUnitIds = Array.isArray(unitIds) ? unitIds : undefined;
357
+ const finishCommand = hasPendingPostVerify ? undefined : buildFinishCommand({ root, planPath, runPath });
323
358
  return {
324
359
  command: 'post-release',
325
360
  status: 'AWAITING_USER_DECISION',
326
361
  planDigest: plan.digest,
362
+ ...(finishCommand ? { finishCommand } : {}),
327
363
  merge: {
328
364
  promptRequired: uncovered.length > 0,
329
365
  alreadyHandledByRelease: uncovered.length === 0,
@@ -574,12 +610,14 @@ async function defaultRun(command, args, options = {}) {
574
610
  watchdogReason: envelope.watchdogReason,
575
611
  ...(envelope.evidence?.spawnError ? { spawnError: envelope.evidence.spawnError } : {}),
576
612
  };
577
- throw attachFoundationFailure(error, { envelope, stdout });
613
+ throw attachFoundationFailure(error, { envelope, stdout, stderr });
578
614
  }
579
615
  return { stdout, stderr };
580
616
  }, { prefix: 'release-skill-host-command-' });
581
617
  }
582
618
 
619
+ export { defaultRun as runLocalFinishCommand };
620
+
583
621
  async function commandAvailable(command, host, run) {
584
622
  try {
585
623
  await run(command, ['--version'], { timeout: 10_000, env: hostEnvironment(host) });
@@ -1388,49 +1426,120 @@ proc unexpectedDirectoryTrust {} {
1388
1426
  exit 147
1389
1427
  }
1390
1428
 
1391
- proc confirmInitialDirectoryTrust {} {
1392
- global expect_out promptPattern
1393
- set dialogBuffer $expect_out(buffer)
1394
- expect {
1395
- -nocase -re {❯[^\\r\\n]*No,[ \\t]*exit} {
1396
- append dialogBuffer $expect_out(buffer)
1429
+ proc readDirectoryTrustDialog {prefix state timeoutCode unknownCode eofCode} {
1430
+ global expect_out
1431
+ set dialogBuffer $prefix
1432
+ set cleanedPrefix [cleanScreen $dialogBuffer]
1433
+ if {![regexp -nocase {Trust this folder\\?} $cleanedPrefix]} {
1434
+ expect {
1435
+ -nocase -re {Trust this folder\\?} {
1436
+ append dialogBuffer $expect_out(buffer)
1437
+ }
1438
+ timeout { failTimeout $state $timeoutCode $unknownCode }
1439
+ eof { failEof $state $eofCode }
1397
1440
  }
1398
- -re {❯[^\\r\\n]*\\r*\\n} {
1399
- puts stderr "KIMI_TUI_STATE:directory-trust:selection-unknown"
1400
- exit 143
1441
+ set cleanedPrefix [cleanScreen $dialogBuffer]
1442
+ }
1443
+ set framed [regexp -nocase {(^|\\n)[ \\t]*─{8,}[ \\t]*\\n[ \\t]*Trust this folder\\?} $cleanedPrefix]
1444
+ if {$framed} {
1445
+ expect {
1446
+ -re {(^|\\r|\\n)[ \\t]*─{8,}[ \\t]*\\r*\\n} {
1447
+ append dialogBuffer $expect_out(buffer)
1448
+ }
1449
+ timeout { failTimeout $state $timeoutCode $unknownCode }
1450
+ eof { failEof $state $eofCode }
1401
1451
  }
1402
- timeout {
1403
- puts stderr "KIMI_TUI_STATE:directory-trust-selected-row:timeout"
1404
- exit 140
1452
+ } else {
1453
+ expect {
1454
+ -nocase -re {↑↓[^\\r\\n]*navigate[^\\r\\n]*(?:Esc[^\\r\\n]*)?\\r*\\n} {
1455
+ append dialogBuffer $expect_out(buffer)
1456
+ }
1457
+ timeout { failTimeout $state $timeoutCode $unknownCode }
1458
+ eof { failEof $state $eofCode }
1405
1459
  }
1406
- eof { failEof directory-trust-selected-row 141 }
1407
1460
  }
1408
- expect {
1409
- -nocase -re {(?:^|\\r|\\n)[ \\t]+Trust this folder[ \\t]*\\r*\\n} {
1410
- append dialogBuffer $expect_out(buffer)
1461
+ return [cleanScreen $dialogBuffer]
1462
+ }
1463
+
1464
+ proc directoryTrustAction {dialog state unknownCode} {
1465
+ set inDialog 0
1466
+ set labels {}
1467
+ set selectedCount 0
1468
+ set selectedIndex -1
1469
+ set selectedLabel ""
1470
+ set trustCount 0
1471
+ set trustIndex -1
1472
+ foreach rawLine [split $dialog "\\n"] {
1473
+ set line [string trim $rawLine]
1474
+ if {[regexp -nocase {^Trust this folder\\?$} $line]} {
1475
+ set inDialog 1
1476
+ continue
1477
+ }
1478
+ if {!$inDialog} { continue }
1479
+ if {$line eq ""} { continue }
1480
+ set selected [regexp {^❯[ \\t]*} $line]
1481
+ if {$selected} {
1482
+ regsub {^❯[ \\t]*} $line {} label
1483
+ set label [string trim $label]
1484
+ } else {
1485
+ set label $line
1411
1486
  }
1412
- -re {(?:^|\\r|\\n)[^\\r\\n]*\\r*\\n} {
1413
- puts stderr "KIMI_TUI_STATE:directory-trust:selection-unknown"
1414
- exit 143
1487
+ set knownAction [expr {
1488
+ [string equal -nocase $label "Trust this folder"]
1489
+ || [string equal -nocase $label "No, exit"]
1490
+ || [string equal -nocase $label "Don't trust"]
1491
+ }]
1492
+ if {!$knownAction && $selected} {
1493
+ puts stderr "KIMI_TUI_STATE:$state:selection-unknown"
1494
+ exit $unknownCode
1495
+ }
1496
+ if {!$knownAction} { continue }
1497
+ set index [llength $labels]
1498
+ lappend labels $label
1499
+ if {[string equal -nocase $label "Trust this folder"]} {
1500
+ incr trustCount
1501
+ set trustIndex $index
1502
+ }
1503
+ if {$selected} {
1504
+ incr selectedCount
1505
+ set selectedIndex $index
1506
+ set selectedLabel $label
1415
1507
  }
1416
- timeout { failTimeout directory-trust-target-row 148 150 }
1417
- eof { failEof directory-trust-target-row 149 }
1418
1508
  }
1419
- set dialog [cleanScreen $dialogBuffer]
1420
- if {![regexp -nocase {(^|\\n)[^\\n]*❯[^\\n]*No,[ \\t]*exit} $dialog]
1421
- || ![regexp -nocase {(^|\\n)[ \\t]+Trust this folder[ \\t]*($|\\n)} $dialog]} {
1422
- puts stderr "KIMI_TUI_STATE:directory-trust:selection-unknown"
1423
- exit 143
1509
+ if {[llength $labels] != 2 || $trustCount != 1 || $selectedCount != 1} {
1510
+ puts stderr "KIMI_TUI_STATE:$state:selection-unknown"
1511
+ exit $unknownCode
1424
1512
  }
1425
- send -- "\\033\\[B"
1426
- expect {
1427
- -nocase -re {❯[^\\r\\n]*Trust this folder} {}
1428
- -re {❯[^\\r\\n]*\\r*\\n} {
1513
+ if {[string equal -nocase $selectedLabel "Trust this folder"]} {
1514
+ return selected-trust
1515
+ }
1516
+ if {![string equal -nocase $selectedLabel "No, exit"]
1517
+ && ![string equal -nocase $selectedLabel "Don't trust"]} {
1518
+ puts stderr "KIMI_TUI_STATE:$state:selection-unknown"
1519
+ exit $unknownCode
1520
+ }
1521
+ if {$selectedIndex + 1 == $trustIndex} { return move-down }
1522
+ if {$selectedIndex - 1 == $trustIndex} { return move-up }
1523
+ puts stderr "KIMI_TUI_STATE:$state:selection-unknown"
1524
+ exit $unknownCode
1525
+ }
1526
+
1527
+ proc confirmInitialDirectoryTrust {} {
1528
+ global expect_out promptPattern
1529
+ set dialog [readDirectoryTrustDialog $expect_out(buffer) directory-trust-selected-row 140 143 141]
1530
+ set action [directoryTrustAction $dialog directory-trust 143]
1531
+ if {$action eq "move-down"} {
1532
+ send -- "\\033\\[B"
1533
+ } elseif {$action eq "move-up"} {
1534
+ send -- "\\033\\[A"
1535
+ }
1536
+ if {$action ne "selected-trust"} {
1537
+ set confirmedDialog [readDirectoryTrustDialog "" directory-trust-confirm-selection 144 146 145]
1538
+ set confirmedAction [directoryTrustAction $confirmedDialog directory-trust-confirm-selection 146]
1539
+ if {$confirmedAction ne "selected-trust"} {
1429
1540
  puts stderr "KIMI_TUI_STATE:directory-trust-confirm-selection:unknown"
1430
1541
  exit 146
1431
1542
  }
1432
- timeout { failTimeout directory-trust-confirm-selection 144 146 }
1433
- eof { failEof directory-trust-confirm-selection 145 }
1434
1543
  }
1435
1544
  send -- "\\033\\[13u"
1436
1545
  expect {
@@ -1679,6 +1788,160 @@ async function runKimiUpdate(target, detected, run, kimiHome, {
1679
1788
  return { status: 'UPDATED', version: target.version, restartRequired: true };
1680
1789
  }
1681
1790
 
1791
+ async function observeCursorClosure(root) {
1792
+ return observeFilesystemTree({ root, rootBinding: await createFilesystemRootBinding(root) });
1793
+ }
1794
+
1795
+ async function cursorMainProcessRunning(run) {
1796
+ const result = await run('/bin/ps', ['-axo', 'comm='], { timeout: 30_000 });
1797
+ if (typeof result?.stdout !== 'string' || result.stdout.trim().length === 0) throw new Error('Cursor process observation is unavailable');
1798
+ return result.stdout.split('\n').some((line) => /(?:^|\/)Cursor$/u.test(line.trim()));
1799
+ }
1800
+
1801
+ async function runCursorLocalUpdate(target, {
1802
+ root, run, cursorPluginsRoot, cursorPlatform = process.platform,
1803
+ cursorIsRunning = () => cursorMainProcessRunning(run),
1804
+ cursorPublication = { publishFixedSet, replaceFixedSetAtomic },
1805
+ cursorMoveBackup = rename,
1806
+ }) {
1807
+ const frozen = await verifyFrozenSnapshot({ root, snapshotPath: target.snapshotPath, expectedDigest: target.manifestDigest });
1808
+ if (!target.cursor?.sourcePath) throw new Error('Cursor local update requires a frozen cursor.sourcePath');
1809
+ if (!/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/u.test(target.plugin)) throw new Error('Cursor plugin identity must use a valid Cursor plugin name');
1810
+ const source = await resolveContained(frozen.snapshotDir, target.cursor.sourcePath);
1811
+ const identity = JSON.parse(await readFileContained(source, '.cursor-plugin/plugin.json', { encoding: 'utf8' }));
1812
+ if (typeof identity.name !== 'string' || !/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/u.test(identity.name)
1813
+ || identity.name !== target.plugin || identity.version !== target.version) throw new Error('Cursor frozen plugin manifest identity conflicts with the release unit');
1814
+ if (typeof cursorPluginsRoot !== 'string' || !isAbsolute(cursorPluginsRoot) || normalize(cursorPluginsRoot) !== cursorPluginsRoot) {
1815
+ throw new Error('Cursor local update requires --cursor-plugins-root <normalized-absolute-directory>');
1816
+ }
1817
+ if (cursorPlatform !== 'darwin') return { status: 'MANUAL_REQUIRED', reason: 'Cursor Local automatic installation currently supports macOS only' };
1818
+ try {
1819
+ if (await cursorIsRunning() !== false) return { status: 'MANUAL_REQUIRED', reason: 'Quit the Cursor main process before updating Local plugins' };
1820
+ } catch (cause) {
1821
+ return { status: 'MANUAL_REQUIRED', reason: `Cannot confirm Cursor has exited: ${cause.message}` };
1822
+ }
1823
+ // Bind the caller-selected root before creating any staging directory.
1824
+ await createFilesystemRootBinding(cursorPluginsRoot);
1825
+ const pluginsRoot = await realpath(cursorPluginsRoot);
1826
+ const sourceObservation = await observeCursorClosure(source);
1827
+ const workspace = await createTemporaryWorkspace({ prefix: 'release-skill-cursor-local-' });
1828
+ let sibling;
1829
+ let preserve = false;
1830
+ try {
1831
+ const candidate = join(workspace.root, 'plugin');
1832
+ await cp(source, candidate, { recursive: true, errorOnExist: true, force: false });
1833
+ if ((await observeCursorClosure(candidate)).membersDigest !== sourceObservation.membersDigest) throw new Error('Cursor candidate differs from the frozen source closure');
1834
+ await chmod(candidate, 0o700);
1835
+ if (target.cursor.dependencyInstall !== undefined) {
1836
+ if (target.cursor.dependencyInstall !== 'npm-ci-ignore-scripts') throw new Error('Unsupported Cursor dependency installation policy');
1837
+ await run('npm', ['ci', '--ignore-scripts', '--prefix', candidate], {
1838
+ cwd: candidate, timeout: target.timeoutMs, env: hostEnvironment('cursor'),
1839
+ });
1840
+ // npm may add dependencies, but must not mutate the frozen plugin inputs.
1841
+ for (const member of sourceObservation.members.filter((entry) => entry.type === 'file')) {
1842
+ await readFileStrict(candidate, member.path, { expectedSha256: member.sha256 });
1843
+ }
1844
+ }
1845
+ const prepared = await observeCursorClosure(candidate);
1846
+ await verifyFrozenSnapshot({ root, snapshotPath: target.snapshotPath, expectedDigest: target.manifestDigest });
1847
+ try {
1848
+ if (await cursorIsRunning() !== false) return { status: 'MANUAL_REQUIRED', reason: 'Cursor started while preparing the plugin; quit Cursor and rerun the update' };
1849
+ } catch (cause) {
1850
+ return { status: 'MANUAL_REQUIRED', reason: `Cannot confirm Cursor has exited before publication: ${cause.message}` };
1851
+ }
1852
+ const local = await resolveContained(pluginsRoot, 'local');
1853
+ await mkdir(local, { recursive: true });
1854
+ await createFilesystemRootBinding(local);
1855
+ const targetPath = await resolveContained(local, target.plugin);
1856
+ let installed = false;
1857
+ let previousClosure;
1858
+ try {
1859
+ await lstat(targetPath);
1860
+ installed = true;
1861
+ } catch (cause) { if (cause.code !== 'ENOENT') throw cause; }
1862
+ if (installed) {
1863
+ const current = JSON.parse(await readFileContained(targetPath, '.cursor-plugin/plugin.json', { encoding: 'utf8' }));
1864
+ if (current.name !== target.plugin || typeof current.version !== 'string') throw new Error('Cursor installed plugin manifest identity conflicts with the frozen plugin');
1865
+ const observed = await observeCursorClosure(targetPath);
1866
+ previousClosure = observed.membersDigest;
1867
+ if (current.version === target.version && observed.membersDigest === prepared.membersDigest) return { status: 'ALREADY_CURRENT', version: target.version, installPath: targetPath };
1868
+ }
1869
+ // Cursor is closed, so this same-parent candidate cannot be discovered by
1870
+ // a live scanner. Only this uniquely-created directory may be cleaned.
1871
+ sibling = await mkdtemp(join(local, `.${target.plugin}-stage-`));
1872
+ await cp(candidate, sibling, { recursive: true, errorOnExist: false, force: false });
1873
+ if ((await observeCursorClosure(sibling)).membersDigest !== prepared.membersDigest) throw new Error('Cursor publication candidate closure drift');
1874
+ const publication = { sourceRoot: sibling, targetParent: local, targetSegment: target.plugin };
1875
+ let backupPath;
1876
+ if (installed) {
1877
+ try {
1878
+ await cursorPublication.replaceFixedSetAtomic(publication);
1879
+ } catch (cause) {
1880
+ preserve = cause.details?.phase === 'post-commit' || cause.details?.publicationState === 'indeterminate' || cause.details?.commitState === 'indeterminate';
1881
+ throw cause;
1882
+ }
1883
+ // Foundation has proved the exchanged mapping. The old complete plugin
1884
+ // is now owned by the user and must leave the Local scanner namespace.
1885
+ preserve = true;
1886
+ try {
1887
+ const backups = await resolveContained(pluginsRoot, `backups/${target.plugin}`);
1888
+ await mkdir(backups, { recursive: true });
1889
+ await createFilesystemRootBinding(backups);
1890
+ const backupDirectory = await mkdtemp(join(backups, 'release-'));
1891
+ backupPath = join(backupDirectory, 'plugin');
1892
+ await cursorMoveBackup(sibling, backupPath);
1893
+ sibling = undefined;
1894
+ preserve = false;
1895
+ } catch (cause) {
1896
+ // One explicit reverse exchange is justified only by the verified
1897
+ // success above and an old directory still at the displaced path.
1898
+ if ((await observeCursorClosure(sibling)).membersDigest !== previousClosure
1899
+ || (await observeCursorClosure(targetPath)).membersDigest !== prepared.membersDigest) throw cause;
1900
+ await cursorPublication.replaceFixedSetAtomic(publication);
1901
+ preserve = false;
1902
+ throw new Error(`Cursor backup move failed; original plugin restored: ${cause.message}`);
1903
+ }
1904
+ } else {
1905
+ const manifest = await createFixedSetPublicationManifest(publication);
1906
+ const receipt = await cursorPublication.publishFixedSet({ ...publication, manifest });
1907
+ if (receipt.status !== 'succeeded') {
1908
+ preserve = receipt.status === 'indeterminate' || receipt.commitState !== 'not-committed';
1909
+ const error = new Error(`Cursor publication failed: ${receipt.error?.message ?? receipt.status}`);
1910
+ error.details = receipt;
1911
+ throw error;
1912
+ }
1913
+ sibling = undefined;
1914
+ }
1915
+ if ((await observeCursorClosure(targetPath)).membersDigest !== prepared.membersDigest) throw new Error('Cursor installed plugin closure drift after publication');
1916
+ return {
1917
+ status: 'UPDATED', version: target.version, installPath: targetPath,
1918
+ ...(backupPath ? { backupPath } : {}), restartRequired: true,
1919
+ reloadInstruction: 'Restart Cursor or run Reload Window, then verify the Local source, version and Skill invocation.',
1920
+ };
1921
+ } catch (error) {
1922
+ if (preserve) error.details = { ...error.details, candidatePath: sibling, workspacePath: workspace.root, manualAction: 'Keep both directories intact; inspect the Cursor Local publication mapping before retrying.' };
1923
+ throw error;
1924
+ } finally {
1925
+ if (!preserve) {
1926
+ // Sealed snapshots carry read-only directories. Restore owner access
1927
+ // only in this invocation's disposable copies before Foundation cleanup.
1928
+ for (const cleanupRoot of [sibling, workspace.root].filter(Boolean)) {
1929
+ const closure = await observeFilesystemTree({
1930
+ root: cleanupRoot,
1931
+ rootBinding: await createFilesystemRootBinding(cleanupRoot),
1932
+ symlinkPolicy: { mode: 'record' },
1933
+ });
1934
+ await chmod(cleanupRoot, 0o700);
1935
+ for (const member of closure.members.filter((entry) => entry.type === 'directory')) {
1936
+ await chmod(await resolveContained(cleanupRoot, member.path), member.statMode | 0o700);
1937
+ }
1938
+ }
1939
+ if (sibling) await rm(sibling, { recursive: true, force: false });
1940
+ await workspace.dispose();
1941
+ }
1942
+ }
1943
+ }
1944
+
1682
1945
  function aggregateStatus(results) {
1683
1946
  const statuses = new Set(results.map((entry) => entry.status));
1684
1947
  const completed = results.some((entry) => ['UPDATED', 'ALREADY_CURRENT'].includes(entry.status));
@@ -1718,6 +1981,11 @@ async function updateLocalHostPluginsInternal({
1718
1981
  detect = (host) => defaultDetect(host, defaultRun),
1719
1982
  run = defaultRun,
1720
1983
  kimiHome,
1984
+ cursorPluginsRoot,
1985
+ cursorPlatform,
1986
+ cursorIsRunning,
1987
+ cursorPublication,
1988
+ cursorMoveBackup,
1721
1989
  verifyInstalledPayload = verifyInstalledMarketplacePayload,
1722
1990
  } = {}) {
1723
1991
  const effectiveKimiHome = kimiHome ?? process.env.KIMI_CODE_HOME ?? join(homedir(), '.kimi-code');
@@ -1732,6 +2000,7 @@ async function updateLocalHostPluginsInternal({
1732
2000
  item.executionMode !== 'manual' && selected.has(item.host)
1733
2001
  ));
1734
2002
  for (const target of targets) {
2003
+ if (target.host === 'cursor') continue;
1735
2004
  if (target.host === 'qoder') assertQoderExecutableTarget(target);
1736
2005
  else assertExecutableTarget(target);
1737
2006
  }
@@ -1739,6 +2008,13 @@ async function updateLocalHostPluginsInternal({
1739
2008
  const results = [];
1740
2009
  for (const target of targets) {
1741
2010
  try {
2011
+ if (target.host === 'cursor') {
2012
+ const outcome = await runCursorLocalUpdate(target, {
2013
+ root, run, cursorPluginsRoot, cursorPlatform, cursorIsRunning, cursorPublication, cursorMoveBackup,
2014
+ });
2015
+ results.push({ host: target.host, unitId: target.unitId, ...outcome });
2016
+ continue;
2017
+ }
1742
2018
  const detected = await detect(target.host);
1743
2019
  if (!detected?.available) {
1744
2020
  results.push({
@@ -25,7 +25,7 @@ import { promisify } from 'node:util';
25
25
 
26
26
  const execFile = promisify(execFileCb);
27
27
 
28
- import { classifyPathInput, writeFileAtomic, withTemporaryWorkspace } from 'skill-family-harness-node';
28
+ import { classifyPathInput, writeFileAtomic, withTemporaryWorkspace, resolveContained, readFileStrict } from 'skill-family-harness-node';
29
29
  import { loadProjectConfig } from '../core/config.mjs';
30
30
  import { captureBaseline } from '../core/baseline.mjs';
31
31
  import { runHook } from '../core/hooks.mjs';
@@ -63,6 +63,7 @@ import {
63
63
  assertSelfBootstrapFacts,
64
64
  } from '../core/derived-artifact-gates.mjs';
65
65
  import { isFoundationPluginVerificationEligible } from '../core/foundation-plugin-verification.mjs';
66
+ import { assertCursorHostScenario } from '../core/foundation-host-verification.mjs';
66
67
  import { PKG_ROOT } from '../core/pkg-root.mjs';
67
68
  import { writeFrozenMarker, FROZEN_MARKER_FILENAME } from '../core/frozen-marker.mjs';
68
69
  import {
@@ -897,12 +898,21 @@ async function processSnapshots(config, root, evidence, runDir, production = fal
897
898
  }
898
899
 
899
900
  /** Validate Hub-backed plugin identities against the already frozen snapshot. */
900
- async function validateFrozenLocalHostUpdatePlugins(unitResults) {
901
- for (const { unit, manifest } of unitResults) {
901
+ async function validateFrozenLocalHostUpdatePlugins(unitResults, resolvedVersions) {
902
+ for (const [index, { unit, manifest }] of unitResults.entries()) {
902
903
  const declaration = unit.postPublish?.localHostUpdate;
903
904
  if (!declaration) continue;
905
+ if (declaration.hosts.includes('cursor')) {
906
+ const source = await resolveContained(manifest.outputDir, declaration.cursor.sourcePath);
907
+ const receipt = await readFileStrict(source, '.cursor-plugin/plugin.json', { encoding: 'utf8' });
908
+ const cursor = JSON.parse(receipt.content);
909
+ if (typeof cursor.name !== 'string' || !/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/u.test(cursor.name)
910
+ || cursor.name !== declaration.plugin || cursor.version !== resolvedVersions[index]) {
911
+ throw new ReleaseError(GATE_FAILED, `unit "${unit.id}" Cursor local plugin manifest must match the frozen plugin name and target version`);
912
+ }
913
+ }
904
914
  const manifestEntries = (manifest.entries ?? [])
905
- .filter((entry) => /(?:^|\/)\.(?:claude|codex|kimi|codebuddy)-plugin\/plugin\.json$/u.test(entry.path));
915
+ .filter((entry) => /(?:^|\/)\.(?:claude|codex|kimi|codebuddy|qoder|cursor)-plugin\/plugin\.json$/u.test(entry.path));
906
916
  const identities = new Set();
907
917
  for (const entry of manifestEntries) {
908
918
  try {
@@ -1884,6 +1894,7 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1884
1894
  // Undeclared stays absent so legacy frozen plans remain byte-identical.
1885
1895
  const frozenUnitDists = frozenDistributions?.get(unit.id) ?? null;
1886
1896
  for (const platform of PLATFORMS) {
1897
+ if (platform.actionType === null) continue;
1887
1898
  const dist = frozenUnitDists
1888
1899
  ? frozenUnitDists.find((d) => d.type === platform.distributionType)
1889
1900
  : (unit.distributions ?? []).find((d) => d.type === platform.distributionType);
@@ -1896,6 +1907,10 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1896
1907
  // but no frozen ref/marketplaceCommitSha (production-only bindings),
1897
1908
  // keeping the two loops' shapes aligned for plan completeness.
1898
1909
  const externalMarketplace = dist.marketplaceRepo !== undefined && dist.marketplaceRepo !== null;
1910
+ const marketplaceSource = dist.marketplaceSource
1911
+ ?? (platform.id === 'qoder' && externalMarketplace
1912
+ ? `https://github.com/${dist.marketplaceRepo}.git`
1913
+ : undefined);
1899
1914
  // Normalized marketplace form: explicit mutually exclusive declaration.
1900
1915
  // bundled-family: marketplace and plugin live in the same repo.
1901
1916
  // standalone-index: external marketplace repo indexes a separate plugin repo.
@@ -1937,7 +1952,7 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1937
1952
  consumer: platform.id,
1938
1953
  plugin: dist.plugin,
1939
1954
  ...(dist.marketplace !== undefined ? { marketplace: dist.marketplace } : {}),
1940
- ...(dist.marketplaceSource !== undefined ? { marketplaceSource: dist.marketplaceSource } : {}),
1955
+ ...(marketplaceSource !== undefined ? { marketplaceSource } : {}),
1941
1956
  repo: externalMarketplace ? dist.marketplaceRepo : unit.publicRepo,
1942
1957
  version,
1943
1958
  entrySkill: dist.entrySkill,
@@ -1965,7 +1980,7 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1965
1980
  installed: true,
1966
1981
  plugin: dist.plugin,
1967
1982
  ...(dist.marketplace !== undefined ? { marketplace: dist.marketplace } : {}),
1968
- ...(dist.marketplaceSource !== undefined ? { marketplaceSource: dist.marketplaceSource } : {}),
1983
+ ...(marketplaceSource !== undefined ? { marketplaceSource } : {}),
1969
1984
  version,
1970
1985
  entrySkill: dist.entrySkill,
1971
1986
  ...(externalMarketplace ? { marketplaceLocation: 'external', repo: dist.marketplaceRepo } : {}),
@@ -2127,6 +2142,7 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
2127
2142
  // entrySkillFound/manifestDigest expected). Marketplace identity follows
2128
2143
  // the distribution declaration, same as the non-production loop.
2129
2144
  for (const platform of PLATFORMS) {
2145
+ if (platform.actionType === null) continue;
2130
2146
  const dist = frozenUnitDists
2131
2147
  ? frozenUnitDists.find((d) => d.type === platform.distributionType)
2132
2148
  : (unit.distributions ?? []).find((d) => d.type === platform.distributionType);
@@ -2143,6 +2159,10 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
2143
2159
  // bind this unit's own frozen snapshot — the payload authority is the unit
2144
2160
  // snapshot, unchanged. Inline form (no marketplaceRepo) is byte-identical.
2145
2161
  const externalMarketplace = dist.marketplaceRepo !== undefined && dist.marketplaceRepo !== null;
2162
+ const marketplaceSource = dist.marketplaceSource
2163
+ ?? (platform.id === 'qoder' && externalMarketplace
2164
+ ? `https://github.com/${dist.marketplaceRepo}.git`
2165
+ : undefined);
2146
2166
  const freeze = externalMarketplace ? externalFreezes.get(`${unit.id} ${dist.type}`) : null;
2147
2167
  // Normalized marketplace form: explicit mutually exclusive declaration.
2148
2168
  // All four platforms carry marketplaceForm and sourceDescriptor.
@@ -2181,7 +2201,7 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
2181
2201
  consumer: platform.id,
2182
2202
  plugin: dist.plugin,
2183
2203
  ...(dist.marketplace !== undefined ? { marketplace: dist.marketplace } : {}),
2184
- ...(dist.marketplaceSource !== undefined ? { marketplaceSource: dist.marketplaceSource } : {}),
2204
+ ...(marketplaceSource !== undefined ? { marketplaceSource } : {}),
2185
2205
  repo: externalMarketplace ? dist.marketplaceRepo : unit.publicRepo,
2186
2206
  ref: externalMarketplace ? freeze.ref : resolvedTag,
2187
2207
  version: unitVersion,
@@ -2225,7 +2245,7 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
2225
2245
  consumer: platform.id,
2226
2246
  plugin: dist.plugin,
2227
2247
  ...(dist.marketplace !== undefined ? { marketplace: dist.marketplace } : {}),
2228
- ...(dist.marketplaceSource !== undefined ? { marketplaceSource: dist.marketplaceSource } : {}),
2248
+ ...(marketplaceSource !== undefined ? { marketplaceSource } : {}),
2229
2249
  repo: externalMarketplace ? dist.marketplaceRepo : unit.publicRepo,
2230
2250
  version: unitVersion,
2231
2251
  ref: externalMarketplace ? freeze.ref : resolvedTag,
@@ -2613,6 +2633,35 @@ async function resolveDistributionManifestFacts(unitResults, resolvedVersions) {
2613
2633
  if (!platform || dist.type === 'npm') continue;
2614
2634
  const key = `${unit.id} ${dist.type}`;
2615
2635
 
2636
+ if (platform.installMethod === 'foundation-host-verification') {
2637
+ assertCursorHostScenario(dist.hostVerification);
2638
+ const entrySkillPath = resolve(snapshotDir, 'skills', dist.entrySkill, 'SKILL.md');
2639
+ let entrySkillStat;
2640
+ try {
2641
+ entrySkillStat = await lstat(entrySkillPath);
2642
+ } catch (err) {
2643
+ throw new ReleaseError(
2644
+ GATE_FAILED,
2645
+ `unit "${unit.id}" ${dist.type} entry skill "${dist.entrySkill}" is absent from the frozen payload`,
2646
+ { unitId: unit.id, distributionType: dist.type, cause: err.code },
2647
+ );
2648
+ }
2649
+ if (!entrySkillStat.isFile() || entrySkillStat.isSymbolicLink()) {
2650
+ throw new ReleaseError(
2651
+ GATE_FAILED,
2652
+ `unit "${unit.id}" ${dist.type} entry skill must be a regular non-symlink SKILL.md`,
2653
+ { unitId: unit.id, distributionType: dist.type },
2654
+ );
2655
+ }
2656
+ distributionFacts.set(key, {
2657
+ kind: 'host-verification',
2658
+ platform,
2659
+ entrySkill: dist.entrySkill,
2660
+ entrySkillRelativePath: `skills/${dist.entrySkill}/SKILL.md`,
2661
+ });
2662
+ continue;
2663
+ }
2664
+
2616
2665
  // 校验 marketplaceSourceType:从配置读取,不允许硬编码默认值
2617
2666
  const sourceTypeResult = validateMarketplaceSourceSelection(platform.id, dist, dist);
2618
2667
  if (!sourceTypeResult.valid) {
@@ -2829,6 +2878,7 @@ async function runPrepareSkillResourceClosureGate({
2829
2878
  (p) => p.distributionType === distribution.type,
2830
2879
  );
2831
2880
  if (!platform) continue;
2881
+ if (platform.installMethod === 'foundation-host-verification') continue;
2832
2882
  const frozenManifest = frozenManifestByDist.get(
2833
2883
  `${unit.id} ${distribution.type}`,
2834
2884
  );
@@ -2903,6 +2953,7 @@ async function runPrepareSkillResourceClosureGate({
2903
2953
  (p) => p.distributionType === distribution.type,
2904
2954
  );
2905
2955
  if (!platform) continue;
2956
+ if (platform.installMethod === 'foundation-host-verification') continue;
2906
2957
  expectedHosts.push(await normalizeHostId(platform.buildAdapter.name));
2907
2958
  }
2908
2959
  const hostCoverage = evaluateDeclaredHostSurfaceCoverage(
@@ -4267,7 +4318,7 @@ export async function prepareRelease(options) {
4267
4318
  const { unitResults, snapshotDigests } = await processSnapshots(
4268
4319
  selectedConfig, realRoot, evidence, runDir, production,
4269
4320
  );
4270
- await validateFrozenLocalHostUpdatePlugins(unitResults);
4321
+ await validateFrozenLocalHostUpdatePlugins(unitResults, resolvedVersions);
4271
4322
 
4272
4323
  // Snapshot gates always run on disposable writable copies. The public
4273
4324
  // snapshot authority is re-digested after every gate and is never exposed
@@ -4488,6 +4539,20 @@ export async function prepareRelease(options) {
4488
4539
  if (!platform || dist.type === 'npm') return dist;
4489
4540
  const key = `${unit.id} ${dist.type}`;
4490
4541
  const facts = distributionFacts.get(key);
4542
+ if (facts?.kind === 'host-verification') {
4543
+ const { hostVerification, ...frozenDistribution } = dist;
4544
+ return {
4545
+ ...frozenDistribution,
4546
+ hostVerificationContract: {
4547
+ contractVersion: 1,
4548
+ scenario: structuredClone(hostVerification),
4549
+ hostId: facts.platform.id,
4550
+ payloadDigest: productionAssets?.[idx]?.manifestDigest ?? manifest.snapshotDigest,
4551
+ manifestRelativePath: facts.entrySkillRelativePath,
4552
+ entrySkill: facts.entrySkill,
4553
+ },
4554
+ };
4555
+ }
4491
4556
 
4492
4557
  // includeMarketplaceEntry 代表"契约实际包含一条市场条目",
4493
4558
  // 不能只代表平台理论上支持市场条目。
@@ -662,6 +662,7 @@ export async function publishRelease(options) {
662
662
  for (const dist of frozenUnit?.distributions ?? []) {
663
663
  const platform = PLATFORMS.find((p) => p.distributionType === dist.type);
664
664
  if (!platform) continue;
665
+ if (platform.installMethod === 'foundation-host-verification') continue;
665
666
  const contract = dist.installationContract;
666
667
  if (!contract?.normalizedManifest) continue;
667
668
  const pluginRoot = pluginRootFromManifestRelativePath(contract.manifestRelativePath);
@@ -696,7 +697,9 @@ export async function publishRelease(options) {
696
697
  const expectedHosts = [];
697
698
  for (const distribution of frozenUnit?.distributions ?? []) {
698
699
  const platform = PLATFORMS.find((item) => item.distributionType === distribution.type);
699
- if (platform) expectedHosts.push(await normalizeHostId(platform.buildAdapter.name));
700
+ if (platform && platform.installMethod !== 'foundation-host-verification') {
701
+ expectedHosts.push(await normalizeHostId(platform.buildAdapter.name));
702
+ }
700
703
  }
701
704
  const hostCoverage = evaluateDeclaredHostSurfaceCoverage(
702
705
  expectedHosts,