Finding
Validation run bednet-spot-check/20260606-2013 (seeded 3,4,6) ran on confirmed v0.13.557 (opp name carried #721's 20260606-2013 · Bednet Spot-Check front-prefix — proof the new connect-opp-setup code was active). Despite that, Phase 4 shipped an unclaimable-by-design opp and the #722 budget guard did not halt:
connect-opp-setup.md / run_state.yaml: payment unit Household Visit, amount_cents: 50, org_amount 50 cents, max_total: 10; opp total_budget: $50.
- Real capacity:
number_of_users = total_budget / Σ(max_total × (amount + org_amount)) = 50 / (10 × (50 + 50)) = 0.05 — well under 1.
phases.connect-setup.status: done (NOT error). No [BLOCKER]. Guard never tripped.
- Deliver smoke "passed" only because
total_budget(50) == minimum_budget_per_visit(50), so commcare-connect's per-visit claim guard passes at equality — a coincidence, not a healthy config.
Why #722 failed
#722 added the guard as SKILL.md prose ("compute min_budget_for_one_user from the created PUs, assert total_budget ≥ it, else HALT"). Two prose-level failure modes both fired:
- Cents still used. The new whole-units-only
amount guidance did NOT stop the agent passing 50 (cents) for a $0.50 PDD rate. A sub-$1 rate is unrepresentable in whole USD, so the agent fell back to cents — exactly what the guidance forbids, but prose doesn't enforce it.
- Guard computed in the wrong unit. Most likely the agent evaluated the guard in dollars (
min = 10 × (0.5 + 0.5) = $10 ≤ $50 → "passes") while storing cents (50). The cents/dollars ambiguity the guard was meant to eliminate re-entered inside the guard. A prose guard cannot guarantee unit-consistency.
(Confirmed unit: whole USD — the earlier stale run's mobile Download gate showed "Earn up to 100 USD for visit" for amount=100.)
Fix — enforce at the MCP boundary (code, not prose)
Per CLAUDE.md "class-level preventers > instance fixes" and "MCP capabilities are atomic / enforce at the boundary":
- Code-enforced capacity guard. In the connect MCP, compute
number_of_users from the integer request values actually sent (reliable — unlike the HTML-scraped connect_list_payment_units read-back where amount is undefined) and hard-reject when < 1. Best enforcement point: connect_create_payment_unit (knows the PU's amount/org_amount/max_total from the request) cross-checked against a fetched opportunity.total_budget; for multi-PU opps accumulate across PUs (or enforce at connect_activate_opportunity, the transition that invites/claims depend on). Reject with a typed error naming $X total_budget vs $Y = Σ(max_total×(amount+org_amount)).
- Kill the cents path at the boundary too. Amounts are whole USD integers; a sub-$1 PDD rate that rounds to 0 should be a typed rejection, not a silent cents fallback.
- Fix the bednet smoke PDD (
ACE/bednet-spot-check/inputs/) to a whole-dollar rate (e.g. $1/visit) so the smoke is representable — removes the sub-$1 trigger (instance fix, complements the class fix).
Note: MCP code changes need a full Claude Code restart to take effect (CLAUDE.md § MCP changes need a full Claude restart), and re-validation needs another seeded 3,4,6 run.
Evidence
Fix lands here: mcp/connect/ (capability map + backend for connect_create_payment_unit / connect_activate_opportunity), plus the bednet smoke PDD input.
Finding
Validation run
bednet-spot-check/20260606-2013(seeded 3,4,6) ran on confirmedv0.13.557(opp name carried #721's20260606-2013 · Bednet Spot-Checkfront-prefix — proof the new connect-opp-setup code was active). Despite that, Phase 4 shipped an unclaimable-by-design opp and the #722 budget guard did not halt:connect-opp-setup.md/run_state.yaml: payment unitHousehold Visit,amount_cents: 50,org_amount50 cents,max_total: 10; opptotal_budget: $50.number_of_users = total_budget / Σ(max_total × (amount + org_amount)) = 50 / (10 × (50 + 50)) = 0.05— well under 1.phases.connect-setup.status: done(NOTerror). No[BLOCKER]. Guard never tripped.total_budget(50) == minimum_budget_per_visit(50), so commcare-connect's per-visit claim guard passes at equality — a coincidence, not a healthy config.Why #722 failed
#722 added the guard as SKILL.md prose ("compute
min_budget_for_one_userfrom the created PUs, asserttotal_budget ≥it, else HALT"). Two prose-level failure modes both fired:amountguidance did NOT stop the agent passing50(cents) for a$0.50PDD rate. A sub-$1 rate is unrepresentable in whole USD, so the agent fell back to cents — exactly what the guidance forbids, but prose doesn't enforce it.min = 10 × (0.5 + 0.5) = $10 ≤ $50→ "passes") while storing cents (50). The cents/dollars ambiguity the guard was meant to eliminate re-entered inside the guard. A prose guard cannot guarantee unit-consistency.(Confirmed unit: whole USD — the earlier stale run's mobile Download gate showed "Earn up to 100 USD for visit" for
amount=100.)Fix — enforce at the MCP boundary (code, not prose)
Per CLAUDE.md "class-level preventers > instance fixes" and "MCP capabilities are atomic / enforce at the boundary":
number_of_usersfrom the integer request values actually sent (reliable — unlike the HTML-scrapedconnect_list_payment_unitsread-back whereamountis undefined) and hard-reject when< 1. Best enforcement point:connect_create_payment_unit(knows the PU'samount/org_amount/max_totalfrom the request) cross-checked against a fetchedopportunity.total_budget; for multi-PU opps accumulate across PUs (or enforce atconnect_activate_opportunity, the transition that invites/claims depend on). Reject with a typed error naming$X total_budgetvs$Y = Σ(max_total×(amount+org_amount)).ACE/bednet-spot-check/inputs/) to a whole-dollar rate (e.g. $1/visit) so the smoke is representable — removes the sub-$1 trigger (instance fix, complements the class fix).Note: MCP code changes need a full Claude Code restart to take effect (CLAUDE.md § MCP changes need a full Claude restart), and re-validation needs another seeded 3,4,6 run.
Evidence
bednet-spot-check/runs/20260606-2013—4-connect/connect-opp-setup.md,run_state.yaml(amount_cents: 50,connect-setup.status: done).skills/connect-opp-setup/SKILL.md).Fix lands here:
mcp/connect/(capability map + backend forconnect_create_payment_unit/connect_activate_opportunity), plus the bednet smoke PDD input.