greprag 5.84.0 → 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.
@@ -1,18 +1,20 @@
1
1
  "use strict";
2
- // `greprag skill` — opt-in management for bundled advisor skills.
2
+ // `greprag skill` — bundled advisor skills, and read access to the skill mirror.
3
3
  //
4
4
  // The package bundles `greprag/` (the front-door, auto-installed by init)
5
- // alongside opt-in advisor skills (`discord/`, `content-advisor/`,
6
- // `mechanic/`, future ones). Operators choose which advisors to add
7
- // to their Claude Code skill registry via this verb instead of having
8
- // them force-installed on every `npm i -g greprag@<new>`.
5
+ // alongside opt-in advisor skills (`content-advisor/`, `mechanic/`, future
6
+ // ones). Operators choose which advisors to add to their Claude Code skill
7
+ // registry via this verb instead of having them force-installed on every
8
+ // `npm i -g greprag@<new>`.
9
9
  //
10
- // Verbs:
11
- // greprag skill list — show bundled skills + install status
12
- // greprag skill install <name> — copy bundle/<name>/ → ~/.claude/skills/<name>/
13
- // greprag skill uninstall <name> — remove ~/.claude/skills/<name>/ (greprag/ refused)
10
+ // Every other skill lives in git: dotclaude `skills/<name>/` (global) or
11
+ // `<repo>/.claude/skills/<name>/` (project). GrepRAG mirrors the committed
12
+ // files read-only; `skill mirror sync` ingests them. Commands that used to
13
+ // write the mirror (edit, apply, snapshot create/promote, revert) refuse
14
+ // and name the git folder to edit.
14
15
  //
15
16
  // adr: adr/skill-bundle-shape.md
17
+ // adr: adr/skill-ownership-git.md
16
18
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
19
  if (k2 === undefined) k2 = k;
18
20
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -47,118 +49,85 @@ var __importStar = (this && this.__importStar) || (function () {
47
49
  };
48
50
  })();
49
51
  Object.defineProperty(exports, "__esModule", { value: true });
50
- exports.readOnlyNames = readOnlyNames;
51
52
  exports.runSkill = runSkill;
52
53
  const fs = __importStar(require("fs"));
53
54
  const os = __importStar(require("os"));
54
55
  const path = __importStar(require("path"));
55
- const proc_1 = require("../proc");
56
- const claude_lifecycle_cutover_1 = require("../claude-lifecycle-cutover");
57
56
  const native_skill_mirror_1 = require("../native-skill-mirror");
58
57
  const project_anchor_1 = require("../project-anchor");
59
58
  const skill_mirror_client_1 = require("../skill-mirror-client");
60
- const skill_landing_1 = require("../skill-landing");
59
+ const skill_git_ingest_1 = require("../skill-git-ingest");
61
60
  const skill_staleness_1 = require("../skill-staleness");
62
- const load_1 = require("./load");
61
+ const skill_landing_1 = require("../skill-landing");
63
62
  const CORE_SKILL = 'greprag';
64
- const SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
65
- /** `--only a,b` / `--only=a,b` / repeated `--only a --only b` → skill names. */
66
- function readOnlyNames(args) {
67
- const raw = [];
68
- for (let i = 0; i < args.length; i++) {
69
- if (args[i] === '--only' && args[i + 1])
70
- raw.push(args[++i]);
71
- else if (args[i].startsWith('--only='))
72
- raw.push(args[i].slice('--only='.length));
73
- }
74
- const names = [...new Set(raw.flatMap(value => value.split(',')).map(v => v.trim()).filter(Boolean))];
75
- const invalid = names.filter(name => !SKILL_NAME_RE.test(name));
76
- if (invalid.length > 0)
77
- throw new Error(`--only got invalid skill name(s): ${invalid.join(', ')}`);
78
- return names;
79
- }
80
- const SKILL_HELP = `greprag skill — manage bundled advisor skills
63
+ const SKILL_HELP = `greprag skill — bundled advisor skills and the read-only skill mirror
81
64
 
82
65
  USAGE
83
66
  greprag skill list Show bundled skills and install status
84
67
  greprag skill install <name> Install a bundled advisor skill
85
68
  greprag skill uninstall <name> Remove a previously-installed advisor
86
- greprag skill edit <name> --reason "<why>" [--skill-id <id>] [--scope project --project-id <id>]
87
- Export/edit/apply helper; opens $EDITOR unless --no-open or Codex
88
69
  greprag skill history <name> [--skill-id <id>] [--limit <n>]
89
- Every canon revision: date, author, reason, changes
70
+ Every mirrored revision: date, source commit, changes
90
71
  greprag skill diff <name> <revA> [<revB>]
91
72
  Unified diff between two revisions (revB defaults to current)
92
- greprag skill revert <name> --to <rev> --reason "<why>" --expected-hash <current-hash>
93
- Make an old revision current again as a new revision
94
- greprag skill mirror sync [harness] Install native GrepRAG skill adapters
95
- claude-code --cutover-lifecycle Cut over commit/deploy/release only
96
- claude-code --only <a,b> Install adapters for named mirrored skills
97
- (closes the gaps greprag load marks with *)
73
+ greprag skill mirror sync [codex|opencode|all] [--force]
74
+ Ingest committed skills from git into the mirror,
75
+ then refresh Codex/OpenCode launchers
98
76
  greprag skill mirror export <name> --to <dir>
99
- Export canonical skill bytes for editing
100
- greprag skill mirror apply <dir> --skill <name> --expected-hash <hash> --reason "<why>" [--allow-remove]
101
- Promote edited bytes with stale-write guard;
102
- the folder must come from export (it records
103
- the revision it holds), and a missing file
104
- is refused unless --allow-remove
105
- greprag skill mirror snapshot create <name> --name <snapshot>
106
- greprag skill mirror snapshot list <name>
107
- greprag skill mirror snapshot show <name> --name <snapshot>
77
+ Copy the mirrored files into a folder (read only)
78
+ greprag skill mirror snapshot list|show <name> [--name <snapshot>]
108
79
  greprag skill mirror snapshot restore <name> --name <snapshot> --to <dir>
109
- greprag skill mirror snapshot promote <name> --name <snapshot> --expected-hash <hash> --reason "<why>"
110
- Promote snapshot bytes as the newest revision
111
- Named package rollback points
80
+ Archived named snapshots (read only)
112
81
  greprag skill source stamp <name> --project <p> --path <rel> [--path <rel>...]
