greprag 5.83.2 → 5.85.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.
Files changed (35) hide show
  1. package/dist/commands/agent-doctrine-file.js +5 -128
  2. package/dist/commands/agents.js +125 -846
  3. package/dist/commands/app-model.js +1 -1
  4. package/dist/commands/delivery-reminder.js +1 -1
  5. package/dist/commands/discord.js +12 -61
  6. package/dist/commands/inbox-watch.js +0 -13
  7. package/dist/commands/init.js +17 -29
  8. package/dist/commands/load.js +15 -35
  9. package/dist/commands/parity.js +1 -1
  10. package/dist/commands/reminder-registry.js +1 -8
  11. package/dist/commands/session.js +2 -2
  12. package/dist/commands/skill-mirror-reminder.js +1 -19
  13. package/dist/commands/skill.js +236 -448
  14. package/dist/hook.js +31 -54
  15. package/dist/index.js +19 -152
  16. package/dist/native-adapter-refresh.js +15 -18
  17. package/dist/native-skill-mirror.js +11 -89
  18. package/dist/opencode-plugin.bundle.js +82 -282
  19. package/dist/reminder-injector.js +1 -1
  20. package/dist/skill-activation-manifest.js +14 -37
  21. package/dist/skill-git-ingest.js +297 -0
  22. package/dist/skill-landing.js +7 -173
  23. package/dist/skill-mirror-client.js +4 -251
  24. package/dist/skill-staleness.js +3 -6
  25. package/package.json +1 -1
  26. package/skill/commander/SKILL.md +10 -12
  27. package/skill/greprag/SKILL.md +1 -1
  28. package/skill/greprag/docs/discord-handoff.md +6 -30
  29. package/skill/mechanic/docs/skill-fix-conventions.md +1 -1
  30. package/skill/templates/chip-spawn.md +6 -2
  31. package/skill/templates/delivery.md +16 -0
  32. package/skill/templates/os.md +1 -1
  33. package/skill/templates/prompt-audit.md +26 -49
  34. package/skill/templates/skill-change.md +15 -11
  35. package/dist/commands/sms.js +0 -215
@@ -195,7 +195,7 @@ async function injectReminders(input, cfg, short, platform = 'claude-code') {
195
195
  // Harness scoping comes from each module's own `harnesses:` tag (the whitelist-drift
196
196
  // killer), NOT from an ad hoc id filter here. The old hardcoded codex/watcher-arm
197
197
  // exclusion made the per-turn surface disagree with the announce surface.
198
- const base = (0, app_settings_1.filterEnabledInterrupts)((0, reminder_registry_1.harnessModules)(platform, (0, reminder_registry_1.promptModules)()));
198
+ const base = (0, app_settings_1.filterEnabledInterrupts)((0, reminder_registry_1.harnessModules)(platform));
199
199
  const reg = mechanicKilled() ? base.filter(m => m.id !== 'mechanic-friction') : base;
200
200
  const fired = (0, reminder_registry_1.collectReminders)(env, reg);
201
201
  if (!fired.length)
@@ -45,7 +45,6 @@ exports.readNativeSkillNames = readNativeSkillNames;
45
45
  exports.nativeAdapterGap = nativeAdapterGap;
46
46
  exports.buildMirroredSkillActivation = buildMirroredSkillActivation;
47
47
  exports.nativeAdapterInstallCommand = nativeAdapterInstallCommand;
48
- exports.localCopyCutoverCommand = localCopyCutoverCommand;
49
48
  exports.activationFromApiRows = activationFromApiRows;
50
49
  const fs = __importStar(require("fs"));
51
50
  const path = __importStar(require("path"));
@@ -170,30 +169,23 @@ function readNativeSkillNames(params) {
170
169
  }
171
170
  return names;
172
171
  }
