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.
- package/dist/claude-lifecycle-cutover.js +14 -6
- package/dist/commands/announce.js +97 -0
- package/dist/commands/init.js +14 -1
- package/dist/commands/load.js +21 -1
- package/dist/commands/reminder-registry.js +89 -1
- package/dist/commands/skill-mirror-reminder.js +19 -2
- package/dist/commands/skill.js +33 -5
- package/dist/hook-once.js +12 -0
- package/dist/hook.js +90 -10
- package/dist/index.js +8 -0
- package/dist/opencode-plugin.bundle.js +168 -143
- package/dist/skill-activation-manifest.js +16 -0
- package/package.json +1 -1
- package/skill/templates/chip-spawn.md +15 -30
|
@@ -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
|
|
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
|
|
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(
|
|
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 !==
|
|
149
|
-
|
|
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
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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')) {
|
package/dist/commands/load.js
CHANGED
|
@@ -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
|
-
|
|
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
|
};
|
package/dist/commands/skill.js
CHANGED
|
@@ -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
|
-
|
|
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' &&
|
|
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
|
|
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}
|
|
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 {
|