sshlg-skills 1.77.0 → 1.78.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ ## v1.78.0 (2026-10-24): quiet core skills listed by name only
2
+
3
+ - **B-254:** `visibility` lists quiet core skills by name only on Claude Code. Measured
4
+ 2026-10-24 on Claude Code 2.1.299: `Skill listing over budget: 132 skills, 61725 chars > 30000
5
+ budget`; over budget the host keeps bundled skills whole, gives the rest their description
6
+ most-used first and lists the remainder by name. A personal skill in the core that was not
7
+ used within 14 days and is not named in `~/.claude/CLAUDE.md` now gets
8
+ `skillOverrides: "name-only"` (on this machine 22 skills, the launcher's estimate 46,402 to
9
+ 35,853 characters). `--full-days N|off`; ownership of each value is recorded
10
+ (`claudeNameOnly`), `--revert` removes only values still as written, and `update` re-applies
11
+ the tier only after an `--apply` that recorded `fullDays`. The listing estimate now counts
12
+ Claude's `- name: description` form, and the preview prints Claude Code's own last count
13
+ from its debug logs. `test/visibility_test.js` 22 to 29 checks,
14
+ `test/visibility_e2e_test.js` 11 to 15. Release impact card:
15
+ `docs/evidence/specs/2026-10-24-v1780-release-impact.md`.
16
+ - **Context audit, the same request:** B-255 (the listing still over budget: family and foreign
17
+ plugin descriptions) and B-256 (the 15,410-character routing block in every host file) are
18
+ open rows with their measurements; oversized project instruction files are tracked in the
19
+ HANDOFF entry.
20
+
1
21
  ## v1.77.0 (2026-10-24): every release with users carries its impact card
2
22
 
3
23
  - **Pins:** task-pipeline 1.102.0, the release impact card (operator, 2026-10-24): a release in a
package/README.md CHANGED
@@ -686,6 +686,17 @@ hides every other installed pack; an existing file is replaced only with `--repl
686
686
  so a plugin's skills stay listed (switch a foreign plugin off with `enabledPlugins`).
687
687
  The pack marks and the overrides below do not change them; `--keep <skill>` keeps a
688
688
  skill listed there.
689
+ - **Claude Code name-only tier** (B-254, since v1.78.0): a personal skill in the core that
690
+ was not used within 14 days and is not named in `~/.claude/CLAUDE.md` stays listed by name
691
+ without its description (`skillOverrides: "name-only"`). Claude Code 2.1.299 does the same
692
+ on its own once the listing is over budget (bundled skills whole, the others' descriptions
693
+ most-used first, the rest by name), so this picks the quiet ones by the operator's usage
694
+ instead of the host's order. `--full-days N` sets the window, `--full-days off` turns the
695
+ tier off. `update` re-applies it only after one `visibility --apply` that recorded
696
+ `fullDays`; an older record re-applies without it. The preview prints the launcher's
697
+ estimate of the listing and Claude Code's own last count from its debug logs (the one
698
+ `Skill listing over budget` line); the estimate leaves out bundled skills, plugin commands
699
+ and plugins loaded from other roots, so the logged count is the one to trust.
689
700
  - **Gemini, Pi, OpenCode and Hermes** hide the skills of marked packs, except the usage
690
701
  core. Strongest first: a family pack is never hidden; the usage core is never hidden
691
702
  (family skills, skills used in the last 60 days by Claude Code usage, skills
@@ -698,12 +709,13 @@ hides every other installed pack; an existing file is replaced only with `--repl
698
709
  npx sshlg-skills config set visibility.pack.anthropics/financial-services show # or hide, or default
699
710
  npx sshlg-skills visibility # preview: per-host counts, nothing written
700
711
  npx sshlg-skills visibility --apply # write each host's own switch, backups first; records consent
712
+ npx sshlg-skills visibility --apply --full-days off # Claude: keep every core description
701
713
  npx sshlg-skills visibility --revert # remove exactly what --apply wrote; withdraws consent
