@cohortapp/agent-sdk 2.3.2 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/framework-features.json +30 -0
  2. package/lib/backlog.mjs +136 -0
  3. package/lib/cadences.mjs +63 -2
  4. package/lib/cadences.test.mjs +105 -0
  5. package/lib/capability/inventory.mjs +542 -0
  6. package/lib/capability/inventory.test.mjs +232 -0
  7. package/lib/capability/probe.mjs +255 -0
  8. package/lib/channels/contract.mjs +37 -1
  9. package/lib/channels/contract.test.mjs +25 -1
  10. package/lib/claude-bin.mjs +37 -3
  11. package/lib/claude-bin.test.mjs +42 -8
  12. package/lib/execution/disposition.mjs +501 -0
  13. package/lib/execution/disposition.test.mjs +482 -0
  14. package/lib/execution/drive.mjs +352 -0
  15. package/lib/execution/drive.test.mjs +270 -0
  16. package/lib/execution/effects.mjs +340 -0
  17. package/lib/execution/effects.test.mjs +193 -0
  18. package/lib/execution/index.mjs +152 -0
  19. package/lib/execution/intake.mjs +581 -0
  20. package/lib/execution/intake.test.mjs +343 -0
  21. package/lib/execution/journal.mjs +374 -0
  22. package/lib/execution/journal.test.mjs +261 -0
  23. package/lib/execution/match.mjs +331 -0
  24. package/lib/execution/match.test.mjs +235 -0
  25. package/lib/execution/pipeline.mjs +341 -0
  26. package/lib/execution/pipeline.test.mjs +389 -0
  27. package/lib/execution/route.mjs +332 -0
  28. package/lib/execution/route.test.mjs +186 -0
  29. package/lib/execution/surface-policy.mjs +446 -0
  30. package/lib/execution/surface-policy.test.mjs +162 -0
  31. package/lib/goals/admission.mjs +209 -0
  32. package/lib/goals/admission.test.mjs +139 -0
  33. package/lib/goals/classify.mjs +206 -0
  34. package/lib/goals/classify.test.mjs +109 -0
  35. package/lib/goals/collaborate.mjs +415 -0
  36. package/lib/goals/collaborate.test.mjs +324 -0
  37. package/lib/goals/gaps.mjs +111 -0
  38. package/lib/goals/gaps.test.mjs +284 -0
  39. package/lib/goals/loop.mjs +537 -0
  40. package/lib/goals/loop.test.mjs +719 -0
  41. package/lib/identity/persona.mjs +247 -0
  42. package/lib/identity/persona.test.mjs +117 -0
  43. package/lib/kpi.mjs +469 -0
  44. package/lib/kpi.test.mjs +244 -0
  45. package/lib/mandate/audit.mjs +168 -0
  46. package/lib/mandate/audit.test.mjs +195 -0
  47. package/lib/mandate/cache.mjs +162 -0
  48. package/lib/mandate/derive.mjs +317 -0
  49. package/lib/mandate/derive.test.mjs +224 -0
  50. package/lib/mandate/model.mjs +352 -0
  51. package/lib/mandate/model.test.mjs +145 -0
  52. package/lib/mandate/refresh.mjs +187 -0
  53. package/lib/mandate/refresh.test.mjs +293 -0
  54. package/lib/mcp/server.test.mjs +4 -4
  55. package/lib/org/approvals.mjs +14 -2
  56. package/lib/org/client.mjs +58 -22
  57. package/lib/org/client.test.mjs +3 -1
  58. package/lib/org/inbound/directedness.mjs +720 -0
  59. package/lib/org/inbound/directedness.test.mjs +543 -0
  60. package/lib/org/inbound/facts.mjs +501 -0
  61. package/lib/org/inbound/facts.test.mjs +375 -0
  62. package/lib/org/inbound/hydrate.mjs +535 -0
  63. package/lib/org/inbound/hydrate.test.mjs +326 -0
  64. package/lib/org/inbound/index.mjs +233 -0
  65. package/lib/org/inbound/index.test.mjs +324 -0
  66. package/lib/org/inbound/io.mjs +141 -0
  67. package/lib/org/inbound/project.mjs +201 -0
  68. package/lib/org/inbound/project.test.mjs +287 -0
  69. package/lib/org/inbound/surfaces.mjs +257 -0
  70. package/lib/org/knowledge.mjs +10 -1
  71. package/lib/org/knowledge.test.mjs +8 -1
  72. package/lib/org/leases.mjs +5 -0
  73. package/lib/org/mesh.mjs +17 -2
  74. package/lib/org/messaging.mjs +40 -4
  75. package/lib/org/messaging.test.mjs +40 -0
  76. package/lib/org/param-contract.mjs +694 -0
  77. package/lib/org/param-contract.test.mjs +451 -0
  78. package/lib/org/protocol.checksum +1 -1
  79. package/lib/org/protocol.mjs +8 -0
  80. package/lib/org/protocol.test.mjs +5 -1
  81. package/lib/org/push.mjs +1025 -0
  82. package/lib/org/push.test.mjs +690 -0
  83. package/lib/org/tool-surface.mjs +138 -38
  84. package/lib/org/tool-surface.test.mjs +13 -8
  85. package/lib/org/typing.mjs +341 -0
  86. package/lib/org/typing.test.mjs +291 -0
  87. package/lib/plan/compile.mjs +510 -0
  88. package/lib/plan/compile.test.mjs +286 -0
  89. package/lib/plan/emit.mjs +256 -0
  90. package/lib/plan/emit.test.mjs +246 -0
  91. package/lib/plan/explain.mjs +226 -0
  92. package/lib/plan/explain.test.mjs +188 -0
  93. package/lib/plan/schema.mjs +140 -0
  94. package/lib/resource-governor.mjs +47 -1
  95. package/lib/resource-governor.test.mjs +21 -1
  96. package/lib/setup/enroll-from-cohort.mjs +105 -17
  97. package/lib/setup/enroll-from-cohort.test.mjs +68 -1
  98. package/lib/setup/sections/identity.mjs +15 -4
  99. package/lib/setup/sections/identity.test.mjs +94 -0
  100. package/lib/setup/sections/inventory.mjs +178 -0
  101. package/lib/setup/sections/inventory.test.mjs +198 -0
  102. package/lib/setup/sections/mandate.mjs +392 -0
  103. package/lib/setup/sections/mandate.test.mjs +373 -0
  104. package/lib/setup/sections/subagents.mjs +427 -0
  105. package/lib/setup/sections/subagents.test.mjs +429 -0
  106. package/lib/setup/sections/verify.mjs +121 -0
  107. package/lib/setup/sections/verify.test.mjs +175 -0
  108. package/lib/setup/sot.mjs +2 -0
  109. package/lib/subagents/cli.mjs +463 -0
  110. package/lib/subagents/cli.test.mjs +389 -0
  111. package/lib/subagents/client.mjs +373 -0
  112. package/lib/subagents/client.test.mjs +309 -0
  113. package/lib/subagents/gap.mjs +268 -0
  114. package/lib/subagents/gap.test.mjs +234 -0
  115. package/lib/subagents/lock.mjs +296 -0
  116. package/lib/subagents/lock.test.mjs +248 -0
  117. package/lib/subagents/manifest.mjs +224 -0
  118. package/lib/subagents/manifest.test.mjs +175 -0
  119. package/lib/subagents/refs.mjs +274 -0
  120. package/lib/subagents/refs.test.mjs +204 -0
  121. package/lib/subagents/resolve.mjs +455 -0
  122. package/lib/subagents/resolve.test.mjs +422 -0
  123. package/lib/subagents/schema.mjs +467 -0
  124. package/lib/subagents/schema.test.mjs +306 -0
  125. package/package.json +8 -3
  126. package/plugins/maestro-skills/.claude-plugin/marketplace.json +16 -0
  127. package/policies/ai-disclosure.yaml +42 -2
  128. package/scaffold/CLAUDE.md +16 -2
  129. package/schedules/triggers/goal-steward.md +79 -0
  130. package/scripts/ci/conformance-org-api.mjs +792 -0
  131. package/scripts/ci/conformance-org-api.test.mjs +417 -0
  132. package/scripts/daemon/agent-daemon.mjs +36 -4
  133. package/scripts/daemon/cadence-handlers.mjs +145 -1
  134. package/scripts/daemon/goal-steward-cadence.test.mjs +243 -0
  135. package/scripts/daemon/inbox-deferral.mjs +45 -2
  136. package/scripts/daemon/inbox-deferral.test.mjs +56 -0
  137. package/scripts/daemon/inbox-wake.mjs +282 -0
  138. package/scripts/daemon/inbox-wake.test.mjs +199 -0
  139. package/scripts/daemon/prompt-builder.mjs +41 -1
  140. package/scripts/daemon/typing-registry.mjs +55 -2
  141. package/scripts/daemon/typing-registry.test.mjs +25 -0
  142. package/scripts/local-triggers/generate-plists.test.mjs +5 -5
  143. package/scripts/setup/gen-subagent-manifest.mjs +95 -0
  144. package/scripts/setup/gen-subagent-manifest.test.mjs +124 -0
  145. package/scripts/setup/generate-plan.mjs +108 -0
  146. package/scripts/setup/init-capability-manifest.mjs +70 -0
  147. package/scripts/setup/init-skill-marketplace.mjs +155 -0
  148. package/scripts/setup/init-skill-marketplace.test.mjs +193 -0
