@djordje-stojanovic/sigmaskills 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to [Sigmaskills](https://github.com/Djordje-Stojanovic/Sigma
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.6.1] — 2026-10-06
8
+
9
+ ### Changed
10
+
11
+ - A changed front-matter `description` in `SKILL.md` now counts as a Skill Customization. `status` reports it as `valid-customization`, not as corruption. `update --yes` keeps your description when it installs a new Release. If the new Release changes the description itself, `update` still asks for `--outside-edit` first. (#94)
12
+
7
13
  ## [0.6.0] — 2026-10-06
8
14
 
9
15
  ### Added
@@ -226,7 +232,8 @@ Ship high-rigor, installable agent skills (not apps): full-repo audits, performa
226
232
  | 2026-08-11 | `5dab832` | Add CHANGELOG and mark v0.1.0 as the first Sigmaskills release |
227
233
  | 2026-08-11 | `124a666` | Polish v0.1.0 docs for multi-host install and use |
228
234
 
229
- [Unreleased]: https://github.com/Djordje-Stojanovic/Sigmaskills/compare/v0.6.0...HEAD
235
+ [Unreleased]: https://github.com/Djordje-Stojanovic/Sigmaskills/compare/v0.6.1...HEAD
236
+ [0.6.1]: https://github.com/Djordje-Stojanovic/Sigmaskills/compare/v0.6.0...v0.6.1
230
237
  [0.6.0]: https://github.com/Djordje-Stojanovic/Sigmaskills/compare/v0.5.1...v0.6.0
231
238
  [0.5.1]: https://github.com/Djordje-Stojanovic/Sigmaskills/compare/v0.5.0...v0.5.1
232
239
  [0.5.0]: https://github.com/Djordje-Stojanovic/Sigmaskills/compare/v0.4.0...v0.5.0
package/README.md CHANGED
@@ -6,7 +6,7 @@ Portable [Agent Skills](https://agentskills.io/) that install once and run on **
6
6
 
7
7
  | | |
8
8
  |---|---|
9
- | **Release** | [**v0.6.0**](https://github.com/Djordje-Stojanovic/Sigmaskills/releases/tag/v0.6.0) |
9
+ | **Release** | [**v0.6.1**](https://github.com/Djordje-Stojanovic/Sigmaskills/releases/tag/v0.6.1) |
10
10
  | **Changelog** | [CHANGELOG.md](CHANGELOG.md) |
11
11
  | **License** | [MIT](LICENSE) |
12
12
  | **Spec** | [agentskills.io](https://agentskills.io/) |
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "name": "sigmaskills",
4
- "version": "0.6.0",
4
+ "version": "0.6.1",
5
5
  "description": "Portable Agent Skills monorepo and first-party Sigma Installer",
6
6
  "skills": [
7
7
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djordje-stojanovic/sigmaskills",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Portable Agent Skills monorepo and first-party Sigma Installer",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -71,6 +71,16 @@
71
71
  "references/brief-method.md": "1bd9200bec7d41106bdff4f0ab39b86597bb2524b8d37af0eb663e9ec854770e",
72
72
  "references/prompt-contract.md": "198bc5ab7a4c27fa14f3c168fe2f79bf1851e1ab7ef89cd95e551df5e398485a"
73
73
  }
74
+ },
75
+ {
76
+ "release": "0.6.0",
77
+ "revision": "4ec9b13a2e3e0cf10f6f218ee5e733f290575f8870bf56feb29e7d69e6b884c0",
78
+ "files": {
79
+ "SKILL.md": "442f9ba7785a30510df995b85b89b95f429d3dd5fe051dc85850309d6361d9e5",
80
+ "agents/openai.yaml": "69c0219b0f8ab7974527ca9f6fb06b67b21683a42ac1de63ef6014cdef1dde85",
81
+ "references/brief-method.md": "1bd9200bec7d41106bdff4f0ab39b86597bb2524b8d37af0eb663e9ec854770e",
82
+ "references/prompt-contract.md": "198bc5ab7a4c27fa14f3c168fe2f79bf1851e1ab7ef89cd95e551df5e398485a"
83
+ }
74
84
  }
75
85
  ],
76
86
  "sigmaperformance": [
@@ -192,6 +202,17 @@
192
202
  "references/method.md": "659848b0d8a7e2dbcbde3fa88de09d07f20b68bb91ea808e766d526b137adc03",
193
203
  "references/report.md": "ebabc522ac35dcfd7538abe4657d4933bf795d5ea20242a968592e0c569e7b41"
194
204
  }
205
+ },
206
+ {
207
+ "release": "0.6.0",
208
+ "revision": "5ce9fb447bb7c3035c98cc3c8d2e46f960f93fb5ca45482a7c94233f1929e196",
209
+ "files": {
210
+ "SKILL.md": "d900e0d033861065542308c966a9e2e2ec5322a70ce2614aba6e8bddb7d9fb07",
211
+ "agents/openai.yaml": "fc9f585d84b751aa9978d3b275a9f8b0383a1d57cb49fec27e905e794fa14d5a",
212
+ "references/measure.md": "efd083364caedfce9c5e69a2267209d3317cfa218029b48455e49cdc2f9affe0",
213
+ "references/method.md": "659848b0d8a7e2dbcbde3fa88de09d07f20b68bb91ea808e766d526b137adc03",
214
+ "references/report.md": "ebabc522ac35dcfd7538abe4657d4933bf795d5ea20242a968592e0c569e7b41"
215
+ }
195
216
  }
196
217
  ],
197
218
  "sigmawrite": [
@@ -250,6 +271,14 @@
250
271
  "SKILL.md": "8db1872ac240efceec949d6b5e13c651b38e1de7b4c82588375c7dcd8f123cb1",
251
272
  "agents/openai.yaml": "19b53dc100195c2d8cfd03720028f577ce8e3ba95829551c6d31d96a23021144"
252
273
  }
274
+ },
275
+ {
276
+ "release": "0.6.0",
277
+ "revision": "ff7cf7f2ff2851b916a5ed4aa856b0b9421095e94e60e8fe88fa0614b363cc9c",
278
+ "files": {
279
+ "SKILL.md": "8db1872ac240efceec949d6b5e13c651b38e1de7b4c82588375c7dcd8f123cb1",
280
+ "agents/openai.yaml": "19b53dc100195c2d8cfd03720028f577ce8e3ba95829551c6d31d96a23021144"
281
+ }
253
282
  }
