greprag 5.79.0 → 5.80.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.
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ /** `greprag announce` — print the full SessionStart announce.
3
+ *
4
+ * The Claude Code harness inlines only ~2KB of hook context (measured; see
5
+ * ANNOUNCE_INLINE_BUDGET). The recap hook therefore parks the complete announce
6
+ * under ~/.greprag/announce/<session>.md and inlines a pointer naming this
7
+ * command. Without it the overflow is unreachable — which is exactly the
8
+ * failure this exists to end.
9
+ *
10
+ * adr: adr/announce-inline-budget.md
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.runAnnounce = runAnnounce;
47
+ const fs = __importStar(require("fs"));
48
+ const os = __importStar(require("os"));
49
+ const path = __importStar(require("path"));
50
+ const session_id_1 = require("../session-id");
51
+ function announceDir() {
52
+ return path.join(os.homedir(), '.greprag', 'announce');
53
+ }
54
+ /** Newest cached announce, by mtime. Used when no session id is given — the
55
+ * common case, since an agent asking for its own announce rarely wants to
56
+ * first go find its session id. */
57
+ function newestCached() {
58
+ try {
59
+ const dir = announceDir();
60
+ const files = fs.readdirSync(dir)
61
+ .filter(f => f.endsWith('.md'))
62
+ .map(f => ({ f, t: fs.statSync(path.join(dir, f)).mtimeMs }))
63
+ .sort((a, b) => b.t - a.t);
64
+ return files.length ? path.join(dir, files[0].f) : null;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ function runAnnounce(args) {
71
+ if (args.includes('--help') || args.includes('-h')) {
72
+ process.stdout.write('greprag announce — print the full SessionStart announce\n\n'
73
+ + 'USAGE\n'
74
+ + ' greprag announce [--session <8hex>] [--path]\n\n'
75
+ + ' The harness inlines only ~2KB of session-start context. The rest is\n'
76
+ + ' parked on disk; this prints all of it.\n\n'
77
+ + ' --session <8hex> a specific session (default: most recent)\n'
78
+ + ' --path print the cache file path instead of its contents\n');
79
+ return;
80
+ }
81
+ const i = args.indexOf('--session');
82
+ const wanted = i !== -1 ? args[i + 1] : undefined;
83
+ const file = wanted
84
+ ? path.join(announceDir(), `${(0, session_id_1.truncateSessionId)(wanted) || wanted}.md`)
85
+ : newestCached();
86
+ if (!file || !fs.existsSync(file)) {
87
+ process.stderr.write('No cached announce found. It is written at SessionStart, so a session '
88
+ + 'must have started since this greprag version was installed.\n');
89
+ process.exitCode = 1;
90
+ return;
91
+ }
92
+ if (args.includes('--path')) {
93
+ process.stdout.write(file + '\n');
94
+ return;
95
+ }
96
+ process.stdout.write(fs.readFileSync(file, 'utf-8').trimEnd() + '\n');
97
+ }
@@ -1459,13 +1459,26 @@ function applySettings(settings, apiKey) {
1459
1459
  matcher: '',
1460
1460
  hooks: [{ type: 'command', command: 'greprag-hook store', timeout: 10000 }],
1461
1461
  };
1462
+ // Migrate stale installs onto the every-source matcher. `hasGrepragHook` only
1463
+ // asks whether a recap hook EXISTS, so an install written by an older version
1464
+ // kept its narrow matcher forever while init reported "already configured".
1465
+ // Field state 2026-09-03: `matcher: 'startup'`, so a resumed session — and
1466
+ // every /new and dashboard-dispatched agent — got no announce at all, Persona
1467
+ // included. Same failure and same fix as the Grok path above.
1468
+ // adr: adr/announce-inline-budget.md
1469
+ const retargetedRecap = settings.hooks.SessionStart
1470
+ ? retargetGrepragHookMatcher(settings.hooks.SessionStart, 'recap', ['startup', 'startup|resume', 'startup|resume|compact', 'startup|clear|compact'], recapHook.matcher)
1471
+ : 0;
1472
+ if (retargetedRecap > 0) {
1473
+ changes.push(`Retargeted SessionStart recap matcher to every source (${retargetedRecap})`);
1474
+ }
1462
1475
  if (!hasGrepragHook(settings.hooks.SessionStart, 'recap')) {
1463
1476
  if (!settings.hooks.SessionStart)
1464
1477
  settings.hooks.SessionStart = [];
1465
1478
  settings.hooks.SessionStart.push(recapHook);
1466
1479
  changes.push('Added SessionStart hook (memory recap + inbox digest)');
1467
1480
  }
1468
- else {
1481
+ else if (retargetedRecap === 0) {
1469
1482
  changes.push('SessionStart hook already configured (skipped)');
1470
1483
  }
1471
1484
  if (!hasGrepragHook(settings.hooks.Stop, 'store')) {
@@ -5,10 +5,12 @@
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.compactReannounceModules = exports.commandModules = exports.promptModules = exports.harnessModules = exports.REGISTRY = void 0;
8
+ exports.ANNOUNCE_HARNESS_CAP = exports.ANNOUNCE_INLINE_BUDGET = exports.compactReannounceModules = exports.commandModules = exports.promptModules = exports.harnessModules = exports.REGISTRY = void 0;
9
9
  exports.collectReminders = collectReminders;
10
10
  exports.bootOrder = bootOrder;
11
11
  exports.collectAnnounces = collectAnnounces;
12
+ exports.collectAnnounceBlocks = collectAnnounceBlocks;
13
+ exports.fitAnnounceBudget = fitAnnounceBudget;
12
14
  const os_primer_reminder_1 = require("./os-primer-reminder");
13
15
  const inbox_primer_reminder_1 = require("./inbox-primer-reminder");
14
16
  const load_primer_reminder_1 = require("./load-primer-reminder");
@@ -147,3 +149,89 @@ function collectAnnounces(env, registry = exports.REGISTRY) {
147
149
  }
148
150
  return out;
149
151
  }
152
+ /** Same as collectAnnounces, but keeps each block paired with the module that
153
+ * produced it so the budget fitter can rank and name them.
154
+ * adr: adr/announce-inline-budget.md */
155
+ function collectAnnounceBlocks(env, registry = exports.REGISTRY) {
156
+ const out = [];
157
+ for (const m of bootOrder(registry)) {
158
+ let a = null;
159
+ try {
160
+ a = m.announce(env);
161
+ }
162
+ catch {
163
+ continue;
164
+ }
165
+ if (a)
166
+ out.push({ id: m.id, text: a });
167
+ }
168
+ return out;
169
+ }
170
+ /** Bytes of SessionStart context a harness will actually inline. MEASURED, not
171
+ * guessed: Claude Code wraps any hook output over 2048 bytes in
172
+ * `<persisted-output>`, spills the full text to a tool-results file, and injects
173
+ * only "Preview (first 2KB)". This is true of BOTH raw stdout and the
174
+ * `additionalContext` envelope — the cap is on inline context, not the channel.
175
+ * Measured 2026-09-03 with an 80-marker ruler: markers 250..2000 arrived, 2250
176
+ * and beyond did not. 1800 leaves headroom for the harness's own wrapper text.
177
+ * adr: adr/announce-inline-budget.md */
178
+ exports.ANNOUNCE_INLINE_BUDGET = 1650;
179
+ /** The harness's hard ceiling. Past this, output is replaced by a 2KB preview and
180
+ * a `<persisted-output>` file reference. ANNOUNCE_INLINE_BUDGET must stay under
181
+ * it with room for the trailing memory-recap pointer. */
182
+ exports.ANNOUNCE_HARNESS_CAP = 2048;
183
+ /** Inline-worthiness ranking, most-keepable first. The rule: a block earns inline
184
+ * space when it is SHORT, SESSION-SPECIFIC, and available nowhere else. A block
185
+ * that is long, static, and already loadable on demand does not — it is exactly
186
+ * what `greprag load` exists for. Anything unlisted sorts last.
187
+ *
188
+ * Persona leads because it is 523 bytes of tenant-set speaking instructions that
189
+ * no other surface carries. The grepragOS laws are deliberately NOT here: 1753
190
+ * bytes of static doctrine that `greprag load os` already serves on demand, which
191
+ * until now consumed the entire budget and starved everything behind it. */
192
+ const ANNOUNCE_PRIORITY = [
193
+ 'persona-announce',
194
+ 'setup-warning',
195
+ 'version-upgrade',
196
+ 'enrichment-health',
197
+ 'watcher-arm',
198
+ 'skill-mirror-announce',
199
+ 'delivery-control',
200
+ 'doc-pointer-announce',
201
+ ];
202
+ function announceRank(id) {
203
+ const i = ANNOUNCE_PRIORITY.indexOf(id);
204
+ return i === -1 ? ANNOUNCE_PRIORITY.length : i;
205
+ }
206
+ /** Fit the announce into the harness's inline budget.
207
+ *
208
+ * Returns the blocks that fit (restored to boot order, so a primer still precedes
209
+ * anything that depends on it) plus the ids that did not. The caller is expected
210
+ * to persist the FULL text and give the agent a way to read it — dropping content
211
+ * silently is the failure this whole mechanism exists to end.
212
+ *
213
+ * A single block larger than the budget is never emitted; it would blow the cap
214
+ * on its own and take everything after it down too. */
215
+ function fitAnnounceBudget(blocks, budget = exports.ANNOUNCE_INLINE_BUDGET, reserve = 0) {
216
+ const order = new Map(blocks.map((b, i) => [b.id, i]));
217
+ const ranked = [...blocks].sort((a, b) => {
218
+ const d = announceRank(a.id) - announceRank(b.id);
219
+ return d !== 0 ? d : (order.get(a.id) - order.get(b.id));
220
+ });
221
+ const kept = [];
222
+ const droppedIds = [];
223
+ let used = reserve;
224
+ const SEP = 2; // the '\n\n' join between blocks
225
+ for (const b of ranked) {
226
+ const cost = b.text.length + (kept.length ? SEP : 0);
227
+ if (used + cost <= budget) {
228
+ kept.push(b);
229
+ used += cost;
230
+ }
231
+ else {
232
+ droppedIds.push(b.id);
233
+ }
234
+ }
235
+ kept.sort((a, b) => order.get(a.id) - order.get(b.id));
236
+ return { kept, droppedIds };
237
+ }
package/dist/hook.js CHANGED
@@ -40,8 +40,10 @@ var __importStar = (this && this.__importStar) || (function () {
40
40
  };
41
41
  })();
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.announceCachePath = announceCachePath;
43
44
  const path = __importStar(require("path"));
44
45
  const fs = __importStar(require("fs"));
46
+ const os = __importStar(require("os"));
45
47
  const crypto = __importStar(require("crypto"));
46
48
  const proc_1 = require("./proc");
47
49
  const docptr_refs_1 = require("./docptr-refs");
@@ -1334,6 +1336,43 @@ function grokSidecarHead(short, full) {
1334
1336
  + 'Chip: `greprag load grok-chip-spawn` then `greprag grok spawn`. Helper: spawn_subagent. Parent keeps ONE watch. Peers: `greprag inbox watchers` then `greprag send`.\n'
1335
1337
  + 'If a greprag inbox monitor is already listed, do not start another. Instant exit = already armed.\n\n';
1336
1338
  }
1339
+ /** Where the full, unabridged announce is parked for on-demand reading.
1340
+ * adr: adr/announce-inline-budget.md */
1341
+ function announceCachePath(short) {
1342
+ return path.join(os.homedir(), '.greprag', 'announce', `${short}.md`);
1343
+ }
1344
+ /** Assemble the SessionStart announce so it SURVIVES the harness's inline cap.
1345
+ *
1346
+ * Writes the full text to the announce cache, inlines the highest-value blocks
1347
+ * that fit the budget, and — when anything was left out — appends one line that
1348
+ * NAMES the omitted sections and the command that prints them. The old behavior
1349
+ * emitted all 18.5KB and let the harness silently keep the first 2KB.
1350
+ *
1351
+ * Returns null when there is nothing to announce. */
1352
+ function buildInlineAnnounce(blocks, short) {
1353
+ if (!blocks.length)
1354
+ return null;
1355
+ const full = blocks.map(b => b.text).join('\n\n');
1356
+ if (short) {
1357
+ try {
1358
+ const file = announceCachePath(short);
1359
+ fs.mkdirSync(path.dirname(file), { recursive: true });
1360
+ fs.writeFileSync(file, full, 'utf-8');
1361
+ }
1362
+ catch { /* cache is a convenience; never block session start */ }
1363
+ }
1364
+ // Reserve room for the pointer line before fitting, so adding it can never be
1365
+ // what pushes the payload over the cap.
1366
+ const POINTER_RESERVE = 220;
1367
+ const { kept, droppedIds } = (0, reminder_registry_1.fitAnnounceBudget)(blocks, reminder_registry_1.ANNOUNCE_INLINE_BUDGET, POINTER_RESERVE);
1368
+ if (!droppedIds.length)
1369
+ return kept.map(b => b.text).join('\n\n') || null;
1370
+ const names = droppedIds.join(', ');
1371
+ const pointer = `[greprag announce — ${droppedIds.length} more section(s) not inlined `
1372
+ + `(harness caps SessionStart context at ${reminder_registry_1.ANNOUNCE_INLINE_BUDGET}b): ${names}. `
1373
+ + `Read them with \`greprag announce\`.]`;
1374
+ return [...kept.map(b => b.text), pointer].join('\n\n');
1375
+ }
1337
1376
  function writeRecapOutput(text, mode, grokShort, grokFull) {
1338
1377
  if (grokShort)
1339
1378
  (0, grok_session_1.writeGrokSidecar)(grokShort, grokSidecarHead(grokShort, grokFull) + (text || ''));
@@ -1737,7 +1776,12 @@ async function recap(input, mode = 'plain', opts = {}) {
1737
1776
  // adr: adr/monitor-resilience.md, docs/reminder-interrupt.md
1738
1777
  if (opts.compact)
1739
1778
  announceReg = (0, reminder_registry_1.compactReannounceModules)(announceReg);
1740
- const announceBlock = (0, reminder_registry_1.collectAnnounces)(announceEnv, announceReg).join('\n\n') || null;
1779
+ // Fit the announce to what the harness will actually inline (2048 bytes; see
1780
+ // ANNOUNCE_INLINE_BUDGET). The full text is persisted first and the overflow is
1781
+ // NAMED, so nothing is lost silently — the failure this replaces was an 18.5KB
1782
+ // announce of which only the first ~2KB ever reached a session, with no signal
1783
+ // that the rest existed. adr: adr/announce-inline-budget.md
1784
+ const announceBlock = buildInlineAnnounce((0, reminder_registry_1.collectAnnounceBlocks)(announceEnv, announceReg), (0, session_id_1.truncateSessionId)(input.session_id));
1741
1785
  if (opts.compact) {
1742
1786
  writeRecapOutput(announceBlock ? announceBlock + '\n' : '', mode, grokShort, input.session_id);
1743
1787
  return;
@@ -1773,7 +1817,17 @@ async function recap(input, mode = 'plain', opts = {}) {
1773
1817
  parts.push(announceBlock);
1774
1818
  parts.push('');
1775
1819
  }
1776
- parts.push(body);
1820
+ // The memory recap shares the SAME inline cap as the announce — it is appended
1821
+ // to the very payload the harness truncates. Inline it only if it fits in what
1822
+ // the announce left; otherwise name the command that prints it. Silently
1823
+ // emitting a body that pushes the payload over the cap is what made the whole
1824
+ // announce disappear. adr: adr/announce-inline-budget.md
1825
+ const usedSoFar = parts.join('\n').length;
1826
+ const roomForBody = reminder_registry_1.ANNOUNCE_INLINE_BUDGET - usedSoFar;
1827
+ parts.push(body.length <= roomForBody
1828
+ ? body
1829
+ : '[greprag memory — session recap not inlined (over the SessionStart cap). '
1830
+ + 'Read it with `greprag memory recap`.]');
1777
1831
  writeRecapOutput(parts.join('\n') + '\n', mode, grokShort, input.session_id);
1778
1832
  }
1779
1833
  /** Arm-state detection moved LOCAL (2026-06-04). The former `isSessionArmed`
@@ -2233,7 +2287,12 @@ async function main() {
2233
2287
  }
2234
2288
  const harness = (0, harness_1.inferCurrentHarness)();
2235
2289
  if (subcommand === 'recap') {
2236
- await recap(input, harness === 'grok' ? 'additionalContext' : 'plain', {
2290
+ // ALWAYS additionalContext. Raw stdout is treated by the Claude Code harness
2291
+ // like command output: past ~2KB it spills to a file and injects a preview,
2292
+ // which silently ate ~89% of an 18.7KB announce (Persona sits at byte 10,841).
2293
+ // additionalContext is the documented injection channel. Grok keeps its
2294
+ // sidecar via the platform arg. adr: adr/announce-delivery-channel.md
2295
+ await recap(input, 'additionalContext', {
2237
2296
  platform: harness === 'grok' ? 'grok' : undefined,
2238
2297
  });
2239
2298
  }
package/dist/index.js CHANGED
@@ -299,6 +299,10 @@ function readProjectRegistry() {
299
299
  * desk ensure idempotently spawn a detached desk-line if none is running
300
300
  * desk status is a desk-line up + what local truth would it report now
301
301
  * adr: adr/desk-line-relay.md */
302
+ async function runAnnounce(args) {
303
+ const { runAnnounce: run } = await Promise.resolve().then(() => __importStar(require('./commands/announce')));
304
+ run(args);
305
+ }
302
306
  async function runDesk(args) {
303
307
  ensureEnv(); // load ~/.greprag/.env so GREPRAG_API_KEY is present (self-heals in the detached child)
304
308
  const sub = args[0] || 'status';
@@ -1906,6 +1910,10 @@ async function main() {
1906
1910
  return;
1907
1911
  }
1908
1912
  case 'inbox': return inbox(subArgs);
1913
+ // The full SessionStart announce. The harness inlines only ~2KB of it, so the
1914
+ // overflow is parked here and the inline pointer names this command.
1915
+ // adr: adr/announce-inline-budget.md
1916
+ case 'announce': return runAnnounce(subArgs);
1909
1917
  case 'desk': return runDesk(subArgs);
1910
1918
  case 'email': return (0, email_1.runEmail)(subArgs);
1911
1919
  case 'send': return send(subArgs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "greprag",
3
- "version": "5.79.0",
3
+ "version": "5.80.0",
4
4
  "description": "Private GrepRAG client for existing Claude Code, Codex, OpenCode, and Grok Build tenants.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {