greprag 5.84.0 → 5.85.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,16 @@
1
1
  "use strict";
2
- /** `greprag instructions` — canonical GrepRAG Instructions adoption/materialization.
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
- * `greprag agents` remains as a compatibility alias. */
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.collectOriginalSnapshots = collectOriginalSnapshots;
40
- exports.bootstrapAgentDoctrine = bootstrapAgentDoctrine;
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
- const agent_doctrine_file_1 = require("./agent-doctrine-file");
50
- function help(commandName) {
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 gitFileAt(root, ref, relativePath) {
110
- try {
111
- return String((0, proc_1.safeExecFileSync)('git', ['-C', root, 'show', `${ref}:${relativePath}`], {
112
- encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
113
- maxBuffer: 1024 * 1024,
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 firstInstructionTouch(root, relativePath) {
70
+ function readOrNull(file) {
121
71
  try {
122
- const out = String((0, proc_1.safeExecFileSync)('git', [
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
- function looksManagedOrRenamed(content) {
142
- return /Agent Doctrine|Doctrine Mirror|agent doctrine|greprag agents|greprag instructions|GrepRAG Instructions/i.test(content);
143
- }
144
- function relativeDirectory(root, requested) {
145
- const absolute = requested
146
- ? path.resolve(root, requested)
147
- : path.resolve(process.cwd());
148
- const relative = path.relative(root, absolute).replace(/\\/g, '/') || '.';
149
- if (relative === '..' || relative.startsWith('../')) {
150
- throw new Error('doctrine directory must be inside the current repo');
151
- }
152
- return relative;
153
- }
154
- function globalTargets() {
155
- const home = os.homedir();
156
- return [
157
- {
158
- harness: 'claude-code',
159
- actualPath: path.join(home, '.claude', 'CLAUDE.md'),
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
- async function fetchFollowerBaselines(unitName, apiKey) {
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
- const body = await apiJson('/v1/parity/list', undefined, apiKey);
408
- const unit = body.units.find((candidate) => candidate.name === unitName);
409
- const baselines = new Map();
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 new Map();
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
- async function overlayCommand(args) {
573
- const action = args[0];
574
- const harnessRaw = args[1];
575
- if (action !== 'set' && action !== 'clear' && action !== 'derive') {
576
- throw new Error('overlay requires set, derive, or clear');
577
- }
578
- const harness = inferHarness(undefined, harnessRaw);
579
- if (harnessRaw !== harness)
580
- throw new Error('overlay harness must be claude-code, codex, or opencode');
581
- const file = action === 'set' ? args[2] : undefined;
582
- if (action === 'set' && !file)
583
- throw new Error('overlay set requires a source file');
584
- const sharedFile = action === 'derive' ? args[2] : undefined;
585
- const renderedFile = action === 'derive' ? args[3] : undefined;
586
- if (action === 'derive' && (!sharedFile || !renderedFile)) {
587
- throw new Error('overlay derive requires <shared-file> <rendered-file>');
588
- }
589
- const sourcePath = file ? path.resolve(file) : null;
590
- if (sourcePath && !fs.existsSync(sourcePath))
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
- projectId: ctx.projectId,
620
- directory: ctx.directory,
621
- sourceHarness: harness,
622
- sourcePath: renderedPath || sourcePath,
133
+ scope: 'global',
134
+ directory: '.',
135
+ sourceHarness: 'claude-code',
136
+ sourcePath: instructionSource(home),
623
137
  content,
624
- overlayHarness: harness,
625
- expectedRevision: current.revision,
626
- followers: ctx.targets.map((target) => ({
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
- const present = snapshots.filter((snapshot) => snapshot.status === 'present').length;
768
- console.log(`${params.scope}: stored snapshot '${params.snapshotName}' for ${snapshots.length} follower(s) (${present} present) at revision ${body.document.revision}.`);
769
- return snapshots.length;
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
- async function syncCommand(args, checkOverride = false) {
852
- const requested = flag(args, '--scope') || 'all';
853
- if (!['global', 'repo', 'all'].includes(requested)) {
854
- throw new Error('--scope must be global|repo|all');
855
- }
856
- const scopes = requested === 'all' ? ['global', 'repo'] : [requested];
857
- const check = checkOverride || args.includes('--check');
858
- let drift = 0;
859
- for (const scope of scopes) {
860
- console.log(`${scope}:`);
861
- const stats = await syncOne({ scope, args, check, force: args.includes('--force') });
862
- drift += stats.drift;
863
- }
864
- if (check && drift > 0) {
865
- console.error(`${drift} follower(s) need sync or reconciliation.`);
866
- process.exitCode = 1;
867
- }
868
- else if (check) {
869
- console.log('All materialized followers are current.');
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
- /** Init-time seed for the active harness. No canon is a clean no-op; only a
873
- * missing/empty follower is written. Every existing nonmatching file requires
874
- * an explicit `instructions push` or `instructions pull`. */
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 === 'pull')
898
- return exactSourceCommand(args.slice(1), 'pull');
899
- if (sub === 'push')
900
- return syncCommand(args.slice(1));
901
- if (sub === 'adopt')
902
- return exactSourceCommand(args.slice(1), 'adopt');
903
- if (sub === 'revise')
904
- return exactSourceCommand(args.slice(1), 'revise');
905
- if (sub === 'overlay')
906
- return overlayCommand(args.slice(1));
907
- if (sub === 'snapshot')
908
- return snapshotCommand(args.slice(1));
909
- if (sub === 'restore')
910
- return restoreCommand(args.slice(1));
911
- if (sub === 'optimize') {
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
- if (sub === 'sync')
915
- return syncCommand(args.slice(1));
916
- if (sub === 'status')
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');