greprag 5.83.1 → 5.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/app-model.js +1 -2
- package/dist/commands/collision-check.js +30 -16
- package/dist/commands/delivery-reminder.js +1 -1
- package/dist/commands/discord.js +12 -61
- package/dist/commands/inbox-primer-reminder.js +1 -1
- package/dist/commands/inbox-watch.js +0 -13
- package/dist/commands/load.js +4 -2
- package/dist/commands/merge-guard.js +1 -1
- package/dist/commands/merge-lock.js +31 -5
- package/dist/commands/reminder-registry.js +1 -8
- package/dist/commands/session.js +2 -2
- package/dist/commands/skill-mirror-reminder.js +0 -1
- package/dist/commands/skill.js +241 -24
- package/dist/deploy-locks.js +48 -0
- package/dist/hook.js +14 -16
- package/dist/index.js +14 -147
- package/dist/native-skill-mirror.js +9 -6
- package/dist/opencode-plugin-helpers.js +1 -1
- package/dist/opencode-plugin.bundle.js +11 -9
- package/dist/reminder-injector.js +1 -1
- package/dist/skill-mirror-client.js +29 -0
- package/dist/skill-staleness.js +2 -2
- package/package.json +1 -1
- package/skill/commander/SKILL.md +10 -12
- package/skill/greprag/SKILL.md +3 -3
- package/skill/greprag/docs/discord-handoff.md +6 -30
- package/skill/greprag/docs/inbox-watch.md +1 -1
- package/skill/greprag/docs/inbox.md +2 -2
- package/skill/mechanic/docs/skill-fix-conventions.md +1 -1
- package/skill/templates/chip-spawn.md +6 -2
- package/skill/templates/delivery.md +18 -1
- package/skill/templates/skill-change.md +3 -1
- package/dist/commands/sms.js +0 -215
package/dist/commands/skill.js
CHANGED
|
@@ -83,21 +83,30 @@ USAGE
|
|
|
83
83
|
greprag skill list Show bundled skills and install status
|
|
84
84
|
greprag skill install <name> Install a bundled advisor skill
|
|
85
85
|
greprag skill uninstall <name> Remove a previously-installed advisor
|
|
86
|
-
greprag skill edit <name> [--skill-id <id>] [--scope project --project-id <id>]
|
|
86
|
+
greprag skill edit <name> --reason "<why>" [--skill-id <id>] [--scope project --project-id <id>]
|
|
87
87
|
Export/edit/apply helper; opens $EDITOR unless --no-open or Codex
|
|
88
|
+
greprag skill history <name> [--skill-id <id>] [--limit <n>]
|
|
89
|
+
Every canon revision: date, author, reason, changes
|
|
90
|
+
greprag skill diff <name> <revA> [<revB>]
|
|
91
|
+
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
|
|
88
94
|
greprag skill mirror sync [harness] Install native GrepRAG skill adapters
|
|
89
95
|
claude-code --cutover-lifecycle Cut over commit/deploy/release only
|
|
90
96
|
claude-code --only <a,b> Install adapters for named mirrored skills
|
|
91
97
|
(closes the gaps greprag load marks with *)
|
|
92
98
|
greprag skill mirror export <name> --to <dir>
|
|
93
99
|
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
|
|
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
|
|
96
105
|
greprag skill mirror snapshot create <name> --name <snapshot>
|
|
97
106
|
greprag skill mirror snapshot list <name>
|
|
98
107
|
greprag skill mirror snapshot show <name> --name <snapshot>
|
|
99
108
|
greprag skill mirror snapshot restore <name> --name <snapshot> --to <dir>
|
|
100
|
-
greprag skill mirror snapshot promote <name> --name <snapshot> --expected-hash <hash>
|
|
109
|
+
greprag skill mirror snapshot promote <name> --name <snapshot> --expected-hash <hash> --reason "<why>"
|
|
101
110
|
Promote snapshot bytes as the newest revision
|
|
102
111
|
Named package rollback points
|
|
103
112
|
greprag skill source stamp <name> --project <p> --path <rel> [--path <rel>...]
|
|
@@ -114,6 +123,9 @@ NOTES
|
|
|
114
123
|
• Mirrored skill edits use export/apply semantics. \`skill edit\` only wraps
|
|
115
124
|
that flow: export current bytes, open an editor, show a diff summary, then
|
|
116
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\`.
|
|
117
129
|
• Codex agents must use a non-interactive path: \`skill mirror export\`, patch
|
|
118
130
|
files, then \`skill mirror apply\`. In Codex, \`skill edit\` skips the editor
|
|
119
131
|
and prints the guarded apply command; \`--no-open\` does the same explicitly.
|
|
@@ -368,10 +380,50 @@ function writeSkillPackage(targetDir, files, opts) {
|
|
|
368
380
|
for (const file of files) {
|
|
369
381
|
const destination = safePackagePath(target, file.path);
|
|
370
382
|
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
371
|
-
fs.writeFileSync(destination, file.content
|
|
383
|
+
fs.writeFileSync(destination, file.content, 'utf8');
|
|
372
384
|
}
|
|
373
385
|
return true;
|
|
374
386
|
}
|
|
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
|
+
/** Read the folder back and fail unless it holds exactly the files the mirror
|
|
417
|
+
* 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. */
|
|
419
|
+
function verifyWrittenPackage(targetDir, files) {
|
|
420
|
+
const written = (0, skill_mirror_client_1.collectSkillFiles)(path.resolve(targetDir)) || [];
|
|
421
|
+
const diff = diffSkillPackages(files, written);
|
|
422
|
+
if (!diff.length)
|
|
423
|
+
return;
|
|
424
|
+
const detail = diff.map(item => ` ${item.status === 'removed' ? 'missing' : item.status} ${item.path}`).join('\n');
|
|
425
|
+
throw new Error(`export did not write the revision it fetched to ${path.resolve(targetDir)}:\n${detail}`);
|
|
426
|
+
}
|
|
375
427
|
function printSkillMirrorConflict(params) {
|
|
376
428
|
const local = new Map(params.localFiles.map(file => [file.path, file.content]));
|
|
377
429
|
const remote = new Map(params.remoteFiles.map(file => [file.path, file.content]));
|
|
@@ -394,14 +446,12 @@ async function runSkillMirrorExport(args) {
|
|
|
394
446
|
const to = getArg(args, '--to');
|
|
395
447
|
if (!name || !to)
|
|
396
448
|
throw new Error('Usage: greprag skill mirror export <name> --to <dir> [--force] [--check]');
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
.filter((file) => typeof file.path === 'string' && typeof file.content === 'string');
|
|
400
|
-
if (!files.length)
|
|
401
|
-
throw new Error(`No mirrored files returned for ${name}.`);
|
|
402
|
-
if (!writeSkillPackage(to, files, { force: args.includes('--force'), check: args.includes('--check'), exact: true }))
|
|
449
|
+
const loaded = await loadMirrorPackage(name, args);
|
|
450
|
+
if (!writeSkillPackage(to, loaded.files, { force: args.includes('--force'), check: args.includes('--check'), exact: true }))
|
|
403
451
|
return;
|
|
404
|
-
|
|
452
|
+
verifyWrittenPackage(to, loaded.files);
|
|
453
|
+
writeMirrorBase(to, { skill: name, ...loaded });
|
|
454
|
+
console.log(`Exported ${name} revision ${loaded.revision} (${loaded.contentHash})`);
|
|
405
455
|
}
|
|
406
456
|
async function loadMirrorPackage(skill, args) {
|
|
407
457
|
const data = await mirrorApiJson('GET', `/v1/skillmirror/${encodeURIComponent(skill)}${mirrorLookupQuery(args)}`);
|
|
@@ -441,6 +491,16 @@ function refuseBundledSkillWrite(skill) {
|
|
|
441
491
|
return;
|
|
442
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.`);
|
|
443
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
|
+
}
|
|
444
504
|
async function applySkillPackage(params) {
|
|
445
505
|
refuseBundledSkillWrite(params.skill);
|
|
446
506
|
const hash = (0, skill_mirror_client_1.computeMirrorHash)(params.files);
|
|
@@ -448,6 +508,8 @@ async function applySkillPackage(params) {
|
|
|
448
508
|
files: params.files,
|
|
449
509
|
contentHash: hash,
|
|
450
510
|
...mirrorBodyScope(params.args),
|
|
511
|
+
reason: params.reason,
|
|
512
|
+
author: (0, skill_mirror_client_1.skillRevisionAuthor)(params.via),
|
|
451
513
|
expectedHash: params.expectedHash,
|
|
452
514
|
...(typeof params.expectedRevision === 'number' ? { expectedRevision: params.expectedRevision } : {}),
|
|
453
515
|
};
|
|
@@ -477,11 +539,16 @@ async function assertCurrentMirrorHash(params) {
|
|
|
477
539
|
}
|
|
478
540
|
async function runSkillEdit(args) {
|
|
479
541
|
const skill = args[0];
|
|
542
|
+
const usage = 'greprag skill edit <name> --reason "<why>" [--dir <dir>] [--no-open] [--force] [--keep]';
|
|
480
543
|
if (!skill)
|
|
481
|
-
throw new Error(
|
|
544
|
+
throw new Error(`Usage: ${usage}`);
|
|
482
545
|
refuseBundledSkillWrite(skill);
|
|
483
546
|
const explicitDir = getArg(args, '--dir') || getArg(args, '--to');
|
|
484
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);
|
|
485
552
|
const keep = args.includes('--keep') || Boolean(explicitDir);
|
|
486
553
|
const workDir = explicitDir
|
|
487
554
|
? path.resolve(explicitDir)
|
|
@@ -491,15 +558,16 @@ async function runSkillEdit(args) {
|
|
|
491
558
|
force: args.includes('--force') || !explicitDir,
|
|
492
559
|
exact: true,
|
|
493
560
|
});
|
|
561
|
+
verifyWrittenPackage(workDir, loaded.files);
|
|
562
|
+
writeMirrorBase(workDir, { skill, ...loaded });
|
|
494
563
|
console.log(`Editing ${skill} revision ${loaded.revision} (${loaded.contentHash})`);
|
|
495
564
|
console.log(`Work dir: ${workDir}`);
|
|
496
|
-
const skipEditor = noOpen || codexHarnessDetected();
|
|
497
565
|
if (skipEditor) {
|
|
498
566
|
console.log(noOpen
|
|
499
567
|
? 'Editor skipped.'
|
|
500
568
|
: 'Codex harness detected; editor skipped to avoid launching an interactive editor.');
|
|
501
569
|
const forward = forwardedMirrorArgs(args);
|
|
502
|
-
console.log(`Apply after editing: greprag skill mirror apply ${shellArg(workDir)} --skill ${skill}${forward ? ` ${forward}` : ''} --expected-hash ${loaded.contentHash}`);
|
|
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>"'}`);
|
|
503
571
|
return;
|
|
504
572
|
}
|
|
505
573
|
runEditor(path.join(workDir, 'SKILL.md'));
|
|
@@ -512,7 +580,7 @@ async function runSkillEdit(args) {
|
|
|
512
580
|
return;
|
|
513
581
|
}
|
|
514
582
|
await assertCurrentMirrorHash({ skill, expectedHash: loaded.contentHash, localFiles: edited, args });
|
|
515
|
-
const data = await applySkillPackage({ skill, files: edited, expectedHash: loaded.contentHash, args });
|
|
583
|
+
const data = await applySkillPackage({ skill, files: edited, expectedHash: loaded.contentHash, reason, via: 'skill-edit', args });
|
|
516
584
|
console.log(`Applied ${skill}: revision ${Number(data.revision || 0)} (${String(data.contentHash || (0, skill_mirror_client_1.computeMirrorHash)(edited))})`);
|
|
517
585
|
if (!keep)
|
|
518
586
|
fs.rmSync(workDir, { recursive: true, force: true });
|
|
@@ -520,20 +588,38 @@ async function runSkillEdit(args) {
|
|
|
520
588
|
async function runSkillMirrorApply(args) {
|
|
521
589
|
const dir = args[0];
|
|
522
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>"';
|
|
523
592
|
if (!dir || !skill)
|
|
524
|
-
throw new Error(
|
|
593
|
+
throw new Error(`Usage: ${usage}`);
|
|
594
|
+
refuseBundledSkillWrite(skill);
|
|
595
|
+
const reason = requireReason(args, usage);
|
|
525
596
|
const expectedRevisionRaw = getArg(args, '--expected-revision') || getArg(args, '--expectedRevision');
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
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.`);
|
|
529
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;
|
|
530
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
|
+
}
|
|
531
615
|
await assertCurrentMirrorHash({ skill, expectedHash, localFiles: files, args });
|
|
532
616
|
const data = await applySkillPackage({
|
|
533
617
|
skill,
|
|
534
618
|
files,
|
|
535
619
|
expectedHash,
|
|
536
620
|
expectedRevision: expectedRevisionRaw ? Number(expectedRevisionRaw) : undefined,
|
|
621
|
+
reason,
|
|
622
|
+
via: 'mirror-apply',
|
|
537
623
|
args,
|
|
538
624
|
});
|
|
539
625
|
console.log(`Applied ${skill}: revision ${Number(data.revision || 0)} (${String(data.contentHash || (0, skill_mirror_client_1.computeMirrorHash)(files))})`);
|
|
@@ -590,6 +676,8 @@ async function runSkillMirrorSnapshot(args) {
|
|
|
590
676
|
console.log(`Snapshot ${String(snap.snapshotName || name)} stored for ${skill}: revision ${Number(snap.revision || 0)}`);
|
|
591
677
|
return;
|
|
592
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) : '';
|
|
593
681
|
const data = await mirrorApiJson('GET', `/v1/skillmirror/${encodeURIComponent(skill)}/snapshots/${encodeURIComponent(name)}${mirrorLookupQuery(rest)}`);
|
|
594
682
|
const snapshot = data.snapshot;
|
|
595
683
|
if (verb === 'show') {
|
|
@@ -610,13 +698,13 @@ async function runSkillMirrorSnapshot(args) {
|
|
|
610
698
|
const files = snapshotPresentFiles(snapshot);
|
|
611
699
|
if (!writeSkillPackage(to, files, { force: rest.includes('--force'), check: rest.includes('--check'), exact: true }))
|
|
612
700
|
return;
|
|
613
|
-
console.log(`Restored snapshot ${name} for ${skill} to ${path.resolve(to)}.
|
|
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>".`);
|
|
614
702
|
return;
|
|
615
703
|
}
|
|
616
704
|
if (verb === 'promote') {
|
|
617
705
|
const expectedHash = getArg(rest, '--expected-hash') || getArg(rest, '--expectedHash');
|
|
618
706
|
if (!expectedHash) {
|
|
619
|
-
throw new Error(
|
|
707
|
+
throw new Error(`Usage: ${promoteUsage}`);
|
|
620
708
|
}
|
|
621
709
|
const files = snapshotPresentFiles(snapshot);
|
|
622
710
|
if (!files.some(file => file.path === 'SKILL.md')) {
|
|
@@ -629,12 +717,129 @@ async function runSkillMirrorSnapshot(args) {
|
|
|
629
717
|
return;
|
|
630
718
|
}
|
|
631
719
|
await assertCurrentMirrorHash({ skill, expectedHash, localFiles: files, args: rest });
|
|
632
|
-
const data = await applySkillPackage({
|
|
720
|
+
const data = await applySkillPackage({
|
|
721
|
+
skill, files, expectedHash, reason: `promote snapshot "${name}": ${promoteReason}`, via: 'snapshot-promote', args: rest,
|
|
722
|
+
});
|
|
633
723
|
console.log(`Promoted snapshot ${name} for ${skill}: revision ${Number(data.revision || 0)} (${String(data.contentHash || (0, skill_mirror_client_1.computeMirrorHash)(files))})`);
|
|
634
724
|
return;
|
|
635
725
|
}
|
|
636
726
|
throw new Error('Usage: greprag skill mirror snapshot create|list|show|restore|promote <skill> [--name <snapshot>]');
|
|
637
727
|
}
|
|
728
|
+
// ---------- Revision history: history / diff / revert ------------------------
|
|
729
|
+
// adr: adr/skill-revision-history.md
|
|
730
|
+
function revisionArg(raw, label) {
|
|
731
|
+
const value = Number(String(raw || '').replace(/^r/i, ''));
|
|
732
|
+
if (!raw || !Number.isInteger(value) || value < 0)
|
|
733
|
+
throw new Error(`${label} must be a revision number such as 12 or r12`);
|
|
734
|
+
return value;
|
|
735
|
+
}
|
|
736
|
+
function positionals(args) {
|
|
737
|
+
const out = [];
|
|
738
|
+
for (let i = 0; i < args.length; i++) {
|
|
739
|
+
if (args[i].startsWith('--')) {
|
|
740
|
+
if (args[i + 1] && !args[i + 1].startsWith('--'))
|
|
741
|
+
i++;
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
out.push(args[i]);
|
|
745
|
+
}
|
|
746
|
+
return out;
|
|
747
|
+
}
|
|
748
|
+
function authorLabel(author) {
|
|
749
|
+
if (!author)
|
|
750
|
+
return '';
|
|
751
|
+
const parts = [
|
|
752
|
+
typeof author.harness === 'string' ? author.harness : '',
|
|
753
|
+
typeof author.sessionId === 'string' ? author.sessionId.slice(0, 8) : '',
|
|
754
|
+
typeof author.chip === 'string' ? author.chip : '',
|
|
755
|
+
].filter(Boolean);
|
|
756
|
+
const via = typeof author.via === 'string' ? author.via : '';
|
|
757
|
+
return [via, parts.join(' ')].filter(Boolean).join(' · ');
|
|
758
|
+
}
|
|
759
|
+
async function runSkillHistory(args) {
|
|
760
|
+
const skill = args[0];
|
|
761
|
+
if (!skill)
|
|
762
|
+
throw new Error('Usage: greprag skill history <name> [--skill-id <id>] [--limit <n>]');
|
|
763
|
+
const limit = getArg(args, '--limit');
|
|
764
|
+
const qs = mirrorLookupQuery(args);
|
|
765
|
+
const sep = qs ? '&' : '?';
|
|
766
|
+
const data = await mirrorApiJson('GET', `/v1/skillmirror/${encodeURIComponent(skill)}/history${qs}${limit ? `${sep}limit=${encodeURIComponent(limit)}` : ''}`);
|
|
767
|
+
const revisions = data.revisions || [];
|
|
768
|
+
if (!revisions.length) {
|
|
769
|
+
console.log(`No recorded revisions for ${skill}.`);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
console.log(`${skill} (${String(data.skillId || '')}) — newest first`);
|
|
773
|
+
console.log(`current: r${String(revisions[0].revision)} ${String(revisions[0].contentHash || '')}`);
|
|
774
|
+
for (const r of revisions) {
|
|
775
|
+
const date = String(r.createdAt || '').replace('T', ' ').slice(0, 16);
|
|
776
|
+
console.log('');
|
|
777
|
+
console.log(`r${String(r.revision).padEnd(4)} ${date} ${String(r.kind).padEnd(8)} ${authorLabel(r.author)}`);
|
|
778
|
+
console.log(` ${String(r.reason || '')}`);
|
|
779
|
+
const labels = r.labels || [];
|
|
780
|
+
if (labels.length)
|
|
781
|
+
console.log(` snapshots: ${labels.join(', ')}`);
|
|
782
|
+
if (typeof r.revertedFrom === 'number')
|
|
783
|
+
console.log(` bytes of r${r.revertedFrom}`);
|
|
784
|
+
const changes = r.changes || [];
|
|
785
|
+
if (r.comparedTo === null || r.comparedTo === undefined) {
|
|
786
|
+
console.log(` first recorded revision (${Number(r.fileCount || 0)} files)`);
|
|
787
|
+
}
|
|
788
|
+
else if (!changes.length) {
|
|
789
|
+
console.log(` no file changes vs r${String(r.comparedTo)}`);
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
const files = changes.map(c => `${String(c.status)} ${String(c.path)}`).join(', ');
|
|
793
|
+
console.log(` +${Number(r.added || 0)} -${Number(r.removed || 0)} vs r${String(r.comparedTo)}: ${files}`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
async function runSkillDiff(args) {
|
|
798
|
+
const [skill, revA, revB] = positionals(args);
|
|
799
|
+
if (!skill || !revA)
|
|
800
|
+
throw new Error('Usage: greprag skill diff <name> <revA> [<revB>] [--skill-id <id>]');
|
|
801
|
+
const from = revisionArg(revA, 'revA');
|
|
802
|
+
const to = revB === undefined ? undefined : revisionArg(revB, 'revB');
|
|
803
|
+
const qs = mirrorLookupQuery(args);
|
|
804
|
+
const params = `${qs ? `${qs}&` : '?'}from=${from}${to === undefined ? '' : `&to=${to}`}`;
|
|
805
|
+
const data = await mirrorApiJson('GET', `/v1/skillmirror/${encodeURIComponent(skill)}/diff${params}`);
|
|
806
|
+
const files = data.files || [];
|
|
807
|
+
console.log(`${skill}: r${String(data.from)} → r${String(data.to)}`);
|
|
808
|
+
if (!files.length) {
|
|
809
|
+
console.log('No differences.');
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
for (const file of files) {
|
|
813
|
+
console.log('');
|
|
814
|
+
console.log(String(file.patch || `${String(file.status)} ${String(file.path)}`));
|
|
815
|
+
}
|
|
816
|
+
}
|
|
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
|
+
}
|
|
638
843
|
async function runSkill(args) {
|
|
639
844
|
const sub = args[0];
|
|
640
845
|
if (!sub || sub === '--help' || sub === '-h' || sub === 'help') {
|
|
@@ -646,6 +851,15 @@ async function runSkill(args) {
|
|
|
646
851
|
case 'edit':
|
|
647
852
|
await runSkillEdit(args.slice(1));
|
|
648
853
|
return;
|
|
854
|
+
case 'history':
|
|
855
|
+
await runSkillHistory(args.slice(1));
|
|
856
|
+
return;
|
|
857
|
+
case 'diff':
|
|
858
|
+
await runSkillDiff(args.slice(1));
|
|
859
|
+
return;
|
|
860
|
+
case 'revert':
|
|
861
|
+
await runSkillRevert(args.slice(1));
|
|
862
|
+
return;
|
|
649
863
|
case 'mirror': {
|
|
650
864
|
const mirrorVerb = args[1] || 'sync';
|
|
651
865
|
if (mirrorVerb === 'export') {
|
|
@@ -780,7 +994,10 @@ async function runSkill(args) {
|
|
|
780
994
|
const apiKey = process.env.GREPRAG_API_KEY || '';
|
|
781
995
|
if (apiKey) {
|
|
782
996
|
const apiUrl = process.env.GREPRAG_API_URL || 'https://api.greprag.com';
|
|
783
|
-
const outcome = await (0, skill_mirror_client_1.maybeMirrorSkill)({
|
|
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
|
+
});
|
|
784
1001
|
console.log(`Mirror: ${outcome}`);
|
|
785
1002
|
}
|
|
786
1003
|
else {
|
package/dist/deploy-locks.js
CHANGED
|
@@ -59,6 +59,9 @@ exports.releaseLock = releaseLock;
|
|
|
59
59
|
exports.landedClaimsPath = landedClaimsPath;
|
|
60
60
|
exports.isAncestorOfHead = isAncestorOfHead;
|
|
61
61
|
exports.commitExists = commitExists;
|
|
62
|
+
exports.integrationRef = integrationRef;
|
|
63
|
+
exports.refTip = refTip;
|
|
64
|
+
exports.landedVerdict = landedVerdict;
|
|
62
65
|
exports.readLandedClaims = readLandedClaims;
|
|
63
66
|
exports.openClaims = openClaims;
|
|
64
67
|
exports.recordLandedClaim = recordLandedClaim;
|
|
@@ -66,6 +69,7 @@ exports.dropLandedClaim = dropLandedClaim;
|
|
|
66
69
|
exports.readMergeState = readMergeState;
|
|
67
70
|
const fs = __importStar(require("fs"));
|
|
68
71
|
const path = __importStar(require("path"));
|
|
72
|
+
const delivery_config_1 = require("./delivery-config");
|
|
69
73
|
const proc_1 = require("./proc");
|
|
70
74
|
const session_id_1 = require("./session-id");
|
|
71
75
|
/** The basename already on disk in the repos that have a deploy lock today. The
|
|
@@ -210,6 +214,7 @@ function readMergeLock(cwd) {
|
|
|
210
214
|
function acquireLock(file, label, staleMs = exports.DEPLOY_STALE_MS, options = {}) {
|
|
211
215
|
const session = lockSession();
|
|
212
216
|
const record = {
|
|
217
|
+
...(options.base ? { base: options.base } : {}),
|
|
213
218
|
// A process-held lock must record the process that is actually DEPLOYING, which for
|
|
214
219
|
// a script that shells out to this CLI is the caller, not this short-lived command.
|
|
215
220
|
// Recording our own pid there would make the lock read as stale the moment it exists.
|
|
@@ -267,6 +272,49 @@ function commitExists(cwd, sha) {
|
|
|
267
272
|
});
|
|
268
273
|
return result.status === 0;
|
|
269
274
|
}
|
|
275
|
+
/** The branch merges land on: the profile's default branch, else this checkout's HEAD.
|
|
276
|
+
* A ref, not HEAD, because the lock is shared across worktrees and a linked worktree's
|
|
277
|
+
* HEAD is the very branch being merged. */
|
|
278
|
+
function integrationRef(cwd) {
|
|
279
|
+
const branch = (0, delivery_config_1.resolveDeliveryProfile)(cwd).profile?.git?.defaultBranch;
|
|
280
|
+
if (branch) {
|
|
281
|
+
const ref = `refs/heads/${branch}`;
|
|
282
|
+
const probe = (0, proc_1.safeSpawnSync)('git', ['rev-parse', '--verify', '--quiet', ref], {
|
|
283
|
+
encoding: 'utf8', cwd, stdio: ['ignore', 'pipe', 'pipe'],
|
|
284
|
+
});
|
|
285
|
+
if (probe.status === 0)
|
|
286
|
+
return ref;
|
|
287
|
+
}
|
|
288
|
+
return 'HEAD';
|
|
289
|
+
}
|
|
290
|
+
function refTip(cwd, ref) {
|
|
291
|
+
const result = (0, proc_1.safeSpawnSync)('git', ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], {
|
|
292
|
+
encoding: 'utf8', cwd, stdio: ['ignore', 'pipe', 'pipe'],
|
|
293
|
+
});
|
|
294
|
+
return result.status === 0 ? String(result.stdout).trim() || null : null;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Is `sha` a merge that landed under this lock?
|
|
298
|
+
*
|
|
299
|
+
* Release used to take any commit on trust, and the commit agents passed was
|
|
300
|
+
* `$(git rev-parse HEAD)`, which is on HEAD by definition. On 2026-10-06 in C:/sift a
|
|
301
|
+
* fast-forward was refused, a pipe to `tail` hid the refusal, and release recorded the
|
|
302
|
+
* peer commit already at the tip as this session's landed work; the deploy then shipped
|
|
303
|
+
* master without the branch. The claim must be checked against what the merge changed:
|
|
304
|
+
* it has to be on the integration branch now, and it cannot be the tip that was there
|
|
305
|
+
* before the lock was taken.
|
|
306
|
+
*/
|
|
307
|
+
function landedVerdict(cwd, sha, base) {
|
|
308
|
+
const ref = integrationRef(cwd);
|
|
309
|
+
const ancestor = (0, proc_1.safeSpawnSync)('git', ['merge-base', '--is-ancestor', sha, ref], {
|
|
310
|
+
encoding: 'utf8', cwd, stdio: ['ignore', 'pipe', 'pipe'],
|
|
311
|
+
});
|
|
312
|
+
if (ancestor.status !== 0)
|
|
313
|
+
return 'not-landed';
|
|
314
|
+
if (base && sha === base)
|
|
315
|
+
return 'nothing-new';
|
|
316
|
+
return 'landed';
|
|
317
|
+
}
|
|
270
318
|
function readLandedClaims(cwd) {
|
|
271
319
|
try {
|
|
272
320
|
const raw = JSON.parse(fs.readFileSync(landedClaimsPath(cwd), 'utf8'));
|
package/dist/hook.js
CHANGED
|
@@ -115,9 +115,9 @@ const merge_guard_1 = require("./commands/merge-guard");
|
|
|
115
115
|
// injects the pointer so the post-compact model can Read the verbatim record.
|
|
116
116
|
// All real logic lives in ./commands/archive. adr: adr/governed-compact.md
|
|
117
117
|
const archive_1 = require("./commands/archive");
|
|
118
|
-
// Collision schematic (SessionStart) — detect another
|
|
119
|
-
//
|
|
120
|
-
// logic lives in ./commands/collision-check. docs/schematic
|
|
118
|
+
// Collision schematic (SessionStart) — detect another greprag watcher session
|
|
119
|
+
// (OpenCode, Grok) in the same repo and inject a coordinate-now directive. All
|
|
120
|
+
// real logic lives in ./commands/collision-check. docs/collision-schematic.md
|
|
121
121
|
const collision_check_1 = require("./commands/collision-check");
|
|
122
122
|
// (The asyncRewake idle-wake poll was REVERTED 2026-06-23 — falsified: a detached
|
|
123
123
|
// process's exit-2 has no harness event to attach to in true deep idle, so it never
|
|
@@ -1165,6 +1165,7 @@ async function store(input, source = 'claude-code') {
|
|
|
1165
1165
|
if (mirrorSkill) {
|
|
1166
1166
|
await (0, skill_mirror_client_1.maybeMirrorSkill)({
|
|
1167
1167
|
skill: mirrorSkill, cwd, apiUrl: cfg.apiUrl, apiKey: cfg.apiKey,
|
|
1168
|
+
sessionId: input.session_id,
|
|
1168
1169
|
});
|
|
1169
1170
|
}
|
|
1170
1171
|
}
|
|
@@ -1525,7 +1526,7 @@ async function recap(input, mode = 'plain', opts = {}) {
|
|
|
1525
1526
|
return;
|
|
1526
1527
|
// Ensure the machine's desk-line is up (idempotent singleton). One per machine,
|
|
1527
1528
|
// not per session — it long-polls the cloud and answers "what's truly live?"
|
|
1528
|
-
// from local pidfile truth, so the
|
|
1529
|
+
// from local pidfile truth, so the live-session list never comes from a stale registry.
|
|
1529
1530
|
// The lock makes this a no-op on every SessionStart after the first.
|
|
1530
1531
|
// adr: adr/desk-line-relay.md
|
|
1531
1532
|
try {
|
|
@@ -2114,13 +2115,13 @@ async function armCheck(input) {
|
|
|
2114
2115
|
}) + '\n');
|
|
2115
2116
|
}
|
|
2116
2117
|
/** Collision schematic (SessionStart) — `greprag-hook collision-check`. Detects
|
|
2117
|
-
* another
|
|
2118
|
-
* injects a directive telling the agent to message
|
|
2119
|
-
* worktree. The hook NEVER sends — AI never auto-acts.
|
|
2120
|
-
* resume/compact re-fire pings each peer at most
|
|
2121
|
-
* directive logic lives in ./commands/collision-check
|
|
2122
|
-
* only the watcher registry (no inbox content) — no
|
|
2123
|
-
* Fail-quiet: any miss → silent. docs/schematic
|
|
2118
|
+
* another greprag watcher session (OpenCode, Grok) working in THIS repo via the
|
|
2119
|
+
* cloud watcher registry and injects a directive telling the agent to message
|
|
2120
|
+
* the peer + work in a worktree. The hook NEVER sends — AI never auto-acts.
|
|
2121
|
+
* Dedup is per-session so a resume/compact re-fire pings each peer at most
|
|
2122
|
+
* once. All detection/dedup/directive logic lives in ./commands/collision-check
|
|
2123
|
+
* (pure + tested). Reads only the watcher registry (no inbox content) — no
|
|
2124
|
+
* eavesdrop surface. Fail-quiet: any miss → silent. docs/collision-schematic.md */
|
|
2124
2125
|
async function collisionCheck(input) {
|
|
2125
2126
|
const cwd = input.cwd || process.cwd();
|
|
2126
2127
|
const cfg = (0, hook_runtime_1.getHookConfig)(cwd);
|
|
@@ -2353,11 +2354,8 @@ async function main() {
|
|
|
2353
2354
|
}
|
|
2354
2355
|
const harness = (0, harness_1.inferCurrentHarness)();
|
|
2355
2356
|
if (subcommand === 'recap') {
|
|
2356
|
-
// ALWAYS additionalContext
|
|
2357
|
-
//
|
|
2358
|
-
// which silently ate ~89% of an 18.7KB announce (Persona sits at byte 10,841).
|
|
2359
|
-
// additionalContext is the documented injection channel. Grok keeps its
|
|
2360
|
-
// sidecar via the platform arg. adr: adr/announce-delivery-channel.md
|
|
2357
|
+
// ALWAYS additionalContext, the documented injection channel. Grok keeps its
|
|
2358
|
+
// sidecar via the platform arg. adr: adr/announce-inline-budget.md
|
|
2361
2359
|
await recap(input, 'additionalContext', {
|
|
2362
2360
|
platform: harness === 'grok' ? 'grok' : undefined,
|
|
2363
2361
|
});
|