greprag 5.79.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 (64) 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/announce.js +97 -0
  5. package/dist/commands/app-model.js +0 -1
  6. package/dist/commands/arm-reminder.js +9 -7
  7. package/dist/commands/collision-check.js +7 -6
  8. package/dist/commands/corpus/client.js +13 -3
  9. package/dist/commands/delivery-reminder.js +35 -14
  10. package/dist/commands/deploy-gate.js +55 -0
  11. package/dist/commands/deploy-lock.js +100 -0
  12. package/dist/commands/deploy-record.js +145 -0
  13. package/dist/commands/deploy-verify.js +111 -0
  14. package/dist/commands/inbox-primer-reminder.js +5 -5
  15. package/dist/commands/inbox-watch.js +2 -4
  16. package/dist/commands/init.js +96 -1
  17. package/dist/commands/load.js +40 -0
  18. package/dist/commands/loadout-reminder.js +1 -1
  19. package/dist/commands/merge-guard.js +419 -0
  20. package/dist/commands/merge-lock.js +176 -0
  21. package/dist/commands/parity-reminder.js +53 -0
  22. package/dist/commands/persona-reminder.js +11 -0
  23. package/dist/commands/persona.js +50 -0
  24. package/dist/commands/procedure.js +77 -6
  25. package/dist/commands/reminder-registry.js +107 -3
  26. package/dist/commands/repodoc.js +433 -0
  27. package/dist/commands/search.js +149 -0
  28. package/dist/commands/skillgain.js +33 -25
  29. package/dist/delivery-lifecycle.js +16 -1
  30. package/dist/deploy-gate.js +355 -0
  31. package/dist/deploy-locks.js +339 -0
  32. package/dist/deploy-verify.js +209 -0
  33. package/dist/env-redaction.js +157 -0
  34. package/dist/harness-limits.js +17 -0
  35. package/dist/hook-runtime.js +11 -1
  36. package/dist/hook.js +229 -88
  37. package/dist/index.js +601 -567
  38. package/dist/inline-atom-episode.js +15 -7
  39. package/dist/inline-atom.js +8 -2
  40. package/dist/native-skill-adoption.js +11 -0
  41. package/dist/native-skill-mirror.js +8 -1
  42. package/dist/node-identity.bundle.js +1166 -0
  43. package/dist/opencode-plugin.bundle.js +307 -119
  44. package/dist/procedure-enabled.js +55 -0
  45. package/dist/procedure-runtime.js +6 -0
  46. package/dist/procedure-scope.js +190 -0
  47. package/dist/procedure-watch.js +29 -16
  48. package/dist/procedure.js +111 -5
  49. package/dist/project-anchor.js +1 -14
  50. package/dist/reminder-injector.js +11 -10
  51. package/dist/repodoc-client.js +296 -0
  52. package/dist/session-id.js +7 -8
  53. package/dist/skill-landing.js +57 -2
  54. package/dist/skill-mirror-client.js +14 -0
  55. package/dist/skill-mirror-files.js +18 -0
  56. package/package.json +2 -2
  57. package/scripts/bundle-node-identity.mjs +47 -0
  58. package/skill/templates/chip-spawn.md +7 -1
  59. package/skill/templates/delivery.md +105 -0
  60. package/skill/templates/prompt-audit.md +196 -0
  61. package/skill/templates/skill-change.md +25 -2
  62. package/dist/assistant-doctrine.js +0 -85
  63. package/dist/commands/assistant-reminder.js +0 -19
  64. package/dist/commands/assistant.js +0 -95
@@ -38,8 +38,38 @@ var __importStar = (this && this.__importStar) || (function () {
38
38
  };
39
39
  })();
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.fitPersonaAnnounce = fitPersonaAnnounce;
41
42
  exports.runPersona = runPersona;
42
43
  const fs = __importStar(require("fs"));