173
- /** Mirrored skills this harness does not run from canon. `missing` have no
174
- * native package, so they never reach the harness's own skill list.
175
- * `localCopies` are listed from a hand-kept package instead of an adapter, so
176
- * the harness keeps running that copy after canon moves on. Only Claude Code
177
- * reports them: its targeted cutover is the one writer that replaces a global
178
- * package GrepRAG does not own, while the Codex/OpenCode sync leaves such
179
- * packages in place. An external package is neither: the harness lists it, and
180
- * its owner (a link target or another installer) is not GrepRAG. A bundled skill is never a gap; its canon is the bundle
181
- * the installer writes as a real body. adr: adr/skill-bundle-shape.md */
172
+ /** Mirrored skills with no native package on this harness, so they never reach
173
+ * its own skill list. Codex and OpenCode close that gap with launchers; Claude
174
+ * Code has none to install: it reads skills from their git folders, and a
175
+ * mirrored skill with no git folder is archived history `greprag load` still
176
+ * serves. A bundled skill is never a gap; its canon is the bundle the
177
+ * installer writes as a real body. adr: adr/skill-ownership-git.md */
182
178
  function nativeAdapterGap(platform, mirrored, native) {
183
179
  const missing = [];
184
- const localCopies = [];
185
- if (!platform)
186
- return { missing, localCopies };
187
- for (const { skillName, scope } of mirrored) {
180
+ if (platform !== 'codex' && platform !== 'opencode')
181
+ return { missing };
182
+ for (const { skillName } of mirrored) {
188
183
  if (!skillName || skill_landing_1.BUNDLED_SKILLS.includes(skillName))
189
184
  continue;
190
- const kind = native.get(skillName.toLowerCase());
191
- if (!kind)
185
+ if (!native.get(skillName.toLowerCase()))
192
186
  missing.push(skillName);
193
- else if (kind === 'local' && platform === 'claude-code' && scope !== 'project')
194
- localCopies.push(skillName);
195
187
  }
196
- return { missing, localCopies };
188
+ return { missing };
197
189
  }
198
190
  /** Remove native-advertised duplicates while retaining the canonical count. */
199
191
  function buildMirroredSkillActivation(entries, native, platform) {
@@ -208,27 +200,12 @@ function buildMirroredSkillActivation(entries, native, platform) {
208
200
  ...nativeAdapterGap(platform, valid, native),
209
201
  };
210
202
  }
211
- /** The one command that materializes native adapters for the named mirrored
212
- * skills on this harness. claude-code is not swept wholesale (adapters there
213
- * are opt-in per name); codex/opencode sync their whole roster. Null when
214
- * nothing is missing. */
203
+ /** The one command that installs the Codex/OpenCode launchers for mirrored
204
+ * skills. Null when nothing is missing or the harness takes no launchers. */
215
205
  function nativeAdapterInstallCommand(platform, missingNames) {
216
206
  if (missingNames.length === 0)
217
207
  return null;
218
- if (platform === 'claude-code') {
219
- return `greprag skill mirror sync claude-code --only ${[...missingNames].sort().join(',')}`;
220
- }
221
- if (platform === 'codex' || platform === 'opencode') {
222
- return `greprag skill mirror sync ${platform}`;
223
- }
224
- return null;
225
- }
226
- /** The command that replaces one hand-kept copy with its adapter, backing up
227
- * the copy's SKILL.md first. Replacing a live copy is a per-skill call, so the
228
- * command names one placeholder rather than the whole list. Null when there
229
- * are none. */
230
- function localCopyCutoverCommand(platform, localCopies) {
231
- return localCopies.length > 0 ? nativeAdapterInstallCommand(platform, ['<name>']) : null;
208
+ return platform === 'codex' || platform === 'opencode' ? `greprag skill mirror sync ${platform}` : null;
232
209
  }
233
210
  /** Convert the mirror API response into a harness-specific activation signal. */
234
211
  function activationFromApiRows(params) {
@@ -0,0 +1,297 @@
1
+ "use strict";
2
+ /** Git-sourced skill mirror ingestion: the only writer of skill canon.
3
+ *
4
+ * A skill's one true copy is its git folder: dotclaude `skills/<name>/` for
5
+ * global skills, `<repo>/.claude/skills/<name>/` for project skills. This
6
+ * reads COMMITTED bytes (`HEAD`, never the working tree, so an uncommitted
7
+ * edit is served nowhere) and uploads a skill only when its committed content
8
+ * differs from the mirror. Each upload records `git <hash>: <subject>` as the
9
+ * revision reason.
10
+ *
11
+ * Cheap on every session start: one `git ls-tree` per source lists each skill
12
+ * folder's tree hash; only folders whose tree changed since the last run are
13
+ * read (one `git cat-file --batch` for all their files) and hashed.
14
+ *
15
+ * adr: adr/skill-ownership-git.md */
16
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ var desc = Object.getOwnPropertyDescriptor(m, k);
19
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
+ desc = { enumerable: true, get: function() { return m[k]; } };
21
+ }
22
+ Object.defineProperty(o, k2, desc);
23
+ }) : (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ }));
27
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
29
+ }) : function(o, v) {
30
+ o["default"] = v;
31
+ });
32
+ var __importStar = (this && this.__importStar) || (function () {
33
+ var ownKeys = function(o) {
34
+ ownKeys = Object.getOwnPropertyNames || function (o) {
35
+ var ar = [];
36
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
37
+ return ar;
38
+ };
39
+ return ownKeys(o);
40
+ };
41
+ return function (mod) {
42
+ if (mod && mod.__esModule) return mod;
43
+ var result = {};
44
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
45
+ __setModuleDefault(result, mod);
46
+ return result;
47
+ };
48
+ })();
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.gitSkillPath = gitSkillPath;
51
+ exports.globalSkillSource = globalSkillSource;
52
+ exports.projectSkillSource = projectSkillSource;
53
+ exports.listCommittedSkills = listCommittedSkills;
54
+ exports.readCommittedSkillFiles = readCommittedSkillFiles;
55
+ exports.ingestCommittedSkills = ingestCommittedSkills;
56
+ exports.fetchMirrorHashes = fetchMirrorHashes;
57
+ exports.gitSkillSources = gitSkillSources;
58
+ const fs = __importStar(require("node:fs"));
59
+ const os = __importStar(require("node:os"));
60
+ const path = __importStar(require("node:path"));
61
+ const proc_1 = require("./proc");
62
+ const project_anchor_1 = require("./project-anchor");
63
+ const skill_landing_1 = require("./skill-landing");
64
+ const skill_mirror_files_1 = require("./skill-mirror-files");
65
+ /** The server's caps (SKILL_MIRROR_* in @greprag/core), restated: the CLI is core-free. */
66
+ const MAX_FILES = 20;
67
+ const MAX_FILE_CHARS = 100_000;
68
+ const SKILL_NAME_RE = /^[A-Za-z0-9._-]{1,64}$/;
69
+ const STATE_FILE = path.join(os.homedir(), '.greprag', 'state', 'skill-git-ingest.json');
70
+ function git(repo, args, input) {
71
+ const r = (0, proc_1.safeSpawnSync)('git', args, { cwd: repo, encoding: 'utf8', timeout: 15_000, input, maxBuffer: 64 << 20 });
72
+ return r.status === 0 ? String(r.stdout || '') : null;
73
+ }
74
+ function gitTop(dir) {
75
+ const out = fs.existsSync(dir) ? git(dir, ['rev-parse', '--show-toplevel']) : null;
76
+ return out ? path.resolve(out.trim()) : null;
77
+ }
78
+ /** Where Claude Code finds a skill, and so where to edit it. */
79
+ function gitSkillPath(scope, name, projectRoot) {
80
+ return scope === 'project' && projectRoot
81
+ ? path.join(projectRoot, '.claude', 'skills', name)
82
+ : path.join(os.homedir(), '.claude', 'skills', name);
83
+ }
84
+ function globalSkillSource(home = os.homedir()) {
85
+ const dir = path.join(home, '.claude', 'skills');
86
+ const repo = gitTop(dir);
87
+ if (!repo)
88
+ return null;
89
+ const prefix = path.relative(repo, dir).split(path.sep).join('/');
90
+ return { scope: 'global', repo, prefix };
91
+ }
92
+ /** The current repo's `.claude/skills`, unless the repo is the dotclaude repo
93
+ * itself (its skills are global) or the folder is absent. */
94
+ function projectSkillSource(cwd, home = os.homedir()) {
95
+ const repo = gitTop(cwd);
96
+ if (!repo)
97
+ return null;
98
+ const dotclaude = gitTop(path.join(home, '.claude'));
99
+ if (dotclaude && path.resolve(dotclaude) === repo)
100
+ return null;
101
+ if (!fs.existsSync(path.join(repo, '.claude', 'skills')))
102
+ return null;
103
+ const anchor = (0, project_anchor_1.readAnchor)(repo);
104
+ if (!anchor.initialized)
105
+ return null;
106
+ return { scope: 'project', repo, prefix: '.claude/skills', projectId: anchor.projectId, projectName: anchor.projectName };
107
+ }
108
+ /** Skill folders committed at HEAD under the source prefix, with tree hashes.
109
+ * A folder that is a link in the working tree belongs to the repo it points
110
+ * at and is ingested from there. */
111
+ function listCommittedSkills(source) {
112
+ const out = git(source.repo, ['ls-tree', 'HEAD', `${source.prefix}/`]);
113
+ if (!out)
114
+ return [];
115
+ const skills = [];
116
+ for (const line of out.split('\n')) {
117
+ const m = line.match(/^\d+ tree ([0-9a-f]+)\t(.+)$/);
118
+ if (!m)
119
+ continue;
120
+ const folder = m[2];
121
+ const name = folder.split('/').pop() || '';
122
+ if (!SKILL_NAME_RE.test(name))
123
+ continue;
124
+ // A bundled skill's source is packages/cli/skill/<name>/ and the bundle serves it.
125
+ if (source.scope === 'global' && skill_landing_1.BUNDLED_SKILLS.includes(name))
126
+ continue;
127
+ if ((0, skill_mirror_files_1.isLinkedSkillFolder)(path.join(source.repo, ...folder.split('/'))))
128
+ continue;
129
+ skills.push({ name, folder, tree: m[1] });
130
+ }
131
+ return skills;
132
+ }
133
+ /** Read the committed mirror file set of each skill: SKILL.md plus
134
+ * `docs/**\/*.md`, the mirror's file contract. One `git cat-file --batch`. */
135
+ function readCommittedSkillFiles(repo, skills) {
136
+ const wanted = [];
137
+ if (!skills.length)
138
+ return new Map();
139
+ // One listing for every folder: a spawn per skill costs seconds on Windows.
140
+ const byFolder = new Map();
141
+ const listing = git(repo, ['ls-tree', '-r', '--name-only', 'HEAD', '--', ...new Set(skills.map(s => `${s.folder}/`))]) || '';
142
+ for (const p of listing.split('\n').filter(Boolean)) {
143
+ const skill = skills.find(s => p.startsWith(`${s.folder}/`));
144
+ if (skill)
145
+ byFolder.set(skill.folder, [...(byFolder.get(skill.folder) || []), p]);
146
+ }
147
+ for (const skill of skills) {
148
+ const rels = (byFolder.get(skill.folder) || []).map(p => p.slice(skill.folder.length + 1))
149
+ .filter(rel => rel === 'SKILL.md' || (rel.startsWith('docs/') && rel.toLowerCase().endsWith('.md')))
150
+ .sort((a, b) => (a === 'SKILL.md' ? -1 : b === 'SKILL.md' ? 1 : a.localeCompare(b)))
151
+ .slice(0, MAX_FILES);
152
+ if (!rels.includes('SKILL.md'))
153
+ continue;
154
+ for (const rel of rels)
155
+ wanted.push({ skill: skill.name, rel, spec: `HEAD:${skill.folder}/${rel}` });
156
+ }
157
+ const result = new Map();
158
+ if (!wanted.length)
159
+ return result;
160
+ const r = (0, proc_1.safeSpawnSync)('git', ['cat-file', '--batch'], {
161
+ cwd: repo, input: wanted.map(w => w.spec).join('\n') + '\n', timeout: 30_000, maxBuffer: 256 << 20,
162
+ });
163
+ if (r.status !== 0 || !r.stdout)
164
+ return result;
165
+ const buf = r.stdout;
166
+ let at = 0;
167
+ for (const w of wanted) {
168
+ const nl = buf.indexOf(0x0a, at);
169
+ const header = buf.subarray(at, nl).toString('utf8');
170
+ at = nl + 1;
171
+ const m = header.match(/^[0-9a-f]+ blob (\d+)$/);
172
+ if (!m)
173
+ continue;
174
+ const size = Number(m[1]);
175
+ const content = buf.subarray(at, at + size).toString('utf8');
176
+ at += size + 1;
177
+ if (content.length > MAX_FILE_CHARS)
178
+ continue;
179
+ const files = result.get(w.skill) || [];
180
+ files.push({ path: w.rel, content });
181
+ result.set(w.skill, files);
182
+ }
183
+ // A committed launcher is never content (adr/skill-adapter-never-canon.md).
184
+ for (const [name, files] of result) {
185
+ const main = files.find(f => f.path === 'SKILL.md');
186
+ if (!main || (0, skill_mirror_files_1.isGeneratedSkillAdapter)(main.content))
187
+ result.delete(name);
188
+ }
189
+ return result;
190
+ }
191
+ function lastCommit(repo, folder) {
192
+ const out = (git(repo, ['log', '-1', '--format=%h%x00%s', 'HEAD', '--', folder]) || '').trim();
193
+ const [hash = 'unknown', subject = ''] = out.split('\0');
194
+ return { hash, subject };
195
+ }
196
+ function readState() {
197
+ try {
198
+ return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
199
+ }
200
+ catch {
201
+ return {};
202
+ }
203
+ }
204
+ function writeState(state) {
205
+ try {
206
+ fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
207
+ fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 1));
208
+ }
209
+ catch { /* best effort: the next run re-reads every folder */ }
210
+ }
211
+ function stateKey(source, name) {
212
+ return `${source.scope}:${source.projectId || ''}:${name}`;
213
+ }
214
+ /** Upload every committed skill whose content differs from the mirror.
215
+ * `mirrorHashes` maps `<scope>:<name>` to the mirror's contentHash. `force`
216
+ * re-reads folders whose tree hash is unchanged since the last run. */
217
+ async function ingestCommittedSkills(params) {
218
+ const result = { uploaded: [], current: 0, failed: [] };
219
+ const state = readState();
220
+ for (const source of params.sources) {
221
+ const all = listCommittedSkills(source);
222
+ const changed = params.force ? all : all.filter(s => state[stateKey(source, s.name)] !== s.tree);
223
+ result.current += all.length - changed.length;
224
+ const files = readCommittedSkillFiles(source.repo, changed);
225
+ for (const skill of changed) {
226
+ const set = files.get(skill.name);
227
+ if (!set) {
228
+ state[stateKey(source, skill.name)] = skill.tree;
229
+ continue;
230
+ }
231
+ const contentHash = (0, skill_mirror_files_1.computeSkillFileHash)(set);
232
+ if (params.mirrorHashes.get(`${source.scope}:${skill.name}`) === contentHash) {
233
+ state[stateKey(source, skill.name)] = skill.tree;
234
+ result.current++;
235
+ continue;
236
+ }
237
+ const commit = lastCommit(source.repo, skill.folder);
238
+ try {
239
+ const res = await fetch(`${params.apiUrl}/v1/skillmirror/${encodeURIComponent(skill.name)}`, {
240
+ method: 'POST',
241
+ headers: { 'Authorization': `Bearer ${params.apiKey}`, 'Content-Type': 'application/json' },
242
+ body: JSON.stringify({
243
+ files: set,
244
+ contentHash,
245
+ scope: source.scope,
246
+ ...(source.scope === 'project'
247
+ ? { projectId: source.projectId, projectName: source.projectName, replaces: [`global:${skill.name}`] }
248
+ : {}),
249
+ reason: `git ${commit.hash}: ${commit.subject}`.slice(0, 500),
250
+ author: {
251
+ via: 'git-ingest',
252
+ commit: commit.hash,
253
+ ...(params.harness ? { harness: params.harness } : {}),
254
+ ...(params.sessionId ? { sessionId: params.sessionId } : {}),
255
+ },
256
+ }),
257
+ signal: AbortSignal.timeout(15_000),
258
+ });
259
+ if (!res.ok) {
260
+ const body = await res.json().catch(() => ({}));
261
+ throw new Error(`HTTP ${res.status} ${body.detail || body.error || ''}`.trim());
262
+ }
263
+ state[stateKey(source, skill.name)] = skill.tree;
264
+ result.uploaded.push(skill.name);
265
+ }
266
+ catch (err) {
267
+ result.failed.push({ name: skill.name, error: err instanceof Error ? err.message : String(err) });
268
+ }
269
+ }
270
+ }
271
+ writeState(state);
272
+ return result;
273
+ }
274
+ /** The mirror's content hashes, keyed `<scope>:<name>`, from the list route. */
275
+ async function fetchMirrorHashes(apiUrl, apiKey, projectId) {
276
+ // Two plain lists, not `effective`: effective refuses a same-name project
277
+ // skill that predates `replaces`, and ingestion is what fixes that.
278
+ const queries = ['?scope=global', ...(projectId ? [`?scope=project&projectId=${encodeURIComponent(projectId)}`] : [])];
279
+ const out = new Map();
280
+ for (const q of queries) {
281
+ const res = await fetch(`${apiUrl}/v1/skillmirror${q}`, {
282
+ headers: { 'Authorization': `Bearer ${apiKey}` },
283
+ signal: AbortSignal.timeout(10_000),
284
+ });
285
+ if (!res.ok)
286
+ throw new Error(`skill mirror list failed: HTTP ${res.status}`);
287
+ const body = await res.json();
288
+ for (const s of body.skills || [])
289
+ out.set(`${s.scope}:${s.skillName}`, s.contentHash || '');
290
+ }
291
+ return out;
292
+ }
293
+ /** Both sources for a working directory: dotclaude's global skills and, in a
294
+ * project repo, its `.claude/skills`. */
295
+ function gitSkillSources(cwd) {
296
+ return [globalSkillSource(), projectSkillSource(cwd)].filter((s) => !!s);
297
+ }
@@ -1,35 +1,11 @@
1
1
  "use strict";
