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
@@ -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,106 +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> [--skill-id <id>] [--scope project --project-id <id>]
87
- Export/edit/apply helper; opens $EDITOR unless --no-open or Codex
88
- greprag skill mirror sync [harness] Install native GrepRAG skill adapters
89
- claude-code --cutover-lifecycle Cut over commit/deploy/release only
90
- claude-code --only <a,b> Install adapters for named mirrored skills
91
- (closes the gaps greprag load marks with *)
69
+ greprag skill history <name> [--skill-id <id>] [--limit <n>]
70
+ Every mirrored revision: date, source commit, changes
71
+ greprag skill diff <name> <revA> [<revB>]
72
+ Unified diff between two revisions (revB defaults to current)
73
+ greprag skill mirror sync [codex|opencode|all] [--force]
74
+ Ingest committed skills from git into the mirror,
75
+ then refresh Codex/OpenCode launchers
92
76
  greprag skill mirror export <name> --to <dir>
93
- Export canonical skill bytes for editing
94
- greprag skill mirror apply <dir> --skill <name> --expected-hash <hash>
95
- Promote edited bytes with stale-write guard
96
- greprag skill mirror snapshot create <name> --name <snapshot>
97
- greprag skill mirror snapshot list <name>
98
- 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>]
99
79
  greprag skill mirror snapshot restore <name> --name <snapshot> --to <dir>
100
- greprag skill mirror snapshot promote <name> --name <snapshot> --expected-hash <hash>
101
- Promote snapshot bytes as the newest revision
102
- Named package rollback points
80
+ Archived named snapshots (read only)
103
81
  greprag skill source stamp <name> --project <p> --path <rel> [--path <rel>...]
104
- [--root <dir>] [--baseline <ref>]
105
- Stamp a skill's local source baseline
82
+ Stamp a source binding into the skill's git SKILL.md
83
+
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.
106
93
 
107
- NOTES
108
- • The 'greprag' skill is auto-installed and refreshed on every upgrade.
109
- It cannot be uninstalled via this verb (delete the dir manually if
110
- you really want it gone — postinstall will reinstall on next upgrade).
111
- • Other bundled skills are opt-in and refresh-only on upgrade: once
112
- installed, they stay current with the bundle; if never installed,
113
- upgrades leave them out of your skill registry.
114
- • Mirrored skill edits use export/apply semantics. \`skill edit\` only wraps
115
- that flow: export current bytes, open an editor, show a diff summary, then
116
- apply with the exported content hash.
117
- • Codex agents must use a non-interactive path: \`skill mirror export\`, patch
118
- files, then \`skill mirror apply\`. In Codex, \`skill edit\` skips the editor
119
- and prints the guarded apply command; \`--no-open\` does the same explicitly.
120
- • Source stamps are local Git facts. They update the local canonical skill
121
- file, then mirror to GrepRAG when GREPRAG_API_KEY is configured.`;
122
- function formatAdoptionLine(prefix, entry) {
123
- return entry.reason
124
- ? ` ${prefix} ${entry.scope}:${entry.skillName} — ${entry.reason}`
125
- : ` ${prefix} ${entry.scope}:${entry.skillName}`;
126
- }
127
- function printAdoptionDryRun(platform, result) {
128
- const plan = result.plan;
129
- const candidateCount = plan.candidates.length;
130
- const managedCount = plan.managed.length;
131
- const blockedCount = plan.blocked.length;
132
- const skippedCount = plan.skipped.length;
133
- const ignoredCount = plan.ignoredRoots.reduce((sum, root) => sum + root.skillCount, 0);
134
- console.log(`${platform}: adoption dry-run: ${candidateCount} candidate(s), ${managedCount} managed, ${blockedCount} blocked, ${skippedCount} skipped, ${ignoredCount} ignored in unowned roots`);
135
- for (const candidate of plan.candidates)
136
- console.log(formatAdoptionLine('adopt', candidate));
137
- for (const blocked of plan.blocked)
138
- console.log(formatAdoptionLine('block', blocked));
139
- for (const skipped of plan.skipped)
140
- console.log(formatAdoptionLine('skip ', skipped));
141
- for (const root of plan.ignoredRoots) {
142
- console.log(` ignore ${root.skillCount} skill(s) under ${root.root} — ${root.reason}`);
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 };
143
106
  }