@@ -0,0 +1,341 @@
1
+ /**
2
+ * lib/execution/pipeline.mjs — the ONE call the inbox pipeline makes.
3
+ *
4
+ * `index.handleInboundEvent` decides and acts on one event, but it demands a
5
+ * `{candidate, verdict}` pair and an already-matched obligation. Nothing that
6
+ * actually holds inbound events has either: the daemon has flattened inbox items
7
+ * on disk, the channel adapters have their own item shape, the diagnostics
8
+ * modules have alert records, and the compiled plan sits unread in
9
+ * `config/plan.yaml`. This module closes that distance so a caller can hand over
10
+ * whatever it happens to have:
11
+ *
12
+ * import { processInbound } from "../../lib/execution/pipeline.mjs";
13
+ *
14
+ * const { results, stats } = await processInbound({
15
+ * items, // inbox items | MessageEvents | raw rows | alerts
16
+ * me: cfg.org.cohort.memberId,
17
+ * agentRoot,
18
+ * runtime, // emergencyStop / directive / staleness / budget
19
+ * effects: defaultEffects({ agentRoot, react, me }),
20
+ * log,
21
+ * });
22
+ *
23
+ * What it adds over calling `handleInboundEvent` in a loop, all of which is
24
+ * wrong if a caller re-implements it:
25
+ *
26
+ * **One history load per batch.** `loadHistory` scans the journal tail; doing
27
+ * it per item turns a 200-event sweep into 200 file reads.
28
+ *
29
+ * **In-batch dedupe, ping-pong and flood accounting.** The on-disk history is
30
+ * a SNAPSHOT taken before the batch. Without an overlay, an event delivered
31
+ * twice inside one sweep (the SSE push and the poll backstop converge — that
32
+ * convergence is the design, per §6.1) passes the dedupe guard twice, and a
33
+ * burst from one actor never trips the flood limit. The overlay makes the
34
+ * guards behave as if each event had been processed alone, in order.
35
+ *
36
+ * **Obligation binding.** The plan is read ONCE and every event is matched
37
+ * against its REACT obligations, so `decide` gets the argument that lifts it
38
+ * off the rung-1 cap — and an uncovered event produces the proposed obligation
39
+ * §6.1 asks for, not just a complaint.
40
+ *
41
+ * **Per-item isolation.** One malformed row cannot take a sweep down. It is
42
+ * caught, logged as an error (never swallowed), counted, and the batch
43
+ * continues.
44
+ *
45
+ * @module lib/execution/pipeline
46
+ */
47
+
48
+ "use strict";
49
+
50
+ import { handleInboundEvent } from "./index.mjs";
51
+ import { intake } from "./intake.mjs";
52
+ import { loadHistory, threadKey, dedupeKey } from "./journal.mjs";
53
+ import { matchObligation, proposeReact, loadReactObligations } from "./match.mjs";
54
+ import { tierOf, willAct } from "./disposition.mjs";
55
+
56
+ /** Blank-safe string. */
57
+ function s(v) {
58
+ return v == null ? "" : String(v);
59
+ }
60
+
61
+ /**
62
+ * Wrap a history snapshot so decisions taken EARLIER IN THIS BATCH count.
63
+ *
64
+ * The journal is appended by `drive` as it goes, but `loadHistory` already read
65
+ * its tail; re-reading per item would be correct and slow. This overlay is the
66
+ * cheap half: it tracks the same three facts the guards ask for and answers from
67
+ * the union of "on disk before the batch" and "decided during it".
68
+ *
69
+ * @param {object} base a `loadHistory` view
70
+ * @returns {object} the same interface, plus `note(decision, spoke)`
71
+ */
72
+ export function batchHistory(base) {
73
+ const seen = new Set();
74
+ const chains = new Map();
75
+ const actors = new Map();
76
+ // Threads a human spoke in DURING this batch. The on-disk chain count for them
77
+ // is stale by definition, so it is masked rather than added to — otherwise a
78
+ // human re-entering a conversation mid-sweep would not unblock the agent until
79
+ // the next sweep, which is the exact bug the human-turn reset exists to stop.
80
+ const reset = new Set();
81
+
82
+ return {
83
+ seen: (key) => seen.has(s(key)) || (base ? base.seen(key) : false),
84
+ chainDepth: (tk) =>
85
+ (chains.get(s(tk)) || 0) + (reset.has(s(tk)) || !base ? 0 : base.chainDepth(tk)),
86
+ actorActivity: (a) => (actors.get(s(a)) || 0) + (base ? base.actorActivity(a) : 0),
87
+ failures: (k) => (base ? base.failures(k) : 0),
88
+ rows: base ? base.rows : [],
89
+ degraded: base ? !!base.degraded : true,
90
+ degradedReason: base ? base.degradedReason : "no history supplied",
91
+
92
+ /**
93
+ * A human spoke in this thread. Called BEFORE the decision that follows it,
94
+ * mirroring `handleInboundEvent`'s ordering.
95
+ */
96
+ humanTurn(tk) {
97
+ const k = s(tk);
98
+ if (!k) return;
99
+ reset.add(k);
100
+ chains.set(k, 0);
101
+ },
102
+
103
+ /** Fold one processed event back into the overlay. */
104
+ note(decision, spoke) {
105
+ if (!decision) return;
106
+ if (decision.key) seen.add(s(decision.key));
107
+ if (decision.actor) actors.set(s(decision.actor), (actors.get(s(decision.actor)) || 0) + 1);
108
+ // Only a turn where the agent actually SPOKE deepens a conversation —
109
+ // identical to the rule `loadHistory` applies to journal rows, and for the
110
+ // same reason: three ignores in a row must not gag the fourth message.
111
+ if (spoke === true && decision.thread) {
112
+ chains.set(s(decision.thread), (chains.get(s(decision.thread)) || 0) + 1);
113
+ }
114
+ },
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Decide and act on ONE inbound artefact of any shape.
120
+ *
121
+ * @param {object} o
122
+ * @param {object} o.item inbox item | MessageEvent | raw row | alert | {candidate,verdict}
123
+ * @param {string} [o.me]
124
+ * @param {string} [o.agentRoot]
125
+ * @param {object} [o.runtime]
126
+ * @param {object} [o.effects]
127
+ * @param {object} [o.history] a `batchHistory` view (made here if absent)
128
+ * @param {object[]} [o.obligations] REACT obligations from the compiled plan
129
+ * @param {Array} [o.manifest] capability manifest
130
+ * @param {object} [o.facts] delegation/authority facts for this item
131
+ * @param {boolean} [o.humanTurn]
132
+ * @param {number} [o.nowMs]
133
+ * @param {Function} [o.log]
134
+ * @param {string} [o.traceId]
135
+ * @returns {Promise<object>} { ok, origin, decision, effect, ref, spoke, degraded, error, obligationKey, proposed }
136
+ */
137
+ export async function processOne(o = {}) {
138
+ const log = typeof o.log === "function" ? o.log : () => {};
139
+ const nowMs = Number.isFinite(o.nowMs) ? o.nowMs : Date.now();
140
+ const nowIso = new Date(nowMs).toISOString();
141
+
142
+ const norm = intake(o.item, { me: o.me, now: nowIso, facts: o.facts, opts: o.intakeOpts });
143
+ for (const d of norm.degraded) log("warn", `[execution] intake degraded (${norm.origin}): ${d}`);
144
+
145
+ const tier = norm.verdict && norm.verdict.directed ? tierOf(norm.verdict.reason) : null;
146
+
147
+ // Bind the event to the plan. An unbindable event is not an error — it is the
148
+ // signal that the plan has a hole, and it is reported as one below.
149
+ const match = matchObligation({
150
+ candidate: norm.candidate,
151
+ verdict: norm.verdict,
152
+ plan: { obligations: Array.isArray(o.obligations) ? o.obligations : [] },
153
+ tier,
154
+ });
155
+ for (const line of match.why) log("debug", `[execution] match: ${line}`);
156
+
157
+ const history = o.history || batchHistory(loadHistory({ agentRoot: o.agentRoot, nowMs }));
158
+ if (o.humanTurn === true && norm.candidate && typeof history.humanTurn === "function") {
159
+ history.humanTurn(threadKey(norm.candidate));
160
+ }
161
+
162
+ const res = await handleInboundEvent({
163
+ candidate: norm.candidate,
164
+ verdict: norm.verdict,
165
+ me: o.me,
166
+ history,
167
+ runtime: o.runtime,
168
+ obligation: match.obligation,
169
+ need: o.need,
170
+ manifest: o.manifest,
171
+ facts: o.facts,
172
+ actorFloodLimit: o.actorFloodLimit,
173
+ effects: o.effects,
174
+ agentRoot: o.agentRoot,
175
+ nowMs,
176
+ traceId: o.traceId,
177
+ log,
178
+ humanTurn: o.humanTurn === true,
179
+ });
180
+
181
+ // Fold the intake's own degradations onto the decision so the journal row and
182
+ // the caller see one list rather than two.
183
+ if (res.decision && Array.isArray(res.decision.degraded) && norm.degraded.length) {
184
+ res.decision.degraded = res.decision.degraded.concat(norm.degraded.map((d) => `intake:${d}`));
185
+ }
186
+ history.note(res.decision, res.spoke);
187
+
188
+ // §6.1's other half: an event that was unmistakably mine and matched nothing
189
+ // proposes the obligation that WOULD have covered it.
190
+ let proposed = null;
191
+ if (!match.obligation && willAct(res.decision) && !match.suspended.length) {
192
+ const p = proposeReact({ candidate: norm.candidate, verdict: norm.verdict, tier });
193
+ if (p.obligation) {
194
+ proposed = p.obligation.key;
195
+ const fn = o.effects && typeof o.effects.proposeObligation === "function" ? o.effects.proposeObligation : null;
196
+ if (fn) {
197
+ try {
198
+ await fn({ obligation: p.obligation, decision: res.decision, candidate: norm.candidate });
199
+ log("info", `[execution] ${p.why}`);
200
+ } catch (err) {
201
+ log("error", `[execution] could not record the proposed obligation ${p.obligation.key}: ${err && err.message ? err.message : String(err)}`);
202
+ }
203
+ } else {
204
+ log("warn", `[execution] no proposeObligation effect wired — ${p.obligation.key} was derived but not recorded`);
205
+ }
206
+ } else {
207
+ log("warn", `[execution] uncovered event produced no proposable obligation: ${p.why}`);
208
+ }
209
+ }
210
+
211
+ return {
212
+ ...res,
213
+ origin: norm.origin,
214
+ intakeWhy: norm.why,
215
+ tier,
216
+ obligationKey: match.obligation ? match.obligation.key || null : null,
217
+ suspendedObligations: match.suspended.map((x) => x.key),
218
+ proposed,
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Decide and act on a BATCH — the shape a sweep, an SSE drain or a cadence tick
224
+ * actually has.
225
+ *
226
+ * Items are processed IN ORDER. Order matters: the in-batch overlay means an
227
+ * earlier event's decision constrains a later one (dedupe, ping-pong, flood), so
228
+ * shuffling the batch would change the outcome. Callers should preserve ledger
229
+ * order, which every producer in the tree already does.
230
+ *
231
+ * @param {object} o as {@link processOne}, with `items` instead of `item`, plus:
232
+ * @param {object[]} o.items
233
+ * @param {object[]} [o.obligations] pre-loaded; read from config/plan.yaml when absent
234
+ * @param {boolean} [o.loadPlan] set false to skip the plan read entirely
235
+ * @param {Function} [o.factsFor] (norm, item) => facts, per item
236
+ * @param {Function} [o.isHumanTurn] (item) => boolean
237
+ * @returns {Promise<{results:object[], stats:object}>}
238
+ */
239
+ export async function processInbound(o = {}) {
240
+ const log = typeof o.log === "function" ? o.log : () => {};
241
+ const nowMs = Number.isFinite(o.nowMs) ? o.nowMs : Date.now();
242
+ const items = Array.isArray(o.items) ? o.items : [];
243
+
244
+ const stats = {
245
+ total: items.length,
246
+ processed: 0,
247
+ acted: 0,
248
+ ignored: 0,
249
+ failed: 0,
250
+ byDisposition: {},
251
+ bySurface: {},
252
+ byReason: {},
253
+ uncovered: 0,
254
+ proposed: [],
255
+ degraded: [],
256
+ };
257
+ if (items.length === 0) return { results: [], stats };
258
+
259
+ // ── the plan, read once ──────────────────────────────────────────────────
260
+ let obligations = Array.isArray(o.obligations) ? o.obligations : null;
261
+ if (!obligations && o.loadPlan !== false) {
262
+ const loaded = await loadReactObligations(o.agentRoot);
263
+ obligations = loaded.obligations;
264
+ if (loaded.degraded) {
265
+ // NEVER SILENT: with no plan every event reads as uncovered and the whole
266
+ // reactive lane is pinned at rung ≤1. That is a legitimate state for an
267
+ // un-adopted agent, but the operator has to be able to see it.
268
+ stats.degraded.push(`plan_unavailable:${loaded.reason}`);
269
+ log("warn", `[execution] no compiled REACT obligations (${loaded.reason}) — every event will route at rung ≤1`);
270
+ } else if (obligations.length === 0) {
271
+ stats.degraded.push("plan_has_no_react_obligations");
272
+ log("warn", "[execution] config/plan.yaml has no REACT obligations — the reactive lane is uncovered by construction");
273
+ }
274
+ }
275
+ if (!obligations) obligations = [];
276
+
277
+ // ── the journal tail, read once, then kept live for the batch ────────────
278
+ const base = o.history || loadHistory({ agentRoot: o.agentRoot, nowMs });
279
+ if (base && base.degraded) {
280
+ stats.degraded.push(`history_degraded:${base.degradedReason || "unknown"}`);
281
+ log("warn", `[execution] journal history degraded (${base.degradedReason || "unknown"}) — dedupe/ping-pong guards are weakened`);
282
+ }
283
+ const history = batchHistory(base);
284
+
285
+ const results = [];
286
+ for (const item of items) {
287
+ let res;
288
+ try {
289
+ const facts = typeof o.factsFor === "function" ? o.factsFor(item) : o.facts;
290
+ res = await processOne({
291
+ ...o,
292
+ item,
293
+ facts,
294
+ history,
295
+ obligations,
296
+ nowMs,
297
+ humanTurn: typeof o.isHumanTurn === "function" ? o.isHumanTurn(item) === true : o.humanTurn === true,
298
+ });
299
+ stats.processed += 1;
300
+ } catch (err) {
301
+ // A per-item throw is a bug somewhere below; it must not cost the caller
302
+ // the other 199 events. Loud, counted, and the batch continues.
303
+ const msg = err && err.message ? err.message : String(err);
304
+ stats.failed += 1;
305
+ log("error", `[execution] item threw and was skipped: ${msg}`);
306
+ results.push({ ok: false, error: msg, decision: null, origin: null });
307
+ continue;
308
+ }
309
+
310
+ results.push(res);
311
+ const d = res.decision || {};
312
+ const disp = s(d.disposition) || "unknown";
313
+ stats.byDisposition[disp] = (stats.byDisposition[disp] || 0) + 1;
314
+ const surf = s(d.surface) || "unknown";
315
+ stats.bySurface[surf] = (stats.bySurface[surf] || 0) + 1;
316
+ const reason = s(d.reason) || "unknown";
317
+ stats.byReason[reason] = (stats.byReason[reason] || 0) + 1;
318
+ if (willAct(d)) stats.acted += 1;
319
+ else stats.ignored += 1;
320
+ if (res.ok === false) stats.failed += 1;
321
+ if (!res.obligationKey && willAct(d)) stats.uncovered += 1;
322
+ if (res.proposed) stats.proposed.push(res.proposed);
323
+ }
324
+
325
+ const summary = Object.entries(stats.byDisposition)
326
+ .map(([k, v]) => `${k}×${v}`)
327
+ .sort()
328
+ .join(", ");
329
+ log(
330
+ "info",
331
+ `[execution] ${stats.processed}/${stats.total} event(s): ${summary || "none"}` +
332
+ (stats.uncovered ? ` · ${stats.uncovered} uncovered by the plan` : "") +
333
+ (stats.failed ? ` · ${stats.failed} failed` : ""),
334
+ );
335
+
336
+ return { results, stats };
337
+ }
338
+
339
+ export { dedupeKey, threadKey };
340
+
341
+ export default { processInbound, processOne, batchHistory };