2
- /** Skill-gain landing — the Skill Learning Loop's write-back half
3
- * (docs/skill-learning-loop.md §fuse shape, operator-ratified 2026-07-01).
2
+ /** Skill-gain artifacts on disk: where a skill's folder is, and the undo for
3
+ * gains the Skill Learning Loop landed before 2026-10-13.
4
4
  *
5
- * THE SHAPE (Tony Stark's briefcase suit): the server-side distiller packaged
6
- * each gain as a focused doc + a blind one-liner. This leg — pure code, never
7
- * agent voluntarism (the skill-tuning autopsy's law) — lands both files:
8
- *
9
- * docs/learned/<slug>.md the laser-focused learning doc
10
- * SKILL.md (EOF) one FUSE line: the one-liner + the schematic
11
- * telling the NEXT invocation exactly what to do —
12
- * expand the doc, fold it in where it belongs,
13
- * delete the fuse and the doc.
14
- *
15
- * The fuse is self-applying because it loads WITH the skill: the next agent
16
- * to invoke the skill reads it as part of the skill's own instructions, does
17
- * the short tune with the whole skill in context (the only actor that ever
18
- * legitimately has it), and the skill converges clean. If a run skips the
19
- * tune, the fuse persists as ordinary progressive disclosure — the learning
20
- * stays live either way (graceful degradation, no failure mode).
21
- *
22
- * Guards: CAP 3 undigested fuses per skill (overflow waits server-side —
23
- * a skill drowning in briefcases isn't being used anyway); every artifact
24
- * stamped `skillgain:<id>`; `greprag skillgain reject <id>` is the undo
25
- * (removes fuse + doc). Landing routes through resolveSkillDir — the ONE
26
- * seam that swaps to the greprag-load skill store when skills move into the
27
- * CLI (the no-SKILL.md endgame).
28
- *
29
- * Write-target routing (the trap): greprag's bundled skills publish from the
30
- * repo source (packages/cli/skill/<name>/) — ~/.claude/skills/<name>/ is an
31
- * init-overwritten artifact. Bundled + repo present → repo source; otherwise
32
- * the global skills dir; neither → the gain stays pending. */
5
+ * GrepRAG no longer lands gains into skill folders or into `greprag load`
6
+ * payloads: git owns skills and GrepRAG never edits their content
7
+ * (adr/skill-ownership-git.md). `greprag skillgain reject <id>` still removes
8
+ * a fuse line and learned doc that an earlier version wrote. */
33
9
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
34
10
  if (k2 === undefined) k2 = k;