254
283
  ],
255
284
  "sigmarefactor": [
@@ -288,6 +317,15 @@
288
317
  "agents/openai.yaml": "baaa351482edffe072ae708968dcc1c70842caebf38a8aa7fd667b8da7a01c70",
289
318
  "references/scan.md": "049715a6948138109b64994e969a1ed5541cab8151b8c8d9070d5eae379d45c5"
290
319
  }
320
+ },
321
+ {
322
+ "release": "0.6.0",
323
+ "revision": "25b4efaa5d9d9309791dbbec0f1a32eab409137c4cd16892f646714019b04115",
324
+ "files": {
325
+ "SKILL.md": "a6bef0c53cf780c79da20204cbbd15db65fa98dac3afa0ef1c8475725cd7f4cb",
326
+ "agents/openai.yaml": "baaa351482edffe072ae708968dcc1c70842caebf38a8aa7fd667b8da7a01c70",
327
+ "references/scan.md": "049715a6948138109b64994e969a1ed5541cab8151b8c8d9070d5eae379d45c5"
328
+ }
291
329
  }
292
330
  ],
293
331
  "sigmaimprove": [
@@ -323,6 +361,17 @@
323
361
  "references/issue.md": "59300698c9a1d32e1d868173b22dea5fcc7ea7b7ca97198faf688256e5638300",
324
362
  "references/lenses.md": "c5f2982ed1f4330035895dabacef550a5c1b241e0e1a370547b8715f89f3f8b3"
325
363
  }
364
+ },
365
+ {
366
+ "release": "0.6.0",
367
+ "revision": "9efbc885f031786dd84637009a3b8c38806c50d587b84799692fe60828c32da0",
368
+ "files": {
369
+ "SKILL.md": "118dacdc212f6f359ad9be0564492451bb2dc93f1f03c8af0094994caeff3060",
370
+ "agents/openai.yaml": "ba80d59887cef951c006ad52ddd42fa1999d12b464dff17b6bb0b711f7eb505f",
371
+ "references/categories.md": "58ad43e0da22b5a67f0bec01abc13760ffa835dec9efc07343b07e8326cba7bd",
372
+ "references/issue.md": "59300698c9a1d32e1d868173b22dea5fcc7ea7b7ca97198faf688256e5638300",
373
+ "references/lenses.md": "c5f2982ed1f4330035895dabacef550a5c1b241e0e1a370547b8715f89f3f8b3"
374
+ }
326
375
  }
