greprag 5.80.0 → 5.82.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 (63) hide show
  1. package/dist/capture-manifest.js +2 -1
  2. package/dist/codex-fast-hook.js +6 -0
  3. package/dist/codex-steering.js +1 -1
  4. package/dist/commands/app-model.js +0 -1
  5. package/dist/commands/arm-reminder.js +9 -7
  6. package/dist/commands/collision-check.js +7 -6
  7. package/dist/commands/corpus/client.js +13 -3
  8. package/dist/commands/delivery-reminder.js +35 -14
  9. package/dist/commands/deploy-gate.js +55 -0
  10. package/dist/commands/deploy-lock.js +100 -0
  11. package/dist/commands/deploy-record.js +145 -0
  12. package/dist/commands/deploy-verify.js +111 -0
  13. package/dist/commands/inbox-primer-reminder.js +5 -5
  14. package/dist/commands/inbox-watch.js +2 -4
  15. package/dist/commands/init.js +82 -0
  16. package/dist/commands/load.js +40 -0
  17. package/dist/commands/loadout-reminder.js +1 -1
  18. package/dist/commands/merge-guard.js +419 -0
  19. package/dist/commands/merge-lock.js +176 -0
  20. package/dist/commands/parity-reminder.js +53 -0
  21. package/dist/commands/persona-reminder.js +11 -0
  22. package/dist/commands/persona.js +50 -0
  23. package/dist/commands/procedure.js +77 -6
  24. package/dist/commands/reminder-registry.js +21 -5
  25. package/dist/commands/repodoc.js +433 -0
  26. package/dist/commands/search.js +149 -0
  27. package/dist/commands/skillgain.js +33 -25
  28. package/dist/delivery-lifecycle.js +16 -1
  29. package/dist/deploy-gate.js +355 -0
  30. package/dist/deploy-locks.js +339 -0
  31. package/dist/deploy-verify.js +209 -0
  32. package/dist/env-redaction.js +157 -0
  33. package/dist/harness-limits.js +17 -0
  34. package/dist/hook-runtime.js +11 -1
  35. package/dist/hook.js +170 -88
  36. package/dist/index.js +593 -567
  37. package/dist/inline-atom-episode.js +15 -7
  38. package/dist/inline-atom.js +8 -2
  39. package/dist/native-skill-adoption.js +11 -0
  40. package/dist/native-skill-mirror.js +8 -1
  41. package/dist/node-identity.bundle.js +1166 -0
  42. package/dist/opencode-plugin.bundle.js +307 -119
  43. package/dist/procedure-enabled.js +55 -0
  44. package/dist/procedure-runtime.js +6 -0
  45. package/dist/procedure-scope.js +190 -0
  46. package/dist/procedure-watch.js +29 -16
  47. package/dist/procedure.js +111 -5
  48. package/dist/project-anchor.js +1 -14
  49. package/dist/reminder-injector.js +11 -10
  50. package/dist/repodoc-client.js +296 -0
  51. package/dist/session-id.js +7 -8
  52. package/dist/skill-landing.js +57 -2
  53. package/dist/skill-mirror-client.js +14 -0
  54. package/dist/skill-mirror-files.js +18 -0
  55. package/package.json +2 -2
  56. package/scripts/bundle-node-identity.mjs +47 -0
  57. package/skill/templates/chip-spawn.md +7 -1
  58. package/skill/templates/delivery.md +105 -0
  59. package/skill/templates/prompt-audit.md +196 -0
  60. package/skill/templates/skill-change.md +25 -2
  61. package/dist/assistant-doctrine.js +0 -85
  62. package/dist/commands/assistant-reminder.js +0 -19
  63. package/dist/commands/assistant.js +0 -95
package/dist/hook.js CHANGED
@@ -49,7 +49,6 @@ const proc_1 = require("./proc");
49
49
  const docptr_refs_1 = require("./docptr-refs");
50
50
  const project_anchor_1 = require("./project-anchor");
51
51
  const mechanic_role_1 = require("./mechanic-role");
52
- const assistant_doctrine_1 = require("./assistant-doctrine");
53
52
  const session_id_1 = require("./session-id");
54
53
  const hook_runtime_1 = require("./hook-runtime");
55
54
  const harness_1 = require("./harness");
@@ -67,9 +66,12 @@ const email_pull_1 = require("./email-pull");
67
66
  const watcher_registry_1 = require("./commands/watcher-registry");
68
67
  // adr: adr/secret-scrubber.md — secret detection is canonical in @greprag/core.