35
11
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -64,27 +40,18 @@ var __importStar = (this && this.__importStar) || (function () {
64
40
  };
65
41
  })();
66
42
  Object.defineProperty(exports, "__esModule", { value: true });
67
- exports.isGeneratedAdapter = exports.MAX_FUSES_PER_SKILL = exports.LEARNINGS_RELPATH = exports.LEARNED_DIR_RELPATH = exports.BUNDLED_SKILLS = void 0;
43
+ exports.LEARNINGS_RELPATH = exports.LEARNED_DIR_RELPATH = exports.BUNDLED_SKILLS = void 0;
68
44
  exports.resolveSkillDir = resolveSkillDir;
69
45
  exports.hasGainStamp = hasGainStamp;
70
- exports.countFuses = countFuses;
71
- exports.buildFuseLine = buildFuseLine;
72
- exports.buildLoadFuseBlock = buildLoadFuseBlock;
73
- exports.landBriefcase = landBriefcase;
74
46
  exports.removeLandedGain = removeLandedGain;
75
- exports.landPendingGains = landPendingGains;
76
47
  const path = __importStar(require("path"));
77
48
  const fs = __importStar(require("fs"));
78
- const skill_mirror_files_1 = require("./skill-mirror-files");
79
49
  /** Skills that ship FROM the greprag repo — their canonical source is
80
50
  * packages/cli/skill/<name>/; the installed copy is overwritten at init. */
