Credit Committee Packets
You produce a single JSON object that conforms to the task's answer_template.json. The
underlying data lives behind a read-only HTTP API; the policy endpoint is the single
source of truth for every threshold and formula. Never hardcode thresholds from memory or
from a worked example — re-read them from the API each run, because branch limits and policy
bands vary per task.
Golden rules (apply to every task)
- Output is JSON only. No prose, no markdown fences, nothing outside the object. Emit
exactly the keys the template lists, in the template's structure. Extra keys are risk;
missing required keys fail.
- The API is the only data source. Use
curl/urllib against http://127.0.0.1:8003.
Ignore any mention of env/setup.sh, local DBs, or files — the service is already up.
Treat every number as authoritative; do not re-estimate it.
- Re-read
/api/policies every run and drive all bands/weights/formulas from it. Branch
objects carry their own cre_policy_limit_pct, sector_ceiling_pct, and
lending_capacity_q1; sector rows carry their own limit_pct. Use the per-entity value,
not a global default.
- Use only enum values the template allows. Reason codes, decisions, actions, postures,
risk classes, conditions, owners — all are closed vocabularies. Never invent a value.
- Respect rounding and ordering exactly (see "Precision & ordering" below). The grader
compares numbers and list order literally.
- Compute the helper script once.
scripts/credit_office.py wraps the API and the
policy math (risk re-derivation, CDFI scoring, stress, ratios). Read it and reuse it
instead of re-deriving formulas by hand.
How to approach any task in this family
- Read the prompt and the staged
input/payloads/answer_template.json. The template's
required_top_level_keys, item_required_keys, allowed_values/choices, ordering,
and precision/numeric_precision notes ARE the spec. Build your output shape from it.
- Identify the variant from the requested keys and verbs (see
references/playbooks.md):
- regrade / rating migration → portfolio_regrade + npa_benchmark + material_downgrades + top_problem_credit
- watch-list stress / workout → watch_list_summary + stress_results + workout_queue + severe_bucket_counts
- allocation / pending applications → allocation + decisions + concentration_flags + decline_reasons + post_approval_concentrations
- competing CRE decision → applications_compared + recommended_path + stress + concentration + conditions
- credit-union segment posture → posture + state_metrics + peer_comparison + controls + escalation_triggers + interpretation
- Pull
/api/policies and the entity data the prompt names (branch_id or segment_id).
Fetch exactly the surfaces the prompt lists; extra fetches are fine but do not invent data.
- Apply the playbook for that variant (
references/playbooks.md) using policy thresholds.
- Assemble JSON, apply precision/ordering, validate enums, print only the JSON.
Core business rules (driven by /api/policies, schema in references/policy.md)
These are stable across tasks; the exact numbers come from the live policy each run.
Risk-rating re-derivation (dominant-factor rule). For a loan, compute a candidate rating
from each available objective factor and take the worst (highest) numeric rating:
- DSCR →
risk_rating.dscr_thresholds (first band whose min is met, descending; if DSCR <
the lowest min, use the max_below band, i.e. rating 7).
- LTV →
risk_rating.ltv_thresholds (first band whose max is satisfied, ascending; if LTV
exceeds the top max, use the min_above band, rating 7).
- Delinquency →
risk_rating.delinquency_minimums[payment_status] is a floor (e.g.
Nonaccrual ≥ 8, 90+ ≥ 7, 60 ≥ 5, 30 ≥ 4; "Current" is null = no floor).
- Final rating = max of the available candidates. If no factor is available (no DSCR, no
LTV, payment_status Current → null floor), keep the loan's existing
current_rating.
- A downgrade is material when
final_rating − current_rating ≥ risk_rating.material_downgrade_notches.
CDFI factor scoring (additive, lower is better). Sum the four factor scores from
cdfi_factor_scores for FICO, LTV, debt_to_asset, liquidity_months. A factor that is missing
contributes 0 (it cannot be scored). Map the total to a class via the score bands
(Prime 0-5, Desirable 6-9, Satisfactory 10-13, Watch 14-18, Doubtful ≥19). Projected Loss
override: a credit already in the Watch band or worse whose LTV > 1.0 (underwater
collateral) is reported as Projected Loss rather than its score-band class.
CRE weighted score (lower is better). cre_weighted_score.weights (capacity 0.45,
collateral_exposure 0.36, conditions 0.11, character 0.05, capital 0.03) weight CDFI-style
factor scores. Class from cre_weighted_score.classes: approve_quality ≤ 2.0,
conditional ≤ 3.0, weak > 3.0. The class and the resulting decision are the gradeable
outputs; weight the per-C factor scores as described in references/playbooks.md.
Stress formulas (from stress):
- Watch-list +200bp parallel shock:
stressed_dscr = dscr / (1 + 0.18).
- CRE dual stress:
stressed_dscr = dscr * 0.85 / (1 + 0.18).
- Breach when
stressed_dscr < coverage_breach_threshold (1.0). Only loans/apps with a
DSCR appear in stress results; skip rows where DSCR is null.
Recommended-action / workout mapping keyed off the effective severity rating
(re-derived final rating, or current_rating where the variant specifies):
rating 8 / Nonaccrual / Projected Loss → partial_chargeoff_review; rating 7 →
special_assets; rating 6 → watchlist. Lower/healthier → monitor. Reserve
legal_referral/workout for cases the prompt explicitly calls out.
Capacity & concentration.
lending_capacity_q1 is the branch quarterly capacity. bank_capacity_used for an approved
app = approved amount, reduced by participation (bank retains a portion) and by SBA
guaranty (approved × (1 − sba_guaranty_pct)).
committed_capacity_amount = Σ bank_capacity_used; remaining_capacity =
lending_capacity_q1 − committed_capacity_amount; gross_approved_amount = Σ approved
amounts (full, before participation/SBA reduction) for approved + conditional apps.
- Concentration % = sector exposure ÷ post-approval total portfolio (Σ all sector
current_exposures + all approved additions), rounded to 4 decimals. Compare to the sector's
limit_pct (or branch sector_ceiling_pct). Existing over-ceiling exposure may be
grandfathered, but a new approval may not worsen a breached sector without a mitigation
(participation_required, reduced_amount, board_exception).
Benchmark variance. Branch ratio − benchmark ratio. variance_bps is computed from the
UNROUNDED ratios (branch_ratio − benchmark_ratio) × 10000 then rounded to 2 dp — do NOT
multiply the already-rounded 4-dp variance_ratio, which loses precision. Pick the benchmark
metric the template's enum specifies (e.g. total_loans_noncurrent_pct for NPA,
total_real_estate_30_89_pct for CRE real-estate delinquency).
Precision & ordering (grader compares literally)
- Currency (USD) fields → 2 decimals. Ratios/percentages-as-ratios → 4 decimals. bps → 2
decimals (from unrounded inputs, per above). CRE weighted score → 1 decimal. DSCR base/
stressed → 2 decimals. Counts/ratings → integers.
- Emit ratios as decimals (0.1897), never as "18.97%".
- Apply each list's stated
ordering exactly. Common ones: ascending loan_id /
application_id (string sort), ascending final_rating, reason codes ascending
alphabetically, sectors ascending, peer states ascending, workout queue descending
exposure then ascending loan_id, severe buckets ascending current_rating then ascending
payment_status (plain string sort, so "90+ Days Past Due" sorts before "Current").
- NCUA state-benchmark integers (delinquency_bps, loan_to_share_pct, roaa_bps,
positive_net_income_pct) are reported exactly as the table gives them — no recomputation.
Common misjudgments to avoid
- Don't drop the no-factor loans. A loan with no DSCR/LTV and a Current status stays at
its
current_rating and still counts in totals — it does not vanish or become rating 0.
- Population filters are inclusive and literal. "rated 3 or worse" =
current_rating ≥ 3;
"current_rating 6 or worse" = ≥ 6; "adversely rated" uses the rating the prompt names.
- Stress only covers DSCR-available rows. Missing-DSCR loans are excluded from
stress_results/results but may still appear in other sections.
- bps from unrounded ratios (see above) — the single most common numeric error.
- Concentration denominator is the portfolio post-approval total, not metrics
total_loans_outstanding and not the lending capacity.
- SBA guaranty and participation reduce capacity used, not the approved/gross amount.
- Enums only. A plausible-sounding label that isn't in the template's list is wrong.
- score_class/decision, not the raw weighted score, are usually graded — when a weighted
decomposition is ambiguous, get the class band and the decision right and keep the score
consistent with the band.
- NPA/CRE benchmark metric must match the template enum, not whatever metric seems closest.
References
references/policy.md — annotated structure of /api/policies and every band/weight, plus
the exact endpoint list and the fields that matter on each surface.
references/playbooks.md — step-by-step SOP for each of the five known variants, including
field-by-field derivation and the data surfaces each variant needs.
scripts/credit_office.py — runnable helper: API client + policy-driven functions for risk
re-derivation, CDFI scoring, stress, ratios, and rounding/ordering. Import or copy it.
1---2name: credit-committee-packets-23description: Produce committee-ready JSON answers for the shared Credit Office / lending-committee task family backed by the HTTP API at http://127.0.0.1:8003. Use this skill WHENEVER a prompt asks you to prepare a credit-risk, lending-committee, or credit-union-segment packet against that environment — including branch loan-rating migration / regrade reviews, watch-list stress and workout queues, pending-application capacity & concentration allocations, competing CRE underwriting decisions, and credit-union segment posture pages. Trigger on cues like "rating migration", "regrade", "watch-list stress", "+200bp DSCR stress", "lending-committee allocation", "concentration flags", "decline reason codes", "weighted CRE / CDFI score", "segment posture", "branch_id", "segment_id", "answer_template.json", or any request that says to follow input/payloads/answer_template.json and return only JSON. Always read the live /api/policies endpoint for authoritative thresholds rather than guessing numbers.4---56# Credit Committee Packets78You produce a single JSON object that conforms to the task's `answer_template.json`. The9underlying data lives behind a read-only HTTP API; the **policy endpoint is the single10source of truth** for every threshold and formula. Never hardcode thresholds from memory or11from a worked example — re-read them from the API each run, because branch limits and policy12bands vary per task.1314## Golden rules (apply to every task)15161. **Output is JSON only.** No prose, no markdown fences, nothing outside the object. Emit17 exactly the keys the template lists, in the template's structure. Extra keys are risk;18 missing required keys fail.192. **The API is the only data source.** Use `curl`/`urllib` against `http://127.0.0.1:8003`.20 Ignore any mention of `env/setup.sh`, local DBs, or files — the service is already up.21 Treat every number as authoritative; do not re-estimate it.223. **Re-read `/api/policies` every run** and drive all bands/weights/formulas from it. Branch23 objects carry their own `cre_policy_limit_pct`, `sector_ceiling_pct`, and24 `lending_capacity_q1`; sector rows carry their own `limit_pct`. Use the per-entity value,25 not a global default.264. **Use only enum values the template allows.** Reason codes, decisions, actions, postures,27 risk classes, conditions, owners — all are closed vocabularies. Never invent a value.285. **Respect rounding and ordering exactly** (see "Precision & ordering" below). The grader29 compares numbers and list order literally.306. **Compute the helper script once.** `scripts/credit_office.py` wraps the API and the31 policy math (risk re-derivation, CDFI scoring, stress, ratios). Read it and reuse it32 instead of re-deriving formulas by hand.3334## How to approach any task in this family35361. Read the prompt and the staged `input/payloads/answer_template.json`. The template's37 `required_top_level_keys`, `item_required_keys`, `allowed_values`/`choices`, `ordering`,38 and `precision`/`numeric_precision` notes ARE the spec. Build your output shape from it.392. Identify the variant from the requested keys and verbs (see `references/playbooks.md`):40 - **regrade / rating migration** → portfolio_regrade + npa_benchmark + material_downgrades + top_problem_credit41 - **watch-list stress / workout** → watch_list_summary + stress_results + workout_queue + severe_bucket_counts42 - **allocation / pending applications** → allocation + decisions + concentration_flags + decline_reasons + post_approval_concentrations43 - **competing CRE decision** → applications_compared + recommended_path + stress + concentration + conditions44 - **credit-union segment posture** → posture + state_metrics + peer_comparison + controls + escalation_triggers + interpretation453. Pull `/api/policies` and the entity data the prompt names (`branch_id` or `segment_id`).46 Fetch exactly the surfaces the prompt lists; extra fetches are fine but do not invent data.474. Apply the playbook for that variant (`references/playbooks.md`) using policy thresholds.485. Assemble JSON, apply precision/ordering, validate enums, print only the JSON.4950## Core business rules (driven by `/api/policies`, schema in `references/policy.md`)5152These are stable across tasks; the exact numbers come from the live policy each run.5354**Risk-rating re-derivation (dominant-factor rule).** For a loan, compute a candidate rating55from each *available* objective factor and take the **worst (highest) numeric rating**:56- DSCR → `risk_rating.dscr_thresholds` (first band whose `min` is met, descending; if DSCR <57 the lowest `min`, use the `max_below` band, i.e. rating 7).58- LTV → `risk_rating.ltv_thresholds` (first band whose `max` is satisfied, ascending; if LTV59 exceeds the top `max`, use the `min_above` band, rating 7).60- Delinquency → `risk_rating.delinquency_minimums[payment_status]` is a *floor* (e.g.61 Nonaccrual ≥ 8, 90+ ≥ 7, 60 ≥ 5, 30 ≥ 4; "Current" is null = no floor).62- Final rating = max of the available candidates. **If no factor is available (no DSCR, no63 LTV, payment_status Current → null floor), keep the loan's existing `current_rating`.**64- A downgrade is **material** when `final_rating − current_rating ≥ risk_rating.material_downgrade_notches`.6566**CDFI factor scoring (additive, lower is better).** Sum the four factor scores from67`cdfi_factor_scores` for FICO, LTV, debt_to_asset, liquidity_months. A factor that is missing68contributes 0 (it cannot be scored). Map the total to a class via the score bands69(Prime 0-5, Desirable 6-9, Satisfactory 10-13, Watch 14-18, Doubtful ≥19). **Projected Loss70override:** a credit already in the Watch band or worse whose **LTV > 1.0** (underwater71collateral) is reported as `Projected Loss` rather than its score-band class.7273**CRE weighted score (lower is better).** `cre_weighted_score.weights` (capacity 0.45,74collateral_exposure 0.36, conditions 0.11, character 0.05, capital 0.03) weight CDFI-style75factor scores. Class from `cre_weighted_score.classes`: `approve_quality` ≤ 2.0,76`conditional` ≤ 3.0, `weak` > 3.0. The class and the resulting decision are the gradeable77outputs; weight the per-C factor scores as described in `references/playbooks.md`.7879**Stress formulas** (from `stress`):80- Watch-list +200bp parallel shock: `stressed_dscr = dscr / (1 + 0.18)`.81- CRE dual stress: `stressed_dscr = dscr * 0.85 / (1 + 0.18)`.82- Breach when `stressed_dscr < coverage_breach_threshold` (1.0). Only loans/apps **with a83 DSCR** appear in stress results; skip rows where DSCR is null.8485**Recommended-action / workout mapping** keyed off the effective severity rating86(re-derived final rating, or current_rating where the variant specifies):87rating 8 / Nonaccrual / Projected Loss → `partial_chargeoff_review`; rating 7 →88`special_assets`; rating 6 → `watchlist`. Lower/healthier → `monitor`. Reserve89`legal_referral`/`workout` for cases the prompt explicitly calls out.9091**Capacity & concentration.**92- `lending_capacity_q1` is the branch quarterly capacity. `bank_capacity_used` for an approved93 app = approved amount, **reduced** by participation (bank retains a portion) and by SBA94 guaranty (`approved × (1 − sba_guaranty_pct)`).95- `committed_capacity_amount` = Σ `bank_capacity_used`; `remaining_capacity` =96 `lending_capacity_q1 − committed_capacity_amount`; `gross_approved_amount` = Σ approved97 amounts (full, before participation/SBA reduction) for approved + conditional apps.98- Concentration % = sector exposure ÷ **post-approval total portfolio** (Σ all sector99 current_exposures + all approved additions), rounded to 4 decimals. Compare to the sector's100 `limit_pct` (or branch `sector_ceiling_pct`). Existing over-ceiling exposure may be101 grandfathered, but a new approval may not worsen a breached sector without a mitigation102 (`participation_required`, `reduced_amount`, `board_exception`).103104**Benchmark variance.** Branch ratio − benchmark ratio. **`variance_bps` is computed from the105UNROUNDED ratios** `(branch_ratio − benchmark_ratio) × 10000` then rounded to 2 dp — do NOT106multiply the already-rounded 4-dp `variance_ratio`, which loses precision. Pick the benchmark107metric the template's enum specifies (e.g. `total_loans_noncurrent_pct` for NPA,108`total_real_estate_30_89_pct` for CRE real-estate delinquency).109110## Precision & ordering (grader compares literally)111112- Currency (USD) fields → 2 decimals. Ratios/percentages-as-ratios → 4 decimals. bps → 2113 decimals (from unrounded inputs, per above). CRE weighted score → 1 decimal. DSCR base/114 stressed → 2 decimals. Counts/ratings → integers.115- Emit ratios as decimals (0.1897), never as "18.97%".116- Apply each list's stated `ordering` exactly. Common ones: ascending `loan_id` /117 `application_id` (string sort), ascending `final_rating`, reason codes ascending118 alphabetically, sectors ascending, peer states ascending, workout queue **descending119 exposure then ascending loan_id**, severe buckets ascending current_rating then ascending120 payment_status (plain string sort, so "90+ Days Past Due" sorts before "Current").121- NCUA state-benchmark integers (delinquency_bps, loan_to_share_pct, roaa_bps,122 positive_net_income_pct) are reported **exactly as the table gives them** — no recomputation.123124## Common misjudgments to avoid125126- **Don't drop the no-factor loans.** A loan with no DSCR/LTV and a Current status stays at127 its `current_rating` and still counts in totals — it does not vanish or become rating 0.128- **Population filters are inclusive and literal.** "rated 3 or worse" = `current_rating ≥ 3`;129 "current_rating 6 or worse" = `≥ 6`; "adversely rated" uses the rating the prompt names.130- **Stress only covers DSCR-available rows.** Missing-DSCR loans are excluded from131 `stress_results`/`results` but may still appear in other sections.132- **bps from unrounded ratios** (see above) — the single most common numeric error.133- **Concentration denominator is the portfolio post-approval total, not metrics134 `total_loans_outstanding` and not the lending capacity.**135- **SBA guaranty and participation reduce capacity used, not the approved/gross amount.**136- **Enums only.** A plausible-sounding label that isn't in the template's list is wrong.137- **score_class/decision, not the raw weighted score, are usually graded** — when a weighted138 decomposition is ambiguous, get the class band and the decision right and keep the score139 consistent with the band.140- **NPA/CRE benchmark metric must match the template enum**, not whatever metric seems closest.141142## References143144- `references/policy.md` — annotated structure of `/api/policies` and every band/weight, plus145 the exact endpoint list and the fields that matter on each surface.146- `references/playbooks.md` — step-by-step SOP for each of the five known variants, including147 field-by-field derivation and the data surfaces each variant needs.148- `scripts/credit_office.py` — runnable helper: API client + policy-driven functions for risk149 re-derivation, CDFI scoring, stress, ratios, and rounding/ordering. Import or copy it.