instar 1.3.474 → 1.3.475

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,201 @@
1
+ # Side-Effects Review — Proactive Growth Digest Publisher (Slice 2)
2
+
3
+ **Version / slug:** `proactive-growth-digest-publisher-slice2`
4
+ **Date:** `2026-06-10`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required (Tier 2 — converged + approved spec, 8-reviewer panel, 3 iterations)`
7
+
8
+ ## Summary of the change
9
+
10
+ Adds `GrowthDigestPublisher` (`src/monitoring/GrowthDigestPublisher.ts`): an in-process
11
+ component that consumes the existing `monitoring.growthAnalyst.digestCron` and, on that
12
+ cadence, formats ONE consolidated "growth check-in" from the already-computed
13
+ `GrowthMilestoneAnalyst.buildDigest()` and routes it through the existing flood-guarded
14
+ post-update path. It is the cadence + delivery half (Slice 2) of the growth analyst;
15
+ Slice 1 (compute + read routes) already shipped. To give the publisher a delivery path
16
+ that provably cannot bypass the dedup/budget/tone guards, the change carves a pure,
17
+ `res`-free `evaluateOutbound` funnel out of `checkOutboundMessage` in
18
+ `src/server/routes.ts` and adds a `postToUpdatesTopic` helper both the route and the
19
+ publisher share. New config keys (`digestDelivery`, `digestTimezone`,
20
+ `digestSendOnCalmWeeks`) land in `ConfigDefaults.ts` + `types.ts`; wiring + teardown in
21
+ `AgentServer.ts`. Ships dark (`digestDelivery: 'off'` by default, even on a dev agent).
22
+
23
+ ## Decision-point inventory
24
+
25
+ - `evaluateOutbound` (`src/server/routes.ts`) — **add** — pure res-free extraction of the
26
+ localhost-link guard + tone-gate decision out of `checkOutboundMessage`. Both the route
27
+ adapter and the publisher's `postToUpdatesTopic` call this single function.
28
+ - `checkOutboundMessage` (`src/server/routes.ts`) — **modify** — now a thin route adapter:
29
+ fires the two observe-only observers (route-side) + delegates the decision to
30
+ `evaluateOutbound` + maps the decision to res. Behavior byte-identical for callers.
31
+ - `postToUpdatesTopic` (`src/server/routes.ts`) — **add** — the publisher's guarded sender:
32
+ resolve Updates topic → `evaluateOutbound` → `sendToTopic`. Returns a flat DeliveryResult.
33
+ - `GrowthDigestPublisher.publishOnce` — **add** — the lease-gate → mode → in-flight →
34
+ calm → format → deliver decision chain. The publisher holds NO authority: it only sends
35
+ a message or stays quiet; a guard block is a normal, non-error outcome it never re-acts on.
36
+ - `digestDelivery` / `digestSendOnCalmWeeks` / `digestTimezone` config — **add** — the
37
+ rollout + cadence dials under `monitoring.growthAnalyst`.
38
+
39
+ ---
40
+
41
+ ## 1. Over-block
42
+
43
+ **What legitimate inputs does this change reject that it shouldn't?**
44
+
45
+ The `evaluateOutbound` extraction preserves the EXACT block/allow logic of the prior
46
+ `checkOutboundMessage` (localhost-link guard + tone gate). No new block surface is added;
47
+ the decision function returns the same 422 bodies for the same inputs (verified by the
48
+ existing `post-update-gate-budget-route`, `localhost-link-guard-route`, and
49
+ `outbound-content-dedup-route` suites — all 20 tests pass unchanged). The publisher itself
50
+ adds no block/allow surface — it is a sender, not a gate. Over-block: not applicable to the
51
+ new code; preserved for the refactored code.
52
+
53
+ ---
54
+
55
+ ## 2. Under-block
56
+
57
+ **What failure modes does this still miss?**
58
+
59
+ The publisher's bounded multi-machine handoff edge (§3.7): if a lease handoff falls between
60
+ a window's fire time and the newly-awake machine's `.start()`/catch-up, that one window can
61
+ be re-sent once (the per-machine audit log has no record of the old machine's send). This is
62
+ a deliberately-accepted under-guard in the SAFE direction — a single bounded re-send through
63
+ the same aggregating/budgeted/deduped funnel, biased toward "the check-in arrives" over
64
+ "silently dropped," which is the slice's entire reason to exist. The near-simultaneous case
65
+ is absorbed by the shared `evaluateOutbound` dedup. No new under-block in the message-gate
66
+ logic (the refactor is behavior-preserving).
67
+
68
+ ---
69
+
70
+ ## 3. Level-of-abstraction fit
71
+
72
+ **Is this at the right layer?**
73
+
74
+ Correct layers. The publisher is a thin cadence/delivery wrapper (low-level orchestration)
75
+ that DELEGATES the block/allow decision to the existing smart gate via the shared
76
+ `evaluateOutbound` — it does not re-implement guarding. The formatter is a pure render with
77
+ no decision authority (the analyst already decided what crosses a rule). The funnel
78
+ extraction is specifically a level-of-abstraction fix: it ensures the publisher FEEDS the
79
+ existing chokepoint rather than running a parallel un-guarded `sendToTopic`. The two
80
+ observe-only observers (`observeSelfViolation`, `observePrincipalCoherence`) deliberately
81
+ stay route-side, not in `evaluateOutbound` — they have nothing to catch on a proactive
82
+ digest (it credits no operator role and is authored by no principal), so keeping them out
83
+ of the shared funnel both preserves byte-identical caller behavior and avoids meaningless
84
+ telemetry.
85
+
86
+ ---
87
+
88
+ ## 4. Signal vs authority compliance
89
+
90
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
91
+
92
+ **Does this change hold blocking authority with brittle logic?**
93
+
94
+ - [x] No — this change has no block/allow surface (the publisher is a sender; the
95
+ refactored `evaluateOutbound` keeps the SAME smart gate — `messagingToneGate`, an
96
+ LLM-backed authority with recent conversational context — as the sole decider).
97
+
98
+ The publisher produces a message or stays silent; it never blocks, delays, or rewrites
99
+ anything. The §3.5 superseded-job belt is a SIGNAL (an audit line), never a cross-component
100
+ mutation — the publisher never disables another component's job. The funnel extraction
101
+ keeps the single smart authority (`messagingToneGate`) and merely makes the publisher route
102
+ through it rather than around it. No brittle detector gains block authority.
103
+
104
+ ---
105
+
106
+ ## 5. Interactions
107
+
108
+ **Does this interact with existing checks, recovery paths, or infrastructure?**
109
+
110
+ - **Shadowing:** `evaluateOutbound` runs the same localhost-guard-then-tone-gate order as
111
+ before; `checkOutboundMessage` still fires the two observers FIRST (route-side), so no
112
+ existing route caller's observation is shadowed. The publisher path intentionally does NOT
113
+ fire those observers (nothing to catch) — confirmed not relied on for the digest path.
114
+ - **Double-fire:** the in-process cron runs on BOTH the awake and standby machine → the
115
+ lease gate (`isAwake`) ensures only the awake machine sends (mirrors the
116
+ scheduler/ActivitySentinel precedent the superseded `initiative-digest-review` relied on).
117
+ The superseded job + the publisher both fire `0 11 * * 1` — handled by §3.5 (durable
118
+ source-template disable at the live-flip, NOT at this dark merge; while `digestDelivery`
119
+ is `off`/`dry-run` the publisher doesn't send, so the job stays the sole voice).
120
+ - **Races:** the in-flight guard + croner `protect:true` prevent overlapping `publishOnce`
121
+ passes. The audit log is the single shared state; window-key idempotency is recorded only
122
+ on a real post-lease decision.
123
+ - **Feedback loops:** none — the digest reads analyst state and emits one message; it feeds
124
+ no system that feeds back into the analyst.
125
+
126
+ ---
127
+
128
+ ## 6. External surfaces
129
+
130
+ **Does this change anything visible outside the immediate code path?**
131
+
132
+ - **Other agents on the same machine:** none — internal component, no shared external state.
133
+ - **Install base:** none at merge — ships `digestDelivery: 'off'`; existing agents get the
134
+ three new config defaults via `applyDefaults` add-missing-only deep-merge (no migrateConfig
135
+ needed, matching the rest of the `growthAnalyst` block). No CLAUDE.md template change at
136
+ this dark merge (it rides the live-flip per the Agent Awareness Standard, parent §9).
137
+ - **External systems (Telegram):** only when an operator flips `digestDelivery` to
138
+ `dry-run`/`live` — and then it sends ONE message into the EXISTING Agent Updates topic via
139
+ the same guarded path as `/telegram/post-update`. Never a new topic, never per-finding.
140
+ - **Persistent state:** a new append-only audit log at `logs/growth-digest.jsonl`
141
+ (best-effort, never throws). No DB, no schema change.
142
+ - **Timing:** the weekly cron + a 60s settle-delayed catch-up; the cadence sanity-floor
143
+ refuses a sub-hourly `digestCron`.
144
+
145
+ ---
146
+
147
+ ## 7. Rollback cost
148
+
149
+ **If this turns out wrong in production, what's the back-out?**
150
+
151
+ Pure code change shipping dark — revert and ship a patch. No persistent state needs cleanup
152
+ (`logs/growth-digest.jsonl` is an inert append-only log; leaving it is harmless). No agent
153
+ state repair (the publisher is simply not constructed when `digestDelivery: 'off'`). No
154
+ user-visible regression during the rollback window, because at merge nothing is sent
155
+ (default off). The `evaluateOutbound` extraction is behavior-preserving and covered by the
156
+ pre-existing route suites, so reverting it carries no caller-facing risk either.
157
+
158
+ ---
159
+
160
+ ## Conclusion
161
+
162
+ This review found no new block/allow surface and no signal-vs-authority violation: the
163
+ publisher is a sender that delegates every guard decision to the existing smart gate via a
164
+ deliberately-extracted single funnel, closing off the "second un-guarded send path" the
165
+ review flagged in the draft. The multi-machine double-send (the one SERIOUS finding from
166
+ convergence) is fixed by the lease gate; the bounded handoff re-send is an accepted,
167
+ SAFE-direction tradeoff. The change ships dark with zero user-facing surface at merge, so
168
+ rollback is a plain revert. Clear to ship.
169
+
170
+ ---
171
+
172
+ ## Second-pass review (if required)
173
+
174
+ **Reviewer:** not required
175
+
176
+ The change carries a converged + approved Tier-2 spec
177
+ (`docs/specs/PROACTIVE-GROWTH-DIGEST-PUBLISHER-SLICE2-SPEC.md`) reviewed by an 8-reviewer
178
+ panel (security, scalability, adversarial, integration, lessons-aware + gemini) over 3
179
+ iterations; the convergence report is the independent-read record.
180
+
181
+ ---
182
+
183
+ ## Evidence pointers
184
+
185
+ - `src/monitoring/GrowthDigestPublisher.ts` — the publisher + pure `formatDigest` +
186
+ `createGrowthDigestAuditSink`.
187
+ - `src/server/routes.ts` — `evaluateOutbound` (pure funnel), `checkOutboundMessage`
188
+ (thin adapter), `postToUpdatesTopic` (publisher sender), `attachSender` hookup.
189
+ - `src/server/AgentServer.ts` — construction gate (`analyst && digestDelivery !== 'off'`),
190
+ lease gate (`isAwake`), `.stop()` teardown.
191
+ - `tests/unit/GrowthDigestPublisher.test.ts` — 21 tests (publishOnce matrix, lease gate,
192
+ in-flight guard, missed-run catch-up idempotency, sanity-floor, formatter guarantees).
193
+ - `tests/integration/growth-digest-publisher.test.ts` +
194
+ `tests/integration/growth-digest-publisher-wiring.test.ts` — guarded delivery + single-
195
+ funnel + sender-not-a-no-op + lease-gate wiring (10 tests).
196
+ - `tests/e2e/growth-digest-publisher-lifecycle.test.ts` — boots the real AgentServer:
197
+ LIVE lands one check-in in the Updates topic; OFF / no-analyst → publisher null (3 tests).
198
+ - `tests/integration/notification-flood-burst-invariant.test.ts` — 500-finding burst → one
199
+ bounded message, high-priority finding rendered in full.
200
+ - `npm run lint` clean (incl. dev-agent-dark-gate); analyst + ConfigDefaults regression
201
+ suites green (80 tests).