81
51
  exports.BUNDLED_SKILLS = ['greprag', 'commander', 'content-advisor', 'mechanic'];
82
52
  exports.LEARNED_DIR_RELPATH = path.join('docs', 'learned');
83
53
  /** v1 legacy zone — reject still scans it for stamps landed before the recut. */
84
54
  exports.LEARNINGS_RELPATH = path.join('docs', 'learnings.md');
85
- /** Max undigested fuse lines a SKILL.md may carry; past this, new gains wait
86
- * server-side. Operator-ratified (2026-07-01). */
87
- exports.MAX_FUSES_PER_SKILL = 3;
88
55
  /** Resolve the canonical directory for a skill. `existsFn` injectable for
89
56
  * tests. Order: bundled repo source (when cwd IS the greprag repo) → global
90
57
  * skills dir → null (defer). THE storage seam — the greprag-load skill-store
@@ -105,115 +72,6 @@ function resolveSkillDir(skill, cwd, homeDir, existsFn = fs.existsSync) {
105
72
  function hasGainStamp(content, nodeId) {
106
73
  return content.includes(`skillgain:${nodeId}`);
107
74
  }
108
- /** Count undigested fuse lines in a SKILL.md body (digested ones were deleted
109
- * by the tune, so the count self-heals as briefcases get absorbed). PURE. */
110
- function countFuses(skillMdContent) {
111
- return (skillMdContent.match(/<!-- skillgain:[0-9a-f]{8}/g) || []).length;
112
- }
113
- /** The fuse line — the briefcase's button. One line, blind (the one-liner
114
- * names the learning; the mass lives in the doc), carrying the exact
115
- * schematic for the next invocation. PURE — unit-tested. */
116
- function buildFuseLine(gain, docRelPath, projectName, dateISO) {
117
- // The anchor is the attribution, shown. A gain claims THIS skill by quoting
118
- // this skill's own purpose back; printing that quote turns "is this even
119
- // mine?" into a one-glance check instead of a trip through the learned doc.
120
- // The 2026-09-03 field audit found 7 of 8 fuses on the wrong skill, and
121
- // nothing on the line said why any of them belonged there.
122
- // adr: adr/skill-gain-attribution.md
123
- const claim = gain.anchor
124
- ? `Claims this skill via its purpose: "${gain.anchor}". `
125
- : '';
126
- return (`⚙ SKILL UPDATE PENDING (${gain.gainType}) — ${gain.oneLiner} `
127
- + claim
128
- + `→ BEFORE working: read ${docRelPath.replace(/\\/g, '/')}, fold the learning into this `
129
- + `skill where it belongs (one small edit, keep it lean), then DELETE this line and the doc. `
130
- + `Doesn't belong to this skill, or bogus? \`greprag skillgain reject ${gain.nodeId}\` instead. `
131
- + `<!-- skillgain:${gain.nodeId} ${dateISO.slice(0, 10)} project:${projectName} -->`);
132
- }
133
- /** Render pending gains for a mirror-managed skill into its `greprag load`
134
- * payload — the fuse's replacement for skills whose SKILL.md is a generated
135
- * adapter nothing local may write to.
136
- *
137
- * Carries strictly more than the fuse line did: the learning doc is inlined
138
- * rather than pointed at, because a managed skill has no local docs/learned/
139
- * to read. Empty string when there is nothing pending, so the caller can
140
- * append unconditionally. PURE — unit-tested. adr: adr/skill-fuse-delivery.md */
141
- function buildLoadFuseBlock(skill, gains) {
142
- const usable = gains.filter(g => g.oneLiner && g.skill === skill);
143
- if (usable.length === 0)
144
- return '';
145
- const head = usable.length === 1
146
- ? `⚙ 1 PENDING SKILL UPDATE for ${skill} — fold it in before acting on this skill.`
147
- : `⚙ ${usable.length} PENDING SKILL UPDATES for ${skill} — fold them in before acting on this skill.`;
148
- const items = usable.map((g, i) => {
149
- const claim = g.anchor
150
- ? `\n Claims this skill via its purpose: "${g.anchor}"`
151
- : '';
152
- const doc = g.docBody ? `\n\n${g.docBody.trim()}\n` : '';
153
- return (`${i + 1}. (${g.gainType}) ${g.oneLiner}${claim}${doc}\n`
154
- + ` Digest: make the edit, then \`greprag skillgain done ${g.nodeId}\`.\n`
155
- + ` Doesn't belong to this skill, or bogus? \`greprag skillgain reject ${g.nodeId}\`.`);
156
- });
157
- return `\n---\n\n${head}\n\n${items.join('\n\n')}\n`;
158
- }
159
- /** A generated mirror adapter is machine-owned: regenerated from the mirror on
160
- * any sync, hash-locked against edits in between. One recogniser for the whole
161
- * CLI lives in `skill-mirror-files`. */
162
- var skill_mirror_files_2 = require("./skill-mirror-files");
163
- Object.defineProperty(exports, "isGeneratedAdapter", { enumerable: true, get: function () { return skill_mirror_files_2.isGeneratedSkillAdapter; } });
164
- /** Land ONE briefcase: write the learned doc + append the fuse to SKILL.md.
165
- * Deterministic file work only — never throws (failure defers the gain). */
166
- function landBriefcase(gain, cwd, homeDir, projectName) {
167
- try {
168
- if (!gain.oneLiner || !gain.docSlug || !gain.docBody)
169
- return 'deferred'; // payload-less legacy row
170
- const dir = resolveSkillDir(gain.skill, cwd, homeDir);
171
- if (!dir)
172
- return 'deferred';
173
- // A linked skill folder belongs to its target (a tracked repo file, another
174
- // installer's store), so the fuse is not GrepRAG's to write there. The gain
175
- // stays pending; a mirrored skill still gets it from `greprag load`.
176
- // adr: adr/skill-fuse-delivery.md
177
- if ((0, skill_mirror_files_1.isSkillFolderOwnedElsewhere)(dir))
178
- return 'deferred';
179
- const skillMd = path.join(dir, 'SKILL.md');
180
- let body;
181
- try {
182
- body = fs.readFileSync(skillMd, 'utf-8');
183
- }
184
- catch {
185
- return 'deferred';
186
- }
187
- if (hasGainStamp(body, gain.nodeId))
188
- return 'already-landed';
189
- // A mirror-managed skill's SKILL.md is a GENERATED adapter: the mirror
190
- // regenerates it on every sync and hash-locks it in between. Appending a
191
- // fuse there makes two writers own one file — the fuse is wiped by the next
192
- // sync, or (worse) the divergence guard refuses the sync and the operator
193
- // has to untangle it by hand. Happened twice on 2026-09-04.
194
- //
195
- // For these skills the fuse is served by `greprag load` instead, which is
196
- // the payload the adapter tells the agent to read — same arrival moment,
197
- // one writer. The gain stays PENDING so the load path keeps surfacing it
198
- // until it is digested. adr: adr/skill-fuse-delivery.md
199
- if ((0, skill_mirror_files_1.isGeneratedSkillAdapter)(body))
200
- return 'served-by-load';
201
- if (countFuses(body) >= exports.MAX_FUSES_PER_SKILL)
202
- return 'capped';
203
- // Doc first (a fuse pointing at a missing doc is the one broken state).
204
- const docRel = path.join(exports.LEARNED_DIR_RELPATH, `${gain.docSlug}.md`);
205
- const docAbs = path.join(dir, docRel);
206
- fs.mkdirSync(path.dirname(docAbs), { recursive: true });
207
- fs.writeFileSync(docAbs, gain.docBody.trimEnd()
208
- + `\n\n<!-- skillgain:${gain.nodeId} — auto-landed by the Skill Learning Loop; deleted when digested -->\n`);
209
- const fuse = buildFuseLine(gain, docRel, projectName, new Date().toISOString());
210
- fs.writeFileSync(skillMd, body + (body.endsWith('\n') ? '' : '\n') + fuse + '\n');
211
- return 'landed';
212
- }
213
- catch {
214
- return 'deferred';
215
- }
216
- }
217
75
  /** Remove a gain's artifacts wherever they landed — the `skillgain reject`
218
76
  * undo: the fuse line (SKILL.md), the learned doc, and any v1-legacy
219
77
  * learnings.md line. Returns true if anything was removed. Never throws. */
