@clossys/launcher 0.3.1 → 0.4.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 (285) hide show
  1. package/README.md +1324 -60
  2. package/contracts/conversation-contract.md +2 -2
  3. package/contracts/product-ci-workflow.yml +74 -0
  4. package/contracts/repository-inventory.json +53 -0
  5. package/dist/admission-fixture.d.ts +168 -0
  6. package/dist/admission-fixture.d.ts.map +1 -0
  7. package/dist/admission-fixture.js +453 -0
  8. package/dist/admission-fixture.js.map +1 -0
  9. package/dist/admission.d.ts +124 -0
  10. package/dist/admission.d.ts.map +1 -0
  11. package/dist/admission.js +799 -0
  12. package/dist/admission.js.map +1 -0
  13. package/dist/agents-guide.d.ts +9 -0
  14. package/dist/agents-guide.d.ts.map +1 -0
  15. package/dist/agents-guide.js +26 -0
  16. package/dist/agents-guide.js.map +1 -0
  17. package/dist/apply-command-options.check.d.ts +12 -0
  18. package/dist/apply-command-options.check.d.ts.map +1 -0
  19. package/dist/apply-command-options.check.js +20 -0
  20. package/dist/apply-command-options.check.js.map +1 -0
  21. package/dist/apply-plan-cli.d.ts +39 -1
  22. package/dist/apply-plan-cli.d.ts.map +1 -1
  23. package/dist/apply-plan-cli.js +432 -15
  24. package/dist/apply-plan-cli.js.map +1 -1
  25. package/dist/apply-plan.d.ts +46 -59
  26. package/dist/apply-plan.d.ts.map +1 -1
  27. package/dist/apply-plan.js +112 -97
  28. package/dist/apply-plan.js.map +1 -1
  29. package/dist/apply-step-fixture.d.ts +87 -0
  30. package/dist/apply-step-fixture.d.ts.map +1 -0
  31. package/dist/apply-step-fixture.js +199 -0
  32. package/dist/apply-step-fixture.js.map +1 -0
  33. package/dist/apply-store.d.ts +93 -0
  34. package/dist/apply-store.d.ts.map +1 -0
  35. package/dist/apply-store.js +625 -0
  36. package/dist/apply-store.js.map +1 -0
  37. package/dist/approval-sheet.d.ts +21 -0
  38. package/dist/approval-sheet.d.ts.map +1 -0
  39. package/dist/approval-sheet.js +157 -0
  40. package/dist/approval-sheet.js.map +1 -0
  41. package/dist/body-command.d.ts +42 -0
  42. package/dist/body-command.d.ts.map +1 -0
  43. package/dist/body-command.js +143 -0
  44. package/dist/body-command.js.map +1 -0
  45. package/dist/change-set-contract.d.ts +381 -0
  46. package/dist/change-set-contract.d.ts.map +1 -0
  47. package/dist/change-set-contract.js +738 -0
  48. package/dist/change-set-contract.js.map +1 -0
  49. package/dist/change-set-digest.d.ts +28 -0
  50. package/dist/change-set-digest.d.ts.map +1 -0
  51. package/dist/change-set-digest.js +65 -0
  52. package/dist/change-set-digest.js.map +1 -0
  53. package/dist/check-cli.d.ts.map +1 -1
  54. package/dist/check-cli.js +14 -3
  55. package/dist/check-cli.js.map +1 -1
  56. package/dist/cli.d.ts +17 -6
  57. package/dist/cli.d.ts.map +1 -1
  58. package/dist/cli.js +84 -23
  59. package/dist/cli.js.map +1 -1
  60. package/dist/core.d.ts +79 -22
  61. package/dist/core.d.ts.map +1 -1
  62. package/dist/core.js +843 -268
  63. package/dist/core.js.map +1 -1
  64. package/dist/dry-materialize.d.ts +63 -0
  65. package/dist/dry-materialize.d.ts.map +1 -0
  66. package/dist/dry-materialize.js +330 -0
  67. package/dist/dry-materialize.js.map +1 -0
  68. package/dist/generated/contract-schema.generated.d.ts +97 -0
  69. package/dist/generated/contract-schema.generated.d.ts.map +1 -0
  70. package/dist/generated/contract-schema.generated.js +496 -0
  71. package/dist/generated/contract-schema.generated.js.map +1 -0
  72. package/dist/generated/package-scope.generated.d.ts +6 -0
  73. package/dist/generated/package-scope.generated.d.ts.map +1 -0
  74. package/dist/generated/package-scope.generated.js +10 -0
  75. package/dist/generated/package-scope.generated.js.map +1 -0
  76. package/dist/generated/plan-contracts.generated.d.ts +3 -0
  77. package/dist/generated/plan-contracts.generated.d.ts.map +1 -0
  78. package/dist/generated/plan-contracts.generated.js +2840 -0
  79. package/dist/generated/plan-contracts.generated.js.map +1 -0
  80. package/dist/host.d.ts.map +1 -1
  81. package/dist/host.js +11 -0
  82. package/dist/host.js.map +1 -1
  83. package/dist/identity.d.ts +15 -0
  84. package/dist/identity.d.ts.map +1 -0
  85. package/dist/identity.js +48 -0
  86. package/dist/identity.js.map +1 -0
  87. package/dist/index.d.ts +34 -5
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +19 -2
  90. package/dist/index.js.map +1 -1
  91. package/dist/inventory-adoption.d.ts +24 -5
  92. package/dist/inventory-adoption.d.ts.map +1 -1
  93. package/dist/inventory-adoption.js +70 -25
  94. package/dist/inventory-adoption.js.map +1 -1
  95. package/dist/inventory-choice.d.ts +40 -0
  96. package/dist/inventory-choice.d.ts.map +1 -0
  97. package/dist/inventory-choice.js +156 -0
  98. package/dist/inventory-choice.js.map +1 -0
  99. package/dist/inventory-contract.d.ts +89 -0
  100. package/dist/inventory-contract.d.ts.map +1 -0
  101. package/dist/inventory-contract.js +121 -0
  102. package/dist/inventory-contract.js.map +1 -0
  103. package/dist/key-editor.d.ts +30 -0
  104. package/dist/key-editor.d.ts.map +1 -0
  105. package/dist/key-editor.js +445 -0
  106. package/dist/key-editor.js.map +1 -0
  107. package/dist/ledger-contract.d.ts +187 -0
  108. package/dist/ledger-contract.d.ts.map +1 -0
  109. package/dist/ledger-contract.js +532 -0
  110. package/dist/ledger-contract.js.map +1 -0
  111. package/dist/ledger-trust.d.ts +90 -0
  112. package/dist/ledger-trust.d.ts.map +1 -0
  113. package/dist/ledger-trust.js +198 -0
  114. package/dist/ledger-trust.js.map +1 -0
  115. package/dist/lockfile-invariants.d.ts +48 -0
  116. package/dist/lockfile-invariants.d.ts.map +1 -0
  117. package/dist/lockfile-invariants.js +375 -0
  118. package/dist/lockfile-invariants.js.map +1 -0
  119. package/dist/lockfile-readers.d.ts +72 -0
  120. package/dist/lockfile-readers.d.ts.map +1 -0
  121. package/dist/lockfile-readers.js +713 -0
  122. package/dist/lockfile-readers.js.map +1 -0
  123. package/dist/lockfile-regen.d.ts +106 -0
  124. package/dist/lockfile-regen.d.ts.map +1 -0
  125. package/dist/lockfile-regen.js +760 -0
  126. package/dist/lockfile-regen.js.map +1 -0
  127. package/dist/lockfile-tool-env.d.ts +29 -0
  128. package/dist/lockfile-tool-env.d.ts.map +1 -0
  129. package/dist/lockfile-tool-env.js +111 -0
  130. package/dist/lockfile-tool-env.js.map +1 -0
  131. package/dist/materialize.d.ts +113 -0
  132. package/dist/materialize.d.ts.map +1 -0
  133. package/dist/materialize.js +840 -0
  134. package/dist/materialize.js.map +1 -0
  135. package/dist/observe-repository.d.ts +90 -0
  136. package/dist/observe-repository.d.ts.map +1 -0
  137. package/dist/observe-repository.js +1367 -0
  138. package/dist/observe-repository.js.map +1 -0
  139. package/dist/plan-bundle-setup-fixture.d.ts +68 -0
  140. package/dist/plan-bundle-setup-fixture.d.ts.map +1 -0
  141. package/dist/plan-bundle-setup-fixture.js +167 -0
  142. package/dist/plan-bundle-setup-fixture.js.map +1 -0
  143. package/dist/plan-bundle.d.ts +250 -0
  144. package/dist/plan-bundle.d.ts.map +1 -0
  145. package/dist/plan-bundle.js +827 -0
  146. package/dist/plan-bundle.js.map +1 -0
  147. package/dist/plan-command.d.ts +29 -0
  148. package/dist/plan-command.d.ts.map +1 -0
  149. package/dist/plan-command.js +493 -0
  150. package/dist/plan-command.js.map +1 -0
  151. package/dist/plan-contract.d.ts +153 -0
  152. package/dist/plan-contract.d.ts.map +1 -0
  153. package/dist/plan-contract.js +61 -0
  154. package/dist/plan-contract.js.map +1 -0
  155. package/dist/plan-digest.d.ts +25 -0
  156. package/dist/plan-digest.d.ts.map +1 -0
  157. package/dist/plan-digest.js +106 -0
  158. package/dist/plan-digest.js.map +1 -0
  159. package/dist/plan-rules.d.ts +23 -0
  160. package/dist/plan-rules.d.ts.map +1 -0
  161. package/dist/plan-rules.js +177 -0
  162. package/dist/plan-rules.js.map +1 -0
  163. package/dist/planned-bundle.d.ts +20 -0
  164. package/dist/planned-bundle.d.ts.map +1 -0
  165. package/dist/planned-bundle.js +191 -0
  166. package/dist/planned-bundle.js.map +1 -0
  167. package/dist/product-repository.d.ts +4 -0
  168. package/dist/product-repository.d.ts.map +1 -1
  169. package/dist/product-repository.js +9 -1
  170. package/dist/product-repository.js.map +1 -1
  171. package/dist/provenance-gate.d.ts +48 -0
  172. package/dist/provenance-gate.d.ts.map +1 -0
  173. package/dist/provenance-gate.js +324 -0
  174. package/dist/provenance-gate.js.map +1 -0
  175. package/dist/pull-request-body.d.ts +45 -0
  176. package/dist/pull-request-body.d.ts.map +1 -0
  177. package/dist/pull-request-body.js +232 -0
  178. package/dist/pull-request-body.js.map +1 -0
  179. package/dist/registry-snapshot.d.ts +141 -0
  180. package/dist/registry-snapshot.d.ts.map +1 -0
  181. package/dist/registry-snapshot.js +483 -0
  182. package/dist/registry-snapshot.js.map +1 -0
  183. package/dist/release-age-edit.d.ts +52 -0
  184. package/dist/release-age-edit.d.ts.map +1 -0
  185. package/dist/release-age-edit.js +413 -0
  186. package/dist/release-age-edit.js.map +1 -0
  187. package/dist/root-entries.d.ts +36 -0
  188. package/dist/root-entries.d.ts.map +1 -0
  189. package/dist/root-entries.js +80 -0
  190. package/dist/root-entries.js.map +1 -0
  191. package/dist/setup-template-scripts.d.ts +34 -0
  192. package/dist/setup-template-scripts.d.ts.map +1 -0
  193. package/dist/setup-template-scripts.js +557 -0
  194. package/dist/setup-template-scripts.js.map +1 -0
  195. package/dist/setup-templates.d.ts +54 -0
  196. package/dist/setup-templates.d.ts.map +1 -0
  197. package/dist/setup-templates.js +427 -0
  198. package/dist/setup-templates.js.map +1 -0
  199. package/dist/skills.d.ts +34 -1
  200. package/dist/skills.d.ts.map +1 -1
  201. package/dist/skills.js +129 -17
  202. package/dist/skills.js.map +1 -1
  203. package/dist/status.d.ts +63 -0
  204. package/dist/status.d.ts.map +1 -0
  205. package/dist/status.js +539 -0
  206. package/dist/status.js.map +1 -0
  207. package/dist/types.d.ts +151 -13
  208. package/dist/types.d.ts.map +1 -1
  209. package/package.json +4 -4
  210. package/skeleton/README.md +14 -9
  211. package/skeleton/package.json +2 -1
  212. package/skill/SKILL.md +23 -7
  213. package/skill-catalogue/advisor/SKILL.md +59 -6
  214. package/skill-catalogue/architect/SKILL.md +2 -2
  215. package/skill-catalogue/bouncer/SKILL.md +2 -2
  216. package/skill-catalogue/builder/SKILL.md +2 -2
  217. package/skill-catalogue/butler/SKILL.md +2 -2
  218. package/skill-catalogue/controller/SKILL.md +2 -2
  219. package/skill-catalogue/customer/SKILL.md +2 -2
  220. package/skill-catalogue/designer/SKILL.md +4 -2
  221. package/skill-catalogue/giver/SKILL.md +2 -2
  222. package/skill-catalogue/influencer/SKILL.md +2 -2
  223. package/skill-catalogue/inspector/SKILL.md +2 -2
  224. package/skill-catalogue/integrator/SKILL.md +2 -2
  225. package/skill-catalogue/keeper/SKILL.md +2 -2
  226. package/skill-catalogue/launcher/SKILL.md +23 -7
  227. package/skill-catalogue/locksmith/SKILL.md +2 -2
  228. package/skill-catalogue/messenger/SKILL.md +2 -2
  229. package/skill-catalogue/observer/SKILL.md +2 -2
  230. package/skill-catalogue/publisher/SKILL.md +2 -2
  231. package/skill-catalogue/starter/SKILL.md +3 -2
  232. package/skill-catalogue/strategist/SKILL.md +12 -4
  233. package/skill-catalogue/writer/SKILL.md +2 -2
  234. package/src/admission-fixture.ts +572 -0
  235. package/src/admission.ts +816 -0
  236. package/src/agents-guide.ts +29 -0
  237. package/src/apply-command-options.check.ts +27 -0
  238. package/src/apply-plan-cli.ts +454 -14
  239. package/src/apply-plan.ts +112 -124
  240. package/src/apply-step-fixture.ts +236 -0
  241. package/src/apply-store.ts +584 -0
  242. package/src/approval-sheet.ts +164 -0
  243. package/src/body-command.ts +162 -0
  244. package/src/change-set-contract.ts +937 -0
  245. package/src/change-set-digest.ts +70 -0
  246. package/src/check-cli.ts +14 -3
  247. package/src/cli.ts +90 -22
  248. package/src/core.ts +973 -275
  249. package/src/dry-materialize.ts +353 -0
  250. package/src/generated/contract-schema.generated.ts +520 -0
  251. package/src/generated/package-scope.generated.ts +10 -0
  252. package/src/generated/plan-contracts.generated.ts +2840 -0
  253. package/src/host.ts +10 -0
  254. package/src/identity.ts +51 -0
  255. package/src/index.ts +72 -3
  256. package/src/inventory-adoption.ts +107 -29
  257. package/src/inventory-choice.ts +172 -0
  258. package/src/inventory-contract.ts +166 -0
  259. package/src/key-editor.ts +446 -0
  260. package/src/ledger-contract.ts +637 -0
  261. package/src/ledger-trust.ts +267 -0
  262. package/src/lockfile-invariants.ts +421 -0
  263. package/src/lockfile-readers.ts +749 -0
  264. package/src/lockfile-regen.ts +851 -0
  265. package/src/lockfile-tool-env.ts +131 -0
  266. package/src/materialize.ts +886 -0
  267. package/src/observe-repository.ts +1365 -0
  268. package/src/plan-bundle-setup-fixture.ts +200 -0
  269. package/src/plan-bundle.ts +964 -0
  270. package/src/plan-command.ts +509 -0
  271. package/src/plan-contract.ts +179 -0
  272. package/src/plan-digest.ts +102 -0
  273. package/src/plan-rules.ts +188 -0
  274. package/src/planned-bundle.ts +211 -0
  275. package/src/product-repository.ts +10 -1
  276. package/src/provenance-gate.ts +352 -0
  277. package/src/pull-request-body.ts +261 -0
  278. package/src/registry-snapshot.ts +534 -0
  279. package/src/release-age-edit.ts +430 -0
  280. package/src/root-entries.ts +81 -0
  281. package/src/setup-template-scripts.ts +571 -0
  282. package/src/setup-templates.ts +471 -0
  283. package/src/skills.ts +161 -18
  284. package/src/status.ts +557 -0
  285. package/src/types.ts +148 -13