69
68
  // This imports the CLI MIRROR (the zero-dep hook can't import core at runtime).
70
- // buildRedactionMap (the .env/process.env walker) is client-only and stays in
71
- // this file — the pure module is deliberately filesystem-free.
69
+ // buildRedactionMap (the .env/process.env walker) is client-only and lives in
70
+ // ./env-redaction beside this hook — the pure module is deliberately
71
+ // filesystem-free, and the walker's classification (a path is never a secret)
72
+ // is unit-tested there.
72
73
  const secret_scrubber_1 = require("./secret-scrubber");
74
+ const env_redaction_1 = require("./env-redaction");
73
75
  // adr: adr/opencode-monitor-relay.md 2026-06-06 (g) — recap renderer is the
74
76
  // single source of truth for the recap body pushed at session start. Both
75
77
  // Claude/Codex SessionStart and the opencode plugin use the same renderer so
@@ -85,6 +87,7 @@ const guard_1 = require("./guard");
85
87
  // auto-silences when resolved (the arm-check pattern). The hook below assembles the
86
88
  // live env (ALL I/O) and emits; the modules are pure.
87
89
  const reminder_registry_1 = require("./commands/reminder-registry");
90
+ const persona_1 = require("./commands/persona");
88
91
  const app_settings_1 = require("./app-settings");
89
92
  const inbox_primer_reminder_1 = require("./commands/inbox-primer-reminder");
90
93
  const skill_activation_manifest_1 = require("./skill-activation-manifest");
@@ -105,6 +108,7 @@ const context_governor_1 = require("./commands/context-governor");
105
108
  // lives in ./commands/pipe-wrap. docs/pre-llm-compression.md, adr/pipe-wrap-hook.md
106
109
  const pipe_wrap_1 = require("./commands/pipe-wrap");
107
110
  const search_guard_1 = require("./commands/search-guard");
111
+ const merge_guard_1 = require("./commands/merge-guard");
108
112
  // Governed compact (Chip D) — PreCompact renders the transcript to pixel-font
109
113
  // PNG pages (lossless ~3x-density archive, CCR-indexed); SessionStart(compact)
110
114
  // injects the pointer so the post-compact model can Read the verbatim record.
@@ -129,13 +133,16 @@ const procedure_1 = require("./procedure");
129
133
  // per turn by the Stop hook (observeProcedureTurn), closed by negative
130
134
  // continuity, distilled server-side, transitions applied to the local store.
131
135
  const procedure_watch_1 = require("./procedure-watch");
136
+ const procedure_scope_1 = require("./procedure-scope");
132
137
  const procedure_runtime_1 = require("./procedure-runtime");
138
+ const procedure_enabled_1 = require("./procedure-enabled");
133
139
  const procedure_shadow_1 = require("./procedure-shadow");
134
140
  const inline_atom_hook_1 = require("./inline-atom-hook");
135
141
  const inline_atom_background_1 = require("./inline-atom-background");
136
142
  const reminder_injector_1 = require("./reminder-injector");
137
143
  const skill_landing_1 = require("./skill-landing");
138
144
  const skill_mirror_client_1 = require("./skill-mirror-client");
145
+ const repodoc_client_1 = require("./repodoc-client");
139
146
  const native_skill_pending_1 = require("./native-skill-pending");
140
147
  const doc_mirror_client_1 = require("./doc-mirror-client");
141
148
  const equipped_loadouts_cache_1 = require("./equipped-loadouts-cache");
@@ -620,60 +627,11 @@ function detectArtifacts(toolCalls, cwd) {
620
627
  return events;
621
628
  }
622
629
  // ---------- Env-var scrubbing ---------------------------------------------
623
- // Pattern + entropy detection now lives in ./secret-scrubber — the CLI MIRROR of
630
+ // Pattern + entropy detection lives in ./secret-scrubber — the CLI MIRROR of
624
631
  // the canonical @greprag/core module (the zero-dep hook can't import core at