144
- }
145
- function printAdoptionSync(platform, result) {
146
- const candidateCount = result.plan.candidates.length;
147
- if (!candidateCount && !result.blocked && !result.skipped)
148
- return;
149
- console.log(`${platform}: native skill adoption: ${result.adopted}/${candidateCount} adopted, ${result.blocked} blocked, ${result.skipped} skipped`);
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.');
150
131
  }
151
132
  function bundleRoot() {
152
133
  // Same resolution as init.ts Step 6 + postinstall.js
@@ -183,16 +164,6 @@ function getArg(args, name) {
183
164
  const idx = args.indexOf(name);
184
165
  return idx >= 0 ? args[idx + 1] : undefined;
185
166
  }
186
- function getMany(args, name) {
187
- const out = [];
188
- for (let i = 0; i < args.length; i++) {
189
- if (args[i] === name && args[i + 1]) {
190
- out.push(args[i + 1]);
191
- i++;
192
- }
193
- }
194
- return out;
195
- }
196
167
  function mirrorApiConfig() {
197
168
  const apiKey = process.env.GREPRAG_API_KEY || '';
198
169
  if (!apiKey)
@@ -234,18 +205,6 @@ function mirrorLookupQuery(args) {
234
205
  const qs = params.toString();
235
206
  return qs ? `?${qs}` : '';
236
207
  }
237
- function mirrorBodyScope(args) {
238
- const scope = getArg(args, '--scope');
239
- const projectId = getArg(args, '--project-id') || getArg(args, '--projectId');
240
- const projectName = getArg(args, '--project-name') || getArg(args, '--projectName');
241
- const replaces = getMany(args, '--replaces');
242
- return {
243
- ...(scope ? { scope } : {}),
244
- ...(projectId ? { projectId } : {}),
245
- ...(projectName ? { projectName } : {}),
246
- ...(replaces.length ? { replaces } : {}),
247
- };
248
- }
249
208
  function validMirrorRelPath(relative) {
250
209
  return /^(?!\.{1,2}(\/|$))[A-Za-z0-9._\/-]{1,200}\.md$/.test(relative) && !relative.includes('..');
251
210
  }
@@ -288,12 +247,6 @@ function listMarkdownFiles(root) {
288
247
  walk(root, '');
289
248
  return out;
290
249
  }
291
- function collectSkillPackageOrThrow(root) {
292
- const files = (0, skill_mirror_client_1.collectSkillFiles)(path.resolve(root));
293
- if (!files)
294
- throw new Error(`No valid SKILL.md package found at ${path.resolve(root)}.`);
295
- return files;
296
- }
297
250
  function fileMap(files) {
298
251
  return new Map(files.map(file => [file.path, file.content]));
299
252
  }
@@ -311,38 +264,6 @@ function diffSkillPackages(before, after) {
311
264
  }
312
265
  return diff;
313
266
  }
314
- function printSkillPackageDiff(label, diff) {
315
- if (!diff.length) {
316
- console.log(`${label}: no markdown changes`);
317
- return;
318
- }
319
- console.log(`${label}:`);
320
- for (const item of diff)
321
- console.log(` ${item.status.padEnd(7)} ${item.path}`);
322
- }
323
- function shellArg(value) {
324
- return /^[A-Za-z0-9._:\/\\-]+$/.test(value)
325
- ? value
326
- : `"${value.replace(/(["\\$`])/g, '\\$1')}"`;
327
- }
328
- function forwardedMirrorArgs(args) {
329
- const forwarded = new Set([
330
- '--skill-id', '--skillId',
331
- '--scope',
332
- '--project-id', '--projectId',
333
- '--project-name', '--projectName',
334
- '--replaces',
335
- ]);
336
- const parts = [];
337
- for (let i = 1; i < args.length; i++) {
338
- const arg = args[i];
339
- if (!forwarded.has(arg) || !args[i + 1])
340
- continue;
341
- parts.push(arg, shellArg(args[i + 1]));
342
- i++;
343
- }
344
- return parts.join(' ');
345
- }
346
267
  /** Returns whether it wrote: under `check` it only lists the plan, so a caller reports success only on true. */
