greprag 5.78.7 → 5.80.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.
@@ -66,11 +66,11 @@ function adapterFor(skill) {
66
66
  function safeTimestamp(now) {
67
67
  return now.toISOString().replace(/[:.]/g, '-');
68
68
  }
69
- function requiredSkills(skills) {
69
+ function requiredSkills(skills, names) {
70
70
  const byName = new Map(skills
71
71
  .filter(skill => skill.scope !== 'project')
72
72
  .map(skill => [skill.skillName, skill]));
73
- return exports.CLAUDE_LIFECYCLE_SKILLS.map(name => {
73
+ return names.map(name => {
74
74
  const skill = byName.get(name);
75
75
  if (!skill)
76
76
  throw new Error(`Canonical Claude lifecycle skill missing: ${name}.`);
@@ -80,12 +80,17 @@ function requiredSkills(skills) {
80
80
  /**
81
81
  * Explicit, narrow Claude cutover. Only SKILL.md becomes mirror-managed;
82
82
  * companion scripts and docs remain untouched in the native package.
83
+ *
84
+ * `names` selects which mirrored skills to materialize; it defaults to the
85
+ * lifecycle triple. Naming skills explicitly is how a Claude Code operator
86
+ * closes an adapter gap without arming a fleet-wide sweep.
83
87
  */
84
88
  function applyClaudeLifecycleCutover(params) {
85
89
  const homeDir = params.homeDir || os.homedir();
86
90
  const root = (0, native_skill_mirror_1.nativeSkillRoot)('claude-code', homeDir);
87
91
  const state = readState(root);
88
- const prepared = requiredSkills(params.skills).map(skill => {
92
+ const names = params.names?.length ? params.names : exports.CLAUDE_LIFECYCLE_SKILLS;
93
+ const prepared = requiredSkills(params.skills, names).map(skill => {
89
94
  const target = path.join(root, skill.skillName);
90
95
  const skillMd = path.join(target, 'SKILL.md');
91
96
  const adapterFiles = adapterFor(skill);
@@ -137,16 +142,18 @@ function applyClaudeLifecycleCutover(params) {
137
142
  return result;
138
143
  }
139
144
  async function syncClaudeLifecycleCutover(params) {
145
+ const names = params.names?.length ? [...new Set(params.names)] : [...exports.CLAUDE_LIFECYCLE_SKILLS];
140
146
  const list = await fetch(`${params.apiUrl}/v1/skillmirror`, {
141
147
  headers: { Authorization: `Bearer ${params.apiKey}` },
142
148
  });
143
149
  if (!list.ok)
144
150
  throw new Error(`skill mirror list failed: HTTP ${list.status}`);
145
151
  const data = await list.json();
146
- const wanted = new Set(exports.CLAUDE_LIFECYCLE_SKILLS);
152
+ const wanted = new Set(names);
147
153
  const rows = (data.skills || []).filter(row => wanted.has(row.skillName) && row.scope !== 'project');
148
- if (rows.length !== exports.CLAUDE_LIFECYCLE_SKILLS.length) {
149
- throw new Error('Canonical Claude lifecycle skill list is incomplete; native files preserved.');
154
+ if (rows.length !== names.length) {
155
+ const missing = names.filter(name => !rows.some(row => row.skillName === name));
156
+ throw new Error(`Not in the global skill mirror: ${missing.join(', ')}; native files preserved.`);
150
157
  }
151
158
  const fetched = await Promise.all(rows.map(async (row) => {
152
159
  const query = new URLSearchParams({ skillId: row.skillId || `global:${row.skillName}` });
@@ -170,5 +177,6 @@ async function syncClaudeLifecycleCutover(params) {
170
177
  return applyClaudeLifecycleCutover({
171
178
  skills: fetched,
172
179
  homeDir: params.homeDir,
180
+ names,
173
181
  });
174
182
  }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ /** `greprag announce` — print the full SessionStart announce.
3
+ *
4
+ * The Claude Code harness inlines only ~2KB of hook context (measured; see
5
+ * ANNOUNCE_INLINE_BUDGET). The recap hook therefore parks the complete announce
6
+ * under ~/.greprag/announce/<session>.md and inlines a pointer naming this
7
+ * command. Without it the overflow is unreachable — which is exactly the
8
+ * failure this exists to end.
9
+ *
10
+ * adr: adr/announce-inline-budget.md
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.runAnnounce = runAnnounce;
47
+ const fs = __importStar(require("fs"));
48
+ const os = __importStar(require("os"));
49
+ const path = __importStar(require("path"));
50
+ const session_id_1 = require("../session-id");
51
+ function announceDir() {
52
+ return path.join(os.homedir(), '.greprag', 'announce');
53
+ }
54
+ /** Newest cached announce, by mtime. Used when no session id is given — the
55
+ * common case, since an agent asking for its own announce rarely wants to
56
+ * first go find its session id. */
57
+ function newestCached() {
58
+ try {
59
+ const dir = announceDir();
60
+ const files = fs.readdirSync(dir)
61
+ .filter(f => f.endsWith('.md'))
62
+ .map(f => ({ f, t: fs.statSync(path.join(dir, f)).mtimeMs }))
63
+ .sort((a, b) => b.t - a.t);
64
+ return files.length ? path.join(dir, files[0].f) : null;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ function runAnnounce(args) {
71
+ if (args.includes('--help') || args.includes('-h')) {
72
+ process.stdout.write('greprag announce — print the full SessionStart announce\n\n'
73
+ + 'USAGE\n'
74
+ + ' greprag announce [--session <8hex>] [--path]\n\n'
75
+ + ' The harness inlines only ~2KB of session-start context. The rest is\n'
76
+ + ' parked on disk; this prints all of it.\n\n'
77
+ + ' --session <8hex> a specific session (default: most recent)\n'
78
+ + ' --path print the cache file path instead of its contents\n');
79
+ return;
80
+ }
81
+ const i = args.indexOf('--session');
82
+ const wanted = i !== -1 ? args[i + 1] : undefined;
83
+ const file = wanted
84
+ ? path.join(announceDir(), `${(0, session_id_1.truncateSessionId)(wanted) || wanted}.md`)
85
+ : newestCached();
86
+ if (!file || !fs.existsSync(file)) {
87
+ process.stderr.write('No cached announce found. It is written at SessionStart, so a session '
88
+ + 'must have started since this greprag version was installed.\n');
89
+ process.exitCode = 1;
90
+ return;
91
+ }
92
+ if (args.includes('--path')) {
93
+ process.stdout.write(file + '\n');
94
+ return;
95
+ }
96
+ process.stdout.write(fs.readFileSync(file, 'utf-8').trimEnd() + '\n');
97
+ }
@@ -1459,13 +1459,26 @@ function applySettings(settings, apiKey) {
1459
1459
  matcher: '',
1460
1460
  hooks: [{ type: 'command', command: 'greprag-hook store', timeout: 10000 }],
1461
1461
  };
1462
+ // Migrate stale installs onto the every-source matcher. `hasGrepragHook` only
1463
+ // asks whether a recap hook EXISTS, so an install written by an older version
1464
+ // kept its narrow matcher forever while init reported "already configured".
1465
+ // Field state 2026-09-03: `matcher: 'startup'`, so a resumed session — and
1466
+ // every /new and dashboard-dispatched agent — got no announce at all, Persona
1467
+ // included. Same failure and same fix as the Grok path above.
1468
+ // adr: adr/announce-inline-budget.md
1469
+ const retargetedRecap = settings.hooks.SessionStart
1470
+ ? retargetGrepragHookMatcher(settings.hooks.SessionStart, 'recap', ['startup', 'startup|resume', 'startup|resume|compact', 'startup|clear|compact'], recapHook.matcher)
1471
+ : 0;
1472
+ if (retargetedRecap > 0) {
1473
+ changes.push(`Retargeted SessionStart recap matcher to every source (${retargetedRecap})`);
1474
+ }
1462
1475
  if (!hasGrepragHook(settings.hooks.SessionStart, 'recap')) {
1463
1476
  if (!settings.hooks.SessionStart)
1464
1477
  settings.hooks.SessionStart = [];
1465
1478
  settings.hooks.SessionStart.push(recapHook);
1466
1479
  changes.push('Added SessionStart hook (memory recap + inbox digest)');
1467
1480
  }
1468
- else {
1481
+ else if (retargetedRecap === 0) {
1469
1482
  changes.push('SessionStart hook already configured (skipped)');
1470
1483
  }
1471
1484
  if (!hasGrepragHook(settings.hooks.Stop, 'store')) {
@@ -55,6 +55,9 @@ exports.runLoad = runLoad;
55
55
  exports.normalizeLoadName = normalizeLoadName;
56
56
  const fs = __importStar(require("fs"));
57
57
  const path = __importStar(require("path"));
58
+ const os = __importStar(require("os"));
59
+ const harness_1 = require("../harness");
60
+ const skill_activation_manifest_1 = require("../skill-activation-manifest");
58
61
  const project_anchor_1 = require("../project-anchor");
59
62
  const proc_1 = require("../proc");
60
63
  const skill_staleness_1 = require("../skill-staleness");
@@ -171,13 +174,30 @@ async function printCatalog() {
171
174
  const data = await mirrorGet('/v1/skillmirror');
172
175
  const skills = data?.skills || [];
173
176
  if (skills.length > 0) {
177
+ // A mirrored skill with no native adapter on this harness never reaches the
178
+ // harness's own skill list — the agent can only reach it through this
179
+ // catalog. Mark the gap here and name the command that closes it.
180
+ const platform = (0, harness_1.inferCurrentHarness)() || '';
181
+ const installed = platform
182
+ ? (0, skill_activation_manifest_1.readNativeSkillNames)({ platform, cwd: process.cwd(), homeDir: os.homedir() })
183
+ : new Set();
184
+ const missing = platform
185
+ ? skills.map((s) => s.skillName || '').filter((n) => n && !installed.has(n.toLowerCase()))
186
+ : [];
174
187
  const w = Math.max(...skills.map((s) => (s.skillName || '').length), 8);
175
188
  console.log('\nYour mirrored skills (auto-fresh from use — work on any machine/harness):\n');
176
189
  for (const s of skills) {
177
190
  if (!s.skillName)
178
191
  continue;
179
192
  const scope = s.scope === 'project' && s.projectId ? ` [project:${s.projectId.slice(0, 8)}]` : '';
180
- console.log(` ${(s.skillName + scope).padEnd(w + 20)} ${(s.description || '').slice(0, 110)}`);
193
+ const gap = missing.includes(s.skillName) ? ' *' : '';
194
+ console.log(` ${(s.skillName + scope + gap).padEnd(w + 20)} ${(s.description || '').slice(0, 110)}`);
195
+ }
196
+ const installCommand = (0, skill_activation_manifest_1.nativeAdapterInstallCommand)(platform, missing);
197
+ if (installCommand) {
198
+ console.log(`\n * ${missing.length} skill(s) have no ${platform} adapter installed — they never appear`);
199
+ console.log(' in this harness\'s own skill list. Install the adapters with:\n');
200
+ console.log(` ${installCommand}\n`);
181
201
  }
182
202
  }
183
203
  console.log('\n greprag load <name> print the full entry / mirrored skill');
@@ -5,10 +5,12 @@
5
5
  * PURE over ReminderEnv — the hook assembles env (i/o) and emits the returned lines;
6
6
  * a broken module never blocks a turn (fail-open per module). */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.compactReannounceModules = exports.commandModules = exports.promptModules = exports.harnessModules = exports.REGISTRY = void 0;
8
+ exports.ANNOUNCE_HARNESS_CAP = exports.ANNOUNCE_INLINE_BUDGET = exports.compactReannounceModules = exports.commandModules = exports.promptModules = exports.harnessModules = exports.REGISTRY = void 0;
9
9
  exports.collectReminders = collectReminders;
10
10
  exports.bootOrder = bootOrder;
11
11
  exports.collectAnnounces = collectAnnounces;
12
+ exports.collectAnnounceBlocks = collectAnnounceBlocks;
13
+ exports.fitAnnounceBudget = fitAnnounceBudget;
12
14
  const os_primer_reminder_1 = require("./os-primer-reminder");
13
15
  const inbox_primer_reminder_1 = require("./inbox-primer-reminder");
14
16
  const load_primer_reminder_1 = require("./load-primer-reminder");
@@ -147,3 +149,89 @@ function collectAnnounces(env, registry = exports.REGISTRY) {
147
149
  }
148
150
  return out;
149
151
  }
152
+ /** Same as collectAnnounces, but keeps each block paired with the module that
153
+ * produced it so the budget fitter can rank and name them.
154
+ * adr: adr/announce-inline-budget.md */
155
+ function collectAnnounceBlocks(env, registry = exports.REGISTRY) {
156
+ const out = [];
157
+ for (const m of bootOrder(registry)) {
158
+ let a = null;
159
+ try {
160
+ a = m.announce(env);
161
+ }
162
+ catch {
163
+ continue;
164
+ }
165
+ if (a)
166
+ out.push({ id: m.id, text: a });
167
+ }
168
+ return out;
169
+ }
170
+ /** Bytes of SessionStart context a harness will actually inline. MEASURED, not
171
+ * guessed: Claude Code wraps any hook output over 2048 bytes in
172
+ * `<persisted-output>`, spills the full text to a tool-results file, and injects
173
+ * only "Preview (first 2KB)". This is true of BOTH raw stdout and the
174
+ * `additionalContext` envelope — the cap is on inline context, not the channel.
175
+ * Measured 2026-09-03 with an 80-marker ruler: markers 250..2000 arrived, 2250
176
+ * and beyond did not. 1800 leaves headroom for the harness's own wrapper text.
177
+ * adr: adr/announce-inline-budget.md */
178
+ exports.ANNOUNCE_INLINE_BUDGET = 1650;
179
+ /** The harness's hard ceiling. Past this, output is replaced by a 2KB preview and
180
+ * a `<persisted-output>` file reference. ANNOUNCE_INLINE_BUDGET must stay under
181
+ * it with room for the trailing memory-recap pointer. */
182
+ exports.ANNOUNCE_HARNESS_CAP = 2048;
183
+ /** Inline-worthiness ranking, most-keepable first. The rule: a block earns inline
184
+ * space when it is SHORT, SESSION-SPECIFIC, and available nowhere else. A block
185
+ * that is long, static, and already loadable on demand does not — it is exactly
186
+ * what `greprag load` exists for. Anything unlisted sorts last.
187
+ *
188
+ * Persona leads because it is 523 bytes of tenant-set speaking instructions that
189
+ * no other surface carries. The grepragOS laws are deliberately NOT here: 1753
190
+ * bytes of static doctrine that `greprag load os` already serves on demand, which
191
+ * until now consumed the entire budget and starved everything behind it. */
192
+ const ANNOUNCE_PRIORITY = [
193
+ 'persona-announce',
194
+ 'setup-warning',
195
+ 'version-upgrade',
196
+ 'enrichment-health',
197
+ 'watcher-arm',
198
+ 'skill-mirror-announce',
199
+ 'delivery-control',
200
+ 'doc-pointer-announce',
201
+ ];
202
+ function announceRank(id) {
203
+ const i = ANNOUNCE_PRIORITY.indexOf(id);
204
+ return i === -1 ? ANNOUNCE_PRIORITY.length : i;
205
+ }
206
+ /** Fit the announce into the harness's inline budget.
207
+ *
208
+ * Returns the blocks that fit (restored to boot order, so a primer still precedes
209
+ * anything that depends on it) plus the ids that did not. The caller is expected
210
+ * to persist the FULL text and give the agent a way to read it — dropping content
211
+ * silently is the failure this whole mechanism exists to end.
212
+ *
213
+ * A single block larger than the budget is never emitted; it would blow the cap
214
+ * on its own and take everything after it down too. */
215
+ function fitAnnounceBudget(blocks, budget = exports.ANNOUNCE_INLINE_BUDGET, reserve = 0) {
216
+ const order = new Map(blocks.map((b, i) => [b.id, i]));
217
+ const ranked = [...blocks].sort((a, b) => {
218
+ const d = announceRank(a.id) - announceRank(b.id);
219
+ return d !== 0 ? d : (order.get(a.id) - order.get(b.id));
220
+ });
221
+ const kept = [];
222
+ const droppedIds = [];
223
+ let used = reserve;
224
+ const SEP = 2; // the '\n\n' join between blocks
225
+ for (const b of ranked) {
226
+ const cost = b.text.length + (kept.length ? SEP : 0);
227
+ if (used + cost <= budget) {
228
+ kept.push(b);
229
+ used += cost;
230
+ }
231
+ else {
232
+ droppedIds.push(b.id);
233
+ }
234
+ }
235
+ kept.sort((a, b) => order.get(a.id) - order.get(b.id));
236
+ return { kept, droppedIds };
237
+ }
@@ -8,13 +8,14 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.skillMirrorAnnounceModule = exports.SKILL_ACTIVATION_MANIFEST_MAX_CHARS = void 0;
10
10
  exports.buildSkillMirrorAnnounce = buildSkillMirrorAnnounce;
11
+ const skill_activation_manifest_1 = require("../skill-activation-manifest");
11
12
  exports.SKILL_ACTIVATION_MANIFEST_MAX_CHARS = 64_000;
12
13
  function renderEntry(skill) {
13
14
  const description = skill.triggerDescription.trim().replace(/\r\n?/g, '\n');
14
15
  const lines = description.split('\n');
15
16
  return `- ${skill.skillName}: ${lines[0]}${lines.slice(1).map(line => `\n ${line}`).join('')}`;
16
17
  }
17
- function buildSkillMirrorAnnounce(m) {
18
+ function buildSkillMirrorAnnounce(m, platform) {
18
19
  if (!m || m.count <= 0)
19
20
  return null;
20
21
  if (m.skills.length === 0) {
@@ -23,6 +24,21 @@ function buildSkillMirrorAnnounce(m) {
23
24
  + 'keep storage mechanics internal unless diagnosing a failure or the user asks how it works. '
24
25
  + 'Use `greprag load <skill>` to read skill instructions.]';
25
26
  }
27
+ // The entries this harness does not advertise are exactly the mirrored
28
+ // skills with no native adapter installed. Name the gap and the one command
29
+ // that closes it, or it stays invisible until the operator spots it by hand.
30
+ const installCommand = (0, skill_activation_manifest_1.nativeAdapterInstallCommand)(platform || '', m.skills.map(s => s.skillName));
31
+ const gapSection = installCommand ? [
32
+ '### Missing Native Adapters',
33
+ `${m.skills.length} of these skills are in your GrepRAG mirror but have NO adapter installed in this harness, `
34
+ + 'so they never appear in the native skill list. Install the adapters once with:',
35
+ '',
36
+ ` ${installCommand}`,
37
+ '',
38
+ 'Offer that command when the user asks why a skill is missing, or when you needed one of them this session. '
39
+ + 'Until adapters are installed, the trigger rules below are the only way these skills activate.',
40
+ '',
41
+ ] : [];
26
42
  const header = [
27
43
  '## GrepRAG Skills',
28
44
  `Slash skills are available through \`greprag load\`; ${m.skills.length} skills need this startup trigger list`
@@ -30,6 +46,7 @@ function buildSkillMirrorAnnounce(m) {
30
46
  + '.',
31
47
  'When discussing skills with the user, say use/create/update/refresh `/skill`; keep storage mechanics internal unless diagnosing a failure or the user asks how it works.',
32
48
  '',
49
+ ...gapSection,
33
50
  '### Trigger Rules',
34
51
  'If the user names a skill (with or without `/`) OR the request clearly matches a skill\'s description below, '
35
52
  + 'you MUST activate that skill for the current turn. Match meaning, not exact keywords.',
@@ -58,6 +75,6 @@ exports.skillMirrorAnnounceModule = {
58
75
  source: 'prompt',
59
76
  dependsOn: ['load-primer'], // references `greprag load` — the primer establishes it
60
77
  detect: (_env) => ({ tier: 'silent' }), // announce-only
61
- announce: (env) => buildSkillMirrorAnnounce(env.mirroredSkills),
78
+ announce: (env) => buildSkillMirrorAnnounce(env.mirroredSkills, env.platform),
62
79
  reminder: () => null,
63
80
  };
@@ -47,6 +47,7 @@ var __importStar = (this && this.__importStar) || (function () {
47
47
  };
48
48
  })();
49
49
  Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.readOnlyNames = readOnlyNames;
50
51
  exports.runSkill = runSkill;
51
52
  const fs = __importStar(require("fs"));
52
53
  const os = __importStar(require("os"));
@@ -59,6 +60,22 @@ const skill_mirror_client_1 = require("../skill-mirror-client");
59
60
  const skill_landing_1 = require("../skill-landing");
60
61
  const skill_staleness_1 = require("../skill-staleness");
61
62
  const CORE_SKILL = 'greprag';
63
+ const SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
64
+ /** `--only a,b` / `--only=a,b` / repeated `--only a --only b` → skill names. */
65
+ function readOnlyNames(args) {
66
+ const raw = [];
67
+ for (let i = 0; i < args.length; i++) {
68
+ if (args[i] === '--only' && args[i + 1])
69
+ raw.push(args[++i]);
70
+ else if (args[i].startsWith('--only='))
71
+ raw.push(args[i].slice('--only='.length));
72
+ }
73
+ const names = [...new Set(raw.flatMap(value => value.split(',')).map(v => v.trim()).filter(Boolean))];
74
+ const invalid = names.filter(name => !SKILL_NAME_RE.test(name));
75
+ if (invalid.length > 0)
76
+ throw new Error(`--only got invalid skill name(s): ${invalid.join(', ')}`);
77
+ return names;
78
+ }
62
79
  const SKILL_HELP = `greprag skill — manage bundled advisor skills
63
80
 
64
81
  USAGE
@@ -69,6 +86,8 @@ USAGE
69
86
  Export/edit/apply helper; opens $EDITOR unless --no-open or Codex
70
87
  greprag skill mirror sync [harness] Install native GrepRAG skill adapters
71
88
  claude-code --cutover-lifecycle Cut over commit/deploy/release only
89
+ claude-code --only <a,b> Install adapters for named mirrored skills
90
+ (closes the gaps greprag load marks with *)
72
91
  greprag skill mirror export <name> --to <dir>
73
92
  Export canonical skill bytes for editing
74
93
  greprag skill mirror apply <dir> --skill <name> --expected-hash <hash>
@@ -631,7 +650,11 @@ async function runSkill(args) {
631
650
  }
632
651
  const dryRun = args.includes('--dry-run');
633
652
  const claudeLifecycleCutover = args.includes('--cutover-lifecycle');
634
- const positional = args.slice(2).filter(arg => !arg.startsWith('-'));
653
+ // `--only a,b` names the mirrored skills to materialize as claude-code
654
+ // adapters — the one command that closes an adapter gap the catalog and
655
+ // the SessionStart announce report, without arming a fleet-wide sweep.
656
+ const onlyNames = readOnlyNames(args.slice(2));
657
+ const positional = args.slice(2).filter((arg, index, all) => !arg.startsWith('-') && all[index - 1] !== '--only');
635
658
  const requested = positional[0] || 'all';
636
659
  const platforms = requested === 'all'
637
660
  ? [...native_skill_mirror_1.NATIVE_SKILL_ADAPTER_PLATFORMS]
@@ -645,23 +668,28 @@ async function runSkill(args) {
645
668
  if (claudeLifecycleCutover && requested !== 'claude-code') {
646
669
  throw new Error('--cutover-lifecycle requires harness claude-code.');
647
670
  }
671
+ if (onlyNames.length > 0 && requested !== 'claude-code') {
672
+ throw new Error('--only requires harness claude-code.');
673
+ }
674
+ const claudeTargeted = claudeLifecycleCutover || onlyNames.length > 0;
648
675
  const apiUrl = process.env.GREPRAG_API_URL || 'https://api.greprag.com';
649
676
  const anchor = (0, project_anchor_1.readAnchor)(process.cwd());
650
677
  if (requested === 'all') {
651
678
  console.log('claude-code: native skill adapter sync disabled until Claude cutover approval.');
652
679
  }
653
680
  for (const platform of platforms) {
654
- if (platform === 'claude-code' && claudeLifecycleCutover) {
681
+ if (platform === 'claude-code' && claudeTargeted) {
682
+ const names = onlyNames.length > 0 ? onlyNames : [...claude_lifecycle_cutover_1.CLAUDE_LIFECYCLE_SKILLS];
655
683
  if (dryRun) {
656
- console.log(`claude-code: targeted lifecycle cutover would manage only ${claude_lifecycle_cutover_1.CLAUDE_LIFECYCLE_SKILLS.join(', ')}; companion files remain untouched.`);
684
+ console.log(`claude-code: targeted cutover would manage only ${names.join(', ')}; companion files remain untouched.`);
657
685
  continue;
658
686
  }
659
687
  const apiKey = process.env.GREPRAG_API_KEY || '';
660
688
  if (!apiKey)
661
689
  throw new Error('GREPRAG_API_KEY not set — run `greprag init` first.');
662
- const result = await (0, claude_lifecycle_cutover_1.syncClaudeLifecycleCutover)({ apiUrl, apiKey });
690
+ const result = await (0, claude_lifecycle_cutover_1.syncClaudeLifecycleCutover)({ apiUrl, apiKey, names });
663
691
  const active = result.installed + result.updated + result.current;
664
- console.log(`claude-code: ${active} lifecycle adapter(s) active; all other Claude skills remain unmanaged.`);
692
+ console.log(`claude-code: ${active} adapter(s) active (${names.join(', ')}); all other Claude skills remain unmanaged.`);
665
693
  if (result.backupDir)
666
694
  console.log(`claude-code: replaced prompts backed up at ${result.backupDir}`);
667
695
  continue;
package/dist/hook-once.js CHANGED
@@ -41,6 +41,16 @@ const fs = __importStar(require("fs"));
41
41
  const os = __importStar(require("os"));
42
42
  const path = __importStar(require("path"));
43
43
  const TTL_MS = 60_000;
44
+ /** Events that fire once per TOOL CALL, not once per turn.
45
+ *
46
+ * The stamp key is (session, event, subcommand, turn), so it can only dedupe an
47
+ * event whose natural rate is one per turn — i.e. a leaked duplicate hook
48
+ * registration double-firing `store`/`recap`/`notify`. A per-call event issues
49
+ * many invocations under that ONE key, so claiming it lets the first call
50
+ * through and silently drops every later one. For a validator such as
51
+ * `pre-spawn-check` that is a fail-OPEN: the second chip of a turn is never
52
+ * checked. These events are not dedupable by this key and are never claimed. */
53
+ const PER_TOOL_CALL_EVENTS = new Set(['PreToolUse', 'PostToolUse']);
44
54
  function stampPath(sessionId, event, sub, turnId) {
45
55
  const safe = [sessionId, event, sub, turnId].join('-').replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 180);
46
56
  return path.join(os.homedir(), '.greprag', 'hook-once', `${safe}.json`);
@@ -49,6 +59,8 @@ function stampPath(sessionId, event, sub, turnId) {
49
59
  function claimHookOnce(sessionId, event, subcommand, turnId) {
50
60
  if (!sessionId)
51
61
  return true;
62
+ if (event && PER_TOOL_CALL_EVENTS.has(event))
63
+ return true;
52
64
  const key = turnId || 'noturn';
53
65
  const file = stampPath(sessionId, event || 'unknown', subcommand, key);
54
66
  try {