625
- // runtime). scrubString / scrubObject are imported at the top of this file. The
626
- // 18 original provider patterns are preserved verbatim there, so this path
627
- // scrubs identically — now backed by ~150 patterns + a high-entropy catch-all.
628
- // buildRedactionMap below is CLIENT-ONLY (reads .env + process.env) and stays
629
- // here on purpose: the pure module is filesystem-free. adr: adr/secret-scrubber.md
630
- /** Build a redaction map of value → [REDACTED:VAR_NAME] from .env files and process.env. */
631
- function buildRedactionMap(cwd) {
632
- const entries = [];
633
- const seenValues = new Set();
634
- const addEntry = (key, value) => {
635
- if (!value || value.length < 16)
636
- return;
637
- if (seenValues.has(value))
638
- return;
639
- seenValues.add(value);
640
- entries.push([value, `[REDACTED:${key}]`]);
641
- };
642
- // Walk up looking for .env files
643
- let dir = path.resolve(cwd);
644
- for (let depth = 0; depth < 8; depth++) {
645
- const envPath = path.join(dir, '.env');
646
- if (fs.existsSync(envPath)) {
647
- try {
648
- const raw = fs.readFileSync(envPath, 'utf-8');
649
- for (const line of raw.split('\n')) {
650
- const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.+?)\s*$/);
651
- if (!m)
652
- continue;
653
- let value = m[2].trim();
654
- if ((value.startsWith('"') && value.endsWith('"')) ||
655
- (value.startsWith("'") && value.endsWith("'"))) {
656
- value = value.slice(1, -1);
657
- }
658
- addEntry(m[1], value);
659
- }
660
- }
661
- catch { /* skip */ }
662
- }
663
- const parent = path.dirname(dir);
664
- if (parent === dir)
665
- break;
666
- dir = parent;
667
- }
668
- // Process env
669
- for (const [key, value] of Object.entries(process.env)) {
670
- if (typeof value === 'string')
671
- addEntry(key, value);
672
- }
673
- // Sort by value length desc so longer values are replaced first
674
- entries.sort((a, b) => b[0].length - a[0].length);
675
- return entries;
676
- }
632
+ // runtime). The CLIENT-ONLY .env/process.env walker (buildRedactionMap) lives
633
+ // in ./env-redaction; both are imported at the top of this file.
634
+ // adr: adr/secret-scrubber.md
677
635
  // ---------- Field-size cap ------------------------------------------------
