skills-atlas-cli 0.9.0 โ†’ 0.10.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/README.md CHANGED
@@ -70,9 +70,12 @@ skills-atlas doctor # health check: orphans, drift, l
70
70
  skills-atlas kit # detect this project & install the right skills for it
71
71
  skills-atlas sync # reproduce a project's kit (skills-atlas.kit.json)
72
72
 
73
- # ๐Ÿค– Autopilot & capability gaps (opt-in)
73
+ # ๐Ÿค– Autopilot, gaps & cleanup (opt-in)
74
74
  skills-atlas hook on # Claude proactively offers a fitting skill as you work
75
+ skills-atlas hook lang en|zh # language the autopilot replies in (default English)
76
+ skills-atlas hook model # which model powers gaps/cleanup (default Haiku)
75
77
  skills-atlas gaps # Claude spots kinds of work you keep doing without a skill
78
+ skills-atlas prune # Claude flags installed skills you no longer use
76
79
 
77
80
  # ๐ŸŒ Catalog & sources
78
81
  skills-atlas categories # the 20 top-level categories
@@ -86,44 +89,37 @@ skills-atlas mcp # run as an MCP server (any MCP c
86
89
 
87
90
  **โ›“ Workflows, not just skills.** Many skills belong to a curated chain (e.g.
88
91
  `brainstorming โ†’ writing-plans โ†’ executing-plans โ†’ โ€ฆ`). `install <skill> --chain`
89
- installs the whole pipeline in one archive download, ready to run in order.
92
+ installs the whole pipeline in one step, ready to run in order.
90
93
 
91
94
  **๐Ÿ“ฆ Project kits.** `skills-atlas kit` detects what this project is (frontend / backend /
92
95
  data / infra) and installs a tailored set (a universal dev workflow plus archetype
93
96
  add-ons) into `./.claude/skills/`, then writes a committable `skills-atlas.kit.json`.
94
97
  A teammate runs `skills-atlas sync` to reproduce it exactly.
95
98
 
96
- Output is English by default; add `--zh` for Chinese, or `--json` to any command for machine-readable output.
97
- After installing a skill, start a new Claude Code session to load it.
98
-
99
- ## How install works
100
-
101
- The real value is the **catalog**: `search` / `info` / `categories` work fully
102
- offline and map *which* skill fits. It's function-organized, bilingual, tagged with
103
- use-case / when-to-use / personas / โ›“ chains. That's what `npx skills add` and
104
- GitHub search don't give you.
99
+ **Where skills land.** Running `install` / `use` yourself installs **globally**
100
+ (`~/.claude/skills/`, every project) โ€” for general workflows you want everywhere. Skills
101
+ suggested by autopilot, installed via the Claude Code plugin, or set up by `kit` go into
102
+ **this project** (`./.claude/skills/`, committable for teammates). Override either way
103
+ with `--global` / `--project`.
105
104
 
106
- On top of that, `install` can place a skill straight into `.claude/skills/`:
105
+ Output is English by default; add `--zh` for Chinese.
106
+ After installing a skill, start a new Claude Code session to load it.
107
107
 
108
- - For a repo that exposes a **per-skill folder**, it downloads only that folder
109
- (via the repo archive, with **no GitHub API rate limit**) into
110
- `<target>/.claude/skills/<skill>/`, not the whole repo.
111
- - Several sources? The best installable one is auto-picked. Pass `--source <id>` to
112
- choose, `--yes` for non-interactive runs.
113
- - Other sources (whole-repo / marketplace) print their official command instead
114
- (e.g. `npx skills add owner/repo`).
115
- - `GITHUB_TOKEN` is only needed if you fall back to the API and hit its 60/h limit.
108
+ ## Why a catalog
116
109
 
117
- ## Keeping the catalog fresh
110
+ `search` / `info` / `categories` work fully offline and tell you *which* skill fits โ€”
111
+ organized by function, bilingual, tagged with use-case, when-to-use, personas and โ›“
112
+ chains. `install` then drops just that skill's folder into `.claude/skills/` (not the
113
+ whole repo); when several repos offer the same skill, the best one is picked for you.
118
114
 