package/src/skills.ts CHANGED
@@ -6,10 +6,36 @@ import type { SkillManifestEntry, WorkspaceHost } from "./types.js";
6
6
  export interface SkillCompositionResult {
7
7
  readonly composed: readonly string[];
8
8
  readonly skipped: readonly { readonly packageDir: string; readonly note: string }[];
9
+ /** The checkouts this run composed skills into: the hub. */
9
10
  readonly rosterTargets?: readonly string[];
10
- readonly rosterSkipped?: readonly { readonly inventoryId: string; readonly note: string }[];
11
+ /**
12
+ * Each inventoried repository other than the hub, with what a hub run
13
+ * found for it. A hub run never writes into one: a product repository's
14
+ * team arrives only once it is staffed in an approved plan, with that
15
+ * plan's setup pull request. `inventoryId` is already a stored-inventory
16
+ * position label (e.g. `repositories[0] in the stored inventory`), never
17
+ * the raw id -- populated by ./core.ts's composeSkillRoster; see its own
18
+ * doc comment.
19
+ */
20
+ readonly siblings?: readonly { readonly inventoryId: string; readonly note: string }[];
11
21
  /** Skills pruned this run: present in the directory's previous manifest, absent from this run's composed set. */
12
22
  readonly retired: readonly string[];
23
+ /**
24
+ * Skills this run left exactly as found instead of rewriting or retiring them,
25
+ * because a file at their composed location is not one Launcher can show it
26
+ * wrote (#1473). Each needs the client's decision; see `note`.
27
+ */
28
+ readonly preserved: readonly SkillPreservation[];
29
+ }
30
+
31
+ /** One composed skill left untouched because its on-disk content is not provably Launcher's (#1473). */
32
+ export interface SkillPreservation {
33
+ readonly packageDir: string;
34
+ /** What this run would have done to the skill had Launcher still owned it. */
35
+ readonly action: "rewrite" | "retire";
36
+ /** The file or directory that failed the ownership check, relative to the composed directory. */
37
+ readonly path: string;
38
+ readonly note: string;
13
39
  }
14
40
 
15
41
  export interface ComposeSkillsOptions {
@@ -123,17 +149,14 @@ export function listSkillPackageCandidates(host: WorkspaceHost, options: Compose
123
149
  return [...names].sort((a, b) => a.localeCompare(b));
124
150
  }
125
151
 
126
- function writeAgentsSkill(host: WorkspaceHost, directory: string, packageDir: string, body: string): void {
127
- const relativeDir = join(AGENTS_SKILLS_REL, `clossys-${packageDir}`);
128
- const skillRel = join(relativeDir, "SKILL.md");
129
- const target = containedPath(directory, skillRel);
152
+ function writeAgentsSkill(host: WorkspaceHost, directory: string, packageDir: string, content: string): void {
153
+ const target = containedPath(directory, join(agentsSkillDirRel(packageDir), "SKILL.md"));
130
154
  host.mkdirp(dirname(target));
131
- host.writeText(target, body.endsWith("\n") ? body : `${body}\n`);
155
+ host.writeText(target, content);
132
156
  }
133
157
 
134
- function writeDiscoveryLink(host: WorkspaceHost, directory: string, packageDir: string, skillBody: string): void {
135
- const agentsDirRel = join(AGENTS_SKILLS_REL, `clossys-${packageDir}`);
136
- const linkTarget = join("..", "..", agentsDirRel);
158
+ function writeDiscoveryLink(host: WorkspaceHost, directory: string, packageDir: string, content: string): void {
159
+ const linkTarget = join("..", "..", agentsSkillDirRel(packageDir));
137
160
  for (const prefix of DISCOVERY_PREFIXES) {
138
161
  const prefixPath = containedPath(directory, prefix);
139
162
  // A directory-symlink onto `.agents/skills` already exposes every composed
@@ -148,14 +171,99 @@ function writeDiscoveryLink(host: WorkspaceHost, directory: string, packageDir:
148
171
  } catch {
149
172
  const fallbackDir = containedPath(directory, join(linkRel, "SKILL.md"));
150
173
  host.mkdirp(dirname(fallbackDir));
151
- host.writeText(fallbackDir, skillBody.endsWith("\n") ? skillBody : `${skillBody}\n`);
174
+ host.writeText(fallbackDir, content);
152
175
  }
153
176
  }
154
177
  }
155
178
 
179
+ function agentsSkillDirRel(packageDir: string): string {
180
+ return join(AGENTS_SKILLS_REL, `clossys-${packageDir}`);
181
+ }
182
+
183
+ /**
184
+ * The on-disk locations that hold a copy of this skill's composed content:
185
+ * `.agents/skills/clossys-<pkg>/`, plus any host discovery path that is a real
186
+ * directory rather than a symlink (the copy `writeDiscoveryLink` falls back to
187
+ * when a symlink cannot be made, or a directory someone else put there). A
188
+ * discovery symlink holds no content of its own and is not listed.
189
+ */
190
+ function skillCopyLocations(host: WorkspaceHost, directory: string, packageDir: string): { rel: string; replacedWhole: boolean }[] {
191
+ const locations: { rel: string; replacedWhole: boolean }[] = [{ rel: agentsSkillDirRel(packageDir), replacedWhole: false }];
192
+ for (const prefix of DISCOVERY_PREFIXES) {
193
+ if (host.isSymlink(containedPath(directory, prefix))) continue;
194
+ const rel = join(prefix, `clossys-${packageDir}`);
195
+ const path = containedPath(directory, rel);
196
+ if (host.isSymlink(path) || !host.exists(path)) continue;
197
+ // `host.symlink` removes whatever sits at a discovery path before linking, so
198
+ // everything in that directory is replaced, not just SKILL.md.
199
+ locations.push({ rel, replacedWhole: true });
200
+ }
201
+ return locations;
202
+ }
203
+
204
+ /**
205
+ * Decides whether Launcher may rewrite (`intendedBody` given) or remove
206
+ * (`intendedBody` undefined) a composed skill (#1473). The digest recorded in
207
+ * the previous manifest is the ownership record: a copy whose bytes hash to it
208
+ * is still exactly what Launcher last wrote. A copy byte-identical to what this
209
+ * run would write is also safe to adopt, since nothing would be lost. Anything
210
+ * else is the client's, and is left alone. Returns the first location that
211
+ * fails, or null when every location may be changed.
212
+ */
213
+ function unownedSkillCopy(
214
+ host: WorkspaceHost,
215
+ directory: string,
216
+ packageDir: string,
217
+ recordedSha256: string | undefined,
218
+ intendedBody: string | undefined,
219
+ ): { path: string; location: string; reason: string } | null {
220
+ const removing = intendedBody === undefined;
221
+ for (const location of skillCopyLocations(host, directory, packageDir)) {
222
+ const dirPath = containedPath(directory, location.rel);
223
+ if (!host.exists(dirPath)) continue;
224
+ if (!host.isDirectory(dirPath)) {
225
+ return { path: location.rel, location: location.rel, reason: "is not a directory Launcher wrote" };
226
+ }
227
+ if (removing || location.replacedWhole) {
228
+ // .DS_Store is macOS Finder metadata, written just by viewing the folder; it holds no client content.
229
+ const others = host.readDir(dirPath).filter((name) => name !== "SKILL.md" && name !== ".DS_Store");
230
+ if (others.length > 0) {
231
+ return { path: location.rel, location: location.rel, reason: `holds files Launcher did not write (${others.sort().join(", ")})` };
232
+ }
233
+ }
234
+ const skillRel = join(location.rel, "SKILL.md");
235
+ const skillPath = containedPath(directory, skillRel);
236
+ const current = host.readText(skillPath);
237
+ if (current === null) {
238
+ // readText returns null for any read failure (EACCES, EISDIR, ...), not only a missing file.
239
+ if (!host.exists(skillPath)) continue;
240
+ return { path: skillRel, location: location.rel, reason: "exists but could not be read, so Launcher cannot check it against its digest" };
241
+ }
242
+ if (recordedSha256 !== undefined && sha256Hex(current) === recordedSha256) continue;
243
+ if (intendedBody !== undefined && current === intendedBody) continue;
244
+ return {
245
+ path: skillRel,
246
+ location: location.rel,
247
+ reason:
248
+ recordedSha256 === undefined
249
+ ? "has no digest recorded in clossys/.state/skills.json, so Launcher cannot show it wrote this file"
250
+ : "was edited since Launcher last wrote it (its content no longer matches the digest in clossys/.state/skills.json)",
251
+ };
252
+ }
253
+ return null;
254
+ }
255
+
256
+ function preservationNote(action: "rewrite" | "retire", unowned: { path: string; location: string; reason: string }): string {
257
+ const remedy =
258
+ action === "rewrite"
259
+ ? `to take Launcher's current version instead, move your copy aside, delete \`${unowned.location}\`, and run launcher again`
260
+ : `Launcher no longer composes this skill; delete \`${unowned.location}\` yourself if you no longer want it, and the next run completes the retirement`;
261
+ return `${unowned.path} ${unowned.reason}; left as is -- ${remedy}`;
262
+ }
263
+
156
264
  /** Removes a composed skill's `.agents/skills/` directory and its host discovery links. Missing paths are no-ops. */
157
265
  function removeComposedSkill(host: WorkspaceHost, directory: string, packageDir: string): void {
158
- host.remove(containedPath(directory, join(AGENTS_SKILLS_REL, `clossys-${packageDir}`)));
266
+ host.remove(containedPath(directory, agentsSkillDirRel(packageDir)));
159
267
  for (const prefix of DISCOVERY_PREFIXES) {
160
268
  const prefixPath = containedPath(directory, prefix);
161
269
  if (host.isSymlink(prefixPath)) continue; // shared view onto .agents/skills; already handled above
@@ -174,6 +282,14 @@ function removeComposedSkill(host: WorkspaceHost, directory: string, packageDir:
174
282
  * but not composed this run is retired — its composed output is pruned and it is
175
283
  * dropped from the new manifest. Never touches a skill this directory's manifest
176
284
  * did not itself write.
285
+ *
286
+ * Before rewriting or retiring a skill, the on-disk copy is checked against the
287
+ * digest that manifest recorded (#1473). A copy that no longer matches (the
288
+ * client edited it), or one with no recorded digest that differs from what this
289
+ * run would write, is left exactly as found and reported in `preserved`; its
290
+ * previous manifest entry, if any, is carried forward unchanged so the next run
291
+ * checks it again. A missing copy is recreated: `.agents/skills` is
292
+ * Launcher-generated output, and recreating it loses no client content.
177
293
  */
178
294
  export function composeSkills(
179
295
  host: WorkspaceHost,
@@ -182,9 +298,13 @@ export function composeSkills(
182
298
  ): SkillCompositionResult {
183
299
  const composed: string[] = [];
184
300
  const skipped: { packageDir: string; note: string }[] = [];
301
+ const preserved: SkillPreservation[] = [];
185
302
  const manifestEntries: SkillManifestEntry[] = [];
186
303
  const contractBlock = readContractBlock(host, options);
187
304
  const launcherVersion = readLauncherVersion(host, options);
305
+ const previousManifest = parseSkillManifest(host.readText(join(directory, SKILLS_MANIFEST_REL)));
306
+ const previousEntries = new Map((previousManifest?.skills ?? []).map((entry) => [entry.name, entry]));
307
+ const handled = new Set<string>();
188
308
 
189
309
  for (const packageDir of listSkillPackageCandidates(host, options)) {
190
310
  const found = readSkillBody(host, packageDir, options, directory);
@@ -193,23 +313,46 @@ export function composeSkills(
193
313
  continue;
194
314
  }
195
315
  const body = contractBlock === null ? found.body : injectContract(found.body, contractBlock);
196
- writeAgentsSkill(host, directory, packageDir, body);
197
- writeDiscoveryLink(host, directory, packageDir, body);
316
+ const content = body.endsWith("\n") ? body : `${body}\n`;
317
+ const previous = previousEntries.get(packageDir);
318
+ handled.add(packageDir);
319
+ const unowned = unownedSkillCopy(host, directory, packageDir, previous?.sha256, content);
320
+ if (unowned !== null) {
321
+ preserved.push({
322
+ packageDir,
323
+ action: "rewrite",
324
+ path: unowned.path,
325
+ note: preservationNote("rewrite", unowned),
326
+ });
327
+ if (previous !== undefined) manifestEntries.push(previous);
328
+ continue;
329
+ }
330
+ writeAgentsSkill(host, directory, packageDir, content);
331
+ writeDiscoveryLink(host, directory, packageDir, content);
198
332
  composed.push(packageDir);
199
333
  const version = found.source === "installed" ? readInstalledVersion(host, directory, packageDir) : launcherVersion;
200
334
  manifestEntries.push({
201
335
  name: packageDir,
202
336
  source: found.source,
203
- sha256: sha256Hex(body.endsWith("\n") ? body : `${body}\n`),
337
+ sha256: sha256Hex(content),
204
338
  ...(version === undefined ? {} : { version }),
205
339
  });
206
340
  }
207
341
 
208
- const previousManifest = parseSkillManifest(host.readText(join(directory, SKILLS_MANIFEST_REL)));
209
- const composedSet = new Set(composed);
210
342
  const retired: string[] = [];
211
343
  for (const entry of previousManifest?.skills ?? []) {
212
- if (composedSet.has(entry.name)) continue;
344
+ if (handled.has(entry.name)) continue;
345
+ const unowned = unownedSkillCopy(host, directory, entry.name, entry.sha256, undefined);
346
+ if (unowned !== null) {
347
+ preserved.push({
348
+ packageDir: entry.name,
349
+ action: "retire",
350
+ path: unowned.path,
351
+ note: preservationNote("retire", unowned),
352
+ });
353
+ manifestEntries.push(entry);
354
+ continue;
355
+ }
213
356
  removeComposedSkill(host, directory, entry.name);
214
357
  retired.push(entry.name);
215
358
  }
@@ -221,5 +364,5 @@ export function composeSkills(
221
364
  serializeSkillManifest({ schemaVersion: 1, generatedAt: host.now(), skills: manifestEntries }),
222
365
  );
223
366
 
224
- return { composed, skipped, retired };
367
+ return { composed, skipped, retired, preserved };
225
368
  }