702
714
  ```
703
715
 
704
716
  | Host | The switch `visibility` writes | Source | A hidden skill can still |
705
717
  |---|---|---|---|
706
- | Claude Code | `skillOverrides: "user-invocable-only"` in `~/.claude/settings.json`; plugin skills are out of its reach | since v1.55.0; Claude Code settings reference: overrides do not apply to plugin skills | be run from the `/` menu by a person |
718
+ | Claude Code | `skillOverrides: "user-invocable-only"` (hidden) or `"name-only"` (quiet core) in `~/.claude/settings.json`; plugin skills are out of its reach | since v1.55.0; Claude Code settings reference: overrides do not apply to plugin skills | be run from the `/` menu by a person |
707
719
  | Codex | `enabled = false` entries in `~/.codex/config.toml` `[skills] config` | since v1.55.0 | be read from its SKILL.md path when asked |
708
720
  | Gemini CLI 0.63.0 | `skills.disabled` in `~/.gemini/settings.json`, the list `gemini skills disable <name> --scope user` writes; read back with `gemini skills list` (`[Disabled]`) | `packages/cli/src/utils/skillSettings.ts`; `SkillManager.getSkills()` drops disabled skills | come back with `gemini skills enable <name>` |
709
721
  | Pi 1.1.0 | `-<path>` exact exclusions in the `skills` array of `~/.pi/agent/settings.json`, one per folder holding the skill (`~/.agents/skills`, `~/.pi/agent/skills`); the file is created if absent and removed again by `--revert` | Pi's own docs, settings → Resources; `isEnabledByOverrides` in its `dist/core/package-manager.js` | be loaded for one session with `pi --skill <path>` |
@@ -314,8 +314,9 @@ Usage:
314
314
  # report header + footer, links looked up
315
315
  npx sshlg-skills toolkit [--for "<task>"] [--find "<concept>, …"] [--expand <provider>]
316
316
  # every skill this machine can reach
317
- npx sshlg-skills visibility [--apply | --revert] [--days 60] [--keep a,b]
317
+ npx sshlg-skills visibility [--apply | --revert] [--days 60] [--full-days 14|off] [--keep a,b]
318
318
  # Claude/Codex: hide all outside the core;
319
+ # Claude: quiet core skills name-only;
319
320
  # Gemini/Pi/OpenCode/Hermes: hide marked
320
321
  # packs; Cursor/Kimi: cannot hide
321
322
  npx sshlg-skills visibility marks [--init --show <owner/repo,…> [--replace] [--apply]]
@@ -773,6 +774,9 @@ function reapplyVisibility(f) {
773
774
  log(`\n== Skill visibility (re-applied: this machine ran \`visibility --apply\`; hosts: ${[...only].join(', ')}) ==`);
774
775
  return runVisibility({
775
776
  mode: 'apply', days: Number(rec.days) || undefined, keep: Array.isArray(rec.keep) ? rec.keep : [], only, reapply: true,
777
+ // A record from before the name-only tier carries no fullDays: re-apply keeps the old
778
+ // behaviour until the operator runs `visibility --apply` once (B-254, explicit consent).
779
+ fullDays: typeof rec.fullDays === 'number' ? rec.fullDays : null,
776
780
  }) === 0;
777
781
  }
778
782
 
@@ -2139,9 +2143,19 @@ function cmdVisibility(argv) {
2139
2143
  mode: flag('--revert') ? 'revert' : flag('--apply') ? 'apply' : 'preview',
2140
2144
  days: Number(value('--days')) || V.DEFAULT_DAYS,
2141
2145
  keep: (value('--keep') || '').split(',').map((s) => s.trim()).filter(Boolean),
2146
+ fullDays: fullDaysArg(value('--full-days'), V.DEFAULT_FULL_DAYS),
2142
2147
  });
2143
2148
  }
2144
2149
 
