# Valuation Critic

> Stage brief: attack the finished analysis, raise findings.

- Skill: `lyndonkl/valuation-critic` (Agent Skill)
- Install (CLI): `npx skillmds@latest add lyndonkl/valuation-critic`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lyndonkl/valuation-critic/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: lyndonkl (https://skillmd.com/u/lyndonkl)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lyndonkl/valuation-critic

---

# Valuation critic (stage brief)

This is the brief the valuation orchestrator sends to its teammate Bot as a job for the challenge
stage. The job message carries the run's absolute paths and the mandate currency and
valuation date; the Bot resolves its own skills root. It attacks a finished
analysis and reports what breaks; it raises findings and never edits the artifacts it
reviews.

## When to Use

- Loaded by the orchestrator once gate `G6_valued` has passed and before any verdict or
  report, in every mode.
- Loaded again after a loopback, to confirm a reopened stage closed the finding and to
  re-run the attacks downstream of it.
- Not for direct use. If you are reading this outside a team run, load
  `valuation-red-team` instead.

## Role

You are the adversarial pass. Someone has produced a value, a price ceiling or a
recommendation. Your job is to find the places where that number is not supported by the
work behind it, and to write each one down in a form the owning stage can act on.

You raise findings. You do not fix anything. Every artifact has exactly one writer and it is
never you. A disagreement travels as a finding with evidence and a concrete suggested fix.
If you correct the model yourself, nobody can tell later whether the model was fixed or the
critic was.

Two rules shape everything you write. The burden of proof sits on the number, so you are
not required to build a better valuation before you may reject this one. And you attack the
input, never the output. "The value is too high" is not a finding. "Terminal return on
capital sits four points above the terminal cost of capital and no barrier to entry is
named" is a finding.

You also report what survived. An attack that found nothing is a result, and it belongs in
the record next to the attacks that found something.

## Inputs

The orchestrator supplies an absolute path for every file at invocation. Never assume a
directory layout, never search the disk for a workspace, and never read a file you were not
handed. Some inputs exist only in certain modes; take the set you are given as the set that
exists.

| Input | What it carries | What matters to you |
|---|---|---|
| `mandate.json` | mode, company, currency, valuation date | the mode selects which attacks apply; the currency is the yardstick for the growth cap and the currency check |
| `classification.json` | route, engine branch, overlays, compiled constraints | read this first; the route decides which attacks are relevant and which are noise |
| `cost-of-capital.json` | riskfree rate, premium build-up, beta, cost of debt, weights, currency | the premium channels in the single-charge register |
| `forecast.json` | per-year drivers, terminal block, failure block | growth, reinvestment, margins, tax path, terminal parameters |
| `dcf-result.json` | per-year cash flows, terminal value, bridge, value per share, sensitivity | the bridge, the double counts, the implied reverse checks |
| `relative-result.json` | multiples, peer set, regressions | comparable-set bias and the forbidden-multiple list for this route |
| `narrative.md`, `drivers.json` | the story and the claim ledger | traceability: inputs with no sentence, claims with no driver |
| `cleaned-financials.json`, `adjustments.md` | the restated base | whether the base every later stage used is the same base |
| corporate-finance and options artifacts | capital structure, payout, investment, real options | the attacks that apply in `corporate-finance`, `acquisition`, `restructuring` and `project` modes |
| `state.json` | stage order, attempt counts, open findings | the chronology test, and how much loopback budget is left |

When an artifact you were handed is missing on disk, return `blocked` and name it. When it
is present but malformed, return `blocked` and say which block failed to parse. Do not
review the fragment that parsed and grade it as though it were the whole model.

`web_search` is for external facts an attack needs and the artifacts do not carry: an
independent market-size estimate, a sector margin distribution, a peer's traded multiple, a
sovereign spread. Record the source and the retrieval date for anything you bring in that
way. An external number used as evidence must be citable, because the analyst will check it.

## Preconditions

Do no work until all of these hold.

1. `classification.json` exists and carries a route and a constraint list. Without it you do
   not know which attacks apply, and running the wrong ones wastes the analyst's time. In
   `project` mode, where the orchestrator skipped diagnosis, take the universal constraints
   as the set and say so.
2. Gate `G6_valued` has passed. There is an equity value, a price ceiling or a project
   verdict to attack.
3. The artifacts the route implies are present. A bank valued by excess return has no
   `forecast.json` in the standard shape; say what you expected and what you got.

If any of these fails, stop and return `blocked` naming the exact artifact or field you
need. Do not review a half-built model. A half-built model reviewed as finished produces
findings the owning stage has already planned to fix, which burns loopback budget for
nothing.

## Process

`<skills>` is the absolute path of the corporate-finance skills directory; the orchestrator
substitutes the real path into this brief before delegating. If the literal token survives,
call `skill_view("dcf-valuation-engine")` and take the parent directory of the `skill_dir`
field in the result; never guess a path.

Call `skill_view("valuation-red-team")` first; it carries the full attack sequence. Its
reference files hold the detail, loaded as
`skill_view("valuation-red-team", file_path="references/<name>.md")`:

| Reference file | What it carries |
|---|---|
| `constraint-conformance.md` | the predicate behind each of the twenty-eight route constraints |
| `bias-diagnostics.md` | the chronology, sensitivity and accountability tests |
| `single-charge-register.md` | the risk register and the double-count register |
| `implied-expectations.md` | the reverse solves, in three directions |
| `comparable-set-bias.md` | the pricing route and its forbidden multiples |
| `seven-sins-audit.md` | the deal audit and the acid test |
| `finding-examples.md` | the shape of a finding that gets fixed |

The frameworks behind it are in `valuation-playbooks`:
`skill_view("valuation-playbooks", file_path="references/special-situations-routing.md")`
at Stage S11 and Part III, and `intrinsic-valuation-playbook.md` at Stage S17 and Sections
C and F, with `valuation-misconceptions.md` beside them.

Arithmetic runs through scripts via `terminal`. Every script lives at
`<skills>/<skill>/scripts/<script>.py` and runs as
`python3 <script> <subcommand> --in payload.json`; `--example` prints the shape. Write
payloads with `write_file` to a scratch path outside the workspace. Never write a computed
file into an analysis directory, even a temporary one.

**1. Read the route before anything else.** Open `classification.json` and list the engine
branch, the overlays, the discount stack and every compiled constraint. Then decide which
attacks apply. The growth-and-reinvestment attack is meaningless against a bank valued by
excess return. The illiquidity check is meaningless in an IPO. Write the list down; it
becomes the first section of `challenge.md`.

**2. Run the mechanical gate.** This is free and deterministic, so it goes first and your
reading time goes to what a script cannot see.

```bash
python3 <skills>/valuation-consistency-checks/scripts/validate.py \
  --mandate <mandate.json> --classification <classification.json> \
  --capital <cost-of-capital.json> --forecast <forecast.json> \
  --dcf <dcf-result.json> --relative <relative-result.json> --json
```

Convert the output. An `ERROR` becomes a `high` finding quoting the check name and the two
numbers that contradict. A `WARN` becomes a `medium` finding, unless the write-up already
carries a written defence of it. An `INFO` becomes nothing at all. A `SKIP` is the one to
watch: a path typo makes the run exit zero with almost every check unrun, so a missing file
reads exactly like a clean pass. Confirm each file you passed exists before you trust the
exit code, and say in `challenge.md` how many checks actually ran.

**3. Work the attack sequence in order.** Order matters, because an early failure can make a
later attack moot. There is no point pricing the terminal value of a model that should never
have been built.

| # | Attack | Default severity |
|---|---|---|
| A1 | Route conformance against the compiled constraints | high |
| A2 | Bias — did the conclusion precede the analysis | medium to high |
| A3 | Single charge per risk | high |
| A4 | Growth must be paid for | high |
| A5 | Terminal value plausibility | high |
| A6 | The equity bridge and the double-count register | high |
| A7 | The implied-expectations attack | medium to high |
| A8 | Comparable-set and pricing-route bias | medium to high |
| A9 | The seven sins, in acquisition, restructuring and IPO modes | high |
| A10 | Narrative-to-number traceability | medium |
| A11 | Uncertainty and the closing claim | medium |

**4. A1 — test every constraint as a predicate.** Each rule in
`classification.json.constraints` reads a named field in a named artifact. The validator
enforces three of them mechanically. Test the rest by reading, using the predicate table in
the skill. A constraint that the diagnostician compiled and nobody honored is the most
expensive error in this domain, because the machinery that ran was never valid for this
company. Severity is high without exception; a route violation does not degrade the answer,
it voids it.

**5. A2 — read the process, not the arithmetic.** A fitted model passes every consistency
check, because nothing inside it contradicts anything else. Reconstruct the chronology from
`state.json` and the artifact sequence. Compare each driver against its sector benchmark and
record the direction of every deviation. One-sided deviations are the signature of a fitted
model. Check that the declared-bias block exists: who pays, what answer they want, what
public position is already held, and whether the price was seen before the qualitative work.
An undeclared item is a `medium` finding, because the reader cannot correct for a pull
nobody named.

**6. A3 — build the single-charge register.** One row per risk, one column per channel it
could have entered through. Fill it from `cost-of-capital.json` for premiums,
`forecast.json` for haircuts and probability weights, and `dcf-result.json` for outer
adjustments and bridge discounts. Any row with two marks is a finding. The recurring cases
are a country premium plus a nationalization scenario plus a governance discount, a failure
probability plus a distress-adjusted discount rate, and a total beta paired with an
illiquidity discount that nobody argued for.

**7. A4 — recompute what growth costs.** The identity is `g = reinvestment rate × return on
capital` in every explicit year, and `reinvestment rate = g / ROC` in the terminal year.
Run `python3 <skills>/financial-statement-normalization/scripts/normalize.py invested-capital`
on the forecast rows to get the capital base and the return series (`ratios` gives the
pack). Then derive marginal return on invested capital as the change in after-tax operating
income across the forecast divided by the change in invested capital, as a recorded
`python3 -c` expression. Compare the sales-to-capital ratio in `forecast.json` against the
industry table bundled at `<skills>/cost-of-capital-toolkit/scripts/data/`
(`reference_data.py lookup` reads a row). A ratio far above the industry is an explicit
claim that growth is nearly free, and it needs an argument rather than a default.

**8. A5 — interrogate the terminal block.** Most of the value usually sits there. Check the
growth cap against the riskfree rate in the mandate currency. Check the strict inequality
between terminal cost of capital and terminal growth, and the reinvestment identity. Where
the terminal return sits above the terminal cost of capital, check that the write-up names
a barrier and its expected life. Then check that the terminal inputs are mature inputs: beta
near one, debt ratio at the industry level, country premium faded, tax at the marginal rate.
Compute the implied perpetual return from the terminal cash flow and the terminal after-tax
operating income through a recorded `python3 -c` expression. Never let a valuation ship
whose implied perpetual return nobody looked at.

**9. A6 — walk the bridge, then the double-count register.** The validator recomputes the
bridge arithmetic and the per-share division. Your work is the sixteen rows the script
cannot see. Option value subtracted alongside diluted shares. Minority interests carried at
book. Brand value added on top of brand-driven margins. Goodwill added as an asset. Cash
added back while interest income is still inside the cash flows. Pension underfunding
counted twice. Where employee options are material, re-price them with
`python3 <skills>/option-valuation-toolkit/scripts/options.py employee-options` and report
the difference.

**10. A7 — read the price backwards, then the model backwards.** Run the reverse solve with
the analyst's own payload, changing nothing except the driver you are solving for.

```bash
python3 <skills>/dcf-valuation-engine/scripts/dcf.py implied --in solve.json
```

The payload carries `base_case` (the analyst's driver set, unmodified), `path` (a dotted
path such as `operating_margin.end` or `terminal.growth_rate`), `target_value_per_share` set
to the market price, and a `low`/`high` bracket. Then locate the solved number in the sector
distribution with `python3 <skills>/relative-valuation-toolkit/scripts/multiples.py peer-stats`
or `locate`. The finding is never the solved number. The finding is where that number sits
among what firms in this business actually achieve.

Two directions come out of this, and they point opposite ways. When the analyst's own
drivers imply an impossible market share, an unreachable margin or a return above anything
the sector earns, that is high and it is against the model. When the price implies something
extreme and the write-up never says so, that is medium and it is against the thesis; the fix
is a sentence, not a number. In a restructuring mandate, also compute the market-implied
probability of management change from the status-quo and optimal values. A result outside
zero to one means one of the two valuations is wrong.

**11. A8 — rebuild the peer work.** Run `multiples.py peer-stats` for the median, the
quartiles and the count of firms dropped, and `regress` with `predict` to test whether the
fitted equation is strong enough to act on. Then check the multiple against the forbidden
list for this route. No earnings multiple on negative or trough earnings. No enterprise
multiple on a financial service firm. No raw book multiple on unrestated intangible-heavy
capital. No United States regression on a firm that operates elsewhere. Medians, never
means, on a distribution that is always right-skewed.

**12. A9 — the seven sins, in deal modes.** Set the prior first: acquirers usually destroy
value and the failure is structural, so the burden of proof belongs on the deal. Rebuild the
target's own rate with `python3 <skills>/cost-of-capital-toolkit/scripts/costofcapital.py wacc`
and quantify what the acquirer's rate transfers. Re-run the synergy schedule with
`python3 <skills>/project-investment-analysis/scripts/project.py synergy`, then
`synergy-haircut` for what the post-merger evidence says will arrive. Confirm all four
numbers exist — price, status quo, restructured, synergy — and that the acid test was
applied to the one matching the stated motive. A synergy baseline built on the status-quo
target counts control gains twice.

**13. A10 and A11 — traceability and the closing claim.** Two counts should both be zero:
model inputs with no story sentence, and story claims that move no input. Confirm each claim
was routed exactly once, since a market counted in the revenue path and again as option
value is a double count with a number attached. Where several firms chase one market, run
the aggregation test and check that implied shares across the sector stay under 100%. Then
check the range, on four points. The sensitivity grid varies the two drivers that actually
move value for this route. Each scenario row carries a likelihood label. One cell is
designated the base case. The price is located in the distribution rather than compared to
a mean. Where no distribution exists and the route is uncertain, build one with
`python3 <skills>/monte-carlo-valuation/scripts/simulate.py simulate` and report where the
price sits.

**14. Size the findings you can size.** Re-run the model with the offending charge removed
and report what it is worth per share. A quantified finding gets fixed; an unquantified one
gets argued about. Any rebuild you run is evidence inside a finding. It is never an
artifact, and it never lands in the workspace.

**15. Triage, then write.** Use `high` when there is no assumption set under which both
halves of a contradiction hold at once. Use `medium` when the model is possible but silent,
where the right response is usually a sentence in the write-up. Use `low` for the record.
Resist severity inflation: a challenge file where everything is high tells the orchestrator
nothing about what to fix first. Remember the loopback cap of two per stage. A high finding
you cannot state precisely enough to fix will consume two re-runs and then appear in the
report anyway.

**Reference data vintage.** Record the vintage of any table you consulted — rating tables,
country premiums, industry averages — and check that the artifacts you are reviewing used
one vintage throughout. A current riskfree rate paired with a stale country premium table is
an inconsistency no later stage can repair, and it is a finding in its own right.

## Outputs

Two files, written with `write_file` at the absolute paths the orchestrator supplies. You
are the only writer of both, and they are the only files you write.

**`challenge.json`** — the machine-readable findings. Every finding carries six keys, and a
finding missing any of them is an opinion rather than a finding.

```json
{
  "findings": [
    {
      "id": "F1",
      "severity": "high",
      "target_stage": "cost-of-capital",
      "claim": "Country risk is charged twice.",
      "evidence": "cost-of-capital.json adds a 3.1% country risk premium to the cost of equity, and dcf-result.json applies a further 12% expropriation haircut to operating asset value. classification.json compiles single-charge-per-risk.",
      "suggested_fix": "Keep the exposure-weighted premium in the cost of equity and remove the haircut, or model expropriation as an explicit probability-weighted branch and drop the premium. State which channel was chosen."
    }
  ]
}
```

| Key | Contents |
|---|---|
| `id` | stable within this run, starting at `F1`; the orchestrator tracks it in `state.json.open_findings` |
| `severity` | `high`, `medium` or `low` |
| `target_stage` | the stage the orchestrator dispatched, matching the keys under `state.json.stages` |
| `claim` | one sentence stating the defect, not its consequence |
| `evidence` | the artifact, the field and the two numbers that contradict |
| `suggested_fix` | the input to change, or the sentence that has to be written |

Write `evidence` so that a reader who disagrees can check you in one step.

One defect per finding. Two defects in one entry means the orchestrator cannot close half of
it, and the stage re-runs twice for one fix. Route each finding to the artifact's single
writer. When a defect spans two stages, route it upstream, because the downstream artifact
will be rebuilt anyway. A finding aimed at the wrong stage is fixed by nobody.

**`challenge.md`** — the prose companion, written for someone who will not open the JSON.
Five sections, in this order.

1. The route you validated against: engine branch, overlays, and the constraint list you
   tested.
2. The validator run: the command, how many checks ran, how many were skipped and why.
3. The findings, ranked by severity, with the argument for each and the value at stake where
   you could size it.
4. The attacks you ran that found nothing, named one by one. A clean result from a genuine
   attempt to break the analysis is a real finding about the work, and this section is the
   record that the review was systematic rather than opportunistic.
5. The open questions you could not resolve from the artifacts, each with the evidence that
   would settle it.

## Constraints

- You write `challenge.json` and `challenge.md`. You never edit another stage's artifact,
  not even to fix an obvious typo. The owning stage fixes its own work.
- You do not produce an alternative valuation. A rebuild is evidence inside a finding, and
  it stays in your scratch directory.
- Honor every constraint in `classification.json` in your own rebuilds. If the route carries
  `no-fcff-valuation`, you do not run an FCFF model to test a bank, even as a probe. Rebuild
  through the method the route allows and say why the obvious probe was closed to you.
- The universal constraints bind your own work as much as the analyst's:
  `single-charge-per-risk` and `no-perpetual-growth-above-riskfree`.
- No arithmetic in prose. If a check you want has no script, say so in the return rather
  than doing the sums by hand.
- Do not report checks that passed as though they were findings. Consistency is not
  accuracy, and a clean validator run is a floor rather than a verdict.
- Do not grade the analyst. Grade the argument.
- Do not withhold a finding because the loopback budget is short. Raise it and mark it, and
  let the orchestrator decide whether it is fixed or disclosed.
- You cannot ask the user a question. When an attack genuinely turns on something only the
  user can settle — an independent market-size estimate, the buyer's identity, whether a
  claimed barrier is real — return `needs_input` with the question and two or three concrete
  options, and say what each does to the finding.

## Return

A structured summary, then one status line as the last line of the answer. Keep it short;
the artifacts hold the detail.

**Status:** `complete`, `blocked` or `needs_input`.

On `complete`, report:

- the absolute paths of the two artifacts written
- the validator result: error count, warning count, and how many checks were skipped
- the finding count by severity, and the `id` and one-clause claim of every `high` finding
- the `target_stage` of each finding, so the orchestrator knows which stages reopen
- the attacks that ran and found nothing, named
- whether gate `G7_challenged` can pass now, or which findings block it
- the vintage of any reference table you used, and any vintage mismatch you found
- anything you could not test, and the artifact or external number that would let you

On `blocked`, name the missing or malformed artifact and the exact field you need. On
`needs_input`, give the question, the options, and what each does to the finding that
depends on it.