327
376
  ],
328
377
  "sigmaship": [
@@ -355,6 +404,40 @@
355
404
  "references/land.md": "a1ae956d9017a9b67af68d6d71521d969b78e97cc1f9c3997d4bcedf9d69465f",
356
405
  "references/review-round.md": "e9062112683c71c28dd3bee5329afd6b9bd555ac83a1643b5f7ae5946b09df3a"
357
406
  }
407
+ },
408
+ {
409
+ "release": "0.6.0",
410
+ "revision": "248e37ea81d74561de3600e936143dbd1b7ae5543492fb7f1f8770f61182e090",
411
+ "files": {
412
+ "SKILL.md": "572d6c64273cde938ecdb9e1bccb39bf6d72e21fc386704035a75b18930ee315",
413
+ "agents/openai.yaml": "29f73f7ef65b71dfd4efa27dfef26a961dae98eee2882c63084528b7abef3b96",
414
+ "references/land.md": "a1ae956d9017a9b67af68d6d71521d969b78e97cc1f9c3997d4bcedf9d69465f",
415
+ "references/review-round.md": "e9062112683c71c28dd3bee5329afd6b9bd555ac83a1643b5f7ae5946b09df3a"
416
+ }
417
+ }
418
+ ],
419
+ "easytalk": [
420
+ {
421
+ "release": "0.6.0",
422
+ "revision": "7e945683bf985978a80f8bddc752d9358063281729d306f537d2f542b7223fcf",
423
+ "files": {
424
+ "SKILL.md": "88d7dc079348c00ae89f5395403d93be10b45fe319799ef62ccb303c4ef64d7c",
425
+ "agents/openai.yaml": "2b37c8e632553692e561b7d1de7aa2d00ab7612136e5f873934bd49395c08c9e",
426
+ "template.html": "e59ee52b94e26f2727f4b007af4c29bc20c0c048775b74caa7fbd53988b94483"
427
+ }
428
+ }
429
+ ],
430
+ "sigmaresearch": [
431
+ {
432
+ "release": "0.6.0",
433
+ "revision": "7a69af76abe0c396745776dd75f94cef3e877642fe187427e4f32be92437cf7c",
434
+ "files": {
435
+ "SKILL.md": "76e09762fe34e212c9d6cd191b2a3773bad6e4446822a7e4a0d9375998ecffa0",
436
+ "agents/openai.yaml": "151f7a91edc15ded3969ade41ff3766c35bb73c4a0067bec4af77b42001265a3",
437
+ "references/public-sources.md": "97c328c261953dd19332abc3946d1c272a2572755daa61bbf93560d765667f0b",
438
+ "references/reddit.md": "0dfd0d2a78e40cf84103b3e0cfe29d8264c98d73b97b139e4b9f1738fdd52fcb",
439
+ "references/x.md": "63f24a0fb1f80e55d93ded34ba18d3721dea26f83fbaaa7041662278ec4577cc"
440
+ }
358
441
  }
359
442
  ]
360
443
  }
@@ -318,6 +318,61 @@ export function injectRawCustomContent(baseMarkdownContent, rawCustomContent, sk
318
318
  return `${baseMarkdownContent.slice(0, startIndex + CUSTOM_BLOCK_START.length)}${rawCustomContent}${baseMarkdownContent.slice(endIndex)}`;
319
319
  }
320
320
 
