greprag 5.61.1 → 5.63.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.
Files changed (45) hide show
  1. package/dist/codex-chip-hooks.js +2 -1
  2. package/dist/codex-chip-hooks.js.map +1 -1
  3. package/dist/commands/arm-reminder.js +5 -0
  4. package/dist/commands/arm-reminder.js.map +1 -1
  5. package/dist/commands/codex-chip/prompt.js +1 -1
  6. package/dist/commands/codex-chip/prompt.js.map +1 -1
  7. package/dist/commands/inbox-primer-reminder.js +10 -2
  8. package/dist/commands/inbox-primer-reminder.js.map +1 -1
  9. package/dist/commands/load-primer-reminder.d.ts +6 -0
  10. package/dist/commands/load-primer-reminder.js +22 -2
  11. package/dist/commands/load-primer-reminder.js.map +1 -1
  12. package/dist/commands/mechanic-manifest.d.ts +12 -0
  13. package/dist/commands/mechanic-manifest.js +62 -0
  14. package/dist/commands/mechanic-manifest.js.map +1 -1
  15. package/dist/commands/mechanic-spawn.js +37 -2
  16. package/dist/commands/mechanic-spawn.js.map +1 -1
  17. package/dist/commands/mechanic.js +29 -1
  18. package/dist/commands/mechanic.js.map +1 -1
  19. package/dist/commands/opencode-chip-mission.js +1 -0
  20. package/dist/commands/opencode-chip-mission.js.map +1 -1
  21. package/dist/commands/opencode-interrupt.d.ts +15 -5
  22. package/dist/commands/opencode-interrupt.js +22 -20
  23. package/dist/commands/opencode-interrupt.js.map +1 -1
  24. package/dist/commands/os-primer-reminder.js +5 -2
  25. package/dist/commands/os-primer-reminder.js.map +1 -1
  26. package/dist/commands/reminder-registry.d.ts +7 -0
  27. package/dist/commands/reminder-registry.js +9 -1
  28. package/dist/commands/reminder-registry.js.map +1 -1
  29. package/dist/commands/reminder-types.d.ts +13 -1
  30. package/dist/commands/state-trigger.d.ts +17 -0
  31. package/dist/commands/state-trigger.js +152 -16
  32. package/dist/commands/state-trigger.js.map +1 -1
  33. package/dist/index.js +1 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/opencode-plugin-png.d.ts +151 -0
  36. package/dist/opencode-plugin-png.js +275 -0
  37. package/dist/opencode-plugin-png.js.map +1 -0
  38. package/dist/opencode-plugin.bundle.js +683 -29
  39. package/dist/opencode-plugin.js +76 -1
  40. package/dist/opencode-plugin.js.map +1 -1
  41. package/package.json +2 -2
  42. package/skill/greprag/docs/codex-chip.md +5 -0
  43. package/skill/mechanic/SKILL.md +5 -0
  44. package/skill/templates/chip-spawn.md +8 -0
  45. package/skill/templates/codex-chip-spawn.md +23 -0
