greprag 5.84.0 → 5.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agent-doctrine-file.js +5 -128
- package/dist/commands/agents.js +125 -846
- package/dist/commands/init.js +17 -29
- package/dist/commands/load.js +14 -36
- package/dist/commands/parity.js +1 -1
- package/dist/commands/skill-mirror-reminder.js +1 -18
- package/dist/commands/skill.js +137 -566
- package/dist/hook.js +30 -54
- package/dist/index.js +5 -5
- package/dist/native-adapter-refresh.js +15 -18
- package/dist/native-skill-mirror.js +11 -92
- package/dist/opencode-plugin.bundle.js +81 -283
- package/dist/skill-activation-manifest.js +14 -37
- package/dist/skill-git-ingest.js +297 -0
- package/dist/skill-landing.js +7 -173
- package/dist/skill-mirror-client.js +4 -280
- package/dist/skill-staleness.js +3 -6
- package/package.json +1 -1
- package/skill/mechanic/docs/skill-fix-conventions.md +1 -1
- package/skill/templates/os.md +1 -1
- package/skill/templates/prompt-audit.md +26 -49
- package/skill/templates/skill-change.md +15 -13
package/dist/commands/agents.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/** `greprag instructions` —
|
|
2
|
+
/** `greprag instructions` — render the Codex/OpenCode AGENTS.md files from
|
|
3
|
+
* `~/.claude/CLAUDE.md`, and keep GrepRAG's read-only copy of it.
|
|
3
4
|
*
|
|
4
|
-
* `
|
|
5
|
+
* `~/.claude/CLAUDE.md` in the dotclaude git repo is the only source. Each
|
|
6
|
+
* AGENTS.md is that file plus an optional per-harness addition kept beside it
|
|
7
|
+
* in git (`~/.claude/instructions/codex.md`, `~/.claude/instructions/opencode.md`),
|
|
8
|
+
* joined byte for byte. SessionStart runs the render; GrepRAG mirrors the
|
|
9
|
+
* COMMITTED CLAUDE.md for search and `greprag load instructions`, and never
|
|
10
|
+
* writes it. Repo-level CLAUDE.md/AGENTS.md are ordinary files in their repos.
|
|
11
|
+
*
|
|
12
|
+
* `greprag agents` remains as a compatibility alias.
|
|
13
|
+
* adr: adr/skill-ownership-git.md */
|
|
5
14
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
15
|
if (k2 === undefined) k2 = k;
|
|
7
16
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -36,857 +45,130 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
45
|
};
|
|
37
46
|
})();
|
|
38
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.
|
|
40
|
-
exports.
|
|
48
|
+
exports.instructionSource = instructionSource;
|
|
49
|
+
exports.instructionTargets = instructionTargets;
|
|
50
|
+
exports.renderAgentsFile = renderAgentsFile;
|
|
51
|
+
exports.renderGlobalInstructions = renderGlobalInstructions;
|
|
52
|
+
exports.committedInstructionSource = committedInstructionSource;
|
|
53
|
+
exports.mirrorCommittedInstructions = mirrorCommittedInstructions;
|
|
41
54
|
exports.runInstructions = runInstructions;
|
|
42
55
|
exports.runAgents = runAgents;
|
|
43
|
-
const crypto = __importStar(require("node:crypto"));
|
|
44
56
|
const fs = __importStar(require("node:fs"));
|
|
45
57
|
const os = __importStar(require("node:os"));
|
|
46
58
|
const path = __importStar(require("node:path"));
|
|
47
|
-
const project_anchor_1 = require("../project-anchor");
|
|
48
59
|
const proc_1 = require("../proc");
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const aliasNote = commandName === 'agents'
|
|
52
|
-
? '\n\nNOTE\n `greprag agents` is a deprecated alias. Use `greprag instructions`.'
|
|
53
|
-
: '';
|
|
54
|
-
return `greprag ${commandName} — canonical GrepRAG Instructions for CLAUDE.md / AGENTS.md
|
|
55
|
-
|
|
56
|
-
USAGE
|
|
57
|
-
greprag ${commandName} pull <file> [--scope global|repo] [--harness <name>] [--push]
|
|
58
|
-
greprag ${commandName} push [--scope global|repo|all] [--check] [--force]
|
|
59
|
-
greprag ${commandName} adopt <file> [--scope global|repo] [--harness <name>] [--no-sync]
|
|
60
|
-
greprag ${commandName} revise <file> [--scope global|repo] [--harness <name>] [--no-sync]
|
|
61
|
-
greprag ${commandName} overlay set <harness> <file> [--scope global|repo] [--no-sync]
|
|
62
|
-
greprag ${commandName} overlay derive <harness> <shared-file> <rendered-file> [--scope global|repo] [--no-sync]
|
|
63
|
-
greprag ${commandName} overlay clear <harness> [--scope global|repo] [--no-sync]
|
|
64
|
-
greprag ${commandName} snapshot create --name <name> [--scope global|repo|all] [--harness <name>] [--from-ref <git-ref>] [--force]
|
|
65
|
-
greprag ${commandName} snapshot list [--scope global|repo|all]
|
|
66
|
-
greprag ${commandName} snapshot show <name> [--scope global|repo|all]
|
|
67
|
-
greprag ${commandName} restore --snapshot <name> [--scope global|repo|all] [--harness <name>] [--check] [--force]
|
|
68
|
-
greprag ${commandName} sync [--scope global|repo|all] [--check] [--force]
|
|
69
|
-
greprag ${commandName} status [--scope global|repo|all]
|
|
70
|
-
|
|
71
|
-
MODEL
|
|
72
|
-
Adopt and revise copy source bytes exactly; no model rewrites instructions.
|
|
73
|
-
The first adoption stores Original Snapshots for every managed follower.
|
|
74
|
-
Harness translations are raw additive suffixes. Pull writes disk to canon.
|
|
75
|
-
Push writes canon to disk only when the external sync baseline is clean.
|
|
76
|
-
Snapshots are named exact-byte rollback points and do not change canon.
|
|
77
|
-
Status/check never writes.${aliasNote}`;
|
|
78
|
-
}
|
|
79
|
-
function config(apiKeyOverride) {
|
|
80
|
-
return {
|
|
81
|
-
apiUrl: process.env.GREPRAG_API_URL || 'https://api.greprag.com',
|
|
82
|
-
apiKey: apiKeyOverride || process.env.GREPRAG_API_KEY || '',
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
function flag(args, name) {
|
|
86
|
-
const index = args.indexOf(name);
|
|
87
|
-
return index >= 0 ? args[index + 1] : undefined;
|
|
88
|
-
}
|
|
89
|
-
function repoRoot(cwd = process.cwd()) {
|
|
90
|
-
try {
|
|
91
|
-
return String((0, proc_1.safeExecSync)('git rev-parse --show-toplevel', {
|
|
92
|
-
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
93
|
-
})).trim();
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
return path.resolve(cwd);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
function fileGitRoot(filePath) {
|
|
100
|
-
try {
|
|
101
|
-
return String((0, proc_1.safeExecFileSync)('git', ['-C', path.dirname(filePath), 'rev-parse', '--show-toplevel'], {
|
|
102
|
-
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
103
|
-
})).trim();
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
60
|
+
function instructionSource(home = os.homedir()) {
|
|
61
|
+
return path.join(home, '.claude', 'CLAUDE.md');
|
|
108
62
|
}
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
63
|
+
function instructionTargets(home = os.homedir()) {
|
|
64
|
+
const additions = path.join(home, '.claude', 'instructions');
|
|
65
|
+
return [
|
|
66
|
+
{ harness: 'codex', path: path.join(home, '.codex', 'AGENTS.md'), addition: path.join(additions, 'codex.md') },
|
|
67
|
+
{ harness: 'opencode', path: path.join(home, '.config', 'opencode', 'AGENTS.md'), addition: path.join(additions, 'opencode.md') },
|
|
68
|
+
];
|
|
119
69
|
}
|
|
120
|
-
function
|
|
70
|
+
function readOrNull(file) {
|
|
121
71
|
try {
|
|
122
|
-
|
|
123
|
-
'-C', root,
|
|
124
|
-
'log',
|
|
125
|
-
'--reverse',
|
|
126
|
-
'--format=%H',
|
|
127
|
-
'--all',
|
|
128
|
-
'-G', 'Agent Doctrine|Doctrine Mirror|agent doctrine|greprag agents|greprag instructions|GrepRAG Instructions',
|
|
129
|
-
'--',
|
|
130
|
-
relativePath,
|
|
131
|
-
], {
|
|
132
|
-
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
133
|
-
maxBuffer: 1024 * 1024,
|
|
134
|
-
})).trim();
|
|
135
|
-
return out.split(/\r?\n/).find(Boolean) || null;
|
|
72
|
+
return fs.readFileSync(file, 'utf8');
|
|
136
73
|
}
|
|
137
74
|
catch {
|
|
138
75
|
return null;
|
|
139
76
|
}
|
|
140
77
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
{
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
manifestPath: '~/.claude/CLAUDE.md',
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
harness: 'codex',
|
|
164
|
-
actualPath: path.join(home, '.codex', 'AGENTS.md'),
|
|
165
|
-
manifestPath: '~/.codex/AGENTS.md',
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
harness: 'opencode',
|
|
169
|
-
actualPath: path.join(home, '.config', 'opencode', 'AGENTS.md'),
|
|
170
|
-
manifestPath: '~/.config/opencode/AGENTS.md',
|
|
171
|
-
},
|
|
172
|
-
];
|
|
173
|
-
}
|
|
174
|
-
function repoTargets(root, directory) {
|
|
175
|
-
const base = directory === '.' ? root : path.join(root, ...directory.split('/'));
|
|
176
|
-
return [
|
|
177
|
-
{
|
|
178
|
-
harness: 'claude-code',
|
|
179
|
-
actualPath: path.join(base, 'CLAUDE.md'),
|
|
180
|
-
manifestPath: path.join(base, 'CLAUDE.md'),
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
harness: 'codex',
|
|
184
|
-
actualPath: path.join(base, 'AGENTS.md'),
|
|
185
|
-
manifestPath: path.join(base, 'AGENTS.md'),
|
|
186
|
-
note: 'shared AGENTS.md follower consumed by Codex and OpenCode',
|
|
187
|
-
combinedAgents: true,
|
|
188
|
-
},
|
|
189
|
-
];
|
|
190
|
-
}
|
|
191
|
-
function parityUnitName(scope, projectId, directory) {
|
|
192
|
-
if (scope === 'global')
|
|
193
|
-
return 'agent-doctrine-global';
|
|
194
|
-
if (!projectId)
|
|
195
|
-
throw new Error('projectId required for repo doctrine');
|
|
196
|
-
const suffix = directory === '.'
|
|
197
|
-
? 'root'
|
|
198
|
-
: crypto.createHash('sha256').update(directory).digest('hex').slice(0, 8);
|
|
199
|
-
return `agent-doctrine-${projectId.slice(0, 8)}-${suffix}`;
|
|
200
|
-
}
|
|
201
|
-
function snapshotContentHash(value) {
|
|
202
|
-
return crypto.createHash('sha256').update(value).digest('hex');
|
|
203
|
-
}
|
|
204
|
-
function normalizeSnapshotName(value) {
|
|
205
|
-
const name = (value || '').trim();
|
|
206
|
-
if (!/^[a-z0-9][a-z0-9._-]{0,79}$/i.test(name)) {
|
|
207
|
-
throw new Error('snapshot name must be 1-80 chars: letters, numbers, dot, underscore, or dash');
|
|
208
|
-
}
|
|
209
|
-
return name;
|
|
210
|
-
}
|
|
211
|
-
function snapshotNameOf(snapshot) {
|
|
212
|
-
return snapshot.snapshotName || 'original';
|
|
213
|
-
}
|
|
214
|
-
function originalSnapshotForTarget(target, scope, projectId, directory, capturedAt) {
|
|
215
|
-
const base = {
|
|
216
|
-
snapshotName: 'original',
|
|
217
|
-
harness: target.harness,
|
|
218
|
-
path: target.manifestPath,
|
|
219
|
-
scope,
|
|
220
|
-
projectId,
|
|
221
|
-
directory,
|
|
222
|
-
capturedAt,
|
|
223
|
-
};
|
|
224
|
-
if (!fs.existsSync(target.actualPath)) {
|
|
225
|
-
return {
|
|
226
|
-
...base,
|
|
227
|
-
status: 'missing',
|
|
228
|
-
content: '',
|
|
229
|
-
contentHash: snapshotContentHash(''),
|
|
230
|
-
source: 'missing',
|
|
231
|
-
sourceRef: null,
|
|
232
|
-
reason: 'follower file did not exist at adoption time',
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
const diskContent = fs.readFileSync(target.actualPath, 'utf8');
|
|
236
|
-
if (looksManagedOrRenamed(diskContent)) {
|
|
237
|
-
const gitRoot = fileGitRoot(target.actualPath);
|
|
238
|
-
if (gitRoot) {
|
|
239
|
-
const rel = path.relative(gitRoot, target.actualPath).replace(/\\/g, '/');
|
|
240
|
-
const firstTouch = firstInstructionTouch(gitRoot, rel);
|
|
241
|
-
if (firstTouch) {
|
|
242
|
-
const historical = gitFileAt(gitRoot, `${firstTouch}^`, rel);
|
|
243
|
-
if (historical !== null) {
|
|
244
|
-
return {
|
|
245
|
-
...base,
|
|
246
|
-
status: 'present',
|
|
247
|
-
content: historical,
|
|
248
|
-
contentHash: snapshotContentHash(historical),
|
|
249
|
-
source: 'git-history',
|
|
250
|
-
sourceRef: `${firstTouch}^`,
|
|
251
|
-
reason: `pre-GrepRAG-Instructions snapshot from parent of ${firstTouch.slice(0, 12)}`,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return {
|
|
258
|
-
...base,
|
|
259
|
-
status: 'present',
|
|
260
|
-
content: diskContent,
|
|
261
|
-
contentHash: snapshotContentHash(diskContent),
|
|
262
|
-
source: 'disk',
|
|
263
|
-
sourceRef: null,
|
|
264
|
-
reason: looksManagedOrRenamed(diskContent)
|
|
265
|
-
? 'historical pre-instructions file was not recoverable; captured disk bytes'
|
|
266
|
-
: 'captured disk bytes before first GrepRAG Instructions adoption',
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
function diskSnapshotForTarget(target, scope, projectId, directory, snapshotName, capturedAt) {
|
|
270
|
-
const base = {
|
|
271
|
-
snapshotName,
|
|
272
|
-
harness: target.harness,
|
|
273
|
-
path: target.manifestPath,
|
|
274
|
-
scope,
|
|
275
|
-
projectId,
|
|
276
|
-
directory,
|
|
277
|
-
capturedAt,
|
|
278
|
-
};
|
|
279
|
-
if (!fs.existsSync(target.actualPath)) {
|
|
280
|
-
return {
|
|
281
|
-
...base,
|
|
282
|
-
status: 'missing',
|
|
283
|
-
content: '',
|
|
284
|
-
contentHash: snapshotContentHash(''),
|
|
285
|
-
source: 'missing',
|
|
286
|
-
sourceRef: null,
|
|
287
|
-
reason: `snapshot ${snapshotName}: follower file did not exist`,
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
const content = fs.readFileSync(target.actualPath, 'utf8');
|
|
291
|
-
return {
|
|
292
|
-
...base,
|
|
293
|
-
status: 'present',
|
|
294
|
-
content,
|
|
295
|
-
contentHash: snapshotContentHash(content),
|
|
296
|
-
source: 'disk',
|
|
297
|
-
sourceRef: null,
|
|
298
|
-
reason: `snapshot ${snapshotName}: captured disk bytes`,
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
function gitRefSnapshotForTarget(target, scope, projectId, directory, snapshotName, capturedAt, ref) {
|
|
302
|
-
const base = {
|
|
303
|
-
snapshotName,
|
|
304
|
-
harness: target.harness,
|
|
305
|
-
path: target.manifestPath,
|
|
306
|
-
scope,
|
|
307
|
-
projectId,
|
|
308
|
-
directory,
|
|
309
|
-
capturedAt,
|
|
310
|
-
};
|
|
311
|
-
const gitRoot = fileGitRoot(target.actualPath);
|
|
312
|
-
if (gitRoot) {
|
|
313
|
-
const rel = path.relative(gitRoot, target.actualPath).replace(/\\/g, '/');
|
|
314
|
-
const content = gitFileAt(gitRoot, ref, rel);
|
|
315
|
-
if (content !== null) {
|
|
316
|
-
return {
|
|
317
|
-
...base,
|
|
318
|
-
status: 'present',
|
|
319
|
-
content,
|
|
320
|
-
contentHash: snapshotContentHash(content),
|
|
321
|
-
source: 'git-history',
|
|
322
|
-
sourceRef: ref,
|
|
323
|
-
reason: `snapshot ${snapshotName}: captured ${rel} from ${ref}`,
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
return {
|
|
328
|
-
...base,
|
|
329
|
-
status: 'missing',
|
|
330
|
-
content: '',
|
|
331
|
-
contentHash: snapshotContentHash(''),
|
|
332
|
-
source: 'missing',
|
|
333
|
-
sourceRef: ref,
|
|
334
|
-
reason: `snapshot ${snapshotName}: no file at ${ref}`,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
function collectOriginalSnapshots(targets, scope, projectId, directory) {
|
|
338
|
-
const capturedAt = new Date().toISOString();
|
|
339
|
-
return targets.map((target) => originalSnapshotForTarget(target, scope, projectId, directory, capturedAt));
|
|
340
|
-
}
|
|
341
|
-
function uniqueNonempty(values) {
|
|
342
|
-
const seen = new Set();
|
|
343
|
-
const out = [];
|
|
344
|
-
for (const value of values) {
|
|
345
|
-
if (!value)
|
|
346
|
-
continue;
|
|
347
|
-
if (seen.has(value))
|
|
348
|
-
continue;
|
|
349
|
-
seen.add(value);
|
|
350
|
-
out.push(value);
|
|
351
|
-
}
|
|
352
|
-
return out;
|
|
353
|
-
}
|
|
354
|
-
function renderForTarget(doc, target) {
|
|
355
|
-
if (target.combinedAgents) {
|
|
356
|
-
return doc.shared + uniqueNonempty([doc.overlays.codex, doc.overlays.opencode]).join('');
|
|
357
|
-
}
|
|
358
|
-
return doc.shared + (doc.overlays[target.harness] || '');
|
|
359
|
-
}
|
|
360
|
-
async function apiJson(pathname, init, apiKeyOverride) {
|
|
361
|
-
const cfg = config(apiKeyOverride);
|
|
362
|
-
if (!cfg.apiKey)
|
|
363
|
-
throw new Error('GREPRAG_API_KEY not set — run `greprag init` first.');
|
|
364
|
-
const response = await fetch(`${cfg.apiUrl}${pathname}`, {
|
|
365
|
-
...init,
|
|
366
|
-
headers: {
|
|
367
|
-
'Authorization': `Bearer ${cfg.apiKey}`,
|
|
368
|
-
...(init?.body ? { 'Content-Type': 'application/json' } : {}),
|
|
369
|
-
...(init?.headers || {}),
|
|
370
|
-
},
|
|
78
|
+
/** AGENTS.md = CLAUDE.md + the harness addition, raw concatenation. PURE. */
|
|
79
|
+
function renderAgentsFile(source, addition) {
|
|
80
|
+
return source + (addition || '');
|
|
81
|
+
}
|
|
82
|
+
/** Write each AGENTS.md that differs from its render; `check` only reports. */
|
|
83
|
+
function renderGlobalInstructions(opts = {}) {
|
|
84
|
+
const home = opts.home || os.homedir();
|
|
85
|
+
const source = readOrNull(instructionSource(home));
|
|
86
|
+
return instructionTargets(home).map(target => {
|
|
87
|
+
if (source === null)
|
|
88
|
+
return { path: target.path, status: 'no-source' };
|
|
89
|
+
const next = renderAgentsFile(source, readOrNull(target.addition));
|
|
90
|
+
if (readOrNull(target.path) === next)
|
|
91
|
+
return { path: target.path, status: 'current' };
|
|
92
|
+
if (opts.check)
|
|
93
|
+
return { path: target.path, status: 'stale' };
|
|
94
|
+
fs.mkdirSync(path.dirname(target.path), { recursive: true });
|
|
95
|
+
fs.writeFileSync(target.path, next, 'utf8');
|
|
96
|
+
return { path: target.path, status: 'written' };
|
|
371
97
|
});
|
|
372
|
-
const body = await response.json().catch(() => ({}));
|
|
373
|
-
if (!response.ok)
|
|
374
|
-
throw new Error(body.error || `HTTP ${response.status}`);
|
|
375
|
-
return body;
|
|
376
|
-
}
|
|
377
|
-
function context(scope, args) {
|
|
378
|
-
const root = repoRoot();
|
|
379
|
-
if (scope === 'global') {
|
|
380
|
-
return { projectId: null, directory: '.', root, targets: globalTargets() };
|
|
381
|
-
}
|
|
382
|
-
const anchor = (0, project_anchor_1.readAnchor)(root);
|
|
383
|
-
const directory = relativeDirectory(root, flag(args, '--directory'));
|
|
384
|
-
return {
|
|
385
|
-
projectId: anchor.projectId,
|
|
386
|
-
directory,
|
|
387
|
-
root,
|
|
388
|
-
targets: repoTargets(root, directory),
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
async function fetchDocument(scope, projectId, directory, apiKey) {
|
|
392
|
-
const params = new URLSearchParams({ scope, directory, harness: 'all' });
|
|
393
|
-
if (projectId)
|
|
394
|
-
params.set('projectId', projectId);
|
|
395
|
-
try {
|
|
396
|
-
const body = await apiJson(`/v1/agents?${params.toString()}`, undefined, apiKey);
|
|
397
|
-
return body.documents[0] || null;
|
|
398
|
-
}
|
|
399
|
-
catch (error) {
|
|
400
|
-
if (error instanceof Error && /not found/.test(error.message))
|
|
401
|
-
return null;
|
|
402
|
-
throw error;
|
|
403
|
-
}
|
|
404
98
|
}
|
|
405
|
-
|
|
99
|
+
/** The committed bytes of `~/.claude/CLAUDE.md` (HEAD of the dotclaude repo). */
|
|
100
|
+
function committedInstructionSource(home = os.homedir()) {
|
|
101
|
+
const dir = path.join(home, '.claude');
|
|
406
102
|
try {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
for (const follower of unit?.followers || []) {
|
|
411
|
-
if (!follower.path)
|
|
412
|
-
continue;
|
|
413
|
-
baselines.set(`${follower.harness}\0${follower.path}`, {
|
|
414
|
-
syncedRevision: follower.syncedRevision,
|
|
415
|
-
syncedHash: follower.syncedHash,
|
|
416
|
-
syncedAt: follower.syncedAt,
|
|
417
|
-
dirtyAt: follower.dirtyAt,
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
return baselines;
|
|
103
|
+
return String((0, proc_1.safeExecFileSync)('git', ['-C', dir, 'show', 'HEAD:CLAUDE.md'], {
|
|
104
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 4 * 1024 * 1024,
|
|
105
|
+
}));
|
|
421
106
|
}
|
|
422
107
|
catch {
|
|
423
|
-
return
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
function inferHarness(file, explicit) {
|
|
427
|
-
if (explicit === 'claude-code' || explicit === 'codex' || explicit === 'opencode')
|
|
428
|
-
return explicit;
|
|
429
|
-
return file && path.basename(file).toUpperCase() === 'CLAUDE.MD' ? 'claude-code' : 'codex';
|
|
430
|
-
}
|
|
431
|
-
function parseOptionalHarness(explicit) {
|
|
432
|
-
if (!explicit)
|
|
433
|
-
return undefined;
|
|
434
|
-
if (explicit === 'claude-code' || explicit === 'codex' || explicit === 'opencode')
|
|
435
|
-
return explicit;
|
|
436
|
-
throw new Error('--harness must be claude-code, codex, or opencode');
|
|
437
|
-
}
|
|
438
|
-
function inferScope(file, explicit) {
|
|
439
|
-
if (explicit === 'global' || explicit === 'repo')
|
|
440
|
-
return explicit;
|
|
441
|
-
if (file) {
|
|
442
|
-
const absolute = path.resolve(file).toLowerCase();
|
|
443
|
-
if (globalTargets().some((target) => target.actualPath.toLowerCase() === absolute))
|
|
444
|
-
return 'global';
|
|
445
|
-
}
|
|
446
|
-
return 'repo';
|
|
447
|
-
}
|
|
448
|
-
async function reportSync(unitName, target, revision, renderHash, apiKey) {
|
|
449
|
-
await apiJson('/v1/agents/sync', {
|
|
450
|
-
method: 'POST',
|
|
451
|
-
body: JSON.stringify({
|
|
452
|
-
unitName,
|
|
453
|
-
harness: target.harness,
|
|
454
|
-
path: target.manifestPath,
|
|
455
|
-
revision,
|
|
456
|
-
renderHash,
|
|
457
|
-
}),
|
|
458
|
-
}, apiKey);
|
|
459
|
-
}
|
|
460
|
-
function targetMatchesHarness(target, harness) {
|
|
461
|
-
if (!harness || target.harness === harness)
|
|
462
|
-
return true;
|
|
463
|
-
return harness === 'opencode' && target.combinedAgents === true;
|
|
464
|
-
}
|
|
465
|
-
async function syncOne(params) {
|
|
466
|
-
const ctx = context(params.scope, params.args);
|
|
467
|
-
const doc = await fetchDocument(params.scope, ctx.projectId, ctx.directory, params.apiKey);
|
|
468
|
-
if (!doc) {
|
|
469
|
-
if (!params.quiet)
|
|
470
|
-
console.log(`${params.scope}: no canonical doctrine`);
|
|
471
|
-
return { canonical: false, current: 0, changed: 0, enrolled: 0, blocked: 0, drift: 0 };
|
|
472
|
-
}
|
|
473
|
-
const unitName = parityUnitName(params.scope, ctx.projectId, ctx.directory);
|
|
474
|
-
const baselines = await fetchFollowerBaselines(unitName, params.apiKey);
|
|
475
|
-
const stats = { canonical: true, current: 0, changed: 0, enrolled: 0, blocked: 0, drift: 0 };
|
|
476
|
-
for (const target of ctx.targets.filter(t => targetMatchesHarness(t, params.harness))) {
|
|
477
|
-
const source = fs.existsSync(target.actualPath)
|
|
478
|
-
? fs.readFileSync(target.actualPath, 'utf8')
|
|
479
|
-
: '';
|
|
480
|
-
const rendered = renderForTarget(doc, target);
|
|
481
|
-
const outcome = (0, agent_doctrine_file_1.planDoctrineFollowerWrite)(source, {
|
|
482
|
-
scopeKey: doc.scopeKey,
|
|
483
|
-
harness: target.harness,
|
|
484
|
-
revision: doc.revision,
|
|
485
|
-
body: rendered,
|
|
486
|
-
}, baselines.get(`${target.harness}\0${target.manifestPath}`), { force: params.force });
|
|
487
|
-
const current = outcome.action === 'current';
|
|
488
|
-
if (current)
|
|
489
|
-
stats.current++;
|
|
490
|
-
else
|
|
491
|
-
stats.drift++;
|
|
492
|
-
const detail = 'reason' in outcome ? ` — ${outcome.reason}` : '';
|
|
493
|
-
if (!params.quiet) {
|
|
494
|
-
console.log(` ${current ? 'fresh' : outcome.action.padEnd(9)} ${target.actualPath}${detail}`);
|
|
495
|
-
}
|
|
496
|
-
const seedOnlyBlocked = params.seedOnly && !current && outcome.action !== 'seed';
|
|
497
|
-
if (outcome.action === 'conflict' || outcome.action === 'local-change'
|
|
498
|
-
|| outcome.action === 'unmanaged' || seedOnlyBlocked) {
|
|
499
|
-
stats.blocked++;
|
|
500
|
-
continue;
|
|
501
|
-
}
|
|
502
|
-
if (params.check)
|
|
503
|
-
continue;
|
|
504
|
-
if (current) {
|
|
505
|
-
if (outcome.shouldReportSync) {
|
|
506
|
-
await reportSync(unitName, target, doc.revision, outcome.renderHash, params.apiKey);
|
|
507
|
-
stats.enrolled++;
|
|
508
|
-
}
|
|
509
|
-
continue;
|
|
510
|
-
}
|
|
511
|
-
fs.mkdirSync(path.dirname(target.actualPath), { recursive: true });
|
|
512
|
-
fs.writeFileSync(target.actualPath, outcome.content, 'utf8');
|
|
513
|
-
await reportSync(unitName, target, doc.revision, outcome.renderHash, params.apiKey);
|
|
514
|
-
stats.changed++;
|
|
515
|
-
}
|
|
516
|
-
return stats;
|
|
517
|
-
}
|
|
518
|
-
async function exactSourceCommand(args, mode) {
|
|
519
|
-
const from = args[0] && !args[0].startsWith('-') ? args[0] : undefined;
|
|
520
|
-
if (!from)
|
|
521
|
-
throw new Error(`${mode} requires a source file`);
|
|
522
|
-
const sourcePath = path.resolve(from);
|
|
523
|
-
if (!fs.existsSync(sourcePath))
|
|
524
|
-
throw new Error(`source file not found: ${sourcePath}`);
|
|
525
|
-
const scope = inferScope(sourcePath, flag(args, '--scope'));
|
|
526
|
-
const harness = inferHarness(sourcePath, flag(args, '--harness'));
|
|
527
|
-
const ctx = context(scope, args);
|
|
528
|
-
const current = mode === 'adopt' ? null : await fetchDocument(scope, ctx.projectId, ctx.directory);
|
|
529
|
-
const endpointMode = mode === 'pull'
|
|
530
|
-
? current ? 'revise' : 'adopt'
|
|
531
|
-
: mode;
|
|
532
|
-
const fileContent = fs.readFileSync(sourcePath, 'utf8');
|
|
533
|
-
const content = (0, agent_doctrine_file_1.stripLegacyGrepRagConventionBlocks)((0, agent_doctrine_file_1.extractDoctrineFollowerBody)(fileContent) ?? fileContent);
|
|
534
|
-
const body = await apiJson(`/v1/agents/${endpointMode}`, {
|
|
535
|
-
method: 'POST',
|
|
536
|
-
body: JSON.stringify({
|
|
537
|
-
scope,
|
|
538
|
-
projectId: ctx.projectId,
|
|
539
|
-
directory: ctx.directory,
|
|
540
|
-
sourceHarness: harness,
|
|
541
|
-
sourcePath,
|
|
542
|
-
content,
|
|
543
|
-
expectedRevision: endpointMode === 'revise' ? current?.revision ?? null : null,
|
|
544
|
-
originalSnapshots: endpointMode === 'adopt'
|
|
545
|
-
? collectOriginalSnapshots(ctx.targets, scope, ctx.projectId, ctx.directory)
|
|
546
|
-
: undefined,
|
|
547
|
-
followers: ctx.targets.map((target) => ({
|
|
548
|
-
harness: target.harness,
|
|
549
|
-
path: target.manifestPath,
|
|
550
|
-
note: target.note,
|
|
551
|
-
})),
|
|
552
|
-
}),
|
|
553
|
-
});
|
|
554
|
-
const verb = mode === 'pull' ? 'Pulled' : mode === 'adopt' ? 'Adopted' : 'Revised';
|
|
555
|
-
console.log(`${verb} ${body.document.scopeKey} revision ${body.document.revision}.`);
|
|
556
|
-
if (endpointMode === 'adopt' && body.document.originalSnapshots?.length) {
|
|
557
|
-
const historical = body.document.originalSnapshots.filter((s) => s.source === 'git-history').length;
|
|
558
|
-
console.log(`Original Snapshots stored: ${body.document.originalSnapshots.length} (${historical} from git history).`);
|
|
559
|
-
}
|
|
560
|
-
const shouldPush = mode === 'pull'
|
|
561
|
-
? args.includes('--push')
|
|
562
|
-
: !args.includes('--no-sync');
|
|
563
|
-
if (shouldPush) {
|
|
564
|
-
await syncOne({
|
|
565
|
-
scope,
|
|
566
|
-
args,
|
|
567
|
-
check: false,
|
|
568
|
-
force: args.includes('--force'),
|
|
569
|
-
});
|
|
108
|
+
return null;
|
|
570
109
|
}
|
|
571
110
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
const
|
|
582
|
-
if (
|
|
583
|
-
throw new Error(
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
throw new Error(`source file not found: ${sourcePath}`);
|
|
592
|
-
const sharedPath = sharedFile ? path.resolve(sharedFile) : null;
|
|
593
|
-
const renderedPath = renderedFile ? path.resolve(renderedFile) : null;
|
|
594
|
-
if (sharedPath && !fs.existsSync(sharedPath))
|
|
595
|
-
throw new Error(`shared file not found: ${sharedPath}`);
|
|
596
|
-
if (renderedPath && !fs.existsSync(renderedPath))
|
|
597
|
-
throw new Error(`rendered file not found: ${renderedPath}`);
|
|
598
|
-
const scope = inferScope(undefined, flag(args, '--scope'));
|
|
599
|
-
const ctx = context(scope, args);
|
|
600
|
-
const current = await fetchDocument(scope, ctx.projectId, ctx.directory);
|
|
601
|
-
if (!current)
|
|
602
|
-
throw new Error('canonical doctrine does not exist; adopt it first');
|
|
603
|
-
let content = '';
|
|
604
|
-
if (sourcePath) {
|
|
605
|
-
content = fs.readFileSync(sourcePath, 'utf8');
|
|
606
|
-
}
|
|
607
|
-
else if (sharedPath && renderedPath) {
|
|
608
|
-
const shared = fs.readFileSync(sharedPath, 'utf8');
|
|
609
|
-
const rendered = fs.readFileSync(renderedPath, 'utf8');
|
|
610
|
-
if (!rendered.startsWith(shared)) {
|
|
611
|
-
throw new Error('rendered file does not start with shared file bytes; cannot derive exact suffix');
|
|
612
|
-
}
|
|
613
|
-
content = rendered.slice(shared.length);
|
|
614
|
-
}
|
|
615
|
-
const body = await apiJson('/v1/agents/overlay', {
|
|
111
|
+
/** Keep GrepRAG's read-only copy equal to the committed CLAUDE.md. One GET;
|
|
112
|
+
* a write only when the commit moved. Returns what happened. */
|
|
113
|
+
async function mirrorCommittedInstructions(params) {
|
|
114
|
+
const home = params.home || os.homedir();
|
|
115
|
+
const content = committedInstructionSource(home);
|
|
116
|
+
if (!content || !content.trim())
|
|
117
|
+
return 'no-source';
|
|
118
|
+
const headers = { 'Authorization': `Bearer ${params.apiKey}`, 'Content-Type': 'application/json' };
|
|
119
|
+
const query = new URLSearchParams({ scope: 'global', directory: '.', harness: 'all' });
|
|
120
|
+
const res = await fetch(`${params.apiUrl}/v1/agents?${query.toString()}`, { headers, signal: AbortSignal.timeout(10_000) });
|
|
121
|
+
if (!res.ok && res.status !== 404)
|
|
122
|
+
throw new Error(`instructions mirror read failed: HTTP ${res.status}`);
|
|
123
|
+
const current = res.ok
|
|
124
|
+
? (await res.json()).documents?.[0]
|
|
125
|
+
: undefined;
|
|
126
|
+
if (current && current.shared === content)
|
|
127
|
+
return 'current';
|
|
128
|
+
const mode = current ? 'revise' : 'adopt';
|
|
129
|
+
const write = await fetch(`${params.apiUrl}/v1/agents/${mode}`, {
|
|
616
130
|
method: 'POST',
|
|
131
|
+
headers,
|
|
617
132
|
body: JSON.stringify({
|
|
618
|
-
scope,
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
sourcePath: renderedPath || sourcePath,
|
|
133
|
+
scope: 'global',
|
|
134
|
+
directory: '.',
|
|
135
|
+
sourceHarness: 'claude-code',
|
|
136
|
+
sourcePath: instructionSource(home),
|
|
623
137
|
content,
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
followers:
|
|
627
|
-
harness: target.harness,
|
|
628
|
-
path: target.manifestPath,
|
|
629
|
-
note: target.note,
|
|
630
|
-
})),
|
|
631
|
-
}),
|
|
632
|
-
});
|
|
633
|
-
const verb = action === 'set' ? 'Set' : action === 'derive' ? 'Derived' : 'Cleared';
|
|
634
|
-
console.log(`${verb} ${harness} overlay at revision ${body.document.revision}.`);
|
|
635
|
-
if (!args.includes('--no-sync')) {
|
|
636
|
-
await syncOne({ scope, args, check: false, force: args.includes('--force') });
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
function originalSnapshotForRenderTarget(doc, target, scope, projectId, directory, snapshotName) {
|
|
640
|
-
const snapshots = doc.originalSnapshots || [];
|
|
641
|
-
return snapshots.find((snapshot) => snapshot.harness === target.harness
|
|
642
|
-
&& snapshotNameOf(snapshot) === snapshotName
|
|
643
|
-
&& snapshot.path === target.manifestPath
|
|
644
|
-
&& snapshot.scope === scope
|
|
645
|
-
&& (scope === 'global' || snapshot.projectId === projectId)
|
|
646
|
-
&& snapshot.directory === directory) || null;
|
|
647
|
-
}
|
|
648
|
-
async function restoreOne(params) {
|
|
649
|
-
const ctx = context(params.scope, params.args);
|
|
650
|
-
const doc = await fetchDocument(params.scope, ctx.projectId, ctx.directory);
|
|
651
|
-
if (!doc) {
|
|
652
|
-
console.log(`${params.scope}: no canonical doctrine`);
|
|
653
|
-
return { canonical: false, current: 0, changed: 0, blocked: 0 };
|
|
654
|
-
}
|
|
655
|
-
const stats = { canonical: true, current: 0, changed: 0, blocked: 0 };
|
|
656
|
-
for (const target of ctx.targets.filter(t => targetMatchesHarness(t, params.harness))) {
|
|
657
|
-
const snapshot = originalSnapshotForRenderTarget(doc, target, params.scope, ctx.projectId, ctx.directory, params.snapshotName);
|
|
658
|
-
if (!snapshot) {
|
|
659
|
-
console.log(` blocked ${target.actualPath} — no snapshot '${params.snapshotName}' stored for this follower`);
|
|
660
|
-
stats.blocked++;
|
|
661
|
-
continue;
|
|
662
|
-
}
|
|
663
|
-
const exists = fs.existsSync(target.actualPath);
|
|
664
|
-
const source = exists ? fs.readFileSync(target.actualPath, 'utf8') : '';
|
|
665
|
-
const alreadyOriginal = snapshot.status === 'missing'
|
|
666
|
-
? !exists
|
|
667
|
-
: exists && source === snapshot.content;
|
|
668
|
-
if (alreadyOriginal) {
|
|
669
|
-
console.log(` current ${target.actualPath}`);
|
|
670
|
-
stats.current++;
|
|
671
|
-
continue;
|
|
672
|
-
}
|
|
673
|
-
const currentRender = renderForTarget(doc, target);
|
|
674
|
-
const cleanManagedFollower = !exists || source === currentRender;
|
|
675
|
-
if (!cleanManagedFollower && !params.force) {
|
|
676
|
-
console.log(` blocked ${target.actualPath} — local edits differ from the current GrepRAG render; rerun with --force only after saving them`);
|
|
677
|
-
stats.blocked++;
|
|
678
|
-
continue;
|
|
679
|
-
}
|
|
680
|
-
const action = snapshot.status === 'missing' ? 'remove' : 'restore';
|
|
681
|
-
const sourceNote = snapshot.source === 'git-history' && snapshot.sourceRef
|
|
682
|
-
? `${snapshot.source}:${snapshot.sourceRef}`
|
|
683
|
-
: snapshot.source;
|
|
684
|
-
console.log(` ${params.check ? `would-${action}` : action} ${target.actualPath} — snapshot '${params.snapshotName}' from ${sourceNote}`);
|
|
685
|
-
stats.changed++;
|
|
686
|
-
if (params.check)
|
|
687
|
-
continue;
|
|
688
|
-
if (snapshot.status === 'missing') {
|
|
689
|
-
if (exists)
|
|
690
|
-
fs.rmSync(target.actualPath);
|
|
691
|
-
}
|
|
692
|
-
else {
|
|
693
|
-
fs.mkdirSync(path.dirname(target.actualPath), { recursive: true });
|
|
694
|
-
fs.writeFileSync(target.actualPath, snapshot.content, 'utf8');
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
return stats;
|
|
698
|
-
}
|
|
699
|
-
async function restoreCommand(args) {
|
|
700
|
-
const snapshotName = normalizeSnapshotName(flag(args, '--snapshot'));
|
|
701
|
-
const requested = flag(args, '--scope') || 'all';
|
|
702
|
-
if (!['global', 'repo', 'all'].includes(requested)) {
|
|
703
|
-
throw new Error('--scope must be global|repo|all');
|
|
704
|
-
}
|
|
705
|
-
const harness = parseOptionalHarness(flag(args, '--harness'));
|
|
706
|
-
const scopes = requested === 'all' ? ['global', 'repo'] : [requested];
|
|
707
|
-
const check = args.includes('--check');
|
|
708
|
-
let changed = 0;
|
|
709
|
-
let blocked = 0;
|
|
710
|
-
for (const scope of scopes) {
|
|
711
|
-
console.log(`${scope}:`);
|
|
712
|
-
const stats = await restoreOne({
|
|
713
|
-
scope,
|
|
714
|
-
args,
|
|
715
|
-
check,
|
|
716
|
-
force: args.includes('--force'),
|
|
717
|
-
snapshotName,
|
|
718
|
-
harness,
|
|
719
|
-
});
|
|
720
|
-
changed += stats.changed;
|
|
721
|
-
blocked += stats.blocked;
|
|
722
|
-
}
|
|
723
|
-
if (changed > 0 && !check) {
|
|
724
|
-
console.log('Restored snapshot bytes. Canon is unchanged; run `greprag instructions pull <file>` to promote restored bytes back into canon.');
|
|
725
|
-
}
|
|
726
|
-
if (check && changed > 0) {
|
|
727
|
-
console.error(`${changed} follower(s) would be restored.`);
|
|
728
|
-
process.exitCode = 1;
|
|
729
|
-
}
|
|
730
|
-
if (blocked > 0)
|
|
731
|
-
process.exitCode = 1;
|
|
732
|
-
}
|
|
733
|
-
function snapshotSummary(doc) {
|
|
734
|
-
const out = new Map();
|
|
735
|
-
for (const snapshot of doc.originalSnapshots || []) {
|
|
736
|
-
const name = snapshotNameOf(snapshot);
|
|
737
|
-
const rows = out.get(name) || [];
|
|
738
|
-
rows.push(snapshot);
|
|
739
|
-
out.set(name, rows);
|
|
740
|
-
}
|
|
741
|
-
return out;
|
|
742
|
-
}
|
|
743
|
-
async function snapshotCreateOne(params) {
|
|
744
|
-
const ctx = context(params.scope, params.args);
|
|
745
|
-
const doc = await fetchDocument(params.scope, ctx.projectId, ctx.directory);
|
|
746
|
-
if (!doc) {
|
|
747
|
-
console.log(`${params.scope}: no canonical doctrine`);
|
|
748
|
-
return 0;
|
|
749
|
-
}
|
|
750
|
-
const targets = ctx.targets.filter(t => targetMatchesHarness(t, params.harness));
|
|
751
|
-
const capturedAt = new Date().toISOString();
|
|
752
|
-
const snapshots = targets.map((target) => params.fromRef
|
|
753
|
-
? gitRefSnapshotForTarget(target, params.scope, ctx.projectId, ctx.directory, params.snapshotName, capturedAt, params.fromRef)
|
|
754
|
-
: diskSnapshotForTarget(target, params.scope, ctx.projectId, ctx.directory, params.snapshotName, capturedAt));
|
|
755
|
-
const body = await apiJson('/v1/agents/snapshot', {
|
|
756
|
-
method: 'POST',
|
|
757
|
-
body: JSON.stringify({
|
|
758
|
-
scope: params.scope,
|
|
759
|
-
projectId: ctx.projectId,
|
|
760
|
-
directory: ctx.directory,
|
|
761
|
-
snapshotName: params.snapshotName,
|
|
762
|
-
snapshots,
|
|
763
|
-
replace: params.force,
|
|
764
|
-
expectedRevision: doc.revision,
|
|
138
|
+
expectedRevision: current ? current.revision : null,
|
|
139
|
+
originalSnapshots: [],
|
|
140
|
+
followers: [{ harness: 'claude-code', path: '~/.claude/CLAUDE.md', note: 'git source; GrepRAG holds a read-only copy' }],
|
|
765
141
|
}),
|
|
142
|
+
signal: AbortSignal.timeout(10_000),
|
|
766
143
|
});
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
return
|
|
770
|
-
}
|
|
771
|
-
async function snapshotListOne(scope, args) {
|
|
772
|
-
const ctx = context(scope, args);
|
|
773
|
-
const doc = await fetchDocument(scope, ctx.projectId, ctx.directory);
|
|
774
|
-
if (!doc) {
|
|
775
|
-
console.log(`${scope}: no canonical doctrine`);
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
console.log(`${scope}: ${doc.scopeKey} revision ${doc.revision}`);
|
|
779
|
-
const groups = snapshotSummary(doc);
|
|
780
|
-
if (groups.size === 0) {
|
|
781
|
-
console.log(' no snapshots');
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
for (const [name, rows] of [...groups.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
785
|
-
const present = rows.filter((row) => row.status === 'present').length;
|
|
786
|
-
const capturedAt = rows.map((row) => row.capturedAt).sort()[0] || '(unknown)';
|
|
787
|
-
console.log(` ${name}: ${rows.length} follower(s), ${present} present, first captured ${capturedAt}`);
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
async function snapshotShowOne(scope, args, snapshotName) {
|
|
791
|
-
const ctx = context(scope, args);
|
|
792
|
-
const doc = await fetchDocument(scope, ctx.projectId, ctx.directory);
|
|
793
|
-
if (!doc) {
|
|
794
|
-
console.log(`${scope}: no canonical doctrine`);
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
const rows = (doc.originalSnapshots || [])
|
|
798
|
-
.filter((snapshot) => snapshotNameOf(snapshot) === snapshotName);
|
|
799
|
-
console.log(`${scope}: ${doc.scopeKey} revision ${doc.revision}`);
|
|
800
|
-
if (rows.length === 0) {
|
|
801
|
-
console.log(` snapshot '${snapshotName}' not found`);
|
|
802
|
-
return;
|
|
803
|
-
}
|
|
804
|
-
for (const row of rows) {
|
|
805
|
-
const ref = row.sourceRef ? ` ${row.sourceRef}` : '';
|
|
806
|
-
console.log(` ${row.harness} ${row.path}`);
|
|
807
|
-
console.log(` ${row.status} ${row.source}${ref} ${row.contentHash}`);
|
|
808
|
-
if (row.reason)
|
|
809
|
-
console.log(` ${row.reason}`);
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
async function snapshotCommand(args) {
|
|
813
|
-
const action = args[0];
|
|
814
|
-
const requested = flag(args, '--scope') || 'all';
|
|
815
|
-
if (!['global', 'repo', 'all'].includes(requested)) {
|
|
816
|
-
throw new Error('--scope must be global|repo|all');
|
|
817
|
-
}
|
|
818
|
-
const scopes = requested === 'all' ? ['global', 'repo'] : [requested];
|
|
819
|
-
if (action === 'create') {
|
|
820
|
-
const snapshotName = normalizeSnapshotName(flag(args, '--name'));
|
|
821
|
-
const harness = parseOptionalHarness(flag(args, '--harness'));
|
|
822
|
-
const fromRef = flag(args, '--from-ref');
|
|
823
|
-
let count = 0;
|
|
824
|
-
for (const scope of scopes) {
|
|
825
|
-
count += await snapshotCreateOne({
|
|
826
|
-
scope,
|
|
827
|
-
args,
|
|
828
|
-
snapshotName,
|
|
829
|
-
fromRef,
|
|
830
|
-
force: args.includes('--force'),
|
|
831
|
-
harness,
|
|
832
|
-
});
|
|
833
|
-
}
|
|
834
|
-
if (count > 0)
|
|
835
|
-
console.log('Snapshot storage only changed rollback metadata; canon render bytes are unchanged.');
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
if (action === 'list') {
|
|
839
|
-
for (const scope of scopes)
|
|
840
|
-
await snapshotListOne(scope, args);
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
if (action === 'show') {
|
|
844
|
-
const snapshotName = normalizeSnapshotName(args[1] && !args[1].startsWith('-') ? args[1] : flag(args, '--name'));
|
|
845
|
-
for (const scope of scopes)
|
|
846
|
-
await snapshotShowOne(scope, args, snapshotName);
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
throw new Error('snapshot requires create, list, or show');
|
|
144
|
+
if (!write.ok)
|
|
145
|
+
throw new Error(`instructions mirror write failed: HTTP ${write.status}`);
|
|
146
|
+
return 'mirrored';
|
|
850
147
|
}
|
|
851
|
-
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
148
|
+
function help(commandName) {
|
|
149
|
+
const aliasNote = commandName === 'agents'
|
|
150
|
+
? '\n\nNOTE\n `greprag agents` is a deprecated alias. Use `greprag instructions`.'
|
|
151
|
+
: '';
|
|
152
|
+
return `greprag ${commandName} — AGENTS.md files rendered from ~/.claude/CLAUDE.md
|
|
153
|
+
|
|
154
|
+
USAGE
|
|
155
|
+
greprag ${commandName} render [--check] Rewrite ~/.codex/AGENTS.md and ~/.config/opencode/AGENTS.md,
|
|
156
|
+
then refresh GrepRAG's read-only copy of the committed CLAUDE.md
|
|
157
|
+
greprag ${commandName} status Report whether each AGENTS.md matches its render (no writes)
|
|
158
|
+
|
|
159
|
+
MODEL
|
|
160
|
+
~/.claude/CLAUDE.md in the dotclaude git repo is the only source. Edit it there
|
|
161
|
+
and commit. Each AGENTS.md is CLAUDE.md plus an optional addition kept in git
|
|
162
|
+
at ~/.claude/instructions/<codex|opencode>.md. Session start runs the render.
|
|
163
|
+
Repo CLAUDE.md/AGENTS.md files are ordinary files in their repos.
|
|
164
|
+
|
|
165
|
+
REMOVED
|
|
166
|
+
pull, push, sync, adopt, revise, overlay, snapshot and restore refuse: GrepRAG
|
|
167
|
+
no longer holds instruction canon.${aliasNote}`;
|
|
871
168
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
async function bootstrapAgentDoctrine(apiKey, harness) {
|
|
876
|
-
const result = {
|
|
877
|
-
canonicalScopes: [], currentFollowers: 0, changedFollowers: 0, blockedFollowers: 0,
|
|
878
|
-
};
|
|
879
|
-
for (const scope of ['global', 'repo']) {
|
|
880
|
-
const stats = await syncOne({
|
|
881
|
-
scope, args: [], check: false, force: false, apiKey, harness, quiet: true, seedOnly: true,
|
|
882
|
-
});
|
|
883
|
-
if (stats.canonical)
|
|
884
|
-
result.canonicalScopes.push(scope);
|
|
885
|
-
result.currentFollowers += stats.current;
|
|
886
|
-
result.changedFollowers += stats.changed;
|
|
887
|
-
result.blockedFollowers += stats.blocked;
|
|
888
|
-
}
|
|
889
|
-
return result;
|
|
169
|
+
function printResults(results) {
|
|
170
|
+
for (const r of results)
|
|
171
|
+
console.log(` ${r.status.padEnd(9)} ${r.path}`);
|
|
890
172
|
}
|
|
891
173
|
async function runInstructionsCommand(args, commandName) {
|
|
892
174
|
const sub = args[0];
|
|
@@ -894,28 +176,25 @@ async function runInstructionsCommand(args, commandName) {
|
|
|
894
176
|
console.log(help(commandName));
|
|
895
177
|
return;
|
|
896
178
|
}
|
|
897
|
-
if (sub === '
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
return
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
throw new Error(`${commandName} optimize was removed: use lossless ${commandName} pull or explicit ${commandName} overlay`);
|
|
179
|
+
if (sub === 'status' || (sub === 'render' && args.includes('--check'))) {
|
|
180
|
+
const results = renderGlobalInstructions({ check: true });
|
|
181
|
+
printResults(results);
|
|
182
|
+
if (results.some(r => r.status === 'stale'))
|
|
183
|
+
process.exitCode = 1;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (sub === 'render') {
|
|
187
|
+
printResults(renderGlobalInstructions());
|
|
188
|
+
const apiKey = process.env.GREPRAG_API_KEY || '';
|
|
189
|
+
if (apiKey) {
|
|
190
|
+
const apiUrl = process.env.GREPRAG_API_URL || 'https://api.greprag.com';
|
|
191
|
+
console.log(` mirror ${await mirrorCommittedInstructions({ apiUrl, apiKey })}`);
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
913
194
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
return syncCommand(args.slice(1), true);
|
|
918
|
-
throw new Error(`unknown ${commandName} command: ${sub}`);
|
|
195
|
+
throw new Error(`\`greprag ${commandName} ${sub}\` was removed: ~/.claude/CLAUDE.md in the dotclaude git repo is the source. `
|
|
196
|
+
+ `Edit it there and commit; \`greprag ${commandName} render\` rewrites the AGENTS.md files `
|
|
197
|
+
+ '(per-harness additions live in ~/.claude/instructions/<codex|opencode>.md).');
|
|
919
198
|
}
|
|
920
199
|
async function runInstructions(args) {
|
|
921
200
|
return runInstructionsCommand(args, 'instructions');
|