678
636
  function capField(text) {
679
637
  if (!text || text.length <= MAX_FIELD_CHARS)
@@ -773,6 +731,14 @@ function collectDocEvents(filesTouched, cwd) {
773
731
  /** One observed turn of the active watch. Never throws (the caller also
774
732
  * guards); every network call fail-open. */
775
733
  async function observeProcedureTurn(cfg, anchor, turn, cwd, redaction) {
734
+ // Master switch. Off since 2026-09-11. No watch can open while it is off, but
735
+ // a watch left open by the last enabled session must not keep observing and
736
+ // must never distill — so clear it rather than resume it.
737
+ // adr: adr/procedure-system-off.md
738
+ if (!procedure_enabled_1.PROCEDURE_SYSTEM_ENABLED) {
739
+ (0, procedure_watch_1.clearProcedureWatch)(anchor.projectId);
740
+ return;
741
+ }
776
742
  const watch = (0, procedure_watch_1.readProcedureWatch)(anchor.projectId);
777
743
  if (!watch) {
778
744
  // File exists but is unreadable/corrupt — drop it so it can't wedge the observer.
@@ -868,7 +834,7 @@ async function observeProcedureTurn(cfg, anchor, turn, cwd, redaction) {
868
834
  }
869
835
  else {
870
836
  const store = (0, procedure_1.readProcedureStore)(anchor.projectId);
871
- const { store: nextStore, action } = (0, procedure_watch_1.applyDistillTransition)(store, next, outcome, distilled, (0, procedure_watch_1.learnTriggersForVerb)(next.verb, store));
837
+ const { store: nextStore, action } = (0, procedure_watch_1.applyDistillTransition)(store, next, outcome, distilled, (0, procedure_watch_1.learnTriggersForVerb)(next.verb, store), (0, procedure_scope_1.projectCheckoutRoots)(cwd));
872
838
  if (action !== 'discarded')
873
839
  (0, procedure_1.writeProcedureStore)(anchor.projectId, nextStore);
874
840
  }
@@ -1110,7 +1076,7 @@ async function store(input, source = 'claude-code') {
1110
1076
  ? (0, turn_provenance_1.provenanceElisionMarker)(provenance, (0, turn_provenance_1.provenanceHint)(provenance, turn.userPrompt))
1111
1077
  : turn.userPrompt;
1112
1078
  // Env-var scrub
1113
- const redaction = buildRedactionMap(cwd);
1079
+ const redaction = (0, env_redaction_1.buildRedactionMap)(cwd);
1114
1080
  const userPrompt = capField((0, secret_scrubber_1.scrubString)(userPromptForStore, redaction));
1115
1081
  const agentResponse = capField((0, secret_scrubber_1.scrubString)(turn.agentResponse, redaction));
1116
1082
  // Strip internal-only fields (_output, _useId) before sending to API,
@@ -1218,6 +1184,17 @@ async function store(input, source = 'claude-code') {
1218
1184
  });
1219
1185
  }
1220
1186
  }
1187
+ // Repo chunk index (adr/node-identity-port.md): the chunks are DERIVED from a
1188
+ // document plus the node graph committed beside it, so they are rebuilt when
1189
+ // the document moves rather than when someone remembers to run a push. Same
1190
+ // event as docptr and the skill mirror — the turn already knows what it wrote.
1191
+ // Only a repository that has been indexed at least once participates, and
1192
+ // within it only documents whose graph actually moved. Best-effort, never
1193
+ // throws: a stale index is a worse search result, a thrown Stop hook is lost
1194
+ // work.
1195
+ await (0, repodoc_client_1.maybeRebuildChunks)({
1196
+ filesTouched: turn.filesTouched, cwd, apiUrl: cfg.apiUrl, apiKey: cfg.apiKey,
1197
+ });
1221
1198
  }
1222
1199
  // ---------- Main ---------------------------------------------------------
1223
1200
  // ---------- Recap (SessionStart) ------------------------------------------
@@ -1329,13 +1306,56 @@ function shouldEmitDriftWarning(storedId, derivedId) {
1329
1306
  return (Date.now() - lastMs) >= DRIFT_WARNING_TTL_MS;
1330
1307
  }
1331
1308
  function grokSidecarHead(short, full) {
1332
- const arm = (0, session_id_1.armMonitorCommand)(short, null, false, false, 'grok');
1309
+ const arm = (0, session_id_1.armMonitorCommand)(short, null, false, 'grok');
1333
1310
  return (0, session_id_1.buildSessionIdContext)(short, (0, session_id_1.readIdentityAlias)())
1334
1311
  + `\n\nARM (Grok monitor persistent:true): \`${arm}\`\n`
1335
1312
  + `Send with --from-session ${full || short} (full UUID or 16-hex, never 8-hex).\n`
1336
1313
  + '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'
1337
1314
  + 'If a greprag inbox monitor is already listed, do not start another. Instant exit = already armed.\n\n';
1338
1315
  }
1316
+ /** SessionStart(persona) / PostCompact(persona) — Persona on its own hook, with
1317
+ * its own inline budget.
1318
+ *
1319
+ * The harness caps EACH hook invocation at ~2048 bytes independently, so a
1320
+ * dedicated hook buys Persona a full allowance nothing else competes for. That
1321
+ * matters because Persona is the one block that shapes every token the agent
1322
+ * emits; the rest of the announce is reference material `greprag load` serves
1323
+ * on demand. It also means the operator can grow their Persona without silently
1324
+ * pushing another announce section out.
1325
+ *
1326
+ * Registered on SessionStart AND PostCompact, for Claude Code and Codex alike.
1327
+ * PostCompact is not an optional extra: a compaction wipes SessionStart context,
1328
+ * so without it a long session silently loses its speaking rules mid-flight.
1329
+ *
1330
+ * The emitted `hookEventName` is the CONSTANT 'SessionStart', never the payload's
1331
+ * event name. `PostCompact` is a hook REGISTRATION point, not a value the harness
1332
+ * accepts in `hookSpecificOutput` — Claude Code validates that field against a
1333
+ * fixed enum and REJECTS the whole payload when it echoes 'PostCompact', so the
1334
+ * persona is dropped on exactly the path this hook exists to cover. Every other
1335
+ * greprag PostCompact hook (`recompact`, `session-id`) already emits 'SessionStart'
1336
+ * for this reason.
1337
+ *
1338
+ * Fetch failures stay silent — a missing Persona is not worth a startup error.
1339
+ * adr: adr/persona.md, adr/announce-inline-budget.md */
1340
+ async function personaHook(input) {
1341
+ const cwd = input.cwd || process.cwd();
1342
+ const cfg = (0, hook_runtime_1.getHookConfig)(cwd);
1343
+ if (!cfg.enabled || !cfg.apiKey)
1344
+ return;
1345
+ try {
1346
+ const res = await fetch(`${cfg.apiUrl}/v1/persona`, {
1347
+ headers: { 'Authorization': `Bearer ${cfg.apiKey}` },
1348
+ });
1349
+ if (!res.ok)
1350
+ return;
1351
+ const data = await res.json();
1352
+ const announce = typeof data.announce === 'string' ? data.announce.trim() : '';
1353
+ if (!announce)
1354
+ return;
1355
+ (0, hook_runtime_1.writeAdditionalContext)('SessionStart', (0, persona_1.fitPersonaAnnounce)(announce));
1356
+ }
1357
+ catch { /* never block session start on Persona */ }
1358
+ }
1339
1359
  /** Where the full, unabridged announce is parked for on-demand reading.
1340
1360
  * adr: adr/announce-inline-budget.md */
1341
1361
  function announceCachePath(short) {
@@ -1362,9 +1382,9 @@ function buildInlineAnnounce(blocks, short) {
1362
1382
  catch { /* cache is a convenience; never block session start */ }
1363
1383
  }
1364
1384
  // 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);
1385
+ // what pushes the payload over the cap. The number lives beside the budget in
1386
+ // reminder-registry so the `mustInline` build guard charges the same reserve.
1387
+ const { kept, droppedIds } = (0, reminder_registry_1.fitAnnounceBudget)(blocks, reminder_registry_1.ANNOUNCE_INLINE_BUDGET, reminder_registry_1.ANNOUNCE_POINTER_RESERVE);
1368
1388
  if (!droppedIds.length)
1369
1389
  return kept.map(b => b.text).join('\n\n') || null;
1370
1390
  const names = droppedIds.join(', ');
@@ -1373,9 +1393,14 @@ function buildInlineAnnounce(blocks, short) {
1373
1393
  + `Read them with \`greprag announce\`.]`;
1374
1394
  return [...kept.map(b => b.text), pointer].join('\n\n');
1375
1395
  }
1376
- function writeRecapOutput(text, mode, grokShort, grokFull) {
1377
- if (grokShort)
1378
- (0, grok_session_1.writeGrokSidecar)(grokShort, grokSidecarHead(grokShort, grokFull) + (text || ''));
1396
+ /** `grokExtra` is sidecar-ONLY text. The Grok sidecar is a FILE the agent reads
1397
+ * (adr/grok-platform.md) — it has no inline cap, so a block that cannot fit the
1398
+ * inline budget still belongs there whole. Persona is the one such block today.
1399
+ * adr: adr/persona.md */
1400
+ function writeRecapOutput(text, mode, grokShort, grokFull, grokExtra) {
1401
+ if (grokShort) {
1402
+ (0, grok_session_1.writeGrokSidecar)(grokShort, grokSidecarHead(grokShort, grokFull) + (grokExtra || '') + (text || ''));
1403
+ }
1379
1404
  if (!text)
1380
1405
  return;
1381
1406
  if (mode === 'additionalContext') {
@@ -1667,11 +1692,38 @@ async function recap(input, mode = 'plain', opts = {}) {
1667
1692
  return null;
1668
1693
  }
1669
1694
  })();
1695
+ // Parity units THIS harness still owes. The lead is filtered out server-side by
1696
+ // state, and diverged units are dropped here — two-sided advancement needs the
1697
+ // operator, not an unattended port. adr: adr/parity-system.md
1698
+ const parityPendingPromise = (async () => {
1699
+ try {
1700
+ const res = await fetch(`${cfg.apiUrl}/v1/parity/list`, {
1701
+ headers: { 'Authorization': `Bearer ${cfg.apiKey}` },
1702
+ });
1703
+ if (!res.ok)
1704
+ return undefined;
1705
+ const data = await res.json();
1706
+ const pending = (data.units || [])
1707
+ .filter(u => !u.divergedHint && u.lead?.harness && u.lead.harness !== platform)
1708
+ .flatMap(u => (u.followers || [])
1709
+ .filter(f => f.state === 'stale' && f.harness === platform && f.path)
1710
+ .map(f => ({
1711
+ unit: String(u.name || ''),
1712
+ summary: String(u.summary || ''),
1713
+ path: String(f.path),
1714
+ leadHarness: String(u.lead?.harness || ''),
1715
+ })));
1716
+ return pending.length ? pending : undefined;
1717
+ }
1718
+ catch {
1719
+ return undefined;
1720
+ }
1721
+ })();
1670
1722
  const updateAvailablePromise = checkForUpdate();
1671
1723
  const bodyPromise = (!opts.compact && anchor.sessionStartRecap)
1672
1724
  ? (0, opencode_plugin_helpers_1.buildRecapBody)(cfg.apiUrl, cfg.apiKey, anchor)
1673
1725
  : Promise.resolve('');
1674
- const [, corpusApiDocs, docPointers, enrichmentDown, skillGains, mirroredSkills, loadoutState, personaAnnounce, updateAvailable, body,] = await Promise.all([
1726
+ const [, corpusApiDocs, docPointers, enrichmentDown, skillGains, mirroredSkills, loadoutState, personaAnnounce, parityPending, updateAvailable, body,] = await Promise.all([
1675
1727
  matchsetPromise,
1676
1728
  corpusApiDocsPromise,
1677
1729
  docPointersPromise,
@@ -1680,6 +1732,7 @@ async function recap(input, mode = 'plain', opts = {}) {
1680
1732
  mirroredSkillsPromise,
1681
1733
  loadoutStatePromise,
1682
1734
  personaPromise,
1735
+ parityPendingPromise,
1683
1736
  updateAvailablePromise,
1684
1737
  bodyPromise,
1685
1738
  ]);
@@ -1699,12 +1752,6 @@ async function recap(input, mode = 'plain', opts = {}) {
1699
1752
  const top = behind.reduce((a, b) => (isNewer(a.minCliVersion, b.minCliVersion) ? b : a));
1700
1753
  return { from: top.fromHandle || 'another tenant', needed: top.minCliVersion, current: installed };
1701
1754
  })();
1702
- // Assistant doctrine auto-load — fires ONLY for the flagged assistant project
1703
- // (isAssistantProject), so a normal project sees zero change. The hook owns the
1704
- // doctrine-file read; the assistant-doctrine module routes the text. adr: adr/assistant-role.md
1705
- const assistantDoctrine = (0, project_anchor_1.isAssistantProject)(anchor)
1706
- ? (0, assistant_doctrine_1.buildAssistantDoctrineContext)(cwd)
1707
- : null;
1708
1755
  // Active Procedure installs (adr/procedure-install-control-plane.md). Local,
1709
1756
  // operator-owned state; missing/corrupt installs fail closed to no announce.
1710
1757
  const procedureAnnounces = (() => {
@@ -1717,7 +1764,7 @@ async function recap(input, mode = 'plain', opts = {}) {
1717
1764
  })();
1718
1765
  // ── THE single SessionStart announce assembly (docs/reminder-interrupt.md §Registry
1719
1766
  // spec) ──────────────────────────────────────────────────────────────────────────────
1720
- // Every agent-facing announce — setup-warning, front-desk, assistant-doctrine,
1767
+ // Every agent-facing announce — setup-warning, front-desk, delivery-control,
1721
1768
  // watcher-arm, mechanic — is now a registry module. The hook did the I/O above; here it
1722
1769
  // stuffs the results into ONE env and collectAnnounces renders them in registry order.
1723
1770
  // Loaded once + re-fired on compact (recap matcher ''). The Mechanic kill switch drops
@@ -1739,19 +1786,12 @@ async function recap(input, mode = 'plain', opts = {}) {
1739
1786
  short: announceShort, turnCount: 0, stress: 0, armed: false, sessionUnread: 0,
1740
1787
  ownerPid: hasLocalMonitorLifecycle ? sessionOwnerPid : null,
1741
1788
  alias: (0, session_id_1.readIdentityAlias)(),
1742
- assistant: (() => { try {
1743
- return (0, project_anchor_1.isAssistantProject)(anchor);
1744
- }
1745
- catch {
1746
- return false;
1747
- } })(),
1748
1789
  mechanic: announceMechanic,
1749
1790
  mechanicLive: announceMechanicLive,
1750
1791
  platform,
1751
1792
  projectName: anchor.projectName,
1752
1793
  deliveryRepo: (0, project_anchor_1.computeGitDerivedProjectId)(cwd) !== null,
1753
1794
  setupWarning,
1754
- assistantDoctrine,
1755
1795
  corpusApiDocs,
1756
1796
  docPointers,
1757
1797
  enrichmentDown,
@@ -1760,11 +1800,36 @@ async function recap(input, mode = 'plain', opts = {}) {
1760
1800
  procedureAnnounces,
1761
1801
  equippedLoadouts,
1762
1802
  personaAnnounce,
1803
+ parityPending,
1763
1804
  updateAvailable,
1764
1805
  activeCommandDrift: activeCommandDrift(),
1765
1806
  loadoutUpdateRequired,
1766
1807
  };
1767
- let announceReg = (0, app_settings_1.filterEnabledInterrupts)(reminder_registry_1.REGISTRY);
1808
+ // Derive the registry for THIS harness first, so a module's `harnesses:` tag is
1809
+ // honored here the way it already is on the opencode path.
1810
+ let announceReg = (0, app_settings_1.filterEnabledInterrupts)((0, reminder_registry_1.harnessModules)(platform, reminder_registry_1.REGISTRY));
1811
+ // Persona NEVER rides this inline path, on any harness or any event. Every
1812
+ // harness that reaches recap/recompact delivers it through the dedicated
1813
+ // `greprag-hook persona` hook (Claude Code + Codex, SessionStart AND
1814
+ // PostCompact) or, for Grok, through the uncapped sidecar file below — each
1815
+ // with a full ANNOUNCE_HARNESS_CAP allowance to itself. Competing inside
1816
+ // ANNOUNCE_INLINE_BUDGET never worked: the operator's 1,886-byte persona is
1817
+ // larger than the whole budget, so `fitAnnounceBudget` deferred it WHOLE on
1818
+ // every path except Claude Code SessionStart.
1819
+ //
1820
+ // Unconditional is the point. The previous attempt scoped this by harness and
1821
+ // broke Codex PostCompact, because `recompact` is one subcommand shared by
1822
+ // Claude Code and Codex and cannot tell which one invoked it. A rule that needs
1823
+ // no harness knowledge cannot be got wrong by a harness it cannot see.
1824
+ //
1825
+ // OpenCode is unaffected: its plugin never calls recap(), it renders
1826
+ // `persona-announce` from the same registry straight into the system prompt,
1827
+ // and that path has no inline cap. So the module stays registered for everyone.
1828
+ // adr: adr/persona.md, adr/announce-inline-budget.md
1829
+ const personaSidecar = platform === 'grok' && personaAnnounce
1830
+ ? personaAnnounce.trim() + '\n\n'
1831
+ : '';
1832
+ announceReg = announceReg.filter(m => m.id !== 'persona-announce');
1768
1833
  if ((0, reminder_injector_1.mechanicKilled)())
1769
1834
  announceReg = announceReg.filter(m => m.id !== 'mechanic-friction');
1770
1835
  if (!announceShort)
@@ -1783,7 +1848,7 @@ async function recap(input, mode = 'plain', opts = {}) {
1783
1848
  // that the rest existed. adr: adr/announce-inline-budget.md
1784
1849
  const announceBlock = buildInlineAnnounce((0, reminder_registry_1.collectAnnounceBlocks)(announceEnv, announceReg), (0, session_id_1.truncateSessionId)(input.session_id));
1785
1850
  if (opts.compact) {
1786
- writeRecapOutput(announceBlock ? announceBlock + '\n' : '', mode, grokShort, input.session_id);
1851
+ writeRecapOutput(announceBlock ? announceBlock + '\n' : '', mode, grokShort, input.session_id, personaSidecar);
1787
1852
  return;
1788
1853
  }
1789
1854
  const preamble = () => (announceBlock ? announceBlock + '\n' : '');
@@ -1792,7 +1857,7 @@ async function recap(input, mode = 'plain', opts = {}) {
1792
1857
  // hook globally. Setup warnings still fire above.
1793
1858
  if (!anchor.sessionStartRecap) {
1794
1859
  const out = preamble();
1795
- writeRecapOutput(out, mode, grokShort, input.session_id);
1860
+ writeRecapOutput(out, mode, grokShort, input.session_id, personaSidecar);
1796
1861
  return;
1797
1862
  }
1798
1863
  // Recap body is hourlies-only — daily summaries used to render here (one
@@ -1809,7 +1874,7 @@ async function recap(input, mode = 'plain', opts = {}) {
1809
1874
  // if they're pending.
1810
1875
  if (!body) {
1811
1876
  const out = preamble();
1812
- writeRecapOutput(out, mode, grokShort, input.session_id);
1877
+ writeRecapOutput(out, mode, grokShort, input.session_id, personaSidecar);
1813
1878
  return;
1814
1879
  }
1815
1880
  const parts = [];
@@ -1828,7 +1893,7 @@ async function recap(input, mode = 'plain', opts = {}) {
1828
1893
  ? body
1829
1894
  : '[greprag memory — session recap not inlined (over the SessionStart cap). '
1830
1895
  + 'Read it with `greprag memory recap`.]');
1831
- writeRecapOutput(parts.join('\n') + '\n', mode, grokShort, input.session_id);
1896
+ writeRecapOutput(parts.join('\n') + '\n', mode, grokShort, input.session_id, personaSidecar);
1832
1897
  }
1833
1898
  /** Arm-state detection moved LOCAL (2026-06-04). The former `isSessionArmed`
1834
1899
  * asked the server's watcher registry "does this session have a live socket?"
@@ -1975,9 +2040,14 @@ async function notify(input, source = 'claude-code', chipContext) {
1975
2040
  // proven-registered `notify` hook — so arming never depends on a freshly-merged
1976
2041
  // subcommand reaching the operator's settings. Fires whatever's deficient (stacking),
1977
2042
  // and auto-silences each module the instant its deficiency resolves.
2043
+ // Claude Code inbox delivery is TURN-BASED (2026-09-04). The Monitor watcher is
2044
+ // retired here: Claude Code has native in-harness session messaging, so greprag's
2045
+ // job is only to land OUTSIDE mail — and it does that on the same UserPromptSubmit
2046
+ // steering Codex uses. adr: adr/monitor-resilience.md
2047
+ const inbox = await (0, codex_steering_1.buildCodexInboxSteering)(input, cfg);
1978
2048
  const reminders = await (0, reminder_injector_1.injectReminders)(input, cfg, short);
1979
2049
  const inlineAtoms = (0, inline_atom_hook_1.buildPendingInlineAtomContext)(input, short);
1980
- const parts = [reminders, inlineAtoms].filter((p) => !!p);
2050
+ const parts = [inbox, reminders, inlineAtoms].filter((p) => !!p);
1981
2051
  if (parts.length)
1982
2052
  (0, hook_runtime_1.writeAdditionalContext)('UserPromptSubmit', parts.join('\n\n'));
1983
2053
  }
@@ -2256,7 +2326,7 @@ async function main() {
2256
2326
  const validSubs = new Set([
2257
2327
  'store', 'recap', 'recompact', 'notify', 'mail', 'friction-reminder', 'session-id', 'pre-spawn-check', 'armcheck',
2258
2328
  'collision-check', 'drain', 'coordinate-gate', 'codex-coordinate-gate', 'procedure-check',
2259
- 'guard', 'guard-refresh', 'guard-flush',
2329
+ 'guard', 'guard-refresh', 'guard-flush', 'persona',
2260
2330
  'context-probe', 'context-check', 'crush-wrap',
2261
2331
  'pre-compact', 'archive-pointer',
2262
2332
  'codex-store', 'codex-notify', 'codex-inbox', 'codex-recap',
@@ -2431,7 +2501,10 @@ async function main() {
2431
2501
  // silence on any non-qualifying command. A narrow rg preflight denial runs
2432
2502
  // before wrap because generated/temp crawls are harmful before output exists.
2433
2503
  // adr: adr/pipe-wrap-hook.md
2434
- const out = (0, search_guard_1.runSearchGuard)(input)
2504
+ // The merge guard runs FIRST: it is a deny, and denying a command must not
2505
+ // depend on whether a later rewrite step happens to fire.
2506
+ const out = (0, merge_guard_1.runMergeGuard)(input)
2507
+ || (0, search_guard_1.runSearchGuard)(input)
2435
2508
  || (0, pipe_wrap_1.runPipeWrap)(input);
2436
2509
  if (out)
2437
2510
  process.stdout.write(JSON.stringify(out) + '\n');
@@ -2444,6 +2517,15 @@ async function main() {
2444
2517
  if (message)
2445
2518
  process.stdout.write(JSON.stringify({ systemMessage: message }) + '\n');
2446
2519
  }
2520
+ else if (subcommand === 'persona') {
2521
+ // Persona rides its OWN hook, not the announce. The harness caps each hook's
2522
+ // inline output at ~2048 bytes independently, so a dedicated invocation gives
2523
+ // Persona a full allowance that no other announce section can crowd out.
2524
+ // Persona governs every token the agent emits; everything else in the
2525
+ // announce is reference material that `greprag load` can serve on demand.
2526
+ // adr: adr/announce-inline-budget.md
2527
+ await personaHook(input);
2528
+ }
2447
2529
  else if (subcommand === 'archive-pointer') {
2448
2530
  // SessionStart(matcher=compact) — the one documented post-compact channel
2449
2531
  // whose output reaches the model. Points at the PNG archive.