greprag 5.83.2 → 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 -1
- package/dist/commands/delivery-reminder.js +1 -1
- package/dist/commands/discord.js +12 -61
- package/dist/commands/inbox-watch.js +0 -13
- package/dist/commands/load.js +4 -2
- 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/hook.js +2 -1
- package/dist/index.js +14 -147
- package/dist/native-skill-mirror.js +9 -6
- package/dist/opencode-plugin.bundle.js +10 -8
- 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 +1 -1
- package/skill/greprag/docs/discord-handoff.md +6 -30
- package/skill/mechanic/docs/skill-fix-conventions.md +1 -1
- package/skill/templates/chip-spawn.md +6 -2
- package/skill/templates/delivery.md +16 -0
- package/skill/templates/skill-change.md +3 -1
- package/dist/commands/sms.js +0 -215
|
@@ -292,7 +292,7 @@ async function buildAppModel(cwd, requestedProjectId, homeDir = os.homedir()) {
|
|
|
292
292
|
group: 'Other',
|
|
293
293
|
}),
|
|
294
294
|
enabled: !disabled.has(module.id),
|
|
295
|
-
surface:
|
|
295
|
+
surface: 'Agent guidance',
|
|
296
296
|
harnesses: module.harnesses || ['claude-code', 'codex', 'opencode', 'grok'],
|
|
297
297
|
})),
|
|
298
298
|
},
|
|
@@ -50,7 +50,7 @@ function buildDeliveryAnnounce(env) {
|
|
|
50
50
|
`[DELIVERY — ${project}]`,
|
|
51
51
|
'Commit useful, passing work. Only committed state delivers; ignore uncommitted and untracked work in every checkout.',
|
|
52
52
|
coordinationLine(env.platform),
|
|
53
|
-
'Land every ready same-repo commit: update the default branch, rebase your worktree onto its tip, retest there, take greprag merge-lock in the canonical checkout, recheck the tip, merge --ff-only, release --landed. Tip moved? Free the lock and prepare again. Never rebase a shared default branch.',
|
|
53
|
+
'Land every ready same-repo commit: update the default branch, rebase your worktree onto its tip, retest there, take greprag merge-lock in the canonical checkout, recheck the tip, merge --ff-only, release --landed. Tip moved? Free the lock and prepare again. A chip\'s branch arrives rebased by the chip: only fast-forward it; refused, free the lock and send it back. Never rebase in another session\'s worktree or a shared default branch.',
|
|
54
54
|
'Deploy through this repo\'s delivery profile, from its canonical primary checkout unless the profile says otherwise. Worktrees build; they do not deploy. Then verify production serves the commit you shipped.',
|
|
55
55
|
'THE GOAL ALREADY AUTHORIZES ALL OF THIS. Do not stop to ask permission to commit, merge, or deploy. Separate approval is for effects outside code delivery: destructive data, secrets, money, customer-facing messages.',
|
|
56
56
|
'Why each rule, and the details: `greprag load delivery`.',
|
package/dist/commands/discord.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/** greprag discord <pair|unpair|me|
|
|
2
|
+
/** greprag discord <pair|unpair|me|typing> — Discord bot CLI.
|
|
3
3
|
*
|
|
4
4
|
* pair POSTs /v1/discord/pair with the current project anchor; prints
|
|
5
5
|
* the 6-char code + DM-the-bot instructions.
|
|
6
6
|
* unpair DELETEs /v1/discord/pair.
|
|
7
|
-
* me GETs /v1/discord/me — pairing
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* me GETs /v1/discord/me — pairing state.
|
|
8
|
+
* typing POSTs /v1/discord/typing — refreshes the typing indicator.
|
|
9
|
+
*
|
|
10
|
+
* Sessions DM the operator with `greprag send --to discord:<snowflake>`.
|
|
11
|
+
* Nothing reads DMs sent to the bot: the Front Desk and the handoff pin were
|
|
12
|
+
* retired 2026-10-06, so the bot answers each one with a fixed line.
|
|
11
13
|
*/
|
|
12
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
15
|
exports.DISCORD_HELP = void 0;
|
|
14
16
|
exports.callDiscordPair = callDiscordPair;
|
|
15
17
|
exports.callDiscordUnpair = callDiscordUnpair;
|
|
16
18
|
exports.callDiscordMe = callDiscordMe;
|
|
17
|
-
exports.callDiscordHandoff = callDiscordHandoff;
|
|
18
|
-
exports.callDiscordUnhandoff = callDiscordUnhandoff;
|
|
19
19
|
exports.callDiscordTyping = callDiscordTyping;
|
|
20
20
|
async function callDiscordPair(deps, projectId) {
|
|
21
21
|
const f = deps.fetchImpl ?? fetch;
|
|
@@ -45,30 +45,6 @@ async function callDiscordMe(deps) {
|
|
|
45
45
|
});
|
|
46
46
|
return (await res.json());
|
|
47
47
|
}
|
|
48
|
-
async function callDiscordHandoff(deps, args) {
|
|
49
|
-
const f = deps.fetchImpl ?? fetch;
|
|
50
|
-
const res = await f(`${deps.apiUrl}/v1/discord/handoff`, {
|
|
51
|
-
method: 'POST',
|
|
52
|
-
headers: {
|
|
53
|
-
'Authorization': `Bearer ${deps.apiKey}`,
|
|
54
|
-
'Content-Type': 'application/json',
|
|
55
|
-
},
|
|
56
|
-
body: JSON.stringify({
|
|
57
|
-
project_id: args.projectId,
|
|
58
|
-
session_id: args.sessionId,
|
|
59
|
-
ttl_minutes: args.ttlMinutes,
|
|
60
|
-
}),
|
|
61
|
-
});
|
|
62
|
-
return (await res.json());
|
|
63
|
-
}
|
|
64
|
-
async function callDiscordUnhandoff(deps) {
|
|
65
|
-
const f = deps.fetchImpl ?? fetch;
|
|
66
|
-
const res = await f(`${deps.apiUrl}/v1/discord/handoff`, {
|
|
67
|
-
method: 'DELETE',
|
|
68
|
-
headers: { 'Authorization': `Bearer ${deps.apiKey}` },
|
|
69
|
-
});
|
|
70
|
-
return (await res.json());
|
|
71
|
-
}
|
|
72
48
|
async function callDiscordTyping(deps) {
|
|
73
49
|
const f = deps.fetchImpl ?? fetch;
|
|
74
50
|
const res = await f(`${deps.apiUrl}/v1/discord/typing`, {
|
|
@@ -78,44 +54,19 @@ async function callDiscordTyping(deps) {
|
|
|
78
54
|
return (await res.json());
|
|
79
55
|
}
|
|
80
56
|
exports.DISCORD_HELP = `
|
|
81
|
-
greprag discord —
|
|
57
|
+
greprag discord — let your sessions DM you on Discord.
|
|
82
58
|
|
|
83
|
-
Pair once
|
|
84
|
-
|
|
85
|
-
away from the desk and want to keep the same thread alive on your phone).
|
|
59
|
+
Pair once; then any session can message you (see Outbound below). The bot does
|
|
60
|
+
not read DMs you send it: the Front Desk was retired 2026-10-06.
|
|
86
61
|
|
|
87
62
|
Subcommands:
|
|
88
63
|
greprag discord pair Generate a 15-min code; DM @greprag-bot
|
|
89
64
|
/pair <code> to link this Discord account
|
|
90
65
|
to the current project.
|
|
91
66
|
greprag discord unpair Remove the pairing.
|
|
92
|
-
greprag discord me Show current pairing
|
|
93
|
-
greprag discord handoff [--ttl N] Pin DMs to (current project, current
|
|
94
|
-
session) for N minutes (default 60,
|
|
95
|
-
max 1440). Bot DMs you a confirmation,
|
|
96
|
-
then this command BLOCKS and streams
|
|
97
|
-
inbound DMs to stdout (one per line)
|
|
98
|
-
until killed. Run it under Claude's
|
|
99
|
-
Monitor tool: each line = one
|
|
100
|
-
notification right back into the
|
|
101
|
-
agent's turn. Sliding TTL: each
|
|
102
|
-
inbound DM extends the pin by >=30 min.
|
|
103
|
-
[--session <8-hex>] Override session id (default: CLAUDE_SESSION_ID).
|
|
104
|
-
[--project <name>] Override project (default: cwd anchor).
|
|
105
|
-
[--orchestrator] Tenant-wide watcher — sees inbox traffic
|
|
106
|
-
from any project under this tenant. The
|
|
107
|
-
DM pin still routes here, but the agent
|
|
108
|
-
has portfolio-wide eyes. Use when this
|
|
109
|
-
session is the user's orchestrator
|
|
110
|
-
(/business-advisor loaded, etc.).
|
|
111
|
-
[--json] Emit raw JSON per line (for Monitor).
|
|
112
|
-
[--no-watch] Pin and exit — skip the inline watcher.
|
|
113
|
-
For scripted use; loses the live DM stream.
|
|
114
|
-
greprag discord unhandoff Clear the active handoff — DMs revert
|
|
115
|
-
to default project.
|
|
67
|
+
greprag discord me Show current pairing state.
|
|
116
68
|
greprag discord typing Refresh the "typing..." indicator on
|
|
117
|
-
Discord for ~10s.
|
|
118
|
-
inbound DM; call manually during long
|
|
69
|
+
Discord for ~10s. Call it during long
|
|
119
70
|
agent turns so the user sees activity
|
|
120
71
|
instead of silence.
|
|
121
72
|
|
|
@@ -251,19 +251,6 @@ function printMessage(msg) {
|
|
|
251
251
|
function emitDmReplyDirective(msg, session) {
|
|
252
252
|
if (!session)
|
|
253
253
|
return;
|
|
254
|
-
// Front Desk Switchboard relay: the operator reached THIS session through the
|
|
255
|
-
// receptionist and `references.reply_to` is the rail to answer on. Fires for
|
|
256
|
-
// ANY message_type (the relay uses plain 'message'), and takes precedence over
|
|
257
|
-
// the legacy Discord-DM path. Without this the session falls back to the bare
|
|
258
|
-
// sender handle (travis@greprag.com) — the reply lands in MAIN, never reaching
|
|
259
|
-
// the operator on Discord/SMS. Live failure 2026-06-08. spec:
|
|
260
|
-
// docs/front-desk-switchboard.md §4 (return leg).
|
|
261
|
-
const replyTo = msg.references?.reply_to;
|
|
262
|
-
if (replyTo) {
|
|
263
|
-
console.log(`[greprag] Operator via front desk — answer now, even mid-task: greprag send --to ${replyTo} "…" ` +
|
|
264
|
-
`(exact addr; bare handle is lost). If they reply inline here, don't echo to Discord/SMS.`);
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
254
|
if (msg.message_type !== 'discord_dm')
|
|
268
255
|
return;
|
|
269
256
|
const snowflake = msg.references?.discord?.snowflake ?? '<snowflake>';
|
package/dist/commands/load.js
CHANGED
|
@@ -453,9 +453,11 @@ async function printMirroredSkill(name, args) {
|
|
|
453
453
|
const identityArg = args.skillId ? ` --skill-id ${args.skillId}` : '';
|
|
454
454
|
console.error(`Mirrored skill "${name}" has no canonical body — the mirror is holding the generated adapter.\n` +
|
|
455
455
|
`Refusing to serve it: the adapter is the launcher that sent you here, not the skill.\n\n` +
|
|
456
|
-
`Restore the real body from the pre-adoption snapshot:\n` +
|
|
456
|
+
`Restore the real body from an earlier revision or the pre-adoption snapshot:\n` +
|
|
457
|
+
` greprag skill history ${name}${identityArg}\n` +
|
|
458
|
+
` greprag skill revert ${name} --to <rev>${identityArg} --reason "<why>" --expected-hash <current-hash>\n` +
|
|
457
459
|
` greprag skill mirror snapshot list ${name}${identityArg}\n` +
|
|
458
|
-
` greprag skill mirror snapshot promote ${name} --name <snapshot>${identityArg} --expected-hash <current-hash
|
|
460
|
+
` greprag skill mirror snapshot promote ${name} --name <snapshot>${identityArg} --expected-hash <current-hash> --reason "<why>"\n`);
|
|
459
461
|
process.exitCode = 1;
|
|
460
462
|
return true;
|
|
461
463
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* PURE over ReminderEnv — the hook assembles env (i/o) and emits the returned lines;
|
|
6
6
|
* a broken module never blocks a turn (fail-open per module). */
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.ANNOUNCE_POINTER_RESERVE = exports.ANNOUNCE_HARNESS_CAP = exports.ANNOUNCE_INLINE_BUDGET = exports.compactReannounceModules = exports.
|
|
8
|
+
exports.ANNOUNCE_POINTER_RESERVE = exports.ANNOUNCE_HARNESS_CAP = exports.ANNOUNCE_INLINE_BUDGET = exports.compactReannounceModules = exports.harnessModules = exports.REGISTRY = void 0;
|
|
9
9
|
exports.collectReminders = collectReminders;
|
|
10
10
|
exports.bootOrder = bootOrder;
|
|
11
11
|
exports.collectAnnounces = collectAnnounces;
|
|
@@ -66,13 +66,6 @@ exports.REGISTRY = [
|
|
|
66
66
|
* update a parallel id list. (Root-cause repair, chip/opencode-fix-chip-system-parity.) */
|
|
67
67
|
const harnessModules = (harness, registry = exports.REGISTRY) => registry.filter((m) => !m.harnesses || m.harnesses.includes(harness));
|
|
68
68
|
exports.harnessModules = harnessModules;
|
|
69
|
-
/** Split the registry by READ surface (docs/reminder-interrupt.md). UserPromptSubmit
|
|
70
|
-
* evaluates `prompt`-source modules (the default); PreToolUse evaluates `command`-source
|
|
71
|
-
* modules (they read env.toolCommand). Announces are always prompt-side (SessionStart). */
|
|
72
|
-
const promptModules = (registry = exports.REGISTRY) => registry.filter((m) => (m.source ?? 'prompt') === 'prompt');
|
|
73
|
-
exports.promptModules = promptModules;
|
|
74
|
-
const commandModules = (registry = exports.REGISTRY) => registry.filter((m) => m.source === 'command');
|
|
75
|
-
exports.commandModules = commandModules;
|
|
76
69
|
/** PostCompact re-announce set (docs/reminder-interrupt.md). A compaction wipes the
|
|
77
70
|
* SessionStart announces from context, so the announce modules re-present afterward —
|
|
78
71
|
* by DEFAULT all of them; a module opts out with `reannounceOnCompact: false`. The
|
package/dist/commands/session.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* retitle <session-8hex> "<title>" — force-set a session's registry title,
|
|
5
5
|
* overriding the gpt-5.4-nano auto-title. Used by the multi-chip /chip-leader
|
|
6
6
|
* convention: a chip stamps its own label-aligned title ("Chip A — …") in
|
|
7
|
-
* Block 1 so the FleetView watcher list
|
|
8
|
-
*
|
|
7
|
+
* Block 1 so the FleetView watcher list lines up with the leader's chip
|
|
8
|
+
* labels. Thin HTTP client over POST /v1/memory/title. */
|
|
9
9
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
10
|
if (k2 === undefined) k2 = k;
|
|
11
11
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -90,7 +90,6 @@ function buildSkillMirrorAnnounce(m, platform) {
|
|
|
90
90
|
}
|
|
91
91
|
exports.skillMirrorAnnounceModule = {
|
|
92
92
|
id: 'skill-mirror-announce',
|
|
93
|
-
source: 'prompt',
|
|
94
93
|
dependsOn: ['load-primer'], // references `greprag load` — the primer establishes it
|
|
95
94
|
detect: (_env) => ({ tier: 'silent' }), // announce-only
|
|
96
95
|
announce: (env) => buildSkillMirrorAnnounce(env.mirroredSkills, env.platform),
|
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/hook.js
CHANGED
|
@@ -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 {
|