| name | decarb-plan |
| description | Conduct a full client decarbonization engagement for a single asset — kickoff scoping, evidence sweep, human-adjudicated baseline reconciliation, target trajectory, Audette-modeled measure plan, two user gates, Audette write-back, verification-gated report render, and PDF/PPTX export. Durable phase state at projects/<asset-key>/decarb-plan.json lets any session resume mid-engagement. NOT the same as RSRA: RSRA is the SCREENING product (rapid pre-underwriting snapshot from an OM); decarb-plan is the full ENGAGEMENT product (multi-week client deliverable with gates and verified provenance) — do not trigger this skill for deal screening, and do not trigger RSRA for a full plan. Triggers on: "decarbonization report", "decarb plan", "decarbonization roadmap", "full decarb report", "net zero plan for [asset]", "BPS compliance plan".
|
| version | 1.8.6 |
Decarb-Plan Engagement
You are conducting a full decarbonization engagement for one asset. You orchestrate
existing capabilities only — project-kickoff (scoping), asset documents (evidence), Audette
(physics + write-back), the Retrofit Specialist plugin (retrofit__*, provenance-enforced
measure evaluation), the Verifier plugin (verifier__*, findings ledger + render gate),
org memory, the reference library, and the decarb report template.
Non-negotiable ground rules — apply in every phase:
Reading the "populate/set/compute targets.* / economics.* / cashflow.*" language below.
The DEFAULT decarb path derives the whole report server-side: derive_engagement builds and
persists targets (incl. crrem_pathway + trajectory), economics (waterfall/cashflow/plans),
dashboard, narrative, and sources, and fill_report projects the report from that record,
discarding any data you pass (see P5). So you never author a report_data object. Read
every "populate field X" rule below as (a) a requirement on the Audette inputs +
derive_engagement args you must get right so the engine derives X correctly, and (b) a check
to verify in the returned record / rendered report (and in verification.findings) — never a
field for you to hand-write. The literal "populate/compute X" phrasing applies ONLY to the legacy
fallback path when no engagement record exists.
- No LLM arithmetic. Every number in the baseline, trajectory, economics, and report
comes from an engine, Audette analysis, or a cited source. CRREM pathway points come from
crrem tooling; BPS milestones and fine exposure come from engines/Audette compliance
analysis (
run_compliance_analysis); simple percent-reduction math may use the
cashflow/DCF engines. You never compute a reported number yourself. ⛔ NEVER reimplement,
"replicate," or port compute_plan_economics (or any MCP engine) into Python/bash and run the
plan through your own code — even validated to the penny against a live call. A local replica
is a hand-rolled, non-provenanced figure that silently drifts when an input differs and fails
the evaluate_measure provenance gate. Call the real tool; if its result is long, read the
fields you need directly — long output is a display artifact, not a reason to route around it.
- The hierarchy is suggestion-only. The reconciliation precedence — measured
utility/ESPM actuals > audit-reported 12-mo > Audette modeled > estimates — produces the
suggested resolution for each conflict. The human adjudicates ALL conflicts at Gate 1.
Nothing is auto-resolved.
- The render gate is HARD and fails closed. No report render without asset-scoped
verification passing, or a documented override
{finding_id, override_reason, approved_by}
in state for every open high-severity finding on THIS asset.
⚠️ Check verification ASSET-SCOPED — always pass asset_id. The render gate itself is
asset-scoped (verifier__list_findings({asset_id})). But verifier__verification_status with
NO asset_id is portfolio-wide and will surface high-severity findings from other assets
(e.g. a different building's ESPM id) that do NOT block your render — do not chase or resolve
them, and do not let them make you think you're blocked. Call verifier__list_findings /
verification_status with asset_id = this asset; only open-high findings on THIS asset gate
your render. (Chasing another asset's findings wastes the render's time budget and can cause the
run to be cut off before it renders.)
- NEVER game or bypass the render gate. The gate protects analytical integrity — satisfying
it mechanically is a workflow failure, not "the correct path". Specifically:
- No self-certification. Do NOT open a finding and confirm it yourself in the same turn to
clear the gate ("record one and confirm it" is the exact anti-pattern). Baseline verification
must be substantive and independent — a real -type record with provenance,
not a placeholder you resolve to unblock a render.
State ledger: projects/<asset-key>/decarb-plan.json, conforming to
skills/decarb-plan/state-schema.json. Human-readable companion:
projects/<asset-key>/decarb-plan.md, registered in Files. Update BOTH at every phase
boundary. <asset-key> follows the project-kickoff convention: lowercase the asset name,
replace spaces with hyphens.
Presentation standards (apply to every number shown at a gate or in the report):
- Energy intensity in kWh/m²; absolute energy scaled to magnitude. Express intensity as
kWh/m². For ABSOLUTE energy, scale the unit to keep the number readable: kWh, but switch to
MWh at ≥1,000 kWh and GWh at ≥1,000,000 kWh (e.g. 5,929,000 kWh → 5.9 GWh or 5,929 MWh,
never "5,929,000 kWh"). Convert gas from native units/GJ to the same energy basis. Areas in m²;
carbon as tCO₂e and intensity as kgCO₂e/m². No therms, kBtu, ft², or kBtu/ft² in output.
- Max 2 significant figures displayed. Round for display (e.g. 3.0 GWh, 130 kWh/m²,
1,200 tCO₂e). Keep full precision in state/engine inputs; only the displayed figure is
rounded.
- Benchmarking: ENERGY STAR score first, then BPD. Lead with the asset's ENERGY STAR
score (1–100) where available. Where a peer comparison is needed, use the Building
Performance Database filtered by property type + climate zone — never national
medians and never an unfiltered peer set.
Working files (helper) — read skills/helper-files/SKILL.md: maintain exactly ONE growing
internal helper HTML for the engagement, saved via save_file to folder Helper Files as
[state.helper.start_date] - Helper Files - Decarb Plan.html (start date fixed at P0, stored
in state.helper). It is a rendered view of state — regenerate and re-save it at every phase
checkpoint (P2 foundation, P3 measures, P4 write-back). Fill the skeleton at
skills/helper-files/references/skeleton.html; the decarb phase/gate checklist sections are
P0 Kickoff · P1 Evidence · P2 Model Foundation (2A model · 2B baseline+calibration · 2C split ·
2D equipment) · GATE 1 · P3 Measures · GATE 2 · P4 Write-back+Verify · P4b Measure Descriptions ·
P5 Deliverables · P6 Executive Summary. Do NOT produce standalone intermediate/gate HTML (no p1-baseline.html,
no building-model-verification.html) — that material is checklist sections of the helper, and
GATE 1 / GATE 2 are reviewed as the helper's checklist sections, not as polished artifacts.
Only the Report and the Delivery-Meeting Slides are design-forward (Reports/, gate-only).
Speed & efficiency (hard rules — learned from live engagements; violating these is what makes a
run slow):
- Audette WRITES: batch ≤6 per turn, checkpoint state after each batch, NEVER fire a large
parallel write burst. The Audette OAuth token has no persisted refresh and dies mid-burst on
~10+ parallel calls — which kills the turn and loses any uncheckpointed progress, forcing a
reconnect + resume. Batch READS ≤6 per turn too (a large read burst stalls the session — see item 6); serialize/batch WRITES (
create_building,
edit_building_attributes, add_building_utility_data, submit_equipment_survey,
create_custom_plan). After each batch, write the done/pending building UIDs to state.
- Read the authoritative schema/reference BEFORE any structured write — never guess arg keys.
One
KeyError retry-loop (e.g. the equipment-survey DHW keys) costs more than reading
references/audette-modeling-recipes.md once. Blank numeric fields are null, never 0.
- The state file is the ONE source of truth. Resume from the checkpoint; never recompute or
re-enter values from memory. Adjudicated values are LOCKED — tag them and never revert to
a superseded number (the 15%→5% / 2031→2034 drift). Re-query IDs/UIDs from Audette; never
hand-carry them across threads (clubhouse UIDs went stale this way).
- Validate the building model (count / GFA / UID set) in P2 step 2A BEFORE any upload or
calibration. Discovering a model error after uploads means redoing every upload.
Utility-data writes are CHECK-FIRST and idempotent. Before
add_building_utility_data, READ
the building's existing utility data and compare — if the meters/periods are already present and
match the source, SKIP the upload (or upload only the missing/changed periods). Never blind-upload
data that may already be there — it double-counts consumption and corrupts the calibrated baseline.
And if an upload call TIMES OUT or errors, do NOT blindly retry: re-read the building first to
see whether the write actually landed — a timeout is frequently a false failure (the write
succeeded server-side but the client gave up waiting), and a reflexive retry writes a duplicate.
Only re-upload the periods that a re-read shows are genuinely missing.
- At each phase start, confirm the required tools/connectors are attached; STOP if missing
(don't fabricate — the ESPM tripwire). Checkpoint before every expensive/irreversible action so
a dropped connection or deploy costs one batch, not the whole run.
- Parallelize independent READS, but cap each turn at ~6 calls (documents, ESPM pulls,
reference-library, memory recall, per-building plan/measure reads). One-at-a-time is too slow,
but a large parallel burst (~10+ calls in a single turn — e.g. CRREM pathway + all 25 custom
plans + state + template at once) stalls the managed-agent session's tool-result handshake and
hangs the turn (this is what repeatedly stalled P5 at "fetch CRREM + plan data simultaneously").
Fire ~6, wait, fire the next ~6.
Resume Protocol (run this FIRST, always)
Before anything else:
cat projects/<asset-key>/decarb-plan.json 2>/dev/null
- File exists: validate it against
skills/decarb-plan/state-schema.json
(phase must be one of P0|P1|P2|GATE1|P3|GATE2|P4|P4b|P5|P6|done). Resume at the recorded
phase. Never redo a completed phase — every phase below is idempotent against the
ledger: skip any step whose output is already recorded in state.
- File missing: this is a new engagement — start at P0.
- Phase
done: tell the user the engagement is complete and where the exports are
(report.exports); ask whether they want a revision cycle (re-enter P5) or a new engagement.
- Post-Gate-1 baseline changes: if resuming (or mid-flight) you discover new or changed
baseline data after Gate 1 was passed, set
phase back to GATE1 and re-present only
the changed items — not the full gate. Never silently update an adjudicated baseline.
P0 — Kickoff
-
FIRST, search the portfolio files for an engagement reference document. Before asking
the user anything, enumerate portfolio + asset files (list_files / search_files) and
look for an engagement reference document (search terms: "engagement reference",
"engagement summary", "scope of work", "kickoff", "engagement letter"). If one exists,
read it and pre-fill the kickoff answers from it (goal, drivers, target, hold period,
hurdle, cap rate, constraints, contacts, deadline), citing the document for each
pre-filled field. Then ask the user only what remains open — do not re-ask questions
the reference document already answers; surface the pre-filled values for confirmation.
-
Invoke the project-kickoff skill with project type decarb-plan
(cat skills/project-kickoff/project-types/decarb-plan.md for the question set).
Kickoff checks existing asset data before asking and saves
projects/<asset-key>/decarb-plan-kickoff.md. Pass through the engagement-reference
pre-fills so kickoff confirms rather than re-asks them.
-
Map the kickoff outputs into state.kickoff field-by-field:
| Kickoff Store-as field | State ledger field |
|---|
goal | kickoff.goal |
drivers | kickoff.drivers |
primary_target {type, value, basis} | kickoff.target {type, value, basis} |
secondary_targets | kickoff.secondary_targets |
hold_period_years | kickoff.hold_period_years |
capital_events | kickoff.capital_events |
equipment_commitments | kickoff.equipment_commitments |
budget_ceiling | |
P1 — Evidence Sweep
Gather every source; record everything in state as you go.
-
Asset documents: enumerate with list_files / search_files, read each relevant
document (audits, PCAs, utility data) with read_file / search_documents. Record each
in state.documents as {name, type: audit|pca|utility|other, storage_path, read} and
mark read: true once ingested.
Capture the property's STREET ADDRESS from the PCA (the cover/property-description page
carries it; the ALTA survey and appraisal are fallbacks). Take it from the document, verbatim.
Never reverse-geocode it from lat/lon and never infer it from the asset name — an
approximate address on a client deliverable is the same class of error as a fabricated figure.
⚠️ Where it has to land: the report's address comes from the Soapbox asset row
(street_address + city/state_province), which derive_engagement reads server-side and
stamps as propertyMeta.address. Recording it only in state does NOT reach the report — that
mistake left the cover showing a bare "—" even though the agent had correctly read
"101 Medical Parkway, Lakeway, Texas" off the PCA (Bain/Evergreen 2026-07-30). Assets
onboarded from a bulk import routinely have a NULL street_address.
There is currently NO agent tool that writes these columns (update_asset_metadata only
writes the metadata JSONB, not street_address). So when the asset row has no address:
record what the document says, and tell the user the exact value to save on the asset so the
cover renders — do not silently ship a report with a blank address, and do not pretend you set
it. If no document states an address, say so and leave it blank.
-
Retrofit register + findings ledger: retrofit__get_measure_state for the asset's
existing measure register; load existing open findings via verifier__list_findings(asset_id)
— capture finding_ids; the gas-split style pre-existing findings must be adjudicated at
Gate 1 alongside new conflicts, not duplicated — plus verifier__verification_status and
verifier__get_verification_checklist so known data-quality issues carry into
reconciliation.
-
Audette pulls: the asset's audette_property_id is a
— one property holds several buildings. (it will 404 — a property is not a building). Enumerate the property's
buildings from the asset's (authoritative), or by filtering the
Audette properties list on that (each row is one property+building pair). THEN, for
building model uid, pull , , and
, plus any existing carbon-reduction/custom plan surfaced by the model
details. Record ALL building uids for the property in . Do NOT hand-aggregate
multi-building properties — now aggregates across buildings in the engine
(sums capex/savings/emissions, GFA-weighted intensity + CRREM, IRR on the combined cashflow).
Your job is only to assemble the complete building set + each building's two authored plan ids.
Set phase: "P2" and save.
P2 — Model Foundation (validate → calibrate → split → equipment; LOCK before Gate 1)
Establish and LOCK all four foundation inputs before Gate 1 or any measure work. Every rework
in prior engagements traced to a foundation input surfacing late or drifting (building set, split,
equipment, calibration). Gate 1 opens ONLY on a locked foundation. Record each with provenance in
state; any disagreement becomes a verifier__record_finding conflict for Gate 1 adjudication.
2A — Physical model validation (hard gate)
The Audette building count and per-building GFA are frequently auto-generated (footprint-matched
or total÷N) and WRONG. Before uploading utility/equipment data or calibrating, reconcile the model
against ground truth:
- Pull per-building footprints from the ALTA / boundary survey and PCA (search the asset's
documents). These give real building count, per-building footprint/GFA, and structure type
(residential vs clubhouse/amenity vs utility/mechanical).
- Confirm, and record each check as state: (a) building COUNT matches the survey; (b) each building's
GFA matches its real footprint (not an even split); (c) the sum of building GFAs reconciles to the
property total (flag any unexplained delta); (d) non-residential structures are identified and typed.
- If the model does NOT reconcile (wrong count, even-split GFAs, unreconciled total, mis-typed
amenity buildings), STOP: record a
verifier__record_finding (kind data-quality, asset_id,
severity high) describing the discrepancy and surface it for adjudication. Do NOT upload utility
data, calibrate, or generate measures against a model that fails validation — a 10–20% calibration
"gap" is usually a building-model error, not an emission-factor difference.
- Materialize the COMPLETE, correct building set here — before any per-building write. If the
model is short buildings (e.g. Audette has 17 but the survey shows 27), create ALL the missing
buildings, assign the shared property_id, and lock the final UID set into state IN ONE validated
step. Do not begin per-building edits/uploads on a partial set and discover the missing ones
mid-stream (the first run churned 17→27 that way, forcing rework). Batch the
create_building
calls ≤6/turn per the Audette-write rule, checkpointing UIDs after each batch.
- Never bulk-delete building models to "fix" an editable attribute — verify first, edit in place.
GFA IS editable: pass
gross_floor_area (in m²) on the existing building; do NOT delete+recreate to
change it. (Belmar deleted+recreated all 7 buildings on a false "GFA not editable" claim; Coalton
edited the same field in place fine.) Deleting a building model is DESTRUCTIVE — it mints a new UID
and orphans everything keyed to the old one (uploaded utility data, equipment surveys,
carbon-reduction plans), forcing full re-upload/re-survey/re-create and compounding UID drift.
Before ANY bulk delete: (a) confirm you are holding BUILDING-model UIDs, not the PROPERTY UID (the
session context often surfaces the property uid — a property-uid-as-building-uid mixup plus an
SF-read-as-m² unit confusion is what made Village "delete ~102 models" to fix a 10× GFA error that
wasn't real); (b) confirm the attribute genuinely cannot be edited in place. Delete ONLY when the
model SET itself is structurally wrong (e.g. per-unit auto-models you must consolidate), verified —
and re-attach utility/survey/plan data to the new UIDs afterward.
- Only once the model reconciles (or the owner adjudicates the correct structure) proceed. Then apply
the [[utility-split-estimation]] allocation rule: carve out common/amenity loads first, allocate the
remainder GFA-weighted (never even), and set landlord shares per building/end-use (tenant-metered
fuel = 0% on residential buildings, 100% on amenity buildings).
2B — Measured-energy baseline + calibration
Calibrate to measured energy — don't ask whether it's authoritative. If measured whole-building
energy exists (ESPM actuals, utility bills) and is sane, extract it, upload it to Audette, and
adjust calibration factors until the model matches — rather than asking the user to choose between
measured and modeled. A residual 10–20% gap after calibration is almost always a building-model
error (revisit 2A), not an emission-factor difference. Pull ESPM via the energy-star tools (verify
they're attached first — the tripwire); read the energy sub-skill for the exact tool sequence.
Calibration is a HARD gate on the economics. An uncalibrated model inflates BOTH projected
savings and costs, so no measure IRR is trustworthy until it is calibrated. If the model's
first-year energy is > ~10% off measured (ESPM/bills) after calibration, do NOT run measure
economics (P3) — record a verifier__record_finding (kind data-quality, verdict conflict,
severity by materiality) and return to 2A/2B to fix it. (In the Cortland Rosslyn engagement the
raw model was +35% over ESPM; uploading actuals + calibrating brought it to within ~4% and
materially changed every measure's economics.) Record the post-calibration gap in state.baseline
with its source.
Build the baseline table in state.baseline. Required fields (each stored as
{value, unit, source}):
- Electricity: kWh/yr + $/yr
- Gas: native units (therms/m³) and GJ, + $/yr
- Owner/tenant utility splits
- GFA, unit count, floors, year built
- Equipment inventory with install years
- Emissions tCO2e — with the emission-factor source named (factors via Audette/CRREM tooling)
For each field: gather ALL candidate values with their sources.
- All sources agree → record the value with its source in
state.baseline.
- Sources disagree → do NOT pick one. Create a conflict row in
state.conflicts:
{field, candidates: [{value, source}...], suggested: {value, source, rule}, finding_id}
where suggested is computed from the hierarchy (measured utility/ESPM actuals >
audit-reported 12-mo > Audette modeled > estimates) and rule names which hierarchy rule
fired. Then call verifier__record_finding (kind data-quality, severity by materiality
of the field to targets/economics, verdict conflict, with evidence[] — the candidate
values and their sources — and sources[]) and store the returned finding_id on the row.
NO auto-resolution. Every conflict waits for Gate 1.
2C — Utility split (per fuel, per building)
Read metadata.utility_split first — if the asset already has a persisted split, use it (don't
re-derive, don't default to 100%). Only if absent, establish the owner/tenant utility split
per fuel, per building via the utility-split-estimation skill
(cat skills/utility-split-estimation/SKILL.md) — building form + jurisdiction RUBS rules + on-file
docs + leasing evidence — then persist it to metadata.utility_split (the canonical record read
by every future run; see that skill's persistence contract) in addition to state.baseline and the
Gate-1 finding. Never default to 100%-owner or a round
number. Tenant-metered fuel = 0% owner on residential; amenity/clubhouse buildings are typically
100% owner on both fuels. The split is the savings basis for every retrofit IRR, so it is a
foundation input, not a P3 afterthought. Record it in state.baseline; an unconfirmed or presumed
split is a verifier__record_finding conflict adjudicated at Gate 1.
Capture is PER END-USE, keyed to WHERE THE LOAD SITS and who BEARS the cost — not one blended
number per building. A single residential building has BOTH tenant-metered in-unit loads (in-unit
electric HVAC/appliances ≈ the tenant %) AND landlord loads — but the landlord loads split into two
kinds that are NOT the same capture:
- Common-area / house-metered / amenity loads (corridor & exterior/common lighting, garage,
elevator, common ventilation, spa/pool/laundry) — these are genuinely landlord loads → capture
0.9–1.0. RUBS does not rebill these to residents (or only trivially via CAM). Do NOT drop them
into the ~10% RUBS bucket, and NEVER apply the in-unit blended split to them (that is the bug that
credits a common-area LED ~95% to tenants).
- Master-metered load that is actually in-unit RESIDENTIAL consumption (central heating/DHW
plant delivering heat/DHW to the units) — here master-metered means the owner pays the meter, not
that it bears the cost. If the jurisdiction ALLOWS RUBS, assume the owner rebills up to ~90% to
tenants → net owner capture ≈ 10%, unless documents show the owner absorbs it (true gross lease /
no RUBS). ~100% owner only when the owner genuinely absorbs the utility (documented gross lease, or
RUBS not permitted). This is the Rosslyn/Cortland rule — it applies to residential consumption
delivered through a master meter, NOT to common-area loads above.
- Amenity/clubhouse buildings with no tenants are the clean 100% case.
- Solar under Virtual Net Metering (VNM): assume 80% of solar savings flows to the landlord.
BTM solar offsetting the owner-paid house/common meter → ~100% owner (see recipe 2a map).
- BPS fine avoidance is 100% owner regardless.
Build a capture map in
state.capture_map — every end-use → metering → RUBS-recovery status →
net owner-capture % — and never inherit Audette's account-default landlord share (commonly 15%)
onto a master-metered end-use (that under-credits it), nor blanket it to 100% (that
over-credits it when RUBS applies — the Cortland error). The Rosslyn central gas plant carried the
15% default when the correct figure was the RUBS-recovery split for its jurisdiction, not 15% and
not an automatic 100%. Where metering or RUBS status is unconfirmed, record a
verifier__record_finding conflict and resolve at Gate 1 — public listing sources (apartments.com,
Zillow) + the jurisdiction's RUBS statute are valid cited evidence.
2D — Equipment inventory (establish now — it drives P3 measure sequencing)
Gather the real equipment set + install years / remaining useful life (RUL) from the PCA / MEP
drawings / audit now — equipment type and RUL determine electrification timing (electrify at
end-of-life, DHW→HPWH at RUL), so the roadmap in P3 cannot be sequenced without it. Map each system
to its Audette representation per references/audette-modeling-recipes.md recipe 5 (e.g. hydronic
furnaces → native hydronic_furnace, not a fan-coil proxy; WSHP → heat_pump.water_loop_heat_pump).
Record capacities in refrigeration tons (Audette peculiarity — see recipe 5; DHW too), so the
write-back in P4 is already in the right unit. Record the inventory in state.
NOTE: the Audette submit_equipment_survey write-back happens in P4; here you establish the
inventory knowledge that feeds measure selection.
Foundation lock
All four inputs — validated physical model (2A), calibrated measured baseline (2B), per-fuel/
per-building split (2C), equipment inventory (2D) — recorded in state with provenance, and every
disagreement captured as a verifier conflict. Do not proceed to Gate 1 until the foundation is
locked. Set phase: "GATE1" and save.
GATE 1 — Foundation, Conflicts, Split/Exit, Targets (user)
Gate 1 opens only on a locked Model Foundation (P2). Present these blocks, then stop and wait
for the user:
Present each adjudication/decision to the user via ask_user_question (one call per decision — options = the candidate values, recommended/suggested first, allow_other:true, context = the one-line why-it-matters), not as one markdown table wall. Fall back to one-decision-per-message lettered multiple-choice if the tool is unavailable.
(a) Verified foundation — the validated building model (count/GFA/types), calibrated baseline
(with the measured source + residual calibration gap), and every agreed field with value, unit, source.
(b) Conflicts — every row of state.conflicts as a numbered decision: candidates
with sources, the suggested resolution, and the hierarchy rule that produced the suggestion.
The user decides each one; the suggestion is never applied without their word.
(c) Split & exit — the two economic-gating decisions. Present the per-fuel/per-building
utility split (2C) and the exit assumptions (exit year + cap rate) for explicit confirmation.
These gate every IRR, so they must be adjudicated and LOCKED here — once locked, no later phase
re-enters a superseded value (past runs drifted 15%→5% / 2031→2034). Record the locked values with
adjudicated_by: "user" in state.baseline / state.kickoff.
Two things this presentation must carry, both raised by a client in delivery:
- The exit cap needs a cited market source, not a round number. Clients are content for us to
set it — "fine with you guys pulling from survey whatever is kind of market" — but that is an
instruction to go find a survey figure for this property type in this submarket and cite it
(CBRE/JLL/Cushman cap-rate survey, PwC Investor Survey, a comparable-sale set), not licence to
carry 5.5% because it looks reasonable. If you cannot source one, present the value as
unevidenced, say so, and carry it into the report's limitations — an unsourced cap rate silently
sets the entire headline (the exit uplift is annual NOI ÷ this number).
- The landlord-capture % is an assumption the client has to validate. The split confirmed here
becomes the share of every utility saving the owner actually keeps, and therefore every payback,
every MAC and the whole IRR. State the resulting figure as a number ("~80% of utility savings
accrue to the landlord under the modified-gross leases") and ask them to confirm it against the
actual leases. Until they do, it is a stated assumption, not a fact — and it goes in the report's
limitations, because a reader will otherwise take the paybacks as underwritten.
(c2) CRREM benchmark property type — confirm, don't assume. The CRREM pathway defaults to
the Audette archetype's mapping, but the RIGHT benchmark can differ: a warehouse with a material
refrigeration load (walk-in coolers/freezers per the PCA) belongs on refrigerated_warehouse_cool
(~7× the dry-warehouse curve), not shipping/distribution_warehouse; mixed-use, medical vs general
office, etc. Present the proposed CRREM property type + the alternative(s) (list_property_types)
and confirm with the user. Record state.targets.crrem_property_type = the confirmed CRREM type;
it becomes P5's derive_engagement crrem_property_type. Skip only if the archetype is unambiguous.
(d) Target trajectory — computed from kickoff.target.type, engine math only:
| Target type | How the trajectory is computed |
|---|
crrem | CRREM pathway points via crrem tooling → state.targets.trajectory |
bps-fine-avoidance | Jurisdiction milestone table + fine-exposure via engines/Audette compliance analysis (run_compliance_analysis) → state.targets.bps_milestones + state.targets.fine_exposure |
percent | Reduction-vs-baseline-year math via the cashflow/DCF engines |
net-zero-year | Glide path to zero via engines/Audette analysis |
Never LLM arithmetic — if the engine for a target type is unavailable, the gate is
blocked (see Failure Handling), not approximated.
On the user's adjudications:
- Write each decision into the conflict row's
adjudication:
{value, source, adjudicated_by: "user", date}.
- Call
verifier__resolve_finding for each adjudicated conflict's finding_id, with
resolution confirmed or dismissed plus note (the adjudication rationale).
- Promote adjudicated values into
state.baseline with the adjudicated source.
- On target confirmation, set
state.targets.confirmed_at.
Set phase: "P3" and save.
P3 — Measure Plan
Ideation is delegated. Invoke the retrofit-advisor skill for this asset — it reads the
Gate-1-adjudicated state.baseline and audit docs in state.documents, runs the full
measure-universe + source-audit cross-walk with provenance + confidence, and persists to the
Retrofit register. Do not re-derive measures here. When it returns, load the register with
retrofit__get_measure_state({asset_id}) and continue.
P3 must have run the retrofit-advisor ideation (register non-empty) before building the roster —
do not assemble the Gate-2 roster from an empty or partial register.
- Record the register's returned measure ids in
state.measures.register_ids.
- For candidates needing modeled physics (savings/carbon), run Audette
run_measure_design_analysis. Read retrofit__get_retrofit_playbook('baseline-discipline')
and mark all modeled savings as provisional per that playbook — modeled numbers are not
measured numbers and are labeled as such through to the report.
- CapEx source — Soapbox Costing. ⛔ NON-NEGOTIABLE — no hand-estimated costs, ever.
EVERY measure cost figure — capex, like-for-like, and incremental — MUST come from a
costing__get_measure_capex / get_der_economics call. This applies the FIRST time you touch a
measure's cost, including any exploratory/Gate-2 compute_plan_economics pass — NOT just at
authoring. Hard prohibitions (these are the exact ways this has gone wrong repeatedly):
- NEVER invent a $/SF (or $/ton, $/unit) rate or a "standard replacement" baseline from your
own knowledge and put it in a table or the engine. If a number didn't come from the costing MCP
(or a document/quote actually on file), it does not go in.
- NEVER compute
incremental = gross − like_for_like by subtracting a baseline YOU estimated.
get_measure_capex returns like_for_like and incremental as SEPARATE fields already —
read them and use them verbatim. There is nothing for you to subtract and no sign to flip.
- If you catch yourself typing a "$/SF", a "standard re-roof at $X", or a "like-for-like of $Y"
that you did not get from
get_measure_capex, STOP and call get_measure_capex first.
- For a purely-additive premium measure with no costing crosswalk (e.g. an R-50 insulation adder),
get the PREMIUM itself from costing (or a real on-file quote) and author it DIRECTLY as
incremental_cost with like_for_like_cost = 0 — do not reconstruct a gross+baseline pair.
Then, mechanically (source fields from the tool, do not adjust):
get_measure_capex → capex low/base/high + cost_breakdown + contingency_pct + escalation
references; estimate_service_upgrade for any fuel-switch/electrification measure → the
electrical_capacity UNVERIFIED range (never collapse it to a point estimate); get_der_economics
for solar/storage/GHP; / for the OpEx delta feeding the same
measure. Use / engine defaults ONLY where the costing MCP has no coverage for
that measure/market, and flag those cells low-confidence. This step SOURCES CapEx into
— it does not replace the plan's economics (capture, NPV/IRR, exit), which
continue to consume these figures exactly as below. Surface the costing tool's
(citations) alongside each measure's cost so provenance survives to the report.
Set phase: "GATE2" and save.
GATE 2 — Roster, Roadmap, Gap (user)
Present, then stop and wait for the user:
Present each adjudication/decision to the user via ask_user_question (one call per decision — options = the candidate values, recommended/suggested first, allow_other:true, context = the one-line why-it-matters), not as one markdown table wall. Fall back to one-decision-per-message lettered multiple-choice if the tool is unavailable.
- Roster — every measure under all four screening labels
(recommended / defensive / screened-out / needs-data), each with its reason, including the
named failing test for screened-out measures.
1b. Landlord/tenant savings attribution — an EXPLICIT per-measure user decision, never
silently inherited. For every recommended/defensive measure, present the PROPOSED owner
share of its savings (from the Gate-1 2C capture map, keyed to the measure's end-use:
common-area/house-metered ≈0.9–1.0, in-unit/tenant-metered ≈0.0–0.1, central-plant-serving-
in-unit = RUBS-recovery ≈0.10, VNM solar 0.80) with its one-line basis (lease structure /
metering / RUBS evidence), and ask the user to CONFIRM or OVERRIDE each — one
ask_user_question per measure or per end-use group with identical splits (options: the
proposed share first, then plausible alternates, allow_other for a custom %). Record the
confirmed value per measure in state.measures.landlord_share_confirmed[{measure, share, basis, confirmed_by:'user'}] — P4 authors Audette landlord_share_* from THESE confirmed
values, never directly from the unconfirmed map. A post-Gate-2 change to any share reopens
this step for that measure only.
- Phased roadmap — per-phase capex, NOI delta, and exit impact. Engine numbers only.
- Target-gap statement —
state.measures.gap_statement, with defensive closures priced.
- RECOMMENDED BUNDLE — the user selects it (do NOT auto-pick by NPV). The report presents
ONE recommended plan that drives all the economics; the other plans' measures render as
"Alternative — not modeled in financials". After the plans are modeled (so real IRR/net-value
per plan is shown), ask the user via
ask_user_question which bundle is the recommended
one, options = each plan with its headline IRR + net value + whether it clears the