Private Wealth Advisory Planning Skill
You are given, per task: input/prompt.txt, input/payloads/request_memo.md, and
input/payloads/answer_template.json. The prompt names a client (e.g. CLT-1003)
and an engagement type. The memo gives engagement context and sometimes an explicit
planning horizon year. The answer_template lists the required top-level keys,
the exact field names, the enum value sets, and rounding/date conventions.
Your job: fetch the client's data from the remote API, run the correct analysis,
and return ONE JSON object (no prose) matching the template.
0. Output discipline (always)
- Return a single JSON object only, no prose, no markdown fences.
- Include
task_id (e.g. train_003 / test_003 — derive from the task folder
name; use the same numeric suffix the harness used) and client_id (e.g.
CLT-1003, exactly as given).
- Set
analysis_type to the single enum the template fixes for that task
(roth_conversion_rmd, ilit_crummey_implementation, trust_comparison,
estate_liquidity_action_plan).
- All USD amounts rounded to 2 decimals (cents). Emit JSON numbers, not strings.
- All dates ISO
YYYY-MM-DD strings.
- Build the object from the template's
required_top_level_keys and fields
list — produce every key it names, nested exactly as the dotted paths imply
(e.g. recommendation.primary_action → {"recommendation":{"primary_action":...}}).
- Only use enum string values that appear in the template for that field. Never
invent new enum strings.
- Where a field is
list ... sorted alphabetically, sort the final list with a
plain ascending string sort.
1. API usage (read-only, curl via Bash)
Base URL comes from the environment (the harness exposes it; the file
environment_access.md gives it explicitly, e.g. <remote-env-url>).
All endpoints are GET and return JSON. Use curl -s --max-time 30.
| Endpoint |
Use |
GET /api/health |
liveness check (optional first call) |
GET /api/clients/<client_id> |
base client record (age, filing_status, planning_year, estate_value, liquid_assets, marital_status) |
GET /api/source-documents?client_id=<id> |
list of conflicting documents; each has source_type, effective_date, facts{} |
GET /api/retirement-accounts?client_id=<id> |
IRA export: traditional_balance, roth_balance, expected_return, rmd_start_age, recommended_conversion_years; source_type = CUSTODIAN_EXPORT |
GET /api/life-insurance?client_id=<id> |
policy: death_benefit, annual_premium, planned_contribution_date, is_existing_policy_transfer, proposed_owner |
GET /api/trust-candidates?client_id=<id> |
asset_value, expected_growth_rate, grat_term_years, grat_annuity_rate, crat_term_years, crat_payout_rate |
GET /api/policies/tax |
constants (see below) |
GET /api/rmd-factors |
map age -> divisor (age 73..99) |
Always fetch: the client record, the source-documents, the tax policy, and
the endpoint(s) relevant to the analysis. For any task touching estate tax you
also need /api/policies/tax. For RMD you need /api/rmd-factors.
Run helper math with python (NOT python3).
Tax policy constants (shape observed)
annual_gift_exclusion: {"2025":19000, "2026":20000} # per beneficiary, by year
estate_tax_exemption: {"2025":13990000, "2026":13610000} # by year
estate_tax_rate: 0.40
conversion_bracket_targets: {"MFJ":394600, "SINGLE":197300, "HOH":263500} # top of target bracket
max_crat_term_years: 20
charitable_deduction_rate: 0.35
Pick exemption / gift-exclusion by the client's planning_year (usually 2026).
Pick the bracket target by the client's filing_status.
2. Source-document conflict resolution (applies to EVERY task)
Each client has three source documents that disagree. Resolve by source-type
priority, which also matches recency:
Priority (highest → lowest):
SIGNED_PROFILE (newest, e.g. 2026-02-06) > ATTORNEY_MEMO (e.g. 2026-01-18)
> CRM_NOTE (oldest, e.g. 2025-11-20, this is the "stale" import).
Rules:
- For profile / goal / beneficiary / income fields (
beneficiary_count,
marginal_tax_rate, annual_non_ira_income, family_transfer_priority,
philanthropic_intent, filing_status, liquid_assets, estate_value,
age, planning_year): use SIGNED_PROFILE. The base client record
matches the SIGNED_PROFILE for overlapping fields, so they corroborate.
→ source_resolution.controlling_profile_source = SIGNED_PROFILE
→ source_resolution.controlling_goal_source = SIGNED_PROFILE
→ source_resolution.controlling_beneficiary_source = SIGNED_PROFILE
- For retirement-account numbers (balances, return, rmd_start_age,
recommended_conversion_years): the only/authoritative source is the custodian
export. →
source_resolution.controlling_account_source = CUSTODIAN_EXPORT.
- For life-insurance / policy numbers (death_benefit, premium, dates):
authoritative source is the policy/custodian record.
→
source_resolution.controlling_policy_source = CUSTODIAN_EXPORT.
- For asset/estate figures in trust comparison (
controlling_asset_source):
the ATTORNEY_MEMO is the controlling authority for estate/asset values in a
trust-planning context (it carries estate_value and is the legal-planning
document). → source_resolution.controlling_asset_source = ATTORNEY_MEMO.
General tiebreak if you ever see a new field: prefer the document whose
source_type is highest priority above; among same type, prefer the latest
effective_date. The enum value sets for each controlling_* field are fixed by
the template — pick from that list only. STALE_MARKETING_INTAKE corresponds to
a stale CRM-type import and is essentially never the controlling source.
3. Analysis: roth_conversion_rmd (e.g. Mercer, Patel)
Inputs: SIGNED_PROFILE (annual_non_ira_income, marginal_tax_rate,
filing_status), retirement account (traditional_balance, roth_balance,
expected_return = ret, rmd_start_age, recommended_conversion_years), RMD
factors, tax policy (conversion_bracket_targets), and the horizon year from
the memo.
Derived quantities:
first_conversion_year = planning_year (conversions start now).
first_rmd_year = planning_year + (rmd_start_age - age).
conversion_window = max(0, first_rmd_year - first_conversion_year) (years
before RMDs begin).
bracket_headroom = conversion_bracket_targets[filing_status] - annual_non_ira_income.
annual_conversion_amount = min(bracket_headroom, traditional_balance / recommended_conversion_years)
— stage conversions to stay within the target bracket.
conversion_years = recommended_conversion_years (the planned count).
conversion_years_positive = min(recommended_conversion_years, conversion_window)
— the years actually executed before RMDs start. (For a near-RMD client this is
small or 0; for a client years away from RMD it equals the planned count.)
horizon_year = the memo's horizon; first_rmd_year as above.
Two-scenario projection from planning_year through horizon_year inclusive
(simulate year by year; keep the order consistent in both scenarios):
For each year, in this order:
- Conversion (conversion scenario only), if the year is within the
conversion_years_positive window starting at first_conversion_year:
move amt = min(annual_conversion_amount, traditional_balance) from
traditional to Roth; accumulate total_converted += amt and
total_conversion_tax += amt * marginal_tax_rate (tax paid from outside funds).
- RMD, if
age >= rmd_start_age: rmd = traditional_balance / rmd_factor[age];
accumulate rmd_tax += rmd * marginal_tax_rate; subtract rmd from traditional.
- Growth: multiply both traditional and Roth balances by
(1 + ret).
Run the loop once with conversions off (baseline) and once on (conversion):
rmd_projection.baseline_rmd_tax_through_horizon = baseline rmd_tax total.
rmd_projection.conversion_rmd_tax_through_horizon = conversion rmd_tax total.
rmd_projection.rmd_tax_savings_through_horizon = baseline − conversion.
conversion_plan.total_converted, conversion_plan.total_conversion_tax from
the conversion run.
legacy_projection.projected_roth_balance_horizon = ending Roth (conversion run).
legacy_projection.projected_traditional_balance_horizon = ending traditional
(conversion run).
(Modeling note: the exact intra-year ordering and whether growth precedes RMD is a
judgement call; pick one convention and apply it identically to both scenarios so
the savings difference is consistent. The simulation above — convert, then RMD,
then grow — is internally consistent and recommended.)
Enums:
recommendation.primary_action: STAGED_ROTH_CONVERSION when there is a usable
conversion window (conversion_years_positive >= 1) with positive savings;
DEFER when the window is effectively gone but a later look is warranted;
NO_CONVERSION when conversion adds no value.
recommendation.suitability: SUITABLE when the window is healthy (multiple
positive years and meaningful savings); BORDERLINE when only 1 positive year /
thin savings; DEFER when no window.
recommendation.risk_flag: RMD_NEAR_TERM when conversion_window <= 1 /
client at or just under rmd_start_age; LIQUIDITY_CONSTRAINT when
total_conversion_tax is large relative to liquid_assets;
TAX_BRACKET_MANAGEMENT otherwise (ample headroom, staging is the main lever).
legacy_projection.heir_tax_profile from Roth share of horizon balances
f = roth/(roth+traditional): MOSTLY_TAX_FREE if f >= ~0.66,
MIXED_TAXABLE_AND_TAX_FREE if ~0.34 <= f < ~0.66, MOSTLY_TAXABLE if
f < ~0.34.
4. Analysis: ilit_crummey_implementation (e.g. Keating)
Inputs: SIGNED_PROFILE beneficiary_count; life-insurance policy
(annual_premium, death_benefit, planned_contribution_date,
is_existing_policy_transfer); tax policy (annual_gift_exclusion[planning_year],
exemption, rate).
annual_exclusion_per_beneficiary = annual_gift_exclusion[planning_year] (e.g.
$20,000 in 2026).
beneficiary_count from SIGNED_PROFILE.
annual_exclusion_capacity = exclusion_per_beneficiary * beneficiary_count.
annual_premium from the policy.
premium_gap = max(0, annual_premium - annual_exclusion_capacity) — the
shortfall not covered by annual-exclusion gifts. (If capacity ≥ premium, gap = 0
and there is no shortfall.)
administration.notices_required = beneficiary_count (one Crummey notice each).
administration.dedicated_bank_account_required = true (an ILIT should pay
premiums from its own account after the withdrawal window).
Dates (ISO):
administration.contribution_date = planned_contribution_date.
administration.notice_due_date = the contribution date (notices go out
promptly upon contribution; same day is the defensible due date).
administration.withdrawal_window_end = contribution_date + 30 days (standard
Crummey withdrawal window).
administration.earliest_premium_payment_date = withdrawal_window_end (pay the
premium only after the withdrawal window closes).
Estate result:
estate_result.death_benefit = death_benefit.
estate_result.projected_outside_estate_if_implemented = death_benefit for a
new policy (is_existing_policy_transfer == false); for a transfer of an
existing policy still inside the 3-year lookback, treat the proceeds as not yet
reliably outside the estate (→ 0 or flag the lookback).
estate_result.tax_liquidity_support = min(death_benefit, estate_tax_exposure)
where estate_tax_exposure = max(0, estate_value - exemption[planning_year]) * estate_tax_rate.
Risk flag (recommendation.risk_flag and estate_result.estate_inclusion_risk,
same enum):
- new policy + no shortfall →
LOW_IF_FORMALITIES_MET
- new policy +
premium_gap > 0 → EXCLUSION_SHORTFALL
- existing-policy transfer + no shortfall →
THREE_YEAR_LOOKBACK
- existing-policy transfer +
premium_gap > 0 → THREE_YEAR_LOOKBACK_AND_EXCLUSION_SHORTFALL
Recommendation primary_action:
- no shortfall, new policy →
FUND_WITH_CRUMMEY_NOTICES
- shortfall, new policy →
USE_LIFETIME_EXEMPTION_FOR_SHORTFALL
- existing-policy transfer (lookback) →
USE_NEW_POLICY_OR_ACCEPT_LOOKBACK
(or DISCLOSE_LOOKBACK_AND_USE_EXEMPTION when there is ALSO a shortfall).
recommendation.suitability: SUITABLE_WITH_ADMINISTRATION when formalities (notices,
dedicated account, window) cleanly cover the premium; BORDERLINE with a modest
shortfall; NOT_SUITABLE only in severe lookback+shortfall cases.
5. Analysis: trust_comparison — GRAT vs CRAT (e.g. Alvarez)
Inputs: trust-candidate params; tax policy (estate_tax_rate,
charitable_deduction_rate, max_crat_term_years); SIGNED_PROFILE goal fields
(family_transfer_priority, philanthropic_intent); exemption + estate_value.
Estate context:
estate_context.taxable_estate = max(0, estate_value - exemption[planning_year]).
estate_context.estate_tax_exposure = taxable_estate * estate_tax_rate.
estate_context.liquidity_gap_before_planning = max(0, estate_tax_exposure - liquid_assets).
GRAT (term = grat_term_years, annuity rate = grat_annuity_rate, growth =
expected_growth_rate):
annuity = asset_value * grat_annuity_rate (level annuity each year).
fv_asset = asset_value * (1 + growth) ** term.
fv_annuity = sum over t=1..term of annuity * (1 + growth) ** (term - t)
(annuity payments reinvested/grown at the growth rate to end of term).
grat.projected_remainder_to_heirs = fv_asset - fv_annuity.
grat.estimated_estate_tax_reduction = projected_remainder_to_heirs * estate_tax_rate
(value moved out of the taxable estate × rate).
grat.mortality_inclusion_risk = TERM_SURVIVAL_REQUIRED (fixed enum: grantor
must outlive the term).
grat.term_years = grat_term_years.
CRAT (term = crat_term_years, capped at max_crat_term_years=20; payout =
crat_payout_rate):
crat.term_years = min(crat_term_years, max_crat_term_years).
payout = asset_value * crat_payout_rate.
fv_asset = asset_value * (1 + growth) ** term.
fv_payout = sum over t=1..term of payout * (1 + growth) ** (term - t).
crat.projected_charitable_remainder = fv_asset - fv_payout.
crat.estimated_income_tax_deduction = asset_value * charitable_deduction_rate
(charitable deduction approximated as the funded value × deduction rate).
crat.family_transfer_fit: LOW (a CRAT sends the remainder to charity, not
family) — use LOW unless the goal data clearly elevates family use of the CRAT.
Recommendation (goal-driven; goal source = SIGNED_PROFILE):
family_transfer_priority == high and philanthropic_intent != high
→ preferred_strategy = GRAT, rationale_code = CHILDREN_TRANSFER_PRIORITY,
alternate_role = SECONDARY_CHARITABLE_TOOL.
philanthropic_intent == high (and family not higher)
→ preferred_strategy = CRAT, rationale_code = PHILANTHROPIC_PRIORITY,
alternate_role = SECONDARY_FAMILY_TRANSFER_TOOL.
- Otherwise default to the family-transfer (GRAT) branch, since these clients are
large taxable estates whose primary motive is moving appreciation to heirs.
6. Analysis: estate_liquidity_action_plan — integrated (e.g. Chen)
Combines the ILIT and trust-transfer analyses into a coordinated plan. Reuse §4
and §5 computations.
Estate context (same formulas as §5):
taxable_estate = max(0, estate_value - exemption[planning_year]).
estate_tax_exposure = taxable_estate * estate_tax_rate.
liquidity_gap_before_planning = max(0, estate_tax_exposure - liquid_assets).
ilit block:
annual_exclusion_capacity, premium_gap as in §4.
estate_inclusion_risk as in §4 (new policy + no shortfall →
LOW_IF_FORMALITIES_MET, etc.).
projected_outside_estate_if_implemented = death_benefit for a new policy.
trust_transfer block (pick GRAT vs CRAT by goal as in §5):
preferred_strategy, projected_remainder_to_heirs (GRAT remainder),
estimated_estate_tax_reduction (GRAT reduction),
projected_charitable_remainder (CRAT remainder). Provide all of these so the
attorney sees both branches.
recommendation:
primary_action: COMBINE_ILIT_AND_GRAT when family-transfer priority is high
and the estate needs both liquidity (ILIT) and transfer (GRAT);
CRAT_WITH_LIQUIDITY_REVIEW when philanthropic priority dominates;
ILIT_WITH_EXEMPTION_REVIEW when the ILIT premium creates an exclusion shortfall
needing lifetime-exemption allocation.
sequencing: ILIT_FIRST_THEN_GRAT for the combine case;
TRUST_DECISION_FIRST when the trust choice (GRAT vs CRAT) is unresolved;
ILIT_FIRST_THEN_ATTORNEY_REVIEW when documents must be drafted before the trust.
risk_flag: the ILIT estate-inclusion risk flag (§4 enum).
action_set — a list of enums sorted alphabetically ascending, drawn only
from: ATTORNEY_DRAFT_REVIEW, CRAT_FOR_CHARITABLE_REMAINDER,
GRAT_FOR_APPRECIATING_SHARES, ILIT_CRUMMEY_NOTICE_CYCLE,
LIFETIME_EXEMPTION_ALLOCATION. Build it by including:
ILIT_CRUMMEY_NOTICE_CYCLE whenever an ILIT/policy is in play.
GRAT_FOR_APPRECIATING_SHARES when preferred trust = GRAT (family priority),
else CRAT_FOR_CHARITABLE_REMAINDER when preferred = CRAT (philanthropic).
LIFETIME_EXEMPTION_ALLOCATION when premium_gap > 0 (shortfall) or the estate
needs exemption allocation.
ATTORNEY_DRAFT_REVIEW essentially always (these need legal drafting).
Then sorted(set(actions)).
7. Common pitfalls / checklist
- Do not read the client record's possibly-stale fields directly when a
SIGNED_PROFILE fact disagrees — SIGNED_PROFILE wins. (In practice the client
record already matches SIGNED_PROFILE, but always cross-check the documents.)
- Beneficiary count drives Crummey notices and exclusion capacity — take it
from SIGNED_PROFILE (CRM count is usually different and stale).
- Bracket target is per filing_status; use SINGLE vs MFJ vs HOH correctly.
- Exemption / gift exclusion are per planning_year — read the right year key.
- CRAT term is capped at
max_crat_term_years (20).
- premium_gap / liquidity_gap are non-negative (
max(0, …)): a "gap"/"shortfall"
of zero means fully covered; never report a negative gap.
- estate_tax_exposure uses
estate_value - exemption, then × 0.40 — not the
full estate value.
- Roth two-scenario consistency: apply the identical intra-year ordering in
baseline and conversion runs; savings = baseline − conversion must be ≥ 0.
- conversion_years_positive ≤ conversion_window: a near-RMD client gets few or
zero positive conversion years; reflect that in suitability/risk enums.
- Rounding: round every USD figure to 2 decimals at output time (not midway,
to avoid compounding rounding error).
- Dates: emit ISO
YYYY-MM-DD; compute the 30-day Crummey window by real date
arithmetic (use python datetime.date + timedelta(days=30)).
- Enums: copy enum spellings verbatim from the template; any typo fails.
- task_id: match the harness's numbering (
train_00X for train, test_00X
for test) using the numeric suffix of the task.
8. Quick procedure
- Read prompt, memo (note horizon year if any), and answer_template (note required
keys, enums,
analysis_type).
curl health, /clients/<id>, /source-documents?client_id=<id>,
/policies/tax, plus the analysis-specific endpoint(s) and /rmd-factors if
Roth.
- Resolve conflicts via §2 (SIGNED_PROFILE > ATTORNEY_MEMO > CRM_NOTE; accounts =
CUSTODIAN_EXPORT; assets in trust comparison = ATTORNEY_MEMO).
- Run the matching analysis (§3–§6) in a
python helper; keep formulas
internally consistent and self-check sign/order.
- Assemble the JSON per the template, set
source_resolution.* enums, round USD,
ISO dates, sort any list fields.
- Output the single JSON object only.
1---2name: private-wealth-advisory-planning3description: Solve private wealth advisory structured-output tasks (Roth conversion / RMD tax, ILIT Crummey funding, GRAT vs CRAT comparison, and integrated estate-liquidity action plans) by querying a read-only advisory HTTP API, resolving conflicting source documents by priority, applying the derived business formulas, and emitting a single JSON object that conforms to the task's answer_template.json. Use this whenever a task asks for a structured planning output for a client CLT-#### in the private-wealth domain.4---56# Private Wealth Advisory Planning Skill78You are given, per task: `input/prompt.txt`, `input/payloads/request_memo.md`, and9`input/payloads/answer_template.json`. The prompt names a client (e.g. `CLT-1003`)10and an engagement type. The memo gives engagement context and sometimes an explicit11**planning horizon year**. The answer_template lists the required top-level keys,12the exact field names, the enum value sets, and rounding/date conventions.1314Your job: fetch the client's data from the remote API, run the correct analysis,15and return ONE JSON object (no prose) matching the template.1617---1819## 0. Output discipline (always)2021- Return a **single JSON object only**, no prose, no markdown fences.22- Include `task_id` (e.g. `train_003` / `test_003` — derive from the task folder23 name; use the same numeric suffix the harness used) and `client_id` (e.g.24 `CLT-1003`, exactly as given).25- Set `analysis_type` to the single enum the template fixes for that task26 (`roth_conversion_rmd`, `ilit_crummey_implementation`, `trust_comparison`,27 `estate_liquidity_action_plan`).28- **All USD amounts rounded to 2 decimals (cents).** Emit JSON numbers, not strings.29- **All dates ISO `YYYY-MM-DD` strings.**30- Build the object from the template's `required_top_level_keys` and `fields`31 list — produce every key it names, nested exactly as the dotted paths imply32 (e.g. `recommendation.primary_action` → `{"recommendation":{"primary_action":...}}`).33- Only use enum string values that appear in the template for that field. Never34 invent new enum strings.35- Where a field is `list ... sorted alphabetically`, sort the final list with a36 plain ascending string sort.3738---3940## 1. API usage (read-only, curl via Bash)4142Base URL comes from the environment (the harness exposes it; the file43`environment_access.md` gives it explicitly, e.g. `<remote-env-url>`).44All endpoints are GET and return JSON. Use `curl -s --max-time 30`.4546| Endpoint | Use |47|---|---|48| `GET /api/health` | liveness check (optional first call) |49| `GET /api/clients/<client_id>` | base client record (age, filing_status, planning_year, estate_value, liquid_assets, marital_status) |50| `GET /api/source-documents?client_id=<id>` | list of conflicting documents; each has `source_type`, `effective_date`, `facts{}` |51| `GET /api/retirement-accounts?client_id=<id>` | IRA export: `traditional_balance`, `roth_balance`, `expected_return`, `rmd_start_age`, `recommended_conversion_years`; `source_type` = `CUSTODIAN_EXPORT` |52| `GET /api/life-insurance?client_id=<id>` | policy: `death_benefit`, `annual_premium`, `planned_contribution_date`, `is_existing_policy_transfer`, `proposed_owner` |53| `GET /api/trust-candidates?client_id=<id>` | `asset_value`, `expected_growth_rate`, `grat_term_years`, `grat_annuity_rate`, `crat_term_years`, `crat_payout_rate` |54| `GET /api/policies/tax` | constants (see below) |55| `GET /api/rmd-factors` | map `age -> divisor` (age 73..99) |5657**Always fetch:** the client record, the source-documents, the tax policy, and58the endpoint(s) relevant to the analysis. For any task touching estate tax you59also need `/api/policies/tax`. For RMD you need `/api/rmd-factors`.6061Run helper math with `python` (NOT `python3`).6263### Tax policy constants (shape observed)6465```66annual_gift_exclusion: {"2025":19000, "2026":20000} # per beneficiary, by year67estate_tax_exemption: {"2025":13990000, "2026":13610000} # by year68estate_tax_rate: 0.4069conversion_bracket_targets: {"MFJ":394600, "SINGLE":197300, "HOH":263500} # top of target bracket70max_crat_term_years: 2071charitable_deduction_rate: 0.3572```7374Pick exemption / gift-exclusion **by the client's `planning_year`** (usually 2026).75Pick the bracket target by the client's `filing_status`.7677---7879## 2. Source-document conflict resolution (applies to EVERY task)8081Each client has three source documents that **disagree**. Resolve by source-type82priority, which also matches recency:8384**Priority (highest → lowest):**85`SIGNED_PROFILE` (newest, e.g. 2026-02-06) **>** `ATTORNEY_MEMO` (e.g. 2026-01-18)86**>** `CRM_NOTE` (oldest, e.g. 2025-11-20, this is the "stale" import).8788Rules:89- For **profile / goal / beneficiary / income** fields (`beneficiary_count`,90 `marginal_tax_rate`, `annual_non_ira_income`, `family_transfer_priority`,91 `philanthropic_intent`, `filing_status`, `liquid_assets`, `estate_value`,92 `age`, `planning_year`): **use `SIGNED_PROFILE`.** The base client record93 matches the SIGNED_PROFILE for overlapping fields, so they corroborate.94 → `source_resolution.controlling_profile_source = SIGNED_PROFILE`95 → `source_resolution.controlling_goal_source = SIGNED_PROFILE`96 → `source_resolution.controlling_beneficiary_source = SIGNED_PROFILE`97- For **retirement-account numbers** (balances, return, rmd_start_age,98 recommended_conversion_years): the only/authoritative source is the custodian99 export. → `source_resolution.controlling_account_source = CUSTODIAN_EXPORT`.100- For **life-insurance / policy numbers** (death_benefit, premium, dates):101 authoritative source is the policy/custodian record.102 → `source_resolution.controlling_policy_source = CUSTODIAN_EXPORT`.103- For **asset/estate figures in trust comparison** (`controlling_asset_source`):104 the `ATTORNEY_MEMO` is the controlling authority for estate/asset values in a105 trust-planning context (it carries `estate_value` and is the legal-planning106 document). → `source_resolution.controlling_asset_source = ATTORNEY_MEMO`.107108General tiebreak if you ever see a new field: prefer the document whose109`source_type` is highest priority above; among same type, prefer the latest110`effective_date`. The enum value sets for each `controlling_*` field are fixed by111the template — pick from that list only. `STALE_MARKETING_INTAKE` corresponds to112a stale CRM-type import and is essentially never the controlling source.113114---115116## 3. Analysis: `roth_conversion_rmd` (e.g. Mercer, Patel)117118Inputs: SIGNED_PROFILE (`annual_non_ira_income`, `marginal_tax_rate`,119`filing_status`), retirement account (`traditional_balance`, `roth_balance`,120`expected_return` = ret, `rmd_start_age`, `recommended_conversion_years`), RMD121factors, tax policy (`conversion_bracket_targets`), and the **horizon year** from122the memo.123124Derived quantities:125126- `first_conversion_year = planning_year` (conversions start now).127- `first_rmd_year = planning_year + (rmd_start_age - age)`.128- `conversion_window = max(0, first_rmd_year - first_conversion_year)` (years129 before RMDs begin).130- `bracket_headroom = conversion_bracket_targets[filing_status] - annual_non_ira_income`.131- `annual_conversion_amount = min(bracket_headroom, traditional_balance / recommended_conversion_years)`132 — stage conversions to stay within the target bracket.133- `conversion_years` = `recommended_conversion_years` (the planned count).134- `conversion_years_positive = min(recommended_conversion_years, conversion_window)`135 — the years actually executed before RMDs start. (For a near-RMD client this is136 small or 0; for a client years away from RMD it equals the planned count.)137- `horizon_year` = the memo's horizon; `first_rmd_year` as above.138139Two-scenario projection from `planning_year` through `horizon_year` inclusive140(simulate year by year; keep the order consistent in both scenarios):141142For each year, in this order:1431. **Conversion** (conversion scenario only), if the year is within the144 `conversion_years_positive` window starting at `first_conversion_year`:145 move `amt = min(annual_conversion_amount, traditional_balance)` from146 traditional to Roth; accumulate `total_converted += amt` and147 `total_conversion_tax += amt * marginal_tax_rate` (tax paid from outside funds).1482. **RMD**, if `age >= rmd_start_age`: `rmd = traditional_balance / rmd_factor[age]`;149 accumulate `rmd_tax += rmd * marginal_tax_rate`; subtract `rmd` from traditional.1503. **Growth**: multiply both traditional and Roth balances by `(1 + ret)`.151152Run the loop once with conversions off (baseline) and once on (conversion):153- `rmd_projection.baseline_rmd_tax_through_horizon` = baseline rmd_tax total.154- `rmd_projection.conversion_rmd_tax_through_horizon` = conversion rmd_tax total.155- `rmd_projection.rmd_tax_savings_through_horizon` = baseline − conversion.156- `conversion_plan.total_converted`, `conversion_plan.total_conversion_tax` from157 the conversion run.158- `legacy_projection.projected_roth_balance_horizon` = ending Roth (conversion run).159- `legacy_projection.projected_traditional_balance_horizon` = ending traditional160 (conversion run).161162(Modeling note: the exact intra-year ordering and whether growth precedes RMD is a163judgement call; pick one convention and apply it identically to both scenarios so164the *savings difference* is consistent. The simulation above — convert, then RMD,165then grow — is internally consistent and recommended.)166167Enums:168- `recommendation.primary_action`: `STAGED_ROTH_CONVERSION` when there is a usable169 conversion window (`conversion_years_positive >= 1`) with positive savings;170 `DEFER` when the window is effectively gone but a later look is warranted;171 `NO_CONVERSION` when conversion adds no value.172- `recommendation.suitability`: `SUITABLE` when the window is healthy (multiple173 positive years and meaningful savings); `BORDERLINE` when only 1 positive year /174 thin savings; `DEFER` when no window.175- `recommendation.risk_flag`: `RMD_NEAR_TERM` when `conversion_window <= 1` /176 client at or just under `rmd_start_age`; `LIQUIDITY_CONSTRAINT` when177 `total_conversion_tax` is large relative to `liquid_assets`;178 `TAX_BRACKET_MANAGEMENT` otherwise (ample headroom, staging is the main lever).179- `legacy_projection.heir_tax_profile` from Roth share of horizon balances180 `f = roth/(roth+traditional)`: `MOSTLY_TAX_FREE` if `f >= ~0.66`,181 `MIXED_TAXABLE_AND_TAX_FREE` if `~0.34 <= f < ~0.66`, `MOSTLY_TAXABLE` if182 `f < ~0.34`.183184---185186## 4. Analysis: `ilit_crummey_implementation` (e.g. Keating)187188Inputs: SIGNED_PROFILE `beneficiary_count`; life-insurance policy189(`annual_premium`, `death_benefit`, `planned_contribution_date`,190`is_existing_policy_transfer`); tax policy (`annual_gift_exclusion[planning_year]`,191exemption, rate).192193- `annual_exclusion_per_beneficiary = annual_gift_exclusion[planning_year]` (e.g.194 $20,000 in 2026).195- `beneficiary_count` from SIGNED_PROFILE.196- `annual_exclusion_capacity = exclusion_per_beneficiary * beneficiary_count`.197- `annual_premium` from the policy.198- `premium_gap = max(0, annual_premium - annual_exclusion_capacity)` — the199 shortfall not covered by annual-exclusion gifts. (If capacity ≥ premium, gap = 0200 and there is no shortfall.)201- `administration.notices_required = beneficiary_count` (one Crummey notice each).202- `administration.dedicated_bank_account_required = true` (an ILIT should pay203 premiums from its own account after the withdrawal window).204205Dates (ISO):206- `administration.contribution_date = planned_contribution_date`.207- `administration.notice_due_date` = the contribution date (notices go out208 promptly upon contribution; same day is the defensible due date).209- `administration.withdrawal_window_end = contribution_date + 30 days` (standard210 Crummey withdrawal window).211- `administration.earliest_premium_payment_date = withdrawal_window_end` (pay the212 premium only after the withdrawal window closes).213214Estate result:215- `estate_result.death_benefit = death_benefit`.216- `estate_result.projected_outside_estate_if_implemented = death_benefit` for a217 **new** policy (`is_existing_policy_transfer == false`); for a transfer of an218 existing policy still inside the 3-year lookback, treat the proceeds as not yet219 reliably outside the estate (→ 0 or flag the lookback).220- `estate_result.tax_liquidity_support = min(death_benefit, estate_tax_exposure)`221 where `estate_tax_exposure = max(0, estate_value - exemption[planning_year]) * estate_tax_rate`.222223Risk flag (`recommendation.risk_flag` and `estate_result.estate_inclusion_risk`,224same enum):225- new policy + no shortfall → `LOW_IF_FORMALITIES_MET`226- new policy + `premium_gap > 0` → `EXCLUSION_SHORTFALL`227- existing-policy transfer + no shortfall → `THREE_YEAR_LOOKBACK`228- existing-policy transfer + `premium_gap > 0` → `THREE_YEAR_LOOKBACK_AND_EXCLUSION_SHORTFALL`229230Recommendation `primary_action`:231- no shortfall, new policy → `FUND_WITH_CRUMMEY_NOTICES`232- shortfall, new policy → `USE_LIFETIME_EXEMPTION_FOR_SHORTFALL`233- existing-policy transfer (lookback) → `USE_NEW_POLICY_OR_ACCEPT_LOOKBACK`234 (or `DISCLOSE_LOOKBACK_AND_USE_EXEMPTION` when there is ALSO a shortfall).235236`recommendation.suitability`: `SUITABLE_WITH_ADMINISTRATION` when formalities (notices,237dedicated account, window) cleanly cover the premium; `BORDERLINE` with a modest238shortfall; `NOT_SUITABLE` only in severe lookback+shortfall cases.239240---241242## 5. Analysis: `trust_comparison` — GRAT vs CRAT (e.g. Alvarez)243244Inputs: trust-candidate params; tax policy (`estate_tax_rate`,245`charitable_deduction_rate`, `max_crat_term_years`); SIGNED_PROFILE goal fields246(`family_transfer_priority`, `philanthropic_intent`); exemption + estate_value.247248Estate context:249- `estate_context.taxable_estate = max(0, estate_value - exemption[planning_year])`.250- `estate_context.estate_tax_exposure = taxable_estate * estate_tax_rate`.251- `estate_context.liquidity_gap_before_planning = max(0, estate_tax_exposure - liquid_assets)`.252253GRAT (term = `grat_term_years`, annuity rate = `grat_annuity_rate`, growth =254`expected_growth_rate`):255- `annuity = asset_value * grat_annuity_rate` (level annuity each year).256- `fv_asset = asset_value * (1 + growth) ** term`.257- `fv_annuity = sum over t=1..term of annuity * (1 + growth) ** (term - t)`258 (annuity payments reinvested/grown at the growth rate to end of term).259- `grat.projected_remainder_to_heirs = fv_asset - fv_annuity`.260- `grat.estimated_estate_tax_reduction = projected_remainder_to_heirs * estate_tax_rate`261 (value moved out of the taxable estate × rate).262- `grat.mortality_inclusion_risk = TERM_SURVIVAL_REQUIRED` (fixed enum: grantor263 must outlive the term).264- `grat.term_years = grat_term_years`.265266CRAT (term = `crat_term_years`, capped at `max_crat_term_years`=20; payout =267`crat_payout_rate`):268- `crat.term_years = min(crat_term_years, max_crat_term_years)`.269- `payout = asset_value * crat_payout_rate`.270- `fv_asset = asset_value * (1 + growth) ** term`.271- `fv_payout = sum over t=1..term of payout * (1 + growth) ** (term - t)`.272- `crat.projected_charitable_remainder = fv_asset - fv_payout`.273- `crat.estimated_income_tax_deduction = asset_value * charitable_deduction_rate`274 (charitable deduction approximated as the funded value × deduction rate).275- `crat.family_transfer_fit`: `LOW` (a CRAT sends the remainder to charity, not276 family) — use `LOW` unless the goal data clearly elevates family use of the CRAT.277278Recommendation (goal-driven; goal source = SIGNED_PROFILE):279- `family_transfer_priority == high` and `philanthropic_intent != high`280 → `preferred_strategy = GRAT`, `rationale_code = CHILDREN_TRANSFER_PRIORITY`,281 `alternate_role = SECONDARY_CHARITABLE_TOOL`.282- `philanthropic_intent == high` (and family not higher)283 → `preferred_strategy = CRAT`, `rationale_code = PHILANTHROPIC_PRIORITY`,284 `alternate_role = SECONDARY_FAMILY_TRANSFER_TOOL`.285- Otherwise default to the family-transfer (GRAT) branch, since these clients are286 large taxable estates whose primary motive is moving appreciation to heirs.287288---289290## 6. Analysis: `estate_liquidity_action_plan` — integrated (e.g. Chen)291292Combines the ILIT and trust-transfer analyses into a coordinated plan. Reuse §4293and §5 computations.294295Estate context (same formulas as §5):296- `taxable_estate = max(0, estate_value - exemption[planning_year])`.297- `estate_tax_exposure = taxable_estate * estate_tax_rate`.298- `liquidity_gap_before_planning = max(0, estate_tax_exposure - liquid_assets)`.299300`ilit` block:301- `annual_exclusion_capacity`, `premium_gap` as in §4.302- `estate_inclusion_risk` as in §4 (new policy + no shortfall →303 `LOW_IF_FORMALITIES_MET`, etc.).304- `projected_outside_estate_if_implemented = death_benefit` for a new policy.305306`trust_transfer` block (pick GRAT vs CRAT by goal as in §5):307- `preferred_strategy`, `projected_remainder_to_heirs` (GRAT remainder),308 `estimated_estate_tax_reduction` (GRAT reduction),309 `projected_charitable_remainder` (CRAT remainder). Provide all of these so the310 attorney sees both branches.311312`recommendation`:313- `primary_action`: `COMBINE_ILIT_AND_GRAT` when family-transfer priority is high314 and the estate needs both liquidity (ILIT) and transfer (GRAT);315 `CRAT_WITH_LIQUIDITY_REVIEW` when philanthropic priority dominates;316 `ILIT_WITH_EXEMPTION_REVIEW` when the ILIT premium creates an exclusion shortfall317 needing lifetime-exemption allocation.318- `sequencing`: `ILIT_FIRST_THEN_GRAT` for the combine case;319 `TRUST_DECISION_FIRST` when the trust choice (GRAT vs CRAT) is unresolved;320 `ILIT_FIRST_THEN_ATTORNEY_REVIEW` when documents must be drafted before the trust.321- `risk_flag`: the ILIT estate-inclusion risk flag (§4 enum).322323`action_set` — a list of enums **sorted alphabetically ascending**, drawn only324from: `ATTORNEY_DRAFT_REVIEW`, `CRAT_FOR_CHARITABLE_REMAINDER`,325`GRAT_FOR_APPRECIATING_SHARES`, `ILIT_CRUMMEY_NOTICE_CYCLE`,326`LIFETIME_EXEMPTION_ALLOCATION`. Build it by including:327- `ILIT_CRUMMEY_NOTICE_CYCLE` whenever an ILIT/policy is in play.328- `GRAT_FOR_APPRECIATING_SHARES` when preferred trust = GRAT (family priority),329 else `CRAT_FOR_CHARITABLE_REMAINDER` when preferred = CRAT (philanthropic).330- `LIFETIME_EXEMPTION_ALLOCATION` when `premium_gap > 0` (shortfall) or the estate331 needs exemption allocation.332- `ATTORNEY_DRAFT_REVIEW` essentially always (these need legal drafting).333Then `sorted(set(actions))`.334335---336337## 7. Common pitfalls / checklist338339- **Do not** read the client record's possibly-stale fields directly when a340 SIGNED_PROFILE fact disagrees — SIGNED_PROFILE wins. (In practice the client341 record already matches SIGNED_PROFILE, but always cross-check the documents.)342- **Beneficiary count drives** Crummey notices and exclusion capacity — take it343 from SIGNED_PROFILE (CRM count is usually different and stale).344- **Bracket target is per filing_status**; use SINGLE vs MFJ vs HOH correctly.345- **Exemption / gift exclusion are per planning_year** — read the right year key.346- **CRAT term is capped** at `max_crat_term_years` (20).347- **premium_gap / liquidity_gap are non-negative** (`max(0, …)`): a "gap"/"shortfall"348 of zero means fully covered; never report a negative gap.349- **estate_tax_exposure uses `estate_value - exemption`, then × 0.40** — not the350 full estate value.351- **Roth two-scenario consistency**: apply the identical intra-year ordering in352 baseline and conversion runs; savings = baseline − conversion must be ≥ 0.353- **conversion_years_positive ≤ conversion_window**: a near-RMD client gets few or354 zero positive conversion years; reflect that in suitability/risk enums.355- **Rounding**: round every USD figure to 2 decimals at output time (not midway,356 to avoid compounding rounding error).357- **Dates**: emit ISO `YYYY-MM-DD`; compute the 30-day Crummey window by real date358 arithmetic (use python `datetime.date` + `timedelta(days=30)`).359- **Enums**: copy enum spellings verbatim from the template; any typo fails.360- **task_id**: match the harness's numbering (`train_00X` for train, `test_00X`361 for test) using the numeric suffix of the task.362363## 8. Quick procedure3643651. Read prompt, memo (note horizon year if any), and answer_template (note required366 keys, enums, `analysis_type`).3672. `curl` health, `/clients/<id>`, `/source-documents?client_id=<id>`,368 `/policies/tax`, plus the analysis-specific endpoint(s) and `/rmd-factors` if369 Roth.3703. Resolve conflicts via §2 (SIGNED_PROFILE > ATTORNEY_MEMO > CRM_NOTE; accounts =371 CUSTODIAN_EXPORT; assets in trust comparison = ATTORNEY_MEMO).3724. Run the matching analysis (§3–§6) in a `python` helper; keep formulas373 internally consistent and self-check sign/order.3745. Assemble the JSON per the template, set `source_resolution.*` enums, round USD,375 ISO dates, sort any list fields.3766. Output the single JSON object only.