feat: wire pilot guardrails into agent runtime + close approval bypass#4
feat: wire pilot guardrails into agent runtime + close approval bypass#4mtmtian wants to merge 4 commits into
Conversation
WHY: docs/growth/00 §7.3 disciplines existed only as pure functions in
src/lib/growth/{pilot-gates,budget-guard}.ts — zero act-time enforcement.
A $5K pilot could overspend and SKAN-immature campaigns could be
auto-adjusted; executeApprovedDecision also skipped all guardrails.
WHAT: register pilot_budget_cap (fail-closed, activates via pilotStartDate
config), skan_maturity (fail-closed, <72h reject / learning-phase warn),
tier_cac_ceiling (fail-open, increase-only) in the evaluator registry +
schemas; re-evaluate guardrails at execution time inside
executeApprovedDecision (all five caller routes inherit), with rollback
exemption via existing DecisionStep.rollbackOf. Tests for boundaries,
fail-closed paths, and the bypass fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WHY: perceive() attached GrowthSnapshot to the snapshot but plan.v1.md had no growth placeholder — funnel/gate/budget signals were computed then dropped before reaching the LLM. WHAT: add GROWTH_JSON block to plan.v1.md (after the cache-control marker) with explicit rules — gate=kill means reduce/stop only, budget non-ok forbids any spend-increasing action; render it in plan.ts; regression test pins the placeholder position relative to the cache split. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WHY: wiring an MMP into ConversionEvent alongside GA4 double-counts installs (source is part of the idempotency key, cohort aggregation is source-blind), splits channels (network names all fall through to organic), and zeroes retention/LTV (adid vs pseudo_id namespaces). WHAT: docs/growth/06-mmp-ingest.md — the three holes, two canon decisions to make first, and an executable S2S-callback integration checklist; pointer comments on the legacy Report-only clients. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WHY: 04-status.md is the build-status handoff doc but stopped at the 2026-07-04 upstream snapshot (PR oratis#1–oratis#3); anyone reading it would miss the guardrail wiring, Adjust ingest, BI dashboard, and campaign-name canon now in review. WHAT: append the PR oratis#4–oratis#7 table with scopes and ops prerequisites; normalize the two pre-existing table separators to the padded style the linter enforces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🤖 Code review — Claude CodeVerdict: Request changes — the core approval-bypass fix is correctly implemented, but a High double-counting bug breaks the arithmetic of this PR's headline guardrail.
SummaryCloses a real approval-bypass: Findings
Core fix verified sound: traced every caller of Test coverageAdequate for the core logic —
Strengths
Independent automated review. Findings are advisory — use judgment. |
…success WHY (review oratis#4): sync writes both an account-level row and per-campaign rows for the same spend, so the unfiltered sum double-counted and tripped the $5K cap at ~half real spend. The execution-time guardrail re-check was also discarded on the success path, silently dropping warn-level signals from the audit trail. WHAT: pilot_budget_cap filters level:'account' (the one-per-platform- per-day contract row); success-path decisionStep.update persists guardrailReport. Regression tests for both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
✅ Changes applied (fixup pushed)Both review findings addressed:
Commit
|
Summary
Wires the four P21 pilot disciplines (docs/growth/00 §7.3) into the agent's actual runtime — they previously existed only as pure functions in
src/lib/growth/{pilot-gates,budget-guard}.tswith zero act-time enforcement — and closes the guardrail bypass inexecuteApprovedDecision.1. Three new guardrail evaluators (
guardrails.ts+ schemas)pilot_budget_cap(fail-closed) — inert until an org setspilotStartDatein the rule config, so existing orgs are unaffected. SumsReport.spendsince pilot start; ≥95% of cap blocks spend-increasing tools, ≥80% warns.skan_maturity(fail-closed) — meta/tiktokapp_installcampaigns: <72h hard-reject automated adjustments; learning phase (≤7d) warns only, unless daily spend >200% of budget. MissingstartDate/budget data rejects.tier_cac_ceiling(fail-open to avoid cold-start lockout) — blocks bid/budget increases when the channel's latestCohortSnapshot.cacexceedstierCacCeiling(firstMonthNet); decreases always pass.2. Approval-path bypass closed (
act.ts)executeApprovedDecisionnow re-runsevaluateGuardrailsagainst fresh state immediately before eachtool.execute— approvals can sit pending for hours, so the stored report can't be trusted. All five callers (approvals, bulk approvals, rollback, bulk rollback, Slack interactive) inherit from this single choke point. Rollbacks (detected via existingDecisionStep.rollbackOf) are exempt from the budget cap and CAC ceiling so a bad state can't get stranded; SKAN warnings surface but don't block.3. Growth snapshot now reaches the plan LLM
perceive()computed funnel/gate/budget signals butplan.v1.mdhad no growth placeholder — the data was dropped before prompting. Added aGROWTH_JSONblock (after the prompt-cache marker, with a regression test pinning that position) plus explicit rules:gate=kill→ reduce/stop only; budget non-ok → no spend-increasing actions.4. Docs: MMP ingestion prerequisites
docs/growth/06-mmp-ingest.md— why plugging Adjust/AppsFlyer intoConversionEventnext to GA4 double-counts installs (idempotency key includessource; cohort aggregation is source-blind), splits channels, and zeroes retention/LTV; plus an executable S2S-callback integration checklist. Pointer comments on the legacy Report-only clients.Test plan
npx vitest run— 275/275 across 27 files (new: evaluator boundaries at 95%/72h/7d/ceiling, fail-closed paths, decrease pass-through;act.test.tscovers block-at-execution, rollback exemptions, and a control case where rollback still blocks on unrelated rules)npx tsc --noEmitclean;npm run lint0 errors (35 pre-existing warnings)🤖 Generated with Claude Code