113
- [--root <dir>] [--baseline <ref>]
114
- Stamp a skill's local source baseline
82
+ Stamp a source binding into the skill's git SKILL.md
115
83
 
116
- NOTES
117
- • The 'greprag' skill is auto-installed and refreshed on every upgrade.
118
- It cannot be uninstalled via this verb (delete the dir manually if
119
- you really want it gone — postinstall will reinstall on next upgrade).
120
- • Other bundled skills are opt-in and refresh-only on upgrade: once
121
- installed, they stay current with the bundle; if never installed,
122
- upgrades leave them out of your skill registry.
123
- • Mirrored skill edits use export/apply semantics. \`skill edit\` only wraps
124
- that flow: export current bytes, open an editor, show a diff summary, then
125
- apply with the exported content hash.
126
- • Every canon write needs --reason "<one line>". GrepRAG keeps each
127
- revision's bytes with its reason, author and date; read them with
128
- \`skill history\`, compare with \`skill diff\`, undo with \`skill revert\`.
129
- • Codex agents must use a non-interactive path: \`skill mirror export\`, patch
130
- files, then \`skill mirror apply\`. In Codex, \`skill edit\` skips the editor
131
- and prints the guarded apply command; \`--no-open\` does the same explicitly.
132
- • Source stamps are local Git facts. They update the local canonical skill
133
- file, then mirror to GrepRAG when GREPRAG_API_KEY is configured.`;
134
- function formatAdoptionLine(prefix, entry) {
135
- return entry.reason
136
- ? ` ${prefix} ${entry.scope}:${entry.skillName} — ${entry.reason}`
137
- : ` ${prefix} ${entry.scope}:${entry.skillName}`;
138
- }
139
- function printAdoptionDryRun(platform, result) {
140
- const plan = result.plan;
141
- const candidateCount = plan.candidates.length;
142
- const managedCount = plan.managed.length;
143
- const blockedCount = plan.blocked.length;
144
- const skippedCount = plan.skipped.length;
145
- const ignoredCount = plan.ignoredRoots.reduce((sum, root) => sum + root.skillCount, 0);
146
- console.log(`${platform}: adoption dry-run: ${candidateCount} candidate(s), ${managedCount} managed, ${blockedCount} blocked, ${skippedCount} skipped, ${ignoredCount} ignored in unowned roots`);
147
- for (const candidate of plan.candidates)
148
- console.log(formatAdoptionLine('adopt', candidate));
149
- for (const blocked of plan.blocked)
150
- console.log(formatAdoptionLine('block', blocked));
151
- for (const skipped of plan.skipped)
152
- console.log(formatAdoptionLine('skip ', skipped));
153
- for (const root of plan.ignoredRoots) {
154
- console.log(` ignore ${root.skillCount} skill(s) under ${root.root} — ${root.reason}`);
155
- }
156
- }
157
- function printAdoptionSync(platform, result) {
158
- const candidateCount = result.plan.candidates.length;
159
- if (!candidateCount && !result.blocked && !result.skipped)
160
- return;
161
- console.log(`${platform}: native skill adoption: ${result.adopted}/${candidateCount} adopted, ${result.blocked} blocked, ${result.skipped} skipped`);
84
+ WHERE SKILLS LIVE
85
+ Git owns every skill. Edit the folder, commit, and the mirror follows at the
86
+ next session start or \`greprag skill mirror sync\`:
87
+ global ~/.claude/skills/<name>/ (the dotclaude repo)
88
+ project <repo>/.claude/skills/<name>/
89
+ bundled packages/cli/skill/<name>/ (the greprag repo)
90
+ Claude Code reads those folders directly. Codex and OpenCode get launchers
91
+ that run \`greprag load <name>\`, which serves the committed content.
92
+ Uncommitted edits are never mirrored.
93
+
94
+ REMOVED
95
+ skill edit, skill mirror apply, skill mirror snapshot promote|create,
96
+ and skill revert refuse: they wrote the mirror, which is
97
+ now read only. Use git (git log, git diff, git revert) on the skill folder.`;
98
+ /** The current repo's own copy of a skill, if it has one. */
99
+ function projectSkillDir(skill) {
100
+ for (const source of (0, skill_git_ingest_1.gitSkillSources)(process.cwd())) {
101
+ if (source.scope !== 'project')
102
+ continue;
103
+ const dir = path.join(source.repo, ...source.prefix.split('/'), skill);
104
+ if (fs.existsSync(path.join(dir, 'SKILL.md')))
105
+ return { dir, projectName: source.projectName };
106
+ }
107
+ return null;
108
+ }
109
+ /** The folder holding a skill's one true copy: this repo's, else the global one. */
110
+ function gitSkillDir(skill) {
111
+ const project = projectSkillDir(skill);
112
+ if (project)
113
+ return project.dir;
114
+ const global = path.join(os.homedir(), '.claude', 'skills', skill);
115
+ return fs.existsSync(path.join(global, 'SKILL.md')) ? global : null;
116
+ }
117
+ /** Where a skill's one true copy lives, for refusal text. */
118
+ function gitSkillLocation(skill) {
119
+ if (skill_landing_1.BUNDLED_SKILLS.includes(skill))
120
+ return `packages/cli/skill/${skill}/ in the greprag repo`;
121
+ const project = projectSkillDir(skill);
122
+ if (project)
123
+ return `${project.dir} (the ${project.projectName || 'project'} repo)`;
124
+ return `${path.join(os.homedir(), '.claude', 'skills', skill)} (the dotclaude repo)`;
125
+ }
126
+ function refuseMirrorWrite(command, skill) {
127
+ const where = skill && !skill.startsWith('-') ? gitSkillLocation(skill) : 'the skill\'s git folder';
128
+ throw new Error(`Refusing \`greprag skill ${command}\`: the GrepRAG skill mirror is read only; git owns the skill. `
129
+ + `Edit ${where}, commit, then run \`greprag skill mirror sync\` (session start runs it too). `
130
+ + 'History and rollback are git log / git revert on that folder.');
162
131
  }
163
132
  function bundleRoot() {
164
133
  // Same resolution as init.ts Step 6 + postinstall.js
@@ -195,16 +164,6 @@ function getArg(args, name) {
195
164
  const idx = args.indexOf(name);
196
165
  return idx >= 0 ? args[idx + 1] : undefined;
197
166
  }
198
- function getMany(args, name) {
199
- const out = [];
200
- for (let i = 0; i < args.length; i++) {
201
- if (args[i] === name && args[i + 1]) {
202
- out.push(args[i + 1]);
203
- i++;
204
- }
205
- }
206
- return out;
207
- }
208
167
  function mirrorApiConfig() {
209
168
  const apiKey = process.env.GREPRAG_API_KEY || '';
210
169
  if (!apiKey)
@@ -246,18 +205,6 @@ function mirrorLookupQuery(args) {
246
205
  const qs = params.toString();
247
206
  return qs ? `?${qs}` : '';
248
207
  }
249
- function mirrorBodyScope(args) {
250
- const scope = getArg(args, '--scope');
251
- const projectId = getArg(args, '--project-id') || getArg(args, '--projectId');
252
- const projectName = getArg(args, '--project-name') || getArg(args, '--projectName');
253
- const replaces = getMany(args, '--replaces');
254
- return {
255
- ...(scope ? { scope } : {}),
256
- ...(projectId ? { projectId } : {}),
257
- ...(projectName ? { projectName } : {}),
258
- ...(replaces.length ? { replaces } : {}),
259
- };
260
- }
261
208
  function validMirrorRelPath(relative) {
262
209
  return /^(?!\.{1,2}(\/|$))[A-Za-z0-9._\/-]{1,200}\.md$/.test(relative) && !relative.includes('..');
263
210
  }
@@ -300,12 +247,6 @@ function listMarkdownFiles(root) {
300
247
  walk(root, '');
301
248
  return out;
302
249
  }
303
- function collectSkillPackageOrThrow(root) {
304
- const files = (0, skill_mirror_client_1.collectSkillFiles)(path.resolve(root));
305
- if (!files)
306
- throw new Error(`No valid SKILL.md package found at ${path.resolve(root)}.`);
307
- return files;
308
- }
309
250
  function fileMap(files) {
310
251
  return new Map(files.map(file => [file.path, file.content]));
311
252
  }
@@ -323,38 +264,6 @@ function diffSkillPackages(before, after) {
323
264
  }
324
265
  return diff;
325
266
  }
326
- function printSkillPackageDiff(label, diff) {
327
- if (!diff.length) {
328
- console.log(`${label}: no markdown changes`);
329
- return;
330
- }
331
- console.log(`${label}:`);
332
- for (const item of diff)
333
- console.log(` ${item.status.padEnd(7)} ${item.path}`);
334
- }
335
- function shellArg(value) {
336
- return /^[A-Za-z0-9._:\/\\-]+$/.test(value)
337
- ? value
338
- : `"${value.replace(/(["\\$`])/g, '\\$1')}"`;
339
- }
340
- function forwardedMirrorArgs(args) {
341
- const forwarded = new Set([
342
- '--skill-id', '--skillId',
343
- '--scope',
344
- '--project-id', '--projectId',
345
- '--project-name', '--projectName',
346
- '--replaces',
347
- ]);
348
- const parts = [];
349
- for (let i = 1; i < args.length; i++) {
350
- const arg = args[i];
351
- if (!forwarded.has(arg) || !args[i + 1])
352
- continue;
353
- parts.push(arg, shellArg(args[i + 1]));
354
- i++;
355
- }
356
- return parts.join(' ');
357
- }
358
267
  /** Returns whether it wrote: under `check` it only lists the plan, so a caller reports success only on true. */