2150
+ /** `--full-days N` (a whole number of days), `--full-days off` (no name-only tier), or the default. */
2151
+ function fullDaysArg(raw, dflt) {
2152
+ if (raw === null) return dflt;
2153
+ if (raw === 'off') return null;
2154
+ const n = Number(raw);
2155
+ if (!Number.isInteger(n) || n < 0) { log(`visibility: --full-days takes a whole number of days or "off", got "${raw}"; using ${dflt}`); return dflt; }
2156
+ return n;
2157
+ }
2158
+
2145
2159
  /**
2146
2160
  * The work of `visibility`, callable from `update` (which re-applies it on a machine whose
2147
2161
  * record shows the operator ran `--apply`). `opts.only` limits the run to a set of host
@@ -2163,6 +2177,29 @@ function runVisibility(opts) {
2163
2177
  finally { lock.release(home, got.token); }
2164
2178
  }
2165
2179
 
2180
+ /**
2181
+ * Claude Code's own last listing count, read from its debug logs (newest first). Only the
2182
+ * one warning line is parsed; nothing else in a log is read into the output.
2183
+ */
2184
+ function lastListingMeasure(claudeRoot) {
2185
+ const dir = path.join(claudeRoot, 'debug');
2186
+ let files = [];
2187
+ try {
2188
+ files = fs.readdirSync(dir).filter((n) => n.endsWith('.txt'))
2189
+ .map((n) => ({ n, m: fs.statSync(path.join(dir, n)).mtimeMs })).sort((a, b) => b.m - a.m).slice(0, 40);
2190
+ } catch (e) { return null; }
2191
+ const re = /^(\S+) \[WARN\] Skill listing over budget: (\d+) skills, (\d+) chars > (\d+) budget/m;
2192
+ for (const f of files) {
2193
+ let text;
2194
+ try { text = fs.readFileSync(path.join(dir, f.n), 'utf8'); } catch (e) { continue; }
2195
+ const all = text.match(new RegExp(re.source, 'gm'));
2196
+ if (!all) continue;
2197
+ const m = re.exec(all[all.length - 1]);
2198
+ return { at: m[1], skills: Number(m[2]), chars: Number(m[3]), budget: Number(m[4]) };
2199
+ }
2200
+ return null;
2201
+ }
2202
+
2166
2203
  function runVisibilityUnlocked(opts) {
2167
2204
  const V = require(path.join(ROOT, 'lib', 'visibility.js'));
2168
2205
  const HV = require(path.join(ROOT, 'lib', 'hostvis.js'));
@@ -2179,6 +2216,8 @@ function runVisibilityUnlocked(opts) {
2179
2216
  const doRevert = opts.mode === 'revert';
2180
2217
  const days = opts.days || V.DEFAULT_DAYS;
2181
2218
  const keep = opts.keep || [];
2219
+ // null: no name-only tier (a re-apply of a record that predates it, or `--full-days off`).
2220
+ const fullDays = typeof opts.fullDays === 'number' ? opts.fullDays : null;
2182
2221
  const inScope = (id) => !opts.only || opts.only.has(id);
2183
2222
 
2184
2223
  const recordPath = path.join(home, '.sshlg-skills', 'visibility.json');
@@ -2226,11 +2265,24 @@ function runVisibilityUnlocked(opts) {
2226
2265
  try { settingsData = JSON.parse(settingsText || '{}'); } catch (e) { settingsData = {}; }
2227
2266
  const overrides = settingsData.skillOverrides || {};
2228
2267
  const mine = new Set(owned.claude.filter((n) => overrides[n] === V.HIDE));
2268
+ const ownedNameOnly = Array.isArray(owned.claudeNameOnly) ? owned.claudeNameOnly : [];
2269
+ const mineNameOnly = new Set(ownedNameOnly.filter((n) => overrides[n] === V.NAME_ONLY));
2229
2270
  const foreign = {};
2230
- for (const [k, v] of Object.entries(overrides)) if (!mine.has(k)) foreign[k] = v;
2231
- const plan = doRevert ? { hide: [], respected: [] } : V.claudePlan({ personal, core: coreSet, overrides: foreign, keep });
2232
- const claudeSet = plan.hide.filter((n) => !mine.has(n));
2233
- const claudeUnset = [...mine].filter((n) => !plan.hide.includes(n));
2271
+ for (const [k, v] of Object.entries(overrides)) if (!mine.has(k) && !mineNameOnly.has(k)) foreign[k] = v;
2272
+ const full = doRevert || fullDays === null ? null
2273
+ : V.fullSet({ personal, usage, now, fullDays, instructions });
2274
+ const plan = doRevert ? { hide: [], nameOnly: [], respected: [] }
2275
+ : V.claudePlan({ personal, core: coreSet, full, overrides: foreign, keep });
2276
+ // Values this run writes, and values it removes (only while they still hold what it wrote).
2277
+ const claudeSetMap = {};
2278
+ for (const n of plan.hide) if (overrides[n] !== V.HIDE) claudeSetMap[n] = V.HIDE;
2279
+ for (const n of plan.nameOnly) if (overrides[n] !== V.NAME_ONLY) claudeSetMap[n] = V.NAME_ONLY;
2280
+ const planned = new Set(plan.hide.concat(plan.nameOnly));
2281
+ const claudeUnsetMap = {};
2282
+ for (const n of mine) if (!planned.has(n)) claudeUnsetMap[n] = V.HIDE;
2283
+ for (const n of mineNameOnly) if (!planned.has(n)) claudeUnsetMap[n] = V.NAME_ONLY;
2284
+ const claudeSet = Object.keys(claudeSetMap);
2285
+ const claudeUnset = Object.keys(claudeUnsetMap);
2234
2286
  const stateOf = (s, ov) => {
2235
2287
  if (s.loaded === false) return 'hidden'; // installed, not loaded here (B-185): not in the listing
2236
2288
  if (s.namespace !== 'claude:plain') return 'on';
@@ -2239,7 +2291,7 @@ function runVisibilityUnlocked(opts) {
2239
2291
  return v === 'name-only' ? 'name-only' : 'on';
2240
2292
  };
2241
2293
  const after = Object.assign({}, overrides);
2242
- for (const n of claudeSet) after[n] = V.HIDE;
2294
+ for (const n of claudeSet) after[n] = claudeSetMap[n];
2243
2295
  for (const n of claudeUnset) delete after[n];
2244
2296
  const charsBefore = V.listingChars(skills.map((s) => ({ name: s.plugin && s.namespace === 'claude:plugin' ? `${s.plugin.split('@')[0]}:${s.id}` : s.id, description: s.description, state: stateOf(s, overrides) })));
2245
2297
  const charsAfter = V.listingChars(skills.map((s) => ({ name: s.plugin && s.namespace === 'claude:plugin' ? `${s.plugin.split('@')[0]}:${s.id}` : s.id, description: s.description, state: stateOf(s, after) })));
@@ -2336,11 +2388,21 @@ function runVisibilityUnlocked(opts) {
2336
2388
  log(` overrides (config.json): hidePacks ${(ov.hidePacks || []).length}, showPacks ${(ov.showPacks || []).length} — Gemini, Pi, OpenCode and Hermes only`);
2337
2389
  }
2338
2390
  if (inScope('claude')) {
2339
- log(` Claude: ${personal.length} personal skills; hide ${claudeSet.length} more, un-hide ${claudeUnset.length}; ` +
2391
+ const newHides = claudeSet.filter((n) => claudeSetMap[n] === V.HIDE).length;
2392
+ log(` Claude: ${personal.length} personal skills; hide ${newHides} more, name-only ${claudeSet.length - newHides} more, restore ${claudeUnset.length}; ` +
2340
2393
  `${Object.keys(foreign).length} operator-set override(s) left alone`);
2341
- log(` Claude listing ≈ ${charsBefore} → ${charsAfter} characters (names + descriptions, 1536 cap each).`);
2342
- log(' Budget: Claude Code logs it — `claude -p x --debug` then look for "Skill listing over budget"');
2343
- log(' (30000 characters measured on 2026-10-10). Over budget, descriptions drop least-used first.');
2394
+ log(fullDays === null
2395
+ ? ' name-only tier: off (`--full-days N` turns it on: core skills unused for N days keep their name, lose their description)'
2396
+ : ` name-only tier: core skills not used within ${fullDays} days and not named in the instructions are listed by name only (${plan.nameOnly.length})`);
2397
+ log(` Claude listing, launcher's estimate ≈ ${charsBefore} → ${charsAfter} characters`);
2398
+ log(' (personal and registered plugin skills in Claude\'s `- name: description` form; bundled skills,');
2399
+ log(' plugin commands and plugins loaded from other roots are not counted).');
2400
+ const measured = lastListingMeasure(claudeRoot);
2401
+ log(measured
2402
+ ? ` Claude Code's own count (debug log ${measured.at}): ${measured.skills} entries, ${measured.chars} characters > ${measured.budget} budget.`
2403
+ : ' Claude Code\'s own count: none logged yet. `claude -p x --debug`, then look for "Skill listing over budget".');
2404
+ log(' Over budget, Claude Code 2.1.299 keeps bundled skills whole, gives the others their description');
2405
+ log(' most-used first and lists the rest by name only.');
2344
2406
  }
2345
2407
  if (inScope('codex')) {
2346
2408
  log(` Codex: ${shared.length} shared skills; disable up to ${codexAdd.length} more (entries already present are left as they are), re-enable ${codexDrop.length}` +
@@ -2404,7 +2466,7 @@ function runVisibilityUnlocked(opts) {
2404
2466
  const degraded = [];
2405
2467
  let record = Object.assign({}, owned, {
2406
2468
  note: 'Written by `npx sshlg-skills visibility`; --revert removes these entries. Pending holds a write-ahead receipt; applied is consent for update on hosts only.',
2407
- days, keep, marksRevision: marksData && !markProblems.length ? marksData.revision : null,
2469
+ days, keep, fullDays, marksRevision: marksData && !markProblems.length ? marksData.revision : null,
2408
2470
  updatedAt: new Date().toISOString(), created: Object.assign({}, owned.created || {}),
2409
2471
  files: Object.assign({}, owned.files || {}, Object.fromEntries(Object.entries(files).filter(([id]) => inScope(id)))),
2410
2472
  });
@@ -2442,13 +2504,14 @@ function runVisibilityUnlocked(opts) {
2442
2504
  if (id === 'claude') {
2443
2505
  if (claudeSet.length && !('skillOverrides' in settingsData)) next.created.claude = ['skillOverrides'];
2444
2506
  if (doRevert) delete next.created.claude;
2507
+ next.claudeNameOnly = plan.nameOnly.slice().sort();
2445
2508
  }
2446
2509
  if (r.text === before) { journal.save(next); log(` ${id}: already as planned`); return; }
2447
2510
  transact(id, file, before, r.text, next, () => S.write(file, r.text, home));
2448
2511
  };
2449
2512
  const claudeCreated = createdOf('claude').includes('skillOverrides') || !Object.prototype.hasOwnProperty.call(owned, 'applied');
2450
2513
  writeSimple('claude', settingsPath, settingsText,
2451
- (t) => V.editClaudeSettings(t, { set: claudeSet, unset: claudeUnset, prune: claudeCreated }), () => plan.hide.slice().sort());
2514
+ (t) => V.editClaudeSettings(t, { set: claudeSetMap, unset: claudeUnsetMap, prune: claudeCreated }), () => plan.hide.slice().sort());
2452
2515
  writeSimple('codex', codexPath, codexText,
2453
2516
  (t) => V.editCodexConfig(t, { disable: codexAdd, enable: codexDrop }),
2454
2517
  (r) => [...new Set(owned.codex.filter((p) => codexWant.includes(p)).concat(r.added || []))].sort());
@@ -39,6 +39,8 @@ function validate(r) {
39
39
  if (!r || typeof r !== 'object' || Array.isArray(r)) throw new Error('visibility record is not an object');
40
40
  for (const h of HOSTS) if (r[h] !== undefined && (!Array.isArray(r[h]) || r[h].some((x) => typeof x !== 'string')))
41
41
  throw new Error(`visibility record ${h} is not a list of strings`);
42
+ if (r.claudeNameOnly !== undefined && (!Array.isArray(r.claudeNameOnly) || r.claudeNameOnly.some((x) => typeof x !== 'string')))
43
+ throw new Error('visibility record claudeNameOnly is not a list of strings');
42
44
  if (r.files !== undefined && (!r.files || typeof r.files !== 'object' || Array.isArray(r.files)
43
45
  || Object.entries(r.files).some(([h, f]) => !HOSTS.includes(h) || typeof f !== 'string' || !path.isAbsolute(f))))
44
46
  throw new Error('visibility record files is not a host-to-absolute-path map');
package/lib/visibility.js CHANGED
@@ -23,8 +23,15 @@
23
23
  */
24
24
 
25
25
  const HIDE = 'user-invocable-only';
26
+ const NAME_ONLY = 'name-only';
26
27
  const DESC_CAP = 1536;
27
28
  const DEFAULT_DAYS = 60;
29
+ // B-254. Measured 2026-10-24: Claude Code 2.1.299 logged `Skill listing over budget:
30
+ // 132 skills, 61725 chars > 30000 budget`. Over budget it keeps bundled skills whole and
31
+ // gives the rest their description most-used first; the remainder are listed by name
32
+ // only. So a quiet core skill loses its description anyway. Within the core, only what
33
+ // was used within DEFAULT_FULL_DAYS or is named in the instructions keeps it.
34
+ const DEFAULT_FULL_DAYS = 14;
28
35
 
29
36
  /** Skill names the instruction text references deliberately: inside backticks. */
30
37
  function namedIn(text, names) {
@@ -53,16 +60,39 @@ function core({ personal, family, usage, now, days, instructions }) {
53
60
  return set;
54
61
  }
55
62
 
56
- function claudePlan({ personal, core: coreSet, overrides, keep }) {
63
+ /**
64
+ * The core members that keep their description: used within `fullDays`, or named in
65
+ * the instructions (an agent told to use a skill by name is already told when).
66
+ */
67
+ function fullSet({ personal, usage, now, fullDays, instructions }) {
68
+ const window = (fullDays == null ? DEFAULT_FULL_DAYS : fullDays) * 86400e3;
69
+ const set = new Set();
70
+ for (const n of personal || []) {
71
+ const u = (usage || {})[n];
72
+ if (u && typeof u.lastUsedAt === 'number' && now - u.lastUsedAt <= window) set.add(n);
73
+ }
74
+ for (const n of namedIn(instructions, personal)) set.add(n);
75
+ return set;
76
+ }
77
+
78
+ /**
79
+ * `hide`: outside the core. `nameOnly`: in the core but outside `full`, so listed by name
80
+ * without its description. Without `full` nothing goes name-only. A key the operator set
81
+ * is never planned over, in either list.
82
+ */
83
+ function claudePlan({ personal, core: coreSet, full, overrides, keep }) {
57
84
  const kept = new Set(keep || []);
58
85
  const hide = [];
86
+ const nameOnly = [];
59
87
  const respected = [];
60
88
  for (const n of personal || []) {
61
- if (coreSet.has(n) || kept.has(n)) continue;
89
+ if (kept.has(n)) continue;
90
+ const inCore = coreSet.has(n);
91
+ if (inCore && (!full || full.has(n))) continue;
62
92
  if (overrides && Object.prototype.hasOwnProperty.call(overrides, n)) { respected.push(n); continue; }
63
- hide.push(n);
93
+ (inCore ? nameOnly : hide).push(n);
64
94
  }
65
- return { hide: hide.sort(), respected: respected.sort() };
95
+ return { hide: hide.sort(), nameOnly: nameOnly.sort(), respected: respected.sort() };
66
96
  }
67
97
 
68
98
  /**
@@ -77,12 +107,19 @@ function editClaudeSettings(text, { set, unset, prune }) {
77
107
  if (JSON.stringify(data, null, 2) + '\n' !== text) {
78
108
  return { ok: false, reason: 'settings.json is not in the 2-space JSON format Claude Code writes; refusing to reformat it' };
79
109
  }
110
+ // `set` and `unset` map a name to its value; a bare list means the hidden value, the
111
+ // form callers used before the name-only tier. `unset` removes a key only while it
112
+ // still holds the value the launcher wrote.
113
+ const asMap = (x) => (Array.isArray(x) ? Object.fromEntries(x.map((n) => [n, HIDE])) : (x || {}));
114
+ const setMap = asMap(set);
115
+ const unsetMap = asMap(unset);
80
116
  const ov = Object.assign({}, data.skillOverrides || {});
81
117
  let changed = false;
82
- for (const n of set || []) if (ov[n] !== HIDE) { ov[n] = HIDE; changed = true; }
118
+ for (const [n, v] of Object.entries(setMap)) if (ov[n] !== v) { ov[n] = v; changed = true; }
83
119
  const kept = [];
84
- for (const n of unset || []) {
85
- if (ov[n] === HIDE) { delete ov[n]; changed = true; }
120
+ for (const [n, v] of Object.entries(unsetMap)) {
121
+ if (Object.prototype.hasOwnProperty.call(setMap, n)) continue;
122
+ if (ov[n] === v) { delete ov[n]; changed = true; }
86
123
  else if (n in ov) kept.push(n);
87
124
  }
88
125
  // Nothing to do leaves the file exactly as it was — including an empty map the
@@ -177,15 +214,21 @@ function editCodexConfig(text, { disable, enable }) {
177
214
  return { ok: true, text: lines.join('\n'), added };
178
215
  }
179
216
 
180
- /** Characters a listing spends: a listed name, plus its description while `on`. */
217
+ /**
218
+ * Characters a listing spends, in the form Claude Code 2.1.299 prints it: `- name: description`
219
+ * while `on` (description capped at 1536), `- name` when name-only, one newline between
220
+ * entries; hidden costs nothing.
221
+ */
181
222
  function listingChars(rows) {
182
223
  let n = 0;
224
+ let entries = 0;
183
225
  for (const r of rows || []) {
184
226
  if (r.state === 'hidden') continue;
185
- n += String(r.name).length;
186
- if (r.state === 'on') n += Math.min(DESC_CAP, String(r.description || '').length);
227
+ entries += 1;
228
+ n += 2 + String(r.name).length;
229
+ if (r.state === 'on') n += 2 + Math.min(DESC_CAP, String(r.description || '').length);
187
230
  }
188
- return n;
231
+ return entries ? n + entries - 1 : 0;
189
232
  }
190
233
 
191
234
  // ── Packs: the operator's marks (pack-marks.json) and per-machine overrides ──
@@ -453,7 +496,8 @@ function jsonAt(text, keys) {
453
496
  }
454
497
 
455
498
  module.exports = {
456
- core, claudePlan, editClaudeSettings, editCodexConfig, listingChars, namedIn, HIDE, DEFAULT_DAYS,
499
+ core, fullSet, claudePlan, editClaudeSettings, editCodexConfig, listingChars, namedIn,
500
+ HIDE, NAME_ONLY, DEFAULT_DAYS, DEFAULT_FULL_DAYS,
457
501
  MARKS_SCHEMA, marksProblems, marksIndex, marksFromLock, packHidden, hostHide, setPackOverride,
458
502
  editJsonList, editJsonMap, jsonAt,
459
503
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sshlg-skills",
3
- "version": "1.77.0",
3
+ "version": "1.78.0",
4
4
  "skillsCli": "skills@1.7.2",
5
5
  "description": "The ssheleg harness skill layer: routing, delivery gates, specialist skills and durable handoffs for coding agents.",
6
6
  "bin": {