44
+ const harness_limits_1 = require("../harness-limits");
45
+ /** Last line of defence against a silently half-delivered Persona.
46
+ *
47
+ * `validatePersonaContent` rejects an oversize persona at set time, but rows
48
+ * written before that check existed are still in the table and the column has no
49
+ * DB constraint — so a rendered announce can still arrive over the cap. The
50
+ * harness's own handling is the failure this ADR exists to end: it keeps the
51
+ * first 2KB, drops the rest, and says nothing.
52
+ *
53
+ * So the hook cuts it deliberately and SAYS SO. A session that knows its
54
+ * speaking rules are incomplete, and where to read the rest, is strictly better
55
+ * off than one silently obeying half of them — and better off than one given
56
+ * nothing, since a truncated persona still carries whatever the operator put
57
+ * first. adr: adr/persona.md */
58
+ function fitPersonaAnnounce(announce, cap = harness_limits_1.ANNOUNCE_HARNESS_CAP) {
59
+ if (Buffer.byteLength(announce, 'utf8') <= cap)
60
+ return announce;
61
+ const notice = '\n[/Persona]\n[greprag: persona TRUNCATED — the stored persona is larger than the '
62
+ + `${cap} bytes this harness inlines per hook, so the rules above are INCOMPLETE. `
63
+ + 'Read the whole thing with `greprag persona show`.]';
64
+ const room = cap - Buffer.byteLength(notice, 'utf8');
65
+ // Slicing bytes can land mid-codepoint; a trailing U+FFFD is the evidence.
66
+ let body = Buffer.from(announce, 'utf8').subarray(0, room).toString('utf8').replace(/�+$/, '');
67
+ // Prefer a whole-line cut, but never throw away most of the persona to get one.
68
+ const nl = body.lastIndexOf('\n');
69
+ if (nl > room * 0.5)
70
+ body = body.slice(0, nl);
71
+ return body + notice;
72
+ }
43
73
  function getConfig() {
44
74
  return {
45
75
  apiUrl: process.env.GREPRAG_API_URL || 'https://api.greprag.com',
@@ -161,9 +191,29 @@ async function show() {
161
191
  }
162
192
  const source = p.mode === 'template' ? `template ${p.templateId}` : 'custom';
163
193
  console.log(`Persona (${source})`);
194
+ const warning = deliveryWarning(data.announce);
195
+ if (warning)
196
+ console.log(warning);
164
197
  console.log('');
165
198
  console.log(p.content || '');
166
199
  }
200
+ /** Say plainly when what is STORED is bigger than what is DELIVERED.
201
+ *
202
+ * The server returns the rendered announce — content plus its provenance
203
+ * envelope — and that is the string the harness caps. A row written before the
204
+ * set-time limit existed can still be over it, and until now `show` printed it
205
+ * in full as if all of it reached a session. adr: adr/persona.md */
206
+ function deliveryWarning(announce) {
207
+ if (typeof announce !== 'string' || !announce)
208
+ return null;
209
+ const bytes = Buffer.byteLength(announce, 'utf8');
210
+ if (bytes <= harness_limits_1.ANNOUNCE_HARNESS_CAP)
211
+ return null;
212
+ return `⚠ OVER THE DELIVERY LIMIT — this persona renders to ${bytes} bytes, but the harness `
213
+ + `inlines only ${harness_limits_1.ANNOUNCE_HARNESS_CAP} per hook. About ${bytes - harness_limits_1.ANNOUNCE_HARNESS_CAP} bytes `
214
+ + 'never reach a session; what does arrive is marked as truncated. Shorten it with '
215
+ + '`greprag persona set`.';
216
+ }
167
217
  async function clear() {
168
218
  const { ok, status, data } = await api('DELETE', '/v1/persona');
169
219
  if (!ok)
@@ -7,6 +7,8 @@
7
7
  * show <verb> — print one procedure + effective runtime behavior
8
8
  * seed [--git] — write the git-ecosystem seed set (commit/merge/push/deploy/release)
9
9
  * register --verb V --steps "..." [--caveats "..."] [--endpoint "..."] [--trigger T ...] [--destructive]
10
+ * — non-lifecycle verbs only; a lifecycle verb is refused, never
11
+ * half-written (the seed refresh would reclaim the row on read)
10
12
  * rm <verb> — remove a procedure
11
13
  * match "<text>" — dry-run: effective prompt-boundary behavior
12
14
  *
@@ -19,7 +21,9 @@ exports.runProcedure = runProcedure;
19
21
  const procedure_1 = require("../procedure");
20
22
  const procedure_run_1 = require("../procedure-run");
21
23
  const procedure_runtime_1 = require("../procedure-runtime");
24
+ const delivery_lifecycle_1 = require("../delivery-lifecycle");
22
25
  const project_anchor_1 = require("../project-anchor");
26
+ const procedure_enabled_1 = require("../procedure-enabled");
23
27
  function getFlag(args, flag) {
24
28
  const idx = args.indexOf(flag);
25
29
  if (idx === -1 || idx + 1 >= args.length)
@@ -64,6 +68,11 @@ function runtimeState(proc, install) {
64
68
  return (0, procedure_runtime_1.procedureRuntimeState)(install.state, proc);
65
69
  }
66
70
  function printRuntimeDisposition(proc, install) {
71
+ // adr: adr/procedure-system-off.md
72
+ if (!procedure_enabled_1.PROCEDURE_SYSTEM_ENABLED) {
73
+ console.log('Inert. The Procedure System is off, so the prompt boundary injects nothing for any verb.');
74
+ return true;
75
+ }
67
76
  const state = runtimeState(proc, install);
68
77
  if (state === 'active')
69
78
  return false;
@@ -108,6 +117,17 @@ function printInstall(proc, install) {
108
117
  if (install.ratifiedAt)
109
118
  console.log(` ratified: ${install.ratifiedAt} (${install.ratifiedBy || 'operator'})`);
110
119
  }
120
+ /** Did the store honour exactly what `register` asked for? Compares only the
121
+ * register-owned fields (the store stamps `updatedAt` of its own accord).
122
+ * The invariant: the CLI never reports a write it will not honour. */
123
+ function storedMatchesRegistration(stored, asked) {
124
+ return stored.steps === asked.steps
125
+ && (stored.caveats ?? '') === (asked.caveats ?? '')
126
+ && (stored.endpoint ?? '') === (asked.endpoint ?? '')
127
+ && stored.status === asked.status
128
+ && !!stored.destructive === !!asked.destructive
129
+ && JSON.stringify(stored.triggers) === JSON.stringify(asked.triggers);
130
+ }
111
131
  function installForState(proc, current, state, settings = current.settings) {
112
132
  const now = new Date().toISOString();
113
133
  return {
@@ -121,6 +141,8 @@ function installForState(proc, current, state, settings = current.settings) {
121
141
  };
122
142
  }
123
143
  async function runProcedure(args) {
144
+ if (!procedure_enabled_1.PROCEDURE_SYSTEM_ENABLED)
145
+ console.log(procedure_enabled_1.PROCEDURE_SYSTEM_OFF_NOTE + '\n');
124
146
  const sub = args[0];
125
147
  const rest = args.slice(1);
126
148
  const { projectId, projectName } = resolveProjectId();
@@ -390,13 +412,16 @@ async function runProcedure(args) {
390
412
  // --git is the only seed set today; default to it so bare `seed` works.
391
413
  const store = (0, procedure_1.readProcedureStore)(projectId);
392
414
  let next = store;
393
- for (const p of procedure_1.GIT_SEED_SET) {
415
+ const seeds = procedure_1.GIT_SEED_SET;
416
+ for (const p of seeds) {
394
417
  next = (0, procedure_1.upsertProcedure)(next, p, /* force */ true);
395
418
  }
396
419
  (0, procedure_1.writeProcedureStore)(projectId, next);
397
- console.log(`Seeded ${procedure_1.GIT_SEED_SET.length} git-ecosystem procedures for ${projectName}:`);
398
- for (const p of procedure_1.GIT_SEED_SET)
399
- console.log(` • ${p.verb}${p.destructive ? ' (destructive — seed-only)' : ''}`);
420
+ console.log(`Seeded ${seeds.length} git-ecosystem procedures for ${projectName}:`);
421
+ for (const p of seeds) {
422
+ const note = p.destructive ? ' (destructive — seed-only)' : '';
423
+ console.log(` • ${p.verb}${note}`);
424
+ }
400
425
  console.log(`\n store: ${(0, procedure_1.procedureStorePath)(projectId)}`);
401
426
  return;
402
427
  }
@@ -407,6 +432,39 @@ async function runProcedure(args) {
407
432
  console.error('Usage: greprag procedure register --verb <v> --steps "<...>" [--caveats "<...>"] [--endpoint "<...>"] [--trigger <t> ...] [--destructive]');
408
433
  process.exit(1);
409
434
  }
435
+ // A delivery-owned verb cannot carry a recipe. Its text belongs to the
436
+ // repo's Delivery Profile document, and a copy here forks from that
437
+ // document the moment the document is corrected — with nothing to surface
438
+ // the divergence. The bundled definition reclaims the row on the next read
439
+ // either way, so refuse the write rather than confirm one that will not
440
+ // survive. adr: adr/delivery-owned-procedures.md
441
+ if ((0, delivery_lifecycle_1.isDeliveryOwnedVerb)(verb)) {
442
+ const lifecycle = (0, delivery_lifecycle_1.isDeliveryLifecycleVerb)(verb);
443
+ console.error(`Refusing to register "${verb}" — nothing was written.`);
444
+ if (lifecycle) {
445
+ console.error(` The lifecycle verbs (${delivery_lifecycle_1.DELIVERY_LIFECYCLE_VERBS.join(', ')}) are vocabulary-only during the`);
446
+ console.error(' announce-first Delivery System pilot. Procedure records shadow evidence for them and');
447
+ console.error(' never injects steps, and the bundled seed definition reclaims the row on the next read.');
448
+ }
449
+ else {
450
+ console.error(` "${verb}" is delivery-owned: its recipe is a section of this repo's Delivery Profile`);
451
+ console.error(' document, and Procedure injects a live pointer to that section rather than a copy.');
452
+ console.error(' Correcting the document is what changes what a session is told — there is no');
453
+ console.error(' registered text to keep in sync, which is exactly why this write is refused.');
454
+ }
455
+ console.error(` Put the ${verb} recipe in this repo's Delivery Profile document instead:`);
456
+ console.error(' greprag delivery resolve # names the profile + its documents');
457
+ console.error(' See docs/procedure-system.md (Seeding) and docs/delivery-profiles.md.');
458
+ process.exit(1);
459
+ }
460
+ // Every other verb: the system is off, so a registered recipe would never
461
+ // reach a session, and the CLI never reports a write it will not honour.
462
+ // adr: adr/procedure-system-off.md
463
+ if (!procedure_enabled_1.PROCEDURE_SYSTEM_ENABLED) {
464
+ console.error(`Refusing to register "${verb}" — nothing was written.`);
465
+ console.error(` ${procedure_enabled_1.PROCEDURE_SYSTEM_OFF_NOTE}`);
466
+ process.exit(1);
467
+ }
410
468
  const triggers = collectFlags(rest, '--trigger');
411
469
  const proc = {
412
470
  verb,
@@ -419,8 +477,21 @@ async function runProcedure(args) {
419
477
  };
420
478
  const store = (0, procedure_1.readProcedureStore)(projectId);
421
479
  (0, procedure_1.writeProcedureStore)(projectId, (0, procedure_1.upsertProcedure)(store, proc, /* force */ true));
480
+ // Report the STORED row, never the in-memory candidate. Echoing the
481
+ // candidate is what let a discarded write look like a success.
482
+ const after = (0, procedure_1.readProcedureStore)(projectId);
483
+ const stored = after.procedures.find(p => p.verb === verb);
484
+ if (!stored || !storedMatchesRegistration(stored, proc)) {
485
+ console.error(`Registered "${verb}" did not survive the store round-trip — the definition was NOT honoured.`);
486
+ if (stored) {
487
+ console.error(' the store now holds:');
488
+ printProcedure(stored, after);
489
+ }
490
+ console.error(` store: ${(0, procedure_1.procedureStorePath)(projectId)}`);
491
+ process.exit(1);
492
+ }
422
493
  console.log(`Registered procedure "${verb}" for ${projectName}.`);
423
- printProcedure(proc);
494
+ printProcedure(stored, after);
424
495
  return;
425
496
  }
426
497
  case 'rm': {
@@ -476,7 +547,7 @@ async function runProcedure(args) {
476
547
  console.log(' run-status <id> show latest run + evidence');
477
548
  console.log(' complete <id> <outcome> close the current run');
478
549
  console.log(' seed --git write the git-ecosystem seed set');
479
- console.log(' register --verb ... hand-roll a procedure');
550
+ console.log(' register --verb ... hand-roll a procedure (lifecycle verbs are refused — see the Delivery Profile)');
480
551
  console.log(' rm <verb> remove a procedure');
481
552
  console.log(' match "<text>" dry-run effective prompt-boundary behavior');
482
553
  return;
@@ -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_POINTER_RESERVE = 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");
@@ -16,7 +18,6 @@ const email_primer_reminder_1 = require("./email-primer-reminder");
16
18
  const corpus_reminder_1 = require("./corpus-reminder");
17
19
  const doc_pointer_reminder_1 = require("./doc-pointer-reminder");
18
20
  const setup_reminder_1 = require("./setup-reminder");
19
- const assistant_reminder_1 = require("./assistant-reminder");
20
21
  const memory_reflex_1 = require("./memory-reflex");
21
22
  const arm_reminder_1 = require("./arm-reminder");
22
23
  const friction_reminder_1 = require("./friction-reminder");
@@ -28,6 +29,7 @@ const procedure_reminder_1 = require("./procedure-reminder");
28
29
  const loadout_reminder_1 = require("./loadout-reminder");
29
30
  const delivery_reminder_1 = require("./delivery-reminder");
30
31
  const persona_reminder_1 = require("./persona-reminder");
32
+ const parity_reminder_1 = require("./parity-reminder");
31
33
  /** Registry order = display order. THE single agent-facing announce/reminder assembly:
32
34
  * the hook does I/O → fills ReminderEnv → collectAnnounces (SessionStart) / collectReminders
33
35
  * (per turn) render every module here in this order. A module may own either surface or both;
@@ -47,11 +49,11 @@ exports.REGISTRY = [
47
49
  procedure_reminder_1.procedureAnnounceModule, // active project procedures, rendered generically from operator-owned installs
48
50
  delivery_reminder_1.deliveryControlModule, // repo-scoped announce-first Delivery System pilot (all harnesses)
49
51
  persona_reminder_1.personaAnnounceModule, // tenant-scoped Persona, shared across projects/harnesses
52
+ parity_reminder_1.parityPendingModule, // what THIS harness still owes when another one advanced
50
53
  loadout_reminder_1.loadoutRegistrarModule, // equipped loadouts: one advisor announce each + per-turn keyword Match (docs/loadout.md)
51
54
  setup_reminder_1.setupWarningModule,
52
55
  version_reminder_1.versionUpgradeModule, // Deficiency-gated announce — silent unless a newer release exists
53
56
  enrichment_health_reminder_1.enrichmentHealthModule, // Deficiency-gated announce — silent unless the Gemini probe fails (fix c2eb8777)
54
- assistant_reminder_1.assistantDoctrineModule,
55
57
  memory_reflex_1.memoryPrimerModule,
56
58
  arm_reminder_1.watcherArmModule,
57
59
  friction_reminder_1.mechanicFrictionModule,
@@ -147,3 +149,105 @@ 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
+ *
183
+ * Re-exported, not redeclared: the number now has one owner in
184
+ * `@greprag/core`'s `harness-limits`, mirrored into the CLI at
185
+ * `src/harness-limits.ts`. The server derives the Persona content limit from
186
+ * the same value, and a second literal is how `PERSONA_MAX_CHARS = 12_000`
187
+ * became a limit that governed nothing. */
188
+ var harness_limits_1 = require("../harness-limits");
189
+ Object.defineProperty(exports, "ANNOUNCE_HARNESS_CAP", { enumerable: true, get: function () { return harness_limits_1.ANNOUNCE_HARNESS_CAP; } });
190
+ /** Bytes the overflow pointer line costs, reserved before fitting so adding it can
191
+ * never be what pushes the payload over the cap. Declared here beside the budget
192
+ * because the `mustInline` guard has to charge a module the same reserve the hook
193
+ * does — a module sized against the raw budget would still be dropped in the field.
194
+ * adr: adr/announce-inline-budget.md */
195
+ exports.ANNOUNCE_POINTER_RESERVE = 220;
196
+ /** Inline-worthiness ranking, most-keepable first. The rule: a block earns inline
197
+ * space when it is SHORT, SESSION-SPECIFIC, and available nowhere else. A block
198
+ * that is long, static, and already loadable on demand does not — it is exactly
199
+ * what `greprag load` exists for. Anything unlisted sorts last.
200
+ *
201
+ * Persona leads because it is 523 bytes of tenant-set speaking instructions that
202
+ * no other surface carries. The grepragOS laws are deliberately NOT here: 1753
203
+ * bytes of static doctrine that `greprag load os` already serves on demand, which
204
+ * until now consumed the entire budget and starved everything behind it. */
205
+ const ANNOUNCE_PRIORITY = [
206
+ 'persona-announce',
207
+ // Work this harness owes. Deficiency-gated, so it is silent almost always — and
208
+ // when it does fire it is the most actionable thing in the announce.
209
+ 'parity-pending',
210
+ 'setup-warning',
211
+ 'version-upgrade',
212
+ 'enrichment-health',
213
+ 'watcher-arm',
214
+ 'skill-mirror-announce',
215
+ 'delivery-control',
216
+ 'doc-pointer-announce',
217
+ ];
218
+ function announceRank(id) {
219
+ const i = ANNOUNCE_PRIORITY.indexOf(id);
220
+ return i === -1 ? ANNOUNCE_PRIORITY.length : i;
221
+ }
222
+ /** Fit the announce into the harness's inline budget.
223
+ *
224
+ * Returns the blocks that fit (restored to boot order, so a primer still precedes
225
+ * anything that depends on it) plus the ids that did not. The caller is expected
226
+ * to persist the FULL text and give the agent a way to read it — dropping content
227
+ * silently is the failure this whole mechanism exists to end.
228
+ *
229
+ * A single block larger than the budget is never emitted; it would blow the cap
230
+ * on its own and take everything after it down too. */
231
+ function fitAnnounceBudget(blocks, budget = exports.ANNOUNCE_INLINE_BUDGET, reserve = 0) {
232
+ const order = new Map(blocks.map((b, i) => [b.id, i]));
233
+ const ranked = [...blocks].sort((a, b) => {
234
+ const d = announceRank(a.id) - announceRank(b.id);
235
+ return d !== 0 ? d : (order.get(a.id) - order.get(b.id));
236
+ });
237
+ const kept = [];
238
+ const droppedIds = [];
239
+ let used = reserve;
240
+ const SEP = 2; // the '\n\n' join between blocks
241
+ for (const b of ranked) {
242
+ const cost = b.text.length + (kept.length ? SEP : 0);
243
+ if (used + cost <= budget) {
244
+ kept.push(b);
245
+ used += cost;
246
+ }
247
+ else {
248
+ droppedIds.push(b.id);
249
+ }
250
+ }
251
+ kept.sort((a, b) => order.get(a.id) - order.get(b.id));
252
+ return { kept, droppedIds };
253
+ }