@@ -1747,7 +1747,7 @@ function crushMessages(messages, opts) {
1747
1747
 
1748
1748
  // src/commands/os-primer-reminder.ts
1749
1749
  function buildOsPrimer(env) {
1750
- const spawnEntry = env?.platform === "codex" ? "codex-chip-spawn" : "chip-spawn";
1750
+ const spawnEntry = env?.platform === "codex" ? "codex-chip-spawn" : env?.platform === "opencode" ? "chip-bootloader" : "chip-spawn";
1751
1751
  return [
1752
1752
  "[grepragOS \u2014 the operating laws. Full doctrine: `greprag load os`.]",
1753
1753
  '\u2022 Doctrine vs state: methods ship in the CLI (`greprag load`); live state lives in the repo. A skill that depends on repo state carries a "STATE \u2014 read these first" block naming exact paths.',
@@ -1776,14 +1776,18 @@ function armMonitorCommand(short, ownerPid, assistant = false, mechanic = false)
1776
1776
  // src/commands/inbox-primer-reminder.ts
1777
1777
  function buildInboxPrimer(env) {
1778
1778
  const codex = env.platform === "codex";
1779
+ const opencode = env.platform === "opencode";
1779
1780
  const arm = armMonitorCommand(env.short, env.ownerPid, env.assistant, env.mechanic);
1780
1781
  return [
1781
1782
  // LEAD — the forcing function (restored 2026-06-22). ARM is an ALARM, not reference:
1782
1783
  // you are unreachable until you do it, and it must be re-done every time the watcher
1783
1784
  // drops. It leads the announce and is loud by design — folding it into a buried bullet
1784
1785
  // (v5.49.6) softened it into banner-blindness (fix 16a29a8b's failure mode).
1785
- codex ? "[CODEX INBOX \u2014 greprag is your agent-to-agent mesh. Codex-to-Codex: use native task messaging. Reaching ANY non-Codex peer (Claude Code / opencode / another tenant's agent): `greprag send` IS the live primary path \u2014 native Codex messaging cannot reach them.]" : "[\u26A0 ARM YOUR INBOX WATCHER NOW \u2014 you are UNREACHABLE until you do. Peers are live in your mesh trying to coordinate with this session, and the operator can DM you \u2014 but they reach you ONLY while armed. Unarmed, you are invisible and their messages silently pile up. The watcher can DROP ANYTIME (crash / reload / OOM) \u2014 EVERY turn you are flagged unarmed you MUST re-arm, or coordination breaks.]",
1786
- codex ? "CODEX: Native task/thread messaging is the normal live Codex-to-Codex path. Install the GrepRAG startup watcher with `greprag init --codex --tenant-id <handle> --install-watcher` or `greprag codex startup install` \u2014 it is how messages from non-Codex peers (Claude Code / opencode) reach you; without it they pile up unread. Turn hooks are boundary fallback: if hooks exist but do not fire, open Codex Desktop Settings -> Settings -> Hooks, trust the GrepRAG commands, and start a fresh Codex session. Then drain what is waiting: `greprag inbox`." : `ARM: Monitor is a deferred tool \u2014 \`ToolSearch select:Monitor\`, then call Monitor with persistent:true, timeout_ms:3600000, description:"greprag inbox ${env.short}", command: \`${arm}\`. The \`--session ${env.short}\` is REQUIRED \u2014 a bare \`greprag inbox watch\` writes no pidfile, so the arm-check reads you unarmed forever. NEVER run it bare also because it is a foreground stream and hangs the turn. Then drain what is waiting: \`greprag inbox\`.`,
1786
+ // opencode gets NO alarm: its relay self-arms in the plugin (startSessionRelay) —
1787
+ // "ToolSearch select:Monitor" is unactionable there (neither tool exists) and an
1788
+ // un-followable alarm decays into banner-blindness for the announces around it.
1789
+ codex ? "[CODEX INBOX \u2014 greprag is your agent-to-agent mesh. Codex-to-Codex: use native task messaging. Reaching ANY non-Codex peer (Claude Code / opencode / another tenant's agent): `greprag send` IS the live primary path \u2014 native Codex messaging cannot reach them.]" : opencode ? "[OPENCODE INBOX \u2014 greprag is your agent-to-agent mesh. Inbound delivery is AUTOMATIC here: the greprag plugin arms a relay for this session (nothing to arm or re-arm) and inbound peer messages arrive as injected turns.]" : "[\u26A0 ARM YOUR INBOX WATCHER NOW \u2014 you are UNREACHABLE until you do. Peers are live in your mesh trying to coordinate with this session, and the operator can DM you \u2014 but they reach you ONLY while armed. Unarmed, you are invisible and their messages silently pile up. The watcher can DROP ANYTIME (crash / reload / OOM) \u2014 EVERY turn you are flagged unarmed you MUST re-arm, or coordination breaks.]",
1790
+ codex ? "CODEX: Native task/thread messaging is the normal live Codex-to-Codex path. Install the GrepRAG startup watcher with `greprag init --codex --tenant-id <handle> --install-watcher` or `greprag codex startup install` \u2014 it is how messages from non-Codex peers (Claude Code / opencode) reach you; without it they pile up unread. Turn hooks are boundary fallback: if hooks exist but do not fire, open Codex Desktop Settings -> Settings -> Hooks, trust the GrepRAG commands, and start a fresh Codex session. Then drain what is waiting: `greprag inbox`." : opencode ? "DELIVERY: the relay arms itself on this session's first turn and injects inbound messages as they land \u2014 treat an injected `Message from <handle> (session <8hex>):` turn as async peer mail, not the operator typing. Drain anything already waiting: `greprag inbox`." : `ARM: Monitor is a deferred tool \u2014 \`ToolSearch select:Monitor\`, then call Monitor with persistent:true, timeout_ms:3600000, description:"greprag inbox ${env.short}", command: \`${arm}\`. The \`--session ${env.short}\` is REQUIRED \u2014 a bare \`greprag inbox watch\` writes no pidfile, so the arm-check reads you unarmed forever. NEVER run it bare also because it is a foreground stream and hangs the turn. Then drain what is waiting: \`greprag inbox\`.`,
1787
1791
  "",
1788
1792
  // The facts + directives — a flat list (no MODEL/RULES scaffolding; the labels were
1789
1793
  // human doc-structure, dead weight to an agent). Everything that helps the agent DECIDE
@@ -1818,6 +1822,10 @@ var CHIP_SPAWN_POINTER = [
1818
1822
  "[greprag chips \u2014 agent coordination is built in (messaging plumbing in the inbox primer).]",
1819
1823
  "\u2022 About to spawn a chip (`spawn_task`)? \u2192 run `greprag load chip-spawn` FIRST for the method (Block 1 worktree + Block 2 report-back); it carries the multi-chip upgrade path inline. Non-negotiable \u2014 the spawn validator rejects an unstructured chip."
1820
1824
  ].join("\n");
1825
+ var OPENCODE_CHIP_SPAWN_POINTER = [
1826
+ "[greprag chips \u2014 agent coordination is built in (messaging plumbing in the inbox primer).]",
1827
+ "\u2022 Delegating a component of a plan to an isolated chip session, or `greprag fix spawn` just printed a FIX-chip mission? \u2192 run `greprag load chip-bootloader` FIRST for the method: OpenCode chips spawn natively via `greprag opencode chip goal create` + `greprag opencode chip spawn` (HTTP child session; manual paste only when the API is down). \u22652 chips at one objective \u2192 `greprag load chip-leader-opencode` BEFORE the first spawn."
1828
+ ].join("\n");
1821
1829
  var CODEX_CHIP_SPAWN_POINTER = [
1822
1830
  "[greprag Codex delegation \u2014 internal subagent, 1\u20132-chip quick path with the initiator as LEAD, or a separate LEAD for larger/seamed missions.]",
1823
1831
  "\u2022 About to delegate a Codex task? \u2192 run `greprag load codex-chip-spawn` FIRST. Use exact first-line titles `LEAD: <Mission>` and `Chip A/B/C: <Specific Purview>`. Chips are ordinary writable native tasks; completion is a native Codex task reply to the LEAD after committing. A `MECHANIC: <Mission>` task loads `greprag load mechanic` first."
@@ -1833,7 +1841,18 @@ var chipSpawnPointerModule = {
1833
1841
  // Says "greprag load chip-spawn" → the agent must already know the loader exists.
1834
1842
  dependsOn: ["load-primer"],
1835
1843
  detect: (_env) => ({ tier: "silent" }),
1836
- announce: (env) => env.platform === "codex" ? null : CHIP_SPAWN_POINTER,
1844
+ // Per-platform payload, ONE module: codex has its own sibling module below;
1845
+ // opencode gets the native HTTP-spawn pointer (spawn_task doesn't exist there);
1846
+ // claude-code keeps the spawn_task pointer. Gating INSIDE the module (not a
1847
+ // per-harness registry fork) is what lets harnessModules() stay a pure
1848
+ // exclusion filter.
1849
+ announce: (env) => {
1850
+ if (env.platform === "codex")
1851
+ return null;
1852
+ if (env.platform === "opencode")
1853
+ return OPENCODE_CHIP_SPAWN_POINTER;
1854
+ return CHIP_SPAWN_POINTER;
1855
+ },
1837
1856
  reminder: () => null
1838
1857
  };
1839
1858
  var codexChipSpawnPointerModule = {
@@ -1969,6 +1988,11 @@ function buildArmReminder(d, env) {
1969
1988
  }
1970
1989
  var watcherArmModule = {
1971
1990
  id: "watcher-arm",
1991
+ // The arm poke rides Claude Code's Monitor tool (ToolSearch select:Monitor); codex
1992
+ // keeps its historical full-registry behavior. opencode is EXCLUDED: its inbox
1993
+ // delivery is the plugin-armed relay (startSessionRelay), so "arm your Monitor"
1994
+ // is unactionable noise there.
1995
+ harnesses: ["claude-code", "codex"],
1972
1996
  detect: armDetect,
1973
1997
  announce: () => null,
1974
1998
  reminder: (d, env) => buildArmReminder(d, env)
@@ -2298,6 +2322,7 @@ var REGISTRY = [
2298
2322
  collisionMatchModule
2299
2323
  // source: 'command' — evaluated at PreToolUse, not UserPromptSubmit
2300
2324
  ];
2325
+ var harnessModules = (harness, registry = REGISTRY) => registry.filter((m) => !m.harnesses || m.harnesses.includes(harness));
2301
2326
  function collectReminders(env, registry = REGISTRY) {
2302
2327
  const out = [];
2303
2328
  for (const m of registry) {
@@ -2358,26 +2383,22 @@ function collectAnnounces(env, registry = REGISTRY) {
2358
2383
  }
2359
2384
 
2360
2385
  // src/commands/opencode-interrupt.ts
2361
- var OPENCODE_MODULE_IDS = /* @__PURE__ */ new Set([
2362
- "inbox-primer",
2363
- "load-primer",
2364
- "corpus-announce",
2365
- "setup-warning",
2366
- "version-upgrade",
2367
- "memory-primer",
2368
- "procedure-announce",
2369
- "collision"
2370
- ]);
2371
- var opencodeRegistry = REGISTRY.filter(
2372
- (m) => OPENCODE_MODULE_IDS.has(m.id)
2373
- );
2386
+ var opencodeRegistry = harnessModules("opencode");
2374
2387
  function buildOpenCodeEnv(params) {
2375
2388
  return {
2376
2389
  short: params.short,
2377
2390
  turnCount: params.turnCount,
2391
+ // No opencode-native friction detector is wired yet: Claude Code's stress /
2392
+ // bootstrap-friction signals come from state-trigger.ts reading the
2393
+ // transcript in hook.ts, and opencode has no equivalent source plumbed.
2394
+ // 0 = calm keeps the mechanic-friction REMINDER silent while its ANNOUNCE
2395
+ // (the fix-spawn doctrine) still lands every session. Detector design is
2396
+ // parked with scored options in the chip report
2397
+ // (chip/opencode-fix-chip-system-parity).
2378
2398
  stress: 0,
2379
- armed: false,
2399
+ armed: params.armed ?? false,
2380
2400
  sessionUnread: 0,
2401
+ platform: "opencode",
2381
2402
  projectName: params.projectName,
2382
2403
  setupWarning: params.setupWarning ?? null,
2383
2404
  corpusApiDocs: params.corpusApiDocs ?? [],
@@ -2385,9 +2406,8 @@ function buildOpenCodeEnv(params) {
2385
2406
  procedureAnnounces: params.procedureAnnounces ?? []
2386
2407
  };
2387
2408
  }
2388
- var OPENCODE_CHIP_BOOTLOADER_POINTER = '[greprag chips \u2014 agent coordination is built in (messaging plumbing in the inbox primer).]\n\u2022 Delegating a component of a plan to an isolated sub-session (a "chip") for focused, efficient work? \u2192 run `greprag load chip-bootloader` for the copyable bootloader template (Block 1 setup + Block 2 report-back).';
2389
2409
  function getOpenCodeAnnounces(env) {
2390
- return [...collectAnnounces(env, opencodeRegistry), OPENCODE_CHIP_BOOTLOADER_POINTER];
2410
+ return collectAnnounces(env, opencodeRegistry);
2391
2411
  }
2392
2412
  function getOpenCodeReminders(env) {
2393
2413
  return collectReminders(env, opencodeRegistry);
@@ -2501,6 +2521,588 @@ function activeProcedureAnnounces(store) {
2501
2521
  return lines;
2502
2522
  }
2503
2523
 
2524
+ // src/archive/glyph-font.ts
2525
+ var GLYPH_W = 8;
2526
+ var GLYPH_H = 8;
2527
+ var FIRST_CODE = 32;
2528
+ var LAST_CODE = 126;
2529
+ var FONT_HEX = [
2530
+ "0000000000000000",
2531
+ "183c3c1818001800",
2532
+ "3636000000000000",
2533
+ "36367f367f363600",
2534
+ "0c3e031e301f0c00",
2535
+ "006333180c666300",
2536
+ "1c361c6e3b336e00",
2537
+ "0606030000000000",
2538
+ "180c0606060c1800",
2539
+ "060c1818180c0600",
2540
+ "00663cff3c660000",
2541
+ "000c0c3f0c0c0000",
2542
+ "00000000000c0c06",
2543
+ "0000003f00000000",
2544
+ "00000000000c0c00",
2545
+ "6030180c06030100",
2546
+ "3e63737b6f673e00",
2547
+ "0c0e0c0c0c0c3f00",
2548
+ "1e33301c06333f00",
2549
+ "1e33301c30331e00",
2550
+ "383c36337f307800",
2551
+ "3f031f3030331e00",
2552
+ "1c06031f33331e00",
2553
+ "3f3330180c0c0c00",
2554
+ "1e33331e33331e00",
2555
+ "1e33333e30180e00",
2556
+ "000c0c00000c0c00",
2557
+ "000c0c00000c0c06",
2558
+ "180c0603060c1800",
2559
+ "00003f00003f0000",
2560
+ "060c1830180c0600",
2561
+ "1e3330180c000c00",
2562
+ "3e637b7b7b031e00",
2563
+ "0c1e33333f333300",
2564
+ "3f66663e66663f00",
2565
+ "3c66030303663c00",
2566
+ "1f36666666361f00",
2567
+ "7f46161e16467f00",
2568
+ "7f46161e16060f00",
2569
+ "3c66030373667c00",
2570
+ "3333333f33333300",
2571
+ "1e0c0c0c0c0c1e00",
2572
+ "7830303033331e00",
2573
+ "6766361e36666700",
2574
+ "0f06060646667f00",
2575
+ "63777f7f6b636300",
2576
+ "63676f7b73636300",
2577
+ "1c36636363361c00",
2578
+ "3f66663e06060f00",
2579
+ "1e3333333b1e3800",
2580
+ "3f66663e36666700",
2581
+ "1e33070e38331e00",
2582
+ "3f2d0c0c0c0c1e00",
2583
+ "3333333333333f00",
2584
+ "33333333331e0c00",
2585
+ "6363636b7f776300",
2586
+ "6363361c1c366300",
2587
+ "3333331e0c0c1e00",
2588
+ "7f6331184c667f00",
2589
+ "1e06060606061e00",
2590
+ "03060c1830604000",
2591
+ "1e18181818181e00",
2592
+ "081c366300000000",
2593
+ "00000000000000ff",
2594
+ "0c0c180000000000",
2595
+ "00001e303e336e00",
2596
+ "0706063e66663b00",
2597
+ "00001e3303331e00",
2598
+ "3830303e33336e00",
2599
+ "00001e333f031e00",
2600
+ "1c36060f06060f00",
2601
+ "00006e33333e301f",
2602
+ "0706366e66666700",
2603
+ "0c000e0c0c0c1e00",
2604
+ "300030303033331e",
2605
+ "070666361e366700",
2606
+ "0e0c0c0c0c0c1e00",
2607
+ "0000337f7f6b6300",
2608
+ "00001f3333333300",
2609
+ "00001e3333331e00",
2610
+ "00003b66663e060f",
2611
+ "00006e33333e3078",
2612
+ "00003b6e66060f00",
2613
+ "00003e031e301f00",
2614
+ "080c3e0c0c2c1800",
2615
+ "0000333333336e00",
2616
+ "00003333331e0c00",
2617
+ "0000636b7f7f3600",
2618
+ "000063361c366300",
2619
+ "00003333333e301f",
2620
+ "00003f190c263f00",
2621
+ "380c0c070c0c3800",
2622
+ "1818180018181800",
2623
+ "070c0c380c0c0700",
2624
+ "6e3b000000000000"
2625
+ ];
2626
+ function glyphRows(code) {
2627
+ if (code < FIRST_CODE || code > LAST_CODE)
2628
+ return null;
2629
+ const hex = FONT_HEX[code - FIRST_CODE];
2630
+ const out = [];
2631
+ for (let i = 0; i < 8; i++)
2632
+ out.push(parseInt(hex.slice(i * 2, i * 2 + 2), 16));
2633
+ return out;
2634
+ }
2635
+ var reverse = null;
2636
+ function codeForSignature(sigHex) {
2637
+ if (!reverse) {
2638
+ reverse = /* @__PURE__ */ new Map();
2639
+ for (let c = FIRST_CODE; c <= LAST_CODE; c++)
2640
+ reverse.set(FONT_HEX[c - FIRST_CODE], c);
2641
+ }
2642
+ return reverse.get(sigHex) ?? null;
2643
+ }
2644
+ function signatureForRows(bytes) {
2645
+ return bytes.map((b) => (b & 255).toString(16).padStart(2, "0")).join("");
2646
+ }
2647
+
2648
+ // src/archive/png-codec.ts
2649
+ var zlib = __toESM(require("zlib"));
2650
+ var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
2651
+ var crcTable = null;
2652
+ function getCrcTable() {
2653
+ if (crcTable)
2654
+ return crcTable;
2655
+ crcTable = new Uint32Array(256);
2656
+ for (let n = 0; n < 256; n++) {
2657
+ let c = n;
2658
+ for (let k = 0; k < 8; k++) {
2659
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
2660
+ }
2661
+ crcTable[n] = c >>> 0;
2662
+ }
2663
+ return crcTable;
2664
+ }
2665
+ function crc32(buf) {
2666
+ const table = getCrcTable();
2667
+ let c = 4294967295;
2668
+ for (let i = 0; i < buf.length; i++) {
2669
+ c = table[(c ^ buf[i]) & 255] ^ c >>> 8;
2670
+ }
2671
+ return (c ^ 4294967295) >>> 0;
2672
+ }
2673
+ function chunk(type, data) {
2674
+ const len = Buffer.alloc(4);
2675
+ len.writeUInt32BE(data.length, 0);
2676
+ const typeAndData = Buffer.concat([Buffer.from(type, "ascii"), data]);
2677
+ const crc = Buffer.alloc(4);
2678
+ crc.writeUInt32BE(crc32(typeAndData), 0);
2679
+ return Buffer.concat([len, typeAndData, crc]);
2680
+ }
2681
+ function encodePng(img) {
2682
+ const { width, height, pixels } = img;
2683
+ if (pixels.length !== width * height) {
2684
+ throw new Error(`pixel buffer ${pixels.length} != ${width}x${height}`);
2685
+ }
2686
+ const ihdr = Buffer.alloc(13);
2687
+ ihdr.writeUInt32BE(width, 0);
2688
+ ihdr.writeUInt32BE(height, 4);
2689
+ ihdr[8] = 8;
2690
+ ihdr[9] = 0;
2691
+ ihdr[10] = 0;
2692
+ ihdr[11] = 0;
2693
+ ihdr[12] = 0;
2694
+ const raw = Buffer.alloc(height * (width + 1));
2695
+ for (let y = 0; y < height; y++) {
2696
+ raw[y * (width + 1)] = 0;
2697
+ pixels.copy(raw, y * (width + 1) + 1, y * width, (y + 1) * width);
2698
+ }
2699
+ const idat = zlib.deflateSync(raw, { level: 9 });
2700
+ return Buffer.concat([
2701
+ PNG_SIGNATURE,
2702
+ chunk("IHDR", ihdr),
2703
+ chunk("IDAT", idat),
2704
+ chunk("IEND", Buffer.alloc(0))
2705
+ ]);
2706
+ }
2707
+ function paeth(a, b, c) {
2708
+ const p = a + b - c;
2709
+ const pa = Math.abs(p - a);
2710
+ const pb = Math.abs(p - b);
2711
+ const pc = Math.abs(p - c);
2712
+ if (pa <= pb && pa <= pc)
2713
+ return a;
2714
+ if (pb <= pc)
2715
+ return b;
2716
+ return c;
2717
+ }
2718
+ function decodePng(buf) {
2719
+ if (buf.length < 8 || !buf.subarray(0, 8).equals(PNG_SIGNATURE)) {
2720
+ throw new Error("not a PNG (bad signature)");
2721
+ }
2722
+ let width = 0;
2723
+ let height = 0;
2724
+ const idatParts = [];
2725
+ let off = 8;
2726
+ while (off + 8 <= buf.length) {
2727
+ const len = buf.readUInt32BE(off);
2728
+ const type = buf.toString("ascii", off + 4, off + 8);
2729
+ const data = buf.subarray(off + 8, off + 8 + len);
2730
+ if (type === "IHDR") {
2731
+ width = data.readUInt32BE(0);
2732
+ height = data.readUInt32BE(4);
2733
+ const bitDepth = data[8];
2734
+ const colorType = data[9];
2735
+ const interlace = data[12];
2736
+ if (bitDepth !== 8 || colorType !== 0 || interlace !== 0) {
2737
+ throw new Error(`unsupported PNG (bitDepth=${bitDepth} colorType=${colorType} interlace=${interlace})`);
2738
+ }
2739
+ } else if (type === "IDAT") {
2740
+ idatParts.push(data);
2741
+ } else if (type === "IEND") {
2742
+ break;
2743
+ }
2744
+ off += 8 + len + 4;
2745
+ }
2746
+ if (!width || !height)
2747
+ throw new Error("PNG missing IHDR");
2748
+ if (idatParts.length === 0)
2749
+ throw new Error("PNG missing IDAT");
2750
+ const raw = zlib.inflateSync(Buffer.concat(idatParts));
2751
+ const stride = width + 1;
2752
+ if (raw.length < height * stride) {
2753
+ throw new Error(`PNG data short: ${raw.length} < ${height * stride}`);
2754
+ }
2755
+ const pixels = Buffer.alloc(width * height);
2756
+ for (let y = 0; y < height; y++) {
2757
+ const filter = raw[y * stride];
2758
+ const rowIn = raw.subarray(y * stride + 1, y * stride + 1 + width);
2759
+ const rowOut = pixels.subarray(y * width, (y + 1) * width);
2760
+ for (let x = 0; x < width; x++) {
2761
+ const a = x > 0 ? rowOut[x - 1] : 0;
2762
+ const b = y > 0 ? pixels[(y - 1) * width + x] : 0;
2763
+ const c = x > 0 && y > 0 ? pixels[(y - 1) * width + x - 1] : 0;
2764
+ let v;
2765
+ switch (filter) {
2766
+ case 0:
2767
+ v = rowIn[x];
2768
+ break;
2769
+ case 1:
2770
+ v = rowIn[x] + a;
2771
+ break;
2772
+ case 2:
2773
+ v = rowIn[x] + b;
2774
+ break;
2775
+ case 3:
2776
+ v = rowIn[x] + (a + b >> 1);
2777
+ break;
2778
+ case 4:
2779
+ v = rowIn[x] + paeth(a, b, c);
2780
+ break;
2781
+ default:
2782
+ throw new Error(`unsupported PNG filter ${filter} on row ${y}`);
2783
+ }
2784
+ rowOut[x] = v & 255;
2785
+ }
2786
+ }
2787
+ return { width, height, pixels };
2788
+ }
2789
+
2790
+ // src/archive/page-renderer.ts
2791
+ var PAGE_SIZE = 1568;
2792
+ var CELL_W = GLYPH_W;
2793
+ var CELL_H = 10;
2794
+ var COLS = Math.floor(PAGE_SIZE / CELL_W);
2795
+ var ROWS = Math.floor(PAGE_SIZE / CELL_H);
2796
+ var CHARS_PER_PAGE = COLS * ROWS;
2797
+ var BG = 255;
2798
+ var FG = 0;
2799
+ function sanitizeForGrid(text) {
2800
+ const out = [];
2801
+ for (const ch of text.replace(/\r\n/g, "\n").replace(/\r/g, "\n")) {
2802
+ const code = ch.codePointAt(0);
2803
+ if (ch === "\n") {
2804
+ out.push("\n");
2805
+ } else if (ch === " ") {
2806
+ out.push("\\t");
2807
+ } else if (code >= 32 && code <= 126) {
2808
+ out.push(ch);
2809
+ } else if (code <= 65535) {
2810
+ out.push("\\u" + code.toString(16).padStart(4, "0"));
2811
+ } else {
2812
+ out.push("\\u{" + code.toString(16) + "}");
2813
+ }
2814
+ }
2815
+ return out.join("");
2816
+ }
2817
+ function layoutRows(sanitized, cols = COLS) {
2818
+ const rows = [];
2819
+ let offset = 0;
2820
+ for (const line of sanitized.split("\n")) {
2821
+ if (line.length === 0) {
2822
+ rows.push({ text: " ".repeat(cols), charStart: offset });
2823
+ } else {
2824
+ for (let i = 0; i < line.length; i += cols) {
2825
+ const slice = line.slice(i, i + cols);
2826
+ rows.push({ text: slice.padEnd(cols, " "), charStart: offset + i });
2827
+ }
2828
+ }
2829
+ offset += line.length + 1;
2830
+ }
2831
+ return rows;
2832
+ }
2833
+ function renderPage(pageRows, cols = COLS, rows = ROWS) {
2834
+ const rowCount = Math.max(1, Math.min(pageRows.length, rows));
2835
+ const width = cols * CELL_W;
2836
+ const height = rowCount * CELL_H;
2837
+ const pixels = Buffer.alloc(width * height, BG);
2838
+ for (let r = 0; r < Math.min(pageRows.length, rowCount); r++) {
2839
+ const row = pageRows[r];
2840
+ for (let c = 0; c < cols; c++) {
2841
+ const glyph = glyphRows(row.text.charCodeAt(c));
2842
+ if (!glyph)
2843
+ continue;
2844
+ const px = c * CELL_W;
2845
+ const py = r * CELL_H;
2846
+ for (let gy = 0; gy < GLYPH_H; gy++) {
2847
+ const bits = glyph[gy];
2848
+ if (bits === 0)
2849
+ continue;
2850
+ const base = (py + gy) * width + px;
2851
+ for (let gx = 0; gx < GLYPH_W; gx++) {
2852
+ if (bits & 1 << gx)
2853
+ pixels[base + gx] = FG;
2854
+ }
2855
+ }
2856
+ }
2857
+ }
2858
+ const img = { width, height, pixels };
2859
+ const charStart = pageRows.length ? pageRows[0].charStart : 0;
2860
+ const last = pageRows[pageRows.length - 1];
2861
+ const charEnd = last ? last.charStart + last.text.trimEnd().length : charStart;
2862
+ return {
2863
+ png: encodePng(img),
2864
+ gridText: pageRows.map((r) => r.text).join("\n"),
2865
+ charStart,
2866
+ charEnd
2867
+ };
2868
+ }
2869
+ function paginate(rowsIn, rowsPerPage = ROWS) {
2870
+ const pages = [];
2871
+ for (let i = 0; i < rowsIn.length; i += rowsPerPage) {
2872
+ pages.push(rowsIn.slice(i, i + rowsPerPage));
2873
+ }
2874
+ return pages;
2875
+ }
2876
+ function decodePageText(png, cols = COLS) {
2877
+ const img = decodePng(png);
2878
+ const rows = Math.floor(img.height / CELL_H);
2879
+ const usableCols = Math.min(cols, Math.floor(img.width / CELL_W));
2880
+ const lines = [];
2881
+ for (let r = 0; r < rows; r++) {
2882
+ const chars = [];
2883
+ for (let c = 0; c < usableCols; c++) {
2884
+ const bytes = [];
2885
+ for (let gy = 0; gy < GLYPH_H; gy++) {
2886
+ let b = 0;
2887
+ const base = (r * CELL_H + gy) * img.width + c * CELL_W;
2888
+ for (let gx = 0; gx < GLYPH_W; gx++) {
2889
+ if (img.pixels[base + gx] < 128)
2890
+ b |= 1 << gx;
2891
+ }
2892
+ bytes.push(b);
2893
+ }
2894
+ const code = codeForSignature(signatureForRows(bytes));
2895
+ chars.push(code === null ? "\uFFFD" : String.fromCharCode(code));
2896
+ }
2897
+ lines.push(chars.join(""));
2898
+ }
2899
+ return lines.join("\n");
2900
+ }
2901
+ function verifyRoundTrip(page) {
2902
+ const decoded = decodePageText(page.png);
2903
+ if (decoded === page.gridText)
2904
+ return null;
2905
+ const a = page.gridText;
2906
+ const b = decoded;
2907
+ const n = Math.min(a.length, b.length);
2908
+ for (let i = 0; i < n; i++) {
2909
+ if (a[i] !== b[i]) {
2910
+ return `mismatch at char ${i}: rendered ${JSON.stringify(a[i])} decoded ${JSON.stringify(b[i])}`;
2911
+ }
2912
+ }
2913
+ return `length mismatch: rendered ${a.length} decoded ${b.length}`;
2914
+ }
2915
+
2916
+ // src/opencode-plugin-png.ts
2917
+ var CCR_MARKER_HASH_LEN2 = 16;
2918
+ function ccrPngMarker(hash) {
2919
+ return `<<ccr:${hash.slice(0, CCR_MARKER_HASH_LEN2)}>>`;
2920
+ }
2921
+ var CCR_MARKER_RE2 = /<<ccr:/;
2922
+ var DEFAULT_THRESHOLD_CHARS = 4e5;
2923
+ var DEFAULT_MIN_PART_CHARS = 4e3;
2924
+ var DEFAULT_LIVE_TAIL_MESSAGES = 1;
2925
+ var DEFAULT_VISION_MODEL_SUBSTRINGS = [
2926
+ "gpt-4o",
2927
+ "gpt-4.1",
2928
+ "gpt-4-turbo",
2929
+ "gpt-5",
2930
+ "o1",
2931
+ "o3",
2932
+ "o4",
2933
+ "claude-3",
2934
+ "claude-4",
2935
+ "claude-opus",
2936
+ "claude-sonnet",
2937
+ "claude-haiku",
2938
+ "claude-fable",
2939
+ "gemini",
2940
+ "llama-3.2-vision",
2941
+ "llama-4",
2942
+ "qwen2.5-vl",
2943
+ "qwen2-vl",
2944
+ "qwen3-vl",
2945
+ "pixtral",
2946
+ "mistral-small-3",
2947
+ "grok-vision",
2948
+ "grok-4",
2949
+ "internvl",
2950
+ "-vl",
2951
+ "vision"
2952
+ ];
2953
+ function inputModalities(modalities) {
2954
+ if (!modalities)
2955
+ return null;
2956
+ if (Array.isArray(modalities))
2957
+ return modalities.map((m) => String(m));
2958
+ if (typeof modalities === "object" && Array.isArray(modalities.input)) {
2959
+ return modalities.input.map((m) => String(m));
2960
+ }
2961
+ return null;
2962
+ }
2963
+ function isVisionCapable(opts) {
2964
+ const mods = inputModalities(opts.modalities);
2965
+ if (mods) {
2966
+ return mods.some((m) => m.toLowerCase() === "image");
2967
+ }
2968
+ const id = (opts.modelId || "").toLowerCase();
2969
+ if (!id)
2970
+ return false;
2971
+ const allow = [...DEFAULT_VISION_MODEL_SUBSTRINGS, ...opts.visionAllowlist || []];
2972
+ return allow.some((sub) => sub && id.includes(String(sub).toLowerCase()));
2973
+ }
2974
+ function renderTextToImageParts(text, hash) {
2975
+ const sanitized = sanitizeForGrid(text);
2976
+ const pages = paginate(layoutRows(sanitized));
2977
+ const parts = [];
2978
+ let pngBytes = 0;
2979
+ let selfTestError = null;
2980
+ for (let i = 0; i < pages.length; i++) {
2981
+ const rendered = renderPage(pages[i]);
2982
+ const err = verifyRoundTrip(rendered);
2983
+ if (err && !selfTestError)
2984
+ selfTestError = `page ${i + 1}: ${err}`;
2985
+ pngBytes += rendered.png.length;
2986
+ parts.push({
2987
+ type: "file",
2988
+ mime: "image/png",
2989
+ url: `data:image/png;base64,${rendered.png.toString("base64")}`,
2990
+ filename: `greprag-png-${hash.slice(0, CCR_MARKER_HASH_LEN2)}-p${String(i + 1).padStart(3, "0")}.png`
2991
+ });
2992
+ }
2993
+ return { parts, pages: pages.length, pngBytes, selfTestError };
2994
+ }
2995
+ function partsOf(msg) {
2996
+ if (!msg || typeof msg !== "object")
2997
+ return null;
2998
+ const parts = msg.parts;
2999
+ return Array.isArray(parts) ? parts : null;
3000
+ }
3001
+ function partCharLen(p) {
3002
+ if (!p || typeof p !== "object")
3003
+ return 0;
3004
+ const part = p;
3005
+ if (part.type === "text" && typeof part.text === "string")
3006
+ return part.text.length;
3007
+ if (part.type === "file" && typeof part.url === "string")
3008
+ return part.url.length;
3009
+ if (part.type === "tool" && part.state && typeof part.state === "object") {
3010
+ const out = part.state.output;
3011
+ if (typeof out === "string")
3012
+ return out.length;
3013
+ }
3014
+ return 0;
3015
+ }
3016
+ function maybeRecodePart(p, minPartChars, stash) {
3017
+ if (!p || typeof p !== "object")
3018
+ return null;
3019
+ const part = p;
3020
+ if (part.type !== "text" || typeof part.text !== "string")
3021
+ return null;
3022
+ const text = part.text;
3023
+ if (CCR_MARKER_RE2.test(text))
3024
+ return null;
3025
+ if (text.length < minPartChars)
3026
+ return null;
3027
+ const hash = stash(text);
3028
+ const { parts: fileParts, pages, pngBytes, selfTestError } = renderTextToImageParts(text, hash);
3029
+ const breadcrumb = {
3030
+ type: "text",
3031
+ text: `[greprag-png-recode: ${pages} page(s) of prior history rendered as pixel-text image(s) above; verbatim original ${ccrPngMarker(hash)} (greprag retrieve ${hash.slice(0, CCR_MARKER_HASH_LEN2)})]`
3032
+ };
3033
+ return { fileParts, breadcrumb, pages, pngBytes, selfTestError };
3034
+ }
3035
+ function recodeMessagesToPng(messages, opts) {
3036
+ const thresholdChars = opts.thresholdChars ?? DEFAULT_THRESHOLD_CHARS;
3037
+ const minPartChars = opts.minPartChars ?? DEFAULT_MIN_PART_CHARS;
3038
+ const liveTail = opts.liveTailMessages ?? DEFAULT_LIVE_TAIL_MESSAGES;
3039
+ const stats = {
3040
+ fired: false,
3041
+ visionCapable: false,
3042
+ totalChars: 0,
3043
+ thresholdChars,
3044
+ partsRecoded: 0,
3045
+ pagesRendered: 0,
3046
+ pngBytes: 0,
3047
+ selfTestError: null,
3048
+ reason: ""
3049
+ };
3050
+ if (!Array.isArray(messages)) {
3051
+ stats.reason = "messages not an array";
3052
+ return stats;
3053
+ }
3054
+ for (const msg of messages) {
3055
+ const parts = partsOf(msg);
3056
+ if (!parts)
3057
+ continue;
3058
+ for (const p of parts)
3059
+ stats.totalChars += partCharLen(p);
3060
+ }
3061
+ const vision = isVisionCapable(opts);
3062
+ stats.visionCapable = vision;
3063
+ if (!vision) {
3064
+ stats.reason = `model not vision-capable (modelId=${opts.modelId || "?"}) \u2014 pass-through`;
3065
+ return stats;
3066
+ }
3067
+ if (stats.totalChars < thresholdChars) {
3068
+ stats.reason = `below threshold (${stats.totalChars} < ${thresholdChars} chars)`;
3069
+ return stats;
3070
+ }
3071
+ const lastRecodableIdx = Math.max(0, messages.length - Math.max(0, liveTail));
3072
+ for (let mi = 0; mi < lastRecodableIdx; mi++) {
3073
+ const msg = messages[mi];
3074
+ const parts = partsOf(msg);
3075
+ if (!parts)
3076
+ continue;
3077
+ let changed = false;
3078
+ const newParts = [];
3079
+ for (const p of parts) {
3080
+ let rec = null;
3081
+ try {
3082
+ rec = maybeRecodePart(p, minPartChars, opts.stash);
3083
+ } catch {
3084
+ rec = null;
3085
+ }
3086
+ if (rec) {
3087
+ newParts.push(...rec.fileParts, rec.breadcrumb);
3088
+ changed = true;
3089
+ stats.partsRecoded++;
3090
+ stats.pagesRendered += rec.pages;
3091
+ stats.pngBytes += rec.pngBytes;
3092
+ if (rec.selfTestError && !stats.selfTestError)
3093
+ stats.selfTestError = rec.selfTestError;
3094
+ } else {
3095
+ newParts.push(p);
3096
+ }
3097
+ }
3098
+ if (changed)
3099
+ msg.parts = newParts;
3100
+ }
3101
+ stats.fired = stats.partsRecoded > 0;
3102
+ stats.reason = stats.fired ? `recoded ${stats.partsRecoded} part(s) -> ${stats.pagesRendered} page(s), ${stats.pngBytes} png bytes` : "threshold crossed but no recodable text parts (all below per-part floor or already marked)";
3103
+ return stats;
3104
+ }
3105
+
2504
3106
  // src/opencode-plugin.ts
2505
3107
  var DEBUG_LOG_PATH = path3.join(os.homedir(), ".greprag", "opencode-plugin-debug.log");
2506
3108
  var _debugLogReady = false;
@@ -2749,11 +3351,11 @@ function startCaptureWatcher() {
2749
3351
  fs3.writeFileSync(WATCHER_LOCK, String(child.pid));
2750
3352
  } catch {
2751
3353
  }
2752
- child.stderr?.on("data", (chunk) => {
2753
- process.stderr.write(`[greprag-watch] ${chunk.toString("utf-8")}`);
3354
+ child.stderr?.on("data", (chunk2) => {
3355
+ process.stderr.write(`[greprag-watch] ${chunk2.toString("utf-8")}`);
2754
3356
  });
2755
- child.stdout?.on("data", (chunk) => {
2756
- process.stderr.write(`[greprag-watch] ${chunk.toString("utf-8")}`);
3357
+ child.stdout?.on("data", (chunk2) => {
3358
+ process.stderr.write(`[greprag-watch] ${chunk2.toString("utf-8")}`);
2757
3359
  });
2758
3360
  child.on("exit", (code, signal) => {
2759
3361
  dlog(`startCaptureWatcher: child pid=${child.pid ?? "<none>"} exited code=${code} signal=${signal || "none"}`);
@@ -2958,11 +3560,11 @@ function startSessionRelay(sessionId, serverUrl) {
2958
3560
  fs3.writeFileSync(lockPath, String(child.pid));
2959
3561
  } catch {
2960
3562
  }
2961
- child.stderr?.on("data", (chunk) => {
2962
- process.stderr.write(`[greprag-relay] ${chunk.toString("utf-8")}`);
3563
+ child.stderr?.on("data", (chunk2) => {
3564
+ process.stderr.write(`[greprag-relay] ${chunk2.toString("utf-8")}`);
2963
3565
  });
2964
- child.stdout?.on("data", (chunk) => {
2965
- process.stderr.write(`[greprag-relay] ${chunk.toString("utf-8")}`);
3566
+ child.stdout?.on("data", (chunk2) => {
3567
+ process.stderr.write(`[greprag-relay] ${chunk2.toString("utf-8")}`);
2966
3568
  });
2967
3569
  child.on("exit", (code, signal) => {
2968
3570
  try {
@@ -3052,6 +3654,39 @@ function inlineCrushEnabled() {
3052
3654
  const v = process.env.GREPRAG_OPENCODE_CRUSH;
3053
3655
  return v === "true" || v === "1";
3054
3656
  }
3657
+ function pngRecodeEnabled() {
3658
+ const v = process.env.GREPRAG_OPENCODE_PNG;
3659
+ return v === "true" || v === "1";
3660
+ }
3661
+ function intEnv(name, allowZero = false) {
3662
+ const n = parseInt(process.env[name] || "", 10);
3663
+ if (!Number.isFinite(n))
3664
+ return void 0;
3665
+ return n > 0 || allowZero && n === 0 ? n : void 0;
3666
+ }
3667
+ function runPngRecode(input, msgs) {
3668
+ const model = input?.model;
3669
+ const modelId = model?.modelID || model?.id || model?.name || null;
3670
+ const modalities = model?.modalities || model?.info?.modalities || null;
3671
+ const allowEnv = (process.env.GREPRAG_OPENCODE_PNG_VISION_MODELS || "").split(",").map((s) => s.trim()).filter(Boolean);
3672
+ const stash = makeCcrStash();
3673
+ const stats = recodeMessagesToPng(msgs, {
3674
+ stash: (original) => stash(original, "log"),
3675
+ modelId,
3676
+ modalities,
3677
+ thresholdChars: intEnv("GREPRAG_OPENCODE_PNG_THRESHOLD"),
3678
+ minPartChars: intEnv("GREPRAG_OPENCODE_PNG_MIN_PART"),
3679
+ liveTailMessages: intEnv("GREPRAG_OPENCODE_PNG_TAIL", true),
3680
+ visionAllowlist: allowEnv.length ? allowEnv : void 0
3681
+ });
3682
+ if (stats.partsRecoded > 0) {
3683
+ dlog(
3684
+ `messages.transform png: recoded=${stats.partsRecoded} pages=${stats.pagesRendered} pngBytes=${stats.pngBytes} totalChars=${stats.totalChars} vision=${stats.visionCapable}` + (stats.selfTestError ? ` selfTestError=${stats.selfTestError}` : "")
3685
+ );
3686
+ } else {
3687
+ dlog(`messages.transform png: no-op (${stats.reason})`);
3688
+ }
3689
+ }
3055
3690
  function makeCcrStash() {
3056
3691
  return (original, sourceType) => {
3057
3692
  const hash = crypto2.createHash("sha256").update(original, "utf8").digest("hex").slice(0, 16);
@@ -3147,6 +3782,13 @@ ALWAYS pass \`--from-session ${sid82}\` when you run \`greprag send\`, so recipi
3147
3782
  );
3148
3783
  dlog(`pushed session-id awareness for sid=${sid82}`);
3149
3784
  }
3785
+ if (inlineCrushEnabled()) {
3786
+ pushSystemPrompt(
3787
+ output,
3788
+ "Context compression: when you see a `<<ccr:HASH>>` marker, the text immediately before it was losslessly compressed to save tokens. To recover the original verbatim, run `greprag retrieve <HASH>` in your shell (HASH = the 16 hex chars in the marker). Retrieve only when you actually need the elided detail."
3789
+ );
3790
+ dlog(`pushed CCR retrieve instruction for sid=${sidShort}`);
3791
+ }
3150
3792
  const sid8 = sid && sidShort && sidShort !== "<none>" ? sidShort : sid ? sid.slice(0, 8) : "";
3151
3793
  if (sid8) {
3152
3794
  const turnCount = (turnCountBySession.get(sid8) ?? 0) + 1;
@@ -3155,6 +3797,9 @@ ALWAYS pass \`--from-session ${sid82}\` when you run \`greprag send\`, so recipi
3155
3797
  short: sid8,
3156
3798
  turnCount,
3157
3799
  projectName: anchor.projectName,
3800
+ // The relay IS opencode's watcher: armed the moment startSessionRelay
3801
+ // committed the arm for this session (earlier in this same fire).
3802
+ armed: sid ? relayArmedBySession.has(sid) : false,
3158
3803
  procedureAnnounces: (() => {
3159
3804
  try {
3160
3805
  return activeProcedureAnnounces(readProcedureStore(anchor.projectId));
@@ -3190,7 +3835,16 @@ ALWAYS pass \`--from-session ${sid82}\` when you run \`greprag send\`, so recipi
3190
3835
  // Contract: we mutate ONLY this request-scoped `output.messages` copy,
3191
3836
  // never opencode's persisted session history. adr:
3192
3837
  // adr/opencode-context-compressor.md
3193
- "experimental.chat.messages.transform": async (_input, output) => {
3838
+ "experimental.chat.messages.transform": async (input, output) => {
3839
+ try {
3840
+ if (pngRecodeEnabled()) {
3841
+ const msgs0 = output && output.messages;
3842
+ if (Array.isArray(msgs0))
3843
+ runPngRecode(input, msgs0);
3844
+ }
3845
+ } catch (e) {
3846
+ dlog(`messages.transform png: swallowed error (turn unaffected): ${e.message}`);
3847
+ }
3194
3848
  try {
3195
3849
  if (!inlineCrushEnabled())
3196
3850
  return;