321
+ function descriptionSpan(markdown) {
322
+ const lines = markdown.split(/(?<=\n)/);
323
+ const bare = (line) => line.replace(/\r?\n$/, '');
324
+ if (bare(lines[0] || '') !== '---') return null;
325
+ const close = lines.findIndex((line, index) => index > 0 && bare(line) === '---');
326
+ const start = lines.findIndex((line, index) => index > 0 && index < close && /^description\s*:/.test(line));
327
+ if (close === -1 || start === -1) return null;
328
+ // Continuation lines are indented; blank lines inside a block scalar belong to it too.
329
+ let stop = start + 1;
330
+ for (let index = start + 1; index < close; index += 1) {
331
+ if (/^[ \t]+\S/.test(lines[index])) stop = index + 1;
332
+ else if (bare(lines[index]).trim() !== '') break;
333
+ }
334
+ return { lines, start, stop };
335
+ }
336
+
337
+ /**
338
+ * The raw front-matter `description` entry: its key line and indented continuation lines.
339
+ *
340
+ * @param {string} markdown
341
+ * @returns {string|null}
342
+ */
343
+ export function readDescriptionEntry(markdown) {
344
+ const span = typeof markdown === 'string' ? descriptionSpan(markdown) : null;
345
+ return span ? span.lines.slice(span.start, span.stop).join('') : null;
346
+ }
347
+
348
+ /**
349
+ * Put a raw `description` entry into the front matter, in the file's own line endings.
350
+ * Returns the markdown unchanged when it has no description entry or no entry is given.
351
+ *
352
+ * @param {string} markdown
353
+ * @param {string|null} entry
354
+ * @returns {string}
355
+ */
356
+ export function replaceDescriptionEntry(markdown, entry) {
357
+ const span = typeof entry === 'string' ? descriptionSpan(markdown) : null;
358
+ if (!span) return markdown;
359
+ const eol = span.lines[0].endsWith('\r\n') ? '\r\n' : '\n';
360
+ const lines = [...span.lines];
361
+ lines.splice(span.start, span.stop - span.start, entry.replace(/\r?\n/g, eol));
362
+ return lines.join('');
363
+ }
364
+
365
+ /**
366
+ * The live markdown with the official front-matter description in place of the local one.
367
+ *
368
+ * @param {string} liveMarkdown
369
+ * @param {string|null} officialMarkdown
370
+ * @returns {string}
371
+ */
372
+ export function withOfficialDescription(liveMarkdown, officialMarkdown) {
373
+ return replaceDescriptionEntry(liveMarkdown, readDescriptionEntry(officialMarkdown));
374
+ }
375
+
321
376
  export function escapeRegExp(string) {
322
377
  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
323
378
  }
package/src/status.js CHANGED
@@ -3,7 +3,12 @@ import path from 'node:path';
3
3
  import { walkLiveFiles } from './backup.js';
4
4
  import { findPackageRoot } from './catalog.js';
5
5
  import { isPathInside } from './paths.js';