359
268
  function writeSkillPackage(targetDir, files, opts) {
360
269
  const target = path.resolve(targetDir);
@@ -384,38 +293,9 @@ function writeSkillPackage(targetDir, files, opts) {
384
293
  }
385
294
  return true;
386
295
  }
387
- /** The record an export leaves beside the files it wrote: which revision the
388
- * folder holds and the bytes of every file in it. Apply takes its base from
389
- * here, never from a hash the caller fetched separately, so an old folder
390
- * cannot be paired with a newer revision's hash and written over it.
391
- * adr: adr/skill-bundle-shape.md */
392
- const MIRROR_BASE_FILE = '.greprag-mirror-base.json';
393
- function fileDigest(content) {
394
- return (0, skill_mirror_client_1.computeMirrorHash)([{ path: '', content }]);
395
- }
396
- function writeMirrorBase(targetDir, base) {
397
- const record = {
398
- skill: base.skill,
399
- revision: base.revision,
400
- contentHash: base.contentHash,
401
- files: Object.fromEntries(base.files.map(file => [file.path, fileDigest(file.content)])),
402
- };
403
- fs.writeFileSync(path.join(path.resolve(targetDir), MIRROR_BASE_FILE), JSON.stringify(record, null, 2) + '\n', 'utf8');
404
- }
405
- function readMirrorBase(dir) {
406
- try {
407
- const raw = JSON.parse(fs.readFileSync(path.join(path.resolve(dir), MIRROR_BASE_FILE), 'utf8'));
408
- if (typeof raw.skill !== 'string' || typeof raw.contentHash !== 'string' || !raw.files || typeof raw.files !== 'object')
409
- return null;
410
- return { skill: raw.skill, revision: Number(raw.revision || 0), contentHash: raw.contentHash, files: raw.files };
411
- }
412
- catch {
413
- return null;
414
- }
415
- }
416
296
  /** Read the folder back and fail unless it holds exactly the files the mirror
417
297
  * sent, byte for byte. Catches a partial write and any file the package
418
- * reader would skip (size or count limits) before the user edits on top. */
298
+ * reader would skip (size or count limits). */
419
299
  function verifyWrittenPackage(targetDir, files) {
420
300
  const written = (0, skill_mirror_client_1.collectSkillFiles)(path.resolve(targetDir)) || [];
421
301
  const diff = diffSkillPackages(files, written);
@@ -424,205 +304,20 @@ function verifyWrittenPackage(targetDir, files) {
424
304
  const detail = diff.map(item => ` ${item.status === 'removed' ? 'missing' : item.status} ${item.path}`).join('\n');
425
305
  throw new Error(`export did not write the revision it fetched to ${path.resolve(targetDir)}:\n${detail}`);
426
306
  }
427
- function printSkillMirrorConflict(params) {
428
- const local = new Map(params.localFiles.map(file => [file.path, file.content]));
429
- const remote = new Map(params.remoteFiles.map(file => [file.path, file.content]));
430
- const paths = [...new Set([...local.keys(), ...remote.keys()])].sort();
431
- console.error(`Conflict: ${params.skill} mirror changed since export.`);
432
- console.error(` expected: ${params.expectedHash}`);
433
- console.error(` current: ${params.currentHash}`);
434
- for (const relative of paths) {
435
- const status = !local.has(relative) ? 'remote-only'
436
- : !remote.has(relative) ? 'local-only'
437
- : local.get(relative) === remote.get(relative) ? 'same'
438
- : 'changed';
439
- if (status !== 'same')
440
- console.error(` ${status.padEnd(11)} ${relative}`);
441
- }
442
- console.error(`Export current mirror: greprag skill mirror export ${params.skill} --to <dir> --force`);
443
- }
444
307
  async function runSkillMirrorExport(args) {
445
308
  const name = args[0];
446
309
  const to = getArg(args, '--to');
447
310
  if (!name || !to)
448
311
  throw new Error('Usage: greprag skill mirror export <name> --to <dir> [--force] [--check]');
449
- const loaded = await loadMirrorPackage(name, args);
450
- if (!writeSkillPackage(to, loaded.files, { force: args.includes('--force'), check: args.includes('--check'), exact: true }))
451
- return;
452
- verifyWrittenPackage(to, loaded.files);
453
- writeMirrorBase(to, { skill: name, ...loaded });
454
- console.log(`Exported ${name} revision ${loaded.revision} (${loaded.contentHash})`);
455
- }
456
- async function loadMirrorPackage(skill, args) {
457
- const data = await mirrorApiJson('GET', `/v1/skillmirror/${encodeURIComponent(skill)}${mirrorLookupQuery(args)}`);
312
+ const data = await mirrorApiJson('GET', `/v1/skillmirror/${encodeURIComponent(name)}${mirrorLookupQuery(args)}`);
458
313
  const files = (data.files || [])
459
314
  .filter((file) => typeof file.path === 'string' && typeof file.content === 'string');
460
315
  if (!files.length)
461
- throw new Error(`No mirrored files returned for ${skill}.`);
462
- return {
463
- files,
464
- revision: Number(data.revision || 0),
465
- contentHash: typeof data.contentHash === 'string' ? data.contentHash : (0, skill_mirror_client_1.computeMirrorHash)(files),
466
- };
467
- }
468
- function editorCommand() {
469
- return process.env.GREPRAG_EDITOR
470
- || process.env.VISUAL
471
- || process.env.EDITOR
472
- || (process.platform === 'win32' ? 'notepad' : 'vi');
473
- }
474
- function codexHarnessDetected() {
475
- return Boolean(process.env.CODEX_THREAD_ID || process.env.CODEX_CI || process.env.CODEX_SHELL);
476
- }
477
- function runEditor(target) {
478
- const editor = editorCommand();
479
- const result = (0, proc_1.safeSpawnSync)(editor, [target], { stdio: 'inherit', shell: true });
480
- if (result.error)
481
- throw new Error(`editor failed: ${result.error.message}`);
482
- if (typeof result.status === 'number' && result.status !== 0) {
483
- throw new Error(`editor exited with status ${result.status}`);
484
- }
485
- }
486
- /** A skill the CLI ships as its own bundle has one canon, the bundle, and
487
- * `greprag load` serves it for that skill's global id. A mirror write under
488
- * its name would land in a row nothing reads. */
489
- function refuseBundledSkillWrite(skill) {
490
- if (!(0, load_1.bundleOwnsSkill)(skill))
491
- return;
492
- throw new Error(`${skill} ships with the greprag CLI, so its canon is the bundle, not the skill mirror. Edit packages/cli/skill/${skill}/ in the greprag repo.`);
493
- }
494
- /** Every canon write carries a one-line reason into the skill's revision
495
- * history; the server refuses a write without one, so refuse it here first,
496
- * naming the flag. adr: adr/skill-revision-history.md */
497
- function requireReason(args, usage) {
498
- const reason = (getArg(args, '--reason') || '').trim();
499
- if (!reason || reason.startsWith('--')) {
500
- throw new Error(`Refusing: --reason "<one line>" is required. GrepRAG records it in the skill's revision history.\nUsage: ${usage}`);
501
- }
502
- return reason;
503
- }
504
- async function applySkillPackage(params) {
505
- refuseBundledSkillWrite(params.skill);
506
- const hash = (0, skill_mirror_client_1.computeMirrorHash)(params.files);
507
- const body = {
508
- files: params.files,
509
- contentHash: hash,
510
- ...mirrorBodyScope(params.args),
511
- reason: params.reason,
512
- author: (0, skill_mirror_client_1.skillRevisionAuthor)(params.via),
513
- expectedHash: params.expectedHash,
514
- ...(typeof params.expectedRevision === 'number' ? { expectedRevision: params.expectedRevision } : {}),
515
- };
516
- return mirrorApiJson('POST', `/v1/skillmirror/${encodeURIComponent(params.skill)}`, body);
517
- }
518
- async function assertCurrentMirrorHash(params) {
519
- try {
520
- const current = await mirrorApiJson('GET', `/v1/skillmirror/${encodeURIComponent(params.skill)}${mirrorLookupQuery(params.args)}`);
521
- const currentHash = typeof current.contentHash === 'string' ? current.contentHash : '';
522
- if (currentHash && currentHash !== params.expectedHash) {
523
- const remoteFiles = (current.files || [])
524
- .filter((file) => typeof file.path === 'string' && typeof file.content === 'string');
525
- printSkillMirrorConflict({
526
- skill: params.skill,
527
- expectedHash: params.expectedHash,
528
- currentHash,
529
- localFiles: params.localFiles,
530
- remoteFiles,
531
- });
532
- throw new Error('Refusing stale apply; re-export or merge against the current mirror.');
533
- }
534
- }
535
- catch (err) {
536
- if (!(err instanceof Error) || !/HTTP 404\b/.test(err.message))
537
- throw err;
538
- }
539
- }
540
- async function runSkillEdit(args) {
541
- const skill = args[0];
542
- const usage = 'greprag skill edit <name> --reason "<why>" [--dir <dir>] [--no-open] [--force] [--keep]';
543
- if (!skill)
544
- throw new Error(`Usage: ${usage}`);
545
- refuseBundledSkillWrite(skill);
546
- const explicitDir = getArg(args, '--dir') || getArg(args, '--to');
547
- const noOpen = args.includes('--no-open');
548
- const skipEditor = noOpen || codexHarnessDetected();
549
- // The editor path applies in this run, so it needs its reason before export;
550
- // --no-open only prints the apply command, which carries the reason instead.
551
- const reason = skipEditor ? (getArg(args, '--reason') || '').trim() : requireReason(args, usage);
552
- const keep = args.includes('--keep') || Boolean(explicitDir);
553
- const workDir = explicitDir
554
- ? path.resolve(explicitDir)
555
- : fs.mkdtempSync(path.join(os.tmpdir(), `greprag-skill-edit-${skill}-`));
556
- const loaded = await loadMirrorPackage(skill, args);
557
- writeSkillPackage(workDir, loaded.files, {
558
- force: args.includes('--force') || !explicitDir,
559
- exact: true,
560
- });
561
- verifyWrittenPackage(workDir, loaded.files);
562
- writeMirrorBase(workDir, { skill, ...loaded });
563
- console.log(`Editing ${skill} revision ${loaded.revision} (${loaded.contentHash})`);
564
- console.log(`Work dir: ${workDir}`);
565
- if (skipEditor) {
566
- console.log(noOpen
567
- ? 'Editor skipped.'
568
- : 'Codex harness detected; editor skipped to avoid launching an interactive editor.');
569
- const forward = forwardedMirrorArgs(args);
570
- console.log(`Apply after editing: greprag skill mirror apply ${shellArg(workDir)} --skill ${skill}${forward ? ` ${forward}` : ''} --expected-hash ${loaded.contentHash} --reason ${reason ? shellArg(reason) : '"<why>"'}`);
571
- return;
572
- }
573
- runEditor(path.join(workDir, 'SKILL.md'));
574
- const edited = collectSkillPackageOrThrow(workDir);
575
- const diff = diffSkillPackages(loaded.files, edited);
576
- printSkillPackageDiff('Diff', diff);
577
- if (!diff.length) {
578
- if (!keep)
579
- fs.rmSync(workDir, { recursive: true, force: true });
316
+ throw new Error(`No mirrored files returned for ${name}.`);
317
+ if (!writeSkillPackage(to, files, { force: args.includes('--force'), check: args.includes('--check'), exact: true }))
580
318
  return;
581
- }
582
- await assertCurrentMirrorHash({ skill, expectedHash: loaded.contentHash, localFiles: edited, args });
583
- const data = await applySkillPackage({ skill, files: edited, expectedHash: loaded.contentHash, reason, via: 'skill-edit', args });
584
- console.log(`Applied ${skill}: revision ${Number(data.revision || 0)} (${String(data.contentHash || (0, skill_mirror_client_1.computeMirrorHash)(edited))})`);
585
- if (!keep)
586
- fs.rmSync(workDir, { recursive: true, force: true });
587
- }
588
- async function runSkillMirrorApply(args) {
589
- const dir = args[0];
590
- const skill = getArg(args, '--skill') || (dir ? path.basename(path.resolve(dir)) : '');
591
- const usage = 'greprag skill mirror apply <dir> --skill <name> --expected-hash <hash> --reason "<why>"';
592
- if (!dir || !skill)
593
- throw new Error(`Usage: ${usage}`);
594
- refuseBundledSkillWrite(skill);
595
- const reason = requireReason(args, usage);
596
- const expectedRevisionRaw = getArg(args, '--expected-revision') || getArg(args, '--expectedRevision');
597
- const passedHash = getArg(args, '--expected-hash') || getArg(args, '--expectedHash');
598
- const base = readMirrorBase(dir);
599
- if (!base) {
600
- throw new Error(`Refusing apply: ${path.resolve(dir)} has no export record (${MIRROR_BASE_FILE}). Run greprag skill mirror export ${skill} --to <dir>, edit that folder, then apply it.`);
601
- }
602
- if (base.skill !== skill) {
603
- throw new Error(`Refusing apply: ${path.resolve(dir)} was exported from ${base.skill}, not ${skill}.`);
604
- }
605
- if (passedHash && passedHash !== base.contentHash) {
606
- throw new Error(`Refusing apply: this folder was exported at ${base.contentHash} (revision ${base.revision}), but --expected-hash is ${passedHash}. A hash from a different export does not describe these files; export again and redo the edits there.`);
607
- }
608
- const expectedHash = base.contentHash;
609
- const files = collectSkillPackageOrThrow(dir);
610
- const present = new Set(files.map(file => file.path));
611
- const missing = Object.keys(base.files).filter(relative => !present.has(relative));
612
- if (missing.length && !args.includes('--allow-remove')) {
613
- throw new Error(`Refusing apply: files from the export are missing in ${path.resolve(dir)}:\n${missing.map(relative => ` missing ${relative}`).join('\n')}\nRestore them, or pass --allow-remove if deleting them is intended.`);
614
- }
615
- await assertCurrentMirrorHash({ skill, expectedHash, localFiles: files, args });
616
- const data = await applySkillPackage({
617
- skill,
618
- files,
619
- expectedHash,
620
- expectedRevision: expectedRevisionRaw ? Number(expectedRevisionRaw) : undefined,
621
- reason,
622
- via: 'mirror-apply',
623
- args,
624
- });
625
- console.log(`Applied ${skill}: revision ${Number(data.revision || 0)} (${String(data.contentHash || (0, skill_mirror_client_1.computeMirrorHash)(files))})`);
319
+ verifyWrittenPackage(to, files);
320
+ console.log(`Exported ${name} revision ${Number(data.revision || 0)} (${String(data.contentHash || '')}) — a read-only copy; edit ${gitSkillLocation(name)} instead.`);
626
321
  }
627
322
  function snapshotNameArg(args) {
628
323
  const explicit = getArg(args, '--name');
@@ -647,9 +342,10 @@ async function runSkillMirrorSnapshot(args) {
647
342
  const verb = args[0];
648
343
  const skill = args[1];
649
344
  const rest = args.slice(2);
650
- if (!verb || !skill) {
651
- throw new Error('Usage: greprag skill mirror snapshot create|list|show|restore|promote <skill> [--name <snapshot>]');
652
- }
345
+ if (verb === 'promote' || verb === 'create')
346
+ refuseMirrorWrite(`mirror snapshot ${verb}`, skill);
347
+ if (!verb || !skill)
348
+ throw new Error('Usage: greprag skill mirror snapshot list|show|restore <skill> [--name <snapshot>]');
653
349
  if (verb === 'list') {
654
350
  const data = await mirrorApiJson('GET', `/v1/skillmirror/${encodeURIComponent(skill)}/snapshots${mirrorLookupQuery(rest)}`);
655
351
  const snapshots = data.snapshots || [];
@@ -665,19 +361,6 @@ async function runSkillMirrorSnapshot(args) {
665
361
  const name = snapshotNameArg(rest);
666
362
  if (!name)
667
363
  throw new Error(`Usage: greprag skill mirror snapshot ${verb} <skill> --name <snapshot>`);
668
- if (verb === 'create') {
669
- const body = {
670
- ...mirrorBodyScope(rest),
671
- reason: getArg(rest, '--reason'),
672
- force: rest.includes('--force'),
673
- };
674
- const data = await mirrorApiJson('POST', `/v1/skillmirror/${encodeURIComponent(skill)}/snapshots/${encodeURIComponent(name)}`, body);
675
- const snap = data.snapshot;
676
- console.log(`Snapshot ${String(snap.snapshotName || name)} stored for ${skill}: revision ${Number(snap.revision || 0)}`);
677
- return;
678
- }
679
- const promoteUsage = 'greprag skill mirror snapshot promote <skill> --name <snapshot> --expected-hash <current-live-hash> --reason "<why>"';
680
- const promoteReason = verb === 'promote' && !rest.includes('--check') ? requireReason(rest, promoteUsage) : '';
681
364
  const data = await mirrorApiJson('GET', `/v1/skillmirror/${encodeURIComponent(skill)}/snapshots/${encodeURIComponent(name)}${mirrorLookupQuery(rest)}`);
682
365
  const snapshot = data.snapshot;
683
366
  if (verb === 'show') {
@@ -698,34 +381,53 @@ async function runSkillMirrorSnapshot(args) {
698
381
  const files = snapshotPresentFiles(snapshot);
699
382
  if (!writeSkillPackage(to, files, { force: rest.includes('--force'), check: rest.includes('--check'), exact: true }))
700
383
  return;
701
- console.log(`Restored snapshot ${name} for ${skill} to ${path.resolve(to)}. To make it canon, run greprag skill mirror snapshot promote ${skill} --name ${name} --expected-hash <current-live-hash> --reason "<why>".`);
384
+ console.log(`Restored snapshot ${name} for ${skill} to ${path.resolve(to)}. To make it current, copy what you need into ${gitSkillLocation(skill)} and commit.`);
702
385
  return;
703
386
  }
704
- if (verb === 'promote') {
705
- const expectedHash = getArg(rest, '--expected-hash') || getArg(rest, '--expectedHash');
706
- if (!expectedHash) {
707
- throw new Error(`Usage: ${promoteUsage}`);
708
- }
709
- const files = snapshotPresentFiles(snapshot);
710
- if (!files.some(file => file.path === 'SKILL.md')) {
711
- throw new Error(`Snapshot ${name} for ${skill} has no present SKILL.md file.`);
712
- }
713
- const diff = diffSkillPackages([], files);
714
- if (rest.includes('--check')) {
715
- console.log(`Would promote snapshot ${name} for ${skill} with expected hash ${expectedHash}.`);
716
- printSkillPackageDiff('Snapshot files', diff);
717
- return;
718
- }
719
- await assertCurrentMirrorHash({ skill, expectedHash, localFiles: files, args: rest });
720
- const data = await applySkillPackage({
721
- skill, files, expectedHash, reason: `promote snapshot "${name}": ${promoteReason}`, via: 'snapshot-promote', args: rest,
722
- });
723
- console.log(`Promoted snapshot ${name} for ${skill}: revision ${Number(data.revision || 0)} (${String(data.contentHash || (0, skill_mirror_client_1.computeMirrorHash)(files))})`);
387
+ throw new Error('Usage: greprag skill mirror snapshot list|show|restore <skill> [--name <snapshot>]');
388
+ }
389
+ async function runSkillMirrorSync(args) {
390
+ if (args.includes('--cutover-lifecycle') || args.some(arg => arg === '--only' || arg.startsWith('--only='))) {
391
+ throw new Error('Claude Code reads skills from their git folders directly; there are no Claude Code adapters to install.');
392
+ }
393
+ const requested = args.find(arg => !arg.startsWith('-')) || 'all';
394
+ if (requested === 'claude-code') {
395
+ console.log('claude-code: no adapters; Claude Code reads ~/.claude/skills and <repo>/.claude/skills from git.');
396
+ }
397
+ const platforms = requested === 'all' || requested === 'claude-code'
398
+ ? [...native_skill_mirror_1.NATIVE_SKILL_ADAPTER_PLATFORMS]
399
+ : native_skill_mirror_1.NATIVE_SKILL_ADAPTER_PLATFORMS.includes(requested)
400
+ ? [requested]
401
+ : [];
402
+ if (!platforms.length)
403
+ throw new Error('Harness must be codex, opencode, claude-code or all.');
404
+ const { apiUrl, apiKey } = mirrorApiConfig();
405
+ const sources = (0, skill_git_ingest_1.gitSkillSources)(process.cwd());
406
+ const projectId = sources.find(source => source.scope === 'project')?.projectId;
407
+ if (args.includes('--dry-run')) {
408
+ for (const source of sources)
409
+ console.log(`would ingest committed skills from ${source.repo} ${source.prefix}/ (${source.scope})`);
724
410
  return;
725
411
  }
726
- throw new Error('Usage: greprag skill mirror snapshot create|list|show|restore|promote <skill> [--name <snapshot>]');
412
+ const hashes = await (0, skill_git_ingest_1.fetchMirrorHashes)(apiUrl, apiKey, projectId);
413
+ const ingest = await (0, skill_git_ingest_1.ingestCommittedSkills)({ apiUrl, apiKey, sources, mirrorHashes: hashes, force: args.includes('--force') });
414
+ console.log(`mirror: ${ingest.uploaded.length} skill(s) ingested from git${ingest.uploaded.length ? ` (${ingest.uploaded.join(', ')})` : ''}, ${ingest.current} current`);
415
+ for (const failure of ingest.failed)
416
+ console.error(`mirror: ${failure.name} failed — ${failure.error}`);
417
+ if (requested === 'claude-code')
418
+ return;
419
+ const anchor = (0, project_anchor_1.readAnchor)(process.cwd());
420
+ for (const platform of platforms) {
421
+ const result = await (0, native_skill_mirror_1.syncNativeSkillMirror)({
422
+ platform, apiUrl, apiKey,
423
+ projectId: anchor.projectId,
424
+ projectDir: process.cwd(),
425
+ });
426
+ console.log(`${platform}: ${(0, native_skill_mirror_1.nativeSkillMirrorSummary)(result)}`);
427
+ }
428
+ console.log('Restart open Codex/OpenCode sessions to refresh their native skill manifest.');
727
429
  }
728
- // ---------- Revision history: history / diff / revert ------------------------
430
+ // ---------- Revision history: history / diff (read only) ------------------------
729
431
  // adr: adr/skill-revision-history.md
730
432
  function revisionArg(raw, label) {
731
433
  const value = Number(String(raw || '').replace(/^r/i, ''));
@@ -814,32 +516,6 @@ async function runSkillDiff(args) {
814
516
  console.log(String(file.patch || `${String(file.status)} ${String(file.path)}`));
815
517
  }
816
518
  }
817
- async function runSkillRevert(args) {
818
- const usage = 'greprag skill revert <name> --to <rev> --reason "<why>" --expected-hash <current-hash> [--skill-id <id>]';
819
- const skill = args[0];
820
- if (!skill || skill.startsWith('--'))
821
- throw new Error(`Usage: ${usage}`);
822
- refuseBundledSkillWrite(skill);
823
- const to = revisionArg(getArg(args, '--to'), '--to');
824
- const reason = requireReason(args, usage);
825
- const expectedHash = getArg(args, '--expected-hash') || getArg(args, '--expectedHash');
826
- if (!expectedHash) {
827
- throw new Error(`Refusing: --expected-hash <current-hash> is required (the hash \`greprag skill history ${skill}\` or the adapter shows for the current revision).\nUsage: ${usage}`);
828
- }
829
- const skillId = getArg(args, '--skill-id') || getArg(args, '--skillId');
830
- const scope = getArg(args, '--scope');
831
- const projectId = getArg(args, '--project-id') || getArg(args, '--projectId');
832
- const data = await mirrorApiJson('POST', `/v1/skillmirror/${encodeURIComponent(skill)}/revert`, {
833
- to,
834
- reason,
835
- expectedHash,
836
- author: (0, skill_mirror_client_1.skillRevisionAuthor)('revert'),
837
- ...(skillId ? { skillId } : {}),
838
- ...(scope ? { scope } : {}),
839
- ...(projectId ? { projectId } : {}),
840
- });
841
- console.log(`Reverted ${skill} to the bytes of r${to}: new revision ${Number(data.revision || 0)} (${String(data.contentHash || '')})`);
842
- }
843
519
  async function runSkill(args) {
844
520
  const sub = args[0];
845
521
  if (!sub || sub === '--help' || sub === '-h' || sub === 'help') {
@@ -849,17 +525,14 @@ async function runSkill(args) {
849
525
  const bundled = listBundled();
850
526
  switch (sub) {
851
527
  case 'edit':
852
- await runSkillEdit(args.slice(1));
853
- return;
528
+ case 'revert':
529
+ refuseMirrorWrite(sub, args[1]);
854
530
  case 'history':
855
531
  await runSkillHistory(args.slice(1));
856
532
  return;
857
533
  case 'diff':
858
534
  await runSkillDiff(args.slice(1));
859
535
  return;
860
- case 'revert':
861
- await runSkillRevert(args.slice(1));
862
- return;
863
536
  case 'mirror': {
864
537
  const mirrorVerb = args[1] || 'sync';
865
538
  if (mirrorVerb === 'export') {
@@ -867,142 +540,40 @@ async function runSkill(args) {
867
540
  return;
868
541
  }
869
542
  if (mirrorVerb === 'apply') {
870
- await runSkillMirrorApply(args.slice(2));
871
- return;
543
+ const skill = getArg(args, '--skill') || (args[2] ? path.basename(path.resolve(args[2])) : undefined);
544
+ refuseMirrorWrite('mirror apply', skill);
872
545
  }
873
546
  if (mirrorVerb === 'snapshot') {
874
547
  await runSkillMirrorSnapshot(args.slice(2));
875
548
  return;
876
549
  }
877
- if (mirrorVerb !== 'sync') {
878
- console.error('Usage: greprag skill mirror sync|export|apply|snapshot ...');
879
- process.exit(1);
880
- }
881
- const dryRun = args.includes('--dry-run');
882
- const claudeLifecycleCutover = args.includes('--cutover-lifecycle');
883
- // `--only a,b` names the mirrored skills to materialize as claude-code
884
- // adapters — the one command that closes an adapter gap the catalog and
885
- // the SessionStart announce report, without arming a fleet-wide sweep.
886
- const onlyNames = readOnlyNames(args.slice(2));
887
- const positional = args.slice(2).filter((arg, index, all) => !arg.startsWith('-') && all[index - 1] !== '--only');
888
- const requested = positional[0] || 'all';
889
- const platforms = requested === 'all'
890
- ? [...native_skill_mirror_1.NATIVE_SKILL_ADAPTER_PLATFORMS]
891
- : ['claude-code', 'codex', 'opencode'].includes(requested)
892
- ? [requested]
893
- : [];
894
- if (platforms.length === 0) {
895
- console.error('Harness must be claude-code, codex, opencode, or all.');
896
- process.exit(1);
897
- }
898
- if (claudeLifecycleCutover && requested !== 'claude-code') {
899
- throw new Error('--cutover-lifecycle requires harness claude-code.');
900
- }
901
- if (onlyNames.length > 0 && requested !== 'claude-code') {
902
- throw new Error('--only requires harness claude-code.');
903
- }
904
- const claudeTargeted = claudeLifecycleCutover || onlyNames.length > 0;
905
- const apiUrl = process.env.GREPRAG_API_URL || 'https://api.greprag.com';
906
- const anchor = (0, project_anchor_1.readAnchor)(process.cwd());
907
- if (requested === 'all') {
908
- console.log('claude-code: native skill adapter sync disabled until Claude cutover approval.');
909
- }
910
- for (const platform of platforms) {
911
- if (platform === 'claude-code' && claudeTargeted) {
912
- const names = onlyNames.length > 0 ? onlyNames : [...claude_lifecycle_cutover_1.CLAUDE_LIFECYCLE_SKILLS];
913
- if (dryRun) {
914
- console.log(`claude-code: targeted cutover would manage only ${names.join(', ')}; companion files remain untouched.`);
915
- continue;
916
- }
917
- const apiKey = process.env.GREPRAG_API_KEY || '';
918
- if (!apiKey)
919
- throw new Error('GREPRAG_API_KEY not set — run `greprag init` first.');
920
- const result = await (0, claude_lifecycle_cutover_1.syncClaudeLifecycleCutover)({ apiUrl, apiKey, names });
921
- const active = result.installed + result.updated + result.current;
922
- console.log(`claude-code: ${active} adapter(s) active (${names.join(', ')}); all other Claude skills remain unmanaged.`);
923
- if (result.backupDir)
924
- console.log(`claude-code: replaced prompts backed up at ${result.backupDir}`);
925
- continue;
926
- }
927
- if (!(0, native_skill_mirror_1.nativeSkillMirrorEnabled)(platform)) {
928
- console.log(`${platform}: native skill adapter sync disabled until Claude cutover approval.`);
929
- continue;
930
- }
931
- if (dryRun) {
932
- printAdoptionDryRun(platform, {
933
- plan: (0, skill_mirror_client_1.dryRunNativeSkillAdoption)({
934
- platform,
935
- projectId: anchor.projectId,
936
- projectName: anchor.projectName,
937
- projectDir: process.cwd(),
938
- }),
939
- adopted: 0,
940
- blocked: 0,
941
- skipped: 0,
942
- });
943
- continue;
944
- }
945
- const apiKey = process.env.GREPRAG_API_KEY || '';
946
- if (!apiKey)
947
- throw new Error('GREPRAG_API_KEY not set — run `greprag init` first.');
948
- const adoption = await (0, skill_mirror_client_1.syncNativeSkillAdoptions)({
949
- platform, apiUrl, apiKey,
950
- projectId: anchor.projectId,
951
- projectName: anchor.projectName,
952
- projectDir: process.cwd(),
953
- });
954
- printAdoptionSync(platform, adoption);
955
- const result = await (0, native_skill_mirror_1.syncNativeSkillMirror)({
956
- platform, apiUrl, apiKey,
957
- projectId: anchor.projectId,
958
- projectDir: process.cwd(),
959
- });
960
- console.log(`${platform}: ${(0, native_skill_mirror_1.nativeSkillMirrorSummary)(result)}`);
961
- }
962
- if (!dryRun)
963
- console.log('Restart open harness sessions to refresh their native skill manifest.');
550
+ if (mirrorVerb !== 'sync')
551
+ throw new Error('Usage: greprag skill mirror sync|export|snapshot ...');
552
+ await runSkillMirrorSync(args.slice(2));
964
553
  return;
965
554
  }
966
555
  case 'source': {
967
- if (args[1] !== 'stamp') {
968
- console.error('Usage: greprag skill source stamp <name> --project <p> --path <rel> [--path <rel>...] [--root <dir>] [--baseline <ref>]');
969
- process.exit(1);
970
- }
556
+ const usage = 'Usage: greprag skill source stamp <name> --project <p> --path <rel> [--path <rel>...] [--root <dir>] [--baseline <ref>]';
971
557
  const name = args[2];
972
558
  const rest = args.slice(3);
973
559
  const project = getArg(rest, '--project');
974
- const paths = getMany(rest, '--path');
975
- if (!name || !project || paths.length === 0) {
976
- console.error('Usage: greprag skill source stamp <name> --project <p> --path <rel> [--path <rel>...] [--root <dir>] [--baseline <ref>]');
977
- process.exit(1);
978
- }
979
- const homeDir = process.env.HOME || process.env.USERPROFILE || '';
980
- const skillDir = (0, skill_landing_1.resolveSkillDir)(name, process.cwd(), homeDir);
560
+ const paths = rest.flatMap((arg, i) => (arg === '--path' && rest[i + 1] ? [rest[i + 1]] : []));
561
+ if (args[1] !== 'stamp' || !name || !project || paths.length === 0)
562
+ throw new Error(usage);
563
+ const skillDir = gitSkillDir(name);
981
564
  if (!skillDir)
982
- throw new Error(`No local canonical skill found for ${name}.`);
565
+ throw new Error(`No git folder with a SKILL.md found for ${name}; looked at ${gitSkillLocation(name)}.`);
983
566
  const root = getArg(rest, '--root') || (0, skill_staleness_1.resolveSourceRoot)(project, process.cwd());
984
567
  if (!root)
985
568
  throw new Error(`No local Git checkout found for source project ${project}.`);
986
569
  const skillPath = path.join(skillDir, 'SKILL.md');
987
- const before = fs.readFileSync(skillPath, 'utf-8');
988
570
  const stamped = (0, skill_staleness_1.stampSourceBinding)({
989
- skillMd: before, project, root, paths,
571
+ skillMd: fs.readFileSync(skillPath, 'utf-8'), project, root, paths,
990
572
  baselineCommit: getArg(rest, '--baseline'),
991
573
  });
992
574
  fs.writeFileSync(skillPath, stamped.content.endsWith('\n') ? stamped.content : stamped.content + '\n');
993
- console.log(`Stamped ${name}: ${project} @ ${stamped.binding.baselineCommit.slice(0, 8)} (${stamped.binding.paths.length} paths)`);
994
- const apiKey = process.env.GREPRAG_API_KEY || '';
995
- if (apiKey) {
996
- const apiUrl = process.env.GREPRAG_API_URL || 'https://api.greprag.com';
997
- const outcome = await (0, skill_mirror_client_1.maybeMirrorSkill)({
998
- skill: name, cwd: process.cwd(), apiUrl, apiKey,
999
- reason: `source stamp ${project} @ ${stamped.binding.baselineCommit.slice(0, 8)}`,
1000
- });
1001
- console.log(`Mirror: ${outcome}`);
1002
- }
1003
- else {
1004
- console.log('Mirror: skipped (GREPRAG_API_KEY not set; next skill use will retry).');
1005
- }
575
+ console.log(`Stamped ${name}: ${project} @ ${stamped.binding.baselineCommit.slice(0, 8)} (${stamped.binding.paths.length} paths) in ${skillPath}`);
576
+ console.log('Commit it; the mirror picks it up at the next session start or `greprag skill mirror sync`.');
1006
577
  return;
1007
578
  }
1008
579
  case 'list': {