@@ -254,27 +112,3 @@ function removeLandedGain(skill, nodeId, cwd, homeDir) {
254
112
  return false;
255
113
  }
256
114
  }
257
- /** Bound the file work per session start; overflow waits (still pending). */
258
- const MAX_LANDS_PER_START = 5;
259
- /** The full landing pass. Every gain with a payload lands as a briefcase
260
- * (capped per skill + per start); payload-less/unroutable gains stay pending.
261
- * Returns the report (announce) + the status updates to POST back. */
262
- function landPendingGains(pending, cwd, homeDir, projectName) {
263
- const report = { landed: [] };
264
- const statusUpdates = [];
265
- for (const gain of pending) {
266
- if (report.landed.length >= MAX_LANDS_PER_START)
267
- break;
268
- const outcome = landBriefcase(gain, cwd, homeDir, projectName);
269
- if (outcome === 'landed' || outcome === 'already-landed') {
270
- statusUpdates.push({ nodeId: gain.nodeId, status: 'landed' });
271
- if (outcome === 'landed') {
272
- report.landed.push({
273
- nodeId: gain.nodeId, skill: gain.skill, oneLiner: gain.oneLiner || gain.text,
274
- });
275
- }
276
- }
277
- // 'capped' / 'deferred' → stays pending, retried next session start
278
- }
279
- return { report, statusUpdates };
280
- }