347
268
  function writeSkillPackage(targetDir, files, opts) {
348
269
  const target = path.resolve(targetDir);
@@ -368,26 +289,20 @@ function writeSkillPackage(targetDir, files, opts) {
368
289
  for (const file of files) {
369
290
  const destination = safePackagePath(target, file.path);
370
291
  fs.mkdirSync(path.dirname(destination), { recursive: true });
371
- fs.writeFileSync(destination, file.content.endsWith('\n') ? file.content : file.content + '\n', 'utf8');
292
+ fs.writeFileSync(destination, file.content, 'utf8');
372
293
  }
373
294
  return true;
374
295
  }
375
- function printSkillMirrorConflict(params) {
376
- const local = new Map(params.localFiles.map(file => [file.path, file.content]));
377
- const remote = new Map(params.remoteFiles.map(file => [file.path, file.content]));
378
- const paths = [...new Set([...local.keys(), ...remote.keys()])].sort();
379
- console.error(`Conflict: ${params.skill} mirror changed since export.`);
380
- console.error(` expected: ${params.expectedHash}`);
381
- console.error(` current: ${params.currentHash}`);
382
- for (const relative of paths) {
383
- const status = !local.has(relative) ? 'remote-only'
384
- : !remote.has(relative) ? 'local-only'
385
- : local.get(relative) === remote.get(relative) ? 'same'
386
- : 'changed';
387
- if (status !== 'same')
388
- console.error(` ${status.padEnd(11)} ${relative}`);
389
- }
390
- console.error(`Export current mirror: greprag skill mirror export ${params.skill} --to <dir> --force`);
296
+ /** Read the folder back and fail unless it holds exactly the files the mirror
297
+ * sent, byte for byte. Catches a partial write and any file the package
298
+ * reader would skip (size or count limits). */
299
+ function verifyWrittenPackage(targetDir, files) {
300
+ const written = (0, skill_mirror_client_1.collectSkillFiles)(path.resolve(targetDir)) || [];
301
+ const diff = diffSkillPackages(files, written);
302
+ if (!diff.length)
303
+ return;
304
+ const detail = diff.map(item => ` ${item.status === 'removed' ? 'missing' : item.status} ${item.path}`).join('\n');
305
+ throw new Error(`export did not write the revision it fetched to ${path.resolve(targetDir)}:\n${detail}`);
391
306
  }
392
307
  async function runSkillMirrorExport(args) {
393
308
  const name = args[0];
@@ -401,142 +316,8 @@ async function runSkillMirrorExport(args) {
401
316
  throw new Error(`No mirrored files returned for ${name}.`);
402
317
  if (!writeSkillPackage(to, files, { force: args.includes('--force'), check: args.includes('--check'), exact: true }))
403
318
  return;
404
- console.log(`Exported ${name} revision ${Number(data.revision || 0)} (${String(data.contentHash || '')})`);
405
- }
406
- async function loadMirrorPackage(skill, args) {
407
- const data = await mirrorApiJson('GET', `/v1/skillmirror/${encodeURIComponent(skill)}${mirrorLookupQuery(args)}`);
408
- const files = (data.files || [])
409
- .filter((file) => typeof file.path === 'string' && typeof file.content === 'string');
410
- if (!files.length)
411
- throw new Error(`No mirrored files returned for ${skill}.`);
412
- return {
413
- files,
414
- revision: Number(data.revision || 0),
415
- contentHash: typeof data.contentHash === 'string' ? data.contentHash : (0, skill_mirror_client_1.computeMirrorHash)(files),
416
- };
417
- }
418
- function editorCommand() {
419
- return process.env.GREPRAG_EDITOR
420
- || process.env.VISUAL
421
- || process.env.EDITOR
422
- || (process.platform === 'win32' ? 'notepad' : 'vi');
423
- }
424
- function codexHarnessDetected() {
425
- return Boolean(process.env.CODEX_THREAD_ID || process.env.CODEX_CI || process.env.CODEX_SHELL);
426
- }
427
- function runEditor(target) {
428
- const editor = editorCommand();
429
- const result = (0, proc_1.safeSpawnSync)(editor, [target], { stdio: 'inherit', shell: true });
430
- if (result.error)
431
- throw new Error(`editor failed: ${result.error.message}`);
432
- if (typeof result.status === 'number' && result.status !== 0) {
433
- throw new Error(`editor exited with status ${result.status}`);
434
- }
435
- }
436
- /** A skill the CLI ships as its own bundle has one canon, the bundle, and
437
- * `greprag load` serves it for that skill's global id. A mirror write under
438
- * its name would land in a row nothing reads. */
439
- function refuseBundledSkillWrite(skill) {
440
- if (!(0, load_1.bundleOwnsSkill)(skill))
441
- return;
442
- 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.`);
443
- }
444
- async function applySkillPackage(params) {
445
- refuseBundledSkillWrite(params.skill);
446
- const hash = (0, skill_mirror_client_1.computeMirrorHash)(params.files);
447
- const body = {
448
- files: params.files,
449
- contentHash: hash,
450
- ...mirrorBodyScope(params.args),
451
- expectedHash: params.expectedHash,
452
- ...(typeof params.expectedRevision === 'number' ? { expectedRevision: params.expectedRevision } : {}),
453
- };
454
- return mirrorApiJson('POST', `/v1/skillmirror/${encodeURIComponent(params.skill)}`, body);
455
- }
456
- async function assertCurrentMirrorHash(params) {
457
- try {
458
- const current = await mirrorApiJson('GET', `/v1/skillmirror/${encodeURIComponent(params.skill)}${mirrorLookupQuery(params.args)}`);
459
- const currentHash = typeof current.contentHash === 'string' ? current.contentHash : '';
460
- if (currentHash && currentHash !== params.expectedHash) {
461
- const remoteFiles = (current.files || [])
462
- .filter((file) => typeof file.path === 'string' && typeof file.content === 'string');
463
- printSkillMirrorConflict({
464
- skill: params.skill,
465
- expectedHash: params.expectedHash,
466
- currentHash,
467
- localFiles: params.localFiles,
468
- remoteFiles,
469
- });
470
- throw new Error('Refusing stale apply; re-export or merge against the current mirror.');
471
- }
472
- }
473
- catch (err) {
474
- if (!(err instanceof Error) || !/HTTP 404\b/.test(err.message))
475
- throw err;
476
- }
477
- }
478
- async function runSkillEdit(args) {
479
- const skill = args[0];
480
- if (!skill)
481
- throw new Error('Usage: greprag skill edit <name> [--dir <dir>] [--no-open] [--force] [--keep]');
482
- refuseBundledSkillWrite(skill);
483
- const explicitDir = getArg(args, '--dir') || getArg(args, '--to');
484
- const noOpen = args.includes('--no-open');
485
- const keep = args.includes('--keep') || Boolean(explicitDir);
486
- const workDir = explicitDir
487
- ? path.resolve(explicitDir)
488
- : fs.mkdtempSync(path.join(os.tmpdir(), `greprag-skill-edit-${skill}-`));
489
- const loaded = await loadMirrorPackage(skill, args);
490
- writeSkillPackage(workDir, loaded.files, {
491
- force: args.includes('--force') || !explicitDir,
492
- exact: true,
493
- });
494
- console.log(`Editing ${skill} revision ${loaded.revision} (${loaded.contentHash})`);
495
- console.log(`Work dir: ${workDir}`);
496
- const skipEditor = noOpen || codexHarnessDetected();
497
- if (skipEditor) {
498
- console.log(noOpen
499
- ? 'Editor skipped.'
500
- : 'Codex harness detected; editor skipped to avoid launching an interactive editor.');
501
- const forward = forwardedMirrorArgs(args);
502
- console.log(`Apply after editing: greprag skill mirror apply ${shellArg(workDir)} --skill ${skill}${forward ? ` ${forward}` : ''} --expected-hash ${loaded.contentHash}`);
503
- return;
504
- }
505
- runEditor(path.join(workDir, 'SKILL.md'));
506
- const edited = collectSkillPackageOrThrow(workDir);
507
- const diff = diffSkillPackages(loaded.files, edited);
508
- printSkillPackageDiff('Diff', diff);
509
- if (!diff.length) {
510
- if (!keep)
511
- fs.rmSync(workDir, { recursive: true, force: true });
512
- return;
513
- }
514
- await assertCurrentMirrorHash({ skill, expectedHash: loaded.contentHash, localFiles: edited, args });
515
- const data = await applySkillPackage({ skill, files: edited, expectedHash: loaded.contentHash, args });
516
- console.log(`Applied ${skill}: revision ${Number(data.revision || 0)} (${String(data.contentHash || (0, skill_mirror_client_1.computeMirrorHash)(edited))})`);
517
- if (!keep)
518
- fs.rmSync(workDir, { recursive: true, force: true });
519
- }
520
- async function runSkillMirrorApply(args) {
521
- const dir = args[0];
522
- const skill = getArg(args, '--skill') || (dir ? path.basename(path.resolve(dir)) : '');
523
- if (!dir || !skill)
524
- throw new Error('Usage: greprag skill mirror apply <dir> --skill <name> --expected-hash <hash>');
525
- const expectedRevisionRaw = getArg(args, '--expected-revision') || getArg(args, '--expectedRevision');
526
- const expectedHash = getArg(args, '--expected-hash') || getArg(args, '--expectedHash');
527
- if (!expectedHash) {
528
- throw new Error('Refusing unguarded apply; export first, then pass the exported --expected-hash <hash>.');
529
- }
530
- const files = collectSkillPackageOrThrow(dir);
531
- await assertCurrentMirrorHash({ skill, expectedHash, localFiles: files, args });
532
- const data = await applySkillPackage({
533
- skill,
534
- files,
535
- expectedHash,
536
- expectedRevision: expectedRevisionRaw ? Number(expectedRevisionRaw) : undefined,
537
- args,
538
- });
539
- 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.`);
540
321
  }
541
322
  function snapshotNameArg(args) {
542
323
  const explicit = getArg(args, '--name');
@@ -561,9 +342,10 @@ async function runSkillMirrorSnapshot(args) {
561
342
  const verb = args[0];
562
343
  const skill = args[1];
563
344
  const rest = args.slice(2);
564
- if (!verb || !skill) {
565
- throw new Error('Usage: greprag skill mirror snapshot create|list|show|restore|promote <skill> [--name <snapshot>]');
566
- }
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>]');
567
349
  if (verb === 'list') {
568
350
  const data = await mirrorApiJson('GET', `/v1/skillmirror/${encodeURIComponent(skill)}/snapshots${mirrorLookupQuery(rest)}`);
569
351
  const snapshots = data.snapshots || [];
@@ -579,17 +361,6 @@ async function runSkillMirrorSnapshot(args) {
579
361
  const name = snapshotNameArg(rest);
580
362
  if (!name)
581
363
  throw new Error(`Usage: greprag skill mirror snapshot ${verb} <skill> --name <snapshot>`);
582
- if (verb === 'create') {
583
- const body = {
584
- ...mirrorBodyScope(rest),
585
- reason: getArg(rest, '--reason'),
586
- force: rest.includes('--force'),
587
- };
588
- const data = await mirrorApiJson('POST', `/v1/skillmirror/${encodeURIComponent(skill)}/snapshots/${encodeURIComponent(name)}`, body);
589
- const snap = data.snapshot;
590
- console.log(`Snapshot ${String(snap.snapshotName || name)} stored for ${skill}: revision ${Number(snap.revision || 0)}`);
591
- return;
592
- }
593
364
  const data = await mirrorApiJson('GET', `/v1/skillmirror/${encodeURIComponent(skill)}/snapshots/${encodeURIComponent(name)}${mirrorLookupQuery(rest)}`);
594
365
  const snapshot = data.snapshot;
595
366
  if (verb === 'show') {
@@ -610,30 +381,140 @@ async function runSkillMirrorSnapshot(args) {
610
381
  const files = snapshotPresentFiles(snapshot);
611
382
  if (!writeSkillPackage(to, files, { force: rest.includes('--force'), check: rest.includes('--check'), exact: true }))
612
383
  return;
613
- console.log(`Restored snapshot ${name} for ${skill} to ${path.resolve(to)}. Run apply with the expected revision if this should become canon.`);
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.`);
614
385
  return;
615
386
  }
616
- if (verb === 'promote') {
617
- const expectedHash = getArg(rest, '--expected-hash') || getArg(rest, '--expectedHash');
618
- if (!expectedHash) {
619
- throw new Error('Usage: greprag skill mirror snapshot promote <skill> --name <snapshot> --expected-hash <current-live-hash>');
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})`);
410
+ return;
411
+ }
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.');
429
+ }
430
+ // ---------- Revision history: history / diff (read only) ------------------------
431
+ // adr: adr/skill-revision-history.md
432
+ function revisionArg(raw, label) {
433
+ const value = Number(String(raw || '').replace(/^r/i, ''));
434
+ if (!raw || !Number.isInteger(value) || value < 0)
435
+ throw new Error(`${label} must be a revision number such as 12 or r12`);
436
+ return value;
437
+ }
438
+ function positionals(args) {
439
+ const out = [];
440
+ for (let i = 0; i < args.length; i++) {
441
+ if (args[i].startsWith('--')) {
442
+ if (args[i + 1] && !args[i + 1].startsWith('--'))
443
+ i++;
444
+ continue;
620
445
  }
621
- const files = snapshotPresentFiles(snapshot);
622
- if (!files.some(file => file.path === 'SKILL.md')) {
623
- throw new Error(`Snapshot ${name} for ${skill} has no present SKILL.md file.`);
446
+ out.push(args[i]);
447
+ }
448
+ return out;
449
+ }
450
+ function authorLabel(author) {
451
+ if (!author)
452
+ return '';
453
+ const parts = [
454
+ typeof author.harness === 'string' ? author.harness : '',
455
+ typeof author.sessionId === 'string' ? author.sessionId.slice(0, 8) : '',
456
+ typeof author.chip === 'string' ? author.chip : '',
457
+ ].filter(Boolean);
458
+ const via = typeof author.via === 'string' ? author.via : '';
459
+ return [via, parts.join(' ')].filter(Boolean).join(' · ');
460
+ }
461
+ async function runSkillHistory(args) {
462
+ const skill = args[0];
463
+ if (!skill)
464
+ throw new Error('Usage: greprag skill history <name> [--skill-id <id>] [--limit <n>]');
465
+ const limit = getArg(args, '--limit');
466
+ const qs = mirrorLookupQuery(args);
467
+ const sep = qs ? '&' : '?';
468
+ const data = await mirrorApiJson('GET', `/v1/skillmirror/${encodeURIComponent(skill)}/history${qs}${limit ? `${sep}limit=${encodeURIComponent(limit)}` : ''}`);
469
+ const revisions = data.revisions || [];
470
+ if (!revisions.length) {
471
+ console.log(`No recorded revisions for ${skill}.`);
472
+ return;
473
+ }
474
+ console.log(`${skill} (${String(data.skillId || '')}) — newest first`);
475
+ console.log(`current: r${String(revisions[0].revision)} ${String(revisions[0].contentHash || '')}`);
476
+ for (const r of revisions) {
477
+ const date = String(r.createdAt || '').replace('T', ' ').slice(0, 16);
478
+ console.log('');
479
+ console.log(`r${String(r.revision).padEnd(4)} ${date} ${String(r.kind).padEnd(8)} ${authorLabel(r.author)}`);
480
+ console.log(` ${String(r.reason || '')}`);
481
+ const labels = r.labels || [];
482
+ if (labels.length)
483
+ console.log(` snapshots: ${labels.join(', ')}`);
484
+ if (typeof r.revertedFrom === 'number')
485
+ console.log(` bytes of r${r.revertedFrom}`);
486
+ const changes = r.changes || [];
487
+ if (r.comparedTo === null || r.comparedTo === undefined) {
488
+ console.log(` first recorded revision (${Number(r.fileCount || 0)} files)`);
624
489
  }
625
- const diff = diffSkillPackages([], files);
626
- if (rest.includes('--check')) {
627
- console.log(`Would promote snapshot ${name} for ${skill} with expected hash ${expectedHash}.`);
628
- printSkillPackageDiff('Snapshot files', diff);
629
- return;
490
+ else if (!changes.length) {
491
+ console.log(` no file changes vs r${String(r.comparedTo)}`);
630
492
  }
631
- await assertCurrentMirrorHash({ skill, expectedHash, localFiles: files, args: rest });
632
- const data = await applySkillPackage({ skill, files, expectedHash, args: rest });
633
- console.log(`Promoted snapshot ${name} for ${skill}: revision ${Number(data.revision || 0)} (${String(data.contentHash || (0, skill_mirror_client_1.computeMirrorHash)(files))})`);
493
+ else {
494
+ const files = changes.map(c => `${String(c.status)} ${String(c.path)}`).join(', ');
495
+ console.log(` +${Number(r.added || 0)} -${Number(r.removed || 0)} vs r${String(r.comparedTo)}: ${files}`);
496
+ }
497
+ }
498
+ }
499
+ async function runSkillDiff(args) {
500
+ const [skill, revA, revB] = positionals(args);
501
+ if (!skill || !revA)
502
+ throw new Error('Usage: greprag skill diff <name> <revA> [<revB>] [--skill-id <id>]');
503
+ const from = revisionArg(revA, 'revA');
504
+ const to = revB === undefined ? undefined : revisionArg(revB, 'revB');
505
+ const qs = mirrorLookupQuery(args);
506
+ const params = `${qs ? `${qs}&` : '?'}from=${from}${to === undefined ? '' : `&to=${to}`}`;
507
+ const data = await mirrorApiJson('GET', `/v1/skillmirror/${encodeURIComponent(skill)}/diff${params}`);
508
+ const files = data.files || [];
509
+ console.log(`${skill}: r${String(data.from)} → r${String(data.to)}`);
510
+ if (!files.length) {
511
+ console.log('No differences.');
634
512
  return;
635
513
  }
636
- throw new Error('Usage: greprag skill mirror snapshot create|list|show|restore|promote <skill> [--name <snapshot>]');
514
+ for (const file of files) {
515
+ console.log('');
516
+ console.log(String(file.patch || `${String(file.status)} ${String(file.path)}`));
517
+ }
637
518
  }
638
519
  async function runSkill(args) {
639
520
  const sub = args[0];
@@ -644,7 +525,13 @@ async function runSkill(args) {
644
525
  const bundled = listBundled();
645
526
  switch (sub) {
646
527
  case 'edit':
647
- await runSkillEdit(args.slice(1));
528
+ case 'revert':
529
+ refuseMirrorWrite(sub, args[1]);
530
+ case 'history':
531
+ await runSkillHistory(args.slice(1));
532
+ return;
533
+ case 'diff':
534
+ await runSkillDiff(args.slice(1));
648
535
  return;
649
536
  case 'mirror': {
650
537
  const mirrorVerb = args[1] || 'sync';
@@ -653,139 +540,40 @@ async function runSkill(args) {
653
540
  return;
654
541
  }
655
542
  if (mirrorVerb === 'apply') {
656
- await runSkillMirrorApply(args.slice(2));
657
- return;
543
+ const skill = getArg(args, '--skill') || (args[2] ? path.basename(path.resolve(args[2])) : undefined);
544
+ refuseMirrorWrite('mirror apply', skill);
658
545
  }
659
546
  if (mirrorVerb === 'snapshot') {
660
547
  await runSkillMirrorSnapshot(args.slice(2));
661
548
  return;
662
549
  }
663
- if (mirrorVerb !== 'sync') {
664
- console.error('Usage: greprag skill mirror sync|export|apply|snapshot ...');
665
- process.exit(1);
666
- }
667
- const dryRun = args.includes('--dry-run');
668
- const claudeLifecycleCutover = args.includes('--cutover-lifecycle');
669
- // `--only a,b` names the mirrored skills to materialize as claude-code
670
- // adapters — the one command that closes an adapter gap the catalog and
671
- // the SessionStart announce report, without arming a fleet-wide sweep.
672
- const onlyNames = readOnlyNames(args.slice(2));
673
- const positional = args.slice(2).filter((arg, index, all) => !arg.startsWith('-') && all[index - 1] !== '--only');
674
- const requested = positional[0] || 'all';
675
- const platforms = requested === 'all'
676
- ? [...native_skill_mirror_1.NATIVE_SKILL_ADAPTER_PLATFORMS]
677
- : ['claude-code', 'codex', 'opencode'].includes(requested)
678
- ? [requested]
679
- : [];
680
- if (platforms.length === 0) {
681
- console.error('Harness must be claude-code, codex, opencode, or all.');
682
- process.exit(1);
683
- }
684
- if (claudeLifecycleCutover && requested !== 'claude-code') {
685
- throw new Error('--cutover-lifecycle requires harness claude-code.');
686
- }
687
- if (onlyNames.length > 0 && requested !== 'claude-code') {
688
- throw new Error('--only requires harness claude-code.');
689
- }
690
- const claudeTargeted = claudeLifecycleCutover || onlyNames.length > 0;
691
- const apiUrl = process.env.GREPRAG_API_URL || 'https://api.greprag.com';
692
- const anchor = (0, project_anchor_1.readAnchor)(process.cwd());
693
- if (requested === 'all') {
694
- console.log('claude-code: native skill adapter sync disabled until Claude cutover approval.');
695
- }
696
- for (const platform of platforms) {
697
- if (platform === 'claude-code' && claudeTargeted) {
698
- const names = onlyNames.length > 0 ? onlyNames : [...claude_lifecycle_cutover_1.CLAUDE_LIFECYCLE_SKILLS];
699
- if (dryRun) {
700
- console.log(`claude-code: targeted cutover would manage only ${names.join(', ')}; companion files remain untouched.`);
701
- continue;
702
- }
703
- const apiKey = process.env.GREPRAG_API_KEY || '';
704
- if (!apiKey)
705
- throw new Error('GREPRAG_API_KEY not set — run `greprag init` first.');
706
- const result = await (0, claude_lifecycle_cutover_1.syncClaudeLifecycleCutover)({ apiUrl, apiKey, names });
707
- const active = result.installed + result.updated + result.current;
708
- console.log(`claude-code: ${active} adapter(s) active (${names.join(', ')}); all other Claude skills remain unmanaged.`);
709
- if (result.backupDir)
710
- console.log(`claude-code: replaced prompts backed up at ${result.backupDir}`);
711
- continue;
712
- }
713
- if (!(0, native_skill_mirror_1.nativeSkillMirrorEnabled)(platform)) {
714
- console.log(`${platform}: native skill adapter sync disabled until Claude cutover approval.`);
715
- continue;
716
- }
717
- if (dryRun) {
718
- printAdoptionDryRun(platform, {
719
- plan: (0, skill_mirror_client_1.dryRunNativeSkillAdoption)({
720
- platform,
721
- projectId: anchor.projectId,
722
- projectName: anchor.projectName,
723
- projectDir: process.cwd(),
724
- }),
725
- adopted: 0,
726
- blocked: 0,
727
- skipped: 0,
728
- });
729
- continue;
730
- }
731
- const apiKey = process.env.GREPRAG_API_KEY || '';
732
- if (!apiKey)
733
- throw new Error('GREPRAG_API_KEY not set — run `greprag init` first.');
734
- const adoption = await (0, skill_mirror_client_1.syncNativeSkillAdoptions)({
735
- platform, apiUrl, apiKey,
736
- projectId: anchor.projectId,
737
- projectName: anchor.projectName,
738
- projectDir: process.cwd(),
739
- });
740
- printAdoptionSync(platform, adoption);
741
- const result = await (0, native_skill_mirror_1.syncNativeSkillMirror)({
742
- platform, apiUrl, apiKey,
743
- projectId: anchor.projectId,
744
- projectDir: process.cwd(),
745
- });
746
- console.log(`${platform}: ${(0, native_skill_mirror_1.nativeSkillMirrorSummary)(result)}`);
747
- }
748
- if (!dryRun)
749
- 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));
750
553
  return;
751
554
  }
752
555
  case 'source': {
753
- if (args[1] !== 'stamp') {
754
- console.error('Usage: greprag skill source stamp <name> --project <p> --path <rel> [--path <rel>...] [--root <dir>] [--baseline <ref>]');
755
- process.exit(1);
756
- }
556
+ const usage = 'Usage: greprag skill source stamp <name> --project <p> --path <rel> [--path <rel>...] [--root <dir>] [--baseline <ref>]';
757
557
  const name = args[2];
758
558
  const rest = args.slice(3);
759
559
  const project = getArg(rest, '--project');
760
- const paths = getMany(rest, '--path');
761
- if (!name || !project || paths.length === 0) {
762
- console.error('Usage: greprag skill source stamp <name> --project <p> --path <rel> [--path <rel>...] [--root <dir>] [--baseline <ref>]');
763
- process.exit(1);
764
- }
765
- const homeDir = process.env.HOME || process.env.USERPROFILE || '';
766
- 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);
767
564
  if (!skillDir)
768
- 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)}.`);
769
566
  const root = getArg(rest, '--root') || (0, skill_staleness_1.resolveSourceRoot)(project, process.cwd());
770
567
  if (!root)
771
568
  throw new Error(`No local Git checkout found for source project ${project}.`);
772
569
  const skillPath = path.join(skillDir, 'SKILL.md');
773
- const before = fs.readFileSync(skillPath, 'utf-8');
774
570
  const stamped = (0, skill_staleness_1.stampSourceBinding)({
775
- skillMd: before, project, root, paths,
571
+ skillMd: fs.readFileSync(skillPath, 'utf-8'), project, root, paths,
776
572
  baselineCommit: getArg(rest, '--baseline'),
777
573
  });
778
574
  fs.writeFileSync(skillPath, stamped.content.endsWith('\n') ? stamped.content : stamped.content + '\n');
779
- console.log(`Stamped ${name}: ${project} @ ${stamped.binding.baselineCommit.slice(0, 8)} (${stamped.binding.paths.length} paths)`);
780
- const apiKey = process.env.GREPRAG_API_KEY || '';
781
- if (apiKey) {
782
- const apiUrl = process.env.GREPRAG_API_URL || 'https://api.greprag.com';
783
- const outcome = await (0, skill_mirror_client_1.maybeMirrorSkill)({ skill: name, cwd: process.cwd(), apiUrl, apiKey });
784
- console.log(`Mirror: ${outcome}`);
785
- }
786
- else {
787
- console.log('Mirror: skipped (GREPRAG_API_KEY not set; next skill use will retry).');
788
- }
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`.');
789
577
  return;
790
578
  }
791
579
  case 'list': {