AGENTS.md@apps/python/buddye · git:20260917.2209027 · 2026-09-17 · sha256 17da27e395677122

AGENTS.md@apps/python/buddye git:20260917.2209027A

Immutable. This exact content is served forever at /api/v1/blob/17da27e395677122.

# BuddyE agent notes

BuddyE calls frail people during a hazard and decides who needs help. Two rules below are the
product; the rest keep it from doing harm on the way there.

## The two that are not negotiable

- **Silence is a finding.** `NO_ANSWER`, `FAILED` and `INVALID_RESULT` go **into** `decide()` with
  the neighbour's triage attached, and come back `CheckOutcome.UNREACHABLE`. Never add an early
  return that skips an unanswered call, never retry silence away, and never let a neighbour reach
  the end of a sweep with no outcome and no row in `unaccounted()`. If you catch yourself treating
  a call that did not connect as an error to be handled, stop: that call is the output.
- **Never dial an emergency service.** The ladder is EMERGENCY_CONTACT (BuddyE may call) →
  BLOCK_CAPTAIN (BuddyE notifies) → RESPONDER (BuddyE *prepares* a `HandoffPacket` and stops).
  `escalate.release()` is the only function that assigns `released_at`, it requires a human's name,
  and `tests/test_escalate.py` pins both at the source level. Do not import a provider into
  `app/orchestrator/escalate.py`. Do not add a code path that transmits a packet without calling
  `require_released()`. `released_at = null` means nobody has been told anything, and that must stay
  true of every packet the system produces on its own.

## Calls cost real money and reach real people

- CALL-E places REAL phone calls and the free tier is small. Never run `CALL_PROVIDER=calle_sdk` or
  `calle_mcp` as a test or a check. The suite and every default use the mock provider.
- Never place a call as a verification step. `calle call plan` may consume quota; treat it as live.
- Consent is enforced twice on purpose — in `risk.call_order` and again on the row in the runner.
  Do not "simplify" one of them away.
- The dial allowlist and the call budget are enforced in code (`app/calls/budget.py`). Do not add a
  path around them, and do not count the budget from `CheckCall`: the `SpentCall` ledger survives a
  demo reset precisely so the reset button cannot refund the free tier.
- Phone numbers in committed files are fictional `+1 555-01xx` only. Real numbers come from
  `DEMO_PHONE_*` env at seed time and must also appear in `DIALABLE_NUMBERS`.

## Health data

- `conditions`, `power_dependent`, medications, `access_notes` and addresses are sensitive health
  information about named people. Redact at **egress** (`app.obs.redact`), never at the source: a
  triage reason with "oxygen concentrator" removed is useless to the captain it was written for.
- The **call task** is the exception in the other direction. It is persisted and published on the
  event stream, so it is built from derived facts (`power_dependent`, `mobility`, `lives_alone`) and
  must never interpolate a condition, a medication, or an address. First name only. On voicemail,
  not even the reason for the call.
- The handoff packet is deliberately unredacted beyond phones. Do not add a `redact()` to
  `build_handoff_packet` — a packet with the address masked helps nobody.

## The call contract

- The hazard decides which facts are `required` in the CALL-E `result_schema`. Do not relax the
  schema to make a call "succeed": a required field that came back `unknown` is a finding.
- `required` means "the call may not come home without this fact". It does not mean "this must be
  yes". `checks.has_power == "no"` during a blackout is the finding the ladder exists for.
- Every required field must be answerable when nobody picked up (tri-states have `unknown`, arrays
  may be empty, quotes may be `""`). A schema only a completed conversation could satisfy would make
  the most important outcome in the product look like a validation error.
- Keep `assert_calle_schema_subset()` and keep it passing. No nullable type arrays, one level of
  object nesting, arrays of plain strings, every string enum carries `unknown`.
- Field descriptions reach CALL-E's extraction model. Write them as extraction guidance, judged by
  meaning: "yeah I'm alright" and "I'm fine, just a bit warm" are different answers.

## Quotes and words

- `alarming_quote` and `concerns` are the person's own words and are copied verbatim into a handoff
  packet that a responder may hear read out. Never paraphrase into them, and never let the reconcile
  step introduce a sentence that cannot be traced to a human turn in the transcript.
- The reconciler may resolve an `unknown`; it may never overturn a definite answer CALL-E gave.

## Housekeeping

- Never log or print `CALLE_API_KEY`, `CALLE_WEBHOOK_SECRET`, OAuth tokens, or callback URLs.
- `app/models.py` and `app/domain/state.py` are the spine. Changing a state machine changes what the
  ladder is allowed to do; do it deliberately, with the transition table.
- Run `cd backend && .venv/bin/python -m pytest -q` before finishing, and
  `python3 scripts/validate_repository.py` from the repo root before any PR.