6
- import { inspectCustomizationBlock, CUSTOM_BLOCK_END, CUSTOM_BLOCK_START } from './customization.js';
6
+ import {
7
+ inspectCustomizationBlock,
8
+ withOfficialDescription,
9
+ CUSTOM_BLOCK_END,
10
+ CUSTOM_BLOCK_START,
11
+ } from './customization.js';
7
12
  import {
8
13
  UNIVERSAL_PROJECT_DESTINATION,
9
14
  listGlobalDestinationGroups,
@@ -60,16 +65,21 @@ function officialMarkdownShell(markdown) {
60
65
  return `${markdown.slice(0, start + CUSTOM_BLOCK_START.length)}${markdown.slice(end)}`;
61
66
  }
62
67
 
63
- function inspectSkillMarkdown(liveMarkdown, skillId, liveHash, bundledMarkdown, bundledHash) {
68
+ function inspectSkillMarkdown(liveMarkdown, skillId, liveHash, bundledMarkdown, bundledHash, installedHash) {
64
69
  const inspection = inspectCustomizationBlock(liveMarkdown, skillId);
65
- if (inspection.status !== 'valid') {
70
+ // An untouched copy of the installed Release is not a local description, even when the new Release changed it.
71
+ const localEdit = Boolean(installedHash) && liveHash !== installedHash;
72
+ const live = inspection.status === 'malformed' || !localEdit
73
+ ? liveMarkdown
74
+ : withOfficialDescription(liveMarkdown, bundledMarkdown);
75
+ if (inspection.status !== 'valid' && live === liveMarkdown) {
66
76
  return {
67
77
  status: inspection.status,
68
78
  officialHash: liveHash,
69
79
  hasCustomContent: false,
70
80
  };
71
81
  }
72
- if (typeof bundledMarkdown === 'string' && officialMarkdownShell(liveMarkdown) === officialMarkdownShell(bundledMarkdown)) {
82
+ if (typeof bundledMarkdown === 'string' && officialMarkdownShell(live) === officialMarkdownShell(bundledMarkdown)) {
73
83
  return {
74
84
  status: 'valid',
75
85
  officialHash: bundledHash || liveHash,
@@ -87,7 +97,7 @@ function isPackagedResource(file) {
87
97
  return /^(references|scripts|assets|agents)\//.test(file);
88
98
  }
89
99
 
90
- function classifyLiveTree({ liveFiles, bundledFiles, skillId, skillMarkdown, bundledMarkdown }) {
100
+ function classifyLiveTree({ liveFiles, bundledFiles, skillId, skillMarkdown, bundledMarkdown, installedFiles }) {
91
101
  const classifications = [];
92
102
  const officialFiles = { ...liveFiles };
93
103
 
@@ -98,6 +108,7 @@ function classifyLiveTree({ liveFiles, bundledFiles, skillId, skillMarkdown, bun
98
108
  liveFiles['SKILL.md'],
99
109
  bundledMarkdown,
100
110
  bundledFiles['SKILL.md'],
111
+ installedFiles?.['SKILL.md'],
101
112
  );
102
113
  officialFiles['SKILL.md'] = markdown.officialHash;
103
114
  if (markdown.status === 'malformed') classifications.push('malformed-markers');
@@ -308,6 +319,7 @@ export function collectStatus(options = {}) {
308
319
  skillId,
309
320
  skillMarkdown,
310
321
  bundledMarkdown,
322
+ installedFiles: copyEntry?.baseHashes || entry?.baseHashes,
311
323
  });
312
324
  destinations.push({
313
325
  relativeDestination,
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import { acquireFileLock } from './concurrency-lock.js';
3
3
  import path from 'node:path';
4
4
  import { findPackageRoot, validateSkill } from './catalog.js';
5
- import { injectRawCustomContent } from './customization.js';
5
+ import { injectRawCustomContent, replaceDescriptionEntry } from './customization.js';
6
6
  import { commitSkillBackup, exportSkillTree, getBackupRoot, pruneOlderBackups } from './backup.js';
7
7
  import { createInstallPlan } from './plan.js';
8
8
  import { isForeignProjectLock, loadProjectLock, saveProjectLock, updateProjectLockSkill, PROJECT_LOCK_FILENAME } from './project-lock.js';
@@ -254,13 +254,16 @@ function stageSkill(tx) {
254
254
  );
255
255
  }
256
256
  fileHashes = validatedStaged.files;
257
- if (params.preservedCustomRaw !== undefined) {
257
+ if (params.preservedCustomRaw !== undefined || params.preservedDescription !== undefined) {
258
258
  const stagedSkillMd = path.join(stagingDir, 'SKILL.md');
259
259
  if (pathExists(stagedSkillMd)) {
260
- const stagedMarkdown = fs.readFileSync(stagedSkillMd, 'utf8');
260
+ let stagedMarkdown = fs.readFileSync(stagedSkillMd, 'utf8');
261
+ if (params.preservedCustomRaw !== undefined) {
262
+ stagedMarkdown = injectRawCustomContent(stagedMarkdown, params.preservedCustomRaw, skillId);
263
+ }
261
264
  fs.writeFileSync(
262
265
  stagedSkillMd,
263
- injectRawCustomContent(stagedMarkdown, params.preservedCustomRaw, skillId),
266
+ replaceDescriptionEntry(stagedMarkdown, params.preservedDescription),
264
267
  'utf8',
265
268
  );
266
269
  }
package/src/update.js CHANGED
@@ -8,6 +8,8 @@ import {
8
8
  extractRawCustomContent,
9
9
  injectRawCustomContent,
10
10
  inspectCustomizationBlock,
11
+ readDescriptionEntry,
12
+ withOfficialDescription,
11
13
  } from './customization.js';
12
14
  import {
13
15
  UNIVERSAL_PROJECT_DESTINATION,
@@ -190,20 +192,25 @@ function readSkillMarkdown(absDir) {
190
192
  return fs.readFileSync(skillMd, 'utf8');
191
193
  }
192
194
 
193
- function officialLiveFiles(liveFiles, liveMarkdown, bundledMarkdown, skillId) {
195
+ function officialLiveFiles(liveFiles, liveMarkdown, bundledMarkdown, skillId, baseHashes) {
194
196
  const next = { ...liveFiles };
195
197
  if (typeof liveMarkdown !== 'string' || typeof bundledMarkdown !== 'string' || !next['SKILL.md']) {
196
- return next;
198
+ return { files: next, localDescription: false };
197
199
  }
198
200
  const inspection = inspectCustomizationBlock(liveMarkdown, skillId);
199
- if (inspection.status !== 'valid' && inspection.status !== 'empty') return next;
200
- const emptied = injectRawCustomContent(
201
- liveMarkdown,
202
- extractRawCustomContent(bundledMarkdown, skillId),
203
- skillId,
204
- );
205
- next['SKILL.md'] = hashBytes(Buffer.from(emptied, 'utf8'));
206
- return next;
201
+ if (inspection.status !== 'valid' && inspection.status !== 'empty') return { files: next, localDescription: false };
202
+ const officialHash = (markdown) => hashBytes(Buffer.from(
203
+ injectRawCustomContent(markdown, extractRawCustomContent(bundledMarkdown, skillId), skillId),
204
+ 'utf8',
205
+ ));
206
+ next['SKILL.md'] = officialHash(liveMarkdown);
207
+ // A local description counts as customization only when it is the one difference from the installed Release.
208
+ const withDescription = withOfficialDescription(liveMarkdown, bundledMarkdown);
209
+ const localDescription = withDescription !== liveMarkdown
210
+ && next['SKILL.md'] !== baseHashes['SKILL.md']
211
+ && officialHash(withDescription) === baseHashes['SKILL.md'];
212
+ if (localDescription) next['SKILL.md'] = baseHashes['SKILL.md'];
213
+ return { files: next, localDescription };
207
214
  }
208
215
 
209
216
  function customizationKind(markdown, skillId) {
@@ -236,7 +243,13 @@ function compareSkill(options) {
236
243
  const diagnosis = typeof liveMarkdown === 'string'
237
244
  ? diagnoseCustomizationMarkers(liveMarkdown, skillId)
238
245
  : { status: 'absent', shape: 'missing-file', repairable: false };
239
- const officialLive = officialLiveFiles(liveFiles, liveMarkdown, bundledMarkdown, skillId);
246
+ const { files: officialLive, localDescription } = officialLiveFiles(
247
+ liveFiles,
248
+ liveMarkdown,
249
+ bundledMarkdown,
250
+ skillId,
251
+ baseHashes,
252
+ );
240
253
  const liveChanged = !mapsEqual(officialLive, baseHashes)
241
254
  || symlinkEffects(inventory).length > 0
242
255
  || detectTypeChanges(baseHashes, inventory).length > 0;
@@ -264,13 +277,13 @@ function compareSkill(options) {
264
277
  } else if (liveChanged) {
265
278
  comparison = 'local-only';
266
279
  changeKind = 'local-only';
267
- } else if (upstreamChanged && customKind === 'populated') {
280
+ } else if (upstreamChanged && (customKind === 'populated' || localDescription)) {
268
281
  comparison = 'upstream-and-customization';
269
282
  changeKind = 'upstream-only';
270
283
  } else if (upstreamChanged) {
271
284
  comparison = 'upstream-only';
272
285
  changeKind = 'upstream-only';
273
- } else if (customKind === 'populated') {
286
+ } else if (customKind === 'populated' || localDescription) {
274
287
  comparison = 'customization-only';
275
288
  }
276
289
 
@@ -348,6 +361,8 @@ function compareSkill(options) {
348
361
  rawCustom: (customKind === 'populated' || customKind === 'empty') && typeof liveMarkdown === 'string'
349
362
  ? extractRawCustomContent(liveMarkdown, skillId)
350
363
  : undefined,
364
+ localDescription,
365
+ rawDescription: localDescription ? readDescriptionEntry(liveMarkdown) : undefined,
351
366
  backup: { required: needsResolution },
352
367
  exportPath,
353
368
  exportCollision: pathExists(exportPath),
@@ -504,6 +519,7 @@ function installOptionsFor(skill, options, scope) {
504
519
  adoptChanged: 'replace',
505
520
  adoptMalformed: options.malformedMarkers === 'replace' ? 'replace' : undefined,
506
521
  preservedCustomRaw: skill.rawCustom !== undefined ? skill.rawCustom : undefined,
522
+ preservedDescription: skill.rawDescription,
507
523
  registry: options.registry || loadHostRegistry(findPackageRoot()),
508
524
  afterBackup: options.afterBackup,
509
525
  saveState: options.saveState,
@@ -776,6 +792,7 @@ export function formatUpdateHuman(plan) {
776
792
  lines.push(`${title}:`);
777
793
  for (const skill of skills) {
778
794
  lines.push(` ${skill.title} (${skill.id}) [${skill.comparison}]`);
795
+ if (skill.localDescription) lines.push(' local description: kept');
779
796
  if (skill.changelog) {
780
797
  lines.push(' Whole-skill diff:');
781
798
  for (const line of skill.changelog.split('\n')) lines.push(` ${line}`);