119
- The catalog ships inside the package and works offline. `skills-atlas update` pulls
120
- the latest from the public feed (cached under `~/.cache/skills-atlas/`).
115
+ The catalog ships with the tool and works offline, and refreshes itself in the
116
+ background so new skills show up on their own. Run `skills-atlas update` to pull the
117
+ latest on demand.
121
118
 
122
- ## Private / org catalog sources
119
+ ## Your org's private skills
123
120
 
124
- Point the CLI at your organization's own catalog (a `data.json` in the same
125
- schema) so internal skills show up in `search` / `info` / `install` / `kit`
126
- alongside the public Atlas:
121
+ Point the CLI at your organization's own catalog so internal skills show up in
122
+ `search` / `info` / `install` / `kit` alongside the public Atlas:
127
123
 
128
124
  ```bash
129
125
  skills-atlas registry add https://skills.acme.internal/data.json # or a local path
@@ -131,10 +127,7 @@ skills-atlas registry list
131
127
  skills-atlas registry remove https://skills.acme.internal/data.json
132
128
  ```
133
129
 
134
- Private skills **merge** with the public catalog (a private source wins a same-name
135
- clash). Sources are cached locally and merged offline. For a private URL behind
136
- auth, set `SKILLS_ATLAS_TOKEN` (sent as a Bearer header); in CI,
137
- `SKILLS_ATLAS_SOURCES=url1,url2` adds sources without touching config.
130
+ Private skills merge with the public catalog (your own wins a name clash) and are cached locally.
138
131
 
139
132
  ## In Claude Code
140
133
 
@@ -149,9 +142,9 @@ Just describe what you need, or use `/skills-atlas:skill-search`, `:skill-info`,
149
142
 
150
143
  ## In any MCP client
151
144
 
152
- `skills-atlas mcp` runs a zero-dependency [MCP](https://modelcontextprotocol.io)
153
- server over stdio, so any MCP-capable client (Claude Desktop, other agents) can use
154
- the catalog. Add it to your client's config:
145
+ `skills-atlas mcp` runs an [MCP](https://modelcontextprotocol.io) server so any
146
+ MCP-capable client (Claude Desktop, other agents) can use the catalog. Add it to your
147
+ client's config:
155
148
 
156
149
  ```json
157
150
  { "mcpServers": { "skills-atlas": { "command": "npx", "args": ["-y", "skills-atlas-cli", "mcp"] } } }
@@ -164,37 +157,35 @@ anywhere.
164
157
  ## Autopilot (opt-in): the right skill finds you
165
158
 
166
159
  ```bash
167
- skills-atlas hook on # enable (skills-atlas hook off / status)
160
+ skills-atlas hook on # turn it on (hook off / hook status)
161
+ ```
162
+
163
+ With autopilot on, whenever what you're doing lines up with a catalog skill you don't
164
+ have, Claude offers it โ€” explained, with one tap to use it now, see what it covers, or
165
+ skip. You don't have to know the skill exists. It's **off by default**, **quiet** (stays
166
+ silent on greetings and generic asks, never repeats itself, never suggests a skill you
167
+ already have), and **safe** (never auto-installs; a hiccup never blocks your prompt).
168
+
169
+ Two more proactive helpers:
170
+
171
+ - **๐Ÿ”ญ Capability gaps** โ€” `skills-atlas gaps` notices the recurring kinds of work you
172
+ keep doing without a skill, and recommends one.
173
+ - **๐Ÿงน Cleanup** โ€” `skills-atlas prune` flags installed skills you no longer use and
174
+ offers to remove them (never on its own; recent installs are left alone).
175
+
176
+ Tune it to taste:
177
+
178
+ ```bash
179
+ skills-atlas hook suggest on|off # the per-prompt suggestions
180
+ skills-atlas hook gaps on|off # gap recommendations (on by default)
181
+ skills-atlas hook prune on|off # cleanup suggestions (off by default)
182
+ skills-atlas hook model [name] # which model powers gaps/cleanup (default Haiku)
183
+ skills-atlas hook lang en|zh # the language it replies in (default English)
168
184
  ```
169
185
 
170
- Registers a Claude Code `UserPromptSubmit` hook. When what you ask matches the
171
- territory of a catalog skill you don't have, the hook hands Claude a short
172
- shortlist of candidates and **Claude decides** whether any genuinely fits. If it
173
- does, Claude explains **what it does and why it fits your task**, then offers a choice:
174
- use it now, see what it covers first (`skills-atlas info`), or skip. You don't
175
- have to know the skill exists. The split is deliberate: the hook does **recall**
176
- (a distinctive-word match against the catalog, so the right skill is on the
177
- table), Claude does **precision** (it understands your intent and stays silent
178
- unless one truly fits, or searches further itself). It's:
179
-
180
- - **Off by default.** You turn it on explicitly; `hook off` removes it cleanly.
181
- - **Quiet.** Only fires on a distinctive match (greetings and generic actions
182
- like "fix the typo" stay silent), never for an already-installed skill, never
183
- the same skill twice, with a cooldown between suggestions. Claude is the
184
- final filter on relevance.
185
- - **Local and private.** Your prompt is matched against the bundled catalog
186
- on your machine; nothing is sent anywhere.
187
- - **Safe.** Never auto-installs (always your call), and fails open (a hook
188
- error never blocks your prompt).
189
-
190
- **๐Ÿ”ญ Capability gaps.** `skills-atlas gaps` shows Claude your *recent activity* and
191
- lets **Claude** spot the recurring kinds of work you keep doing that no installed
192
- skill covers yet, then recommend one with the pattern as evidence. We don't guess
193
- with heuristics; we just give Claude the memory it lacks (your recent prompts, read
194
- from Claude Code's own local transcripts; **nothing is stored or sent**) plus the
195
- catalog. With the hook on, it also nudges in-conversation now and then. The two
196
- layers are independent: `skills-atlas hook suggest on|off` (per-prompt) and
197
- `skills-atlas hook gaps on|off` (the proactive nudge).
186
+ The per-prompt suggestions are matched on your own machine and sent nowhere. The gaps
187
+ and cleanup helpers hand your recent activity to the model you picked (the same provider
188
+ Claude Code already uses) to judge it.
198
189
 
199
190
  ## License
200
191
 
package/bin/skills.js CHANGED
@@ -17,6 +17,7 @@ const hook = require('../src/commands/hook');
17
17
  const gaps = require('../src/commands/gaps');
18
18
  const gapAnalyze = require('../src/commands/gap-analyze');
19
19
  const prune = require('../src/commands/prune');
20
+ const feedback = require('../src/commands/feedback');
20
21
  const update = require('../src/commands/update');
21
22
  const mcp = require('../src/commands/mcp');
22
23
  const { categories, list } = require('../src/commands/categories');
@@ -24,7 +25,7 @@ const { categories, list } = require('../src/commands/categories');
24
25
  const VERSION = require('../package.json').version;
25
26
  // `use` = install + activate inline (emit the SKILL.md so an agent follows it now).
26
27
  const use = argv => install([...argv, '--inline']);
27
- const commands = { search, info, install, use, kit, sync, installed, upgrade, remove, outdated, doctor, suggest, hook, gaps, 'gap-analyze': gapAnalyze, prune, update, categories, list, registry, mcp };
28
+ const commands = { search, info, install, use, kit, sync, installed, upgrade, remove, outdated, doctor, suggest, hook, gaps, 'gap-analyze': gapAnalyze, prune, feedback, update, categories, list, registry, mcp };
28
29
 
29
30
  const HELP = `skills-atlas โ€” search, install & manage AI agent skills
30
31
 
@@ -49,6 +50,7 @@ autopilot (opt-in):
49
50
  hook on|off|status proactively suggest a skill in Claude when your prompt fits one
50
51
  gaps kinds of work you keep doing without a skill (run: skills-atlas hook on)
51
52
  prune installed skills you no longer use โ€” Claude suggests removing them
53
+ feedback what the autopilot learned from your installs/removes (sharpens it)
52
54
 
53
55
  catalog:
54
56
  update refresh the catalog from the public data feed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills-atlas-cli",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Search, install and learn AI agent skills from the terminal โ€” powered by the Skills Atlas catalog.",
5
5
  "bin": {
6
6
  "skills-atlas": "bin/skills.js",
@@ -0,0 +1,53 @@
1
+ // `skills-atlas feedback` โ€” the autopilot's local suppression list. Two scopes:
2
+ // skills you've dismissed (never suggested, anywhere) and skills you've removed from
3
+ // THIS project (not suggested here). Show / add to / reset it. Nothing is sent.
4
+ 'use strict';
5
+
6
+ const { parse } = require('../args');
7
+ const feedback = require('../feedback');
8
+ const { green, dim } = require('../format');
9
+
10
+ const HELP = `usage: skills-atlas feedback [dismiss <skill> | reset]
11
+
12
+ Skills the autopilot won't suggest again. Two kinds, local only โ€” nothing is sent:
13
+ โ€ข dismissed โ€” you said "never suggest this"; applies in every project
14
+ โ€ข removed โ€” you removed it from THIS project; suppressed here only
15
+ Installing a skill clears both. (Removing one from the global scope is just an
16
+ uninstall โ€” it does not suppress; use 'dismiss' for a blanket no.)
17
+
18
+ feedback show the suppression list (this project + global)
19
+ feedback dismiss <skill> never suggest a skill again, in any project
20
+ feedback reset forget everything (global + this project)
21
+ --json`;
22
+
23
+ module.exports = async function feedbackCmd(argv) {
24
+ const { values, positionals } = parse(argv, ['json']);
25
+ if (values.help) { console.log(HELP); return; }
26
+ const sub = positionals[0];
27
+
28
+ if (sub === 'reset') {
29
+ feedback.clear();
30
+ console.log(values.json ? JSON.stringify({ reset: true }) : `${green('โœ“')} feedback reset.`);
31
+ return;
32
+ }
33
+ if (sub === 'dismiss') {
34
+ const x = positionals.slice(1).join(' ');
35
+ if (!x) { console.error('usage: skills-atlas feedback dismiss <skill>'); process.exitCode = 1; return; }
36
+ feedback.dismiss(x);
37
+ console.log(values.json ? JSON.stringify({ dismissed: x }) : `${green('โœ“')} won't suggest ${x} again (any project).`);
38
+ return;
39
+ }
40
+
41
+ const cur = feedback.current();
42
+ if (values.json) {
43
+ console.log(JSON.stringify({ dismissed: [...cur.global], removedHere: [...cur.project] }, null, 2));
44
+ return;
45
+ }
46
+ if (!cur.global.size && !cur.project.size) {
47
+ console.log(dim('nothing suppressed โ€” run `skills-atlas feedback dismiss <skill>`, or remove a\nskill from this project, and the autopilot stops offering it.'));
48
+ return;
49
+ }
50
+ if (cur.global.size) console.log(`\n${green("won't suggest anywhere")} (${cur.global.size}): ${[...cur.global].join(', ')}`);
51
+ if (cur.project.size) console.log(`${green("won't suggest in this project")} (${cur.project.size}): ${[...cur.project].join(', ')}`);
52
+ console.log(dim('re-install one to clear it, or: skills-atlas feedback reset'));
53
+ };
@@ -29,7 +29,7 @@ const INSTRUCTION = dismissed =>
29
29
  `is built for and they haven't installed. For each real recurring need (ignore one-offs and ` +
30
30
  `anything already covered): state the pattern + rough frequency as evidence, then recommend the ` +
31
31
  `skill โ€” verify it exists with \`skills-atlas search "<intent>"\` or \`skills-atlas info <skill>\`, ` +
32
- `and install with \`skills-atlas use <skill> --yes\`.` +
32
+ `and install with \`skills-atlas use <skill> --yes --project\`.` +
33
33
  (dismissed.length ? ` Already dismissed (skip these): ${dismissed.join(', ')}.` : '') +
34
34
  ` If nothing clearly recurs, say there are no gaps.`;
35
35
 
@@ -253,6 +253,9 @@ module.exports = async function install(argv) {
253
253
  return;
254
254
  }
255
255
 
256
+ // installing clears any prior suppression (global dismiss + this project's removal).
257
+ try { require('../feedback').installed(skill); } catch { /* ignore */ }
258
+
256
259
  if (values.json) {
257
260
  const out = {
258
261
  skill, mode: 'folder', source: src.name,
@@ -40,6 +40,11 @@ module.exports = async function remove(argv) {
40
40
  if (fsu.dirExists(dest)) fsu.rmrf(dest);
41
41
  manifest.remove(root, name);
42
42
 
43
+ // Removing a PROJECT skill is a "not in this project" signal โ€” suppress it here only,
44
+ // never across projects (it may be useful elsewhere). Removing a GLOBAL skill is just
45
+ // uninstalling; for a blanket "never suggest this", use `feedback dismiss`.
46
+ if (!global) { try { require('../feedback').removedInProject(name); } catch { /* ignore */ } }
47
+
43
48
  if (values.json) { console.log(JSON.stringify({ removed: name, dest })); return; }
44
49
  console.log(`${green('โœ“')} removed ${name} ${dim(fsu.tildify(dest))}`);
45
50
  if (otherHas) console.log(dim(`note: '${name}' is still installed in the ${global ? 'project' : 'global'} scope โ€” remove that too: skills-atlas remove ${name} ${otherFlag}`));
@@ -78,7 +78,7 @@ module.exports = async function suggest() {
78
78
  if (txt && !/^NONE\b/i.test(txt)) {
79
79
  const body = pending.source === 'fallback'
80
80
  ? txt // already a full digest for the main agent to judge
81
- : `[Skills Atlas โ€” capability gaps] ${txt}\nOffer this to the user only if it genuinely fits โ€” verify with \`skills-atlas info <skill>\`, install with \`skills-atlas use <skill> --yes\`; otherwise stay silent.`;
81
+ : `[Skills Atlas โ€” capability gaps] ${txt}\nOffer this to the user only if it genuinely fits โ€” verify with \`skills-atlas info <skill>\`, install with \`skills-atlas use <skill> --yes --project\`; otherwise stay silent.`;
82
82
  emit(body + langHint(ap.replyLang));
83
83
  gapstate.touchNudge(gapstate.activitySignature(recent));
84
84
  writeState(file, state);
@@ -131,12 +131,12 @@ module.exports = async function suggest() {
131
131
  for (const s of fsu.scopesFor({})) for (const e of manifest.list(s.root)) installed.add(e.skill);
132
132
  const suggested = new Set(state.suggested || []);
133
133
 
134
- const { fire, candidates } = suggestCandidates(flatRows, prompt, { installed, suggested });
134
+ const { fire, candidates } = suggestCandidates(flatRows, prompt, { installed, suggested, feedback: require('../feedback').current() });
135
135
  if (!fire || !candidates.length) { writeState(file, state); return; }
136
136
 
137
137
  const lines = candidates.map(c => {
138
138
  const uc = (c.row.use_case_en || c.row.use_case || '').replace(/\s+/g, ' ').trim().slice(0, 80);
139
- return `- ${c.skill}${uc ? ` โ€” ${uc}` : ''} (details: \`skills-atlas info ${c.skill}\` ยท use now: \`skills-atlas use ${c.skill} --yes\`)`;
139
+ return `- ${c.skill}${uc ? ` โ€” ${uc}` : ''} (details: \`skills-atlas info ${c.skill}\` ยท use now: \`skills-atlas use ${c.skill} --yes --project\`)`;
140
140
  }).join('\n');
141
141
  emit(
142
142
  `[Skills Atlas autopilot] The user may be doing something one of these installable agent ` +
@@ -144,9 +144,10 @@ module.exports = async function suggest() {
144
144
  `genuinely fits what they actually asked:\n${lines}\n` +
145
145
  `If one genuinely fits, DON'T just name it: in one line tell the user what it does and why it ` +
146
146
  `fits THIS task (it's a curated skill from the Skills Atlas catalog, not something you made up), ` +
147
- `then let them choose โ€” activate it now (\`skills-atlas use <skill> --yes\` installs + applies it ` +
147
+ `then let them choose โ€” activate it now (\`skills-atlas use <skill> --yes --project\` installs + applies it ` +
148
148
  `immediately), see what it covers first (\`skills-atlas info <skill>\`), or skip and you'll just ` +
149
- `do the task yourself. If none fit but the task plainly needs a specialized skill, you may run ` +
149
+ `do the task yourself; if they ask to never suggest it again, run \`skills-atlas feedback dismiss <skill>\`. ` +
150
+ `If none fit but the task plainly needs a specialized skill, you may run ` +
150
151
  `\`skills-atlas search "<short intent>"\` to look further. If nothing fits, say nothing about this ` +
151
152
  `at all โ€” don't mention this hook, these skills, or that a suggestion was made.` + langHint(ap.replyLang));
152
153
  state.lastSuggestedCount = state.count;
@@ -0,0 +1,86 @@
1
+ // Local suppression memory for the autopilot. Two scopes, because the two signals
2
+ // mean different things:
3
+ //
4
+ // โ€ข dismiss โ€” you explicitly said "never suggest this skill" โ†’ GLOBAL table
5
+ // โ€ข removed โ€” you removed a project-scoped skill you'd installed โ†’ PROJECT table
6
+ //
7
+ // A dismiss is a deliberate, blanket "no", so it applies everywhere. A removal is
8
+ // contextual ("done with it here") โ€” it must NOT leak to other projects, so it lives
9
+ // in a per-project table keyed by the project root. The LATEST action per skill wins:
10
+ // installing a skill clears both its global dismiss and this project's removal.
11
+ //
12
+ // All local; project tables live under the cache (keyed by path hash), never in the
13
+ // repo, so personal suppression is never committed. Nothing is sent anywhere.
14
+ 'use strict';
15
+
16
+ const fs = require('fs');
17
+ const os = require('os');
18
+ const path = require('path');
19
+ const crypto = require('crypto');
20
+
21
+ const MAX_EVENTS = 500;
22
+
23
+ function baseDir() {
24
+ const base = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
25
+ return path.join(base, 'skills-atlas');
26
+ }
27
+ function globalFile() { return path.join(baseDir(), 'feedback.json'); }
28
+ function projectFile(root) {
29
+ const key = crypto.createHash('sha1').update(path.resolve(root || process.cwd())).digest('hex').slice(0, 16);
30
+ return path.join(baseDir(), 'projects', `${key}.json`);
31
+ }
32
+
33
+ function readStore(file) {
34
+ try { const s = JSON.parse(fs.readFileSync(file, 'utf8')); if (!Array.isArray(s.events)) s.events = []; return s; }
35
+ catch { return { events: [] }; }
36
+ }
37
+ function writeStore(file, s) {
38
+ try { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, JSON.stringify(s)); } catch { /* best-effort */ }
39
+ }
40
+ function append(file, skill, signal) {
41
+ if (!skill || !signal) return;
42
+ const s = readStore(file);
43
+ s.events.push({ skill, signal, at: new Date().toISOString() });
44
+ if (s.events.length > MAX_EVENTS) s.events = s.events.slice(-MAX_EVENTS);
45
+ writeStore(file, s);
46
+ }
47
+
48
+ // Pure: skills whose MOST RECENT action in this store is the suppressing signal.
49
+ // (An 'accepted' afterwards clears it โ€” you installed it, so you changed your mind.)
50
+ function suppressedFrom(events, suppressSignal) {
51
+ const latest = new Map();
52
+ for (const e of events || []) {
53
+ if (!e || !e.skill) continue;
54
+ const prev = latest.get(e.skill);
55
+ if (!prev || Date.parse(e.at) >= Date.parse(prev.at)) latest.set(e.skill, e);
56
+ }
57
+ const out = new Set();
58
+ for (const [skill, e] of latest) if (e.signal === suppressSignal) out.add(skill);
59
+ return out;
60
+ }
61
+
62
+ // --- writes ---
63
+ function dismiss(skill) { append(globalFile(), skill, 'dismissed'); } // explicit "never, anywhere"
64
+ function removedInProject(skill, root) { append(projectFile(root), skill, 'removed'); } // "not in this project"
65
+ function installed(skill, root) { append(globalFile(), skill, 'accepted'); append(projectFile(root), skill, 'accepted'); }
66
+
67
+ // --- reads ---
68
+ function globalDismissed() { return suppressedFrom(readStore(globalFile()).events, 'dismissed'); }
69
+ function projectRemoved(root) { return suppressedFrom(readStore(projectFile(root)).events, 'removed'); }
70
+
71
+ // Merged view for the autopilot hook, which runs in a project cwd: a skill is hidden
72
+ // if it's globally dismissed OR removed in this project.
73
+ function current(root) {
74
+ const global = globalDismissed();
75
+ const project = projectRemoved(root);
76
+ const suppressed = new Set([...global, ...project]);
77
+ return { suppressed, isSuppressed: s => suppressed.has(s), global, project };
78
+ }
79
+
80
+ function clear(root) { writeStore(globalFile(), { events: [] }); writeStore(projectFile(root), { events: [] }); }
81
+
82
+ module.exports = {
83
+ globalFile, projectFile, readStore, writeStore,
84
+ dismiss, removedInProject, installed,
85
+ globalDismissed, projectRemoved, current, clear,
86
+ };
@@ -270,10 +270,11 @@ function contentDf(rows) {
270
270
  // `fire` is true only when there's a distinctive anchor (a rare name word, or โ‰ฅ2
271
271
  // matched name words) or an otherwise strong (non-weak) match โ€” so the hook stays
272
272
  // quiet on greetings and generic dev actions ("fix the typo", "build the app").
273
- function suggestCandidates(rows, prompt, { installed = new Set(), suggested = new Set(), limit = 5 } = {}) {
273
+ function suggestCandidates(rows, prompt, { installed = new Set(), suggested = new Set(), feedback = null, limit = 5 } = {}) {
274
274
  const tokens = tokenize(lc(prompt));
275
275
  if (tokens.length < 2) return { fire: false, candidates: [], weak: false };
276
- const taken = new Set([...installed, ...suggested]);
276
+ // `taken` also drops what you've dismissed or installed-then-removed โ€” never re-suggest those.
277
+ const taken = new Set([...installed, ...suggested, ...(feedback ? feedback.suppressed : [])]);
277
278
  const info = segmentDf(rows);
278
279
 
279
280
  // 1. name anchors โ€” ONLY contentful (non-generic) name words count, weighted by
@@ -365,6 +366,9 @@ function suggestCandidates(rows, prompt, { installed = new Set(), suggested = ne
365
366
  anchors.some(a => a.strong >= 2 || a.specific >= 1) ||
366
367
  contentAnchors.some(contentQualifies)
367
368
  );
369
+ // NB: feedback only SUPPRESSES (via `taken` above) โ€” that changes which skills make
370
+ // the shortlist. We deliberately don't reorder the final โ‰ค5: Claude reads all of them
371
+ // and picks by fit, so reordering them would do nothing.
368
372
  return { fire, candidates: out.slice(0, limit), weak };
369
373
  }
370
374